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.view.jsp;
018
019import java.io.*;
020
021import org.apache.juneau.http.*;
022import org.apache.juneau.http.response.*;
023import org.apache.juneau.rest.server.*;
024import org.apache.juneau.rest.server.view.*;
025
026/**
027 * Mixin that wires JSP view-rendering onto any Juneau REST resource.
028 *
029 * <p>
030 * Compose into a host resource via
031 * {@link Rest#mixins() @Rest(mixins=JspMixin.class)}; the host then:
032 *
033 * <ol class='spaced-list'>
034 *    <li>Gains a default mount at {@code /jsp/*} that serves raw {@code .jsp} resources from the
035 *       importer's classpath via
036 *       {@link jakarta.servlet.RequestDispatcher#forward forward(...)}.
037 *    <li>Registers {@link JspViewRenderer} for the mixin's <b>own</b> endpoints (e.g. the
038 *       {@code /jsp/*} route above) via the mixin's own
039 *       {@link Rest#responseProcessors() @Rest(responseProcessors=...)} declaration.
040 * </ol>
041 *
042 * <h5 class='section'>Auto-wiring the renderer into the host's own endpoints:</h5>
043 *
044 * <p>
045 * A bare {@code @Rest(mixins=JspMixin.class)} is enough &mdash; a host whose own {@code @RestOp} method returns
046 * {@link JspView} renders it through the JSP engine with no extra wiring.  That works because {@code JspMixin}
047 * declares {@link Rest#mergeResponseProcessorsIntoHost() @Rest(mergeResponseProcessorsIntoHost=true)} on its own
048 * class, so the framework folds {@code JspMixin}'s {@link JspViewRenderer} into the host's own response-processor
049 * chain automatically.
050 *
051 * <p>
052 * This fold is per-mixin, opt-in, and response-processor-scoped: it happens only because {@code JspMixin}
053 * declares the opt-in (a mixin that does not is still scoped to its own endpoints, the standard
054 * {@link Rest#mixins() @Rest(mixins=...)} rule), and it folds only the renderer &mdash; never {@code guards},
055 * {@code serializers}, or any other list-shaped attribute.
056 *
057 * <p>
058 * Two explicit wirings remain supported &mdash; useful when you want to be explicit, or to fold more than the
059 * renderer:
060 *
061 * <ol class='spaced-list'>
062 *    <li><b>{@link Mixin#mergeIntoHost() @Mixin(mergeIntoHost=true)}</b> via the rich {@link Rest#mixinDefs()
063 *       mixinDefs} form &mdash; folds <b>all</b> of {@code JspMixin}'s list-shaped {@code @Rest} attributes into
064 *       the host's own chain, not just the renderer.
065 *    <li><b>List {@link JspViewRenderer JspViewRenderer.class} explicitly</b> in the host's own
066 *       {@code @Rest(responseProcessors=...)} &mdash; the fully manual equivalent.
067 * </ol>
068 *
069 * <p>
070 * The {@link org.apache.juneau.rest.server.processor.ResponseProcessorList} partition pass repositions
071 * {@link JspViewRenderer} (a
072 * {@link ViewRenderer ViewRenderer}) ahead of
073 * {@code SerializedPojoProcessor} in every case, so the {@link JspView} bean is dispatched to the JSP engine
074 * rather than bean-serialized.
075 *
076 * <h5 class='figure'>Composition example (microservice):</h5>
077 *
078 * <p class='bjava'>
079 *    <ja>@Rest</ja>(path=<js>"/app"</js>, mixins=JspMixin.<jk>class</jk>)
080 *    <jk>public class</jk> AppResource <jk>extends</jk> RestServlet {
081 *
082 *       <ja>@Bean</ja> JspMixin jsp() {
083 *          <jk>return</jk> JspMixin.<jsm>create</jsm>()
084 *             .basePath(<js>"/WEB-INF/views/"</js>)
085 *             .build();
086 *       }
087 *
088 *       <ja>@RestGet</ja>(<js>"/hello/{name}"</js>)
089 *       <jk>public</jk> View hello(<ja>@Path</ja> String <jv>name</jv>) {
090 *          <jk>return</jk> JspView.<jsm>of</jsm>(<js>"hello.jsp"</js>).attr(<js>"name"</js>, <jv>name</jv>);
091 *       }
092 *    }
093 * </p>
094 *
095 * <h5 class='section'>Configurable mount path:</h5>
096 *
097 * <p>
098 * The default mount {@code /jsp/*} can be overridden via the SVL variable
099 * {@code ${juneau.jsp.path:jsp}} &mdash; set via system property
100 * ({@code -Djuneau.jsp.path=views}), environment variable
101 * ({@code JUNEAU_JSP_PATH=views}), or {@code Config} key
102 * ({@code juneau.jsp.path = views}) to change the runtime mount without subclassing.
103 * Resolution happens once at {@link RestContext} construction time (SVL resolution in {@code @RestOp(path)}).
104 *
105 * <p>
106 * Override accepts bare token ({@code jsp}), leading slash ({@code /jsp}), trailing slash
107 * ({@code jsp/}), or wildcard suffix ({@code /jsp/*}) &mdash; all resolve to the same mount.
108 *
109 * <h5 class='section'>Mixin-only deployment:</h5>
110 *
111 * <p>
112 * This resource is designed for composition via {@code @Rest(mixins=...)}. The mount path is
113 * pinned at the op level by {@link RestGet @RestGet(path="/&#123;juneau.jsp.path:jsp&#125;/*")} on
114 * {@link #render}; a class-level {@code @Rest(paths=...)} declaration would be silently
115 * ignored under the mixin pattern (see {@link Rest#paths() @Rest(paths)} Javadoc).
116 *
117 * <h5 class='section'>Engine-agnostic packaging:</h5>
118 *
119 * <p>
120 * The {@code juneau-rest-server-view-jsp} module ships <b>only</b> the JSP API + JSTL impl in
121 * {@code provided} scope. <b>No JSP engine</b> is bundled with the bridge module. Consumers add
122 * the engine matching their container:
123 *
124 * <ul class='spaced-list'>
125 *    <li><b>Jetty 12 EE11</b> (microservice-jetty, Spring Boot embedded Jetty):
126 *       {@code org.eclipse.jetty.ee11:jetty-ee11-apache-jsp}.
127 *    <li><b>Embedded Tomcat</b> (Spring Boot default):
128 *       {@code org.apache.tomcat.embed:tomcat-embed-jasper}.
129 *    <li><b>External-WAR</b> deployments: container-supplied; no additional dependency.
130 * </ul>
131 *
132 * <p>
133 * When no engine is on the classpath, the renderer surfaces
134 * {@link JspViewRenderer#NO_ENGINE_DIAGNOSTIC} naming the missing dependency.
135 *
136 * <h5 class='section'>Spring Boot fat-jar caveat:</h5>
137 *
138 * <p>
139 * Spring Boot's repackaged fat jar does not place {@code .jsp} files where the embedded JSP
140 * engine looks by default. Place JSP resources under
141 * {@code src/main/resources/META-INF/resources/WEB-INF/views/...} (the {@code META-INF/resources/}
142 * prefix is the Servlet 3.0 convention Spring Boot honors for embedded servlets). Both
143 * {@code mvn spring-boot:run} and the deployed jar pick the files up this way.
144 *
145 * <h5 class='section'>Multiple base paths:</h5>
146 *
147 * <p>
148 * A host with {@code /views/} JSPs and {@code /admin/views/} JSPs registers two
149 * {@code JspMixin} beans, each mounted via a subclass declaring its own op-level
150 * {@code @RestGet(path=...)} override and its own {@code basePath}. See the
151 * {@code JspMixin_MultiBasePath_Test} test in {@code juneau-integration-tests} for the canonical
152 * pattern.
153 *
154 * <h5 class='section'>OpenAPI surface:</h5>
155 *
156 * <p>
157 * The greedy {@code /*} handler is not API-meaningful and is excluded from generated
158 * Swagger / OpenAPI specs via {@link OpSwagger#ignore() @OpSwagger(ignore=true)}.
159 *
160 * <h5 class='section'>See Also:</h5><ul>
161 *    <li class='jc'>{@link JspView}
162 *    <li class='jc'>{@link JspViewRenderer}
163 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/JspViewSupport">JSP View Support</a>
164 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/RestServerComposition">REST Server — Mixins and Multi-Mount Paths</a>
165 * </ul>
166 *
167 * @since 10.0.0
168 */
169// @formatter:off
170@Rest(
171   responseProcessors={JspViewRenderer.class},
172   mergeResponseProcessorsIntoHost=true
173)
174public class JspMixin {
175
176   /** Default base path applied when no {@link Builder#basePath(String)} call has been made. */
177   public static final String DEFAULT_BASE_PATH = JspDispatcher.DEFAULT_BASE_PATH;
178
179   /**
180    * Default cache-templates setting &mdash; {@code true}. Reported for
181    * {@link ViewMixinBuilder} conformance only; see {@link Builder#cacheTemplates(boolean)}.
182    */
183   public static final boolean DEFAULT_CACHE_TEMPLATES = true;
184
185   private final JspDispatcher worker;
186   private final boolean cacheTemplates;
187
188   /**
189    * Creates a new builder.
190    *
191    * @return A new builder.
192    */
193   public static Builder create() {
194      return new Builder();
195   }
196
197   /**
198    * No-arg constructor &mdash; uses {@link #DEFAULT_BASE_PATH} as the base path.
199    *
200    * <p>
201    * The mixin walk falls back to this constructor when the {@link org.apache.juneau.commons.inject.BeanStore
202    * BeanStore} does not have a registered {@code JspMixin} bean.
203    */
204   public JspMixin() {
205      this(create());
206   }
207
208   /**
209    * Builder constructor.
210    *
211    * @param builder The builder. Must not be {@code null}.
212    */
213   protected JspMixin(Builder builder) {
214      worker = builder.worker.build();
215      cacheTemplates = builder.cacheTemplates;
216   }
217
218   /**
219    * Returns the base path under which {@code .jsp} resources are resolved.
220    *
221    * <p>
222    * Read by {@link JspViewRenderer} when dispatching a {@link JspView}-typed return value, and
223    * by the {@link #render render(...)} handler when forwarding raw-{@code .jsp} requests under
224    * {@code /jsp/*}.
225    *
226    * @return The base path. Never {@code null}.
227    */
228   public String getBasePath() {
229      return worker.getBasePath();
230   }
231
232   /**
233    * Returns the configured cache-templates flag.
234    *
235    * <p>
236    * Reported for {@link ViewMixinBuilder} conformance only &mdash; JSP recompilation is
237    * delegated entirely to the servlet container (see {@link Builder#cacheTemplates(boolean)}
238    * for the full caveat).
239    *
240    * @return The cache flag. Defaults to {@link #DEFAULT_CACHE_TEMPLATES}.
241    */
242   public boolean isCacheTemplates() {
243      return cacheTemplates;
244   }
245
246   /**
247    * [GET /jsp/*] &mdash; forward the request to the JSP engine.
248    *
249    * <p>
250    * The {@code @Path("/*") String path} captures the multi-segment trailing remainder (e.g. a
251    * request for {@code /jsp/admin/dashboard.jsp} matches the mount with
252    * {@code path = "admin/dashboard.jsp"}). Delegates to the shared {@link JspDispatcher} worker,
253    * which dispatches via {@code ServletContext.getRequestDispatcher(basePath + path).forward(...)}
254    * so the container's JSP engine renders the template.
255    *
256    * <p>
257    * Missing JSP resources surface as a 404 from the underlying container; missing JSP engine
258    * surfaces as {@link JspViewRenderer#NO_ENGINE_DIAGNOSTIC}.
259    *
260    * @param path The trailing path segment after {@code /jsp/} (the JSP file name relative to
261    *    the configured {@link #getBasePath() base path}).
262    * @param req The current REST request.
263    * @param res The current REST response.
264    * @throws IOException If the underlying servlet writer fails.
265    * @throws NotFound If the JSP resource cannot be resolved.
266    */
267   @RestGet(
268      path="/#{pathToken(${juneau.jsp.path:jsp})}/*",
269      summary="JSP view",
270      description="Forward a request to the JSP engine for a raw .jsp resource under the configured base path.",
271      swagger=@OpSwagger(ignore=true)
272   )
273   public void render(@Path("/*") String path, RestRequest req, RestResponse res) throws IOException, NotFound {
274      worker.render(path, req, res);
275   }
276
277   /**
278    * Builder for {@link JspMixin}.
279    *
280    * <p>
281    * Mirrors {@link JspDispatcher.Builder}'s configuration methods on its own surface and forwards
282    * each call into a held {@link JspDispatcher.Builder} (worker-bean composition).
283    * Implements {@link ViewMixinBuilder} so {@link #basePath(String)} is guaranteed to transfer
284    * to the sibling Thymeleaf / Mustache / FreeMarker bridge builders; see
285    * {@link #cacheTemplates(boolean)} for this bridge's no-op conformance caveat.
286    */
287   public static class Builder implements ViewMixinBuilder<Builder> {
288
289      private final JspDispatcher.Builder worker = JspDispatcher.create();
290      private boolean cacheTemplates = DEFAULT_CACHE_TEMPLATES;
291
292      /** Constructor &mdash; package access for {@link JspMixin#create()}. */
293      protected Builder() {}
294
295      /**
296       * Sets the classpath / webapp base path under which {@code .jsp} resources are resolved.
297       *
298       * <p>
299       * Defaults to {@link JspMixin#DEFAULT_BASE_PATH "/"}. A typical configured value
300       * is {@code "/WEB-INF/views/"} &mdash; the Servlet-spec convention for hiding template
301       * files from direct HTTP access.
302       *
303       * @param value The base path. {@code null} or blank values reset to the default
304       *    {@link JspMixin#DEFAULT_BASE_PATH}.
305       * @return This object.
306       */
307      @Override /* ViewMixinBuilder */
308      public Builder basePath(String value) {
309         worker.basePath(value);
310         return this;
311      }
312
313      /**
314       * Accepted for {@link ViewMixinBuilder} conformance; has <b>no effect</b> on the JSP
315       * bridge.
316       *
317       * <p>
318       * JSP recompilation is delegated entirely to the servlet container via
319       * {@code RequestDispatcher.forward(...)} &mdash; there is no bridge-owned template
320       * cache to disable. Containers manage their own JSP recompilation policy (e.g. Tomcat's
321       * Jasper {@code development} / {@code modificationTestInterval} servlet-init-params,
322       * or Jetty's equivalent) outside this bridge's control. The value is stored and
323       * reported back by {@link #isCacheTemplates()} / {@link JspMixin#isCacheTemplates()}
324       * purely so callers coding against the shared {@link ViewMixinBuilder} contract get a
325       * consistent round-trip, not because it changes rendering behavior.
326       *
327       * @param value The cache flag (recorded only; see above).
328       * @return This object.
329       */
330      @Override /* ViewMixinBuilder */
331      public Builder cacheTemplates(boolean value) {
332         cacheTemplates = value;
333         return this;
334      }
335
336      /**
337       * Reads the current base path setting (test/inspection helper).
338       *
339       * @return The base path. Never {@code null}.
340       */
341      public String getBasePath() {
342         return worker.getBasePath();
343      }
344
345      /**
346       * Reads the current cache-templates setting (test/inspection helper).
347       *
348       * <p>
349       * See {@link #cacheTemplates(boolean)} &mdash; this bridge has no template cache to
350       * report on; the value is whatever was last recorded.
351       *
352       * @return The cache flag.
353       */
354      public boolean isCacheTemplates() {
355         return cacheTemplates;
356      }
357
358      /**
359       * Builds the {@link JspMixin}.
360       *
361       * @return A new {@link JspMixin} instance.
362       */
363      public JspMixin build() {
364         return new JspMixin(this);
365      }
366   }
367}