001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.juneau.rest.server.staticfile; 018 019import java.util.*; 020 021import org.apache.juneau.http.*; 022import org.apache.juneau.http.response.*; 023import org.apache.juneau.rest.server.*; 024 025/** 026 * Mixin that serves static files from the active {@link StaticFiles} implementation under 027 * {@code /static/*} (configurable). 028 * 029 * <p> 030 * Wraps the existing {@link BasicStaticFiles} plumbing (a {@link StaticFiles} implementation, not a 031 * servlet) in a servlet-level mixin so any Juneau resource can opt into static-file serving via 032 * {@link Rest#mixins() @Rest(mixins=StaticFilesMixin.class)}. The mixin reads the active 033 * {@link StaticFiles} from the bean store at request time — importer's 034 * {@code @Bean StaticFiles} declarations + classpath defaults are picked up automatically. 035 * 036 * <h5 class='section'>Configurable mount path:</h5> 037 * 038 * <p> 039 * The default mount {@code /static/*} can be overridden via the SVL variable 040 * {@code ${juneau.staticfiles.path:static}} — set via system property 041 * ({@code -Djuneau.staticfiles.path=assets}), environment variable 042 * ({@code JUNEAU_STATICFILES_PATH=assets}), or {@code Config} key 043 * ({@code juneau.staticfiles.path = assets}) to change the runtime mount without subclassing. 044 * Resolution happens once at {@link RestContext} construction time (SVL resolution in {@code @RestOp(path)}). 045 * 046 * <p> 047 * Override accepts bare token ({@code static}), leading slash ({@code /static}), trailing slash 048 * ({@code static/}), or wildcard suffix ({@code /static/*}) — all resolve to the same mount. 049 * 050 * <p> 051 * <b>Migration note (10.0.0):</b> Earlier development snapshots of this mixin mounted at both 052 * {@code /static/*} <i>and</i> {@code /htdocs/*} as historical convenience aliases — that 053 * dual default has been collapsed to a single SVL-configurable mount as part of the 054 * "single path per op" principle. Deployers who relied on the {@code /htdocs/*} 055 * alias must now either set {@code -Djuneau.staticfiles.path=htdocs} or compose a second 056 * resource mounted with the {@code htdocs} override. The default {@link StaticFiles} classpath 057 * search root still walks both {@code static/} and {@code htdocs/} directories — only the 058 * URL-side mount is now single. 059 * 060 * <h5 class='section'>Mixin-only deployment:</h5> 061 * 062 * <p> 063 * This resource is designed for composition via {@code @Rest(mixins=...)}. The mount path is 064 * pinned at the op level by 065 * {@link RestGet @RestGet(path="/{juneau.staticfiles.path:static}/*")} on {@link #getStaticFile} 066 * (and the matching {@link RestOp} for {@code HEAD}); a class-level {@code @Rest(paths=...)} 067 * declaration would be silently ignored under the mixin pattern (see 068 * {@link Rest#paths() @Rest(paths)} Javadoc). 069 * 070 * <h5 class='figure'>Composition example:</h5> 071 * 072 * <p class='bjava'> 073 * <jc>// Vanilla resource gains static-file serving at /static/*.</jc> 074 * <ja>@Rest</ja>(path=<js>"/api"</js>, mixins=StaticFilesMixin.<jk>class</jk>) 075 * <jk>public class</jk> ApiResource <jk>extends</jk> RestServlet { 076 * <ja>@RestGet</ja>(<js>"/items"</js>) <jk>public</jk> List<Item> items() { ... } 077 * } 078 * </p> 079 * 080 * <h5 class='section'>Behavior:</h5> 081 * 082 * <ul class='spaced-list'> 083 * <li>GET hits the active {@link StaticFiles} bean (resolved via 084 * {@link RestRequest#getStaticFiles()}); missing paths surface as 085 * {@link NotFound} thrown from the handler. 086 * <li>{@code HEAD} requests are accepted via the standard servlet 087 * {@code HEAD}-via-{@code GET} contract: the response carries identical headers to the 088 * corresponding {@code GET} (including {@code Cache-Control} and {@code Content-Type}) with 089 * an empty body. 090 * <li>{@code Cache-Control} headers come from the {@link BasicStaticFiles} default 091 * ({@code max-age=86400, public}); importers can override via 092 * {@code BasicStaticFiles.create(beanStore).headers(...)} when registering their own 093 * {@code @Bean StaticFiles}. 094 * <li>Default classpath base searches both {@code static/} and {@code htdocs/} directories on 095 * the importer's classpath (recursive walk via {@link BasicStaticFiles}'s built-in 096 * {@link ResourceSupplier} hook). 097 * </ul> 098 * 099 * <h5 class='section'>Path matching:</h5> 100 * 101 * <p> 102 * The handler declares {@code @RestGet(path="/${juneau.staticfiles.path:static}/*")} so a single 103 * Java method binds to the configured prefix (default {@code /static/*}). The trailing 104 * {@code /*} captures the multi-segment remainder via {@code @Path("/*") String path}. Juneau's 105 * {@code UrlPathMatcher} does not support the Spring/JAX-RS {@code {var:regex}} syntax — 106 * each {@code {var}} matches a single segment only, and multi-segment matching is only available 107 * via the trailing-{@code *} pattern shown here (same idiom as the legacy 108 * {@code BasicRestServlet.getHtdoc(...)} accessor). 109 * </p> 110 * 111 * <h5 class='section'>OpenAPI surface:</h5> 112 * 113 * <p> 114 * The greedy {@code /*} handler is not API-meaningful and is excluded from generated Swagger / 115 * OpenAPI specs via {@link OpSwagger#ignore() @OpSwagger(ignore=true)}. 116 * </p> 117 * 118 * <h5 class='section'>See Also:</h5><ul> 119 * <li class='jc'>{@link BasicStaticFiles} 120 * <li class='jc'>{@link StaticFiles} 121 * <li class='jc'>{@link org.apache.juneau.rest.server.ops.HtdocMixin} — legacy fixed {@code /htdocs/*} mount; 122 * shares this mixin's {@link #resolveStaticFile} resolution logic 123 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/StaticFiles">Static files</a> 124 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/RestServerComposition">REST Server — Mixins and Multi-Mount Paths</a> 125 * </ul> 126 * 127 * @since 10.0.0 128 */ 129// @formatter:off 130@Rest 131public class StaticFilesMixin { 132 133 /** 134 * [GET /static/*] — serve a static file from the active {@link StaticFiles} bean. 135 * 136 * <p> 137 * The {@code @Path("/*") String path} captures the multi-segment trailing remainder. The 138 * {@code Locale} parameter is auto-injected from the request and forwarded to 139 * {@link StaticFiles#resolve(String,Locale) StaticFiles.resolve(...)} so localized lookups 140 * (e.g. {@code styles_fr.css}) work out of the box. 141 * 142 * @param req The current REST request — supplies {@link RestRequest#getStaticFiles()}. 143 * @param path The trailing remainder after the mount prefix (the file path within the 144 * {@code static/} or {@code htdocs/} classpath / filesystem search roots). 145 * @param locale The request locale (used for localized resource lookups). 146 * @return The matching {@link HttpResource} (with content type + cache headers). 147 * @throws NotFound If no resource matches the requested path. 148 */ 149 @RestGet( 150 path="/#{pathToken(${juneau.staticfiles.path:static})}/*", 151 summary="Static files", 152 description="Static file retrieval.", 153 swagger=@OpSwagger(ignore=true) 154 ) 155 public HttpResource getStaticFile(RestRequest req, @Path("/*") String path, Locale locale) { 156 return resolveStaticFile(req, path, locale); 157 } 158 159 /** 160 * [HEAD /static/*] — return GET headers for a static file without the body. 161 * 162 * <p> 163 * Per RFC 7231 §4.3.2, {@code HEAD} mirrors the equivalent {@code GET}'s headers (Content-Type, 164 * Content-Length, Cache-Control) but emits an empty response body. The shared 165 * {@link org.apache.juneau.rest.server.processor.HttpResourceProcessor} performs the body suppression 166 * at the response-processor layer based on {@link RestRequest#getMethod()}, so this handler can 167 * delegate to {@link #getStaticFile} verbatim. 168 * 169 * @param req The current REST request — supplies {@link RestRequest#getStaticFiles()}. 170 * @param path The trailing remainder after the mount prefix. 171 * @param locale The request locale (used for localized resource lookups). 172 * @return The matching {@link HttpResource} (with headers; body suppressed by the processor). 173 * @throws NotFound If no resource matches the requested path. 174 */ 175 @RestOp( 176 method="HEAD", 177 path="/#{pathToken(${juneau.staticfiles.path:static})}/*", 178 summary="Static files (HEAD)", 179 description="Static file metadata retrieval.", 180 swagger=@OpSwagger(ignore=true) 181 ) 182 public HttpResource headStaticFile(RestRequest req, @Path("/*") String path, Locale locale) { 183 return getStaticFile(req, path, locale); 184 } 185 186 /** 187 * Shared static-file resolution chokepoint — resolves the requested path against the request's active 188 * {@link StaticFiles} bean. 189 * 190 * <p> 191 * Used by both this mixin's {@link #getStaticFile} / {@link #headStaticFile} handlers and by 192 * {@link org.apache.juneau.rest.server.ops.HtdocMixin}'s legacy {@code /htdocs/*} endpoint, so there is a 193 * single implementation of the resolution logic even though the two mixins expose different mounts. 194 * 195 * @param req The current REST request — supplies {@link RestRequest#getStaticFiles()}. 196 * @param path The path to resolve (the trailing remainder after whichever mount prefix the caller uses). 197 * @param locale The request locale (used for localized resource lookups). 198 * @return The matching {@link HttpResource} (with content type + cache headers). 199 * @throws NotFound If no resource matches the requested path. 200 */ 201 public static HttpResource resolveStaticFile(RestRequest req, String path, Locale locale) throws NotFound { 202 return req.getStaticFiles().resolve(path, locale).orElseThrow(NotFound::new); 203 } 204}