- Long story short
- About ReMap
- Mapping operations
- Validation
- Features
- Limitations
- How to use
- Spring integration
- Migration guide
- How to contribute
ReMap is a library that simplifies conversion of objects field by field and greatly reduces the efforts for unit testing mapper classes. You can get this library via Maven Central using the following coordinates
<dependency>
<groupId>com.remondis</groupId>
<artifactId>remap</artifactId>
<version>3.0.2</version>
</dependency>
...or in Gradle using compile "com.remondis:remap:3.0.2"
.
The following code snippet shows how to map a source type to a destination type:
Mapping.from(Customer.class)
.to(Person.class)
.omitInSource(Customer::getAddress)
.omitInDestination(Person::getBodyHeight)
.reassign(Customer::getTitle)
.to(Person::getSalutation)
.replace(Customer::getGender, Person::getGender)
.withSkipWhenNull(Gender::valueOf)
.mapper();
The resulting mapper does the following:
- maps
Customer
objects toPerson
objects Customer
has the fieldaddress
,Person
does not, hence it is omittedPerson
has the fieldbodyHeight
andCustomer
does not, hence it is omitted- the field
Customer.title
is mapped to the fieldPerson.salutation
- the field
Customer.gender
of typeString
is mapped to the FieldCustomer.gender
of enum typeGender
usingGender.valueOf()
as a transformation function
You can find this demo and the involved classes here
ReMap is a library that simplifies conversion of objects field by field. It was developed to make conversion of database entities to DTOs (data transfer objects) easier. The use of ReMap makes converter classes and unit tests for converters obsolete: ReMap only needs a specification of what fields are to be mapped, but the amount of code that actually performs the assignments and transformations is minimized. Therefore the code that must be unit-tested is also minimized.
ReMap maps a objects of a source to a destination type. As per default ReMap tries to map all fields from the source to the destination object if the fields have equal name and type. Only differences between the source type and the target type must be specified when creating a mapper.
The following operations can be declared on a mapper:
omitInSource
: omits a field in the source type and skips the mapping.omitInDestination
: omits a field in the destination type and skips the mapping.reassign
: maps a source field to the destination field of the same type while changing the field name.replace
: converts a source field to the destination field while changing the field name and the type. To transform the source object into the destination type a transformation function is to be specified.useMapper
: registers a specific mapper instance that is used to convert referenced types.
ReMap validates the mapping configuration of a mapper at instantiation time and denies the following states:
- A source field was not mapped to a destination field
- A destination field is not covered by a source field
- Multiple mappings where defined for a destination field
omit
is specified for a source field that already has a mapping configuration
These validation rules make sure that all fields are covered by the mapping configuration when a mapper instance is created.
Since ReMap relies on getter and setter references like Address::getId
to specify a mapping,
the compiler does not allow mappings between fields that are incompatible. When a mapper is
instantiated, ReMap performs the above-mentioned validations.
Thus, the only things you need to test in a unit test are:
- that
Mapping.mapper()
does not throw aMappingException
telling you that one of the validations failed - any transformation functions specified for
replace
operations
Optionally, you may want to assert that your specification matches certain expectations to prevent regressions to creep into your codebase (see Tests).
ReMap supports
- out-of-the-box mapping of primitives, built-in types, custom Java Beans and enumeration values
- type inheritance
- mapping object references to fields
- restrictive visibilities
- mapping from interface to Java Bean type
- mapping of nested collections (Attention: maps are not collections!)
- mapping of maps using
replace
and a transformation function that maps key and values - unit testing of mapping specifications
- mapping without invasively changing code of involved objects
- objects that are part of the mapping process must meet the Java Bean convention
- fields can have any visibility
- fields have properly named public get/set methods
- fields of primitive type boolean have public is/set methods
- the declaring type has a public default constructor (this is only necessary for the destination object)
- keywords like
transient
do not have an effect on the mapping
- circular references are currently not supported
- mapping equal types does not copy object instances!
- multi-classloader environments are currently not supported. All types must be loaded by the same classloader.
- Generics cannot be used without limitations: It is possible to build a mapper for generic types, but due to the class literals used when declaring the mapping, the generic type informations gets lost.
The short mapping shown under Long story short uses all common operations. When a mapping becomes a little more complex the following code snippets may help.
ReMap can be used to flatten object references. The following example maps the field OrderEntity.address
of type Address
to the field OrderDTO.addressId
of type long
.
Mapper<OrderEntity, OrderDTO> mapper = Mapping
.from(OrderEntity.class)
.to(OrderDTO.class)
.replace(OrderEntity::getAddress, OrderDTO::getAddressId)
.with(Addresss::getId)
.mapper();
As mentioned above ReMap does not directly support the mapping of java.util.Map
. The following example maps a map in A
to a map of different key-value-types in AResource
. The field bmap
in A
is a map that may look like this Map<Integer, B>
while the target field bmap
in AResource
is a map of type Map<String, BResource>
. For this mapping we need a function that transforms the map into another map of the specified type and a mapper to map B
to BResource
.
Use the following code snippet to map maps using the replace
operation:
Mapper<B, BResource> bMapper = Mapping.from(B.class)
.to(BResource.class)
.mapper();
Mapper<A, AResource> mapper = Mapping.from(A.class)
.to(AResource.class)
// specify a replace operation involving the source and the destination field holding the map
.replace(A::getBmap, AResource::getBmap)
// specify a transformation function (Map<Integer, B>) -> Map<String, BResource>
.with(iToBMap -> {
return iToBMap.entrySet()
.stream()
.map(e -> {
// Perform the type conversion while iterating over the entry set
return new AbstractMap.SimpleEntry<String, BResource>(String.valueOf(e.getKey()),
bMapper.map(e.getValue()));
})
.collect(Collectors.toMap(Entry::getKey, Entry::getValue));
})
.useMapper(bMapper)
.mapper();
When performing a replace
operation on collections in earlier versions of ReMap you had to manually iterate over the collection to apply the conversion. Since ReMap version 1.0.0
you can use the operation replaceCollection
to apply the transformation function automatically on the collection items.
The following code snippet shows how to use replaceCollection
:
Mapper<Source, Destination> mapper = Mapping.from(Source.class)
.to(Destination.class)
.replaceCollection(Source::getIds, Destination::getIds)
.with(id -> Id.builder()
.id(id)
.build())
.mapper();
The following code asserts the above mapping:
AssertMapping.of(mapper)
.expectReplaceCollection(Source::getIds, Destination::getIds)
.andTest(id -> Id.builder()
.id(id)
.build())
.ensure();
You can find this demo and the involved classes here
ReMap provides a class to combine two mapper instances to a bidirectional mapping. Given the following mappings:
Mapper<Person, Human> to = Mapping.from(Person.class)
.to(Human.class)
.mapper();
Mapper<Human, Person> from = Mapping.from(Human.class)
.to(Person.class)
.mapper();
a bidirectional mapper can be created to map a Person
to Human
and vice-versa:
BidirectionalMapper<Person, Human> bidirectionalMapper = BidirectionalMapper.of(to, from);
Person person = new Person("Peter");
Human human = bidirectionalMapper.map(person);
Person mappedBackToPerson = bidirectionalMapper.mapFrom(human);
You can find this demo and the involved classes here
ReMap provides an easy way to assert the mapping specification for a mapper instance. These assertions should be used in unit tests to provide regression tests for your mapping configuration. The following example shows how to assert a mapping specification:
Given the following mapping...
Mapper<B, BResource> bMapper = Mapping.from(B.class)
.to(BResource.class)
.mapper();
Mapper<A, AResource> mapper = Mapping.from(A.class)
.to(AResource.class)
.reassign(A::getString)
.to(AResource::getAnotherString)
.replace(A::getInteger, AResource::getIntegerAsString)
.with(String::valueOf)
.omitInSource(A::getOmitted)
.omitInDestination(AResource::getOmitted)
.useMapper(bMapper)
.mapper();
...the following assertion can be made to ensure regression validity for the mapping specification:
AssertMapping.of(mapper)
.expectReassign(A::getString)
.to(AResource::getAnotherString)
.expectReplace(A::getInteger, AResource::getIntegerAsString)
.andTest(String::valueOf)
.expectOmitInSource(A::getOmitted)
.expectOmitInDestination(AResource::getOmitted)
.ensure();
The asserts check that the expected mappings are also configured on the specified mapper. If there are differences, the ensure()
method will throw an assertion error.
Note: The replace
operation supports two null-strategies and the mapper needs to specify the same strategy as the asserts! The transformation function in this example is checked against a null
when ensure()
is invoked. If the replace
operation was added using withSkipWhenNull()
the specified transformation function is not checked against null
.
ReMap can be nicely integrated in Spring Applications so that mapper instances can be injected using @Autowired
. Spring also checks the generic type of the mapper to autowire the correct mapping requested.
The following bean configuration creates mappers to convert a Person
into Human
and vice versa:
@Configuration
static class TestConfiguration {
@Bean
BidirectionalMapper<Person, Human> bidiPersonHumanMapper(Mapper<Person, Human> personHumanMapper,
Mapper<Human, Person> humanPersonMapper) {
return BidirectionalMapper.of(personHumanMapper, humanPersonMapper);
}
@Bean
Mapper<Person, Human> personHumanMapper(){
return Mapping.from(Person.class)
.to(Human.class)
.mapper();
}
@Bean
Mapper<Human, Person> humanPersonMapper(){
return Mapping.from(Human.class)
.to(Person.class)
.mapper();
}
}
Use the following code snippet in components to inject the mapper instances:
@Autowired
Mapper<Person, Human> mapper1;
@Autowired
Mapper<Human, Person> mapper2;
There were API changes that break backward compatibility:
- The method
com.remondis.remap.ReplaceCollectionAssertBuilder.andTestButSkipWhenNull(Transform<RD, RS> transformation)
changed tocom.remondis.remap.ReplaceCollectionAssertBuilder.andSkipWhenNull()
. Specifying transform function is not longer required. Please check your mapping assertions to match the new API.
There were API changes that break backward compatibility:
- The method
com.remondis.remap.ReplaceAssertBuilder.andTestButSkipWhenNull(Transform<RD, RS> transformation)
changed tocom.remondis.remap.ReplaceAssertBuilder.andSkipWhenNull()
. Specifying transform function is not longer required. Please check your mapping assertions to match the new API. - The generic type in
com.remondis.remap.Transform<D, S>
changed tocom.remondis.remap.Transform<S, D>
- According to the above change please check your mapping configuration and assertions for the
replace
operation to match the new API. The affected classes are:com.remondis.remap.ReplaceBuilder
com.remondis.remap.ReplaceAssertBuilder
com.remondis.remap.ReplaceCollectionBuilder
com.remondis.remap.ReplaceCollectionAssertBuilder
com.remondis.remap.ReplaceTransformation
com.remondis.remap.Transformation
Please refer to the project's contribution guide