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.Shorts.*;
020import static org.apache.juneau.commons.utils.StringUtils.*;
021
022import java.io.*;
023
024import org.apache.juneau.http.response.*;
025import org.apache.juneau.rest.server.*;
026import org.apache.juneau.rest.server.view.*;
027
028/**
029 * Flavor-neutral worker bean carrying the JSP raw-template-dispatch logic shared by the
030 * {@link JspMixin} mixin, the {@link JspServlet} servlet, and the {@link JspResource} child flavors.
031 *
032 * <p>
033 * This is the <i>flavor-neutral worker</i> for the JSP capability: it is <b>not</b> one of the
034 * three {@code @Rest} flavor classes &mdash; it is a plain bean that implements
035 * {@link RawTemplateDispatcher} and holds the capability state ({@code basePath}) plus the raw-serve
036 * logic. Each flavor independently holds an instance of this worker and delegates to it, so no flavor
037 * is another flavor's worker.
038 *
039 * <h5 class='section'>See Also:</h5><ul>
040 *    <li class='jc'>{@link JspMixin}
041 *    <li class='jc'>{@link JspServlet}
042 *    <li class='jc'>{@link JspResource}
043 *    <li class='jic'>{@link RawTemplateDispatcher}
044 * </ul>
045 *
046 * @since 10.0.0
047 */
048public class JspDispatcher implements RawTemplateDispatcher {
049
050   /** Default base path applied when no {@link Builder#basePath(String)} call has been made. */
051   public static final String DEFAULT_BASE_PATH = "/";
052
053   private final String basePath;
054
055   /**
056    * Creates a new builder.
057    *
058    * @return A new builder.
059    */
060   public static Builder create() {
061      return new Builder();
062   }
063
064   /**
065    * Builder constructor.
066    *
067    * @param builder The builder. Must not be {@code null}.
068    */
069   protected JspDispatcher(Builder builder) {
070      basePath = builder.basePath;
071   }
072
073   /**
074    * Returns the base path under which {@code .jsp} resources are resolved.
075    *
076    * @return The base path. Never {@code null}.
077    */
078   public String getBasePath() {
079      return basePath;
080   }
081
082   /**
083    * Forwards the request to the JSP engine for the raw {@code .jsp} resource at the trailing path.
084    *
085    * @param path The trailing path segment after the mount prefix (the JSP file name relative to
086    *    the configured {@link #getBasePath() base path}).
087    * @param req The current REST request.
088    * @param res The current REST response.
089    * @throws IOException If the underlying servlet writer fails.
090    * @throws NotFound If the JSP resource cannot be resolved.
091    */
092   @Override /* RawTemplateDispatcher */
093   public void render(String path, RestRequest req, RestResponse res) throws IOException, NotFound {
094      // joinPath delegates to FileUtils.resolveVirtualPathSafely, which throws IAE on any
095      // resolved target that escapes basePath (../, %2e%2e treated as literal, absolute-path
096      // injection, etc.). Map IAE → 403 (Forbidden); attackers can't distinguish from the
097      // container-layer rejection of malformed paths.
098      String target;
099      try {
100         target = JspViewRenderer.joinPath(basePath, path);
101      } catch (IllegalArgumentException ex) {
102         throw new Forbidden("Path escapes configured base path.");
103      }
104      try {
105         var ctx = req.getServletContext();
106         var rd = ctx.getRequestDispatcher(target);
107         if (rd == null)
108            throw new InternalServerError("Could not resolve RequestDispatcher for '%s'. %s",
109               target, JspViewRenderer.NO_ENGINE_DIAGNOSTIC);
110         rd.forward(req.getHttpServletRequest(), res.getHttpServletResponse());
111      } catch (InternalServerError | IOException | NotFound ex) {
112         // Re-throw as-is so the specific diagnostic constructed above, or a framework-level IOException
113         // or NotFound, isn't discarded by the generic fallback below.
114         throw ex;
115      } catch (NoClassDefFoundError ex) {
116         throw new InternalServerError(ex, JspViewRenderer.NO_ENGINE_DIAGNOSTIC);
117      } catch (Exception ex) {
118         throw new InternalServerError(ex, "JSP render failed for '%s'", target);
119      }
120   }
121
122   /**
123    * Builder for {@link JspDispatcher}.
124    */
125   public static class Builder {
126
127      String basePath = DEFAULT_BASE_PATH;
128
129      /** Constructor &mdash; package access for {@link JspDispatcher#create()}. */
130      protected Builder() {}
131
132      /**
133       * Sets the classpath / webapp base path under which {@code .jsp} resources are resolved.
134       *
135       * @param value The base path. {@code null} or blank values reset to the default
136       *    {@link JspDispatcher#DEFAULT_BASE_PATH}.
137       * @return This object.
138       */
139      public Builder basePath(String value) {
140         basePath = isBlank(value) ? DEFAULT_BASE_PATH : value;
141         return this;
142      }
143
144      /**
145       * Reads the current base path setting (test/inspection helper).
146       *
147       * @return The base path. Never {@code null}.
148       */
149      public String getBasePath() {
150         return basePath;
151      }
152
153      /**
154       * Builds the {@link JspDispatcher}.
155       *
156       * @return A new {@link JspDispatcher} instance.
157       */
158      public JspDispatcher build() {
159         if (basePath == null)
160            throw iaex("basePath must not be null");
161         return new JspDispatcher(this);
162      }
163   }
164}