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 — 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 — never {@code guards}, 055 * {@code serializers}, or any other list-shaped attribute. 056 * 057 * <p> 058 * Two explicit wirings remain supported — 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 — 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=...)} — 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}} — 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/*}) — 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="/{juneau.jsp.path:jsp}/*")} 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 — {@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 — 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 — 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/*] — 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 — 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/"} — 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(...)} — 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)} — 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}