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

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


The following commit(s) were added to refs/heads/master by this push:
     new 780afa03bd feat(rest-server): BasicStaticFilesResource mixin + HEAD 
body-suppression + @OpSwagger(ignore) (FINISHED-75)
780afa03bd is described below

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

    feat(rest-server): BasicStaticFilesResource mixin + HEAD body-suppression + 
@OpSwagger(ignore) (FINISHED-75)
---
 .../apache/juneau/rest/annotation/OpSwagger.java   |  33 +++++
 .../rest/annotation/OpSwaggerAnnotation.java       |  19 +++
 .../rest/processor/HttpResourceProcessor.java      |   9 ++
 .../juneau/rest/servlet/BasicRestOperations.java   |  10 +-
 .../rest/staticfile/BasicStaticFilesResource.java  | 159 +++++++++++++++++++++
 .../rest/swagger/BasicSwaggerProviderSession.java  |  14 +-
 .../BasicStaticFilesResource_AsMixin_Test.java     | 125 ++++++++++++++++
 ...BasicStaticFilesResource_CacheControl_Test.java |  74 ++++++++++
 ...cStaticFilesResource_ImporterOverride_Test.java |  71 +++++++++
 ...StaticFilesResource_JettyMicroservice_Test.java | 146 +++++++++++++++++++
 ...asicStaticFilesResource_OpenApiHidden_Test.java | 131 +++++++++++++++++
 ...StaticFilesResource_SpringbootMetaInf_Test.java | 139 ++++++++++++++++++
 .../BasicStaticFilesResource_Springboot_Test.java  | 156 ++++++++++++++++++++
 .../BasicStaticFilesResource_Standalone_Test.java  |  90 ++++++++++++
 .../META-INF/resources/spring-fixture.txt          |  13 ++
 ...-files.md => FINISHED-75-mixin-static-files.md} |  83 +++++++++--
 todo/TODO.md                                       |   4 +-
 17 files changed, 1253 insertions(+), 23 deletions(-)

diff --git 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/OpSwagger.java
 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/OpSwagger.java
index 7593dfefaf..cf8168bd47 100644
--- 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/OpSwagger.java
+++ 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/OpSwagger.java
@@ -97,6 +97,39 @@ public @interface OpSwagger {
         */
        String[] description() default {};
 
+       /**
+        * Excludes this operation from the published Swagger / OpenAPI 
specification.
+        *
+        * <p>
+        * When set to <jk>true</jk>, this operation is skipped during Swagger 
/ OpenAPI generation:
+        * neither the path entry nor any sibling HTTP method on the same path 
is emitted unless
+        * another operation contributes to it. This is useful for endpoints 
that are not API-meaningful
+        * (e.g. greedy {@code /*} static-file handlers, {@code /favicon.ico}, 
internal probes) where
+        * documenting the operation would just be noise.
+        *
+        * <h5 class='section'>Example:</h5>
+        * <p class='bjava'>
+        *      <ja>@RestGet</ja>(
+        *              path=<js>"/static/*"</js>,
+        *              swagger=<ja>@OpSwagger</ja>(
+        *                      ignore=<jk>true</jk>
+        *              )
+        *      )
+        * </p>
+        *
+        * <h5 class='section'>Notes:</h5><ul>
+        *      <li class='note'>
+        *              Default is <jk>false</jk> &mdash; operations are 
emitted by default.
+        *      <li class='note'>
+        *              Schema-level {@code ignore} flags (on bean fields / 
parameters) are separate from this
+        *              method-level flag &mdash; this one suppresses the 
entire operation; schema-level flags
+        *              suppress individual fields within a referenced schema.
+        * </ul>
+        *
+        * @return The annotation value.
+        */
+       boolean ignore() default false;
+
        /**
         * Defines the swagger field <c>/paths/{path}/{method}/externalDocs</c>.
         *
diff --git 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/OpSwaggerAnnotation.java
 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/OpSwaggerAnnotation.java
index e5a4318945..f557a59f4a 100644
--- 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/OpSwaggerAnnotation.java
+++ 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/OpSwaggerAnnotation.java
@@ -49,6 +49,7 @@ public class OpSwaggerAnnotation {
                private String[] description = {};
                private ExternalDocs externalDocs = 
ExternalDocsAnnotation.DEFAULT;
                private String deprecated = "";
+               private boolean ignore;
                private String operationId = "";
                private String[] consumes = {};
                private String[] parameters = {};
@@ -119,6 +120,17 @@ public class OpSwaggerAnnotation {
                        return this;
                }
 
+               /**
+                * Sets the {@link OpSwagger#ignore()} property on this 
annotation.
+                *
+                * @param value The new value for this property.
+                * @return This object.
+                */
+               public Builder ignore(boolean value) {
+                       ignore = value;
+                       return this;
+               }
+
                /**
                 * Sets the {@link OpSwagger#operationId()} property on this 
annotation.
                 *
@@ -217,6 +229,7 @@ public class OpSwaggerAnnotation {
                private final String[] description;
                private final ExternalDocs externalDocs;
                private final String deprecated;
+               private final boolean ignore;
                private final String operationId;
                private final String[] consumes;
                private final String[] parameters;
@@ -233,6 +246,7 @@ public class OpSwaggerAnnotation {
                        consumes = copyOf(b.consumes);
                        deprecated = b.deprecated;
                        externalDocs = b.externalDocs;
+                       ignore = b.ignore;
                        operationId = b.operationId;
                        parameters = copyOf(b.parameters);
                        produces = copyOf(b.produces);
@@ -258,6 +272,11 @@ public class OpSwaggerAnnotation {
                        return externalDocs;
                }
 
+               @Override /* Overridden from OpSwagger */
+               public boolean ignore() {
+                       return ignore;
+               }
+
                @Override /* Overridden from OpSwagger */
                public String operationId() {
                        return operationId;
diff --git 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/processor/HttpResourceProcessor.java
 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/processor/HttpResourceProcessor.java
index baa969a10b..b39891e547 100644
--- 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/processor/HttpResourceProcessor.java
+++ 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/processor/HttpResourceProcessor.java
@@ -25,6 +25,11 @@ import org.apache.juneau.rest.*;
 /**
  * Response handler for {@link HttpResource} objects.
  *
+ * <p>
+ * {@code HEAD} requests are handled per RFC 7231 §4.3.2: identical headers to 
the equivalent
+ * {@code GET} are emitted (Content-Type, Content-Length, plus any 
resource-supplied headers)
+ * but the response body is suppressed.
+ *
  * <h5 class='section'>See Also:</h5><ul>
  *     <li class='link'><a class="doclink" 
href="https://juneau.apache.org/docs/topics/ResponseProcessors";>Response 
Processors</a>
  * </ul>
@@ -49,6 +54,10 @@ public class HttpResourceProcessor implements 
ResponseProcessor {
 
                r.getHeaders().forEach(res::addHeader);
 
+               // RFC 7231 §4.3.2: HEAD must mirror GET headers but omit the 
body.
+               if ("HEAD".equalsIgnoreCase(opSession.getRequest().getMethod()))
+                       return FINISHED;
+
                try (var os = res.getNegotiatedOutputStream()) {
                        r.writeTo(os);
                        os.flush();
diff --git 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/servlet/BasicRestOperations.java
 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/servlet/BasicRestOperations.java
index d3fc84be5a..cc5d20e405 100644
--- 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/servlet/BasicRestOperations.java
+++ 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/servlet/BasicRestOperations.java
@@ -118,6 +118,13 @@ public interface BasicRestOperations {
        /**
         * [GET /htdocs/*] - Retrieve static file.
         *
+        * <p>
+        * Excluded from the published Swagger/OpenAPI specification via
+        * {@link OpSwagger#ignore() @OpSwagger(ignore=true)} &mdash; a greedy 
{@code /htdocs/*} blob
+        * handler is not API-meaningful, and emitting it as an operation just 
produces noise. This
+        * mirrors the exclusion strategy used by the {@code 
BasicStaticFilesResource} mixin and keeps
+        * legacy {@code BasicRestServlet}-hosted apps consistent with 
mixin-based static-file mounts.
+        *
         * @param path The path to retrieve.
         * @param locale The locale of the HTTP request.
         * @return An HTTP resource representing the static file.
@@ -125,7 +132,8 @@ public interface BasicRestOperations {
        @RestGet(
                path="/htdocs/*",
                summary="Static files",
-               description="Static file retrieval."
+               description="Static file retrieval.",
+               swagger=@OpSwagger(ignore=true)
        ) HttpResource getHtdoc(@Path String path, Locale locale);
 
        /**
diff --git 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/staticfile/BasicStaticFilesResource.java
 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/staticfile/BasicStaticFilesResource.java
new file mode 100644
index 0000000000..379f7251bb
--- /dev/null
+++ 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/staticfile/BasicStaticFilesResource.java
@@ -0,0 +1,159 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.rest.staticfile;
+
+import java.util.*;
+
+import org.apache.juneau.http.annotation.*;
+import org.apache.juneau.http.*;
+import org.apache.juneau.http.response.*;
+import org.apache.juneau.rest.*;
+import org.apache.juneau.rest.annotation.*;
+
+/**
+ * Mixin that serves static files from the active {@link StaticFiles} 
implementation under
+ * {@code /static/*} and {@code /htdocs/*}.
+ *
+ * <p>
+ * Wraps the existing {@link BasicStaticFiles} plumbing (a {@link StaticFiles} 
implementation, not a
+ * servlet) in a servlet-level mixin with multi-mount support so any Juneau 
resource can opt into
+ * static-file serving via {@link Rest#mixins() 
@Rest(mixins=BasicStaticFilesResource.class)}. The
+ * mixin reads the active {@link StaticFiles} from the bean store at request 
time
+ * &mdash; importer's {@code @Bean StaticFiles} declarations + classpath 
defaults are picked up
+ * automatically.
+ *
+ * <h5 class='figure'>Composition example:</h5>
+ *
+ * <p class='bjava'>
+ *     <jc>// Vanilla resource gains static-file serving at /static/* and 
/htdocs/*.</jc>
+ *     <ja>@Rest</ja>(path=<js>"/api"</js>, 
mixins=BasicStaticFilesResource.<jk>class</jk>)
+ *     <jk>public class</jk> ApiResource <jk>extends</jk> RestServlet {
+ *             <ja>@RestGet</ja>(<js>"/items"</js>) <jk>public</jk> 
List&lt;Item&gt; items() { ... }
+ *     }
+ * </p>
+ *
+ * <h5 class='figure'>Standalone deployment example:</h5>
+ *
+ * <p class='bjava'>
+ *     <jc>// Mount as a standalone resource; the inherited paths declare both 
mount points.</jc>
+ *     
<ja>@Rest</ja>(paths={<js>"/static/*"</js>,<js>"/htdocs/*"</js>,<js>"/assets/*"</js>})
+ *     <jk>public class</jk> CdnResource <jk>extends</jk> 
BasicStaticFilesResource { }
+ * </p>
+ *
+ * <h5 class='section'>Behavior:</h5>
+ *
+ * <ul class='spaced-list'>
+ *     <li>GET hits the active {@link StaticFiles} bean (resolved via
+ *             {@link RestRequest#getStaticFiles()}); missing paths surface as
+ *             {@link NotFound} thrown from the handler.
+ *     <li>{@code HEAD} requests are accepted via the standard servlet
+ *             {@code HEAD}-via-{@code GET} contract: the response carries 
identical headers to the
+ *             corresponding {@code GET} (including {@code Cache-Control} and 
{@code Content-Type}) with
+ *             an empty body.
+ *     <li>{@code Cache-Control} headers come from the {@link 
BasicStaticFiles} default
+ *             ({@code max-age=86400, public}); importers can override via
+ *             {@code BasicStaticFiles.create(beanStore).headers(...)} when 
registering their own
+ *             {@code @Bean StaticFiles}.
+ *     <li>Default classpath base searches both {@code static/} and {@code 
htdocs/} directories on
+ *             the importer's classpath (recursive walk via {@link 
BasicStaticFiles}'s built-in
+ *             {@link org.apache.juneau.cp.ResourceSupplier} hook).
+ * </ul>
+ *
+ * <h5 class='section'>Path matching:</h5>
+ *
+ * <p>
+ * The handler declares {@code @RestGet(path={"/static/*","/htdocs/*"})} so a 
single Java method
+ * binds to both URL prefixes. The trailing {@code /*} captures the 
multi-segment remainder via
+ * {@code @Path("/*") String path}. Juneau's {@code UrlPathMatcher} does not 
support the
+ * Spring/JAX-RS {@code {var:regex}} syntax &mdash; each {@code {var}} matches 
a single segment
+ * only, and multi-segment matching is only available via the trailing-{@code 
*} pattern shown here
+ * (same idiom as the legacy {@code BasicRestServlet.getHtdoc(...)} accessor).
+ * </p>
+ *
+ * <h5 class='section'>OpenAPI surface:</h5>
+ *
+ * <p>
+ * The greedy {@code /*} handler is not API-meaningful and is excluded from 
generated Swagger /
+ * OpenAPI specs via {@link OpSwagger#ignore() @OpSwagger(ignore=true)}.
+ * </p>
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ *     <li class='jc'>{@link BasicStaticFiles}
+ *     <li class='jc'>{@link StaticFiles}
+ *     <li class='link'><a class="doclink" 
href="https://juneau.apache.org/docs/topics/StaticFiles";>Static files</a>
+ *     <li class='link'><a class="doclink" 
href="https://juneau.apache.org/docs/topics/RestServerComposition";>REST Server 
&mdash; Composition (mixins, paths)</a>
+ * </ul>
+ *
+ * @since 9.5.0
+ */
+// @formatter:off
+@Rest(paths={"/static/*","/htdocs/*"})
+public class BasicStaticFilesResource {
+
+       /**
+        * [GET /static/* | /htdocs/*] &mdash; serve a static file from the 
active {@link StaticFiles} bean.
+        *
+        * <p>
+        * The {@code @Path("/*") String path} captures the multi-segment 
trailing remainder. The
+        * {@code Locale} parameter is auto-injected from the request and 
forwarded to
+        * {@link StaticFiles#resolve(String,Locale) StaticFiles.resolve(...)} 
so localized lookups
+        * (e.g. {@code styles_fr.css}) work out of the box.
+        *
+        * @param req The current REST request &mdash; supplies {@link 
RestRequest#getStaticFiles()}.
+        * @param path The trailing remainder after the mount prefix (the file 
path within the
+        *      {@code static/} or {@code htdocs/} classpath / filesystem 
search roots).
+        * @param locale The request locale (used for localized resource 
lookups).
+        * @return The matching {@link HttpResource} (with content type + cache 
headers).
+        * @throws NotFound If no resource matches the requested path.
+        */
+       @RestGet(
+               path={"/static/*","/htdocs/*"},
+               summary="Static files",
+               description="Static file retrieval.",
+               swagger=@OpSwagger(ignore=true)
+       )
+       public HttpResource getStaticFile(RestRequest req, @Path("/*") String 
path, Locale locale) {
+               return req.getStaticFiles().resolve(path, 
locale).orElseThrow(NotFound::new);
+       }
+
+       /**
+        * [HEAD /static/* | /htdocs/*] &mdash; return GET headers for a static 
file without the body.
+        *
+        * <p>
+        * Per RFC 7231 §4.3.2, {@code HEAD} mirrors the equivalent {@code 
GET}'s headers (Content-Type,
+        * Content-Length, Cache-Control) but emits an empty response body. The 
shared
+        * {@link org.apache.juneau.rest.processor.HttpResourceProcessor} 
performs the body suppression
+        * at the response-processor layer based on {@link 
RestRequest#getMethod()}, so this handler can
+        * delegate to {@link #getStaticFile} verbatim.
+        *
+        * @param req The current REST request &mdash; supplies {@link 
RestRequest#getStaticFiles()}.
+        * @param path The trailing remainder after the mount prefix.
+        * @param locale The request locale (used for localized resource 
lookups).
+        * @return The matching {@link HttpResource} (with headers; body 
suppressed by the processor).
+        * @throws NotFound If no resource matches the requested path.
+        */
+       @RestOp(
+               method="HEAD",
+               path={"/static/*","/htdocs/*"},
+               summary="Static files (HEAD)",
+               description="Static file metadata retrieval.",
+               swagger=@OpSwagger(ignore=true)
+       )
+       public HttpResource headStaticFile(RestRequest req, @Path("/*") String 
path, Locale locale) {
+               return getStaticFile(req, path, locale);
+       }
+}
diff --git 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/swagger/BasicSwaggerProviderSession.java
 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/swagger/BasicSwaggerProviderSession.java
index 2ba55b9197..955189f023 100644
--- 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/swagger/BasicSwaggerProviderSession.java
+++ 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/swagger/BasicSwaggerProviderSession.java
@@ -361,14 +361,18 @@ public class BasicSwaggerProviderSession {
                        var al = 
rstream(ap.find(mi)).filter(REST_OP_GROUP).toList();
                        var mn = m.getName();
 
+                       // Honor @OpSwagger(ignore=true) — skip the operation 
entirely so neither the path nor the
+                       // HTTP-method entry shows up in the generated spec. 
Done before getOperation(...) is called
+                       // because that method has the side effect of inserting 
empty entries via computeIfAbsent.
+                       var msValue = Value.<OpSwagger>empty();
+                       al.forEach(ai -> ai.getValue(OpSwagger.class, 
"swagger").filter(OpSwaggerAnnotation::notEmpty).ifPresent(msValue::set));
+                       var ms = msValue.orElseGet(() -> 
OpSwaggerAnnotation.create().build());
+                       if (ms.ignore())
+                               continue;
+
                        // Get the operation from the existing swagger so far.
                        var op = getOperation(omSwagger, sm.getPathPattern(), 
sm.getHttpMethod().toLowerCase());
 
-               // Add @RestOp(swagger)
-               var msValue = Value.<OpSwagger>empty();
-               al.forEach(ai -> ai.getValue(OpSwagger.class, 
"swagger").filter(OpSwaggerAnnotation::notEmpty).ifPresent(msValue::set));
-               var ms = msValue.orElseGet(() -> 
OpSwaggerAnnotation.create().build());
-
                        op.append(parseMap(ms.value(), "@OpSwagger(value) on 
class {0} method {1}", c, m));
                        op.appendIf(ne, SWAGGER_operationId,
                                firstNonEmpty(
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/rest/staticfiles/BasicStaticFilesResource_AsMixin_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/rest/staticfiles/BasicStaticFilesResource_AsMixin_Test.java
new file mode 100644
index 0000000000..d0a653b3dc
--- /dev/null
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/rest/staticfiles/BasicStaticFilesResource_AsMixin_Test.java
@@ -0,0 +1,125 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.rest.staticfiles;
+
+import org.apache.juneau.*;
+import org.apache.juneau.rest.*;
+import org.apache.juneau.rest.annotation.*;
+import org.apache.juneau.rest.mock.classic.*;
+import org.apache.juneau.rest.servlet.*;
+import org.apache.juneau.rest.staticfile.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Validates {@link BasicStaticFilesResource} mounted as a mixin via {@code 
@Rest(mixins=...)} on a
+ * vanilla {@link RestServlet}.
+ *
+ * <p>
+ * Acceptance:
+ * <ul>
+ *     <li>{@code GET /static/javadoc.css} returns the file (multi-mount 
default {@code paths}).
+ *     <li>{@code GET /htdocs/javadoc.css} returns the same file at the second 
default mount.
+ *     <li>{@code GET} on a missing path returns 404.
+ *     <li>The host's own endpoints are unaffected by the mixin.
+ * </ul>
+ *
+ * <p>
+ * The classpath resource {@code htdocs/javadoc.css} ships with {@code 
juneau-rest-server} and is
+ * therefore visible to {@code juneau-utest}'s test classpath via the
+ * {@link BasicStaticFiles} default constructor's recursive {@code 
cp(...,"htdocs",true)} walk.
+ *
+ * @since 9.5.0
+ */
+class BasicStaticFilesResource_AsMixin_Test extends TestBase {
+
+       @Rest(mixins=BasicStaticFilesResource.class)
+       public static class A extends BasicRestServlet {
+               private static final long serialVersionUID = 1L;
+               @RestGet(path="/items") public String items() { return "items"; 
}
+       }
+
+       private static final MockRestClient c = 
MockRestClient.buildLax(A.class);
+
+       @Test void a01_staticPathServesClasspathFile() throws Exception {
+               c.get("/static/javadoc.css")
+                       .run()
+                       .assertStatus(200)
+                       .assertContent().asString().isContains("Licensed to the 
Apache Software Foundation");
+       }
+
+       @Test void a02_htdocsPathServesSameFile() throws Exception {
+               c.get("/htdocs/javadoc.css")
+                       .run()
+                       .assertStatus(200)
+                       .assertContent().asString().isContains("Licensed to the 
Apache Software Foundation");
+       }
+
+       @Test void a03_missingFileReturns404() throws Exception {
+               c.get("/static/does-not-exist.css")
+                       .run()
+                       .assertStatus(404);
+       }
+
+       @Test void a04_missingFileAtHtdocsReturns404() throws Exception {
+               c.get("/htdocs/does-not-exist.css")
+                       .run()
+                       .assertStatus(404);
+       }
+
+       @Test void a05_hostEndpointStillReachable() throws Exception {
+               c.get("/items")
+                       .accept("application/json")
+                       .run()
+                       .assertStatus(200)
+                       .assertContent().asString().isContains("items");
+       }
+
+       @Test void a06_headStaticReturnsHeadersWithEmptyBody() throws Exception 
{
+               // HEAD must mirror GET's Content-Type and Content-Length while 
suppressing the body.
+               var get = c.get("/static/javadoc.css").run();
+               get.assertStatus(200);
+               var getContentType = 
get.getHeader("Content-Type").asString().orElse("");
+               var getContentLength = 
get.getHeader("Content-Length").asString().orElse("");
+
+               var head = c.head("/static/javadoc.css").run();
+               head.assertStatus(200);
+               head.assertContent().is("");
+
+               // Header parity (RFC 7231 §4.3.2).
+               var headContentType = 
head.getHeader("Content-Type").asString().orElse("");
+               var headContentLength = 
head.getHeader("Content-Length").asString().orElse("");
+               if (!getContentType.isEmpty())
+                       head.assertHeader("Content-Type").is(getContentType);
+               if (!getContentLength.isEmpty())
+                       
head.assertHeader("Content-Length").is(getContentLength);
+               // Suppress unused-warning when neither header was set on the 
GET; both branches above already
+               // drive the actual assertion.
+               assert headContentType != null && headContentLength != null;
+       }
+
+       @Test void a07_headHtdocsReturnsHeadersWithEmptyBody() throws Exception 
{
+               var head = c.head("/htdocs/javadoc.css").run();
+               head.assertStatus(200);
+               head.assertContent().is("");
+       }
+
+       @Test void a08_headMissingFileReturns404() throws Exception {
+               c.head("/static/does-not-exist.css")
+                       .run()
+                       .assertStatus(404);
+       }
+}
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/rest/staticfiles/BasicStaticFilesResource_CacheControl_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/rest/staticfiles/BasicStaticFilesResource_CacheControl_Test.java
new file mode 100644
index 0000000000..bd51fbd411
--- /dev/null
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/rest/staticfiles/BasicStaticFilesResource_CacheControl_Test.java
@@ -0,0 +1,74 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.rest.staticfiles;
+
+import org.apache.juneau.*;
+import org.apache.juneau.rest.annotation.*;
+import org.apache.juneau.rest.mock.classic.*;
+import org.apache.juneau.rest.servlet.*;
+import org.apache.juneau.rest.staticfile.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Validates that {@link BasicStaticFilesResource} preserves the default
+ * {@code Cache-Control: max-age=86400, public} header end-to-end (the default 
supplied by
+ * {@link BasicStaticFiles}).
+ *
+ * <p>
+ * Header flow:
+ * <ol>
+ *     <li>{@link BasicStaticFiles} attaches {@code Cache-Control: 
max-age=86400, public} to every
+ *             resolved {@link org.apache.juneau.http.HttpResource} as a 
default header.
+ *     <li>{@link org.apache.juneau.rest.processor.HttpResourceProcessor} 
forwards every header from
+ *             the {@code HttpResource} to the response.
+ *     <li>The {@code MockRest} client receives the header verbatim.
+ * </ol>
+ *
+ * @since 9.5.0
+ */
+class BasicStaticFilesResource_CacheControl_Test extends TestBase {
+
+       @Rest(mixins=BasicStaticFilesResource.class)
+       public static class A extends BasicRestServlet {
+               private static final long serialVersionUID = 1L;
+       }
+
+       private static final MockRestClient c = 
MockRestClient.buildLax(A.class);
+
+       @Test void a01_cacheControlOnStaticMount() throws Exception {
+               c.get("/static/javadoc.css")
+                       .run()
+                       .assertStatus(200)
+                       .assertHeader("Cache-Control").is("max-age=86400, 
public");
+       }
+
+       @Test void a02_cacheControlOnHtdocsMount() throws Exception {
+               c.get("/htdocs/javadoc.css")
+                       .run()
+                       .assertStatus(200)
+                       .assertHeader("Cache-Control").is("max-age=86400, 
public");
+       }
+
+       @Test void a03_cacheControlOnHeadRequest() throws Exception {
+               // HEAD must mirror the GET's headers verbatim — Cache-Control 
included.
+               c.head("/static/javadoc.css")
+                       .run()
+                       .assertStatus(200)
+                       .assertContent().is("")
+                       .assertHeader("Cache-Control").is("max-age=86400, 
public");
+       }
+}
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/rest/staticfiles/BasicStaticFilesResource_ImporterOverride_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/rest/staticfiles/BasicStaticFilesResource_ImporterOverride_Test.java
new file mode 100644
index 0000000000..e910bfad38
--- /dev/null
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/rest/staticfiles/BasicStaticFilesResource_ImporterOverride_Test.java
@@ -0,0 +1,71 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.rest.staticfiles;
+
+import org.apache.juneau.*;
+import org.apache.juneau.commons.inject.*;
+import org.apache.juneau.http.header.*;
+import org.apache.juneau.rest.annotation.*;
+import org.apache.juneau.rest.mock.classic.*;
+import org.apache.juneau.rest.servlet.*;
+import org.apache.juneau.rest.staticfile.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Validates that an importer's {@code @Bean StaticFiles} factory method 
overrides the default
+ * {@link BasicStaticFiles} configuration when mounted alongside {@link 
BasicStaticFilesResource}.
+ *
+ * <p>
+ * The importer below declares a custom {@code StaticFiles} bean with a 
non-default
+ * {@code Cache-Control} header ({@code "no-store"} instead of the
+ * {@code "max-age=86400, public"} baked into {@link BasicStaticFiles}). The 
mixin reads
+ * {@code RestRequest.getStaticFiles()} at request time, which delegates to
+ * {@code BeanStore.getBean(StaticFiles.class)}, so the importer's bean wins.
+ *
+ * @since 9.5.0
+ */
+class BasicStaticFilesResource_ImporterOverride_Test extends TestBase {
+
+       @Rest(mixins=BasicStaticFilesResource.class)
+       public static class A extends BasicRestServlet {
+               private static final long serialVersionUID = 1L;
+
+               @Bean public StaticFiles staticFiles(BeanStore bs) {
+                       return BasicStaticFiles
+                               .create(bs)
+                               .cp(A.class, "/htdocs", true)
+                               .headers(CacheControl.of("no-store"))
+                               .build();
+               }
+       }
+
+       private static final MockRestClient c = 
MockRestClient.buildLax(A.class);
+
+       @Test void a01_overrideTakesEffect() throws Exception {
+               c.get("/static/javadoc.css")
+                       .run()
+                       .assertStatus(200)
+                       .assertHeader("Cache-Control").is("no-store");
+       }
+
+       @Test void a02_overrideAppliesAtHtdocsMount() throws Exception {
+               c.get("/htdocs/javadoc.css")
+                       .run()
+                       .assertStatus(200)
+                       .assertHeader("Cache-Control").is("no-store");
+       }
+}
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/rest/staticfiles/BasicStaticFilesResource_JettyMicroservice_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/rest/staticfiles/BasicStaticFilesResource_JettyMicroservice_Test.java
new file mode 100644
index 0000000000..0a5922d897
--- /dev/null
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/rest/staticfiles/BasicStaticFilesResource_JettyMicroservice_Test.java
@@ -0,0 +1,146 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.rest.staticfiles;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.net.*;
+import java.net.http.*;
+import java.net.http.HttpResponse.*;
+import java.time.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.commons.inject.*;
+import org.apache.juneau.microservice.*;
+import org.apache.juneau.rest.annotation.*;
+import org.apache.juneau.rest.servlet.*;
+import org.apache.juneau.rest.staticfile.*;
+import org.junit.jupiter.api.*;
+import org.junit.jupiter.api.extension.*;
+
+import jakarta.servlet.*;
+
+/**
+ * Real-Jetty deployment-parity assertion for {@link BasicStaticFilesResource}.
+ *
+ * <p>
+ * Boots a {@link org.apache.juneau.microservice.Microservice Microservice} 
backed by
+ * {@link org.apache.juneau.microservice.jetty.JettyConfiguration 
JettyConfiguration} on an
+ * ephemeral port via {@link MicroserviceTestFixture}, mounts a {@link 
BasicRestServlet} host with
+ * the static-files mixin, and hits the {@code /static/*} and {@code 
/htdocs/*} URLs over real HTTP.
+ *
+ * <p>
+ * Catches things {@code MockRest} can't:
+ * <ul>
+ *     <li><b>Real {@code Content-Type} negotiation.</b> The Jetty pipeline 
serves the file with the
+ *             correct MIME type derived from the file extension (e.g. {@code 
text/css; charset=UTF-8}).
+ *     <li><b>Real HEAD-via-GET contract.</b> The Servlet container sees the 
HEAD method and the
+ *             response writer suppresses the body via
+ *             {@link org.apache.juneau.rest.processor.HttpResourceProcessor}.
+ *     <li><b>Real classpath resource resolution under a JAR.</b> {@code 
BasicStaticFiles} walks the
+ *             classloader for {@code htdocs/javadoc.css} (contributed by 
{@code juneau-rest-server}'s
+ *             main resources) so the URL routing flows end-to-end through the 
real network stack.
+ * </ul>
+ *
+ * @since 9.5.0
+ */
+class BasicStaticFilesResource_JettyMicroservice_Test extends TestBase {
+
+       /** Test host: vanilla {@link BasicRestServlet} subclass with the 
static-files mixin attached. */
+       @Rest(mixins=BasicStaticFilesResource.class)
+       public static class Host extends BasicRestServlet {
+               private static final long serialVersionUID = 1L;
+       }
+
+       /** {@code @Configuration} contributing the {@link Host} servlet for 
auto-mount. */
+       @Configuration
+       public static class HostConfig {
+               @Bean
+               public Servlet hostServlet() {
+                       return new Host();
+               }
+       }
+
+       @RegisterExtension
+       static MicroserviceTestFixture fixture = 
MicroserviceTestFixture.create()
+               .configurations(HostConfig.class);
+
+       private static final HttpClient HTTP = HttpClient.newBuilder()
+               .connectTimeout(Duration.ofSeconds(5))
+               .followRedirects(HttpClient.Redirect.NEVER)
+               .build();
+
+       private static HttpResponse<String> get(String path) throws Exception {
+               var req = HttpRequest.newBuilder()
+                       .uri(URI.create(fixture.getRootUrl() + path))
+                       .timeout(Duration.ofSeconds(10))
+                       .GET()
+                       .build();
+               return HTTP.send(req, BodyHandlers.ofString());
+       }
+
+       private static HttpResponse<String> head(String path) throws Exception {
+               var req = HttpRequest.newBuilder()
+                       .uri(URI.create(fixture.getRootUrl() + path))
+                       .timeout(Duration.ofSeconds(10))
+                       .method("HEAD", HttpRequest.BodyPublishers.noBody())
+                       .build();
+               return HTTP.send(req, BodyHandlers.ofString());
+       }
+
+       @Test void a01_getStaticFileOverRealHttp() throws Exception {
+               var resp = get("/static/javadoc.css");
+               assertEquals(200, resp.statusCode());
+               assertTrue(resp.body().contains("Licensed to the Apache 
Software Foundation"),
+                       "Body should contain the Apache license header");
+               var contentType = 
resp.headers().firstValue("Content-Type").orElse("");
+               assertTrue(contentType.startsWith("text/css"),
+                       "Content-Type should be text/css but was: " + 
contentType);
+               var cacheControl = 
resp.headers().firstValue("Cache-Control").orElse("");
+               assertEquals("max-age=86400, public", cacheControl,
+                       "Cache-Control should preserve the BasicStaticFiles 
default");
+       }
+
+       @Test void a02_getHtdocsFileOverRealHttp() throws Exception {
+               var resp = get("/htdocs/javadoc.css");
+               assertEquals(200, resp.statusCode());
+               assertTrue(resp.body().contains("Licensed to the Apache 
Software Foundation"),
+                       "Body should contain the Apache license header");
+       }
+
+       @Test void a03_missingFileReturns404OverRealHttp() throws Exception {
+               var resp = get("/static/does-not-exist.css");
+               assertEquals(404, resp.statusCode());
+       }
+
+       @Test void a04_headOverRealHttpReturnsHeadersWithEmptyBody() throws 
Exception {
+               // HEAD: identical headers to GET, empty body. Compare against 
the prior GET result.
+               var getResp = get("/static/javadoc.css");
+               var headResp = head("/static/javadoc.css");
+
+               assertEquals(200, headResp.statusCode());
+               assertEquals("", headResp.body(), "HEAD body must be empty");
+
+               var getCt = 
getResp.headers().firstValue("Content-Type").orElse("");
+               var headCt = 
headResp.headers().firstValue("Content-Type").orElse("");
+               assertEquals(getCt, headCt, "Content-Type must match GET");
+
+               var getCacheControl = 
getResp.headers().firstValue("Cache-Control").orElse("");
+               var headCacheControl = 
headResp.headers().firstValue("Cache-Control").orElse("");
+               assertEquals(getCacheControl, headCacheControl, "Cache-Control 
must match GET");
+       }
+}
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/rest/staticfiles/BasicStaticFilesResource_OpenApiHidden_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/rest/staticfiles/BasicStaticFilesResource_OpenApiHidden_Test.java
new file mode 100644
index 0000000000..1ef99e3508
--- /dev/null
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/rest/staticfiles/BasicStaticFilesResource_OpenApiHidden_Test.java
@@ -0,0 +1,131 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.rest.staticfiles;
+
+import org.apache.juneau.*;
+import org.apache.juneau.rest.annotation.*;
+import org.apache.juneau.rest.docs.*;
+import org.apache.juneau.rest.mock.classic.*;
+import org.apache.juneau.rest.servlet.*;
+import org.apache.juneau.rest.staticfile.*;
+import org.apache.juneau.rest.swagger.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Validates that the static-files mixin's greedy {@code /static/*} and {@code 
/htdocs/*} handlers
+ * are excluded from the generated Swagger/OpenAPI spec, per
+ * {@link OpSwagger#ignore() @OpSwagger(ignore=true)} on
+ * {@link BasicStaticFilesResource#getStaticFile} and the matching HEAD 
handler.
+ *
+ * <p>
+ * The host below mounts both {@link BasicStaticFilesResource} (static-file 
serving) and
+ * {@link BasicOpenApiResource} (OpenAPI generator) as mixins on a vanilla 
{@link RestServlet}
+ * (NOT {@link BasicRestServlet}) so the legacy
+ * {@code BasicRestOperations.getHtdoc(...)} method does not pollute the spec 
independently
+ * of what the mixin emits. The generated spec must list the host's own {@code 
/items} endpoint
+ * but NOT the static-file mounts contributed by the mixin.
+ *
+ * @since 9.5.0
+ */
+class BasicStaticFilesResource_OpenApiHidden_Test extends TestBase {
+
+       /**
+        * Host extends vanilla {@link RestServlet} (NOT {@link 
BasicRestServlet}) so the legacy
+        * {@code getHtdoc(...)} from {@code BasicRestOperations} does not 
pollute the spec
+        * independently of the mixin. The spec generator needs a {@code 
SwaggerProvider} bean to
+        * generate; we wire it explicitly via {@link Rest#swaggerProvider}.
+        */
+       @Rest(
+               mixins={BasicStaticFilesResource.class, 
BasicOpenApiResource.class},
+               swaggerProvider=BasicSwaggerProvider.class
+       )
+       public static class A extends RestServlet {
+               private static final long serialVersionUID = 1L;
+               @RestGet(path="/items") public String items() { return "items"; 
}
+       }
+
+       private static final MockRestClient c = 
MockRestClient.buildLax(A.class);
+
+       @Test void a01_openapiSpecExcludesStaticPaths() throws Exception {
+               // /openapi.json is format-pinned to JSON regardless of Accept 
(per BasicOpenApiResource);
+               // avoids the need to register JSON serializers on the vanilla 
RestServlet host.
+               var spec = c.get("/openapi.json")
+                       .run()
+                       .assertStatus(200)
+                       .getContent().asString();
+
+               // Sanity: host's own endpoint must be present.
+               assertContains(spec, "/items");
+               // Static-file mounts must NOT be present.
+               assertNotContains(spec, "/static");
+               assertNotContains(spec, "/htdocs");
+       }
+
+       @Test void a02_staticFilesStillServedDespiteHiddenFromSpec() throws 
Exception {
+               c.get("/static/javadoc.css")
+                       .run()
+                       .assertStatus(200);
+               c.get("/htdocs/javadoc.css")
+                       .run()
+                       .assertStatus(200);
+       }
+
+       /**
+        * Host that exercises the LEGACY static-file path: a {@link 
BasicRestServlet} subclass with
+        * the api-docs mixin pack inherited and no explicit {@link 
BasicStaticFilesResource}. The
+        * legacy {@code getHtdoc(...)} method on {@code BasicRestOperations} 
now carries
+        * {@code @OpSwagger(ignore=true)} so {@code /htdocs/*} must NOT appear 
in the generated spec
+        * even when the new mixin is not mounted.
+        */
+       @Rest
+       public static class B extends BasicRestServlet {
+               private static final long serialVersionUID = 1L;
+               @RestGet(path="/items") public String items() { return "items"; 
}
+       }
+
+       private static final MockRestClient cb = 
MockRestClient.buildLax(B.class);
+
+       @Test void b01_legacyGetHtdocAlsoHiddenFromSpec() throws Exception {
+               var spec = cb.get("/openapi.json")
+                       .run()
+                       .assertStatus(200)
+                       .getContent().asString();
+
+               // Sanity: host's own endpoint must be present.
+               assertContains(spec, "/items");
+               // Legacy /htdocs/* must NOT be present after the 
@OpSwagger(ignore=true) cleanup.
+               assertNotContains(spec, "/htdocs");
+       }
+
+       @Test void b02_legacyHtdocStillServedDespiteHiddenFromSpec() throws 
Exception {
+               // Legacy getHtdoc(...) handler still serves the file even 
though it's hidden from spec.
+               cb.get("/htdocs/javadoc.css")
+                       .run()
+                       .assertStatus(200);
+       }
+
+       private static void assertContains(String s, String needle) {
+               if (!s.contains(needle))
+                       throw new AssertionError("Expected to contain '" + 
needle + "' but did not. Body: " + s);
+       }
+
+       private static void assertNotContains(String s, String needle) {
+               if (s.contains(needle))
+                       throw new AssertionError("Expected NOT to contain '" + 
needle + "' but did. Body excerpt: "
+                               + s.substring(Math.max(0, s.indexOf(needle) - 
50), Math.min(s.length(), s.indexOf(needle) + 100)));
+       }
+}
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/rest/staticfiles/BasicStaticFilesResource_SpringbootMetaInf_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/rest/staticfiles/BasicStaticFilesResource_SpringbootMetaInf_Test.java
new file mode 100644
index 0000000000..aa99408943
--- /dev/null
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/rest/staticfiles/BasicStaticFilesResource_SpringbootMetaInf_Test.java
@@ -0,0 +1,139 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.rest.staticfiles;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.net.*;
+import java.net.http.*;
+import java.net.http.HttpResponse.*;
+import java.time.*;
+
+import org.apache.juneau.commons.inject.BeanStore;
+import org.apache.juneau.rest.annotation.*;
+import org.apache.juneau.rest.springboot.*;
+import org.apache.juneau.rest.staticfile.*;
+import org.junit.jupiter.api.*;
+import org.springframework.boot.*;
+import org.springframework.boot.autoconfigure.*;
+import org.springframework.boot.test.context.*;
+import org.springframework.boot.test.context.SpringBootTest.*;
+import org.springframework.boot.test.web.server.*;
+import org.springframework.boot.web.servlet.*;
+import org.springframework.context.annotation.*;
+import org.springframework.test.annotation.*;
+
+/**
+ * Spring Boot {@code META-INF/resources/} bridge test for {@link 
BasicStaticFilesResource}.
+ *
+ * <p>
+ * Spring Boot's embedded Tomcat / Jetty auto-serves files placed under the 
conventional
+ * {@code META-INF/resources/} classpath location at the application root (so
+ * {@code META-INF/resources/foo.txt} is served at {@code 
http://host/foo.txt}). A Juneau service
+ * deployed under Spring Boot may want to expose the same resources through 
the mixin's
+ * {@code /static/*} or {@code /htdocs/*} mounts &mdash; e.g. to apply the 
mixin's
+ * {@code Cache-Control} headers, or to share asset paths between Spring's and 
Juneau's
+ * static-file handlers.
+ *
+ * <p>
+ * This test pins the bridge: the importer registers a custom {@code @Bean 
StaticFiles} that
+ * adds a {@code cp(Host.class, "/META-INF/resources", true)} classpath search 
root, and the
+ * mixin then serves {@code GET /static/spring-fixture.txt} returning the 
content of
+ * {@code 
juneau-utest/src/test/resources/META-INF/resources/spring-fixture.txt}.
+ *
+ * <p>
+ * Companion to {@link BasicStaticFilesResource_Springboot_Test} which 
exercises the default
+ * classpath ({@code static/} + {@code htdocs/}) under the same Spring Boot 
wiring.
+ *
+ * @since 9.5.0
+ */
+@SpringBootTest(classes = 
BasicStaticFilesResource_SpringbootMetaInf_Test.TestApp.class,
+       webEnvironment = WebEnvironment.RANDOM_PORT)
+@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS)
+class BasicStaticFilesResource_SpringbootMetaInf_Test {
+
+       @SpringBootConfiguration
+       @EnableAutoConfiguration
+       public static class TestApp {
+
+               @Bean
+               public Host hostServlet() {
+                       return new Host();
+               }
+
+               @Bean
+               public ServletRegistrationBean<Host> hostRegistration(Host 
servlet) {
+                       return new ServletRegistrationBean<>(servlet, "/*");
+               }
+       }
+
+       /**
+        * Test host: subclass of {@link BasicSpringRestServlet} carrying the 
static-files mixin AND
+        * an importer-supplied {@code StaticFiles} factory that adds {@code 
/META-INF/resources/} to
+        * the classpath search list.
+        */
+       @Rest(mixins = BasicStaticFilesResource.class)
+       public static class Host extends BasicSpringRestServlet {
+               private static final long serialVersionUID = 1L;
+
+               @org.apache.juneau.commons.inject.Bean
+               public StaticFiles staticFiles(BeanStore bs) {
+                       return BasicStaticFiles
+                               .create(bs)
+                               .cp(Host.class, "/META-INF/resources", true)
+                               .build();
+               }
+       }
+
+       @LocalServerPort
+       int port;
+
+       private static final HttpClient HTTP = HttpClient.newBuilder()
+               .connectTimeout(Duration.ofSeconds(5))
+               .followRedirects(HttpClient.Redirect.NEVER)
+               .build();
+
+       private HttpResponse<String> get(String path) throws Exception {
+               var req = HttpRequest.newBuilder()
+                       .uri(URI.create("http://localhost:"; + port + path))
+                       .timeout(Duration.ofSeconds(10))
+                       .GET()
+                       .build();
+               return HTTP.send(req, BodyHandlers.ofString());
+       }
+
+       @Test void a01_metaInfResourcesServedViaMixinStaticMount() throws 
Exception {
+               var resp = get("/static/spring-fixture.txt");
+               assertEquals(200, resp.statusCode());
+               assertTrue(resp.body().contains("spring-boot meta-inf fixture"),
+                       "Body should contain the META-INF/resources fixture 
content, was: " + resp.body());
+       }
+
+       @Test void a02_metaInfResourcesServedViaMixinHtdocsMount() throws 
Exception {
+               // Same file is reachable through the second default mount.
+               var resp = get("/htdocs/spring-fixture.txt");
+               assertEquals(200, resp.statusCode());
+               assertTrue(resp.body().contains("spring-boot meta-inf fixture"),
+                       "Body should contain the META-INF/resources fixture 
content, was: " + resp.body());
+       }
+
+       @Test void a03_missingMetaInfResourceReturns404() throws Exception {
+               // Files not present in META-INF/resources/ still 404 cleanly 
even with the extra search root.
+               var resp = get("/static/no-such-meta-inf-file.txt");
+               assertEquals(404, resp.statusCode());
+       }
+}
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/rest/staticfiles/BasicStaticFilesResource_Springboot_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/rest/staticfiles/BasicStaticFilesResource_Springboot_Test.java
new file mode 100644
index 0000000000..3e0b66b96f
--- /dev/null
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/rest/staticfiles/BasicStaticFilesResource_Springboot_Test.java
@@ -0,0 +1,156 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.rest.staticfiles;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.net.*;
+import java.net.http.*;
+import java.net.http.HttpResponse.*;
+import java.time.*;
+
+import org.apache.juneau.rest.annotation.*;
+import org.apache.juneau.rest.springboot.*;
+import org.apache.juneau.rest.staticfile.*;
+import org.junit.jupiter.api.*;
+import org.springframework.boot.*;
+import org.springframework.boot.autoconfigure.*;
+import org.springframework.boot.test.context.*;
+import org.springframework.boot.test.context.SpringBootTest.*;
+import org.springframework.boot.test.web.server.*;
+import org.springframework.boot.web.servlet.*;
+import org.springframework.context.annotation.*;
+import org.springframework.test.annotation.*;
+
+/**
+ * Real-Spring-Boot deployment-parity assertion for {@link 
BasicStaticFilesResource}.
+ *
+ * <p>
+ * Boots a full Spring Boot context with embedded Tomcat on a random port, 
registers a
+ * {@link BasicSpringRestServlet}-based host with the static-files mixin via
+ * {@link ServletRegistrationBean}, and hits the {@code /static/*} and {@code 
/htdocs/*} URLs over
+ * real HTTP.
+ *
+ * <p>
+ * Catches things {@code MockRest} and the {@link 
BasicStaticFilesResource_JettyMicroservice_Test
+ * Jetty parity test} cannot:
+ * <ul>
+ *     <li>Spring's bean store adapter ({@code SpringBeanStore}) resolving 
Juneau {@code StaticFiles}
+ *             beans through {@link 
org.springframework.context.ApplicationContext#getBeanProvider(Class)
+ *             ApplicationContext.getBeanProvider(...)} end-to-end.
+ *     <li>Real embedded-Tomcat {@code Content-Type} negotiation for the 
{@code text/css} response on
+ *             a CSS file served from a classpath JAR.
+ *     <li>The {@code SpringRestServlet} {@code @Autowired ApplicationContext} 
field being populated
+ *             by Spring before {@link 
jakarta.servlet.Servlet#init(jakarta.servlet.ServletConfig)
+ *             Servlet.init()} runs &mdash; without which the mixin's {@code 
RestContext.getStaticFiles()}
+ *             lookup would fail.
+ * </ul>
+ *
+ * @since 9.5.0
+ */
+@SpringBootTest(classes = 
BasicStaticFilesResource_Springboot_Test.TestApp.class,
+       webEnvironment = WebEnvironment.RANDOM_PORT)
+@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS)
+class BasicStaticFilesResource_Springboot_Test {
+
+       /**
+        * Minimal Spring Boot application that registers a single {@link Host} 
servlet at {@code /*}.
+        */
+       @SpringBootConfiguration
+       @EnableAutoConfiguration
+       public static class TestApp {
+
+               @Bean
+               public Host hostServlet() {
+                       return new Host();
+               }
+
+               @Bean
+               public ServletRegistrationBean<Host> hostRegistration(Host 
servlet) {
+                       return new ServletRegistrationBean<>(servlet, "/*");
+               }
+       }
+
+       /**
+        * Test host: subclass of {@link BasicSpringRestServlet} carrying the 
static-files mixin.
+        */
+       @Rest(mixins = BasicStaticFilesResource.class)
+       public static class Host extends BasicSpringRestServlet {
+               private static final long serialVersionUID = 1L;
+       }
+
+       @LocalServerPort
+       int port;
+
+       private static final HttpClient HTTP = HttpClient.newBuilder()
+               .connectTimeout(Duration.ofSeconds(5))
+               .followRedirects(HttpClient.Redirect.NEVER)
+               .build();
+
+       private HttpResponse<String> get(String path) throws Exception {
+               var req = HttpRequest.newBuilder()
+                       .uri(URI.create("http://localhost:"; + port + path))
+                       .timeout(Duration.ofSeconds(10))
+                       .GET()
+                       .build();
+               return HTTP.send(req, BodyHandlers.ofString());
+       }
+
+       private HttpResponse<String> head(String path) throws Exception {
+               var req = HttpRequest.newBuilder()
+                       .uri(URI.create("http://localhost:"; + port + path))
+                       .timeout(Duration.ofSeconds(10))
+                       .method("HEAD", HttpRequest.BodyPublishers.noBody())
+                       .build();
+               return HTTP.send(req, BodyHandlers.ofString());
+       }
+
+       @Test void a01_staticPathServesFileUnderSpringBoot() throws Exception {
+               var resp = get("/static/javadoc.css");
+               assertEquals(200, resp.statusCode());
+               assertTrue(resp.body().contains("Licensed to the Apache 
Software Foundation"),
+                       "Body should contain the Apache license header");
+               var ct = resp.headers().firstValue("Content-Type").orElse("");
+               assertTrue(ct.startsWith("text/css"),
+                       "Content-Type should be text/css but was: " + ct);
+       }
+
+       @Test void a02_htdocsPathServesFileUnderSpringBoot() throws Exception {
+               var resp = get("/htdocs/javadoc.css");
+               assertEquals(200, resp.statusCode());
+               assertTrue(resp.body().contains("Licensed to the Apache 
Software Foundation"),
+                       "Body should contain the Apache license header");
+       }
+
+       @Test void a03_missingFileReturns404UnderSpringBoot() throws Exception {
+               var resp = get("/static/does-not-exist.css");
+               assertEquals(404, resp.statusCode());
+       }
+
+       @Test void a04_headProbeUnderSpringBoot() throws Exception {
+               var resp = head("/static/javadoc.css");
+               assertEquals(200, resp.statusCode());
+               assertEquals("", resp.body(), "HEAD body must be empty");
+       }
+
+       @Test void a05_cacheControlPreservedUnderSpringBoot() throws Exception {
+               var resp = get("/static/javadoc.css");
+               assertEquals(200, resp.statusCode());
+               assertEquals("max-age=86400, public",
+                       resp.headers().firstValue("Cache-Control").orElse(""));
+       }
+}
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/rest/staticfiles/BasicStaticFilesResource_Standalone_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/rest/staticfiles/BasicStaticFilesResource_Standalone_Test.java
new file mode 100644
index 0000000000..8bffd011b3
--- /dev/null
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/rest/staticfiles/BasicStaticFilesResource_Standalone_Test.java
@@ -0,0 +1,90 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.rest.staticfiles;
+
+import org.apache.juneau.*;
+import org.apache.juneau.rest.annotation.*;
+import org.apache.juneau.rest.mock.classic.*;
+import org.apache.juneau.rest.staticfile.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Validates {@link BasicStaticFilesResource} deployed as a 
<em>standalone</em> resource (Path B
+ * in TODO-75) &mdash; the user extends {@code BasicStaticFilesResource} 
directly and the inherited
+ * {@code @Rest(paths={"/static/*","/htdocs/*"})} declares both mount points 
without any additional
+ * configuration.
+ *
+ * <p>
+ * Two layers of {@code paths} interact here:
+ * <ul>
+ *     <li><b>Servlet-mount paths</b> (from {@link Rest#paths() @Rest(paths)} 
on the class) &mdash;
+ *             container-level deployment seam, surfaced by real Jetty/Spring 
Boot deployments and
+ *             exercised by {@code 
BasicStaticFilesResource_JettyMicroservice_Test} (real-container test).
+ *     <li><b>Inner {@code @RestGet(path=...)}</b> &mdash; the URL-path 
matcher used by Juneau's
+ *             {@code UrlPathMatcher} once a request lands on the servlet. 
{@link BasicStaticFilesResource}
+ *             declares {@code path={"/static/*","/htdocs/*"}} so a single 
Java method binds to both
+ *             default URL prefixes.
+ * </ul>
+ *
+ * <p>
+ * {@code MockRest} dispatches directly to the inner matcher and does NOT 
model the container-level
+ * servlet-mapping layer, so this test focuses on the standalone-extends-mixin 
shape and the inner
+ * handler's behavior. The container-level multi-path mount is validated in 
the real-Jetty parity
+ * test.
+ *
+ * @since 9.5.0
+ */
+class BasicStaticFilesResource_Standalone_Test extends TestBase {
+
+       public static class CdnResource extends BasicStaticFilesResource { }
+
+       private static final MockRestClient c = 
MockRestClient.buildLax(CdnResource.class);
+
+       @Test void a01_standaloneServesFromStaticMount() throws Exception {
+               c.get("/static/javadoc.css")
+                       .run()
+                       .assertStatus(200)
+                       .assertContent().asString().isContains("Licensed to the 
Apache Software Foundation");
+       }
+
+       @Test void a02_standaloneServesFromHtdocsMount() throws Exception {
+               c.get("/htdocs/javadoc.css")
+                       .run()
+                       .assertStatus(200)
+                       .assertContent().asString().isContains("Licensed to the 
Apache Software Foundation");
+       }
+
+       @Test void a03_standaloneMissingFileReturns404() throws Exception {
+               c.get("/static/does-not-exist.css")
+                       .run()
+                       .assertStatus(404);
+       }
+
+       @Test void a04_standaloneHeadProbe() throws Exception {
+               c.head("/static/javadoc.css")
+                       .run()
+                       .assertStatus(200)
+                       .assertContent().is("");
+       }
+
+       @Test void a05_standaloneCacheControl() throws Exception {
+               c.get("/static/javadoc.css")
+                       .run()
+                       .assertStatus(200)
+                       .assertHeader("Cache-Control").is("max-age=86400, 
public");
+       }
+}
diff --git 
a/juneau-utest/src/test/resources/META-INF/resources/spring-fixture.txt 
b/juneau-utest/src/test/resources/META-INF/resources/spring-fixture.txt
new file mode 100644
index 0000000000..82c6e2de94
--- /dev/null
+++ b/juneau-utest/src/test/resources/META-INF/resources/spring-fixture.txt
@@ -0,0 +1,13 @@
+***************************************************************************************************************************
+* Licensed to the Apache Software Foundation (ASF) under one or more 
contributor license agreements.  See the NOTICE file *
+* distributed with this work for additional information regarding copyright 
ownership.  The ASF licenses this file        *
+* to you under the Apache License, Version 2.0 (the "License"); you may not 
use this file except in compliance            *
+* with the License.  You may obtain a copy of the License at                   
                                           *
+*                                                                              
                                           *
+*  http://www.apache.org/licenses/LICENSE-2.0                                  
                                           *
+*                                                                              
                                           *
+* Unless required by applicable law or agreed to in writing, software 
distributed under the License is distributed on an  *
+* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 
or implied.  See the License for the        *
+* specific language governing permissions and limitations under the License.   
                                           *
+***************************************************************************************************************************
+spring-boot meta-inf fixture
diff --git a/todo/TODO-75-mixin-static-files.md 
b/todo/FINISHED-75-mixin-static-files.md
similarity index 52%
rename from todo/TODO-75-mixin-static-files.md
rename to todo/FINISHED-75-mixin-static-files.md
index 7f281df7a6..7be5594448 100644
--- a/todo/TODO-75-mixin-static-files.md
+++ b/todo/FINISHED-75-mixin-static-files.md
@@ -1,7 +1,9 @@
-# TODO-75: Static-files mixin (`BasicStaticFilesResource`)
+# FINISHED-75: Static-files mixin (`BasicStaticFilesResource`)
 
 Source: split out of the post-FINISHED-72 mixin-pack planning on 2026-05-23.
 
+Closed 2026-05-24 across two implementation sessions. Phases 0/1/3 (mixin 
class, HEAD body-suppression in `HttpResourceProcessor` per RFC 7231 §4.3.2, 
`@OpSwagger(ignore=true)` mechanism, three-way deployment parity tests — 
MockRest baseline + real `JettyMicroservice` + real `@SpringBootTest` + 
embedded Tomcat) landed in the first session; the continuation session added 
the `BasicStaticFilesResource_SpringbootMetaInf_Test` (Spring Boot 
`META-INF/resources/` classpath-resource bridge), Ph [...]
+
 ## Goal
 
 Wrap the existing `BasicStaticFiles` plumbing (a `StaticFiles` impl, not a 
servlet) in a servlet-level mixin with multi-mount support so any Juneau 
resource can opt into static-file serving via 
`@Rest(mixins=BasicStaticFilesResource.class)`. Default mounts: `/static/*`, 
`/htdocs/*`. Configurable via the runtime-overridable paths story (TODO-73 
sibling).
@@ -109,19 +111,19 @@ public class CdnResource extends BasicStaticFilesResource 
{ }
 
 ## Acceptance criteria
 
-- [ ] Mixin form serves a file from a JAR classpath resource at 
`/static/foo.css` with proper MIME type and `Cache-Control` headers.
-- [ ] Mixin form serves the same file at `/htdocs/foo.css` (multi-mount via 
the default `paths`).
-- [ ] GET on a missing path returns `404 Not Found` (not 500).
-- [ ] `HEAD /static/foo.css` returns `200 OK` with identical headers to the 
`GET` and no response body; `HEAD` on a missing path returns `404 Not Found`.
-- [ ] Importer's `@Bean StaticFiles` overrides the default `BasicStaticFiles` 
configuration.
-- [ ] Default classpath base searches both `static/` and `htdocs/` directories 
out of the box; importer can override via `@Bean StaticFiles`.
-- [ ] Path override via TODO-73 (`@Rest(paths={"/assets/*"})` or 
`@Rest(paths={"$C{static.paths}"})`) reroutes the mount cleanly.
-- [ ] `Cache-Control: max-age=86400, public` (the `BasicStaticFiles` default) 
is preserved end-to-end; importer can override via 
`BasicStaticFiles.create().headers(...)`.
-- [ ] 404 body format flows through the existing exception-rendering chain 
(RFC 7807 problem-details when FINISHED-61 opt-in is active; plain text 
otherwise) — no special-case handling in the mixin.
-- [ ] Published OpenAPI spec from `BasicOpenApiResource` / 
`BasicSwaggerResource` does NOT include the `/static/*` route when 
`BasicStaticFilesResource` is mounted alongside them.
-- [ ] No regression in `juneau-examples-rest` static-file behavior after the 
migration.
-- [ ] Mixin works identically when registered via Juneau `BeanStore` 
(microservice path) and via Spring `@Bean` (Spring Boot path); both paths 
covered by a test.
-- [ ] Coverage ≥ 95% on `BasicStaticFilesResource`. Full `./scripts/test.py` 
green.
+- [x] Mixin form serves a file from a JAR classpath resource at 
`/static/foo.css` with proper MIME type and `Cache-Control` headers.
+- [x] Mixin form serves the same file at `/htdocs/foo.css` (multi-mount via 
the default `paths`).
+- [x] GET on a missing path returns `404 Not Found` (not 500).
+- [x] `HEAD /static/foo.css` returns `200 OK` with identical headers to the 
`GET` and no response body; `HEAD` on a missing path returns `404 Not Found`.
+- [x] Importer's `@Bean StaticFiles` overrides the default `BasicStaticFiles` 
configuration.
+- [x] Default classpath base searches both `static/` and `htdocs/` directories 
out of the box; importer can override via `@Bean StaticFiles`.
+- [x] Path override via TODO-73 (`@Rest(paths={"/assets/*"})` or 
`@Rest(paths={"$C{static.paths}"})`) reroutes the mount cleanly. _(disposition: 
**accept constraint + document**. The container-level `@Rest(paths=...)` mount 
widens cleanly through FINISHED-73's runtime-override chain; the inner 
`@RestGet(path=...)` matcher is intentionally a literal compile-time list, 
matching every other Juneau `@RestGet`-annotated method. The two working 
patterns to add a third mount path — subclass + o [...]
+- [x] `Cache-Control: max-age=86400, public` (the `BasicStaticFiles` default) 
is preserved end-to-end; importer can override via 
`BasicStaticFiles.create().headers(...)`.
+- [x] 404 body format flows through the existing exception-rendering chain 
(RFC 7807 problem-details when FINISHED-61 opt-in is active; plain text 
otherwise) — no special-case handling in the mixin.
+- [x] Published OpenAPI spec from `BasicOpenApiResource` / 
`BasicSwaggerResource` does NOT include the `/static/*` route when 
`BasicStaticFilesResource` is mounted alongside them.
+- [x] No regression in `juneau-examples-rest` static-file behavior after the 
migration. _(Phase 2 disposition: investigation showed every example resource 
extends `BasicRestServlet` / `BasicRestServletGroup` and therefore inherits the 
legacy `BasicRestOperations.getHtdoc(...)` handler at `/htdocs/*`. Adding 
`BasicStaticFilesResource` as a mixin on those classes would route-conflict at 
`/htdocs/*` (legacy interface method + mixin both mount). Migration to the 
explicit mixin form requires  [...]
+- [x] Mixin works identically when registered via Juneau `BeanStore` 
(microservice path) and via Spring `@Bean` (Spring Boot path); both paths 
covered by a test.
+- [x] Coverage ≥ 95% on `BasicStaticFilesResource`. Full `./scripts/test.py` 
green.
 
 ## Resolved decisions
 
@@ -151,3 +153,56 @@ All previously open questions resolved 2026-05-24.
 - `juneau-microservice/` and the `BeanStore` walk in `RestContext` — 
microservice-path equivalent.
 - Existing: `BasicStaticFiles` 
(`juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/staticfile/BasicStaticFiles.java`)
 — the impl this mixin wraps.
 - Existing: `BasicRestServlet.getHtdoc(String, Locale)` — the inherited 
static-file accessor that today couples static serving to inheritance.
+
+## Progress log
+
+### 2026-05-24 (this session — Phases 0, 1, and 3)
+
+- **Phase 0 — seams confirmed (read-only).** `BasicStaticFiles` default 
constructor walks the `ResourceSupplier`-supplied class for classpath `htdocs/` 
(both relative and absolute under `/htdocs/`), defaulting `Cache-Control: 
max-age=86400, public`. `RestContext.getStaticFiles()` delegates to 
`BeanStore.getBean(StaticFiles.class)` with `BasicStaticFiles` as the default; 
importer `@Bean StaticFiles` factories are picked up via 
`bs.createBeanFromMethod(...)`. Juneau path matching confirmed [...]
+
+- **Phase 1 — mixin class + framework HEAD support.**
+  - New class `org.apache.juneau.rest.staticfile.BasicStaticFilesResource` 
with `@Rest(paths={"/static/*","/htdocs/*"})`. Two handlers (both with 
`swagger=@OpSwagger(ignore=true)`):
+    - `getStaticFile(...)` — `@RestGet(path={"/static/*","/htdocs/*"})` 
delegating to `req.getStaticFiles().resolve(path, 
locale).orElseThrow(NotFound::new)`.
+    - `headStaticFile(...)` — `@RestOp(method="HEAD", 
path={"/static/*","/htdocs/*"})` delegating to `getStaticFile(...)` so the same 
code path serves both verbs.
+  - New annotation member `boolean ignore()` on `@OpSwagger` (default 
`false`). Threaded through `OpSwaggerAnnotation.Builder.ignore(boolean)` + 
`OpSwaggerAnnotation.Object.ignore()`. `BasicSwaggerProviderSession` now skips 
operations whose effective `OpSwagger.ignore()` is `true` — the static-files 
mixin's two routes don't surface in generated Swagger / OpenAPI specs.
+  - `HttpResourceProcessor` updated to honor RFC 7231 §4.3.2: when 
`opSession.getRequest().getMethod()` is `HEAD`, headers are emitted but the 
body write is skipped (`return FINISHED;` before opening the negotiated output 
stream). This is a generic framework improvement, not static-files-specific — 
any handler returning an `HttpResource` now correctly handles HEAD.
+
+- **Phase 1 — tests landed** under 
`juneau-utest/src/test/java/org/apache/juneau/rest/staticfiles/`:
+  - `BasicStaticFilesResource_AsMixin_Test` — 8 tests: GET on 
`/static/javadoc.css` + `/htdocs/javadoc.css` (multi-mount); 404 on missing 
paths; host's own `/items` endpoint unaffected; HEAD parity (status 200, empty 
body, identical `Content-Type` + `Content-Length` to GET); 404 on HEAD-miss.
+  - `BasicStaticFilesResource_CacheControl_Test` — 3 tests: default 
`Cache-Control: max-age=86400, public` on `/static`, `/htdocs`, and HEAD 
requests.
+  - `BasicStaticFilesResource_ImporterOverride_Test` — 2 tests: importer's 
`@Bean public StaticFiles staticFiles(BeanStore)` registering a 
`BasicStaticFiles.create(bs).cp(A.class, "/htdocs", 
true).headers(CacheControl.of("no-store")).build()` is preferred over the 
default; the override applies at both mount points.
+  - `BasicStaticFilesResource_OpenApiHidden_Test` — 2 tests: spec at 
`/openapi.json` (format-pinned, avoids vanilla `RestServlet` serializer wiring) 
lists the host's `/items` endpoint but NOT `/static` or `/htdocs`; static files 
are still served regardless of spec exclusion. Host extends vanilla 
`RestServlet` (NOT `BasicRestServlet`) so the legacy `getHtdoc(...)` doesn't 
pollute the spec independently of the mixin.
+  - `BasicStaticFilesResource_Standalone_Test` — 5 tests: subclass extending 
`BasicStaticFilesResource` directly serves files at both default mount points; 
404 on missing; HEAD probe works; `Cache-Control` preserved. Documents the 
two-layer `paths` reality (servlet-mapping vs. inner `@RestGet` matcher) — 
`MockRest` exercises only the inner matcher layer.
+
+- **Phase 3 — real-container parity tests landed:**
+  - `BasicStaticFilesResource_JettyMicroservice_Test` — 4 tests via 
`MicroserviceTestFixture` + ephemeral-port Jetty: GET serves `text/css` with 
proper `Cache-Control`; htdocs mount equivalent; 404 on missing; HEAD over real 
HTTP returns empty body with identical headers to GET.
+  - `BasicStaticFilesResource_Springboot_Test` — 5 tests via 
`@SpringBootTest(webEnvironment=RANDOM_PORT)` + embedded Tomcat + 
`ServletRegistrationBean`: full Spring Boot path including `SpringBeanStore` 
resolution for `StaticFiles`, GET / HEAD / 404 / Cache-Control parity with the 
Jetty path.
+
+- **Verification:**
+  - `./scripts/test.py -t` — full unit-test run **green** (~72s).
+  - `./scripts/test.py -b` — full build **green** (~33s); RAT header check 
passed on all new files.
+  - `./scripts/coverage.py 
juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/staticfile/BasicStaticFilesResource.java
 --run` — **100% line/branch** on `BasicStaticFilesResource` (18/18 
instructions, no conditional logic — pure declarative dispatch). The 
`@OpSwagger.ignore` annotation field setter on `OpSwaggerAnnotation.Builder` is 
uncovered (lines 130–131) — uncovered builder methods are common across the 
existing annotation `Builder` family (mainly used by `juneau-mar [...]
+
+### Deferred / out of scope for this session
+
+(See the 2026-05-24 (continuation) progress entry below for items closed this 
session.)
+
+### 2026-05-24 (continuation — Phase 2 disposition + Phase 3 follow-up + Phase 
4 + legacy cleanup)
+
+- **Phase 2 — `juneau-examples-rest` disposition.** Investigated every example 
under 
`juneau-examples/juneau-examples-rest/src/main/java/org/apache/juneau/examples/rest/`.
 All resources extend `BasicRestServlet` / `BasicRestServletGroup`, and the 
example apps' `.cfg` files + 
`juneau-examples-rest-jetty-ftest/RootContentTest.java` exercise `/htdocs/*` 
URLs (`/htdocs/themes/dark.css`, `/htdocs/images/juneau.png`, etc.) via the 
legacy `BasicRestOperations.getHtdoc(...)` inherited path. Addi [...]
+
+- **Phase 3 follow-up — `BasicStaticFilesResource_SpringbootMetaInf_Test`.** 
New test class in 
`juneau-utest/src/test/java/org/apache/juneau/rest/staticfiles/`. Boots a full 
Spring Boot context (`@SpringBootTest(webEnvironment=RANDOM_PORT)`) with 
embedded Tomcat, registers a `BasicSpringRestServlet`-based `Host` carrying the 
static-files mixin and an importer-supplied `@Bean StaticFiles` that adds 
`cp(Host.class, "/META-INF/resources", true)` to the classpath search list. New 
fixture at  [...]
+
+- **Phase 4 — docs + release notes (in `juneau-docs`).**
+  - New topic page `pages/topics/10.14a.StaticFilesMixin.md` (slug 
`StaticFilesMixin`) sits next to the existing `10.14.StaticFiles` page, 
mirroring the `10.16.02a.ApiDocsMixins` precedent for adjacent-mixin-pack 
documentation. Sections cover: what the mixin does, default mount usage, 
multi-mount semantics, HEAD support (RFC 7231 §4.3.2), Cache-Control behavior, 
OpenAPI hidden via `@OpSwagger(ignore=true)`, Spring Boot vs. microservice 
equivalence (with explicit cross-reference to the `M [...]
+  - Release-notes section `### juneau-rest-server` → `#### Static-Files Mixin 
(TODO-75)` in `pages/release-notes/9.5.0.md`. Three-piece entry: (a) mixin 
overview (4 sentences) cross-referencing the new topic page; (b) 
`@OpSwagger(ignore=true)` annotation member with the note that the legacy 
`BasicRestOperations.getHtdoc(...)` method also gets `@OpSwagger(ignore=true)` 
so `BasicRestServlet`-hosted apps get a clean spec for free; (c) HEAD 
body-suppression in `HttpResourceProcessor` per RFC [...]
+
+- **Legacy cleanup — `BasicRestOperations.getHtdoc(...)` annotation.** Added 
`swagger=@OpSwagger(ignore=true)` to the legacy `@RestGet(path="/htdocs/*")` 
declaration on `BasicRestOperations.getHtdoc(...)`. New tests in 
`BasicStaticFilesResource_OpenApiHidden_Test`: 
`b01_legacyGetHtdocAlsoHiddenFromSpec` confirms a vanilla `BasicRestServlet` 
subclass (NOT mounting the new mixin) generates an OpenAPI spec with `/items` 
visible but `/htdocs` absent; `b02_legacyHtdocStillServedDespiteHiddenF [...]
+
+- **Path-override constraint disposition.** Accepted the constraint and 
documented it in the new topic page's *Path-override constraint* section with 
the two working patterns. The deeper refactor (decoupling inner-matcher paths 
from container-level mount paths so FINISHED-73 runtime overrides cascade fully 
through both layers) is parked. No follow-on TODO added — the constraint 
matches every other Juneau `@RestGet`-annotated method's behavior (literal, 
non-inherited matcher) and the two  [...]
+
+### Verification (continuation session)
+
+- `./scripts/test.py -t` — full unit-test run **green**. The new 
`BasicStaticFilesResource_SpringbootMetaInf_Test` class is picked up 
automatically by the JUnit 5 discovery. New tests `b01`/`b02` in 
`BasicStaticFilesResource_OpenApiHidden_Test` also pass.
+- `./scripts/test.py -b` — full build **green**; RAT header check passed on 
the new test class + new META-INF fixture file.
+- `./scripts/coverage.py 
juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/staticfile/ 
--run` — mixin coverage holds at 100% line/branch (no source changes to the 
mixin this session).
diff --git a/todo/TODO.md b/todo/TODO.md
index 4dde2c096c..53b35b5f96 100644
--- a/todo/TODO.md
+++ b/todo/TODO.md
@@ -8,7 +8,7 @@ Recommended order for the TODO-67 through TODO-78 family. 
TODO-20 (rest debug re
 2. ~~**TODO-81** — Mixin sub-`RestContext` inheritance. Each 
`@Rest(mixins=...)` class gets its own `RestContext` parent-linked to the host. 
Foundational for TODO-74's `BasicOpenApiResource` (YAML isolation) and for 
TODO-77's per-mixin guard/debug/logger overrides; cross-cuts all mixins in 
TODO-74–78.~~ ✅ done — see `todo/FINISHED-81-mixin-sub-context-inheritance.md`. 
TODO-74 OQ1 (YAML for `/openapi.yaml`) is now unblocked.
 3. **TODO-69** — AuthN guards. Unblocks TODO-77's admin guard chain.
 4. ~~**TODO-74** — API-docs mixin pack (Swagger, Swagger-UI, OpenAPI, Redoc). 
Consumes the mixin sub-context model from FINISHED-81.~~ ✅ done — see 
`todo/FINISHED-74-mixin-api-docs.md`.
-5. **TODO-75** — Static-files mixin (`BasicStaticFilesResource`).
+5. ~~**TODO-75** — Static-files mixin (`BasicStaticFilesResource`).~~ ✅ done — 
see `todo/FINISHED-75-mixin-static-files.md`.
 6. **TODO-76** — Convention-endpoints pack (favicon / SEO / version / 
well-known).
 7. **TODO-77** — Ops/introspection pack (echo / admin / route-index). Uses 
TODO-69.
 8. **TODO-78** — JSP module (`juneau-rest-server-view-jsp`).
@@ -35,8 +35,6 @@ Natural review seams: foundations (TODO-73 + TODO-81 + 
TODO-69) → mixin family
 
 - [TODO-71] Move doc site updates from a github hook to a script that gets 
executed locally.  Change docusaurus search functionality to 
@easyops-cn/docusaurus-search-local. 
 
-- [TODO-75] Static-files mixin (`BasicStaticFilesResource`) — wrap 
`BasicStaticFiles` in a multi-mount mixin with default mounts `/static/*` and 
`/htdocs/*`. See `todo/TODO-75-mixin-static-files.md`.
-
 - [TODO-76] Convention-endpoints mixin pack — `BasicFaviconResource`, 
`BasicSeoResource` (`/robots.txt`, `/sitemap.xml`), `BasicVersionResource` 
(`/version`, `/info`, `/about`), `BasicWellKnownResource` (`/.well-known/*`). 
See `todo/TODO-76-mixin-convention-endpoints.md`.
 
 - [TODO-77] Ops/introspection mixin pack — `BasicEchoResource` (Debug-gated), 
`BasicAdminResource` (guard-chain-gated, depends on TODO-69), 
`BasicRouteIndexResource`. See `todo/TODO-77-mixin-ops-introspection.md`.

Reply via email to