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.http.*; 022import org.apache.juneau.http.response.*; 023import org.apache.juneau.rest.server.*; 024import org.apache.juneau.rest.server.view.*; 025 026import com.github.mustachejava.*; 027 028/** 029 * Mixin that wires Mustache view-rendering onto any Juneau REST resource. 030 * 031 * <p> 032 * Compose into a host resource via 033 * {@link Rest#mixins() @Rest(mixins=MustacheMixin.class)}; the host then: 034 * 035 * <ol class='spaced-list'> 036 * <li>Gains a default mount at {@code /mustache/*} that serves raw Mustache templates from the 037 * importer's classpath by asking the configured {@link MustacheFactory} to compile and 038 * render them with no scope (raw render path; callers who want attributes use 039 * {@link MustacheView} from a typed handler instead). 040 * <li>Routes the host's own {@code @RestOp}-method return values of type {@link MustacheView} through the 041 * Mustache engine automatically. {@code MustacheMixin} declares 042 * {@link Rest#mergeResponseProcessorsIntoHost() @Rest(mergeResponseProcessorsIntoHost=true)}, so a plain 043 * {@code @Rest(mixins=MustacheMixin.class)} folds its {@link MustacheViewRenderer} into the host's own 044 * response-processor chain — no {@code mergeIntoHost} or explicit {@code responseProcessors=} on the 045 * host needed. 046 * </ol> 047 * 048 * <h5 class='figure'>Composition example (microservice):</h5> 049 * 050 * <p class='bjava'> 051 * <ja>@Rest</ja>(path=<js>"/app"</js>, mixins=MustacheMixin.<jk>class</jk>) 052 * <jk>public class</jk> AppResource <jk>extends</jk> RestServlet { 053 * 054 * <ja>@Bean</ja> MustacheMixin mustache() { 055 * <jk>return</jk> MustacheMixin.<jsm>create</jsm>() 056 * .basePath(<js>"/templates/"</js>) 057 * .templateSuffix(<js>".mustache"</js>) 058 * .build(); 059 * } 060 * 061 * <ja>@RestGet</ja>(<js>"/hello/{name}"</js>) 062 * <jk>public</jk> View hello(<ja>@Path</ja> String <jv>name</jv>) { 063 * <jk>return</jk> MustacheView.<jsm>of</jsm>(<js>"hello"</js>).attr(<js>"name"</js>, <jv>name</jv>); 064 * } 065 * } 066 * </p> 067 * 068 * <h5 class='section'>Configurable mount path:</h5> 069 * 070 * <p> 071 * The default mount {@code /mustache/*} can be overridden via the SVL variable 072 * {@code ${juneau.mustache.path:mustache}} — set via system property 073 * ({@code -Djuneau.mustache.path=views}), environment variable 074 * ({@code JUNEAU_MUSTACHE_PATH=views}), or {@code Config} key 075 * ({@code juneau.mustache.path = views}) to change the runtime mount without subclassing. 076 * Resolution happens once at {@link RestContext} construction time (SVL resolution in {@code @RestOp(path)}). 077 * 078 * <h5 class='section'>Mixin-only deployment:</h5> 079 * 080 * <p> 081 * This resource is designed for composition via {@code @Rest(mixins=...)}. The mount path is 082 * pinned at the op level by 083 * {@link RestGet @RestGet(path="/{juneau.mustache.path:mustache}/*")} on {@link #render}; 084 * a class-level {@code @Rest(paths=...)} declaration would be silently ignored under the mixin 085 * pattern (see {@link Rest#paths() @Rest(paths)} Javadoc). 086 * 087 * <h5 class='section'>Choosing a MustacheFactory:</h5> 088 * 089 * <p> 090 * The {@code juneau-rest-server-view-mustache} module ships <b>only</b> the mustache.java 091 * compiler API in {@code provided} scope. <b>No factory bean is bundled.</b> The bridge resolves 092 * a {@link MustacheFactory} at first use from three sources, in order: 093 * 094 * <ul class='spaced-list'> 095 * <li><b>User-supplied bean:</b> register a {@code @Bean MustacheFactory} (Spring) or 096 * {@code BasicBeanStore.put(MustacheFactory.class, factory)} (microservice). Allows full 097 * control of resolvers, object handlers, and encoders. 098 * <li><b>Bridge default:</b> when nothing is registered, the bridge constructs a default 099 * {@link DefaultMustacheFactory} anchored on a resource root derived from 100 * {@link #getBasePath() basePath} (leading + trailing slashes trimmed; a {@code "/"} base 101 * yields a no-prefix factory). With {@code basePath("/templates/")}, the factory resolves 102 * template names like {@code "hello.mustache"} as classpath resource 103 * {@code templates/hello.mustache}. 104 * </ul> 105 * 106 * <p> 107 * mustache.java's {@code DefaultMustacheFactory} caches compiled templates internally. The 108 * bridge-default factory honors {@link Builder#cacheTemplates(boolean) cacheTemplates(...)} 109 * (default {@code true} — production-safe): set {@code false} to have the bridge rebuild 110 * the factory from a fresh, empty compile-cache on every render (dev hot-reload). Consumers who 111 * supply their own {@code @Bean MustacheFactory} manage its caching themselves — the flag 112 * only affects the bridge-default factory. 113 * 114 * <p> 115 * When no Mustache engine is on the classpath, the renderer surfaces 116 * {@link MustacheViewRenderer#NO_ENGINE_DIAGNOSTIC} naming the missing dependency. 117 * 118 * <h5 class='section'>Template suffix:</h5> 119 * 120 * <p> 121 * Unlike the Thymeleaf bridge (which always appends {@code .html} via the engine resolver's 122 * suffix setting), mustache.java has no resolver-suffix concept — the literal template name 123 * is what {@link MustacheFactory#compile(String) factory.compile(...)} sees. The 124 * {@link Builder#templateSuffix(String) templateSuffix(...)} builder knob fills the gap: when 125 * non-blank, the bridge appends the configured suffix to template names that don't already end 126 * with it (idempotent), so callers can write {@code MustacheView.of("hello")} or request 127 * {@code /mustache/hello} and have it resolve to {@code hello.mustache} on the classpath. 128 * 129 * <h5 class='section'>Multiple base paths:</h5> 130 * 131 * <p> 132 * A host with {@code /templates/} and {@code /admin/templates/} template trees registers two 133 * {@code MustacheMixin} beans, each mounted via a subclass declaring its own op-level 134 * {@code @RestGet(path=...)} override and its own {@code basePath}. See the 135 * {@code MustacheMixin_MockRest_Test} test in {@code juneau-integration-tests} for the canonical 136 * pattern. 137 * 138 * <h5 class='section'>OpenAPI surface:</h5> 139 * 140 * <p> 141 * The greedy {@code /*} handler is not API-meaningful and is excluded from generated 142 * Swagger / OpenAPI specs via {@link OpSwagger#ignore() @OpSwagger(ignore=true)}. 143 * 144 * <h5 class='section'>See Also:</h5><ul> 145 * <li class='jc'>{@link MustacheView} 146 * <li class='jc'>{@link MustacheViewRenderer} 147 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/MustacheViewSupport">Mustache View Support</a> 148 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/RestServerComposition">REST Server — Mixins and Multi-Mount Paths</a> 149 * </ul> 150 * 151 * @since 10.0.0 152 */ 153// @formatter:off 154@Rest( 155 responseProcessors={MustacheViewRenderer.class}, 156 mergeResponseProcessorsIntoHost=true 157) 158public class MustacheMixin { 159 160 /** Default base path applied when no {@link Builder#basePath(String)} call has been made. */ 161 public static final String DEFAULT_BASE_PATH = MustacheDispatcher.DEFAULT_BASE_PATH; 162 163 /** Default template suffix — empty (literal template names, no implicit suffix). */ 164 public static final String DEFAULT_TEMPLATE_SUFFIX = MustacheDispatcher.DEFAULT_TEMPLATE_SUFFIX; 165 166 /** Default cache-templates setting — {@code true} (cache forever; production-safe). */ 167 public static final boolean DEFAULT_CACHE_TEMPLATES = MustacheDispatcher.DEFAULT_CACHE_TEMPLATES; 168 169 private final MustacheDispatcher worker; 170 171 /** 172 * Creates a new builder. 173 * 174 * @return A new builder. 175 */ 176 public static Builder create() { 177 return new Builder(); 178 } 179 180 /** 181 * No-arg constructor — uses {@link #DEFAULT_BASE_PATH} as the base path and 182 * {@link #DEFAULT_TEMPLATE_SUFFIX} for the template suffix. 183 * 184 * <p> 185 * The mixin walk falls back to this constructor when the 186 * {@link org.apache.juneau.commons.inject.BeanStore BeanStore} does not have a registered 187 * {@code MustacheMixin} bean. 188 */ 189 public MustacheMixin() { 190 this(create()); 191 } 192 193 /** 194 * Builder constructor. 195 * 196 * @param builder The builder. Must not be {@code null}. 197 */ 198 protected MustacheMixin(Builder builder) { 199 worker = builder.worker.build(); 200 } 201 202 /** 203 * Returns the base path under which template resources are resolved. 204 * 205 * @return The base path. Never {@code null}. 206 */ 207 public String getBasePath() { 208 return worker.getBasePath(); 209 } 210 211 /** 212 * Returns the configured template-name suffix. 213 * 214 * @return The template suffix. Never {@code null}. 215 */ 216 public String getTemplateSuffix() { 217 return worker.getTemplateSuffix(); 218 } 219 220 /** 221 * Returns whether the bridge's default factory caches compiled templates. 222 * 223 * @return The cache flag. Defaults to {@link #DEFAULT_CACHE_TEMPLATES}. 224 */ 225 public boolean isCacheTemplates() { 226 return worker.isCacheTemplates(); 227 } 228 229 /** 230 * Appends {@link #getTemplateSuffix()} to {@code name} if not already present (idempotent). 231 * 232 * @param name The template name. Must not be {@code null}. 233 * @return The template name with the configured suffix appended (if applicable). 234 */ 235 public String applyTemplateSuffix(String name) { 236 return worker.applyTemplateSuffix(name); 237 } 238 239 /** 240 * Resolves the active {@link MustacheFactory} via the shared {@link MustacheDispatcher} worker. 241 * 242 * @param req The current REST request. 243 * @return The active Mustache factory. Never {@code null}. 244 */ 245 public MustacheFactory resolveMustacheFactory(RestRequest req) { 246 return worker.resolveMustacheFactory(req); 247 } 248 249 /** 250 * [GET /mustache/*] — render a raw template through the Mustache engine. 251 * 252 * <p> 253 * The {@code @Path("/*") String path} captures the multi-segment trailing remainder (e.g. a 254 * request for {@code /mustache/about.mustache} matches the mount with 255 * {@code path = "about.mustache"}). Delegates to the shared {@link MustacheDispatcher} worker 256 * (path-traversal hardening, suffix application, factory resolution, and render). 257 * 258 * <p> 259 * Missing template surfaces as the engine's own {@link MustacheNotFoundException} 260 * (HTTP 500 from the renderer's catch-all); missing engine surfaces as 261 * {@link MustacheViewRenderer#NO_ENGINE_DIAGNOSTIC}. 262 * 263 * @param path The trailing path segment after {@code /mustache/} (the template name relative 264 * to the configured {@link #getBasePath() base path}; configured suffix appended if 265 * missing). 266 * @param req The current REST request. 267 * @param res The current REST response. 268 * @throws IOException If the underlying response writer fails. 269 * @throws BasicHttpException On boundary violation (403), missing engine (500), or render 270 * failure (500). 271 */ 272 @RestGet( 273 path="/${juneau.mustache.path:mustache}/*", 274 summary="Mustache view", 275 description="Render a raw Mustache template under the configured base path.", 276 swagger=@OpSwagger(ignore=true) 277 ) 278 public void render(@Path("/*") String path, RestRequest req, RestResponse res) 279 throws IOException, BasicHttpException { 280 worker.render(path, req, res); 281 } 282 283 /** 284 * Builder for {@link MustacheMixin}. 285 * 286 * <p> 287 * Mirrors {@link MustacheDispatcher.Builder}'s configuration methods on its own surface and 288 * forwards each call into a held {@link MustacheDispatcher.Builder} (worker-bean 289 * composition). Implements {@link ViewMixinBuilder} so {@link #basePath(String)} and 290 * {@link #cacheTemplates(boolean)} are guaranteed to transfer to the sibling JSP / Thymeleaf / 291 * FreeMarker bridge builders. 292 */ 293 public static class Builder implements ViewMixinBuilder<Builder> { 294 295 private final MustacheDispatcher.Builder worker = MustacheDispatcher.create(); 296 297 /** Constructor — package access for {@link MustacheMixin#create()}. */ 298 protected Builder() {} 299 300 /** 301 * Sets the classpath base path under which template resources are resolved. 302 * 303 * <p> 304 * Defaults to {@link MustacheMixin#DEFAULT_BASE_PATH "/"}. A typical configured 305 * value is {@code "/templates/"} — the same Spring-Boot-compatible layout 306 * convention the sibling JSP / Thymeleaf bridges use. 307 * 308 * @param value The base path. {@code null} or blank values reset to the default 309 * {@link MustacheMixin#DEFAULT_BASE_PATH}. 310 * @return This object. 311 */ 312 @Override /* ViewMixinBuilder */ 313 public Builder basePath(String value) { 314 worker.basePath(value); 315 return this; 316 } 317 318 /** 319 * Sets the suffix to append to template names that don't already end with it (idempotent). 320 * 321 * <p> 322 * Defaults to {@link MustacheMixin#DEFAULT_TEMPLATE_SUFFIX ""} (no implicit 323 * suffix — literal template names). Typical configured value is 324 * {@code ".mustache"}, the mustache.java convention. 325 * 326 * @param value The new suffix. {@code null} resets to 327 * {@link MustacheMixin#DEFAULT_TEMPLATE_SUFFIX}. 328 * @return This object. 329 */ 330 public Builder templateSuffix(String value) { 331 worker.templateSuffix(value); 332 return this; 333 } 334 335 /** 336 * Sets whether the bridge's default factory caches compiled templates. 337 * 338 * <p> 339 * Defaults to {@link MustacheMixin#DEFAULT_CACHE_TEMPLATES true} (cache forever 340 * — production-safe). Set {@code false} to enable dev hot-reload (the bridge rebuilds 341 * the factory from a fresh, empty compile-cache on every render). This setting only 342 * applies to the bridge's fallback factory; if a {@link MustacheFactory} bean is 343 * registered, its own caching behavior is honored. 344 * 345 * @param value The new value. 346 * @return This object. 347 */ 348 @Override /* ViewMixinBuilder */ 349 public Builder cacheTemplates(boolean value) { 350 worker.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 worker.getBasePath(); 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 worker.getTemplateSuffix(); 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 worker.isCacheTemplates(); 379 } 380 381 /** 382 * Builds the {@link MustacheMixin}. 383 * 384 * @return A new {@link MustacheMixin} instance. 385 */ 386 public MustacheMixin build() { 387 return new MustacheMixin(this); 388 } 389 } 390}