Skip to content

Latest commit

 

History

History
252 lines (210 loc) · 9.51 KB

generic-mapping.md

File metadata and controls

252 lines (210 loc) · 9.51 KB

Generic Mapping Proposal

This change was merged into master. Module: common/mapper Since: Helidon 1.2.2 Please consult javadocs for current documentation.

Provide an API to map an arbitrary Java type to another arbitrary Java type.

Proposal

The API consist of the following classes (may be changed due to implementation details):

  • MapperManager - the entry point to mapping of types
  • Mapper - a class capable of converting one type to another
  • MapperProvider - SPI class to support providers loaded through Java Service loader or configured through a builder
  • MapperException - RuntimeException thrown when a mapping is missing or the mapping itself failed

This API should be also added to Config as an additional source of mappings.

Mapper Manager

Mapping provides tools to map a type to another type.

The types may be

  • a java class (String.class)
  • a generic type (io.helidon.common.GenericType for any java type, such as Supplier<String>)

Mappings can be provided either through Java Service loader, or through explicitly configured providers using a builder.

The mapping function gets either a pair of Class objects, or a pair of GenericType objects that defines the SOURCE and the TARGET of the mapping.

For Class parameters, lookup is done as follows:

  1. Ask each mapping provider if such a pair of classes is supported, if yes, use the first mapper
  2. Convert each class to GenericType and ask each mapping provider if supported, if yes, use the first mapper

For GenericType parameters, lookup is done as follows:

  1. Ask each mapping provider if such a pair of types is supported, if yes, use the first mapper
  2. If both generic types represent a Class, convert each to a Class and ask each mapping provider if supported, if yes, use the first mapper

The results are cached (so lookup for a defined pair is done only once). In case no mapper is found, the result should be cached as well.

The main API class is MapperManager.

/**
 * Map from source to target.
 *
 * @param source     object to map
 * @param sourceType type of the source object (to locate the mapper)
 * @param targetType type of the target object (to locate the mapper)
 * @return result of the mapping
 * @throws io.helidon.common.mapping.MapperException in case the mapper was not found or failed
 */
<SOURCE, TARGET> TARGET map(SOURCE source, GenericType<SOURCE> sourceType, GenericType<TARGET> targetType);

/**
 * Map from source to target.
 *
 * @param source     object to map
 * @param sourceType class of the source object (to locate the mapper)
 * @param targetType class of teh target object (to locate the mapper)
 * @return result of the mapping
 * @throws io.helidon.common.mapping.MapperException in case the mapper was not found or failed
 */
<SOURCE, TARGET> TARGET map(SOURCE source, Class<SOURCE> sourceType, Class<TARGET> targetType);

MapperManager can be created using the usual Builder:

/**
 * Replace the service loader with the one provided.
 * @param serviceLoader fully configured service loader to be used to load mapper providers
 * @return updated builder instance
 */
Builder mapperProviders(HelidonServiceLoader<MapperProvider> serviceLoader);

/**
 * Add a new {@link io.helidon.common.mapping.spi.MapperProvider} to the list of providers loaded from
 *  system service loader.
 *  
 * @param provider prioritized mapper provider to use
 * @return updated builder instance
 */
Builder addMapperProvider(MapperProvider provider);

/**
 * Add a mapper to the list of mapper. 
 * 
 * @param mapper the mapper to map source instances to target instances
 * @param sourceType class of the source instance
 * @param targetType class of the target instance
 * @param <S> type of source
 * @param <T> type of target
 * @return updated builder instance
 */
<S, T> Builder addMapper(Mapper<S, T> mapper, Class<S> sourceType, Class<T> targetType);

/**
 * Add a mapper to the list of mapper.
 *
 * @param mapper the mapper to map source instances to target instances
 * @param sourceType generic type of the source instance
 * @param targetType generic type of the target instance
 * @param <S> type of source
 * @param <T> type of target
 * @return updated builder instance
 */
<S, T> Builder addMapper(Mapper<S, T> mapper, GenericType<S> sourceType, GenericType<T> targetType);

Mapper implementation

Mapper is the class doing the actual work of mapping one type to another. Mappers are either provided by user when creating the MapperManager or obtained from MapperProvider services.

Mapper:

/**
 * Map an instance of source type to an instance of target type.
 *
 * @param source object to map
 * @return result of the mapping
 */
TARGET map(SOURCE source);

The Mapper provides unidirectional mapping - there can be a mapper from String to Long and another one from Long to String.

The knowledge of the SOURCE and TARGET types comes from the developer providing these mappers - there is no possibility to register a Mapper without explicitly defining the types it is registered for.

Mapper provider implementations

Service implementation can use @Priority or implement io.helidon.common.Prioritized to define its priority (lower number will be used first)

Service implementation requires implementation for Class types, may also implement support for GenericType.

The main interface for SPI is MapperProvider:

/**
 * Find a mapper that is capable of mapping from source to target classes.
 *
 * @param sourceClass class of the source
 * @param targetClass class of the target
 * @param <SOURCE> type of the source
 * @param <TARGET> type of the target
 * @return a mapper that is capable of mapping (or converting) sources to targets
 */
<SOURCE, TARGET> Optional<Mapper<SOURCE, TARGET>> mapper(Class<SOURCE> sourceClass, Class<TARGET> targetClass);

/**
 * Find a mapper that is capable of mapping from source to target types.
 * This method supports mapping to/from types that contain generics.
 *
 * @param sourceType generic type of the source
 * @param targetType generic type of the target
 * @param <SOURCE> type of the source
 * @param <TARGET> type of the target
 * @return a mapper that is capable of mapping (or converting) sources to targets
 */
default <SOURCE, TARGET> Optional<Mapper<SOURCE, TARGET>> mapper(GenericType<SOURCE> sourceType, GenericType<TARGET> targetType) {
    return Optional.empty();
}

Built-in mapper

We may provide (as a separate library?) a set of built-in generic mappers, especially for primitive types. For each pair define here, we should have

  • bi-directional mapping between the types
  • mapping of List <-> List
  • mapping of List <-> Set
  • mapping of Set <-> Set
  • mapping of Array <-> List
  • mapping of Stream <-> Stream

Suggested supported mapping pairs (primitive types should be supported as well):

  • String to the same types as defined in ConfigMappers.initBuiltInMappers except for Map and Properties
  • BigInteger, Long - should throw an exception if too big
  • BigInteger, Integer - should throw an exception if too big
  • BigInteger, BigDecimal
  • Long, Integer - should throw an exception if too big
  • Integer, Short - should throw an exception if too big

Other reasonable mapping pairs can be added.

Examples

Using the MapperManager

The following example creates the MapperManager from Java Service loader

// creates a mapper manager from system service loader
MapperManager mm = MapperManager.create();

// this will work if a String to Long mapper is configured
Long longValue = mm.map("1094444", String.class, Long.class);

// this will work if a List<String> to List<Long> mapper is configured
List<String> stringList = CollectionsHelper.listOf("140", "145");
List<Long> longList = mm.map(stringList, new GenericType<List<String>>(){}, new GenericType<List<Long>>(){});

Creating a MapperProvider

The following example creates a service implementation that supports mapping to/from String and Long and a mapping from List<String> to List<Long>:

private static final Class<Long> LONG_CLASS = Long.class;
private static final GenericType<List<Long>> LONG_LIST = new GenericType<List<Long>>() { };
private static final Class<String> STRING_CLASS = String.class;
private static final GenericType<List<String>> STRING_LIST = new GenericType<List<String>>() { };

@Override
public <SOURCE, TARGET> Optional<Mapper<SOURCE, TARGET>> mapper(Class<SOURCE> sourceClass, Class<TARGET> targetClass) {
    if (sourceClass.equals(LONG_CLASS) && targetClass.equals(STRING_CLASS)) {
        return Optional.of((Mapper<SOURCE, TARGET>) longToString());
    }
    if (sourceClass.equals(STRING_CLASS) && targetClass.equals(LONG_CLASS)) {
        return Optional.of((Mapper<SOURCE, TARGET>) stringToLong());
    }
    return Optional.empty();
}

@Override
public <SOURCE, TARGET> Optional<Mapper<SOURCE, TARGET>> mapper(GenericType<SOURCE> sourceType,
                                                                GenericType<TARGET> targetType) {
    if (sourceType.equals(STRING_LIST) && targetType.equals(LONG_LIST)) {
        return Optional.of((Mapper<SOURCE, TARGET>) stringListToLongList());
    }
    return Optional.empty();
}

private Mapper<List<String>, List<Long>> stringListToLongList() {
    return strings -> strings.stream()
            .map(Long::parseLong)
            .collect(Collectors.toList());
}

private Mapper<String, Long> stringToLong() {
    return Long::parseLong;
}

private Mapper<Long, String> longToString() {
    return String::valueOf;
}