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 — 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 — empty (literal template names, no implicit suffix). */ 063 public static final String DEFAULT_TEMPLATE_SUFFIX = ""; 064 065 /** Default cache-templates setting — {@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)} — any 148 * user-supplied bean (Spring {@code @Bean}, microservice {@code BasicBeanStore.put}, 149 * etc.). Never cached or rebuilt by this bridge — {@link #isCacheTemplates()} does 150 * not apply to user-supplied factories. 151 * <li>Bridge default — 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 — 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 — 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 — 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}