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 * — 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}