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.freemarker; 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 an Apache FreeMarker template. 029 * 030 * <p> 031 * Companion to {@link FreemarkerMixin} and {@link FreemarkerViewRenderer}: the mixin sets 032 * up the {@code /freemarker/*} mount and registers the renderer; the renderer detects 033 * {@code FreemarkerView} returns in the response-processor chain and asks the configured 034 * {@code freemarker.template.Configuration} to {@code getTemplate(templateName)} and then 035 * {@code process(dataModel, writer)} directly onto the response writer. 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> FreemarkerView.<jsm>of</jsm>(<js>"hello.ftlh"</js>) 043 * .attr(<js>"name"</js>, <jv>name</jv>) 044 * .attr(<js>"ts"</js>, Instant.<jsm>now</jsm>()); 045 * } 046 * </p> 047 * 048 * <p> 049 * The template name is configuration-relative — the configured 050 * {@code freemarker.template.Configuration} template loader prepends its own resource root. With 051 * the bridge's default configuration + {@code basePath("/templates/")}, 052 * {@code FreemarkerView.of("hello.ftlh")} resolves to {@code /templates/hello.ftlh} on the 053 * classpath. 054 * 055 * <h5 class='section'>{@code .ftl} vs {@code .ftlh} auto-escape:</h5> 056 * 057 * <p> 058 * FreeMarker auto-selects HTML escaping by file extension: {@code .ftlh} templates emit 059 * HTML-escaped output (variable references are escaped automatically), while {@code .ftl} 060 * templates emit raw output. For HTML responses, prefer the {@code .ftlh} extension to avoid an 061 * XSS regression sneaking in via a future attribute-binding change. 062 * 063 * <h5 class='section'>Template suffix:</h5> 064 * 065 * <p> 066 * The {@link FreemarkerMixin.Builder#templateSuffix(String) templateSuffix(...)} builder 067 * knob lets the bridge append a suffix (e.g. {@code ".ftlh"}) to template names that don't 068 * already end with it, so callers can write {@code FreemarkerView.of("hello")} and have the 069 * bridge resolve it as {@code "hello.ftlh"} on the classpath (idempotent — explicit suffix 070 * is honored). 071 * 072 * <h5 class='section'>Immutability:</h5> 073 * 074 * <p> 075 * Each {@code attr(...)} / {@code attrs(...)} / {@code header(...)} call returns a <b>new</b> 076 * {@code FreemarkerView} carrying the additional binding; the original instance is unchanged. 077 * This keeps {@code FreemarkerView} a value type safe to share across requests (e.g. as a static 078 * singleton built once and returned from many handlers). 079 * 080 * <h5 class='section'>See Also:</h5><ul> 081 * <li class='jc'>{@link FreemarkerMixin} 082 * <li class='jc'>{@link FreemarkerViewRenderer} 083 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/FreemarkerViewSupport">FreeMarker View Support</a> 084 * </ul> 085 * 086 * @since 10.0.0 087 */ 088public final class FreemarkerView implements View { 089 090 private final String templateName; 091 private final Map<String,Object> attributes; 092 private final Map<String,String> responseHeaders; 093 094 /** 095 * Creates a new {@code FreemarkerView} carrying the given template name and no attributes. 096 * 097 * <p> 098 * The template name is configuration-relative; the configured 099 * {@code freemarker.template.Configuration} template loader supplies the prefix (resource 100 * root). With the bridge's default configuration + {@code basePath("/templates/")}, the name 101 * {@code "hello.ftlh"} resolves to {@code /templates/hello.ftlh} on the classpath. 102 * 103 * @param templateName The template name (configuration-relative, suffix optional when the 104 * mixin's {@link FreemarkerMixin.Builder#templateSuffix(String) templateSuffix} is 105 * configured). Must not be {@code null} or blank. 106 * @return A new {@code FreemarkerView} instance. 107 * @throws IllegalArgumentException If {@code templateName} is {@code null} or blank. 108 */ 109 public static FreemarkerView of(String templateName) { 110 if (isBlank(templateName)) 111 throw iaex("templateName must not be null or blank"); 112 return new FreemarkerView(templateName, Map.of(), Map.of()); 113 } 114 115 private FreemarkerView(String templateName, Map<String,Object> attributes, Map<String,String> responseHeaders) { 116 this.templateName = templateName; 117 this.attributes = attributes; 118 this.responseHeaders = responseHeaders; 119 } 120 121 /** 122 * Returns a copy of this view with the given attribute added (or replaced if a binding for 123 * {@code key} already exists). 124 * 125 * <p> 126 * The full attribute map is passed to FreeMarker as the data model, so each entry is 127 * accessible inside the template as {@code ${key}}. {@code null} values are rejected at 128 * build-time because FreeMarker's default null-handling renders missing/null bindings as an 129 * error or empty string depending on configuration, which typically masks what is a caller 130 * bug; rejecting up-front surfaces the mistake at the call site (matches the 131 * Thymeleaf / Mustache bridge precedents). 132 * 133 * @param key The attribute key (exposed inside the template as {@code ${key}}). Must not be 134 * {@code null} or blank. 135 * @param value The attribute value. Must not be {@code null}. 136 * @return A new {@code FreemarkerView} carrying the additional attribute. 137 * @throws IllegalArgumentException If {@code key} is {@code null} or blank, or {@code value} 138 * is {@code null}. 139 */ 140 public FreemarkerView attr(String key, Object value) { 141 if (isBlank(key)) 142 throw iaex("attribute key must not be null or blank"); 143 if (value == null) 144 throw iaex("attribute value must not be null (attribute '%s')", key); 145 var copy = new LinkedHashMap<>(attributes); 146 copy.put(key, value); 147 return new FreemarkerView(templateName, Collections.unmodifiableMap(copy), responseHeaders); 148 } 149 150 /** 151 * Returns a copy of this view with all entries from the given map added (or replaced if a 152 * binding for any key already exists). 153 * 154 * @param values The attributes to add. {@code null} is treated as an empty map (the result is 155 * the same instance with no entries added). 156 * @return A new {@code FreemarkerView} carrying the additional attributes. 157 * @throws IllegalArgumentException If any entry has a {@code null}/blank key or a {@code null} 158 * value. 159 */ 160 public FreemarkerView attrs(Map<String,?> values) { 161 if (ie(values)) 162 return this; 163 var copy = new LinkedHashMap<>(attributes); 164 values.forEach((k, v) -> { 165 if (isBlank(k)) 166 throw iaex("attribute key must not be null or blank"); 167 if (v == null) 168 throw iaex("attribute value must not be null (attribute '%s')", k); 169 copy.put(k, v); 170 }); 171 return new FreemarkerView(templateName, Collections.unmodifiableMap(copy), responseHeaders); 172 } 173 174 /** 175 * Returns a copy of this view with the given response header set on the rendered response. 176 * 177 * <p> 178 * Typical usage: {@code .header("Content-Type", "text/html; charset=UTF-8")} or 179 * {@code .header("Cache-Control", "no-store")}. The renderer applies these headers via 180 * {@link jakarta.servlet.http.HttpServletResponse#setHeader(String, String) 181 * HttpServletResponse.setHeader(...)} before writing the rendered output. 182 * 183 * @param name The header name. Must not be {@code null} or blank. 184 * @param value The header value. Must not be {@code null}. 185 * @return A new {@code FreemarkerView} carrying the additional response header. 186 * @throws IllegalArgumentException If {@code name} is {@code null} or blank, or {@code value} 187 * is {@code null}. 188 */ 189 public FreemarkerView header(String name, String value) { 190 if (isBlank(name)) 191 throw iaex("header name must not be null or blank"); 192 if (value == null) 193 throw iaex("header value must not be null (header '%s')", name); 194 var copy = new LinkedHashMap<>(responseHeaders); 195 copy.put(name, value); 196 return new FreemarkerView(templateName, attributes, Map.copyOf(copy)); 197 } 198 199 /** 200 * Marker for an attribute value that promises to be adopted during the render (unwrapped from the data model by 201 * some directive and applied to render-scoped state), and wants the render to fail if that never happens 202 * instead of silently behaving as if the attribute was absent. 203 * 204 * <p> 205 * {@link FreemarkerViewRenderer} checks every attribute value after a successful {@code Template.process(...)}: 206 * any value implementing this interface whose {@link #consumed()} is still {@code false} fails the render with 207 * {@link #notConsumedMessage(String)}. 208 */ 209 public interface MustConsume { 210 211 /** @return Whether this attribute was adopted during the render. */ 212 boolean consumed(); 213 214 /** 215 * @param templateName The template that was rendered. 216 * @return The message to raise when {@link #consumed()} is still {@code false} after the render. 217 */ 218 String notConsumedMessage(String templateName); 219 } 220 221 @Override /* Overridden from View */ 222 public String getTemplateName() { 223 return templateName; 224 } 225 226 @Override /* Overridden from View */ 227 public Map<String,Object> getAttributes() { 228 return attributes; 229 } 230 231 @Override /* Overridden from View */ 232 public Map<String,String> getResponseHeaders() { 233 return responseHeaders; 234 } 235 236 @Override /* Overridden from Object */ 237 public String toString() { 238 return "FreemarkerView[template=" + templateName + ", attrs=" + attributes.keySet() 239 + ", headers=" + responseHeaders.keySet() + "]"; 240 } 241}