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  ←  &lt;p&gt;Hello, 
${name}!&lt;/p&gt;
+ * </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  ←  &lt;p&gt;Hello, 
{{name}}!&lt;/p&gt;
+ * </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  ←  &lt;span 
th:text="${name}"&gt;...&lt;/span&gt;
+ * </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");
+       }
+}


Reply via email to