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.
The API consist of the following classes (may be changed due to implementation details):
MapperManager
- the entry point to mapping of typesMapper
- a class capable of converting one type to anotherMapperProvider
- SPI class to support providers loaded through Java Service loader or configured through a builderMapperException
-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.
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 asSupplier<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:
- Ask each mapping provider if such a pair of classes is supported, if yes, use the first mapper
- 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:
- Ask each mapping provider if such a pair of types is supported, if yes, use the first mapper
- If both generic types represent a
Class
, convert each to aClass
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
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.
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();
}
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 inConfigMappers.initBuiltInMappers
except forMap
andProperties
BigInteger
,Long
- should throw an exception if too bigBigInteger
,Integer
- should throw an exception if too bigBigInteger
,BigDecimal
Long
,Integer
- should throw an exception if too bigInteger
,Short
- should throw an exception if too big
Other reasonable mapping pairs can be added.
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>>(){});
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;
}