Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit a7a8aa3

Browse files
committedDec 23, 2024·
Introduce ConversionService in junit-platform-commons
1 parent efc375d commit a7a8aa3

File tree

8 files changed

+308
-63
lines changed

8 files changed

+308
-63
lines changed
 

‎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
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
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+
*
19+
*
20+
* @since 1.12
21+
*/
22+
@API(status = EXPERIMENTAL, since = "1.12")
23+
public interface ConversionService {
24+
25+
boolean canConvert(Object source, Class<?> targetType, ClassLoader classLoader);
26+
27+
Object convert(Object source, Class<?> targetType, ClassLoader classLoader) throws ConversionException;
28+
29+
}

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

+37-48
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;
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;
1915
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.junit.platform.commons.util.ClassLoaderUtils;
@@ -30,16 +29,7 @@
3029
@API(status = EXPERIMENTAL, since = "1.11")
3130
public final class ConversionSupport {
3231

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-
));
32+
private static final ConversionService DEFAULT_CONVERSION_SERVICE = new DefaultConversionService();
4333

4434
private ConversionSupport() {
4535
/* no-op */
@@ -82,7 +72,7 @@ private ConversionSupport() {
8272
* accepts a String. Use the constructor if present.</li>
8373
* </ol>
8474
*
85-
* <p>If multiple suitable factory methods are discovered they will be ignored.
75+
* <p>If multiple suitable factory methods are discovered, they will be ignored.
8676
* If neither a single factory method nor a single constructor is found, the
8777
* convention-based conversion strategy will not apply.
8878
*
@@ -97,48 +87,47 @@ private ConversionSupport() {
9787
* type is a reference type
9888
*
9989
* @since 1.11
90+
* @deprecated Use {@link #convert(Object, Class, ClassLoader)} instead.
10091
*/
10192
@SuppressWarnings("unchecked")
93+
@Deprecated
10294
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-
}
95+
return (T) DEFAULT_CONVERSION_SERVICE.convert(source, targetType, getClassLoader(classLoader));
96+
}
11097

111-
if (String.class.equals(targetType)) {
112-
return (T) source;
113-
}
98+
/**
99+
*
100+
*
101+
* @param source
102+
* @param targetType
103+
* @param classLoader
104+
* @param <T>
105+
* @return
106+
*
107+
* @since 1.12
108+
*/
109+
@API(status = EXPERIMENTAL, since = "1.12")
110+
@SuppressWarnings("unchecked")
111+
public static <T> T convert(Object source, Class<T> targetType, ClassLoader classLoader) {
112+
ClassLoader classLoaderToUse = getClassLoader(classLoader);
113+
ServiceLoader<ConversionService> serviceLoader = ServiceLoader.load(ConversionService.class, classLoaderToUse);
114+
115+
Optional<ConversionService> conversionServices = Stream.concat(
116+
StreamSupport.stream(serviceLoader.spliterator(), false), //
117+
Stream.of(DEFAULT_CONVERSION_SERVICE)) //
118+
.filter(candidate -> candidate.canConvert(source, targetType, classLoader)) //
119+
.findFirst();
114120

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-
}
121+
if (conversionServices.isPresent()) {
122+
return (T) conversionServices.get().convert(source, targetType, classLoaderToUse);
133123
}
134124

135-
throw new ConversionException(
136-
"No built-in converter for source type java.lang.String and target type " + targetType.getTypeName());
125+
throw new ConversionException("No built-in converter for source type " + source.getClass().getTypeName()
126+
+ " and target type " + targetType.getTypeName());
137127
}
138128

139-
private static Class<?> toWrapperType(Class<?> targetType) {
140-
Class<?> wrapperType = getWrapperType(targetType);
141-
return wrapperType != null ? wrapperType : targetType;
129+
private static ClassLoader getClassLoader(ClassLoader classLoader) {
130+
return classLoader != null ? classLoader : ClassLoaderUtils.getDefaultClassLoader();
142131
}
143132

144133
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
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 java.util.Arrays.asList;
14+
import static java.util.Collections.unmodifiableList;
15+
import static org.junit.platform.commons.util.ReflectionUtils.getWrapperType;
16+
17+
import java.io.File;
18+
import java.math.BigDecimal;
19+
import java.math.BigInteger;
20+
import java.net.URI;
21+
import java.net.URL;
22+
import java.util.Currency;
23+
import java.util.List;
24+
import java.util.Locale;
25+
import java.util.Optional;
26+
import java.util.UUID;
27+
28+
import org.junit.platform.commons.util.ClassLoaderUtils;
29+
30+
/**
31+
* {@code DefaultConversionService} is the default implementation of the
32+
* {@link ConversionService} API.
33+
*
34+
* <p>The {@code DefaultConversionService} is able to convert from strings to a
35+
* number of primitive types and their corresponding wrapper types (Byte, Short,
36+
* Integer, Long, Float, and Double), date and time types from the
37+
* {@code java.time} package, and some additional common Java types such as
38+
* {@link File}, {@link BigDecimal}, {@link BigInteger}, {@link Currency},
39+
* {@link Locale}, {@link URI}, {@link URL}, {@link UUID}, etc.
40+
*
41+
* <p>If the source and target types are identical, the source object will not
42+
* be modified.
43+
*
44+
* @since 1.12
45+
*/
46+
class DefaultConversionService implements ConversionService {
47+
48+
private static final List<StringToObjectConverter> stringToObjectConverters = unmodifiableList(asList( //
49+
new StringToBooleanConverter(), //
50+
new StringToCharacterConverter(), //
51+
new StringToNumberConverter(), //
52+
new StringToClassConverter(), //
53+
new StringToEnumConverter(), //
54+
new StringToJavaTimeConverter(), //
55+
new StringToCommonJavaTypesConverter(), //
56+
new FallbackStringToObjectConverter() //
57+
));
58+
59+
@Override
60+
public boolean canConvert(Object source, Class<?> targetType, ClassLoader classLoader) {
61+
return source instanceof String;
62+
}
63+
64+
/**
65+
* Convert the supplied source {@code String} into an instance of the specified
66+
* target type.
67+
*
68+
* <p>If the target type is {@code String}, the source {@code String} will not
69+
* be modified.
70+
*
71+
* <p>Some forms of conversion require a {@link ClassLoader}. If none is
72+
* provided, the {@linkplain ClassLoaderUtils#getDefaultClassLoader() default
73+
* ClassLoader} will be used.
74+
*
75+
* <p>This method is able to convert strings into primitive types and their
76+
* corresponding wrapper types ({@link Boolean}, {@link Character}, {@link Byte},
77+
* {@link Short}, {@link Integer}, {@link Long}, {@link Float}, and
78+
* {@link Double}), enum constants, date and time types from the
79+
* {@code java.time} package, as well as common Java types such as {@link Class},
80+
* {@link java.io.File}, {@link java.nio.file.Path}, {@link java.nio.charset.Charset},
81+
* {@link java.math.BigDecimal}, {@link java.math.BigInteger},
82+
* {@link java.util.Currency}, {@link java.util.Locale}, {@link java.util.UUID},
83+
* {@link java.net.URI}, and {@link java.net.URL}.
84+
*
85+
* <p>If the target type is not covered by any of the above, a convention-based
86+
* conversion strategy will be used to convert the source {@code String} into the
87+
* given target type by invoking a static factory method or factory constructor
88+
* defined in the target type. The search algorithm used in this strategy is
89+
* outlined below.
90+
*
91+
* <h4>Search Algorithm</h4>
92+
*
93+
* <ol>
94+
* <li>Search for a single, non-private static factory method in the target
95+
* type that converts from a String to the target type. Use the factory method
96+
* if present.</li>
97+
* <li>Search for a single, non-private constructor in the target type that
98+
* accepts a String. Use the constructor if present.</li>
99+
* </ol>
100+
*
101+
* <p>If multiple suitable factory methods are discovered, they will be ignored.
102+
* If neither a single factory method nor a single constructor is found, the
103+
* convention-based conversion strategy will not apply.
104+
*
105+
* @param source the source {@code String} to convert; may be {@code null}
106+
* but only if the target type is a reference type
107+
* @param targetType the target type the source should be converted into;
108+
* never {@code null}
109+
* @param classLoader the {@code ClassLoader} to use; never {@code null}
110+
* @return the converted object; may be {@code null} but only if the target
111+
* type is a reference type
112+
*/
113+
@Override
114+
public Object convert(Object source, Class<?> targetType, ClassLoader classLoader) {
115+
if (source == null) {
116+
if (targetType.isPrimitive()) {
117+
throw new ConversionException(
118+
"Cannot convert null to primitive value of type " + targetType.getTypeName());
119+
}
120+
return null;
121+
}
122+
123+
if (String.class.equals(targetType)) {
124+
return source;
125+
}
126+
127+
// FIXME move/copy next three lines to canConvert?
128+
Class<?> targetTypeToUse = toWrapperType(targetType);
129+
Optional<StringToObjectConverter> converter = stringToObjectConverters.stream().filter(
130+
candidate -> candidate.canConvertTo(targetTypeToUse)).findFirst();
131+
if (converter.isPresent()) {
132+
try {
133+
return converter.get().convert((String) source, targetTypeToUse, classLoader);
134+
}
135+
catch (Exception ex) {
136+
if (ex instanceof ConversionException) {
137+
// simply rethrow it
138+
throw (ConversionException) ex;
139+
}
140+
// else
141+
throw new ConversionException(
142+
String.format("Failed to convert String \"%s\" to type %s", source, targetType.getTypeName()), ex);
143+
}
144+
}
145+
146+
throw new ConversionException(
147+
"No built-in converter for source type java.lang.String and target type " + targetType.getTypeName());
148+
}
149+
150+
private static Class<?> toWrapperType(Class<?> targetType) {
151+
Class<?> wrapperType = getWrapperType(targetType);
152+
return wrapperType != null ? wrapperType : targetType;
153+
}
154+
155+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
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+
import org.junit.platform.commons.util.Preconditions;
17+
import org.junit.platform.commons.util.ReflectionUtils;
18+
19+
/**
20+
*
21+
*
22+
* @since 1.12
23+
*/
24+
@API(status = EXPERIMENTAL, since = "1.12")
25+
public abstract class TypedConversionService<S, T> implements ConversionService {
26+
27+
private final Class<S> sourceType;
28+
private final Class<T> targetType;
29+
30+
protected TypedConversionService(Class<S> sourceType, Class<T> targetType) {
31+
this.sourceType = Preconditions.notNull(sourceType, "sourceType must not be null");
32+
this.targetType = Preconditions.notNull(targetType, "targetType must not be null");
33+
}
34+
35+
@Override
36+
public final boolean canConvert(Object source, Class<?> targetType, ClassLoader classLoader) {
37+
return sourceType.isInstance(source) && ReflectionUtils.isAssignableTo(this.targetType, targetType);
38+
}
39+
40+
@Override
41+
public final Object convert(Object source, Class<?> targetType, ClassLoader classLoader) {
42+
return source == null ? convert(null) : convert(this.sourceType.cast(source));
43+
}
44+
45+
protected abstract T convert(S source) throws ConversionException;
46+
47+
}

‎jupiter-tests/src/test/java/org/junit/jupiter/params/converter/DefaultArgumentConverterTests.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -347,7 +347,8 @@ void convertsStringToCurrency() {
347347
@SuppressWarnings("deprecation")
348348
void convertsStringToLocale() {
349349
assertConverts("en", Locale.class, Locale.ENGLISH);
350-
assertConverts("en_us", Locale.class, new Locale(Locale.US.toString()));
350+
assertConverts("en-US", Locale.class, Locale.US); // FIXME revert
351+
assertConverts(null, Locale.class, null); // FIXME remove
351352
}
352353

353354
@Test
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
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.jupiter.params.converter;
12+
13+
import java.util.Locale;
14+
15+
import org.junit.platform.commons.support.conversion.TypedConversionService;
16+
17+
// FIXME delete
18+
public class LocaleConversionService extends TypedConversionService<String, Locale> {
19+
20+
public LocaleConversionService() {
21+
super(String.class, Locale.class);
22+
}
23+
24+
@Override
25+
protected Locale convert(String source) {
26+
return Locale.forLanguageTag(source);
27+
}
28+
29+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
org.junit.jupiter.params.converter.LocaleConversionService

0 commit comments

Comments
 (0)
Please sign in to comment.