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 &mdash; 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 &mdash; 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}