diff --git a/documentation/src/main/asciidoc/mapstruct-reference-guide.asciidoc b/documentation/src/main/asciidoc/mapstruct-reference-guide.asciidoc index 0f4eeee23..10fc413f9 100644 --- a/documentation/src/main/asciidoc/mapstruct-reference-guide.asciidoc +++ b/documentation/src/main/asciidoc/mapstruct-reference-guide.asciidoc @@ -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 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. <> 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): diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/BeanMappingMethod.java b/processor/src/main/java/org/mapstruct/ap/internal/model/BeanMappingMethod.java index 6774d23ff..7f416519d 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/BeanMappingMethod.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/BeanMappingMethod.java @@ -175,10 +175,13 @@ public class BeanMappingMethod extends MappingMethod { sortPropertyMappingsByDependencies(); - List beforeMappingMethods = - LifecycleCallbackFactory.beforeMappingMethods( method, selectionParameters, ctx ); + List beforeMappingMethods = LifecycleCallbackFactory.beforeMappingMethods( + method, + selectionParameters, + ctx, + existingVariableNames ); List afterMappingMethods = - LifecycleCallbackFactory.afterMappingMethods( method, selectionParameters, ctx ); + LifecycleCallbackFactory.afterMappingMethods( method, selectionParameters, ctx, existingVariableNames ); return new BeanMappingMethod( method, diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/EnumMappingMethod.java b/processor/src/main/java/org/mapstruct/ap/internal/model/EnumMappingMethod.java index 245a37ceb..42418dc76 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/EnumMappingMethod.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/EnumMappingMethod.java @@ -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 existingVariables = new HashSet( method.getParameterNames() ); List beforeMappingMethods = - LifecycleCallbackFactory.beforeMappingMethods( method, selectionParameters, ctx ); + LifecycleCallbackFactory.beforeMappingMethods( method, selectionParameters, ctx, existingVariables ); List afterMappingMethods = - LifecycleCallbackFactory.afterMappingMethods( method, selectionParameters, ctx ); + LifecycleCallbackFactory.afterMappingMethods( method, selectionParameters, ctx, existingVariables ); return new EnumMappingMethod( method, enumMappings, beforeMappingMethods, afterMappingMethods ); } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/IterableMappingMethod.java b/processor/src/main/java/org/mapstruct/ap/internal/model/IterableMappingMethod.java index ea723b581..81c4b9add 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/IterableMappingMethod.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/IterableMappingMethod.java @@ -149,10 +149,19 @@ public class IterableMappingMethod extends MappingMethod { factoryMethod = ctx.getMappingResolver().getFactoryMethod( method, method.getResultType(), null ); } - List beforeMappingMethods = - LifecycleCallbackFactory.beforeMappingMethods( method, selectionParameters, ctx ); - List afterMappingMethods = - LifecycleCallbackFactory.afterMappingMethods( method, selectionParameters, ctx ); + Set existingVariables = new HashSet( method.getParameterNames() ); + existingVariables.add( loopVariableName ); + + List beforeMappingMethods = LifecycleCallbackFactory.beforeMappingMethods( + method, + selectionParameters, + ctx, + existingVariables ); + List afterMappingMethods = LifecycleCallbackFactory.afterMappingMethods( + method, + selectionParameters, + ctx, + existingVariables ); return new IterableMappingMethod( method, diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/LifecycleCallbackFactory.java b/processor/src/main/java/org/mapstruct/ap/internal/model/LifecycleCallbackFactory.java index 35ab07da1..648ba8a22 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/LifecycleCallbackFactory.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/LifecycleCallbackFactory.java @@ -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 beforeMappingMethods( - Method method, SelectionParameters selectionParameters, MappingBuilderContext ctx) { + public static List beforeMappingMethods(Method method, + SelectionParameters selectionParameters, + MappingBuilderContext ctx, + Set 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 afterMappingMethods( - Method method, SelectionParameters selectionParameters, MappingBuilderContext ctx) { + public static List afterMappingMethods(Method method, + SelectionParameters selectionParameters, + MappingBuilderContext ctx, + Set existingVariableNames) { return collectLifecycleCallbackMethods( method, selectionParameters, filterAfterMappingMethods( ctx.getSourceModel() ), - ctx ); + ctx, + existingVariableNames ); } private static List collectLifecycleCallbackMethods( - Method method, SelectionParameters selectionParameters, List callbackMethods, - MappingBuilderContext ctx) { + Method method, SelectionParameters selectionParameters, List callbackMethods, + MappingBuilderContext ctx, Set existingVariableNames) { Map> parameterAssignmentsForSourceMethod = new HashMap>(); @@ -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 filterCandidatesByQualifiers(Method method, @@ -99,16 +113,19 @@ public final class LifecycleCallbackFactory { false) ); } - private static List toLifecycleCallbackMethodRefs( + private static List toLifecycleCallbackMethodRefs(Method method, List candidates, Map> parameterAssignmentsForSourceMethod, - MappingBuilderContext ctx) { - + MappingBuilderContext ctx, Set existingVariableNames) { List result = new ArrayList(); 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 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 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 getAvailableParameters(Method method, MappingBuilderContext ctx) { List availableParams = new ArrayList( method.getParameters() ); if ( method.getMappingTargetParameter() == null ) { @@ -154,7 +181,9 @@ public final class LifecycleCallbackFactory { private static void markMapperReferenceAsUsed(List 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; diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/LifecycleCallbackMethodReference.java b/processor/src/main/java/org/mapstruct/ap/internal/model/LifecycleCallbackMethodReference.java index 23045933d..0ffc67441 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/LifecycleCallbackMethodReference.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/LifecycleCallbackMethodReference.java @@ -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 parameterAssignments; + private final Type methodReturnType; + private final Type methodResultType; + private final String instanceVariableName; + private final String targetVariableName; - public LifecycleCallbackMethodReference(SourceMethod method, List parameterAssignments) { + public LifecycleCallbackMethodReference(SourceMethod method, List parameterAssignments, + Type methodReturnType, Type methodResultType, + Set 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(); + } } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/MapMappingMethod.java b/processor/src/main/java/org/mapstruct/ap/internal/model/MapMappingMethod.java index 06643a27f..35946f59d 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/MapMappingMethod.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/MapMappingMethod.java @@ -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 existingVariables = new HashSet( method.getParameterNames() ); List beforeMappingMethods = - LifecycleCallbackFactory.beforeMappingMethods( method, null, ctx ); + LifecycleCallbackFactory.beforeMappingMethods( method, null, ctx, existingVariables ); List afterMappingMethods = - LifecycleCallbackFactory.afterMappingMethods( method, null, ctx ); + LifecycleCallbackFactory.afterMappingMethods( method, null, ctx, existingVariables ); return new MapMappingMethod( method, diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/ValueMappingMethod.java b/processor/src/main/java/org/mapstruct/ap/internal/model/ValueMappingMethod.java index f0dd0946d..3e4d0d095 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/ValueMappingMethod.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/ValueMappingMethod.java @@ -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 beforeMappingMethods - = LifecycleCallbackFactory.beforeMappingMethods( method, selectionParameters, ctx ); - List afterMappingMethods - = LifecycleCallbackFactory.afterMappingMethods( method, selectionParameters, ctx ); + Set existingVariables = new HashSet( method.getParameterNames() ); + List beforeMappingMethods = + LifecycleCallbackFactory.beforeMappingMethods( method, selectionParameters, ctx, existingVariables ); + List 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 ); } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/processor/MethodRetrievalProcessor.java b/processor/src/main/java/org/mapstruct/ap/internal/processor/MethodRetrievalProcessor.java index 2c20a2e5a..95f7a33ab 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/processor/MethodRetrievalProcessor.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/processor/MethodRetrievalProcessor.java @@ -286,7 +286,7 @@ public class MethodRetrievalProcessor implements ModelElementProcessor parameters) { @@ -386,7 +386,7 @@ public class MethodRetrievalProcessor implements ModelElementProcessor -<#if declaringType??>${instanceVariableName}.${name}(<#list parameterAssignments as param> <#if param.targetType><@includeModel object=ext.targetType raw=true/>.class<#elseif param.mappingTarget>${ext.targetBeanName}<#else>${param.name}<#if param_has_next>,<#else> ); +<@compress single_line=true> + <#if hasReturnType()> + <@includeModel object=methodResultType /> ${targetVariableName} = + + <#if declaringType??>${instanceVariableName}.${name}( + <#list parameterAssignments as param> + <#if param.targetType><@includeModel object=ext.targetType raw=true/>.class<#elseif param.mappingTarget>${ext.targetBeanName}<#else>${param.name}<#if param_has_next>,<#else> + ); + +<#if hasReturnType()><#nt> +if ( ${targetVariableName} != null ) { + return<#if methodReturnType.name != "void"> ${targetVariableName}; +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/callbacks/returning/Attribute.java b/processor/src/test/java/org/mapstruct/ap/test/callbacks/returning/Attribute.java new file mode 100644 index 000000000..3f242e840 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/callbacks/returning/Attribute.java @@ -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 + "]"; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/callbacks/returning/AttributeDTO.java b/processor/src/test/java/org/mapstruct/ap/test/callbacks/returning/AttributeDTO.java new file mode 100644 index 000000000..db2821cce --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/callbacks/returning/AttributeDTO.java @@ -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 + "]"; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/callbacks/returning/CallbacksWithReturnValuesTest.java b/processor/src/test/java/org/mapstruct/ap/test/callbacks/returning/CallbacksWithReturnValuesTest.java new file mode 100644 index 000000000..f609bf31e --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/callbacks/returning/CallbacksWithReturnValuesTest.java @@ -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 contextLevel = new AtomicReference( 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(); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/callbacks/returning/Node.java b/processor/src/test/java/org/mapstruct/ap/test/callbacks/returning/Node.java new file mode 100644 index 000000000..3e50d90ae --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/callbacks/returning/Node.java @@ -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 children; + private List attributes; + + public Node() { + // default constructor for MapStruct + } + + public Node(String name) { + this.name = name; + this.children = new ArrayList(); + this.attributes = new ArrayList(); + } + + 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 getChildren() { + return children; + } + + public void setChildren(List children) { + this.children = children; + } + + public void addChild(Node node) { + children.add( node ); + node.setParent( this ); + } + + public List getAttributes() { + return attributes; + } + + public void setAttributes(List attributes) { + this.attributes = attributes; + } + + public void addAttribute(Attribute attribute) { + attributes.add( attribute ); + attribute.setNode( this ); + } + + @Override + public String toString() { + return "Node [name=" + name + "]"; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/callbacks/returning/NodeDTO.java b/processor/src/test/java/org/mapstruct/ap/test/callbacks/returning/NodeDTO.java new file mode 100644 index 000000000..97339009a --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/callbacks/returning/NodeDTO.java @@ -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 children; + private List 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 getChildren() { + return children; + } + + public void setChildren(List children) { + this.children = children; + } + + public List getAttributes() { + return attributes; + } + + public void setAttributes(List attributes) { + this.attributes = attributes; + } + + @Override + public String toString() { + return "NodeDTO [name=" + name + "]"; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/callbacks/returning/NodeMapperContext.java b/processor/src/test/java/org/mapstruct/ap/test/callbacks/returning/NodeMapperContext.java new file mode 100644 index 000000000..6f734b856 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/callbacks/returning/NodeMapperContext.java @@ -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 LEVEL = new ThreadLocal(); + private static final ThreadLocal> MAPPING = new ThreadLocal>(); + + /** Only for test-inspection */ + private static final List LISTENERS = new CopyOnWriteArrayList(); + + private NodeMapperContext() { + // Only allow static access + } + + @BeforeMapping + @SuppressWarnings( "unchecked" ) + public static T getInstance(Object source, @TargetType Class type) { + fireMethodCalled( LEVEL.get(), "getInstance", source, null ); + Map 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() ); + } + 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); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/callbacks/returning/NodeMapperDefault.java b/processor/src/test/java/org/mapstruct/ap/test/callbacks/returning/NodeMapperDefault.java new file mode 100644 index 000000000..88be582b5 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/callbacks/returning/NodeMapperDefault.java @@ -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); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/callbacks/returning/NodeMapperWithContext.java b/processor/src/test/java/org/mapstruct/ap/test/callbacks/returning/NodeMapperWithContext.java new file mode 100644 index 000000000..6ac7bdda0 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/callbacks/returning/NodeMapperWithContext.java @@ -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); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/callbacks/returning/Number.java b/processor/src/test/java/org/mapstruct/ap/test/callbacks/returning/Number.java new file mode 100644 index 000000000..23b6add7c --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/callbacks/returning/Number.java @@ -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 + "]"; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/callbacks/returning/NumberMapperContext.java b/processor/src/test/java/org/mapstruct/ap/test/callbacks/returning/NumberMapperContext.java new file mode 100644 index 000000000..ad70b80f6 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/callbacks/returning/NumberMapperContext.java @@ -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 CACHE = new HashMap(); + + private static final List VISITED = new ArrayList(); + + 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 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 visitNumber(@MappingTarget T number) { + VISITED.add( number ); + return number; + } + + @AfterMapping + public static Map withMap(Map source, @MappingTarget Map target) { + return target; + } + + @AfterMapping + public static List withList(Set source, @MappingTarget List 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!" ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/callbacks/returning/NumberMapperDefault.java b/processor/src/test/java/org/mapstruct/ap/test/callbacks/returning/NumberMapperDefault.java new file mode 100644 index 000000000..945c7a98a --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/callbacks/returning/NumberMapperDefault.java @@ -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); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/callbacks/returning/NumberMapperWithContext.java b/processor/src/test/java/org/mapstruct/ap/test/callbacks/returning/NumberMapperWithContext.java new file mode 100644 index 000000000..d8571257e --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/callbacks/returning/NumberMapperWithContext.java @@ -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 longMapToIntegerMap(Map target); + + public abstract List setToList(Set target); +}