#304 Allowing to configure dependencies between properties via @Mapping#dependsOn()

This commit is contained in:
Gunnar Morling 2015-03-01 19:17:37 +01:00
parent 9b888847ea
commit 2d7ab089ff
14 changed files with 957 additions and 23 deletions

View File

@ -137,10 +137,21 @@ public @interface Mapping {
*/ */
Class<? extends Annotation>[] qualifiedBy() default { }; Class<? extends Annotation>[] qualifiedBy() default { };
/** /**
* Specifies the result type of the mapping method to be used in case multiple mapping methods qualify. * Specifies the result type of the mapping method to be used in case multiple mapping methods qualify.
* *
* @return the resultType to select * @return the resultType to select
*/ */
Class<?> resultType() default void.class; 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.
* <p>
* 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 { };
} }

View File

@ -141,4 +141,15 @@ public @interface Mapping {
* @return the resultType to select * @return the resultType to select
*/ */
Class<?> resultType() default void.class; 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.
* <p>
* 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 { };
} }

View File

@ -21,6 +21,8 @@ package org.mapstruct.ap.model;
import java.text.MessageFormat; import java.text.MessageFormat;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.Iterator; 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.PropertyMapping.PropertyMappingBuilder;
import org.mapstruct.ap.model.common.Parameter; import org.mapstruct.ap.model.common.Parameter;
import org.mapstruct.ap.model.common.Type; 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.Mapping;
import org.mapstruct.ap.model.source.SourceMethod; import org.mapstruct.ap.model.source.SourceMethod;
import org.mapstruct.ap.model.source.SourceReference; import org.mapstruct.ap.model.source.SourceReference;
@ -146,11 +149,14 @@ public class BeanMappingMethod extends MappingMethod {
if ( !resultType.isAssignableTo( method.getResultType() ) ) { if ( !resultType.isAssignableTo( method.getResultType() ) ) {
ctx.getMessager().printMessage( method.getExecutable(), ctx.getMessager().printMessage( method.getExecutable(),
beanMappingPrism.mirror, beanMappingPrism.mirror,
Message.BEANMAPPING_NOT_ASSIGNABLE, resultType, method.getResultType()); Message.BEANMAPPING_NOT_ASSIGNABLE, resultType, method.getResultType()
);
} }
} }
} }
sortPropertyMappingsByDependencies();
return new BeanMappingMethod( return new BeanMappingMethod(
method, method,
propertyMappings, 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<PropertyMapping>() {
@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 * Iterates over all defined mapping methods ({@code @Mapping(s)}), either directly given or inherited from the
* inverse mapping method. * inverse mapping method.
@ -221,6 +255,7 @@ public class BeanMappingMethod extends MappingMethod {
.resultType( mapping.getResultType() ) .resultType( mapping.getResultType() )
.dateFormat( mapping.getDateFormat() ) .dateFormat( mapping.getDateFormat() )
.existingVariableNames( existingVariableNames ) .existingVariableNames( existingVariableNames )
.dependsOn( mapping.getDependsOn() )
.build(); .build();
handledTargets.add( mapping.getTargetName() ); handledTargets.add( mapping.getTargetName() );
unprocessedSourceParameters.remove( sourceRef.getParameter() ); unprocessedSourceParameters.remove( sourceRef.getParameter() );
@ -239,10 +274,12 @@ public class BeanMappingMethod extends MappingMethod {
.sourceMethod( method ) .sourceMethod( method )
.constantExpression( "\"" + mapping.getConstant() + "\"" ) .constantExpression( "\"" + mapping.getConstant() + "\"" )
.targetAccessor( targetProperty ) .targetAccessor( targetProperty )
.targetPropertyName( mapping.getTargetName() )
.dateFormat( mapping.getDateFormat() ) .dateFormat( mapping.getDateFormat() )
.qualifiers( mapping.getQualifiers() ) .qualifiers( mapping.getQualifiers() )
.resultType( mapping.getResultType() ) .resultType( mapping.getResultType() )
.existingVariableNames( existingVariableNames ) .existingVariableNames( existingVariableNames )
.dependsOn( mapping.getDependsOn() )
.build(); .build();
handledTargets.add( mapping.getTargetName() ); handledTargets.add( mapping.getTargetName() );
} }
@ -256,6 +293,8 @@ public class BeanMappingMethod extends MappingMethod {
.javaExpression( mapping.getJavaExpression() ) .javaExpression( mapping.getJavaExpression() )
.targetAccessor( targetProperty ) .targetAccessor( targetProperty )
.existingVariableNames( existingVariableNames ) .existingVariableNames( existingVariableNames )
.targetPropertyName( mapping.getTargetName() )
.dependsOn( mapping.getDependsOn() )
.build(); .build();
handledTargets.add( mapping.getTargetName() ); handledTargets.add( mapping.getTargetName() );
} }
@ -333,6 +372,7 @@ public class BeanMappingMethod extends MappingMethod {
.resultType( mapping != null ? mapping.getResultType() : null ) .resultType( mapping != null ? mapping.getResultType() : null )
.dateFormat( mapping != null ? mapping.getDateFormat() : null ) .dateFormat( mapping != null ? mapping.getDateFormat() : null )
.existingVariableNames( existingVariableNames ) .existingVariableNames( existingVariableNames )
.dependsOn( mapping != null ? mapping.getDependsOn() : Collections.<String>emptyList() )
.build(); .build();
unprocessedSourceParameters.remove( sourceParameter ); unprocessedSourceParameters.remove( sourceParameter );
@ -394,6 +434,7 @@ public class BeanMappingMethod extends MappingMethod {
.resultType( mapping != null ? mapping.getResultType() : null ) .resultType( mapping != null ? mapping.getResultType() : null )
.dateFormat( mapping != null ? mapping.getDateFormat() : null ) .dateFormat( mapping != null ? mapping.getDateFormat() : null )
.existingVariableNames( existingVariableNames ) .existingVariableNames( existingVariableNames )
.dependsOn( mapping != null ? mapping.getDependsOn() : Collections.<String>emptyList() )
.build(); .build();
propertyMappings.add( propertyMapping ); propertyMappings.add( propertyMapping );

View File

@ -20,8 +20,10 @@ package org.mapstruct.ap.model;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.ExecutableElement;
import javax.lang.model.type.TypeMirror; 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.GetterWrapperForCollectionsAndMaps;
import org.mapstruct.ap.model.assignment.NewCollectionOrMapWrapper; import org.mapstruct.ap.model.assignment.NewCollectionOrMapWrapper;
import org.mapstruct.ap.model.assignment.NullCheckWrapper; 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.SetterWrapper;
import org.mapstruct.ap.model.assignment.SetterWrapperForCollectionsAndMaps;
import org.mapstruct.ap.model.common.ModelElement; import org.mapstruct.ap.model.common.ModelElement;
import org.mapstruct.ap.model.common.Parameter; import org.mapstruct.ap.model.common.Parameter;
import org.mapstruct.ap.model.common.Type; 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;
import org.mapstruct.ap.model.source.SourceReference.PropertyEntry; import org.mapstruct.ap.model.source.SourceReference.PropertyEntry;
import org.mapstruct.ap.util.Executables; import org.mapstruct.ap.util.Executables;
import org.mapstruct.ap.util.Message;
import org.mapstruct.ap.util.Strings; 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.DIRECT;
import static org.mapstruct.ap.model.assignment.Assignment.AssignmentType.TYPE_CONVERTED; import static org.mapstruct.ap.model.assignment.Assignment.AssignmentType.TYPE_CONVERTED;
import static org.mapstruct.ap.model.assignment.Assignment.AssignmentType.TYPE_CONVERTED_MAPPED; 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 * 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 { public class PropertyMapping extends ModelElement {
private final String name;
private final String sourceBeanName; private final String sourceBeanName;
private final String targetAccessorName; private final String targetAccessorName;
private final Type targetType; private final Type targetType;
private final Assignment assignment; private final Assignment assignment;
private final List<String> dependsOn;
public static class PropertyMappingBuilder { public static class PropertyMappingBuilder {
@ -74,6 +78,7 @@ public class PropertyMapping extends ModelElement {
private TypeMirror resultType; private TypeMirror resultType;
private SourceReference sourceReference; private SourceReference sourceReference;
private Collection<String> existingVariableNames; private Collection<String> existingVariableNames;
private List<String> dependsOn;
public PropertyMappingBuilder mappingContext(MappingBuilderContext mappingContext) { public PropertyMappingBuilder mappingContext(MappingBuilderContext mappingContext) {
this.ctx = mappingContext; this.ctx = mappingContext;
@ -120,6 +125,11 @@ public class PropertyMapping extends ModelElement {
return this; return this;
} }
public PropertyMappingBuilder dependsOn(List<String> dependsOn) {
this.dependsOn = dependsOn;
return this;
}
private enum TargetAccessorType { private enum TargetAccessorType {
GETTER, GETTER,
@ -195,10 +205,12 @@ public class PropertyMapping extends ModelElement {
} }
return new PropertyMapping( return new PropertyMapping(
targetPropertyName,
sourceReference.getParameter().getName(), sourceReference.getParameter().getName(),
targetAccessor.getSimpleName().toString(), targetAccessor.getSimpleName().toString(),
targetType, targetType,
assignment assignment,
dependsOn
); );
} }
@ -465,10 +477,12 @@ public class PropertyMapping extends ModelElement {
private SourceMethod method; private SourceMethod method;
private String constantExpression; private String constantExpression;
private ExecutableElement targetAccessor; private ExecutableElement targetAccessor;
private String targetPropertyName;
private String dateFormat; private String dateFormat;
private List<TypeMirror> qualifiers; private List<TypeMirror> qualifiers;
private TypeMirror resultType; private TypeMirror resultType;
private Collection<String> existingVariableNames; private Collection<String> existingVariableNames;
private List<String> dependsOn;
public ConstantMappingBuilder mappingContext(MappingBuilderContext mappingContext) { public ConstantMappingBuilder mappingContext(MappingBuilderContext mappingContext) {
this.ctx = mappingContext; this.ctx = mappingContext;
@ -490,6 +504,11 @@ public class PropertyMapping extends ModelElement {
return this; return this;
} }
public ConstantMappingBuilder targetPropertyName(String targetPropertyName) {
this.targetPropertyName = targetPropertyName;
return this;
}
public ConstantMappingBuilder dateFormat(String dateFormat) { public ConstantMappingBuilder dateFormat(String dateFormat) {
this.dateFormat = dateFormat; this.dateFormat = dateFormat;
return this; return this;
@ -510,6 +529,11 @@ public class PropertyMapping extends ModelElement {
return this; return this;
} }
public ConstantMappingBuilder dependsOn(List<String> dependsOn) {
this.dependsOn = dependsOn;
return this;
}
public PropertyMapping build() { public PropertyMapping build() {
// source // source
@ -525,8 +549,6 @@ public class PropertyMapping extends ModelElement {
targetType = ctx.getTypeFactory().getReturnType( targetAccessor ); targetType = ctx.getTypeFactory().getReturnType( targetAccessor );
} }
String targetPropertyName = Executables.getPropertyName( targetAccessor );
Assignment assignment = ctx.getMappingResolver().getTargetAssignment( Assignment assignment = ctx.getMappingResolver().getTargetAssignment(
method, method,
mappedElement, 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 String javaExpression;
private ExecutableElement targetAccessor; private ExecutableElement targetAccessor;
private Collection<String> existingVariableNames; private Collection<String> existingVariableNames;
private String targetPropertyName;
private List<String> dependsOn;
public JavaExpressionMappingBuilder mappingContext(MappingBuilderContext mappingContext) { public JavaExpressionMappingBuilder mappingContext(MappingBuilderContext mappingContext) {
this.ctx = mappingContext; this.ctx = mappingContext;
@ -602,6 +632,16 @@ public class PropertyMapping extends ModelElement {
return this; return this;
} }
public JavaExpressionMappingBuilder targetPropertyName(String targetPropertyName) {
this.targetPropertyName = targetPropertyName;
return this;
}
public JavaExpressionMappingBuilder dependsOn(List<String> dependsOn) {
this.dependsOn = dependsOn;
return this;
}
public PropertyMapping build() { public PropertyMapping build() {
Assignment assignment = AssignmentFactory.createDirect( javaExpression ); 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. // Constructor for creating mappings of constant expressions.
private PropertyMapping(String targetAccessorName, Type targetType, Assignment propertyAssignment) { private PropertyMapping(String name, String targetAccessorName, Type targetType, Assignment propertyAssignment,
this( null, targetAccessorName, targetType, propertyAssignment ); List<String> 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<String> dependsOn) {
this.name = name;
this.sourceBeanName = sourceBeanName; this.sourceBeanName = sourceBeanName;
this.targetAccessorName = targetAccessorName; this.targetAccessorName = targetAccessorName;
this.targetType = targetType; this.targetType = targetType;
this.assignment = assignment; this.assignment = assignment;
this.dependsOn = dependsOn != null ? dependsOn : Collections.<String>emptyList();
}
/**
* Returns the name of this mapping (property name on the target side)
*/
public String getName() {
return name;
} }
public String getSourceBeanName() { public String getSourceBeanName() {
@ -664,12 +718,17 @@ public class PropertyMapping extends ModelElement {
return assignment.getImportTypes(); return assignment.getImportTypes();
} }
public List<String> getDependsOn() {
return dependsOn;
}
@Override @Override
public String toString() { public String toString() {
return "PropertyMapping {" return "PropertyMapping {"
+ "\n targetName='" + targetAccessorName + "\'," + "\n name='" + name + "\',"
+ "\n targetType=" + targetType + "," + "\n targetType=" + targetType + ","
+ "\n propertyAssignment=" + assignment + "\n propertyAssignment=" + assignment + ","
+ "\n dependsOn=" + dependsOn
+ "\n}"; + "\n}";
} }
} }

View File

@ -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<String, Node> nodes = new HashMap<String, Node>();
private final Set<List<String>> cycles = new HashSet<List<String>>();
private final Stack<Node> currentPath = new Stack<Node>();
public void addNode(String name, List<String> 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.
* <p>
* <b>Note</b>:The list will only be complete if the graph contains no cycles.
*/
public Set<String> getAllDescendants(String name) {
Node node = nodes.get( name );
return node != null ? node.getAllDescendants() : Collections.<String>emptySet();
}
public Set<List<String>> 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<String> getCurrentCycle(Node start) {
List<String> cycle = new ArrayList<String>();
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;
}
}

View File

@ -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<Node> descendants;
/**
* All descendants of this node, direct and transitive ones, as discovered through graph traversal.
*/
private final Set<String> allDescendants;
public Node(String name) {
this.name = name;
descendants = new ArrayList<Node>();
allDescendants = new HashSet<String>();
}
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<Node> getDescendants() {
return descendants;
}
public Set<String> 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;
}
}

View File

@ -19,12 +19,12 @@
package org.mapstruct.ap.model.source; package org.mapstruct.ap.model.source;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.AnnotationValue; import javax.lang.model.element.AnnotationValue;
import javax.lang.model.element.ElementKind; import javax.lang.model.element.ElementKind;
@ -57,6 +57,7 @@ public class Mapping {
private final List<TypeMirror> qualifiers; private final List<TypeMirror> qualifiers;
private final TypeMirror resultType; private final TypeMirror resultType;
private final boolean isIgnored; private final boolean isIgnored;
private final List<String> dependsOn;
private final AnnotationMirror mirror; private final AnnotationMirror mirror;
private final AnnotationValue sourceAnnotationValue; private final AnnotationValue sourceAnnotationValue;
@ -120,6 +121,8 @@ public class Mapping {
boolean resultTypeIsDefined = !TypeKind.VOID.equals( mappingPrism.resultType().getKind() ); boolean resultTypeIsDefined = !TypeKind.VOID.equals( mappingPrism.resultType().getKind() );
TypeMirror resultType = resultTypeIsDefined ? mappingPrism.resultType() : null; TypeMirror resultType = resultTypeIsDefined ? mappingPrism.resultType() : null;
List<String> dependsOn =
mappingPrism.dependsOn() != null ? mappingPrism.dependsOn() : Collections.<String>emptyList();
return new Mapping( return new Mapping(
source, source,
@ -132,7 +135,8 @@ public class Mapping {
mappingPrism.mirror, mappingPrism.mirror,
mappingPrism.values.source(), mappingPrism.values.source(),
mappingPrism.values.target(), mappingPrism.values.target(),
resultType resultType,
dependsOn
); );
} }
@ -141,7 +145,7 @@ public class Mapping {
String dateFormat, List<TypeMirror> qualifiers, String dateFormat, List<TypeMirror> qualifiers,
boolean isIgnored, AnnotationMirror mirror, boolean isIgnored, AnnotationMirror mirror,
AnnotationValue sourceAnnotationValue, AnnotationValue targetAnnotationValue, AnnotationValue sourceAnnotationValue, AnnotationValue targetAnnotationValue,
TypeMirror resultType ) { TypeMirror resultType, List<String> dependsOn) {
this.sourceName = sourceName; this.sourceName = sourceName;
this.constant = constant; this.constant = constant;
this.javaExpression = javaExpression; this.javaExpression = javaExpression;
@ -153,6 +157,7 @@ public class Mapping {
this.sourceAnnotationValue = sourceAnnotationValue; this.sourceAnnotationValue = sourceAnnotationValue;
this.targetAnnotationValue = targetAnnotationValue; this.targetAnnotationValue = targetAnnotationValue;
this.resultType = resultType; this.resultType = resultType;
this.dependsOn = dependsOn;
} }
private static String getExpression(MappingPrism mappingPrism, ExecutableElement element, private static String getExpression(MappingPrism mappingPrism, ExecutableElement element,
@ -243,6 +248,10 @@ public class Mapping {
return resultType; return resultType;
} }
public List<String> getDependsOn() {
return dependsOn;
}
private boolean hasPropertyInReverseMethod(String name, SourceMethod method) { private boolean hasPropertyInReverseMethod(String name, SourceMethod method) {
CollectionMappingStrategyPrism cms = method.getMapperConfiguration().getCollectionMappingStrategy(); CollectionMappingStrategyPrism cms = method.getMapperConfiguration().getCollectionMappingStrategy();
return method.getResultType().getTargetAccessors( cms ).containsKey( name ); return method.getResultType().getTargetAccessors( cms ).containsKey( name );
@ -287,7 +296,8 @@ public class Mapping {
mirror, mirror,
sourceAnnotationValue, sourceAnnotationValue,
targetAnnotationValue, targetAnnotationValue,
null null,
Collections.<String>emptyList()
); );
reverse.init( method, messager, typeFactory ); reverse.init( method, messager, typeFactory );
@ -311,8 +321,9 @@ public class Mapping {
mirror, mirror,
sourceAnnotationValue, sourceAnnotationValue,
targetAnnotationValue, targetAnnotationValue,
resultType resultType,
); dependsOn
);
if ( sourceReference != null ) { if ( sourceReference != null ) {
mapping.sourceReference = sourceReference.copyForInheritanceTo( method ); mapping.sourceReference = sourceReference.copyForInheritanceTo( method );

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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);
}

View File

@ -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<String> asStrings(Set<List<String>> cycles) {
Set<String> asStrings = new HashSet<String>();
for ( List<String> cycle : cycles ) {
asStrings.add( asString( cycle ) );
}
return asStrings;
}
private String asString(List<String> 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<String> normalize(List<String> 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;
}
}

View File

@ -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" );
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}