Skip to content

Commit e362540

Browse files
committed
Introduce Converter in junit-platform-commons
1 parent 773b9b7 commit e362540

27 files changed

+758
-216
lines changed

junit-jupiter-params/src/main/java/org/junit/jupiter/params/aggregator/DefaultArgumentsAccessor.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import org.apiguardian.api.API;
2121
import org.jspecify.annotations.Nullable;
2222
import org.junit.jupiter.params.converter.DefaultArgumentConverter;
23+
import org.junit.platform.commons.support.conversion.TypeDescriptor;
2324
import org.junit.platform.commons.util.ClassUtils;
2425
import org.junit.platform.commons.util.Preconditions;
2526

@@ -45,7 +46,8 @@ public static DefaultArgumentsAccessor create(int invocationIndex, ClassLoader c
4546
Preconditions.notNull(classLoader, "ClassLoader must not be null");
4647

4748
BiFunction<@Nullable Object, Class<?>, @Nullable Object> converter = (source,
48-
targetType) -> DefaultArgumentConverter.INSTANCE.convert(source, targetType, classLoader);
49+
targetType) -> DefaultArgumentConverter.INSTANCE.convert(source, TypeDescriptor.forClass(targetType),
50+
classLoader);
4951
return new DefaultArgumentsAccessor(converter, invocationIndex, arguments);
5052
}
5153

junit-jupiter-params/src/main/java/org/junit/jupiter/params/converter/DefaultArgumentConverter.java

Lines changed: 16 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,10 @@
2626
import org.jspecify.annotations.Nullable;
2727
import org.junit.jupiter.api.extension.ParameterContext;
2828
import org.junit.jupiter.params.support.FieldContext;
29+
import org.junit.platform.commons.support.conversion.ConversionContext;
2930
import org.junit.platform.commons.support.conversion.ConversionException;
3031
import org.junit.platform.commons.support.conversion.ConversionSupport;
32+
import org.junit.platform.commons.support.conversion.TypeDescriptor;
3133
import org.junit.platform.commons.util.ReflectionUtils;
3234

3335
/**
@@ -41,7 +43,7 @@
4143
* {@link File}, {@link BigDecimal}, {@link BigInteger}, {@link Currency},
4244
* {@link Locale}, {@link URI}, {@link URL}, {@link UUID}, etc.
4345
*
44-
* <p>If the source and target types are identical the source object will not
46+
* <p>If the source and target types are identical, the source object will not
4547
* be modified.
4648
*
4749
* @since 5.0
@@ -58,49 +60,42 @@ private DefaultArgumentConverter() {
5860

5961
@Override
6062
public final @Nullable Object convert(@Nullable Object source, ParameterContext context) {
61-
Class<?> targetType = context.getParameter().getType();
6263
ClassLoader classLoader = getClassLoader(context.getDeclaringExecutable().getDeclaringClass());
63-
return convert(source, targetType, classLoader);
64+
return convert(source, TypeDescriptor.forParameter(context.getParameter()), classLoader);
6465
}
6566

6667
@Override
6768
public final @Nullable Object convert(@Nullable Object source, FieldContext context)
6869
throws ArgumentConversionException {
69-
70-
Class<?> targetType = context.getField().getType();
7170
ClassLoader classLoader = getClassLoader(context.getField().getDeclaringClass());
72-
return convert(source, targetType, classLoader);
71+
return convert(source, TypeDescriptor.forField(context.getField()), classLoader);
7372
}
7473

75-
public final @Nullable Object convert(@Nullable Object source, Class<?> targetType, ClassLoader classLoader) {
74+
public final @Nullable Object convert(@Nullable Object source, TypeDescriptor targetType, ClassLoader classLoader) {
7675
if (source == null) {
7776
if (targetType.isPrimitive()) {
7877
throw new ArgumentConversionException(
79-
"Cannot convert null to primitive value of type " + targetType.getTypeName());
78+
"Cannot convert null to primitive value of type " + targetType.getType().getTypeName());
8079
}
8180
return null;
8281
}
8382

84-
if (ReflectionUtils.isAssignableTo(source, targetType)) {
83+
if (ReflectionUtils.isAssignableTo(source, targetType.getType())) {
8584
return source;
8685
}
8786

88-
if (source instanceof String string) {
89-
try {
90-
return convert(string, targetType, classLoader);
91-
}
92-
catch (ConversionException ex) {
93-
throw new ArgumentConversionException(ex.getMessage(), ex);
94-
}
87+
try {
88+
ConversionContext context = new ConversionContext(source, targetType, classLoader);
89+
return delegateConversion(source, context);
90+
}
91+
catch (ConversionException ex) {
92+
throw new ArgumentConversionException(ex.getMessage(), ex);
9593
}
96-
97-
throw new ArgumentConversionException("No built-in converter for source type %s and target type %s".formatted(
98-
source.getClass().getTypeName(), targetType.getTypeName()));
9994
}
10095

10196
@Nullable
102-
Object convert(@Nullable String source, Class<?> targetType, ClassLoader classLoader) {
103-
return ConversionSupport.convert(source, targetType, classLoader);
97+
Object delegateConversion(@Nullable Object source, ConversionContext context) {
98+
return ConversionSupport.convert(source, context);
10499
}
105100

106101
}

junit-platform-commons/src/main/java/module-info.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,5 +56,6 @@
5656
org.junit.platform.suite.engine,
5757
org.junit.platform.testkit,
5858
org.junit.vintage.engine;
59+
uses org.junit.platform.commons.support.conversion.Converter;
5960
uses org.junit.platform.commons.support.scanning.ClasspathScanner;
6061
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
* Copyright 2015-2025 the original author or authors.
3+
*
4+
* All rights reserved. This program and the accompanying materials are
5+
* made available under the terms of the Eclipse Public License v2.0 which
6+
* accompanies this distribution and is available at
7+
*
8+
* https://www.eclipse.org/legal/epl-v20.html
9+
*/
10+
11+
package org.junit.platform.commons.support.conversion;
12+
13+
import static org.apiguardian.api.API.Status.EXPERIMENTAL;
14+
import static org.junit.platform.commons.util.ClassLoaderUtils.getDefaultClassLoader;
15+
16+
import org.apiguardian.api.API;
17+
import org.jspecify.annotations.Nullable;
18+
19+
/**
20+
* {@code ConversionContext} encapsulates the <em>context</em> in which the
21+
* current conversion is being executed.
22+
*
23+
* <p>{@link Converter Converters} are provided an instance of
24+
* {@code ConversionContext} to perform their work.
25+
*
26+
* @param sourceType the descriptor of the source type
27+
* @param targetType the descriptor of the type the source should be converted into
28+
* @param classLoader the {@code ClassLoader} to use
29+
*
30+
* @since 6.0
31+
* @see Converter
32+
*/
33+
@API(status = EXPERIMENTAL, since = "6.0")
34+
public record ConversionContext(TypeDescriptor sourceType, TypeDescriptor targetType, ClassLoader classLoader) {
35+
36+
/**
37+
* Create a new {@code ConversionContext}, expecting an instance of the
38+
* source instead of its type descriptor.
39+
*
40+
* @param source the source instance; may be {@code null}
41+
* @param targetType the descriptor of the type the source should be converted into
42+
* @param classLoader the {@code ClassLoader} to use; may be {@code null} to
43+
* use the default {@code ClassLoader}
44+
*/
45+
public ConversionContext(@Nullable Object source, TypeDescriptor targetType, @Nullable ClassLoader classLoader) {
46+
this(TypeDescriptor.forInstance(source), targetType,
47+
classLoader != null ? classLoader : getDefaultClassLoader());
48+
}
49+
50+
}

junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/ConversionException.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import java.io.Serial;
1616

1717
import org.apiguardian.api.API;
18+
import org.jspecify.annotations.Nullable;
1819
import org.junit.platform.commons.JUnitException;
1920

2021
/**
@@ -33,7 +34,7 @@ public ConversionException(String message) {
3334
super(message);
3435
}
3536

36-
public ConversionException(String message, Throwable cause) {
37+
public ConversionException(String message, @Nullable Throwable cause) {
3738
super(message, cause);
3839
}
3940

junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/ConversionSupport.java

Lines changed: 37 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,16 @@
1010

1111
package 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;
1315
import 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

1921
import org.apiguardian.api.API;
2022
import org.jspecify.annotations.Nullable;
21-
import org.junit.platform.commons.util.ClassLoaderUtils;
2223

2324
/**
2425
* {@code ConversionSupport} provides static utility methods for converting a
@@ -29,17 +30,6 @@
2930
@API(status = MAINTAINED, since = "1.13.3")
3031
public 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

Comments
 (0)