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.mustache; 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 Mustache template. 029 * 030 * <p> 031 * Companion to {@link MustacheMixin} and {@link MustacheViewRenderer}: the mixin sets up 032 * the {@code /mustache/*} mount and registers the renderer; the renderer detects 033 * {@code MustacheView} returns in the response-processor chain and asks the configured 034 * {@code com.github.mustachejava.MustacheFactory} to {@code compile(templateName)} and then 035 * {@code execute(writer, scope)} 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> MustacheView.<jsm>of</jsm>(<js>"hello.mustache"</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 factory-relative — the configured 050 * {@code com.github.mustachejava.MustacheFactory} resource resolver prepends its own resource 051 * root. With the bridge's default factory + {@code basePath("/templates/")}, 052 * {@code MustacheView.of("hello.mustache")} resolves to {@code templates/hello.mustache} on the 053 * classpath. 054 * 055 * <h5 class='section'>Template suffix:</h5> 056 * 057 * <p> 058 * Unlike the Thymeleaf bridge (which always appends {@code .html} via the engine resolver's 059 * suffix setting), mustache.java does not have a built-in resolver-suffix concept — the 060 * literal template name is what {@code factory.compile(...)} sees. The 061 * {@link MustacheMixin.Builder#templateSuffix(String) templateSuffix(...)} builder knob 062 * fills the gap: when set, the bridge appends the suffix to template names that don't already 063 * end with it (idempotent), so the caller can write {@code MustacheView.of("hello")} and the 064 * bridge resolves it as {@code "hello.mustache"} on the classpath. 065 * 066 * <h5 class='section'>Immutability:</h5> 067 * 068 * <p> 069 * Each {@code attr(...)} / {@code attrs(...)} / {@code header(...)} call returns a <b>new</b> 070 * {@code MustacheView} carrying the additional binding; the original instance is unchanged. This 071 * keeps {@code MustacheView} a value type safe to share across requests (e.g. as a static 072 * singleton built once and returned from many handlers). 073 * 074 * <h5 class='section'>See Also:</h5><ul> 075 * <li class='jc'>{@link MustacheMixin} 076 * <li class='jc'>{@link MustacheViewRenderer} 077 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/MustacheViewSupport">Mustache View Support</a> 078 * </ul> 079 * 080 * @since 10.0.0 081 */ 082public final class MustacheView implements View { 083 084 private final String templateName; 085 private final Map<String,Object> attributes; 086 private final Map<String,String> responseHeaders; 087 088 /** 089 * Creates a new {@code MustacheView} carrying the given template name and no attributes. 090 * 091 * <p> 092 * The template name is factory-relative; the configured 093 * {@code com.github.mustachejava.MustacheFactory} resource resolver supplies the prefix 094 * (resource root). With the bridge's default factory + {@code basePath("/templates/")} 095 * and {@code templateSuffix(".mustache")}, {@code "hello"} resolves to 096 * {@code templates/hello.mustache}; without {@code templateSuffix}, the literal name is 097 * used as-is. 098 * 099 * @param templateName The template name (factory-relative, suffix optional). Must not be 100 * {@code null} or blank. 101 * @return A new {@code MustacheView} instance. 102 * @throws IllegalArgumentException If {@code templateName} is {@code null} or blank. 103 */ 104 public static MustacheView of(String templateName) { 105 if (isBlank(templateName)) 106 throw iaex("templateName must not be null or blank"); 107 return new MustacheView(templateName, Map.of(), Map.of()); 108 } 109 110 private MustacheView(String templateName, Map<String,Object> attributes, Map<String,String> responseHeaders) { 111 this.templateName = templateName; 112 this.attributes = attributes; 113 this.responseHeaders = responseHeaders; 114 } 115 116 /** 117 * Returns a copy of this view with the given attribute added (or replaced if a binding for 118 * {@code key} already exists). 119 * 120 * <p> 121 * The full attribute map is passed to mustache.java as the rendering scope, so each entry 122 * is accessible inside the template as {@code {{key}}}. {@code null} values are rejected at 123 * build-time because a {@code null} scope binding in mustache.java renders as an empty 124 * string, which silently masks what is typically a caller bug; rejecting up-front surfaces 125 * the mistake at the call site (matches the Thymeleaf-bridge precedent). 126 * 127 * @param key The attribute key (exposed inside the template as {@code {{key}}}). Must not be 128 * {@code null} or blank. 129 * @param value The attribute value. Must not be {@code null}. 130 * @return A new {@code MustacheView} carrying the additional attribute. 131 * @throws IllegalArgumentException If {@code key} is {@code null} or blank, or {@code value} 132 * is {@code null}. 133 */ 134 public MustacheView attr(String key, Object value) { 135 if (isBlank(key)) 136 throw iaex("attribute key must not be null or blank"); 137 if (value == null) 138 throw iaex("attribute value must not be null (attribute '%s')", key); 139 var copy = new LinkedHashMap<>(attributes); 140 copy.put(key, value); 141 return new MustacheView(templateName, Map.copyOf(copy), responseHeaders); 142 } 143 144 /** 145 * Returns a copy of this view with all entries from the given map added (or replaced if a 146 * binding for any key already exists). 147 * 148 * @param values The attributes to add. {@code null} is treated as an empty map (the result is 149 * the same instance with no entries added). 150 * @return A new {@code MustacheView} carrying the additional attributes. 151 * @throws IllegalArgumentException If any entry has a {@code null}/blank key or a {@code null} 152 * value. 153 */ 154 public MustacheView attrs(Map<String,?> values) { 155 if (ie(values)) 156 return this; 157 var copy = new LinkedHashMap<>(attributes); 158 values.forEach((k, v) -> { 159 if (isBlank(k)) 160 throw iaex("attribute key must not be null or blank"); 161 if (v == null) 162 throw iaex("attribute value must not be null (attribute '%s')", k); 163 copy.put(k, v); 164 }); 165 return new MustacheView(templateName, Map.copyOf(copy), responseHeaders); 166 } 167 168 /** 169 * Returns a copy of this view with the given response header set on the rendered response. 170 * 171 * <p> 172 * Typical usage: {@code .header("Content-Type", "text/html; charset=UTF-8")} or 173 * {@code .header("Cache-Control", "no-store")}. The renderer applies these headers via 174 * {@link jakarta.servlet.http.HttpServletResponse#setHeader(String, String) 175 * HttpServletResponse.setHeader(...)} before writing the rendered output. 176 * 177 * @param name The header name. Must not be {@code null} or blank. 178 * @param value The header value. Must not be {@code null}. 179 * @return A new {@code MustacheView} carrying the additional response header. 180 * @throws IllegalArgumentException If {@code name} is {@code null} or blank, or {@code value} 181 * is {@code null}. 182 */ 183 public MustacheView header(String name, String value) { 184 if (isBlank(name)) 185 throw iaex("header name must not be null or blank"); 186 if (value == null) 187 throw iaex("header value must not be null (header '%s')", name); 188 var copy = new LinkedHashMap<>(responseHeaders); 189 copy.put(name, value); 190 return new MustacheView(templateName, attributes, Map.copyOf(copy)); 191 } 192 193 @Override /* Overridden from View */ 194 public String getTemplateName() { 195 return templateName; 196 } 197 198 @Override /* Overridden from View */ 199 public Map<String,Object> getAttributes() { 200 return attributes; 201 } 202 203 @Override /* Overridden from View */ 204 public Map<String,String> getResponseHeaders() { 205 return responseHeaders; 206 } 207 208 @Override /* Overridden from Object */ 209 public String toString() { 210 return "MustacheView[template=" + templateName + ", attrs=" + attributes.keySet() 211 + ", headers=" + responseHeaders.keySet() + "]"; 212 } 213}