This is an automated email from the ASF dual-hosted git repository.
jamesbognar pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/juneau.git
The following commit(s) were added to refs/heads/master by this push:
new 3aa11bdcbf test: view-integration typed-handler matrices for JSP,
Thymeleaf, Mustache, FreeMarker (TODO-97/107/108/109)
3aa11bdcbf is described below
commit 3aa11bdcbfc8f345a4beb963f196e22c29a447ac
Author: James Bognar <[email protected]>
AuthorDate: Wed May 27 13:22:17 2026 -0400
test: view-integration typed-handler matrices for JSP, Thymeleaf, Mustache,
FreeMarker (TODO-97/107/108/109)
---
.../juneau/rest/view/ViewIntegrationTestBase.java | 83 +++++++++
.../FreemarkerView_TypedHandler_Test.java | 196 +++++++++++++++++++++
.../rest/view/jsp/JspView_TypedHandler_Test.java | 188 ++++++++++++++++++++
.../mustache/MustacheView_TypedHandler_Test.java | 195 ++++++++++++++++++++
.../thymeleaf/ThymeleafView_TypedHandler_Test.java | 189 ++++++++++++++++++++
5 files changed, 851 insertions(+)
diff --git
a/juneau-utest/src/test/java/org/apache/juneau/rest/view/ViewIntegrationTestBase.java
b/juneau-utest/src/test/java/org/apache/juneau/rest/view/ViewIntegrationTestBase.java
new file mode 100644
index 0000000000..4b14496e6e
--- /dev/null
+++
b/juneau-utest/src/test/java/org/apache/juneau/rest/view/ViewIntegrationTestBase.java
@@ -0,0 +1,83 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.rest.view;
+
+import org.apache.juneau.*;
+import org.apache.juneau.rest.mock.classic.*;
+
+/**
+ * Shared base for typed-handler {@link View} integration test matrices.
+ *
+ * <p>
+ * Each concrete subclass targets one engine (JSP, Thymeleaf, Mustache,
FreeMarker) and covers
+ * five assertions:
+ *
+ * <ol>
+ * <li><b>a01</b> — A {@code @RestGet} returning an engine-specific {@code
View} reaches the
+ * corresponding {@code ViewRenderer}, not {@code
SerializedPojoProcessor}. The response
+ * body must NOT contain {@code "templateName"} (which would
appear only in a Juneau-bean
+ * JSON dump of the view object).
+ * <li><b>a02</b> — A {@code @RestGet} returning a plain {@code String} on
the same host falls
+ * through to {@code SerializedPojoProcessor} and is returned
unchanged (regression guard).
+ * <li><b>a03</b> — A host with two renderers dispatches each {@code View}
type to the correct
+ * renderer. Neither engine crosses to the other.
+ * <li><b>a04</b> — A typed handler returning a view with {@code
.attr("name", "Alice")} either
+ * (a) produces a rendered body containing {@code "Alice"}
(Thymeleaf / Mustache / FreeMarker
+ * which render from classpath without a container) or (b) does
NOT bean-serialize the view
+ * object (JSP, which requires a real servlet container).
+ * <li><b>a05</b> — A host with <em>no</em> renderer in {@code
responseProcessors} bean-serializes
+ * a {@code View}-typed return value; the body DOES contain {@code
"templateName"}.
+ * </ol>
+ *
+ * <h5 class='section'>Architecture note:</h5>
+ *
+ * <p>
+ * The mixin's {@code @Rest(responseProcessors=...)} only covers requests
handled by the mixin's
+ * own routes (e.g. {@code /thymeleaf/*}). Host-class {@code @RestOp} methods
that return
+ * engine-specific {@code View} objects require the renderer to be added to
the <em>host's</em>
+ * {@code @Rest(responseProcessors=...)}:
+ *
+ * <p class='bjava'>
+ *
<ja>@Rest</ja>(<jv>responseProcessors</jv>={ThymeleafViewRenderer.<jk>class</jk>})
+ * <jk>public class</jk> AppResource <jk>extends</jk> BasicRestServlet {
+ * <ja>@RestGet</ja>(<js>"/hello/{name}"</js>)
+ * <jk>public</jk> View hello(<ja>@Path</ja> String <jv>name</jv>)
{
+ * <jk>return</jk>
ThymeleafView.<jsm>of</jsm>(<js>"templates/hello.html"</js>).attr(<js>"name"</js>,
<jv>name</jv>);
+ * }
+ * }
+ * </p>
+ *
+ * <p>
+ * The {@link org.apache.juneau.rest.processor.ResponseProcessorList}
partition pass (added in
+ * 9.5.0) then automatically repositions the renderer before
+ * {@link org.apache.juneau.rest.processor.SerializedPojoProcessor} in the
chain.
+ *
+ * @since 9.5.0
+ */
+public abstract class ViewIntegrationTestBase extends TestBase {
+
+ /**
+ * Wraps the given resource class in a {@link MockRestClient}
configured in lax mode
+ * (tolerates non-2xx status codes).
+ *
+ * @param resourceClass The {@code @Rest}-annotated host class to wrap.
+ * @return A ready-to-use {@link MockRestClient}.
+ */
+ protected static MockRestClient buildResource(Class<?> resourceClass) {
+ return MockRestClient.buildLax(resourceClass);
+ }
+}
diff --git
a/juneau-utest/src/test/java/org/apache/juneau/rest/view/freemarker/FreemarkerView_TypedHandler_Test.java
b/juneau-utest/src/test/java/org/apache/juneau/rest/view/freemarker/FreemarkerView_TypedHandler_Test.java
new file mode 100644
index 0000000000..8ec788ab1f
--- /dev/null
+++
b/juneau-utest/src/test/java/org/apache/juneau/rest/view/freemarker/FreemarkerView_TypedHandler_Test.java
@@ -0,0 +1,196 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.rest.view.freemarker;
+
+import org.apache.juneau.*;
+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.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Typed-handler View integration matrix for {@link FreemarkerViewRenderer}.
+ *
+ * <p>
+ * Verifies that a host-class {@code @RestGet} method returning {@link
FreemarkerView} reaches
+ * {@link FreemarkerViewRenderer} (not {@code SerializedPojoProcessor}) when
the renderer is
+ * registered in the host's {@code @Rest(responseProcessors=...)}.
+ *
+ * <p>
+ * Apache FreeMarker has zero servlet-container dependencies and renders
directly to a
+ * {@link java.io.Writer}. Under MockRest the engine resolves templates from
the test classpath
+ * via its default classpath-root {@link freemarker.template.Configuration},
so {@code a04} can
+ * assert the actual interpolated output — body contains the {@code name}
attribute value passed
+ * via {@link FreemarkerView#attr(String, Object)}.
+ *
+ * <p>
+ * Spring Boot's {@code spring-boot-starter-freemarker} autoconfigures a
+ * {@link freemarker.template.Configuration} bean that the bridge picks up
automatically via
+ * {@code BeanStore.getBean(Configuration.class)} — the cleanest of the four
Spring Boot view
+ * stories. That end-to-end path is exercised in the Spring Boot
integration-test class.
+ *
+ * <h5 class='figure'>Template fixture used by a04:</h5>
+ * <pre>
+ * src/test/resources/freemarker-templates/hello.ftlh ← <p>Hello,
${name}!</p>
+ * </pre>
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ * <li class='link'><a class="doclink"
href="https://juneau.apache.org/docs/topics/FreemarkerViewSupport">FreeMarker
View Support</a>
+ * </ul>
+ *
+ * @since 9.5.0
+ */
+class FreemarkerView_TypedHandler_Test extends ViewIntegrationTestBase {
+
+ //
-----------------------------------------------------------------------------------------------------------------
+ // a01: FreemarkerView-returning @RestGet reaches
FreemarkerViewRenderer (not SerializedPojoProcessor).
+ //
-----------------------------------------------------------------------------------------------------------------
+
+ @Rest(responseProcessors={FreemarkerViewRenderer.class})
+ public static class A01_Host extends BasicRestServlet {
+ private static final long serialVersionUID = 1L;
+
+ @RestGet("/hello")
+ public View hello() {
+ // Use a non-existent template; we only assert the
renderer was reached.
+ return
FreemarkerView.of("__nonexistent__").attr("greeting", "Hello");
+ }
+ }
+
+ /** FreemarkerView-returning @RestGet reaches FreemarkerViewRenderer —
body is NOT a JSON bean dump. */
+ @Test void a01_freemarkerViewReturn_reachesRenderer() throws Exception {
+ buildResource(A01_Host.class)
+ .get("/hello")
+ .accept("application/json")
+ .run()
+
.assertContent().asString().isNotContains("templateName");
+ }
+
+ //
-----------------------------------------------------------------------------------------------------------------
+ // a02: A plain String return on the same host falls through to
SerializedPojoProcessor unchanged.
+ //
-----------------------------------------------------------------------------------------------------------------
+
+ @Rest(responseProcessors={FreemarkerViewRenderer.class})
+ public static class A02_Host extends BasicRestServlet {
+ private static final long serialVersionUID = 1L;
+
+ @RestGet("/items")
+ public String items() {
+ return "items";
+ }
+ }
+
+ /** String-returning @RestGet on a FreeMarker host still goes through
SerializedPojoProcessor. */
+ @Test void a02_fallThrough_nonViewReturn() throws Exception {
+ buildResource(A02_Host.class)
+ .get("/items")
+ .accept("application/json")
+ .run()
+ .assertStatus(200)
+ .assertContent().asString().isContains("items");
+ }
+
+ //
-----------------------------------------------------------------------------------------------------------------
+ // a03: Host with both FreemarkerViewRenderer and ThymeleafViewRenderer
dispatches each View
+ // type to the correct renderer. Neither engine crosses to the
other.
+ //
-----------------------------------------------------------------------------------------------------------------
+
+ @Rest(responseProcessors={FreemarkerViewRenderer.class,
ThymeleafViewRenderer.class})
+ public static class A03_Host extends BasicRestServlet {
+ private static final long serialVersionUID = 1L;
+
+ @RestGet("/fm-hello")
+ public View fmHello() {
+ return
FreemarkerView.of("freemarker-templates/hello.ftlh").attr("name", "Alice");
+ }
+
+ @RestGet("/thy-hello")
+ public View thyHello() {
+ return
ThymeleafView.of("templates/hello.html").attr("name", "Alice");
+ }
+ }
+
+ /** FreemarkerView → FreemarkerViewRenderer ("Alice"); ThymeleafView →
ThymeleafViewRenderer ("Alice"). */
+ @Test void a03_multipleRenderers_correctDispatch() throws Exception {
+ var c = buildResource(A03_Host.class);
+ // FreemarkerView rendered by FreeMarker engine.
+ c.get("/fm-hello")
+ .run()
+ .assertStatus(200)
+ .assertContent().asString().isContains("Alice");
+ // ThymeleafView rendered by Thymeleaf engine — confirms no
cross-dispatch.
+ c.get("/thy-hello")
+ .run()
+ .assertStatus(200)
+ .assertContent().asString().isContains("Alice");
+ }
+
+ //
-----------------------------------------------------------------------------------------------------------------
+ // a04: Typed handler returns FreemarkerView with attributes.
FreeMarker renders from classpath
+ // under MockRest, so we can assert actual attribute interpolation
(${name} → "Alice").
+ //
-----------------------------------------------------------------------------------------------------------------
+
+ @Rest(responseProcessors={FreemarkerViewRenderer.class})
+ public static class A04_Host extends BasicRestServlet {
+ private static final long serialVersionUID = 1L;
+
+ @RestGet("/hello/{name}")
+ public View hello(@Path String name) {
+ return
FreemarkerView.of("freemarker-templates/hello.ftlh").attr("name", name);
+ }
+ }
+
+ /**
+ * Typed handler interpolates the {@code name} attribute via
FreeMarker's {@code ${name}} — body
+ * contains the actual name value ("Alice"), not a bean-JSON dump.
+ */
+ @Test void a04_renderInterpolatesAttributesFromTypedHandler() throws
Exception {
+ buildResource(A04_Host.class)
+ .get("/hello/Alice")
+ .run()
+ .assertStatus(200)
+ .assertContent().asString().isContains("Alice")
+
.assertContent().asString().isNotContains("templateName");
+ }
+
+ //
-----------------------------------------------------------------------------------------------------------------
+ // a05: Host with NO renderer — FreemarkerView is bean-serialized by
SerializedPojoProcessor.
+ // Response body DOES contain "templateName".
+ //
-----------------------------------------------------------------------------------------------------------------
+
+ @Rest
+ public static class A05_Host extends BasicRestServlet {
+ private static final long serialVersionUID = 1L;
+
+ @RestGet("/hello")
+ public View hello() {
+ return
FreemarkerView.of("freemarker-templates/hello.ftlh").attr("name", "Alice");
+ }
+ }
+
+ /** No renderer in responseProcessors → FreemarkerView bean-serialized
→ body contains "templateName". */
+ @Test void a05_noRenderer_fallsToSerializedPojo() throws Exception {
+ buildResource(A05_Host.class)
+ .get("/hello")
+ .accept("application/json")
+ .run()
+ .assertStatus(200)
+ .assertContent().asString().isContains("templateName");
+ }
+}
diff --git
a/juneau-utest/src/test/java/org/apache/juneau/rest/view/jsp/JspView_TypedHandler_Test.java
b/juneau-utest/src/test/java/org/apache/juneau/rest/view/jsp/JspView_TypedHandler_Test.java
new file mode 100644
index 0000000000..3e026f0b1c
--- /dev/null
+++
b/juneau-utest/src/test/java/org/apache/juneau/rest/view/jsp/JspView_TypedHandler_Test.java
@@ -0,0 +1,188 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.rest.view.jsp;
+
+import org.apache.juneau.*;
+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.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Typed-handler View integration matrix for {@link JspViewRenderer}.
+ *
+ * <p>
+ * Verifies that a host-class {@code @RestGet} method returning {@link
JspView} reaches
+ * {@link JspViewRenderer} (not {@code SerializedPojoProcessor}) when the
renderer is registered
+ * in the host's {@code @Rest(responseProcessors=...)}.
+ *
+ * <p>
+ * JSP rendering requires a real servlet container with a JSP engine —
MockRest provides neither.
+ * Tests that assert rendering therefore verify only that the renderer was
reached (the response
+ * body does NOT contain the Juneau-bean JSON field {@code "templateName"}
that would appear if
+ * {@code SerializedPojoProcessor} ran instead). The renderer may complete
with 200 (empty body
+ * when the dispatcher silently drops the forward) or 5xx (engine absent),
both of which prove
+ * the renderer was invoked.
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ * <li class='link'><a class="doclink"
href="https://juneau.apache.org/docs/topics/JspViewSupport">JSP View Support</a>
+ * </ul>
+ *
+ * @since 9.5.0
+ */
+class JspView_TypedHandler_Test extends ViewIntegrationTestBase {
+
+ //
-----------------------------------------------------------------------------------------------------------------
+ // a01: JspView-returning @RestGet reaches JspViewRenderer (not
SerializedPojoProcessor).
+ //
-----------------------------------------------------------------------------------------------------------------
+
+ @Rest(responseProcessors={JspViewRenderer.class})
+ public static class A01_Host extends BasicRestServlet {
+ private static final long serialVersionUID = 1L;
+
+ @RestGet("/hello")
+ public View hello() {
+ return JspView.of("hello.jsp").attr("greeting",
"Hello");
+ }
+ }
+
+ /** JspView-returning @RestGet reaches JspViewRenderer — body is NOT a
JSON bean dump of JspView. */
+ @Test void a01_jspViewReturn_reachesRenderer() throws Exception {
+ buildResource(A01_Host.class)
+ .get("/hello")
+ .accept("application/json")
+ .run()
+
.assertContent().asString().isNotContains("templateName");
+ }
+
+ //
-----------------------------------------------------------------------------------------------------------------
+ // a02: A plain String return on the same host falls through to
SerializedPojoProcessor unchanged.
+ //
-----------------------------------------------------------------------------------------------------------------
+
+ @Rest(responseProcessors={JspViewRenderer.class})
+ public static class A02_Host extends BasicRestServlet {
+ private static final long serialVersionUID = 1L;
+
+ @RestGet("/items")
+ public String items() {
+ return "items";
+ }
+ }
+
+ /** String-returning @RestGet on a host with JspViewRenderer still goes
through SerializedPojoProcessor. */
+ @Test void a02_fallThrough_nonViewReturn() throws Exception {
+ buildResource(A02_Host.class)
+ .get("/items")
+ .accept("application/json")
+ .run()
+ .assertStatus(200)
+ .assertContent().asString().isContains("items");
+ }
+
+ //
-----------------------------------------------------------------------------------------------------------------
+ // a03: Host with both JspViewRenderer and ThymeleafViewRenderer
dispatches each View type to the
+ // correct renderer. JspView → JspViewRenderer (body NOT
"templateName").
+ // ThymeleafView → ThymeleafViewRenderer (Thymeleaf renders from
classpath → body contains "Alice").
+ //
-----------------------------------------------------------------------------------------------------------------
+
+ @Rest(responseProcessors={JspViewRenderer.class,
ThymeleafViewRenderer.class})
+ public static class A03_Host extends BasicRestServlet {
+ private static final long serialVersionUID = 1L;
+
+ @RestGet("/jsp-hello")
+ public View jspHello() {
+ return JspView.of("hello.jsp").attr("name", "Alice");
+ }
+
+ @RestGet("/thy-hello")
+ public View thyHello() {
+ return
ThymeleafView.of("templates/hello.html").attr("name", "Alice");
+ }
+ }
+
+ /** JspView → JspViewRenderer (not bean-serialized); ThymeleafView →
ThymeleafViewRenderer (renders "Alice"). */
+ @Test void a03_multipleRenderers_correctDispatch() throws Exception {
+ var c = buildResource(A03_Host.class);
+ // JspView goes to JspViewRenderer — NOT a bean-JSON dump.
+ c.get("/jsp-hello")
+ .accept("application/json")
+ .run()
+
.assertContent().asString().isNotContains("templateName");
+ // ThymeleafView goes to ThymeleafViewRenderer — Thymeleaf
renders from classpath.
+ c.get("/thy-hello")
+ .run()
+ .assertStatus(200)
+ .assertContent().asString().isContains("Alice");
+ }
+
+ //
-----------------------------------------------------------------------------------------------------------------
+ // a04: Typed handler returns JspView with attributes set. JSP
requires a real servlet container so
+ // full rendering cannot be asserted under MockRest. The key
invariant is that the renderer is
+ // reached (body NOT "templateName") — attributes are passed to
the renderer before the container
+ // call fails.
+ //
-----------------------------------------------------------------------------------------------------------------
+
+ @Rest(responseProcessors={JspViewRenderer.class})
+ public static class A04_Host extends BasicRestServlet {
+ private static final long serialVersionUID = 1L;
+
+ @RestGet("/hello/{name}")
+ public View hello(@Path String name) {
+ return JspView.of("hello.jsp").attr("name", name);
+ }
+ }
+
+ /**
+ * Typed handler returns JspView with attributes; renderer is reached
(body NOT "templateName").
+ * Full JSP interpolation requires a real servlet container — see the
JettyMicroservice / Spring Boot
+ * integration-test classes for end-to-end attribute substitution
assertions.
+ */
+ @Test void a04_renderInterpolatesAttributesFromTypedHandler() throws
Exception {
+ buildResource(A04_Host.class)
+ .get("/hello/Alice")
+ .accept("application/json")
+ .run()
+
.assertContent().asString().isNotContains("templateName");
+ }
+
+ //
-----------------------------------------------------------------------------------------------------------------
+ // a05: Host with NO renderer — JspView is bean-serialized by
SerializedPojoProcessor.
+ // Response body DOES contain "templateName" (invariant holds in
both directions).
+ //
-----------------------------------------------------------------------------------------------------------------
+
+ @Rest
+ public static class A05_Host extends BasicRestServlet {
+ private static final long serialVersionUID = 1L;
+
+ @RestGet("/hello")
+ public View hello() {
+ return JspView.of("hello.jsp").attr("greeting",
"Hello");
+ }
+ }
+
+ /** No renderer in responseProcessors → JspView bean-serialized → body
contains "templateName". */
+ @Test void a05_noRenderer_fallsToSerializedPojo() throws Exception {
+ buildResource(A05_Host.class)
+ .get("/hello")
+ .accept("application/json")
+ .run()
+ .assertStatus(200)
+ .assertContent().asString().isContains("templateName");
+ }
+}
diff --git
a/juneau-utest/src/test/java/org/apache/juneau/rest/view/mustache/MustacheView_TypedHandler_Test.java
b/juneau-utest/src/test/java/org/apache/juneau/rest/view/mustache/MustacheView_TypedHandler_Test.java
new file mode 100644
index 0000000000..12807b79ee
--- /dev/null
+++
b/juneau-utest/src/test/java/org/apache/juneau/rest/view/mustache/MustacheView_TypedHandler_Test.java
@@ -0,0 +1,195 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.rest.view.mustache;
+
+import org.apache.juneau.*;
+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.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Typed-handler View integration matrix for {@link MustacheViewRenderer}.
+ *
+ * <p>
+ * Verifies that a host-class {@code @RestGet} method returning {@link
MustacheView} reaches
+ * {@link MustacheViewRenderer} (not {@code SerializedPojoProcessor}) when the
renderer is
+ * registered in the host's {@code @Rest(responseProcessors=...)}.
+ *
+ * <p>
+ * mustache.java has zero servlet-container dependencies and renders directly
to a
+ * {@link java.io.Writer}. Under MockRest the factory resolves templates from
the test
+ * classpath, so {@code a04} can assert actual interpolated output — body
contains the
+ * {@code name} attribute value passed via {@link MustacheView#attr(String,
Object)}.
+ *
+ * <p>
+ * The {@link MustacheViewRenderer} is mustache.java-specific. Note that
+ * {@code spring-boot-starter-mustache} ships {@code jmustache} (a different
library); the
+ * Spring Boot flavor requires an explicit {@code @Bean
DefaultMustacheFactory} that wires
+ * mustache.java.
+ *
+ * <h5 class='figure'>Template fixture used by a04:</h5>
+ * <pre>
+ * src/test/resources/mustache-templates/hello.mustache ← <p>Hello,
{{name}}!</p>
+ * </pre>
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ * <li class='link'><a class="doclink"
href="https://juneau.apache.org/docs/topics/MustacheViewSupport">Mustache View
Support</a>
+ * </ul>
+ *
+ * @since 9.5.0
+ */
+class MustacheView_TypedHandler_Test extends ViewIntegrationTestBase {
+
+ //
-----------------------------------------------------------------------------------------------------------------
+ // a01: MustacheView-returning @RestGet reaches MustacheViewRenderer
(not SerializedPojoProcessor).
+ //
-----------------------------------------------------------------------------------------------------------------
+
+ @Rest(responseProcessors={MustacheViewRenderer.class})
+ public static class A01_Host extends BasicRestServlet {
+ private static final long serialVersionUID = 1L;
+
+ @RestGet("/hello")
+ public View hello() {
+ // Use a non-existent template; we only assert the
renderer was reached.
+ return
MustacheView.of("__nonexistent__").attr("greeting", "Hello");
+ }
+ }
+
+ /** MustacheView-returning @RestGet reaches MustacheViewRenderer — body
is NOT a JSON bean dump. */
+ @Test void a01_mustacheViewReturn_reachesRenderer() throws Exception {
+ buildResource(A01_Host.class)
+ .get("/hello")
+ .accept("application/json")
+ .run()
+
.assertContent().asString().isNotContains("templateName");
+ }
+
+ //
-----------------------------------------------------------------------------------------------------------------
+ // a02: A plain String return on the same host falls through to
SerializedPojoProcessor unchanged.
+ //
-----------------------------------------------------------------------------------------------------------------
+
+ @Rest(responseProcessors={MustacheViewRenderer.class})
+ public static class A02_Host extends BasicRestServlet {
+ private static final long serialVersionUID = 1L;
+
+ @RestGet("/items")
+ public String items() {
+ return "items";
+ }
+ }
+
+ /** String-returning @RestGet on a Mustache host still goes through
SerializedPojoProcessor. */
+ @Test void a02_fallThrough_nonViewReturn() throws Exception {
+ buildResource(A02_Host.class)
+ .get("/items")
+ .accept("application/json")
+ .run()
+ .assertStatus(200)
+ .assertContent().asString().isContains("items");
+ }
+
+ //
-----------------------------------------------------------------------------------------------------------------
+ // a03: Host with both MustacheViewRenderer and ThymeleafViewRenderer
dispatches each View
+ // type to the correct renderer. Neither engine crosses to the
other.
+ //
-----------------------------------------------------------------------------------------------------------------
+
+ @Rest(responseProcessors={MustacheViewRenderer.class,
ThymeleafViewRenderer.class})
+ public static class A03_Host extends BasicRestServlet {
+ private static final long serialVersionUID = 1L;
+
+ @RestGet("/mu-hello")
+ public View muHello() {
+ return
MustacheView.of("mustache-templates/hello.mustache").attr("name", "Alice");
+ }
+
+ @RestGet("/thy-hello")
+ public View thyHello() {
+ return
ThymeleafView.of("templates/hello.html").attr("name", "Alice");
+ }
+ }
+
+ /** MustacheView → MustacheViewRenderer ("Alice"); ThymeleafView →
ThymeleafViewRenderer ("Alice"). */
+ @Test void a03_multipleRenderers_correctDispatch() throws Exception {
+ var c = buildResource(A03_Host.class);
+ // MustacheView rendered by mustache.java.
+ c.get("/mu-hello")
+ .run()
+ .assertStatus(200)
+ .assertContent().asString().isContains("Alice");
+ // ThymeleafView rendered by Thymeleaf engine — confirms no
cross-dispatch.
+ c.get("/thy-hello")
+ .run()
+ .assertStatus(200)
+ .assertContent().asString().isContains("Alice");
+ }
+
+ //
-----------------------------------------------------------------------------------------------------------------
+ // a04: Typed handler returns MustacheView with attributes.
mustache.java renders from classpath
+ // under MockRest, so we can assert actual attribute interpolation
({{name}} → "Alice").
+ //
-----------------------------------------------------------------------------------------------------------------
+
+ @Rest(responseProcessors={MustacheViewRenderer.class})
+ public static class A04_Host extends BasicRestServlet {
+ private static final long serialVersionUID = 1L;
+
+ @RestGet("/hello/{name}")
+ public View hello(@Path String name) {
+ return
MustacheView.of("mustache-templates/hello.mustache").attr("name", name);
+ }
+ }
+
+ /**
+ * Typed handler interpolates the {@code name} attribute via Mustache's
{@code {{name}}} — body
+ * contains the actual name value ("Alice"), not a bean-JSON dump.
+ */
+ @Test void a04_renderInterpolatesAttributesFromTypedHandler() throws
Exception {
+ buildResource(A04_Host.class)
+ .get("/hello/Alice")
+ .run()
+ .assertStatus(200)
+ .assertContent().asString().isContains("Alice")
+
.assertContent().asString().isNotContains("templateName");
+ }
+
+ //
-----------------------------------------------------------------------------------------------------------------
+ // a05: Host with NO renderer — MustacheView is bean-serialized by
SerializedPojoProcessor.
+ // Response body DOES contain "templateName".
+ //
-----------------------------------------------------------------------------------------------------------------
+
+ @Rest
+ public static class A05_Host extends BasicRestServlet {
+ private static final long serialVersionUID = 1L;
+
+ @RestGet("/hello")
+ public View hello() {
+ return
MustacheView.of("mustache-templates/hello.mustache").attr("name", "Alice");
+ }
+ }
+
+ /** No renderer in responseProcessors → MustacheView bean-serialized →
body contains "templateName". */
+ @Test void a05_noRenderer_fallsToSerializedPojo() throws Exception {
+ buildResource(A05_Host.class)
+ .get("/hello")
+ .accept("application/json")
+ .run()
+ .assertStatus(200)
+ .assertContent().asString().isContains("templateName");
+ }
+}
diff --git
a/juneau-utest/src/test/java/org/apache/juneau/rest/view/thymeleaf/ThymeleafView_TypedHandler_Test.java
b/juneau-utest/src/test/java/org/apache/juneau/rest/view/thymeleaf/ThymeleafView_TypedHandler_Test.java
new file mode 100644
index 0000000000..d76834fb44
--- /dev/null
+++
b/juneau-utest/src/test/java/org/apache/juneau/rest/view/thymeleaf/ThymeleafView_TypedHandler_Test.java
@@ -0,0 +1,189 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.rest.view.thymeleaf;
+
+import org.apache.juneau.*;
+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.freemarker.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Typed-handler View integration matrix for {@link ThymeleafViewRenderer}.
+ *
+ * <p>
+ * Verifies that a host-class {@code @RestGet} method returning {@link
ThymeleafView} reaches
+ * {@link ThymeleafViewRenderer} (not {@code SerializedPojoProcessor}) when
the renderer is
+ * registered in the host's {@code @Rest(responseProcessors=...)}.
+ *
+ * <p>
+ * Unlike JSP, Thymeleaf's core engine has zero servlet-container dependencies
and renders
+ * directly to a {@link java.io.Writer}. Under MockRest the engine resolves
templates from the
+ * test classpath, so {@code a04} can assert the actual interpolated output —
body contains the
+ * {@code name} attribute value passed via {@link ThymeleafView#attr(String,
Object)}.
+ *
+ * <h5 class='figure'>Template fixture used by a04:</h5>
+ * <pre>
+ * src/test/resources/templates/hello.html ← <span
th:text="${name}">...</span>
+ * </pre>
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ * <li class='link'><a class="doclink"
href="https://juneau.apache.org/docs/topics/ThymeleafViewSupport">Thymeleaf
View Support</a>
+ * </ul>
+ *
+ * @since 9.5.0
+ */
+class ThymeleafView_TypedHandler_Test extends ViewIntegrationTestBase {
+
+ //
-----------------------------------------------------------------------------------------------------------------
+ // a01: ThymeleafView-returning @RestGet reaches ThymeleafViewRenderer
(not SerializedPojoProcessor).
+ //
-----------------------------------------------------------------------------------------------------------------
+
+ @Rest(responseProcessors={ThymeleafViewRenderer.class})
+ public static class A01_Host extends BasicRestServlet {
+ private static final long serialVersionUID = 1L;
+
+ @RestGet("/hello")
+ public View hello() {
+ // Use a non-existent template; we only assert the
renderer was reached.
+ return
ThymeleafView.of("__nonexistent__").attr("greeting", "Hello");
+ }
+ }
+
+ /** ThymeleafView-returning @RestGet reaches ThymeleafViewRenderer —
body is NOT a JSON bean dump. */
+ @Test void a01_thymeleafViewReturn_reachesRenderer() throws Exception {
+ buildResource(A01_Host.class)
+ .get("/hello")
+ .accept("application/json")
+ .run()
+
.assertContent().asString().isNotContains("templateName");
+ }
+
+ //
-----------------------------------------------------------------------------------------------------------------
+ // a02: A plain String return on the same host falls through to
SerializedPojoProcessor unchanged.
+ //
-----------------------------------------------------------------------------------------------------------------
+
+ @Rest(responseProcessors={ThymeleafViewRenderer.class})
+ public static class A02_Host extends BasicRestServlet {
+ private static final long serialVersionUID = 1L;
+
+ @RestGet("/items")
+ public String items() {
+ return "items";
+ }
+ }
+
+ /** String-returning @RestGet on a Thymeleaf host still goes through
SerializedPojoProcessor. */
+ @Test void a02_fallThrough_nonViewReturn() throws Exception {
+ buildResource(A02_Host.class)
+ .get("/items")
+ .accept("application/json")
+ .run()
+ .assertStatus(200)
+ .assertContent().asString().isContains("items");
+ }
+
+ //
-----------------------------------------------------------------------------------------------------------------
+ // a03: Host with both ThymeleafViewRenderer and FreemarkerViewRenderer
dispatches each View
+ // type to the correct renderer. Neither engine crosses to the
other.
+ //
-----------------------------------------------------------------------------------------------------------------
+
+ @Rest(responseProcessors={ThymeleafViewRenderer.class,
FreemarkerViewRenderer.class})
+ public static class A03_Host extends BasicRestServlet {
+ private static final long serialVersionUID = 1L;
+
+ @RestGet("/thy-hello")
+ public View thyHello() {
+ return
ThymeleafView.of("templates/hello.html").attr("name", "Alice");
+ }
+
+ @RestGet("/fm-hello")
+ public View fmHello() {
+ return
FreemarkerView.of("freemarker-templates/hello.ftlh").attr("name", "Alice");
+ }
+ }
+
+ /** ThymeleafView → ThymeleafViewRenderer ("Alice"); FreemarkerView →
FreemarkerViewRenderer ("Alice"). */
+ @Test void a03_multipleRenderers_correctDispatch() throws Exception {
+ var c = buildResource(A03_Host.class);
+ // ThymeleafView rendered by Thymeleaf engine.
+ c.get("/thy-hello")
+ .run()
+ .assertStatus(200)
+ .assertContent().asString().isContains("Alice");
+ // FreemarkerView rendered by FreeMarker engine.
+ c.get("/fm-hello")
+ .run()
+ .assertStatus(200)
+ .assertContent().asString().isContains("Alice");
+ }
+
+ //
-----------------------------------------------------------------------------------------------------------------
+ // a04: Typed handler returns ThymeleafView with attributes. Thymeleaf
renders from classpath
+ // under MockRest, so we can assert actual attribute interpolation
("Alice" in the response body).
+ //
-----------------------------------------------------------------------------------------------------------------
+
+ @Rest(responseProcessors={ThymeleafViewRenderer.class})
+ public static class A04_Host extends BasicRestServlet {
+ private static final long serialVersionUID = 1L;
+
+ @RestGet("/hello/{name}")
+ public View hello(@Path String name) {
+ return
ThymeleafView.of("templates/hello.html").attr("name", name);
+ }
+ }
+
+ /**
+ * Typed handler interpolates the {@code name} attribute via
Thymeleaf's {@code th:text} — body
+ * contains the actual name value ("Alice"), not a bean-JSON dump.
+ */
+ @Test void a04_renderInterpolatesAttributesFromTypedHandler() throws
Exception {
+ buildResource(A04_Host.class)
+ .get("/hello/Alice")
+ .run()
+ .assertStatus(200)
+ .assertContent().asString().isContains("Alice")
+
.assertContent().asString().isNotContains("templateName");
+ }
+
+ //
-----------------------------------------------------------------------------------------------------------------
+ // a05: Host with NO renderer — ThymeleafView is bean-serialized by
SerializedPojoProcessor.
+ // Response body DOES contain "templateName".
+ //
-----------------------------------------------------------------------------------------------------------------
+
+ @Rest
+ public static class A05_Host extends BasicRestServlet {
+ private static final long serialVersionUID = 1L;
+
+ @RestGet("/hello")
+ public View hello() {
+ return
ThymeleafView.of("templates/hello.html").attr("name", "Alice");
+ }
+ }
+
+ /** No renderer in responseProcessors → ThymeleafView bean-serialized →
body contains "templateName". */
+ @Test void a05_noRenderer_fallsToSerializedPojo() throws Exception {
+ buildResource(A05_Host.class)
+ .get("/hello")
+ .accept("application/json")
+ .run()
+ .assertStatus(200)
+ .assertContent().asString().isContains("templateName");
+ }
+}