This is an automated email from the ASF dual-hosted git repository.

jamesbognar pushed a commit to branch docs
in repository https://gitbox.apache.org/repos/asf/juneau.git


The following commit(s) were added to refs/heads/docs by this push:
     new e5c1257dc5 docs: Thymeleaf view module topic page + 9.5.0 
release-notes entry (TODO-82)
e5c1257dc5 is described below

commit e5c1257dc54a20259b49bb67a0d1910c1604f82b
Author: James Bognar <[email protected]>
AuthorDate: Tue May 26 12:09:16 2026 -0400

    docs: Thymeleaf view module topic page + 9.5.0 release-notes entry (TODO-82)
    
    Co-authored-by: Cursor <[email protected]>
---
 pages/release-notes/9.5.0.md                |  56 +++++++
 pages/topics/10.14e.ThymeleafViewSupport.md | 241 ++++++++++++++++++++++++++++
 sidebars.ts                                 |   5 +
 3 files changed, 302 insertions(+)

diff --git a/pages/release-notes/9.5.0.md b/pages/release-notes/9.5.0.md
index 551adbb311..c91c7ad3d3 100644
--- a/pages/release-notes/9.5.0.md
+++ b/pages/release-notes/9.5.0.md
@@ -3751,6 +3751,62 @@ public class AppResource extends RestServlet {
 
 See [JSP View Support](/docs/topics/JspViewSupport) for the full topic — 
engine-selection matrix, Spring Boot fat-jar caveats, and known limitations.
 
+### juneau-rest-server-view-thymeleaf (new module)
+
+A new opt-in REST module, `juneau-rest-server-view-thymeleaf`, adds 
[Thymeleaf](https://www.thymeleaf.org/) view-rendering to `juneau-rest-server` 
— sibling to `juneau-rest-server-view-jsp`, but for the engine that Spring Boot 
autoconfigures by default. The same `View` interface (see 
[juneau-rest-server](#juneau-rest-server)) shipped with 9.5.0 backs both 
bridges. Engine-agnostic POM stance: the bridge module declares 
`org.thymeleaf:thymeleaf` in `provided` scope only — consumers add the [...]
+
+Unlike JSP, Thymeleaf's core engine has zero servlet-container dependencies — 
it renders directly to a `java.io.Writer`. The raw-template mount under 
`/thymeleaf/*` works fully under MockRest, Jetty microservices, and Spring Boot 
uniformly. The bridge picks up a Spring-autoconfigured `SpringTemplateEngine` 
automatically via `BeanStore.getBean(TemplateEngine.class)`; when no engine 
bean is registered the bridge constructs a default `TemplateEngine` with a 
single `ClassLoaderTemplateResolv [...]
+
+#### New Classes
+
+- **`org.apache.juneau.rest.view.thymeleaf.BasicThymeleafResource`** — REST 
mixin attachable via `@Rest(mixins=BasicThymeleafResource.class)`. Adds a 
default `/thymeleaf/*` mount that renders raw `.html` templates under the 
configured base path, and contributes `ThymeleafViewRenderer` to the mixin's 
response-processor chain. Builder API: 
`BasicThymeleafResource.create().basePath("/templates/").cacheTemplates(false).templateMode(TemplateMode.HTML).build()`.
 Configurable mount path via SVL [...]
+- **`org.apache.juneau.rest.view.thymeleaf.ThymeleafView`** — `View` 
implementation. Immutable value class; fluent builder: 
`ThymeleafView.of("hello").attr("name", name).header("Cache-Control", 
"no-store")`. `attr(...)` rejects `null` values because a Thymeleaf `Context` 
silently drops `null`-valued bindings, which masks caller bugs.
+- **`org.apache.juneau.rest.view.thymeleaf.ThymeleafViewRenderer`** — 
`ResponseProcessor` that detects `ThymeleafView`-typed return values and asks 
the configured `org.thymeleaf.TemplateEngine` to render them directly onto the 
response writer. When no Thymeleaf engine is on the classpath, surfaces 
`NO_ENGINE_DIAGNOSTIC` naming the missing dependency.
+
+#### Dependency
+
+```xml
+<dependency>
+    <groupId>org.apache.juneau</groupId>
+    <artifactId>juneau-rest-server-view-thymeleaf</artifactId>
+    <version>9.5.0</version>
+</dependency>
+<!-- pick ONE of these (engine-agnostic stance): -->
+<dependency>
+    <groupId>org.springframework.boot</groupId>
+    <artifactId>spring-boot-starter-thymeleaf</artifactId>
+    <!-- Spring Boot autoconfigured SpringTemplateEngine -->
+</dependency>
+<!-- OR -->
+<dependency>
+    <groupId>org.thymeleaf</groupId>
+    <artifactId>thymeleaf</artifactId>
+    <version>3.1.3.RELEASE</version>      <!-- Juneau microservice / Jetty -->
+</dependency>
+```
+
+#### Composition example
+
+```java
+@Rest(path="/app", mixins=BasicThymeleafResource.class)
+public class AppResource extends RestServlet {
+
+    @Bean
+    BasicThymeleafResource thymeleaf() {
+        return BasicThymeleafResource.create()
+            .basePath("/templates/")
+            .build();
+    }
+
+    @RestGet("/hello/{name}")
+    public View hello(@Path String name) {
+        return ThymeleafView.of("hello").attr("name", name);
+    }
+}
+```
+
+See [Thymeleaf View Support](/docs/topics/ThymeleafViewSupport) for the full 
topic — engine-selection matrix, Spring Boot integration notes, path-traversal 
hardening, and known limitations.
+
 ### juneau-bean-rfc7807 (new module)
 
 A new bean module, `juneau-bean-rfc7807`, provides typed beans for [RFC 7807 — 
Problem Details for HTTP APIs](https://www.rfc-editor.org/rfc/rfc7807) 
(`application/problem+json`). RFC 7807 was obsoleted by [RFC 
9457](https://www.rfc-editor.org/rfc/rfc9457) in July 2023, but the data model 
and the IANA media-type registration are unchanged.
diff --git a/pages/topics/10.14e.ThymeleafViewSupport.md 
b/pages/topics/10.14e.ThymeleafViewSupport.md
new file mode 100644
index 0000000000..58a4293f4f
--- /dev/null
+++ b/pages/topics/10.14e.ThymeleafViewSupport.md
@@ -0,0 +1,241 @@
+---
+title: "Thymeleaf View Support"
+slug: ThymeleafViewSupport
+---
+
+# Thymeleaf View Support
+
+The `juneau-rest-server-view-thymeleaf` module adds 
[Thymeleaf](https://www.thymeleaf.org/)
+view-rendering to `juneau-rest-server` without bleeding the Thymeleaf engine 
dependency into
+the core.
+
+> This page covers the Thymeleaf-specific bridge. For the engine-agnostic
+> [`View`](/site/apidocs/org/apache/juneau/rest/view/View.html) interface 
itself, see the
+> [9.5.0 release notes](/docs/release-notes/9.5.0) under the 
`juneau-rest-server` section.
+> The sibling [JSP View Support](/docs/topics/JspViewSupport) page covers the 
JSP bridge with
+> the same shape; future Mustache / FreeMarker bridges will ship the same way.
+
+## Why Thymeleaf?
+
+Thymeleaf is **the default web view technology for Spring Boot** — it's the 
engine Spring
+Boot autoconfigures when `spring-boot-starter-thymeleaf` is on the classpath, 
and it's the
+template engine Spring's reference docs recommend over JSP for embedded 
servlet containers.
+It also has zero servlet-container dependencies: the core engine renders 
directly to a
+`java.io.Writer` and works under MockRest, Jetty microservices, and Spring 
Boot equally well.
+
+## Module contents
+
+| Class | Role |
+|---|---|
+| [`View`](/site/apidocs/org/apache/juneau/rest/view/View.html) (in 
`juneau-rest-server` core) | Engine-agnostic contract: `getTemplateName()`, 
`getAttributes()`, `getResponseHeaders()`. |
+| 
[`BasicThymeleafResource`](/site/apidocs/org/apache/juneau/rest/view/thymeleaf/BasicThymeleafResource.html)
 | Mixin. Mounts `/thymeleaf/*` for raw `.html` template requests; registers 
`ThymeleafViewRenderer` on the response-processor chain. Builder: 
`basePath(String)` (default `/`), `cacheTemplates(boolean)` (default `true`), 
`templateMode(TemplateMode)` (default `HTML`). |
+| 
[`ThymeleafView`](/site/apidocs/org/apache/juneau/rest/view/thymeleaf/ThymeleafView.html)
 | `View` implementation. Immutable; fluent: 
`ThymeleafView.of("hello").attr("name", name).header("Cache-Control", 
"no-store")`. |
+| 
[`ThymeleafViewRenderer`](/site/apidocs/org/apache/juneau/rest/view/thymeleaf/ThymeleafViewRenderer.html)
 | `ResponseProcessor` that detects `ThymeleafView` returns and asks the 
configured `org.thymeleaf.TemplateEngine` to `process(templateName, context, 
writer)` directly onto the response. |
+
+## Engine-agnostic packaging
+
+`juneau-rest-server-view-thymeleaf` ships **only `org.thymeleaf:thymeleaf` in 
`provided`
+scope**. No engine is bundled with the bridge module. Consumers add the engine 
matching their
+deployment.
+
+### Choosing a `TemplateEngine`
+
+| Deployment | Recommended setup | Maven coordinates |
+|---|---|---|
+| **Spring Boot** | Spring Boot autoconfigured `SpringTemplateEngine` (extends 
`TemplateEngine`) | `org.springframework.boot:spring-boot-starter-thymeleaf` |
+| **Juneau microservice / Jetty** | Bridge-default `TemplateEngine` (built on 
first use, anchored on the importer's classloader) | `org.thymeleaf:thymeleaf` |
+| **Custom resolvers / dialects** | User-supplied `@Bean TemplateEngine` (e.g. 
`FileTemplateResolver`, custom Spring `MessageSource`, Spring Security dialect) 
| Whatever you need on top of `org.thymeleaf:thymeleaf` |
+
+The bridge picks up a `TemplateEngine` bean from the request's `BeanStore` via
+`BeanStore.getBean(TemplateEngine.class)` first; if no engine bean is 
registered, it
+constructs a default `TemplateEngine` with a single 
`ClassLoaderTemplateResolver` configured
+with `prefix=basePath`, `suffix=".html"`, `templateMode=HTML`, and 
`cacheable=true`.
+
+When no Thymeleaf engine is on the classpath, the renderer surfaces a clear 
diagnostic naming
+the missing dependency:
+
+```text
+No Thymeleaf engine is available on the classpath. Add one of:
+  - org.springframework.boot:spring-boot-starter-thymeleaf  (Spring Boot 
autoconfig)
+  - org.thymeleaf:thymeleaf                                  (Juneau 
microservice / Jetty)
+Or register a custom @Bean TemplateEngine that picks up your preferred 
resolvers.
+See https://juneau.apache.org/docs/topics/ThymeleafViewSupport for the full 
matrix.
+```
+
+## Hello-world
+
+### Maven
+
+```xml
+<dependency>
+    <groupId>org.apache.juneau</groupId>
+    <artifactId>juneau-rest-server-view-thymeleaf</artifactId>
+    <version>9.5.0</version>
+</dependency>
+<!-- pick ONE engine setup -->
+<dependency>
+    <groupId>org.thymeleaf</groupId>
+    <artifactId>thymeleaf</artifactId>
+    <version>3.1.3.RELEASE</version>
+</dependency>
+```
+
+### Resource layout
+
+```text
+src/main/resources/
+  templates/
+    hello.html
+```
+
+### Thymeleaf template (`hello.html`)
+
+```html
+<!DOCTYPE html>
+<html xmlns:th="http://www.thymeleaf.org";>
+<head><title>Hello</title></head>
+<body>
+<p>Hello, <span th:text="${name}">name</span>!</p>
+</body>
+</html>
+```
+
+### REST resource — `View`-return dispatch
+
+```java
+import org.apache.juneau.http.annotation.*;
+import org.apache.juneau.rest.annotation.*;
+import org.apache.juneau.rest.servlet.*;
+import org.apache.juneau.rest.view.*;
+import org.apache.juneau.rest.view.thymeleaf.*;
+
+@Rest(path="/app", mixins=BasicThymeleafResource.class)
+public class AppResource extends RestServlet {
+
+    @Bean
+    BasicThymeleafResource thymeleaf() {
+        return BasicThymeleafResource.create()
+            .basePath("/templates/")
+            .cacheTemplates(false)  // dev convenience; default true is 
production-safe
+            .build();
+    }
+
+    @RestGet("/hello/{name}")
+    public View hello(@Path String name) {
+        return ThymeleafView.of("hello").attr("name", name);
+    }
+}
+```
+
+`GET /app/hello/world` returns `Hello, world!` — `ThymeleafViewRenderer` 
intercepts the
+`ThymeleafView` return, populates a Thymeleaf `Context` from the view's 
attributes, and asks
+the active `TemplateEngine` to render `/templates/hello.html` directly onto 
the response.
+
+### REST resource — raw-template mount
+
+The mixin also installs a default `/thymeleaf/*` mount that renders raw 
`.html` templates
+under the configured base path. With `basePath("/templates/")`, a request for
+`GET /app/thymeleaf/about` renders `/templates/about.html` directly — no Java 
handler
+required. The handler strips a trailing `.html` so `/app/thymeleaf/about.html` 
works too.
+
+The greedy `/*` handler is excluded from the generated Swagger / OpenAPI spec 
via
+`@OpSwagger(ignore=true)` since the path isn't API-meaningful.
+
+### Configurable mount path (SVL)
+
+The default mount `/thymeleaf/*` can be overridden via the SVL variable
+`${juneau.thymeleaf.path:thymeleaf}` — set via system property
+(`-Djuneau.thymeleaf.path=views`), environment variable 
(`JUNEAU_THYMELEAF_PATH=views`), or
+`Config` key (`juneau.thymeleaf.path = views`) to change the runtime mount 
without
+subclassing.
+
+## Path-traversal protection
+
+The raw-template handler funnels every user-supplied `@Path("/*") String path` 
through
+[`FileUtils.resolveVirtualPathSafely(String, 
String)`](/site/apidocs/org/apache/juneau/commons/utils/FileUtils.html#resolveVirtualPathSafely-java.lang.String-java.lang.String-)
+and rejects any `..` traversal that escapes the configured `basePath` with 
HTTP 403. Both
+direct (`/thymeleaf/../secret`) and nested (`/thymeleaf/a/b/../../../secret`) 
traversal
+attempts are blocked at the handler boundary before reaching the engine 
resolver. This is
+the same hardening the JSP bridge applies; see
+[`BasicThymeleafResource_PathTraversal_Test`](https://github.com/apache/juneau/blob/main/juneau-utest/src/test/java/org/apache/juneau/rest/view/thymeleaf/BasicThymeleafResource_PathTraversal_Test.java)
+for the canonical coverage.
+
+## Spring Boot integration
+
+Spring Boot's reference docs explicitly recommend Thymeleaf as the default web 
view
+technology — there is no awkward fat-jar resource-layout dance (Thymeleaf 
doesn't depend on
+servlet semantics; the engine just resolves `classpath:/templates/*.html` 
directly), and
+Spring Boot autoconfigures a `SpringTemplateEngine` (which extends 
`TemplateEngine`) that
+the bridge picks up automatically through 
`BeanStore.getBean(TemplateEngine.class)`.
+
+### `@Bean` registration
+
+```java
+@Configuration
+public class AppConfig {
+
+    @Bean
+    public BasicThymeleafResource thymeleaf() {
+        return BasicThymeleafResource.create()
+            .basePath("/templates/")
+            .build();
+    }
+}
+```
+
+The Spring `BeanStore` adapter resolves the bean through
+`ApplicationContext.getBean(BasicThymeleafResource.class)`; no additional 
plumbing is
+required.
+
+### Known constraint — response-processor ordering
+
+Juneau's default response-processor chain runs `SerializedPojoProcessor` ahead 
of
+mixin-registered processors. When the host class has a method returning 
`ThymeleafView`, the
+host class needs to add `ThymeleafViewRenderer` to its *own* 
`responseProcessors` list to
+take precedence over the generic POJO serializer. Tracked as a framework 
enhancement to add
+a `prepend` mechanism for mixin processors (TODO-96); the real-container 
integration tests
+that exercise the `View`-return path under embedded Tomcat / Jetty are 
deferred until that
+lands (the Thymeleaf analog of TODO-97 for JSP).
+
+## Multiple base paths
+
+Some apps want `/views/` for the public site and `/admin/views/` for the admin 
console.
+Register two `BasicThymeleafResource` beans, each in its own subclass with its 
own `paths`
+override:
+
+```java
+@Rest(paths={"/views/*"})
+public class PublicViewsResource extends BasicThymeleafResource {
+    public PublicViewsResource() {
+        super(BasicThymeleafResource.create().basePath("/templates/public/"));
+    }
+}
+
+@Rest(paths={"/admin/views/*"})
+public class AdminViewsResource extends BasicThymeleafResource {
+    public AdminViewsResource() {
+        super(BasicThymeleafResource.create().basePath("/templates/admin/"));
+    }
+}
+```
+
+Both subclasses mount independently and each resolves templates against its 
own `basePath`.
+
+## Limitations and out-of-scope
+
+- **`thymeleaf-spring6` is not a direct dep of the bridge module.** The bridge 
depends only
+  on `org.thymeleaf:thymeleaf` (the engine core). Spring Boot users pull 
`thymeleaf-spring6`
+  in transitively via `spring-boot-starter-thymeleaf`; the bridge picks up the 
resulting
+  `SpringTemplateEngine` through the standard `BeanStore` lookup.
+- **Thymeleaf 2.x is not supported.** Thymeleaf 3.x is the LTS line and ships 
with Spring
+  Boot 3.x. Apps still on Thymeleaf 2.x should migrate before adopting the 
bridge.
+- **Custom dialects** (Spring Security, Layout, etc.) are users' 
responsibility — register
+  them on your own `@Bean TemplateEngine` and the bridge will use it instead 
of the default.
+
+## See also
+
+- [REST Server — Composition (mixins, 
paths)](/docs/topics/RestServerComposition)
+- [JSP View Support](/docs/topics/JspViewSupport) — sibling bridge for 
JSP-based apps
+- [Response Processors](/docs/topics/ResponseProcessors)
+- [9.5.0 release notes — `juneau-rest-server-view-thymeleaf` (new 
module)](/docs/release-notes/9.5.0)
+- [Thymeleaf 3 documentation](https://www.thymeleaf.org/documentation.html)
diff --git a/sidebars.ts b/sidebars.ts
index 2e9ccc1f1f..c1e8351368 100644
--- a/sidebars.ts
+++ b/sidebars.ts
@@ -1397,6 +1397,11 @@ const sidebars: SidebarsConfig = {
                                                        id: 
'topics/10.14d.JspViewSupport',
                                                        label: '10.14d. JSP 
View Support',
                                                },
+                                               {
+                                                       type: 'doc',
+                                                       id: 
'topics/10.14e.ThymeleafViewSupport',
+                                                       label: '10.14e. 
Thymeleaf View Support',
+                                               },
                                                {
                                                        type: 'doc',
                                                        id: 
'topics/10.15.ClientVersioning',

Reply via email to