Skip to content

Commit a7a8aa3

Browse files
committed
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

Lines changed: 8 additions & 14 deletions
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

Lines changed: 37 additions & 48 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;
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+
}

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy