#807 meta annotations and duck typing (#1979)

This commit is contained in:
Sjaak Derksen 2019-12-07 22:20:11 +01:00 committed by GitHub
parent 071e5dc6b2
commit ee794d042c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 430 additions and 37 deletions

View File

@ -142,7 +142,7 @@ import static org.mapstruct.NullValueCheckStrategy.ON_IMPLICIT_CONVERSION;
@Repeatable(Mappings.class) @Repeatable(Mappings.class)
@Retention(RetentionPolicy.CLASS) @Retention(RetentionPolicy.CLASS)
@Target(ElementType.METHOD) @Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE })
public @interface Mapping { public @interface Mapping {
/** /**

View File

@ -42,7 +42,7 @@ import java.lang.annotation.Target;
* @author Gunnar Morling * @author Gunnar Morling
*/ */
@Retention(RetentionPolicy.CLASS) @Retention(RetentionPolicy.CLASS)
@Target(ElementType.METHOD) @Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE })
public @interface Mappings { public @interface Mappings {
/** /**

View File

@ -124,6 +124,50 @@ Collection-typed attributes with the same element type will be copied by creatin
MapStruct takes all public properties of the source and target types into account. This includes properties declared on super-types. MapStruct takes all public properties of the source and target types into account. This includes properties declared on super-types.
[[mapping-composition]]
=== Mapping Composition (experimental)
MapStruct supports the use of meta annotations. The `@Mapping` annotation supports now `@Target` with `ElementType#ANNOTATION_TYPE` in addition to `ElementType#METHOD`. This allows `@Mapping` to be used on other (user defined) annotations for re-use purposes. For example:
====
[source, java, linenums]
[subs="verbatim,attributes"]
----
@Retention(RetentionPolicy.CLASS)
@Mapping(target = "id", ignore = true)
@Mapping(target = "creationDate", expression = "java(new java.util.Date())")
@Mapping(target = "name", source = "groupName")
public @interface ToEntity { }
----
====
Can be used to characterise an `Entity` without the need to have a common base type. For instance, `ShelveEntity` and `BoxEntity` do not share a common base type in the `StorageMapper` below.
====
[source, java, linenums]
[subs="verbatim,attributes"]
----
@Mapper
public interface StorageMapper {
StorageMapper INSTANCE = Mappers.getMapper( StorageMapper.class );
@ToEntity
@Mapping( target = "weightLimit", source = "maxWeight")
ShelveEntity map(ShelveDto source);
@ToEntity
@Mapping( target = "label", source = "designation")
BoxEntity map(BoxDto source);
}
----
====
Still, they do have some properties in common. The `@ToEntity` assumes both target beans `ShelveEntity` and `BoxEntity` have properties: `"id"`, `"creationDate"` and `"name"`. It furthermore assumes that the source beans `ShelveDto` and `BoxDto` always have a property `"groupName"`. This concept is also known as "duck-typing". In other words, if it quacks like duck, walks like a duck its probably a duck.
This feature is still experimental. Error messages are not mature yet: the method on which the problem occurs is displayed, as well as the concerned values in the `@Mapping` annotation. However, the composition aspect is not visible. The messages are "as if" the `@Mapping` would be present on the concerned method directly.
Therefore, the user should use this feature with care, especially when uncertain when a property is always present.
A more typesafe (but also more verbose) way would be to define base classes / interfaces on the target bean and the source bean and use `@InheritConfiguration` to achieve the same result (see <<mapping-configuration-inheritance>>).
[[adding-custom-methods]] [[adding-custom-methods]]
=== Adding custom methods to mappers === Adding custom methods to mappers

View File

@ -92,36 +92,25 @@ public class Mapping {
return mappings.stream().filter( mapping -> mapping.targetName.equals( targetName ) ).findAny().orElse( null ); return mappings.stream().filter( mapping -> mapping.targetName.equals( targetName ) ).findAny().orElse( null );
} }
public static Set<Mapping> fromMappingsPrism(MappingsPrism mappingsAnnotation, ExecutableElement method, public static void addFromMappingsPrism(MappingsPrism mappingsAnnotation, ExecutableElement method,
FormattingMessager messager, Types typeUtils) { FormattingMessager messager, Types typeUtils, Set<Mapping> mappings) {
Set<Mapping> mappings = new LinkedHashSet<>();
for ( MappingPrism mappingPrism : mappingsAnnotation.value() ) { for ( MappingPrism mappingPrism : mappingsAnnotation.value() ) {
Mapping mapping = fromMappingPrism( mappingPrism, method, messager, typeUtils ); addFromMappingPrism( mappingPrism, method, messager, typeUtils, mappings );
if ( mapping != null ) {
if ( mappings.contains( mapping ) ) {
messager.printMessage( method, Message.PROPERTYMAPPING_DUPLICATE_TARGETS, mappingPrism.target() );
}
else {
mappings.add( mapping );
}
}
} }
return mappings;
} }
public static Mapping fromMappingPrism(MappingPrism mappingPrism, ExecutableElement element, public static void addFromMappingPrism(MappingPrism mappingPrism, ExecutableElement method,
FormattingMessager messager, Types typeUtils) { FormattingMessager messager, Types typeUtils, Set<Mapping> mappings) {
if (!isConsistent( mappingPrism, element, messager ) ) { if (!isConsistent( mappingPrism, method, messager ) ) {
return null; return;
} }
String source = mappingPrism.source().isEmpty() ? null : mappingPrism.source(); String source = mappingPrism.source().isEmpty() ? null : mappingPrism.source();
String constant = mappingPrism.values.constant() == null ? null : mappingPrism.constant(); String constant = mappingPrism.values.constant() == null ? null : mappingPrism.constant();
String expression = getExpression( mappingPrism, element, messager ); String expression = getExpression( mappingPrism, method, messager );
String defaultExpression = getDefaultExpression( mappingPrism, element, messager ); String defaultExpression = getDefaultExpression( mappingPrism, method, messager );
String dateFormat = mappingPrism.values.dateFormat() == null ? null : mappingPrism.dateFormat(); String dateFormat = mappingPrism.values.dateFormat() == null ? null : mappingPrism.dateFormat();
String numberFormat = mappingPrism.values.numberFormat() == null ? null : mappingPrism.numberFormat(); String numberFormat = mappingPrism.values.numberFormat() == null ? null : mappingPrism.numberFormat();
String defaultValue = mappingPrism.values.defaultValue() == null ? null : mappingPrism.defaultValue(); String defaultValue = mappingPrism.values.defaultValue() == null ? null : mappingPrism.defaultValue();
@ -136,7 +125,7 @@ public class Mapping {
numberFormat, numberFormat,
mappingPrism.mirror, mappingPrism.mirror,
mappingPrism.values.dateFormat(), mappingPrism.values.dateFormat(),
element method
); );
SelectionParameters selectionParams = new SelectionParameters( SelectionParameters selectionParams = new SelectionParameters(
mappingPrism.qualifiedBy(), mappingPrism.qualifiedBy(),
@ -155,7 +144,7 @@ public class Mapping {
? null ? null
: NullValuePropertyMappingStrategyPrism.valueOf( mappingPrism.nullValuePropertyMappingStrategy() ); : NullValuePropertyMappingStrategyPrism.valueOf( mappingPrism.nullValuePropertyMappingStrategy() );
return new Mapping( Mapping mapping = new Mapping(
source, source,
constant, constant,
expression, expression,
@ -174,6 +163,13 @@ public class Mapping {
nullValuePropertyMappingStrategy, nullValuePropertyMappingStrategy,
null null
); );
if ( mappings.contains( mapping ) ) {
messager.printMessage( method, Message.PROPERTYMAPPING_DUPLICATE_TARGETS, mappingPrism.target() );
}
else {
mappings.add( mapping );
}
} }
public static Mapping forIgnore( String targetName) { public static Mapping forIgnore( String targetName) {

View File

@ -11,6 +11,9 @@ import java.util.HashSet;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier; import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement; import javax.lang.model.element.TypeElement;
@ -57,6 +60,11 @@ import static org.mapstruct.ap.internal.util.Executables.getAllEnclosedExecutabl
*/ */
public class MethodRetrievalProcessor implements ModelElementProcessor<Void, List<SourceMethod>> { public class MethodRetrievalProcessor implements ModelElementProcessor<Void, List<SourceMethod>> {
private static final String JAVA_LANG_ANNOTATION_PGK = "java.lang.annotation";
private static final String ORG_MAPSTRUCT_PKG = "org.mapstruct";
private static final String MAPPING_FQN = "org.mapstruct.Mapping";
private static final String MAPPINGS_FQN = "org.mapstruct.Mappings";
private FormattingMessager messager; private FormattingMessager messager;
private TypeFactory typeFactory; private TypeFactory typeFactory;
private AccessorNamingUtils accessorNaming; private AccessorNamingUtils accessorNaming;
@ -243,7 +251,7 @@ public class MethodRetrievalProcessor implements ModelElementProcessor<Void, Lis
.setParameters( parameters ) .setParameters( parameters )
.setReturnType( returnType ) .setReturnType( returnType )
.setExceptionTypes( exceptionTypes ) .setExceptionTypes( exceptionTypes )
.setMappings( getMappings( method ) ) .setMappings( getMappings( method, method, new LinkedHashSet<>(), new HashSet<>() ) )
.setIterableMapping( .setIterableMapping(
IterableMapping.fromPrism( IterableMapping.fromPrism(
IterableMappingPrism.getInstanceOn( method ), IterableMappingPrism.getInstanceOn( method ),
@ -492,26 +500,52 @@ public class MethodRetrievalProcessor implements ModelElementProcessor<Void, Lis
* method. * method.
* *
* @param method The method of interest * @param method The method of interest
* @param element Element of interest: method, or (meta) annotation
* @param mappings LinkedSet of mappings found so far
* *
* @return The mappings for the given method, keyed by target property name * @return The mappings for the given method, keyed by target property name
*/ */
private Set<Mapping> getMappings(ExecutableElement method) { private Set<Mapping> getMappings(ExecutableElement method, Element element, Set<Mapping> mappings,
Set<Mapping> mappings = new LinkedHashSet<>( ); Set<Element> handledElements) {
MappingPrism mappingAnnotation = MappingPrism.getInstanceOn( method ); for ( AnnotationMirror annotationMirror : element.getAnnotationMirrors() ) {
MappingsPrism mappingsAnnotation = MappingsPrism.getInstanceOn( method ); Element lElement = annotationMirror.getAnnotationType().asElement();
if ( isAnnotation( lElement, MAPPING_FQN ) ) {
if ( mappingAnnotation != null ) { // although getInstanceOn does a search on annotation mirrors, the order is preserved
mappings.add( Mapping.fromMappingPrism( mappingAnnotation, method, messager, typeUtils ) ); MappingPrism mappingAnnotation = MappingPrism.getInstanceOn( element );
Mapping.addFromMappingPrism( mappingAnnotation, method, messager, typeUtils, mappings );
}
else if ( isAnnotation( lElement, MAPPINGS_FQN ) ) {
// although getInstanceOn does a search on annotation mirrors, the order is preserved
MappingsPrism mappingsAnnotation = MappingsPrism.getInstanceOn( element );
Mapping.addFromMappingsPrism( mappingsAnnotation, method, messager, typeUtils, mappings );
}
else if ( !isAnnotationInPackage( lElement, JAVA_LANG_ANNOTATION_PGK )
&& !isAnnotationInPackage( lElement, ORG_MAPSTRUCT_PKG )
&& !handledElements.contains( lElement )
) {
// recur over annotation mirrors
handledElements.add( lElement );
getMappings( method, lElement, mappings, handledElements );
}
} }
if ( mappingsAnnotation != null ) {
mappings.addAll( Mapping.fromMappingsPrism( mappingsAnnotation, method, messager, typeUtils ) );
}
return mappings; return mappings;
} }
private boolean isAnnotationInPackage(Element element, String packageFQN ) {
if ( ElementKind.ANNOTATION_TYPE == element.getKind() ) {
return packageFQN.equals( elementUtils.getPackageOf( element ).getQualifiedName().toString() );
}
return false;
}
private boolean isAnnotation(Element element, String annotationFQN) {
if ( ElementKind.ANNOTATION_TYPE == element.getKind() ) {
return annotationFQN.equals( ( (TypeElement) element ).getQualifiedName().toString() );
}
return false;
}
/** /**
* Retrieves the mappings configured via {@code @ValueMapping} from the given * Retrieves the mappings configured via {@code @ValueMapping} from the given
* method. * method.

View File

@ -0,0 +1,37 @@
/*
* 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.mappingcomposition;
public class BoxDto {
private String groupName;
private String designation;
private ShelveDto shelve;
public String getGroupName() {
return groupName;
}
public void setGroupName(String groupName) {
this.groupName = groupName;
}
public String getDesignation() {
return designation;
}
public void setDesignation(String designation) {
this.designation = designation;
}
public ShelveDto getShelve() {
return shelve;
}
public void setShelve(ShelveDto shelve) {
this.shelve = shelve;
}
}

View File

@ -0,0 +1,57 @@
/*
* 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.mappingcomposition;
import java.util.Date;
public class BoxEntity {
private Date creationDate;
private Long id;
private String name;
private String label;
private ShelveEntity shelve;
public Date getCreationDate() {
return creationDate;
}
public void setCreationDate(Date creationDate) {
this.creationDate = creationDate;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getLabel() {
return label;
}
public void setLabel(String label) {
this.label = label;
}
public ShelveEntity getShelve() {
return shelve;
}
public void setShelve(ShelveEntity shelve) {
this.shelve = shelve;
}
}

View File

@ -0,0 +1,71 @@
/*
* 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.mappingcomposition;
import java.util.Date;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mapstruct.ap.testutil.IssueKey;
import org.mapstruct.ap.testutil.WithClasses;
import org.mapstruct.ap.testutil.runner.AnnotationProcessorTestRunner;
import static org.assertj.core.api.Assertions.assertThat;
/**
*
* @author Sjaak Derksen
*/
@IssueKey("807")
@WithClasses({
BoxDto.class,
BoxEntity.class,
ShelveDto.class,
ShelveEntity.class,
StorageMapper.class,
ToEntity.class
})
@RunWith(AnnotationProcessorTestRunner.class)
public class CompositionTest {
@Test
public void shouldCompose() {
Date now = new Date();
Date anHourAgo = new Date( now.getTime() - 3600 * 1000 );
Date anHourFromNow = new Date( now.getTime() + 3600 * 1000 );
ShelveDto shelve = new ShelveDto();
shelve.setGroupName( "blue things" );
shelve.setMaxWeight( 10.0d );
shelve.setPath( "row5" );
shelve.setStandNumber( 3 );
BoxDto box = new BoxDto();
box.setDesignation( "blue item" );
box.setGroupName( "blue stuff" );
box.setShelve( shelve );
BoxEntity boxEntity = StorageMapper.INSTANCE.map( box );
// box
assertThat( boxEntity ).isNotNull();
assertThat( boxEntity.getCreationDate() ).isBetween( anHourAgo, anHourFromNow );
assertThat( boxEntity.getId() ).isNull();
assertThat( boxEntity.getName() ).isEqualTo( "blue stuff" );
assertThat( boxEntity.getLabel() ).isEqualTo( "blue item" );
// shelve
ShelveEntity shelveEntity = boxEntity.getShelve();
assertThat( shelveEntity.getCreationDate() ).isBetween( anHourAgo, anHourFromNow );
assertThat( shelveEntity.getId() ).isNull();
assertThat( shelveEntity.getName() ).isEqualTo( "blue things" );
assertThat( shelveEntity.getPath() ).isEqualTo( "row5" );
assertThat( shelveEntity.getStandNumber() ).isEqualTo( 3 );
assertThat( shelveEntity.getWeightLimit() ).isEqualTo( 10.0d );
}
}

View File

@ -0,0 +1,46 @@
/*
* 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.mappingcomposition;
public class ShelveDto {
private String groupName;
private String path;
private int standNumber;
private double maxWeight;
public String getGroupName() {
return groupName;
}
public void setGroupName(String groupName) {
this.groupName = groupName;
}
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
public int getStandNumber() {
return standNumber;
}
public void setStandNumber(int standNumber) {
this.standNumber = standNumber;
}
public double getMaxWeight() {
return maxWeight;
}
public void setMaxWeight(double maxWeight) {
this.maxWeight = maxWeight;
}
}

View File

@ -0,0 +1,67 @@
/*
* 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.mappingcomposition;
import java.util.Date;
public class ShelveEntity {
private Date creationDate;
private Long id;
private String name;
private String path;
private int standNumber;
private double weightLimit;
public Date getCreationDate() {
return creationDate;
}
public void setCreationDate(Date creationDate) {
this.creationDate = creationDate;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
public int getStandNumber() {
return standNumber;
}
public void setStandNumber(int standNumber) {
this.standNumber = standNumber;
}
public double getWeightLimit() {
return weightLimit;
}
public void setWeightLimit(double weightLimit) {
this.weightLimit = weightLimit;
}
}

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.mappingcomposition;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;
@Mapper
public interface StorageMapper {
StorageMapper INSTANCE = Mappers.getMapper( StorageMapper.class );
@ToEntity
@Mapping( target = "weightLimit", source = "maxWeight")
ShelveEntity map(ShelveDto source);
@ToEntity
@Mapping( target = "label", source = "designation")
BoxEntity map(BoxDto source);
}

View File

@ -0,0 +1,17 @@
/*
* 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.mappingcomposition;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import org.mapstruct.Mapping;
@Retention(RetentionPolicy.CLASS)
@Mapping(target = "id", ignore = true)
@Mapping(target = "creationDate", expression = "java(new java.util.Date())")
@Mapping(target = "name", source = "groupName")
public @interface ToEntity { }