Skip to content

Commit

Permalink
Introduce ConversionService in junit-platform-commons
Browse files Browse the repository at this point in the history
  • Loading branch information
scordio committed Dec 31, 2024
1 parent 529da15 commit 77afea6
Show file tree
Hide file tree
Showing 8 changed files with 308 additions and 63 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
* {@link File}, {@link BigDecimal}, {@link BigInteger}, {@link Currency},
* {@link Locale}, {@link URI}, {@link URL}, {@link UUID}, etc.
*
* <p>If the source and target types are identical the source object will not
* <p>If the source and target types are identical, the source object will not
* be modified.
*
* @since 5.0
Expand Down Expand Up @@ -74,20 +74,14 @@ public final Object convert(Object source, Class<?> targetType, ParameterContext
return source;
}

if (source instanceof String) {
Class<?> declaringClass = context.getDeclaringExecutable().getDeclaringClass();
ClassLoader classLoader = ClassLoaderUtils.getClassLoader(declaringClass);
try {
return ConversionSupport.convert((String) source, targetType, classLoader);
}
catch (ConversionException ex) {
throw new ArgumentConversionException(ex.getMessage(), ex);
}
Class<?> declaringClass = context.getDeclaringExecutable().getDeclaringClass();
ClassLoader classLoader = ClassLoaderUtils.getClassLoader(declaringClass);
try {
return ConversionSupport.convert(source, targetType, classLoader);
}
catch (ConversionException ex) {
throw new ArgumentConversionException(ex.getMessage(), ex);
}

throw new ArgumentConversionException(
String.format("No built-in converter for source type %s and target type %s",
source.getClass().getTypeName(), targetType.getTypeName()));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* Copyright 2015-2024 the original author or authors.
*
* All rights reserved. This program and the accompanying materials are
* made available under the terms of the Eclipse Public License v2.0 which
* accompanies this distribution and is available at
*
* https://www.eclipse.org/legal/epl-v20.html
*/

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

import static org.apiguardian.api.API.Status.EXPERIMENTAL;

import org.apiguardian.api.API;

/**
*
*
* @since 1.12
*/
@API(status = EXPERIMENTAL, since = "1.12")
public interface ConversionService {

boolean canConvert(Object source, Class<?> targetType, ClassLoader classLoader);

Object convert(Object source, Class<?> targetType, ClassLoader classLoader) throws ConversionException;

}
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,12 @@

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

import static java.util.Arrays.asList;
import static java.util.Collections.unmodifiableList;
import static org.apiguardian.api.API.Status.EXPERIMENTAL;
import static org.junit.platform.commons.util.ReflectionUtils.getWrapperType;

import java.util.List;
import java.util.Optional;
import java.util.ServiceLoader;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

import org.apiguardian.api.API;
import org.junit.platform.commons.util.ClassLoaderUtils;
Expand All @@ -30,16 +29,7 @@
@API(status = EXPERIMENTAL, since = "1.11")
public final class ConversionSupport {

private static final List<StringToObjectConverter> stringToObjectConverters = unmodifiableList(asList( //
new StringToBooleanConverter(), //
new StringToCharacterConverter(), //
new StringToNumberConverter(), //
new StringToClassConverter(), //
new StringToEnumConverter(), //
new StringToJavaTimeConverter(), //
new StringToCommonJavaTypesConverter(), //
new FallbackStringToObjectConverter() //
));
private static final ConversionService DEFAULT_CONVERSION_SERVICE = new DefaultConversionService();

private ConversionSupport() {
/* no-op */
Expand Down Expand Up @@ -82,7 +72,7 @@ private ConversionSupport() {
* accepts a String. Use the constructor if present.</li>
* </ol>
*
* <p>If multiple suitable factory methods are discovered they will be ignored.
* <p>If multiple suitable factory methods are discovered, they will be ignored.
* If neither a single factory method nor a single constructor is found, the
* convention-based conversion strategy will not apply.
*
Expand All @@ -97,48 +87,47 @@ private ConversionSupport() {
* type is a reference type
*
* @since 1.11
* @deprecated Use {@link #convert(Object, Class, ClassLoader)} instead.
*/
@SuppressWarnings("unchecked")
@Deprecated
public static <T> T convert(String source, Class<T> targetType, ClassLoader classLoader) {
if (source == null) {
if (targetType.isPrimitive()) {
throw new ConversionException(
"Cannot convert null to primitive value of type " + targetType.getTypeName());
}
return null;
}
return (T) DEFAULT_CONVERSION_SERVICE.convert(source, targetType, getClassLoader(classLoader));
}

if (String.class.equals(targetType)) {
return (T) source;
}
/**
*
*
* @param source
* @param targetType
* @param classLoader
* @param <T>
* @return
*
* @since 1.12
*/
@API(status = EXPERIMENTAL, since = "1.12")
@SuppressWarnings("unchecked")
public static <T> T convert(Object source, Class<T> targetType, ClassLoader classLoader) {
ClassLoader classLoaderToUse = getClassLoader(classLoader);
ServiceLoader<ConversionService> serviceLoader = ServiceLoader.load(ConversionService.class, classLoaderToUse);

Optional<ConversionService> conversionServices = Stream.concat(
StreamSupport.stream(serviceLoader.spliterator(), false), //
Stream.of(DEFAULT_CONVERSION_SERVICE)) //
.filter(candidate -> candidate.canConvert(source, targetType, classLoader)) //
.findFirst();

Class<?> targetTypeToUse = toWrapperType(targetType);
Optional<StringToObjectConverter> converter = stringToObjectConverters.stream().filter(
candidate -> candidate.canConvertTo(targetTypeToUse)).findFirst();
if (converter.isPresent()) {
try {
ClassLoader classLoaderToUse = classLoader != null ? classLoader
: ClassLoaderUtils.getDefaultClassLoader();
return (T) converter.get().convert(source, targetTypeToUse, classLoaderToUse);
}
catch (Exception ex) {
if (ex instanceof ConversionException) {
// simply rethrow it
throw (ConversionException) ex;
}
// else
throw new ConversionException(
String.format("Failed to convert String \"%s\" to type %s", source, targetType.getTypeName()), ex);
}
if (conversionServices.isPresent()) {
return (T) conversionServices.get().convert(source, targetType, classLoaderToUse);
}

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

private static Class<?> toWrapperType(Class<?> targetType) {
Class<?> wrapperType = getWrapperType(targetType);
return wrapperType != null ? wrapperType : targetType;
private static ClassLoader getClassLoader(ClassLoader classLoader) {
return classLoader != null ? classLoader : ClassLoaderUtils.getDefaultClassLoader();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
/*
* Copyright 2015-2024 the original author or authors.
*
* All rights reserved. This program and the accompanying materials are
* made available under the terms of the Eclipse Public License v2.0 which
* accompanies this distribution and is available at
*
* https://www.eclipse.org/legal/epl-v20.html
*/

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

import static java.util.Arrays.asList;
import static java.util.Collections.unmodifiableList;
import static org.junit.platform.commons.util.ReflectionUtils.getWrapperType;

import java.io.File;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.net.URI;
import java.net.URL;
import java.util.Currency;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.UUID;

import org.junit.platform.commons.util.ClassLoaderUtils;

/**
* {@code DefaultConversionService} is the default implementation of the
* {@link ConversionService} API.
*
* <p>The {@code DefaultConversionService} is able to convert from strings to a
* number of primitive types and their corresponding wrapper types (Byte, Short,
* Integer, Long, Float, and Double), date and time types from the
* {@code java.time} package, and some additional common Java types such as
* {@link File}, {@link BigDecimal}, {@link BigInteger}, {@link Currency},
* {@link Locale}, {@link URI}, {@link URL}, {@link UUID}, etc.
*
* <p>If the source and target types are identical, the source object will not
* be modified.
*
* @since 1.12
*/
class DefaultConversionService implements ConversionService {

private static final List<StringToObjectConverter> stringToObjectConverters = unmodifiableList(asList( //
new StringToBooleanConverter(), //
new StringToCharacterConverter(), //
new StringToNumberConverter(), //
new StringToClassConverter(), //
new StringToEnumConverter(), //
new StringToJavaTimeConverter(), //
new StringToCommonJavaTypesConverter(), //
new FallbackStringToObjectConverter() //
));

@Override
public boolean canConvert(Object source, Class<?> targetType, ClassLoader classLoader) {
return source instanceof String;
}

/**
* Convert the supplied source {@code String} into an instance of the specified
* target type.
*
* <p>If the target type is {@code String}, the source {@code String} will not
* be modified.
*
* <p>Some forms of conversion require a {@link ClassLoader}. If none is
* provided, the {@linkplain ClassLoaderUtils#getDefaultClassLoader() default
* ClassLoader} will be used.
*
* <p>This method is able to convert strings into primitive types and their
* corresponding wrapper types ({@link Boolean}, {@link Character}, {@link Byte},
* {@link Short}, {@link Integer}, {@link Long}, {@link Float}, and
* {@link Double}), enum constants, date and time types from the
* {@code java.time} package, as well as common Java types such as {@link Class},
* {@link java.io.File}, {@link java.nio.file.Path}, {@link java.nio.charset.Charset},
* {@link java.math.BigDecimal}, {@link java.math.BigInteger},
* {@link java.util.Currency}, {@link java.util.Locale}, {@link java.util.UUID},
* {@link java.net.URI}, and {@link java.net.URL}.
*
* <p>If the target type is not covered by any of the above, a convention-based
* conversion strategy will be used to convert the source {@code String} into the
* given target type by invoking a static factory method or factory constructor
* defined in the target type. The search algorithm used in this strategy is
* outlined below.
*
* <h4>Search Algorithm</h4>
*
* <ol>
* <li>Search for a single, non-private static factory method in the target
* type that converts from a String to the target type. Use the factory method
* if present.</li>
* <li>Search for a single, non-private constructor in the target type that
* accepts a String. Use the constructor if present.</li>
* </ol>
*
* <p>If multiple suitable factory methods are discovered, they will be ignored.
* If neither a single factory method nor a single constructor is found, the
* convention-based conversion strategy will not apply.
*
* @param source the source {@code String} to convert; may be {@code null}
* but only if the target type is a reference type
* @param targetType the target type the source should be converted into;
* never {@code null}
* @param classLoader the {@code ClassLoader} to use; never {@code null}
* @return the converted object; may be {@code null} but only if the target
* type is a reference type
*/
@Override
public Object convert(Object source, Class<?> targetType, ClassLoader classLoader) {
if (source == null) {
if (targetType.isPrimitive()) {
throw new ConversionException(
"Cannot convert null to primitive value of type " + targetType.getTypeName());
}
return null;
}

if (String.class.equals(targetType)) {
return source;
}

// FIXME move/copy next three lines to canConvert?
Class<?> targetTypeToUse = toWrapperType(targetType);
Optional<StringToObjectConverter> converter = stringToObjectConverters.stream().filter(
candidate -> candidate.canConvertTo(targetTypeToUse)).findFirst();
if (converter.isPresent()) {
try {
return converter.get().convert((String) source, targetTypeToUse, classLoader);
}
catch (Exception ex) {
if (ex instanceof ConversionException) {
// simply rethrow it
throw (ConversionException) ex;
}
// else
throw new ConversionException(
String.format("Failed to convert String \"%s\" to type %s", source, targetType.getTypeName()), ex);
}
}

throw new ConversionException(
"No built-in converter for source type java.lang.String and target type " + targetType.getTypeName());
}

private static Class<?> toWrapperType(Class<?> targetType) {
Class<?> wrapperType = getWrapperType(targetType);
return wrapperType != null ? wrapperType : targetType;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* Copyright 2015-2024 the original author or authors.
*
* All rights reserved. This program and the accompanying materials are
* made available under the terms of the Eclipse Public License v2.0 which
* accompanies this distribution and is available at
*
* https://www.eclipse.org/legal/epl-v20.html
*/

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

import static org.apiguardian.api.API.Status.EXPERIMENTAL;

import org.apiguardian.api.API;
import org.junit.platform.commons.util.Preconditions;
import org.junit.platform.commons.util.ReflectionUtils;

/**
*
*
* @since 1.12
*/
@API(status = EXPERIMENTAL, since = "1.12")
public abstract class TypedConversionService<S, T> implements ConversionService {

private final Class<S> sourceType;
private final Class<T> targetType;

protected TypedConversionService(Class<S> sourceType, Class<T> targetType) {
this.sourceType = Preconditions.notNull(sourceType, "sourceType must not be null");
this.targetType = Preconditions.notNull(targetType, "targetType must not be null");
}

@Override
public final boolean canConvert(Object source, Class<?> targetType, ClassLoader classLoader) {
return sourceType.isInstance(source) && ReflectionUtils.isAssignableTo(this.targetType, targetType);
}

@Override
public final Object convert(Object source, Class<?> targetType, ClassLoader classLoader) {
return source == null ? convert(null) : convert(this.sourceType.cast(source));
}

protected abstract T convert(S source) throws ConversionException;

}
Loading

0 comments on commit 77afea6

Please sign in to comment.