|
| 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 | +} |
0 commit comments