1010
1111package org .junit .platform .commons .support .conversion ;
1212
13+ import static org .apiguardian .api .API .Status .DEPRECATED ;
14+ import static org .apiguardian .api .API .Status .EXPERIMENTAL ;
1315import static org .apiguardian .api .API .Status .MAINTAINED ;
14- import static org .junit .platform .commons .util .ReflectionUtils .getWrapperType ;
1516
16- import java .util .List ;
17- import java .util .Optional ;
17+ import java .util .ServiceLoader ;
18+ import java .util .stream .Stream ;
19+ import java .util .stream .StreamSupport ;
1820
1921import org .apiguardian .api .API ;
2022import org .jspecify .annotations .Nullable ;
21- import org .junit .platform .commons .util .ClassLoaderUtils ;
2223
2324/**
2425 * {@code ConversionSupport} provides static utility methods for converting a
2930@ API (status = MAINTAINED , since = "1.13.3" )
3031public final class ConversionSupport {
3132
32- private static final List <StringToObjectConverter > stringToObjectConverters = List .of ( //
33- new StringToBooleanConverter (), //
34- new StringToCharacterConverter (), //
35- new StringToNumberConverter (), //
36- new StringToClassConverter (), //
37- new StringToEnumConverter (), //
38- new StringToJavaTimeConverter (), //
39- new StringToCommonJavaTypesConverter (), //
40- new FallbackStringToObjectConverter () //
41- );
42-
4333 private ConversionSupport () {
4434 /* no-op */
4535 }
@@ -48,48 +38,6 @@ private ConversionSupport() {
4838 * Convert the supplied source {@code String} into an instance of the specified
4939 * target type.
5040 *
51- * <p>If the target type is {@code String}, the source {@code String} will not
52- * be modified.
53- *
54- * <p>Some forms of conversion require a {@link ClassLoader}. If none is
55- * provided, the {@linkplain ClassLoaderUtils#getDefaultClassLoader() default
56- * ClassLoader} will be used.
57- *
58- * <p>This method is able to convert strings into primitive types and their
59- * corresponding wrapper types ({@link Boolean}, {@link Character}, {@link Byte},
60- * {@link Short}, {@link Integer}, {@link Long}, {@link Float}, and
61- * {@link Double}), enum constants, date and time types from the
62- * {@code java.time} package, as well as common Java types such as {@link Class},
63- * {@link java.io.File}, {@link java.nio.file.Path}, {@link java.nio.charset.Charset},
64- * {@link java.math.BigDecimal}, {@link java.math.BigInteger},
65- * {@link java.util.Currency}, {@link java.util.Locale}, {@link java.util.UUID},
66- * {@link java.net.URI}, and {@link java.net.URL}.
67- *
68- * <p>If the target type is not covered by any of the above, a convention-based
69- * conversion strategy will be used to convert the source {@code String} into the
70- * given target type by invoking a static factory method or factory constructor
71- * defined in the target type. The search algorithm used in this strategy is
72- * outlined below.
73- *
74- * <h4>Search Algorithm</h4>
75- *
76- * <ol>
77- * <li>Search for a single, non-private static factory method in the target
78- * type that converts from a {@link String} to the target type. Use the
79- * factory method if present.</li>
80- * <li>Search for a single, non-private constructor in the target type that
81- * accepts a {@link String}. Use the constructor if present.</li>
82- * <li>Search for a single, non-private static factory method in the target
83- * type that converts from a {@link CharSequence} to the target type. Use the
84- * factory method if present.</li>
85- * <li>Search for a single, non-private constructor in the target type that
86- * accepts a {@link CharSequence}. Use the constructor if present.</li>
87- * </ol>
88- *
89- * <p>If multiple suitable factory methods or constructors are discovered they
90- * will be ignored. If neither a single factory method nor a single constructor
91- * is found, the convention-based conversion strategy will not apply.
92- *
9341 * @param source the source {@code String} to convert; may be {@code null}
9442 * but only if the target type is a reference type
9543 * @param targetType the target type the source should be converted into;
@@ -101,49 +49,44 @@ private ConversionSupport() {
10149 * type is a reference type
10250 *
10351 * @since 1.11
52+ * @see DefaultConverter
53+ * @deprecated Use {@link #convert(Object, ConversionContext)} instead.
10454 */
105- @ SuppressWarnings ("unchecked" )
55+ @ Deprecated
56+ @ API (status = DEPRECATED , since = "6.0" )
10657 public static <T > @ Nullable T convert (@ Nullable String source , Class <T > targetType ,
10758 @ Nullable ClassLoader classLoader ) {
108- if (source == null ) {
109- if (targetType .isPrimitive ()) {
110- throw new ConversionException (
111- "Cannot convert null to primitive value of type " + targetType .getTypeName ());
112- }
113- return null ;
114- }
115-
116- if (String .class .equals (targetType )) {
117- return (T ) source ;
118- }
59+ ConversionContext context = new ConversionContext (source , TypeDescriptor .forClass (targetType ), classLoader );
60+ return convert (source , context );
61+ }
11962
120- Class <?> targetTypeToUse = toWrapperType (targetType );
121- Optional <StringToObjectConverter > converter = stringToObjectConverters .stream ().filter (
122- candidate -> candidate .canConvertTo (targetTypeToUse )).findFirst ();
123- if (converter .isPresent ()) {
124- try {
125- ClassLoader classLoaderToUse = classLoader != null ? classLoader
126- : ClassLoaderUtils .getDefaultClassLoader ();
127- return (T ) converter .get ().convert (source , targetTypeToUse , classLoaderToUse );
128- }
129- catch (Exception ex ) {
130- if (ex instanceof ConversionException conversionException ) {
131- // simply rethrow it
132- throw conversionException ;
133- }
134- // else
135- throw new ConversionException (
136- "Failed to convert String \" %s\" to type %s" .formatted (source , targetType .getTypeName ()), ex );
137- }
138- }
63+ /**
64+ * Convert the supplied source object into an instance of the specified
65+ * target type.
66+ *
67+ * @param source the source object to convert; may be {@code null}
68+ * but only if the target type is a reference type
69+ * @param context the context for the conversion
70+ * @param <T> the type of the target
71+ * @return the converted object; may be {@code null} but only if the target
72+ * type is a reference type
73+ * @since 6.0
74+ */
75+ @ API (status = EXPERIMENTAL , since = "6.0" )
76+ @ SuppressWarnings ({ "unchecked" , "rawtypes" , "TypeParameterUnusedInFormals" })
77+ public static <T > @ Nullable T convert (@ Nullable Object source , ConversionContext context ) {
78+ ServiceLoader <Converter > serviceLoader = ServiceLoader .load (Converter .class , context .classLoader ());
13979
140- throw new ConversionException (
141- "No built-in converter for source type java.lang.String and target type " + targetType .getTypeName ());
142- }
80+ Converter converter = Stream .concat ( //
81+ StreamSupport .stream (serviceLoader .spliterator (), false ), //
82+ Stream .of (DefaultConverter .INSTANCE )) //
83+ .filter (candidate -> candidate .canConvert (context )) //
84+ .findFirst () //
85+ .orElseThrow (() -> new ConversionException (
86+ "No registered or built-in converter for source '%s' and target type %s" .formatted ( //
87+ source , context .targetType ())));
14388
144- private static Class <?> toWrapperType (Class <?> targetType ) {
145- Class <?> wrapperType = getWrapperType (targetType );
146- return wrapperType != null ? wrapperType : targetType ;
89+ return (T ) converter .convert (source , context );
14790 }
14891
14992}
0 commit comments