I got sick and tired of type-related errors and decided to write a type conversion service
It's not particularly pretty what with those casts and suppressions, but it seems to do what it was designed for
A few things to note:
- I didn't write javadocs: those are to be written in a non-English language. The most important thing to know is
S
means "source type",T
means "target type" - In case you're wondering, I had to introduce the
S
generic type parameter inConverter
solely for this to work:
@Override
@SuppressWarnings("unchecked")
public <S> Optional<T> convert(S value) {
Class<?> valueType = value.getClass();
// I can't write <? super valueType>
Function<? super S, Optional<T>> mapping = (Function<? super S, Optional<T>>) getMappingForType(valueType);
// if it was an unbounded wildcard, this line wouldn't compile
Optional<T> optionalValue = mapping.apply(value);
return optionalValue;
}
- I realize I don't account for overflows and
NumberFormatExeption
s. If truth be told, I couldn't figure out a good way to handle that
(should ConverterService
catch? should it be the responsibility of individual Converter
s? how do I make it not ugly? try-catches are ugly as is, let alone in lambdas)
Let it throw, for the time being (or forever)
How can I improve it?
I was hoping to make it cleaner and more robust.
Converter
:
package solution.common.converter;
import java.util.Optional;
public interface Converter<T> {
<S> Optional<T> convert(S value);
}
Converter
implementation:
package solution.common.converter;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
public class BasicConverter<T> implements Converter<T> {
private final Map<Class<?>, Function<?, Optional<T>>> mappings = new HashMap<>();
public <S> void addMapping(Class<S> sourceType, Function<S, Optional<T>> mappingFunction) {
mappings.put(sourceType, mappingFunction);
}
@Override
@SuppressWarnings("unchecked")
public <S> Optional<T> convert(S value) {
Class<?> valueType = value.getClass();
Function<? super S, Optional<T>> mapping = (Function<? super S, Optional<T>>) getMappingForType(valueType);
Optional<T> optionalValue = mapping.apply(value);
return optionalValue;
}
private Function<?, Optional<T>> getMappingForType(Class<?> type) {
Function<?, Optional<T>> mapping = mappings.containsKey(type) ?
mappings.get(type) :
findSuperTypeMapping(type);
return mapping;
}
@SuppressWarnings("unchecked")
private <S> Function<? super S, Optional<T>> findSuperTypeMapping(Class<S> valueType) {
for (Class<?> aClass : mappings.keySet()) {
if (aClass.isAssignableFrom(valueType))
return (Function<? super S, Optional<T>>) mappings.get(aClass);
}
return noopMapping();
}
private <S> Function<S, Optional<T>> noopMapping() {
return input -> Optional.empty();
}
}
ConverterService
:
package solution.common.converter;
import java.util.Optional;
public interface ConverterService {
<T> Optional<T> convert(Object value, Class<T> targetType);
}
ConverterService
implementation:
package solution.common.converter;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
public class BasicConverterService implements ConverterService {
private final Map<Class<?>, Converter<?>> converterMap = new HashMap<>();
public <T> void addConverter(Class<T> targetType, Converter<T> converter) {
converterMap.put(targetType, converter);
}
@Override
@SuppressWarnings("unchecked")
public <T> Optional<T> convert(Object value, Class<T> targetType) {
if (targetType.isAssignableFrom(value.getClass()))
return (Optional<T>) Optional.of(value);
Converter<?> converter = (converterMap.containsKey(targetType)) ?
converterMap.get(targetType) :
findSubtypeConverter(targetType);
return (Optional<T>) converter.convert(value);
}
private Converter<?> findSubtypeConverter(Class<?> targetType) {
for (Class<?> aClass : converterMap.keySet()) {
if (targetType.isAssignableFrom(aClass))
return converterMap.get(aClass);
}
return noopConverter();
}
private <T> Converter<T> noopConverter() {
return new Converter<T>() {
@Override
public <S> Optional<T> convert(S value) {
return Optional.empty();
}
};
}
}
Tests:
package solution.common.converter;
import org.junit.jupiter.api.Test;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.*;
class BasicConverterServiceTest {
@Test
void convert_ifNoConvertersAdded_andTargetTypeDoesNotMatchSourceType_returnsEmptyOptional() {
BasicConverterService converterService = new BasicConverterService();
Optional<Integer> valueOptional = converterService.convert("42", Integer.class);
assertFalse(valueOptional.isPresent());
}
@Test
void convert_ifNoConverterAdded_butTargetTypeMatchesSourceType_returnsOptionalOfInputValue() {
BasicConverterService converterService = new BasicConverterService();
Optional<String> valueOptional = converterService.convert("42", String.class);
assertTrue(valueOptional.isPresent());
assertEquals("42", valueOptional.get());
}
@Test
void convert_ifConverterMatchingTypeAdded_returnsOptionalOfThatConvertersResult() {
BasicConverterService converterService = new BasicConverterService();
converterService.addConverter(Integer.class, new Converter<Integer>() {
@Override
public <S> Optional<Integer> convert(S value) {
return Optional.of(Integer.parseInt((String) value));
}
});
Optional<Integer> valueOptional = converterService.convert("42", Integer.class);
assertTrue(valueOptional.isPresent());
assertEquals(42, valueOptional.get());
}
@Test
void convert_ifConverterMatchingSubtypeAdded_returnsOptionalOfThatConvertersResult() {
BasicConverterService converterService = new BasicConverterService();
converterService.addConverter(Integer.class, new Converter<Integer>() {
@Override
public <S> Optional<Integer> convert(S value) {
return Optional.of(Integer.parseInt((String) value));
}
});
Optional<Number> valueOptional = converterService.convert("42", Number.class);
assertTrue(valueOptional.isPresent());
assertEquals(42, valueOptional.get());
}
}
Here's a factory that returns a more fully-featured ConverterService
. It seems to cover most, if not all, of our project's needs. It contains one line that doesn't compile, the one that involves DateUtils
, but I decided to omit that utility for the purpose of this code review. Basically, it checks if the input object is Date
and casts if so; if it's a string, it tries a number of date formats to parse it, one by one
package solution.common.di;
import solution.common.converter.BasicConverter;
import solution.common.converter.BasicConverterService;
import solution.common.converter.Converter;
import solution.common.converter.ConverterService;
import solution.common.date.DateUtils;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.Date;
import java.util.Optional;
public class Converters {
public static ConverterService converterService() {
BasicConverterService converterService = new BasicConverterService();
converterService.addConverter(Date.class, toDateConverter());
converterService.addConverter(Byte.class, toByteConverter());
converterService.addConverter(Short.class, toShortConverter());
converterService.addConverter(Integer.class, toIntegerConverter());
converterService.addConverter(BigInteger.class, toBigIntegerConverter());
converterService.addConverter(Long.class, toLongConverter());
converterService.addConverter(Float.class, toFloatConverter());
converterService.addConverter(Double.class, toDoubleConverter());
converterService.addConverter(BigDecimal.class, toBigDecimalConverter());
converterService.addConverter(String.class, toStringConverter());
return converterService;
}
private static Converter<Date> toDateConverter() {
BasicConverter<Date> converter = new BasicConverter<>();
converter.addMapping(Object.class, DateUtils::asDate);
return converter;
}
private static Converter<Byte> toByteConverter() {
BasicConverter<Byte> converter = new BasicConverter<>();
converter.addMapping(String.class, input -> Optional.of(input)
.map(Byte::parseByte));
converter.addMapping(Number.class, input -> Optional.of(input).map(Number::byteValue));
return converter;
}
private static Converter<Short> toShortConverter() {
BasicConverter<Short> converter = new BasicConverter<>();
converter.addMapping(String.class, input -> Optional.of(input)
.map(Short::parseShort));
converter.addMapping(Number.class, input -> Optional.of(input).map(Number::shortValue));
return converter;
}
private static Converter<Integer> toIntegerConverter() {
BasicConverter<Integer> converter = new BasicConverter<>();
converter.addMapping(String.class, input -> Optional.of(input)
.map(Integer::parseInt));
converter.addMapping(Number.class, input -> Optional.of(input).map(Number::intValue));
return converter;
}
private static Converter<Long> toLongConverter() {
BasicConverter<Long> converter = new BasicConverter<>();
converter.addMapping(String.class, input -> Optional.of(input)
.map(Long::parseLong));
converter.addMapping(Number.class, input -> Optional.of(input).map(Number::longValue));
return converter;
}
private static Converter<BigInteger> toBigIntegerConverter() {
BasicConverter<BigInteger> converter = new BasicConverter<>();
converter.addMapping(String.class, input -> Optional.of(input)
.map(BigInteger::new));
converter.addMapping(Number.class, input -> Optional.of(input)
.map(Number::longValue)
.map(BigInteger::valueOf));
return converter;
}
private static Converter<Float> toFloatConverter() {
BasicConverter<Float> converter = new BasicConverter<>();
converter.addMapping(String.class, input -> Optional.of(input)
.map(Float::parseFloat));
converter.addMapping(Number.class, input -> Optional.of(input).map(Number::floatValue));
return converter;
}
private static Converter<Double> toDoubleConverter() {
BasicConverter<Double> converter = new BasicConverter<>();
converter.addMapping(String.class, input -> Optional.of(input)
.map(Double::parseDouble));
converter.addMapping(Number.class, input -> Optional.of(input).map(Number::doubleValue));
return converter;
}
private static Converter<BigDecimal> toBigDecimalConverter() {
BasicConverter<BigDecimal> converter = new BasicConverter<>();
converter.addMapping(String.class, input -> Optional.of(input)
.map(BigDecimal::new));
converter.addMapping(Number.class, input -> Optional.of(input)
.map(Number::doubleValue)
.map(BigDecimal::new));
return converter;
}
private static Converter<String> toStringConverter() {
BasicConverter<String> converter = new BasicConverter<>();
converter.addMapping(Object.class, input -> Optional.of(input).map(Object::toString));
return converter;
}
}