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.util.*;
023
024import org.apache.juneau.rest.server.view.*;
025
026/**
027 * Immutable value class returned from {@code @RestOp}-annotated methods to ask the framework to
028 * render a JSP template.
029 *
030 * <p>
031 * Companion to {@link JspMixin} and {@link JspViewRenderer}: the mixin sets up the
032 * {@code /jsp/*} mount and registers the renderer; the renderer detects {@code JspView} returns
033 * in the response-processor chain and dispatches via
034 * {@link jakarta.servlet.RequestDispatcher#forward(jakarta.servlet.ServletRequest,
035 * jakarta.servlet.ServletResponse) ServletContext.getRequestDispatcher(...).forward(...)}.
036 *
037 * <h5 class='figure'>Usage:</h5>
038 *
039 * <p class='bjava'>
040 *    <ja>@RestGet</ja>(<js>"/hello/{name}"</js>)
041 *    <jk>public</jk> View hello(<ja>@Path</ja> String <jv>name</jv>) {
042 *       <jk>return</jk> JspView.<jsm>of</jsm>(<js>"hello.jsp"</js>)
043 *          .attr(<js>"name"</js>, <jv>name</jv>)
044 *          .attr(<js>"ts"</js>, Instant.<jsm>now</jsm>());
045 *    }
046 * </p>
047 *
048 * <h5 class='section'>Immutability:</h5>
049 *
050 * <p>
051 * Each {@code attr(...)} / {@code attrs(...)} / {@code header(...)} call returns a <b>new</b>
052 * {@code JspView} carrying the additional binding; the original instance is unchanged. This keeps
053 * {@code JspView} a value type safe to share across requests (e.g. as a static singleton built
054 * once and returned from many handlers).
055 *
056 * <h5 class='section'>See Also:</h5><ul>
057 *    <li class='jc'>{@link JspMixin}
058 *    <li class='jc'>{@link JspViewRenderer}
059 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/JspViewSupport">JSP View Support</a>
060 * </ul>
061 *
062 * @since 10.0.0
063 */
064public final class JspView implements View {
065
066   private final String templateName;
067   private final Map<String,Object> attributes;
068   private final Map<String,String> responseHeaders;
069
070   /**
071    * Creates a new {@code JspView} carrying the given template name and no attributes.
072    *
073    * @param templateName The template name (relative to the
074    *    {@link JspMixin.Builder#basePath base path}). Must not be {@code null} or blank.
075    * @return A new {@code JspView} instance.
076    * @throws IllegalArgumentException If {@code templateName} is {@code null} or blank.
077    */
078   public static JspView of(String templateName) {
079      if (isBlank(templateName))
080         throw iaex("templateName must not be null or blank");
081      return new JspView(templateName, Map.of(), Map.of());
082   }
083
084   private JspView(String templateName, Map<String,Object> attributes, Map<String,String> responseHeaders) {
085      this.templateName = templateName;
086      this.attributes = attributes;
087      this.responseHeaders = responseHeaders;
088   }
089
090   /**
091    * Returns a copy of this view with the given attribute added (or replaced if a binding for
092    * {@code key} already exists).
093    *
094    * <p>
095    * Per the Servlet spec, passing {@code null} to
096    * {@link jakarta.servlet.ServletRequest#setAttribute setAttribute(name, null)} removes the
097    * binding rather than setting it; {@code JspView} mirrors that constraint at build-time so
098    * the renderer never has to short-circuit a request-attribute write at dispatch time.
099    *
100    * @param key The attribute key (exposed inside the JSP as {@code ${key}}). Must not be
101    *    {@code null} or blank.
102    * @param value The attribute value. Must not be {@code null}.
103    * @return A new {@code JspView} carrying the additional attribute.
104    * @throws IllegalArgumentException If {@code key} is {@code null} or blank, or {@code value}
105    *    is {@code null}.
106    */
107   public JspView attr(String key, Object value) {
108      if (isBlank(key))
109         throw iaex("attribute key must not be null or blank");
110      if (value == null)
111         throw iaex("attribute value must not be null (attribute '%s')", key);
112      var copy = new LinkedHashMap<>(attributes);
113      copy.put(key, value);
114      return new JspView(templateName, Map.copyOf(copy), responseHeaders);
115   }
116
117   /**
118    * Returns a copy of this view with all entries from the given map added (or replaced if a
119    * binding for any key already exists).
120    *
121    * @param values The attributes to add. {@code null} is treated as an empty map (the result is
122    *    logically the same instance with no entries added).
123    * @return A new {@code JspView} carrying the additional attributes.
124    */
125   public JspView attrs(Map<String,?> values) {
126      if (ie(values))
127         return this;
128      var copy = new LinkedHashMap<>(attributes);
129      values.forEach((k, v) -> {
130         if (isBlank(k))
131            throw iaex("attribute key must not be null or blank");
132         if (v == null)
133            throw iaex("attribute value must not be null (attribute '%s')", k);
134         copy.put(k, v);
135      });
136      return new JspView(templateName, Map.copyOf(copy), responseHeaders);
137   }
138
139   /**
140    * Returns a copy of this view with the given response header set on the rendered response.
141    *
142    * <p>
143    * Typical usage: {@code .header("Content-Type", "text/html; charset=UTF-8")} or
144    * {@code .header("Cache-Control", "no-store")}. The renderer applies these headers via
145    * {@link jakarta.servlet.http.HttpServletResponse#setHeader(String, String)
146    * HttpServletResponse.setHeader(...)} before forwarding to the JSP engine.
147    *
148    * @param name The header name. Must not be {@code null} or blank.
149    * @param value The header value. Must not be {@code null}.
150    * @return A new {@code JspView} carrying the additional response header.
151    * @throws IllegalArgumentException If {@code name} is {@code null} or blank, or {@code value}
152    *    is {@code null}.
153    */
154   public JspView header(String name, String value) {
155      if (isBlank(name))
156         throw iaex("header name must not be null or blank");
157      if (value == null)
158         throw iaex("header value must not be null (header '%s')", name);
159      var copy = new LinkedHashMap<>(responseHeaders);
160      copy.put(name, value);
161      return new JspView(templateName, attributes, Map.copyOf(copy));
162   }
163
164   @Override /* Overridden from View */
165   public String getTemplateName() {
166      return templateName;
167   }
168
169   @Override /* Overridden from View */
170   public Map<String,Object> getAttributes() {
171      return attributes;
172   }
173
174   @Override /* Overridden from View */
175   public Map<String,String> getResponseHeaders() {
176      return responseHeaders;
177   }
178
179   @Override /* Overridden from Object */
180   public String toString() {
181      return "JspView[template=" + templateName + ", attrs=" + attributes.keySet()
182         + ", headers=" + responseHeaders.keySet() + "]";
183   }
184}