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 &mdash; 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}} &mdash; 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="/&#123;juneau.thymeleaf.path:thymeleaf&#125;/*")} 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} &mdash; 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 &mdash; 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 &mdash; 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/*] &mdash; 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 &mdash; 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/"} &mdash; 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}