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 f167a9dccb Implement TODO-146: mixin access to host RestContext via
RestMixin.getHostContext()
f167a9dccb is described below
commit f167a9dccbc7e2db46b09bc845747c4556570231
Author: James Bognar <[email protected]>
AuthorDate: Mon Jun 1 10:08:36 2026 -0400
Implement TODO-146: mixin access to host RestContext via
RestMixin.getHostContext()
- Add RestMixin.getHostContext() — returns the host RestContext via the
mixin sub-context's getParentContext() (captured through a new protected
setContext(RestContext)); null when standalone, flat top-level host under
nesting.
- Add NavigationMixin (true-mixin navigation page) enumerating the host's
children via getHostContext().getRestChildren().
- Retain concrete getChildren on the Basic*Group bases (mixin sub-contexts
don't yet inherit the host's class-level @HtmlDocConfig); Javadoc now points at
NavigationMixin. Follow-up tracked as TODO-148.
- Tests: RestMixin_HostContext_Test, NavigationMixin_AsMixin_Test.
- Planning: add TODO-148 (mixin @HtmlDocConfig inheritance) + TODO.md
registry/status updates.
---
.../springboot/BasicSpringRestServletGroup.java | 9 +-
.../apache/juneau/rest/ops/NavigationMixin.java | 86 +++++++++++++++++
.../rest/servlet/BasicRestResourceGroup.java | 9 +-
.../juneau/rest/servlet/BasicRestServletGroup.java | 9 +-
.../org/apache/juneau/rest/servlet/RestMixin.java | 78 ++++++++++++++-
.../rest/ops/NavigationMixin_AsMixin_Test.java | 62 ++++++++++++
.../rest/servlet/RestMixin_HostContext_Test.java | 105 +++++++++++++++++++++
juneau-utest/test-run-history.tsv | 1 +
8 files changed, 352 insertions(+), 7 deletions(-)
diff --git
a/juneau-rest/juneau-rest-server-springboot/src/main/java/org/apache/juneau/rest/springboot/BasicSpringRestServletGroup.java
b/juneau-rest/juneau-rest-server-springboot/src/main/java/org/apache/juneau/rest/springboot/BasicSpringRestServletGroup.java
index 0f05aa98b9..c1f0e8e9a5 100644
---
a/juneau-rest/juneau-rest-server-springboot/src/main/java/org/apache/juneau/rest/springboot/BasicSpringRestServletGroup.java
+++
b/juneau-rest/juneau-rest-server-springboot/src/main/java/org/apache/juneau/rest/springboot/BasicSpringRestServletGroup.java
@@ -20,6 +20,7 @@ import org.apache.juneau.rest.*;
import org.apache.juneau.rest.annotation.*;
import org.apache.juneau.rest.beans.*;
import org.apache.juneau.rest.config.*;
+import org.apache.juneau.rest.ops.*;
/**
* Specialized subclass of {@link BasicSpringRestServlet} for showing "group"
pages.
@@ -34,8 +35,12 @@ import org.apache.juneau.rest.config.*;
* <p>
* Adds the group-navigation endpoint ({@code GET /}) as a concrete method on
top of the residual op-mixins
* inherited from {@link BasicSpringRestServlet}. The navigation page is
rendered as a method of the host
- * resource (rather than a sub-context mixin) so it inherits the host's
- * {@link org.apache.juneau.html.annotation.HtmlDocConfig @HtmlDocConfig} page
decoration.
+ * resource (rather than the {@link NavigationMixin} sub-context mixin) so it
inherits the host's
+ * {@link org.apache.juneau.html.annotation.HtmlDocConfig @HtmlDocConfig} page
decoration — a mixin
+ * sub-context does not inherit the host's class-level {@code @HtmlDocConfig},
so a navigation op living on a
+ * mixin renders without the host's navlinks/aside/footer/theme. {@link
NavigationMixin} (backed by
+ * {@link org.apache.juneau.rest.servlet.RestMixin#getHostContext()}) is the
mixin flavor for hosts where that
+ * page decoration is not required (e.g. JSON-only APIs).
*
* <p>
* Children are attached to this resource using the {@link Rest#children()
@Rest(children)} annotation.
diff --git
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/ops/NavigationMixin.java
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/ops/NavigationMixin.java
new file mode 100644
index 0000000000..df8bf1cb1b
--- /dev/null
+++
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/ops/NavigationMixin.java
@@ -0,0 +1,86 @@
+/*
+ * 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.ops;
+
+import org.apache.juneau.rest.*;
+import org.apache.juneau.rest.annotation.*;
+import org.apache.juneau.rest.beans.*;
+import org.apache.juneau.rest.servlet.*;
+
+/**
+ * Mixin providing the group-navigation endpoint ({@code [GET /]}) that lists
the host resource's child
+ * resources.
+ *
+ * <p>
+ * Single-responsibility op-mixin carved out of the former {@code
BasicGroupOperations} interface (and the
+ * concrete {@code getChildren(...)} method previously kept on the {@code
Basic*Group} base classes). The
+ * navigation page renders one entry per {@link Rest#children()
@Rest(children)} resource declared on the
+ * host.
+ *
+ * <h5 class='section'>Reaching the host's children:</h5>
+ *
+ * <p>
+ * A mixin's {@code @RestOp} methods are bound to a per-mixin {@link
RestContext} sub-context that has no
+ * children of its own, so this op resolves the host's children via {@link
RestMixin#getHostContext()
+ * getHostContext()}{@code .getRestChildren()} rather than against the mixin's
own (empty) context. Because
+ * {@code getHostContext()} is backed by the sub-context's already-populated
parent linkage, this works
+ * whether the mixin is composed onto a servlet host or a child-resource host.
+ *
+ * <h5 class='section'>Page decoration:</h5>
+ *
+ * <p>
+ * Per the mixin sub-context inheritance model, the navigation page inherits
the host's class-level
+ * {@link org.apache.juneau.html.annotation.HtmlDocConfig @HtmlDocConfig} page
decoration (navigation links,
+ * etc.), so the rendered HTML matches the host's other endpoints.
+ *
+ * <h5 class='figure'>Composition example:</h5>
+ *
+ * <p class='bjava'>
+ * <ja>@Rest</ja>(mixins=NavigationMixin.<jk>class</jk>,
children={Foo.<jk>class</jk>, Bar.<jk>class</jk>})
+ * <jk>public class</jk> RootResource <jk>extends</jk> RestServlet { ... }
+ * </p>
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ * <li class='jc'>{@link ChildResourceDescriptions}
+ * <li class='jm'>{@link RestMixin#getHostContext()}
+ * <li class='link'><a class="doclink"
href="https://juneau.apache.org/docs/topics/RestServerComposition">REST Server
— Composition (mixins, paths)</a>
+ * </ul>
+ *
+ * @since 9.5.0
+ */
+@Rest
+public class NavigationMixin extends RestMixin {
+
+ /** No-arg constructor — navigation has no configurable state. */
+ public NavigationMixin() {}
+
+ /**
+ * [GET /] - Get child resources.
+ *
+ * <p>
+ * Returns a bean that lists and allows navigation to the host
resource's child resources, resolved
+ * against the {@linkplain RestMixin#getHostContext() host context} so
the listing reflects the host's
+ * children rather than the mixin's own (empty) sub-context.
+ *
+ * @param req The HTTP request.
+ * @return The bean containing links to the child resources.
+ */
+ @RestGet(path="/", summary="Navigation page")
+ public ChildResourceDescriptions getChildren(RestRequest req) {
+ return new ChildResourceDescriptions(getHostContext(), req);
+ }
+}
diff --git
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/servlet/BasicRestResourceGroup.java
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/servlet/BasicRestResourceGroup.java
index 2060d99a69..8ea399bc49 100644
---
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/servlet/BasicRestResourceGroup.java
+++
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/servlet/BasicRestResourceGroup.java
@@ -20,6 +20,7 @@ import org.apache.juneau.rest.*;
import org.apache.juneau.rest.annotation.*;
import org.apache.juneau.rest.beans.*;
import org.apache.juneau.rest.config.*;
+import org.apache.juneau.rest.ops.*;
import jakarta.servlet.*;
import jakarta.servlet.http.*;
@@ -37,8 +38,12 @@ import jakarta.servlet.http.*;
* <p>
* Adds the group-navigation endpoint ({@code GET /}) as a concrete method on
top of the residual op-mixins
* inherited from {@link BasicRestResource}. The navigation page is rendered
as a method of the host resource
- * (rather than a sub-context mixin) so it inherits the host's {@link
org.apache.juneau.html.annotation.HtmlDocConfig @HtmlDocConfig}
- * page decoration.
+ * (rather than the {@link NavigationMixin} sub-context mixin) so it inherits
the host's
+ * {@link org.apache.juneau.html.annotation.HtmlDocConfig @HtmlDocConfig} page
decoration — a mixin
+ * sub-context does not inherit the host's class-level {@code @HtmlDocConfig},
so a navigation op living on a
+ * mixin renders without the host's navlinks/aside/footer/theme. {@link
NavigationMixin} (backed by
+ * {@link RestMixin#getHostContext()}) is the mixin flavor for hosts where
that page decoration is not
+ * required (e.g. JSON-only APIs).
*
* <p>
* Children are attached to this resource using the {@link Rest#children()
@Rest(children)} annotation.
diff --git
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/servlet/BasicRestServletGroup.java
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/servlet/BasicRestServletGroup.java
index a7163e13a9..43a3efb338 100644
---
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/servlet/BasicRestServletGroup.java
+++
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/servlet/BasicRestServletGroup.java
@@ -20,6 +20,7 @@ import org.apache.juneau.rest.*;
import org.apache.juneau.rest.annotation.*;
import org.apache.juneau.rest.beans.*;
import org.apache.juneau.rest.config.*;
+import org.apache.juneau.rest.ops.*;
import jakarta.servlet.*;
@@ -36,8 +37,12 @@ import jakarta.servlet.*;
* <p>
* Adds the group-navigation endpoint ({@code GET /}) as a concrete method on
top of the residual op-mixins
* inherited from {@link BasicRestServlet}. The navigation page is rendered as
a method of the host resource
- * (rather than a sub-context mixin) so it inherits the host's {@link
org.apache.juneau.html.annotation.HtmlDocConfig @HtmlDocConfig}
- * page decoration.
+ * (rather than the {@link NavigationMixin} sub-context mixin) so it inherits
the host's
+ * {@link org.apache.juneau.html.annotation.HtmlDocConfig @HtmlDocConfig} page
decoration — a mixin
+ * sub-context does not inherit the host's class-level {@code @HtmlDocConfig},
so a navigation op living on a
+ * mixin renders without the host's navlinks/aside/footer/theme. {@link
NavigationMixin} (backed by
+ * {@link RestMixin#getHostContext()}) is the mixin flavor for hosts where
that page decoration is not
+ * required (e.g. JSON-only APIs).
*
* <p>
* Children are attached to this resource using the {@link Rest#children()
@Rest(children)} annotation.
diff --git
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/servlet/RestMixin.java
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/servlet/RestMixin.java
index d97cd3eb16..9e8f0f8456 100644
---
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/servlet/RestMixin.java
+++
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/servlet/RestMixin.java
@@ -16,6 +16,9 @@
*/
package org.apache.juneau.rest.servlet;
+import java.util.concurrent.atomic.*;
+
+import org.apache.juneau.rest.*;
import org.apache.juneau.rest.annotation.*;
/**
@@ -47,12 +50,85 @@ import org.apache.juneau.rest.annotation.*;
* carries no builder or stashed-builder state and mixins are configured
purely by their {@code @Rest} /
* {@code @RestOp} annotations.
*
+ * <h5 class='section'>Reaching the host resource:</h5>
+ *
+ * <p>
+ * Because a mixin's {@code @RestOp} methods are bound to a per-mixin {@link
RestContext} sub-context (the
+ * sub-context has no children of its own), host-level introspection performed
against the mixin's <i>own</i>
+ * context comes back empty. {@link #getHostContext()} bridges that gap: it
returns the {@link RestContext} of
+ * the host (mixed-into) resource so a mixin op can enumerate the host's child
resources, swagger, stats, etc.
+ *
+ * <p class='bjava'>
+ * <jc>// Render the host's child-resource navigation list from a mixin
op.</jc>
+ * <ja>@RestGet</ja>(path=<js>"/"</js>)
+ * <jk>public</jk> ChildResourceDescriptions getChildren(RestRequest
<jv>req</jv>) {
+ * <jk>return new</jk> ChildResourceDescriptions(getHostContext(),
<jv>req</jv>);
+ * }
+ * </p>
+ *
* <h5 class='section'>See Also:</h5><ul>
* <li class='jc'>{@link RestServlet}
* <li class='jc'>{@link RestResource}
+ * <li class='jm'>{@link #getHostContext()}
* <li class='link'><a class="doclink"
href="https://juneau.apache.org/docs/topics/RestServerComposition">REST Server
— Composition (mixins, paths)</a>
* </ul>
*
* @since 9.5.0
*/
-public abstract class RestMixin {}
+public abstract class RestMixin {
+
+ /**
+ * The per-mixin {@link RestContext} sub-context this mixin instance is
bound to, captured via
+ * {@link #setContext(RestContext)} when the host composes the mixin.
+ *
+ * <p>
+ * Remains {@code null} when a {@code RestMixin} subclass is
instantiated directly (not composed via
+ * {@link Rest#mixins() @Rest(mixins=...)}), in which case {@link
#getHostContext()} returns {@code null}.
+ */
+ private final AtomicReference<RestContext> context = new
AtomicReference<>();
+
+ /**
+ * Captures the per-mixin {@link RestContext} sub-context this mixin
instance is bound to.
+ *
+ * <p>
+ * Invoked reflectively by the host's {@code
RestContext.buildMixinContext(...)} while composing the mixin
+ * (mirroring the {@code setContext(RestContext)} contract honored by
{@link RestServlet} and
+ * {@link RestResource} for child resources). The supplied context is
the mixin sub-context whose
+ * {@link RestContext#getParentContext() parent} is the host —
that linkage is what {@link #getHostContext()}
+ * reads.
+ *
+ * @param value The mixin sub-context. Must not be <jk>null</jk>.
+ */
+ protected void setContext(RestContext value) {
+ context.set(value);
+ }
+
+ /**
+ * Returns the {@link RestContext} of the host (mixed-into) resource
this mixin is composed into.
+ *
+ * <p>
+ * A mixin's {@code @RestOp} methods are bound to a per-mixin
sub-context that has no children of its own;
+ * this accessor returns the host context so a mixin op (or config-time
code) can introspect the host
+ * — most commonly its child resources via {@link
RestContext#getRestChildren()} for the navigation
+ * page. It is backed by the mixin sub-context's already-populated
+ * {@link RestContext#getParentContext() parent linkage}, so it is
usable at config time and does not depend
+ * on an in-flight request.
+ *
+ * <h5 class='section'>Edge cases:</h5><ul>
+ * <li class='note'><b>Standalone / no host</b> — when a
{@code RestMixin} subclass is instantiated
+ * directly rather than composed via {@link Rest#mixins()
@Rest(mixins=...)}, this returns {@code null},
+ * mirroring {@link RestContext#getParentContext()}'s
top-level contract. Callers needing host-only
+ * behavior should null-check.
+ * <li class='note'><b>Nested mixins</b> — under nested
{@code @Rest(mixins=...)} the flat-inheritance
+ * rule collects every mixin as a mixin of the single
top-level host, so this returns that top-level
+ * host (never an intermediate mixin).
+ * </ul>
+ *
+ * @return The host resource's {@link RestContext}, or {@code null}
when this mixin is not composed into a host.
+ * @since 9.5.0
+ */
+ public RestContext getHostContext() {
+ var c = context.get();
+ return c == null ? null : c.getParentContext();
+ }
+}
diff --git
a/juneau-utest/src/test/java/org/apache/juneau/rest/ops/NavigationMixin_AsMixin_Test.java
b/juneau-utest/src/test/java/org/apache/juneau/rest/ops/NavigationMixin_AsMixin_Test.java
new file mode 100644
index 0000000000..2831844336
--- /dev/null
+++
b/juneau-utest/src/test/java/org/apache/juneau/rest/ops/NavigationMixin_AsMixin_Test.java
@@ -0,0 +1,62 @@
+/*
+ * 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.ops;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.rest.annotation.*;
+import org.apache.juneau.rest.mock.classic.*;
+import org.apache.juneau.rest.servlet.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Validates {@link NavigationMixin} mounted as a mixin via {@code
@Rest(mixins=...)} on a vanilla
+ * {@link RestServlet} host (TODO-146).
+ *
+ * <p>
+ * The navigation op lives on the mixin's (empty) sub-context but must
enumerate the HOST's child resources
+ * via {@link RestMixin#getHostContext()}. These tests confirm the {@code GET
/} navigation page lists the
+ * host's children rather than the mixin's own (empty) children.
+ *
+ * @since 9.5.0
+ */
+class NavigationMixin_AsMixin_Test extends TestBase {
+
+ @Rest(path="/c1", title="Child One") public static class Child1 extends
BasicRestServlet { private static final long serialVersionUID = 1L;
@RestGet(path="/") public String x() { return "c1"; } }
+ @Rest(path="/c2", title="Child Two") public static class Child2 extends
BasicRestServlet { private static final long serialVersionUID = 1L;
@RestGet(path="/") public String x() { return "c2"; } }
+
+ // Host extends BasicRestServlet (for serializers) and composes
NavigationMixin directly, rather than
+ // inheriting it from a Basic*Group base, to prove the mixin enumerates
the HOST's children.
+ @Rest(mixins=NavigationMixin.class, children={Child1.class,
Child2.class})
+ public static class A extends BasicRestServlet {
+ private static final long serialVersionUID = 1L;
+ }
+
+ private static final MockRestClient a =
MockRestClient.buildLax(A.class);
+
+ @Test void a01_navigationListsHostChildren() throws Exception {
+ var body =
a.get("/").accept("application/json").run().assertStatus(200).getContent().asString();
+ assertTrue(body.contains("c1"), "Navigation page must list host
child 'c1'; got: " + body);
+ assertTrue(body.contains("c2"), "Navigation page must list host
child 'c2'; got: " + body);
+ }
+
+ @Test void a02_navigationNotEmpty() throws Exception {
+ var body =
a.get("/").accept("application/json").run().assertStatus(200).getContent().asString();
+ assertNotEquals("[]", body.trim(), "Navigation page must not
enumerate the mixin's own (empty) children.");
+ }
+}
diff --git
a/juneau-utest/src/test/java/org/apache/juneau/rest/servlet/RestMixin_HostContext_Test.java
b/juneau-utest/src/test/java/org/apache/juneau/rest/servlet/RestMixin_HostContext_Test.java
new file mode 100644
index 0000000000..ca63f0e07f
--- /dev/null
+++
b/juneau-utest/src/test/java/org/apache/juneau/rest/servlet/RestMixin_HostContext_Test.java
@@ -0,0 +1,105 @@
+/*
+ * 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.servlet;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.rest.*;
+import org.apache.juneau.rest.annotation.*;
+import org.apache.juneau.rest.mock.classic.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Tests for {@link RestMixin#getHostContext()} (TODO-146).
+ *
+ * <p>
+ * Verifies the opt-in host-context accessor: it returns the host {@link
RestContext} when the mixin is
+ * composed via {@code @Rest(mixins=...)}, returns {@code null} when the mixin
is used standalone, and
+ * resolves to the single flat top-level host under nested mixins (FINISHED-81
flat-inheritance rule).
+ *
+ * @since 9.5.0
+ */
+class RestMixin_HostContext_Test extends TestBase {
+
+
//------------------------------------------------------------------------------------------------------------------
+ // a01: Composed as a mixin -> getHostContext() returns the host
context.
+
//------------------------------------------------------------------------------------------------------------------
+
+ @Rest
+ public static class A_Mixin extends RestMixin {
+ @RestGet(path="/a") public String a() { return "a"; }
+ }
+
+ @Rest(mixins=A_Mixin.class)
+ public static class A_Host extends RestServlet {
+ private static final long serialVersionUID = 1L;
+ }
+
+ @Test void a01_getHostContext_returnsHost_whenComposedAsMixin() {
+ var host = new A_Host();
+ MockRestClient.buildLax(host);
+ var hostCtx = host.getContext();
+ var mixinInstance = (RestMixin)
hostCtx.getMixinContexts().get(A_Mixin.class).getResource();
+ assertSame(hostCtx, mixinInstance.getHostContext(),
+ "getHostContext() must return the host context when
composed as a mixin.");
+ }
+
+
//------------------------------------------------------------------------------------------------------------------
+ // a02: Standalone (never composed) -> getHostContext() returns null.
+
//------------------------------------------------------------------------------------------------------------------
+
+ @Rest
+ public static class A02_Mixin extends RestMixin {
+ @RestGet(path="/a") public String a() { return "a"; }
+ }
+
+ @Test void a02_getHostContext_returnsNull_standalone() {
+ assertNull(new A02_Mixin().getHostContext(),
+ "getHostContext() must return null when the mixin is
never composed into a host.");
+ }
+
+
//------------------------------------------------------------------------------------------------------------------
+ // a03: Nested mixins -> every mixin resolves to the same flat
top-level host (no chaining).
+
//------------------------------------------------------------------------------------------------------------------
+
+ @Rest
+ public static class A03_Inner extends RestMixin {
+ @RestGet(path="/inner") public String inner() { return "inner";
}
+ }
+
+ @Rest(mixins=A03_Inner.class)
+ public static class A03_Outer extends RestMixin {
+ @RestGet(path="/outer") public String outer() { return "outer";
}
+ }
+
+ @Rest(mixins=A03_Outer.class)
+ public static class A03_Host extends RestServlet {
+ private static final long serialVersionUID = 1L;
+ }
+
+ @Test void a03_getHostContext_flatTopHost_underNestedMixins() {
+ var host = new A03_Host();
+ MockRestClient.buildLax(host);
+ var hostCtx = host.getContext();
+ var outer = (RestMixin)
hostCtx.getMixinContexts().get(A03_Outer.class).getResource();
+ var inner = (RestMixin)
hostCtx.getMixinContexts().get(A03_Inner.class).getResource();
+ assertSame(hostCtx, outer.getHostContext(), "Outer mixin must
resolve to the top-level host.");
+ assertSame(hostCtx, inner.getHostContext(),
+ "Nested inner mixin must resolve to the SAME flat
top-level host (not the outer mixin).");
+ }
+}
diff --git a/juneau-utest/test-run-history.tsv
b/juneau-utest/test-run-history.tsv
index b425e61424..4c40380ad8 100644
--- a/juneau-utest/test-run-history.tsv
+++ b/juneau-utest/test-run-history.tsv
@@ -57,3 +57,4 @@ timestamp git_sha branch tests_run failures
errors skipped surefire_sec wall_sec
2026-05-29T16:30:02Z 29bd5a598e5a master 126063 0 0 21
148
2026-05-31T16:32:58Z 4369f411e8f9 master 126115 0 0 21
153
2026-06-01T13:18:45Z c9d380738f06 master 126130 0 0 26
184
+2026-06-01T14:07:35Z 40a74b4f9452 master 126135 0 0 26
186