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}