Skip to main content
4 of 6
added 2 characters in body
toolic
  • 6.2k
  • 4
  • 20
  • 75

ConverterService: type conversion multitool

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:

  1. 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"
  2. In case you're wondering, I had to introduce the S generic type parameter in Converter 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;
    }
  1. I realize I don't account for overflows and NumberFormatExeptions. 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 Converters? 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

package solution.common.converter;

import java.util.Optional;

public interface ConverterService {
    <T> Optional<T> convert(Object value, Class<T> targetType);
}
package solution.common.converter;

import java.util.Optional;

public interface Converter<T> {
    <S> Optional<T> convert(S value);
}
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();
            }
        };
    }
}
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();
    }
}
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;
    }
}
Sergey
  • 615
  • 1
  • 8