#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)
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.METHOD)
@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE })
public @interface Mapping {
/**

View File

@ -42,7 +42,7 @@ import java.lang.annotation.Target;
* @author Gunnar Morling
*/
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.METHOD)
@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE })
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.
[[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 to mappers

View File

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

View File

@ -11,6 +11,9 @@ import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
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.Modifier;
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>> {
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 TypeFactory typeFactory;
private AccessorNamingUtils accessorNaming;
@ -243,7 +251,7 @@ public class MethodRetrievalProcessor implements ModelElementProcessor<Void, Lis
.setParameters( parameters )
.setReturnType( returnType )
.setExceptionTypes( exceptionTypes )
.setMappings( getMappings( method ) )
.setMappings( getMappings( method, method, new LinkedHashSet<>(), new HashSet<>() ) )
.setIterableMapping(
IterableMapping.fromPrism(
IterableMappingPrism.getInstanceOn( method ),
@ -492,26 +500,52 @@ public class MethodRetrievalProcessor implements ModelElementProcessor<Void, Lis
* method.
*
* @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
*/
private Set<Mapping> getMappings(ExecutableElement method) {
Set<Mapping> mappings = new LinkedHashSet<>( );
private Set<Mapping> getMappings(ExecutableElement method, Element element, Set<Mapping> mappings,
Set<Element> handledElements) {
MappingPrism mappingAnnotation = MappingPrism.getInstanceOn( method );
MappingsPrism mappingsAnnotation = MappingsPrism.getInstanceOn( method );
if ( mappingAnnotation != null ) {
mappings.add( Mapping.fromMappingPrism( mappingAnnotation, method, messager, typeUtils ) );
for ( AnnotationMirror annotationMirror : element.getAnnotationMirrors() ) {
Element lElement = annotationMirror.getAnnotationType().asElement();
if ( isAnnotation( lElement, MAPPING_FQN ) ) {
// although getInstanceOn does a search on annotation mirrors, the order is preserved
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;
}
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
* 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 { }