mirror of
https://github.com/mapstruct/mapstruct.git
synced 2025-07-12 00:00:08 +08:00
parent
071e5dc6b2
commit
ee794d042c
@ -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 {
|
||||
|
||||
/**
|
||||
|
@ -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 {
|
||||
|
||||
/**
|
||||
|
@ -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
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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.
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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 );
|
||||
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
@ -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 { }
|
Loading…
x
Reference in New Issue
Block a user