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',