diff --git a/core-jdk8/src/main/java/org/mapstruct/Mapping.java b/core-jdk8/src/main/java/org/mapstruct/Mapping.java
index eba3d59f2..a61da963c 100644
--- a/core-jdk8/src/main/java/org/mapstruct/Mapping.java
+++ b/core-jdk8/src/main/java/org/mapstruct/Mapping.java
@@ -137,10 +137,21 @@ public @interface Mapping {
*/
Class extends Annotation>[] qualifiedBy() default { };
- /**
+ /**
* Specifies the result type of the mapping method to be used in case multiple mapping methods qualify.
*
* @return the resultType to select
*/
Class> resultType() default void.class;
+
+ /**
+ * One or more properties of the result type on which the mapped property depends. The generated method
+ * implementation will invoke the setters of the result type ordered so that the given dependency relationship(s)
+ * are satisfied. Useful in case one property setter depends on the state of another property of the result type.
+ *
+ * An error will be raised in case a cycle in the dependency relationships is detected.
+ *
+ * @return the dependencies of the mapped property
+ */
+ String[] dependsOn() default { };
}
diff --git a/core/src/main/java/org/mapstruct/Mapping.java b/core/src/main/java/org/mapstruct/Mapping.java
index 01e4ef2cc..5f7539c69 100644
--- a/core/src/main/java/org/mapstruct/Mapping.java
+++ b/core/src/main/java/org/mapstruct/Mapping.java
@@ -141,4 +141,15 @@ public @interface Mapping {
* @return the resultType to select
*/
Class> resultType() default void.class;
+
+ /**
+ * One or more properties of the result type on which the mapped property depends. The generated method
+ * implementation will invoke the setters of the result type ordered so that the given dependency relationship(s)
+ * are satisfied. Useful in case one property setter depends on the state of another property of the result type.
+ *
+ * An error will be raised in case a cycle in the dependency relationships is detected.
+ *
+ * @return the dependencies of the mapped property
+ */
+ String[] dependsOn() default { };
}
diff --git a/processor/src/main/java/org/mapstruct/ap/model/BeanMappingMethod.java b/processor/src/main/java/org/mapstruct/ap/model/BeanMappingMethod.java
index eb99589b1..772f000a5 100644
--- a/processor/src/main/java/org/mapstruct/ap/model/BeanMappingMethod.java
+++ b/processor/src/main/java/org/mapstruct/ap/model/BeanMappingMethod.java
@@ -21,6 +21,8 @@ package org.mapstruct.ap.model;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
@@ -38,6 +40,7 @@ import org.mapstruct.ap.model.PropertyMapping.JavaExpressionMappingBuilder;
import org.mapstruct.ap.model.PropertyMapping.PropertyMappingBuilder;
import org.mapstruct.ap.model.common.Parameter;
import org.mapstruct.ap.model.common.Type;
+import org.mapstruct.ap.model.dependency.GraphAnalyzer;
import org.mapstruct.ap.model.source.Mapping;
import org.mapstruct.ap.model.source.SourceMethod;
import org.mapstruct.ap.model.source.SourceReference;
@@ -146,11 +149,14 @@ public class BeanMappingMethod extends MappingMethod {
if ( !resultType.isAssignableTo( method.getResultType() ) ) {
ctx.getMessager().printMessage( method.getExecutable(),
beanMappingPrism.mirror,
- Message.BEANMAPPING_NOT_ASSIGNABLE, resultType, method.getResultType());
+ Message.BEANMAPPING_NOT_ASSIGNABLE, resultType, method.getResultType()
+ );
}
}
}
+ sortPropertyMappingsByDependencies();
+
return new BeanMappingMethod(
method,
propertyMappings,
@@ -161,6 +167,34 @@ public class BeanMappingMethod extends MappingMethod {
);
}
+ private void sortPropertyMappingsByDependencies() {
+ final GraphAnalyzer graphAnalyzer = new GraphAnalyzer();
+
+ for ( PropertyMapping propertyMapping : propertyMappings ) {
+ graphAnalyzer.addNode( propertyMapping.getName(), propertyMapping.getDependsOn() );
+ }
+
+ graphAnalyzer.analyze();
+
+ Collections.sort(
+ propertyMappings, new Comparator() {
+
+ @Override
+ public int compare(PropertyMapping o1, PropertyMapping o2) {
+ if ( graphAnalyzer.getAllDescendants( o1.getName() ).contains( o2.getName() ) ) {
+ return 1;
+ }
+ else if ( graphAnalyzer.getAllDescendants( o2.getName() ).contains( o1.getName() ) ) {
+ return -1;
+ }
+ else {
+ return 0;
+ }
+ }
+ }
+ );
+ }
+
/**
* Iterates over all defined mapping methods ({@code @Mapping(s)}), either directly given or inherited from the
* inverse mapping method.
@@ -221,6 +255,7 @@ public class BeanMappingMethod extends MappingMethod {
.resultType( mapping.getResultType() )
.dateFormat( mapping.getDateFormat() )
.existingVariableNames( existingVariableNames )
+ .dependsOn( mapping.getDependsOn() )
.build();
handledTargets.add( mapping.getTargetName() );
unprocessedSourceParameters.remove( sourceRef.getParameter() );
@@ -239,10 +274,12 @@ public class BeanMappingMethod extends MappingMethod {
.sourceMethod( method )
.constantExpression( "\"" + mapping.getConstant() + "\"" )
.targetAccessor( targetProperty )
+ .targetPropertyName( mapping.getTargetName() )
.dateFormat( mapping.getDateFormat() )
.qualifiers( mapping.getQualifiers() )
.resultType( mapping.getResultType() )
.existingVariableNames( existingVariableNames )
+ .dependsOn( mapping.getDependsOn() )
.build();
handledTargets.add( mapping.getTargetName() );
}
@@ -256,6 +293,8 @@ public class BeanMappingMethod extends MappingMethod {
.javaExpression( mapping.getJavaExpression() )
.targetAccessor( targetProperty )
.existingVariableNames( existingVariableNames )
+ .targetPropertyName( mapping.getTargetName() )
+ .dependsOn( mapping.getDependsOn() )
.build();
handledTargets.add( mapping.getTargetName() );
}
@@ -333,6 +372,7 @@ public class BeanMappingMethod extends MappingMethod {
.resultType( mapping != null ? mapping.getResultType() : null )
.dateFormat( mapping != null ? mapping.getDateFormat() : null )
.existingVariableNames( existingVariableNames )
+ .dependsOn( mapping != null ? mapping.getDependsOn() : Collections.emptyList() )
.build();
unprocessedSourceParameters.remove( sourceParameter );
@@ -394,6 +434,7 @@ public class BeanMappingMethod extends MappingMethod {
.resultType( mapping != null ? mapping.getResultType() : null )
.dateFormat( mapping != null ? mapping.getDateFormat() : null )
.existingVariableNames( existingVariableNames )
+ .dependsOn( mapping != null ? mapping.getDependsOn() : Collections.emptyList() )
.build();
propertyMappings.add( propertyMapping );
diff --git a/processor/src/main/java/org/mapstruct/ap/model/PropertyMapping.java b/processor/src/main/java/org/mapstruct/ap/model/PropertyMapping.java
index 74151ee11..9a5775031 100644
--- a/processor/src/main/java/org/mapstruct/ap/model/PropertyMapping.java
+++ b/processor/src/main/java/org/mapstruct/ap/model/PropertyMapping.java
@@ -20,8 +20,10 @@ package org.mapstruct.ap.model;
import java.util.Arrays;
import java.util.Collection;
+import java.util.Collections;
import java.util.List;
import java.util.Set;
+
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.type.TypeMirror;
@@ -31,8 +33,8 @@ import org.mapstruct.ap.model.assignment.Assignment;
import org.mapstruct.ap.model.assignment.GetterWrapperForCollectionsAndMaps;
import org.mapstruct.ap.model.assignment.NewCollectionOrMapWrapper;
import org.mapstruct.ap.model.assignment.NullCheckWrapper;
-import org.mapstruct.ap.model.assignment.SetterWrapperForCollectionsAndMaps;
import org.mapstruct.ap.model.assignment.SetterWrapper;
+import org.mapstruct.ap.model.assignment.SetterWrapperForCollectionsAndMaps;
import org.mapstruct.ap.model.common.ModelElement;
import org.mapstruct.ap.model.common.Parameter;
import org.mapstruct.ap.model.common.Type;
@@ -41,12 +43,12 @@ import org.mapstruct.ap.model.source.SourceMethod;
import org.mapstruct.ap.model.source.SourceReference;
import org.mapstruct.ap.model.source.SourceReference.PropertyEntry;
import org.mapstruct.ap.util.Executables;
+import org.mapstruct.ap.util.Message;
import org.mapstruct.ap.util.Strings;
import static org.mapstruct.ap.model.assignment.Assignment.AssignmentType.DIRECT;
import static org.mapstruct.ap.model.assignment.Assignment.AssignmentType.TYPE_CONVERTED;
import static org.mapstruct.ap.model.assignment.Assignment.AssignmentType.TYPE_CONVERTED_MAPPED;
-import org.mapstruct.ap.util.Message;
/**
* Represents the mapping between a source and target property, e.g. from {@code String Source#foo} to
@@ -57,10 +59,12 @@ import org.mapstruct.ap.util.Message;
*/
public class PropertyMapping extends ModelElement {
+ private final String name;
private final String sourceBeanName;
private final String targetAccessorName;
private final Type targetType;
private final Assignment assignment;
+ private final List dependsOn;
public static class PropertyMappingBuilder {
@@ -74,6 +78,7 @@ public class PropertyMapping extends ModelElement {
private TypeMirror resultType;
private SourceReference sourceReference;
private Collection existingVariableNames;
+ private List dependsOn;
public PropertyMappingBuilder mappingContext(MappingBuilderContext mappingContext) {
this.ctx = mappingContext;
@@ -120,6 +125,11 @@ public class PropertyMapping extends ModelElement {
return this;
}
+ public PropertyMappingBuilder dependsOn(List dependsOn) {
+ this.dependsOn = dependsOn;
+ return this;
+ }
+
private enum TargetAccessorType {
GETTER,
@@ -195,10 +205,12 @@ public class PropertyMapping extends ModelElement {
}
return new PropertyMapping(
+ targetPropertyName,
sourceReference.getParameter().getName(),
targetAccessor.getSimpleName().toString(),
targetType,
- assignment
+ assignment,
+ dependsOn
);
}
@@ -465,10 +477,12 @@ public class PropertyMapping extends ModelElement {
private SourceMethod method;
private String constantExpression;
private ExecutableElement targetAccessor;
+ private String targetPropertyName;
private String dateFormat;
private List qualifiers;
private TypeMirror resultType;
private Collection existingVariableNames;
+ private List dependsOn;
public ConstantMappingBuilder mappingContext(MappingBuilderContext mappingContext) {
this.ctx = mappingContext;
@@ -490,6 +504,11 @@ public class PropertyMapping extends ModelElement {
return this;
}
+ public ConstantMappingBuilder targetPropertyName(String targetPropertyName) {
+ this.targetPropertyName = targetPropertyName;
+ return this;
+ }
+
public ConstantMappingBuilder dateFormat(String dateFormat) {
this.dateFormat = dateFormat;
return this;
@@ -510,6 +529,11 @@ public class PropertyMapping extends ModelElement {
return this;
}
+ public ConstantMappingBuilder dependsOn(List dependsOn) {
+ this.dependsOn = dependsOn;
+ return this;
+ }
+
public PropertyMapping build() {
// source
@@ -525,8 +549,6 @@ public class PropertyMapping extends ModelElement {
targetType = ctx.getTypeFactory().getReturnType( targetAccessor );
}
- String targetPropertyName = Executables.getPropertyName( targetAccessor );
-
Assignment assignment = ctx.getMappingResolver().getTargetAssignment(
method,
mappedElement,
@@ -565,7 +587,13 @@ public class PropertyMapping extends ModelElement {
);
}
- return new PropertyMapping( targetAccessor.getSimpleName().toString(), targetType, assignment );
+ return new PropertyMapping(
+ targetPropertyName,
+ targetAccessor.getSimpleName().toString(),
+ targetType,
+ assignment,
+ dependsOn
+ );
}
}
@@ -576,6 +604,8 @@ public class PropertyMapping extends ModelElement {
private String javaExpression;
private ExecutableElement targetAccessor;
private Collection existingVariableNames;
+ private String targetPropertyName;
+ private List dependsOn;
public JavaExpressionMappingBuilder mappingContext(MappingBuilderContext mappingContext) {
this.ctx = mappingContext;
@@ -602,6 +632,16 @@ public class PropertyMapping extends ModelElement {
return this;
}
+ public JavaExpressionMappingBuilder targetPropertyName(String targetPropertyName) {
+ this.targetPropertyName = targetPropertyName;
+ return this;
+ }
+
+ public JavaExpressionMappingBuilder dependsOn(List dependsOn) {
+ this.dependsOn = dependsOn;
+ return this;
+ }
+
public PropertyMapping build() {
Assignment assignment = AssignmentFactory.createDirect( javaExpression );
@@ -623,24 +663,38 @@ public class PropertyMapping extends ModelElement {
);
}
- return new PropertyMapping( targetAccessor.getSimpleName().toString(), targetType, assignment );
+ return new PropertyMapping(
+ targetPropertyName,
+ targetAccessor.getSimpleName().toString(),
+ targetType,
+ assignment,
+ dependsOn
+ );
}
}
// Constructor for creating mappings of constant expressions.
- private PropertyMapping(String targetAccessorName, Type targetType, Assignment propertyAssignment) {
- this( null, targetAccessorName, targetType, propertyAssignment );
+ private PropertyMapping(String name, String targetAccessorName, Type targetType, Assignment propertyAssignment,
+ List dependsOn) {
+ this( name, null, targetAccessorName, targetType, propertyAssignment, dependsOn );
}
- private PropertyMapping(String sourceBeanName, String targetAccessorName, Type targetType, Assignment assignment) {
-
+ private PropertyMapping(String name, String sourceBeanName, String targetAccessorName, Type targetType,
+ Assignment assignment, List dependsOn) {
+ this.name = name;
this.sourceBeanName = sourceBeanName;
-
this.targetAccessorName = targetAccessorName;
this.targetType = targetType;
-
this.assignment = assignment;
+ this.dependsOn = dependsOn != null ? dependsOn : Collections.emptyList();
+ }
+
+ /**
+ * Returns the name of this mapping (property name on the target side)
+ */
+ public String getName() {
+ return name;
}
public String getSourceBeanName() {
@@ -664,12 +718,17 @@ public class PropertyMapping extends ModelElement {
return assignment.getImportTypes();
}
+ public List getDependsOn() {
+ return dependsOn;
+ }
+
@Override
public String toString() {
return "PropertyMapping {"
- + "\n targetName='" + targetAccessorName + "\',"
+ + "\n name='" + name + "\',"
+ "\n targetType=" + targetType + ","
- + "\n propertyAssignment=" + assignment
+ + "\n propertyAssignment=" + assignment + ","
+ + "\n dependsOn=" + dependsOn
+ "\n}";
}
}
diff --git a/processor/src/main/java/org/mapstruct/ap/model/dependency/GraphAnalyzer.java b/processor/src/main/java/org/mapstruct/ap/model/dependency/GraphAnalyzer.java
new file mode 100644
index 000000000..1d581870d
--- /dev/null
+++ b/processor/src/main/java/org/mapstruct/ap/model/dependency/GraphAnalyzer.java
@@ -0,0 +1,134 @@
+/**
+ * Copyright 2012-2015 Gunnar Morling (http://www.gunnarmorling.de/)
+ * and/or other contributors as indicated by the @authors tag. See the
+ * copyright.txt file in the distribution for a full listing of all
+ * contributors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.mapstruct.ap.model.dependency;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.Stack;
+
+/**
+ * Analyzes graphs: Discovers all descendants of given nodes and detects cyclic dependencies between nodes if present.
+ *
+ * @author Gunnar Morling
+ */
+public class GraphAnalyzer {
+
+ private final Map nodes = new HashMap();
+ private final Set> cycles = new HashSet>();
+
+ private final Stack currentPath = new Stack();
+
+ public void addNode(String name, List descendants) {
+ Node node = getNode( name );
+
+ for ( String descendant : descendants ) {
+ node.addDescendant( getNode( descendant ) );
+ }
+ }
+
+ public void addNode(String name, String... descendants) {
+ Node node = getNode( name );
+
+ for ( String descendant : descendants ) {
+ node.addDescendant( getNode( descendant ) );
+ }
+ }
+
+ /**
+ * Performs a full traversal of the graph, detecting potential cycles and calculates the full list of descendants of
+ * the nodes.
+ */
+ public void analyze() {
+ for ( Node node : nodes.values() ) {
+ depthFirstSearch( node );
+ }
+ }
+
+ /**
+ * Returns all the descendants of the given node, either direct or transitive ones.
+ *
+ * Note:The list will only be complete if the graph contains no cycles.
+ */
+ public Set getAllDescendants(String name) {
+ Node node = nodes.get( name );
+ return node != null ? node.getAllDescendants() : Collections.emptySet();
+ }
+
+ public Set> getCycles() {
+ return cycles;
+ }
+
+ private void depthFirstSearch(Node node) {
+ if ( node.isProcessed() ) {
+ return;
+ }
+
+ currentPath.push( node );
+
+ // the node is on the stack already -> cycle
+ if ( node.isVisited() ) {
+ cycles.add( getCurrentCycle( node ) );
+ currentPath.pop();
+ return;
+ }
+
+ node.setVisited( true );
+
+ for ( Node descendant : node.getDescendants() ) {
+ depthFirstSearch( descendant );
+ node.getAllDescendants().addAll( descendant.getAllDescendants() );
+ }
+
+ node.setProcessed( true );
+ currentPath.pop();
+ }
+
+ private List getCurrentCycle(Node start) {
+ List cycle = new ArrayList();
+ boolean inCycle = false;
+
+ for ( Node n : currentPath ) {
+ if ( n.getName().equals( start.getName() ) ) {
+ inCycle = true;
+ }
+
+ if ( inCycle ) {
+ cycle.add( n.getName() );
+ }
+ }
+
+ return cycle;
+ }
+
+ private Node getNode(String name) {
+ Node node = nodes.get( name );
+
+ if ( node == null ) {
+ node = new Node( name );
+ nodes.put( name, node );
+ }
+
+ return node;
+ }
+}
diff --git a/processor/src/main/java/org/mapstruct/ap/model/dependency/Node.java b/processor/src/main/java/org/mapstruct/ap/model/dependency/Node.java
new file mode 100644
index 000000000..fc7ec0f15
--- /dev/null
+++ b/processor/src/main/java/org/mapstruct/ap/model/dependency/Node.java
@@ -0,0 +1,121 @@
+/**
+ * Copyright 2012-2015 Gunnar Morling (http://www.gunnarmorling.de/)
+ * and/or other contributors as indicated by the @authors tag. See the
+ * copyright.txt file in the distribution for a full listing of all
+ * contributors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.mapstruct.ap.model.dependency;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * A node of a directed graph.
+ *
+ * @author Gunnar Morling
+ */
+class Node {
+
+ private final String name;
+ private boolean visited;
+ private boolean processed;
+
+ /**
+ * The direct descendants of this node.
+ */
+ private final List descendants;
+
+ /**
+ * All descendants of this node, direct and transitive ones, as discovered through graph traversal.
+ */
+ private final Set allDescendants;
+
+ public Node(String name) {
+ this.name = name;
+ descendants = new ArrayList();
+ allDescendants = new HashSet();
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public boolean isVisited() {
+ return visited;
+ }
+
+ public void setVisited(boolean visited) {
+ this.visited = visited;
+ }
+
+ public boolean isProcessed() {
+ return processed;
+ }
+
+ public void setProcessed(boolean processed) {
+ this.processed = processed;
+ }
+
+ public void addDescendant(Node node) {
+ descendants.add( node );
+ allDescendants.add( node.getName() );
+ }
+
+ public List getDescendants() {
+ return descendants;
+ }
+
+ public Set getAllDescendants() {
+ return allDescendants;
+ }
+
+ @Override
+ public String toString() {
+ return name;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + ( ( name == null ) ? 0 : name.hashCode() );
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if ( this == obj ) {
+ return true;
+ }
+ if ( obj == null ) {
+ return false;
+ }
+ if ( getClass() != obj.getClass() ) {
+ return false;
+ }
+ Node other = (Node) obj;
+ if ( name == null ) {
+ if ( other.name != null ) {
+ return false;
+ }
+ }
+ else if ( !name.equals( other.name ) ) {
+ return false;
+ }
+ return true;
+ }
+}
diff --git a/processor/src/main/java/org/mapstruct/ap/model/source/Mapping.java b/processor/src/main/java/org/mapstruct/ap/model/source/Mapping.java
index a23c1344b..dfa67fcd5 100644
--- a/processor/src/main/java/org/mapstruct/ap/model/source/Mapping.java
+++ b/processor/src/main/java/org/mapstruct/ap/model/source/Mapping.java
@@ -19,12 +19,12 @@
package org.mapstruct.ap.model.source;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
-
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.AnnotationValue;
import javax.lang.model.element.ElementKind;
@@ -57,6 +57,7 @@ public class Mapping {
private final List qualifiers;
private final TypeMirror resultType;
private final boolean isIgnored;
+ private final List dependsOn;
private final AnnotationMirror mirror;
private final AnnotationValue sourceAnnotationValue;
@@ -120,6 +121,8 @@ public class Mapping {
boolean resultTypeIsDefined = !TypeKind.VOID.equals( mappingPrism.resultType().getKind() );
TypeMirror resultType = resultTypeIsDefined ? mappingPrism.resultType() : null;
+ List dependsOn =
+ mappingPrism.dependsOn() != null ? mappingPrism.dependsOn() : Collections.emptyList();
return new Mapping(
source,
@@ -132,7 +135,8 @@ public class Mapping {
mappingPrism.mirror,
mappingPrism.values.source(),
mappingPrism.values.target(),
- resultType
+ resultType,
+ dependsOn
);
}
@@ -141,7 +145,7 @@ public class Mapping {
String dateFormat, List qualifiers,
boolean isIgnored, AnnotationMirror mirror,
AnnotationValue sourceAnnotationValue, AnnotationValue targetAnnotationValue,
- TypeMirror resultType ) {
+ TypeMirror resultType, List dependsOn) {
this.sourceName = sourceName;
this.constant = constant;
this.javaExpression = javaExpression;
@@ -153,6 +157,7 @@ public class Mapping {
this.sourceAnnotationValue = sourceAnnotationValue;
this.targetAnnotationValue = targetAnnotationValue;
this.resultType = resultType;
+ this.dependsOn = dependsOn;
}
private static String getExpression(MappingPrism mappingPrism, ExecutableElement element,
@@ -243,6 +248,10 @@ public class Mapping {
return resultType;
}
+ public List getDependsOn() {
+ return dependsOn;
+ }
+
private boolean hasPropertyInReverseMethod(String name, SourceMethod method) {
CollectionMappingStrategyPrism cms = method.getMapperConfiguration().getCollectionMappingStrategy();
return method.getResultType().getTargetAccessors( cms ).containsKey( name );
@@ -287,7 +296,8 @@ public class Mapping {
mirror,
sourceAnnotationValue,
targetAnnotationValue,
- null
+ null,
+ Collections.emptyList()
);
reverse.init( method, messager, typeFactory );
@@ -311,8 +321,9 @@ public class Mapping {
mirror,
sourceAnnotationValue,
targetAnnotationValue,
- resultType
- );
+ resultType,
+ dependsOn
+ );
if ( sourceReference != null ) {
mapping.sourceReference = sourceReference.copyForInheritanceTo( method );
diff --git a/processor/src/test/java/org/mapstruct/ap/test/dependency/Address.java b/processor/src/test/java/org/mapstruct/ap/test/dependency/Address.java
new file mode 100644
index 000000000..b991fd86c
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/dependency/Address.java
@@ -0,0 +1,50 @@
+/**
+ * Copyright 2012-2015 Gunnar Morling (http://www.gunnarmorling.de/)
+ * and/or other contributors as indicated by the @authors tag. See the
+ * copyright.txt file in the distribution for a full listing of all
+ * contributors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.mapstruct.ap.test.dependency;
+
+public class Address {
+
+ private String firstName;
+ private String middleName;
+ private String lastName;
+
+ public String getFirstName() {
+ return firstName;
+ }
+
+ public void setFirstName(String firstName) {
+ this.firstName = firstName;
+ }
+
+ public String getMiddleName() {
+ return middleName;
+ }
+
+ public void setMiddleName(String middleName) {
+ this.middleName = middleName;
+ }
+
+ public String getLastName() {
+ return lastName;
+ }
+
+ public void setLastName(String lastName) {
+ this.lastName = lastName;
+ }
+}
diff --git a/processor/src/test/java/org/mapstruct/ap/test/dependency/AddressDto.java b/processor/src/test/java/org/mapstruct/ap/test/dependency/AddressDto.java
new file mode 100644
index 000000000..5c787f406
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/dependency/AddressDto.java
@@ -0,0 +1,58 @@
+/**
+ * Copyright 2012-2015 Gunnar Morling (http://www.gunnarmorling.de/)
+ * and/or other contributors as indicated by the @authors tag. See the
+ * copyright.txt file in the distribution for a full listing of all
+ * contributors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.mapstruct.ap.test.dependency;
+
+public class AddressDto {
+
+ private String givenName;
+ private String middleName;
+ private String surName;
+ private String fullName;
+
+ public String getGivenName() {
+ return givenName;
+ }
+
+ public void setGivenName(String givenName) {
+ this.givenName = givenName;
+ this.fullName = givenName;
+ }
+
+ public String getMiddleName() {
+ return middleName;
+ }
+
+ public void setMiddleName(String middleName) {
+ this.middleName = middleName;
+ this.fullName += " " + middleName;
+ }
+
+ public String getSurName() {
+ return surName;
+ }
+
+ public void setSurName(String surName) {
+ this.surName = surName;
+ this.fullName += " " + surName;
+ }
+
+ public String getFullName() {
+ return fullName;
+ }
+}
diff --git a/processor/src/test/java/org/mapstruct/ap/test/dependency/AddressMapper.java b/processor/src/test/java/org/mapstruct/ap/test/dependency/AddressMapper.java
new file mode 100644
index 000000000..e9079f418
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/dependency/AddressMapper.java
@@ -0,0 +1,42 @@
+/**
+ * Copyright 2012-2015 Gunnar Morling (http://www.gunnarmorling.de/)
+ * and/or other contributors as indicated by the @authors tag. See the
+ * copyright.txt file in the distribution for a full listing of all
+ * contributors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.mapstruct.ap.test.dependency;
+
+import org.mapstruct.Mapper;
+import org.mapstruct.Mapping;
+import org.mapstruct.Mappings;
+import org.mapstruct.factory.Mappers;
+
+@Mapper
+public interface AddressMapper {
+
+ AddressMapper INSTANCE = Mappers.getMapper( AddressMapper.class );
+
+ @Mappings({
+ @Mapping(target = "surName", source = "lastName", dependsOn = "middleName"),
+ @Mapping(target = "middleName", dependsOn = "givenName"),
+ @Mapping(target = "givenName", source = "firstName")
+ })
+ AddressDto addressToDto(Address address);
+
+ @Mappings({
+ @Mapping(target = "lastName", dependsOn = { "firstName", "middleName" })
+ })
+ PersonDto personToDto(Person person);
+}
diff --git a/processor/src/test/java/org/mapstruct/ap/test/dependency/GraphAnalyzerTest.java b/processor/src/test/java/org/mapstruct/ap/test/dependency/GraphAnalyzerTest.java
new file mode 100644
index 000000000..b17947f57
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/dependency/GraphAnalyzerTest.java
@@ -0,0 +1,224 @@
+/**
+ * Copyright 2012-2015 Gunnar Morling (http://www.gunnarmorling.de/)
+ * and/or other contributors as indicated by the @authors tag. See the
+ * copyright.txt file in the distribution for a full listing of all
+ * contributors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.mapstruct.ap.test.dependency;
+
+import static org.fest.assertions.Assertions.assertThat;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mapstruct.ap.model.dependency.GraphAnalyzer;
+import org.mapstruct.ap.util.Strings;
+
+/**
+ * Unit test for {@link GraphAnalyzer}.
+ *
+ * @author Gunnar Morling
+ */
+public class GraphAnalyzerTest {
+
+ private GraphAnalyzer detector;
+
+ @Before
+ public void setUpDetector() {
+ detector = new GraphAnalyzer();
+ }
+
+ @Test
+ public void emptyGraph() {
+ detector.analyze();
+
+ assertThat( detector.getCycles() ).isEmpty();
+ }
+
+ @Test
+ public void singleNode() {
+ detector.addNode( "a" );
+ detector.analyze();
+
+ assertThat( detector.getCycles() ).isEmpty();
+ assertThat( detector.getAllDescendants( "a" ) ).isEmpty();
+ }
+
+ @Test
+ public void twoNodesWithoutCycle() {
+ detector.addNode( "a", "b" );
+ detector.addNode( "b" );
+
+ detector.analyze();
+
+ assertThat( detector.getCycles() ).isEmpty();
+ assertThat( detector.getAllDescendants( "a" ) ).containsOnly( "b" );
+ assertThat( detector.getAllDescendants( "b" ) ).isEmpty();
+ }
+
+ @Test
+ public void twoNodesWithCycle() {
+ detector.addNode( "a", "b" );
+ detector.addNode( "b", "a" );
+
+ detector.analyze();
+
+ assertThat( asStrings( detector.getCycles() ) ).containsOnly( "a -> b -> a" );
+ }
+
+ @Test
+ public void threeNodesWithCycleBetweenTwo() {
+ detector.addNode( "a", "b" );
+ detector.addNode( "b", "a", "c" );
+
+ detector.analyze();
+
+ assertThat( asStrings( detector.getCycles() ) ).containsOnly( "a -> b -> a" );
+ }
+
+ @Test
+ public void twoNodesWithSharedDescendantWithoutCycle() {
+ detector.addNode( "a", "b" );
+ detector.addNode( "b", "c" );
+ detector.addNode( "a", "c" );
+
+ detector.analyze();
+
+ assertThat( asStrings( detector.getCycles() ) ).isEmpty();
+ assertThat( detector.getAllDescendants( "a" ) ).containsOnly( "b", "c" );
+ assertThat( detector.getAllDescendants( "b" ) ).containsOnly( "c" );
+ assertThat( detector.getAllDescendants( "c" ) ).isEmpty();
+ }
+
+ @Test
+ public void threeNodesWithoutCycle() {
+ detector.addNode( "a", "b" );
+ detector.addNode( "c", "b" );
+
+ detector.analyze();
+
+ assertThat( asStrings( detector.getCycles() ) ).isEmpty();
+
+ assertThat( detector.getAllDescendants( "a" ) ).containsOnly( "b" );
+ assertThat( detector.getAllDescendants( "b" ) ).isEmpty();
+ assertThat( detector.getAllDescendants( "c" ) ).containsOnly( "b" );
+ }
+
+ @Test
+ public void fourNodesWithCycleBetweenThree() {
+ detector.addNode( "a", "b" );
+ detector.addNode( "b", "c" );
+ detector.addNode( "c", "d" );
+ detector.addNode( "d", "b" );
+
+ detector.analyze();
+
+ assertThat( asStrings( detector.getCycles() ) ).containsOnly( "b -> c -> d -> b" );
+ }
+
+ @Test
+ public void fourNodesWithTwoCycles() {
+ detector.addNode( "a", "b" );
+ detector.addNode( "b", "a" );
+ detector.addNode( "c", "d" );
+ detector.addNode( "d", "c" );
+
+ detector.analyze();
+
+ assertThat( asStrings( detector.getCycles() ) ).containsOnly( "a -> b -> a", "c -> d -> c" );
+ }
+
+ @Test
+ public void fourNodesWithoutCycle() {
+ detector.addNode( "a", "b1" );
+ detector.addNode( "a", "b2" );
+ detector.addNode( "b1", "c" );
+ detector.addNode( "b2", "c" );
+
+ detector.analyze();
+
+ assertThat( asStrings( detector.getCycles() ) ).isEmpty();
+ assertThat( detector.getAllDescendants( "a" ) ).containsOnly( "b1", "b2", "c" );
+ assertThat( detector.getAllDescendants( "b1" ) ).containsOnly( "c" );
+ assertThat( detector.getAllDescendants( "b2" ) ).containsOnly( "c" );
+ assertThat( detector.getAllDescendants( "c" ) ).isEmpty();
+ }
+
+ @Test
+ public void fourNodesWithCycle() {
+ detector.addNode( "a", "b1" );
+ detector.addNode( "a", "b2" );
+ detector.addNode( "b1", "c" );
+ detector.addNode( "b2", "c" );
+ detector.addNode( "c", "a" );
+
+ detector.analyze();
+
+ assertThat( asStrings( detector.getCycles() ) ).containsOnly( "a -> b1 -> c -> a", "a -> b2 -> c -> a" );
+ }
+
+ @Test
+ public void eightNodesWithoutCycle() {
+ detector.addNode( "a", "b1" );
+ detector.addNode( "a", "b2" );
+ detector.addNode( "b1", "c1" );
+ detector.addNode( "b1", "c2" );
+ detector.addNode( "b2", "c3" );
+ detector.addNode( "b2", "c4" );
+
+ detector.analyze();
+
+ assertThat( detector.getCycles() ).isEmpty();
+
+ assertThat( detector.getAllDescendants( "a" ) ).containsOnly( "b1", "b2", "c1", "c2", "c3", "c4" );
+ assertThat( detector.getAllDescendants( "b1" ) ).containsOnly( "c1", "c2" );
+ assertThat( detector.getAllDescendants( "b2" ) ).containsOnly( "c3", "c4" );
+ }
+
+ private Set asStrings(Set> cycles) {
+ Set asStrings = new HashSet();
+
+ for ( List cycle : cycles ) {
+ asStrings.add( asString( cycle ) );
+ }
+
+ return asStrings;
+ }
+
+ private String asString(List cycle) {
+ return Strings.join( normalize( cycle ), " -> " );
+ }
+
+ /**
+ * "Normalizes" a cycle so that the minimum element comes first. E.g. both the cycles {@code b -> c -> a -> b} and
+ * {@code c -> a -> b -> c} would be normalized to {@code a -> b -> c -> a}.
+ */
+ private List normalize(List cycle) {
+ // remove the first element
+ cycle = cycle.subList( 1, cycle.size() );
+
+ // rotate the cycle so the minimum element comes first
+ Collections.rotate( cycle, -cycle.indexOf( Collections.min( cycle ) ) );
+
+ // add the first element add the end to re-close the cycle
+ cycle.add( cycle.get( 0 ) );
+
+ return cycle;
+ }
+}
diff --git a/processor/src/test/java/org/mapstruct/ap/test/dependency/OrderingTest.java b/processor/src/test/java/org/mapstruct/ap/test/dependency/OrderingTest.java
new file mode 100644
index 000000000..b7feb526c
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/dependency/OrderingTest.java
@@ -0,0 +1,66 @@
+/**
+ * Copyright 2012-2015 Gunnar Morling (http://www.gunnarmorling.de/)
+ * and/or other contributors as indicated by the @authors tag. See the
+ * copyright.txt file in the distribution for a full listing of all
+ * contributors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.mapstruct.ap.test.dependency;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mapstruct.Mapping;
+import org.mapstruct.ap.testutil.IssueKey;
+import org.mapstruct.ap.testutil.WithClasses;
+import org.mapstruct.ap.testutil.runner.AnnotationProcessorTestRunner;
+
+import static org.fest.assertions.Assertions.assertThat;
+
+/**
+ * Test for ordering mapped attributes by means of {@link Mapping#dependsOn()}.
+ *
+ * @author Gunnar Morling
+ */
+@WithClasses({ Person.class, PersonDto.class, Address.class, AddressDto.class, AddressMapper.class })
+@RunWith(AnnotationProcessorTestRunner.class)
+public class OrderingTest {
+
+ @Test
+ @IssueKey("304")
+ public void shouldApplyChainOfDependencies() {
+ Address source = new Address();
+ source.setFirstName( "Bob" );
+ source.setMiddleName( "J." );
+ source.setLastName( "McRobb" );
+
+ AddressDto target = AddressMapper.INSTANCE.addressToDto( source );
+
+ assertThat( target ).isNotNull();
+ assertThat( target.getFullName() ).isEqualTo( "Bob J. McRobb" );
+ }
+
+ @Test
+ @IssueKey("304")
+ public void shouldApplySeveralDependenciesConfiguredForOneProperty() {
+ Person source = new Person();
+ source.setFirstName( "Bob" );
+ source.setMiddleName( "J." );
+ source.setLastName( "McRobb" );
+
+ PersonDto target = AddressMapper.INSTANCE.personToDto( source );
+
+ assertThat( target ).isNotNull();
+ assertThat( target.getFullName() ).isEqualTo( "Bob J. McRobb" );
+ }
+}
diff --git a/processor/src/test/java/org/mapstruct/ap/test/dependency/Person.java b/processor/src/test/java/org/mapstruct/ap/test/dependency/Person.java
new file mode 100644
index 000000000..5f86aa3a9
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/dependency/Person.java
@@ -0,0 +1,50 @@
+/**
+ * Copyright 2012-2015 Gunnar Morling (http://www.gunnarmorling.de/)
+ * and/or other contributors as indicated by the @authors tag. See the
+ * copyright.txt file in the distribution for a full listing of all
+ * contributors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.mapstruct.ap.test.dependency;
+
+public class Person {
+
+ private String firstName;
+ private String middleName;
+ private String lastName;
+
+ public String getFirstName() {
+ return firstName;
+ }
+
+ public void setFirstName(String firstName) {
+ this.firstName = firstName;
+ }
+
+ public String getMiddleName() {
+ return middleName;
+ }
+
+ public void setMiddleName(String middleName) {
+ this.middleName = middleName;
+ }
+
+ public String getLastName() {
+ return lastName;
+ }
+
+ public void setLastName(String lastName) {
+ this.lastName = lastName;
+ }
+}
diff --git a/processor/src/test/java/org/mapstruct/ap/test/dependency/PersonDto.java b/processor/src/test/java/org/mapstruct/ap/test/dependency/PersonDto.java
new file mode 100644
index 000000000..cb16058af
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/dependency/PersonDto.java
@@ -0,0 +1,56 @@
+/**
+ * Copyright 2012-2015 Gunnar Morling (http://www.gunnarmorling.de/)
+ * and/or other contributors as indicated by the @authors tag. See the
+ * copyright.txt file in the distribution for a full listing of all
+ * contributors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.mapstruct.ap.test.dependency;
+
+public class PersonDto {
+
+ private String firstName;
+ private String middleName;
+ private String lastName;
+ private String fullName;
+
+ public String getFirstName() {
+ return firstName;
+ }
+
+ public void setFirstName(String firstName) {
+ this.firstName = firstName;
+ }
+
+ public String getMiddleName() {
+ return middleName;
+ }
+
+ public void setMiddleName(String middleName) {
+ this.middleName = middleName;
+ }
+
+ public String getLastName() {
+ return lastName;
+ }
+
+ public void setLastName(String lastName) {
+ this.lastName = lastName;
+ this.fullName = firstName + " " + middleName + " " + lastName;
+ }
+
+ public String getFullName() {
+ return fullName;
+ }
+}