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 80186dea7c Replace Mutaters with BasicConverter
80186dea7c is described below

commit 80186dea7c2c9301f378223d3218a3db046daf8c
Author: James Bognar <[email protected]>
AuthorDate: Thu Apr 2 17:57:43 2026 -0700

    Replace Mutaters with BasicConverter
---
 .../commons/conversion/ConfigurableConverter.java  |  78 ++++-------
 .../commons/conversion/ConversionFinder.java       |  59 ++++++++
 .../main/java/org/apache/juneau/BeanContext.java   |  34 +----
 .../conversion/ConfigurableConverter_Test.java     | 151 +++++++++------------
 4 files changed, 152 insertions(+), 170 deletions(-)

diff --git 
a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/conversion/ConfigurableConverter.java
 
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/conversion/ConfigurableConverter.java
index 6cfddb85a4..152bf0ab6d 100644
--- 
a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/conversion/ConfigurableConverter.java
+++ 
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/conversion/ConfigurableConverter.java
@@ -17,95 +17,69 @@
 package org.apache.juneau.commons.conversion;
 
 import java.util.*;
-import java.util.concurrent.*;
 
 /**
- * A {@link BasicConverter} subclass that supports runtime registration of 
custom type conversions.
+ * A {@link BasicConverter} subclass that supports pluggable type conversion 
discovery via {@link ConversionFinder}.
  *
  * <p>
- * Use {@link #add(Class, Class, Conversion)} to register a custom {@link 
Conversion} function for a specific
- * input/output type pair before the first conversion for that pair is 
requested.
- * Registered conversions take priority over the built-in {@link 
BasicConverter} reflection logic.
- *
- * <p>
- * This class is intended to be instantiated and held as a field (e.g., on a 
{@code BeanContext}) so that
- * custom conversions can be injected at configuration time.
+ * Pass one or more {@link ConversionFinder} instances to the constructor. 
Finders are consulted in order before
+ * falling back to the built-in {@link BasicConverter} reflection logic.
  *
  * <h5 class='section'>Example:</h5>
  * <p class='bjava'>
- *     ConfigurableConverter <jv>converter</jv> = <jk>new</jk> 
ConfigurableConverter()
- *             .add(String.<jk>class</jk>, MyBean.<jk>class</jk>, 
(<jv>in</jv>, <jv>memberOf</jv>, <jv>args</jv>) -> 
MyBean.fromString(<jv>in</jv>));
+ *     ConfigurableConverter <jv>converter</jv> = <jk>new</jk> 
ConfigurableConverter(
+ *             (<jv>in</jv>, <jv>out</jv>) -> <jv>in</jv> == 
String.<jk>class</jk> &amp;&amp; <jv>out</jv> == MyBean.<jk>class</jk>
+ *                     ? (<jv>s</jv>, <jv>memberOf</jv>, <jv>session</jv>, 
<jv>args</jv>) -> MyBean.fromString((<jk>String</jk>) <jv>s</jv>)
+ *                     : <jk>null</jk>
+ *     );
  *
  *     MyBean <jv>bean</jv> = <jv>converter</jv>.to(<js>"value"</js>, 
MyBean.<jk>class</jk>);
  * </p>
  *
- * <h5 class='section'>Thread Safety:</h5>
- * <p>
- * This class is thread-safe provided that all {@link #add} calls complete 
before the converter is shared across
- * threads. Registering conversions concurrently with active {@link #to} calls 
is also safe due to the underlying
- * {@link ConcurrentHashMap}, but registered conversions may not be visible 
immediately if the cache has already
- * been populated for that type pair.
- * </p>
- *
  * <h5 class='section'>See Also:</h5><ul>
  *     <li class='jc'>{@link BasicConverter}
+ *     <li class='jc'>{@link ConversionFinder}
  *     <li class='jc'>{@link Conversion}
  * </ul>
  */
-@SuppressWarnings({
-       "unchecked" // Type erasure requires unchecked casts in registry lookup
-})
 public class ConfigurableConverter extends BasicConverter {
 
-       private final Map<Class<?>, Map<Class<?>, Conversion<?,?>>> registered 
= new ConcurrentHashMap<>();
+       private final List<ConversionFinder> finders;
 
        /**
         * Constructor.
-        */
-       public ConfigurableConverter() {}
-
-       /**
-        * Registers a custom conversion function for the specified 
input/output type pair.
-        *
-        * <p>
-        * The registered conversion takes priority over the built-in {@link 
BasicConverter} reflection logic.
-        * Registrations should be made before the converter is shared across 
threads or before the first conversion
-        * for the given type pair is requested.
         *
-        * @param <I> The input type.
-        * @param <O> The output type.
-        * @param inType The input type class.
-        * @param outType The output type class.
-        * @param conversion The conversion function to register.
-        * @return This object.
+        * @param finders Optional {@link ConversionFinder} instances consulted 
in order before registered
+        *   type-pair conversions and the built-in {@link BasicConverter} 
reflection logic.
         */
-       public <I, O> ConfigurableConverter add(Class<I> inType, Class<O> 
outType, Conversion<I, O> conversion) {
-               registered
-                       .computeIfAbsent(inType, k -> new ConcurrentHashMap<>())
-                       .put(outType, conversion);
-               return this;
+       public ConfigurableConverter(ConversionFinder... finders) {
+               this.finders = List.of(finders);
        }
 
        /**
-        * Returns <jk>true</jk> if a custom conversion has been explicitly 
registered for the specified type pair.
+        * Returns <jk>true</jk> if any registered {@link ConversionFinder} can 
convert the specified type pair.
         *
         * <p>
-        * This only checks the user-registered conversions, not the built-in 
{@link BasicConverter} reflection logic.
+        * This only checks the registered finders, not the built-in {@link 
BasicConverter} reflection logic.
         *
         * @param inType The input type class.
         * @param outType The output type class.
-        * @return <jk>true</jk> if a custom conversion exists for the 
specified type pair.
+        * @return <jk>true</jk> if a finder-provided conversion exists for the 
specified type pair.
         */
        public boolean hasCustomConversion(Class<?> inType, Class<?> outType) {
-               var inner = registered.get(inType);
-               return inner != null && inner.containsKey(outType);
+               for (var finder : finders)
+                       if (finder.find(inType, outType) != null)
+                               return true;
+               return false;
        }
 
        @Override
+       @SuppressWarnings({
+               "unchecked" // Type safety guaranteed by ConversionFinder 
contract
+       })
        protected <I, O> Conversion<I, O> findConversion(Class<I> inType, 
Class<O> outType) {
-               var inner = registered.get(inType);
-               if (inner != null) {
-                       var fn = (Conversion<I, O>) inner.get(outType);
+               for (var finder : finders) {
+                       var fn = (Conversion<I, O>) finder.find(inType, 
outType);
                        if (fn != null)
                                return fn;
                }
diff --git 
a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/conversion/ConversionFinder.java
 
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/conversion/ConversionFinder.java
new file mode 100644
index 0000000000..642d642c75
--- /dev/null
+++ 
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/conversion/ConversionFinder.java
@@ -0,0 +1,59 @@
+/*
+ * 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.conversion;
+
+/**
+ * A pluggable strategy for discovering {@link Conversion} functions between 
type pairs.
+ *
+ * <p>
+ * Implementations are registered on a {@link ConfigurableConverter} via
+ * {@link ConfigurableConverter#addFinders(ConversionFinder...)} and are 
consulted in order before
+ * the built-in {@link BasicConverter} reflection logic.
+ *
+ * <p>
+ * Return <jk>null</jk> from {@link #find} to indicate that this finder has no 
conversion for the
+ * given type pair; the next registered finder (or the built-in logic) will be 
tried instead.
+ *
+ * <h5 class='section'>Example:</h5>
+ * <p class='bjava'>
+ *     ConfigurableConverter <jv>converter</jv> = <jk>new</jk> 
ConfigurableConverter(
+ *             (<jv>inType</jv>, <jv>outType</jv>) -> {
+ *                     <jk>if</jk> (<jv>inType</jv> == String.<jk>class</jk> 
&amp;&amp; <jv>outType</jv> == MyBean.<jk>class</jk>)
+ *                             <jk>return</jk> (<jv>in</jv>, 
<jv>memberOf</jv>, <jv>session</jv>, <jv>args</jv>) -> 
MyBean.fromString((<jk>String</jk>) <jv>in</jv>);
+ *                     <jk>return null</jk>;
+ *             }
+ *     );
+ * </p>
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ *     <li class='jc'>{@link ConfigurableConverter}
+ *     <li class='jc'>{@link Conversion}
+ * </ul>
+ */
+@FunctionalInterface
+public interface ConversionFinder {
+
+       /**
+        * Returns a {@link Conversion} for the specified input/output type 
pair, or <jk>null</jk> if this finder
+        * has no conversion for the pair.
+        *
+        * @param inType The input type class.
+        * @param outType The output type class.
+        * @return A {@link Conversion} function, or <jk>null</jk> if no 
conversion is available.
+        */
+       Conversion<?,?> find(Class<?> inType, Class<?> outType);
+}
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanContext.java 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanContext.java
index cd4c0d65e3..efa087b137 100644
--- 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanContext.java
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanContext.java
@@ -400,35 +400,6 @@ public class BeanContext extends Context {
                        return this;
                }
 
-               /**
-                * Registers a custom type conversion on this context.
-                *
-                * <p>
-                * User-registered conversions take priority over built-in type 
dispatch in
-                * {@link BeanSession#convertToMemberType(Object, Object, 
ClassMeta)}, but are checked after
-                * {@link ObjectSwap} processing.
-                *
-                * <h5 class='section'>Example:</h5>
-                * <p class='bjava'>
-                *      <jc>// Register a custom String-to-MyBean 
conversion.</jc>
-                *      BeanContext <jv>bc</jv> = BeanContext
-                *              .<jsm>create</jsm>()
-                *              .addConverter(String.<jk>class</jk>, 
MyBean.<jk>class</jk>, (<jv>in</jv>, <jv>memberOf</jv>, <jv>session</jv>, 
<jv>args</jv>) -&gt; MyBean.fromString(<jv>in</jv>))
-                *              .build();
-                * </p>
-                *
-                * @param <I> The input type.
-                * @param <O> The output type.
-                * @param inType The input type class.
-                * @param outType The output type class.
-                * @param conversion The conversion function.
-                * @return This object.
-                */
-               public <I, O> Builder addConverter(Class<I> inType, Class<O> 
outType, Conversion<I, O> conversion) {
-                       converter.add(inType, outType, conversion);
-                       return this;
-               }
-
                /**
                 * Minimum bean class visibility.
                 *
@@ -3841,10 +3812,9 @@ public class BeanContext extends Context {
        public final Visibility getBeanFieldVisibility() { return 
beanFieldVisibility; }
 
        /**
-        * Returns the custom converter registered on this context, or 
<jk>null</jk> if none.
+        * Returns the converter for this context.
         *
-        * @see BeanContext.Builder#addConverter(Class, Class, Conversion)
-        * @return The custom converter, or <jk>null</jk> if no custom 
conversions have been registered.
+        * @return The converter.
         */
        public final ConfigurableConverter getConverter() { return converter; }
 
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/commons/conversion/ConfigurableConverter_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/commons/conversion/ConfigurableConverter_Test.java
index d3106f6d4c..d330881505 100644
--- 
a/juneau-utest/src/test/java/org/apache/juneau/commons/conversion/ConfigurableConverter_Test.java
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/commons/conversion/ConfigurableConverter_Test.java
@@ -19,7 +19,6 @@ package org.apache.juneau.commons.conversion;
 import static org.junit.jupiter.api.Assertions.*;
 
 import java.util.concurrent.*;
-import java.util.concurrent.atomic.*;
 
 import org.junit.jupiter.api.*;
 
@@ -29,48 +28,56 @@ import org.junit.jupiter.api.*;
 public class ConfigurableConverter_Test {
 
        // 
=================================================================================================================
-       // a - Registration and basic conversion
+       // a - ConversionFinder integration
        // 
=================================================================================================================
 
-       /**
-        * Custom value type used in registration tests.
-        */
        public static class A01_Value {
                public final String raw;
-               public A01_Value(String raw) { this.raw = raw; }
+               private A01_Value(String raw) { this.raw = raw; }
        }
 
-       @Test void a01_registeredConversionIsUsed() {
-               var c = new ConfigurableConverter()
-                       .add(String.class, A01_Value.class, (in, memberOf, 
session, args) -> new A01_Value(in));
+       @Test void a01_finderConversionIsUsed() {
+               var c = new ConfigurableConverter(
+                       (in, out) -> in == String.class && out == 
A01_Value.class
+                               ? (s, memberOf, session, args) -> new 
A01_Value((String) s)
+                               : null
+               );
                var result = c.to("hello", A01_Value.class);
                assertNotNull(result);
                assertEquals("hello", result.raw);
        }
 
-       @Test void a02_registeredConversionTakesPriorityOverBuiltIn() {
-               // Integer→String has a built-in conversion; register a custom 
one that wraps with brackets
-               var c = new ConfigurableConverter()
-                       .add(Integer.class, String.class, (in, memberOf, 
session, args) -> "[" + in + "]");
+       @Test void a02_finderTakesPriorityOverBuiltIn() {
+               var c = new ConfigurableConverter(
+                       (in, out) -> in == Integer.class && out == String.class
+                               ? (i, memberOf, session, args) -> "[" + i + "]"
+                               : null
+               );
                assertEquals("[42]", c.to(42, String.class));
        }
 
-       @Test void a03_builtInConversionUsedWhenNoRegistration() {
+       @Test void a03_builtInConversionUsedWhenNoFinderMatch() {
                var c = new ConfigurableConverter();
-               // String→Integer has a built-in conversion
                assertEquals(Integer.valueOf(42), c.to("42", Integer.class));
        }
 
        @Test void a04_nullInputReturnsNull() {
-               var c = new ConfigurableConverter()
-                       .add(String.class, A01_Value.class, (in, memberOf, 
session, args) -> new A01_Value(in));
+               var c = new ConfigurableConverter(
+                       (in, out) -> in == String.class && out == 
A01_Value.class
+                               ? (s, memberOf, session, args) -> new 
A01_Value((String) s)
+                               : null
+               );
                assertNull(c.to(null, A01_Value.class));
        }
 
-       @Test void a05_canConvertReturnsTrueForRegisteredType() {
-               var c = new ConfigurableConverter()
-                       .add(String.class, A01_Value.class, (in, memberOf, 
session, args) -> new A01_Value(in));
-               assertTrue(c.canConvert(String.class, A01_Value.class));
+       @Test void a05_hasCustomConversionReturnsTrueForFinderMatch() {
+               var c = new ConfigurableConverter(
+                       (in, out) -> in == String.class && out == 
A01_Value.class
+                               ? (s, memberOf, session, args) -> new 
A01_Value((String) s)
+                               : null
+               );
+               assertTrue(c.hasCustomConversion(String.class, 
A01_Value.class));
+               assertFalse(c.hasCustomConversion(Integer.class, 
A01_Value.class));
        }
 
        @Test void a06_canConvertReturnsTrueForBuiltInType() {
@@ -78,46 +85,49 @@ public class ConfigurableConverter_Test {
                assertTrue(c.canConvert(String.class, Integer.class));
        }
 
-       @Test void a07_canConvertReturnsFalseForUnregisteredUnknownType() {
+       @Test void a07_canConvertReturnsFalseForUnknownType() {
                var c = new ConfigurableConverter();
                assertFalse(c.canConvert(A01_Value.class, 
ConcurrentLinkedQueue.class));
        }
 
-       @Test void a08_multipleRegistrationsOnSameConverter() {
-               var c = new ConfigurableConverter()
-                       .add(String.class, A01_Value.class, (in, memberOf, 
session, args) -> new A01_Value(in))
-                       .add(Integer.class, A01_Value.class, (in, memberOf, 
session, args) -> new A01_Value(String.valueOf(in)));
-               assertEquals("hello", c.to("hello", A01_Value.class).raw);
-               assertEquals("42", c.to(42, A01_Value.class).raw);
+       @Test void a08_multipleFindersConsultedInOrder() {
+               // First finder handles String→A01_Value, second handles 
Integer→A01_Value
+               var c = new ConfigurableConverter(
+                       (in, out) -> in == String.class && out == 
A01_Value.class
+                               ? (s, memberOf, session, args) -> new 
A01_Value("string:" + s)
+                               : null,
+                       (in, out) -> in == Integer.class && out == 
A01_Value.class
+                               ? (i, memberOf, session, args) -> new 
A01_Value("int:" + i)
+                               : null
+               );
+               assertEquals("string:hello", c.to("hello", 
A01_Value.class).raw);
+               assertEquals("int:42", c.to(42, A01_Value.class).raw);
        }
 
-       /**
-        * A type with no public single-arg String constructor (so 
BasicConverter cannot convert it without registration).
-        */
-       public static class A09_Value {
-               public final String raw;
-               private A09_Value(String raw) { this.raw = raw; }  // private - 
not discoverable by BasicConverter
-       }
-
-       @Test void a09_registrationDoesNotAffectOtherInstances() {
-               var c1 = new ConfigurableConverter()
-                       .add(String.class, A09_Value.class, (in, memberOf, 
session, args) -> new A09_Value(in));
+       @Test void a09_findersDoNotAffectOtherInstances() {
+               var c1 = new ConfigurableConverter(
+                       (in, out) -> in == String.class && out == 
A01_Value.class
+                               ? (s, memberOf, session, args) -> new 
A01_Value((String) s)
+                               : null
+               );
                var c2 = new ConfigurableConverter();
-               assertNotNull(c1.to("x", A09_Value.class));
-               assertFalse(c2.canConvert(String.class, A09_Value.class));
+               assertNotNull(c1.to("x", A01_Value.class));
+               assertFalse(c2.canConvert(String.class, A01_Value.class));
        }
 
        // 
=================================================================================================================
-       // b - memberOf parameter is forwarded to registered conversion
+       // b - memberOf parameter is forwarded to finder conversion
        // 
=================================================================================================================
 
-       @Test void b01_memberOfIsForwardedToRegisteredConversion() {
-               // Use String→Integer (different types) so the identity-check 
shortcut is not triggered
+       @Test void b01_memberOfIsForwardedToFinderConversion() {
                var memberOf = new Object();
-               var captured = new AtomicReference<Object>();
-               var c = new ConfigurableConverter()
-                       .add(String.class, Integer.class, (in, m, session, 
args) -> { captured.set(m); return Integer.parseInt(in); });
-               c.to("42", memberOf, (ConverterSession)null, Integer.class);
+               var captured = new 
java.util.concurrent.atomic.AtomicReference<Object>();
+               var c = new ConfigurableConverter(
+                       (in, out) -> in == String.class && out == Integer.class
+                               ? (s, m, session, args) -> { captured.set(m); 
return Integer.parseInt((String) s); }
+                               : null
+               );
+               c.to("42", memberOf, (ConverterSession) null, Integer.class);
                assertSame(memberOf, captured.get());
        }
 
@@ -125,46 +135,15 @@ public class ConfigurableConverter_Test {
        // c - Thread safety
        // 
=================================================================================================================
 
-       /**
-        * Type with private constructor used to verify concurrent registration 
without built-in conflict.
-        */
-       public static class C01_Value {
-               public final String raw;
-               private C01_Value(String raw) { this.raw = raw; }
-       }
-
-       @Test void c01_concurrentRegistrationsAreThreadSafe() throws Exception {
-               // Register String→C01_Value (not convertible by BasicConverter 
due to private constructor)
-               var c = new ConfigurableConverter();
-               var threads = 16;
-               var latch = new CountDownLatch(1);
-               var errors = new AtomicInteger(0);
-               var pool = Executors.newFixedThreadPool(threads);
-               for (int i = 0; i < threads; i++) {
-                       pool.submit(() -> {
-                               try {
-                                       latch.await();
-                                       c.add(String.class, C01_Value.class, 
(in, m, session, args) -> new C01_Value(in.toUpperCase()));
-                               } catch (Exception e) {
-                                       errors.incrementAndGet();
-                               }
-                       });
-               }
-               latch.countDown();
-               pool.shutdown();
-               pool.awaitTermination(5, TimeUnit.SECONDS);
-               assertEquals(0, errors.get());
-               var result = c.to("hello", C01_Value.class);
-               assertNotNull(result);
-               assertEquals("HELLO", result.raw);
-       }
-
-       @Test void c02_concurrentConvertsAreThreadSafe() throws Exception {
-               var c = new ConfigurableConverter()
-                       .add(String.class, Integer.class, (in, m, session, 
args) -> Integer.parseInt(in) * 2);
+       @Test void c01_concurrentConvertsAreThreadSafe() throws Exception {
+               var c = new ConfigurableConverter(
+                       (in, out) -> in == String.class && out == Integer.class
+                               ? (s, m, session, args) -> 
Integer.parseInt((String) s) * 2
+                               : null
+               );
                var threads = 32;
                var latch = new CountDownLatch(1);
-               var errors = new AtomicInteger(0);
+               var errors = new java.util.concurrent.atomic.AtomicInteger(0);
                var pool = Executors.newFixedThreadPool(threads);
                for (int i = 0; i < threads; i++) {
                        pool.submit(() -> {

Reply via email to