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 748c0412c6ca..7f583260120f 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 @@ -25,7 +25,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..adbb606da691 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,13 +18,13 @@ 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.conversion.ConversionException; +import org.junit.platform.commons.support.conversion.StringConversionSupport; import org.junit.platform.commons.util.ClassLoaderUtils; import org.junit.platform.commons.util.ReflectionUtils; @@ -47,23 +44,13 @@ * * @since 5.0 * @see org.junit.jupiter.params.converter.ArgumentConverter + * @see org.junit.platform.commons.support.conversion.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 } @@ -88,34 +75,19 @@ public final Object convert(Object source, Class targetType, ParameterContext } 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); - } + Class declaringClass = context.getDeclaringExecutable().getDeclaringClass(); + ClassLoader classLoader = ClassLoaderUtils.getClassLoader(declaringClass); + try { + return StringConversionSupport.convert((String) 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())); } - 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 76d432b9564c..b2b1e7667722 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/conversion/ConversionException.java b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/ConversionException.java new file mode 100644 index 000000000000..439334635776 --- /dev/null +++ b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/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.conversion; + +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/conversion/FallbackStringToObjectConverter.java b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/FallbackStringToObjectConverter.java new file mode 100644 index 000000000000..06dc29153e31 --- /dev/null +++ b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/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.conversion; + +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/conversion/StringConversionSupport.java b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/StringConversionSupport.java new file mode 100644 index 000000000000..2ac35dc67461 --- /dev/null +++ b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/StringConversionSupport.java @@ -0,0 +1,123 @@ +/* + * 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.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.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; + +/** + * {@code StringConversionSupport} 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 target type is {@code String} the source {@code String} will not + * be modified. + * + * @since 1.11 + */ +@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 */ + } + + /** + * Convert a {@code String} into an object of the supplied type. + * + *

Some underlying converters can require a {@code ClassLoader}. + * If none is provided, the default one given by + * {@link ClassLoaderUtils#getDefaultClassLoader()} will be used. + * + * @param source the source {@code String} to convert; may be {@code null} + * @param targetType the target type the source should be converted into; + * never {@code null} + * @param classLoader the {@code ClassLoader} to use; may be {@code null} + * @param the type of the target + * @return the converted object; may be {@code null} but only if the target + * type is a reference type + * + * @since 1.11 + */ + @SuppressWarnings("unchecked") + public static T convert(String 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 (T) source; + } + + Class targetTypeToUse = toWrapperType(targetType); + Optional converter = stringToObjectConverters.stream().filter( + candidate -> candidate.canConvert(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); + } + } + + 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; + } + +} diff --git a/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/StringToBooleanConverter.java b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/StringToBooleanConverter.java new file mode 100644 index 000000000000..2bde9ac323c2 --- /dev/null +++ b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/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.conversion; + +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/conversion/StringToCharacterConverter.java b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/StringToCharacterConverter.java new file mode 100644 index 000000000000..925acdfe62d5 --- /dev/null +++ b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/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.conversion; + +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/conversion/StringToClassConverter.java b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/StringToClassConverter.java new file mode 100644 index 000000000000..df2b0164ad5c --- /dev/null +++ b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/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.conversion; + +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/conversion/StringToCommonJavaTypesConverter.java b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/StringToCommonJavaTypesConverter.java new file mode 100644 index 000000000000..2988714318e3 --- /dev/null +++ b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/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.conversion; + +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/conversion/StringToEnumConverter.java b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/StringToEnumConverter.java new file mode 100644 index 000000000000..48c07fa59eb7 --- /dev/null +++ b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/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.conversion; + +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/conversion/StringToJavaTimeConverter.java b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/StringToJavaTimeConverter.java new file mode 100644 index 000000000000..6ecbf84b25e5 --- /dev/null +++ b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/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.conversion; + +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/conversion/StringToNumberConverter.java b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/StringToNumberConverter.java new file mode 100644 index 000000000000..b8cd6e7d3e4f --- /dev/null +++ b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/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.conversion; + +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/conversion/StringToObjectConverter.java b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/StringToObjectConverter.java new file mode 100644 index 000000000000..243bfeec4afc --- /dev/null +++ b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/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.conversion; + +/** + * 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); + } + +} diff --git a/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/package-info.java b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/package-info.java new file mode 100644 index 000000000000..e51977179941 --- /dev/null +++ b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/package-info.java @@ -0,0 +1,5 @@ +/** + * Maintained conversion APIs provided by the JUnit Platform. + */ + +package org.junit.platform.commons.support.conversion; diff --git a/junit-platform-commons/src/module/org.junit.platform.commons/module-info.java b/junit-platform-commons/src/module/org.junit.platform.commons/module-info.java index f33ffd314feb..774684198f9f 100644 --- a/junit-platform-commons/src/module/org.junit.platform.commons/module-info.java +++ b/junit-platform-commons/src/module/org.junit.platform.commons/module-info.java @@ -36,6 +36,7 @@ org.junit.platform.testkit, org.junit.vintage.engine; exports org.junit.platform.commons.support; + exports org.junit.platform.commons.support.conversion; exports org.junit.platform.commons.util to org.junit.jupiter.api, org.junit.jupiter.engine, diff --git a/platform-tooling-support-tests/projects/jar-describe-module/junit-platform-commons.expected.txt b/platform-tooling-support-tests/projects/jar-describe-module/junit-platform-commons.expected.txt index 5c0c9b44e4ef..cb3eb72ae947 100644 --- a/platform-tooling-support-tests/projects/jar-describe-module/junit-platform-commons.expected.txt +++ b/platform-tooling-support-tests/projects/jar-describe-module/junit-platform-commons.expected.txt @@ -3,6 +3,7 @@ exports org.junit.platform.commons exports org.junit.platform.commons.annotation exports org.junit.platform.commons.function exports org.junit.platform.commons.support +exports org.junit.platform.commons.support.conversion requires java.base mandated requires java.logging requires java.management