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

commit fec229f716c7f98ba3a1f2a4f28ec793acb49ae4
Author: James Bognar <[email protected]>
AuthorDate: Thu Apr 2 10:00:56 2026 -0700

    Replace Mutaters with BasicConverter
---
 .../juneau/commons/conversion/BasicConverter.java  |  54 +++-
 .../commons/conversion/ConfigurableConverter.java  |  99 +++++++
 .../main/java/org/apache/juneau/BeanSession.java   |  11 +-
 .../src/main/java/org/apache/juneau/ClassMeta.java | 212 +++++++------
 .../java/org/apache/juneau/reflect/Mutater.java    |  46 ---
 .../java/org/apache/juneau/reflect/Mutaters.java   | 330 ---------------------
 .../apache/juneau/rest/client/ResponseContent.java |   7 +-
 .../juneau/rest/httppart/RequestContent.java       |   7 +-
 .../rest/swagger/BasicSwaggerProviderSession.java  |   3 +-
 .../conversion/ConfigurableConverter_Test.java     | 188 ++++++++++++
 .../java/org/apache/juneau/utils/MutatersTest.java | 165 -----------
 11 files changed, 455 insertions(+), 667 deletions(-)

diff --git 
a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/conversion/BasicConverter.java
 
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/conversion/BasicConverter.java
index 9bb4e16e99..49308a6f0e 100644
--- 
a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/conversion/BasicConverter.java
+++ 
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/conversion/BasicConverter.java
@@ -383,6 +383,16 @@ public class BasicConverter extends CachingConverter {
        private <I, O> Conversion<I, O> findArrayConversion(Class<I> inType, 
Class<O> outType) {
                if (Collection.class.isAssignableFrom(inType) || 
inType.isArray()) {
                        var componentType = outType.getComponentType();
+                       // For array→array, validate that element-level 
conversion is possible.
+                       // Skip the pre-check for Object[] — runtime elements 
may be of a more specific type.
+                       // Collections use runtime element types so we skip the 
pre-check there too.
+                       if (inType.isArray()) {
+                               var inComponentType = inType.getComponentType();
+                               if (inComponentType != componentType
+                                               && inComponentType != 
Object.class
+                                               && !canConvert(inComponentType, 
componentType))
+                                       return null;
+                       }
                        return (in, memberOf, args) -> {
                                if (Collection.class.isAssignableFrom(inType)) {
                                        var list = (Collection<?>) in;
@@ -436,16 +446,44 @@ public class BasicConverter extends CachingConverter {
                                return (in, memberOf, args) -> 
opt.get().invoke(null, in);
                }
 
-               var inName = inType.getSimpleName();
-               for (var prefix : new String[]{"from", "for", "parse"}) {
-                       var opt = findStaticMethod(ci, prefix + inName, inType, 
outType);
-                       if (opt.isPresent())
-                               return (in, memberOf, args) -> 
opt.get().invoke(null, in);
+               // Walk the type hierarchy (superclasses then interfaces) so 
that e.g. InputStreamReader
+               // can be passed to a method declared as fromReader(Reader r).
+               for (Class<?> c = inType; c != null && c != Object.class; c = 
c.getSuperclass()) {
+                       var inName = c.getSimpleName();
+                       for (var prefix : new String[]{"from", "for", "parse"}) 
{
+                               var opt = findStaticMethod(ci, prefix + inName, 
inType, outType);
+                               if (opt.isPresent())
+                                       return (in, memberOf, args) -> 
opt.get().invoke(null, in);
+                       }
+               }
+               for (var iface : allInterfaces(inType)) {
+                       var inName = iface.getSimpleName();
+                       for (var prefix : new String[]{"from", "for", "parse"}) 
{
+                               var opt = findStaticMethod(ci, prefix + inName, 
inType, outType);
+                               if (opt.isPresent())
+                                       return (in, memberOf, args) -> 
opt.get().invoke(null, in);
+                       }
                }
 
                return null;
        }
 
+       private static List<Class<?>> allInterfaces(Class<?> c) {
+               var result = new ArrayList<Class<?>>();
+               for (var x = c; x != null; x = x.getSuperclass())
+                       for (var iface : x.getInterfaces())
+                               collectInterfaces(iface, result);
+               return result;
+       }
+
+       private static void collectInterfaces(Class<?> iface, List<Class<?>> 
result) {
+               if (!result.contains(iface)) {
+                       result.add(iface);
+                       for (var parent : iface.getInterfaces())
+                               collectInterfaces(parent, result);
+               }
+       }
+
        private Optional<MethodInfo> findStaticMethod(ClassInfo ci, String 
name, Class<?> inType, Class<?> outType) {
                return ci.getPublicMethod(m ->
                        m.isStatic()
@@ -493,12 +531,14 @@ public class BasicConverter extends CachingConverter {
        
//-----------------------------------------------------------------------------------------------------------------
 
        private <I, O> Conversion<I, O> findToXMethod(Class<I> inType, Class<O> 
outType) {
-               var methodName = "to" + outType.getSimpleName();
+               // Use getNameReadable() so that array types use "Array" suffix 
(e.g. "toStringArray" for String[]).
+               // Use equalsIgnoreCase to match Mutaters' behavior (e.g. 
"toByteArray" matches "tobyteArray" from byte[]).
+               var methodName = "to" + info(outType).getNameReadable();
                var opt = info(inType).getPublicMethod(m ->
                        m.isNotStatic()
                        && m.isNotDeprecated()
                        && m.getParameterCount() == 0
-                       && m.hasName(methodName)
+                       && m.getNameSimple().equalsIgnoreCase(methodName)
                        && m.hasReturnTypeParent(outType)
                );
                if (opt.isPresent()) {
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
new file mode 100644
index 0000000000..243014ac36
--- /dev/null
+++ 
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/conversion/ConfigurableConverter.java
@@ -0,0 +1,99 @@
+/*
+ * 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;
+
+import java.util.*;
+import java.util.concurrent.*;
+
+/**
+ * A {@link BasicConverter} subclass that supports runtime registration of 
custom type conversions.
+ *
+ * <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.
+ *
+ * <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>));
+ *
+ *     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 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<>();
+
+       /**
+        * 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.
+        */
+       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;
+       }
+
+       @Override
+       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);
+                       if (fn != null)
+                               return fn;
+               }
+               return super.findConversion(inType, outType);
+       }
+}
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanSession.java 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanSession.java
index 9535ba2942..4aafb9e8cf 100644
--- 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanSession.java
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanSession.java
@@ -1412,8 +1412,15 @@ public class BeanSession extends ContextSession {
                        if (to.isArray()) {
                                if (from.isCollection())
                                        return (T)toArray(to, 
(Collection)value);
-                               else if (from.isArray())
-                                       return (T)toArray(to, 
l((Object[])value));
+                               else if (from.isArray()) {
+                                       // Use reflection to build the list so 
primitive arrays (e.g. boolean[]) are handled correctly.
+                                       // Array.get() auto-boxes primitives, 
whereas casting to Object[] fails for primitive arrays.
+                                       var len = Array.getLength(value);
+                                       var list = new ArrayList<>(len);
+                                       for (var i = 0; i < len; i++)
+                                               list.add(Array.get(value, i));
+                                       return (T)toArray(to, list);
+                               }
                                else if (startsWith(value.toString(), '['))
                                        return (T)toArray(to, 
JsonList.ofJson(value.toString()).setBeanSession(this));
                                else if (to.hasMutaterFrom(from))
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/ClassMeta.java 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/ClassMeta.java
index 41ddb2b486..d9c0c408a6 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/ClassMeta.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/ClassMeta.java
@@ -31,18 +31,17 @@ import java.time.*;
 import java.time.temporal.*;
 import java.util.*;
 import java.util.List;
-import java.util.concurrent.*;
 import java.util.function.*;
 import java.util.stream.*;
 
 import org.apache.juneau.annotation.*;
 import org.apache.juneau.commons.collections.*;
+import org.apache.juneau.commons.conversion.*;
 import org.apache.juneau.commons.function.*;
 import org.apache.juneau.commons.reflect.*;
 import org.apache.juneau.commons.utils.*;
 import org.apache.juneau.cp.*;
 import org.apache.juneau.json.*;
-import org.apache.juneau.reflect.*;
 import org.apache.juneau.swap.*;
 
 /**
@@ -68,9 +67,8 @@ import org.apache.juneau.swap.*;
  */
 @Bean(properties = 
"innerClass,elementType,keyType,valueType,notABeanReason,initException,beanMeta")
 @SuppressWarnings({
-       "deprecation", // Mutaters is deprecated but still used here pending 
full migration to Converter.INSTANCE
        "java:S1200",  // Class has 23 dependencies, acceptable for this core 
reflection metadata class
-       "java:S1452"   // Wildcard required - ClassMeta<?>, ObjectSwap<T,?>, 
Mutater<T,?>, etc. for element/component types
+       "java:S1452"   // Wildcard required - ClassMeta<?>, ObjectSwap<T,?>, 
etc. for element/component types
 })
 public class ClassMeta<T> extends ClassInfoTyped<T> {
 
@@ -157,7 +155,6 @@ public class ClassMeta<T> extends ClassInfoTyped<T> {
        private final NullableSupplier<FieldInfo> exampleField;                 
   // The @Example-annotated field (if it has one).
        private final NullableSupplier<MethodInfo> exampleMethod;               
   // The example() or @Example-annotated method (if it has one).
        private final Supplier<BidiMap<Object,String>> enumValues;
-       private final Map<Class<?>,Mutater<?,T>> fromMutaters = new 
ConcurrentHashMap<>();
        private final NullableSupplier<MethodInfo> fromStringMethod;            
   // Static fromString(String) or equivalent method
        private final NullableSupplier<ClassInfoTyped<? extends T>> implClass;  
   // The implementation class to use if this is an interface.
        private final Supplier<KeyValueTypes> keyValueTypes;                    
    // Key and value types for MAP types.
@@ -166,10 +163,8 @@ public class ClassMeta<T> extends ClassInfoTyped<T> {
        private final NullableSupplier<ConstructorInfo> noArgConstructor;       
   // The no-arg constructor for this class (if it has one).
        private final Supplier<Property<T,Object>> parentProperty;              
   // The method to set the parent on an object (if it has one).
        private final Cache<String,Optional<?>> properties;
-       private final Mutater<String,T> stringMutater;
        private final NullableSupplier<ConstructorInfo> stringConstructor;      
   // The X(String) constructor (if it has one).
        private final Supplier<List<ObjectSwap<T,?>>> swaps;                    
   // The object POJO swaps associated with this bean (if it has any).
-       private final Map<Class<?>,Mutater<T,?>> toMutaters = new 
ConcurrentHashMap<>();
        private final NullableSupplier<BeanMeta.BeanMetaValue<T>> beanMeta;
 
        private record KeyValueTypes(ClassMeta<?> keyType, ClassMeta<?> 
valueType) {
@@ -278,7 +273,6 @@ public class ClassMeta<T> extends ClassInfoTyped<T> {
                swaps = memoize(this::findSwaps);
 
                this.args = null;
-               this.stringMutater = Mutaters.get(String.class, inner());
        }
 
        protected ObjectSwap<?,?> findSwap(Class<?> c) {
@@ -308,7 +302,6 @@ public class ClassMeta<T> extends ClassInfoTyped<T> {
                this.keyValueTypes = memoize(this::findKeyValueTypes);
                this.beanMeta = memoize(this::findBeanMeta);
                this.swaps = memoize(this::findSwaps);
-               this.stringMutater = null;
                this.fromStringMethod = memoize(this::findFromStringMethod);
                this.exampleMethod = memoize(this::findExampleMethod);
                this.parentProperty = memoize(this::findParentProperty);
@@ -345,7 +338,6 @@ public class ClassMeta<T> extends ClassInfoTyped<T> {
                this.swaps = mainType.swaps;
                this.exampleMethod = mainType.exampleMethod;
                this.args = null;
-               this.stringMutater = mainType.stringMutater;
                this.parentProperty = mainType.parentProperty;
                this.nameProperty = mainType.nameProperty;
                this.exampleField = mainType.exampleField;
@@ -598,28 +590,13 @@ public class ClassMeta<T> extends ClassInfoTyped<T> {
        }
 
        /**
-        * Returns the transform for this class for creating instances from 
other object types.
+        * Returns <jk>true</jk> if this class can be instantiated from the 
specified input type.
         *
-        * @param <I> The transform-from class.
-        * @param c The transform-from class.
-        * @return The transform, or <jk>null</jk> if no such transform exists.
+        * @param c The input class type.
+        * @return <jk>true</jk> if a conversion exists.
         */
-       
-       @SuppressWarnings({
-               "rawtypes", // Raw types necessary for generic type 
mutation/conversion for generic type mutation/conversion for generic type 
mutation/conversion
-               "unchecked", // Type erasure requires unchecked casts in type 
mutation
-       })
-       public <I> Mutater<I,T> getFromMutater(Class<I> c) {
-               Mutater t = fromMutaters.get(c);
-               if (t == Mutaters.NULL)
-                       return null;
-               if (t == null) {
-                       t = Mutaters.get(c, inner());
-                       if (t == null)
-                               t = Mutaters.NULL;
-                       fromMutaters.put(c, t);
-               }
-               return t == Mutaters.NULL ? null : t;
+       public boolean canConvertFrom(Class<?> c) {
+               return BasicConverter.INSTANCE.canConvert(c, inner());
        }
 
        /**
@@ -632,12 +609,6 @@ public class ClassMeta<T> extends ClassInfoTyped<T> {
                return implClass.map(x -> 
x.getNoArgConstructor(conVis).orElse(null)).orElse(null);
        }
 
-       /**
-        * Returns the transform for this class for creating instances from an 
InputStream.
-        *
-        * @return The transform, or <jk>null</jk> if no such transform exists.
-        */
-       public Mutater<InputStream,T> getInputStreamMutater() { return 
getFromMutater(InputStream.class); }
 
        /**
         * For {@code Map} types, returns the class type of the keys of the 
{@code Map}.
@@ -743,12 +714,6 @@ public class ClassMeta<T> extends ClassInfoTyped<T> {
                return beanMeta.get().optBeanMeta().map(x -> 
x.getBeanProxyInvocationHandler()).orElse(null);
        }
 
-       /**
-        * Returns the transform for this class for creating instances from a 
Reader.
-        *
-        * @return The transform, or <jk>null</jk> if no such transform exists.
-        */
-       public Mutater<Reader,T> getReaderMutater() { return 
getFromMutater(Reader.class); }
 
        /**
         * Returns the serialized (swapped) form of this class if there is an 
{@link ObjectSwap} associated with it.
@@ -763,12 +728,6 @@ public class ClassMeta<T> extends ClassInfoTyped<T> {
                return (ps == null ? this : ps.getSwapClassMeta(session));
        }
 
-       /**
-        * Returns the transform for this class for creating instances from a 
String.
-        *
-        * @return The transform, or <jk>null</jk> if no such transform exists.
-        */
-       public Mutater<String,T> getStringMutater() { return stringMutater; }
 
        /**
         * Returns the {@link ObjectSwap} associated with this class that's the 
best match for the specified session.
@@ -802,28 +761,13 @@ public class ClassMeta<T> extends ClassInfoTyped<T> {
        }
 
        /**
-        * Returns the transform for this class for creating instances from 
other object types.
+        * Returns <jk>true</jk> if this class can be converted to the 
specified output type.
         *
-        * @param <O> The transform-to class.
-        * @param c The transform-from class.
-        * @return The transform, or <jk>null</jk> if no such transform exists.
+        * @param c The output class type.
+        * @return <jk>true</jk> if a conversion exists.
         */
-       
-       @SuppressWarnings({
-               "rawtypes", // Raw types necessary for generic type 
mutation/conversion
-               "unchecked", // Type erasure requires unchecked casts in type 
mutation
-       })
-       public <O> Mutater<T,O> getToMutater(Class<O> c) {
-               Mutater t = toMutaters.get(c);
-               if (t == Mutaters.NULL)
-                       return null;
-               if (t == null) {
-                       t = Mutaters.get(inner(), c);
-                       if (t == null)
-                               t = Mutaters.NULL;
-                       toMutaters.put(c, t);
-               }
-               return t == Mutaters.NULL ? null : t;
+       public boolean canConvertTo(Class<?> c) {
+               return BasicConverter.INSTANCE.canConvert(inner(), c);
        }
 
        /**
@@ -838,68 +782,126 @@ public class ClassMeta<T> extends ClassInfoTyped<T> {
        /**
         * Returns <jk>true</jk> if this class has a transform associated with 
it that allows it to be created from an InputStream.
         *
+        * <p>
+        * Only returns <jk>true</jk> for types that can be explicitly 
constructed from an {@link InputStream} (e.g. via a
+        * public constructor or static factory). Excludes assignable 
supertypes and the generic {@code toString()} fallback.
+        *
         * @return <jk>true</jk> if this class has a transform associated with 
it that allows it to be created from an InputStream.
         */
        public boolean hasInputStreamMutater() {
-               return hasMutaterFrom(InputStream.class);
+               // Exclude String: BasicConverter maps InputStream→String via 
stream.toString() which doesn't read content.
+               // Exclude supertypes of InputStream (e.g. Object, Closeable): 
assignability, not an explicit transform.
+               if (inner() == String.class || 
inner().isAssignableFrom(InputStream.class))
+                       return false;
+               return BasicConverter.INSTANCE.canConvert(InputStream.class, 
inner());
        }
 
        /**
-        * Returns <jk>true</jk> if this class can be instantiated from the 
specified type.
+        * Returns <jk>true</jk> if this class can be instantiated from the 
specified type via an explicit conversion.
+        *
+        * <p>
+        * Same-type conversions (identity) always return <jk>true</jk>. Strict 
supertype relationships (where this
+        * class is already a supertype of the source) return <jk>false</jk> — 
those are assignability, not explicit
+        * transforms.
         *
         * @param c The class type to convert from.
-        * @return <jk>true</jk> if this class can be instantiated from the 
specified type.
+        * @return <jk>true</jk> if an explicit conversion from {@code c} to 
this type exists.
         */
        public boolean hasMutaterFrom(Class<?> c) {
-               return nn(getFromMutater(c));
+               // Same type is always "convertible" (identity).
+               if (inner() == c)
+                       return true;
+               // Exclude strict supertype: if this type is already a strict 
supertype of c, it's assignability only.
+               if (inner().isAssignableFrom(c))
+                       return false;
+               // Exclude array→array: BeanSession handles those directly; 
BasicConverter infers
+               // array conversions from element types, which can produce 
false positives here.
+               if (inner().isArray() && c.isArray())
+                       return false;
+               // Exclude Collections and Maps: BeanSession handles those via 
convertToCollectionType/convertToMapType;
+               // BasicConverter's generic collection/map conversions produce 
false positives (Mutaters never did these).
+               if (Collection.class.isAssignableFrom(inner()) || 
Map.class.isAssignableFrom(inner()))
+                       return false;
+               return BasicConverter.INSTANCE.canConvert(c, inner());
        }
 
        /**
-        * Returns <jk>true</jk> if this class can be instantiated from the 
specified type.
+        * Returns <jk>true</jk> if this class can be instantiated from the 
specified type via an explicit conversion.
         *
         * @param c The class type to convert from.
-        * @return <jk>true</jk> if this class can be instantiated from the 
specified type.
+        * @return <jk>true</jk> if an explicit conversion from {@code c} to 
this type exists.
         */
        public boolean hasMutaterFrom(ClassMeta<?> c) {
-               return nn(getFromMutater(c.inner()));
+               return hasMutaterFrom(c.inner());
        }
 
        /**
-        * Returns <jk>true</jk> if this class can be transformed to the 
specified type.
+        * Returns <jk>true</jk> if this class can be transformed to the 
specified type via an explicit conversion.
         *
-        * @param c The class type to convert from.
-        * @return <jk>true</jk> if this class can be transformed to the 
specified type.
+        * <p>
+        * Same-type conversions (identity) always return <jk>true</jk>. Cases 
where the target is already a strict
+        * supertype of this class return <jk>false</jk> — those are 
assignability, not explicit transforms.
+        *
+        * @param c The class type to convert to.
+        * @return <jk>true</jk> if an explicit conversion from this type to 
{@code c} exists.
         */
        public boolean hasMutaterTo(Class<?> c) {
-               return nn(getToMutater(c));
+               // Same type is always "convertible" (identity).
+               if (inner() == c)
+                       return true;
+               // Exclude strict supertype: if the target is already a strict 
supertype of this type, it's assignability only.
+               if (c.isAssignableFrom(inner()))
+                       return false;
+               // Exclude array→array: BeanSession handles those directly; 
BasicConverter infers
+               // array conversions from element types, which can produce 
false positives here.
+               if (inner().isArray() && c.isArray())
+                       return false;
+               // Exclude Collection/Map targets: BeanSession handles those 
via convertToCollectionType/convertToMapType;
+               // BasicConverter's generic collection/map conversions produce 
false positives (Mutaters never did these).
+               if (Collection.class.isAssignableFrom(c) || 
Map.class.isAssignableFrom(c))
+                       return false;
+               return BasicConverter.INSTANCE.canConvert(inner(), c);
        }
 
        /**
-        * Returns <jk>true</jk> if this class can be transformed to the 
specified type.
+        * Returns <jk>true</jk> if this class can be transformed to the 
specified type via an explicit conversion.
         *
-        * @param c The class type to convert from.
-        * @return <jk>true</jk> if this class can be transformed to the 
specified type.
+        * @param c The class type to convert to.
+        * @return <jk>true</jk> if an explicit conversion from this type to 
{@code c} exists.
         */
        public boolean hasMutaterTo(ClassMeta<?> c) {
-               return nn(getToMutater(c.inner()));
+               return hasMutaterTo(c.inner());
        }
 
        /**
         * Returns <jk>true</jk> if this class has a transform associated with 
it that allows it to be created from a Reader.
         *
+        * <p>
+        * Only returns <jk>true</jk> for types that can be explicitly 
constructed from a {@link Reader} (e.g. via a
+        * public constructor or static factory). Excludes assignable 
supertypes and the generic {@code toString()} fallback.
+        *
         * @return <jk>true</jk> if this class has a transform associated with 
it that allows it to be created from a Reader.
         */
        public boolean hasReaderMutater() {
-               return hasMutaterFrom(Reader.class);
+               // Exclude String: BasicConverter maps Reader→String via 
reader.toString() which doesn't read content.
+               // Exclude supertypes of Reader (e.g. Object, Closeable): 
assignability, not an explicit transform.
+               if (inner() == String.class || 
inner().isAssignableFrom(Reader.class))
+                       return false;
+               return BasicConverter.INSTANCE.canConvert(Reader.class, 
inner());
        }
 
        /**
         * Returns <jk>true</jk> if this class has a transform associated with 
it that allows it to be created from a String.
         *
+        * <p>
+        * Delegates to {@link #hasMutaterFrom(Class)} with {@link String} as 
the source type, which applies the
+        * same exclusions: supertypes of {@link String} (e.g. {@link Object}), 
arrays, and {@link Collection}/{@link Map}
+        * targets are excluded because those are handled by parsers or {@code 
BeanSession}, not explicit string transforms.
+        *
         * @return <jk>true</jk> if this class has a transform associated with 
it that allows it to be created from a String.
         */
        public boolean hasStringMutater() {
-               return nn(stringMutater);
+               return hasMutaterFrom(String.class);
        }
 
        /**
@@ -1195,44 +1197,34 @@ public class ClassMeta<T> extends ClassInfoTyped<T> {
        public boolean isUri() { return cat != null && cat.is(URI); }
 
        /**
-        * Transforms the specified object into an instance of this class.
+        * Converts the specified object into an instance of this class.
         *
-        * @param o The object to transform.
-        * @return The transformed object.
+        * @param o The object to convert.
+        * @return The converted object, or <jk>null</jk> if no conversion is 
available.
         */
-       
-       @SuppressWarnings({
-               "unchecked", // Type erasure requires unchecked casts in type 
mutation
-               "rawtypes", // Raw types necessary for generic type 
mutation/conversion
-       })
        public T mutateFrom(Object o) {
-               Mutater t = getFromMutater(o.getClass());
-               return (T)(t == null ? null : t.mutate(o));
+               return BasicConverter.INSTANCE.to(o, inner());
        }
 
        /**
-        * Transforms the specified object into an instance of this class.
+        * Converts the specified object to the specified output type.
         *
-        * @param <O> The transform-to class.
-        * @param o The object to transform.
-        * @param c The class
-        * @return The transformed object.
+        * @param <O> The output class.
+        * @param o The object to convert.
+        * @param c The target class.
+        * @return The converted object, or <jk>null</jk> if no conversion is 
available.
         */
-       @SuppressWarnings({
-               "unchecked" // Type erasure requires cast for 
Mutater<Object,O>.mutate
-       })
        public <O> O mutateTo(Object o, Class<O> c) {
-               Mutater<Object,O> t = (Mutater<Object,O>)getToMutater(c);
-               return t == null ? null : t.mutate(o);
+               return BasicConverter.INSTANCE.to(o, c);
        }
 
        /**
-        * Transforms the specified object into an instance of this class.
+        * Converts the specified object to the type represented by this class 
meta.
         *
-        * @param <O> The transform-to class.
-        * @param o The object to transform.
-        * @param c The class
-        * @return The transformed object.
+        * @param <O> The output class.
+        * @param o The object to convert.
+        * @param c The target class meta.
+        * @return The converted object, or <jk>null</jk> if no conversion is 
available.
         */
        public <O> O mutateTo(Object o, ClassMeta<O> c) {
                return mutateTo(o, c.inner());
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/reflect/Mutater.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/reflect/Mutater.java
deleted file mode 100644
index 3caa78c836..0000000000
--- 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/reflect/Mutater.java
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * 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.reflect;
-
-/**
- * An interface for creating objects from other objects such as a 
<c>String</c> or <c>Reader</c>.
- *
- *
- * @param <I> Input type.
- * @param <O> Output type.
- */
-public abstract class Mutater<I,O> {
-
-       /**
-        * Method for instantiating an object from another object.
-        *
-        * @param in The input object.
-        * @return The output object.
-        */
-       public O mutate(I in) {
-               return mutate(null, in);
-       }
-
-       /**
-        * Method for instantiating an object from another object.
-        *
-        * @param outer The context object.
-        * @param in The input object.
-        * @return The output object.
-        */
-       public abstract O mutate(Object outer, I in);
-}
\ No newline at end of file
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/reflect/Mutaters.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/reflect/Mutaters.java
deleted file mode 100644
index 43a6460a29..0000000000
--- 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/reflect/Mutaters.java
+++ /dev/null
@@ -1,330 +0,0 @@
-/*
- * 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.reflect;
-
-import static org.apache.juneau.commons.reflect.ReflectionUtils.*;
-import static org.apache.juneau.commons.utils.StringUtils.*;
-import static org.apache.juneau.commons.utils.ThrowableUtils.*;
-import static org.apache.juneau.commons.utils.Utils.*;
-
-import java.util.*;
-import java.util.concurrent.*;
-
-import org.apache.juneau.commons.reflect.*;
-
-/**
- * Cache of object that convert POJOs to and from common types such as 
strings, readers, and input streams.
- *
- * @deprecated Use {@link 
org.apache.juneau.commons.conversion.Converter#INSTANCE} instead.
- */
-@Deprecated
-public class Mutaters {
-
-       /**
-        * Prevents instantiation.
-        */
-       private Mutaters() {}
-
-       private static final 
ConcurrentHashMap<Class<?>,Map<Class<?>,Mutater<?,?>>> CACHE = new 
ConcurrentHashMap<>();
-
-       /**
-        * Represents a non-existent transform.
-        */
-       public static final Mutater<Object,Object> NULL = new Mutater<>() {
-               @Override
-               public Object mutate(Object outer, Object in) {
-                       return null;
-               }
-       };
-
-       // Special cases.
-       static {
-               // @formatter:off
-
-               // TimeZone doesn't follow any standard conventions.
-               add(String.class, TimeZone.class,
-                       new Mutater<String,TimeZone>() {
-                               @Override public TimeZone mutate(Object outer, 
String in) {
-                                       return TimeZone.getTimeZone(in);
-                               }
-                       }
-               );
-               add(TimeZone.class, String.class,
-                       new Mutater<TimeZone,String>() {
-                               @Override public String mutate(Object outer, 
TimeZone in) {
-                                       return in.getID();
-                               }
-                       }
-               );
-
-               // Locale(String) doesn't work on strings like "ja_JP".
-               add(String.class, Locale.class,
-                       new Mutater<String,Locale>() {
-                               @Override
-                               public Locale mutate(Object outer, String in) {
-                                       return 
Locale.forLanguageTag(in.replace('_', '-'));
-                               }
-                       }
-               );
-
-               // String-to-Boolean transform should allow for "null" keyword.
-               add(String.class, Boolean.class,
-                       new Mutater<String,Boolean>() {
-                               @Override
-                               public Boolean mutate(Object outer, String in) {
-                                       if (in == null || "null".equals(in) || 
in.isEmpty())
-                                               return null;
-                                       return bool(in);
-                               }
-                       }
-               );
-               // @formatter:on
-       }
-
-       /**
-        * Adds a transform for the specified input/output types.
-        *
-        * @param ic The input type.
-        * @param oc The output type.
-        * @param t The transform for converting the input to the output.
-        */
-       public static synchronized void add(Class<?> ic, Class<?> oc, 
Mutater<?,?> t) {
-               var m = CACHE.computeIfAbsent(oc, k -> new 
ConcurrentHashMap<>());
-               m.put(ic, t);
-       }
-
-       /**
-        * Constructs a new instance of the specified class from the specified 
string.
-        *
-        * <p>
-        * Class must be one of the following:
-        * <ul>
-        *      <li>Have a public constructor that takes in a single 
<c>String</c> argument.
-        *      <li>Have a static <c>fromString(String)</c> (or related) method.
-        *      <li>Be an <c>enum</c>.
-        * </ul>
-        *
-        * @param <T> The class type.
-        * @param c The class type.
-        * @param s The string to create the instance from.
-        * @return A new object instance, or <jk>null</jk> if a method for 
converting the string to an object could not be found.
-        */
-       public static <T> T fromString(Class<T> c, String s) {
-               var t = get(String.class, c);
-               return t == null ? null : t.mutate(s);
-       }
-
-       /**
-        * Returns the transform for converting the specified input type to the 
specified output type.
-        *
-        * @param <I> The input type.
-        * @param <O> The output type.
-        * @param ic The input type.
-        * @param oc The output type.
-        * @return The transform for performing the conversion, or 
<jk>null</jk> if the conversion cannot be made.
-        */
-       @SuppressWarnings({
-               "unchecked" // Type erasure requires cast for Mutater lookup
-       })
-       public static <I,O> Mutater<I,O> get(Class<I> ic, Class<O> oc) {
-
-               if (ic == null || oc == null)
-                       return null;
-
-               var m = CACHE.computeIfAbsent(oc, k -> new 
ConcurrentHashMap<>());
-
-               var t = m.computeIfAbsent(ic, k -> find(ic, oc, m));
-
-               return t == NULL ? null : (Mutater<I,O>)t;
-       }
-
-       /**
-        * Returns the transform for converting the specified input type to the 
specified output type.
-        *
-        * @param <I> The input type.
-        * @param <O> The output type.
-        * @param ic The input type.
-        * @param oc The output type.
-        * @return The transform for performing the conversion, or 
<jk>null</jk> if the conversion cannot be made.
-        */
-       public static <I,O> boolean hasMutate(Class<I> ic, Class<O> oc) {
-               return get(ic, oc) != NULL;
-       }
-
-       /**
-        * Converts an object to a string.
-        *
-        * <p>
-        * Normally, this is just going to call <c>toString()</c> on the object.
-        * However, the {@link Locale} and {@link TimeZone} objects are treated 
special so that the returned value
-        * works with the {@link #fromString(Class, String)} method.
-        *
-        * @param o The object to convert to a string.
-        * @return The stringified object, or <jk>null</jk> if the object was 
<jk>null</jk>.
-        */
-       @SuppressWarnings({
-               "unchecked", // Type erasure requires unchecked casts in type 
mutation
-               "java:S3776", // Cognitive complexity acceptable for this 
specific logic
-       })
-       public static String toString(Object o) {
-               if (o == null)
-                       return null;
-               var t = (Mutater<Object,String>)get(o.getClass(), String.class);
-               return t == null ? o.toString() : t.mutate(o);
-       }
-
-       @SuppressWarnings({
-               "unchecked", // Cognitive complexity is acceptable for this 
mutater finder
-               "rawtypes", // Raw types necessary for generic type 
mutation/conversion
-               "java:S3776", // Cognitive complexity acceptable for this 
specific logic
-       })
-       private static Mutater find(Class<?> ic, Class<?> oc, 
Map<Class<?>,Mutater<?,?>> m) {
-
-               if (ic == oc) {
-                       return new Mutater() {
-                               @Override
-                               public Object mutate(Object outer, Object in) {
-                                       return in;
-                               }
-                       };
-               }
-
-               var ici = info(ic);
-               var oci = info(oc);
-
-               var pic = ici.getAllParents().stream().filter(x -> 
nn(m.get(x.inner()))).findFirst().orElse(null);
-               if (nn(pic))
-                       return m.get(pic.inner());
-
-               if (ic == String.class) {
-                       var oc2 = oci.hasPrimitiveWrapper() ? 
oci.getPrimitiveWrapper() : oc;
-                       var oc2i = info(oc2);
-
-                       // @formatter:off
-                       final var createMethod = oc2i.getPublicMethod(
-                               x -> x.isStatic()
-                               && x.isNotDeprecated()
-                               && x.hasReturnType(oc2)
-                               && x.hasParameterTypes(ic)
-                               && (x.hasName("forName") || 
isStaticCreateMethodName(x, ic))
-                       ).orElse(null);
-                       // @formatter:on
-
-                       if (oc2.isEnum() && createMethod == null) {
-                               return new Mutater<String,Object>() {
-                                       @Override
-                                       public Object mutate(Object outer, 
String in) {
-                                               return Enum.valueOf((Class<? 
extends Enum>)oc2, in);
-                                       }
-                               };
-                       }
-
-                       if (nn(createMethod)) {
-                               return new Mutater<String,Object>() {
-                                       @Override
-                                       public Object mutate(Object outer, 
String in) {
-                                               try {
-                                                       return 
createMethod.invoke(null, in);
-                                               } catch (Exception e) {
-                                                       throw toRex(e);
-                                               }
-                                       }
-                               };
-                       }
-               } else {
-                       // @formatter:off
-                       var createMethod = oci.getPublicMethod(
-                               x -> x.isStatic()
-                               && x.isNotDeprecated()
-                               && x.hasReturnType(oc)
-                               && x.hasParameterTypes(ic)
-                               && isStaticCreateMethodName(x, ic)
-                       ).orElse(null);
-                       // @formatter:on
-
-                       if (nn(createMethod)) {
-                               var cm = createMethod.inner();
-                               return new Mutater() {
-                                       @Override
-                                       public Object mutate(Object context, 
Object in) {
-                                               try {
-                                                       return cm.invoke(null, 
in);
-                                               } catch (Exception e) {
-                                                       throw toRex(e);
-                                               }
-                                       }
-                               };
-                       }
-               }
-
-               var c = oci.getPublicConstructor(x -> 
x.hasParameterTypes(ic)).orElse(null);
-               if (nn(c) && c.isNotDeprecated()) {
-                       var isMemberClass = oci.isNonStaticMemberClass();
-                       return new Mutater() {
-                               @Override
-                               public Object mutate(Object outer, Object in) {
-                                       try {
-                                               if (isMemberClass)
-                                                       return 
c.newInstance(outer, in);
-                                               return c.newInstance(in);
-                                       } catch (Exception e) {
-                                               throw toRex(e);
-                                       }
-                               }
-                       };
-               }
-
-               var toXMethod = findToXMethod(ici, oci);
-               if (nn(toXMethod)) {
-                       return new Mutater() {
-                               @Override
-                               public Object mutate(Object outer, Object in) {
-                                       try {
-                                               return toXMethod.invoke(in);
-                                       } catch (Exception e) {
-                                               throw toRex(e);
-                                       }
-                               }
-                       };
-               }
-
-               return NULL;
-       }
-
-       private static MethodInfo findToXMethod(ClassInfo ic, ClassInfo oc) {
-               var tn = oc.getNameReadable();
-               // @formatter:off
-               return ic.getPublicMethod(
-                       x -> x.isNotStatic()
-                       && x.getParameterCount() == 0
-                       && x.getNameSimple().startsWith("to")
-                       && x.getNameSimple().substring(2).equalsIgnoreCase(tn)
-               ).orElse(null);
-               // @formatter:on
-       }
-
-       private static boolean isStaticCreateMethodName(MethodInfo mi, Class<?> 
ic) {
-               var n = mi.getNameSimple();
-               var cn = ic.getSimpleName();
-               // @formatter:off
-               return isOneOf(n, 
"create","from","fromValue","parse","valueOf","builder")
-                       || (n.startsWith("from") && n.substring(4).equals(cn))
-                       || (n.startsWith("for") && n.substring(3).equals(cn))
-                       || (n.startsWith("parse") && n.substring(5).equals(cn));
-               // @formatter:on
-       }
-}
\ No newline at end of file
diff --git 
a/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/ResponseContent.java
 
b/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/ResponseContent.java
index c0447d7da7..89aee215d7 100644
--- 
a/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/ResponseContent.java
+++ 
b/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/ResponseContent.java
@@ -32,6 +32,7 @@ import org.apache.http.conn.*;
 import org.apache.juneau.*;
 import org.apache.juneau.assertions.*;
 import org.apache.juneau.collections.*;
+import org.apache.juneau.commons.conversion.*;
 import org.apache.juneau.commons.utils.*;
 import org.apache.juneau.http.entity.*;
 import org.apache.juneau.http.resource.*;
@@ -257,7 +258,7 @@ public class ResponseContent implements HttpEntity {
                        var mt = MediaType.of(ct);
 
                        if ((parser == null || 
(mt.toString().contains("text/plain") && ! parser.canHandle(ct))) && 
type.hasStringMutater())
-                               return 
type.getStringMutater().mutate(asString());
+                               return BasicConverter.INSTANCE.to(asString(), 
type.inner());
 
                        if (nn(parser)) {
                                try (Closeable in = parser.isReaderParser() ? 
asReader() : asInputStream()) {
@@ -285,10 +286,10 @@ public class ResponseContent implements HttpEntity {
                        }
 
                        if (type.hasReaderMutater())
-                               return 
type.getReaderMutater().mutate(asReader());
+                               return BasicConverter.INSTANCE.to(asReader(), 
type.inner());
 
                        if (type.hasInputStreamMutater())
-                               return 
type.getInputStreamMutater().mutate(asInputStream());
+                               return 
BasicConverter.INSTANCE.to(asInputStream(), type.inner());
 
                        ct = 
response.getStringHeader(HEADER_ContentType).orElse(null);
 
diff --git 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/httppart/RequestContent.java
 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/httppart/RequestContent.java
index 0bf7c26d80..2b95e4c0e0 100644
--- 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/httppart/RequestContent.java
+++ 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/httppart/RequestContent.java
@@ -27,6 +27,7 @@ import java.util.*;
 
 import org.apache.juneau.*;
 import org.apache.juneau.collections.*;
+import org.apache.juneau.commons.conversion.*;
 import org.apache.juneau.commons.io.*;
 import org.apache.juneau.encoders.*;
 import org.apache.juneau.http.header.*;
@@ -570,15 +571,15 @@ public class RequestContent {
                }
 
                if (cm.hasReaderMutater())
-                       return cm.getReaderMutater().mutate(getReader());
+                       return BasicConverter.INSTANCE.to(getReader(), 
cm.inner());
 
                if (cm.hasInputStreamMutater())
-                       return 
cm.getInputStreamMutater().mutate(getInputStream());
+                       return BasicConverter.INSTANCE.to(getInputStream(), 
cm.inner());
 
                var mt = getMediaType();
 
                if ((isEmpty(s(mt)) || mt.toString().startsWith("text/plain")) 
&& cm.hasStringMutater())
-                       return cm.getStringMutater().mutate(asString());
+                       return BasicConverter.INSTANCE.to(asString(), 
cm.inner());
 
                var ct = req.getHeader(ContentType.class);
                throw new UnsupportedMediaType("Unsupported media-type in 
request header ''Content-Type'': ''{0}''\n\tSupported media-types: {1}",
diff --git 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/swagger/BasicSwaggerProviderSession.java
 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/swagger/BasicSwaggerProviderSession.java
index cb3da86b20..d0eb5ae6db 100644
--- 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/swagger/BasicSwaggerProviderSession.java
+++ 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/swagger/BasicSwaggerProviderSession.java
@@ -29,6 +29,7 @@ import java.util.function.*;
 
 import org.apache.juneau.*;
 import org.apache.juneau.annotation.*;
+import org.apache.juneau.commons.conversion.*;
 import org.apache.juneau.bean.swagger.Swagger;
 import org.apache.juneau.collections.*;
 import org.apache.juneau.commons.lang.*;
@@ -706,7 +707,7 @@ public class BasicSwaggerProviderSession {
                } else {
                        var cm = js.getClassMeta(type);
                        if (cm.hasStringMutater()) {
-                               example = cm.getStringMutater().mutate(sex);
+                               example = BasicConverter.INSTANCE.to(sex, 
cm.inner());
                        }
                }
 
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
new file mode 100644
index 0000000000..5933c12b0d
--- /dev/null
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/commons/conversion/ConfigurableConverter_Test.java
@@ -0,0 +1,188 @@
+/*
+ * 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;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.util.concurrent.*;
+import java.util.concurrent.atomic.*;
+
+import org.junit.jupiter.api.*;
+
+/**
+ * Unit tests for {@link ConfigurableConverter}.
+ */
+public class ConfigurableConverter_Test {
+
+       // 
=================================================================================================================
+       // a - Registration and basic conversion
+       // 
=================================================================================================================
+
+       /**
+        * Custom value type used in registration tests.
+        */
+       public static class A01_Value {
+               public final String raw;
+               public A01_Value(String raw) { this.raw = raw; }
+       }
+
+       @Test void a01_registeredConversionIsUsed() {
+               var c = new ConfigurableConverter()
+                       .add(String.class, A01_Value.class, (in, memberOf, 
args) -> new A01_Value(in));
+               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, args) 
-> "[" + in + "]");
+               assertEquals("[42]", c.to(42, String.class));
+       }
+
+       @Test void a03_builtInConversionUsedWhenNoRegistration() {
+               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, 
args) -> new A01_Value(in));
+               assertNull(c.to(null, A01_Value.class));
+       }
+
+       @Test void a05_canConvertReturnsTrueForRegisteredType() {
+               var c = new ConfigurableConverter()
+                       .add(String.class, A01_Value.class, (in, memberOf, 
args) -> new A01_Value(in));
+               assertTrue(c.canConvert(String.class, A01_Value.class));
+       }
+
+       @Test void a06_canConvertReturnsTrueForBuiltInType() {
+               var c = new ConfigurableConverter();
+               assertTrue(c.canConvert(String.class, Integer.class));
+       }
+
+       @Test void a07_canConvertReturnsFalseForUnregisteredUnknownType() {
+               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, 
args) -> new A01_Value(in))
+                       .add(Integer.class, A01_Value.class, (in, memberOf, 
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);
+       }
+
+       /**
+        * 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, 
args) -> new A09_Value(in));
+               var c2 = new ConfigurableConverter();
+               assertNotNull(c1.to("x", A09_Value.class));
+               assertFalse(c2.canConvert(String.class, A09_Value.class));
+       }
+
+       // 
=================================================================================================================
+       // b - memberOf parameter is forwarded to registered conversion
+       // 
=================================================================================================================
+
+       @Test void b01_memberOfIsForwardedToRegisteredConversion() {
+               // Use String→Integer (different types) so the identity-check 
shortcut is not triggered
+               var memberOf = new Object();
+               var captured = new AtomicReference<Object>();
+               var c = new ConfigurableConverter()
+                       .add(String.class, Integer.class, (in, m, args) -> { 
captured.set(m); return Integer.parseInt(in); });
+               c.to("42", memberOf, Integer.class);
+               assertSame(memberOf, captured.get());
+       }
+
+       // 
=================================================================================================================
+       // 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, 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, args) -> 
Integer.parseInt(in) * 2);
+               var threads = 32;
+               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();
+                                       for (int j = 0; j < 100; j++) {
+                                               var result = c.to("21", 
Integer.class);
+                                               if (result != 42)
+                                                       
errors.incrementAndGet();
+                                       }
+                               } catch (Exception e) {
+                                       errors.incrementAndGet();
+                               }
+                       });
+               }
+               latch.countDown();
+               pool.shutdown();
+               pool.awaitTermination(5, TimeUnit.SECONDS);
+               assertEquals(0, errors.get());
+       }
+}
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/utils/MutatersTest.java 
b/juneau-utest/src/test/java/org/apache/juneau/utils/MutatersTest.java
deleted file mode 100644
index 2e8eff62ef..0000000000
--- a/juneau-utest/src/test/java/org/apache/juneau/utils/MutatersTest.java
+++ /dev/null
@@ -1,165 +0,0 @@
-/*
- * 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.utils;
-
-import static org.apache.juneau.reflect.Mutaters.*;
-import static org.junit.jupiter.api.Assertions.*;
-
-import org.apache.juneau.*;
-import org.junit.jupiter.api.*;
-
-@SuppressWarnings({
-       "java:S1172"   // Unused parameters in tests are intentional
-})
-class MutatersTest extends TestBase {
-
-       
//-----------------------------------------------------------------------------------------------------------------
-       // Constructors.
-       
//-----------------------------------------------------------------------------------------------------------------
-
-       public static class A {
-               private String f;
-               public A(String f) {
-                       this.f = f;
-               }
-               public A(int f) {
-                       this.f = String.valueOf(f);
-               }
-               public A(Integer f) {
-                       this.f = String.valueOf(f);
-               }
-       }
-       @Test void stringConstructor() {
-               assertEquals("foo", get(String.class, A.class).mutate("foo").f);
-       }
-       @Test void intConstructor() {
-               assertEquals("1", get(int.class, A.class).mutate(1).f);
-       }
-       @Test void integerConstructor() {
-               assertEquals("2", get(Integer.class, A.class).mutate(2).f);
-       }
-
-       
//-----------------------------------------------------------------------------------------------------------------
-       // fromString methods.
-       
//-----------------------------------------------------------------------------------------------------------------
-
-       public static class D1 {
-               private String f;
-               public static D1 create(String f) {
-                       var d = new D1(); d.f = f; return d;
-               }
-       }
-       @Test void fromString_create() {
-               assertEquals("foo", get(String.class, 
D1.class).mutate("foo").f);
-       }
-
-       public static class D2 {
-               private String f;
-               public static D2 fromString(String f) {
-                       var d = new D2(); d.f = f; return d;
-               }
-       }
-       @Test void fromString_fromString() {
-               assertEquals("foo", get(String.class, 
D2.class).mutate("foo").f);
-       }
-
-       public static class D3 {
-               private String f;
-               public static D3 fromValue(String f) {
-                       var d = new D3(); d.f = f; return d;
-               }
-       }
-       @Test void fromString_fromValue() {
-               assertEquals("foo", get(String.class, 
D3.class).mutate("foo").f);
-       }
-
-       public static class D4 {
-               private String f;
-               public static D4 valueOf(String f) {
-                       var d = new D4(); d.f = f; return d;
-               }
-       }
-       @Test void fromString_valueOf() {
-               assertEquals("foo", get(String.class, 
D4.class).mutate("foo").f);
-       }
-
-       public static class D5 {
-               private String f;
-               public static D5 parse(String f) {
-                       var d = new D5(); d.f = f; return d;
-               }
-       }
-       @Test void fromString_parse() {
-               assertEquals("foo", get(String.class, 
D5.class).mutate("foo").f);
-       }
-
-       public static class D6 {
-               private String f;
-               public static D6 parseString(String f) {
-                       var d = new D6(); d.f = f; return d;
-               }
-       }
-       @Test void fromString_parseString() {
-               assertEquals("foo", get(String.class, 
D6.class).mutate("foo").f);
-       }
-
-       public static class D7 {
-               private String f;
-               public static D7 forName(String f) {
-                       var d = new D7(); d.f = f; return d;
-               }
-       }
-       @Test void fromString_forName() {
-               assertEquals("foo", get(String.class, 
D7.class).mutate("foo").f);
-       }
-
-       public static class D8 {
-               private String f;
-               public static D8 forString(String f) {
-                       var d = new D8(); d.f = f; return d;
-               }
-       }
-       @Test void fromString_forString() {
-               assertEquals("foo", get(String.class, 
D8.class).mutate("foo").f);
-       }
-
-       
//-----------------------------------------------------------------------------------------------------------------
-       // fromX methods.
-       
//-----------------------------------------------------------------------------------------------------------------
-
-       public static class X {}
-
-       public static class E1 {
-               private String f;
-               public static E1 create(X x) {
-                       var e = new E1(); e.f = "ok"; return e;
-               }
-       }
-       @Test void fromX_create() {
-               assertEquals("ok", get(X.class, E1.class).mutate(new X()).f);
-       }
-
-       public static class E2 {
-               private String f;
-               public static E2 fromX(X x) {
-                       var e = new E2(); e.f = "ok"; return e;
-               }
-       }
-       @Test void fromX_fromX() {
-               assertEquals("ok", get(X.class, E2.class).mutate(new X()).f);
-       }
-}
\ No newline at end of file

Reply via email to