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',