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 java.io.*;
020
021import org.apache.juneau.commons.utils.*;
022import org.apache.juneau.http.response.*;
023import org.apache.juneau.rest.server.*;
024import org.apache.juneau.rest.server.processor.*;
025import org.apache.juneau.rest.server.view.*;
026
027import com.github.mustachejava.*;
028
029/**
030 * {@link ResponseProcessor} that detects {@link MustacheView}-typed return values and asks the
031 * configured {@link MustacheFactory} to render them directly onto the response writer.
032 *
033 * <p>
034 * Auto-registered by {@link MustacheMixin} via
035 * {@link Rest#responseProcessors() @Rest(responseProcessors=...)}
036 * &mdash; callers who add the mixin don't need to wire up this class explicitly. Callers who want
037 * to handle {@code MustacheView} returns <i>without</i> adopting the mixin can add this class to
038 * their own
039 * {@link Rest#responseProcessors() responseProcessors} list.
040 *
041 * <h5 class='section'>Behavior:</h5>
042 *
043 * <ol class='spaced-list'>
044 *    <li>Inspect the response content. If the value is not a {@link MustacheView}, return
045 *       {@link ResponseProcessor#NEXT NEXT} so the rest of the chain runs.
046 *    <li>Read the active {@link MustacheMixin} from the {@code RestContext} bean store to
047 *       discover the {@link MustacheFactory} (lazy default if no factory bean is registered) and
048 *       the optional template-suffix knob.
049 *    <li>Apply every entry from {@link MustacheView#getResponseHeaders()} via
050 *       {@link jakarta.servlet.http.HttpServletResponse#setHeader(String, String)
051 *       res.setHeader(...)}.
052 *    <li>Default {@code Content-Type} to {@code text/html;charset=UTF-8} if the caller did not set
053 *       one explicitly (Mustache is content-neutral, but HTML is overwhelmingly the most common
054 *       output target; an explicit caller header wins).
055 *    <li>Call {@code factory.compile(templateName).execute(res.getWriter(), view.getAttributes())}
056 *       to stream the rendered output.
057 *    <li>When no Mustache engine is on the classpath, the {@link MustacheFactory}-typed import
058 *       here fails to load at first use and surfaces {@link #NO_ENGINE_DIAGNOSTIC} naming the
059 *       missing dependency.
060 * </ol>
061 *
062 * <h5 class='section'>See Also:</h5><ul>
063 *    <li class='jc'>{@link MustacheMixin}
064 *    <li class='jc'>{@link MustacheView}
065 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/ResponseProcessors">Response Processors</a>
066 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/MustacheViewSupport">Mustache View Support</a>
067 * </ul>
068 *
069 * @since 10.0.0
070 */
071@SuppressWarnings({
072   "resource" // Closeables here are framework-managed and not owned/closed by this class; not a real leak.
073})
074public class MustacheViewRenderer implements ViewRenderer {
075
076   /** Default {@code Content-Type} applied when the view does not specify one explicitly. */
077   public static final String DEFAULT_CONTENT_TYPE = "text/html;charset=UTF-8";
078
079   /**
080    * Diagnostic message emitted when no Mustache engine can be found on the classpath at
081    * render-time.
082    *
083    * <p>
084    * Public so tests and consumer apps can pattern-match against the message text without
085    * depending on internal string literals.
086    */
087   public static final String NO_ENGINE_DIAGNOSTIC =
088      """
089      No Mustache engine is available on the classpath. Add:
090        - com.github.spullara.mustache.java:compiler   (mustache.java — the engine the bridge targets)
091      Or register a custom @Bean MustacheFactory that picks up your preferred resolvers.
092      Note: Spring Boot's spring-boot-starter-mustache ships com.samskivert:jmustache, NOT
093      mustache.java; the bridge is mustache.java-specific. To use jmustache, supply your own
094      ResponseProcessor instead of MustacheViewRenderer.
095      See https://juneau.apache.org/docs/topics/MustacheViewSupport for the full matrix.""";
096
097   // Lazily-created fallback used when no MustacheMixin bean is registered.  Cached (double-checked) so
098   // the bridge-default MustacheFactory is built once instead of rebuilt per render.  Created lazily (not
099   // eagerly) so a missing Mustache engine still surfaces at render time as NO_ENGINE_DIAGNOSTIC rather
100   // than during RestContext construction.
101   @SuppressWarnings({
102      "java:S3077" // Publish-once cache: assigned once under double-checked locking in fallbackMixin(); MustacheMixin is fully built (effectively immutable — its only field is final) before assignment, so volatile safe-publication is sufficient.
103   })
104   private volatile MustacheMixin fallbackMixin;
105
106   @Override /* Overridden from ResponseProcessor */
107   public int process(RestOpSession opSession) throws IOException, BasicHttpException {
108      var req = opSession.getRequest();
109      var res = opSession.getResponse();
110
111      var content = res.getContent(Object.class);
112      if (! (content instanceof MustacheView content2))
113         return NEXT;
114
115      // Resolve the bridge resource (carries MustacheFactory + cached default + templateSuffix).
116      // Fall back to a cached MustacheMixin when the renderer is used standalone without
117      // the mixin.
118      var bridge = req.getContext().getBeanStore()
119         .getBean(MustacheMixin.class)
120         .orElseGet(this::fallbackMixin);
121
122      // Apply caller-supplied response headers first so a caller-provided Content-Type wins
123      // over the bridge's default below.
124      content2.getResponseHeaders().forEach(res::setHeader);
125      if (! res.containsHeader("Content-Type"))
126         res.setHeader("Content-Type", DEFAULT_CONTENT_TYPE);
127
128      // Apply the same traversal gate the raw /mustache/* mount uses so a typed-View template name
129      // assembled from user input (e.g. MustacheView.of(userInput)) cannot escape the configured base
130      // path via '../' segments.  Mirrors the JSP bridge's typed-View gate.
131      String safeName;
132      try {
133         safeName = gateTemplateName(bridge.getBasePath(), content2.getTemplateName());
134      } catch (IllegalArgumentException ex) {
135         throw new InternalServerError(ex, "Mustache template name escapes configured base path: '%s'", content2.getTemplateName());
136      }
137
138      var templateName = bridge.applyTemplateSuffix(safeName);
139      try {
140         var factory = bridge.resolveMustacheFactory(req);
141         var mustache = factory.compile(templateName);
142         mustache.execute(res.getWriter(), content2.getAttributes()).flush();
143         return FINISHED;
144      } catch (LinkageError ex) {
145         throw new InternalServerError(ex, NO_ENGINE_DIAGNOSTIC);
146      } catch (IOException ex) {
147         throw ex;
148      } catch (Exception ex) {
149         throw new InternalServerError(ex, "Mustache render failed for '%s'", templateName);
150      }
151   }
152
153   // Rejects '../'-style traversal in a typed-View template name using the same virtual-path gate the raw
154   // mount applies, returning the configuration-relative template name.  Delegates to the shared
155   // FileUtils.resolveVirtualPathSafely + MustacheDispatcher.stripBasePath used by the raw render path.
156   static String gateTemplateName(String basePath, String templateName) {
157      var resolved = FileUtils.resolveVirtualPathSafely(basePath, templateName);
158      return MustacheDispatcher.stripBasePath(basePath, resolved);
159   }
160
161   // Returns the cached fallback mixin, creating it once (double-checked) on first standalone render.
162   private MustacheMixin fallbackMixin() {
163      var local = fallbackMixin;
164      if (local == null) {
165         synchronized (this) {
166            local = fallbackMixin;
167            if (local == null) {
168               local = new MustacheMixin();
169               fallbackMixin = local;
170            }
171         }
172      }
173      return local;
174   }
175}