Skip to content

Commit 0fdb017

Browse files
committed
Introduce Converter in junit-platform-commons
1 parent 261c2f5 commit 0fdb017

File tree

24 files changed

+624
-208
lines changed

24 files changed

+624
-208
lines changed

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import org.jspecify.annotations.Nullable;
2222
import org.junit.jupiter.api.extension.ExtensionContext;
2323
import org.junit.jupiter.params.converter.DefaultArgumentConverter;
24+
import org.junit.platform.commons.support.conversion.TypeDescriptor;
2425
import org.junit.platform.commons.util.ClassUtils;
2526
import org.junit.platform.commons.util.Preconditions;
2627

@@ -47,7 +48,7 @@ public static DefaultArgumentsAccessor create(ExtensionContext context, int invo
4748

4849
BiFunction<@Nullable Object, Class<?>, @Nullable Object> converter = (source,
4950
targetType) -> new DefaultArgumentConverter(context) //
50-
.convert(source, targetType, classLoader);
51+
.convert(source, TypeDescriptor.forClass(targetType), classLoader);
5152
return new DefaultArgumentsAccessor(converter, invocationIndex, arguments);
5253
}
5354

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

Lines changed: 19 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import org.junit.jupiter.params.support.FieldContext;
3131
import org.junit.platform.commons.support.conversion.ConversionException;
3232
import org.junit.platform.commons.support.conversion.ConversionSupport;
33+
import org.junit.platform.commons.support.conversion.TypeDescriptor;
3334
import org.junit.platform.commons.util.ReflectionUtils;
3435

3536
/**
@@ -43,7 +44,7 @@
4344
* {@link File}, {@link BigDecimal}, {@link BigInteger}, {@link Currency},
4445
* {@link Locale}, {@link URI}, {@link URL}, {@link UUID}, etc.
4546
*
46-
* <p>If the source and target types are identical the source object will not
47+
* <p>If the source and target types are identical, the source object will not
4748
* be modified.
4849
*
4950
* @since 5.0
@@ -82,48 +83,41 @@ public DefaultArgumentConverter(ExtensionContext context) {
8283

8384
@Override
8485
public final @Nullable Object convert(@Nullable Object source, ParameterContext context) {
85-
Class<?> targetType = context.getParameter().getType();
8686
ClassLoader classLoader = getClassLoader(context.getDeclaringExecutable().getDeclaringClass());
87-
return convert(source, targetType, classLoader);
87+
return convert(source, TypeDescriptor.forParameter(context.getParameter()), classLoader);
8888
}
8989

9090
@Override
91-
public final @Nullable Object convert(@Nullable Object source, FieldContext context)
92-
throws ArgumentConversionException {
93-
94-
Class<?> targetType = context.getField().getType();
91+
public final @Nullable Object convert(@NullableObject source, FieldContext context) throws ArgumentConversionException {
9592
ClassLoader classLoader = getClassLoader(context.getField().getDeclaringClass());
96-
return convert(source, targetType, classLoader);
93+
return convert(source, TypeDescriptor.forField(context.getField()), classLoader);
9794
}
9895

99-
public final @Nullable Object convert(@Nullable Object source, Class<?> targetType, ClassLoader classLoader) {
96+
public final @Nullable Object convert(@Nullable Object source, TypeDescriptor targetType, ClassLoader classLoader) {
10097
if (source == null) {
10198
if (targetType.isPrimitive()) {
10299
throw new ArgumentConversionException(
103-
"Cannot convert null to primitive value of type " + targetType.getTypeName());
100+
"Cannot convert null to primitive value of type " + targetType.getType().getTypeName());
104101
}
105102
return null;
106103
}
107104

108-
if (ReflectionUtils.isAssignableTo(source, targetType)) {
105+
if (ReflectionUtils.isAssignableTo(source, targetType.getType())) {
109106
return source;
110107
}
111108

112-
if (source instanceof String string) {
113-
if (targetType == Locale.class && getLocaleConversionFormat() == LocaleConversionFormat.BCP_47) {
114-
return Locale.forLanguageTag(string);
115-
}
116-
117-
try {
118-
return convert(string, targetType, classLoader);
119-
}
120-
catch (ConversionException ex) {
121-
throw new ArgumentConversionException(ex.getMessage(), ex);
122-
}
109+
if (source instanceof String //
110+
&& targetType.getType() == Locale.class //
111+
&& getLocaleConversionFormat() == LocaleConversionFormat.BCP_47) {
112+
return Locale.forLanguageTag((String) source);
123113
}
124114

125-
throw new ArgumentConversionException("No built-in converter for source type %s and target type %s".formatted(
126-
source.getClass().getTypeName(), targetType.getTypeName()));
115+
try {
116+
return delegateConversion(source, targetType, classLoader);
117+
}
118+
catch (ConversionException ex) {
119+
throw new ArgumentConversionException(ex.getMessage(), ex);
120+
}
127121
}
128122

129123
private LocaleConversionFormat getLocaleConversionFormat() {
@@ -132,7 +126,7 @@ private LocaleConversionFormat getLocaleConversionFormat() {
132126
}
133127

134128
@Nullable
135-
Object convert(@Nullable String source, Class<?> targetType, ClassLoader classLoader) {
129+
Object delegateConversion(@Nullable Object source, TypeDescriptor targetType, ClassLoader classLoader) {
136130
return ConversionSupport.convert(source, targetType, classLoader);
137131
}
138132

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,5 +58,6 @@
5858
org.junit.platform.suite.engine,
5959
org.junit.platform.testkit,
6060
org.junit.vintage.engine;
61+
uses org.junit.platform.commons.support.conversion.Converter;
6162
uses org.junit.platform.commons.support.scanning.ClasspathScanner;
6263
}

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

Lines changed: 43 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,12 @@
1010

1111
package org.junit.platform.commons.support.conversion;
1212

13-
import static java.util.Arrays.asList;
14-
import static java.util.Collections.unmodifiableList;
13+
import static org.apiguardian.api.API.Status.DEPRECATED;
1514
import static org.apiguardian.api.API.Status.EXPERIMENTAL;
16-
import static org.junit.platform.commons.util.ReflectionUtils.getWrapperType;
1715

18-
import java.util.List;
19-
import java.util.Optional;
16+
import java.util.ServiceLoader;
17+
import java.util.stream.Stream;
18+
import java.util.stream.StreamSupport;
2019

2120
import org.apiguardian.api.API;
2221
import org.jspecify.annotations.Nullable;
@@ -31,17 +30,6 @@
3130
@API(status = EXPERIMENTAL, since = "1.11")
3231
public final class ConversionSupport {
3332

34-
private static final List<StringToObjectConverter> stringToObjectConverters = unmodifiableList(asList( //
35-
new StringToBooleanConverter(), //
36-
new StringToCharacterConverter(), //
37-
new StringToNumberConverter(), //
38-
new StringToClassConverter(), //
39-
new StringToEnumConverter(), //
40-
new StringToJavaTimeConverter(), //
41-
new StringToCommonJavaTypesConverter(), //
42-
new FallbackStringToObjectConverter() //
43-
));
44-
4533
private ConversionSupport() {
4634
/* no-op */
4735
}
@@ -50,43 +38,6 @@ private ConversionSupport() {
5038
* Convert the supplied source {@code String} into an instance of the specified
5139
* target type.
5240
*
53-
* <p>If the target type is {@code String}, the source {@code String} will not
54-
* be modified.
55-
*
56-
* <p>Some forms of conversion require a {@link ClassLoader}. If none is
57-
* provided, the {@linkplain ClassLoaderUtils#getDefaultClassLoader() default
58-
* ClassLoader} will be used.
59-
*
60-
* <p>This method is able to convert strings into primitive types and their
61-
* corresponding wrapper types ({@link Boolean}, {@link Character}, {@link Byte},
62-
* {@link Short}, {@link Integer}, {@link Long}, {@link Float}, and
63-
* {@link Double}), enum constants, date and time types from the
64-
* {@code java.time} package, as well as common Java types such as {@link Class},
65-
* {@link java.io.File}, {@link java.nio.file.Path}, {@link java.nio.charset.Charset},
66-
* {@link java.math.BigDecimal}, {@link java.math.BigInteger},
67-
* {@link java.util.Currency}, {@link java.util.Locale}, {@link java.util.UUID},
68-
* {@link java.net.URI}, and {@link java.net.URL}.
69-
*
70-
* <p>If the target type is not covered by any of the above, a convention-based
71-
* conversion strategy will be used to convert the source {@code String} into the
72-
* given target type by invoking a static factory method or factory constructor
73-
* defined in the target type. The search algorithm used in this strategy is
74-
* outlined below.
75-
*
76-
* <h4>Search Algorithm</h4>
77-
*
78-
* <ol>
79-
* <li>Search for a single, non-private static factory method in the target
80-
* type that converts from a String to the target type. Use the factory method
81-
* if present.</li>
82-
* <li>Search for a single, non-private constructor in the target type that
83-
* accepts a String. Use the constructor if present.</li>
84-
* </ol>
85-
*
86-
* <p>If multiple suitable factory methods are discovered they will be ignored.
87-
* If neither a single factory method nor a single constructor is found, the
88-
* convention-based conversion strategy will not apply.
89-
*
9041
* @param source the source {@code String} to convert; may be {@code null}
9142
* but only if the target type is a reference type
9243
* @param targetType the target type the source should be converted into;
@@ -98,49 +49,51 @@ private ConversionSupport() {
9849
* type is a reference type
9950
*
10051
* @since 1.11
52+
* @see DefaultConverter
53+
* @deprecated Use {@link #convert(Object, TypeDescriptor, ClassLoader)} instead.
10154
*/
102-
@SuppressWarnings("unchecked")
103-
public static <T> @Nullable T convert(@Nullable String source, Class<T> targetType,
104-
@Nullable ClassLoader classLoader) {
105-
if (source == null) {
106-
if (targetType.isPrimitive()) {
107-
throw new ConversionException(
108-
"Cannot convert null to primitive value of type " + targetType.getTypeName());
109-
}
110-
return null;
111-
}
55+
@Deprecated
56+
@API(status = DEPRECATED, since = "5.13")
57+
public static <T> T convert(String source, Class<T> targetType, ClassLoader classLoader) {
58+
return convert(source, TypeDescriptor.forClass(targetType), getClassLoader(classLoader));
59+
}
11260

113-
if (String.class.equals(targetType)) {
114-
return (T) source;
115-
}
61+
/**
62+
* Convert the supplied source object into an instance of the specified
63+
* target type.
64+
*
65+
* @param source the source object to convert; may be {@code null}
66+
* but only if the target type is a reference type
67+
* @param targetType the target type the source should be converted into;
68+
* never {@code null}
69+
* @param classLoader the {@code ClassLoader} to use; may be {@code null} to
70+
* use the default {@code ClassLoader}
71+
* @param <T> the type of the target
72+
* @return the converted object; may be {@code null} but only if the target
73+
* type is a reference type
74+
*
75+
* @since 1.13
76+
*/
77+
@API(status = EXPERIMENTAL, since = "1.13")
78+
@SuppressWarnings("unchecked")
79+
public static <T> @Nullable T convert(@Nullable Object source, TypeDescriptor targetType, @Nullable ClassLoader classLoader) {
80+
TypeDescriptor sourceType = TypeDescriptor.forInstance(source);
81+
ClassLoader classLoaderToUse = getClassLoader(classLoader);
82+
ServiceLoader<Converter> serviceLoader = ServiceLoader.load(Converter.class, classLoaderToUse);
11683

117-
Class<?> targetTypeToUse = toWrapperType(targetType);
118-
Optional<StringToObjectConverter> converter = stringToObjectConverters.stream().filter(
119-
candidate -> candidate.canConvertTo(targetTypeToUse)).findFirst();
120-
if (converter.isPresent()) {
121-
try {
122-
ClassLoader classLoaderToUse = classLoader != null ? classLoader
123-
: ClassLoaderUtils.getDefaultClassLoader();
124-
return (T) converter.get().convert(source, targetTypeToUse, classLoaderToUse);
125-
}
126-
catch (Exception ex) {
127-
if (ex instanceof ConversionException conversionException) {
128-
// simply rethrow it
129-
throw conversionException;
130-
}
131-
// else
132-
throw new ConversionException(
133-
"Failed to convert String \"%s\" to type %s".formatted(source, targetType.getTypeName()), ex);
134-
}
135-
}
84+
Converter converter = Stream.concat( //
85+
StreamSupport.stream(serviceLoader.spliterator(), false), //
86+
Stream.of(DefaultConverter.INSTANCE)) //
87+
.filter(candidate -> candidate.canConvert(source, sourceType, targetType)) //
88+
.findFirst() //
89+
.orElseThrow(() -> new ConversionException("No registered or built-in converter for source '" + source
90+
+ "' and target type " + targetType.getType().getTypeName()));
13691

137-
throw new ConversionException(
138-
"No built-in converter for source type java.lang.String and target type " + targetType.getTypeName());
92+
return (T) converter.convert(source, sourceType, targetType, classLoaderToUse);
13993
}
14094

141-
private static Class<?> toWrapperType(Class<?> targetType) {
142-
Class<?> wrapperType = getWrapperType(targetType);
143-
return wrapperType != null ? wrapperType : targetType;
95+
private static ClassLoader getClassLoader(ClassLoader classLoader) {
96+
return classLoader != null ? classLoader : ClassLoaderUtils.getDefaultClassLoader();
14497
}
14598

14699
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
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+
15+
import org.apiguardian.api.API;
16+
17+
/**
18+
* {@code Converter} is an abstraction that allows an input object to
19+
* be converted to an instance of a different class.
20+
*
21+
* <p>Implementations are loaded via the {@link java.util.ServiceLoader} and must
22+
* follow the service provider requirements. They should not make any assumptions
23+
* regarding when they are instantiated or how often they are called. Since
24+
* instances may potentially be cached and called from different threads, they
25+
* should be thread-safe.
26+
*
27+
* <p>Extend {@link TypedConverter} if your implementation always converts
28+
* from a given source type into a given target type and does not need access to
29+
* the {@link ClassLoader} to perform the conversion.
30+
*
31+
* @since 1.13
32+
* @see ConversionSupport
33+
* @see TypedConverter
34+
*/
35+
@API(status = EXPERIMENTAL, since = "1.13")
36+
public interface Converter {
37+
38+
/**
39+
* Determine if the supplied source object can be converted into an instance
40+
* of the specified target type.
41+
*
42+
* @param source the source object to convert; may be {@code null} but only
43+
* if the target type is a reference type
44+
* @param sourceType the descriptor of the source type; never {@code null}
45+
* @param targetType the descriptor of the type the source should be converted into;
46+
* never {@code null}
47+
* @return {@code true} if the supplied source can be converted
48+
*/
49+
boolean canConvert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType);
50+
51+
/**
52+
* Convert the supplied source object into an instance of the specified
53+
* target type.
54+
* <p>This method will only be invoked if {@link #canConvert(Object, TypeDescriptor, TypeDescriptor)}
55+
* returned {@code true} for the same target type.
56+
*
57+
* @param source the source object to convert; may be {@code null} but only
58+
* if the target type is a reference type
59+
* @param sourceType the descriptor of the source type; never {@code null}
60+
* @param targetType the descriptor of the type the source should be converted into;
61+
* never {@code null}
62+
* @param classLoader the {@code ClassLoader} to use; never {@code null}
63+
* @return the converted object; may be {@code null} but only if the target
64+
* type is a reference type
65+
* @throws ConversionException if an error occurs during the conversion
66+
*/
67+
Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType, ClassLoader classLoader)
68+
throws ConversionException;
69+
70+
}

0 commit comments

Comments
 (0)