#1918 multiple target this mappings (#1920)

This commit is contained in:
Sjaak Derksen 2019-09-23 20:04:56 +02:00 committed by GitHub
parent 61f941aa80
commit 0d23f09e37
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 344 additions and 31 deletions

View File

@ -238,7 +238,7 @@ In this case the source parameter is directly mapped into the target as the exam
If you don't want explicitly name all properties from nested source bean, you can use `.` as target.
This will tell MapStruct to map every property from source bean to target object. The following shows an example:
.Nested multi mapping method
.use of "target this" annotation "."
====
[source, java, linenums]
[subs="verbatim,attributes"]
@ -246,21 +246,23 @@ In this case the source parameter is directly mapped into the target as the exam
@Mapper
public interface CustomerMapper {
@Mapping(source = "record", target = ".")
@Mapping( target = "name", source = "record.name" )
@Mapping( target = ".", source = "record" )
@Mapping( target = ".", source = "account" )
Customer customerDtoToCustomer(CustomerDto customerDto);
}
----
====
The generated code will map every property from `CustomerDto.record` to `Customer` directly, without need to manually name any of them.
The same goes for `Customer.account`.
When there are conflicts, these can be resolved by explicitely defining the mapping. For instance in the example above. `name` occurs in `CustomerDto.record` and in `CustomerDto.account`. The mapping `@Mapping( target = "name", source = "record.name" )` resolves this conflict.
This "target this" notation can be very useful when mapping hierarchical objects to flat objects and vice versa (`@InheritInverseConfiguration`).
Multi mapping notation can be very useful when mapping hierarchical objects to flat objects and opposite way.
[TIP]
====
If you need to send your domain object using JSON, one way to do this is to convert it to single object, where every parent class is represented as separate field in JSON.
And MapStruct allows you easily convert between these 2 types.
====
[[updating-bean-instances]]
=== Updating existing bean instances

View File

@ -657,15 +657,30 @@ public class BeanMappingMethod extends NormalTypeMappingMethod {
* <p>
* When a target property matches its name with the (nested) source property, it is added to the list if and
* only if it is an unprocessed target property.
*
* duplicates will be handled by {@link #applyPropertyNameBasedMapping(List)}
*/
private void applyTargetThisMapping() {
if ( mappingReferences.getTargetThis() != null ) {
List<SourceReference> sourceRefs = mappingReferences.getTargetThis()
Set<String> handledTargetProperties = new HashSet<>();
for ( MappingReference targetThis : mappingReferences.getTargetThisReferences() ) {
// handle all prior unprocessed target properties, but let duplicates fall through
List<SourceReference> sourceRefs = targetThis
.getSourceReference()
.push( ctx.getTypeFactory(), ctx.getMessager(), method ).stream()
.filter( sr -> unprocessedTargetProperties.containsKey( sr.getDeepestPropertyName() ) )
.push( ctx.getTypeFactory(), ctx.getMessager(), method )
.stream()
.filter( sr -> unprocessedTargetProperties.containsKey( sr.getDeepestPropertyName() )
|| handledTargetProperties.contains( sr.getDeepestPropertyName() ) )
.collect( Collectors.toList() );
// apply name based mapping
applyPropertyNameBasedMapping( sourceRefs );
// add handled target properties
handledTargetProperties.addAll( sourceRefs.stream()
.map( SourceReference::getDeepestPropertyName )
.collect(
Collectors.toList() ) );
}
}

View File

@ -5,8 +5,10 @@
*/
package org.mapstruct.ap.internal.model.beanmapping;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import org.mapstruct.ap.internal.model.common.TypeFactory;
@ -19,7 +21,7 @@ public class MappingReferences {
private static final MappingReferences EMPTY = new MappingReferences( Collections.emptySet(), false );
private final Set<MappingReference> mappingReferences;
private final MappingReference targetThis;
private final List<MappingReference> targetThisReferences;
private final boolean restrictToDefinedMappings;
private final boolean forForgedMethods;
@ -31,7 +33,7 @@ public class MappingReferences {
TypeFactory typeFactory) {
Set<MappingReference> references = new LinkedHashSet<>();
MappingReference targetThisReference = null;
List<MappingReference> targetThisReferences = new ArrayList<>( );
for ( Mapping mapping : sourceMethod.getMappingOptions().getMappings() ) {
@ -53,29 +55,29 @@ public class MappingReferences {
MappingReference mappingReference = new MappingReference( mapping, targetReference, sourceReference );
if ( isValidWhenInversed( mappingReference ) ) {
if ( ".".equals( mapping.getTargetName() ) ) {
targetThisReference = mappingReference;
targetThisReferences.add( mappingReference );
}
else {
references.add( mappingReference );
}
}
}
return new MappingReferences( references, targetThisReference, false );
return new MappingReferences( references, targetThisReferences, false );
}
public MappingReferences(Set<MappingReference> mappingReferences, MappingReference targetThis,
public MappingReferences(Set<MappingReference> mappingReferences, List<MappingReference> targetThisReferences,
boolean restrictToDefinedMappings) {
this.mappingReferences = mappingReferences;
this.restrictToDefinedMappings = restrictToDefinedMappings;
this.forForgedMethods = restrictToDefinedMappings;
this.targetThis = targetThis;
this.targetThisReferences = targetThisReferences;
}
public MappingReferences(Set<MappingReference> mappingReferences, boolean restrictToDefinedMappings) {
this.mappingReferences = mappingReferences;
this.restrictToDefinedMappings = restrictToDefinedMappings;
this.forForgedMethods = restrictToDefinedMappings;
this.targetThis = null;
this.targetThisReferences = Collections.emptyList();
}
public MappingReferences(Set<MappingReference> mappingReferences, boolean restrictToDefinedMappings,
@ -83,7 +85,7 @@ public class MappingReferences {
this.mappingReferences = mappingReferences;
this.restrictToDefinedMappings = restrictToDefinedMappings;
this.forForgedMethods = forForgedMethods;
this.targetThis = null;
this.targetThisReferences = Collections.emptyList();
}
public Set<MappingReference> getMappingReferences() {
@ -127,8 +129,8 @@ public class MappingReferences {
return false;
}
public MappingReference getTargetThis() {
return targetThis;
public List<MappingReference> getTargetThisReferences() {
return targetThisReferences;
}
/**

View File

@ -482,6 +482,10 @@ public class Mapping {
if ( this == o ) {
return true;
}
if ( ".".equals( this.targetName ) ) {
// target this will never be equal to any other target this or any other.
return false;
}
if ( o == null || getClass() != o.getClass() ) {
return false;
}

View File

@ -0,0 +1,26 @@
/*
* Copyright MapStruct Authors.
*
* Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
*/
package org.mapstruct.ap.test.targetthis;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;
/**
* @author Sjaak Derksen
*/
@Mapper
public interface ConfictsResolvedNestedMapper {
ConfictsResolvedNestedMapper INSTANCE = Mappers.getMapper( ConfictsResolvedNestedMapper.class );
@Mapping( target = "id", source = "customer.item.id" )
@Mapping( target = ".", source = "customer.item" )
@Mapping( target = "status", source = "item.status" )
@Mapping( target = ".", source = "item" )
OrderItem map(OrderDTO order);
}

View File

@ -5,6 +5,9 @@
*/
package org.mapstruct.ap.test.targetthis;
/**
* @author Dainius Figoras
*/
public class CustomerDTO {
private String name;
private int level;

View File

@ -5,6 +5,9 @@
*/
package org.mapstruct.ap.test.targetthis;
/**
* @author Dainius Figoras
*/
public class CustomerItem {
private String id;
private int status;

View File

@ -0,0 +1,24 @@
/*
* Copyright MapStruct Authors.
*
* Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
*/
package org.mapstruct.ap.test.targetthis;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;
/**
* @author Sjaak Derksen
*/
@Mapper
public interface ErroneousNestedMapper {
ErroneousNestedMapper INSTANCE = Mappers.getMapper( ErroneousNestedMapper.class );
@Mapping( target = ".", source = "customer.item" )
@Mapping( target = ".", source = "item" )
OrderItem map(OrderDTO order);
}

View File

@ -0,0 +1,142 @@
/*
* Copyright MapStruct Authors.
*
* Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
*/
package org.mapstruct.ap.test.targetthis;
import org.mapstruct.InheritInverseConfiguration;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;
/**
* @author Sjaak Derksen
*/
@Mapper
public interface FlatteningMapper {
FlatteningMapper INSTANCE = Mappers.getMapper( FlatteningMapper.class );
@Mapping(target = ".", source = "name")
@Mapping(target = ".", source = "account")
Customer flatten(CustomerDTO customer);
@InheritInverseConfiguration
CustomerDTO expand(Customer customer);
class Customer {
private String name;
private String id;
private String details;
private String number;
public String getName() {
return name;
}
public Customer setName(String name) {
this.name = name;
return this;
}
public String getId() {
return id;
}
public Customer setId(String id) {
this.id = id;
return this;
}
public String getDetails() {
return details;
}
public Customer setDetails(String details) {
this.details = details;
return this;
}
public String getNumber() {
return number;
}
public Customer setNumber(String number) {
this.number = number;
return this;
}
}
class CustomerDTO {
private NameDTO name;
private AccountDTO account;
public NameDTO getName() {
return name;
}
public CustomerDTO setName(NameDTO name) {
this.name = name;
return this;
}
public AccountDTO getAccount() {
return account;
}
public CustomerDTO setAccount(AccountDTO account) {
this.account = account;
return this;
}
}
class NameDTO {
private String name;
private String id;
public String getName() {
return name;
}
public NameDTO setName(String name) {
this.name = name;
return this;
}
public String getId() {
return id;
}
public NameDTO setId(String id) {
this.id = id;
return this;
}
}
class AccountDTO {
private String number;
private String details;
public String getNumber() {
return number;
}
public AccountDTO setNumber(String number) {
this.number = number;
return this;
}
public String getDetails() {
return details;
}
public AccountDTO setDetails(String details) {
this.details = details;
return this;
}
}
}

View File

@ -5,6 +5,9 @@
*/
package org.mapstruct.ap.test.targetthis;
/**
* @author Dainius Figoras
*/
public class ItemDTO {
private String id;
private int status;

View File

@ -9,6 +9,9 @@ import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;
/**
* @author Sjaak Derksen
*/
@Mapper
public interface NestedMapper {

View File

@ -5,6 +5,9 @@
*/
package org.mapstruct.ap.test.targetthis;
/**
* @author Dainius Figoras
*/
public class OrderDTO {
private ItemDTO item;

View File

@ -5,6 +5,9 @@
*/
package org.mapstruct.ap.test.targetthis;
/**
* @author Dainius Figoras
*/
public class OrderItem {
private String id;
private int status;

View File

@ -9,6 +9,9 @@ import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;
/**
* @author Sjaak Derksen
*/
@Mapper
public interface SimpleMapper {
SimpleMapper INSTANCE = Mappers.getMapper( SimpleMapper.class );

View File

@ -9,6 +9,9 @@ import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;
/**
* @author Sjaak Derksen
*/
@Mapper
public interface SimpleMapperWithIgnore {
SimpleMapperWithIgnore INSTANCE = Mappers.getMapper( SimpleMapperWithIgnore.class );

View File

@ -8,10 +8,16 @@ package org.mapstruct.ap.test.targetthis;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mapstruct.ap.testutil.WithClasses;
import org.mapstruct.ap.testutil.compilation.annotation.CompilationResult;
import org.mapstruct.ap.testutil.compilation.annotation.Diagnostic;
import org.mapstruct.ap.testutil.compilation.annotation.ExpectedCompilationOutcome;
import org.mapstruct.ap.testutil.runner.AnnotationProcessorTestRunner;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Dainius Figoras
*/
@RunWith(AnnotationProcessorTestRunner.class)
@WithClasses( {
OrderDTO.class,
@ -82,4 +88,72 @@ public class TargetThisMappingTest {
assertThat( c.getId() ).isNull();
assertThat( c.getStatus() ).isEqualTo( ce.getItem().getStatus() );
}
@Test
@WithClasses( ErroneousNestedMapper.class )
@ExpectedCompilationOutcome(
value = CompilationResult.FAILED,
diagnostics = {
@Diagnostic(type = ErroneousNestedMapper.class,
kind = javax.tools.Diagnostic.Kind.ERROR,
line = 22,
messageRegExp = "^Several possible source properties for target property \"id\"\\.$"),
@Diagnostic(type = ErroneousNestedMapper.class,
kind = javax.tools.Diagnostic.Kind.ERROR,
line = 22,
messageRegExp = "^Several possible source properties for target property \"status\"\\.$")
}
)
public void testNestedDuplicates() {
}
@Test
@WithClasses( ConfictsResolvedNestedMapper.class )
public void testWithConflictsResolved() {
OrderDTO orderDTO = new OrderDTO();
orderDTO.setItem( new ItemDTO() );
orderDTO.getItem().setId( "item1" );
orderDTO.getItem().setStatus( 1 );
orderDTO.setCustomer( new CustomerDTO() );
orderDTO.getCustomer().setName( "customer name" );
orderDTO.getCustomer().setItem( new ItemDTO() );
orderDTO.getCustomer().getItem().setId( "item2" );
orderDTO.getCustomer().getItem().setStatus( 2 );
OrderItem c = ConfictsResolvedNestedMapper.INSTANCE.map( orderDTO );
assertThat( c ).isNotNull();
assertThat( c.getStatus() ).isEqualTo( orderDTO.getItem().getStatus() );
assertThat( c.getId() ).isEqualTo( orderDTO.getCustomer().getItem().getId() );
}
@Test
@WithClasses( FlatteningMapper.class )
public void testFlattening() {
FlatteningMapper.CustomerDTO customerDTO = new FlatteningMapper.CustomerDTO();
customerDTO.setName( new FlatteningMapper.NameDTO() );
customerDTO.getName().setName( "john doe" );
customerDTO.getName().setId( "1" );
customerDTO.setAccount( new FlatteningMapper.AccountDTO() );
customerDTO.getAccount().setDetails( "nice guys" );
customerDTO.getAccount().setNumber( "11223344" );
FlatteningMapper.Customer customer = FlatteningMapper.INSTANCE.flatten( customerDTO );
assertThat( customer ).isNotNull();
assertThat( customer.getName() ).isEqualTo( "john doe" );
assertThat( customer.getId() ).isEqualTo( "1" );
assertThat( customer.getDetails() ).isEqualTo( "nice guys" );
assertThat( customer.getNumber() ).isEqualTo( "11223344" );
FlatteningMapper.CustomerDTO customerDTO2 = FlatteningMapper.INSTANCE.expand( customer );
assertThat( customerDTO2 ).isNotNull();
assertThat( customerDTO2.getName().getName() ).isEqualTo( "john doe" );
assertThat( customerDTO2.getName().getId() ).isEqualTo( "1" );
assertThat( customerDTO2.getAccount().getDetails() ).isEqualTo( "nice guys" );
assertThat( customerDTO2.getAccount().getNumber() ).isEqualTo( "11223344" );
}
}