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 static org.apache.juneau.commons.utils.StringUtils.*;
020
021import java.io.*;
022
023import org.apache.juneau.commons.utils.*;
024import org.apache.juneau.http.response.*;
025import org.apache.juneau.rest.server.*;
026import org.apache.juneau.rest.server.processor.*;
027import org.apache.juneau.rest.server.view.*;
028
029/**
030 * {@link ResponseProcessor} that detects {@link JspView}-typed return values and dispatches them
031 * to the underlying JSP engine via
032 * {@link jakarta.servlet.RequestDispatcher#forward(jakarta.servlet.ServletRequest,
033 * jakarta.servlet.ServletResponse) ServletContext.getRequestDispatcher(...).forward(...)}.
034 *
035 * <p>
036 * Auto-registered by {@link JspMixin} via
037 * {@link Rest#responseProcessors() @Rest(responseProcessors=...)}
038 * &mdash; callers who add the mixin don't need to wire up this class explicitly. Callers who want
039 * to handle {@code JspView} returns <i>without</i> adopting the mixin (e.g. mounting their own
040 * raw-JSP path) can add this class to their own
041 * {@link Rest#responseProcessors() responseProcessors} list.
042 *
043 * <h5 class='section'>Behavior:</h5>
044 *
045 * <ol class='spaced-list'>
046 *    <li>Inspect the response content. If the value is not a {@link JspView}, return
047 *       {@link ResponseProcessor#NEXT NEXT} so the rest of the chain runs (the standard POJO
048 *       serializer / plain-text fallback handle non-{@code JspView} returns).
049 *    <li>Read the active {@link JspMixin} from the {@code RestContext} bean store to
050 *       discover the configured base path; fall back to {@code "/"} when none is registered (a
051 *       caller using the renderer standalone without the mixin).
052 *    <li>Copy every entry from {@link JspView#getAttributes()} onto the request as a request
053 *       attribute via {@link jakarta.servlet.ServletRequest#setAttribute(String, Object)
054 *       req.setAttribute(...)} so the JSP / JSTL EL pipeline can resolve them.
055 *    <li>Apply every entry from {@link JspView#getResponseHeaders()} via
056 *       {@link jakarta.servlet.http.HttpServletResponse#setHeader(String, String)
057 *       res.setHeader(...)}.
058 *    <li>Obtain the underlying {@link jakarta.servlet.ServletContext ServletContext},
059 *       {@code getRequestDispatcher(basePath + templateName)}, and call {@code forward(...)}.
060 *    <li>When no JSP engine is on the classpath, the dispatcher returns {@code null} (or forward
061 *       fails with {@code ClassNotFoundException}). Surface a human-readable diagnostic naming the
062 *       missing dependency and linking to the "Choosing a JSP engine" matrix.
063 * </ol>
064 *
065 * <h5 class='section'>Note on {@code forward()}:</h5>
066 *
067 * <p>
068 * {@link jakarta.servlet.RequestDispatcher#forward forward()} resets the response output stream.
069 * If a {@link RestPreCall @RestPreCall} hook has already
070 * written response headers/body before the renderer runs, the forward may fail or produce
071 * malformed output. The renderer runs at response-resolution time so this is rarely hit by
072 * accident; document the constraint for any user relying on {@code RestPreCall}.
073 *
074 * <h5 class='section'>See Also:</h5><ul>
075 *    <li class='jc'>{@link JspMixin}
076 *    <li class='jc'>{@link JspView}
077 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/ResponseProcessors">Response Processors</a>
078 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/JspViewSupport">JSP View Support</a>
079 * </ul>
080 *
081 * @since 10.0.0
082 */
083@SuppressWarnings({
084   "resource" // Closeables here are framework-managed and not owned/closed by this class; not a real leak.
085})
086public class JspViewRenderer implements ViewRenderer {
087
088   /**
089    * Diagnostic message emitted when no JSP engine can be found on the classpath at forward-time.
090    *
091    * <p>
092    * Public so tests and consumer apps can pattern-match against the message text without
093    * depending on internal string literals.
094    */
095   public static final String NO_ENGINE_DIAGNOSTIC =
096      """
097      No JSP engine is available on the classpath. Add one of:
098        - org.eclipse.jetty.ee11:jetty-ee11-apache-jsp  (Jetty 12 EE11)
099        - org.apache.tomcat.embed:tomcat-embed-jasper  (embedded Tomcat / Spring Boot default)
100        - Or rely on the deployment container's bundled engine (Tomcat / JBoss / WildFly / ...)
101      See https://juneau.apache.org/docs/topics/JspViewSupport for the full matrix.""";
102
103   @Override /* Overridden from ResponseProcessor */
104   public int process(RestOpSession opSession) throws IOException, BasicHttpException {
105      var req = opSession.getRequest();
106      var res = opSession.getResponse();
107
108      var content = res.getContent(Object.class);
109      if (! (content instanceof JspView content2))
110         return NEXT;
111
112      // Resolve the base path. Prefer the registered JspMixin bean (which the mixin
113      // instantiates), fall back to "/" when the renderer is used standalone.
114      var basePath = req.getContext().getBeanStore()
115         .getBean(JspMixin.class)
116         .map(JspMixin::getBasePath)
117         .orElse("/");
118
119      // Copy attributes onto the request so the JSP / JSTL EL can resolve them.
120      content2.getAttributes().forEach(req::setAttribute);
121
122      // Apply caller-supplied response headers (e.g. Content-Type, Cache-Control).
123      content2.getResponseHeaders().forEach(res::setHeader);
124
125      // Resolve the dispatch path: basePath + templateName, normalized for a single separator.
126      // A path that escapes basePath (e.g. template names assembled from user input that
127      // included ../ segments) is rejected with InternalServerError — template names are
128      // caller-controlled, so an escape attempt indicates a server-side bug, not a request-
129      // side attack. (User-input flowing into JSP rendering is handled by JspMixin.render
130      // which catches IAE and surfaces it as Forbidden.)
131      String target;
132      try {
133         target = joinPath(basePath, content2.getTemplateName());
134      } catch (IllegalArgumentException ex) {
135         throw new InternalServerError(ex, "JSP template name escapes configured base path: '%s'",
136            content2.getTemplateName());
137      }
138
139      // Find the JSP engine via the servlet context. Both forward() failure modes (missing
140      // engine, dispatcher==null, ClassNotFoundException during forward) surface as a clear
141      // diagnostic naming the missing dependency.
142      try {
143         var ctx = req.getServletContext();
144         var rd = ctx.getRequestDispatcher(target);
145         if (rd == null)
146            throw new InternalServerError("Could not resolve RequestDispatcher for '%s'. %s",
147               target, NO_ENGINE_DIAGNOSTIC);
148         rd.forward(req.getHttpServletRequest(), res.getHttpServletResponse());
149         return FINISHED;
150      } catch (InternalServerError | IOException ex) {
151         // Re-throw as-is so the specific diagnostic constructed above, or a framework-level IOException,
152         // isn't discarded by the generic fallback below.
153         throw ex;
154      } catch (NoClassDefFoundError ex) {
155         throw new InternalServerError(ex, NO_ENGINE_DIAGNOSTIC);
156      } catch (Exception ex) {
157         throw new InternalServerError(ex, "JSP render failed for '%s'", target);
158      }
159   }
160
161   /**
162    * Joins a base path and a template name with a single {@code "/"} separator, rejecting any
163    * resolved target that escapes {@code basePath} via {@code ..} segments.
164    *
165    * <p>
166    * Delegates to {@link FileUtils#resolveVirtualPathSafely(String, String)} — the canonical
167    * shared implementation reused by {@code JspMixin.render(...)} for the
168    * raw-{@code .jsp} dispatch path. Handles every {@code (basePath, template)} combination of
169    * trailing/leading slashes uniformly so callers can pass {@code basePath("/WEB-INF/views")} or
170    * {@code basePath("/WEB-INF/views/")} and {@code "hello.jsp"} or {@code "/hello.jsp"} without
171    * worrying about double-slash artifacts.
172    *
173    * <p>
174    * A {@code null} or empty {@code basePath} is normalized to {@code "/"} (the
175    * {@link JspMixin#DEFAULT_BASE_PATH default}) before the boundary check runs.
176    *
177    * @param basePath The base path (e.g. {@code "/WEB-INF/views/"}).
178    * @param template The template name (e.g. {@code "hello.jsp"}).
179    * @return The joined path (e.g. {@code "/WEB-INF/views/hello.jsp"}).
180    * @throws IllegalArgumentException If {@code template} escapes {@code basePath} via
181    *    {@code ..} segments.
182    */
183   static String joinPath(String basePath, String template) {
184      var bp = isEmpty(basePath) ? "/" : basePath;
185      return FileUtils.resolveVirtualPathSafely(bp, template);
186   }
187}