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
commit bd62c1e3a8da33afee409a241a8496bb83f1881f Author: James Bognar <[email protected]> AuthorDate: Tue Aug 18 13:37:55 2026 -0400 FINISHED-411/412/413: Resolve ConsoleChromeMixin's chrome.css/logo/page-bg endpoints under both mount styles ConsoleChromeMixin's @RestGet paths are absolute-looking literals baked into the jar. Juneau resolves an operation's path against getContextPath() + getServletPath(), so the same mixin resolves differently depending on whether the host resource is mounted "composed" (hanging off an app's existing /rest/* mount) or "standalone" (its own /juneau-console/* servlet mapping) -- one of the two mount styles was always one path segment short. Fixed by declaring both a PREFIXED and UNPREFIXED @RestGet path per endpoint and picking the live one from servletPath at request time; the new assetUrl() resolver makes the CSS's own emitted url(...) references resolve the same way. cachedBody becomes cachedBodies, keyed by mount, since the two mount styles now serve genuinely different bytes from one mixin instance. Also: RestOpContext.matchPattern changes from "keep looping after the first match" to an early return on the same dispatch path this fix touches. Behaviorally a no-op today (it never overwrote the first match) -- included here as a small cleanup on the code this commit is already re-reading, not as a claimed behavior fix. Partial-file note: the post-backup addition to ConsoleChromeMixin_Test is one contiguous block that interleaves this fix's tests with an unrelated theme fix's tests. This commit contains only test methods l01-l06 and m01-m12 from that block (18 tests: standalone-vs-composed mount resolution, URL-prefix correctness, cache-buster/cached-body mount independence), plus the four private helpers they need. A sibling commit adds n01-o02 to the same file for a red-tag/token-order fix that happens to append to the same block. Reading either commit in isolation, the file will already contain test methods the diff does not touch -- that is expected, not a sign the split is wrong. --- .../rest/server/console/ConsoleChromeMixin.java | 103 ++++++++-- .../server/console/ConsoleChromeMixin_Test.java | 208 +++++++++++++++++++++ .../apache/juneau/rest/server/RestOpContext.java | 11 +- 3 files changed, 300 insertions(+), 22 deletions(-) diff --git a/juneau-rest/juneau-rest-server-console-ui/src/main/java/org/apache/juneau/rest/server/console/ConsoleChromeMixin.java b/juneau-rest/juneau-rest-server-console-ui/src/main/java/org/apache/juneau/rest/server/console/ConsoleChromeMixin.java index 423efb4a25..70ac79517c 100644 --- a/juneau-rest/juneau-rest-server-console-ui/src/main/java/org/apache/juneau/rest/server/console/ConsoleChromeMixin.java +++ b/juneau-rest/juneau-rest-server-console-ui/src/main/java/org/apache/juneau/rest/server/console/ConsoleChromeMixin.java @@ -45,6 +45,30 @@ import org.apache.juneau.rest.server.*; * structural {@code chrome.css} (shipped in this module's classpath) with the active theme's tokens appended as a * {@code :root{}} block. * + * <h5 class='section'>Mount styles:</h5> + * <p> + * Both of the following arrangements serve the assets at the {@link #CHROME_CSS_PATH} / + * {@link #LOGO_ASSET_PATH} / {@link #PAGE_BG_ASSET_PATH} URLs with no path juggling by the host resource: + * <ul> + * <li><b>Composed</b> — the host resource is mounted wherever the application already mounts it (e.g. + * {@code /rest/*}) and the assets hang off that mount, at + * <code><host-mount>/juneau-console/chrome.css</code>. + * <li><b>Standalone</b> — the host resource is registered with the servlet container at url-pattern + * {@code /juneau-console/*} so the assets sit at a fixed site-root URL, independent of which page or tab + * rendered the referencing {@code <link>}. + * </ul> + * <p> + * The two arrangements need different operation paths, because a container mount at {@code /juneau-console/*} + * reports {@code servletPath="/juneau-console"} and Juneau resolves an operation's path against the request URI + * with {@code contextPath + servletPath} already removed — leaving only {@code /chrome.css} to match. + * Each operation below therefore declares <i>both</i> its prefixed path and the same path minus the + * {@code /juneau-console} prefix, so whichever one the arrangement leaves to be matched resolves. + * <p> + * The same duality applies to the logo/page-background {@code url()}s written into the served stylesheet: they are + * resolved per-request against the container's context path and the host's mount, picking the path form the active + * arrangement leaves unconsumed, so the browser fetches them from the mount that served the stylesheet rather than + * from the site root. + * * <h5 class='section'>Theme precedence:</h5> * <p> * <code>{@link Builder#theme(Theme) mixin.theme(...)}</code> wins over a {@link ThemeSettings} {@code BeanStore} @@ -73,6 +97,18 @@ public class ConsoleChromeMixin { /** The URL path at which the configured page-background asset is served (relative to the host mount). */ public static final String PAGE_BG_ASSET_PATH = "/juneau-console/assets/page-bg"; + /** {@link #CHROME_CSS_PATH} minus the {@code /juneau-console} prefix - see the class javadoc's mount-styles section. */ + static final String CHROME_CSS_PATH_UNPREFIXED = "/chrome.css"; + + /** {@link #LOGO_ASSET_PATH} minus the {@code /juneau-console} prefix - see the class javadoc's mount-styles section. */ + static final String LOGO_ASSET_PATH_UNPREFIXED = "/assets/logo"; + + /** {@link #PAGE_BG_ASSET_PATH} minus the {@code /juneau-console} prefix - see the class javadoc's mount-styles section. */ + static final String PAGE_BG_ASSET_PATH_UNPREFIXED = "/assets/page-bg"; + + /** The prefix the {@code *_UNPREFIXED} constants drop - see the class javadoc's mount-styles section. */ + static final String MOUNT_PREFIX = "/juneau-console"; + /** Classpath location of the shipped structural stylesheet. */ static final String CHROME_CSS_RESOURCE = "/org/apache/juneau/console/chrome.css"; @@ -96,8 +132,8 @@ public class ConsoleChromeMixin { private final String logoResource; private final String pageBackgroundResource; - /** Per-mixin-instance cache of the fully-assembled (static + theme blocks) response body. Never shared across mounts. */ - private volatile byte[] cachedBody; + /** Per-mixin-instance cache of the fully-assembled (static + theme blocks) response body, keyed by mount (see {@link #mountKey}). */ + private final Map<String,byte[]> cachedBodies = new ConcurrentHashMap<>(); /** Test-only diagnostic: counts every call to {@link #buildBody(RestRequest)} (i.e. every cache miss / every call when caching is disabled). */ private final AtomicInteger buildCount = new AtomicInteger(); @@ -140,7 +176,7 @@ public class ConsoleChromeMixin { * — the resource is shipped in the same jar as this class). */ @RestGet( - path=CHROME_CSS_PATH, + path={CHROME_CSS_PATH, CHROME_CSS_PATH_UNPREFIXED}, summary="Admin-console chrome stylesheet", description="Structural CSS for the admin-console chrome, with the active theme's tokens appended.", swagger=@OpSwagger(ignore=true) @@ -157,7 +193,7 @@ public class ConsoleChromeMixin { * @throws IOException If the configured resource could not be read. */ @RestGet( - path=LOGO_ASSET_PATH, + path={LOGO_ASSET_PATH, LOGO_ASSET_PATH_UNPREFIXED}, summary="Configured logo image asset", swagger=@OpSwagger(ignore=true) ) @@ -174,7 +210,7 @@ public class ConsoleChromeMixin { * @throws IOException If the configured resource could not be read. */ @RestGet( - path=PAGE_BG_ASSET_PATH, + path={PAGE_BG_ASSET_PATH, PAGE_BG_ASSET_PATH_UNPREFIXED}, summary="Configured page-background image asset", swagger=@OpSwagger(ignore=true) ) @@ -201,19 +237,31 @@ public class ConsoleChromeMixin { ); } - /** Returns the fully-assembled response body, computing (and instance-caching) it on first call. */ + /** + * Returns the fully-assembled response body for the mount the request arrived under, computing (and caching) it + * on first call for that mount. + * + * <p> + * The cache is keyed by mount rather than held in a single field because the emitted asset URLs are mount-derived + * (see {@link #assetUrl}), and one mixin instance can be reached under more than one container mapping. + */ private byte[] cachedBody(RestRequest req) throws IOException { - var b = cachedBody; - if (b == null) { - synchronized (this) { - b = cachedBody; - if (b == null) { // HTT: the "already set" branch is only reachable under a lock-acquisition race - unhittable single-threaded. - b = buildBody(req); - cachedBody = b; + try { + return cachedBodies.computeIfAbsent(mountKey(req), k -> { + try { + return buildBody(req); + } catch (IOException e) { // HTT: staticCss() is the only throwing call and reads a resource shipped in this jar. + throw new UncheckedIOException(e); } - } + }); + } catch (UncheckedIOException e) { // HTT: see above - the wrapped read cannot fail in a well-formed jar. + throw e.getCause(); } - return b; + } + + /** The request's mount identity: the two request properties every emitted asset URL is derived from. */ + private static String mountKey(RestRequest req) { + return req.getContextPath() + '\n' + req.getServletPath(); } /** @@ -231,16 +279,37 @@ public class ConsoleChromeMixin { if (! active.getName().equals(Theme.OPEN.getName())) sb.append('\n').append(rootBlock(active)); if (pageBackgroundResource != null) - sb.append('\n').append("html, body{background-image:url(\"").append(PAGE_BG_ASSET_PATH) + sb.append('\n').append("html, body{background-image:url(\"").append(assetUrl(req, PAGE_BG_ASSET_PATH, PAGE_BG_ASSET_PATH_UNPREFIXED)) .append("?v=").append(buildVersion()).append('-').append(assetContentHash(pageBackgroundResource)) .append("\"), var(--jc-page-bg);}"); if (logoResource != null) - sb.append('\n').append(".jc-logo{background-image:url(\"").append(LOGO_ASSET_PATH) + sb.append('\n').append(".jc-logo{background-image:url(\"").append(assetUrl(req, LOGO_ASSET_PATH, LOGO_ASSET_PATH_UNPREFIXED)) .append("?v=").append(buildVersion()).append('-').append(assetContentHash(logoResource)) .append("\");}"); return sb.toString().getBytes(StandardCharsets.UTF_8); } + /** + * Resolves one of the mixin's asset endpoints to a URL a browser can fetch from wherever the referencing + * {@code chrome.css} was served: the container's context path and the host's mount, plus whichever of the + * endpoint's two declared paths the active mount leaves to be matched. + * + * <p> + * Under the <b>standalone</b> mount style the container has already consumed the {@code /juneau-console} segment + * into {@code servletPath}, so resolving the prefixed path against it would emit that segment twice; under the + * <b>composed</b> style {@code servletPath} is the host's own mount and the prefixed segment is exactly what is + * missing. {@code servletPath} ending in {@code /juneau-console} therefore identifies the standalone case. + * + * <p> + * A composing host whose own mount happens to end in {@code /juneau-console} reads as standalone here, which + * emits its unprefixed URL rather than its prefixed one - still a live URL, since every mount serves both forms + * (see the class javadoc's mount-styles section). + */ + private static String assetUrl(RestRequest req, String prefixedPath, String unprefixedPath) { + var standalone = req.getServletPath().endsWith(MOUNT_PREFIX); + return req.getUriResolver().resolve("servlet:" + (standalone ? unprefixedPath : prefixedPath)); + } + /** * Resolves the framework build version for asset cache-busting, falling back to {@code "dev"} when unset * (e.g. running from IDE/test classpath rather than a packaged jar). diff --git a/juneau-rest/juneau-rest-server-console-ui/src/test/java/org/apache/juneau/rest/server/console/ConsoleChromeMixin_Test.java b/juneau-rest/juneau-rest-server-console-ui/src/test/java/org/apache/juneau/rest/server/console/ConsoleChromeMixin_Test.java index d1610f82f6..c91e5e0aca 100644 --- a/juneau-rest/juneau-rest-server-console-ui/src/test/java/org/apache/juneau/rest/server/console/ConsoleChromeMixin_Test.java +++ b/juneau-rest/juneau-rest-server-console-ui/src/test/java/org/apache/juneau/rest/server/console/ConsoleChromeMixin_Test.java @@ -22,6 +22,7 @@ import java.io.*; import java.nio.charset.*; import java.util.*; import java.util.regex.*; +import java.util.zip.*; import org.apache.juneau.*; import org.apache.juneau.commons.inject.*; @@ -543,10 +544,217 @@ class ConsoleChromeMixin_Test extends TestBase { } } + //----------------------------------------------------------------------------------------------------------------- + // l) Mount-style independence: standalone container mount at /juneau-console/* vs. composed onto a host + // mounted elsewhere. + //----------------------------------------------------------------------------------------------------------------- + + /** + * A container mount at url-pattern {@code /juneau-console/*} makes the container report + * {@code servletPath="/juneau-console"}, so the path Juneau matches against is only the remainder + * ({@code /chrome.css}). The mixin's endpoints must resolve at the stable + * {@code /juneau-console/chrome.css} URL in that arrangement without the host having to rewrite + * {@code getServletPath()}. + */ + private static MockRestClient standaloneMounted(Class<?> host) { + return MockRestClient.createLax(host).servletPath("/juneau-console").build(); + } + + @Test void l01_standaloneMount_chromeCssResolvesAtStableUrl() throws Exception { + standaloneMounted(AssetsHost.class).get("/chrome.css").run() + .assertStatus(200) + .assertHeader("Content-Type").isContains("text/css"); + } + + @Test void l02_standaloneMount_logoAssetResolvesAtUrlEmittedInChromeCss() throws Exception { + standaloneMounted(AssetsHost.class).get("/assets/logo").run() + .assertStatus(200) + .assertHeader("Content-Type").isContains("image/svg+xml"); + } + + @Test void l03_standaloneMount_pageBgAssetResolvesAtUrlEmittedInChromeCss() throws Exception { + standaloneMounted(AssetsHost.class).get("/assets/page-bg").run() + .assertStatus(200) + .assertHeader("Content-Type").isContains("image/png"); + } + + @Test void l04_standaloneMount_emittedAssetUrlsAreServableUnderThatMount() throws Exception { + // The chrome.css body references the assets by their absolute /juneau-console/... URL. Under a + // /juneau-console/* container mount at the site root those URLs must hit the very endpoints above. + var body = standaloneMounted(AssetsHost.class).get("/chrome.css").run().assertStatus(200).getContent().asString(); + assertTrue(body.contains("url(\"" + ConsoleChromeMixin.LOGO_ASSET_PATH + "?v="), () -> "missing logo url(), body:\n" + body); + assertTrue(body.contains("url(\"" + ConsoleChromeMixin.PAGE_BG_ASSET_PATH + "?v="), () -> "missing page-bg url(), body:\n" + body); + } + + @Test void l05_composedMount_prefixedPathsStillResolveUnderANonRootHostMount() throws Exception { + // Back-compat guard for the documented composition style: a host mounted at /rest/* keeps serving the + // mixin's endpoints at <host-mount>/juneau-console/... + var c = MockRestClient.createLax(AssetsHost.class).servletPath("/rest").build(); + c.get(ConsoleChromeMixin.CHROME_CSS_PATH).run().assertStatus(200); + c.get(ConsoleChromeMixin.LOGO_ASSET_PATH).run().assertStatus(200); + c.get(ConsoleChromeMixin.PAGE_BG_ASSET_PATH).run().assertStatus(200); + } + + @Test void l06_publicAssetPathConstants_arePinned() { + // These constants are the URLs consumers build <link>/<img> references from - changing a value is a + // silent break for every deployed consumer, so pin them. + assertEquals("/juneau-console/chrome.css", ConsoleChromeMixin.CHROME_CSS_PATH); + assertEquals("/juneau-console/assets/logo", ConsoleChromeMixin.LOGO_ASSET_PATH); + assertEquals("/juneau-console/assets/page-bg", ConsoleChromeMixin.PAGE_BG_ASSET_PATH); + } + + //----------------------------------------------------------------------------------------------------------------- + // m) Mount-style-aware asset URL generation: the logo/page-bg url()s written into the served chrome.css must be + // fetchable by the browser under both mount styles and at any container context path. + //----------------------------------------------------------------------------------------------------------------- + + @Test void m01_standaloneMount_emittedLogoUrl_isByteIdenticalToThePreFixLiteral() throws Exception { + var body = standaloneMounted(AssetsHost.class).get("/chrome.css").run().assertStatus(200).getContent().asString(); + assertEquals("/juneau-console/assets/logo" + expectedCacheBuster(VALID_LOGO), emittedUrl(body, "/assets/logo")); + } + + @Test void m02_standaloneMount_emittedPageBgUrl_isByteIdenticalToThePreFixLiteral() throws Exception { + var body = standaloneMounted(AssetsHost.class).get("/chrome.css").run().assertStatus(200).getContent().asString(); + assertEquals("/juneau-console/assets/page-bg" + expectedCacheBuster(VALID_PAGE_BG), emittedUrl(body, "/assets/page-bg")); + } + + /** + * The regression this guards is mount-style detection inverted in the standalone direction: resolving the + * <i>prefixed</i> constant against a standalone mount's {@code servletPath} (which already ends in + * {@code /juneau-console}) doubles the segment up to {@code /juneau-console/juneau-console/assets/logo}. A + * happy-path "the url contains /assets/logo" assertion passes right through that, and so does an end-to-end + * fetch, because the doubled URL is a live alias under a standalone mount. + */ + @Test void m03_standaloneMount_emittedUrlsAreNotDoublePrefixed() throws Exception { + var body = standaloneMounted(AssetsHost.class).get("/chrome.css").run().assertStatus(200).getContent().asString(); + assertFalse(body.contains("/juneau-console/juneau-console"), () -> "asset url double-prefixed, body:\n" + body); + } + + @Test void m04_composedMount_emittedLogoUrl_carriesTheHostMountSegment() throws Exception { + var body = composedMounted(AssetsHost.class).get(ConsoleChromeMixin.CHROME_CSS_PATH).run().assertStatus(200).getContent().asString(); + assertEquals("/rest/juneau-console/assets/logo" + expectedCacheBuster(VALID_LOGO), emittedUrl(body, "/assets/logo")); + } + + @Test void m05_composedMount_emittedPageBgUrl_carriesTheHostMountSegment() throws Exception { + var body = composedMounted(AssetsHost.class).get(ConsoleChromeMixin.CHROME_CSS_PATH).run().assertStatus(200).getContent().asString(); + assertEquals("/rest/juneau-console/assets/page-bg" + expectedCacheBuster(VALID_PAGE_BG), emittedUrl(body, "/assets/page-bg")); + } + + /** + * The mirror-image regression of (m03): detection inverted in the composed direction, resolving the + * <i>unprefixed</i> constant under a composed mount, yields {@code /rest/assets/logo}. That is not caught by an + * end-to-end fetch either — {@code TODO-411}'s dual path registration makes {@code /rest/assets/logo} a + * live alias too — nor by the pre-fix root-absolute literal {@code /juneau-console/assets/logo}, which + * still <i>looks</i> like a plausible logo URL. Both wrong answers are pinned out explicitly here. + */ + @Test void m06_composedMount_emittedUrlsAreNeitherUnprefixedNorSiteRootAbsolute() throws Exception { + var body = composedMounted(AssetsHost.class).get(ConsoleChromeMixin.CHROME_CSS_PATH).run().assertStatus(200).getContent().asString(); + var logoUrl = emittedUrl(body, "/assets/logo"); + assertFalse(logoUrl.startsWith("/rest/assets/"), () -> "unprefixed constant resolved under a composed mount: " + logoUrl); + assertFalse(logoUrl.startsWith("/juneau-console/"), () -> "site-root-absolute literal emitted under a composed mount: " + logoUrl); + assertFalse(body.contains("/rest/juneau-console/juneau-console"), () -> "asset url double-prefixed, body:\n" + body); + } + + @Test void m07_composedMountUnderANonEmptyContextPath_emittedUrlsCarryTheContextPath() throws Exception { + var c = MockRestClient.createLax(AssetsHost.class).contextPath("/app").servletPath("/rest").build(); + var body = c.get(ConsoleChromeMixin.CHROME_CSS_PATH).run().assertStatus(200).getContent().asString(); + assertEquals("/app/rest/juneau-console/assets/logo" + expectedCacheBuster(VALID_LOGO), emittedUrl(body, "/assets/logo")); + assertEquals("/app/rest/juneau-console/assets/page-bg" + expectedCacheBuster(VALID_PAGE_BG), emittedUrl(body, "/assets/page-bg")); + } + + @Test void m08_standaloneMountUnderANonEmptyContextPath_emittedUrlsCarryTheContextPath() throws Exception { + var c = MockRestClient.createLax(AssetsHost.class).contextPath("/app").servletPath("/juneau-console").build(); + var body = c.get("/chrome.css").run().assertStatus(200).getContent().asString(); + assertEquals("/app/juneau-console/assets/logo" + expectedCacheBuster(VALID_LOGO), emittedUrl(body, "/assets/logo")); + assertEquals("/app/juneau-console/assets/page-bg" + expectedCacheBuster(VALID_PAGE_BG), emittedUrl(body, "/assets/page-bg")); + } + + @Test void m09_composedMount_emittedUrlsActuallyResolveAgainstThatMount() throws Exception { + var c = composedMounted(AssetsHost.class); + var body = c.get(ConsoleChromeMixin.CHROME_CSS_PATH).run().assertStatus(200).getContent().asString(); + c.get(belowMount(emittedUrl(body, "/assets/logo"), "/rest")).run().assertStatus(200).assertHeader("Content-Type").isContains("image/svg+xml"); + c.get(belowMount(emittedUrl(body, "/assets/page-bg"), "/rest")).run().assertStatus(200).assertHeader("Content-Type").isContains("image/png"); + } + + @Test void m10_standaloneMount_emittedUrlsActuallyResolveAgainstThatMount() throws Exception { + var c = standaloneMounted(AssetsHost.class); + var body = c.get("/chrome.css").run().assertStatus(200).getContent().asString(); + c.get(belowMount(emittedUrl(body, "/assets/logo"), "/juneau-console")).run().assertStatus(200).assertHeader("Content-Type").isContains("image/svg+xml"); + c.get(belowMount(emittedUrl(body, "/assets/page-bg"), "/juneau-console")).run().assertStatus(200).assertHeader("Content-Type").isContains("image/png"); + } + + @Test void m11_cacheBuster_isMountIndependent() throws Exception { + // The ?v= suffix hashes the configured asset's own bytes and reads the package version - neither has + // anything to do with the mount, so only the URL prefix may differ between the two mount styles. + var standalone = emittedUrl(standaloneMounted(AssetsHost.class).get("/chrome.css").run().getContent().asString(), "/assets/logo"); + var composed = emittedUrl(composedMounted(AssetsHost.class).get(ConsoleChromeMixin.CHROME_CSS_PATH).run().getContent().asString(), "/assets/logo"); + var buster = expectedCacheBuster(VALID_LOGO); + assertTrue(standalone.endsWith(buster), () -> "standalone url lost its cache-buster: " + standalone); + assertTrue(composed.endsWith(buster), () -> "composed url lost its cache-buster: " + composed); + } + + static final ConsoleChromeMixin MOUNT_CACHE_MIXIN = ConsoleChromeMixin.create().logo(VALID_LOGO).build(); + + @Rest(mixins=ConsoleChromeMixin.class) + public static class MountCacheHost extends BasicRestServlet { + private static final long serialVersionUID = 1L; + @Bean public ConsoleChromeMixin console() { return MOUNT_CACHE_MIXIN; } + } + + /** + * {@code cacheAssets(true)} caches the assembled body, and the body now varies by mount - so the cache has to + * key on the mount. A body cache that ignores the mount serves whichever mount style happened to warm it first + * to the other one, which no single-mount test can see. + */ + @Test void m12_cachedBody_isKeyedByMount_notSharedAcrossMountStyles() throws Exception { + var standalone1 = standaloneMounted(MountCacheHost.class).get("/chrome.css").run().assertStatus(200).getContent().asString(); + var composed = composedMounted(MountCacheHost.class).get(ConsoleChromeMixin.CHROME_CSS_PATH).run().assertStatus(200).getContent().asString(); + var standalone2 = standaloneMounted(MountCacheHost.class).get("/chrome.css").run().assertStatus(200).getContent().asString(); + assertEquals("/juneau-console/assets/logo" + expectedCacheBuster(VALID_LOGO), emittedUrl(standalone1, "/assets/logo")); + assertEquals("/rest/juneau-console/assets/logo" + expectedCacheBuster(VALID_LOGO), emittedUrl(composed, "/assets/logo")); + assertEquals(standalone1, standalone2, "the standalone body must come back from cache unchanged"); + assertEquals(2, MOUNT_CACHE_MIXIN.debugBuildCount(), "expected exactly one assembly per distinct mount"); + } + //----------------------------------------------------------------------------------------------------------------- // Test helpers //----------------------------------------------------------------------------------------------------------------- + /** A host composed onto an existing application mount at {@code /rest/*}. */ + private static MockRestClient composedMounted(Class<?> host) { + return MockRestClient.createLax(host).servletPath("/rest").build(); + } + + /** Extracts the single {@code url("...")} value the served CSS emits for the given asset endpoint. */ + private static String emittedUrl(String body, String assetPathSuffix) { + var m = Pattern.compile("url\\(\"([^\"]*" + Pattern.quote(assetPathSuffix) + "\\?v=[^\"]+)\"\\)").matcher(body); + assertTrue(m.find(), () -> "no emitted url() for " + assetPathSuffix + " in body:\n" + body); + return m.group(1); + } + + /** + * Re-expresses a browser-absolute emitted URL as a path below the given mount, asserting along the way that it + * really does sit below it - which is the half of "the browser can fetch this" that a bare status assertion + * against a hand-written path cannot check. + */ + private static String belowMount(String emittedUrl, String mount) { + assertTrue(emittedUrl.startsWith(mount + "/"), () -> "emitted url '" + emittedUrl + "' is not below the mount '" + mount + "'"); + return emittedUrl.substring(mount.length()); + } + + /** The exact {@code ?v=<buildVersion>-<hash8>} suffix the mixin must still append to every emitted asset URL. */ + private static String expectedCacheBuster(String classpathResource) throws IOException { + byte[] bytes; + try (var in = ConsoleChromeMixin_Test.class.getResourceAsStream(classpathResource)) { + assertNotNull(in); + bytes = in.readAllBytes(); + } + var crc = new CRC32(); + crc.update(bytes); + var v = ConsoleChromeMixin.class.getPackage().getImplementationVersion(); + return "?v=" + (v == null ? "dev" : v) + '-' + String.format("%08x", crc.getValue()); + } + private static String bodyOf(MockRestClient client) throws Exception { return client.get(ConsoleChromeMixin.CHROME_CSS_PATH).run().assertStatus(200).getContent().asString(); } diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/RestOpContext.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/RestOpContext.java index bdeb17e076..927535a929 100644 --- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/RestOpContext.java +++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/RestOpContext.java @@ -1893,11 +1893,12 @@ public class RestOpContext extends Context implements Comparable<RestOpContext> } private UrlPathMatch matchPattern(RestSession call) { - UrlPathMatch pm = null; - for (var pp : pathMatchers.get()) - if (pm == null) - pm = pp.match(call.getUrlPath()); - return pm; + for (var pp : pathMatchers.get()) { + var pm = pp.match(call.getUrlPath()); + if (pm != null) + return pm; + } + return null; } /**
