Skip to content

Commit 2d7ab08

Browse files
committed
mapstruct#304 Allowing to configure dependencies between properties via @mapping#dependsOn()
1 parent 9b88884 commit 2d7ab08

14 files changed

Lines changed: 957 additions & 23 deletions

File tree

core-jdk8/src/main/java/org/mapstruct/Mapping.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,10 +137,21 @@
137137
*/
138138
Class<? extends Annotation>[] qualifiedBy() default { };
139139

140-
/**
140+
/**
141141
* Specifies the result type of the mapping method to be used in case multiple mapping methods qualify.
142142
*
143143
* @return the resultType to select
144144
*/
145145
Class<?> resultType() default void.class;
146+
147+
/**
148+
* One or more properties of the result type on which the mapped property depends. The generated method
149+
* implementation will invoke the setters of the result type ordered so that the given dependency relationship(s)
150+
* are satisfied. Useful in case one property setter depends on the state of another property of the result type.
151+
* <p>
152+
* An error will be raised in case a cycle in the dependency relationships is detected.
153+
*
154+
* @return the dependencies of the mapped property
155+
*/
156+
String[] dependsOn() default { };
146157
}

core/src/main/java/org/mapstruct/Mapping.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,4 +141,15 @@
141141
* @return the resultType to select
142142
*/
143143
Class<?> resultType() default void.class;
144+
145+
/**
146+
* One or more properties of the result type on which the mapped property depends. The generated method
147+
* implementation will invoke the setters of the result type ordered so that the given dependency relationship(s)
148+
* are satisfied. Useful in case one property setter depends on the state of another property of the result type.
149+
* <p>
150+
* An error will be raised in case a cycle in the dependency relationships is detected.
151+
*
152+
* @return the dependencies of the mapped property
153+
*/
154+
String[] dependsOn() default { };
144155
}

processor/src/main/java/org/mapstruct/ap/model/BeanMappingMethod.java

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
import java.text.MessageFormat;
2222
import java.util.ArrayList;
2323
import java.util.Collection;
24+
import java.util.Collections;
25+
import java.util.Comparator;
2426
import java.util.HashMap;
2527
import java.util.HashSet;
2628
import java.util.Iterator;
@@ -38,6 +40,7 @@
3840
import org.mapstruct.ap.model.PropertyMapping.PropertyMappingBuilder;
3941
import org.mapstruct.ap.model.common.Parameter;
4042
import org.mapstruct.ap.model.common.Type;
43+
import org.mapstruct.ap.model.dependency.GraphAnalyzer;
4144
import org.mapstruct.ap.model.source.Mapping;
4245
import org.mapstruct.ap.model.source.SourceMethod;
4346
import org.mapstruct.ap.model.source.SourceReference;
@@ -146,11 +149,14 @@ public BeanMappingMethod build() {
146149
if ( !resultType.isAssignableTo( method.getResultType() ) ) {
147150
ctx.getMessager().printMessage( method.getExecutable(),
148151
beanMappingPrism.mirror,
149-
Message.BEANMAPPING_NOT_ASSIGNABLE, resultType, method.getResultType());
152+
Message.BEANMAPPING_NOT_ASSIGNABLE, resultType, method.getResultType()
153+
);
150154
}
151155
}
152156
}
153157

158+
sortPropertyMappingsByDependencies();
159+
154160
return new BeanMappingMethod(
155161
method,
156162
propertyMappings,
@@ -161,6 +167,34 @@ public BeanMappingMethod build() {
161167
);
162168
}
163169

170+
private void sortPropertyMappingsByDependencies() {
171+
final GraphAnalyzer graphAnalyzer = new GraphAnalyzer();
172+
173+
for ( PropertyMapping propertyMapping : propertyMappings ) {
174+
graphAnalyzer.addNode( propertyMapping.getName(), propertyMapping.getDependsOn() );
175+
}
176+
177+
graphAnalyzer.analyze();
178+
179+
Collections.sort(
180+
propertyMappings, new Comparator<PropertyMapping>() {
181+
182+
@Override
183+
public int compare(PropertyMapping o1, PropertyMapping o2) {
184+
if ( graphAnalyzer.getAllDescendants( o1.getName() ).contains( o2.getName() ) ) {
185+
return 1;
186+
}
187+
else if ( graphAnalyzer.getAllDescendants( o2.getName() ).contains( o1.getName() ) ) {
188+
return -1;
189+
}
190+
else {
191+
return 0;
192+
}
193+
}
194+
}
195+
);
196+
}
197+
164198
/**
165199
* Iterates over all defined mapping methods ({@code @Mapping(s)}), either directly given or inherited from the
166200
* inverse mapping method.
@@ -221,6 +255,7 @@ else if ( mapping.getSourceName() != null ) {
221255
.resultType( mapping.getResultType() )
222256
.dateFormat( mapping.getDateFormat() )
223257
.existingVariableNames( existingVariableNames )
258+
.dependsOn( mapping.getDependsOn() )
224259
.build();
225260
handledTargets.add( mapping.getTargetName() );
226261
unprocessedSourceParameters.remove( sourceRef.getParameter() );
@@ -239,10 +274,12 @@ else if ( mapping.getConstant() != null && targetProperty != null ) {
239274
.sourceMethod( method )
240275
.constantExpression( "\"" + mapping.getConstant() + "\"" )
241276
.targetAccessor( targetProperty )
277+
.targetPropertyName( mapping.getTargetName() )
242278
.dateFormat( mapping.getDateFormat() )
243279
.qualifiers( mapping.getQualifiers() )
244280
.resultType( mapping.getResultType() )
245281
.existingVariableNames( existingVariableNames )
282+
.dependsOn( mapping.getDependsOn() )
246283
.build();
247284
handledTargets.add( mapping.getTargetName() );
248285
}
@@ -256,6 +293,8 @@ else if ( mapping.getJavaExpression() != null && targetProperty != null ) {
256293
.javaExpression( mapping.getJavaExpression() )
257294
.targetAccessor( targetProperty )
258295
.existingVariableNames( existingVariableNames )
296+
.targetPropertyName( mapping.getTargetName() )
297+
.dependsOn( mapping.getDependsOn() )
259298
.build();
260299
handledTargets.add( mapping.getTargetName() );
261300
}
@@ -333,6 +372,7 @@ private void applyPropertyNameBasedMapping() {
333372
.resultType( mapping != null ? mapping.getResultType() : null )
334373
.dateFormat( mapping != null ? mapping.getDateFormat() : null )
335374
.existingVariableNames( existingVariableNames )
375+
.dependsOn( mapping != null ? mapping.getDependsOn() : Collections.<String>emptyList() )
336376
.build();
337377

338378
unprocessedSourceParameters.remove( sourceParameter );
@@ -394,6 +434,7 @@ private void applyParameterNameBasedMapping() {
394434
.resultType( mapping != null ? mapping.getResultType() : null )
395435
.dateFormat( mapping != null ? mapping.getDateFormat() : null )
396436
.existingVariableNames( existingVariableNames )
437+
.dependsOn( mapping != null ? mapping.getDependsOn() : Collections.<String>emptyList() )
397438
.build();
398439

399440
propertyMappings.add( propertyMapping );

processor/src/main/java/org/mapstruct/ap/model/PropertyMapping.java

Lines changed: 74 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,10 @@
2020

2121
import java.util.Arrays;
2222
import java.util.Collection;
23+
import java.util.Collections;
2324
import java.util.List;
2425
import java.util.Set;
26+
2527
import javax.lang.model.element.ExecutableElement;
2628
import javax.lang.model.type.TypeMirror;
2729

@@ -31,8 +33,8 @@
3133
import org.mapstruct.ap.model.assignment.GetterWrapperForCollectionsAndMaps;
3234
import org.mapstruct.ap.model.assignment.NewCollectionOrMapWrapper;
3335
import org.mapstruct.ap.model.assignment.NullCheckWrapper;
34-
import org.mapstruct.ap.model.assignment.SetterWrapperForCollectionsAndMaps;
3536
import org.mapstruct.ap.model.assignment.SetterWrapper;
37+
import org.mapstruct.ap.model.assignment.SetterWrapperForCollectionsAndMaps;
3638
import org.mapstruct.ap.model.common.ModelElement;
3739
import org.mapstruct.ap.model.common.Parameter;
3840
import org.mapstruct.ap.model.common.Type;
@@ -41,12 +43,12 @@
4143
import org.mapstruct.ap.model.source.SourceReference;
4244
import org.mapstruct.ap.model.source.SourceReference.PropertyEntry;
4345
import org.mapstruct.ap.util.Executables;
46+
import org.mapstruct.ap.util.Message;
4447
import org.mapstruct.ap.util.Strings;
4548

4649
import static org.mapstruct.ap.model.assignment.Assignment.AssignmentType.DIRECT;
4750
import static org.mapstruct.ap.model.assignment.Assignment.AssignmentType.TYPE_CONVERTED;
4851
import static org.mapstruct.ap.model.assignment.Assignment.AssignmentType.TYPE_CONVERTED_MAPPED;
49-
import org.mapstruct.ap.util.Message;
5052

5153
/**
5254
* Represents the mapping between a source and target property, e.g. from {@code String Source#foo} to
@@ -57,10 +59,12 @@
5759
*/
5860
public class PropertyMapping extends ModelElement {
5961

62+
private final String name;
6063
private final String sourceBeanName;
6164
private final String targetAccessorName;
6265
private final Type targetType;
6366
private final Assignment assignment;
67+
private final List<String> dependsOn;
6468

6569
public static class PropertyMappingBuilder {
6670

@@ -74,6 +78,7 @@ public static class PropertyMappingBuilder {
7478
private TypeMirror resultType;
7579
private SourceReference sourceReference;
7680
private Collection<String> existingVariableNames;
81+
private List<String> dependsOn;
7782

7883
public PropertyMappingBuilder mappingContext(MappingBuilderContext mappingContext) {
7984
this.ctx = mappingContext;
@@ -120,6 +125,11 @@ public PropertyMappingBuilder existingVariableNames(Collection<String> existingV
120125
return this;
121126
}
122127

128+
public PropertyMappingBuilder dependsOn(List<String> dependsOn) {
129+
this.dependsOn = dependsOn;
130+
return this;
131+
}
132+
123133
private enum TargetAccessorType {
124134

125135
GETTER,
@@ -195,10 +205,12 @@ else if ( targetType.isArrayType() && sourceType.isArrayType() && assignment.get
195205
}
196206

197207
return new PropertyMapping(
208+
targetPropertyName,
198209
sourceReference.getParameter().getName(),
199210
targetAccessor.getSimpleName().toString(),
200211
targetType,
201-
assignment
212+
assignment,
213+
dependsOn
202214
);
203215
}
204216

@@ -465,10 +477,12 @@ public static class ConstantMappingBuilder {
465477
private SourceMethod method;
466478
private String constantExpression;
467479
private ExecutableElement targetAccessor;
480+
private String targetPropertyName;
468481
private String dateFormat;
469482
private List<TypeMirror> qualifiers;
470483
private TypeMirror resultType;
471484
private Collection<String> existingVariableNames;
485+
private List<String> dependsOn;
472486

473487
public ConstantMappingBuilder mappingContext(MappingBuilderContext mappingContext) {
474488
this.ctx = mappingContext;
@@ -490,6 +504,11 @@ public ConstantMappingBuilder targetAccessor(ExecutableElement targetAccessor) {
490504
return this;
491505
}
492506

507+
public ConstantMappingBuilder targetPropertyName(String targetPropertyName) {
508+
this.targetPropertyName = targetPropertyName;
509+
return this;
510+
}
511+
493512
public ConstantMappingBuilder dateFormat(String dateFormat) {
494513
this.dateFormat = dateFormat;
495514
return this;
@@ -510,6 +529,11 @@ public ConstantMappingBuilder existingVariableNames(Collection<String> existingV
510529
return this;
511530
}
512531

532+
public ConstantMappingBuilder dependsOn(List<String> dependsOn) {
533+
this.dependsOn = dependsOn;
534+
return this;
535+
}
536+
513537
public PropertyMapping build() {
514538

515539
// source
@@ -525,8 +549,6 @@ public PropertyMapping build() {
525549
targetType = ctx.getTypeFactory().getReturnType( targetAccessor );
526550
}
527551

528-
String targetPropertyName = Executables.getPropertyName( targetAccessor );
529-
530552
Assignment assignment = ctx.getMappingResolver().getTargetAssignment(
531553
method,
532554
mappedElement,
@@ -565,7 +587,13 @@ public PropertyMapping build() {
565587
);
566588
}
567589

568-
return new PropertyMapping( targetAccessor.getSimpleName().toString(), targetType, assignment );
590+
return new PropertyMapping(
591+
targetPropertyName,
592+
targetAccessor.getSimpleName().toString(),
593+
targetType,
594+
assignment,
595+
dependsOn
596+
);
569597
}
570598
}
571599

@@ -576,6 +604,8 @@ public static class JavaExpressionMappingBuilder {
576604
private String javaExpression;
577605
private ExecutableElement targetAccessor;
578606
private Collection<String> existingVariableNames;
607+
private String targetPropertyName;
608+
private List<String> dependsOn;
579609

580610
public JavaExpressionMappingBuilder mappingContext(MappingBuilderContext mappingContext) {
581611
this.ctx = mappingContext;
@@ -602,6 +632,16 @@ public JavaExpressionMappingBuilder existingVariableNames(Collection<String> exi
602632
return this;
603633
}
604634

635+
public JavaExpressionMappingBuilder targetPropertyName(String targetPropertyName) {
636+
this.targetPropertyName = targetPropertyName;
637+
return this;
638+
}
639+
640+
public JavaExpressionMappingBuilder dependsOn(List<String> dependsOn) {
641+
this.dependsOn = dependsOn;
642+
return this;
643+
}
644+
605645
public PropertyMapping build() {
606646

607647
Assignment assignment = AssignmentFactory.createDirect( javaExpression );
@@ -623,24 +663,38 @@ public PropertyMapping build() {
623663
);
624664
}
625665

626-
return new PropertyMapping( targetAccessor.getSimpleName().toString(), targetType, assignment );
666+
return new PropertyMapping(
667+
targetPropertyName,
668+
targetAccessor.getSimpleName().toString(),
669+
targetType,
670+
assignment,
671+
dependsOn
672+
);
627673
}
628674

629675
}
630676

631677
// Constructor for creating mappings of constant expressions.
632-
private PropertyMapping(String targetAccessorName, Type targetType, Assignment propertyAssignment) {
633-
this( null, targetAccessorName, targetType, propertyAssignment );
678+
private PropertyMapping(String name, String targetAccessorName, Type targetType, Assignment propertyAssignment,
679+
List<String> dependsOn) {
680+
this( name, null, targetAccessorName, targetType, propertyAssignment, dependsOn );
634681
}
635682

636-
private PropertyMapping(String sourceBeanName, String targetAccessorName, Type targetType, Assignment assignment) {
637-
683+
private PropertyMapping(String name, String sourceBeanName, String targetAccessorName, Type targetType,
684+
Assignment assignment, List<String> dependsOn) {
685+
this.name = name;
638686
this.sourceBeanName = sourceBeanName;
639-
640687
this.targetAccessorName = targetAccessorName;
641688
this.targetType = targetType;
642-
643689
this.assignment = assignment;
690+
this.dependsOn = dependsOn != null ? dependsOn : Collections.<String>emptyList();
691+
}
692+
693+
/**
694+
* Returns the name of this mapping (property name on the target side)
695+
*/
696+
public String getName() {
697+
return name;
644698
}
645699

646700
public String getSourceBeanName() {
@@ -664,12 +718,17 @@ public Set<Type> getImportTypes() {
664718
return assignment.getImportTypes();
665719
}
666720

721+
public List<String> getDependsOn() {
722+
return dependsOn;
723+
}
724+
667725
@Override
668726
public String toString() {
669727
return "PropertyMapping {"
670-
+ "\n targetName='" + targetAccessorName + "\',"
728+
+ "\n name='" + name + "\',"
671729
+ "\n targetType=" + targetType + ","
672-
+ "\n propertyAssignment=" + assignment
730+
+ "\n propertyAssignment=" + assignment + ","
731+
+ "\n dependsOn=" + dependsOn
673732
+ "\n}";
674733
}
675734
}

0 commit comments

Comments
 (0)