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[] 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; + } +}