From a6d29515d8ff5c69fc60dda88617a96572c8b484 Mon Sep 17 00:00:00 2001 From: Juliette de Rancourt Date: Tue, 17 Oct 2023 15:21:52 +0200 Subject: [PATCH] Introduce `StringConversionSupport` in `junit-platform-commons` Resolves #3449 --- .../release-notes-5.11.0-M1.adoc | 3 +- .../converter/DefaultArgumentConverter.java | 64 +------ .../DefaultArgumentConverterTests.java | 4 + .../commons/support/ConversionException.java | 37 ++++ .../FallbackStringToObjectConverter.java | 173 ++++++++++++++++++ .../support/StringConversionSupport.java | 106 +++++++++++ .../support/StringToBooleanConverter.java | 30 +++ .../support/StringToCharacterConverter.java | 28 +++ .../support/StringToClassConverter.java | 36 ++++ .../StringToCommonJavaTypesConverter.java | 70 +++++++ .../support/StringToEnumConverter.java | 26 +++ .../support/StringToJavaTimeConverter.java | 65 +++++++ .../support/StringToNumberConverter.java | 50 +++++ .../support/StringToObjectConverter.java | 46 +++++ 14 files changed, 682 insertions(+), 56 deletions(-) create mode 100644 junit-platform-commons/src/main/java/org/junit/platform/commons/support/ConversionException.java create mode 100644 junit-platform-commons/src/main/java/org/junit/platform/commons/support/FallbackStringToObjectConverter.java create mode 100644 junit-platform-commons/src/main/java/org/junit/platform/commons/support/StringConversionSupport.java create mode 100644 junit-platform-commons/src/main/java/org/junit/platform/commons/support/StringToBooleanConverter.java create mode 100644 junit-platform-commons/src/main/java/org/junit/platform/commons/support/StringToCharacterConverter.java create mode 100644 junit-platform-commons/src/main/java/org/junit/platform/commons/support/StringToClassConverter.java create mode 100644 junit-platform-commons/src/main/java/org/junit/platform/commons/support/StringToCommonJavaTypesConverter.java create mode 100644 junit-platform-commons/src/main/java/org/junit/platform/commons/support/StringToEnumConverter.java create mode 100644 junit-platform-commons/src/main/java/org/junit/platform/commons/support/StringToJavaTimeConverter.java create mode 100644 junit-platform-commons/src/main/java/org/junit/platform/commons/support/StringToNumberConverter.java create mode 100644 junit-platform-commons/src/main/java/org/junit/platform/commons/support/StringToObjectConverter.java diff --git a/documentation/src/docs/asciidoc/release-notes/release-notes-5.11.0-M1.adoc b/documentation/src/docs/asciidoc/release-notes/release-notes-5.11.0-M1.adoc index f428b0e5a636..69659b2106a2 100644 --- a/documentation/src/docs/asciidoc/release-notes/release-notes-5.11.0-M1.adoc +++ b/documentation/src/docs/asciidoc/release-notes/release-notes-5.11.0-M1.adoc @@ -23,7 +23,8 @@ JUnit repository on GitHub. ==== New Features and Improvements -* ❓ +* New `StringConversionSupport` in `junit-platform-commons` to expose + internal conversion logic used by Jupiter's `DefaultArgumentConverter` [[release-notes-5.11.0-M1-junit-jupiter]] diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/converter/DefaultArgumentConverter.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/converter/DefaultArgumentConverter.java index fe697c5696e3..c152b558b0bb 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/converter/DefaultArgumentConverter.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/converter/DefaultArgumentConverter.java @@ -10,10 +10,7 @@ package org.junit.jupiter.params.converter; -import static java.util.Arrays.asList; -import static java.util.Collections.unmodifiableList; import static org.apiguardian.api.API.Status.INTERNAL; -import static org.junit.platform.commons.util.ReflectionUtils.getWrapperType; import java.io.File; import java.math.BigDecimal; @@ -21,15 +18,14 @@ 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.apiguardian.api.API; import org.junit.jupiter.api.extension.ParameterContext; +import org.junit.platform.commons.support.ConversionException; +import org.junit.platform.commons.support.StringConversionSupport; import org.junit.platform.commons.util.ClassLoaderUtils; -import org.junit.platform.commons.util.ReflectionUtils; /** * {@code DefaultArgumentConverter} is the default implementation of the @@ -47,23 +43,13 @@ * * @since 5.0 * @see org.junit.jupiter.params.converter.ArgumentConverter + * @see org.junit.platform.commons.support.StringConversionSupport */ @API(status = INTERNAL, since = "5.0") public class DefaultArgumentConverter implements ArgumentConverter { public static final DefaultArgumentConverter INSTANCE = new DefaultArgumentConverter(); - private static final List stringToObjectConverters = unmodifiableList(asList( // - new StringToBooleanConverter(), // - new StringToCharacterConverter(), // - new StringToNumberConverter(), // - new StringToClassConverter(), // - new StringToEnumConverter(), // - new StringToJavaTimeConverter(), // - new StringToCommonJavaTypesConverter(), // - new FallbackStringToObjectConverter() // - )); - private DefaultArgumentConverter() { // nothing to initialize } @@ -75,47 +61,15 @@ public final Object convert(Object source, ParameterContext context) { } public final Object convert(Object source, Class targetType, ParameterContext context) { - if (source == null) { - if (targetType.isPrimitive()) { - throw new ArgumentConversionException( - "Cannot convert null to primitive value of type " + targetType.getTypeName()); - } - return null; - } + Class declaringClass = context.getDeclaringExecutable().getDeclaringClass(); + ClassLoader classLoader = ClassLoaderUtils.getClassLoader(declaringClass); - if (ReflectionUtils.isAssignableTo(source, targetType)) { - return source; + try { + return StringConversionSupport.convert(source, targetType, classLoader); } - - if (source instanceof String) { - Class targetTypeToUse = toWrapperType(targetType); - Optional converter = stringToObjectConverters.stream().filter( - candidate -> candidate.canConvert(targetTypeToUse)).findFirst(); - if (converter.isPresent()) { - Class declaringClass = context.getDeclaringExecutable().getDeclaringClass(); - ClassLoader classLoader = ClassLoaderUtils.getClassLoader(declaringClass); - try { - return converter.get().convert((String) source, targetTypeToUse, classLoader); - } - catch (Exception ex) { - if (ex instanceof ArgumentConversionException) { - // simply rethrow it - throw (ArgumentConversionException) ex; - } - // else - throw new ArgumentConversionException( - "Failed to convert String \"" + source + "\" to type " + targetType.getTypeName(), ex); - } - } + catch (ConversionException e) { + throw new ArgumentConversionException(e.getMessage(), e); } - throw new ArgumentConversionException( - String.format("No built-in converter for source type %s and target type %s", - source.getClass().getTypeName(), targetType.getTypeName())); - } - - private static Class toWrapperType(Class targetType) { - Class wrapperType = getWrapperType(targetType); - return wrapperType != null ? wrapperType : targetType; } } diff --git a/junit-jupiter-params/src/test/java/org/junit/jupiter/params/converter/DefaultArgumentConverterTests.java b/junit-jupiter-params/src/test/java/org/junit/jupiter/params/converter/DefaultArgumentConverterTests.java index f801058f4e59..371c0382b461 100644 --- a/junit-jupiter-params/src/test/java/org/junit/jupiter/params/converter/DefaultArgumentConverterTests.java +++ b/junit-jupiter-params/src/test/java/org/junit/jupiter/params/converter/DefaultArgumentConverterTests.java @@ -157,24 +157,28 @@ void throwsExceptionOnInvalidStringForPrimitiveTypes() { .isThrownBy(() -> convert("ab", char.class)) // .withMessage("Failed to convert String \"ab\" to type char") // .havingCause() // + .havingCause() // .withMessage("String must have length of 1: ab"); assertThatExceptionOfType(ArgumentConversionException.class) // .isThrownBy(() -> convert("tru", boolean.class)) // .withMessage("Failed to convert String \"tru\" to type boolean") // .havingCause() // + .havingCause() // .withMessage("String must be 'true' or 'false' (ignoring case): tru"); assertThatExceptionOfType(ArgumentConversionException.class) // .isThrownBy(() -> convert("null", boolean.class)) // .withMessage("Failed to convert String \"null\" to type boolean") // .havingCause() // + .havingCause() // .withMessage("String must be 'true' or 'false' (ignoring case): null"); assertThatExceptionOfType(ArgumentConversionException.class) // .isThrownBy(() -> convert("NULL", boolean.class)) // .withMessage("Failed to convert String \"NULL\" to type boolean") // .havingCause() // + .havingCause() // .withMessage("String must be 'true' or 'false' (ignoring case): NULL"); } diff --git a/junit-platform-commons/src/main/java/org/junit/platform/commons/support/ConversionException.java b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/ConversionException.java new file mode 100644 index 000000000000..14d914eb4856 --- /dev/null +++ b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/ConversionException.java @@ -0,0 +1,37 @@ +/* + * Copyright 2015-2023 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; + +import static org.apiguardian.api.API.Status.EXPERIMENTAL; + +import org.apiguardian.api.API; +import org.junit.platform.commons.JUnitException; + +/** + * {@code ConversionException} is an exception that can occur when an + * object is converted to another object. + * + * @since 1.11 + */ +@API(status = EXPERIMENTAL, since = "1.11") +public class ConversionException extends JUnitException { + + private static final long serialVersionUID = 1L; + + public ConversionException(String message) { + super(message); + } + + public ConversionException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/junit-platform-commons/src/main/java/org/junit/platform/commons/support/FallbackStringToObjectConverter.java b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/FallbackStringToObjectConverter.java new file mode 100644 index 000000000000..e2ade2548b37 --- /dev/null +++ b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/FallbackStringToObjectConverter.java @@ -0,0 +1,173 @@ +/* + * Copyright 2015-2023 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; + +import static org.junit.platform.commons.util.ReflectionUtils.HierarchyTraversalMode.BOTTOM_UP; +import static org.junit.platform.commons.util.ReflectionUtils.findConstructors; +import static org.junit.platform.commons.util.ReflectionUtils.findMethods; +import static org.junit.platform.commons.util.ReflectionUtils.invokeMethod; +import static org.junit.platform.commons.util.ReflectionUtils.isNotPrivate; +import static org.junit.platform.commons.util.ReflectionUtils.isNotStatic; +import static org.junit.platform.commons.util.ReflectionUtils.newInstance; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Executable; +import java.lang.reflect.Method; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; +import java.util.function.Predicate; + +import org.junit.platform.commons.util.Preconditions; + +/** + * {@code FallbackStringToObjectConverter} is a {@link StringToObjectConverter} + * that provides a fallback conversion strategy for converting from a + * {@link String} to a given target type by invoking a static factory method + * or factory constructor defined in the target type. + * + *

Search Algorithm

+ * + *
    + *
  1. 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.
  2. + *
  3. Search for a single, non-private constructor in the target type that + * accepts a String. Use the constructor if present.
  4. + *
+ * + *

If multiple suitable factory methods are discovered they will be ignored. + * If neither a single factory method nor a single constructor is found, this + * converter acts as a no-op. + * + * @since 1.11 + * @see StringConversionSupport + */ +class FallbackStringToObjectConverter implements StringToObjectConverter { + + /** + * Implementation of the NULL Object Pattern. + */ + private static final Function NULL_EXECUTABLE = source -> source; + + /** + * Cache for factory methods and factory constructors. + * + *

Searches that do not find a factory method or constructor are tracked + * by the presence of a {@link #NULL_EXECUTABLE} object stored in the map. + * This prevents the framework from repeatedly searching for things which + * are already known not to exist. + */ + private static final ConcurrentHashMap, Function> factoryExecutableCache // + = new ConcurrentHashMap<>(64); + + @Override + public boolean canConvert(Class targetType) { + return findFactoryExecutable(targetType) != NULL_EXECUTABLE; + } + + @Override + public Object convert(String source, Class targetType) throws Exception { + Function executable = findFactoryExecutable(targetType); + Preconditions.condition(executable != NULL_EXECUTABLE, + "Illegal state: convert() must not be called if canConvert() returned false"); + + return executable.apply(source); + } + + private static Function findFactoryExecutable(Class targetType) { + return factoryExecutableCache.computeIfAbsent(targetType, type -> { + Method factoryMethod = findFactoryMethod(type); + if (factoryMethod != null) { + return source -> invokeMethod(factoryMethod, null, source); + } + Constructor constructor = findFactoryConstructor(type); + if (constructor != null) { + return source -> newInstance(constructor, source); + } + return NULL_EXECUTABLE; + }); + } + + private static Method findFactoryMethod(Class targetType) { + List factoryMethods = findMethods(targetType, new IsFactoryMethod(targetType), BOTTOM_UP); + if (factoryMethods.size() == 1) { + return factoryMethods.get(0); + } + return null; + } + + private static Constructor findFactoryConstructor(Class targetType) { + List> constructors = findConstructors(targetType, new IsFactoryConstructor(targetType)); + if (constructors.size() == 1) { + return constructors.get(0); + } + return null; + } + + /** + * {@link Predicate} that determines if the {@link Method} supplied to + * {@link #test(Method)} is a non-private static factory method for the + * supplied {@link #targetType}. + */ + static class IsFactoryMethod implements Predicate { + + private final Class targetType; + + IsFactoryMethod(Class targetType) { + this.targetType = targetType; + } + + @Override + public boolean test(Method method) { + // Please do not collapse the following into a single statement. + if (!method.getReturnType().equals(this.targetType)) { + return false; + } + if (isNotStatic(method)) { + return false; + } + return isNotPrivateAndAcceptsSingleStringArgument(method); + } + + } + + /** + * {@link Predicate} that determines if the {@link Constructor} supplied to + * {@link #test(Constructor)} is a non-private factory constructor for the + * supplied {@link #targetType}. + */ + static class IsFactoryConstructor implements Predicate> { + + private final Class targetType; + + IsFactoryConstructor(Class targetType) { + this.targetType = targetType; + } + + @Override + public boolean test(Constructor constructor) { + // Please do not collapse the following into a single statement. + if (!constructor.getDeclaringClass().equals(this.targetType)) { + return false; + } + return isNotPrivateAndAcceptsSingleStringArgument(constructor); + } + + } + + private static boolean isNotPrivateAndAcceptsSingleStringArgument(Executable executable) { + return isNotPrivate(executable) // + && (executable.getParameterCount() == 1) // + && (executable.getParameterTypes()[0] == String.class); + } + +} diff --git a/junit-platform-commons/src/main/java/org/junit/platform/commons/support/StringConversionSupport.java b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/StringConversionSupport.java new file mode 100644 index 000000000000..8727216b923b --- /dev/null +++ b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/StringConversionSupport.java @@ -0,0 +1,106 @@ +/* + * Copyright 2015-2023 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; + +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.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.apiguardian.api.API; +import org.junit.platform.commons.util.ClassLoaderUtils; +import org.junit.platform.commons.util.ReflectionUtils; + +/** + * The {@code DefaultArgumentConverter} 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. + * + *

If the source and target types are identical the source object will not + * be modified. + */ +@API(status = EXPERIMENTAL, since = "1.11") +public final class StringConversionSupport { + + private static final List stringToObjectConverters = unmodifiableList(asList( // + new StringToBooleanConverter(), // + new StringToCharacterConverter(), // + new StringToNumberConverter(), // + new StringToClassConverter(), // + new StringToEnumConverter(), // + new StringToJavaTimeConverter(), // + new StringToCommonJavaTypesConverter(), // + new FallbackStringToObjectConverter() // + )); + + private StringConversionSupport() { + /* no-op */ + } + + @SuppressWarnings("unchecked") + public static T 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 (ReflectionUtils.isAssignableTo(source, targetType)) { + return (T) source; + } + + if (source instanceof String) { + Class targetTypeToUse = toWrapperType(targetType); + Optional converter = stringToObjectConverters.stream().filter( + candidate -> candidate.canConvert(targetTypeToUse)).findFirst(); + if (converter.isPresent()) { + try { + ClassLoader classLoaderToUse = Optional.ofNullable(classLoader) // + .orElseGet(ClassLoaderUtils::getDefaultClassLoader); + return (T) converter.get().convert((String) source, targetTypeToUse, classLoaderToUse); + } + catch (Exception ex) { + if (ex instanceof ConversionException) { + // simply rethrow it + throw (ConversionException) ex; + } + // else + throw new ConversionException( + "Failed to convert String \"" + source + "\" to type " + targetType.getTypeName(), ex); + } + } + } + throw new ConversionException(String.format("No built-in converter for source type %s and target type %s", + source.getClass().getTypeName(), targetType.getTypeName())); + } + + private static Class toWrapperType(Class targetType) { + Class wrapperType = getWrapperType(targetType); + return wrapperType != null ? wrapperType : targetType; + } + +} diff --git a/junit-platform-commons/src/main/java/org/junit/platform/commons/support/StringToBooleanConverter.java b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/StringToBooleanConverter.java new file mode 100644 index 000000000000..83d1aef7bf0c --- /dev/null +++ b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/StringToBooleanConverter.java @@ -0,0 +1,30 @@ +/* + * Copyright 2015-2023 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; + +import org.junit.platform.commons.util.Preconditions; + +class StringToBooleanConverter implements StringToObjectConverter { + + @Override + public boolean canConvert(Class targetType) { + return targetType == Boolean.class; + } + + @Override + public Object convert(String source, Class targetType) { + boolean isTrue = "true".equalsIgnoreCase(source); + Preconditions.condition(isTrue || "false".equalsIgnoreCase(source), + () -> "String must be 'true' or 'false' (ignoring case): " + source); + return isTrue; + } + +} diff --git a/junit-platform-commons/src/main/java/org/junit/platform/commons/support/StringToCharacterConverter.java b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/StringToCharacterConverter.java new file mode 100644 index 000000000000..54d73c64b909 --- /dev/null +++ b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/StringToCharacterConverter.java @@ -0,0 +1,28 @@ +/* + * Copyright 2015-2023 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; + +import org.junit.platform.commons.util.Preconditions; + +class StringToCharacterConverter implements StringToObjectConverter { + + @Override + public boolean canConvert(Class targetType) { + return targetType == Character.class; + } + + @Override + public Object convert(String source, Class targetType) { + Preconditions.condition(source.length() == 1, () -> "String must have length of 1: " + source); + return source.charAt(0); + } + +} diff --git a/junit-platform-commons/src/main/java/org/junit/platform/commons/support/StringToClassConverter.java b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/StringToClassConverter.java new file mode 100644 index 000000000000..673afd1ff97f --- /dev/null +++ b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/StringToClassConverter.java @@ -0,0 +1,36 @@ +/* + * Copyright 2015-2023 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; + +import org.junit.platform.commons.util.ReflectionUtils; + +class StringToClassConverter implements StringToObjectConverter { + + @Override + public boolean canConvert(Class targetType) { + return targetType == Class.class; + } + + @Override + public Object convert(String source, Class targetType) throws Exception { + throw new UnsupportedOperationException("Invoke convert(String, Class, ClassLoader) instead"); + } + + @Override + public Object convert(String className, Class targetType, ClassLoader classLoader) throws Exception { + // @formatter:off + return ReflectionUtils.tryToLoadClass(className, classLoader) + .getOrThrow(cause -> new ConversionException( + "Failed to convert String \"" + className + "\" to type java.lang.Class", cause)); + // @formatter:on + } + +} diff --git a/junit-platform-commons/src/main/java/org/junit/platform/commons/support/StringToCommonJavaTypesConverter.java b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/StringToCommonJavaTypesConverter.java new file mode 100644 index 000000000000..3e807def3351 --- /dev/null +++ b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/StringToCommonJavaTypesConverter.java @@ -0,0 +1,70 @@ +/* + * Copyright 2015-2023 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; + +import static java.util.Collections.unmodifiableMap; + +import java.io.File; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URL; +import java.nio.charset.Charset; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Currency; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.UUID; +import java.util.function.Function; + +class StringToCommonJavaTypesConverter implements StringToObjectConverter { + + private static final Map, Function> CONVERTERS; + + static { + Map, Function> converters = new HashMap<>(); + + // java.io and java.nio + converters.put(File.class, File::new); + converters.put(Charset.class, Charset::forName); + converters.put(Path.class, Paths::get); + // java.net + converters.put(URI.class, URI::create); + converters.put(URL.class, StringToCommonJavaTypesConverter::toURL); + // java.util + converters.put(Currency.class, Currency::getInstance); + converters.put(Locale.class, Locale::new); + converters.put(UUID.class, UUID::fromString); + + CONVERTERS = unmodifiableMap(converters); + } + + @Override + public boolean canConvert(Class targetType) { + return CONVERTERS.containsKey(targetType); + } + + @Override + public Object convert(String source, Class targetType) throws Exception { + return CONVERTERS.get(targetType).apply(source); + } + + private static URL toURL(String url) { + try { + return URI.create(url).toURL(); + } + catch (MalformedURLException ex) { + throw new ConversionException("Failed to convert String \"" + url + "\" to type java.net.URL", ex); + } + } + +} diff --git a/junit-platform-commons/src/main/java/org/junit/platform/commons/support/StringToEnumConverter.java b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/StringToEnumConverter.java new file mode 100644 index 000000000000..ccc4b1b655c4 --- /dev/null +++ b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/StringToEnumConverter.java @@ -0,0 +1,26 @@ +/* + * Copyright 2015-2023 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; + +class StringToEnumConverter implements StringToObjectConverter { + + @Override + public boolean canConvert(Class targetType) { + return targetType.isEnum(); + } + + @Override + @SuppressWarnings({ "unchecked", "rawtypes" }) + public Object convert(String source, Class targetType) throws Exception { + return Enum.valueOf(targetType, source); + } + +} diff --git a/junit-platform-commons/src/main/java/org/junit/platform/commons/support/StringToJavaTimeConverter.java b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/StringToJavaTimeConverter.java new file mode 100644 index 000000000000..4acfcdda5952 --- /dev/null +++ b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/StringToJavaTimeConverter.java @@ -0,0 +1,65 @@ +/* + * Copyright 2015-2023 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; + +import static java.util.Collections.unmodifiableMap; + +import java.time.Duration; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.MonthDay; +import java.time.OffsetDateTime; +import java.time.OffsetTime; +import java.time.Period; +import java.time.Year; +import java.time.YearMonth; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; + +class StringToJavaTimeConverter implements StringToObjectConverter { + + private static final Map, Function> CONVERTERS; + static { + Map, Function> converters = new HashMap<>(); + converters.put(Duration.class, Duration::parse); + converters.put(Instant.class, Instant::parse); + converters.put(LocalDate.class, LocalDate::parse); + converters.put(LocalDateTime.class, LocalDateTime::parse); + converters.put(LocalTime.class, LocalTime::parse); + converters.put(MonthDay.class, MonthDay::parse); + converters.put(OffsetDateTime.class, OffsetDateTime::parse); + converters.put(OffsetTime.class, OffsetTime::parse); + converters.put(Period.class, Period::parse); + converters.put(Year.class, Year::parse); + converters.put(YearMonth.class, YearMonth::parse); + converters.put(ZonedDateTime.class, ZonedDateTime::parse); + converters.put(ZoneId.class, ZoneId::of); + converters.put(ZoneOffset.class, ZoneOffset::of); + CONVERTERS = unmodifiableMap(converters); + } + + @Override + public boolean canConvert(Class targetType) { + return CONVERTERS.containsKey(targetType); + } + + @Override + public Object convert(String source, Class targetType) throws Exception { + return CONVERTERS.get(targetType).apply(source); + } + +} diff --git a/junit-platform-commons/src/main/java/org/junit/platform/commons/support/StringToNumberConverter.java b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/StringToNumberConverter.java new file mode 100644 index 000000000000..e42617d0d529 --- /dev/null +++ b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/StringToNumberConverter.java @@ -0,0 +1,50 @@ +/* + * Copyright 2015-2023 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; + +import static java.util.Collections.unmodifiableMap; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; + +class StringToNumberConverter implements StringToObjectConverter { + + private static final Map, Function> CONVERTERS; + static { + Map, Function> converters = new HashMap<>(); + converters.put(Byte.class, Byte::decode); + converters.put(Short.class, Short::decode); + converters.put(Integer.class, Integer::decode); + converters.put(Long.class, Long::decode); + converters.put(Float.class, Float::valueOf); + converters.put(Double.class, Double::valueOf); + // Technically, BigInteger and BigDecimal constructors are covered by + // FallbackStringToObjectConverter, but we have explicit conversion + // configured for them anyway. + converters.put(BigInteger.class, BigInteger::new); + converters.put(BigDecimal.class, BigDecimal::new); + CONVERTERS = unmodifiableMap(converters); + } + + @Override + public boolean canConvert(Class targetType) { + return CONVERTERS.containsKey(targetType); + } + + @Override + public Object convert(String source, Class targetType) { + return CONVERTERS.get(targetType).apply(source.replace("_", "")); + } + +} diff --git a/junit-platform-commons/src/main/java/org/junit/platform/commons/support/StringToObjectConverter.java b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/StringToObjectConverter.java new file mode 100644 index 000000000000..9a9dc8a47a09 --- /dev/null +++ b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/StringToObjectConverter.java @@ -0,0 +1,46 @@ +/* + * Copyright 2015-2023 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; + +/** + * Internal API for converting arguments of type {@link String} to a specified + * target type. + */ +interface StringToObjectConverter { + + /** + * Determine if this converter can convert from a {@link String} to the + * supplied target type (which is guaranteed to be a wrapper type for + * primitives — for example, {@link Integer} instead of {@code int}). + */ + boolean canConvert(Class targetType); + + /** + * Convert the supplied {@link String} to the supplied target type (which is + * guaranteed to be a wrapper type for primitives — for example, + * {@link Integer} instead of {@code int}). + */ + Object convert(String source, Class targetType) throws Exception; + + /** + * Convert the supplied {@link String} to the supplied target type (which is + * guaranteed to be a wrapper type for primitives — for example, + * {@link Integer} instead of {@code int}). + * + *

The default implementation simply delegates to {@link #convert(String, Class)}. + * Can be overridden by concrete implementations of this interface that need + * access to the supplied {@link ClassLoader}. + */ + default Object convert(String source, Class targetType, ClassLoader classLoader) throws Exception { + return convert(source, targetType); + } + +}