This is an automated email from the ASF dual-hosted git repository.

jamesbognar pushed a commit to branch docs
in repository https://gitbox.apache.org/repos/asf/juneau.git


The following commit(s) were added to refs/heads/docs by this push:
     new bafb94ebe2 docs: StaticFilesMixin topic page + 9.5 release notes 
(FINISHED-75)
bafb94ebe2 is described below

commit bafb94ebe229c77ec80b2b9246377dd710931f86
Author: James Bognar <[email protected]>
AuthorDate: Sun May 24 16:48:22 2026 -0400

    docs: StaticFilesMixin topic page + 9.5 release notes (FINISHED-75)
    
    Co-authored-by: Cursor <[email protected]>
---
 pages/release-notes/9.5.0.md            |  31 ++++
 pages/topics/10.14a.StaticFilesMixin.md | 289 ++++++++++++++++++++++++++++++++
 sidebars.ts                             |   5 +
 3 files changed, 325 insertions(+)

diff --git a/pages/release-notes/9.5.0.md b/pages/release-notes/9.5.0.md
index 18b9aab87a..afb79dfaad 100644
--- a/pages/release-notes/9.5.0.md
+++ b/pages/release-notes/9.5.0.md
@@ -2225,6 +2225,37 @@ public class MyApi extends RestServlet implements 
BasicUniversalConfig { ... }
 
 Apps that subclass `BasicRestServlet` / `BasicRestObject` without setting 
`apiFormat` or relying on the query mirrors get the new behavior (six api-docs 
URLs instead of one) automatically.
 
+#### Static-Files Mixin (TODO-75)
+
+`juneau-rest-server` now ships a `BasicStaticFilesResource` mixin in the new
+`org.apache.juneau.rest.staticfile` package that wraps the existing 
`BasicStaticFiles` plumbing
+in a servlet-level mixin. Any `@Rest`-annotated resource can opt into 
static-file serving via
+`@Rest(mixins = BasicStaticFilesResource.class)` without subclassing 
`BasicRestServlet` just to
+inherit `getHtdoc(...)`. Default mounts are `/static/*` and `/htdocs/*` (both 
routes share the
+same `@RestGet` handler) and the resolved `StaticFiles` bean is read from the 
importer's bean
+store at request time, so `@Bean StaticFiles` factory methods, headers, and 
classpath search
+roots flow through unchanged. See
+[Static-Files Mixin](/docs/topics/StaticFilesMixin) for the full reference, 
including
+multi-mount semantics, HEAD support, Cache-Control behavior, the 
OpenAPI-hidden mechanism, and
+the path-override constraint documented in the topic page's *Path-override 
constraint* section.
+
+- **`@OpSwagger(ignore = true)` (new annotation member).** New boolean member 
on
+  `org.apache.juneau.rest.annotation.OpSwagger` that excludes an operation 
from the published
+  Swagger / OpenAPI specification. Honored by `BasicSwaggerProviderSession` 
(and therefore by
+  the OpenAPI 3.1 emission path, which transforms from the Swagger 2.0 
output). Introduced to
+  hide the static-files mixin's greedy `/*` handlers, but reusable on any 
`@RestOp`-annotated
+  method that should not surface in the published API contract (favicon 
handlers, internal
+  probes, blob handlers). The legacy `BasicRestOperations.getHtdoc(...)` 
method now also
+  carries `@OpSwagger(ignore = true)` so `BasicRestServlet`-hosted apps get a 
clean spec for
+  free, with no need to subclass-and-suppress.
+- **HEAD body-suppression in `HttpResourceProcessor`.** The shared response 
processor for any
+  `HttpResource` return value now suppresses the response body when the 
request method is
+  `HEAD`, per [RFC 7231 
§4.3.2](https://datatracker.ietf.org/doc/html/rfc7231#section-4.3.2):
+  headers (Content-Type, Content-Length, Cache-Control, plus any 
resource-supplied headers)
+  are emitted identically to the equivalent `GET`, but the body is not 
written. This benefits
+  any `@RestOp`-annotated handler that returns an `HttpResource`, not just the 
static-files
+  mixin.
+
 #### Server-side SSE Helpers (TODO-62)
 
 `juneau-rest-server` now includes an SSE helper layer for streaming endpoints:
diff --git a/pages/topics/10.14a.StaticFilesMixin.md 
b/pages/topics/10.14a.StaticFilesMixin.md
new file mode 100644
index 0000000000..6176772bd8
--- /dev/null
+++ b/pages/topics/10.14a.StaticFilesMixin.md
@@ -0,0 +1,289 @@
+---
+title: "Static-Files Mixin"
+slug: StaticFilesMixin
+---
+
+The Juneau REST server ships with a
+[`BasicStaticFilesResource`](/site/apidocs/org/apache/juneau/rest/staticfile/BasicStaticFilesResource.html)
+mixin in the 
[`org.apache.juneau.rest.staticfile`](/site/apidocs/org/apache/juneau/rest/staticfile/package-summary.html)
+package that wraps the existing 
[`BasicStaticFiles`](/site/apidocs/org/apache/juneau/rest/staticfile/BasicStaticFiles.html)
+plumbing (a `StaticFiles` implementation, not a servlet) in a servlet-level 
mixin with
+multi-mount support. Any `@Rest`-annotated resource can opt into static-file 
serving via
+[`@Rest(mixins=...)`](/docs/topics/RestServerCompositionMixinsAndPaths) 
without subclassing
+[`BasicRestServlet`](/site/apidocs/org/apache/juneau/rest/servlet/BasicRestServlet.html)
 just to
+inherit `getHtdoc(...)`.
+
+> Need broader background on the underlying `StaticFiles` bean (classpath 
search roots, caching,
+> exclude patterns)? See [Static files](/docs/topics/StaticFiles) for the 
plain-bean reference;
+> this page covers only the mixin packaging on top.
+
+## What the mixin does
+
+The mixin declares two REST operation methods bound to the same handler so a 
single Java method
+serves both `GET` and `HEAD` for both default mount points:
+
+```java
+@Rest(paths = {"/static/*", "/htdocs/*"})
+public class BasicStaticFilesResource {
+
+    @RestGet(
+        path = {"/static/*", "/htdocs/*"},
+        swagger = @OpSwagger(ignore = true)
+    )
+    public HttpResource getStaticFile(RestRequest req, @Path("/*") String 
path, Locale locale) {
+        return req.getStaticFiles().resolve(path, 
locale).orElseThrow(NotFound::new);
+    }
+
+    @RestOp(
+        method = "HEAD",
+        path = {"/static/*", "/htdocs/*"},
+        swagger = @OpSwagger(ignore = true)
+    )
+    public HttpResource headStaticFile(RestRequest req, @Path("/*") String 
path, Locale locale) {
+        return getStaticFile(req, path, locale);
+    }
+}
+```
+
+The handler reads `RestRequest.getStaticFiles()` at request time, which 
delegates to
+`BeanStore.getBean(StaticFiles.class)` and falls back to a default 
`BasicStaticFiles` instance
+that searches the importer's classpath under `static/` and `htdocs/` plus the 
working-directory
+`static/` and `htdocs/` folders. Missing paths surface as `NotFound` thrown 
from the handler,
+which flows through the standard exception-rendering chain (RFC 7807 
problem-details when
+[FINISHED-61's opt-in](/docs/topics/RestServerProblemDetails) is active; plain 
text otherwise).
+
+## Default mount usage
+
+Add the mixin to any `@Rest`-annotated resource — no subclassing of 
`BasicRestServlet`
+required — and `/static/*` and `/htdocs/*` start serving classpath resources 
immediately:
+
+```java
+@Rest(path = "/api", mixins = BasicStaticFilesResource.class)
+public class ApiResource extends RestServlet {
+
+    @RestGet("/items")
+    public List<Item> items() { ... }
+}
+// Serves /api/items, /api/static/<file>, /api/htdocs/<file>.
+```
+
+`BasicStaticFilesResource` is itself a fully-fledged `@Rest`-annotated 
resource and can also be
+subclassed and mounted as its own top-level servlet:
+
+```java
+@Rest(paths = {"/static/*", "/htdocs/*"})
+public class CdnResource extends BasicStaticFilesResource { }
+```
+
+Both deployment styles (mixin into an existing servlet vs. mount as a sibling 
servlet) work the
+same way under Spring Boot and under the Jetty microservice — see
+[Spring Boot vs. microservice](#spring-boot-vs-microservice) below.
+
+## Multi-mount semantics
+
+The mixin's `@Rest(paths = {"/static/*", "/htdocs/*"})` declares two top-level 
servlet-container
+mount points at the same time. Both URLs route to the *same* 
`BasicStaticFilesResource` instance
+and the *same* `@RestGet(path = {"/static/*","/htdocs/*"})` Java handler — the 
trailing `/*`
+captures the multi-segment remainder via `@Path("/*") String path` and is 
forwarded to
+`StaticFiles.resolve(...)` verbatim, so a `GET /static/css/main.css` and a 
`GET /htdocs/css/main.css`
+both resolve to the same `css/main.css` file under the classpath search roots.
+
+To add a third mount path (e.g. `/assets/*`) without touching the mixin, 
subclass it:
+
+```java
+@Rest(paths = {"/static/*", "/htdocs/*", "/assets/*"})
+public class CdnResource extends BasicStaticFilesResource { }
+```
+
+The inner `@RestGet(path = {"/static/*","/htdocs/*"})` matcher still binds to 
two paths and the
+inherited `BasicStaticFilesResource.getStaticFile(...)` Java handler still 
routes through it; a
+subclass that wants `/assets/*` to dispatch through the same handler needs to 
either re-declare
+the `@RestGet` (`path = {"/static/*","/htdocs/*","/assets/*"}`) on an 
overriding method or
+register two top-level servlet beans (one per mount group). See
+[Path-override constraint](#path-override-constraint) below for the full story.
+
+## HEAD support
+
+The mixin handles `HEAD` per [RFC 7231 
§4.3.2](https://datatracker.ietf.org/doc/html/rfc7231#section-4.3.2):
+the response carries identical headers to the corresponding `GET` 
(Content-Type, Content-Length,
+Cache-Control, plus any resource-supplied headers) with an empty body.
+
+The body-suppression happens at the framework layer, not in the mixin: the 
shared
+[`HttpResourceProcessor`](/site/apidocs/org/apache/juneau/rest/processor/HttpResourceProcessor.html)
+response processor checks `RestRequest.getMethod()` and short-circuits before 
writing the body
+when it sees `HEAD`. The mixin's `headStaticFile(...)` handler delegates to 
`getStaticFile(...)`
+verbatim — the processor takes care of the body suppression. The improvement 
benefits any
+`HttpResource`-returning handler, not just the static-files mixin.
+
+| Request | Response status | Body | Headers |
+|---|---|---|---|
+| `GET /static/foo.css` (file present) | 200 | file body | `Content-Type`, 
`Content-Length`, `Cache-Control`, ... |
+| `GET /static/missing.css` | 404 | exception body | per exception-rendering 
chain |
+| `HEAD /static/foo.css` (file present) | 200 | empty | identical to GET |
+| `HEAD /static/missing.css` | 404 | empty | per exception-rendering chain |
+
+## Cache-Control behavior
+
+The mixin returns the `HttpResource` from `BasicStaticFiles` directly, so any 
headers configured
+on the `StaticFiles` builder — including the default `Cache-Control: 
max-age=86400, public` —
+flow through to the response without modification. To customize, register your 
own
+`@Bean StaticFiles` factory:
+
+```java
+@Rest(mixins = BasicStaticFilesResource.class)
+public class MyResource extends RestServlet {
+
+    @Bean
+    public StaticFiles staticFiles(BeanStore bs) {
+        return BasicStaticFiles
+            .create(bs)
+            .cp(MyResource.class, "/assets", true)
+            .headers(CacheControl.of("max-age=604800, public, immutable"))   
// 7 days, immutable
+            .build();
+    }
+}
+```
+
+The `StaticFiles` bean is resolved through the request's bean store via
+`RestRequest.getStaticFiles()`, which honors the FINISHED-72 `@Bean`-factory 
walk (microservice
+path uses `BasicBeanStore`; Spring Boot path uses `SpringBeanStore` → 
`ApplicationContext.getBeanProvider(...)`).
+If you register multiple `StaticFiles` beans in a Spring `@Configuration`, 
mark exactly one
+`@Primary` so the bean store lookup is deterministic.
+
+## Hidden from OpenAPI
+
+Both `@RestGet` and `@RestOp(method="HEAD")` handlers on 
`BasicStaticFilesResource` are marked
+`swagger = @OpSwagger(ignore = true)`. A greedy `/*` blob handler is not 
API-meaningful, and
+emitting it as an operation just produces noise. The exclusion applies 
symmetrically across
+both spec emitters:
+
+- `BasicSwaggerResource` (Swagger v2 → `/api`, `/swagger`) — skips the 
operation when generating
+  the Swagger document via `BasicSwaggerProviderSession`.
+- `BasicOpenApiResource` (OpenAPI 3.1 → `/openapi`, `/openapi.json`, 
`/openapi.yaml`,
+  `/redoc`) — same skip applies via the Swagger-to-OpenAPI transform.
+
+The same `@OpSwagger(ignore=true)` mechanism is now also applied to the legacy
+`BasicRestOperations.getHtdoc(...)` method, so apps that subclass 
`BasicRestServlet` directly
+also get a clean spec for free. See the
+[API-Docs Mixin Pack](/docs/topics/ApiDocsMixins) page for the other half of 
the api-docs
+composition story.
+
+`@OpSwagger(ignore = true)` is reusable on any `@RestOp`-annotated method — it 
is the
+recommended way to suppress greedy `/*` handlers, internal probes, 
`/favicon.ico`, or any
+operation that should not appear in the published API contract.
+
+## Spring Boot vs. microservice
+
+The mixin works identically under both deployment paths because mixin 
instances are resolved
+through the importing servlet's
+[`BeanStore`](/site/apidocs/org/apache/juneau/commons/inject/BeanStore.html):
+
+- **Microservice path.** `BasicBeanStore` looks up
+  `BasicStaticFilesResource.class` via `getBean(...)` first; if no bean is 
registered, the
+  framework reflects a no-arg constructor. The same lookup applies to the 
resolved
+  `StaticFiles` bean. Verified end-to-end by
+  `BasicStaticFilesResource_JettyMicroservice_Test` in `juneau-utest`, which 
boots a real Jetty
+  `Microservice` on an ephemeral port and hits the mixin's URLs over real HTTP.
+- **Spring Boot path.**
+  
[`SpringBeanStore`](/site/apidocs/org/apache/juneau/rest/springboot/SpringBeanStore.html)
+  extends `BasicBeanStore` and delegates unresolved lookups to
+  `ApplicationContext.getBeanProvider(beanType).getIfAvailable()`. Same 
fallback to a no-arg
+  constructor if no Spring bean is registered. Verified by
+  `BasicStaticFilesResource_Springboot_Test`.
+
+Spring Boot's `META-INF/resources/` convention is also covered: an importer 
that wants the
+mixin to serve files placed under `META-INF/resources/` (Spring Boot's 
auto-served root) can
+add it as a classpath search root via 
`BasicStaticFiles.create(bs).cp(...,"/META-INF/resources",true)`:
+
+```java
+@Bean
+public StaticFiles staticFiles(BeanStore bs) {
+    return BasicStaticFiles
+        .create(bs)
+        .cp(MyResource.class, "/META-INF/resources", true)
+        .build();
+}
+```
+
+This is pinned by `BasicStaticFilesResource_SpringbootMetaInf_Test` — a file at
+`src/main/resources/META-INF/resources/spring-fixture.txt` is reachable 
through both `/static/spring-fixture.txt`
+and `/htdocs/spring-fixture.txt` under the mixin's default mounts. Spring 
Boot's own static-resource
+handler also continues to serve the same file at `/spring-fixture.txt` (the 
root mount), so the
+two handlers coexist without shadowing each other.
+
+## Path-override constraint
+
+The mixin combines two layers of routing:
+
+1. **Container-level mount.** `@Rest(paths = {"/static/*","/htdocs/*"})` 
declares which URLs the
+   servlet container should dispatch to the `BasicStaticFilesResource` 
instance. This rung
+   participates in the [runtime-override paths chain 
(TODO-73)](/docs/topics/RestServerCompositionMixinsAndPaths#runtime-overridable-paths-since-950)
+   so a subclass with `@Rest(paths = {"/assets/*"})` widens the container 
mount to `/assets/*`.
+2. **Inner `@RestGet` matcher.** `@RestGet(path = {"/static/*","/htdocs/*"})` 
declares which
+   URL patterns route to the `getStaticFile(...)` Java handler *within* the 
resource. This rung
+   is *not* affected by the container-level `@Rest(paths=...)` widening — 
`@RestGet(path=...)`
+   is a literal compile-time list that subclasses do not implicitly 
inherit-widen.
+
+The practical consequence: a naive subclass with `@Rest(paths = 
{"/assets/*"})` mounts the
+servlet at `/assets/*` but the inner `@RestGet` matcher still binds to 
`["/static/*", "/htdocs/*"]`,
+so `GET /assets/foo.css` reaches the resource but does not match any handler 
and returns 404.
+
+Two working patterns to add a third mount path:
+
+**Subclass + override the `@RestGet` matcher.** Re-declare the handler with 
the full mount
+list on the subclass:
+
+```java
+@Rest(paths = {"/static/*", "/htdocs/*", "/assets/*"})
+public class CdnResource extends BasicStaticFilesResource {
+
+    @Override
+    @RestGet(
+        path = {"/static/*", "/htdocs/*", "/assets/*"},
+        swagger = @OpSwagger(ignore = true)
+    )
+    public HttpResource getStaticFile(RestRequest req, @Path("/*") String 
path, Locale locale) {
+        return super.getStaticFile(req, path, locale);
+    }
+}
+```
+
+**Register two beans at the servlet-registration layer.** Mount one 
`BasicStaticFilesResource`
+instance at `/static/*, /htdocs/*` and a second at `/assets/*`:
+
+```java
+@Configuration
+public class StaticConfig {
+
+    @Bean
+    public ServletRegistrationBean<BasicStaticFilesResource> defaults() {
+        return new ServletRegistrationBean<>(new BasicStaticFilesResource(), 
"/static/*", "/htdocs/*");
+    }
+
+    @Bean
+    public ServletRegistrationBean<BasicStaticFilesResource> assets() {
+        return new ServletRegistrationBean<>(new BasicStaticFilesResource(), 
"/assets/*");
+    }
+}
+```
+
+The deeper refactor — decoupling inner-matcher paths from container-level 
mount paths so
+FINISHED-73 runtime overrides cascade fully through both layers — was 
considered for this work
+item but deferred. The constraint is intentional: keeping `@RestGet(path=...)` 
as a literal,
+non-inherited matcher matches every other Juneau `@RestGet`-annotated method, 
and the two
+working patterns above are explicit and discoverable. If your app heavily 
relies on the
+container-level mount widening cascading to inner matchers, the second pattern 
(register two
+beans) is the recommended workaround.
+
+## See also
+
+- [Static files](/docs/topics/StaticFiles) — the underlying `StaticFiles` and 
`BasicStaticFiles`
+  reference; classpath search roots, caching, exclude patterns.
+- [REST Server — Mixins and Multi-Mount 
Paths](/docs/topics/RestServerCompositionMixinsAndPaths) —
+  the `@Rest(mixins=...)` and `@Rest(paths=...)` primitives this mixin builds 
on, including the
+  runtime-overridable paths chain (FINISHED-73).
+- [API-Docs Mixin Pack](/docs/topics/ApiDocsMixins) — the four-class api-docs 
surface (FINISHED-74)
+  that also uses the `@OpSwagger(ignore=true)` exclusion mechanism for its 
`/*` handlers.
+- [Response Processors](/docs/topics/ResponseProcessors) — how 
`HttpResourceProcessor` handles
+  `HEAD` body suppression and what other response-processor stages run for 
`HttpResource`
+  returns.
diff --git a/sidebars.ts b/sidebars.ts
index 37d75e415e..b055d50a56 100644
--- a/sidebars.ts
+++ b/sidebars.ts
@@ -1372,6 +1372,11 @@ const sidebars: SidebarsConfig = {
                                                        id: 
'topics/10.14.StaticFiles',
                                                        label: '10.14. Static 
Files',
                                                },
+                                               {
+                                                       type: 'doc',
+                                                       id: 
'topics/10.14a.StaticFilesMixin',
+                                                       label: '10.14a. 
Static-Files Mixin',
+                                               },
                                                {
                                                        type: 'doc',
                                                        id: 
'topics/10.15.ClientVersioning',

Reply via email to