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.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 freemarker.core.*;
031import freemarker.template.*;
032
033/**
034 * Flavor-neutral worker bean carrying the FreeMarker raw-template-dispatch logic shared by the
035 * {@link FreemarkerMixin} mixin, the {@link FreemarkerServlet} servlet, and the
036 * {@link FreemarkerResource} child flavors.
037 *
038 * <p>
039 * This is the <i>flavor-neutral worker</i> for the FreeMarker capability: it is <b>not</b> one
040 * of the three {@code @Rest} flavor classes &mdash; it is a plain bean that implements
041 * {@link RawTemplateDispatcher} and holds the capability state ({@code basePath} /
042 * {@code templateSuffix} / {@code cacheTemplates} / lazy default configuration) plus the raw-serve
043 * and configuration-resolution logic. Each flavor independently holds an instance of this worker and
044 * delegates to it, so no flavor is another flavor's worker.
045 *
046 * <h5 class='section'>See Also:</h5><ul>
047 *    <li class='jc'>{@link FreemarkerMixin}
048 *    <li class='jc'>{@link FreemarkerServlet}
049 *    <li class='jc'>{@link FreemarkerResource}
050 *    <li class='jic'>{@link RawTemplateDispatcher}
051 * </ul>
052 *
053 * @since 10.0.0
054 */
055@SuppressWarnings({
056   "resource" // Closeables here are framework-managed and not owned/closed by this class; not a real leak.
057})
058public class FreemarkerDispatcher implements RawTemplateDispatcher {
059
060   /** Default base path applied when no {@link Builder#basePath(String)} call has been made. */
061   public static final String DEFAULT_BASE_PATH = "/";
062
063   /** Default template suffix &mdash; empty (literal template names, no implicit suffix). */
064   public static final String DEFAULT_TEMPLATE_SUFFIX = "";
065
066   /** Default template-cache flag &mdash; {@code true} (production-safe). */
067   public static final boolean DEFAULT_CACHE_TEMPLATES = true;
068
069   /** Default field-exposure flag &mdash; {@code true} (public-field DTOs render out of the box). */
070   public static final boolean DEFAULT_EXPOSE_FIELDS = true;
071
072   private final String basePath;
073   private final String templateSuffix;
074   private final boolean cacheTemplates;
075   private final boolean exposeFields;
076   private final ObjectWrapper objectWrapper;
077
078   // Lazy bridge-default configuration. Built on first call to resolveConfiguration(...) when
079   // no Configuration bean is registered in the request's BeanStore. Volatile so the
080   // double-checked-locking idiom is safe under concurrent first-request load.
081   @SuppressWarnings({
082      "java:S3077" // volatile is required here for correct double-checked-locking safe-publication of the lazily-built default Configuration in resolveConfiguration(); the reference is publish-once (fully constructed before assignment) and never compound-mutated.
083   })
084   private volatile Configuration defaultConfiguration;
085
086   /**
087    * Creates a new builder.
088    *
089    * @return A new builder.
090    */
091   public static Builder create() {
092      return new Builder();
093   }
094
095   /**
096    * Builder constructor.
097    *
098    * @param builder The builder. Must not be {@code null}.
099    */
100   protected FreemarkerDispatcher(Builder builder) {
101      basePath = builder.basePath;
102      templateSuffix = builder.templateSuffix;
103      cacheTemplates = builder.cacheTemplates;
104      exposeFields = builder.exposeFields;
105      objectWrapper = builder.objectWrapper;
106   }
107
108   /**
109    * Returns the base path under which template resources are resolved.
110    *
111    * @return The base path. Never {@code null}.
112    */
113   public String getBasePath() {
114      return basePath;
115   }
116
117   /**
118    * Returns the configured template-name suffix.
119    *
120    * @return The template suffix. Never {@code null}.
121    */
122   public String getTemplateSuffix() {
123      return templateSuffix;
124   }
125
126   /**
127    * Returns the template-cache flag.
128    *
129    * @return The cache flag.
130    */
131   public boolean isCacheTemplates() {
132      return cacheTemplates;
133   }
134
135   /**
136    * Returns the field-exposure flag applied to the bridge-default object wrapper.
137    *
138    * @return The field-exposure flag. Ignored once {@link #getObjectWrapper()} returns non-{@code null}.
139    */
140   public boolean isExposeFields() {
141      return exposeFields;
142   }
143
144   /**
145    * Returns the user-supplied {@link ObjectWrapper} override, if any.
146    *
147    * @return The object wrapper override, or {@code null} if the bridge builds its own default
148    *    (a {@link DefaultObjectWrapper} configured per {@link #isExposeFields() isExposeFields()}).
149    */
150   public ObjectWrapper getObjectWrapper() {
151      return objectWrapper;
152   }
153
154   /**
155    * Appends {@link #getTemplateSuffix()} to {@code name} if not already present (idempotent).
156    *
157    * @param name The template name. Must not be {@code null}.
158    * @return The template name with the configured suffix appended (if applicable).
159    */
160   public String applyTemplateSuffix(String name) {
161      if (isEmpty(templateSuffix))
162         return name;
163      if (name.endsWith(templateSuffix))
164         return name;
165      return name + templateSuffix;
166   }
167
168   /**
169    * Resolves the active {@link Configuration}.
170    *
171    * <p>
172    * Lookup order:
173    * <ol class='spaced-list'>
174    *    <li>{@code req.getContext().getBeanStore().getBean(Configuration.class)} &mdash; any
175    *       user-supplied bean (Spring {@code @Bean} including Spring Boot autoconfig, microservice
176    *       {@code BasicBeanStore.put}, etc.).
177    *    <li>Lazy bridge default &mdash; constructed on first call when no configuration bean is
178    *       registered. Anchored on a classpath resource root derived from
179    *       {@link #getBasePath() basePath}, with {@code IncompatibleImprovements} pinned to the
180    *       bridge-tested minor version.
181    * </ol>
182    *
183    * @param req The current REST request.
184    * @return The active FreeMarker configuration. Never {@code null}.
185    */
186   public Configuration resolveConfiguration(RestRequest req) {
187      var bean = req.getContext().getBeanStore().getBean(Configuration.class);
188      if (bean.isPresent())
189         return bean.get();
190      var local = defaultConfiguration;
191      if (local == null) {
192         synchronized (this) {
193            local = defaultConfiguration;
194            if (local == null) {
195               local = buildDefaultConfiguration();
196               defaultConfiguration = local;
197            }
198         }
199      }
200      return local;
201   }
202
203   /**
204    * Constructs the bridge-default {@link Configuration}.
205    *
206    * <p>
207    * Anchored on a classpath resource root derived from {@link #getBasePath() basePath} (leading +
208    * trailing slashes trimmed; a {@code "/"} base yields a root-of-classpath resolver). The default
209    * pins {@code IncompatibleImprovements} to {@link Configuration#VERSION_2_3_34} so behavior is
210    * stable across consumer upgrades of {@code org.freemarker:freemarker}; sets
211    * {@code DefaultEncoding} to {@code UTF-8} and {@code OutputFormat} to
212    * {@link HTMLOutputFormat#INSTANCE} so HTML escaping is the natural target; applies
213    * {@code TemplateUpdateDelayMilliseconds} per the configured
214    * {@link #isCacheTemplates() cacheTemplates} flag; and sets an {@code ObjectWrapper} &mdash;
215    * either the user-supplied {@link #getObjectWrapper() objectWrapper} override, or (by default)
216    * a {@link DefaultObjectWrapper} built with {@code exposeFields} set to
217    * {@link #isExposeFields() isExposeFields()}. Field exposure defaults to {@code true} (unlike
218    * FreeMarker's own version-default wrapper, which is getter-only): Juneau's own marshalling is
219    * comfortable with public-field beans elsewhere, so a view-model DTO written with public fields
220    * and no getters renders its field values here too, instead of silently resolving to
221    * {@code null}/missing.
222    *
223    * <p>
224    * Template exceptions are not logged by FreeMarker ({@code LogTemplateExceptions} is {@code false}):
225    * every caller rethrows them as an {@code InternalServerError} carrying the cause, so logging them
226    * as well would only duplicate the stack trace.
227    *
228    * <p>
229    * Subclasses may override to plug in custom loaders / encodings / output formats without
230    * registering a separate {@code @Bean Configuration}.
231    *
232    * @return A new {@link Configuration} instance.
233    */
234   protected Configuration buildDefaultConfiguration() {
235      var cfg = new Configuration(Configuration.VERSION_2_3_34);
236      cfg.setClassLoaderForTemplateLoading(FreemarkerDispatcher.class.getClassLoader(), toResourceRoot(basePath));
237      cfg.setDefaultEncoding("UTF-8");
238      cfg.setLogTemplateExceptions(false);
239      cfg.setOutputFormat(HTMLOutputFormat.INSTANCE);
240      cfg.setTemplateUpdateDelayMilliseconds(cacheTemplates ? Long.MAX_VALUE : 0L);
241      cfg.setObjectWrapper(objectWrapper != null ? objectWrapper : buildDefaultObjectWrapper());
242      return cfg;
243   }
244
245   /**
246    * Builds the bridge-default {@link ObjectWrapper} &mdash; a {@link DefaultObjectWrapper} with
247    * {@code exposeFields} set per {@link #isExposeFields() isExposeFields()}. Only called when no
248    * {@link #getObjectWrapper() objectWrapper} override has been supplied.
249    *
250    * @return A new {@link DefaultObjectWrapper} instance.
251    */
252   private ObjectWrapper buildDefaultObjectWrapper() {
253      var b = new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_34);
254      b.setExposeFields(exposeFields);
255      return b.build();
256   }
257
258   /**
259    * Translates a virtual base path (e.g. {@code "/templates/"}) into a FreeMarker
260    * {@code ClassLoaderTemplateLoader} resource root (e.g. {@code "/templates"}).
261    *
262    * <p>
263    * FreeMarker's
264    * {@link Configuration#setClassLoaderForTemplateLoading(ClassLoader, String)
265    * setClassLoaderForTemplateLoading(ClassLoader, String basePackagePath)} expects a
266    * classpath-relative root with a leading {@code "/"}. We trim any trailing slash and ensure a
267    * single leading slash; a {@code null} / blank / {@code "/"} base yields {@code "/"} (root of
268    * classpath).
269    *
270    * @param base The virtual base path.
271    * @return The FreeMarker classloader resource root (never {@code null}; always starts with
272    *    {@code "/"}).
273    */
274   static String toResourceRoot(String base) {
275      if (isBlank(base))
276         return "/";
277      var s = base;
278      while (s.endsWith("/") && s.length() > 1)
279         s = s.substring(0, s.length() - 1);
280      if (! s.startsWith("/"))
281         s = "/" + s;
282      return s;
283   }
284
285   /**
286    * Renders a raw template through the FreeMarker engine onto the response.
287    *
288    * @param path The trailing path segment after the mount prefix (the template name relative to
289    *    the configured {@link #getBasePath() base path}; configured suffix appended if missing).
290    * @param req The current REST request.
291    * @param res The current REST response.
292    * @throws IOException If the underlying response writer fails.
293    * @throws BasicHttpException On boundary violation (403), missing engine (500), or render
294    *    failure (500).
295    */
296   @Override /* RawTemplateDispatcher */
297   public void render(String path, RestRequest req, RestResponse res)
298         throws IOException, BasicHttpException {
299
300      var template = (path == null) ? "" : path;
301
302      // resolveVirtualPathSafely validates that the resolved virtual path stays inside the
303      // configured basePath. The traversal check operates on basePath + template so a template
304      // name like "../../etc/passwd" or "a/b/../../../secret" is rejected before reaching the
305      // engine. We re-derive the configuration-relative template name from the safe result by
306      // stripping the (already-normalized) basePath prefix.
307      String safeTemplate;
308      try {
309         var resolved = FileUtils.resolveVirtualPathSafely(basePath, template);
310         safeTemplate = stripBasePath(basePath, resolved);
311      } catch (IllegalArgumentException ex) {
312         throw new Forbidden("Path escapes configured base path.");
313      }
314
315      // Apply suffix idempotently so /freemarker/about resolves to about.ftlh (when
316      // templateSuffix is configured), while /freemarker/about.ftlh stays as-is.
317      safeTemplate = applyTemplateSuffix(safeTemplate);
318
319      try {
320         var cfg = resolveConfiguration(req);
321         if (! res.containsHeader("Content-Type"))
322            res.setHeader("Content-Type", FreemarkerViewRenderer.DEFAULT_CONTENT_TYPE);
323         var tpl = cfg.getTemplate(safeTemplate);
324         tpl.process(Map.of(), res.getWriter());
325         res.getWriter().flush();
326      } catch (LinkageError ex) {
327         throw new InternalServerError(ex, FreemarkerViewRenderer.NO_ENGINE_DIAGNOSTIC);
328      } catch (IOException | BasicHttpException ex) {
329         throw ex;
330      } catch (TemplateException | RuntimeException ex) {
331         throw new InternalServerError(ex, "FreeMarker render failed for '%s'", safeTemplate);
332      }
333   }
334
335   /**
336    * Strips the normalized {@code basePath} prefix from a {@code resolveVirtualPathSafely}
337    * result so the leftover string is configuration-relative (the bridge's default configuration
338    * re-adds the configured resource-root prefix via its template loader).
339    *
340    * <p>
341    * Normalization mirrors {@code FileUtils.resolveVirtualPathSafely}: a {@code null} / empty
342    * base normalizes to {@code "/"}; otherwise the base is guaranteed to start with {@code "/"}
343    * and end with {@code "/"} once the helper has normalized it.
344    *
345    * @param base The configured base path (typically {@code "/templates/"}).
346    * @param resolved The output of {@code resolveVirtualPathSafely} (always starts with the
347    *    normalized base).
348    * @return The configuration-relative template name (e.g. {@code "hello.ftlh"} for base
349    *    {@code "/templates/"} and resolved {@code "/templates/hello.ftlh"}).
350    */
351   static String stripBasePath(String base, String resolved) {
352      var bp = isEmpty(base) ? "/" : base;
353      if (! bp.endsWith("/"))
354         bp = bp + "/";
355      if (! bp.startsWith("/"))
356         bp = "/" + bp;
357      if (resolved.startsWith(bp))
358         return resolved.substring(bp.length());
359      throw iaex("Resolved path '%s' does not start with base '%s'", resolved, bp);
360   }
361
362   /**
363    * Builder for {@link FreemarkerDispatcher}.
364    */
365   public static class Builder {
366
367      String basePath = DEFAULT_BASE_PATH;
368      String templateSuffix = DEFAULT_TEMPLATE_SUFFIX;
369      boolean cacheTemplates = DEFAULT_CACHE_TEMPLATES;
370      boolean exposeFields = DEFAULT_EXPOSE_FIELDS;
371      ObjectWrapper objectWrapper;
372
373      /** Constructor &mdash; package access for {@link FreemarkerDispatcher#create()}. */
374      protected Builder() {}
375
376      /**
377       * Sets the classpath base path under which template resources are resolved.
378       *
379       * @param value The base path. {@code null} or blank values reset to the default
380       *    {@link FreemarkerDispatcher#DEFAULT_BASE_PATH}.
381       * @return This object.
382       */
383      public Builder basePath(String value) {
384         basePath = isBlank(value) ? DEFAULT_BASE_PATH : value;
385         return this;
386      }
387
388      /**
389       * Sets the suffix to append to template names that don't already end with it
390       * (idempotent).
391       *
392       * @param value The new suffix. {@code null} resets to
393       *    {@link FreemarkerDispatcher#DEFAULT_TEMPLATE_SUFFIX}.
394       * @return This object.
395       */
396      public Builder templateSuffix(String value) {
397         templateSuffix = (value == null) ? DEFAULT_TEMPLATE_SUFFIX : value;
398         return this;
399      }
400
401      /**
402       * Sets the template-cache flag for the bridge-default configuration.
403       *
404       * @param value The cache flag.
405       * @return This object.
406       */
407      public Builder cacheTemplates(boolean value) {
408         cacheTemplates = value;
409         return this;
410      }
411
412      /**
413       * Sets whether the bridge-default {@code ObjectWrapper} exposes public fields (not just
414       * JavaBean getters) to templates.
415       *
416       * <p>
417       * Defaults to {@link FreemarkerDispatcher#DEFAULT_EXPOSE_FIELDS true} so simple view-model
418       * DTOs written with public fields and no getters render their field values instead of
419       * silently resolving to {@code null}/missing. Has no effect once {@link #objectWrapper}
420       * has been called with a non-{@code null} value &mdash; the explicit wrapper always wins.
421       *
422       * @param value The field-exposure flag.
423       * @return This object.
424       */
425      public Builder exposeFields(boolean value) {
426         exposeFields = value;
427         return this;
428      }
429
430      /**
431       * Sets a fully custom {@code ObjectWrapper} for the bridge-default configuration,
432       * overriding {@link #exposeFields(boolean)} entirely.
433       *
434       * <p>
435       * Escape hatch for consumers who need full control (e.g. a {@code BeansWrapper} with
436       * custom method/property exposure, or a third-party wrapper) without hand-rolling and
437       * registering a whole replacement {@code Configuration} bean.
438       *
439       * @param value The object wrapper. {@code null} reverts to the bridge-built
440       *    {@link DefaultObjectWrapper} (per {@link #exposeFields(boolean)}).
441       * @return This object.
442       */
443      public Builder objectWrapper(ObjectWrapper value) {
444         objectWrapper = value;
445         return this;
446      }
447
448      /**
449       * Reads the current base path setting (test/inspection helper).
450       *
451       * @return The base path. Never {@code null}.
452       */
453      public String getBasePath() {
454         return basePath;
455      }
456
457      /**
458       * Reads the current template-suffix setting (test/inspection helper).
459       *
460       * @return The template suffix. Never {@code null}.
461       */
462      public String getTemplateSuffix() {
463         return templateSuffix;
464      }
465
466      /**
467       * Reads the current cache-templates setting (test/inspection helper).
468       *
469       * @return The cache flag.
470       */
471      public boolean isCacheTemplates() {
472         return cacheTemplates;
473      }
474
475      /**
476       * Reads the current field-exposure setting (test/inspection helper).
477       *
478       * @return The field-exposure flag.
479       */
480      public boolean isExposeFields() {
481         return exposeFields;
482      }
483
484      /**
485       * Reads the current object-wrapper override (test/inspection helper).
486       *
487       * @return The object wrapper override, or {@code null} if none has been set.
488       */
489      public ObjectWrapper getObjectWrapper() {
490         return objectWrapper;
491      }
492
493      /**
494       * Builds the {@link FreemarkerDispatcher}.
495       *
496       * @return A new {@link FreemarkerDispatcher} instance.
497       */
498      public FreemarkerDispatcher build() {
499         if (basePath == null)
500            throw iaex("basePath must not be null");
501         return new FreemarkerDispatcher(this);
502      }
503   }
504}