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.thymeleaf; 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.*; 025import org.thymeleaf.*; 026import org.thymeleaf.templatemode.*; 027 028/** 029 * Mixin that wires Thymeleaf view-rendering onto any Juneau REST resource. 030 * 031 * <p> 032 * Compose into a host resource via 033 * {@link Rest#mixins() @Rest(mixins=ThymeleafMixin.class)}; the host then: 034 * 035 * <ol class='spaced-list'> 036 * <li>Gains a default mount at {@code /thymeleaf/*} that serves raw {@code .html} templates from 037 * the importer's classpath by asking the configured 038 * {@link org.thymeleaf.TemplateEngine TemplateEngine} to render them with the current 039 * request's locale and attributes available to the template. 040 * <li>Routes the host's own {@code @RestOp}-method return values of type {@link ThymeleafView} through the 041 * Thymeleaf engine automatically. {@code ThymeleafMixin} declares 042 * {@link Rest#mergeResponseProcessorsIntoHost() @Rest(mergeResponseProcessorsIntoHost=true)}, so a plain 043 * {@code @Rest(mixins=ThymeleafMixin.class)} folds its {@link ThymeleafViewRenderer} 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=ThymeleafMixin.<jk>class</jk>) 052 * <jk>public class</jk> AppResource <jk>extends</jk> RestServlet { 053 * 054 * <ja>@Bean</ja> ThymeleafMixin thymeleaf() { 055 * <jk>return</jk> ThymeleafMixin.<jsm>create</jsm>() 056 * .basePath(<js>"/templates/"</js>) 057 * .build(); 058 * } 059 * 060 * <ja>@RestGet</ja>(<js>"/hello/{name}"</js>) 061 * <jk>public</jk> View hello(<ja>@Path</ja> String <jv>name</jv>) { 062 * <jk>return</jk> ThymeleafView.<jsm>of</jsm>(<js>"hello"</js>).attr(<js>"name"</js>, <jv>name</jv>); 063 * } 064 * } 065 * </p> 066 * 067 * <h5 class='section'>Configurable mount path:</h5> 068 * 069 * <p> 070 * The default mount {@code /thymeleaf/*} can be overridden via the SVL variable 071 * {@code ${juneau.thymeleaf.path:thymeleaf}} — set via system property 072 * ({@code -Djuneau.thymeleaf.path=views}), environment variable 073 * ({@code JUNEAU_THYMELEAF_PATH=views}), or {@code Config} key 074 * ({@code juneau.thymeleaf.path = views}) to change the runtime mount without subclassing. 075 * Resolution happens once at {@link RestContext} construction time (SVL resolution in {@code @RestOp(path)}). 076 * 077 * <h5 class='section'>Mixin-only deployment:</h5> 078 * 079 * <p> 080 * This resource is designed for composition via {@code @Rest(mixins=...)}. The mount path is 081 * pinned at the op level by 082 * {@link RestGet @RestGet(path="/{juneau.thymeleaf.path:thymeleaf}/*")} on {@link #render}; 083 * a class-level {@code @Rest(paths=...)} declaration would be silently ignored under the mixin 084 * pattern (see {@link Rest#paths() @Rest(paths)} Javadoc). 085 * 086 * <h5 class='section'>Choosing a TemplateEngine:</h5> 087 * 088 * <p> 089 * The {@code juneau-rest-server-view-thymeleaf} module ships <b>only</b> the Thymeleaf core API in 090 * {@code provided} scope. <b>No engine bean is bundled.</b> The bridge resolves a 091 * {@link org.thymeleaf.TemplateEngine TemplateEngine} at first use from three sources, in order: 092 * 093 * <ul class='spaced-list'> 094 * <li><b>Spring Boot autoconfig:</b> {@code spring-boot-starter-thymeleaf} contributes an 095 * autoconfigured {@code SpringTemplateEngine} (which extends {@code TemplateEngine}); the 096 * bridge picks it up via {@code BeanStore.getBean(TemplateEngine.class)}. No further wiring 097 * needed. 098 * <li><b>User-supplied bean:</b> register a {@code @Bean TemplateEngine} (Spring) or 099 * {@code BasicBeanStore.put(TemplateEngine.class, engine)} (microservice). Allows full 100 * control of resolvers, dialects, and template modes. 101 * <li><b>Bridge default:</b> when nothing is registered, the bridge constructs a default 102 * {@link org.thymeleaf.TemplateEngine TemplateEngine} with a single 103 * {@link org.thymeleaf.templateresolver.ClassLoaderTemplateResolver} configured with {@code prefix=basePath}, 104 * {@code suffix=".html"}, {@code templateMode=HTML}, and {@code cacheable=cacheTemplates} 105 * (default {@code true} — production-safe; set {@code cacheTemplates(false)} on the 106 * builder for hot-reload during development). 107 * </ul> 108 * 109 * <p> 110 * When no Thymeleaf engine is on the classpath, the renderer surfaces 111 * {@link ThymeleafViewRenderer#NO_ENGINE_DIAGNOSTIC} naming the missing dependency. 112 * 113 * <h5 class='section'>Multiple base paths:</h5> 114 * 115 * <p> 116 * A host with {@code /templates/} and {@code /admin/templates/} template trees registers two 117 * {@code ThymeleafMixin} beans, each mounted via a subclass declaring its own op-level 118 * {@code @RestGet(path=...)} override and its own {@code basePath}. See the 119 * {@code ThymeleafMixin_MockRest_Test} test in {@code juneau-integration-tests} for the canonical 120 * pattern. 121 * 122 * <h5 class='section'>OpenAPI surface:</h5> 123 * 124 * <p> 125 * The greedy {@code /*} handler is not API-meaningful and is excluded from generated 126 * Swagger / OpenAPI specs via {@link OpSwagger#ignore() @OpSwagger(ignore=true)}. 127 * 128 * <h5 class='section'>See Also:</h5><ul> 129 * <li class='jc'>{@link ThymeleafView} 130 * <li class='jc'>{@link ThymeleafViewRenderer} 131 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/ThymeleafViewSupport">Thymeleaf View Support</a> 132 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/RestServerComposition">REST Server — Mixins and Multi-Mount Paths</a> 133 * </ul> 134 * 135 * @since 10.0.0 136 */ 137// @formatter:off 138@Rest( 139 responseProcessors={ThymeleafViewRenderer.class}, 140 mergeResponseProcessorsIntoHost=true 141) 142@SuppressWarnings({ 143 "java:S1192" // Duplicate string literals are Thymeleaf MIME type strings and template attribute keys; intentional 144}) 145public class ThymeleafMixin { 146 147 /** Default base path applied when no {@link Builder#basePath(String)} call has been made. */ 148 public static final String DEFAULT_BASE_PATH = ThymeleafDispatcher.DEFAULT_BASE_PATH; 149 150 /** Default template-cache flag — production-safe; opt out per-builder for dev. */ 151 public static final boolean DEFAULT_CACHE_TEMPLATES = ThymeleafDispatcher.DEFAULT_CACHE_TEMPLATES; 152 153 /** Default template mode for the bridge's fallback engine. */ 154 public static final TemplateMode DEFAULT_TEMPLATE_MODE = ThymeleafDispatcher.DEFAULT_TEMPLATE_MODE; 155 156 private final ThymeleafDispatcher worker; 157 158 /** 159 * Creates a new builder. 160 * 161 * @return A new builder. 162 */ 163 public static Builder create() { 164 return new Builder(); 165 } 166 167 /** 168 * No-arg constructor — uses {@link #DEFAULT_BASE_PATH} as the base path, 169 * {@link #DEFAULT_CACHE_TEMPLATES} for the cache flag, and {@link #DEFAULT_TEMPLATE_MODE} for 170 * the template mode. 171 * 172 * <p> 173 * The mixin walk falls back to this constructor when the {@link org.apache.juneau.commons.inject.BeanStore 174 * BeanStore} does not have a registered {@code ThymeleafMixin} bean. 175 */ 176 public ThymeleafMixin() { 177 this(create()); 178 } 179 180 /** 181 * Builder constructor. 182 * 183 * @param builder The builder. Must not be {@code null}. 184 */ 185 protected ThymeleafMixin(Builder builder) { 186 worker = builder.worker.build(); 187 } 188 189 /** 190 * Returns the base path under which template resources are resolved. 191 * 192 * @return The base path. Never {@code null}. 193 */ 194 public String getBasePath() { 195 return worker.getBasePath(); 196 } 197 198 /** 199 * Returns whether the bridge's default engine caches resolved templates. 200 * 201 * @return The cache flag. Defaults to {@link #DEFAULT_CACHE_TEMPLATES}. 202 */ 203 public boolean isCacheTemplates() { 204 return worker.isCacheTemplates(); 205 } 206 207 /** 208 * Returns the template mode used by the bridge's default engine. 209 * 210 * @return The template mode. Defaults to {@link #DEFAULT_TEMPLATE_MODE}. 211 */ 212 public TemplateMode getTemplateMode() { 213 return worker.getTemplateMode(); 214 } 215 216 /** 217 * Resolves the active {@link TemplateEngine} via the shared {@link ThymeleafDispatcher} worker. 218 * 219 * @param req The current REST request. 220 * @return The active template engine. Never {@code null}. 221 */ 222 public TemplateEngine resolveTemplateEngine(RestRequest req) { 223 return worker.resolveTemplateEngine(req); 224 } 225 226 /** 227 * [GET /thymeleaf/*] — render a raw template through the Thymeleaf engine. 228 * 229 * <p> 230 * The {@code @Path("/*") String path} captures the multi-segment trailing remainder (e.g. a 231 * request for {@code /thymeleaf/about} matches the mount with {@code path = "about"}). 232 * Delegates to the shared {@link ThymeleafDispatcher} worker ({@code .html}-suffix handling, 233 * path-traversal hardening, engine resolution, and render). 234 * 235 * <p> 236 * Missing template surfaces as the engine's own {@code TemplateInputException} (HTTP 500 from 237 * the renderer's catch-all); missing engine surfaces as 238 * {@link ThymeleafViewRenderer#NO_ENGINE_DIAGNOSTIC}. 239 * 240 * @param path The trailing path segment after {@code /thymeleaf/} (the template name relative 241 * to the configured {@link #getBasePath() base path}; trailing {@code .html} stripped). 242 * @param req The current REST request. 243 * @param res The current REST response. 244 * @throws IOException If the underlying response writer fails. 245 * @throws BasicHttpException On boundary violation (403), missing engine (500), or render 246 * failure (500). 247 */ 248 @RestGet( 249 path="/${juneau.thymeleaf.path:thymeleaf}/*", 250 summary="Thymeleaf view", 251 description="Render a raw .html Thymeleaf template under the configured base path.", 252 swagger=@OpSwagger(ignore=true) 253 ) 254 public void render(@Path("/*") String path, RestRequest req, RestResponse res) 255 throws IOException, BasicHttpException { 256 worker.render(path, req, res); 257 } 258 259 /** 260 * Builder for {@link ThymeleafMixin}. 261 * 262 * <p> 263 * Mirrors {@link ThymeleafDispatcher.Builder}'s configuration methods on its own surface and 264 * forwards each call into a held {@link ThymeleafDispatcher.Builder} (worker-bean 265 * composition). Implements {@link ViewMixinBuilder} so {@link #basePath(String)} and 266 * {@link #cacheTemplates(boolean)} are guaranteed to transfer to the sibling JSP / Mustache / 267 * FreeMarker bridge builders. 268 */ 269 public static class Builder implements ViewMixinBuilder<Builder> { 270 271 private final ThymeleafDispatcher.Builder worker = ThymeleafDispatcher.create(); 272 273 /** Constructor — package access for {@link ThymeleafMixin#create()}. */ 274 protected Builder() {} 275 276 /** 277 * Sets the classpath base path under which template resources are resolved. 278 * 279 * <p> 280 * Defaults to {@link ThymeleafMixin#DEFAULT_BASE_PATH "/"}. A typical configured 281 * value is {@code "/templates/"} — the Spring-Boot-compatible layout convention. 282 * 283 * @param value The base path. {@code null} or blank values reset to the default 284 * {@link ThymeleafMixin#DEFAULT_BASE_PATH}. 285 * @return This object. 286 */ 287 @Override /* ViewMixinBuilder */ 288 public Builder basePath(String value) { 289 worker.basePath(value); 290 return this; 291 } 292 293 /** 294 * Sets whether the bridge's default engine caches resolved templates. 295 * 296 * <p> 297 * Defaults to {@code true} (production-safe). Set to {@code false} during development to 298 * pick up edits without restarting the server. This setting only applies to the bridge's 299 * fallback engine; if a {@link TemplateEngine} bean is registered, its own caching 300 * configuration is honored. 301 * 302 * @param value The new value. 303 * @return This object. 304 */ 305 @Override /* ViewMixinBuilder */ 306 public Builder cacheTemplates(boolean value) { 307 worker.cacheTemplates(value); 308 return this; 309 } 310 311 /** 312 * Sets the template mode used by the bridge's default engine. 313 * 314 * <p> 315 * Defaults to {@link TemplateMode#HTML HTML}. Other modes (`XML`, `TEXT`, `JAVASCRIPT`, 316 * `CSS`, `RAW`) work if the user supplies templates in that mode; engines that want 317 * mixed-mode support should register a custom {@code @Bean TemplateEngine} with multiple 318 * resolvers instead. 319 * 320 * @param value The new value. {@code null} resets to {@link #DEFAULT_TEMPLATE_MODE}. 321 * @return This object. 322 */ 323 public Builder templateMode(TemplateMode value) { 324 worker.templateMode(value); 325 return this; 326 } 327 328 /** 329 * Reads the current base path setting (test/inspection helper). 330 * 331 * @return The base path. Never {@code null}. 332 */ 333 public String getBasePath() { 334 return worker.getBasePath(); 335 } 336 337 /** 338 * Reads the current cache-templates setting (test/inspection helper). 339 * 340 * @return The cache flag. 341 */ 342 public boolean isCacheTemplates() { 343 return worker.isCacheTemplates(); 344 } 345 346 /** 347 * Reads the current template-mode setting (test/inspection helper). 348 * 349 * @return The template mode. Never {@code null}. 350 */ 351 public TemplateMode getTemplateMode() { 352 return worker.getTemplateMode(); 353 } 354 355 /** 356 * Builds the {@link ThymeleafMixin}. 357 * 358 * @return A new {@link ThymeleafMixin} instance. 359 */ 360 public ThymeleafMixin build() { 361 return new ThymeleafMixin(this); 362 } 363 } 364}