You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@juneau.apache.org by ja...@apache.org on 2026/07/24 20:53:25 UTC
(juneau) branch master updated (3dc8ae26ea -> ed33c360d9)
This is an automated email from the ASF dual-hosted git repository.
jamesbognar pushed a change to branch master
in repository https://gitbox.apache.org/repos/asf/juneau.git
from 3dc8ae26ea Fix bean copy() shallow-copying nested beans in OpenAPI-v3 DOM (TODO-266)
new 21f5389e0e Add @Child annotation for host-side seeding of child REST contexts (TODO-182)
new ed33c360d9 Resolve SonarLint findings across core, rest, and test-support modules
The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
Summary of changes:
.../apache/juneau/bean/jsonschema/JsonSchema.java | 39 +++
.../bean/jsonschema/JsonSchemaBeanGenerator.java | 12 +
.../juneau/bean/jsonschema/JsonSchemaMap.java | 3 +
.../bean/jsonschema/JsonSchemaValidator.java | 3 +
.../juneau/bean/openapi3/SchemaInfo_Test.java | 3 +-
.../juneau/commons/bean/BeanMetaInitializer.java | 6 +-
.../apache/juneau/commons/logging/LogRecord.java | 2 -
.../juneau/commons/logging/LogRecord_Test.java | 6 -
.../main/java/org/apache/juneau/config/Config.java | 2 +-
.../org/apache/juneau/config/store/FileStore.java | 19 +-
.../juneau/marshall/hjson/HjsonParserSession.java | 2 +-
.../juneau/marshall/hocon/HoconParserSession.java | 2 +-
.../juneau/ParentPropertyContainer_Test.java | 2 +-
.../markdown/MarkdownParserSession_Test.java | 37 ++-
.../juneau/marshall/toml/TomlTokenizer_Test.java | 31 +--
.../main/java/org/apache/juneau/BeanTester.java | 3 +-
.../apache/juneau/a/rttests/RoundTrip_Tester.java | 8 +-
.../juneau/marshall/testutils/pojos/TestURI.java | 21 +-
.../main/java/org/apache/juneau/XmlTestUtils.java | 6 +
.../juneau/rest/child/ChildContext_Seed_Test.java | 147 +++++++++++
.../rest/child/ChildDefs_Discovery_Test.java | 70 +++++
.../rest/child/ChildDefs_Equivalence_Test.java | 56 ++++
.../child/ChildInheritance_Converters_Test.java | 84 ++++++
.../rest/child/ChildInheritance_Debug_Test.java | 105 ++++++++
.../ChildInheritance_DefaultConfigChild_Test.java | 110 ++++++++
.../rest/child/ChildInheritance_Guards_Test.java | 124 +++++++++
.../child/ChildInheritance_NoInherit_Test.java | 95 +++++++
.../child/ChildInheritance_PartParser_Test.java | 88 +++++++
.../ChildInheritance_PartSerializer_Test.java | 88 +++++++
.../child/ChildInheritance_RoleGuard_Test.java | 95 +++++++
.../rest/child/ChildInheritance_Scalars_Test.java | 114 ++++++++
...otation_Test.java => ChildAnnotation_Test.java} | 91 ++-----
.../rest/server/LazyChildren_ChildDefs_Test.java | 78 ++++++
.../saml/SamlAssertionValidator_Builder_Test.java | 3 +-
.../apache/juneau/http/response/BadGateway.java | 3 +
.../apache/juneau/http/response/BadRequest.java | 3 +
.../org/apache/juneau/http/response/Conflict.java | 3 +
.../juneau/http/response/ExpectationFailed.java | 3 +
.../juneau/http/response/FailedDependency.java | 3 +
.../org/apache/juneau/http/response/Forbidden.java | 3 +
.../juneau/http/response/GatewayTimeout.java | 3 +
.../java/org/apache/juneau/http/response/Gone.java | 3 +
.../http/response/HttpVersionNotSupported.java | 3 +
.../juneau/http/response/InsufficientStorage.java | 3 +
.../juneau/http/response/InternalServerError.java | 3 +
.../juneau/http/response/LengthRequired.java | 3 +
.../org/apache/juneau/http/response/Locked.java | 3 +
.../apache/juneau/http/response/LoopDetected.java | 3 +
.../juneau/http/response/MethodNotAllowed.java | 3 +
.../juneau/http/response/MisdirectedRequest.java | 3 +
.../response/NetworkAuthenticationRequired.java | 3 +
.../apache/juneau/http/response/NotAcceptable.java | 3 +
.../apache/juneau/http/response/NotExtended.java | 3 +
.../org/apache/juneau/http/response/NotFound.java | 3 +
.../juneau/http/response/NotImplemented.java | 3 +
.../juneau/http/response/PayloadTooLarge.java | 3 +
.../juneau/http/response/PreconditionFailed.java | 3 +
.../juneau/http/response/PreconditionRequired.java | 3 +
.../juneau/http/response/RangeNotSatisfiable.java | 3 +
.../http/response/RequestHeaderFieldsTooLarge.java | 3 +
.../juneau/http/response/ServiceUnavailable.java | 3 +
.../juneau/http/response/TooManyRequests.java | 3 +
.../apache/juneau/http/response/Unauthorized.java | 3 +
.../http/response/UnavailableForLegalReasons.java | 3 +
.../juneau/http/response/UnprocessableEntity.java | 3 +
.../juneau/http/response/UnsupportedMediaType.java | 3 +
.../juneau/http/response/UpgradeRequired.java | 3 +
.../apache/juneau/http/response/UriTooLong.java | 3 +
.../http/response/VariantAlsoNegotiates.java | 3 +
.../java/org/apache/juneau/rest/server/Child.java | 165 ++++++++++++
.../apache/juneau/rest/server/ChildAnnotation.java | 225 ++++++++++++++++
.../java/org/apache/juneau/rest/server/Rest.java | 41 +++
.../apache/juneau/rest/server/RestAnnotation.java | 19 ++
.../apache/juneau/rest/server/RestChildren.java | 48 +++-
.../org/apache/juneau/rest/server/RestContext.java | 290 ++++++++++++++++++---
.../juneau/rest/server/RestServerConstants.java | 3 +
.../juneau/rest/server/servlet/RestServlet.java | 6 +
.../server/validation/ValidationException.java | 3 +-
78 files changed, 2276 insertions(+), 184 deletions(-)
create mode 100644 juneau-integration-tests/src/test/java/org/apache/juneau/rest/child/ChildContext_Seed_Test.java
create mode 100644 juneau-integration-tests/src/test/java/org/apache/juneau/rest/child/ChildDefs_Discovery_Test.java
create mode 100644 juneau-integration-tests/src/test/java/org/apache/juneau/rest/child/ChildDefs_Equivalence_Test.java
create mode 100644 juneau-integration-tests/src/test/java/org/apache/juneau/rest/child/ChildInheritance_Converters_Test.java
create mode 100644 juneau-integration-tests/src/test/java/org/apache/juneau/rest/child/ChildInheritance_Debug_Test.java
create mode 100644 juneau-integration-tests/src/test/java/org/apache/juneau/rest/child/ChildInheritance_DefaultConfigChild_Test.java
create mode 100644 juneau-integration-tests/src/test/java/org/apache/juneau/rest/child/ChildInheritance_Guards_Test.java
create mode 100644 juneau-integration-tests/src/test/java/org/apache/juneau/rest/child/ChildInheritance_NoInherit_Test.java
create mode 100644 juneau-integration-tests/src/test/java/org/apache/juneau/rest/child/ChildInheritance_PartParser_Test.java
create mode 100644 juneau-integration-tests/src/test/java/org/apache/juneau/rest/child/ChildInheritance_PartSerializer_Test.java
create mode 100644 juneau-integration-tests/src/test/java/org/apache/juneau/rest/child/ChildInheritance_RoleGuard_Test.java
create mode 100644 juneau-integration-tests/src/test/java/org/apache/juneau/rest/child/ChildInheritance_Scalars_Test.java
copy juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/{MixinAnnotation_Test.java => ChildAnnotation_Test.java} (53%)
create mode 100644 juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/LazyChildren_ChildDefs_Test.java
create mode 100644 juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/Child.java
create mode 100644 juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/ChildAnnotation.java
(juneau) 01/02: Add @Child annotation for host-side seeding of child REST contexts (TODO-182)
Posted by ja...@apache.org.
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
commit 21f5389e0e2242eaea1e17000a0d8e838cf4e153
Author: James Bognar <ja...@apache.org>
AuthorDate: Fri Jul 24 16:52:44 2026 -0400
Add @Child annotation for host-side seeding of child REST contexts (TODO-182)
Adds @Child and @Rest(childrenDefs=...) mirroring @Mixin/mixinDefs, letting a
host seed a curated set of settings onto a routed child's otherwise-isolated
RestContext. Additive-security attributes (guards, converters, roleGuard,
rolesDeclared) prepend/AND-stack; child-wins scalars (callLogger,
partSerializer, partParser, debug, defaultCharset, maxInput) act as fallbacks.
Seeds never override a child's own explicit config; a child's noInherit cuts
the seed. Includes lazy-children seed threading and a fix to
computeRawRestAnnotations() DefaultConfig partitioning. Also includes 7
incidental java:S2259 false-positive suppressions in RestContext.java.
Co-authored-by: Cursor <cu...@cursor.com>
---
.../juneau/rest/child/ChildContext_Seed_Test.java | 147 +++++++++++
.../rest/child/ChildDefs_Discovery_Test.java | 70 +++++
.../rest/child/ChildDefs_Equivalence_Test.java | 56 ++++
.../child/ChildInheritance_Converters_Test.java | 84 ++++++
.../rest/child/ChildInheritance_Debug_Test.java | 105 ++++++++
.../ChildInheritance_DefaultConfigChild_Test.java | 110 ++++++++
.../rest/child/ChildInheritance_Guards_Test.java | 124 +++++++++
.../child/ChildInheritance_NoInherit_Test.java | 95 +++++++
.../child/ChildInheritance_PartParser_Test.java | 88 +++++++
.../ChildInheritance_PartSerializer_Test.java | 88 +++++++
.../child/ChildInheritance_RoleGuard_Test.java | 95 +++++++
.../rest/child/ChildInheritance_Scalars_Test.java | 114 ++++++++
.../juneau/rest/server/ChildAnnotation_Test.java | 118 +++++++++
.../rest/server/LazyChildren_ChildDefs_Test.java | 78 ++++++
.../java/org/apache/juneau/rest/server/Child.java | 165 ++++++++++++
.../apache/juneau/rest/server/ChildAnnotation.java | 225 ++++++++++++++++
.../java/org/apache/juneau/rest/server/Rest.java | 41 +++
.../apache/juneau/rest/server/RestAnnotation.java | 19 ++
.../apache/juneau/rest/server/RestChildren.java | 48 +++-
.../org/apache/juneau/rest/server/RestContext.java | 290 ++++++++++++++++++---
.../juneau/rest/server/RestServerConstants.java | 3 +
21 files changed, 2120 insertions(+), 43 deletions(-)
diff --git a/juneau-integration-tests/src/test/java/org/apache/juneau/rest/child/ChildContext_Seed_Test.java b/juneau-integration-tests/src/test/java/org/apache/juneau/rest/child/ChildContext_Seed_Test.java
new file mode 100644
index 0000000000..5e51695ecb
--- /dev/null
+++ b/juneau-integration-tests/src/test/java/org/apache/juneau/rest/child/ChildContext_Seed_Test.java
@@ -0,0 +1,147 @@
+/*
+ * 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.child;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.commons.inject.*;
+import org.apache.juneau.rest.mock.classic.*;
+import org.apache.juneau.rest.server.*;
+import org.apache.juneau.rest.server.guard.*;
+import org.apache.juneau.rest.server.logger.*;
+import org.apache.juneau.rest.server.servlet.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Phase 2 canary — proves the {@code @Child} seed-injection mechanism end-to-end using {@code callLogger}
+ * (a single child-wins scalar) as a witness, mirroring {@code MixinInheritance_CallLogger_Test}.
+ *
+ * <p>
+ * {@code callLogger} is chosen because {@link RestContext#getCallLogger()} is a direct, public, class-level
+ * getter with no request/op machinery needed to observe it.
+ */
+class ChildContext_Seed_Test extends TestBase {
+
+ public static class SeedLogger extends CallLogger {
+ public SeedLogger(BeanStore bs) { super(bs); }
+ }
+
+ public static class ChildLogger extends CallLogger {
+ public ChildLogger(BeanStore bs) { super(bs); }
+ }
+
+ public static class BlockAllGuard extends RestGuard {
+ @Override public boolean isRequestAllowed(RestRequest req) { return false; }
+ }
+
+ //------------------------------------------------------------------------------------------------------------
+ // a01: child declares no callLogger -> host's @Child(callLogger=...) seed takes effect.
+ //------------------------------------------------------------------------------------------------------------
+
+ @Rest(path="/nologger")
+ public static class ChildNoLoggerDeclared {
+ @RestGet(path="/me") public String me() { return "me"; }
+ }
+
+ @Rest(childrenDefs=@Child(type=ChildNoLoggerDeclared.class, callLogger=SeedLogger.class))
+ public static class HostSeedsLogger extends BasicRestServletGroup {
+ private static final long serialVersionUID = 1L;
+ }
+
+ @Test void a01_childWithNoDeclarationReceivesSeed() {
+ MockRestClient.buildLax(HostSeedsLogger.class);
+ var hostCtx = RestContext.getGlobalRegistry().get(HostSeedsLogger.class);
+ var childCtx = hostCtx.getRestChildren().asMap().get("nologger");
+ assertNotNull(childCtx);
+ assertInstanceOf(SeedLogger.class, childCtx.getCallLogger(),
+ "Child with no callLogger declaration must receive the host's seeded SeedLogger");
+ }
+
+ //------------------------------------------------------------------------------------------------------------
+ // a02: child's own explicit callLogger declaration wins over the host's seed.
+ //------------------------------------------------------------------------------------------------------------
+
+ @Rest(path="/ownlogger", callLogger=ChildLogger.class)
+ public static class ChildWithOwnLogger {
+ @RestGet(path="/me") public String me() { return "me"; }
+ }
+
+ @Rest(childrenDefs=@Child(type=ChildWithOwnLogger.class, callLogger=SeedLogger.class))
+ public static class HostSeedsLoggerButChildWins extends BasicRestServletGroup {
+ private static final long serialVersionUID = 1L;
+ }
+
+ @Test void a02_childsOwnDeclarationWins() {
+ MockRestClient.buildLax(HostSeedsLoggerButChildWins.class);
+ var hostCtx = RestContext.getGlobalRegistry().get(HostSeedsLoggerButChildWins.class);
+ var childCtx = hostCtx.getRestChildren().asMap().get("ownlogger");
+ assertNotNull(childCtx);
+ assertInstanceOf(ChildLogger.class, childCtx.getCallLogger(),
+ "Child's own explicit callLogger must win over the host's seeded SeedLogger");
+ }
+
+ //------------------------------------------------------------------------------------------------------------
+ // a03: isolation preserved -- host's OWN (non-seeded) @Rest(guards=...) must not leak to the child, even
+ // though the child receives a callLogger seed. If guards leaked, the child endpoint would 403.
+ //------------------------------------------------------------------------------------------------------------
+
+ @Rest(path="/isocheck")
+ public static class ChildForIsolation {
+ @RestGet(path="/me") public String me() { return "me"; }
+ }
+
+ @Rest(guards=BlockAllGuard.class, childrenDefs=@Child(type=ChildForIsolation.class, callLogger=SeedLogger.class))
+ public static class HostWithGuardsAndSeed extends BasicRestServletGroup {
+ private static final long serialVersionUID = 1L;
+ }
+
+ @Test void a03_isolationPreserved() throws Exception {
+ var c = MockRestClient.buildLax(HostWithGuardsAndSeed.class);
+ // Host's own (non-seeded) guards must not leak into the child's isolated context.
+ c.get("/isocheck/me").accept("text/plain").run().assertStatus(200).assertContent("me");
+ var hostCtx = RestContext.getGlobalRegistry().get(HostWithGuardsAndSeed.class);
+ var childCtx = hostCtx.getRestChildren().asMap().get("isocheck");
+ assertInstanceOf(SeedLogger.class, childCtx.getCallLogger(),
+ "The seeded callLogger must still apply alongside isolation from the host's own guards");
+ }
+
+ //------------------------------------------------------------------------------------------------------------
+ // a04: the seed is not masked by the synthesized DefaultConfig fallback -- DefaultConfig sets
+ // maxInput="1000000"; a host-seeded @Child(maxInput=...) must still win (seed ranks above DefaultConfig).
+ //------------------------------------------------------------------------------------------------------------
+
+ @Rest(path="/maxcheck")
+ public static class ChildForMaxInput {
+ @RestGet(path="/me") public String me() { return "me"; }
+ }
+
+ @Rest(childrenDefs=@Child(type=ChildForMaxInput.class, maxInput="12345"))
+ public static class HostSeedsMaxInput extends BasicRestServletGroup {
+ private static final long serialVersionUID = 1L;
+ }
+
+ @Test void a04_seedNotMaskedByDefaultConfigFallback() {
+ MockRestClient.buildLax(HostSeedsMaxInput.class);
+ var hostCtx = RestContext.getGlobalRegistry().get(HostSeedsMaxInput.class);
+ var childCtx = hostCtx.getRestChildren().asMap().get("maxcheck");
+ assertNotNull(childCtx);
+ var opCtx = childCtx.getRestOperations().getOpContexts().get(0);
+ assertEquals(12345L, opCtx.getMaxInput(),
+ "Host-seeded maxInput must win over the framework's DefaultConfig fallback (1000000)");
+ }
+}
diff --git a/juneau-integration-tests/src/test/java/org/apache/juneau/rest/child/ChildDefs_Discovery_Test.java b/juneau-integration-tests/src/test/java/org/apache/juneau/rest/child/ChildDefs_Discovery_Test.java
new file mode 100644
index 0000000000..f960dd77bb
--- /dev/null
+++ b/juneau-integration-tests/src/test/java/org/apache/juneau/rest/child/ChildDefs_Discovery_Test.java
@@ -0,0 +1,70 @@
+/*
+ * 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.child;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.util.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.rest.mock.classic.*;
+import org.apache.juneau.rest.server.*;
+import org.apache.juneau.rest.server.servlet.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Discovery-order and dedup tests for {@link Rest#childrenDefs() @Rest(childrenDefs=@Child(...))}, mirroring
+ * {@code MixinDefs_Overrides_Test} sections F/I/M for the {@code mixins}/{@code mixinDefs} discovery rules.
+ */
+class ChildDefs_Discovery_Test extends TestBase {
+
+ @Rest(path="/plain")
+ public static class PlainChild {
+ @RestGet("/ping") public String ping() { return "pong"; }
+ }
+
+ @Rest(path="/plain2")
+ public static class PlainChild2 {
+ @RestGet("/ping") public String ping() { return "pong2"; }
+ }
+
+ // A class named in both children= and childrenDefs= must be mounted exactly once.
+ @Rest(children=PlainChild.class, childrenDefs=@Child(type=PlainChild.class))
+ public static class DedupHost extends BasicRestServletGroup {
+ private static final long serialVersionUID = 1L;
+ }
+
+ @Test void a01_dedupedToFirstOccurrence() throws Exception {
+ var c = MockRestClient.buildLax(DedupHost.class);
+ c.get("/plain/ping").accept("text/plain").run().assertStatus(200).assertContent("pong");
+ var hostCtx = RestContext.getGlobalRegistry().get(DedupHost.class);
+ assertEquals(1, hostCtx.getRestChildren().asMap().size());
+ }
+
+ // Bare children= entries are discovered before childrenDefs= entries.
+ @Rest(children=PlainChild.class, childrenDefs=@Child(type=PlainChild2.class))
+ public static class OrderHost extends BasicRestServletGroup {
+ private static final long serialVersionUID = 1L;
+ }
+
+ @Test void a02_bareDiscoveredBeforeChildrenDefs() {
+ MockRestClient.buildLax(OrderHost.class);
+ var hostCtx = RestContext.getGlobalRegistry().get(OrderHost.class);
+ var keys = new ArrayList<>(hostCtx.getRestChildren().asMap().keySet());
+ assertEquals(List.of("plain", "plain2"), keys);
+ }
+}
diff --git a/juneau-integration-tests/src/test/java/org/apache/juneau/rest/child/ChildDefs_Equivalence_Test.java b/juneau-integration-tests/src/test/java/org/apache/juneau/rest/child/ChildDefs_Equivalence_Test.java
new file mode 100644
index 0000000000..2f950cdbb6
--- /dev/null
+++ b/juneau-integration-tests/src/test/java/org/apache/juneau/rest/child/ChildDefs_Equivalence_Test.java
@@ -0,0 +1,56 @@
+/*
+ * 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.child;
+
+import org.apache.juneau.*;
+import org.apache.juneau.rest.mock.classic.*;
+import org.apache.juneau.rest.server.*;
+import org.apache.juneau.rest.server.servlet.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Feature test for the {@link Rest#childrenDefs() @Rest(childrenDefs=@Child(...))} bare-equivalence rule.
+ *
+ * <p>
+ * Verifies the Phase 1 motivating capability — {@code @Child(type=X)} with zero overrides routes
+ * identically to a bare {@code @Rest(children=X)} entry — mirroring
+ * {@code MixinDefs_Overrides_Test.f01_bareEqualsEmptyDef}.
+ */
+class ChildDefs_Equivalence_Test extends TestBase {
+
+ @Rest(path="/plain")
+ public static class PlainChild {
+ @RestGet("/ping") public String ping() { return "pong"; }
+ }
+
+ @Rest(children=PlainChild.class)
+ public static class Bare extends BasicRestServletGroup {
+ private static final long serialVersionUID = 1L;
+ }
+
+ @Rest(childrenDefs=@Child(type=PlainChild.class))
+ public static class EmptyDef extends BasicRestServletGroup {
+ private static final long serialVersionUID = 1L;
+ }
+
+ @Test void a01_bareEqualsEmptyChildDef() throws Exception {
+ var bare = MockRestClient.buildLax(Bare.class);
+ var def = MockRestClient.buildLax(EmptyDef.class);
+ bare.get("/plain/ping").accept("text/plain").run().assertStatus(200).assertContent("pong");
+ def.get("/plain/ping").accept("text/plain").run().assertStatus(200).assertContent("pong");
+ }
+}
diff --git a/juneau-integration-tests/src/test/java/org/apache/juneau/rest/child/ChildInheritance_Converters_Test.java b/juneau-integration-tests/src/test/java/org/apache/juneau/rest/child/ChildInheritance_Converters_Test.java
new file mode 100644
index 0000000000..831e0006e8
--- /dev/null
+++ b/juneau-integration-tests/src/test/java/org/apache/juneau/rest/child/ChildInheritance_Converters_Test.java
@@ -0,0 +1,84 @@
+/*
+ * 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.child;
+
+import org.apache.juneau.*;
+import org.apache.juneau.http.response.*;
+import org.apache.juneau.marshall.serializer.*;
+import org.apache.juneau.rest.mock.classic.*;
+import org.apache.juneau.rest.server.*;
+import org.apache.juneau.rest.server.converter.*;
+import org.apache.juneau.rest.server.servlet.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Phase 3 — proves the additive-security PREPEND shape for {@code @Child(converters=...)}: the
+ * host-seeded converter applies when the child declares none of its own, and runs BEFORE the child's own
+ * declared converter when both are present (prepend order, verified via string-append ordering).
+ */
+class ChildInheritance_Converters_Test extends TestBase {
+
+ public static class SeedConv implements RestConverter {
+ @Override public Object convert(RestRequest req, Object res) throws BasicHttpException, SerializeException {
+ return res + "+seed";
+ }
+ }
+
+ public static class ChildConv implements RestConverter {
+ @Override public Object convert(RestRequest req, Object res) throws BasicHttpException, SerializeException {
+ return res + "+child";
+ }
+ }
+
+ //------------------------------------------------------------------------------------------------------------
+ // a01: child with no converters of its own -> the seeded converter applies.
+ //------------------------------------------------------------------------------------------------------------
+
+ @Rest(path="/noconv")
+ public static class ChildNoConv {
+ @RestGet(path="/me") public String me() { return "me"; }
+ }
+
+ @Rest(childrenDefs=@Child(type=ChildNoConv.class, converters=SeedConv.class))
+ public static class HostSeedsConvOnly extends BasicRestServletGroup {
+ private static final long serialVersionUID = 1L;
+ }
+
+ @Test void a01_seededConverterAppliesWhenChildHasNone() throws Exception {
+ var c = MockRestClient.buildLax(HostSeedsConvOnly.class);
+ c.get("/noconv/me").accept("text/plain").run().assertStatus(200).assertContent("me+seed");
+ }
+
+ //------------------------------------------------------------------------------------------------------------
+ // a02: child declares its own converter too -> BOTH apply, seed's converter runs first (prepend order).
+ //------------------------------------------------------------------------------------------------------------
+
+ @Rest(path="/bothconv", converters=ChildConv.class)
+ public static class ChildWithOwnConv {
+ @RestGet(path="/me") public String me() { return "me"; }
+ }
+
+ @Rest(childrenDefs=@Child(type=ChildWithOwnConv.class, converters=SeedConv.class))
+ public static class HostSeedsConvOrder extends BasicRestServletGroup {
+ private static final long serialVersionUID = 1L;
+ }
+
+ @Test void a02_seedRunsBeforeChildsOwnConverter() throws Exception {
+ var c = MockRestClient.buildLax(HostSeedsConvOrder.class);
+ c.get("/bothconv/me").accept("text/plain").run().assertStatus(200).assertContent("me+seed+child");
+ }
+}
diff --git a/juneau-integration-tests/src/test/java/org/apache/juneau/rest/child/ChildInheritance_Debug_Test.java b/juneau-integration-tests/src/test/java/org/apache/juneau/rest/child/ChildInheritance_Debug_Test.java
new file mode 100644
index 0000000000..94dd2ac311
--- /dev/null
+++ b/juneau-integration-tests/src/test/java/org/apache/juneau/rest/child/ChildInheritance_Debug_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.child;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.util.logging.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.rest.mock.MockServletRequest;
+import org.apache.juneau.rest.mock.classic.*;
+import org.apache.juneau.rest.server.*;
+import org.apache.juneau.rest.server.debug.*;
+import org.apache.juneau.rest.server.servlet.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Phase 4 — proves that {@code @Child(debug=...)} seeds each of {@code @Debug}'s three sub-fields
+ * (value/format/level) INDEPENDENTLY (per {@code RestContext.debugConfig}), not as one atomic all-or-nothing
+ * scalar: a child declaring only one sub-field still receives the seed's value for the others.
+ */
+class ChildInheritance_Debug_Test extends TestBase {
+
+ public static class SeedFormat implements DebugFormat {
+ @Override public String format(DebugFormatContext context) { return "seed-format"; }
+ }
+
+ public static class ChildFormat implements DebugFormat {
+ @Override public String format(DebugFormatContext context) { return "child-format"; }
+ }
+
+ //------------------------------------------------------------------------------------------------------------
+ // a01: host seeds value="always" + format=SeedFormat; child declares ONLY its own level="FINE".
+ // Expected: child's own level wins (independent sub-field); the seed's value/format still apply because
+ // the child left them at their sentinel.
+ //------------------------------------------------------------------------------------------------------------
+
+ @Rest(path="/subfields", debug=@Debug(level="FINE"))
+ public static class ChildWithOnlyLevel {
+ @RestGet(path="/me") public String me() { return "me"; }
+ }
+
+ @Rest(childrenDefs=@Child(type=ChildWithOnlyLevel.class, debug=@Debug(value="always", format=SeedFormat.class)))
+ public static class HostSeedsValueAndFormat extends BasicRestServletGroup {
+ private static final long serialVersionUID = 1L;
+ }
+
+ @Test void a01_subFieldsIndependentlySeeded() {
+ MockRestClient.buildLax(HostSeedsValueAndFormat.class);
+ var hostCtx = RestContext.getGlobalRegistry().get(HostSeedsValueAndFormat.class);
+ var childCtx = hostCtx.getRestChildren().asMap().get("subfields");
+ assertNotNull(childCtx);
+
+ var debugConfig = childCtx.getBeanStore().getBean(DebugConfig.class).orElse(null);
+ assertNotNull(debugConfig, "Child context must expose a resolved DebugConfig bean");
+
+ var result = debugConfig.resolve(childCtx, MockServletRequest.create());
+ assertTrue(result.enabled(), "Seed's value=\"always\" must enable debug even though the child never declared its own value");
+ assertInstanceOf(SeedFormat.class, result.format(), "Seed's format must apply since the child left format at its sentinel");
+ assertEquals(Level.FINE, result.level(), "Child's own explicit level must win over any (absent) seed level");
+ }
+
+ //------------------------------------------------------------------------------------------------------------
+ // a02: child declares its own value="never" (opting out); the seed's format still applies because the child
+ // left format at its sentinel -- proving independence runs both ways.
+ //------------------------------------------------------------------------------------------------------------
+
+ @Rest(path="/subfields2", debug=@Debug(value="never"))
+ public static class ChildWithOnlyValue {
+ @RestGet(path="/me") public String me() { return "me"; }
+ }
+
+ @Rest(childrenDefs=@Child(type=ChildWithOnlyValue.class, debug=@Debug(value="always", format=SeedFormat.class)))
+ public static class HostSeedsValueButChildOptsOut extends BasicRestServletGroup {
+ private static final long serialVersionUID = 1L;
+ }
+
+ @Test void a02_childsOwnSubFieldWinsOthersInherited() {
+ MockRestClient.buildLax(HostSeedsValueButChildOptsOut.class);
+ var hostCtx = RestContext.getGlobalRegistry().get(HostSeedsValueButChildOptsOut.class);
+ var childCtx = hostCtx.getRestChildren().asMap().get("subfields2");
+ assertNotNull(childCtx);
+
+ var debugConfig = childCtx.getBeanStore().getBean(DebugConfig.class).orElse(null);
+ assertNotNull(debugConfig);
+
+ var result = debugConfig.resolve(childCtx, MockServletRequest.create());
+ assertFalse(result.enabled(), "Child's own explicit value=\"never\" must win over the seed's value=\"always\"");
+ assertInstanceOf(SeedFormat.class, result.format(), "Seed's format must still apply -- the child left format at its sentinel");
+ }
+}
diff --git a/juneau-integration-tests/src/test/java/org/apache/juneau/rest/child/ChildInheritance_DefaultConfigChild_Test.java b/juneau-integration-tests/src/test/java/org/apache/juneau/rest/child/ChildInheritance_DefaultConfigChild_Test.java
new file mode 100644
index 0000000000..447a265e98
--- /dev/null
+++ b/juneau-integration-tests/src/test/java/org/apache/juneau/rest/child/ChildInheritance_DefaultConfigChild_Test.java
@@ -0,0 +1,110 @@
+/*
+ * 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.child;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.rest.mock.classic.*;
+import org.apache.juneau.rest.server.*;
+import org.apache.juneau.rest.server.servlet.*;
+import org.apache.juneau.utest.utils.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Phase 4 hardening — regression test for an edge case not called out explicitly by name in spec §6.6:
+ * a CHILD CLASS that itself transitively implements {@code DefaultConfig} (e.g. by extending
+ * {@code BasicRestServlet}/{@code BasicRestServletGroup}, which implement {@code BasicUniversalConfig extends
+ * DefaultConfig}), rather than being a plain POJO with no framework-default mixin.
+ *
+ * <p>
+ * <b>Bug found and fixed by this test (Phase 4 hardening):</b> {@code computeRawRestAnnotations()}'s
+ * {@code hasDefaultConfig}-true branch originally appended the seed annotation after the ENTIRE {@code raw}
+ * chain unconditionally. For a plain child class (the common case exercised by every other Phase 2-4 test),
+ * {@code raw} never embeds {@code DefaultConfig} itself, so appending after {@code raw} correctly placed the
+ * seed above the framework's separately-synthesized {@code DefaultConfig} fallback. But when the child class
+ * itself transitively implements {@code DefaultConfig}, that interface's own {@code @Rest} entry is already
+ * PART OF {@code raw} (typically at its most-ancestor/least-derived slot) — appending the seed after the
+ * <i>entire</i> {@code raw} list placed the seed BELOW that embedded {@code DefaultConfig} entry, so
+ * {@code DefaultConfig}'s own concrete defaults (e.g. {@code partSerializer=OpenApiSerializer.class}) masked
+ * the host's seed entirely, violating spec §6.6's invariant that the seed ranks above the {@code
+ * DefaultConfig} fallback. Fixed by splitting {@code raw} into "the child's own real declarations" and "the
+ * (embedded) DefaultConfig entries" and inserting the seed between them, mirroring exactly how the
+ * separately-synthesized case already worked.
+ */
+class ChildInheritance_DefaultConfigChild_Test extends TestBase {
+
+ public static class SeedPS extends FakeWriterSerializer {
+ public SeedPS(FakeWriterSerializer.Builder b) { super(b); }
+ }
+
+ //------------------------------------------------------------------------------------------------------------
+ // a01: child class extends BasicRestServlet (transitively implements DefaultConfig) and declares no
+ // partSerializer of its own -> the host's seed must still win over DefaultConfig's own baked-in
+ // partSerializer=OpenApiSerializer.class default, not be masked by it.
+ //------------------------------------------------------------------------------------------------------------
+
+ @Rest(path="/dcchild")
+ public static class ChildImplementingDefaultConfig extends BasicRestServlet {
+ private static final long serialVersionUID = 1L;
+ @RestGet(path="/me") public String me() { return "me"; }
+ }
+
+ @Rest(childrenDefs=@Child(type=ChildImplementingDefaultConfig.class, partSerializer=SeedPS.class))
+ public static class HostSeedsIntoDefaultConfigChild extends BasicRestServletGroup {
+ private static final long serialVersionUID = 1L;
+ }
+
+ @Test void a01_seedNotMaskedByChildsOwnEmbeddedDefaultConfig() {
+ MockRestClient.buildLax(HostSeedsIntoDefaultConfigChild.class);
+ var hostCtx = RestContext.getGlobalRegistry().get(HostSeedsIntoDefaultConfigChild.class);
+ var childCtx = hostCtx.getRestChildren().asMap().get("dcchild");
+ assertNotNull(childCtx);
+ assertEquals(SeedPS.class, childCtx.getPartSerializer().getClass(),
+ "Host-seeded partSerializer must win over DefaultConfig's own baked-in OpenApiSerializer default, "
+ + "even when the child class transitively implements DefaultConfig itself");
+ }
+
+ //------------------------------------------------------------------------------------------------------------
+ // a02: same shape, but the child ALSO declares its own explicit partSerializer -> the child's own
+ // declaration must still win over both the seed AND DefaultConfig (three-way precedence check).
+ //------------------------------------------------------------------------------------------------------------
+
+ public static class ChildPS extends FakeWriterSerializer {
+ public ChildPS(FakeWriterSerializer.Builder b) { super(b); }
+ }
+
+ @Rest(path="/dcchild2", partSerializer=ChildPS.class)
+ public static class ChildImplementingDefaultConfigWithOwnPS extends BasicRestServlet {
+ private static final long serialVersionUID = 1L;
+ @RestGet(path="/me") public String me() { return "me"; }
+ }
+
+ @Rest(childrenDefs=@Child(type=ChildImplementingDefaultConfigWithOwnPS.class, partSerializer=SeedPS.class))
+ public static class HostSeedsIntoDefaultConfigChildWithOwnPS extends BasicRestServletGroup {
+ private static final long serialVersionUID = 1L;
+ }
+
+ @Test void a02_childsOwnDeclarationStillWinsOverSeedAndDefaultConfig() {
+ MockRestClient.buildLax(HostSeedsIntoDefaultConfigChildWithOwnPS.class);
+ var hostCtx = RestContext.getGlobalRegistry().get(HostSeedsIntoDefaultConfigChildWithOwnPS.class);
+ var childCtx = hostCtx.getRestChildren().asMap().get("dcchild2");
+ assertNotNull(childCtx);
+ assertEquals(ChildPS.class, childCtx.getPartSerializer().getClass(),
+ "Child's own explicit partSerializer must win over both the host's seed and DefaultConfig's default");
+ }
+}
diff --git a/juneau-integration-tests/src/test/java/org/apache/juneau/rest/child/ChildInheritance_Guards_Test.java b/juneau-integration-tests/src/test/java/org/apache/juneau/rest/child/ChildInheritance_Guards_Test.java
new file mode 100644
index 0000000000..0f6924bc2e
--- /dev/null
+++ b/juneau-integration-tests/src/test/java/org/apache/juneau/rest/child/ChildInheritance_Guards_Test.java
@@ -0,0 +1,124 @@
+/*
+ * 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.child;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.util.*;
+import java.util.concurrent.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.rest.mock.classic.*;
+import org.apache.juneau.rest.server.*;
+import org.apache.juneau.rest.server.guard.*;
+import org.apache.juneau.rest.server.servlet.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Phase 3 — proves the additive-security PREPEND shape for {@code @Child(guards=...)}: the host-seeded
+ * guard applies when the child declares none of its own (degrades to a plain "set"), the seeded guard runs
+ * outermost (first) when the child also declares its own guard, and the child's own guard is never dropped.
+ */
+class ChildInheritance_Guards_Test extends TestBase {
+
+ static final List<String> ORDER = new CopyOnWriteArrayList<>();
+ private static final List<String> EXPECTED_ORDER = List.of("seed", "child");
+
+ public static class SeedGuard extends RestGuard {
+ @Override public boolean isRequestAllowed(RestRequest req) { ORDER.add("seed"); return true; }
+ }
+
+ public static class ChildGuard extends RestGuard {
+ @Override public boolean isRequestAllowed(RestRequest req) { ORDER.add("child"); return true; }
+ }
+
+ public static class BlockingSeedGuard extends RestGuard {
+ @Override public boolean isRequestAllowed(RestRequest req) {
+ return "yes".equals(req.getHeaderParam("X-Seed-Allowed").orElse(null));
+ }
+ }
+
+ public static class AllowOnlyChildHeader extends RestGuard {
+ @Override public boolean isRequestAllowed(RestRequest req) {
+ return "yes".equals(req.getHeaderParam("X-Child-Allowed").orElse(null));
+ }
+ }
+
+ //------------------------------------------------------------------------------------------------------------
+ // a01: child with no guards of its own -> the seeded guard applies (degrades to a plain "set").
+ //------------------------------------------------------------------------------------------------------------
+
+ @Rest(path="/noguard")
+ public static class ChildNoGuard {
+ @RestGet(path="/me") public String me() { return "me"; }
+ }
+
+ @Rest(childrenDefs=@Child(type=ChildNoGuard.class, guards=BlockingSeedGuard.class))
+ public static class HostSeedsGuardOnly extends BasicRestServletGroup {
+ private static final long serialVersionUID = 1L;
+ }
+
+ @Test void a01_seededGuardAppliesWhenChildHasNone() throws Exception {
+ var c = MockRestClient.buildLax(HostSeedsGuardOnly.class);
+ c.get("/noguard/me").run().assertStatus(403);
+ c.get("/noguard/me").header("X-Seed-Allowed", "yes").accept("text/plain").run().assertStatus(200).assertContent("me");
+ }
+
+ //------------------------------------------------------------------------------------------------------------
+ // a02: child declares its own guard too -> BOTH apply, and the seed's guard runs first (outermost).
+ //------------------------------------------------------------------------------------------------------------
+
+ @Rest(path="/bothguards", guards=ChildGuard.class)
+ public static class ChildWithOwnGuard {
+ @RestGet(path="/me") public String me() { return "me"; }
+ }
+
+ @Rest(childrenDefs=@Child(type=ChildWithOwnGuard.class, guards=SeedGuard.class))
+ public static class HostSeedsGuardOrder extends BasicRestServletGroup {
+ private static final long serialVersionUID = 1L;
+ }
+
+ @Test void a02_bothApplySeedRunsFirst() throws Exception {
+ ORDER.clear();
+ var c = MockRestClient.buildLax(HostSeedsGuardOrder.class);
+ c.get("/bothguards/me").accept("text/plain").run().assertStatus(200).assertContent("me");
+ assertEquals(EXPECTED_ORDER, ORDER,
+ "Host-seeded guard must run outermost (first), before the child's own declared guard");
+ }
+
+ //------------------------------------------------------------------------------------------------------------
+ // a03: the child's own guard is never dropped -- the seed's guard alone would allow the request, but the
+ // child's own (stricter) guard must still gate it.
+ //------------------------------------------------------------------------------------------------------------
+
+ @Rest(path="/dropcheck", guards=AllowOnlyChildHeader.class)
+ public static class ChildWithOwnGuard2 {
+ @RestGet(path="/me") public String me() { return "me"; }
+ }
+
+ @Rest(childrenDefs=@Child(type=ChildWithOwnGuard2.class, guards=SeedGuard.class))
+ public static class HostSeedsGuardKeepsChildGuard extends BasicRestServletGroup {
+ private static final long serialVersionUID = 1L;
+ }
+
+ @Test void a03_childsOwnGuardNeverDropped() throws Exception {
+ var c = MockRestClient.buildLax(HostSeedsGuardKeepsChildGuard.class);
+ // SeedGuard always allows, but the child's own AllowOnlyChildHeader guard must still gate the request.
+ c.get("/dropcheck/me").run().assertStatus(403);
+ c.get("/dropcheck/me").header("X-Child-Allowed", "yes").accept("text/plain").run().assertStatus(200).assertContent("me");
+ }
+}
diff --git a/juneau-integration-tests/src/test/java/org/apache/juneau/rest/child/ChildInheritance_NoInherit_Test.java b/juneau-integration-tests/src/test/java/org/apache/juneau/rest/child/ChildInheritance_NoInherit_Test.java
new file mode 100644
index 0000000000..3a5e42d6c5
--- /dev/null
+++ b/juneau-integration-tests/src/test/java/org/apache/juneau/rest/child/ChildInheritance_NoInherit_Test.java
@@ -0,0 +1,95 @@
+/*
+ * 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.child;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.rest.mock.classic.*;
+import org.apache.juneau.rest.server.*;
+import org.apache.juneau.rest.server.guard.*;
+import org.apache.juneau.rest.server.logger.*;
+import org.apache.juneau.rest.server.servlet.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Phase 5 — proves that a child's own {@code @Rest(noInherit="<property>")} cuts the corresponding
+ * {@code @Child} seed too, per spec §4's "{@code noInherit} interaction" subsection: because the seed is
+ * injected as an ordinary least-derived entry in the child's own {@code getRestAnnotations()} chain
+ * (§6.6), the existing generic {@code noInherit} cutoff scan in {@code getRestAnnotationsForProperty}
+ * naturally truncates it too, with no seed-aware special-casing required. Verified uniformly across BOTH
+ * seedable-property buckets: an additive-security member ({@code guards}) and a child-wins scalar
+ * ({@code callLogger}).
+ */
+class ChildInheritance_NoInherit_Test extends TestBase {
+
+ public static class BlockAllGuard extends RestGuard {
+ @Override public boolean isRequestAllowed(RestRequest req) { return false; }
+ }
+
+ public static class SeedLogger extends CallLogger {
+ public SeedLogger(org.apache.juneau.commons.inject.BeanStore bs) { super(bs); }
+ }
+
+ //------------------------------------------------------------------------------------------------------------
+ // a01: additive-security member (guards) -- child declares noInherit="guards" -> receives NONE of the
+ // host's seeded guards (the endpoint must be reachable WITHOUT satisfying the seeded guard at all, not
+ // merely "the child's own guards are present").
+ //------------------------------------------------------------------------------------------------------------
+
+ @Rest(path="/noinheritguards", noInherit="guards")
+ public static class ChildNoInheritGuards {
+ @RestGet(path="/me") public String me() { return "me"; }
+ }
+
+ @Rest(childrenDefs=@Child(type=ChildNoInheritGuards.class, guards=BlockAllGuard.class))
+ public static class HostSeedsGuardButChildOptsOut extends BasicRestServletGroup {
+ private static final long serialVersionUID = 1L;
+ }
+
+ @Test void a01_noInheritCutsSeededGuards() throws Exception {
+ var c = MockRestClient.buildLax(HostSeedsGuardButChildOptsOut.class);
+ // If the seeded BlockAllGuard were still applied, this request would always 403. noInherit="guards"
+ // must cut it entirely, leaving the child with zero guards.
+ c.get("/noinheritguards/me").accept("text/plain").run().assertStatus(200).assertContent("me");
+ }
+
+ //------------------------------------------------------------------------------------------------------------
+ // a02: child-wins scalar (callLogger) -- child declares noInherit="callLogger" -> receives NONE of the
+ // host's seeded callLogger (falls back to the framework default, not the seed).
+ //------------------------------------------------------------------------------------------------------------
+
+ @Rest(path="/noinheritlogger", noInherit="callLogger")
+ public static class ChildNoInheritLogger {
+ @RestGet(path="/me") public String me() { return "me"; }
+ }
+
+ @Rest(childrenDefs=@Child(type=ChildNoInheritLogger.class, callLogger=SeedLogger.class))
+ public static class HostSeedsLoggerButChildOptsOut extends BasicRestServletGroup {
+ private static final long serialVersionUID = 1L;
+ }
+
+ @Test void a02_noInheritCutsSeededCallLogger() {
+ MockRestClient.buildLax(HostSeedsLoggerButChildOptsOut.class);
+ var hostCtx = RestContext.getGlobalRegistry().get(HostSeedsLoggerButChildOptsOut.class);
+ var childCtx = hostCtx.getRestChildren().asMap().get("noinheritlogger");
+ assertNotNull(childCtx);
+ assertFalse(childCtx.getCallLogger() instanceof SeedLogger,
+ "Child's own noInherit=\"callLogger\" must cut the host's seeded SeedLogger entirely, "
+ + "falling back to the framework default rather than the seed");
+ }
+}
diff --git a/juneau-integration-tests/src/test/java/org/apache/juneau/rest/child/ChildInheritance_PartParser_Test.java b/juneau-integration-tests/src/test/java/org/apache/juneau/rest/child/ChildInheritance_PartParser_Test.java
new file mode 100644
index 0000000000..28f9addd49
--- /dev/null
+++ b/juneau-integration-tests/src/test/java/org/apache/juneau/rest/child/ChildInheritance_PartParser_Test.java
@@ -0,0 +1,88 @@
+/*
+ * 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.child;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.rest.mock.classic.*;
+import org.apache.juneau.rest.server.*;
+import org.apache.juneau.rest.server.servlet.*;
+import org.apache.juneau.utest.utils.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Phase 4 — proves the CHILD-WINS scalar shape for {@code @Child(partParser=...)}: the host-seeded
+ * part parser applies when the child declares none of its own, but the child's own explicit
+ * {@code @Rest(partParser=...)} wins over the seed when present.
+ */
+class ChildInheritance_PartParser_Test extends TestBase {
+
+ public static class SeedPP extends FakeReaderParser {
+ public SeedPP(FakeReaderParser.Builder b) { super(b); }
+ }
+
+ public static class ChildPP extends FakeReaderParser {
+ public ChildPP(FakeReaderParser.Builder b) { super(b); }
+ }
+
+ //------------------------------------------------------------------------------------------------------------
+ // a01: child declares no partParser -> host's seed takes effect.
+ //------------------------------------------------------------------------------------------------------------
+
+ @Rest(path="/nopp")
+ public static class ChildNoPPDeclared {
+ @RestGet(path="/me") public String me() { return "me"; }
+ }
+
+ @Rest(childrenDefs=@Child(type=ChildNoPPDeclared.class, partParser=SeedPP.class))
+ public static class HostSeedsPP extends BasicRestServletGroup {
+ private static final long serialVersionUID = 1L;
+ }
+
+ @Test void a01_childWithNoDeclarationReceivesSeed() {
+ MockRestClient.buildLax(HostSeedsPP.class);
+ var hostCtx = RestContext.getGlobalRegistry().get(HostSeedsPP.class);
+ var childCtx = hostCtx.getRestChildren().asMap().get("nopp");
+ assertNotNull(childCtx);
+ assertEquals(SeedPP.class, childCtx.getPartParser().getClass(),
+ "Child with no partParser declaration must receive the host's seeded SeedPP");
+ }
+
+ //------------------------------------------------------------------------------------------------------------
+ // a02: child's own explicit partParser wins over the host's seed.
+ //------------------------------------------------------------------------------------------------------------
+
+ @Rest(path="/ownpp", partParser=ChildPP.class)
+ public static class ChildWithOwnPP {
+ @RestGet(path="/me") public String me() { return "me"; }
+ }
+
+ @Rest(childrenDefs=@Child(type=ChildWithOwnPP.class, partParser=SeedPP.class))
+ public static class HostSeedsPPButChildWins extends BasicRestServletGroup {
+ private static final long serialVersionUID = 1L;
+ }
+
+ @Test void a02_childsOwnDeclarationWins() {
+ MockRestClient.buildLax(HostSeedsPPButChildWins.class);
+ var hostCtx = RestContext.getGlobalRegistry().get(HostSeedsPPButChildWins.class);
+ var childCtx = hostCtx.getRestChildren().asMap().get("ownpp");
+ assertNotNull(childCtx);
+ assertEquals(ChildPP.class, childCtx.getPartParser().getClass(),
+ "Child's own explicit partParser must win over the host's seeded SeedPP");
+ }
+}
diff --git a/juneau-integration-tests/src/test/java/org/apache/juneau/rest/child/ChildInheritance_PartSerializer_Test.java b/juneau-integration-tests/src/test/java/org/apache/juneau/rest/child/ChildInheritance_PartSerializer_Test.java
new file mode 100644
index 0000000000..4a01122046
--- /dev/null
+++ b/juneau-integration-tests/src/test/java/org/apache/juneau/rest/child/ChildInheritance_PartSerializer_Test.java
@@ -0,0 +1,88 @@
+/*
+ * 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.child;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.rest.mock.classic.*;
+import org.apache.juneau.rest.server.*;
+import org.apache.juneau.rest.server.servlet.*;
+import org.apache.juneau.utest.utils.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Phase 4 — proves the CHILD-WINS scalar shape for {@code @Child(partSerializer=...)}: the host-seeded
+ * part serializer applies when the child declares none of its own, but the child's own explicit
+ * {@code @Rest(partSerializer=...)} wins over the seed when present.
+ */
+class ChildInheritance_PartSerializer_Test extends TestBase {
+
+ public static class SeedPS extends FakeWriterSerializer {
+ public SeedPS(FakeWriterSerializer.Builder b) { super(b); }
+ }
+
+ public static class ChildPS extends FakeWriterSerializer {
+ public ChildPS(FakeWriterSerializer.Builder b) { super(b); }
+ }
+
+ //------------------------------------------------------------------------------------------------------------
+ // a01: child declares no partSerializer -> host's seed takes effect.
+ //------------------------------------------------------------------------------------------------------------
+
+ @Rest(path="/nops")
+ public static class ChildNoPSDeclared {
+ @RestGet(path="/me") public String me() { return "me"; }
+ }
+
+ @Rest(childrenDefs=@Child(type=ChildNoPSDeclared.class, partSerializer=SeedPS.class))
+ public static class HostSeedsPS extends BasicRestServletGroup {
+ private static final long serialVersionUID = 1L;
+ }
+
+ @Test void a01_childWithNoDeclarationReceivesSeed() {
+ MockRestClient.buildLax(HostSeedsPS.class);
+ var hostCtx = RestContext.getGlobalRegistry().get(HostSeedsPS.class);
+ var childCtx = hostCtx.getRestChildren().asMap().get("nops");
+ assertNotNull(childCtx);
+ assertEquals(SeedPS.class, childCtx.getPartSerializer().getClass(),
+ "Child with no partSerializer declaration must receive the host's seeded SeedPS");
+ }
+
+ //------------------------------------------------------------------------------------------------------------
+ // a02: child's own explicit partSerializer wins over the host's seed.
+ //------------------------------------------------------------------------------------------------------------
+
+ @Rest(path="/ownps", partSerializer=ChildPS.class)
+ public static class ChildWithOwnPS {
+ @RestGet(path="/me") public String me() { return "me"; }
+ }
+
+ @Rest(childrenDefs=@Child(type=ChildWithOwnPS.class, partSerializer=SeedPS.class))
+ public static class HostSeedsPSButChildWins extends BasicRestServletGroup {
+ private static final long serialVersionUID = 1L;
+ }
+
+ @Test void a02_childsOwnDeclarationWins() {
+ MockRestClient.buildLax(HostSeedsPSButChildWins.class);
+ var hostCtx = RestContext.getGlobalRegistry().get(HostSeedsPSButChildWins.class);
+ var childCtx = hostCtx.getRestChildren().asMap().get("ownps");
+ assertNotNull(childCtx);
+ assertEquals(ChildPS.class, childCtx.getPartSerializer().getClass(),
+ "Child's own explicit partSerializer must win over the host's seeded SeedPS");
+ }
+}
diff --git a/juneau-integration-tests/src/test/java/org/apache/juneau/rest/child/ChildInheritance_RoleGuard_Test.java b/juneau-integration-tests/src/test/java/org/apache/juneau/rest/child/ChildInheritance_RoleGuard_Test.java
new file mode 100644
index 0000000000..25b93ac137
--- /dev/null
+++ b/juneau-integration-tests/src/test/java/org/apache/juneau/rest/child/ChildInheritance_RoleGuard_Test.java
@@ -0,0 +1,95 @@
+/*
+ * 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.child;
+
+import org.apache.juneau.*;
+import org.apache.juneau.rest.mock.classic.*;
+import org.apache.juneau.rest.server.*;
+import org.apache.juneau.rest.server.servlet.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Phase 3 — proves the additive-security AND-STACK shape for {@code @Child(roleGuard=...)}: a
+ * host-seeded {@code roleGuard} and the child's own explicit {@code roleGuard} both apply as independent,
+ * ANDed {@code RoleBasedRestGuard} instances — the child can add restriction but never remove or
+ * weaken the host's.
+ */
+class ChildInheritance_RoleGuard_Test extends TestBase {
+
+ //------------------------------------------------------------------------------------------------------------
+ // a01: host seeds roleGuard="admin"; child declares its own roleGuard="special" -> BOTH must be satisfied.
+ //------------------------------------------------------------------------------------------------------------
+
+ @Rest(path="/rg", roleGuard="special")
+ public static class ChildWithOwnRoleGuard {
+ @RestGet(path="/me") public String me() { return "me"; }
+ }
+
+ @Rest(childrenDefs=@Child(type=ChildWithOwnRoleGuard.class, roleGuard="admin"))
+ public static class HostSeedsRoleGuard extends BasicRestServletGroup {
+ private static final long serialVersionUID = 1L;
+ }
+
+ @Test void a01_bothRoleGuardsMustBeSatisfied() throws Exception {
+ var c = MockRestClient.buildLax(HostSeedsRoleGuard.class);
+ c.get("/rg/me").roles("admin").run().assertStatus(403); // missing "special"
+ c.get("/rg/me").roles("special").run().assertStatus(403); // missing "admin"
+ c.get("/rg/me").roles("admin", "special").accept("text/plain").run().assertStatus(200).assertContent("me");
+ }
+
+ //------------------------------------------------------------------------------------------------------------
+ // a02: child declares no roleGuard of its own -> only the host-seeded roleGuard applies.
+ //------------------------------------------------------------------------------------------------------------
+
+ @Rest(path="/rg2")
+ public static class ChildNoRoleGuard {
+ @RestGet(path="/me") public String me() { return "me"; }
+ }
+
+ @Rest(childrenDefs=@Child(type=ChildNoRoleGuard.class, roleGuard="admin"))
+ public static class HostSeedsRoleGuardOnly extends BasicRestServletGroup {
+ private static final long serialVersionUID = 1L;
+ }
+
+ @Test void a02_seedOnlyAppliesWhenChildDeclaresNone() throws Exception {
+ var c = MockRestClient.buildLax(HostSeedsRoleGuardOnly.class);
+ c.get("/rg2/me").run().assertStatus(403);
+ c.get("/rg2/me").roles("admin").accept("text/plain").run().assertStatus(200).assertContent("me");
+ }
+
+ //------------------------------------------------------------------------------------------------------------
+ // a03: rolesDeclared accumulates across host seed AND child, into the single shared declared-role set used
+ // by every RoleBasedRestGuard in the chain (RestOpContext.guards()) -- the child's own rolesDeclared="admin"
+ // feeds the host-seeded PATTERN-based roleGuard="ad*", which has no declared-role names of its own.
+ //------------------------------------------------------------------------------------------------------------
+
+ @Rest(path="/rg3", rolesDeclared="admin")
+ public static class ChildDeclaresRolesOnly {
+ @RestGet(path="/me") public String me() { return "me"; }
+ }
+
+ @Rest(childrenDefs=@Child(type=ChildDeclaresRolesOnly.class, roleGuard="ad*"))
+ public static class HostSeedsPatternRoleGuard extends BasicRestServletGroup {
+ private static final long serialVersionUID = 1L;
+ }
+
+ @Test void a03_rolesDeclaredFromChildFeedsHostSeededPatternRoleGuard() throws Exception {
+ var c = MockRestClient.buildLax(HostSeedsPatternRoleGuard.class);
+ c.get("/rg3/me").run().assertStatus(403);
+ c.get("/rg3/me").roles("admin").accept("text/plain").run().assertStatus(200).assertContent("me");
+ }
+}
diff --git a/juneau-integration-tests/src/test/java/org/apache/juneau/rest/child/ChildInheritance_Scalars_Test.java b/juneau-integration-tests/src/test/java/org/apache/juneau/rest/child/ChildInheritance_Scalars_Test.java
new file mode 100644
index 0000000000..53e8b27537
--- /dev/null
+++ b/juneau-integration-tests/src/test/java/org/apache/juneau/rest/child/ChildInheritance_Scalars_Test.java
@@ -0,0 +1,114 @@
+/*
+ * 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.child;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.nio.charset.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.rest.mock.classic.*;
+import org.apache.juneau.rest.server.*;
+import org.apache.juneau.rest.server.servlet.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Phase 4 — proves the CHILD-WINS scalar shape for {@code @Child(defaultCharset=...)} and
+ * {@code @Child(maxInput=...)}, both of which resolve through {@code RestContext.mergeReplacedStringAttribute}
+ * (last-non-sentinel-wins over the annotation chain), exactly like {@code callLogger}/{@code partSerializer}/
+ * {@code partParser}.
+ *
+ * <p>
+ * {@code defaultAccept}/{@code defaultContentType} are intentionally NOT seed slots on {@code @Child} (see
+ * {@link Child} javadoc): they resolve through the {@code defaultRequestHeaders} memoizer's
+ * {@code HttpHeaderList.setDefault(...)} first-wins semantics rather than {@code mergeReplacedStringAttribute},
+ * which would make the host's seed win over the child's own explicit declaration — the opposite of the
+ * child-wins contract. That's pre-existing framework behavior unrelated to {@code @Child} (a plain
+ * superclass/subclass {@code @Rest} hierarchy exhibits the same ancestor-wins-over-descendant behavior), so
+ * these two members are deferred rather than shipped as a contract violation.
+ */
+class ChildInheritance_Scalars_Test extends TestBase {
+
+ //==================================================================================================================
+ // defaultCharset -- true child-wins (via mergeReplacedStringAttribute)
+ //==================================================================================================================
+
+ @Rest(path="/nocharset")
+ public static class ChildNoCharsetDeclared {
+ @RestGet(path="/me") public String me() { return "me"; }
+ }
+
+ @Rest(childrenDefs=@Child(type=ChildNoCharsetDeclared.class, defaultCharset="utf-16"))
+ public static class HostSeedsCharset extends BasicRestServletGroup {
+ private static final long serialVersionUID = 1L;
+ }
+
+ @Test void a01_charset_childWithNoDeclarationReceivesSeed() {
+ MockRestClient.buildLax(HostSeedsCharset.class);
+ var hostCtx = RestContext.getGlobalRegistry().get(HostSeedsCharset.class);
+ var childCtx = hostCtx.getRestChildren().asMap().get("nocharset");
+ assertNotNull(childCtx);
+ var opCtx = childCtx.getRestOperations().getOpContexts().get(0);
+ assertEquals(StandardCharsets.UTF_16, opCtx.getDefaultCharset(),
+ "Child with no defaultCharset declaration must receive the host's seeded utf-16");
+ }
+
+ @Rest(path="/owncharset", defaultCharset="utf-16")
+ public static class ChildWithOwnCharset {
+ @RestGet(path="/me") public String me() { return "me"; }
+ }
+
+ @Rest(childrenDefs=@Child(type=ChildWithOwnCharset.class, defaultCharset="iso-8859-1"))
+ public static class HostSeedsCharsetButChildWins extends BasicRestServletGroup {
+ private static final long serialVersionUID = 1L;
+ }
+
+ @Test void a02_charset_childsOwnDeclarationWins() {
+ MockRestClient.buildLax(HostSeedsCharsetButChildWins.class);
+ var hostCtx = RestContext.getGlobalRegistry().get(HostSeedsCharsetButChildWins.class);
+ var childCtx = hostCtx.getRestChildren().asMap().get("owncharset");
+ assertNotNull(childCtx);
+ var opCtx = childCtx.getRestOperations().getOpContexts().get(0);
+ assertEquals(StandardCharsets.UTF_16, opCtx.getDefaultCharset(),
+ "Child's own explicit defaultCharset must win over the host's seeded iso-8859-1");
+ }
+
+ //==================================================================================================================
+ // maxInput -- true child-wins (via mergeReplacedStringAttribute); seed-applies-when-silent already covered by
+ // ChildContext_Seed_Test.a04 (seed vs. DefaultConfig fallback) -- this file adds the child-wins-when-explicit half.
+ //==================================================================================================================
+
+ @Rest(path="/ownmax", maxInput="111")
+ public static class ChildWithOwnMaxInput {
+ @RestGet(path="/me") public String me() { return "me"; }
+ }
+
+ @Rest(childrenDefs=@Child(type=ChildWithOwnMaxInput.class, maxInput="222"))
+ public static class HostSeedsMaxInputButChildWins extends BasicRestServletGroup {
+ private static final long serialVersionUID = 1L;
+ }
+
+ @Test void a03_maxInput_childsOwnDeclarationWins() {
+ MockRestClient.buildLax(HostSeedsMaxInputButChildWins.class);
+ var hostCtx = RestContext.getGlobalRegistry().get(HostSeedsMaxInputButChildWins.class);
+ var childCtx = hostCtx.getRestChildren().asMap().get("ownmax");
+ assertNotNull(childCtx);
+ var opCtx = childCtx.getRestOperations().getOpContexts().get(0);
+ assertEquals(111L, opCtx.getMaxInput(),
+ "Child's own explicit maxInput must win over the host's seeded 222");
+ }
+}
diff --git a/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/ChildAnnotation_Test.java b/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/ChildAnnotation_Test.java
new file mode 100644
index 0000000000..5fd543d0ce
--- /dev/null
+++ b/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/ChildAnnotation_Test.java
@@ -0,0 +1,118 @@
+/*
+ * 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.server;
+
+import static org.apache.juneau.BasicTestUtils.*;
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.marshall.uon.*;
+import org.apache.juneau.rest.server.auth.*;
+import org.apache.juneau.rest.server.converter.*;
+import org.apache.juneau.rest.server.logger.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Tests for the {@link ChildAnnotation} companion (programmatic builder + synthetic impl) and equivalency with
+ * the declarative {@link Child @Child} annotation form.
+ */
+@SuppressWarnings({
+ "unchecked" // Generic Class<? extends X>[] varargs in the builder slot setters are populated with single literals in tests.
+})
+class ChildAnnotation_Test extends TestBase {
+
+ public static class FooChild {}
+
+ private static ChildAnnotation.Builder fullBuilder() {
+ return ChildAnnotation.create()
+ .type(FooChild.class)
+ .guards(BearerTokenGuard.class)
+ .roleGuard("admin")
+ .rolesDeclared("admin,user")
+ .converters(Traversable.class)
+ .callLogger(BasicCallLogger.class)
+ .partSerializer(UonSerializer.class)
+ .partParser(UonParser.class)
+ .debug(null) // null coalesces to DebugAnnotation.DEFAULT
+ .defaultCharset("utf-8")
+ .maxInput("1M");
+ }
+
+ Child a1 = fullBuilder().build();
+ Child a2 = fullBuilder().build();
+
+ @Test void a01_stringAccessors() {
+ assertEquals(FooChild.class, a1.type());
+ assertEquals("admin", a1.roleGuard());
+ assertEquals("admin,user", a1.rolesDeclared());
+ assertEquals("utf-8", a1.defaultCharset());
+ assertEquals("1M", a1.maxInput());
+ }
+
+ @Test void a01b_classAndAnnotationAccessors() {
+ assertEquals(BearerTokenGuard.class, a1.guards()[0]);
+ assertEquals(Traversable.class, a1.converters()[0]);
+ assertEquals(BasicCallLogger.class, a1.callLogger());
+ assertEquals(UonSerializer.class, a1.partSerializer());
+ assertEquals(UonParser.class, a1.partParser());
+ // debug(null) coalesced to the default @Debug.
+ assertEquals(DebugAnnotation.DEFAULT, a1.debug());
+ }
+
+ @Test void a02_testEquivalency() {
+ assertEquals(a2, a1);
+ assertNotEqualsAny(a1.hashCode(), 0, -1);
+ assertEquals(a1.hashCode(), a2.hashCode());
+ }
+
+ @Test void a03_defaultInstance() {
+ var d = ChildAnnotation.DEFAULT;
+ assertEquals("", d.roleGuard());
+ assertEquals(0, d.guards().length);
+ }
+
+ @Test void a04_debugNonNullValueKept() {
+ // The non-null branch of debug(Debug): a supplied @Debug is kept as-is (not coalesced to DEFAULT).
+ var dbg = DebugAnnotation.create().value("always").build();
+ var m = ChildAnnotation.create().type(FooChild.class).debug(dbg).build();
+ assertEquals(dbg, m.debug());
+ assertEquals("always", m.debug().value());
+ }
+
+ // Comparison with the declarative @Child form.
+
+ @Rest(childrenDefs=@Child(
+ type=FooChild.class,
+ guards=BearerTokenGuard.class,
+ roleGuard="admin",
+ rolesDeclared="admin,user",
+ converters=Traversable.class,
+ callLogger=BasicCallLogger.class,
+ partSerializer=UonSerializer.class,
+ partParser=UonParser.class,
+ defaultCharset="utf-8",
+ maxInput="1M"
+ ))
+ public static class D1 {}
+
+ @Test void d01_comparisonWithDeclarativeAnnotation() {
+ // The builder-produced a1 must be equal+hashCode-equal to the declarative @Child form (same slots).
+ var d1 = D1.class.getAnnotationsByType(Rest.class)[0].childrenDefs()[0];
+ assertEquals(a1, d1);
+ assertEquals(a1.hashCode(), d1.hashCode());
+ }
+}
diff --git a/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/LazyChildren_ChildDefs_Test.java b/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/LazyChildren_ChildDefs_Test.java
new file mode 100644
index 0000000000..e06545d2ca
--- /dev/null
+++ b/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/LazyChildren_ChildDefs_Test.java
@@ -0,0 +1,78 @@
+/*
+ * 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.server;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.rest.mock.classic.*;
+import org.apache.juneau.rest.server.logger.*;
+import org.apache.juneau.rest.server.servlet.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Phase 6 — proves a {@code @Rest(lazyChildren="true", childrenDefs=@Child(...))} child receives its
+ * host-seeded settings on first-request materialization, not just when eagerly built. Mirrors
+ * {@link LazyChildren_Test}'s style/package (same package so it lives alongside the base lazy-children suite).
+ */
+class LazyChildren_ChildDefs_Test extends TestBase {
+
+ public static class SeedLogger extends CallLogger {
+ public SeedLogger(org.apache.juneau.commons.inject.BeanStore bs) { super(bs); }
+ }
+
+ @Rest(path="/lazychild")
+ public static class LazyChildResource {
+ @RestGet("/ping") public String ping() { return "lazy-pong"; }
+ }
+
+ @Rest(
+ path="/root",
+ lazyChildren="true",
+ childrenDefs=@Child(type=LazyChildResource.class, callLogger=SeedLogger.class)
+ )
+ public static class LazySeedParent extends BasicRestServletGroup {
+ private static final long serialVersionUID = 1L;
+ }
+
+ @Test void a01_seedAppliesAfterLazyMaterialization() throws Exception {
+ var parent = new LazySeedParent();
+ var client = MockRestClient.createLax(parent).build();
+ var rc = parent.getContext();
+
+ assertTrue(rc.isLazyChildren());
+
+ // Before first request: registered for routing but not yet materialized.
+ var entries = rc.getRestChildren().getLazyEntries();
+ var entry = entries.get("lazychild");
+ assertNotNull(entry, "Lazy entry key should be 'lazychild'");
+ assertFalse(entry.isMaterialized(), "Should not be materialized before first request");
+
+ // First request triggers materialization.
+ client.get("/lazychild/ping").run().assertStatus(200).assertContent("lazy-pong");
+ assertTrue(entry.isMaterialized(), "Should be materialized after first request");
+
+ // The materialized context must carry the host's seeded callLogger -- proving the ResolvedChild
+ // (including its @Child seed) survived deferred construction, not just eager construction.
+ // (Materialized lazy children live on the LazyChildEntry itself, not in RestChildren.asMap() --
+ // that map holds only eagerly-built children.)
+ var childCtx = entry.materialized;
+ assertNotNull(childCtx);
+ assertInstanceOf(SeedLogger.class, childCtx.getCallLogger(),
+ "Lazily-materialized child must receive the host's seeded SeedLogger, same as an eager child would");
+ }
+}
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/Child.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/Child.java
new file mode 100644
index 0000000000..1edce8670f
--- /dev/null
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/Child.java
@@ -0,0 +1,165 @@
+/*
+ * 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.server;
+
+import static java.lang.annotation.RetentionPolicy.*;
+
+import java.lang.annotation.*;
+
+import org.apache.juneau.marshall.httppart.*;
+import org.apache.juneau.rest.server.converter.*;
+import org.apache.juneau.rest.server.guard.*;
+import org.apache.juneau.rest.server.logger.*;
+
+/**
+ * Host-side setting seed for the {@link Rest#childrenDefs() @Rest(childrenDefs=...)} attribute.
+ *
+ * <p>
+ * Declares a routed child class <b>and</b> lets the <i>host</i> seed a curated set of {@code @Rest}-level
+ * settings onto that child's otherwise-isolated {@link RestContext} — the child-resource analog of
+ * {@link Rest#mixinDefs() @Rest(mixinDefs=...)}. This is the host-side complement to
+ * {@link Rest#children() @Rest(children=...)} (which takes bare classes and offers no seed hook).
+ *
+ * <h5 class='section'>Example:</h5>
+ * <p class='bjava'>
+ * <ja>@Rest</ja>(
+ * childrenDefs=<ja>@Child</ja>(type=FooChild.<jk>class</jk>, callLogger=SeedLogger.<jk>class</jk>)
+ * )
+ * <jk>public class</jk> MyResource { ... }
+ * </p>
+ *
+ * <h5 class='section'>Seed semantics</h5>
+ * <p>
+ * Unlike {@link Mixin @Mixin} overrides (which win over an <i>inherited</i> chain), children are
+ * <b>isolated</b> from the host's resolution chain by design, so a {@code @Child} seed doesn't override
+ * anything — it seeds settings onto an otherwise-isolated child context:
+ * <ul>
+ * <li><b>Additive-security</b> ({@code guards}, {@code converters}, {@code roleGuard}, {@code rolesDeclared})
+ * — the host contributes, the child can't remove or weaken it (list-shaped members prepend; the two
+ * role-based members AND-stack alongside the child's own value).
+ * <li><b>Child-wins scalars</b> ({@code callLogger}, {@code partSerializer}, {@code partParser}, {@code debug},
+ * {@code defaultCharset}, {@code maxInput}) — the seed is a default/fallback; the child's own explicit
+ * {@code @Rest} declaration wins when present.
+ * </ul>
+ *
+ * <p>
+ * A child's own {@code @Rest(noInherit="<property>")} cuts the corresponding {@code @Child} seed too,
+ * for both buckets above — the child always stays in full control of its own configuration.
+ *
+ * <p>
+ * There is no {@code noInherit()} member on {@code @Child} itself (nothing to cut in an isolated context), and
+ * no {@code path()}/{@code paths()} re-mount member (the child's own {@code @Rest(path)} stays authoritative).
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ * <li class='ja'>{@link Rest#childrenDefs()}
+ * <li class='ja'>{@link Rest#children()}
+ * </ul>
+ *
+ * @since 10.0.0
+ */
+@Target({})
+@Retention(RUNTIME)
+public @interface Child {
+
+ /**
+ * The child class to route.
+ *
+ * <p>
+ * Required. Equivalent to a bare entry in {@link Rest#children()}, but with the seed slots below.
+ *
+ * @return The child class.
+ */
+ Class<?> type();
+
+ //-----------------------------------------------------------------------------------------------------------------
+ // Additive-security seed slots — host contributes, child can't remove or weaken.
+ //-----------------------------------------------------------------------------------------------------------------
+
+ /**
+ * Host-seeded {@link Rest#guards() guards} for this child's endpoints (prepended before the child's own).
+ *
+ * @return The annotation value.
+ */
+ Class<? extends RestGuard>[] guards() default {};
+
+ /**
+ * Host-seeded {@link Rest#converters() converters} for this child's endpoints (prepended before the child's own).
+ *
+ * @return The annotation value.
+ */
+ Class<? extends RestConverter>[] converters() default {};
+
+ /**
+ * Host-seeded {@link Rest#roleGuard() roleGuard} for this child's endpoints (AND-stacks with the child's own).
+ *
+ * @return The annotation value.
+ */
+ String roleGuard() default "";
+
+ /**
+ * Host-seeded {@link Rest#rolesDeclared() rolesDeclared} for this child's endpoints (AND-stacks with the child's own).
+ *
+ * @return The annotation value.
+ */
+ String rolesDeclared() default "";
+
+ //-----------------------------------------------------------------------------------------------------------------
+ // Child-wins scalar seed slots — seed is a default/fallback; the child's own explicit value wins.
+ //-----------------------------------------------------------------------------------------------------------------
+
+ /**
+ * Host-seeded {@link Rest#callLogger() callLogger} default for this child's endpoints.
+ *
+ * @return The annotation value.
+ */
+ Class<? extends CallLogger> callLogger() default CallLogger.Void.class;
+
+ /**
+ * Host-seeded {@link Rest#partSerializer() partSerializer} default for this child's endpoints.
+ *
+ * @return The annotation value.
+ */
+ Class<? extends HttpPartSerializer> partSerializer() default HttpPartSerializer.Void.class;
+
+ /**
+ * Host-seeded {@link Rest#partParser() partParser} default for this child's endpoints.
+ *
+ * @return The annotation value.
+ */
+ Class<? extends HttpPartParser> partParser() default HttpPartParser.Void.class;
+
+ /**
+ * Host-seeded {@link Rest#debug() debug} default for this child's endpoints (each sub-field independently seeded).
+ *
+ * @return The annotation value.
+ */
+ Debug debug() default @Debug;
+
+ /**
+ * Host-seeded {@link Rest#defaultCharset() defaultCharset} default for this child's endpoints.
+ *
+ * @return The annotation value.
+ */
+ String defaultCharset() default "";
+
+ /**
+ * Host-seeded {@link Rest#maxInput() maxInput} default for this child's endpoints.
+ *
+ * @return The annotation value.
+ */
+ String maxInput() default "";
+}
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/ChildAnnotation.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/ChildAnnotation.java
new file mode 100644
index 0000000000..2c9cb127e3
--- /dev/null
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/ChildAnnotation.java
@@ -0,0 +1,225 @@
+/*
+ * 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.server;
+
+import static org.apache.juneau.commons.utils.Shorts.*;
+
+import org.apache.juneau.commons.*;
+import org.apache.juneau.marshall.httppart.*;
+import org.apache.juneau.rest.server.converter.*;
+import org.apache.juneau.rest.server.guard.*;
+import org.apache.juneau.rest.server.logger.*;
+
+/**
+ * Utility classes and methods for the {@link Child @Child} annotation.
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ * <li class='ja'>{@link Child}
+ * <li class='ja'>{@link Rest#childrenDefs()}
+ * </ul>
+ *
+ * @since 10.0.0
+ */
+public class ChildAnnotation {
+
+ /**
+ * Prevents instantiation.
+ */
+ private ChildAnnotation() {}
+
+ /**
+ * Builder class.
+ */
+ @SuppressWarnings({
+ "unchecked" // Type erasure requires cast for generic class-array fields.
+ })
+ public static class Builder extends AnnotationObject.Builder {
+
+ Class<?> type = Object.class;
+ Class<? extends RestGuard>[] guards = new Class[0];
+ Class<? extends RestConverter>[] converters = new Class[0];
+ String roleGuard = "";
+ String rolesDeclared = "";
+ Class<? extends CallLogger> callLogger = CallLogger.Void.class;
+ Class<? extends HttpPartSerializer> partSerializer = HttpPartSerializer.Void.class;
+ Class<? extends HttpPartParser> partParser = HttpPartParser.Void.class;
+ Debug debug = DebugAnnotation.DEFAULT;
+ String defaultCharset = "";
+ String maxInput = "";
+
+ /**
+ * Constructor.
+ */
+ protected Builder() {
+ super(Child.class);
+ }
+
+ /**
+ * Sets the {@link Child#type()} property on this annotation.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ public Builder type(Class<?> value) { type = value; return this; }
+
+ /**
+ * Sets the {@link Child#guards()} property on this annotation.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ public Builder guards(Class<? extends RestGuard>... value) { guards = value; return this; }
+
+ /**
+ * Sets the {@link Child#converters()} property on this annotation.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ public Builder converters(Class<? extends RestConverter>... value) { converters = value; return this; }
+
+ /**
+ * Sets the {@link Child#roleGuard()} property on this annotation.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ public Builder roleGuard(String value) { roleGuard = value; return this; }
+
+ /**
+ * Sets the {@link Child#rolesDeclared()} property on this annotation.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ public Builder rolesDeclared(String value) { rolesDeclared = value; return this; }
+
+ /**
+ * Sets the {@link Child#callLogger()} property on this annotation.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ public Builder callLogger(Class<? extends CallLogger> value) { callLogger = value; return this; }
+
+ /**
+ * Sets the {@link Child#partSerializer()} property on this annotation.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ public Builder partSerializer(Class<? extends HttpPartSerializer> value) { partSerializer = value; return this; }
+
+ /**
+ * Sets the {@link Child#partParser()} property on this annotation.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ public Builder partParser(Class<? extends HttpPartParser> value) { partParser = value; return this; }
+
+ /**
+ * Sets the {@link Child#debug()} property on this annotation.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ public Builder debug(Debug value) { debug = value == null ? DebugAnnotation.DEFAULT : value; return this; }
+
+ /**
+ * Sets the {@link Child#defaultCharset()} property on this annotation.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ public Builder defaultCharset(String value) { defaultCharset = value; return this; }
+
+ /**
+ * Sets the {@link Child#maxInput()} property on this annotation.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ public Builder maxInput(String value) { maxInput = value; return this; }
+
+ /**
+ * Instantiates a new {@link Child @Child} object initialized with this builder.
+ *
+ * @return A new {@link Child @Child} object.
+ */
+ public Child build() {
+ return new Impl(this);
+ }
+ }
+
+ @SuppressWarnings({
+ "java:S2160" // equals() inherited from AnnotationObject compares all annotation interface methods; subclass fields are accessed via those methods.
+ })
+ private static class Impl extends AnnotationObject implements Child {
+
+ private final Class<?> type;
+ private final Class<? extends RestGuard>[] guards;
+ private final Class<? extends RestConverter>[] converters;
+ private final String roleGuard;
+ private final String rolesDeclared;
+ private final Class<? extends CallLogger> callLogger;
+ private final Class<? extends HttpPartSerializer> partSerializer;
+ private final Class<? extends HttpPartParser> partParser;
+ private final Debug debug;
+ private final String defaultCharset;
+ private final String maxInput;
+
+ Impl(ChildAnnotation.Builder b) {
+ super(b);
+ type = b.type;
+ guards = cp(b.guards);
+ converters = cp(b.converters);
+ roleGuard = b.roleGuard;
+ rolesDeclared = b.rolesDeclared;
+ callLogger = b.callLogger;
+ partSerializer = b.partSerializer;
+ partParser = b.partParser;
+ debug = b.debug;
+ defaultCharset = b.defaultCharset;
+ maxInput = b.maxInput;
+ }
+
+ @Override /* Overridden from Child */ public Class<?> type() { return type; }
+ @Override /* Overridden from Child */ public Class<? extends RestGuard>[] guards() { return cp(guards); }
+ @Override /* Overridden from Child */ public Class<? extends RestConverter>[] converters() { return cp(converters); }
+ @Override /* Overridden from Child */ public String roleGuard() { return roleGuard; }
+ @Override /* Overridden from Child */ public String rolesDeclared() { return rolesDeclared; }
+ @Override /* Overridden from Child */ public Class<? extends CallLogger> callLogger() { return callLogger; }
+ @Override /* Overridden from Child */ public Class<? extends HttpPartSerializer> partSerializer() { return partSerializer; }
+ @Override /* Overridden from Child */ public Class<? extends HttpPartParser> partParser() { return partParser; }
+ @Override /* Overridden from Child */ public Debug debug() { return debug; }
+ @Override /* Overridden from Child */ public String defaultCharset() { return defaultCharset; }
+ @Override /* Overridden from Child */ public String maxInput() { return maxInput; }
+ }
+
+ /**
+ * Builder creator.
+ *
+ * @return A new builder.
+ */
+ public static Builder create() {
+ return new Builder();
+ }
+
+ /** Default {@link Child} instance (empty seed slots; {@code type=Object.class}). */
+ public static final Child DEFAULT = create().build();
+}
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/Rest.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/Rest.java
index 17c316bd53..266fe2bb51 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/Rest.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/Rest.java
@@ -223,6 +223,47 @@ public @interface Rest {
*/
Class<?>[] children() default {};
+ /**
+ * Rich child definitions — the host-side seed form of {@link #children()}.
+ *
+ * <p>
+ * Each {@link Child @Child} entry names a child class (via {@link Child#type()}) <b>and</b> lets the host
+ * seed a curated set of {@code @Rest}-level settings onto that child's otherwise-isolated
+ * {@link RestContext} — guards, role guards, a call logger, part serializer/parser, debug, and a
+ * handful of default-scalar settings — without editing the child class.
+ *
+ * <p>
+ * Unlike {@link #mixinDefs() mixinDefs}, this does not override an inherited chain (children never inherit
+ * from the host); it seeds settings onto an isolated context. Additive-security seed members ({@code guards},
+ * {@code converters}, {@code roleGuard}, {@code rolesDeclared}) can't be removed or weakened by the child;
+ * the remaining scalar seed members are a fallback the child's own explicit {@code @Rest} declaration wins
+ * over. See {@link Child} for the full semantics.
+ *
+ * <h5 class='section'>Example:</h5>
+ * <p class='bjava'>
+ * <ja>@Rest</ja>(
+ * childrenDefs=<ja>@Child</ja>(type=FooChild.<jk>class</jk>, callLogger=SeedLogger.<jk>class</jk>)
+ * )
+ * <jk>public class</jk> MyResource { ... }
+ * </p>
+ *
+ * <p>
+ * This attribute is additive to {@link #children()}: bare-class entries in {@code children()} and rich
+ * entries here are discovered together (bare classes first, then {@code childrenDefs}). A
+ * {@code @Child(type=X.class)} with no seed members is the exact equivalent of a bare
+ * {@code children=X.class} entry.
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ * <li class='ja'>{@link Child}
+ * <li class='jm'>{@link #children()}
+ * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/ChildResources#host-side-seeding-with-child-1000">Child Resources — Host-side seeding with @Child</a>
+ * </ul>
+ *
+ * @return The annotation value.
+ * @since 10.0.0
+ */
+ Child[] childrenDefs() default {};
+
/**
* REST mixins.
*
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/RestAnnotation.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/RestAnnotation.java
index 26b241119c..b654d6f7c1 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/RestAnnotation.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/RestAnnotation.java
@@ -77,6 +77,7 @@ public class RestAnnotation {
private Debug debug = DebugAnnotation.DEFAULT;
private Class<? extends Serializer>[] serializers = new Class[0];
private Class<?>[] children = {};
+ private Child[] childrenDefs = {};
private Class<?>[] mixins = {};
private Mixin[] mixinDefs = {};
private Class<?>[] parsers = {};
@@ -216,6 +217,17 @@ public class RestAnnotation {
return this;
}
+ /**
+ * Sets the {@link Rest#childrenDefs()} property on this annotation.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ public Builder childrenDefs(Child...value) {
+ childrenDefs = value;
+ return this;
+ }
+
/**
* Sets the {@link Rest#mixins()} property on this annotation.
*
@@ -806,6 +818,7 @@ public class RestAnnotation {
private final Debug debug;
private final Class<? extends Serializer>[] serializers;
private final Class<?>[] children;
+ private final Child[] childrenDefs;
private final Class<?>[] mixins;
private final Mixin[] mixinDefs;
private final Class<?>[] parsers;
@@ -862,6 +875,7 @@ public class RestAnnotation {
callLogger = b.callLogger;
authenticator = b.authenticator;
children = cp(b.children);
+ childrenDefs = cp(b.childrenDefs);
mixins = cp(b.mixins);
mixinDefs = cp(b.mixinDefs);
clientVersionHeader = b.clientVersionHeader;
@@ -947,6 +961,11 @@ public class RestAnnotation {
return children;
}
+ @Override /* Overridden from Rest */
+ public Child[] childrenDefs() {
+ return childrenDefs;
+ }
+
@Override /* Overridden from Rest */
public Class<?>[] mixins() {
return mixins;
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/RestChildren.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/RestChildren.java
index 47dab35d5c..abb3e6b191 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/RestChildren.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/RestChildren.java
@@ -71,6 +71,7 @@ public class RestChildren {
*/
static class LazyChildEntry {
+ final RestContext.ResolvedChild resolvedChild;
final Class<?> resourceClass;
final String path;
final UrlPathMatcher pathMatcher;
@@ -86,8 +87,9 @@ public class RestChildren {
private final Object lock = new Object();
- LazyChildEntry(Class<?> resourceClass, String path, RestContext parent, BeanStore beanStore, ServletConfig servletConfig) {
- this.resourceClass = resourceClass;
+ LazyChildEntry(RestContext.ResolvedChild resolvedChild, String path, RestContext parent, BeanStore beanStore, ServletConfig servletConfig) {
+ this.resolvedChild = resolvedChild;
+ this.resourceClass = resolvedChild.type();
this.path = path;
var p = path;
if (! p.endsWith("/*"))
@@ -120,7 +122,7 @@ public class RestChildren {
return rc;
LOGGER.info(() -> "Lazy REST child materializing: " + resourceClass.getName() + " at path '" + path + "'");
try {
- rc = buildChildContext(parent, beanStore, servletConfig, resourceClass, null, "");
+ rc = buildChildContext(parent, beanStore, servletConfig, resolvedChild, null, "");
rc.postInit();
rc.postInitChildFirst();
} catch (Exception e) {
@@ -215,14 +217,41 @@ public class RestChildren {
* The path is pre-resolved from the resource class so that routing works immediately at parent startup.
* The full {@link RestContext} is built on the first matching request.
*
+ * <p>
+ * Delegates to {@link #addLazy(RestContext.ResolvedChild, String)} with a bare (no-seed) declaration —
+ * this public overload's signature is preserved unchanged (it has no external-caller-visible way to
+ * express a host-declared {@code @Child} seed; see that overload for the seed-carrying entry point used
+ * internally by {@link RestContext}'s {@code childrenDefs} discovery).
+ *
* @param resourceClass The resource class to materialize lazily. Must not be {@code null}.
* @param path The pre-resolved path prefix (without leading slash). Use {@code ""} to read from
* {@link Rest#path() @Rest(path)} on the resource class.
* @return This object.
*/
public Builder addLazy(Class<?> resourceClass, String path) {
+ return addLazy(RestContext.ResolvedChild.ofBare(resourceClass), path);
+ }
+
+ /**
+ * Registers a lazy child resource entry carrying a normalized child declaration, which may include a
+ * host-declared {@code @Child} seed.
+ *
+ * <p>
+ * The path is pre-resolved from the resource class so that routing works immediately at parent startup.
+ * The full {@link RestContext} is built on the first matching request, with the seed (if any) threaded
+ * through to {@link RestChildren#buildChildContext} at that point — so a lazily-materialized child
+ * receives its host-seeded settings identically to an eagerly-built one.
+ *
+ * @param resolvedChild The normalized child declaration (class plus any host-declared seed). Must not be
+ * {@code null}.
+ * @param path The pre-resolved path prefix (without leading slash). Use {@code ""} to read from
+ * {@link Rest#path() @Rest(path)} on the resource class.
+ * @return This object.
+ */
+ Builder addLazy(RestContext.ResolvedChild resolvedChild, String path) {
+ var resourceClass = resolvedChild.type();
var resolvedPath = isNotEmpty(path) ? trimLeadingSlashes(path) : LazyChildEntry.resolvePathForClass(resourceClass);
- lazyList.add(new LazyChildEntry(resourceClass, resolvedPath, parent, beanStore, servletConfig));
+ lazyList.add(new LazyChildEntry(resolvedChild, resolvedPath, parent, beanStore, servletConfig));
return this;
}
@@ -553,7 +582,8 @@ public class RestChildren {
* @param parent The parent {@link RestContext}. Must not be {@code null}.
* @param beanStore The bean store to resolve instances and dependencies from. Must not be {@code null}.
* @param servletConfig The {@link ServletConfig} to pass through to the child. May be {@code null}.
- * @param resourceClass The resource class. Must not be {@code null}.
+ * @param resolvedChild The normalized child declaration (class plus any host-declared {@code @Child} seed).
+ * Must not be {@code null}.
* @param resourceInstance A pre-supplied instance of the resource. If {@code null}, the instance is resolved
* from the bean store or freshly instantiated via {@link BeanInstantiator}.
* @param pathOverride An explicit path segment (relative to the parent). Use {@code ""} to read the path from
@@ -565,9 +595,10 @@ public class RestChildren {
RestContext parent,
BeanStore beanStore,
ServletConfig servletConfig,
- Class<?> resourceClass,
+ RestContext.ResolvedChild resolvedChild,
Object resourceInstance,
String pathOverride) throws Exception {
+ var resourceClass = resolvedChild.type();
Supplier<?> so;
if (resourceInstance != null) {
final Object r = resourceInstance;
@@ -578,7 +609,7 @@ public class RestChildren {
Object o = BeanInstantiator.of(resourceClass, beanStore).run();
so = () -> o;
}
- var cc = new RestContext(new RestContext.Args(resourceClass, parent, servletConfig, so, pathOverride, null, null, null, RestContext.ContextKind.CHILD));
+ var cc = new RestContext(new RestContext.Args(resourceClass, parent, servletConfig, so, pathOverride, null, null, null, new RestContext.ContextKind.Child(resolvedChild)));
var mi = ClassInfo.of(so.get())
.getMethod(x -> x.hasName("setContext") && x.hasParameterTypes(RestContext.class))
.orElse(null);
@@ -592,7 +623,8 @@ public class RestChildren {
throw isex("Cannot add a child to a RestChildren that was not initialized with a parent RestContext.");
RestContext cc;
try {
- cc = buildChildContext(parent, beanStore, servletConfig, resourceClass, resourceInstance, pathOverride);
+ // The dynamic-add API has no seed — a bare ResolvedChild preserves its existing no-override behavior.
+ cc = buildChildContext(parent, beanStore, servletConfig, RestContext.ResolvedChild.ofBare(resourceClass), resourceInstance, pathOverride);
} catch (Exception e) {
throw new ServletException("Failed to build child REST context for " + resourceClass.getName(), unwrapThrowable(e));
}
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/RestContext.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/RestContext.java
index 5ebaacb114..9386a298ce 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/RestContext.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/RestContext.java
@@ -222,9 +222,10 @@ public class RestContext extends Context {
* {@code getPaths()} getter, then {@link Rest#paths()} (SVL-resolved per element and comma-split).
* An empty array explicitly clears the mount list (no top-level mounts).
* @param kind The build-time {@link ContextKind kind} of this context — {@link ContextKind#ROOT ROOT} for a
- * top-level/host/mock resource, {@link ContextKind#CHILD CHILD} for a {@link Rest#children() @Rest(children)}
- * sub-resource (isolated resolution), or a {@link ContextKind.Mixin MIXIN} carrying the {@link ResolvedMixin}
- * that produced a per-mixin sub-context (parent-linked to the host so that
+ * top-level/host/mock resource, a {@link ContextKind.Child CHILD} carrying the {@link ResolvedChild} for a
+ * {@link Rest#children() @Rest(children)}/{@link Rest#childrenDefs() childrenDefs} sub-resource (isolated
+ * resolution, with an optional host-seeded payload), or a {@link ContextKind.Mixin MIXIN} carrying the
+ * {@link ResolvedMixin} that produced a per-mixin sub-context (parent-linked to the host so that
* {@link RestContext#getRestAnnotationsForProperty(String) annotation-property walks} prepend the host's
* {@code @Rest} chain before the mixin's own).
*
@@ -354,6 +355,62 @@ public class RestContext extends Context {
}
}
+ /**
+ * Normalized carrier for a routed child, produced by discovery from either a bare {@link Rest#children()}
+ * class or a rich {@link Rest#childrenDefs() @Child} declaration — the exact structural analog of
+ * {@link ResolvedMixin}.
+ *
+ * <p>
+ * A bare class is normalized to {@code new ResolvedChild(type, ChildAnnotation.DEFAULT)} (i.e.
+ * {@code @Child(type=X)} with an empty seed), so a bare class and an empty-seed {@code @Child} are
+ * structurally identical — the same equivalence {@link ResolvedMixin} establishes for mixins.
+ *
+ * @param type The child class.
+ * @param seed The host-declared seed annotation, or {@link ChildAnnotation#DEFAULT} for a bare class.
+ *
+ * @since 10.0.0
+ */
+ public static record ResolvedChild(Class<?> type, Child seed) {
+
+ /**
+ * Compact canonical constructor — validates required components and null-coalesces {@code seed}.
+ */
+ public ResolvedChild {
+ assertArgNotNull("type", type);
+ if (seed == null)
+ seed = ChildAnnotation.DEFAULT;
+ }
+
+ /**
+ * Normalizes a bare child class to a {@link ResolvedChild} with no seed.
+ *
+ * @param type The child class.
+ * @return A {@link ResolvedChild} carrying {@link ChildAnnotation#DEFAULT} as its seed.
+ */
+ static ResolvedChild ofBare(Class<?> type) {
+ return new ResolvedChild(type, ChildAnnotation.DEFAULT);
+ }
+
+ /**
+ * Normalizes a rich {@code @Child} declaration to a {@link ResolvedChild}.
+ *
+ * @param c The {@code @Child} annotation.
+ * @return A {@link ResolvedChild} carrying {@code c} as its seed.
+ */
+ static ResolvedChild ofDef(Child c) {
+ return new ResolvedChild(c.type(), c);
+ }
+
+ /**
+ * Returns {@code true} when this child carries no host-declared seed (i.e. is equivalent to a bare class).
+ *
+ * @return {@code true} if the seed payload is {@link ChildAnnotation#DEFAULT}.
+ */
+ boolean hasNoSeed() {
+ return seed == ChildAnnotation.DEFAULT;
+ }
+ }
+
/**
* The build-time <i>kind</i> of a {@link RestContext} — which of the three construction flavors produced it.
*
@@ -368,11 +425,14 @@ public class RestContext extends Context {
* parent-linked to the host and <i>inherits</i> the host's {@code @Rest} annotation chain
* ({@link #getRestAnnotationsForProperty(String)} walks host→mixin), so {@code @Mixin} overrides
* <i>layer on top of</i> an inherited chain. A {@link Child} is <i>isolated</i> ({@link #HOST_ONLY_PROPERTIES}
- * includes {@code children}; children retain pre-10.0.0 isolated resolution with no parent walk). So a future
- * {@code @Child} would <i>seed</i> settings onto an isolated context rather than <i>override</i> an inherited
- * chain, and would have no {@code noInherit} analog — it must NOT reuse the mixin override-resolution path.
- * The {@code Child} variant is intentionally shaped so it can later widen to reference a {@code ResolvedChild}
- * (mirroring how {@code Mixin} references a {@code ResolvedMixin}) without re-plumbing this discriminator.
+ * includes {@link RestServerConstants#PROPERTY_children}; children retain pre-10.0.0 isolated resolution with
+ * no parent walk). A {@code @Child} seed therefore does not <i>override</i> an inherited chain the way a
+ * {@code @Mixin} override does — instead, {@link #computeRawRestAnnotations()} injects the seed's synthetic
+ * {@code @Rest} at the <b>least-derived</b> slot of the child's own (still fully isolated) annotation chain,
+ * between the child's real {@code @Rest} declarations and the synthesized {@link
+ * org.apache.juneau.rest.server.config.DefaultConfig DefaultConfig} fallback. This ranks the seed below
+ * everything the child itself declares (so the child's own explicit value wins for child-wins scalars, and
+ * the child's own {@code noInherit} cutoff naturally cuts the seed too) but above the framework defaults.
*
* @since 10.0.0
*/
@@ -381,8 +441,14 @@ public class RestContext extends Context {
/** A top-level (host) resource, mock context, or servlet-mounted resource. */
record Root() implements ContextKind {}
- /** A routed child resource (mounted via {@link Rest#children() @Rest(children)}); isolated resolution. */
- record Child() implements ContextKind {}
+ /**
+ * A routed child resource (mounted via {@link Rest#children()}/{@link Rest#childrenDefs()}); isolated
+ * resolution, with an optional host-seeded {@code @Child} payload injected at the least-derived slot of
+ * its own annotation chain (see the class javadoc above).
+ *
+ * @param def The normalized declaration that produced this sub-context — carries the host-declared seed payload.
+ */
+ record Child(ResolvedChild def) implements ContextKind {}
/**
* A per-mixin sub-context (composed via {@link Rest#mixins()}/{@link Rest#mixinDefs()}); inherits the host chain.
@@ -393,9 +459,6 @@ public class RestContext extends Context {
/** Shared {@link Root} singleton (the common case). */
ContextKind ROOT = new Root();
-
- /** Shared {@link Child} singleton (the path override travels in {@link Args#path()}, not here). */
- ContextKind CHILD = new Child();
}
/**
@@ -836,6 +899,8 @@ public class RestContext extends Context {
protected final ContextKind contextKind;
/** The normalized mixin declaration that produced this sub-context, or {@code null} when not a mixin context. */
protected final ResolvedMixin resolvedMixin;
+ /** The normalized child declaration that produced this sub-context, or {@code null} when not a child context. */
+ protected final ResolvedChild resolvedChild;
protected final String fullPath;
protected final String path;
protected final String[] paths;
@@ -875,6 +940,15 @@ public class RestContext extends Context {
*/
private ResolvedMixin resolvedMixinField() { return resolvedMixin; }
+ /**
+ * Field accessor for {@link #resolvedChild}, callable from memoizer field-initializer lambdas (which run
+ * before the blank-final field is read directly). Returns the normalized child declaration that produced
+ * this sub-context, or {@code null} when this is not a child context.
+ *
+ * @return The resolved child, or {@code null}.
+ */
+ private ResolvedChild resolvedChildField() { return resolvedChild; }
+
/**
* Creates the bean store for this context.
*
@@ -2390,15 +2464,61 @@ public class RestContext extends Context {
}
}
+ /**
+ * Discovers all children declared on this resource's {@code @Rest} chain, normalized to {@link ResolvedChild}.
+ *
+ * <p>
+ * Reads both {@link Rest#children() bare-class} and {@link Rest#childrenDefs() rich @Child} entries (bare
+ * first, then {@code childrenDefs}). First occurrence of a class wins for ordering; a rich {@code @Child}
+ * <b>upgrades</b> an earlier bare entry for the same class (a bare class is just {@code @Child(type=X)} with
+ * an empty seed, so upgrading only adds the host-declared seed).
+ *
+ * <p>
+ * Deliberately <b>non-recursive</b>, unlike {@link #getResolvedMixins()}: children do not flatten transitively
+ * to the host — each child resolves its own nested {@code @Rest(children=...)}/{@code childrenDefs} independently
+ * when its own {@link RestContext} is constructed.
+ *
+ * @return The ordered, de-duplicated normalized children, never {@code null}.
+ */
+ private Collection<ResolvedChild> getResolvedChildren() {
+ var out = new LinkedHashMap<Class<?>,ResolvedChild>();
+ getRestAnnotations().forEach(ai -> {
+ for (var c : ai.inner().children())
+ collectResolvedChild(ResolvedChild.ofBare(c), out);
+ for (var def : ai.inner().childrenDefs())
+ collectResolvedChild(ResolvedChild.ofDef(def), out);
+ });
+ return out.values();
+ }
+
+ private void collectResolvedChild(ResolvedChild rc, LinkedHashMap<Class<?>,ResolvedChild> out) {
+ var type = rc.type();
+ // type==null is defensive only — @Child.type() is required and a declarative @Child requires a class
+ // literal, so it is never null in practice. type==resourceClass() guards a self-reference (skip to
+ // avoid an infinite loop).
+ if (type == null || type == resourceClass()) // HTT: the null arm is not reachable via the annotation API.
+ return;
+ var existing = out.get(type);
+ if (existing != null) {
+ // Already discovered. A rich @Child upgrades a bare entry (in place, preserving position); otherwise
+ // the first occurrence wins. No recursion into the child's own nested children (see javadoc above).
+ if (existing.hasNoSeed() && ! rc.hasNoSeed())
+ out.put(type, rc);
+ return;
+ }
+ out.put(type, rc); // No recursion into type's own children — each child owns its own subtree.
+ }
+
/**
* The {@link RestChildren} for this resource — child {@link RestContext} instances registered via
- * {@link Rest#children() @Rest(children)}.
+ * {@link Rest#children() @Rest(children)}/{@link Rest#childrenDefs() childrenDefs}.
*
* <p>
- * Reads child classes directly from the {@code @Rest(children)} annotation chain (parent-to-child),
- * deduplicates, instantiates each child via the bean store, and builds a {@link RestContext} for each.
- * Eagerly initialized in the constructor (via an explicit {@code .get()} call inside the try-catch
- * block) so that any construction failure surfaces at initialization time rather than lazily.
+ * Reads children (bare and rich) directly from the {@code @Rest} annotation chain via
+ * {@link #getResolvedChildren()}, instantiates each child via the bean store, and builds a {@link RestContext}
+ * for each, threading through any host-declared {@code @Child} seed. Eagerly initialized in the constructor
+ * (via an explicit {@code .get()} call inside the try-catch block) so that any construction failure surfaces
+ * at initialization time rather than lazily.
*/
@SuppressWarnings({
"java:S3776" // cognitive complexity acceptable for child-context construction
@@ -2408,22 +2528,16 @@ public class RestContext extends Context {
var servletConfig = bs.getBean(ServletConfig.class).orElse(null);
var b = RestChildren.create(this, bs, servletConfig);
- // Collect child classes from @Rest(children) on the annotation chain (parent-to-child order).
- // Deduplicate so the same child class registered on both a parent and child annotation
- // doesn't create two contexts.
- var seen = new LinkedHashSet<Class<?>>();
- getRestAnnotations().forEach(ai -> seen.addAll(Arrays.asList(ai.inner().children())));
-
var lazy = isLazyChildren();
- for (var rc2 : seen) {
- if (rc2 == resourceClass())
- continue; // Guard against self-reference infinite loop.
+ for (var rc2 : getResolvedChildren()) {
if (lazy) {
// Lazy: register a routing stub now; defer full RestContext construction to first request.
+ // The stub carries the full ResolvedChild (including any host-declared @Child seed), so a
+ // lazily-materialized child receives its seed on first invocation identically to an eager one.
b.addLazy(rc2, "");
} else {
- // Eager (default): build the full child RestContext immediately.
+ // Eager (default): build the full child RestContext immediately, seed included.
b.add(RestChildren.buildChildContext(this, bs, servletConfig, rc2, null, ""));
}
}
@@ -2474,6 +2588,7 @@ public class RestContext extends Context {
contextKind = builder.args.kind();
isMixinContext = contextKind instanceof ContextKind.Mixin;
resolvedMixin = (contextKind instanceof ContextKind.Mixin contextKind2) ? contextKind2.def() : null;
+ resolvedChild = (contextKind instanceof ContextKind.Child contextKind3) ? contextKind3.def() : null;
resourceClass = builder.resourceClass;
var rs = new ResourceSupplier(resourceClass, assertArgNotNull("resource", builder.args.resource()));
resource = rs;
@@ -2880,6 +2995,51 @@ public class RestContext extends Context {
return b.build();
}
+ /**
+ * The host-declared {@code @Child} seed values translated into a synthetic {@link Rest} annotation, for a
+ * child sub-context that carries a seed. {@code null} for non-child contexts and for children with no seed
+ * (bare classes).
+ *
+ * <p>
+ * Built once at memoizer-init time (zero per-request cost). Unlike {@link #mixinOverrideAnnotation}, this is
+ * injected at the <b>least-derived</b> slot of {@link #computeRawRestAnnotations()} — below the child's own
+ * real {@code @Rest} declarations but above the synthesized {@code DefaultConfig} fallback — so the seed acts
+ * as a default/fallback the child's own explicit configuration wins over (see {@link ContextKind} javadoc).
+ */
+ private final Memoizer<AnnotationInfo<Rest>> childSeedAnnotation = memoizer(() -> {
+ var rc = resolvedChildField();
+ if (rc == null || rc.hasNoSeed())
+ return null;
+ var rest = buildChildSeedRest(rc.seed());
+ return AnnotationInfo.of(ClassInfo.of(getResourceClass()), rest);
+ });
+
+ /**
+ * Translates a host-declared {@link Child @Child} into a synthetic {@link Rest @Rest} carrying only its
+ * seed slots (the §3 scope subset — no {@code encoders}/{@code serializers}/{@code parsers}/
+ * {@code responseProcessors}/{@code restOpArgs}/{@code messages}/default headers or attributes/
+ * {@code produces}/{@code consumes}), so the standard {@code @Rest} resolution chain applies the seed uniformly.
+ *
+ * @param c The {@code @Child} seed declaration.
+ * @return A synthetic {@code @Rest} populated from {@code c}'s seed slots.
+ */
+ private static Rest buildChildSeedRest(Child c) {
+ // @formatter:off
+ var b = RestAnnotation.create()
+ .guards(c.guards())
+ .roleGuard(c.roleGuard())
+ .rolesDeclared(c.rolesDeclared())
+ .converters(c.converters())
+ .callLogger(c.callLogger())
+ .partSerializer(c.partSerializer())
+ .partParser(c.partParser())
+ .debug(c.debug())
+ .defaultCharset(c.defaultCharset())
+ .maxInput(c.maxInput());
+ // @formatter:on
+ return b.build();
+ }
+
/**
* Memoized list of every {@link Rest} annotation on the resource class and its supertypes, in child-to-parent order.
*
@@ -2906,7 +3066,16 @@ public class RestContext extends Context {
/**
* Computes the raw {@code @Rest} annotation chain (most-derived first), folding in the framework
- * {@code DefaultConfig} annotations for non-mixin contexts. Does not include the synthetic builder annotation.
+ * {@code DefaultConfig} annotations for non-mixin contexts, and — for a {@linkplain ContextKind.Child child}
+ * context carrying a host-declared {@code @Child} seed — injecting the seed's synthetic {@code @Rest} at the
+ * least-derived slot: {@code raw ++ [seed] ++ defaultConfigAnnotations}. This ranks the seed below everything
+ * the child itself declares (child-wins scalars resolve to the child's own value when present) but above the
+ * framework's {@code DefaultConfig} fallback (the seed is never masked by framework defaults). This holds
+ * uniformly whether {@code DefaultConfig} is separately synthesized (the common case) or already embedded
+ * within {@code raw} because the child class transitively implements {@code DefaultConfig} itself (e.g. by
+ * extending {@code BasicRestServlet}/{@code BasicRestServletGroup}) — in the embedded case, {@code raw} is
+ * split around the embedded {@code DefaultConfig} entry/entries so the seed still lands between the child's
+ * own declarations and that entry, rather than after it. Does not include the synthetic builder annotation.
*
* @return The raw annotation chain.
*/
@@ -2914,17 +3083,49 @@ public class RestContext extends Context {
var raw = getAnnotationProvider().find(Rest.class, ClassInfo.of(getResourceClass()));
if (isMixinContextField())
return raw;
- var hasDefaultConfig = raw.stream().anyMatch(ai -> ai.getAnnotatable() instanceof ClassInfo ci && DefaultConfig.class.equals(ci.inner()));
- if (hasDefaultConfig)
- return raw;
+ var seed = childSeedAnnotation.get(); // null for non-child contexts and no-seed children
+ // The framework's DefaultConfig fallback is synthesized/appended for EVERY non-mixin context that
+ // doesn't already transitively implement DefaultConfig itself — this happens unconditionally,
+ // regardless of whether this context also carries an @Child seed (that baseline behavior predates
+ // and is independent of @Child). Partition raw into "the resource's own real declarations" and any
+ // DefaultConfig entries already embedded within its own hierarchy (e.g. because it extends
+ // BasicRestServlet/BasicRestServletGroup, which implement DefaultConfig), so that when a seed IS
+ // present it can always be inserted between them — below everything the child itself declares, but
+ // above DefaultConfig's fallback values, whether that fallback is embedded here or synthesized below.
+ var embeddedDefaultConfig = raw.stream().filter(ai -> ai.getAnnotatable() instanceof ClassInfo ci && DefaultConfig.class.equals(ci.inner())).toList();
+ if (!embeddedDefaultConfig.isEmpty()) {
+ if (seed == null)
+ return raw;
+ var ownDeclarations = raw.stream().filter(ai -> !(ai.getAnnotatable() instanceof ClassInfo ci && DefaultConfig.class.equals(ci.inner()))).toList();
+ var combined = new ArrayList<AnnotationInfo<Rest>>(raw.size() + 1);
+ combined.addAll(ownDeclarations);
+ combined.add(seed);
+ combined.addAll(embeddedDefaultConfig);
+ return u(combined);
+ }
var defaultConfigAnnotations = getAnnotationProvider().find(Rest.class, ClassInfo.of(DefaultConfig.class));
if (defaultConfigAnnotations.isEmpty())
- return raw;
+ return seed == null ? raw : appendAnnotation(raw, seed);
var combined = new ArrayList<>(raw);
+ if (seed != null)
+ combined.add(seed);
combined.addAll(defaultConfigAnnotations);
return u(combined);
}
+ /**
+ * Returns a new unmodifiable list with {@code extra} appended after {@code base}.
+ *
+ * @param base The base list.
+ * @param extra The entry to append.
+ * @return An unmodifiable list of {@code base} followed by {@code extra}.
+ */
+ private static List<AnnotationInfo<Rest>> appendAnnotation(List<AnnotationInfo<Rest>> base, AnnotationInfo<Rest> extra) {
+ var combined = new ArrayList<>(base);
+ combined.add(extra);
+ return u(combined);
+ }
+
/**
* Prepends the synthetic builder-supplied {@code @Rest} annotation at the most-derived (child)
* position so its set members win in both child-first ({@code findFirst}) and parent-to-child
@@ -3281,7 +3482,7 @@ public class RestContext extends Context {
* preserves that invariant when a mixin sub-context walks annotations for these properties.
*/
private static final Set<String> HOST_ONLY_PROPERTIES = Set.of(
- PROPERTY_path, PROPERTY_paths, PROPERTY_mixins, "children"
+ PROPERTY_path, PROPERTY_paths, PROPERTY_mixins, PROPERTY_children
);
/**
@@ -3484,6 +3685,9 @@ public class RestContext extends Context {
* @param s The raw string. Can be {@code null}.
* @return The resolved string.
*/
+ @SuppressWarnings({
+ "java:S2259" // getVarResolver() is never null post-construction: registerFrameworkDefaults() always registers a VarResolver default supplier before the constructor returns.
+ })
protected String resolve(String s) {
return getVarResolver().resolve(s);
}
@@ -4144,6 +4348,9 @@ public class RestContext extends Context {
*
* @return The context statistics.
*/
+ @SuppressWarnings({
+ "java:S2259" // getMethodExecStore() is never null post-construction: registerFrameworkDefaults() always registers a MethodExecStore default supplier before the constructor returns.
+ })
public RestContextStats getStats() { return new RestContextStats(startTime, getMethodExecStore().getStatsByTotalTime()); }
/**
@@ -4548,6 +4755,9 @@ public class RestContext extends Context {
return this;
}
+ @SuppressWarnings({
+ "java:S2259" // mi is guarded by the nn(mi) check above; Sonar's flow analysis does not track nn() as a null guard.
+ })
private void initializeResourceContext(Object resource2) {
var mi = ClassInfo.of(resource2).getMethod(x -> x.hasName("setContext") && x.hasParameterTypes(RestContext.class)).orElse(null);
if (nn(mi)) {
@@ -4565,6 +4775,9 @@ public class RestContext extends Context {
* @return This object.
* @throws ServletException Error occurred.
*/
+ @SuppressWarnings({
+ "java:S2259" // getRestChildren() is never null post-construction: registerFrameworkDefaults() always registers a RestChildren default supplier before the constructor returns.
+ })
public RestContext postInitChildFirst() throws ServletException {
if (initialized.get())
return this;
@@ -4580,6 +4793,9 @@ public class RestContext extends Context {
return this;
}
+ @SuppressWarnings({
+ "java:S2259" // getDebugConfig() is never null post-construction: registerFrameworkDefaults() always registers a DebugConfig default supplier before the constructor returns.
+ })
private boolean isDebug(RestSession call) {
return getDebugConfig().resolve(this, call.getRequest()).enabled();
}
@@ -4797,6 +5013,9 @@ public class RestContext extends Context {
* @param m The method to get statistics for.
* @return The cached time-stats object.
*/
+ @SuppressWarnings({
+ "java:S2259" // getMethodExecStore() is never null post-construction: registerFrameworkDefaults() always registers a MethodExecStore default supplier before the constructor returns.
+ })
protected MethodExecStats getMethodExecStats(Method m) {
return getMethodExecStore().getStats(m);
}
@@ -5071,7 +5290,8 @@ public class RestContext extends Context {
* @throws NotImplemented No registered response processors could handle the call.
*/
@SuppressWarnings({
- "java:S127" // Loop counter i resets to -1 on RESTART
+ "java:S127", // Loop counter i resets to -1 on RESTART
+ "java:S2259" // getResponseProcessors() is never null post-construction: registerFrameworkDefaults() always registers a ResponseProcessor[] default supplier before the constructor returns.
})
public void processResponse(RestOpSession opSession) throws IOException, BasicHttpException, NotImplemented {
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/RestServerConstants.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/RestServerConstants.java
index 38d2897a63..345a2200c8 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/RestServerConstants.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/RestServerConstants.java
@@ -232,6 +232,9 @@ public final class RestServerConstants {
/** The {@code "mixins"} annotation attribute name — used in {@code noInherit} matching on {@code @Rest} annotations to cut off the class-chain walk when resolving operation mixins. */
public static final String PROPERTY_mixins = "mixins";
+ /** The {@code "children"} annotation attribute name — used in {@code noInherit} matching on {@code @Rest} annotations to cut off the class-chain walk when resolving routed children. */
+ public static final String PROPERTY_children = "children";
+
/** The {@code "value"} annotation attribute name — used by {@code @RestOp}/verb annotations to hold the (optional method-prefixed) path; folded into {@link #PROPERTY_path}. */
public static final String PROPERTY_value = "value";
(juneau) 02/02: Resolve SonarLint findings across core, rest, and test-support modules
Posted by ja...@apache.org.
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
commit ed33c360d9866cf3fd9ce0af61c4bc7b67c57bd7
Author: James Bognar <ja...@apache.org>
AuthorDate: Fri Jul 24 16:52:58 2026 -0400
Resolve SonarLint findings across core, rest, and test-support modules
Fixes or suppresses a batch of SonarLint issues with no behavior change except
the noted ones: S8786 ReDoS regex rewrite in Config, S2259 null-guards in
FileStore, S1452 CRTP-bounded return-type suppressions in jsonschema,
S5778/S5976 test refactors, S110 suppressions on the HTTP-response hierarchy,
S2886/S2160 dispositions in rest-server, S1135 stale-TODO rewording, and
assorted S125/S112/S1659/S6201/S8924/S3415 cleanups.
Co-authored-by: Cursor <cu...@cursor.com>
---
.../apache/juneau/bean/jsonschema/JsonSchema.java | 39 ++++++++++++++++++++++
.../bean/jsonschema/JsonSchemaBeanGenerator.java | 12 +++++++
.../juneau/bean/jsonschema/JsonSchemaMap.java | 3 ++
.../bean/jsonschema/JsonSchemaValidator.java | 3 ++
.../juneau/bean/openapi3/SchemaInfo_Test.java | 3 +-
.../juneau/commons/bean/BeanMetaInitializer.java | 6 ++--
.../apache/juneau/commons/logging/LogRecord.java | 2 --
.../juneau/commons/logging/LogRecord_Test.java | 6 ----
.../main/java/org/apache/juneau/config/Config.java | 2 +-
.../org/apache/juneau/config/store/FileStore.java | 19 +++++++++--
.../juneau/marshall/hjson/HjsonParserSession.java | 2 +-
.../juneau/marshall/hocon/HoconParserSession.java | 2 +-
.../juneau/ParentPropertyContainer_Test.java | 2 +-
.../markdown/MarkdownParserSession_Test.java | 37 +++++++++-----------
.../juneau/marshall/toml/TomlTokenizer_Test.java | 31 ++++++-----------
.../main/java/org/apache/juneau/BeanTester.java | 3 +-
.../apache/juneau/a/rttests/RoundTrip_Tester.java | 8 +++--
.../juneau/marshall/testutils/pojos/TestURI.java | 21 +++++++++---
.../main/java/org/apache/juneau/XmlTestUtils.java | 6 ++++
.../saml/SamlAssertionValidator_Builder_Test.java | 3 +-
.../apache/juneau/http/response/BadGateway.java | 3 ++
.../apache/juneau/http/response/BadRequest.java | 3 ++
.../org/apache/juneau/http/response/Conflict.java | 3 ++
.../juneau/http/response/ExpectationFailed.java | 3 ++
.../juneau/http/response/FailedDependency.java | 3 ++
.../org/apache/juneau/http/response/Forbidden.java | 3 ++
.../juneau/http/response/GatewayTimeout.java | 3 ++
.../java/org/apache/juneau/http/response/Gone.java | 3 ++
.../http/response/HttpVersionNotSupported.java | 3 ++
.../juneau/http/response/InsufficientStorage.java | 3 ++
.../juneau/http/response/InternalServerError.java | 3 ++
.../juneau/http/response/LengthRequired.java | 3 ++
.../org/apache/juneau/http/response/Locked.java | 3 ++
.../apache/juneau/http/response/LoopDetected.java | 3 ++
.../juneau/http/response/MethodNotAllowed.java | 3 ++
.../juneau/http/response/MisdirectedRequest.java | 3 ++
.../response/NetworkAuthenticationRequired.java | 3 ++
.../apache/juneau/http/response/NotAcceptable.java | 3 ++
.../apache/juneau/http/response/NotExtended.java | 3 ++
.../org/apache/juneau/http/response/NotFound.java | 3 ++
.../juneau/http/response/NotImplemented.java | 3 ++
.../juneau/http/response/PayloadTooLarge.java | 3 ++
.../juneau/http/response/PreconditionFailed.java | 3 ++
.../juneau/http/response/PreconditionRequired.java | 3 ++
.../juneau/http/response/RangeNotSatisfiable.java | 3 ++
.../http/response/RequestHeaderFieldsTooLarge.java | 3 ++
.../juneau/http/response/ServiceUnavailable.java | 3 ++
.../juneau/http/response/TooManyRequests.java | 3 ++
.../apache/juneau/http/response/Unauthorized.java | 3 ++
.../http/response/UnavailableForLegalReasons.java | 3 ++
.../juneau/http/response/UnprocessableEntity.java | 3 ++
.../juneau/http/response/UnsupportedMediaType.java | 3 ++
.../juneau/http/response/UpgradeRequired.java | 3 ++
.../apache/juneau/http/response/UriTooLong.java | 3 ++
.../http/response/VariantAlsoNegotiates.java | 3 ++
.../juneau/rest/server/servlet/RestServlet.java | 6 ++++
.../server/validation/ValidationException.java | 3 +-
57 files changed, 255 insertions(+), 69 deletions(-)
diff --git a/juneau-bean/juneau-bean-jsonschema/src/main/java/org/apache/juneau/bean/jsonschema/JsonSchema.java b/juneau-bean/juneau-bean-jsonschema/src/main/java/org/apache/juneau/bean/jsonschema/JsonSchema.java
index 39f8ad5475..0ea386f038 100644
--- a/juneau-bean/juneau-bean-jsonschema/src/main/java/org/apache/juneau/bean/jsonschema/JsonSchema.java
+++ b/juneau-bean/juneau-bean-jsonschema/src/main/java/org/apache/juneau/bean/jsonschema/JsonSchema.java
@@ -397,6 +397,9 @@ public class JsonSchema<SELF extends JsonSchema<SELF>> {
* @param type The type to generate a schema for. Must not be <jk>null</jk>, or an {@link IllegalArgumentException} is thrown.
* @return The generated schema bean, or <jk>null</jk> if a schema could not be generated for the type.
*/
+ @SuppressWarnings({
+ "java:S1452" // Self-bounded (CRTP) generic: the generated schema's concrete SELF subtype is not known here, so JsonSchema<?> is the only sound return type.
+ })
public static JsonSchema<?> of(Type type) {
return JsonSchemaBeanGenerator.DEFAULT.generate(type);
}
@@ -407,6 +410,9 @@ public class JsonSchema<SELF extends JsonSchema<SELF>> {
* @param type The class to generate a schema for. Must not be <jk>null</jk>, or an {@link IllegalArgumentException} is thrown.
* @return The generated schema bean, or <jk>null</jk> if a schema could not be generated for the class.
*/
+ @SuppressWarnings({
+ "java:S1452" // Self-bounded (CRTP) generic: the generated schema's concrete SELF subtype is not known here, so JsonSchema<?> is the only sound return type.
+ })
public static JsonSchema<?> of(Class<?> type) {
return JsonSchemaBeanGenerator.DEFAULT.generate(type);
}
@@ -882,6 +888,9 @@ public class JsonSchema<SELF extends JsonSchema<SELF>> {
* @return The currently set value, or <jk>null</jk> if the property is not set, or is set as a {@link Boolean}.
*/
@BeanIgnore
+ @SuppressWarnings({
+ "java:S1452" // Self-bounded (CRTP) generic: additionalPropertiesSchema is declared JsonSchema<?> since callers may attach any concrete SELF subtype.
+ })
public JsonSchema<?> getAdditionalPropertiesAsSchema() { return additionalPropertiesSchema; }
/**
@@ -1028,6 +1037,9 @@ public class JsonSchema<SELF extends JsonSchema<SELF>> {
* @return The value of the <property>else</property> property on this bean, or <jk>null</jk> if it is not set.
*/
@BeanProp("else")
+ @SuppressWarnings({
+ "java:S1452" // Self-bounded (CRTP) generic: else_ is declared JsonSchema<?> since callers may attach any concrete SELF subtype.
+ })
public JsonSchema<?> getElse() { return else_; }
/**
@@ -1113,6 +1125,9 @@ public class JsonSchema<SELF extends JsonSchema<SELF>> {
* @return The value of the <property>if</property> property on this bean, or <jk>null</jk> if it is not set.
*/
@BeanProp("if")
+ @SuppressWarnings({
+ "java:S1452" // Self-bounded (CRTP) generic: if_ is declared JsonSchema<?> since callers may attach any concrete SELF subtype.
+ })
public JsonSchema<?> getIf() { return if_; }
/**
@@ -1138,6 +1153,9 @@ public class JsonSchema<SELF extends JsonSchema<SELF>> {
* @return The currently set value, or <jk>null</jk> if the property is not set, or is set as a {@link JsonSchemaArray}.
*/
@BeanIgnore
+ @SuppressWarnings({
+ "java:S1452" // Self-bounded (CRTP) generic: itemsSchema is declared JsonSchema<?> since callers may attach any concrete SELF subtype.
+ })
public JsonSchema<?> getItemsAsSchema() { return itemsSchema; }
/**
@@ -1232,6 +1250,9 @@ public class JsonSchema<SELF extends JsonSchema<SELF>> {
*
* @return The value of the <property>not</property> property on this bean, or <jk>null</jk> if it is not set.
*/
+ @SuppressWarnings({
+ "java:S1452" // Self-bounded (CRTP) generic: 'not' is declared JsonSchema<?> since callers may attach any concrete SELF subtype.
+ })
public JsonSchema<?> getNot() { return not; }
/**
@@ -1283,6 +1304,9 @@ public class JsonSchema<SELF extends JsonSchema<SELF>> {
* @param name The property name. Can be <jk>null</jk> (no property has a <jk>null</jk> name, so <jk>null</jk> is returned).
* @return The property with the specified name, or <jk>null</jk> if no property is specified.
*/
+ @SuppressWarnings({
+ "java:S1452" // Self-bounded (CRTP) generic: 'properties' map values are declared JsonSchema<?> since callers may attach any concrete SELF subtype.
+ })
public JsonSchema<?> getProperty(String name) {
return getProperty(name, false);
}
@@ -1301,6 +1325,9 @@ public class JsonSchema<SELF extends JsonSchema<SELF>> {
* @param resolve If <jk>true</jk>, calls {@link #resolve()} on object before returning.
* @return The property with the specified name, or <jk>null</jk> if no property is specified.
*/
+ @SuppressWarnings({
+ "java:S1452" // Self-bounded (CRTP) generic: 'properties' map values are declared JsonSchema<?> since callers may attach any concrete SELF subtype.
+ })
public JsonSchema<?> getProperty(String name, boolean resolve) {
if (properties == null)
return null;
@@ -1354,6 +1381,9 @@ public class JsonSchema<SELF extends JsonSchema<SELF>> {
* @return The value of the <property>then</property> property on this bean, or <jk>null</jk> if it is not set.
*/
@BeanProp("then")
+ @SuppressWarnings({
+ "java:S1452" // Self-bounded (CRTP) generic: then_ is declared JsonSchema<?> since callers may attach any concrete SELF subtype.
+ })
public JsonSchema<?> getThen() { return then_; }
/**
@@ -1424,6 +1454,9 @@ public class JsonSchema<SELF extends JsonSchema<SELF>> {
*
* @return The value of the <property>unevaluatedItems</property> property on this bean, or <jk>null</jk> if it is not set.
*/
+ @SuppressWarnings({
+ "java:S1452" // Self-bounded (CRTP) generic: unevaluatedItems is declared JsonSchema<?> since callers may attach any concrete SELF subtype.
+ })
public JsonSchema<?> getUnevaluatedItems() { return unevaluatedItems; }
/**
@@ -1434,6 +1467,9 @@ public class JsonSchema<SELF extends JsonSchema<SELF>> {
*
* @return The value of the <property>unevaluatedProperties</property> property on this bean, or <jk>null</jk> if it is not set.
*/
+ @SuppressWarnings({
+ "java:S1452" // Self-bounded (CRTP) generic: unevaluatedProperties is declared JsonSchema<?> since callers may attach any concrete SELF subtype.
+ })
public JsonSchema<?> getUnevaluatedProperties() { return unevaluatedProperties; }
/**
@@ -1467,6 +1503,9 @@ public class JsonSchema<SELF extends JsonSchema<SELF>> {
*
* @return The referenced schema, or <jk>null</jk> if this schema is a <property>$ref</property> whose target is not found in the registered schema map.
*/
+ @SuppressWarnings({
+ "java:S1452" // Self-bounded (CRTP) generic: resolved schemas come from JsonSchemaMap's JsonSchema<?>-typed value, so no narrower type is possible.
+ })
public JsonSchema<?> resolve() {
if (ref == null || master.schemaMap == null)
return this;
diff --git a/juneau-bean/juneau-bean-jsonschema/src/main/java/org/apache/juneau/bean/jsonschema/JsonSchemaBeanGenerator.java b/juneau-bean/juneau-bean-jsonschema/src/main/java/org/apache/juneau/bean/jsonschema/JsonSchemaBeanGenerator.java
index 20a0c4780e..df995f036a 100644
--- a/juneau-bean/juneau-bean-jsonschema/src/main/java/org/apache/juneau/bean/jsonschema/JsonSchemaBeanGenerator.java
+++ b/juneau-bean/juneau-bean-jsonschema/src/main/java/org/apache/juneau/bean/jsonschema/JsonSchemaBeanGenerator.java
@@ -69,6 +69,9 @@ public final class JsonSchemaBeanGenerator {
* @param type The Java type. Must not be <jk>null</jk>, or an {@link IllegalArgumentException} is thrown.
* @return The generated schema bean, or <jk>null</jk> if a schema could not be generated for the type.
*/
+ @SuppressWarnings({
+ "java:S1452" // Self-bounded (CRTP) generic: the generated schema's concrete SELF subtype is not known here, so JsonSchema<?> is the only sound return type.
+ })
public JsonSchema<?> generate(Type type) {
assertArgNotNull("type", type);
try {
@@ -91,6 +94,9 @@ public final class JsonSchemaBeanGenerator {
* @param type The Java class. Must not be <jk>null</jk>, or an {@link IllegalArgumentException} is thrown.
* @return The generated schema bean, or <jk>null</jk> if a schema could not be generated for the class.
*/
+ @SuppressWarnings({
+ "java:S1452" // Self-bounded (CRTP) generic: the generated schema's concrete SELF subtype is not known here, so JsonSchema<?> is the only sound return type.
+ })
public JsonSchema<?> generate(Class<?> type) {
return generate((Type)type);
}
@@ -104,6 +110,9 @@ public final class JsonSchemaBeanGenerator {
* @param o The value to infer a schema from. Must not be <jk>null</jk>, or an {@link IllegalArgumentException} is thrown.
* @return The generated schema bean, or <jk>null</jk> if a schema could not be generated for the value.
*/
+ @SuppressWarnings({
+ "java:S1452" // Self-bounded (CRTP) generic: the generated schema's concrete SELF subtype is not known here, so JsonSchema<?> is the only sound return type.
+ })
public JsonSchema<?> generate(Object o) {
assertArgNotNull("o", o);
try {
@@ -129,6 +138,9 @@ public final class JsonSchemaBeanGenerator {
* @param schemaMap The generated schema map. Must not be <jk>null</jk>, or an {@link IllegalArgumentException} is thrown.
* @return The typed schema bean.
*/
+ @SuppressWarnings({
+ "java:S1452" // Self-bounded (CRTP) generic: the parsed schema's concrete SELF subtype is not known here, so JsonSchema<?> is the only sound return type.
+ })
public static JsonSchema<?> toBean(JsonMap schemaMap) {
assertArgNotNull("schemaMap", schemaMap);
try {
diff --git a/juneau-bean/juneau-bean-jsonschema/src/main/java/org/apache/juneau/bean/jsonschema/JsonSchemaMap.java b/juneau-bean/juneau-bean-jsonschema/src/main/java/org/apache/juneau/bean/jsonschema/JsonSchemaMap.java
index d7ef75fbd7..c05abf8255 100644
--- a/juneau-bean/juneau-bean-jsonschema/src/main/java/org/apache/juneau/bean/jsonschema/JsonSchemaMap.java
+++ b/juneau-bean/juneau-bean-jsonschema/src/main/java/org/apache/juneau/bean/jsonschema/JsonSchemaMap.java
@@ -131,6 +131,9 @@ public abstract class JsonSchemaMap extends ConcurrentHashMap<URI,JsonSchema<?>>
* @param uri The URI to load the schema from.
* @return The parsed schema, or <jk>null</jk> if the document is unreachable.
*/
+ @SuppressWarnings({
+ "java:S1452" // Self-bounded (CRTP) generic: the loaded schema's concrete SELF subtype is not known here, so JsonSchema<?> is the only sound return type.
+ })
public JsonSchema<?> load(URI uri) {
try (var r = getReader(uri)) {
if (r == null)
diff --git a/juneau-bean/juneau-bean-jsonschema/src/main/java/org/apache/juneau/bean/jsonschema/JsonSchemaValidator.java b/juneau-bean/juneau-bean-jsonschema/src/main/java/org/apache/juneau/bean/jsonschema/JsonSchemaValidator.java
index 8aa8619d42..c8e650c912 100644
--- a/juneau-bean/juneau-bean-jsonschema/src/main/java/org/apache/juneau/bean/jsonschema/JsonSchemaValidator.java
+++ b/juneau-bean/juneau-bean-jsonschema/src/main/java/org/apache/juneau/bean/jsonschema/JsonSchemaValidator.java
@@ -148,6 +148,9 @@ public final class JsonSchemaValidator implements PropertyValidator {
*
* @return The schema.
*/
+ @SuppressWarnings({
+ "java:S1452" // Self-bounded (CRTP) generic: the validated schema's concrete SELF subtype is not known here, so JsonSchema<?> is the only sound return type.
+ })
public JsonSchema<?> getSchema() {
return schema;
}
diff --git a/juneau-bean/juneau-bean-openapi-v3/src/test/java/org/apache/juneau/bean/openapi3/SchemaInfo_Test.java b/juneau-bean/juneau-bean-openapi-v3/src/test/java/org/apache/juneau/bean/openapi3/SchemaInfo_Test.java
index 1ddc77bd1d..e0e3af78a6 100644
--- a/juneau-bean/juneau-bean-openapi-v3/src/test/java/org/apache/juneau/bean/openapi3/SchemaInfo_Test.java
+++ b/juneau-bean/juneau-bean-openapi-v3/src/test/java/org/apache/juneau/bean/openapi3/SchemaInfo_Test.java
@@ -130,7 +130,8 @@ class SchemaInfo_Test extends TestBase {
@Test void a09b_addProperty_nullChecks() {
var x = bean();
- assertThrows(IllegalArgumentException.class, () -> x.addProperty(null, schemaInfo("a")));
+ var a = schemaInfo("a");
+ assertThrows(IllegalArgumentException.class, () -> x.addProperty(null, a));
assertThrows(IllegalArgumentException.class, () -> x.addProperty("a", null));
}
diff --git a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/bean/BeanMetaInitializer.java b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/bean/BeanMetaInitializer.java
index c411c95e6e..315bfb430d 100644
--- a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/bean/BeanMetaInitializer.java
+++ b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/bean/BeanMetaInitializer.java
@@ -130,8 +130,8 @@ public interface BeanMetaInitializer {
* Builds the bean-level {@link BeanRegistryLookup} for the supplied class. Returns <jk>null</jk> on the
* commons-side path (no marshalling context).
*
- * @param marshallingContext The marshalling-side context (as an opaque {@link Object}). Can be <jk>null</jk>. The effect is implementation-dependent — see {@link org.apache.juneau.marshall.MarshalledBeanMetaInitializer}, the sole concrete implementer of this SPI.
- * @param beanFilter The bean filter applied to this bean meta. Can be <jk>null</jk>. The effect is implementation-dependent — see {@link org.apache.juneau.marshall.MarshalledBeanMetaInitializer}.
+ * @param marshallingContext The marshalling-side context (as an opaque {@link Object}). Can be <jk>null</jk>. The effect is implementation-dependent — see {@code org.apache.juneau.marshall.MarshalledBeanMetaInitializer}, the sole concrete implementer of this SPI.
+ * @param beanFilter The bean filter applied to this bean meta. Can be <jk>null</jk>. The effect is implementation-dependent — see {@code org.apache.juneau.marshall.MarshalledBeanMetaInitializer}.
* @param classInfo The class info of the bean. Must not be <jk>null</jk>.
* @param config The bean-modeling configuration. Must not be <jk>null</jk>.
* @return The bean-level registry, or <jk>null</jk>.
@@ -152,7 +152,7 @@ public interface BeanMetaInitializer {
* Walks the parent classes/interfaces of {@code classInfo} and returns the first dictionary-name mapping for
* {@code rawClass} found in any parent's registry, or <jk>null</jk> if none.
*
- * @param marshallingContext The marshalling-side context. Can be <jk>null</jk>. The effect is implementation-dependent — see {@link org.apache.juneau.marshall.MarshalledBeanMetaInitializer}.
+ * @param marshallingContext The marshalling-side context. Can be <jk>null</jk>. The effect is implementation-dependent — see {@code org.apache.juneau.marshall.MarshalledBeanMetaInitializer}.
* @param classInfo The bean's class info.
* @param rawClass The raw class to look up.
* @return The dictionary name found in a parent's registry, or <jk>null</jk>.
diff --git a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/logging/LogRecord.java b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/logging/LogRecord.java
index c4f67d1f49..ff87597079 100644
--- a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/logging/LogRecord.java
+++ b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/logging/LogRecord.java
@@ -27,8 +27,6 @@ import java.util.Formatter;
import java.util.function.*;
import java.util.logging.*;
-import org.apache.juneau.commons.utils.*;
-
/**
* LogRecord that supports lazy message formatting.
*
diff --git a/juneau-core/juneau-commons/src/test/java/org/apache/juneau/commons/logging/LogRecord_Test.java b/juneau-core/juneau-commons/src/test/java/org/apache/juneau/commons/logging/LogRecord_Test.java
index 2d6dde98fc..a68fe91aff 100644
--- a/juneau-core/juneau-commons/src/test/java/org/apache/juneau/commons/logging/LogRecord_Test.java
+++ b/juneau-core/juneau-commons/src/test/java/org/apache/juneau/commons/logging/LogRecord_Test.java
@@ -31,12 +31,6 @@ import org.junit.jupiter.params.provider.*;
*/
class LogRecord_Test extends TestBase {
- private static Logger getLogger(String name) {
- var l = Logger.getLogger(name);
- l.setLevel(Level.OFF);
- return l;
- }
-
//====================================================================================================
// Constructor and basic properties
//====================================================================================================
diff --git a/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/Config.java b/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/Config.java
index 55dc323678..c9f4cdfd44 100644
--- a/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/Config.java
+++ b/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/Config.java
@@ -522,7 +522,7 @@ public class Config extends Context implements ConfigEventListener {
var cmd = env("sun.java.command", "not_found").split("\\s+")[0];
if (cmd.endsWith(".jar") && ! co(cmd, "surefirebooter")) { // HTT - not a .jar during tests
- cmd = cmd.replaceAll("(?:.*[\\\\\\/])?([^\\\\\\/]+)\\.jar$", "$1");
+ cmd = cmd.replaceAll("(?:[^\\\\\\/]*[\\\\\\/])*([^\\\\\\/]+)\\.jar$", "$1"); // Unrolled loop (no .* overlapping the separator class) avoids super-linear backtracking; same "strip path up to last separator" semantics.
l.add(cmd + ".cfg");
cmd = cmd.replaceAll("[\\.\\_].*$", ""); // Try also without version in jar name.
l.add(cmd + ".cfg");
diff --git a/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/store/FileStore.java b/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/store/FileStore.java
index 5cab02becb..a68615b309 100644
--- a/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/store/FileStore.java
+++ b/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/store/FileStore.java
@@ -429,7 +429,10 @@ public class FileStore extends ConfigStore {
name = resolveName(name);
var p = resolveFile(name);
- name = p.getFileName().toString();
+ var pName = p.getFileName();
+ if (n(pName))
+ throw ioex("Could not resolve a file name from path '%s'.", p); // Real NPE guard: getFileName() can return null when p resolves to a root path (e.g. a caller-supplied absolute name).
+ name = pName.toString();
var s = cache.get(name);
if (nn(s))
@@ -480,7 +483,10 @@ public class FileStore extends ConfigStore {
dir.mkdirs();
var p = resolveFile(name);
- name = p.getFileName().toString();
+ var pName = p.getFileName();
+ if (n(pName))
+ throw ioex("Could not resolve a file name from path '%s'.", p); // Real NPE guard: getFileName() can return null when p resolves to a root path (e.g. a caller-supplied absolute name).
+ name = pName.toString();
var exists = Files.exists(p);
@@ -526,6 +532,9 @@ public class FileStore extends ConfigStore {
return null;
}
+ @SuppressWarnings({
+ "javabugs:S2259" // p.getParent() is only reached when p does not exist; p is always dir.resolve(nonEmptySegment), so the only way p could be a rootless path with a null parent is if p itself were the filesystem root, which always exists and would skip this branch.
+ })
private synchronized boolean isWritable(Path p) {
try {
if (! Files.exists(p)) {
@@ -550,6 +559,9 @@ public class FileStore extends ConfigStore {
* @param e The file system event.
* @throws IOException Thrown by underlying stream.
*/
+ @SuppressWarnings({
+ "javabugs:S2259" // e.context() for ENTRY_CREATE/DELETE/MODIFY events (OVERFLOW is filtered out before this is called) is documented to return the single relative path segment of the changed entry, which always has a non-null file name.
+ })
protected synchronized void onFileEvent(WatchEvent<Path> e) throws IOException {
var fn = e.context().getFileName().toString();
@@ -572,7 +584,8 @@ public class FileStore extends ConfigStore {
@Override
@SuppressWarnings({
- "java:S3776" // Cognitive complexity acceptable for name resolution logic
+ "java:S3776", // Cognitive complexity acceptable for name resolution logic
+ "javabugs:S2259" // nameCache.get(name) at the end is always preceded by a put for this key in this call (or the key already existed), and no code ever removes entries, so the map is guaranteed to contain it.
})
protected String resolveName(String name) {
if (! nameCache.containsKey(name)) {
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/hjson/HjsonParserSession.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/hjson/HjsonParserSession.java
index a4ebb89eef..42273d0c8e 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/hjson/HjsonParserSession.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/hjson/HjsonParserSession.java
@@ -335,7 +335,7 @@ public class HjsonParserSession extends ReaderParserSession implements RecordRea
* Recursively injects @ParentProperty (and @NameProperty on map values / nested beans) into val.
* Intermediate collections and maps are transparent: a collection/map element's parent is
* parentBean (the nearest enclosing bean), skipping all intermediate containers. This keeps HJSON
- * consistent with the streaming parsers under TODO-291 (Option A).
+ * consistent with the streaming parsers.
*/
private void injectParentAnnotations(ClassMeta<?> cm, Object val, Object node, Object parentBean) throws ExecutableException {
if (val == null || cm == null)
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/hocon/HoconParserSession.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/hocon/HoconParserSession.java
index 6df44d9bff..27519786e9 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/hocon/HoconParserSession.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/hocon/HoconParserSession.java
@@ -520,7 +520,7 @@ public class HoconParserSession extends ReaderParserSession implements RecordRea
* Recursively injects @ParentProperty (and @NameProperty on map values / nested beans) into val.
* Intermediate collections and maps are transparent: a collection/map element's parent is
* parentBean (the nearest enclosing bean), skipping all intermediate containers. This keeps HOCON
- * consistent with the streaming parsers under TODO-291 (Option A).
+ * consistent with the streaming parsers.
*/
private void injectParentAnnotations(ClassMeta<?> cm, Object val, Object node, Object parentBean) throws ExecutableException {
if (val == null || cm == null)
diff --git a/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/ParentPropertyContainer_Test.java b/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/ParentPropertyContainer_Test.java
index 623c64be16..3ffe79a95e 100644
--- a/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/ParentPropertyContainer_Test.java
+++ b/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/ParentPropertyContainer_Test.java
@@ -40,7 +40,7 @@ import org.junit.jupiter.params.*;
import org.junit.jupiter.params.provider.*;
/**
- * Regression tests for TODO-291 / FINISHED-291.
+ * Regression tests for FINISHED-291.
*
* <p>
* Verifies that a bean annotated with {@link ParentProperty} that is nested inside a collection or
diff --git a/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/markdown/MarkdownParserSession_Test.java b/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/markdown/MarkdownParserSession_Test.java
index 21f9ab3c2e..31f9a3fd8e 100644
--- a/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/markdown/MarkdownParserSession_Test.java
+++ b/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/markdown/MarkdownParserSession_Test.java
@@ -161,12 +161,23 @@ class MarkdownParserSession_Test extends TestBase {
assertEquals("---", r);
}
- @Test void c03_keyHeaderVariant() {
- // "Key"/"Value" headers should also be detected as a key/value table.
- var md = "| Key | Value |\n|---|---|\n| name | Bob |\n| age | 25 |";
+ @ParameterizedTest
+ @MethodSource("c03_keyValueTableToBeanProvider")
+ void c03_keyValueTableToBean(String md, String expectedName, int expectedAge) {
var r = MarkdownParser.DEFAULT.read(md, MarkdownParser_Test.A.class);
- assertEquals("Bob", r.name);
- assertEquals(25, r.age);
+ assertEquals(expectedName, r.name);
+ assertEquals(expectedAge, r.age);
+ }
+
+ static Stream<Arguments> c03_keyValueTableToBeanProvider() {
+ return Stream.of(
+ // c03: "Key"/"Value" headers should also be detected as a key/value table.
+ Arguments.of("| Key | Value |\n|---|---|\n| name | Bob |\n| age | 25 |", "Bob", 25),
+ // k02: lines not starting with | should be filtered out of the table-line collection.
+ Arguments.of("| Property | Value |\n|---|---|\n| name | Alice |\nthis is not a row\n| age | 30 |", "Alice", 30),
+ // n02: empty cell for int type -> readCellValue returns null/default-zero.
+ Arguments.of("| Property | Value |\n|---|---|\n| name | Alice |\n| age | |", "Alice", 0)
+ );
}
@Test void c04_tableEmptyHeaderCells() {
@@ -600,14 +611,6 @@ class MarkdownParserSession_Test extends TestBase {
assertEquals("Alice", r.get("name"));
}
- @Test void k02_lineWithoutPipeIgnored() {
- // Lines not starting with | should be filtered out of the table-line collection.
- var md = "| Property | Value |\n|---|---|\n| name | Alice |\nthis is not a row\n| age | 30 |";
- var r = MarkdownParser.DEFAULT.read(md, MarkdownParser_Test.A.class);
- assertEquals("Alice", r.name);
- assertEquals(30, r.age);
- }
-
//====================================================================================================
// l - Multi-column simple element type (readRow simple type fall-through)
//====================================================================================================
@@ -651,14 +654,6 @@ class MarkdownParserSession_Test extends TestBase {
assertEquals(30, r.age);
}
- @Test void n02_emptyCellForIntType() {
- // Empty cell for int type → readCellValue returns null/default-zero at line 578
- var md = "| Property | Value |\n|---|---|\n| name | Alice |\n| age | |";
- var r = MarkdownParser.DEFAULT.read(md, MarkdownParser_Test.A.class);
- assertEquals("Alice", r.name);
- assertEquals(0, r.age);
- }
-
@Test void n03_customNullValueMatch() {
// Cell value matching custom nullValue → readCellValue returns null at line 572
var p = MarkdownParser.create().nullValue("N/A").build();
diff --git a/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/toml/TomlTokenizer_Test.java b/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/toml/TomlTokenizer_Test.java
index 3ef54fb6c4..1ee80c1e72 100644
--- a/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/toml/TomlTokenizer_Test.java
+++ b/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/toml/TomlTokenizer_Test.java
@@ -253,28 +253,19 @@ class TomlTokenizer_Test extends TestBase {
// readBasicString and escapes
//-----------------------------------------------------------------------------------------------------------------
- @Test
- void f01_readBasicStringSimple() throws Exception {
- var tok = t("\"hello\"");
- assertEquals("hello", tok.readBasicString());
- }
-
- @Test
- void f03_readBasicStringAllEscapes() throws Exception {
- var tok = t("\"\\b\\t\\n\\f\\r\\\"\\\\\"");
- assertEquals("\b\t\n\f\r\"\\", tok.readBasicString());
- }
-
- @Test
- void f04_readBasicStringUnicodeShort() throws Exception {
- var tok = t("\"\\u0041\"");
- assertEquals("A", tok.readBasicString());
+ @ParameterizedTest
+ @MethodSource("f01_readBasicStringValidProvider")
+ void f01_readBasicStringValid(String input, String expected) throws Exception {
+ assertEquals(expected, t(input).readBasicString());
}
- @Test
- void f05_readBasicStringUnicodeLong() throws Exception {
- var tok = t("\"\\U00000041\"");
- assertEquals("A", tok.readBasicString());
+ static Stream<Arguments> f01_readBasicStringValidProvider() {
+ return Stream.of(
+ Arguments.of("\"hello\"", "hello"), // f01: simple string
+ Arguments.of("\"\\b\\t\\n\\f\\r\\\"\\\\\"", "\b\t\n\f\r\"\\"), // f03: all standard escapes
+ Arguments.of("\"\\u0041\"", "A"), // f04: short \\u unicode escape
+ Arguments.of("\"\\U00000041\"", "A") // f05: long \\U unicode escape
+ );
}
@ParameterizedTest
diff --git a/juneau-core/juneau-test-marshall-utils/src/main/java/org/apache/juneau/BeanTester.java b/juneau-core/juneau-test-marshall-utils/src/main/java/org/apache/juneau/BeanTester.java
index bd43d585db..886742e8f0 100644
--- a/juneau-core/juneau-test-marshall-utils/src/main/java/org/apache/juneau/BeanTester.java
+++ b/juneau-core/juneau-test-marshall-utils/src/main/java/org/apache/juneau/BeanTester.java
@@ -58,7 +58,8 @@ import org.apache.juneau.test.bct.*;
* @param <T> The type of bean being tested
*/
@SuppressWarnings({
- "unchecked" // Class<T> cast for generic bean type in test utility
+ "unchecked", // Class<T> cast for generic bean type in test utility
+ "java:S5960" // Test utility; JUnit assertions are its intentional purpose, not a production-code side effect.
})
public class BeanTester<T> {
diff --git a/juneau-core/juneau-test-marshall-utils/src/main/java/org/apache/juneau/a/rttests/RoundTrip_Tester.java b/juneau-core/juneau-test-marshall-utils/src/main/java/org/apache/juneau/a/rttests/RoundTrip_Tester.java
index 86bb77b358..b619eeab06 100644
--- a/juneau-core/juneau-test-marshall-utils/src/main/java/org/apache/juneau/a/rttests/RoundTrip_Tester.java
+++ b/juneau-core/juneau-test-marshall-utils/src/main/java/org/apache/juneau/a/rttests/RoundTrip_Tester.java
@@ -30,7 +30,8 @@ import org.apache.juneau.marshall.serializer.*;
import org.apache.juneau.marshall.xml.*;
@SuppressWarnings({
- "unchecked" // Cast from Object to T in round-trip test infra
+ "unchecked", // Cast from Object to T in round-trip test infra
+ "java:S101" // Underscore test-type naming (e.g. sibling ComboRoundTrip_Tester classes) is an established Juneau convention.
})
public class RoundTrip_Tester {
@@ -177,6 +178,9 @@ public class RoundTrip_Tester {
return skipIf != null && skipIf.test(o);
}
+ @SuppressWarnings({
+ "java:S106" // Test harness diagnostic output, gated behind the debug flag; mirrors juneau-test's Assertion.java precedent.
+ })
public <T> Object serialize(T object, Serializer s) throws Exception {
Object out;
@@ -187,7 +191,7 @@ public class RoundTrip_Tester {
}
if (debug)
- System.err.println("Serialized contents from ["+label+"]...\n---START---\n" + (out instanceof byte[] ? toReadableBytes((byte[])out) : out) + "\n---END---\n"); // NOT DEBUG
+ System.err.println("Serialized contents from ["+label+"]...\n---START---\n" + (out instanceof byte[] byteArray ? toReadableBytes(byteArray) : out) + "\n---END---\n"); // NOT DEBUG
if (validateXmlWhitespace)
checkXmlWhitespace(out.toString());
diff --git a/juneau-core/juneau-test-marshall-utils/src/main/java/org/apache/juneau/marshall/testutils/pojos/TestURI.java b/juneau-core/juneau-test-marshall-utils/src/main/java/org/apache/juneau/marshall/testutils/pojos/TestURI.java
index d52c8f25a7..c6bc183137 100755
--- a/juneau-core/juneau-test-marshall-utils/src/main/java/org/apache/juneau/marshall/testutils/pojos/TestURI.java
+++ b/juneau-core/juneau-test-marshall-utils/src/main/java/org/apache/juneau/marshall/testutils/pojos/TestURI.java
@@ -27,23 +27,36 @@ public class TestURI {
@Xml(format=XmlFormat.ATTR)
public String f0 = "f0/x0";
- public URI f1, f2, f3;
+ public URI f1;
+ public URI f2;
+ public URI f3;
@Uri
- public String f4, f5, f6;
+ public String f4;
+ @Uri
+ public String f5;
+ @Uri
+ public String f6;
public URL f7;
public TestURIb f8;
- public String fa, fb, fc, fd, fe;
+ public String fa;
+ public String fb;
+ public String fc;
+ public String fd;
+ public String fe;
@Uri
public String getF9() {
return "f9/x9";
}
- public TestURI() throws Exception {
+ @SuppressWarnings({
+ "java:S1075" // Test fixture; fixed round-trip URIs are the intended test data, not a configurable endpoint.
+ })
+ public TestURI() throws URISyntaxException, MalformedURLException {
f1 = new URI("f1/x1");
f2 = new URI("/f2/x2");
f3 = new URI("http://www.apache.org/f3/x3");
diff --git a/juneau-core/juneau-test-utils/src/main/java/org/apache/juneau/XmlTestUtils.java b/juneau-core/juneau-test-utils/src/main/java/org/apache/juneau/XmlTestUtils.java
index 57b7a390a5..6867bca7b2 100644
--- a/juneau-core/juneau-test-utils/src/main/java/org/apache/juneau/XmlTestUtils.java
+++ b/juneau-core/juneau-test-utils/src/main/java/org/apache/juneau/XmlTestUtils.java
@@ -74,6 +74,9 @@ public final class XmlTestUtils {
* @return The indent depth after processing this line.
* @throws Exception If the line's indentation is wrong, or the line doesn't match any recognized form.
*/
+ @SuppressWarnings({
+ "java:S112" // Test utility; generic exception mirrors checkXmlWhitespace's diagnostic contract, not swallowed by callers.
+ })
private static int checkLineIndent(String line, int lineNum, int indent) throws Exception {
var m = START_TAG.matcher(line);
if (m.matches()) {
@@ -117,6 +120,9 @@ public final class XmlTestUtils {
* @param lineType A human-readable description of the line kind being checked (for error messages).
* @throws Exception If <c>actual != expected</c>.
*/
+ @SuppressWarnings({
+ "java:S112" // Test utility; generic exception mirrors checkXmlWhitespace's diagnostic contract, not swallowed by callers.
+ })
private static void checkIndent(int expected, int actual, int lineNum, String lineType) throws Exception {
if (actual != expected)
throw new Exception("Wrong indentation detected on " + lineType + " line ''" + lineNum + "''");
diff --git a/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/auth/saml/SamlAssertionValidator_Builder_Test.java b/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/auth/saml/SamlAssertionValidator_Builder_Test.java
index 328af829bf..5c2614730d 100644
--- a/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/auth/saml/SamlAssertionValidator_Builder_Test.java
+++ b/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/auth/saml/SamlAssertionValidator_Builder_Test.java
@@ -17,6 +17,7 @@
package org.apache.juneau.rest.server.auth.saml;
import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.mock;
import java.security.*;
import java.time.*;
@@ -120,7 +121,7 @@ class SamlAssertionValidator_Builder_Test extends TestBase {
@Test void d01_credentialAndResolverMutuallyExclusive() throws Exception {
var cred = dummyCredential();
- var meta = org.mockito.Mockito.mock(org.opensaml.saml.metadata.resolver.MetadataResolver.class);
+ var meta = mock(org.opensaml.saml.metadata.resolver.MetadataResolver.class);
assertThrows(IllegalStateException.class, () -> SamlAssertionValidator.create()
.spEntityId("sp").expectedIssuer("idp")
.signingCredential(cred)
diff --git a/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/BadGateway.java b/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/BadGateway.java
index cc5aa1dca1..636c967d8e 100644
--- a/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/BadGateway.java
+++ b/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/BadGateway.java
@@ -38,6 +38,9 @@ import org.apache.juneau.http.*;
*
* @since 9.2.1
*/
+@SuppressWarnings({
+ "java:S110" // intentional — mirrors the HTTP status-code response hierarchy
+})
public class BadGateway extends BasicHttpException {
private static final long serialVersionUID = 1L;
diff --git a/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/BadRequest.java b/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/BadRequest.java
index ea184020d4..e58ab58c38 100644
--- a/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/BadRequest.java
+++ b/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/BadRequest.java
@@ -38,6 +38,9 @@ import org.apache.juneau.http.*;
*
* @since 9.2.1
*/
+@SuppressWarnings({
+ "java:S110" // intentional — mirrors the HTTP status-code response hierarchy
+})
public class BadRequest extends BasicHttpException {
private static final long serialVersionUID = 1L;
diff --git a/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/Conflict.java b/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/Conflict.java
index 901ae07d4f..57845cdc2d 100644
--- a/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/Conflict.java
+++ b/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/Conflict.java
@@ -38,6 +38,9 @@ import org.apache.juneau.http.*;
*
* @since 9.2.1
*/
+@SuppressWarnings({
+ "java:S110" // intentional — mirrors the HTTP status-code response hierarchy
+})
public class Conflict extends BasicHttpException {
private static final long serialVersionUID = 1L;
diff --git a/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/ExpectationFailed.java b/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/ExpectationFailed.java
index d231c5fbfc..20a6a0eec4 100644
--- a/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/ExpectationFailed.java
+++ b/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/ExpectationFailed.java
@@ -38,6 +38,9 @@ import org.apache.juneau.http.*;
*
* @since 9.2.1
*/
+@SuppressWarnings({
+ "java:S110" // intentional — mirrors the HTTP status-code response hierarchy
+})
public class ExpectationFailed extends BasicHttpException {
private static final long serialVersionUID = 1L;
diff --git a/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/FailedDependency.java b/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/FailedDependency.java
index d10b263e8c..59bbe55cc5 100644
--- a/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/FailedDependency.java
+++ b/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/FailedDependency.java
@@ -38,6 +38,9 @@ import org.apache.juneau.http.*;
*
* @since 9.2.1
*/
+@SuppressWarnings({
+ "java:S110" // intentional — mirrors the HTTP status-code response hierarchy
+})
public class FailedDependency extends BasicHttpException {
private static final long serialVersionUID = 1L;
diff --git a/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/Forbidden.java b/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/Forbidden.java
index 6e3a85e6f4..8b8ef643f6 100644
--- a/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/Forbidden.java
+++ b/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/Forbidden.java
@@ -38,6 +38,9 @@ import org.apache.juneau.http.*;
*
* @since 9.2.1
*/
+@SuppressWarnings({
+ "java:S110" // intentional — mirrors the HTTP status-code response hierarchy
+})
public class Forbidden extends BasicHttpException {
private static final long serialVersionUID = 1L;
diff --git a/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/GatewayTimeout.java b/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/GatewayTimeout.java
index 3018273de2..0772114c5b 100644
--- a/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/GatewayTimeout.java
+++ b/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/GatewayTimeout.java
@@ -38,6 +38,9 @@ import org.apache.juneau.http.*;
*
* @since 9.2.1
*/
+@SuppressWarnings({
+ "java:S110" // intentional — mirrors the HTTP status-code response hierarchy
+})
public class GatewayTimeout extends BasicHttpException {
private static final long serialVersionUID = 1L;
diff --git a/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/Gone.java b/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/Gone.java
index 39064c9dc8..c394b77c1b 100644
--- a/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/Gone.java
+++ b/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/Gone.java
@@ -38,6 +38,9 @@ import org.apache.juneau.http.*;
*
* @since 9.2.1
*/
+@SuppressWarnings({
+ "java:S110" // intentional — mirrors the HTTP status-code response hierarchy
+})
public class Gone extends BasicHttpException {
private static final long serialVersionUID = 1L;
diff --git a/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/HttpVersionNotSupported.java b/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/HttpVersionNotSupported.java
index cf1ee7e753..5a6a204c79 100644
--- a/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/HttpVersionNotSupported.java
+++ b/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/HttpVersionNotSupported.java
@@ -38,6 +38,9 @@ import org.apache.juneau.http.*;
*
* @since 9.2.1
*/
+@SuppressWarnings({
+ "java:S110" // intentional — mirrors the HTTP status-code response hierarchy
+})
public class HttpVersionNotSupported extends BasicHttpException {
private static final long serialVersionUID = 1L;
diff --git a/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/InsufficientStorage.java b/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/InsufficientStorage.java
index 89f409bf1b..a197a63f57 100644
--- a/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/InsufficientStorage.java
+++ b/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/InsufficientStorage.java
@@ -38,6 +38,9 @@ import org.apache.juneau.http.*;
*
* @since 9.2.1
*/
+@SuppressWarnings({
+ "java:S110" // intentional — mirrors the HTTP status-code response hierarchy
+})
public class InsufficientStorage extends BasicHttpException {
private static final long serialVersionUID = 1L;
diff --git a/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/InternalServerError.java b/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/InternalServerError.java
index 4648c900c4..f6eb667bb6 100644
--- a/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/InternalServerError.java
+++ b/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/InternalServerError.java
@@ -38,6 +38,9 @@ import org.apache.juneau.http.*;
*
* @since 9.2.1
*/
+@SuppressWarnings({
+ "java:S110" // intentional — mirrors the HTTP status-code response hierarchy
+})
public class InternalServerError extends BasicHttpException {
private static final long serialVersionUID = 1L;
diff --git a/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/LengthRequired.java b/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/LengthRequired.java
index 33b81fd0a9..3c54d1a7ef 100644
--- a/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/LengthRequired.java
+++ b/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/LengthRequired.java
@@ -38,6 +38,9 @@ import org.apache.juneau.http.*;
*
* @since 9.2.1
*/
+@SuppressWarnings({
+ "java:S110" // intentional — mirrors the HTTP status-code response hierarchy
+})
public class LengthRequired extends BasicHttpException {
private static final long serialVersionUID = 1L;
diff --git a/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/Locked.java b/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/Locked.java
index ab6f6b52bc..2952581744 100644
--- a/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/Locked.java
+++ b/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/Locked.java
@@ -38,6 +38,9 @@ import org.apache.juneau.http.*;
*
* @since 9.2.1
*/
+@SuppressWarnings({
+ "java:S110" // intentional — mirrors the HTTP status-code response hierarchy
+})
public class Locked extends BasicHttpException {
private static final long serialVersionUID = 1L;
diff --git a/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/LoopDetected.java b/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/LoopDetected.java
index 322da48ab7..fc2ed657cb 100644
--- a/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/LoopDetected.java
+++ b/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/LoopDetected.java
@@ -38,6 +38,9 @@ import org.apache.juneau.http.*;
*
* @since 9.2.1
*/
+@SuppressWarnings({
+ "java:S110" // intentional — mirrors the HTTP status-code response hierarchy
+})
public class LoopDetected extends BasicHttpException {
private static final long serialVersionUID = 1L;
diff --git a/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/MethodNotAllowed.java b/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/MethodNotAllowed.java
index 8b5b3afa34..2c21d8e3a6 100644
--- a/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/MethodNotAllowed.java
+++ b/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/MethodNotAllowed.java
@@ -38,6 +38,9 @@ import org.apache.juneau.http.*;
*
* @since 9.2.1
*/
+@SuppressWarnings({
+ "java:S110" // intentional — mirrors the HTTP status-code response hierarchy
+})
public class MethodNotAllowed extends BasicHttpException {
private static final long serialVersionUID = 1L;
diff --git a/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/MisdirectedRequest.java b/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/MisdirectedRequest.java
index 21fa2d928d..c8ebe1e228 100644
--- a/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/MisdirectedRequest.java
+++ b/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/MisdirectedRequest.java
@@ -38,6 +38,9 @@ import org.apache.juneau.http.*;
*
* @since 9.2.1
*/
+@SuppressWarnings({
+ "java:S110" // intentional — mirrors the HTTP status-code response hierarchy
+})
public class MisdirectedRequest extends BasicHttpException {
private static final long serialVersionUID = 1L;
diff --git a/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/NetworkAuthenticationRequired.java b/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/NetworkAuthenticationRequired.java
index 7317204dcb..75a003b7be 100644
--- a/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/NetworkAuthenticationRequired.java
+++ b/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/NetworkAuthenticationRequired.java
@@ -38,6 +38,9 @@ import org.apache.juneau.http.*;
*
* @since 9.2.1
*/
+@SuppressWarnings({
+ "java:S110" // intentional — mirrors the HTTP status-code response hierarchy
+})
public class NetworkAuthenticationRequired extends BasicHttpException {
private static final long serialVersionUID = 1L;
diff --git a/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/NotAcceptable.java b/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/NotAcceptable.java
index f775673a08..41f9a5a84d 100644
--- a/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/NotAcceptable.java
+++ b/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/NotAcceptable.java
@@ -38,6 +38,9 @@ import org.apache.juneau.http.*;
*
* @since 9.2.1
*/
+@SuppressWarnings({
+ "java:S110" // intentional — mirrors the HTTP status-code response hierarchy
+})
public class NotAcceptable extends BasicHttpException {
private static final long serialVersionUID = 1L;
diff --git a/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/NotExtended.java b/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/NotExtended.java
index 25f5570c35..4e7e588e07 100644
--- a/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/NotExtended.java
+++ b/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/NotExtended.java
@@ -38,6 +38,9 @@ import org.apache.juneau.http.*;
*
* @since 9.2.1
*/
+@SuppressWarnings({
+ "java:S110" // intentional — mirrors the HTTP status-code response hierarchy
+})
public class NotExtended extends BasicHttpException {
private static final long serialVersionUID = 1L;
diff --git a/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/NotFound.java b/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/NotFound.java
index b8da3fe3b1..701512d0ac 100644
--- a/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/NotFound.java
+++ b/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/NotFound.java
@@ -38,6 +38,9 @@ import org.apache.juneau.http.*;
*
* @since 9.2.1
*/
+@SuppressWarnings({
+ "java:S110" // intentional — mirrors the HTTP status-code response hierarchy
+})
public class NotFound extends BasicHttpException {
private static final long serialVersionUID = 1L;
diff --git a/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/NotImplemented.java b/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/NotImplemented.java
index 1dba2d97af..daff3ecbdc 100644
--- a/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/NotImplemented.java
+++ b/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/NotImplemented.java
@@ -38,6 +38,9 @@ import org.apache.juneau.http.*;
*
* @since 9.2.1
*/
+@SuppressWarnings({
+ "java:S110" // intentional — mirrors the HTTP status-code response hierarchy
+})
public class NotImplemented extends BasicHttpException {
private static final long serialVersionUID = 1L;
diff --git a/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/PayloadTooLarge.java b/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/PayloadTooLarge.java
index 91df923d89..f27a05c383 100644
--- a/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/PayloadTooLarge.java
+++ b/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/PayloadTooLarge.java
@@ -38,6 +38,9 @@ import org.apache.juneau.http.*;
*
* @since 9.2.1
*/
+@SuppressWarnings({
+ "java:S110" // intentional — mirrors the HTTP status-code response hierarchy
+})
public class PayloadTooLarge extends BasicHttpException {
private static final long serialVersionUID = 1L;
diff --git a/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/PreconditionFailed.java b/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/PreconditionFailed.java
index 850e03a91e..aada77712b 100644
--- a/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/PreconditionFailed.java
+++ b/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/PreconditionFailed.java
@@ -38,6 +38,9 @@ import org.apache.juneau.http.*;
*
* @since 9.2.1
*/
+@SuppressWarnings({
+ "java:S110" // intentional — mirrors the HTTP status-code response hierarchy
+})
public class PreconditionFailed extends BasicHttpException {
private static final long serialVersionUID = 1L;
diff --git a/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/PreconditionRequired.java b/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/PreconditionRequired.java
index 8e59d4f1f3..8be1078aeb 100644
--- a/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/PreconditionRequired.java
+++ b/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/PreconditionRequired.java
@@ -38,6 +38,9 @@ import org.apache.juneau.http.*;
*
* @since 9.2.1
*/
+@SuppressWarnings({
+ "java:S110" // intentional — mirrors the HTTP status-code response hierarchy
+})
public class PreconditionRequired extends BasicHttpException {
private static final long serialVersionUID = 1L;
diff --git a/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/RangeNotSatisfiable.java b/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/RangeNotSatisfiable.java
index cb631d1ef8..0241f96ed2 100644
--- a/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/RangeNotSatisfiable.java
+++ b/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/RangeNotSatisfiable.java
@@ -38,6 +38,9 @@ import org.apache.juneau.http.*;
*
* @since 9.2.1
*/
+@SuppressWarnings({
+ "java:S110" // intentional — mirrors the HTTP status-code response hierarchy
+})
public class RangeNotSatisfiable extends BasicHttpException {
private static final long serialVersionUID = 1L;
diff --git a/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/RequestHeaderFieldsTooLarge.java b/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/RequestHeaderFieldsTooLarge.java
index 071a2c75c2..7c864bea8d 100644
--- a/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/RequestHeaderFieldsTooLarge.java
+++ b/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/RequestHeaderFieldsTooLarge.java
@@ -38,6 +38,9 @@ import org.apache.juneau.http.*;
*
* @since 9.2.1
*/
+@SuppressWarnings({
+ "java:S110" // intentional — mirrors the HTTP status-code response hierarchy
+})
public class RequestHeaderFieldsTooLarge extends BasicHttpException {
private static final long serialVersionUID = 1L;
diff --git a/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/ServiceUnavailable.java b/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/ServiceUnavailable.java
index d5796d66f1..2404e7362b 100644
--- a/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/ServiceUnavailable.java
+++ b/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/ServiceUnavailable.java
@@ -38,6 +38,9 @@ import org.apache.juneau.http.*;
*
* @since 9.2.1
*/
+@SuppressWarnings({
+ "java:S110" // intentional — mirrors the HTTP status-code response hierarchy
+})
public class ServiceUnavailable extends BasicHttpException {
private static final long serialVersionUID = 1L;
diff --git a/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/TooManyRequests.java b/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/TooManyRequests.java
index dbc9ad2f5c..665a6fa6e2 100644
--- a/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/TooManyRequests.java
+++ b/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/TooManyRequests.java
@@ -38,6 +38,9 @@ import org.apache.juneau.http.*;
*
* @since 9.2.1
*/
+@SuppressWarnings({
+ "java:S110" // intentional — mirrors the HTTP status-code response hierarchy
+})
public class TooManyRequests extends BasicHttpException {
private static final long serialVersionUID = 1L;
diff --git a/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/Unauthorized.java b/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/Unauthorized.java
index 53c02abf4d..48e1a402a2 100644
--- a/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/Unauthorized.java
+++ b/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/Unauthorized.java
@@ -38,6 +38,9 @@ import org.apache.juneau.http.*;
*
* @since 9.2.1
*/
+@SuppressWarnings({
+ "java:S110" // intentional — mirrors the HTTP status-code response hierarchy
+})
public class Unauthorized extends BasicHttpException {
private static final long serialVersionUID = 1L;
diff --git a/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/UnavailableForLegalReasons.java b/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/UnavailableForLegalReasons.java
index 39889cc43b..081c62e882 100644
--- a/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/UnavailableForLegalReasons.java
+++ b/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/UnavailableForLegalReasons.java
@@ -38,6 +38,9 @@ import org.apache.juneau.http.*;
*
* @since 9.2.1
*/
+@SuppressWarnings({
+ "java:S110" // intentional — mirrors the HTTP status-code response hierarchy
+})
public class UnavailableForLegalReasons extends BasicHttpException {
private static final long serialVersionUID = 1L;
diff --git a/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/UnprocessableEntity.java b/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/UnprocessableEntity.java
index 1920154cc0..0951cb61ae 100644
--- a/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/UnprocessableEntity.java
+++ b/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/UnprocessableEntity.java
@@ -38,6 +38,9 @@ import org.apache.juneau.http.*;
*
* @since 9.2.1
*/
+@SuppressWarnings({
+ "java:S110" // intentional — mirrors the HTTP status-code response hierarchy
+})
public class UnprocessableEntity extends BasicHttpException {
private static final long serialVersionUID = 1L;
diff --git a/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/UnsupportedMediaType.java b/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/UnsupportedMediaType.java
index 8cfce59fc7..5dccc18bb6 100644
--- a/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/UnsupportedMediaType.java
+++ b/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/UnsupportedMediaType.java
@@ -38,6 +38,9 @@ import org.apache.juneau.http.*;
*
* @since 9.2.1
*/
+@SuppressWarnings({
+ "java:S110" // intentional — mirrors the HTTP status-code response hierarchy
+})
public class UnsupportedMediaType extends BasicHttpException {
private static final long serialVersionUID = 1L;
diff --git a/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/UpgradeRequired.java b/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/UpgradeRequired.java
index 583b91f881..70094acd40 100644
--- a/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/UpgradeRequired.java
+++ b/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/UpgradeRequired.java
@@ -38,6 +38,9 @@ import org.apache.juneau.http.*;
*
* @since 9.2.1
*/
+@SuppressWarnings({
+ "java:S110" // intentional — mirrors the HTTP status-code response hierarchy
+})
public class UpgradeRequired extends BasicHttpException {
private static final long serialVersionUID = 1L;
diff --git a/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/UriTooLong.java b/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/UriTooLong.java
index ef779e8c60..b1786c20f6 100644
--- a/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/UriTooLong.java
+++ b/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/UriTooLong.java
@@ -38,6 +38,9 @@ import org.apache.juneau.http.*;
*
* @since 9.2.1
*/
+@SuppressWarnings({
+ "java:S110" // intentional — mirrors the HTTP status-code response hierarchy
+})
public class UriTooLong extends BasicHttpException {
private static final long serialVersionUID = 1L;
diff --git a/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/VariantAlsoNegotiates.java b/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/VariantAlsoNegotiates.java
index bbb5f62a02..c2a5d42d80 100644
--- a/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/VariantAlsoNegotiates.java
+++ b/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/VariantAlsoNegotiates.java
@@ -38,6 +38,9 @@ import org.apache.juneau.http.*;
*
* @since 9.2.1
*/
+@SuppressWarnings({
+ "java:S110" // intentional — mirrors the HTTP status-code response hierarchy
+})
public class VariantAlsoNegotiates extends BasicHttpException {
private static final long serialVersionUID = 1L;
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/servlet/RestServlet.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/servlet/RestServlet.java
index af09eaf577..9e16199f2b 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/servlet/RestServlet.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/servlet/RestServlet.java
@@ -175,6 +175,9 @@ public abstract class RestServlet extends HttpServlet {
*
* @return The context information on this servlet.
*/
+ @SuppressWarnings({
+ "java:S2886" // Reads the AtomicReference 'context' field, which already guarantees atomicity and cross-thread visibility (same as a volatile read); synchronizing this single read with the setContext()/init() lock would add hot-path contention with no correctness benefit.
+ })
public RestContext getContext() {
var rc = context.get();
if (rc == null)
@@ -188,6 +191,9 @@ public abstract class RestServlet extends HttpServlet {
*
* @return The path defined on this servlet, or an empty string if not specified.
*/
+ @SuppressWarnings({
+ "java:S2886" // Reads the AtomicReference 'context' field, which already guarantees atomicity and cross-thread visibility (same as a volatile read); synchronizing this single read with the setContext()/init() lock would add hot-path contention with no correctness benefit.
+ })
public String getPath() {
var context2 = this.context.get();
if (nn(context2))
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/validation/ValidationException.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/validation/ValidationException.java
index d3844cf03b..cca08db5b5 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/validation/ValidationException.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/validation/ValidationException.java
@@ -53,7 +53,8 @@ import org.apache.juneau.http.response.*;
* @since 10.0.0
*/
@SuppressWarnings({
- "java:S110" // Deep inheritance inherent to the exception hierarchy
+ "java:S110", // Deep inheritance inherent to the exception hierarchy
+ "java:S2160" // Exceptions are compared by identity, not by their violations payload; no equals() override needed
})
public class ValidationException extends BadRequest {