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> && <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>
&& <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>) -> 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(() -> {