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
@@ -58,49 +60,42 @@ private DefaultArgumentConverter() {
@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) {
- try {
- return convert(string, targetType, classLoader);
- }
- catch (ConversionException ex) {
- throw new ArgumentConversionException(ex.getMessage(), ex);
- }
+ try {
+ ConversionContext context = new ConversionContext(source, targetType, classLoader);
+ return delegateConversion(source, context);
+ }
+ catch (ConversionException ex) {
+ throw new ArgumentConversionException(ex.getMessage(), ex);
}
-
- throw new ArgumentConversionException("No built-in converter for source type %s and target type %s".formatted(
- source.getClass().getTypeName(), targetType.getTypeName()));
}
@Nullable
- Object convert(@Nullable String source, Class> targetType, ClassLoader classLoader) {
- return ConversionSupport.convert(source, targetType, classLoader);
+ Object delegateConversion(@Nullable Object source, ConversionContext context) {
+ return ConversionSupport.convert(source, context);
}
}
diff --git a/junit-platform-commons/src/main/java/module-info.java b/junit-platform-commons/src/main/java/module-info.java
index 3b83d2ffc150..63303fef0e1d 100644
--- a/junit-platform-commons/src/main/java/module-info.java
+++ b/junit-platform-commons/src/main/java/module-info.java
@@ -56,5 +56,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..42b0cbf42e95
--- /dev/null
+++ b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/ConversionContext.java
@@ -0,0 +1,50 @@
+/*
+ * 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 static org.junit.platform.commons.util.ClassLoaderUtils.getDefaultClassLoader;
+
+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 the descriptor of the source type
+ * @param targetType the descriptor of the type the source should be converted into
+ * @param classLoader the {@code ClassLoader} to use
+ *
+ * @since 6.0
+ * @see Converter
+ */
+@API(status = EXPERIMENTAL, since = "6.0")
+public record ConversionContext(TypeDescriptor sourceType, TypeDescriptor targetType, ClassLoader classLoader) {
+
+ /**
+ * Create a new {@code ConversionContext}, expecting an instance of the
+ * source instead of its type descriptor.
+ *
+ * @param source the source instance; may be {@code null}
+ * @param targetType the descriptor of the type the source should be converted into
+ * @param classLoader the {@code ClassLoader} to use; may be {@code null} to
+ * use the default {@code ClassLoader}
+ */
+ public ConversionContext(@Nullable Object source, TypeDescriptor targetType, @Nullable ClassLoader classLoader) {
+ this(TypeDescriptor.forInstance(source), targetType,
+ classLoader != null ? classLoader : getDefaultClassLoader());
+ }
+
+}
diff --git a/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/ConversionException.java b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/ConversionException.java
index d5fe3902f8fb..92cf75effb74 100644
--- a/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/ConversionException.java
+++ b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/ConversionException.java
@@ -15,6 +15,7 @@
import java.io.Serial;
import org.apiguardian.api.API;
+import org.jspecify.annotations.Nullable;
import org.junit.platform.commons.JUnitException;
/**
@@ -33,7 +34,7 @@ public ConversionException(String message) {
super(message);
}
- public ConversionException(String message, Throwable cause) {
+ public ConversionException(String message, @Nullable Throwable cause) {
super(message, cause);
}
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 34ed6ef458f4..68da9058ea8d 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,16 @@
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.apiguardian.api.API.Status.MAINTAINED;
-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 +30,6 @@
@API(status = MAINTAINED, since = "1.13.3")
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 +38,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 +49,44 @@ private ConversionSupport() {
* type is a reference type
*
* @since 1.11
+ * @see DefaultConverter
+ * @deprecated Use {@link #convert(Object, ConversionContext)} 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;
- }
+ ConversionContext context = new ConversionContext(source, TypeDescriptor.forClass(targetType), classLoader);
+ return convert(source, context);
+ }
- 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 context the context for the conversion
+ * @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", "TypeParameterUnusedInFormals" })
+ public static @Nullable T convert(@Nullable Object source, ConversionContext context) {
+ 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, context.targetType())));
- 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..f30b731ceb45
--- /dev/null
+++ b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/Converter.java
@@ -0,0 +1,64 @@
+/*
+ * 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 SimpleConverter} 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 the type of the source to convert
+ * @param the type the source should be converted into
+ *
+ * @since 6.0
+ * @see ConversionSupport
+ * @see SimpleConverter
+ */
+@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
+ */
+ 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..b886925b1739
--- /dev/null
+++ b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/DefaultConverter.java
@@ -0,0 +1,165 @@
+/*
+ * 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().equals(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());
+ }
+ 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 conversionException) {
+ // simply rethrow it
+ throw conversionException;
+ }
+ // else
+ throw new ConversionException(
+ "Failed to convert String \"%s\" to type %s".formatted(source, context.targetType()), ex);
+ }
+ }
+
+ throw new ConversionException(
+ "No built-in converter for source type java.lang.String and target type " + context.targetType());
+ }
+
+}
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..b682a6a70028 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<@Nullable Object> {
/**
* Implementation of the NULL Object Pattern.
@@ -71,12 +71,13 @@ class FallbackStringToObjectConverter implements StringToObjectConverter {
= new ConcurrentHashMap<>(64);
@Override
- public boolean canConvertTo(Class> targetType) {
+ boolean canConvert(Class> targetType) {
return findFactoryExecutable(targetType) != NULL_EXECUTABLE;
}
@Override
- public @Nullable Object convert(String source, Class> targetType) throws Exception {
+ @Nullable
+ Object convert(String source, Class> targetType) {
Function executable = findFactoryExecutable(targetType);
Preconditions.condition(executable != NULL_EXECUTABLE,
"Illegal state: convert() must not be called if canConvert() returned false");
diff --git a/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/SimpleConverter.java b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/SimpleConverter.java
new file mode 100644
index 000000000000..68e888ae4f0e
--- /dev/null
+++ b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/SimpleConverter.java
@@ -0,0 +1,71 @@
+/*
+ * 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;
+import org.junit.platform.commons.util.Preconditions;
+
+/**
+ * {@code SimpleConverter} is an abstract base class for {@link Converter}
+ * implementations that always convert objects of a given source type into a
+ * given target type.
+ *
+ * @param the type of the source argument to convert
+ * @param the type of the target object to create from the source
+ * @since 6.0
+ */
+@API(status = EXPERIMENTAL, since = "6.0")
+public abstract class SimpleConverter implements Converter {
+
+ private final Class sourceType;
+ private final Class targetType;
+
+ /**
+ * Create a new {@code SimpleConverter}.
+ *
+ * @param sourceType the type of the argument to convert; never {@code null}
+ * @param targetType the type of the target object to create from the source;
+ * never {@code null}
+ */
+ protected SimpleConverter(Class sourceType, Class targetType) {
+ this.sourceType = Preconditions.notNull(sourceType, "sourceType must not be null");
+ this.targetType = Preconditions.notNull(targetType, "targetType must not be null");
+ }
+
+ @Override
+ public final boolean canConvert(ConversionContext context) {
+ // FIXME adjust for subtypes
+ return !context.sourceType().equals(TypeDescriptor.NONE) //
+ && this.sourceType == context.sourceType().getType() //
+ && this.targetType == context.targetType().getType();
+ }
+
+ @Override
+ public final T convert(@Nullable S source, ConversionContext context) {
+ Preconditions.notNull(source, "source cannot be null");
+ return convert(source);
+ }
+
+ /**
+ * Convert the supplied {@code source} object of type {@code S} into an object
+ * of type {@code T}.
+ *
+ * @param source the source object to convert; 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
+ */
+ protected abstract T convert(S source) throws ConversionException;
+
+}
diff --git a/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/StringToBooleanConverter.java b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/StringToBooleanConverter.java
index 4bfefc7b48b1..57c0834c0624 100644
--- a/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/StringToBooleanConverter.java
+++ b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/StringToBooleanConverter.java
@@ -12,15 +12,15 @@
import org.junit.platform.commons.util.Preconditions;
-class StringToBooleanConverter implements StringToObjectConverter {
+class StringToBooleanConverter extends StringToWrapperTypeConverter {
@Override
- public boolean canConvertTo(Class> targetType) {
+ boolean canConvert(Class> targetType) {
return targetType == Boolean.class;
}
@Override
- public Object convert(String source, Class> targetType) {
+ Boolean convert(String source, Class> targetType) throws ConversionException {
boolean isTrue = "true".equalsIgnoreCase(source);
Preconditions.condition(isTrue || "false".equalsIgnoreCase(source),
() -> "String must be 'true' or 'false' (ignoring case): " + source);
diff --git a/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/StringToCharacterConverter.java b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/StringToCharacterConverter.java
index 0f5729a228fc..255e311534b3 100644
--- a/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/StringToCharacterConverter.java
+++ b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/StringToCharacterConverter.java
@@ -12,15 +12,15 @@
import org.junit.platform.commons.util.Preconditions;
-class StringToCharacterConverter implements StringToObjectConverter {
+class StringToCharacterConverter extends StringToWrapperTypeConverter {
@Override
- public boolean canConvertTo(Class> targetType) {
+ boolean canConvert(Class> targetType) {
return targetType == Character.class;
}
@Override
- public Object convert(String source, Class> targetType) {
+ Character convert(String source, Class> targetType) throws ConversionException {
Preconditions.condition(source.length() == 1, () -> "String must have length of 1: " + source);
return source.charAt(0);
}
diff --git a/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/StringToClassConverter.java b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/StringToClassConverter.java
index a2d5cbb9322e..23b81a8211e3 100644
--- a/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/StringToClassConverter.java
+++ b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/StringToClassConverter.java
@@ -12,24 +12,21 @@
import org.jspecify.annotations.Nullable;
import org.junit.platform.commons.support.ReflectionSupport;
+import org.junit.platform.commons.util.Preconditions;
-class StringToClassConverter implements StringToObjectConverter {
+class StringToClassConverter implements Converter> {
@Override
- public boolean canConvertTo(Class> targetType) {
- return targetType == Class.class;
+ public boolean canConvert(ConversionContext context) {
+ return !context.sourceType().equals(TypeDescriptor.NONE) && context.targetType().getType() == Class.class;
}
@Override
- public Object convert(String source, Class> targetType) throws Exception {
- throw new UnsupportedOperationException("Invoke convert(String, Class>, ClassLoader) instead");
- }
-
- @Override
- public @Nullable Object convert(String className, Class> targetType, ClassLoader classLoader) throws Exception {
+ public Class> convert(@Nullable String className, ConversionContext context) {
+ Preconditions.notNull(className, "className cannot be null");
// @formatter:off
- return ReflectionSupport.tryToLoadClass(className, classLoader)
- .getOrThrow(cause -> new ConversionException(
+ return ReflectionSupport.tryToLoadClass(className, context.classLoader())
+ .getNonNullOrThrow(cause -> new ConversionException(
"Failed to convert String \"" + className + "\" to type java.lang.Class", cause));
// @formatter:on
}
diff --git a/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/StringToCommonJavaTypesConverter.java b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/StringToCommonJavaTypesConverter.java
index 36f1eee73ec3..555181e35c05 100644
--- a/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/StringToCommonJavaTypesConverter.java
+++ b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/StringToCommonJavaTypesConverter.java
@@ -25,7 +25,7 @@
import org.junit.platform.commons.util.Preconditions;
-class StringToCommonJavaTypesConverter implements StringToObjectConverter {
+class StringToCommonJavaTypesConverter extends StringToTargetTypeConverter