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.io.*;
023import java.util.*;
024
025import org.apache.juneau.commons.utils.*;
026import org.apache.juneau.http.response.*;
027import org.apache.juneau.rest.server.*;
028import org.apache.juneau.rest.server.view.*;
029
030import com.github.mustachejava.*;
031
032/**
033 * Flavor-neutral worker bean carrying the Mustache raw-template-dispatch logic shared by the
034 * {@link MustacheMixin} mixin, the {@link MustacheServlet} servlet, and the {@link MustacheResource}
035 * child flavors.
036 *
037 * <p>
038 * This is the <i>flavor-neutral worker</i> for the Mustache capability: it is <b>not</b> one
039 * of the three {@code @Rest} flavor classes &mdash; it is a plain bean that implements
040 * {@link RawTemplateDispatcher} and holds the capability state ({@code basePath} /
041 * {@code templateSuffix} / lazy default factory) plus the raw-serve and factory-resolution logic.
042 * Each flavor independently holds an instance of this worker and delegates to it, so no flavor is
043 * another flavor's worker.
044 *
045 * <h5 class='section'>See Also:</h5><ul>
046 *    <li class='jc'>{@link MustacheMixin}
047 *    <li class='jc'>{@link MustacheServlet}
048 *    <li class='jc'>{@link MustacheResource}
049 *    <li class='jic'>{@link RawTemplateDispatcher}
050 * </ul>
051 *
052 * @since 10.0.0
053 */
054@SuppressWarnings({
055   "resource" // Closeables here are framework-managed and not owned/closed by this class; not a real leak.
056})
057public class MustacheDispatcher implements RawTemplateDispatcher {
058
059   /** Default base path applied when no {@link Builder#basePath(String)} call has been made. */
060   public static final String DEFAULT_BASE_PATH = "/";
061
062   /** Default template suffix &mdash; empty (literal template names, no implicit suffix). */
063   public static final String DEFAULT_TEMPLATE_SUFFIX = "";
064
065   /** Default cache-templates setting &mdash; {@code true} (cache forever; production-safe). */
066   public static final boolean DEFAULT_CACHE_TEMPLATES = true;
067
068   private final String basePath;
069   private final String templateSuffix;
070   private final boolean cacheTemplates;
071
072   // Lazy bridge-default factory. Built on first call to resolveMustacheFactory(...) when no
073   // MustacheFactory bean is registered in the request's BeanStore. Volatile so the
074   // double-checked-locking idiom is safe under concurrent first-request load.
075   @SuppressWarnings({
076      "java:S3077" // Publish-once cache: assigned once under double-checked locking in resolveMustacheFactory(); the MustacheFactory is fully built before assignment, so volatile safe-publication is sufficient.
077   })
078   private volatile MustacheFactory defaultFactory;
079
080   /**
081    * Creates a new builder.
082    *
083    * @return A new builder.
084    */
085   public static Builder create() {
086      return new Builder();
087   }
088
089   /**
090    * Builder constructor.
091    *
092    * @param builder The builder. Must not be {@code null}.
093    */
094   protected MustacheDispatcher(Builder builder) {
095      basePath = builder.basePath;
096      templateSuffix = builder.templateSuffix;
097      cacheTemplates = builder.cacheTemplates;
098   }
099
100   /**
101    * Returns the base path under which template resources are resolved.
102    *
103    * @return The base path. Never {@code null}.
104    */
105   public String getBasePath() {
106      return basePath;
107   }
108
109   /**
110    * Returns the configured template-name suffix.
111    *
112    * @return The template suffix. Never {@code null}.
113    */
114   public String getTemplateSuffix() {
115      return templateSuffix;
116   }
117
118   /**
119    * Returns whether the bridge-default {@link MustacheFactory} caches compiled templates.
120    *
121    * @return The cache flag. Defaults to {@link #DEFAULT_CACHE_TEMPLATES}.
122    */
123   public boolean isCacheTemplates() {
124      return cacheTemplates;
125   }
126
127   /**
128    * Appends {@link #getTemplateSuffix()} to {@code name} if not already present (idempotent).
129    *
130    * @param name The template name. Must not be {@code null}.
131    * @return The template name with the configured suffix appended (if applicable).
132    */
133   public String applyTemplateSuffix(String name) {
134      if (isEmpty(templateSuffix))
135         return name;
136      if (name.endsWith(templateSuffix))
137         return name;
138      return name + templateSuffix;
139   }
140
141   /**
142    * Resolves the active {@link MustacheFactory}.
143    *
144    * <p>
145    * Lookup order:
146    * <ol class='spaced-list'>
147    *    <li>{@code req.getContext().getBeanStore().getBean(MustacheFactory.class)} &mdash; any
148    *       user-supplied bean (Spring {@code @Bean}, microservice {@code BasicBeanStore.put},
149    *       etc.). Never cached or rebuilt by this bridge &mdash; {@link #isCacheTemplates()} does
150    *       not apply to user-supplied factories.
151    *    <li>Bridge default &mdash; a {@link DefaultMustacheFactory} anchored on a resource root
152    *       derived from {@link #getBasePath() basePath} (leading + trailing slashes trimmed).
153    *       When {@link #isCacheTemplates()} is {@code true} (the default), a single instance is
154    *       lazily built once and reused (mustache.java's own {@code DefaultMustacheFactory}
155    *       compiles-and-caches templates internally, so reuse means "compile once"). When
156    *       {@code false}, a fresh factory with an empty compile-cache is built on every call, so
157    *       every render recompiles the template from its current classpath contents (hot-reload).
158    * </ol>
159    *
160    * @param req The current REST request.
161    * @return The active Mustache factory. Never {@code null}.
162    */
163   public MustacheFactory resolveMustacheFactory(RestRequest req) {
164      var bean = req.getContext().getBeanStore().getBean(MustacheFactory.class);
165      if (bean.isPresent())
166         return bean.get();
167      if (! cacheTemplates)
168         return buildDefaultFactory();
169      var local = defaultFactory;
170      if (local == null) {
171         synchronized (this) {
172            local = defaultFactory;
173            if (local == null) {
174               local = buildDefaultFactory();
175               defaultFactory = local;
176            }
177         }
178      }
179      return local;
180   }
181
182   /**
183    * Constructs the bridge-default {@link MustacheFactory}.
184    *
185    * <p>
186    * {@link DefaultMustacheFactory} anchored on a classpath resource root derived from
187    * {@link #getBasePath() basePath} (leading + trailing slashes trimmed). A {@code "/"} base
188    * yields a no-prefix factory. Subclasses may override to plug in custom resolvers / object
189    * handlers without registering a separate {@code @Bean MustacheFactory}.
190    *
191    * @return A new {@link MustacheFactory} instance.
192    */
193   protected MustacheFactory buildDefaultFactory() {
194      var root = toResourceRoot(basePath);
195      return root.isEmpty() ? new DefaultMustacheFactory() : new DefaultMustacheFactory(root);
196   }
197
198   /**
199    * Translates a virtual base path (e.g. {@code "/templates/"}) into a mustache.java resource
200    * root (e.g. {@code "templates"}).
201    *
202    * <p>
203    * mustache.java's {@link DefaultMustacheFactory#DefaultMustacheFactory(String)} expects a
204    * classpath-relative root without a leading slash and treats a trailing slash as part of the
205    * resolved path, so we trim both ends. A {@code null} / blank / {@code "/"} base yields an
206    * empty string &mdash; meaning "no prefix" (factory resolves names directly).
207    *
208    * @param base The virtual base path.
209    * @return The mustache.java resource root (never {@code null}).
210    */
211   static String toResourceRoot(String base) {
212      if (isBlank(base))
213         return "";
214      var s = base;
215      while (s.startsWith("/"))
216         s = s.substring(1);
217      while (s.endsWith("/"))
218         s = s.substring(0, s.length() - 1);
219      return s;
220   }
221
222   /**
223    * Renders a raw template through the Mustache engine onto the response.
224    *
225    * @param path The trailing path segment after the mount prefix (the template name relative to
226    *    the configured {@link #getBasePath() base path}; configured suffix appended if missing).
227    * @param req The current REST request.
228    * @param res The current REST response.
229    * @throws IOException If the underlying response writer fails.
230    * @throws BasicHttpException On boundary violation (403), missing engine (500), or render
231    *    failure (500).
232    */
233   @Override /* RawTemplateDispatcher */
234   public void render(String path, RestRequest req, RestResponse res)
235         throws IOException, BasicHttpException {
236
237      var template = (path == null) ? "" : path;
238
239      // resolveVirtualPathSafely validates that the resolved virtual path stays inside the
240      // configured basePath. The traversal check operates on basePath + template so a template
241      // name like "../../etc/passwd" or "a/b/../../../secret" is rejected before reaching the
242      // engine. We re-derive the factory-relative template name from the safe result by
243      // stripping the (already-normalized) basePath prefix.
244      String safeTemplate;
245      try {
246         var resolved = FileUtils.resolveVirtualPathSafely(basePath, template);
247         safeTemplate = stripBasePath(basePath, resolved);
248      } catch (IllegalArgumentException ex) {
249         throw new Forbidden("Path escapes configured base path.");
250      }
251
252      // Apply suffix idempotently so /mustache/about resolves to about.mustache (when
253      // templateSuffix is configured), while /mustache/about.mustache stays as-is.
254      safeTemplate = applyTemplateSuffix(safeTemplate);
255
256      try {
257         var factory = resolveMustacheFactory(req);
258         if (! res.containsHeader("Content-Type"))
259            res.setHeader("Content-Type", MustacheViewRenderer.DEFAULT_CONTENT_TYPE);
260         var mustache = factory.compile(safeTemplate);
261         mustache.execute(res.getWriter(), Map.of()).flush();
262      } catch (LinkageError ex) {
263         throw new InternalServerError(ex, MustacheViewRenderer.NO_ENGINE_DIAGNOSTIC);
264      } catch (IOException | BasicHttpException ex) {
265         throw ex;
266      } catch (Exception ex) {
267         throw new InternalServerError(ex, "Mustache render failed for '%s'", safeTemplate);
268      }
269   }
270
271   /**
272    * Strips the normalized {@code basePath} prefix from a {@code resolveVirtualPathSafely}
273    * result so the leftover string is factory-relative (the bridge's default factory re-adds
274    * the configured resource-root prefix).
275    *
276    * <p>
277    * Normalization mirrors {@code FileUtils.resolveVirtualPathSafely}: a {@code null} / empty
278    * base normalizes to {@code "/"}; otherwise the base is guaranteed to start with {@code "/"}
279    * and end with {@code "/"} once the helper has normalized it.
280    *
281    * @param base The configured base path (typically {@code "/templates/"}).
282    * @param resolved The output of {@code resolveVirtualPathSafely} (always starts with the
283    *    normalized base).
284    * @return The factory-relative template name (e.g. {@code "hello.mustache"} for base
285    *    {@code "/templates/"} and resolved {@code "/templates/hello.mustache"}).
286    */
287   static String stripBasePath(String base, String resolved) {
288      var bp = isEmpty(base) ? "/" : base;
289      if (! bp.endsWith("/"))
290         bp = bp + "/";
291      if (! bp.startsWith("/"))
292         bp = "/" + bp;
293      if (resolved.startsWith(bp))
294         return resolved.substring(bp.length());
295      throw iaex("Resolved path '%s' does not start with base '%s'", resolved, bp);
296   }
297
298   /**
299    * Builder for {@link MustacheDispatcher}.
300    */
301   public static class Builder {
302
303      String basePath = DEFAULT_BASE_PATH;
304      String templateSuffix = DEFAULT_TEMPLATE_SUFFIX;
305      boolean cacheTemplates = DEFAULT_CACHE_TEMPLATES;
306
307      /** Constructor &mdash; package access for {@link MustacheDispatcher#create()}. */
308      protected Builder() {}
309
310      /**
311       * Sets the classpath base path under which template resources are resolved.
312       *
313       * @param value The base path. {@code null} or blank values reset to the default
314       *    {@link MustacheDispatcher#DEFAULT_BASE_PATH}.
315       * @return This object.
316       */
317      public Builder basePath(String value) {
318         basePath = isBlank(value) ? DEFAULT_BASE_PATH : value;
319         return this;
320      }
321
322      /**
323       * Sets the suffix to append to template names that don't already end with it (idempotent).
324       *
325       * @param value The new suffix. {@code null} resets to
326       *    {@link MustacheDispatcher#DEFAULT_TEMPLATE_SUFFIX}.
327       * @return This object.
328       */
329      public Builder templateSuffix(String value) {
330         templateSuffix = (value == null) ? DEFAULT_TEMPLATE_SUFFIX : value;
331         return this;
332      }
333
334      /**
335       * Sets whether the bridge-default {@link MustacheFactory} caches compiled templates.
336       *
337       * <p>
338       * Defaults to {@link MustacheDispatcher#DEFAULT_CACHE_TEMPLATES true} (cache
339       * forever &mdash; production-safe). Set {@code false} to enable dev hot-reload (every
340       * render rebuilds the factory from a fresh, empty compile-cache).
341       *
342       * <p>
343       * This flag only affects the bridge-default factory; a user-supplied
344       * {@code @Bean MustacheFactory} is not modified.
345       *
346       * @param value The cache flag.
347       * @return This object.
348       */
349      public Builder cacheTemplates(boolean value) {
350         cacheTemplates = value;
351         return this;
352      }
353
354      /**
355       * Reads the current base path setting (test/inspection helper).
356       *
357       * @return The base path. Never {@code null}.
358       */
359      public String getBasePath() {
360         return basePath;
361      }
362
363      /**
364       * Reads the current template-suffix setting (test/inspection helper).
365       *
366       * @return The template suffix. Never {@code null}.
367       */
368      public String getTemplateSuffix() {
369         return templateSuffix;
370      }
371
372      /**
373       * Reads the current cache-templates setting (test/inspection helper).
374       *
375       * @return The cache flag.
376       */
377      public boolean isCacheTemplates() {
378         return cacheTemplates;
379      }
380
381      /**
382       * Builds the {@link MustacheDispatcher}.
383       *
384       * @return A new {@link MustacheDispatcher} instance.
385       */
386      public MustacheDispatcher build() {
387         if (basePath == null)
388            throw iaex("basePath must not be null");
389         return new MustacheDispatcher(this);
390      }
391   }
392}