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 — 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 — empty (literal template names, no implicit suffix). */ 064 public static final String DEFAULT_TEMPLATE_SUFFIX = ""; 065 066 /** Default template-cache flag — {@code true} (production-safe). */ 067 public static final boolean DEFAULT_CACHE_TEMPLATES = true; 068 069 /** Default field-exposure flag — {@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)} — any 175 * user-supplied bean (Spring {@code @Bean} including Spring Boot autoconfig, microservice 176 * {@code BasicBeanStore.put}, etc.). 177 * <li>Lazy bridge default — 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} — 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} — 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 — 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 — 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}