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