This is an automated email from the ASF dual-hosted git repository.
jamesbognar pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/juneau.git
The following commit(s) were added to refs/heads/master by this push:
new f8486683b1 Add fluent programmatic builder API for REST resources and
mixins
f8486683b1 is described below
commit f8486683b13c7c3744568a05c76dcdf5207a5e65
Author: James Bognar <[email protected]>
AuthorDate: Mon Jun 1 14:56:33 2026 -0400
Add fluent programmatic builder API for REST resources and mixins
Reintroduces a public, fluent builder surface (RestBuilder /
AbstractRestBuilder)
for configuring @Rest resources and mixins programmatically (the Spring
@Bean use
case) with builder-set values winning over @Rest annotation values.
- RestBuilder<SELF> / AbstractRestBuilder<R, SELF>: full self-type CRTP
spine
(the builder is the override bag); 55 @Rest members + mdcAsyncPropagation
+
the generic set() escape hatch.
- Three flavor builders (RestServlet/RestResource/RestMixin) leave SELF
open for
true covariant chaining, each with a concrete DefaultBuilder<R> leaf.
- Constructor trio (no-arg + Foo(RestBuilder) + Foo.Builder) with a
package-private
restBuilder stash; RestContext.Args carries it and RestContext prepends a
synthesized @Rest so builder values beat annotations (rung-1 precedence).
- OQ-11 mirror-and-forward for the worker-backed multi-flavor capabilities
(FaviconMixin -> FaviconProvider, VersionMixin -> VersionProvider) via
composition; WellKnownMixin/SeoMixin re-parented for a uniform surface.
- BeanInstantiator exact-type builder preference (Option D): strict-typed
builder
candidates are preferred and weak supertype-only candidates are declined
when a
usable direct constructor exists, resolving the builder(Class)
autodetection
collision in mixin sub-context instantiation.
Adds RestBuilder_Test and BeanInstantiator_OptionD_Test; updates the
heuristic-
pinning BeanInstantiator_Test.w09 to the new stricter selection. Also adds
constructor-rationale Javadoc to the worker+builder constructors.
---
.../juneau/commons/inject/BeanInstantiator.java | 104 +++-
.../apache/juneau/rest/AbstractRestBuilder.java | 353 +++++++++++++
.../java/org/apache/juneau/rest/RestBuilder.java | 561 +++++++++++++++++++++
.../java/org/apache/juneau/rest/RestContext.java | 107 +++-
.../juneau/rest/convention/FaviconMixin.java | 53 +-
.../apache/juneau/rest/convention/SeoMixin.java | 21 +-
.../juneau/rest/convention/VersionMixin.java | 44 +-
.../juneau/rest/convention/WellKnownMixin.java | 21 +-
.../org/apache/juneau/rest/servlet/RestMixin.java | 97 +++-
.../apache/juneau/rest/servlet/RestResource.java | 84 +++
.../apache/juneau/rest/servlet/RestServlet.java | 95 +++-
.../inject/BeanInstantiator_OptionD_Test.java | 165 ++++++
.../commons/inject/BeanInstantiator_Test.java | 19 +-
.../org/apache/juneau/rest/RestBuilder_Test.java | 164 ++++++
juneau-utest/test-run-history.tsv | 1 +
15 files changed, 1845 insertions(+), 44 deletions(-)
diff --git
a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/inject/BeanInstantiator.java
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/inject/BeanInstantiator.java
index c09f8aa89a..6dc0641063 100644
---
a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/inject/BeanInstantiator.java
+++
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/inject/BeanInstantiator.java
@@ -2129,17 +2129,30 @@ public class BeanInstantiator<T> {
}
// Priority 3: Autodetect
- // 3a: Look for static create/builder method that returns a
potential builder type
- r = beanSubType.getPublicMethods().stream()
+ // 3a: Look for static create/builder method that returns a
potential builder type.
+ var staticBuilderTypes = beanSubType.getPublicMethods().stream()
.filter(x -> x.isAll(STATIC, NOT_DEPRECATED,
NOT_SYNTHETIC, NOT_BRIDGE))
.filter(x ->
builderMethodNames.contains(x.getNameSimple()))
.filter(x -> ! x.hasReturnType(beanSubType)) // Must
not return the bean type itself
.filter(x -> isValidBuilderType(x.getReturnType()))
- .findFirst()
- .map(MethodInfo::getReturnType);
- if (r.isPresent()) {
- log("Found builder via static method: %s",
r.get().getName());
- return r.get();
+ .map(MethodInfo::getReturnType)
+ .toList();
+ if (! staticBuilderTypes.isEmpty()) {
+ // TODO-143 (Option D): prefer a factory whose builder
builds the EXACT requested type (or a
+ // subtype) over one that only promises a supertype —
e.g. an inherited generic builder(Class)
+ // whose build() erases to a base class. A
supertype-only factory must not displace a stricter path.
+ var strict =
staticBuilderTypes.stream().filter(this::isStrictBuilderType).findFirst();
+ if (strict.isPresent()) {
+ log("Found builder via static method (strict):
%s", strict.get().getName());
+ return strict.get();
+ }
+ var weak = gateWeakBuilder(staticBuilderTypes.get(0));
+ if (weak != null) {
+ log("Found builder via static method: %s",
weak.getName());
+ return weak;
+ }
+ // Weak/supertype-only static factory rejected in favor
of a direct constructor; fall through to
+ // 3b/3c (a stricter inner Builder may still exist) and
ultimately the constructor path.
}
// 3b: Look for inner Builder class
@@ -2150,12 +2163,17 @@ public class BeanInstantiator<T> {
if (r.isPresent()) {
var builderClass = r.get();
if (isValidBuilderType(builderClass)) {
- log("Found builder via inner class: %s",
builderClass.getName());
+ var gated = isStrictBuilderType(builderClass) ?
builderClass : gateWeakBuilder(builderClass);
+ if (gated != null) {
+ log("Found builder via inner class:
%s", gated.getName());
+ return gated;
+ }
+ // Weak inner builder rejected in favor of a
direct constructor; fall through.
+ } else {
+ // Still return it so we can provide a better
error message when trying to use it
+ log("Found builder class via inner class but it
is not valid: %s", builderClass.getName());
return builderClass;
}
- // Still return it so we can provide a better error
message when trying to use it
- log("Found builder class via inner class but it is not
valid: %s", builderClass.getName());
- return builderClass;
}
// 3c: Look for builder in parent classes and implemented
interfaces (skip beanSubType itself,
@@ -2170,8 +2188,13 @@ public class BeanInstantiator<T> {
if (r.isPresent()) {
var builderClass = r.get();
if (isValidBuilderType(builderClass)) {
- log("Found builder via parent inner
class: %s", builderClass.getName());
- return builderClass;
+ var gated =
isStrictBuilderType(builderClass) ? builderClass :
gateWeakBuilder(builderClass);
+ if (gated != null) {
+ log("Found builder via parent
inner class: %s", gated.getName());
+ return gated;
+ }
+ // Weak parent inner builder rejected
in favor of a direct constructor; keep scanning.
+ continue;
}
// Still return it so we can provide a better
error message when trying to use it
log("Found builder class via parent inner class
but it is not valid: %s", builderClass.getName());
@@ -2184,6 +2207,61 @@ public class BeanInstantiator<T> {
return null;
}
+ /**
+ * Tests whether a builder candidate builds the <b>exact</b> requested
bean type (or a subtype of it),
+ * as opposed to only promising a supertype.
+ *
+ * <p>
+ * Used by {@link #findBuilderType()} (TODO-143 Option D) to prefer a
precise builder over an inherited
+ * generic base builder whose {@code build()} erases to a parent class
(e.g. a self-typed REST
+ * {@code DefaultBuilder} whose {@code build()} returns {@code
RestMixin}).
+ *
+ * @param builderCandidate The builder type to test.
+ * @return <jk>true</jk> if the candidate has a 0-arg (or {@code
@Inject}) build/create/get method whose
+ * return type is {@code beanSubType} or a subtype of it.
+ */
+ private boolean isStrictBuilderType(ClassInfo builderCandidate) {
+ return builderCandidate.getPublicMethods().stream()
+ .filter(x -> x.isAll(NOT_STATIC, NOT_DEPRECATED,
NOT_SYNTHETIC, NOT_BRIDGE))
+ .filter(x ->
buildMethodNames.contains(x.getNameSimple()))
+ .filter(x -> { var rt = x.getReturnType(); return
rt.is(beanSubType.inner()) || beanSubType.isParentOf(rt); })
+ .anyMatch(x ->
x.getAnnotations().stream().anyMatch(JsrSupport::isInjectAnnotation) ||
x.getParameterCount() == 0);
+ }
+
+ /**
+ * TODO-143 Option D gate: a builder candidate that only builds a
<i>supertype</i> of the requested bean
+ * type must not be used when the requested type is concretely
instantiable via a direct constructor.
+ *
+ * <p>
+ * Returns the candidate unchanged when it is safe to use, or {@code
null} to signal "prefer the
+ * constructor path". This is only invoked for non-strict
(supertype-only) candidates.
+ *
+ * @param builderCandidate The (weak) builder candidate.
+ * @return The candidate, or {@code null} to prefer the
direct-constructor path.
+ */
+ private ClassInfo gateWeakBuilder(ClassInfo builderCandidate) {
+ if (hasUsableDirectConstructor()) {
+ log("Builder candidate %s only builds a supertype of %s
and a usable direct constructor exists; preferring the constructor.",
builderCandidate.getName(), beanSubType.getName());
+ return null;
+ }
+ return builderCandidate;
+ }
+
+ /**
+ * Tests whether the requested bean type is concretely instantiable via
a non-private constructor whose
+ * parameters can all be resolved from the bean store (a no-arg
constructor always qualifies).
+ *
+ * @return <jk>true</jk> if a usable direct constructor exists on
{@code beanSubType}.
+ */
+ private boolean hasUsableDirectConstructor() {
+ if (beanSubType.isAbstract() || beanSubType.isInterface())
+ return false;
+ return beanSubType.getDeclaredConstructors().stream()
+ .filter(x -> x.isAll(NOT_DEPRECATED, NOT_PRIVATE))
+ .filter(x -> x.isDeclaringClass(beanSubType))
+ .anyMatch(x -> x.canResolveAllParameters(store,
enclosingInstance));
+ }
+
/**
* Finds all builder types, including the primary builder type and any
builder types found in the builder's parent hierarchy.
*
diff --git
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/AbstractRestBuilder.java
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/AbstractRestBuilder.java
new file mode 100644
index 0000000000..4296c9cda0
--- /dev/null
+++
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/AbstractRestBuilder.java
@@ -0,0 +1,353 @@
+/*
+ * 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;
+
+import static org.apache.juneau.commons.utils.AssertionUtils.*;
+
+import java.util.*;
+
+import org.apache.juneau.commons.reflect.*;
+import org.apache.juneau.encoders.*;
+import org.apache.juneau.httppart.*;
+import org.apache.juneau.rest.annotation.*;
+import org.apache.juneau.rest.arg.*;
+import org.apache.juneau.rest.converter.*;
+import org.apache.juneau.rest.guard.*;
+import org.apache.juneau.rest.logger.*;
+import org.apache.juneau.rest.openapi.*;
+import org.apache.juneau.rest.processor.*;
+import org.apache.juneau.rest.staticfile.*;
+import org.apache.juneau.rest.swagger.*;
+import org.apache.juneau.serializer.*;
+
+/**
+ * Shared abstract base for the {@link RestBuilder} flavor builders ({@code
RestServlet.Builder},
+ * {@code RestResource.Builder}, {@code RestMixin.Builder}).
+ *
+ * <p>
+ * Each fluent {@code @Rest}-member setter is implemented <b>once</b> here and
forwards into a backing
+ * {@link RestAnnotation.Builder} — the override bag. At {@link
RestContext} construction time the bag is
+ * turned into a synthetic, highest-priority {@code @Rest} annotation that is
prepended to the resource's
+ * {@code @Rest} chain, so builder-supplied values <b>take precedence</b> over
the class's own
+ * {@link org.apache.juneau.rest.annotation.Rest @Rest} annotation values.
+ *
+ * <h5 class='section'>Self type (CRTP):</h5>
+ *
+ * <p>
+ * {@code SELF} is the concrete builder type, left open through the flavor
builders and their user subclasses so
+ * that bespoke setters on a subclass chain with true covariant returns.
Setters return {@link #self()}.
+ *
+ * @param <R> The resource type produced by {@link #build()}.
+ * @param <SELF> The concrete builder type (self type).
+ * @since 9.5.0
+ */
+@SuppressWarnings({
+ "unchecked", // CRTP self-type cast in self() is safe by construction.
+ "java:S1452" // Wildcard return on getResourceType() is intentional.
+})
+public abstract class AbstractRestBuilder<R, SELF extends
AbstractRestBuilder<R, SELF>> implements RestBuilder<SELF> {
+
+ private final Class<R> resourceType;
+ private final RestAnnotation.Builder anno = RestAnnotation.create();
+ private final Map<String,Object> extras = new LinkedHashMap<>();
+ private Boolean mdcAsyncPropagation;
+
+ /**
+ * Constructor.
+ *
+ * @param resourceType The resource type produced by {@link #build()}.
Must not be <jk>null</jk>.
+ */
+ protected AbstractRestBuilder(Class<R> resourceType) {
+ this.resourceType = assertArgNotNull("resourceType",
resourceType);
+ }
+
+ /**
+ * Returns this builder cast to the self type.
+ *
+ * @return This object.
+ */
+ protected final SELF self() {
+ return (SELF)this;
+ }
+
+ /**
+ * Builds the configured resource instance.
+ *
+ * @return A new resource instance configured by this builder.
+ */
+ public abstract R build();
+
+ /**
+ * Reflectively instantiates the resource type, preferring a {@code
(RestBuilder)} constructor (constructor
+ * injection — TODO-145 §2.4 constructor trio) and falling
back to the no-arg constructor.
+ *
+ * <p>
+ * Flavor builder {@code build()} implementations call this then stash
{@code this} on the returned instance.
+ *
+ * @return A new, uninitialized resource instance.
+ */
+ protected R createResource() {
+ var ci = ClassInfo.of(resourceType);
+ var ctor = ci.getDeclaredConstructor(x ->
x.hasParameterTypes(RestBuilder.class)).orElse(null);
+ if (ctor != null)
+ return ctor.accessible().newInstance(this);
+ var noArg = ci.getNoArgConstructor(Visibility.PRIVATE)
+ .orElseThrow(() -> new IllegalStateException("Resource
class " + resourceType.getName() + " has no no-arg or RestBuilder
constructor."));
+ return noArg.accessible().newInstance();
+ }
+
+
//-----------------------------------------------------------------------------------------------------------------
+ // Override-bag accessors (consumed by RestContext during construction)
+
//-----------------------------------------------------------------------------------------------------------------
+
+ /**
+ * Returns the resource type produced by this builder.
+ *
+ * @return The resource type. Never <jk>null</jk>.
+ */
+ public Class<?> getResourceType() {
+ return resourceType;
+ }
+
+ /**
+ * Returns the synthetic {@code @Rest} annotation carrying all
builder-set members, for use as the
+ * highest-priority contributor of the {@code @Rest} resolution chain.
+ *
+ * @return The synthetic annotation. Never <jk>null</jk>.
+ */
+ public Rest toRestAnnotation() {
+ return anno.build();
+ }
+
+ /**
+ * Returns the programmatic MDC async-propagation override, or
<jk>null</jk> if not set.
+ *
+ * @return The override, or <jk>null</jk>.
+ */
+ public Boolean getMdcAsyncPropagation() {
+ return mdcAsyncPropagation;
+ }
+
+ /**
+ * Returns the forward-compat extras set via {@link #set(String,
Object)}.
+ *
+ * @return An unmodifiable view of the extras map. Never <jk>null</jk>.
+ */
+ public Map<String,Object> getExtras() {
+ return Collections.unmodifiableMap(extras);
+ }
+
+
//-----------------------------------------------------------------------------------------------------------------
+ // Identity & mounting
+
//-----------------------------------------------------------------------------------------------------------------
+
+ @Override /* RestBuilder */
+ public SELF path(String value) { anno.path(value); return self(); }
+
+ @Override /* RestBuilder */
+ public SELF paths(String... value) { anno.paths(value); return self(); }
+
+ @Override /* RestBuilder */
+ public SELF children(Class<?>... value) { anno.children(value); return
self(); }
+
+ @Override /* RestBuilder */
+ public SELF mixins(Class<?>... value) { anno.mixins(value); return
self(); }
+
+ @Override /* RestBuilder */
+ public SELF uriAuthority(String value) { anno.uriAuthority(value);
return self(); }
+
+ @Override /* RestBuilder */
+ public SELF uriContext(String value) { anno.uriContext(value); return
self(); }
+
+ @Override /* RestBuilder */
+ public SELF uriRelativity(String value) { anno.uriRelativity(value);
return self(); }
+
+ @Override /* RestBuilder */
+ public SELF uriResolution(String value) { anno.uriResolution(value);
return self(); }
+
+
//-----------------------------------------------------------------------------------------------------------------
+ // Marshalling
+
//-----------------------------------------------------------------------------------------------------------------
+
+ @Override /* RestBuilder */
+ public SELF serializers(Class<? extends Serializer>... value) {
anno.serializers(value); return self(); }
+
+ @Override /* RestBuilder */
+ public SELF parsers(Class<?>... value) { anno.parsers(value); return
self(); }
+
+ @Override /* RestBuilder */
+ public SELF encoders(Class<? extends Encoder>... value) {
anno.encoders(value); return self(); }
+
+ @Override /* RestBuilder */
+ public SELF partSerializer(Class<? extends HttpPartSerializer> value) {
anno.partSerializer(value); return self(); }
+
+ @Override /* RestBuilder */
+ public SELF partParser(Class<? extends HttpPartParser> value) {
anno.partParser(value); return self(); }
+
+ @Override /* RestBuilder */
+ public SELF consumes(String... value) { anno.consumes(value); return
self(); }
+
+ @Override /* RestBuilder */
+ public SELF produces(String... value) { anno.produces(value); return
self(); }
+
+ @Override /* RestBuilder */
+ public SELF responseProcessors(Class<? extends ResponseProcessor>...
value) { anno.responseProcessors(value); return self(); }
+
+ @Override /* RestBuilder */
+ public SELF allowedSerializerOptions(String... value) {
anno.allowedSerializerOptions(value); return self(); }
+
+ @Override /* RestBuilder */
+ public SELF allowedParserOptions(String... value) {
anno.allowedParserOptions(value); return self(); }
+
+
//-----------------------------------------------------------------------------------------------------------------
+ // Request behavior
+
//-----------------------------------------------------------------------------------------------------------------
+
+ @Override /* RestBuilder */
+ public SELF allowedHeaderParams(String value) {
anno.allowedHeaderParams(value); return self(); }
+
+ @Override /* RestBuilder */
+ public SELF allowedMethodHeaders(String value) {
anno.allowedMethodHeaders(value); return self(); }
+
+ @Override /* RestBuilder */
+ public SELF allowedMethodParams(String value) {
anno.allowedMethodParams(value); return self(); }
+
+ @Override /* RestBuilder */
+ public SELF clientVersionHeader(String value) {
anno.clientVersionHeader(value); return self(); }
+
+ @Override /* RestBuilder */
+ public SELF defaultAccept(String value) { anno.defaultAccept(value);
return self(); }
+
+ @Override /* RestBuilder */
+ public SELF defaultContentType(String value) {
anno.defaultContentType(value); return self(); }
+
+ @Override /* RestBuilder */
+ public SELF defaultCharset(String value) { anno.defaultCharset(value);
return self(); }
+
+ @Override /* RestBuilder */
+ public SELF defaultRequestAttributes(String... value) {
anno.defaultRequestAttributes(value); return self(); }
+
+ @Override /* RestBuilder */
+ public SELF defaultRequestHeaders(String... value) {
anno.defaultRequestHeaders(value); return self(); }
+
+ @Override /* RestBuilder */
+ public SELF defaultResponseHeaders(String... value) {
anno.defaultResponseHeaders(value); return self(); }
+
+ @Override /* RestBuilder */
+ public SELF disableContentParam(String value) {
anno.disableContentParam(value); return self(); }
+
+ @Override /* RestBuilder */
+ public SELF maxInput(String value) { anno.maxInput(value); return
self(); }
+
+ @Override /* RestBuilder */
+ public SELF restOpArgs(Class<? extends RestOpArg>... value) {
anno.restOpArgs(value); return self(); }
+
+
//-----------------------------------------------------------------------------------------------------------------
+ // Security
+
//-----------------------------------------------------------------------------------------------------------------
+
+ @Override /* RestBuilder */
+ public SELF guards(Class<? extends RestGuard>... value) {
anno.guards(value); return self(); }
+
+ @Override /* RestBuilder */
+ public SELF roleGuard(String value) { anno.roleGuard(value); return
self(); }
+
+ @Override /* RestBuilder */
+ public SELF rolesDeclared(String value) { anno.rolesDeclared(value);
return self(); }
+
+ @Override /* RestBuilder */
+ public SELF converters(Class<? extends RestConverter>... value) {
anno.converters(value); return self(); }
+
+
//-----------------------------------------------------------------------------------------------------------------
+ // Lifecycle / perf
+
//-----------------------------------------------------------------------------------------------------------------
+
+ @Override /* RestBuilder */
+ public SELF eagerInit(String value) { anno.eagerInit(value); return
self(); }
+
+ @Override /* RestBuilder */
+ public SELF lazyChildren(String value) { anno.lazyChildren(value);
return self(); }
+
+ @Override /* RestBuilder */
+ public SELF virtualThreads(String value) { anno.virtualThreads(value);
return self(); }
+
+ @Override /* RestBuilder */
+ public SELF asyncTimeoutMillis(String value) {
anno.asyncTimeoutMillis(value); return self(); }
+
+ @Override /* RestBuilder */
+ public SELF asyncCompletionExecutor(String value) {
anno.asyncCompletionExecutor(value); return self(); }
+
+
//-----------------------------------------------------------------------------------------------------------------
+ // Observability / logging / errors
+
//-----------------------------------------------------------------------------------------------------------------
+
+ @Override /* RestBuilder */
+ public SELF callLogger(Class<? extends CallLogger> value) {
anno.callLogger(value); return self(); }
+
+ @Override /* RestBuilder */
+ public SELF debug(String value) { anno.debug(value); return self(); }
+
+ @Override /* RestBuilder */
+ public SELF observability(String value) { anno.observability(value);
return self(); }
+
+ @Override /* RestBuilder */
+ public SELF renderResponseStackTraces(String value) {
anno.renderResponseStackTraces(value); return self(); }
+
+ @Override /* RestBuilder */
+ public SELF problemDetails(String value) { anno.problemDetails(value);
return self(); }
+
+
//-----------------------------------------------------------------------------------------------------------------
+ // Docs / metadata / i18n / static files
+
//-----------------------------------------------------------------------------------------------------------------
+
+ @Override /* RestBuilder */
+ public SELF title(String... value) { anno.title(value); return self(); }
+
+ @Override /* RestBuilder */
+ public SELF description(String... value) { anno.description(value);
return self(); }
+
+ @Override /* RestBuilder */
+ public SELF siteName(String value) { anno.siteName(value); return
self(); }
+
+ @Override /* RestBuilder */
+ public SELF swaggerProvider(Class<? extends SwaggerProvider> value) {
anno.swaggerProvider(value); return self(); }
+
+ @Override /* RestBuilder */
+ public SELF openApiProvider(Class<? extends OpenApiProvider> value) {
anno.openApiProvider(value); return self(); }
+
+ @Override /* RestBuilder */
+ public SELF messages(String value) { anno.messages(value); return
self(); }
+
+ @Override /* RestBuilder */
+ public SELF config(String value) { anno.config(value); return self(); }
+
+ @Override /* RestBuilder */
+ public SELF staticFiles(Class<? extends StaticFiles> value) {
anno.staticFiles(value); return self(); }
+
+ @Override /* RestBuilder */
+ public SELF noInherit(String... value) { anno.noInherit(value); return
self(); }
+
+
//-----------------------------------------------------------------------------------------------------------------
+ // Programmatic-only knob & escape hatch
+
//-----------------------------------------------------------------------------------------------------------------
+
+ @Override /* RestBuilder */
+ public SELF mdcAsyncPropagation(boolean value) { mdcAsyncPropagation =
value; return self(); }
+
+ @Override /* RestBuilder */
+ public SELF set(String key, Object value) {
extras.put(assertArgNotNull("key", key), value); return self(); }
+}
diff --git
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestBuilder.java
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestBuilder.java
new file mode 100644
index 0000000000..0a0faa600d
--- /dev/null
+++
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestBuilder.java
@@ -0,0 +1,561 @@
+/*
+ * 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;
+
+import org.apache.juneau.encoders.*;
+import org.apache.juneau.httppart.*;
+import org.apache.juneau.rest.annotation.*;
+import org.apache.juneau.rest.arg.*;
+import org.apache.juneau.rest.converter.*;
+import org.apache.juneau.rest.guard.*;
+import org.apache.juneau.rest.logger.*;
+import org.apache.juneau.rest.openapi.*;
+import org.apache.juneau.rest.processor.*;
+import org.apache.juneau.rest.staticfile.*;
+import org.apache.juneau.rest.swagger.*;
+import org.apache.juneau.serializer.*;
+
+/**
+ * Public, fluent, self-typed configuration surface for programmatically
configuring a {@code @Rest} resource,
+ * child resource, or mixin instead of (or in addition to) the {@link Rest
@Rest} annotation.
+ *
+ * <p>
+ * This is the user-facing builder surface introduced for programmatic
resource/mixin configuration (e.g.
+ * instantiating a configured resource bean in a Spring {@code @Bean} method).
Builder-supplied values take
+ * <b>precedence</b> over {@link Rest @Rest} annotation values — they
slot in as the highest-priority
+ * (rung 1) contributor of the runtime-override resolution chain
documented on
+ * {@link RestContext#getPaths()}, generalized to every {@code @Rest} member.
+ *
+ * <p class='bjava'>
+ * <jc>// Configure a resource programmatically; builder values win over
the class's @Rest annotation.</jc>
+ * MyRest <jv>r</jv> =
MyRest.<jsm>builder</jsm>().path(<js>"/foo"</js>).allowedHeaderParams(<js>"foo"</js>).build();
+ * </p>
+ *
+ * <h5 class='section'>Self type (CRTP):</h5>
+ *
+ * <p>
+ * The {@code SELF} type parameter is the concrete builder type, so every
fluent setter returns the most-derived
+ * builder type and bespoke setters added by a builder subclass chain with
true covariant returns. This mirrors
+ * the project-wide {@code SELF}/{@code self()} self-type convention.
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ * <li class='jc'>{@link AbstractRestBuilder}
+ * <li class='jc'>{@link
org.apache.juneau.rest.servlet.RestServlet.Builder}
+ * <li class='jc'>{@link
org.apache.juneau.rest.servlet.RestResource.Builder}
+ * <li class='jc'>{@link org.apache.juneau.rest.servlet.RestMixin.Builder}
+ * <li class='link'><a class="doclink"
href="https://juneau.apache.org/docs/topics/RestAnnotatedClassBasics">@Rest-Annotated
Class Basics</a>
+ * </ul>
+ *
+ * @param <SELF> The concrete builder type (self type).
+ * @since 9.5.0
+ */
+public interface RestBuilder<SELF extends RestBuilder<SELF>> {
+
+
//-----------------------------------------------------------------------------------------------------------------
+ // Identity & mounting
+
//-----------------------------------------------------------------------------------------------------------------
+
+ /**
+ * Specifies the {@link Rest#path() path} for this resource.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ SELF path(String value);
+
+ /**
+ * Specifies the {@link Rest#paths() top-level mount paths} for this
resource.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ SELF paths(String... value);
+
+ /**
+ * Specifies the {@link Rest#children() child resources} for this
resource.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ SELF children(Class<?>... value);
+
+ /**
+ * Specifies the {@link Rest#mixins() mixins} for this resource.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ SELF mixins(Class<?>... value);
+
+ /**
+ * Specifies the {@link Rest#uriAuthority() uriAuthority} for this
resource.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ SELF uriAuthority(String value);
+
+ /**
+ * Specifies the {@link Rest#uriContext() uriContext} for this resource.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ SELF uriContext(String value);
+
+ /**
+ * Specifies the {@link Rest#uriRelativity() uriRelativity} for this
resource.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ SELF uriRelativity(String value);
+
+ /**
+ * Specifies the {@link Rest#uriResolution() uriResolution} for this
resource.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ SELF uriResolution(String value);
+
+
//-----------------------------------------------------------------------------------------------------------------
+ // Marshalling
+
//-----------------------------------------------------------------------------------------------------------------
+
+ /**
+ * Specifies the {@link Rest#serializers() serializers} for this
resource.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ SELF serializers(Class<? extends Serializer>... value);
+
+ /**
+ * Specifies the {@link Rest#parsers() parsers} for this resource.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ SELF parsers(Class<?>... value);
+
+ /**
+ * Specifies the {@link Rest#encoders() encoders} for this resource.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ SELF encoders(Class<? extends Encoder>... value);
+
+ /**
+ * Specifies the {@link Rest#partSerializer() partSerializer} for this
resource.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ SELF partSerializer(Class<? extends HttpPartSerializer> value);
+
+ /**
+ * Specifies the {@link Rest#partParser() partParser} for this resource.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ SELF partParser(Class<? extends HttpPartParser> value);
+
+ /**
+ * Specifies the {@link Rest#consumes() consumes} media types for this
resource.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ SELF consumes(String... value);
+
+ /**
+ * Specifies the {@link Rest#produces() produces} media types for this
resource.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ SELF produces(String... value);
+
+ /**
+ * Specifies the {@link Rest#responseProcessors() responseProcessors}
for this resource.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ SELF responseProcessors(Class<? extends ResponseProcessor>... value);
+
+ /**
+ * Specifies the {@link Rest#allowedSerializerOptions()
allowedSerializerOptions} for this resource.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ SELF allowedSerializerOptions(String... value);
+
+ /**
+ * Specifies the {@link Rest#allowedParserOptions()
allowedParserOptions} for this resource.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ SELF allowedParserOptions(String... value);
+
+
//-----------------------------------------------------------------------------------------------------------------
+ // Request behavior / params
+
//-----------------------------------------------------------------------------------------------------------------
+
+ /**
+ * Specifies the {@link Rest#allowedHeaderParams() allowedHeaderParams}
for this resource.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ SELF allowedHeaderParams(String value);
+
+ /**
+ * Specifies the {@link Rest#allowedMethodHeaders()
allowedMethodHeaders} for this resource.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ SELF allowedMethodHeaders(String value);
+
+ /**
+ * Specifies the {@link Rest#allowedMethodParams() allowedMethodParams}
for this resource.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ SELF allowedMethodParams(String value);
+
+ /**
+ * Specifies the {@link Rest#clientVersionHeader() clientVersionHeader}
for this resource.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ SELF clientVersionHeader(String value);
+
+ /**
+ * Specifies the {@link Rest#defaultAccept() defaultAccept} for this
resource.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ SELF defaultAccept(String value);
+
+ /**
+ * Specifies the {@link Rest#defaultContentType() defaultContentType}
for this resource.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ SELF defaultContentType(String value);
+
+ /**
+ * Specifies the {@link Rest#defaultCharset() defaultCharset} for this
resource.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ SELF defaultCharset(String value);
+
+ /**
+ * Specifies the {@link Rest#defaultRequestAttributes()
defaultRequestAttributes} for this resource.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ SELF defaultRequestAttributes(String... value);
+
+ /**
+ * Specifies the {@link Rest#defaultRequestHeaders()
defaultRequestHeaders} for this resource.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ SELF defaultRequestHeaders(String... value);
+
+ /**
+ * Specifies the {@link Rest#defaultResponseHeaders()
defaultResponseHeaders} for this resource.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ SELF defaultResponseHeaders(String... value);
+
+ /**
+ * Specifies the {@link Rest#disableContentParam() disableContentParam}
flag for this resource.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ SELF disableContentParam(String value);
+
+ /**
+ * Specifies the {@link Rest#maxInput() maxInput} for this resource.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ SELF maxInput(String value);
+
+ /**
+ * Specifies the {@link Rest#restOpArgs() restOpArgs} for this resource.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ SELF restOpArgs(Class<? extends RestOpArg>... value);
+
+
//-----------------------------------------------------------------------------------------------------------------
+ // Security
+
//-----------------------------------------------------------------------------------------------------------------
+
+ /**
+ * Specifies the {@link Rest#guards() guards} for this resource.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ SELF guards(Class<? extends RestGuard>... value);
+
+ /**
+ * Specifies the {@link Rest#roleGuard() roleGuard} for this resource.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ SELF roleGuard(String value);
+
+ /**
+ * Specifies the {@link Rest#rolesDeclared() rolesDeclared} for this
resource.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ SELF rolesDeclared(String value);
+
+ /**
+ * Specifies the {@link Rest#converters() converters} for this resource.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ SELF converters(Class<? extends RestConverter>... value);
+
+
//-----------------------------------------------------------------------------------------------------------------
+ // Lifecycle / perf
+
//-----------------------------------------------------------------------------------------------------------------
+
+ /**
+ * Specifies the {@link Rest#eagerInit() eagerInit} flag for this
resource.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ SELF eagerInit(String value);
+
+ /**
+ * Specifies the {@link Rest#lazyChildren() lazyChildren} flag for this
resource.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ SELF lazyChildren(String value);
+
+ /**
+ * Specifies the {@link Rest#virtualThreads() virtualThreads} flag for
this resource.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ SELF virtualThreads(String value);
+
+ /**
+ * Specifies the {@link Rest#asyncTimeoutMillis() asyncTimeoutMillis}
for this resource.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ SELF asyncTimeoutMillis(String value);
+
+ /**
+ * Specifies the {@link Rest#asyncCompletionExecutor()
asyncCompletionExecutor} bean name for this resource.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ SELF asyncCompletionExecutor(String value);
+
+
//-----------------------------------------------------------------------------------------------------------------
+ // Observability / logging / errors
+
//-----------------------------------------------------------------------------------------------------------------
+
+ /**
+ * Specifies the {@link Rest#callLogger() callLogger} for this resource.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ SELF callLogger(Class<? extends CallLogger> value);
+
+ /**
+ * Specifies the {@link Rest#debug() debug} mode for this resource.
+ *
+ * @param value The new value for this property (e.g. {@code "true"},
{@code "conditional"}).
+ * @return This object.
+ */
+ SELF debug(String value);
+
+ /**
+ * Specifies the {@link Rest#observability() observability} flag for
this resource.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ SELF observability(String value);
+
+ /**
+ * Specifies the {@link Rest#renderResponseStackTraces()
renderResponseStackTraces} flag for this resource.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ SELF renderResponseStackTraces(String value);
+
+ /**
+ * Specifies the {@link Rest#problemDetails() problemDetails} flag for
this resource.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ SELF problemDetails(String value);
+
+
//-----------------------------------------------------------------------------------------------------------------
+ // Docs / metadata / i18n / static files
+
//-----------------------------------------------------------------------------------------------------------------
+
+ /**
+ * Specifies the {@link Rest#title() title} for this resource.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ SELF title(String... value);
+
+ /**
+ * Specifies the {@link Rest#description() description} for this
resource.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ SELF description(String... value);
+
+ /**
+ * Specifies the {@link Rest#siteName() siteName} for this resource.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ SELF siteName(String value);
+
+ /**
+ * Specifies the {@link Rest#swaggerProvider() swaggerProvider} for
this resource.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ SELF swaggerProvider(Class<? extends SwaggerProvider> value);
+
+ /**
+ * Specifies the {@link Rest#openApiProvider() openApiProvider} for
this resource.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ SELF openApiProvider(Class<? extends OpenApiProvider> value);
+
+ /**
+ * Specifies the {@link Rest#messages() messages} bundle location for
this resource.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ SELF messages(String value);
+
+ /**
+ * Specifies the {@link Rest#config() config} location for this
resource.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ SELF config(String value);
+
+ /**
+ * Specifies the {@link Rest#staticFiles() staticFiles} for this
resource.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ SELF staticFiles(Class<? extends StaticFiles> value);
+
+ /**
+ * Specifies the {@link Rest#noInherit() noInherit} property names for
this resource.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ SELF noInherit(String... value);
+
+
//-----------------------------------------------------------------------------------------------------------------
+ // Programmatic-only knob (no @Rest member)
+
//-----------------------------------------------------------------------------------------------------------------
+
+ /**
+ * Specifies whether MDC async propagation is enabled for this resource.
+ *
+ * <p>
+ * This is a <b>programmatic-only</b> knob — it has no {@link
Rest @Rest} annotation member (its default is
+ * env-driven). Setting it here overrides the {@code
RestContext.mdcAsyncPropagation} env-driven default.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ SELF mdcAsyncPropagation(boolean value);
+
+
//-----------------------------------------------------------------------------------------------------------------
+ // Forward-compat escape hatch
+
//-----------------------------------------------------------------------------------------------------------------
+
+ /**
+ * Generic forward-compatible override setter, keyed by the {@code
@Rest} property name (e.g.
+ * {@code "allowContentParam"}, {@code "defaultRequestHeaders"}).
+ *
+ * <p>
+ * Use this escape hatch for members not yet exposed as a dedicated
fluent method. Keys reuse the existing
+ * {@code @Rest} member names.
+ *
+ * @param key The {@code @Rest} property name.
+ * @param value The override value.
+ * @return This object.
+ */
+ SELF set(String key, Object value);
+}
diff --git
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestContext.java
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestContext.java
index ecd0ee19eb..895480d266 100644
---
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestContext.java
+++
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestContext.java
@@ -89,6 +89,9 @@ import org.apache.juneau.rest.debug.format.*;
import org.apache.juneau.rest.httppart.*;
import org.apache.juneau.rest.logger.*;
import org.apache.juneau.rest.metrics.*;
+import org.apache.juneau.rest.servlet.RestMixin;
+import org.apache.juneau.rest.servlet.RestResource;
+import org.apache.juneau.rest.servlet.RestServlet;
import org.apache.juneau.rest.processor.*;
import org.apache.juneau.rest.rrpc.*;
import org.apache.juneau.rest.servlet.*;
@@ -238,7 +241,8 @@ public class RestContext extends Context {
Consumer<WritableBeanStore> beanStoreConfigurer,
BeanStore overridingParent,
String[] paths,
- boolean mixinContext
+ boolean mixinContext,
+ RestBuilder restBuilder
) {
/**
@@ -252,6 +256,27 @@ public class RestContext extends Context {
if (beanStoreConfigurer == null)
beanStoreConfigurer = bs -> {};
}
+
+ /**
+ * Back-compatible constructor without the {@code restBuilder}
component (defaults it to <jk>null</jk>).
+ *
+ * <p>
+ * The {@code restBuilder} (TODO-143) is then resolved during
{@link RestContext} construction from the
+ * resource instance's stashed builder, so call sites that
don't carry one keep working unchanged.
+ *
+ * @param resourceClass The resource class.
+ * @param parentContext The parent context, or <jk>null</jk>.
+ * @param servletConfig The servlet config, or <jk>null</jk>.
+ * @param resource The resource supplier.
+ * @param path The mount path override, or <jk>null</jk>.
+ * @param beanStoreConfigurer The bean-store configurer, or
<jk>null</jk>.
+ * @param overridingParent The overriding parent bean store, or
<jk>null</jk>.
+ * @param paths The programmatic mount-paths override, or
<jk>null</jk>.
+ * @param mixinContext Whether this is a mixin sub-context.
+ */
+ public Args(Class<?> resourceClass, RestContext parentContext,
ServletConfig servletConfig, Supplier<?> resource, String path,
Consumer<WritableBeanStore> beanStoreConfigurer, BeanStore overridingParent,
String[] paths, boolean mixinContext) {
+ this(resourceClass, parentContext, servletConfig,
resource, path, beanStoreConfigurer, overridingParent, paths, mixinContext,
null);
+ }
}
/**
@@ -693,6 +718,22 @@ public class RestContext extends Context {
private final Supplier<?> resource;
private AnnotationWorkList annotationWork;
+ /**
+ * The programmatic configuration builder for this resource (TODO-143),
or <jk>null</jk> when configured purely
+ * by annotation. Resolved during construction from {@link
Args#restBuilder()} or, failing that, from the
+ * resource instance's stashed builder. When non-<jk>null</jk>, a
synthetic highest-priority {@code @Rest}
+ * annotation built from its set members is prepended to {@link
#getRestAnnotations()} so builder-supplied
+ * values take precedence over the resource class's own {@code @Rest}
annotation values.
+ */
+ private final RestBuilder restBuilder;
+
+ /**
+ * The synthetic {@code @Rest} annotation built from {@link
#restBuilder}'s set members, or <jk>null</jk> when
+ * there is no programmatic builder. Prepended at the most-derived
(child) position of
+ * {@link #getRestAnnotations()} so builder-supplied values win over
the resource class's own annotation.
+ */
+ private final Rest builderRestAnnotation;
+
// Private accessors used by memoizer lambdas to satisfy Java's
definite-assignment rules for blank final fields.
private WritableBeanStore beanStore() { return beanStore; }
private Supplier<?> resource() { return resource; }
@@ -2096,6 +2137,24 @@ public class RestContext extends Context {
var rs = new ResourceSupplier(resourceClass,
assertArgNotNull("resource", builder.args.resource()));
resource = rs;
+ // TODO-143: resolve the programmatic configuration
builder. Prefer the one carried on Args (set by
+ // RestServlet.init()); otherwise read the builder
stashed on the resource instance (non-reflective) so
+ // programmatic construction via MockRestClient, child
mounting, and mixin composition all honor it.
+ var rb = builder.args.restBuilder();
+ if (rb == null)
+ rb = stashedRestBuilder(rs.get());
+ restBuilder = rb;
+ if (restBuilder instanceof AbstractRestBuilder<?,?>
arb) {
+ // Synthetic, highest-priority @Rest carrying
the builder-set members; prepended (most-derived
+ // child position) to the @Rest chain by the
restAnnotations memoizer so builder values win.
+ builderRestAnnotation = arb.toRestAnnotation();
+ // Programmatic mdcAsyncPropagation knob (no
@Rest member) wins over the env-driven default.
+ if (arb.getMdcAsyncPropagation() != null &&
builder.mdcAsyncPropagation == null)
+ builder.mdcAsyncPropagation =
arb.getMdcAsyncPropagation();
+ } else {
+ builderRestAnnotation = null;
+ }
+
// --- beanStore setup (May 2026 refactor;
precedence-flipped 9.5) ---
// Determine the parent (bootstrap) store: inherited
from parent resource if present.
@@ -2438,7 +2497,15 @@ public class RestContext extends Context {
* a host's explicit per-property values for any property the mixin
doesn't itself declare (e.g.
* {@code partSerializer}, {@code partParser}).
*/
- private final Memoizer<List<AnnotationInfo<Rest>>> restAnnotations =
memoizer(() -> {
+ private final Memoizer<List<AnnotationInfo<Rest>>> restAnnotations =
memoizer(() -> prependBuilderRestAnnotation(computeRawRestAnnotations()));
+
+ /**
+ * 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.
+ *
+ * @return The raw annotation chain.
+ */
+ private List<AnnotationInfo<Rest>> computeRawRestAnnotations() {
var raw = getAnnotationProvider().find(Rest.class,
ClassInfo.of(getResourceClass()));
if (isMixinContextField())
return raw;
@@ -2451,7 +2518,41 @@ public class RestContext extends Context {
var combined = new ArrayList<>(raw);
combined.addAll(defaultConfigAnnotations);
return Collections.unmodifiableList(combined);
- });
+ }
+
+ /**
+ * Prepends the synthetic builder-supplied {@code @Rest} annotation
(TODO-143) at the most-derived (child)
+ * position so its set members win in both child-first ({@code
findFirst}) and parent-to-child
+ * ({@code reduce-last}) resolution walks. Returns {@code base}
unchanged when there is no programmatic builder.
+ *
+ * @param base The raw annotation chain.
+ * @return The chain with the synthetic annotation prepended (when
present).
+ */
+ private List<AnnotationInfo<Rest>>
prependBuilderRestAnnotation(List<AnnotationInfo<Rest>> base) {
+ if (builderRestAnnotation == null)
+ return base;
+ var combined = new ArrayList<AnnotationInfo<Rest>>(base.size()
+ 1);
+
combined.add(AnnotationInfo.of(ClassInfo.of(getResourceClass()),
builderRestAnnotation));
+ combined.addAll(base);
+ return Collections.unmodifiableList(combined);
+ }
+
+ /**
+ * Non-reflectively reads the programmatic configuration builder
stashed on a resource/mixin instance
+ * (TODO-143 §2.4), or returns <jk>null</jk> when the instance
carries none or is not a builder-aware base type.
+ *
+ * @param r The resource instance.
+ * @return The stashed builder, or <jk>null</jk>.
+ */
+ private static RestBuilder stashedRestBuilder(Object r) {
+ if (r instanceof RestServlet x)
+ return x.getRestBuilder();
+ if (r instanceof RestResource x)
+ return x.getRestBuilder();
+ if (r instanceof RestMixin x)
+ return x.getRestBuilder();
+ return null;
+ }
/**
* The {@code @Rest} annotation list for this resource in
parent-to-child (top-down) order.
diff --git
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/convention/FaviconMixin.java
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/convention/FaviconMixin.java
index e574a94cdc..7edaf75efd 100644
---
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/convention/FaviconMixin.java
+++
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/convention/FaviconMixin.java
@@ -17,7 +17,9 @@
package org.apache.juneau.rest.convention;
import org.apache.juneau.http.*;
+import org.apache.juneau.rest.*;
import org.apache.juneau.rest.annotation.*;
+import org.apache.juneau.rest.servlet.*;
/**
* Mixin that serves a {@code favicon.ico} icon at {@code /favicon.ico}.
@@ -101,7 +103,7 @@ import org.apache.juneau.rest.annotation.*;
*/
// @formatter:off
@Rest
-public class FaviconMixin {
+public class FaviconMixin extends RestMixin {
/** Default {@code Cache-Control} header value: {@code max-age=2592000,
public} (30 days). */
public static final String DEFAULT_CACHE_CONTROL =
FaviconProvider.DEFAULT_CACHE_CONTROL;
@@ -136,6 +138,35 @@ public class FaviconMixin {
this.worker = worker;
}
+ /**
+ * Worker + builder constructor (TODO-143 OQ-11 / §2.4).
+ *
+ * <p>
+ * Used by {@link Builder#build()} to both delegate to the shared
{@link FaviconProvider} worker and stash the
+ * programmatic {@link RestBuilder} (carrying any {@code @Rest}-level
overrides such as {@code path}) so those
+ * values take precedence over the mixin class's own {@link Rest @Rest}
annotation.
+ *
+ * <h5 class='section'>Why worker + builder, not the flavor
Builder:</h5>
+ * <ul class='spaced-list'>
+ * <li>Takes the already-built <b>worker bean</b> (not the flavor
{@link Builder}) so the flavor can be
+ * constructed from ANY independently-supplied worker
— e.g. a user's own {@code @Bean FaviconProvider}
+ * or BeanStore bean, per the delegate-bean model —
not only via this flavor's own builder.
+ * <li>Takes the generic {@link RestBuilder} (here {@code this}
from {@link Builder#build()}) so it honors the
+ * uniform §2.4 {@code Foo(RestBuilder)} injection
contract the base class and DI resolution key on; the
+ * base knows nothing about the concrete flavor builder or
the worker type.
+ * <li>Holds the finished worker product (the {@code final} {@link
FaviconProvider} field), not a transient
+ * builder; the worker is materialized exactly once at
{@link Builder#build()} time.
+ * <li>Keeps the capability worker and the REST-level config as
two distinct inputs.
+ * </ul>
+ *
+ * @param worker The shared {@link FaviconProvider} worker this flavor
delegates to. Must not be <jk>null</jk>.
+ * @param builder The programmatic configuration builder. May be
<jk>null</jk>.
+ */
+ protected FaviconMixin(FaviconProvider worker, RestBuilder builder) {
+ super(builder);
+ this.worker = worker;
+ }
+
/**
* [GET /favicon.ico] — serve the configured favicon bytes.
*
@@ -158,13 +189,23 @@ public class FaviconMixin {
* Mirrors {@link FaviconProvider.Builder}'s configuration methods on
the mixin's own surface and
* forwards each call to an underlying {@link FaviconProvider.Builder},
which builds the shared worker
* the mixin delegates to (TODO-145 §2.3.1 / OQ-11).
+ *
+ * <p>
+ * Extends {@link RestMixin.Builder} (TODO-143 Option B) so the mixin's
bespoke worker-config setters
+ * ({@link #bytes(byte[])}, {@link #classpath(String)}, {@link
#cacheControl(String)}) chain with true
+ * covariant returns alongside the inherited {@link RestBuilder}
surface (e.g. {@code path}, {@code roleGuard}).
+ * This is how a multi-flavor capability avoids triplicating its
REST-level config: the worker config is
+ * forwarded once into {@link FaviconProvider.Builder}, and the REST
config is inherited once from
+ * {@link AbstractRestBuilder}.
*/
- public static class Builder {
+ public static class Builder extends RestMixin.Builder<FaviconMixin,
Builder> {
private final FaviconProvider.Builder worker =
FaviconProvider.create();
/** Constructor — package access for {@link
FaviconMixin#create()}. */
- protected Builder() {}
+ protected Builder() {
+ super(FaviconMixin.class);
+ }
/**
* Sets the raw favicon bytes.
@@ -213,12 +254,14 @@ public class FaviconMixin {
}
/**
- * Builds a {@link FaviconMixin} instance.
+ * Builds a {@link FaviconMixin} instance, delegating to the
shared {@link FaviconProvider} worker and
+ * stashing this builder so its {@code @Rest}-level overrides
take precedence over the annotation.
*
* @return A configured instance.
*/
+ @Override /* AbstractRestBuilder */
public FaviconMixin build() {
- return new FaviconMixin(worker.build());
+ return new FaviconMixin(worker.build(), this);
}
}
}
diff --git
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/convention/SeoMixin.java
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/convention/SeoMixin.java
index f09d0e6776..1edebc5015 100644
---
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/convention/SeoMixin.java
+++
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/convention/SeoMixin.java
@@ -23,6 +23,7 @@ import java.util.*;
import org.apache.juneau.rest.*;
import org.apache.juneau.rest.annotation.*;
+import org.apache.juneau.rest.servlet.*;
/**
* Mixin that serves SEO-conventional endpoints {@code /robots.txt} and {@code
/sitemap.xml}.
@@ -106,7 +107,7 @@ import org.apache.juneau.rest.annotation.*;
*/
// @formatter:off
@Rest
-public class SeoMixin {
+public class SeoMixin extends RestMixin {
/** Default robots policy: deny everything. */
public static final String DEFAULT_ROBOTS = "User-agent: *\nDisallow:
/\n";
@@ -131,9 +132,15 @@ public class SeoMixin {
/**
* Builder constructor.
*
+ * <p>
+ * Stashes the programmatic {@link RestBuilder} (the builder itself,
carrying any {@code @Rest}-level
+ * overrides such as {@code path}) via {@code super(builder)} so those
values take precedence over this
+ * mixin's own {@link Rest @Rest} annotation (TODO-143 §2.4).
+ *
* @param builder The builder.
*/
protected SeoMixin(Builder builder) {
+ super(builder);
robotsTxt = builder.buildRobots();
sitemapEntries = List.copyOf(builder.sitemapEntries);
}
@@ -215,15 +222,22 @@ public class SeoMixin {
/**
* Builder for {@link SeoMixin} instances.
+ *
+ * <p>
+ * Extends {@link RestMixin.Builder} (TODO-143 Option B) so the mixin's
bespoke robots/sitemap setters chain
+ * with true covariant returns alongside the inherited {@link
RestBuilder} surface (e.g. {@code path},
+ * {@code roleGuard}).
*/
- public static class Builder {
+ public static class Builder extends RestMixin.Builder<SeoMixin,
Builder> {
private final List<RobotsRule> robotsRules = new ArrayList<>();
private final List<SitemapEntry> sitemapEntries = new
ArrayList<>();
private String customRobotsTxt;
/** Constructor — package access for {@link
SeoMixin#create()}. */
- protected Builder() {}
+ protected Builder() {
+ super(SeoMixin.class);
+ }
/**
* Adds an {@code Allow} rule to the robots policy.
@@ -305,6 +319,7 @@ public class SeoMixin {
*
* @return A configured instance.
*/
+ @Override /* AbstractRestBuilder */
public SeoMixin build() {
return new SeoMixin(this);
}
diff --git
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/convention/VersionMixin.java
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/convention/VersionMixin.java
index 023cb821aa..f74d2b74ab 100644
---
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/convention/VersionMixin.java
+++
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/convention/VersionMixin.java
@@ -23,6 +23,7 @@ import java.util.jar.*;
import org.apache.juneau.commons.inject.*;
import org.apache.juneau.rest.*;
import org.apache.juneau.rest.annotation.*;
+import org.apache.juneau.rest.servlet.*;
/**
* Mixin that serves deployment-introspection metadata at {@code /version}
(configurable).
@@ -136,7 +137,7 @@ import org.apache.juneau.rest.annotation.*;
*/
// @formatter:off
@Rest
-public class VersionMixin {
+public class VersionMixin extends RestMixin {
/** Sentinel value returned for entries that the worker couldn't
resolve. */
public static final String UNKNOWN = VersionProvider.UNKNOWN;
@@ -184,6 +185,35 @@ public class VersionMixin {
this.worker = worker;
}
+ /**
+ * Worker + builder constructor (TODO-143 OQ-11 / §2.4).
+ *
+ * <p>
+ * Used by {@link Builder#build()} to both delegate to the shared
{@link VersionProvider} worker and stash the
+ * programmatic {@link RestBuilder} (carrying any {@code @Rest}-level
overrides such as {@code path}) so those
+ * values take precedence over the mixin class's own {@link Rest @Rest}
annotation.
+ *
+ * <h5 class='section'>Why worker + builder, not the flavor
Builder:</h5>
+ * <ul class='spaced-list'>
+ * <li>Takes the already-built <b>worker bean</b> (not the flavor
{@link Builder}) so the flavor can be
+ * constructed from ANY independently-supplied worker
— e.g. a user's own {@code @Bean VersionProvider}
+ * or BeanStore bean, per the delegate-bean model —
not only via this flavor's own builder.
+ * <li>Takes the generic {@link RestBuilder} (here {@code this}
from {@link Builder#build()}) so it honors the
+ * uniform §2.4 {@code Foo(RestBuilder)} injection
contract the base class and DI resolution key on; the
+ * base knows nothing about the concrete flavor builder or
the worker type.
+ * <li>Holds the finished worker product (the {@code final} {@link
VersionProvider} field), not a transient
+ * builder; the worker is materialized exactly once at
{@link Builder#build()} time.
+ * <li>Keeps the capability worker and the REST-level config as
two distinct inputs.
+ * </ul>
+ *
+ * @param worker The shared {@link VersionProvider} worker this flavor
delegates to. Must not be <jk>null</jk>.
+ * @param builder The programmatic configuration builder. May be
<jk>null</jk>.
+ */
+ protected VersionMixin(VersionProvider worker, RestBuilder builder) {
+ super(builder);
+ this.worker = worker;
+ }
+
/**
* [GET /version] — emit the assembled metadata as a JSON map.
*
@@ -216,13 +246,20 @@ public class VersionMixin {
* Mirrors {@link VersionProvider.Builder}'s configuration methods on
the mixin's own surface and
* forwards each call to an underlying {@link VersionProvider.Builder},
which builds the shared worker
* the mixin delegates to (TODO-145 §2.3.1 / OQ-11).
+ *
+ * <p>
+ * Extends {@link RestMixin.Builder} (TODO-143 Option B) so the mixin's
bespoke worker-config setters chain
+ * with true covariant returns alongside the inherited {@link
RestBuilder} surface (e.g. {@code path},
+ * {@code roleGuard}). The worker config is forwarded once into {@link
VersionProvider.Builder} and the REST
+ * config is inherited once from {@link AbstractRestBuilder} — no
triplication across the Version flavors.
*/
- public static class Builder {
+ public static class Builder extends RestMixin.Builder<VersionMixin,
Builder> {
private final VersionProvider.Builder worker;
/** Constructor — protected access for {@link
VersionMixin#create()}. */
protected Builder(VersionProvider.Builder worker) {
+ super(VersionMixin.class);
this.worker = worker;
}
@@ -325,8 +362,9 @@ public class VersionMixin {
*
* @return A configured instance.
*/
+ @Override /* AbstractRestBuilder */
public VersionMixin build() {
- return new VersionMixin(worker.build());
+ return new VersionMixin(worker.build(), this);
}
}
}
diff --git
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/convention/WellKnownMixin.java
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/convention/WellKnownMixin.java
index 13412ce23e..e4fa4167d3 100644
---
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/convention/WellKnownMixin.java
+++
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/convention/WellKnownMixin.java
@@ -21,6 +21,7 @@ import java.io.*;
import org.apache.juneau.http.response.*;
import org.apache.juneau.rest.*;
import org.apache.juneau.rest.annotation.*;
+import org.apache.juneau.rest.servlet.*;
/**
* Mixin that serves <a href="https://www.rfc-editor.org/rfc/rfc8615">RFC
8615</a>
@@ -103,7 +104,7 @@ import org.apache.juneau.rest.annotation.*;
*/
// @formatter:off
@Rest
-public class WellKnownMixin {
+public class WellKnownMixin extends RestMixin {
/**
* Creates a new builder.
@@ -124,9 +125,15 @@ public class WellKnownMixin {
/**
* Builder constructor.
*
+ * <p>
+ * Stashes the programmatic {@link RestBuilder} (the builder itself,
carrying any {@code @Rest}-level
+ * overrides such as {@code path}) via {@code super(builder)} so those
values take precedence over this
+ * mixin's own {@link Rest @Rest} annotation (TODO-143 §2.4).
+ *
* @param builder The builder.
*/
protected WellKnownMixin(Builder builder) {
+ super(builder);
securityTxt = builder.securityTxt;
}
@@ -163,13 +170,20 @@ public class WellKnownMixin {
/**
* Builder for {@link WellKnownMixin} instances.
+ *
+ * <p>
+ * Extends {@link RestMixin.Builder} (TODO-143 Option B) so the mixin's
bespoke {@link #securityTxt(String)}
+ * setter chains with true covariant returns alongside the inherited
{@link RestBuilder} surface (e.g.
+ * {@code path}, {@code roleGuard}).
*/
- public static class Builder {
+ public static class Builder extends RestMixin.Builder<WellKnownMixin,
Builder> {
private String securityTxt;
/** Constructor — package access for {@link
WellKnownMixin#create()}. */
- protected Builder() {}
+ protected Builder() {
+ super(WellKnownMixin.class);
+ }
/**
* Sets the {@code security.txt} body that will be served at
@@ -193,6 +207,7 @@ public class WellKnownMixin {
*
* @return A configured instance.
*/
+ @Override /* AbstractRestBuilder */
public WellKnownMixin build() {
return new WellKnownMixin(this);
}
diff --git
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/servlet/RestMixin.java
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/servlet/RestMixin.java
index 9e8f0f8456..85c9033def 100644
---
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/servlet/RestMixin.java
+++
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/servlet/RestMixin.java
@@ -41,14 +41,13 @@ import org.apache.juneau.rest.annotation.*;
* base-less; the new single-responsibility op-mixins extend this base to make
the triad membership
* explicit.
*
- * <h5 class='section'>Builder support (deferred):</h5>
+ * <h5 class='section'>Builder support:</h5>
*
* <p>
- * The fluent programmatic-builder surface ({@code RestBuilder}/{@code
RestMixin.Builder}) that would let
- * a mixin be configured programmatically rather than by annotation is
<b>not</b> part of this base yet
- * — it is deferred along with the resource/servlet builder work. Until
then, {@code RestMixin}
- * carries no builder or stashed-builder state and mixins are configured
purely by their {@code @Rest} /
- * {@code @RestOp} annotations.
+ * The fluent programmatic-builder surface ({@link RestBuilder} / {@link
Builder}) lets a mixin be configured
+ * programmatically rather than (or in addition to) by annotation —
builder-supplied values take precedence
+ * over {@link Rest @Rest} annotation values. Use {@link #builder(Class)} for
the common case, or subclass
+ * {@link Builder} for capability mixins that add their own setters (TODO-143
Option B).
*
* <h5 class='section'>Reaching the host resource:</h5>
*
@@ -87,6 +86,49 @@ public abstract class RestMixin {
*/
private final AtomicReference<RestContext> context = new
AtomicReference<>();
+ /**
+ * The programmatic configuration builder stashed on this instance
(TODO-143 §2.4), or <jk>null</jk> when the
+ * mixin was constructed without a builder. Mutable so it can be
written by either the
+ * {@link #RestMixin(RestBuilder)} constructor or {@link
Builder#build()}. Read non-reflectively by
+ * {@link RestContext} during mixin sub-context construction so
builder-supplied values take precedence over
+ * {@link Rest @Rest} annotation values.
+ */
+ RestBuilder restBuilder;
+
+ /**
+ * Default constructor.
+ */
+ protected RestMixin() {}
+
+ /**
+ * Builder-injection constructor (TODO-145 §2.4 constructor trio).
+ *
+ * @param builder The programmatic configuration builder. May be
<jk>null</jk>.
+ */
+ protected RestMixin(RestBuilder builder) {
+ this.restBuilder = builder;
+ }
+
+ /**
+ * Returns the programmatic configuration builder stashed on this
mixin, or <jk>null</jk> if none.
+ *
+ * @return The stashed builder, or <jk>null</jk>.
+ */
+ public RestBuilder getRestBuilder() {
+ return restBuilder;
+ }
+
+ /**
+ * Creates a new fluent builder for programmatically configuring an
instance of the specified mixin type.
+ *
+ * @param <R> The mixin type.
+ * @param type The mixin type to build. Must not be <jk>null</jk>.
+ * @return A new builder.
+ */
+ public static <R extends RestMixin> DefaultBuilder<R> builder(Class<R>
type) {
+ return new DefaultBuilder<>(type);
+ }
+
/**
* Captures the per-mixin {@link RestContext} sub-context this mixin
instance is bound to.
*
@@ -131,4 +173,47 @@ public abstract class RestMixin {
var c = context.get();
return c == null ? null : c.getParentContext();
}
+
+ /**
+ * Fluent builder for programmatically configuring a {@link RestMixin}
subclass.
+ *
+ * <p>
+ * Subclassable, self-typed (CRTP) flavor builder (TODO-143 Option B).
Capability mixins (e.g.
+ * {@code FaviconMixin}) extend this and add their own worker-config
setters, which chain with true covariant
+ * returns alongside the inherited {@link RestBuilder} surface. For
the common (non-subclassed) case use
+ * {@link RestMixin#builder(Class)} which returns the concrete {@link
DefaultBuilder} leaf.
+ *
+ * @param <R> The mixin type produced by {@link #build()}.
+ * @param <SELF> The concrete builder type (self type).
+ */
+ public static class Builder<R extends RestMixin, SELF extends
Builder<R, SELF>> extends AbstractRestBuilder<R, SELF> {
+
+ /**
+ * Constructor.
+ *
+ * @param type The mixin type produced by {@link #build()}.
Must not be <jk>null</jk>.
+ */
+ protected Builder(Class<R> type) {
+ super(type);
+ }
+
+ @Override /* AbstractRestBuilder */
+ public R build() {
+ var r = createResource();
+ r.restBuilder = this;
+ return r;
+ }
+ }
+
+ /**
+ * Concrete default leaf builder returned by {@link
RestMixin#builder(Class)} for the common (non-subclassed)
+ * case.
+ *
+ * @param <R> The mixin type produced by {@link #build()}.
+ */
+ public static final class DefaultBuilder<R extends RestMixin> extends
Builder<R, DefaultBuilder<R>> {
+ DefaultBuilder(Class<R> type) {
+ super(type);
+ }
+ }
}
diff --git
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/servlet/RestResource.java
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/servlet/RestResource.java
index 69c72d48a3..9f24562fa0 100644
---
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/servlet/RestResource.java
+++
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/servlet/RestResource.java
@@ -45,6 +45,49 @@ public abstract class RestResource {
private AtomicReference<RestContext> context = new AtomicReference<>();
+ /**
+ * The programmatic configuration builder stashed on this instance
(TODO-143 §2.4), or <jk>null</jk> when the
+ * resource was constructed without a builder. Mutable so it can be
written by either the
+ * {@link #RestResource(RestBuilder)} constructor or {@link
Builder#build()}. Read non-reflectively by
+ * {@link RestContext} during construction so builder-supplied values
take precedence over {@code @Rest}
+ * annotation values.
+ */
+ RestBuilder restBuilder;
+
+ /**
+ * Default constructor.
+ */
+ protected RestResource() {}
+
+ /**
+ * Builder-injection constructor (TODO-145 §2.4 constructor trio).
+ *
+ * @param builder The programmatic configuration builder. May be
<jk>null</jk>.
+ */
+ protected RestResource(RestBuilder builder) {
+ this.restBuilder = builder;
+ }
+
+ /**
+ * Returns the programmatic configuration builder stashed on this
resource, or <jk>null</jk> if none.
+ *
+ * @return The stashed builder, or <jk>null</jk>.
+ */
+ public RestBuilder getRestBuilder() {
+ return restBuilder;
+ }
+
+ /**
+ * Creates a new fluent builder for programmatically configuring an
instance of the specified resource type.
+ *
+ * @param <R> The resource type.
+ * @param type The resource type to build. Must not be <jk>null</jk>.
+ * @return A new builder.
+ */
+ public static <R extends RestResource> DefaultBuilder<R>
builder(Class<R> type) {
+ return new DefaultBuilder<>(type);
+ }
+
/**
* Returns the current thread-local HTTP request.
*
@@ -177,4 +220,45 @@ public abstract class RestResource {
protected void setContext(RestContext context) throws ServletException {
this.context.set(context);
}
+
+ /**
+ * Fluent builder for programmatically configuring a {@link
RestResource} subclass.
+ *
+ * <p>
+ * Subclassable, self-typed (CRTP) flavor builder (TODO-143 Option B).
For the common (non-subclassed) case
+ * use {@link RestResource#builder(Class)} which returns the concrete
{@link DefaultBuilder} leaf.
+ *
+ * @param <R> The resource type produced by {@link #build()}.
+ * @param <SELF> The concrete builder type (self type).
+ */
+ public static class Builder<R extends RestResource, SELF extends
Builder<R, SELF>> extends AbstractRestBuilder<R, SELF> {
+
+ /**
+ * Constructor.
+ *
+ * @param type The resource type produced by {@link #build()}.
Must not be <jk>null</jk>.
+ */
+ protected Builder(Class<R> type) {
+ super(type);
+ }
+
+ @Override /* AbstractRestBuilder */
+ public R build() {
+ var r = createResource();
+ r.restBuilder = this;
+ return r;
+ }
+ }
+
+ /**
+ * Concrete default leaf builder returned by {@link
RestResource#builder(Class)} for the common (non-subclassed)
+ * case.
+ *
+ * @param <R> The resource type produced by {@link #build()}.
+ */
+ public static final class DefaultBuilder<R extends RestResource>
extends Builder<R, DefaultBuilder<R>> {
+ DefaultBuilder(Class<R> type) {
+ super(type);
+ }
+ }
}
\ No newline at end of file
diff --git
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/servlet/RestServlet.java
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/servlet/RestServlet.java
index 133b33e379..a72fd7901f 100644
---
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/servlet/RestServlet.java
+++
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/servlet/RestServlet.java
@@ -78,6 +78,54 @@ public abstract class RestServlet extends HttpServlet {
private final AtomicReference<RestContext> context = new
AtomicReference<>();
private final AtomicReference<Exception> initException = new
AtomicReference<>();
+ /**
+ * The programmatic configuration builder stashed on this instance
(TODO-143 §2.4), or <jk>null</jk> when the
+ * resource was constructed without a builder. Mutable so it can be
written by either the
+ * {@link #RestServlet(RestBuilder)} constructor or {@link
Builder#build()} (the no-arg-only setter-stash path).
+ * Read non-reflectively by {@link RestContext} during construction so
builder-supplied values take precedence
+ * over {@link Rest @Rest} annotation values.
+ */
+ RestBuilder restBuilder;
+
+ /**
+ * Default constructor.
+ */
+ protected RestServlet() {}
+
+ /**
+ * Builder-injection constructor (TODO-145 §2.4 constructor trio).
+ *
+ * @param builder The programmatic configuration builder. May be
<jk>null</jk>.
+ */
+ protected RestServlet(RestBuilder builder) {
+ this.restBuilder = builder;
+ }
+
+ /**
+ * Returns the programmatic configuration builder stashed on this
resource, or <jk>null</jk> if none.
+ *
+ * @return The stashed builder, or <jk>null</jk>.
+ * @since 9.5.0
+ */
+ public RestBuilder getRestBuilder() {
+ return restBuilder;
+ }
+
+ /**
+ * Creates a new fluent builder for programmatically configuring an
instance of the specified resource type.
+ *
+ * <p>
+ * Builder-supplied values take precedence over the resource class's
own {@link Rest @Rest} annotation values.
+ *
+ * @param <R> The resource type.
+ * @param type The resource type to build. Must not be <jk>null</jk>.
+ * @return A new builder.
+ * @since 9.5.0
+ */
+ public static <R extends RestServlet> DefaultBuilder<R>
builder(Class<R> type) {
+ return new DefaultBuilder<>(type);
+ }
+
@Override /* Overridden from GenericServlet */
public synchronized void destroy() {
if (nn(context.get()))
@@ -200,7 +248,7 @@ public abstract class RestServlet extends HttpServlet {
if (nn(context.get()))
return;
super.init(servletConfig);
- context.set(new RestContext(new
RestContext.Args(this.getClass(), null, servletConfig, () -> this, "", null,
null, null, false)));
+ context.set(new RestContext(new
RestContext.Args(this.getClass(), null, servletConfig, () -> this, "", null,
null, null, false, restBuilder)));
context.get().postInit();
context.get().postInitChildFirst();
} catch (ServletException e) {
@@ -302,4 +350,49 @@ public abstract class RestServlet extends HttpServlet {
this.context.set(context);
}
}
+
+ /**
+ * Fluent builder for programmatically configuring a {@link
RestServlet} subclass.
+ *
+ * <p>
+ * This is the subclassable, self-typed (CRTP) flavor builder. Its
{@code SELF} type parameter is left open so
+ * a user subclass's bespoke setters chain with true covariant returns
alongside the inherited
+ * {@link RestBuilder} surface (TODO-143 Option B). For the common
case where the builder is not subclassed,
+ * use {@link RestServlet#builder(Class)} which returns the concrete
{@link DefaultBuilder} leaf.
+ *
+ * @param <R> The resource type produced by {@link #build()}.
+ * @param <SELF> The concrete builder type (self type).
+ * @since 9.5.0
+ */
+ public static class Builder<R extends RestServlet, SELF extends
Builder<R, SELF>> extends AbstractRestBuilder<R, SELF> {
+
+ /**
+ * Constructor.
+ *
+ * @param type The resource type produced by {@link #build()}.
Must not be <jk>null</jk>.
+ */
+ protected Builder(Class<R> type) {
+ super(type);
+ }
+
+ @Override /* AbstractRestBuilder */
+ public R build() {
+ var r = createResource();
+ r.restBuilder = this;
+ return r;
+ }
+ }
+
+ /**
+ * Concrete default leaf builder returned by {@link
RestServlet#builder(Class)} for the common (non-subclassed)
+ * case, so callers are not forced to spell the {@code SELF} type
parameter.
+ *
+ * @param <R> The resource type produced by {@link #build()}.
+ * @since 9.5.0
+ */
+ public static final class DefaultBuilder<R extends RestServlet> extends
Builder<R, DefaultBuilder<R>> {
+ DefaultBuilder(Class<R> type) {
+ super(type);
+ }
+ }
}
\ No newline at end of file
diff --git
a/juneau-utest/src/test/java/org/apache/juneau/commons/inject/BeanInstantiator_OptionD_Test.java
b/juneau-utest/src/test/java/org/apache/juneau/commons/inject/BeanInstantiator_OptionD_Test.java
new file mode 100644
index 0000000000..605c00da1a
--- /dev/null
+++
b/juneau-utest/src/test/java/org/apache/juneau/commons/inject/BeanInstantiator_OptionD_Test.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.commons.inject;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.apache.juneau.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * TODO-143 (Option D) acceptance tests for {@link BeanInstantiator}'s
stricter builder-candidate selection.
+ *
+ * <p>
+ * A builder candidate whose {@code build()} only promises a <b>supertype</b>
of the requested bean type (e.g.
+ * an inherited generic self-typed base builder, or one reached via a {@code
Class}-parameterized factory) must
+ * not displace a stricter instantiation path — a usable direct constructor,
or a builder that builds the exact
+ * requested type. These tests verify: (a) an exact-typed builder still wins;
(b) a parent-only builder loses to
+ * a direct constructor; (c) a {@code Class}-parameterized parent-only factory
loses to a direct constructor;
+ * (d) a subtype's own exact builder still wins even when a constructor
exists; and (e) plain POJOs are
+ * unaffected.
+ */
+@SuppressWarnings("java:S2094") // Intentionally empty helper bean.
+class BeanInstantiator_OptionD_Test extends TestBase {
+
+ private BasicBeanStore beanStore;
+
+ @BeforeEach
+ void setUp() {
+ beanStore = new BasicBeanStore(null);
+ A_Base.builderUsed = false;
+ C_Base.factoryUsed = false;
+ }
+
+ private <T> T run(Class<T> c) {
+ return BeanInstantiator.of(c, beanStore).run();
+ }
+
+
//-----------------------------------------------------------------------------------------------------------------
+ // a — exact-typed builder still wins; parent-only builder loses to a
direct constructor.
+
//-----------------------------------------------------------------------------------------------------------------
+
+ public static class A_Base {
+ static boolean builderUsed;
+ public static Builder create() { return new Builder(); }
+ public A_Base() {}
+ public static class Builder {
+ public A_Base build() { builderUsed = true; return new
A_Base(); }
+ }
+ }
+
+ // Subtype with a usable no-arg constructor and NO builder of its own;
it only inherits A_Base.create(),
+ // whose build() returns the supertype A_Base.
+ public static class A_Sub extends A_Base {
+ public boolean viaCtor;
+ public A_Sub() { this.viaCtor = true; }
+ }
+
+ @Test
+ void a01_exactTypedBuilderWins() {
+ var bean = run(A_Base.class);
+ assertNotNull(bean);
+ assertTrue(A_Base.builderUsed, "Exact-typed builder should have
been used for A_Base.");
+ }
+
+ @Test
+ void a02_parentOnlyBuilderLosesToDirectConstructor() {
+ var bean = run(A_Sub.class);
+ assertNotNull(bean);
+ assertInstanceOf(A_Sub.class, bean);
+ assertTrue(bean.viaCtor, "A_Sub should be built via its direct
constructor.");
+ assertFalse(A_Base.builderUsed, "The supertype-only builder
must not be used when a direct constructor exists.");
+ }
+
+
//-----------------------------------------------------------------------------------------------------------------
+ // b — Class-parameterized parent-only factory loses to a direct
constructor (mirrors the REST builder(Class) shape).
+
//-----------------------------------------------------------------------------------------------------------------
+
+ public static class C_Base {
+ static boolean factoryUsed;
+ public static <R extends C_Base> DefaultBuilder<R>
builder(Class<R> type) { return new DefaultBuilder<>(type); }
+ public C_Base() {}
+ public static class DefaultBuilder<R extends C_Base> {
+ private final Class<R> type;
+ DefaultBuilder(Class<R> type) { this.type = type; }
+ public R build() {
+ factoryUsed = true;
+ try { return
type.getDeclaredConstructor().newInstance(); }
+ catch (ReflectiveOperationException e) { throw
new RuntimeException(e); }
+ }
+ }
+ }
+
+ public static class C_Sub extends C_Base {
+ public boolean viaCtor;
+ public C_Sub() { this.viaCtor = true; }
+ }
+
+ @Test
+ void b01_classParameterizedParentOnlyFactoryLosesToCtor() {
+ // Make the Class parameter resolvable so that, absent the
Option-D rule, the weak factory would be selected.
+ beanStore.add(Class.class, C_Sub.class);
+ var bean = run(C_Sub.class);
+ assertNotNull(bean);
+ assertInstanceOf(C_Sub.class, bean);
+ assertTrue(bean.viaCtor, "C_Sub should be built via its direct
constructor.");
+ assertFalse(C_Base.factoryUsed, "The Class-parameterized
supertype-only factory must not displace the constructor.");
+ }
+
+
//-----------------------------------------------------------------------------------------------------------------
+ // c — a subtype's OWN exact builder still wins even when a constructor
exists (gate only fires for weak builders).
+
//-----------------------------------------------------------------------------------------------------------------
+
+ public static class D_Base {
+ public D_Base() {}
+ }
+
+ public static class D_Sub extends D_Base {
+ boolean viaBuilder;
+ boolean viaCtor;
+ public static Builder create() { return new Builder(); }
+ public D_Sub() { this.viaCtor = true; }
+ D_Sub(boolean fromBuilder) { this.viaBuilder = fromBuilder; }
+ public static class Builder {
+ public D_Sub build() { return new D_Sub(true); }
+ }
+ }
+
+ @Test
+ void c01_subtypeExactBuilderWinsOverConstructor() {
+ var bean = run(D_Sub.class);
+ assertNotNull(bean);
+ assertTrue(bean.viaBuilder, "D_Sub's own exact-typed builder
should win even though a constructor exists.");
+ assertFalse(bean.viaCtor);
+ }
+
+
//-----------------------------------------------------------------------------------------------------------------
+ // d — plain POJO with no builder is unaffected (constructor path).
+
//-----------------------------------------------------------------------------------------------------------------
+
+ public static class E_Pojo {
+ public boolean viaCtor;
+ public E_Pojo() { this.viaCtor = true; }
+ }
+
+ @Test
+ void d01_plainPojoUsesConstructor() {
+ var bean = run(E_Pojo.class);
+ assertNotNull(bean);
+ assertTrue(bean.viaCtor);
+ }
+}
diff --git
a/juneau-utest/src/test/java/org/apache/juneau/commons/inject/BeanInstantiator_Test.java
b/juneau-utest/src/test/java/org/apache/juneau/commons/inject/BeanInstantiator_Test.java
index 37f184ff6c..817e47ff21 100644
---
a/juneau-utest/src/test/java/org/apache/juneau/commons/inject/BeanInstantiator_Test.java
+++
b/juneau-utest/src/test/java/org/apache/juneau/commons/inject/BeanInstantiator_Test.java
@@ -4588,15 +4588,20 @@ class BeanInstantiator_Test extends TestBase {
}
@Test
- @DisplayName("W09 - Builder inner class is discovered when
declared on a parent class (Priority 3c)")
+ @DisplayName("W09 - Supertype-only parent inner builder loses
to the subtype's direct constructor (TODO-143 Option D)")
void w09_builderInParentClass() {
- // W_BeanWithParentInnerBuilder has no declared
@Builder, no static factory, and no inner
- // class. Priority 3c walks the parent chain and finds
W_BeanWithInnerBuilder.Builder.
+ // W_BeanWithParentInnerBuilder has no declared
@Builder, no static factory, and no inner class of
+ // its own. Priority 3c does find
W_BeanWithInnerBuilder.Builder on the parent, BUT that builder's
+ // build() only promises the *supertype*
W_BeanWithInnerBuilder. Under TODO-143 Option D a
+ // supertype-only builder candidate must not displace a
usable direct constructor on the more-specific
+ // requested type, so selection is declined in favor of
the no-arg constructor. (Previously the
+ // parent-only builder was reported as discovered even
though its output — a parent instance — was
+ // always rejected and discarded in favor of this same
constructor; that was heuristic-pinning, not a
+ // functional path.)
var b = bc(W_BeanWithParentInnerBuilder.class);
- b.run();
- var builderType = b.getBuilderType();
- assertNotNull(builderType, "Builder type must be
discovered via parent inner class");
- assertTrue(builderType.getName().endsWith("$Builder"));
+ var bean = b.run();
+ assertNull(b.getBuilderType(), "Supertype-only parent
inner builder must not be selected when a direct constructor exists.");
+ assertEquals("parent-default", bean.origin, "Instance
must be built via the direct constructor.");
}
public static class W_BeanWithSupplierSetter {
diff --git
a/juneau-utest/src/test/java/org/apache/juneau/rest/RestBuilder_Test.java
b/juneau-utest/src/test/java/org/apache/juneau/rest/RestBuilder_Test.java
new file mode 100644
index 0000000000..b16e8e3146
--- /dev/null
+++ b/juneau-utest/src/test/java/org/apache/juneau/rest/RestBuilder_Test.java
@@ -0,0 +1,164 @@
+/*
+ * 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;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.rest.annotation.*;
+import org.apache.juneau.rest.convention.*;
+import org.apache.juneau.rest.servlet.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * TODO-143 acceptance tests for the fluent {@link RestBuilder} / {@link
AbstractRestBuilder} configuration surface:
+ * builder-set values override {@code @Rest} annotation values; the
constructor trio (no-arg, {@code Foo(RestBuilder)},
+ * {@code Foo.Builder}); subclass builders chaining with true covariant
returns (Option B); and OQ-11
+ * mirror-and-forward per-flavor builders (FaviconMixin) gaining the full REST
surface via {@link RestMixin.Builder}.
+ */
+class RestBuilder_Test extends TestBase {
+
+
//-----------------------------------------------------------------------------------------------------------------
+ // Test resources
+
//-----------------------------------------------------------------------------------------------------------------
+
+ @Rest(path="annpath", clientVersionHeader="Ann-Version",
allowedHeaderParams="AnnHdr")
+ public static class A extends RestResource {}
+
+ @Rest(path="annpath")
+ public static class B extends RestResource {
+ final boolean viaBuilderCtor;
+ public B() { this.viaBuilderCtor = false; }
+ public B(RestBuilder builder) { super(builder);
this.viaBuilderCtor = true; }
+ }
+
+ private static RestContext ctx(RestResource r) throws Exception {
+ return new RestContext(new RestContext.Args(r.getClass(), null,
null, () -> r, "", null, null, null, false))
+ .postInit().postInitChildFirst();
+ }
+
+
//-----------------------------------------------------------------------------------------------------------------
+ // Builder values override @Rest annotation values (precedence).
+
//-----------------------------------------------------------------------------------------------------------------
+
+ @Test
+ void a01_builderOverridesAnnotation_scalar() throws Exception {
+ var r =
RestResource.builder(A.class).path("bldpath").clientVersionHeader("Bld-Version").build();
+ var c = ctx(r);
+ assertEquals("bldpath", c.getFullPath());
+ assertEquals("Bld-Version", c.getClientVersionHeader());
+ }
+
+ @Test
+ void a02_builderOverridesAnnotation_set() throws Exception {
+ var r =
RestResource.builder(A.class).allowedHeaderParams("BldHdr").build();
+ assertTrue(ctx(r).getAllowedHeaderParams().contains("BldHdr"));
+ }
+
+ @Test
+ void a03_annotationUsedWhenNoBuilder() throws Exception {
+ // Control: a plain instance (no builder) falls through to the
@Rest annotation value.
+ assertEquals("annpath", ctx(new A()).getFullPath());
+ }
+
+ @Test
+ void a04_unsetBuilderMembersFallThroughToAnnotation() throws Exception {
+ // Only path is overridden; clientVersionHeader should still
resolve from the annotation.
+ var r = RestResource.builder(A.class).path("bldpath").build();
+ var c = ctx(r);
+ assertEquals("bldpath", c.getFullPath());
+ assertEquals("Ann-Version", c.getClientVersionHeader());
+ }
+
+
//-----------------------------------------------------------------------------------------------------------------
+ // Constructor trio (no-arg, Foo(RestBuilder), Foo.Builder).
+
//-----------------------------------------------------------------------------------------------------------------
+
+ @Test
+ void b01_noArgConstructor() {
+ assertFalse(new B().viaBuilderCtor);
+ assertNull(new B().getRestBuilder());
+ }
+
+ @Test
+ void b02_builderConstructorInjection() {
+ // B declares B(RestBuilder); createResource() must prefer it
over the no-arg constructor.
+ var builder = RestResource.builder(B.class).path("x");
+ var r = builder.build();
+ assertTrue(r.viaBuilderCtor);
+ assertSame(builder, r.getRestBuilder());
+ }
+
+ @Test
+ void b03_builderProducesStashedInstance() {
+ var builder = RestResource.builder(A.class).path("x");
+ var r = builder.build();
+ assertSame(builder, r.getRestBuilder());
+ }
+
+
//-----------------------------------------------------------------------------------------------------------------
+ // Subclass builders chain with true covariant returns (Option B).
+
//-----------------------------------------------------------------------------------------------------------------
+
+ public static class CustomBuilder extends RestResource.Builder<A,
CustomBuilder> {
+ String custom;
+ public CustomBuilder() { super(A.class); }
+ public CustomBuilder custom(String value) { this.custom =
value; return self(); }
+ }
+
+ @Test
+ void c01_subclassCovariantChaining() {
+ // If any inherited setter returned the base type, the trailing
.custom(...) would not compile.
+ CustomBuilder b = new
CustomBuilder().path("/p").custom("a").allowedHeaderParams("h").custom("b");
+ assertEquals("b", b.custom);
+ var r = b.build();
+ assertSame(b, r.getRestBuilder());
+ }
+
+
//-----------------------------------------------------------------------------------------------------------------
+ // OQ-11 mirror-and-forward per-flavor builder (FaviconMixin) + mixin
builder support.
+
//-----------------------------------------------------------------------------------------------------------------
+
+ @Test
+ void d01_faviconMirrorForwardChainsWithRestSurface() {
+ // Worker-config setters (bytes/cacheControl) and the inherited
REST surface (path) chain covariantly.
+ var fm = FaviconMixin.create()
+ .path("/icons")
+ .cacheControl("max-age=10")
+ .bytes(new byte[]{1,2,3})
+ .build();
+ assertNotNull(fm);
+ // The worker was configured (serves without error).
+ assertNotNull(fm.getFavicon());
+ // The inherited REST-surface override flowed into the stashed
builder (would win over @Rest).
+ var rb = fm.getRestBuilder();
+ assertNotNull(rb);
+ assertEquals("/icons",
((AbstractRestBuilder<?,?>)rb).toRestAnnotation().path());
+ }
+
+ @Rest
+ public static class M extends RestMixin {}
+
+ @Test
+ void d02_mixinBuilderViaFactory() {
+ var builder = RestMixin.builder(M.class).path("/p");
+ var m = builder.build();
+ assertNotNull(m);
+ assertSame(builder, m.getRestBuilder());
+ assertSame(RestMixin.DefaultBuilder.class,
m.getRestBuilder().getClass());
+ }
+}
diff --git a/juneau-utest/test-run-history.tsv
b/juneau-utest/test-run-history.tsv
index 719f3ff55a..716f92e14d 100644
--- a/juneau-utest/test-run-history.tsv
+++ b/juneau-utest/test-run-history.tsv
@@ -60,3 +60,4 @@ timestamp git_sha branch tests_run failures
errors skipped surefire_sec wall_sec
2026-06-01T14:07:35Z 40a74b4f9452 master 126135 0 0 26
186
2026-06-01T15:50:15Z f167a9dccbc7 master 126165 0 0 26
180
2026-06-01T17:10:12Z 6f9df91cec12 master 126175 0 0 26
181
+2026-06-01T18:55:33Z 8eceb21bb5b9 master 126190 0 0 26
185