This is an automated email from the ASF dual-hosted git repository.

jamesbognar pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/juneau.git

commit b1996be2ff6dec21b39d7ce4dd4aa65d9468c195
Author: James Bognar <[email protected]>
AuthorDate: Fri May 8 15:39:21 2026 -0400

    refactor(inject): TODO-15 phase-3 batch 13 — migrate findRestOperationArgs 
to BeanInstantiator
    
    BeanInstantiator new opt-in flag: factoryAbstainOnNull().
    
    Default v2 behavior: when a static factory method matches and is invoked but
    returns null, BeanInstantiator falls through to constructor lookup as if the
    factory weren't there.  That works for "factory method as alternate
    construction path" callers, but breaks the legacy BeanCreator pattern of 
using
    a null factory return as a deliberate "this implementation does not handle
    the input — try the next strategy" abstain signal.  Classic example: every
    RestOpArg subclass exposes a static create(ParameterInfo) that returns null
    when the parameter isn't annotated with the marker the subclass handles
    (AttributeArg returns null when @Attr is absent, etc.) — falling through to
    AttributeArg's protected constructor would silently produce a bogus arg.
    
    New flag preserves v2 default for everyone else and gives callers that need
    legacy semantics a one-line opt-in:
        .factoryAbstainOnNull()
    When set, run() returns null immediately after invoking a factory method
    whose return value is null, skipping constructor lookup.
    
    Migrated RestContext.findRestOperationArgs:
        BeanCreator.of(RestOpArg.class, bs).type(c).run()
    ->
        BeanInstantiator.of(RestOpArg.class, bs)
            .beanSubType(c)
            .factoryMethodNames("getInstance", "create")
            .factoryAbstainOnNull()
            .run()
    factoryMethodNames adds "create" to the v2 default ({"getInstance"}) so
    RestOpArg subclasses' static create(ParameterInfo) factories are recognized
    (legacy BeanCreator hard-coded "create"/"builder"/"getInstance" as factory
    methods; v2 reserves "create"/"builder" for builder-type discovery by 
default).
    
    Build clean; juneau-utest passes; jetty-ftest passes (verified that
    AttributeArg / BodyArg / HeaderArg / etc. all resolve correctly again).
    
    Co-authored-by: Cursor <[email protected]>
---
 .../juneau/commons/inject/BeanInstantiator.java    | 64 +++++++++++++++++++---
 .../java/org/apache/juneau/rest/RestContext.java   |  6 +-
 2 files changed, 62 insertions(+), 8 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 c0edc4023e..907df8d040 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
@@ -269,6 +269,7 @@ public class BeanInstantiator<T> {
        private Supplier<? extends T> fallbackSupplier = null;
        private final String name;
        private boolean cached = false;
+       private boolean factoryAbstainOnNull = false;
 
        private Memoizer<ClassInfo> builderType = memoizer(() -> 
findBuilderType());
        private Memoizer<List<ClassInfo>> builderTypes = memoizer(() -> 
findBuilderTypes());
@@ -712,6 +713,49 @@ public class BeanInstantiator<T> {
                return this;
        }
 
+       /**
+        * Treat a {@code null} return value from a static factory method as a 
deliberate "abstain" signal.
+        *
+        * <p>
+        * By default, when a static factory method (one of {@link 
#factoryMethodNames(String...)}) matches and is
+        * invoked, but returns {@code null}, {@link #run()} falls through to 
constructor lookup as if the factory
+        * method weren't there. This works well when the factory method is 
just an alternate construction path.
+        *
+        * <p>
+        * Some factory-method patterns instead use a {@code null} return value 
as a deliberate "this implementation
+        * does not handle the input — try the next strategy" signal, expecting 
the caller to interpret the {@code null}
+        * as a definitive answer. The classic example is a {@code RestOpArg} 
subclass whose
+        * {@code static create(ParameterInfo)} returns {@code null} when the 
parameter isn't annotated with the
+        * marker the subclass handles, so the caller can move on to the next 
subclass in the chain. Falling through
+        * to the (always-present) constructor in that case would silently 
materialize an arg that doesn't apply.
+        *
+        * <p>
+        * When this flag is enabled, a {@code null} return from the matching 
factory method is propagated out of
+        * {@link #run()} unchanged — constructor lookup is skipped. This 
matches legacy {@code BeanCreator}
+        * semantics for static {@code create()} / {@code builder()} / {@code 
getInstance()} methods.
+        *
+        * <h5 class='section'>Example:</h5>
+        * <p class='bjava'>
+        *      <jc>// AttributeArg.create(ParameterInfo) returns null if @Attr 
is not present.</jc>
+        *      <jc>// Without factoryAbstainOnNull, BeanInstantiator would 
then invoke the AttributeArg(ParameterInfo)</jc>
+        *      <jc>// constructor and return a bogus instance.</jc>
+        *      RestOpArg <jv>arg</jv> = BeanInstantiator
+        *              .<jsm>of</jsm>(RestOpArg.<jk>class</jk>, <jv>store</jv>)
+        *              .beanSubType(AttributeArg.<jk>class</jk>)
+        *              .factoryMethodNames(<js>"getInstance"</js>, 
<js>"create"</js>)
+        *              .factoryAbstainOnNull()
+        *              .run();
+        * </p>
+        *
+        * @return This object.
+        */
+       public BeanInstantiator<T> factoryAbstainOnNull() {
+               try (var writeLock = lock.write()) {
+                       factoryAbstainOnNull = true;
+               }
+               return this;
+       }
+
        /**
         * Creates the bean.
         *
@@ -1284,18 +1328,24 @@ public class BeanInstantiator<T> {
                log("Attempting Bean.factoryMethod()");
                // If builder was detected but has no build method, pass it as 
extra bean for factory methods
                Object[] factoryMethodExtraBeans = builder2 != null ? new 
Object[]{builder2} : new Object[0];
-               bean = beanSubType.getPublicMethods().stream()
+               var factoryMethod = beanSubType.getPublicMethods().stream()
                        .filter(x -> x.isAll(STATIC, NOT_DEPRECATED, 
NOT_SYNTHETIC, NOT_BRIDGE))
                        .filter(x -> 
factoryMethodNames.contains(x.getNameSimple()))
                        .filter(x -> x.hasReturnType(beanSubType))
                        .filter(x -> x.canResolveAllParameters(store2, 
factoryMethodExtraBeans))
                        .sorted(methodComparator)
-                       .findFirst()
-                       .map(x -> {
-                               log("Found factory method: %s", 
x.getNameFull());
-                               return (T)beanType.cast(x.inject(store2, null, 
factoryMethodExtraBeans));
-       })
-                       .orElse(null);
+                       .findFirst();
+               if (factoryMethod.isPresent()) {
+                       log("Found factory method: %s", 
factoryMethod.get().getNameFull());
+                       bean = (T) 
beanType.cast(factoryMethod.get().inject(store2, null, 
factoryMethodExtraBeans));
+                       // When factoryAbstainOnNull is set, treat a null 
result from the factory method as a
+                       // deliberate "abstain" signal and return null without 
falling through to constructor
+                       // lookup.  Used by callers like RestOpArg subclasses 
whose create(ParameterInfo)
+                       // returns null when the parameter isn't annotated with 
the marker the subclass
+                       // handles (e.g. AttributeArg.create() returns null 
when @Attr isn't present).
+                       if (bean == null && factoryAbstainOnNull)
+                               return null;
+               }
 
                // Look for Bean().
                // Skip constructor invocation when beanSubType is abstract or 
an interface — Class.newInstance() would
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 d9731ef540..4a4ccd05bc 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
@@ -2697,7 +2697,11 @@ public class RestContext extends Context {
                        bs.addBean(ParameterInfo.class, pi);
                        for (var c : roa) {
                                try {
-                                       ra[i] = BeanCreator.of(RestOpArg.class, 
bs).type(c).run();
+                                       ra[i] = 
BeanInstantiator.of(RestOpArg.class, bs)
+                                               .beanSubType(c)
+                                               
.factoryMethodNames("getInstance", "create")
+                                               .factoryAbstainOnNull()
+                                               .run();
                                        if (nn(ra[i]))
                                                break;
                                } catch (ExecutableException e) {

Reply via email to