#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 { };
/**
/**
* 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.
* <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
*/
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.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<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
* 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.<String>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.<String>emptyList() )
.build();
propertyMappings.add( propertyMapping );

View File

@ -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<String> dependsOn;
public static class PropertyMappingBuilder {
@ -74,6 +78,7 @@ public class PropertyMapping extends ModelElement {
private TypeMirror resultType;
private SourceReference sourceReference;
private Collection<String> existingVariableNames;
private List<String> dependsOn;
public PropertyMappingBuilder mappingContext(MappingBuilderContext mappingContext) {
this.ctx = mappingContext;
@ -120,6 +125,11 @@ public class PropertyMapping extends ModelElement {
return this;
}
public PropertyMappingBuilder dependsOn(List<String> 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<TypeMirror> qualifiers;
private TypeMirror resultType;
private Collection<String> existingVariableNames;
private List<String> 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<String> 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<String> existingVariableNames;
private String targetPropertyName;
private List<String> 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<String> 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<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.targetAccessorName = targetAccessorName;
this.targetType = targetType;
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() {
@ -664,12 +718,17 @@ public class PropertyMapping extends ModelElement {
return assignment.getImportTypes();
}
public List<String> 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}";
}
}

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;
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<TypeMirror> qualifiers;
private final TypeMirror resultType;
private final boolean isIgnored;
private final List<String> 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<String> dependsOn =
mappingPrism.dependsOn() != null ? mappingPrism.dependsOn() : Collections.<String>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<TypeMirror> qualifiers,
boolean isIgnored, AnnotationMirror mirror,
AnnotationValue sourceAnnotationValue, AnnotationValue targetAnnotationValue,
TypeMirror resultType ) {
TypeMirror resultType, List<String> 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<String> 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.<String>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 );

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