/*
* Copyright 2015-2017 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 v1.0 which
* accompanies this distribution and is available at
*
* http://www.eclipse.org/legal/epl-v10.html
*/
package org.junit.jupiter.params.converter;
import static java.util.Arrays.asList;
import static java.util.Collections.unmodifiableList;
import static java.util.Collections.unmodifiableMap;
import static org.junit.platform.commons.meta.API.Usage.Internal;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.OffsetDateTime;
import java.time.OffsetTime;
import java.time.Year;
import java.time.YearMonth;
import java.time.ZonedDateTime;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import org.junit.platform.commons.meta.API;
import org.junit.platform.commons.util.Preconditions;
import org.junit.platform.commons.util.ReflectionUtils;
/**
* {@code DefaultArgumentConverter} is the default implementation of the
* {@link ArgumentConverter} API.
*
* <p>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) as well as date and time types from the
* {@code java.time} package.
*
* <p>If the source and target types are identical the source object will not
* be modified.
*
* @since 5.0
* @see org.junit.jupiter.params.converter.ArgumentConverter
*/
@API(Internal)
public class DefaultArgumentConverter extends SimpleArgumentConverter {
public static final DefaultArgumentConverter INSTANCE = new DefaultArgumentConverter();
private static final List<StringToObjectConverter> stringToObjectConverters = unmodifiableList(
asList(new StringToPrimitiveConverter(), new StringToEnumConverter(), new StringToJavaTimeConverter()));
private DefaultArgumentConverter() {
// nothing to initialize
}
@Override
protected Object convert(Object source, Class<?> targetType) {
if (source == null) {
if (targetType.isPrimitive()) {
throw new ArgumentConversionException(
"Cannot convert null to primitive value of type " + targetType.getName());
}
return null;
}
return convertToTargetType(source, toWrapperType(targetType));
}
private Class<?> toWrapperType(Class<?> targetType) {
Class<?> wrapperType = ReflectionUtils.getWrapperType(targetType);
return wrapperType != null ? wrapperType : targetType;
}
private Object convertToTargetType(Object source, Class<?> targetType) {
if (targetType.isInstance(source)) {
return source;
}
if (source instanceof String) {
Optional<StringToObjectConverter> converter = stringToObjectConverters.stream().filter(
candidate -> candidate.canConvert(targetType)).findFirst();
if (converter.isPresent()) {
try {
return converter.get().convert((String) source, targetType);
}
catch (Exception ex) {
throw new ArgumentConversionException(
"Failed to convert String [" + source + "] to type " + targetType.getName(), ex);
}
}
}
throw new ArgumentConversionException("No implicit conversion to convert object of type "
+ source.getClass().getName() + " to type " + targetType.getName());
}
interface StringToObjectConverter {
boolean canConvert(Class<?> targetType);
Object convert(String source, Class<?> targetType) throws Exception;
}
static class StringToPrimitiveConverter implements StringToObjectConverter {
private static final Map<Class<?>, Function<String, ?>> CONVERTERS;
static {
Map<Class<?>, Function<String, ?>> converters = new HashMap<>();
converters.put(Boolean.class, Boolean::valueOf);
converters.put(Character.class, source -> {
Preconditions.condition(source.length() == 1, () -> "String must have length of 1: " + source);
return source.charAt(0);
});
converters.put(Byte.class, Byte::valueOf);
converters.put(Short.class, Short::valueOf);
converters.put(Integer.class, Integer::valueOf);
converters.put(Long.class, Long::valueOf);
converters.put(Float.class, Float::valueOf);
converters.put(Double.class, Double::valueOf);
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);
}
}
static class StringToEnumConverter implements StringToObjectConverter {
@Override
public boolean canConvert(Class<?> targetType) {
return targetType.isEnum();
}
@Override
public Object convert(String source, Class<?> targetType) throws Exception {
return valueOf(targetType, source);
}
@SuppressWarnings({ "unchecked", "rawtypes" })
private Object valueOf(Class targetType, String source) {
return Enum.valueOf(targetType, source);
}
}
static class StringToJavaTimeConverter implements StringToObjectConverter {
private static final Map<Class<?>, Function<CharSequence, ?>> CONVERTERS;
static {
Map<Class<?>, Function<CharSequence, ?>> converters = new HashMap<>();
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(OffsetDateTime.class, OffsetDateTime::parse);
converters.put(OffsetTime.class, OffsetTime::parse);
converters.put(Year.class, Year::parse);
converters.put(YearMonth.class, YearMonth::parse);
converters.put(ZonedDateTime.class, ZonedDateTime::parse);
CONVERTERS = Collections.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);
}
}
}