#469 Consider return values of BeforeMapping/AfterMapping methods

This allows to handle cycles in object graphs, as demonstrated in the
test case CallbacksWithReturnValuesTest
This commit is contained in:
Pascal 2016-12-04 16:15:19 +01:00 committed by Andreas Gudian
parent 746f49fe3d
commit 075e763556
22 changed files with 1032 additions and 44 deletions

View File

@ -2083,7 +2083,9 @@ public class VehicleMapperImpl extends VehicleMapper {
if ( car == null ) {
return null;
}
// ...
CarDto carDto = new CarDto();
// attributes mapping ...
fillTank( car, carDto );
@ -2093,14 +2095,60 @@ public class VehicleMapperImpl extends VehicleMapper {
----
====
Only methods with return type `void` may be annotated with `@BeforeMapping` or `@AfterMapping`. The methods may or may not have parameters.
If the `@BeforeMapping` / `@AfterMapping` method has parameters, the method invocation is only generated if all parameters can be *assigned* by the source or target parameters of the mapping method:
* A parameter annotated with `@MappingTarget` is populated with the target instance of the mapping.
* A parameter annotated with `@TargetType` is populated with the target type of the mapping.
* Any other parameter is populated with a source parameter of the mapping, whereas each source parameter is used once at most.
If the before/after-mapping method has a return type other than `void`, it will be checked to match the target type of the mapping methods.
Only the callback methods with a matching return type (or `void`) will be called in that mapping method.
If a callback method returns a non-null value, this value will be returned from the mapping method.
As with mapping methods, it is possible to specify type parameters for before/after-mapping methods.
.Mapper with @AfterMapping hook that returns a non-null value
====
[source, java, linenums]
[subs="verbatim,attributes"]
----
@Mapper
public abstract class VehicleMapper {
@PersistenceContext
private EntityManager entityManager;
@AfterMapping
protected <T> T attachEntity(@MappingTarget T entity) {
return entityManager.merge(entity);
}
public abstract CarDto toCarDto(Car car);
}
// Generates something like this:
public class VehicleMapperImpl extends VehicleMapper {
public CarDto toCarDto(Car car) {
if ( car == null ) {
return null;
}
CarDto carDto = new CarDto();
// attributes mapping ...
CarDto target = attachEntity( carDto );
if ( target != null ) {
return target;
}
return carDto;
}
}
----
====
All before/after-mapping methods that *can* be applied to a mapping method *will* be used. <<selection-based-on-qualifiers>> can be used to further control which methods may be chosen and which not. For that, the qualifier annotation needs to be applied to the before/after-method and referenced in `BeanMapping#qualifiedBy` or `IterableMapping#qualifiedBy`.
The order in which the selected methods are applied is roughly determined by their location of definition (although you should consider it a *code smell* if you need to rely on their order):

View File

@ -175,10 +175,13 @@ public class BeanMappingMethod extends MappingMethod {
sortPropertyMappingsByDependencies();
List<LifecycleCallbackMethodReference> beforeMappingMethods =
LifecycleCallbackFactory.beforeMappingMethods( method, selectionParameters, ctx );
List<LifecycleCallbackMethodReference> beforeMappingMethods = LifecycleCallbackFactory.beforeMappingMethods(
method,
selectionParameters,
ctx,
existingVariableNames );
List<LifecycleCallbackMethodReference> afterMappingMethods =
LifecycleCallbackFactory.afterMappingMethods( method, selectionParameters, ctx );
LifecycleCallbackFactory.afterMappingMethods( method, selectionParameters, ctx, existingVariableNames );
return new BeanMappingMethod(
method,

View File

@ -21,7 +21,9 @@ package org.mapstruct.ap.internal.model;
import static org.mapstruct.ap.internal.util.Collections.first;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.lang.model.type.TypeMirror;
@ -99,10 +101,11 @@ public class EnumMappingMethod extends MappingMethod {
SelectionParameters selectionParameters = getSelecionParameters( method );
Set<String> existingVariables = new HashSet<String>( method.getParameterNames() );
List<LifecycleCallbackMethodReference> beforeMappingMethods =
LifecycleCallbackFactory.beforeMappingMethods( method, selectionParameters, ctx );
LifecycleCallbackFactory.beforeMappingMethods( method, selectionParameters, ctx, existingVariables );
List<LifecycleCallbackMethodReference> afterMappingMethods =
LifecycleCallbackFactory.afterMappingMethods( method, selectionParameters, ctx );
LifecycleCallbackFactory.afterMappingMethods( method, selectionParameters, ctx, existingVariables );
return new EnumMappingMethod( method, enumMappings, beforeMappingMethods, afterMappingMethods );
}

View File

@ -149,10 +149,19 @@ public class IterableMappingMethod extends MappingMethod {
factoryMethod = ctx.getMappingResolver().getFactoryMethod( method, method.getResultType(), null );
}
List<LifecycleCallbackMethodReference> beforeMappingMethods =
LifecycleCallbackFactory.beforeMappingMethods( method, selectionParameters, ctx );
List<LifecycleCallbackMethodReference> afterMappingMethods =
LifecycleCallbackFactory.afterMappingMethods( method, selectionParameters, ctx );
Set<String> existingVariables = new HashSet<String>( method.getParameterNames() );
existingVariables.add( loopVariableName );
List<LifecycleCallbackMethodReference> beforeMappingMethods = LifecycleCallbackFactory.beforeMappingMethods(
method,
selectionParameters,
ctx,
existingVariables );
List<LifecycleCallbackMethodReference> afterMappingMethods = LifecycleCallbackFactory.afterMappingMethods(
method,
selectionParameters,
ctx,
existingVariables );
return new IterableMappingMethod(
method,

View File

@ -22,6 +22,7 @@ import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.mapstruct.ap.internal.model.common.Parameter;
import org.mapstruct.ap.internal.model.common.Type;
@ -45,35 +46,43 @@ public final class LifecycleCallbackFactory {
* @param method the method to obtain the beforeMapping methods for
* @param selectionParameters method selectionParameters
* @param ctx the builder context
* @param existingVariableNames the existing variable names in the mapping method
* @return all applicable {@code @BeforeMapping} methods for the given method
*/
public static List<LifecycleCallbackMethodReference> beforeMappingMethods(
Method method, SelectionParameters selectionParameters, MappingBuilderContext ctx) {
public static List<LifecycleCallbackMethodReference> beforeMappingMethods(Method method,
SelectionParameters selectionParameters,
MappingBuilderContext ctx,
Set<String> existingVariableNames) {
return collectLifecycleCallbackMethods(
method,
selectionParameters,
filterBeforeMappingMethods( ctx.getSourceModel() ),
ctx );
ctx,
existingVariableNames );
}
/**
* @param method the method to obtain the afterMapping methods for
* @param selectionParameters method selectionParameters
* @param ctx the builder context
* @param existingVariableNames list of already used variable names
* @return all applicable {@code @AfterMapping} methods for the given method
*/
public static List<LifecycleCallbackMethodReference> afterMappingMethods(
Method method, SelectionParameters selectionParameters, MappingBuilderContext ctx) {
public static List<LifecycleCallbackMethodReference> afterMappingMethods(Method method,
SelectionParameters selectionParameters,
MappingBuilderContext ctx,
Set<String> existingVariableNames) {
return collectLifecycleCallbackMethods(
method,
selectionParameters,
filterAfterMappingMethods( ctx.getSourceModel() ),
ctx );
ctx,
existingVariableNames );
}
private static List<LifecycleCallbackMethodReference> collectLifecycleCallbackMethods(
Method method, SelectionParameters selectionParameters, List<SourceMethod> callbackMethods,
MappingBuilderContext ctx) {
Method method, SelectionParameters selectionParameters, List<SourceMethod> callbackMethods,
MappingBuilderContext ctx, Set<String> existingVariableNames) {
Map<SourceMethod, List<Parameter>> parameterAssignmentsForSourceMethod
= new HashMap<SourceMethod, List<Parameter>>();
@ -83,7 +92,12 @@ public final class LifecycleCallbackFactory {
candidates = filterCandidatesByQualifiers( method, selectionParameters, candidates, ctx );
return toLifecycleCallbackMethodRefs( candidates, parameterAssignmentsForSourceMethod, ctx );
return toLifecycleCallbackMethodRefs(
method,
candidates,
parameterAssignmentsForSourceMethod,
ctx,
existingVariableNames );
}
private static List<SourceMethod> filterCandidatesByQualifiers(Method method,
@ -99,16 +113,19 @@ public final class LifecycleCallbackFactory {
false) );
}
private static List<LifecycleCallbackMethodReference> toLifecycleCallbackMethodRefs(
private static List<LifecycleCallbackMethodReference> toLifecycleCallbackMethodRefs(Method method,
List<SourceMethod> candidates, Map<SourceMethod, List<Parameter>> parameterAssignmentsForSourceMethod,
MappingBuilderContext ctx) {
MappingBuilderContext ctx, Set<String> existingVariableNames) {
List<LifecycleCallbackMethodReference> result = new ArrayList<LifecycleCallbackMethodReference>();
for ( SourceMethod candidate : candidates ) {
markMapperReferenceAsUsed( ctx.getMapperReferences(), candidate );
result.add( new LifecycleCallbackMethodReference(
candidate,
parameterAssignmentsForSourceMethod.get( candidate ) ) );
result.add(
new LifecycleCallbackMethodReference(
candidate,
parameterAssignmentsForSourceMethod.get( candidate ),
method.getReturnType(),
method.getResultType(),
existingVariableNames ) );
}
return result;
}
@ -124,9 +141,7 @@ public final class LifecycleCallbackFactory {
List<Parameter> parameterAssignments =
ParameterAssignmentUtil.getParameterAssignments( availableParams, callback.getParameters() );
if ( parameterAssignments != null
&& callback.matches( extractSourceTypes( parameterAssignments ), method.getResultType() ) ) {
if ( isValidCandidate( callback, method, parameterAssignments ) ) {
parameterAssignmentsForSourceMethod.put( callback, parameterAssignments );
candidates.add( callback );
}
@ -134,6 +149,18 @@ public final class LifecycleCallbackFactory {
return candidates;
}
private static boolean isValidCandidate(SourceMethod candidate, Method method,
List<Parameter> parameterAssignments) {
if ( parameterAssignments == null ) {
return false;
}
if ( !candidate.matches( extractSourceTypes( parameterAssignments ), method.getResultType() ) ) {
return false;
}
return ( candidate.getReturnType().isVoid() || candidate.getReturnType().isTypeVar()
|| candidate.getReturnType().isAssignableTo( method.getResultType() ) );
}
private static List<Parameter> getAvailableParameters(Method method, MappingBuilderContext ctx) {
List<Parameter> availableParams = new ArrayList<Parameter>( method.getParameters() );
if ( method.getMappingTargetParameter() == null ) {
@ -154,7 +181,9 @@ public final class LifecycleCallbackFactory {
private static void markMapperReferenceAsUsed(List<MapperReference> references, Method method) {
for ( MapperReference ref : references ) {
if ( ref.getType().equals( method.getDeclaringMapper() ) ) {
ref.setUsed( !method.isStatic() );
if ( !ref.isUsed() && !method.isStatic() ) {
ref.setUsed( true );
}
ref.setTypeRequiresImport( true );
return;

View File

@ -24,6 +24,7 @@ import java.util.Set;
import org.mapstruct.ap.internal.model.common.Parameter;
import org.mapstruct.ap.internal.model.common.Type;
import org.mapstruct.ap.internal.model.source.Method;
import org.mapstruct.ap.internal.model.source.SourceMethod;
import org.mapstruct.ap.internal.util.Collections;
import org.mapstruct.ap.internal.util.Strings;
@ -37,11 +38,38 @@ public class LifecycleCallbackMethodReference extends MappingMethod {
private final Type declaringType;
private final List<Parameter> parameterAssignments;
private final Type methodReturnType;
private final Type methodResultType;
private final String instanceVariableName;
private final String targetVariableName;
public LifecycleCallbackMethodReference(SourceMethod method, List<Parameter> parameterAssignments) {
public LifecycleCallbackMethodReference(SourceMethod method, List<Parameter> parameterAssignments,
Type methodReturnType, Type methodResultType,
Set<String> existingVariableNames) {
super( method );
this.declaringType = method.getDeclaringMapper();
this.parameterAssignments = parameterAssignments;
this.methodReturnType = methodReturnType;
this.methodResultType = methodResultType;
if ( isStatic() ) {
this.instanceVariableName = declaringType.getName();
}
else if ( declaringType != null ) {
this.instanceVariableName =
Strings.getSaveVariableName( Introspector.decapitalize( declaringType.getName() ) );
}
else {
this.instanceVariableName = null;
}
if ( hasReturnType() ) {
this.targetVariableName = Strings.getSaveVariableName( "target", existingVariableNames );
existingVariableNames.add( this.targetVariableName );
}
else {
this.targetVariableName = null;
}
}
public Type getDeclaringType() {
@ -49,7 +77,31 @@ public class LifecycleCallbackMethodReference extends MappingMethod {
}
public String getInstanceVariableName() {
return Strings.getSaveVariableName( Introspector.decapitalize( declaringType.getName() ) );
return instanceVariableName;
}
/**
* Returns the return type of the mapping method in which this callback method is called
*
* @return return type
* @see Method#getReturnType()
*/
public Type getMethodReturnType() {
return methodReturnType;
}
/**
* Returns the result type of the mapping method in which this callback method is called
*
* @return result type
* @see Method#getResultType()
*/
public Type getMethodResultType() {
return methodResultType;
}
public String getTargetVariableName() {
return targetVariableName;
}
@Override
@ -70,4 +122,11 @@ public class LifecycleCallbackMethodReference extends MappingMethod {
return false;
}
/**
* @return true if this callback method has a return type that is not void
*/
public boolean hasReturnType() {
return !getReturnType().isVoid();
}
}

View File

@ -178,10 +178,11 @@ public class MapMappingMethod extends MappingMethod {
keyAssignment = new LocalVarWrapper( keyAssignment, method.getThrownTypes(), keyTargetType, false );
valueAssignment = new LocalVarWrapper( valueAssignment, method.getThrownTypes(), valueTargetType, false );
Set<String> existingVariables = new HashSet<String>( method.getParameterNames() );
List<LifecycleCallbackMethodReference> beforeMappingMethods =
LifecycleCallbackFactory.beforeMappingMethods( method, null, ctx );
LifecycleCallbackFactory.beforeMappingMethods( method, null, ctx, existingVariables );
List<LifecycleCallbackMethodReference> afterMappingMethods =
LifecycleCallbackFactory.afterMappingMethods( method, null, ctx );
LifecycleCallbackFactory.afterMappingMethods( method, null, ctx, existingVariables );
return new MapMappingMethod(
method,

View File

@ -21,7 +21,9 @@ package org.mapstruct.ap.internal.model;
import static org.mapstruct.ap.internal.util.Collections.first;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.lang.model.type.TypeMirror;
@ -113,13 +115,13 @@ public class ValueMappingMethod extends MappingMethod {
// do before / after lifecycle mappings
SelectionParameters selectionParameters = getSelectionParameters( method );
List<LifecycleCallbackMethodReference> beforeMappingMethods
= LifecycleCallbackFactory.beforeMappingMethods( method, selectionParameters, ctx );
List<LifecycleCallbackMethodReference> afterMappingMethods
= LifecycleCallbackFactory.afterMappingMethods( method, selectionParameters, ctx );
Set<String> existingVariables = new HashSet<String>( method.getParameterNames() );
List<LifecycleCallbackMethodReference> beforeMappingMethods =
LifecycleCallbackFactory.beforeMappingMethods( method, selectionParameters, ctx, existingVariables );
List<LifecycleCallbackMethodReference> afterMappingMethods =
LifecycleCallbackFactory.afterMappingMethods( method, selectionParameters, ctx, existingVariables );
// finallyn return a mapping
// finally return a mapping
return new ValueMappingMethod( method, mappingEntries, nullTarget, defaultTarget,
throwIllegalArgumentException, beforeMappingMethods, afterMappingMethods );
}

View File

@ -286,7 +286,7 @@ public class MethodRetrievalProcessor implements ModelElementProcessor<Void, Lis
}
private boolean isValidLifecycleCallbackMethod(ExecutableElement method, Type returnType) {
return isVoid( returnType ) && Executables.isLifecycleCallbackMethod( method );
return Executables.isLifecycleCallbackMethod( method );
}
private boolean isValidReferencedMethod(List<Parameter> parameters) {
@ -386,7 +386,7 @@ public class MethodRetrievalProcessor implements ModelElementProcessor<Void, Lis
return false;
}
for ( Parameter sourceParameter : sourceParameters ) {
for ( Parameter sourceParameter : sourceParameters ) {
if ( sourceParameter.getType().isTypeVar() ) {
messager.printMessage( method, Message.RETRIEVAL_TYPE_VAR_SOURCE );
return false;

View File

@ -18,4 +18,16 @@
limitations under the License.
-->
<#if declaringType??>${instanceVariableName}.</#if>${name}(<#list parameterAssignments as param> <#if param.targetType><@includeModel object=ext.targetType raw=true/>.class<#elseif param.mappingTarget>${ext.targetBeanName}<#else>${param.name}</#if><#if param_has_next>,<#else> </#if></#list>);
<@compress single_line=true>
<#if hasReturnType()>
<@includeModel object=methodResultType /> ${targetVariableName} =
</#if>
<#if declaringType??>${instanceVariableName}.</#if>${name}(
<#list parameterAssignments as param>
<#if param.targetType><@includeModel object=ext.targetType raw=true/>.class<#elseif param.mappingTarget>${ext.targetBeanName}<#else>${param.name}</#if><#if param_has_next>,<#else> </#if>
</#list>);
</@compress>
<#if hasReturnType()><#nt>
if ( ${targetVariableName} != null ) {
return<#if methodReturnType.name != "void"> ${targetVariableName}</#if>;
}</#if>

View File

@ -0,0 +1,67 @@
/**
* Copyright 2012-2016 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.callbacks.returning;
/**
* @author Pascal Grün
*/
public class Attribute {
private Node node;
private String name;
private String value;
public Attribute() {
// default constructor for MapStruct
}
public Attribute(String name, String value) {
this.name = name;
this.value = value;
}
public Node getNode() {
return node;
}
public void setNode(Node node) {
this.node = node;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
@Override
public String toString() {
return "Attribute [name=" + name + ", value=" + value + "]";
}
}

View File

@ -0,0 +1,58 @@
/**
* Copyright 2012-2016 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.callbacks.returning;
/**
* @author Pascal Grün
*/
public class AttributeDTO {
private NodeDTO node;
private String name;
private String value;
public NodeDTO getNode() {
return node;
}
public void setNode(NodeDTO node) {
this.node = node;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
@Override
public String toString() {
return "AttributeDTO [name=" + name + ", value=" + value + "]";
}
}

View File

@ -0,0 +1,115 @@
/**
* Copyright 2012-2016 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.callbacks.returning;
import static org.assertj.core.api.Assertions.assertThat;
import java.util.Arrays;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mapstruct.ap.test.callbacks.returning.NodeMapperContext.ContextListener;
import org.mapstruct.ap.testutil.IssueKey;
import org.mapstruct.ap.testutil.WithClasses;
import org.mapstruct.ap.testutil.runner.AnnotationProcessorTestRunner;
/**
* Test case for https://github.com/mapstruct/mapstruct/issues/469
*
* @author Pascal Grün
*/
@IssueKey( "469" )
@WithClasses( { Attribute.class, AttributeDTO.class, Node.class, NodeDTO.class, NodeMapperDefault.class,
NodeMapperWithContext.class, NodeMapperContext.class, Number.class, NumberMapperDefault.class,
NumberMapperContext.class, NumberMapperWithContext.class } )
@RunWith( AnnotationProcessorTestRunner.class )
public class CallbacksWithReturnValuesTest {
@Test( expected = StackOverflowError.class )
public void mappingWithDefaultHandlingRaisesStackOverflowError() {
Node root = buildNodes();
NodeMapperDefault.INSTANCE.nodeToNodeDTO( root );
}
@Test( expected = StackOverflowError.class )
public void updatingWithDefaultHandlingRaisesStackOverflowError() {
Node root = buildNodes();
NodeMapperDefault.INSTANCE.nodeToNodeDTO( root, new NodeDTO() );
}
@Test
public void mappingWithContextCorrectlyResolvesCycles() {
final AtomicReference<Integer> contextLevel = new AtomicReference<Integer>( null );
ContextListener contextListener = new ContextListener() {
@Override
public void methodCalled(Integer level, String method, Object source, Object target) {
contextLevel.set( level );
}
};
NodeMapperContext.addContextListener( contextListener );
try {
Node root = buildNodes();
NodeDTO rootDTO = NodeMapperWithContext.INSTANCE.nodeToNodeDTO( root );
assertThat( rootDTO ).isNotNull();
assertThat( contextLevel.get() ).isEqualTo( Integer.valueOf( 1 ) );
}
finally {
NodeMapperContext.removeContextListener( contextListener );
}
}
private static Node buildNodes() {
Node root = new Node( "root" );
root.addAttribute( new Attribute( "name", "root" ) );
Node node1 = new Node( "node1" );
node1.addAttribute( new Attribute( "name", "node1" ) );
root.addChild( node1 );
return root;
}
@Test
public void numberMappingWithoutContextDoesNotUseCache() {
Number n1 = NumberMapperDefault.INSTANCE.integerToNumber( 2342 );
Number n2 = NumberMapperDefault.INSTANCE.integerToNumber( 2342 );
assertThat( n1 ).isEqualTo( n2 );
assertThat( n1 ).isNotSameAs( n2 );
}
@Test
public void numberMappingWithContextUsesCache() {
NumberMapperContext.putCache( new Number( 2342 ) );
Number n1 = NumberMapperWithContext.INSTANCE.integerToNumber( 2342 );
Number n2 = NumberMapperWithContext.INSTANCE.integerToNumber( 2342 );
assertThat( n1 ).isEqualTo( n2 );
assertThat( n1 ).isSameAs( n2 );
NumberMapperContext.clearCache();
}
@Test
public void numberMappingWithContextCallsVisitNumber() {
Number n1 = NumberMapperWithContext.INSTANCE.integerToNumber( 1234 );
Number n2 = NumberMapperWithContext.INSTANCE.integerToNumber( 5678 );
assertThat( NumberMapperContext.getVisited() ).isEqualTo( Arrays.asList( n1, n2 ) );
NumberMapperContext.clearVisited();
}
}

View File

@ -0,0 +1,91 @@
/**
* Copyright 2012-2016 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.callbacks.returning;
import java.util.ArrayList;
import java.util.List;
/**
* @author Pascal Grün
*/
public class Node {
private Node parent;
private String name;
private List<Node> children;
private List<Attribute> attributes;
public Node() {
// default constructor for MapStruct
}
public Node(String name) {
this.name = name;
this.children = new ArrayList<Node>();
this.attributes = new ArrayList<Attribute>();
}
public Node getParent() {
return parent;
}
public void setParent(Node parent) {
this.parent = parent;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public List<Node> getChildren() {
return children;
}
public void setChildren(List<Node> children) {
this.children = children;
}
public void addChild(Node node) {
children.add( node );
node.setParent( this );
}
public List<Attribute> getAttributes() {
return attributes;
}
public void setAttributes(List<Attribute> attributes) {
this.attributes = attributes;
}
public void addAttribute(Attribute attribute) {
attributes.add( attribute );
attribute.setNode( this );
}
@Override
public String toString() {
return "Node [name=" + name + "]";
}
}

View File

@ -0,0 +1,70 @@
/**
* Copyright 2012-2016 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.callbacks.returning;
import java.util.List;
/**
* @author Pascal Grün
*/
public class NodeDTO {
private NodeDTO parent;
private String name;
private List<NodeDTO> children;
private List<AttributeDTO> attributes;
public NodeDTO getParent() {
return parent;
}
public void setParent(NodeDTO parent) {
this.parent = parent;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public List<NodeDTO> getChildren() {
return children;
}
public void setChildren(List<NodeDTO> children) {
this.children = children;
}
public List<AttributeDTO> getAttributes() {
return attributes;
}
public void setAttributes(List<AttributeDTO> attributes) {
this.attributes = attributes;
}
@Override
public String toString() {
return "NodeDTO [name=" + name + "]";
}
}

View File

@ -0,0 +1,114 @@
/**
* Copyright 2012-2016 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.callbacks.returning;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;
import org.mapstruct.AfterMapping;
import org.mapstruct.BeforeMapping;
import org.mapstruct.MappingTarget;
import org.mapstruct.TargetType;
/**
* @author Pascal Grün
*/
public class NodeMapperContext {
private static final ThreadLocal<Integer> LEVEL = new ThreadLocal<Integer>();
private static final ThreadLocal<Map<Object, Object>> MAPPING = new ThreadLocal<Map<Object, Object>>();
/** Only for test-inspection */
private static final List<ContextListener> LISTENERS = new CopyOnWriteArrayList<ContextListener>();
private NodeMapperContext() {
// Only allow static access
}
@BeforeMapping
@SuppressWarnings( "unchecked" )
public static <T> T getInstance(Object source, @TargetType Class<T> type) {
fireMethodCalled( LEVEL.get(), "getInstance", source, null );
Map<Object, Object> mapping = MAPPING.get();
if ( mapping == null ) {
return null;
}
else {
return (T) mapping.get( source );
}
}
@BeforeMapping
public static void setInstance(Object source, @MappingTarget Object target) {
Integer level = LEVEL.get();
fireMethodCalled( level, "setInstance", source, target );
if ( level == null ) {
LEVEL.set( 1 );
MAPPING.set( new IdentityHashMap<Object, Object>() );
}
else {
LEVEL.set( level + 1 );
}
MAPPING.get().put( source, target );
}
@AfterMapping
public static void cleanup() {
Integer level = LEVEL.get();
fireMethodCalled( level, "cleanup", null, null );
if ( level == 1 ) {
MAPPING.set( null );
LEVEL.set( null );
}
else {
LEVEL.set( level - 1 );
}
}
/**
* Only for test-inspection
*/
static void addContextListener(ContextListener contextListener) {
LISTENERS.add( contextListener );
}
/**
* Only for test-inspection
*/
static void removeContextListener(ContextListener contextListener) {
LISTENERS.remove( contextListener );
}
/**
* Only for test-inspection
*/
private static void fireMethodCalled(Integer level, String method, Object source, Object target) {
for ( ContextListener contextListener : LISTENERS ) {
contextListener.methodCalled( level, method, source, target );
}
}
/**
* Only for test-inspection
*/
interface ContextListener {
void methodCalled(Integer level, String method, Object source, Object target);
}
}

View File

@ -0,0 +1,39 @@
/**
* Copyright 2012-2016 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.callbacks.returning;
import org.mapstruct.Mapper;
import org.mapstruct.MappingTarget;
import org.mapstruct.factory.Mappers;
/**
* @author Pascal Grün
*/
@Mapper
public abstract class NodeMapperDefault {
public static final NodeMapperDefault INSTANCE = Mappers.getMapper( NodeMapperDefault.class );
public abstract NodeDTO nodeToNodeDTO(Node node);
public abstract void nodeToNodeDTO(Node node, @MappingTarget NodeDTO nodeDto);
protected abstract AttributeDTO attributeToAttributeDTO(Attribute attribute);
protected abstract void attributeToAttributeDTO(Attribute attribute, @MappingTarget AttributeDTO nodeDto);
}

View File

@ -0,0 +1,39 @@
/**
* Copyright 2012-2016 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.callbacks.returning;
import org.mapstruct.Mapper;
import org.mapstruct.MappingTarget;
import org.mapstruct.factory.Mappers;
/**
* @author Pascal Grün
*/
@Mapper(uses = NodeMapperContext.class )
public abstract class NodeMapperWithContext {
public static final NodeMapperWithContext INSTANCE = Mappers.getMapper( NodeMapperWithContext.class );
public abstract NodeDTO nodeToNodeDTO(Node node);
public abstract void nodeToNodeDTO(Node node, @MappingTarget NodeDTO nodeDto);
protected abstract AttributeDTO attributeToAttributeDTO(Attribute attribute);
protected abstract void attributeToAttributeDTO(Attribute attribute, @MappingTarget AttributeDTO nodeDto);
}

View File

@ -0,0 +1,64 @@
/**
* Copyright 2012-2016 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.callbacks.returning;
/**
* @author Pascal Grün
*/
public class Number {
private int number;
public Number() {
this( 0 );
}
public Number(int number) {
this.number = number;
}
public void setNumber(int number) {
this.number = number;
}
public int getNumber() {
return number;
}
@Override
public int hashCode() {
return 31 + number;
}
@Override
public boolean equals(Object obj) {
if ( this == obj ) {
return true;
}
if ( obj == null || getClass() != obj.getClass() ) {
return false;
}
Number other = (Number) obj;
return number == other.number;
}
@Override
public String toString() {
return "Number[number=" + number + "]";
}
}

View File

@ -0,0 +1,90 @@
/**
* Copyright 2012-2016 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.callbacks.returning;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.mapstruct.AfterMapping;
import org.mapstruct.BeforeMapping;
import org.mapstruct.MappingTarget;
/**
* @author Pascal Grün
*/
public class NumberMapperContext {
private static final Map<Number, Number> CACHE = new HashMap<Number, Number>();
private static final List<Number> VISITED = new ArrayList<Number>();
private NumberMapperContext() {
// Only allow static access
}
public static void putCache(Number number) {
CACHE.put( number, number );
}
public static void clearCache() {
CACHE.clear();
}
public static List<Number> getVisited() {
return VISITED;
}
public static void clearVisited() {
VISITED.clear();
}
@AfterMapping
public static Number getInstance(Integer source, @MappingTarget Number target) {
Number cached = CACHE.get( target );
return ( cached == null ? null : cached );
}
@AfterMapping
public static <T extends Number> T visitNumber(@MappingTarget T number) {
VISITED.add( number );
return number;
}
@AfterMapping
public static Map<String, Integer> withMap(Map<String, Long> source, @MappingTarget Map<String, Integer> target) {
return target;
}
@AfterMapping
public static List<String> withList(Set<Integer> source, @MappingTarget List<String> target) {
return target;
}
@BeforeMapping
public static String neverCalled1(Integer integer) {
throw new IllegalStateException( "This method must never be called, because the return type does not match!" );
}
@AfterMapping
public static String neverCalled2(Integer integer) {
throw new IllegalStateException( "This method must never be called, because the return type does not match!" );
}
}

View File

@ -0,0 +1,32 @@
/**
* Copyright 2012-2016 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.callbacks.returning;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
/**
* @author Pascal Grün
*/
@Mapper
public abstract class NumberMapperDefault {
public static final NumberMapperDefault INSTANCE = Mappers.getMapper( NumberMapperDefault.class );
public abstract Number integerToNumber(Integer number);
}

View File

@ -0,0 +1,43 @@
/**
* Copyright 2012-2016 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.callbacks.returning;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.mapstruct.Mapper;
import org.mapstruct.MappingTarget;
import org.mapstruct.factory.Mappers;
/**
* @author Pascal Grün
*/
@Mapper( uses = NumberMapperContext.class )
public abstract class NumberMapperWithContext {
public static final NumberMapperWithContext INSTANCE = Mappers.getMapper( NumberMapperWithContext.class );
public abstract Number integerToNumber(Integer number);
public abstract void integerToNumber(Integer number, @MappingTarget Number target);
public abstract Map<String, Integer> longMapToIntegerMap(Map<String, Long> target);
public abstract List<String> setToList(Set<Integer> target);
}