Skip to content

Commit 7ea91b8

Browse files
committed
Introduce ConversionService in junit-platform-commons
1 parent 529da15 commit 7ea91b8

File tree

10 files changed

+394
-104
lines changed

10 files changed

+394
-104
lines changed

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

-1
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@
4040
* the {@link ParameterContext} to perform the conversion.
4141
*
4242
* @since 5.0
43-
* @see SimpleArgumentConverter
4443
* @see org.junit.jupiter.params.ParameterizedTest
4544
* @see org.junit.jupiter.params.converter.ConvertWith
4645
* @see org.junit.jupiter.params.support.AnnotationConsumer

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

+8-14
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
* {@link File}, {@link BigDecimal}, {@link BigInteger}, {@link Currency},
4040
* {@link Locale}, {@link URI}, {@link URL}, {@link UUID}, etc.
4141
*
42-
* <p>If the source and target types are identical the source object will not
42+
* <p>If the source and target types are identical, the source object will not
4343
* be modified.
4444
*
4545
* @since 5.0
@@ -74,20 +74,14 @@ public final Object convert(Object source, Class<?> targetType, ParameterContext
7474
return source;
7575
}
7676

77-
if (source instanceof String) {
78-
Class<?> declaringClass = context.getDeclaringExecutable().getDeclaringClass();
79-
ClassLoader classLoader = ClassLoaderUtils.getClassLoader(declaringClass);
80-
try {
81-
return ConversionSupport.convert((String) source, targetType, classLoader);
82-
}
83-
catch (ConversionException ex) {
84-
throw new ArgumentConversionException(ex.getMessage(), ex);
85-
}
77+
Class<?> declaringClass = context.getDeclaringExecutable().getDeclaringClass();
78+
ClassLoader classLoader = ClassLoaderUtils.getClassLoader(declaringClass);
79+
try {
80+
return ConversionSupport.convert(source, targetType, classLoader);
81+
}
82+
catch (ConversionException ex) {
83+
throw new ArgumentConversionException(ex.getMessage(), ex);
8684
}
87-
88-
throw new ArgumentConversionException(
89-
String.format("No built-in converter for source type %s and target type %s",
90-
source.getClass().getTypeName(), targetType.getTypeName()));
9185
}
9286

9387
}

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

+3
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ protected TypedArgumentConverter(Class<S> sourceType, Class<T> targetType) {
4646
this.targetType = Preconditions.notNull(targetType, "targetType must not be null");
4747
}
4848

49+
/**
50+
* {@inheritDoc}
51+
*/
4952
@Override
5053
public final Object convert(Object source, ParameterContext context) throws ArgumentConversionException {
5154
if (source == null) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
* Copyright 2015-2024 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 ConversionService} 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 TypedConversionService} 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.12
32+
* @see ConversionSupport
33+
* @see TypedConversionService
34+
*/
35+
@API(status = EXPERIMENTAL, since = "1.12")
36+
public interface ConversionService {
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 targetType the target type the source should be converted into;
45+
* never {@code null}
46+
* @param classLoader the {@code ClassLoader} to use; never {@code null}
47+
* @return {@code true} if the supplied source can be converted
48+
*/
49+
boolean canConvert(Object source, Class<?> targetType, ClassLoader classLoader);
50+
51+
/**
52+
* Convert the supplied source object into an instance of the specified
53+
* target type.
54+
*
55+
* @param source the source object to convert; may be {@code null} but only
56+
* if the target type is a reference type
57+
* @param targetType the target type the source should be converted into;
58+
* never {@code null}
59+
* @param classLoader the {@code ClassLoader} to use; never {@code null}
60+
* @return the converted object; may be {@code null} but only if the target
61+
* type is a reference type
62+
* @throws ConversionException if an error occurs during the conversion
63+
*/
64+
Object convert(Object source, Class<?> targetType, ClassLoader classLoader) throws ConversionException;
65+
66+
}

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

+39-88
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,11 @@
1010

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

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

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

2119
import org.apiguardian.api.API;
2220
import org.junit.platform.commons.util.ClassLoaderUtils;
@@ -30,17 +28,6 @@
3028
@API(status = EXPERIMENTAL, since = "1.11")
3129
public final class ConversionSupport {
3230

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

115-
Class<?> targetTypeToUse = toWrapperType(targetType);
116-
Optional<StringToObjectConverter> converter = stringToObjectConverters.stream().filter(
117-
candidate -> candidate.canConvertTo(targetTypeToUse)).findFirst();
118-
if (converter.isPresent()) {
119-
try {
120-
ClassLoader classLoaderToUse = classLoader != null ? classLoader
121-
: ClassLoaderUtils.getDefaultClassLoader();
122-
return (T) converter.get().convert(source, targetTypeToUse, classLoaderToUse);
123-
}
124-
catch (Exception ex) {
125-
if (ex instanceof ConversionException) {
126-
// simply rethrow it
127-
throw (ConversionException) ex;
128-
}
129-
// else
130-
throw new ConversionException(
131-
String.format("Failed to convert String \"%s\" to type %s", source, targetType.getTypeName()), ex);
132-
}
133-
}
59+
/**
60+
* Convert the supplied source object into an instance of the specified
61+
* target type.
62+
*
63+
* @param source the source object to convert; may be {@code null}
64+
* but only if the target type is a reference type
65+
* @param targetType the target type the source should be converted into;
66+
* never {@code null}
67+
* @param classLoader the {@code ClassLoader} to use; may be {@code null} to
68+
* use the default {@code ClassLoader}
69+
* @param <T> the type of the target
70+
* @return the converted object; may be {@code null} but only if the target
71+
* type is a reference type
72+
*
73+
* @since 1.12
74+
*/
75+
@API(status = EXPERIMENTAL, since = "1.12")
76+
@SuppressWarnings("unchecked")
77+
public static <T> T convert(Object source, Class<T> targetType, ClassLoader classLoader) {
78+
ClassLoader classLoaderToUse = getClassLoader(classLoader);
79+
ServiceLoader<ConversionService> serviceLoader = ServiceLoader.load(ConversionService.class, classLoaderToUse);
13480

135-
throw new ConversionException(
136-
"No built-in converter for source type java.lang.String and target type " + targetType.getTypeName());
81+
return (T) Stream.concat( //
82+
StreamSupport.stream(serviceLoader.spliterator(), false), //
83+
Stream.of(DefaultConversionService.INSTANCE)) //
84+
.filter(candidate -> candidate.canConvert(source, targetType, classLoader)) //
85+
.findFirst() //
86+
.map(candidate -> candidate.convert(source, targetType, classLoaderToUse)) //
87+
.orElseThrow(() -> new ConversionException("No built-in converter for source type "
88+
+ source.getClass().getTypeName() + " and target type " + targetType.getTypeName()));
13789
}
13890

139-
private static Class<?> toWrapperType(Class<?> targetType) {
140-
Class<?> wrapperType = getWrapperType(targetType);
141-
return wrapperType != null ? wrapperType : targetType;
91+
private static ClassLoader getClassLoader(ClassLoader classLoader) {
92+
return classLoader != null ? classLoader : ClassLoaderUtils.getDefaultClassLoader();
14293
}
14394

14495
}

0 commit comments

Comments
 (0)