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 &mdash; 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}} &mdash; 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/*}) &mdash; 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 &mdash; 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 &mdash; 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="/&#123;juneau.staticfiles.path:static&#125;/*")} 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&lt;Item&gt; 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 &mdash;
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} &mdash; 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/*] &mdash; 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 &mdash; 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/*] &mdash; 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 &mdash; 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 &mdash; 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 &mdash; 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}