If the source and target types are identical the source object will not
+ *
If the source and target types are identical, the source object will not
* be modified.
*
* @since 5.0
@@ -82,48 +83,42 @@ public DefaultArgumentConverter(ExtensionContext context) {
@Override
public final @Nullable Object convert(@Nullable Object source, ParameterContext context) {
- Class> targetType = context.getParameter().getType();
ClassLoader classLoader = getClassLoader(context.getDeclaringExecutable().getDeclaringClass());
- return convert(source, targetType, classLoader);
+ return convert(source, TypeDescriptor.forParameter(context.getParameter()), classLoader);
}
@Override
public final @Nullable Object convert(@Nullable Object source, FieldContext context)
throws ArgumentConversionException {
-
- Class> targetType = context.getField().getType();
ClassLoader classLoader = getClassLoader(context.getField().getDeclaringClass());
- return convert(source, targetType, classLoader);
+ return convert(source, TypeDescriptor.forField(context.getField()), classLoader);
}
- public final @Nullable Object convert(@Nullable Object source, Class> targetType, ClassLoader classLoader) {
+ public final @Nullable Object convert(@Nullable Object source, TypeDescriptor targetType, ClassLoader classLoader) {
if (source == null) {
if (targetType.isPrimitive()) {
throw new ArgumentConversionException(
- "Cannot convert null to primitive value of type " + targetType.getTypeName());
+ "Cannot convert null to primitive value of type " + targetType.getType().getTypeName());
}
return null;
}
- if (ReflectionUtils.isAssignableTo(source, targetType)) {
+ if (ReflectionUtils.isAssignableTo(source, targetType.getType())) {
return source;
}
- if (source instanceof String string) {
- if (targetType == Locale.class && getLocaleConversionFormat() == LocaleConversionFormat.BCP_47) {
- return Locale.forLanguageTag(string);
- }
-
- try {
- return convert(string, targetType, classLoader);
- }
- catch (ConversionException ex) {
- throw new ArgumentConversionException(ex.getMessage(), ex);
- }
+ if (source instanceof String //
+ && targetType.getType() == Locale.class //
+ && getLocaleConversionFormat() == LocaleConversionFormat.BCP_47) {
+ return Locale.forLanguageTag((String) source);
}
- throw new ArgumentConversionException("No built-in converter for source type %s and target type %s".formatted(
- source.getClass().getTypeName(), targetType.getTypeName()));
+ try {
+ return delegateConversion(source, targetType, classLoader);
+ }
+ catch (ConversionException ex) {
+ throw new ArgumentConversionException(ex.getMessage(), ex);
+ }
}
private LocaleConversionFormat getLocaleConversionFormat() {
@@ -132,7 +127,7 @@ private LocaleConversionFormat getLocaleConversionFormat() {
}
@Nullable
- Object convert(@Nullable String source, Class> targetType, ClassLoader classLoader) {
+ Object delegateConversion(@Nullable Object source, TypeDescriptor targetType, ClassLoader classLoader) {
return ConversionSupport.convert(source, targetType, classLoader);
}
diff --git a/junit-platform-commons/src/main/java/module-info.java b/junit-platform-commons/src/main/java/module-info.java
index 092c654cdd51..6b156c764c27 100644
--- a/junit-platform-commons/src/main/java/module-info.java
+++ b/junit-platform-commons/src/main/java/module-info.java
@@ -58,5 +58,6 @@
org.junit.platform.suite.engine,
org.junit.platform.testkit,
org.junit.vintage.engine;
+ uses org.junit.platform.commons.support.conversion.Converter;
uses org.junit.platform.commons.support.scanning.ClasspathScanner;
}
diff --git a/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/ConversionContext.java b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/ConversionContext.java
new file mode 100644
index 000000000000..75eb57a90879
--- /dev/null
+++ b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/ConversionContext.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2015-2025 the original author or authors.
+ *
+ * All rights reserved. This program and the accompanying materials are
+ * made available under the terms of the Eclipse Public License v2.0 which
+ * accompanies this distribution and is available at
+ *
+ * https://www.eclipse.org/legal/epl-v20.html
+ */
+
+package org.junit.platform.commons.support.conversion;
+
+import static org.apiguardian.api.API.Status.EXPERIMENTAL;
+
+import org.apiguardian.api.API;
+import org.jspecify.annotations.Nullable;
+
+/**
+ * {@code ConversionContext} encapsulates the context in which the
+ * current conversion is being executed.
+ *
+ *
{@link Converter Converters} are provided an instance of
+ * {@code ConversionContext} to perform their work.
+ *
+ * @param sourceType
+ * @param targetType
+ * @param classLoader
+ *
+ * @since 6.0
+ * @see Converter
+ */
+@API(status = EXPERIMENTAL, since = "6.0")
+public record ConversionContext(TypeDescriptor sourceType, TypeDescriptor targetType, ClassLoader classLoader) {
+
+ /**
+ *
+ * @param source
+ * @param targetType
+ * @param classLoader
+ */
+ public ConversionContext(@Nullable Object source, TypeDescriptor targetType, ClassLoader classLoader) {
+ this(TypeDescriptor.forInstance(source), targetType, classLoader);
+ }
+
+}
diff --git a/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/ConversionSupport.java b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/ConversionSupport.java
index fe3923f9cb4d..e43facc09dd9 100644
--- a/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/ConversionSupport.java
+++ b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/ConversionSupport.java
@@ -10,15 +10,15 @@
package org.junit.platform.commons.support.conversion;
+import static org.apiguardian.api.API.Status.DEPRECATED;
import static org.apiguardian.api.API.Status.EXPERIMENTAL;
-import static org.junit.platform.commons.util.ReflectionUtils.getWrapperType;
-import java.util.List;
-import java.util.Optional;
+import java.util.ServiceLoader;
+import java.util.stream.Stream;
+import java.util.stream.StreamSupport;
import org.apiguardian.api.API;
import org.jspecify.annotations.Nullable;
-import org.junit.platform.commons.util.ClassLoaderUtils;
/**
* {@code ConversionSupport} provides static utility methods for converting a
@@ -29,17 +29,6 @@
@API(status = EXPERIMENTAL, since = "1.11")
public final class ConversionSupport {
- private static final List stringToObjectConverters = List.of( //
- new StringToBooleanConverter(), //
- new StringToCharacterConverter(), //
- new StringToNumberConverter(), //
- new StringToClassConverter(), //
- new StringToEnumConverter(), //
- new StringToJavaTimeConverter(), //
- new StringToCommonJavaTypesConverter(), //
- new FallbackStringToObjectConverter() //
- );
-
private ConversionSupport() {
/* no-op */
}
@@ -48,43 +37,6 @@ private ConversionSupport() {
* Convert the supplied source {@code String} into an instance of the specified
* target type.
*
- *
If the target type is {@code String}, the source {@code String} will not
- * be modified.
- *
- *
Some forms of conversion require a {@link ClassLoader}. If none is
- * provided, the {@linkplain ClassLoaderUtils#getDefaultClassLoader() default
- * ClassLoader} will be used.
- *
- *
This method is able to convert strings into primitive types and their
- * corresponding wrapper types ({@link Boolean}, {@link Character}, {@link Byte},
- * {@link Short}, {@link Integer}, {@link Long}, {@link Float}, and
- * {@link Double}), enum constants, date and time types from the
- * {@code java.time} package, as well as common Java types such as {@link Class},
- * {@link java.io.File}, {@link java.nio.file.Path}, {@link java.nio.charset.Charset},
- * {@link java.math.BigDecimal}, {@link java.math.BigInteger},
- * {@link java.util.Currency}, {@link java.util.Locale}, {@link java.util.UUID},
- * {@link java.net.URI}, and {@link java.net.URL}.
- *
- *
If the target type is not covered by any of the above, a convention-based
- * conversion strategy will be used to convert the source {@code String} into the
- * given target type by invoking a static factory method or factory constructor
- * defined in the target type. The search algorithm used in this strategy is
- * outlined below.
- *
- *
Search Algorithm
- *
- *
- *
Search for a single, non-private static factory method in the target
- * type that converts from a String to the target type. Use the factory method
- * if present.
- *
Search for a single, non-private constructor in the target type that
- * accepts a String. Use the constructor if present.
- *
- *
- *
If multiple suitable factory methods are discovered they will be ignored.
- * If neither a single factory method nor a single constructor is found, the
- * convention-based conversion strategy will not apply.
- *
* @param source the source {@code String} to convert; may be {@code null}
* but only if the target type is a reference type
* @param targetType the target type the source should be converted into;
@@ -96,49 +48,49 @@ private ConversionSupport() {
* type is a reference type
*
* @since 1.11
+ * @see DefaultConverter
+ * @deprecated Use {@link #convert(Object, TypeDescriptor, ClassLoader)} instead.
*/
- @SuppressWarnings("unchecked")
+ @Deprecated
+ @API(status = DEPRECATED, since = "6.0")
public static @Nullable T convert(@Nullable String source, Class targetType,
@Nullable ClassLoader classLoader) {
- if (source == null) {
- if (targetType.isPrimitive()) {
- throw new ConversionException(
- "Cannot convert null to primitive value of type " + targetType.getTypeName());
- }
- return null;
- }
-
- if (String.class.equals(targetType)) {
- return (T) source;
- }
+ return convert(source, TypeDescriptor.forClass(targetType), classLoader);
+ }
- Class> targetTypeToUse = toWrapperType(targetType);
- Optional converter = stringToObjectConverters.stream().filter(
- candidate -> candidate.canConvertTo(targetTypeToUse)).findFirst();
- if (converter.isPresent()) {
- try {
- ClassLoader classLoaderToUse = classLoader != null ? classLoader
- : ClassLoaderUtils.getDefaultClassLoader();
- return (T) converter.get().convert(source, targetTypeToUse, classLoaderToUse);
- }
- catch (Exception ex) {
- if (ex instanceof ConversionException conversionException) {
- // simply rethrow it
- throw conversionException;
- }
- // else
- throw new ConversionException(
- "Failed to convert String \"%s\" to type %s".formatted(source, targetType.getTypeName()), ex);
- }
- }
+ /**
+ * Convert the supplied source object into an instance of the specified
+ * target type.
+ *
+ * @param source the source object to convert; may be {@code null}
+ * but only if the target type is a reference type
+ * @param targetType the target type the source should be converted into;
+ * never {@code null}
+ * @param classLoader the {@code ClassLoader} to use; may be {@code null} to
+ * use the default {@code ClassLoader}
+ * @param the type of the target
+ * @return the converted object; may be {@code null} but only if the target
+ * type is a reference type
+ *
+ * @since 6.0
+ */
+ @API(status = EXPERIMENTAL, since = "6.0")
+ @SuppressWarnings({ "unchecked", "rawtypes" })
+ public static @Nullable T convert(@Nullable Object source, TypeDescriptor targetType,
+ @Nullable ClassLoader classLoader) {
+ ConversionContext context = new ConversionContext(source, targetType, classLoader);
+ ServiceLoader serviceLoader = ServiceLoader.load(Converter.class, context.classLoader());
- throw new ConversionException(
- "No built-in converter for source type java.lang.String and target type " + targetType.getTypeName());
- }
+ Converter converter = Stream.concat( //
+ StreamSupport.stream(serviceLoader.spliterator(), false), //
+ Stream.of(DefaultConverter.INSTANCE)) //
+ .filter(candidate -> candidate.canConvert(context)) //
+ .findFirst() //
+ .orElseThrow(() -> new ConversionException(
+ "No registered or built-in converter for source '%s' and target type %s".formatted( //
+ source, targetType.getTypeName())));
- private static Class> toWrapperType(Class> targetType) {
- Class> wrapperType = getWrapperType(targetType);
- return wrapperType != null ? wrapperType : targetType;
+ return (T) converter.convert(source, context);
}
}
diff --git a/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/Converter.java b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/Converter.java
new file mode 100644
index 000000000000..08f673d036ca
--- /dev/null
+++ b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/Converter.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2015-2025 the original author or authors.
+ *
+ * All rights reserved. This program and the accompanying materials are
+ * made available under the terms of the Eclipse Public License v2.0 which
+ * accompanies this distribution and is available at
+ *
+ * https://www.eclipse.org/legal/epl-v20.html
+ */
+
+package org.junit.platform.commons.support.conversion;
+
+import static org.apiguardian.api.API.Status.EXPERIMENTAL;
+
+import org.apiguardian.api.API;
+import org.jspecify.annotations.Nullable;
+
+/**
+ * {@code Converter} is an abstraction that allows an input object to
+ * be converted to an instance of a different class.
+ *
+ *
Implementations are loaded via the {@link java.util.ServiceLoader} and must
+ * follow the service provider requirements. They should not make any assumptions
+ * regarding when they are instantiated or how often they are called. Since
+ * instances may potentially be cached and called from different threads, they
+ * should be thread-safe.
+ *
+ *
Extend {@link TypedConverter} if your implementation always converts
+ * from a given source type into a given target type and does not need access to
+ * the {@link ClassLoader} to perform the conversion.
+ *
+ * @param
+ * @param
+ *
+ * @since 6.0
+ * @see ConversionSupport
+ * @see TypedConverter
+ */
+@API(status = EXPERIMENTAL, since = "6.0")
+public interface Converter {
+
+ /**
+ * Determine if the supplied conversion context is supported.
+ *
+ * @param context the context for the conversion; never {@code null}
+ * @return {@code true} if the conversion is supported
+ */
+ boolean canConvert(ConversionContext context);
+
+ /**
+ * Convert the supplied source object according to the supplied conversion context.
+ *
This method will only be invoked if {@link #canConvert(ConversionContext)}
+ * returned {@code true} for the same context.
+ *
+ * @param source the source object to convert; may be {@code null}
+ * but only if the target type is a reference type
+ * @param context the context for the conversion; never {@code null}
+ * @return the converted object; may be {@code null} but only if the target
+ * type is a reference type
+ * @throws ConversionException if an error occurs during the conversion
+ */
+ @Nullable
+ T convert(@Nullable S source, ConversionContext context) throws ConversionException;
+
+}
diff --git a/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/DefaultConverter.java b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/DefaultConverter.java
new file mode 100644
index 000000000000..fac33e7736fc
--- /dev/null
+++ b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/DefaultConverter.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright 2015-2025 the original author or authors.
+ *
+ * All rights reserved. This program and the accompanying materials are
+ * made available under the terms of the Eclipse Public License v2.0 which
+ * accompanies this distribution and is available at
+ *
+ * https://www.eclipse.org/legal/epl-v20.html
+ */
+
+package org.junit.platform.commons.support.conversion;
+
+import static org.apiguardian.api.API.Status.INTERNAL;
+
+import java.io.File;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.net.URI;
+import java.net.URL;
+import java.util.Currency;
+import java.util.List;
+import java.util.Locale;
+import java.util.Optional;
+import java.util.UUID;
+
+import org.apiguardian.api.API;
+import org.jspecify.annotations.Nullable;
+import org.junit.platform.commons.util.ClassLoaderUtils;
+
+/**
+ * {@code DefaultConversionService} is the default implementation of the
+ * {@link Converter} API.
+ *
+ *
The {@code DefaultConversionService} is able to convert from strings to a
+ * number of primitive types and their corresponding wrapper types (Byte, Short,
+ * Integer, Long, Float, and Double), date and time types from the
+ * {@code java.time} package, and some additional common Java types such as
+ * {@link File}, {@link BigDecimal}, {@link BigInteger}, {@link Currency},
+ * {@link Locale}, {@link URI}, {@link URL}, {@link UUID}, etc.
+ *
+ *
If the source and target types are identical, the source object will not
+ * be modified.
+ *
+ * @since 6.0
+ */
+@API(status = INTERNAL, since = "6.0")
+public class DefaultConverter implements Converter {
+
+ static final DefaultConverter INSTANCE = new DefaultConverter();
+
+ private static final List> stringToObjectConverters = List.of(new StringToBooleanConverter(), //
+ new StringToCharacterConverter(), //
+ new StringToNumberConverter(), //
+ new StringToClassConverter(), //
+ new StringToEnumConverter(), //
+ new StringToJavaTimeConverter(), //
+ new StringToCommonJavaTypesConverter(), //
+ new FallbackStringToObjectConverter() //
+ );
+
+ private DefaultConverter() {
+ // nothing to initialize
+ }
+
+ /**
+ * Determine if the supplied conversion context is supported.
+ *
FIXME add more content from {@link Converter#convert} about the conversion algorithm
+ *
+ * @param context the context for the conversion; never {@code null}
+ * @return {@code true} if the conversion is supported
+ */
+ @Override
+ public boolean canConvert(ConversionContext context) {
+ if (context.sourceType() == TypeDescriptor.NONE) {
+ return !context.targetType().isPrimitive();
+ }
+
+ if (!(String.class.equals(context.sourceType().getType()))) {
+ return false;
+ }
+
+ if (String.class.equals(context.targetType().getType())) {
+ return true;
+ }
+
+ return stringToObjectConverters.stream().anyMatch(candidate -> candidate.canConvert(context));
+ }
+
+ /**
+ * Convert the supplied source {@link String} into an instance of the specified
+ * target type.
+ *
If the target type is {@code String}, the source {@code String} will not
+ * be modified.
+ *
Some forms of conversion require a {@link ClassLoader}. If none is
+ * provided, the {@linkplain ClassLoaderUtils#getDefaultClassLoader() default
+ * ClassLoader} will be used.
+ *
This method is able to convert strings into primitive types and their
+ * corresponding wrapper types ({@link Boolean}, {@link Character}, {@link Byte},
+ * {@link Short}, {@link Integer}, {@link Long}, {@link Float}, and
+ * {@link Double}), enum constants, date and time types from the
+ * {@code java.time} package, as well as common Java types such as {@link Class},
+ * {@link java.io.File}, {@link java.nio.file.Path}, {@link java.nio.charset.Charset},
+ * {@link java.math.BigDecimal}, {@link java.math.BigInteger},
+ * {@link java.util.Currency}, {@link java.util.Locale}, {@link java.util.UUID},
+ * {@link java.net.URI}, and {@link java.net.URL}.
+ *
If the target type is not covered by any of the above, a convention-based
+ * conversion strategy will be used to convert the source {@code String} into the
+ * given target type by invoking a static factory method or factory constructor
+ * defined in the target type. The search algorithm used in this strategy is
+ * outlined below.
+ *
Search Algorithm
+ *
+ *
Search for a single, non-private static factory method in the target
+ * type that converts from a String to the target type. Use the factory method
+ * if present.
+ *
Search for a single, non-private constructor in the target type that
+ * accepts a String. Use the constructor if present.
+ *
+ *
If multiple suitable factory methods are discovered, they will be ignored.
+ * If neither a single factory method nor a single constructor is found, the
+ * convention-based conversion strategy will not apply.
+ *
+ * @param source the source {@link String} to convert; may be {@code null}
+ * but only if the target type is a reference type
+ * @param context the context for the conversion; never {@code null}
+ * @return the converted object; may be {@code null} but only if the target
+ * type is a reference type
+ * @throws ConversionException if an error occurs during the conversion
+ */
+ @Override
+ public @Nullable Object convert(@Nullable String source, ConversionContext context) throws ConversionException {
+ if (source == null) {
+ if (context.targetType().isPrimitive()) {
+ throw new ConversionException(
+ "Cannot convert null to primitive value of type " + context.targetType().getTypeName());
+ }
+ return null;
+ }
+
+ if (String.class.equals(context.targetType().getType())) {
+ return source;
+ }
+
+ Optional> converter = stringToObjectConverters.stream().filter(
+ candidate -> candidate.canConvert(context)).findFirst();
+ if (converter.isPresent()) {
+ try {
+ return converter.get().convert(source, context);
+ }
+ catch (Exception ex) {
+ if (ex instanceof ConversionException) {
+ // simply rethrow it
+ throw (ConversionException) ex;
+ }
+ // else
+ throw new ConversionException(
+ "Failed to convert String \"%s\" to type %s".formatted(source, context.targetType().getTypeName()),
+ ex);
+ }
+ }
+
+ throw new ConversionException("No built-in converter for source type java.lang.String and target type "
+ + context.targetType().getTypeName());
+ }
+
+}
diff --git a/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/FallbackStringToObjectConverter.java b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/FallbackStringToObjectConverter.java
index 916406e3fbcb..2a4949f15875 100644
--- a/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/FallbackStringToObjectConverter.java
+++ b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/FallbackStringToObjectConverter.java
@@ -30,7 +30,7 @@
import org.junit.platform.commons.util.Preconditions;
/**
- * {@code FallbackStringToObjectConverter} is a {@link StringToObjectConverter}
+ * {@code FallbackStringToObjectConverter} is a {@link StringToTargetTypeConverter}
* that provides a fallback conversion strategy for converting from a
* {@link String} to a given target type by invoking a static factory method
* or factory constructor defined in the target type.
@@ -52,7 +52,7 @@
* @since 1.11
* @see ConversionSupport
*/
-class FallbackStringToObjectConverter implements StringToObjectConverter {
+class FallbackStringToObjectConverter extends StringToTargetTypeConverter