From 8fa286fe4ce790cc2a65adff55c813bc9717437a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikola=20Iva=C4=8Di=C4=8D?= Date: Mon, 30 May 2022 12:33:47 +0200 Subject: [PATCH] #2688: Support accessing to the target property name --- .../main/java/org/mapstruct/Condition.java | 1 + .../org/mapstruct/TargetPropertyName.java | 25 ++ ...apter-10-advanced-mapping-options.asciidoc | 57 ++++ .../ap/internal/gem/GemGenerator.java | 2 + .../ap/internal/model/common/Parameter.java | 22 +- .../model/common/ParameterBinding.java | 28 +- .../internal/model/source/SourceMethod.java | 9 +- .../model/source/selector/TypeSelector.java | 16 +- .../ap/internal/model/MethodReference.ftl | 3 + .../model/MethodReferencePresenceCheck.ftl | 1 + .../ap/internal/model/PropertyMapping.ftl | 1 + .../ap/internal/model/TypeConversion.ftl | 1 + .../model/assignment/Java8FunctionWrapper.ftl | 1 + .../model/assignment/LocalVarWrapper.ftl | 1 + .../model/assignment/ReturnWrapper.ftl | 1 + .../ap/internal/model/macro/CommonMacros.ftl | 6 +- .../targetpropertyname/Address.java | 21 ++ .../targetpropertyname/AddressDto.java | 21 ++ ...ollectionMapperWithTargetPropertyName.java | 41 +++ ...onalMethodInMapperWithAllExceptTarget.java | 46 +++ ...nditionalMethodInMapperWithAllOptions.java | 52 ++++ ...lMethodInMapperWithTargetPropertyName.java | 32 ++ ...hodInUsesMapperWithTargetPropertyName.java | 36 +++ ...WithTargetPropertyNameInContextMapper.java | 89 ++++++ .../targetpropertyname/DomainModel.java | 15 + .../targetpropertyname/Employee.java | 99 ++++++ .../targetpropertyname/EmployeeDto.java | 98 ++++++ .../TargetPropertyNameTest.java | 281 ++++++++++++++++++ 28 files changed, 993 insertions(+), 13 deletions(-) create mode 100644 core/src/main/java/org/mapstruct/TargetPropertyName.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/conditional/targetpropertyname/Address.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/conditional/targetpropertyname/AddressDto.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/conditional/targetpropertyname/ConditionalMethodForCollectionMapperWithTargetPropertyName.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/conditional/targetpropertyname/ConditionalMethodInMapperWithAllExceptTarget.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/conditional/targetpropertyname/ConditionalMethodInMapperWithAllOptions.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/conditional/targetpropertyname/ConditionalMethodInMapperWithTargetPropertyName.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/conditional/targetpropertyname/ConditionalMethodInUsesMapperWithTargetPropertyName.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/conditional/targetpropertyname/ConditionalMethodWithTargetPropertyNameInContextMapper.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/conditional/targetpropertyname/DomainModel.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/conditional/targetpropertyname/Employee.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/conditional/targetpropertyname/EmployeeDto.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/conditional/targetpropertyname/TargetPropertyNameTest.java diff --git a/core/src/main/java/org/mapstruct/Condition.java b/core/src/main/java/org/mapstruct/Condition.java index ec1bf06a8..148ec4879 100644 --- a/core/src/main/java/org/mapstruct/Condition.java +++ b/core/src/main/java/org/mapstruct/Condition.java @@ -23,6 +23,7 @@ import java.lang.annotation.Target; * e.g. the value given by calling {@code getName()} for the name property of the source bean *
  • The mapping source parameter
  • *
  • {@code @}{@link Context} parameter
  • + *
  • {@code @}{@link TargetPropertyName} parameter
  • * * * Note: The usage of this annotation is mandatory diff --git a/core/src/main/java/org/mapstruct/TargetPropertyName.java b/core/src/main/java/org/mapstruct/TargetPropertyName.java new file mode 100644 index 000000000..17af966fa --- /dev/null +++ b/core/src/main/java/org/mapstruct/TargetPropertyName.java @@ -0,0 +1,25 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * This annotation marks a presence check method parameter as a property name parameter. + *

    + * This parameter enables conditional filtering based on target property name at run-time. + * Parameter must be of type {@link String} and can be present only in {@link Condition} method. + *

    + * @author Nikola Ivačič + * @since 1.6 + */ +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.CLASS) +public @interface TargetPropertyName { +} diff --git a/documentation/src/main/asciidoc/chapter-10-advanced-mapping-options.asciidoc b/documentation/src/main/asciidoc/chapter-10-advanced-mapping-options.asciidoc index 1cc19fcc3..decee792d 100644 --- a/documentation/src/main/asciidoc/chapter-10-advanced-mapping-options.asciidoc +++ b/documentation/src/main/asciidoc/chapter-10-advanced-mapping-options.asciidoc @@ -404,6 +404,61 @@ public class CarMapperImpl implements CarMapper { ---- ==== +Additionally `@TargetPropertyName` of type `java.lang.String` can be used in custom condition check method: + +.Mapper using custom condition check method with `@TargetPropertyName` +==== +[source, java, linenums] +[subs="verbatim,attributes"] +---- +@Mapper +public interface CarMapper { + + CarDto carToCarDto(Car car, @MappingTarget CarDto carDto); + + @Condition + default boolean isNotEmpty(String value, @TargetPropertyName String name) { + if ( name.equals( "owner" ) { + return value != null + && !value.isEmpty() + && !value.equals( value.toLowerCase() ); + } + return value != null && !value.isEmpty(); + } +} +---- +==== + +The generated mapper with `@TargetPropertyName` will look like: + +.Custom condition check in generated implementation +==== +[source, java, linenums] +[subs="verbatim,attributes"] +---- +// GENERATED CODE +public class CarMapperImpl implements CarMapper { + + @Override + public CarDto carToCarDto(Car car, CarDto carDto) { + if ( car == null ) { + return carDto; + } + + if ( isNotEmpty( car.getOwner(), "owner" ) ) { + carDto.setOwner( car.getOwner() ); + } else { + carDto.setOwner( null ); + } + + // Mapping of other properties + + return carDto; + } +} +---- +==== + [IMPORTANT] ==== If there is a custom `@Condition` method applicable for the property it will have a precedence over a presence check method in the bean itself. @@ -412,6 +467,8 @@ If there is a custom `@Condition` method applicable for the property it will hav [NOTE] ==== Methods annotated with `@Condition` in addition to the value of the source property can also have the source parameter as an input. + +`@TargetPropertyName` parameter can only be used in `@Condition` methods. ==== <> is also valid for `@Condition` methods. diff --git a/processor/src/main/java/org/mapstruct/ap/internal/gem/GemGenerator.java b/processor/src/main/java/org/mapstruct/ap/internal/gem/GemGenerator.java index e0491cbc4..70098c952 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/gem/GemGenerator.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/gem/GemGenerator.java @@ -30,6 +30,7 @@ import org.mapstruct.ObjectFactory; import org.mapstruct.Qualifier; import org.mapstruct.SubclassMapping; import org.mapstruct.SubclassMappings; +import org.mapstruct.TargetPropertyName; import org.mapstruct.TargetType; import org.mapstruct.ValueMapping; import org.mapstruct.ValueMappings; @@ -52,6 +53,7 @@ import org.mapstruct.tools.gem.GemDefinition; @GemDefinition(SubclassMapping.class) @GemDefinition(SubclassMappings.class) @GemDefinition(TargetType.class) +@GemDefinition(TargetPropertyName.class) @GemDefinition(MappingTarget.class) @GemDefinition(DecoratedWith.class) @GemDefinition(MapperConfig.class) diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/common/Parameter.java b/processor/src/main/java/org/mapstruct/ap/internal/model/common/Parameter.java index dc62018be..600c8ac33 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/common/Parameter.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/common/Parameter.java @@ -15,6 +15,7 @@ import javax.lang.model.element.VariableElement; import org.mapstruct.ap.internal.gem.ContextGem; import org.mapstruct.ap.internal.gem.MappingTargetGem; import org.mapstruct.ap.internal.gem.TargetTypeGem; +import org.mapstruct.ap.internal.gem.TargetPropertyNameGem; import org.mapstruct.ap.internal.util.Collections; /** @@ -31,6 +32,7 @@ public class Parameter extends ModelElement { private final boolean mappingTarget; private final boolean targetType; private final boolean mappingContext; + private final boolean targetPropertyName; private final boolean varArgs; @@ -42,10 +44,12 @@ public class Parameter extends ModelElement { this.mappingTarget = MappingTargetGem.instanceOn( element ) != null; this.targetType = TargetTypeGem.instanceOn( element ) != null; this.mappingContext = ContextGem.instanceOn( element ) != null; + this.targetPropertyName = TargetPropertyNameGem.instanceOn( element ) != null; this.varArgs = varArgs; } private Parameter(String name, Type type, boolean mappingTarget, boolean targetType, boolean mappingContext, + boolean targetPropertyName, boolean varArgs) { this.element = null; this.name = name; @@ -54,11 +58,12 @@ public class Parameter extends ModelElement { this.mappingTarget = mappingTarget; this.targetType = targetType; this.mappingContext = mappingContext; + this.targetPropertyName = targetPropertyName; this.varArgs = varArgs; } public Parameter(String name, Type type) { - this( name, type, false, false, false, false ); + this( name, type, false, false, false, false, false ); } public Element getElement() { @@ -94,6 +99,7 @@ public class Parameter extends ModelElement { return ( mappingTarget ? "@MappingTarget " : "" ) + ( targetType ? "@TargetType " : "" ) + ( mappingContext ? "@Context " : "" ) + + ( targetPropertyName ? "@TargetPropertyName " : "" ) + "%s " + name; } @@ -110,6 +116,10 @@ public class Parameter extends ModelElement { return mappingContext; } + public boolean isTargetPropertyName() { + return targetPropertyName; + } + public boolean isVarArgs() { return varArgs; } @@ -154,6 +164,7 @@ public class Parameter extends ModelElement { true, false, false, + false, false ); } @@ -195,8 +206,15 @@ public class Parameter extends ModelElement { return parameters.stream().filter( Parameter::isTargetType ).findAny().orElse( null ); } + public static Parameter getTargetPropertyNameParameter(List parameters) { + return parameters.stream().filter( Parameter::isTargetPropertyName ).findAny().orElse( null ); + } + private static boolean isSourceParameter( Parameter parameter ) { - return !parameter.isMappingTarget() && !parameter.isTargetType() && !parameter.isMappingContext(); + return !parameter.isMappingTarget() && + !parameter.isTargetType() && + !parameter.isMappingContext() && + !parameter.isTargetPropertyName(); } } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/common/ParameterBinding.java b/processor/src/main/java/org/mapstruct/ap/internal/model/common/ParameterBinding.java index 36e4d3e86..18274e549 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/common/ParameterBinding.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/common/ParameterBinding.java @@ -22,15 +22,17 @@ public class ParameterBinding { private final boolean targetType; private final boolean mappingTarget; private final boolean mappingContext; + private final boolean targetPropertyName; private final SourceRHS sourceRHS; private ParameterBinding(Type parameterType, String variableName, boolean mappingTarget, boolean targetType, - boolean mappingContext, SourceRHS sourceRHS) { + boolean mappingContext, boolean targetPropertyName, SourceRHS sourceRHS) { this.type = parameterType; this.variableName = variableName; this.targetType = targetType; this.mappingTarget = mappingTarget; this.mappingContext = mappingContext; + this.targetPropertyName = targetPropertyName; this.sourceRHS = sourceRHS; } @@ -62,6 +64,13 @@ public class ParameterBinding { return mappingContext; } + /** + * @return {@code true}, if the parameter being bound is a {@code @TargetPropertyName} parameter. + */ + public boolean isTargetPropertyName() { + return targetPropertyName; + } + /** * @return the type of the parameter that is bound */ @@ -99,6 +108,7 @@ public class ParameterBinding { parameter.isMappingTarget(), parameter.isTargetType(), parameter.isMappingContext(), + parameter.isTargetPropertyName(), null ); } @@ -118,6 +128,7 @@ public class ParameterBinding { false, false, false, + false, null ); } @@ -127,7 +138,14 @@ public class ParameterBinding { * @return a parameter binding representing a target type parameter */ public static ParameterBinding forTargetTypeBinding(Type classTypeOf) { - return new ParameterBinding( classTypeOf, null, false, true, false, null ); + return new ParameterBinding( classTypeOf, null, false, true, false, false, null ); + } + + /** + * @return a parameter binding representing a target property name parameter + */ + public static ParameterBinding forTargetPropertyNameBinding(Type classTypeOf) { + return new ParameterBinding( classTypeOf, null, false, false, false, true, null ); } /** @@ -135,7 +153,7 @@ public class ParameterBinding { * @return a parameter binding representing a mapping target parameter */ public static ParameterBinding forMappingTargetBinding(Type resultType) { - return new ParameterBinding( resultType, null, true, false, false, null ); + return new ParameterBinding( resultType, null, true, false, false, false, null ); } /** @@ -143,10 +161,10 @@ public class ParameterBinding { * @return a parameter binding representing a mapping source type */ public static ParameterBinding forSourceTypeBinding(Type sourceType) { - return new ParameterBinding( sourceType, null, false, false, false, null ); + return new ParameterBinding( sourceType, null, false, false, false, false, null ); } public static ParameterBinding fromSourceRHS(SourceRHS sourceRHS) { - return new ParameterBinding( sourceRHS.getSourceType(), null, false, false, false, sourceRHS ); + return new ParameterBinding( sourceRHS.getSourceType(), null, false, false, false, false, sourceRHS ); } } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/source/SourceMethod.java b/processor/src/main/java/org/mapstruct/ap/internal/model/source/SourceMethod.java index 92180bca8..37b57d902 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/source/SourceMethod.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/source/SourceMethod.java @@ -47,6 +47,7 @@ public class SourceMethod implements Method { private final List parameters; private final Parameter mappingTargetParameter; private final Parameter targetTypeParameter; + private final Parameter targetPropertyNameParameter; private final boolean isObjectFactory; private final boolean isPresenceCheck; private final Type returnType; @@ -248,6 +249,7 @@ public class SourceMethod implements Method { this.mappingTargetParameter = Parameter.getMappingTargetParameter( parameters ); this.targetTypeParameter = Parameter.getTargetTypeParameter( parameters ); + this.targetPropertyNameParameter = Parameter.getTargetPropertyNameParameter( parameters ); this.hasObjectFactoryAnnotation = ObjectFactoryGem.instanceOn( executable ) != null; this.isObjectFactory = determineIfIsObjectFactory(); this.isPresenceCheck = determineIfIsPresenceCheck(); @@ -263,8 +265,9 @@ public class SourceMethod implements Method { private boolean determineIfIsObjectFactory() { boolean hasNoSourceParameters = getSourceParameters().isEmpty(); boolean hasNoMappingTargetParam = getMappingTargetParameter() == null; + boolean hasNoTargetPropertyNameParam = getTargetPropertyNameParameter() == null; return !isLifecycleCallbackMethod() && !returnType.isVoid() - && hasNoMappingTargetParam + && hasNoMappingTargetParam && hasNoTargetPropertyNameParam && ( hasObjectFactoryAnnotation || hasNoSourceParameters ); } @@ -379,6 +382,10 @@ public class SourceMethod implements Method { return targetTypeParameter; } + public Parameter getTargetPropertyNameParameter() { + return targetPropertyNameParameter; + } + public boolean isIterableMapping() { if ( isIterableMapping == null ) { isIterableMapping = getSourceParameters().size() == 1 diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/source/selector/TypeSelector.java b/processor/src/main/java/org/mapstruct/ap/internal/model/source/selector/TypeSelector.java index 74e518c84..8acdd327c 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/source/selector/TypeSelector.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/source/selector/TypeSelector.java @@ -96,7 +96,7 @@ public class TypeSelector implements MethodSelector { availableParams.addAll( ParameterBinding.fromParameters( method.getParameters() ) ); } - addMappingTargetAndTargetTypeBindings( availableParams, targetType ); + addTargetRelevantBindings( availableParams, targetType ); return availableParams; } @@ -116,7 +116,7 @@ public class TypeSelector implements MethodSelector { } } - addMappingTargetAndTargetTypeBindings( availableParams, targetType ); + addTargetRelevantBindings( availableParams, targetType ); return availableParams; } @@ -127,9 +127,10 @@ public class TypeSelector implements MethodSelector { * @param availableParams Already available params, new entries will be added to this list * @param targetType Target type */ - private void addMappingTargetAndTargetTypeBindings(List availableParams, Type targetType) { + private void addTargetRelevantBindings(List availableParams, Type targetType) { boolean mappingTargetAvailable = false; boolean targetTypeAvailable = false; + boolean targetPropertyNameAvailable = false; // search available parameter bindings if mapping-target and/or target-type is available for ( ParameterBinding pb : availableParams ) { @@ -139,6 +140,9 @@ public class TypeSelector implements MethodSelector { else if ( pb.isTargetType() ) { targetTypeAvailable = true; } + else if ( pb.isTargetPropertyName() ) { + targetPropertyNameAvailable = true; + } } if ( !mappingTargetAvailable ) { @@ -147,6 +151,9 @@ public class TypeSelector implements MethodSelector { if ( !targetTypeAvailable ) { availableParams.add( ParameterBinding.forTargetTypeBinding( typeFactory.classTypeOf( targetType ) ) ); } + if ( !targetPropertyNameAvailable ) { + availableParams.add( ParameterBinding.forTargetPropertyNameBinding( typeFactory.getType( String.class ) ) ); + } } private SelectedMethod getMatchingParameterBinding(Type returnType, @@ -301,7 +308,8 @@ public class TypeSelector implements MethodSelector { for ( ParameterBinding candidate : candidateParameters ) { if ( parameter.isTargetType() == candidate.isTargetType() && parameter.isMappingTarget() == candidate.isMappingTarget() - && parameter.isMappingContext() == candidate.isMappingContext() ) { + && parameter.isMappingContext() == candidate.isMappingContext() + && parameter.isTargetPropertyName() == candidate.isTargetPropertyName()) { result.add( candidate ); } } diff --git a/processor/src/main/resources/org/mapstruct/ap/internal/model/MethodReference.ftl b/processor/src/main/resources/org/mapstruct/ap/internal/model/MethodReference.ftl index 6ccdf2687..80f893f1a 100644 --- a/processor/src/main/resources/org/mapstruct/ap/internal/model/MethodReference.ftl +++ b/processor/src/main/resources/org/mapstruct/ap/internal/model/MethodReference.ftl @@ -44,6 +44,8 @@ <#if ext.targetBeanName??>${ext.targetBeanName}<#else>${param.variableName}<#if ext.targetReadAccessorName??>.${ext.targetReadAccessorName}<#t> <#elseif param.mappingContext> ${param.variableName}<#t> + <#elseif param.targetPropertyName> + "${ext.targetPropertyName}"<#t> <#elseif param.sourceRHS??> <@_assignment assignmentToUse=param.sourceRHS/><#t> <#elseif assignment??> @@ -66,5 +68,6 @@ existingInstanceMapping=ext.existingInstanceMapping targetReadAccessorName=ext.targetReadAccessorName targetWriteAccessorName=ext.targetWriteAccessorName + targetPropertyName=ext.targetPropertyName targetType=singleSourceParameterType/> \ No newline at end of file diff --git a/processor/src/main/resources/org/mapstruct/ap/internal/model/MethodReferencePresenceCheck.ftl b/processor/src/main/resources/org/mapstruct/ap/internal/model/MethodReferencePresenceCheck.ftl index 9a2837a02..24f871e21 100644 --- a/processor/src/main/resources/org/mapstruct/ap/internal/model/MethodReferencePresenceCheck.ftl +++ b/processor/src/main/resources/org/mapstruct/ap/internal/model/MethodReferencePresenceCheck.ftl @@ -7,4 +7,5 @@ --> <#-- @ftlvariable name="" type="org.mapstruct.ap.internal.model.MethodReferencePresenceCheck" --> <@includeModel object=methodReference + targetPropertyName=ext.targetPropertyName targetType=ext.targetType/> \ No newline at end of file diff --git a/processor/src/main/resources/org/mapstruct/ap/internal/model/PropertyMapping.ftl b/processor/src/main/resources/org/mapstruct/ap/internal/model/PropertyMapping.ftl index 20e6f1734..e6aef4cec 100644 --- a/processor/src/main/resources/org/mapstruct/ap/internal/model/PropertyMapping.ftl +++ b/processor/src/main/resources/org/mapstruct/ap/internal/model/PropertyMapping.ftl @@ -11,5 +11,6 @@ existingInstanceMapping=ext.existingInstanceMapping targetReadAccessorName=targetReadAccessorName targetWriteAccessorName=targetWriteAccessorName + targetPropertyName=name targetType=targetType defaultValueAssignment=defaultValueAssignment /> \ No newline at end of file diff --git a/processor/src/main/resources/org/mapstruct/ap/internal/model/TypeConversion.ftl b/processor/src/main/resources/org/mapstruct/ap/internal/model/TypeConversion.ftl index 370fe7c97..4a9d356ce 100644 --- a/processor/src/main/resources/org/mapstruct/ap/internal/model/TypeConversion.ftl +++ b/processor/src/main/resources/org/mapstruct/ap/internal/model/TypeConversion.ftl @@ -14,6 +14,7 @@ ${openExpression}<@_assignment/>${closeExpression} existingInstanceMapping=ext.existingInstanceMapping targetReadAccessorName=ext.targetReadAccessorName targetWriteAccessorName=ext.targetWriteAccessorName + targetPropertyName=ext.targetPropertyName targetType=ext.targetType/> diff --git a/processor/src/main/resources/org/mapstruct/ap/internal/model/assignment/Java8FunctionWrapper.ftl b/processor/src/main/resources/org/mapstruct/ap/internal/model/assignment/Java8FunctionWrapper.ftl index 2795dac7b..ce1fcdcd6 100644 --- a/processor/src/main/resources/org/mapstruct/ap/internal/model/assignment/Java8FunctionWrapper.ftl +++ b/processor/src/main/resources/org/mapstruct/ap/internal/model/assignment/Java8FunctionWrapper.ftl @@ -34,5 +34,6 @@ existingInstanceMapping=ext.existingInstanceMapping targetReadAccessorName=ext.targetReadAccessorName targetWriteAccessorName=ext.targetWriteAccessorName + targetPropertyName=ext.targetPropertyName targetType=ext.targetType/> diff --git a/processor/src/main/resources/org/mapstruct/ap/internal/model/assignment/LocalVarWrapper.ftl b/processor/src/main/resources/org/mapstruct/ap/internal/model/assignment/LocalVarWrapper.ftl index 53e987e47..97ea856ec 100644 --- a/processor/src/main/resources/org/mapstruct/ap/internal/model/assignment/LocalVarWrapper.ftl +++ b/processor/src/main/resources/org/mapstruct/ap/internal/model/assignment/LocalVarWrapper.ftl @@ -25,5 +25,6 @@ existingInstanceMapping=ext.existingInstanceMapping targetReadAccessorName=ext.targetReadAccessorName targetWriteAccessorName=ext.targetWriteAccessorName + targetPropertyName=ext.targetPropertyName targetType=ext.targetType/> \ No newline at end of file diff --git a/processor/src/main/resources/org/mapstruct/ap/internal/model/assignment/ReturnWrapper.ftl b/processor/src/main/resources/org/mapstruct/ap/internal/model/assignment/ReturnWrapper.ftl index 9f46f3604..9917dbacf 100644 --- a/processor/src/main/resources/org/mapstruct/ap/internal/model/assignment/ReturnWrapper.ftl +++ b/processor/src/main/resources/org/mapstruct/ap/internal/model/assignment/ReturnWrapper.ftl @@ -13,5 +13,6 @@ return <@_assignment/>; existingInstanceMapping=ext.existingInstanceMapping targetReadAccessorName=ext.targetReadAccessorName targetWriteAccessorName=ext.targetWriteAccessorName + targetPropertyName=ext.targetPropertyName targetType=ext.targetType/> diff --git a/processor/src/main/resources/org/mapstruct/ap/internal/model/macro/CommonMacros.ftl b/processor/src/main/resources/org/mapstruct/ap/internal/model/macro/CommonMacros.ftl index 6d05ddd13..e5fb97060 100644 --- a/processor/src/main/resources/org/mapstruct/ap/internal/model/macro/CommonMacros.ftl +++ b/processor/src/main/resources/org/mapstruct/ap/internal/model/macro/CommonMacros.ftl @@ -16,6 +16,7 @@ <#macro handleSourceReferenceNullCheck> <#if sourcePresenceCheckerReference??> if ( <@includeModel object=sourcePresenceCheckerReference + targetPropertyName=ext.targetPropertyName targetType=ext.targetType/> ) { <#nested> } @@ -58,7 +59,8 @@ --> <#macro handleLocalVarNullCheck needs_explicit_local_var> <#if sourcePresenceCheckerReference??> - if ( <@includeModel object=sourcePresenceCheckerReference /> ) { + if ( <@includeModel object=sourcePresenceCheckerReference + targetPropertyName=ext.targetPropertyName/> ) { <#if needs_explicit_local_var> <@includeModel object=nullCheckLocalVarType/> ${nullCheckLocalVarName} = <@lib.handleAssignment/>; <#nested> @@ -113,6 +115,7 @@ Performs a standard assignment. existingInstanceMapping=ext.existingInstanceMapping targetReadAccessorName=ext.targetReadAccessorName targetWriteAccessorName=ext.targetWriteAccessorName + targetPropertyName=ext.targetPropertyName targetType=ext.targetType/> <#-- @@ -124,6 +127,7 @@ Performs a default assignment with a default value. existingInstanceMapping=ext.existingInstanceMapping targetReadAccessorName=ext.targetReadAccessorName targetWriteAccessorName=ext.targetWriteAccessorName + targetPropertyName=ext.targetPropertyName targetType=ext.targetType defaultValue=ext.defaultValue/> diff --git a/processor/src/test/java/org/mapstruct/ap/test/conditional/targetpropertyname/Address.java b/processor/src/test/java/org/mapstruct/ap/test/conditional/targetpropertyname/Address.java new file mode 100644 index 000000000..162ed118b --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conditional/targetpropertyname/Address.java @@ -0,0 +1,21 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.conditional.targetpropertyname; + +/** + * @author Nikola Ivačič + */ +public class Address implements DomainModel { + private String street; + + public String getStreet() { + return street; + } + + public void setStreet(String street) { + this.street = street; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/conditional/targetpropertyname/AddressDto.java b/processor/src/test/java/org/mapstruct/ap/test/conditional/targetpropertyname/AddressDto.java new file mode 100644 index 000000000..f4cbc7191 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conditional/targetpropertyname/AddressDto.java @@ -0,0 +1,21 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.conditional.targetpropertyname; + +/** + * @author Nikola Ivačič + */ +public class AddressDto implements DomainModel { + private final String street; + + public AddressDto(String street) { + this.street = street; + } + + public String getStreet() { + return street; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/conditional/targetpropertyname/ConditionalMethodForCollectionMapperWithTargetPropertyName.java b/processor/src/test/java/org/mapstruct/ap/test/conditional/targetpropertyname/ConditionalMethodForCollectionMapperWithTargetPropertyName.java new file mode 100644 index 000000000..71baa0942 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conditional/targetpropertyname/ConditionalMethodForCollectionMapperWithTargetPropertyName.java @@ -0,0 +1,41 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.conditional.targetpropertyname; + +import org.mapstruct.Condition; +import org.mapstruct.Mapper; +import org.mapstruct.TargetPropertyName; +import org.mapstruct.factory.Mappers; + +import java.util.Collection; + +/** + * @author Nikola Ivačič + */ +@Mapper +public interface ConditionalMethodForCollectionMapperWithTargetPropertyName { + + ConditionalMethodForCollectionMapperWithTargetPropertyName INSTANCE + = Mappers.getMapper( ConditionalMethodForCollectionMapperWithTargetPropertyName.class ); + + Employee map(EmployeeDto employee); + + @Condition + default boolean isNotEmpty(Collection collection, @TargetPropertyName String propName) { + if ( "addresses".equalsIgnoreCase( propName ) ) { + return false; + } + return collection != null && !collection.isEmpty(); + } + + @Condition + default boolean isNotBlank(String value, @TargetPropertyName String propName) { + if ( propName.equalsIgnoreCase( "lastName" ) ) { + return false; + } + return value != null && !value.trim().isEmpty(); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/conditional/targetpropertyname/ConditionalMethodInMapperWithAllExceptTarget.java b/processor/src/test/java/org/mapstruct/ap/test/conditional/targetpropertyname/ConditionalMethodInMapperWithAllExceptTarget.java new file mode 100644 index 000000000..430303d06 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conditional/targetpropertyname/ConditionalMethodInMapperWithAllExceptTarget.java @@ -0,0 +1,46 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.conditional.targetpropertyname; + +import org.mapstruct.Condition; +import org.mapstruct.Context; +import org.mapstruct.Mapper; +import org.mapstruct.TargetPropertyName; +import org.mapstruct.factory.Mappers; + +import java.util.LinkedHashSet; +import java.util.Set; + +/** + * @author Filip Hrisafov + * @author Nikola Ivačič + */ +@Mapper +public interface ConditionalMethodInMapperWithAllExceptTarget { + + ConditionalMethodInMapperWithAllExceptTarget INSTANCE + = Mappers.getMapper( ConditionalMethodInMapperWithAllExceptTarget.class ); + + class PresenceUtils { + Set visited = new LinkedHashSet<>(); + Set visitedSources = new LinkedHashSet<>(); + } + + Employee map(EmployeeDto employee, @Context PresenceUtils utils); + + @Condition + default boolean isNotBlank(String value, + DomainModel source, + @TargetPropertyName String propName, + @Context PresenceUtils utils) { + utils.visited.add( propName ); + utils.visitedSources.add( source.getClass().getSimpleName() ); + if ( propName.equalsIgnoreCase( "firstName" ) ) { + return true; + } + return value != null && !value.trim().isEmpty(); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/conditional/targetpropertyname/ConditionalMethodInMapperWithAllOptions.java b/processor/src/test/java/org/mapstruct/ap/test/conditional/targetpropertyname/ConditionalMethodInMapperWithAllOptions.java new file mode 100644 index 000000000..a18ba0db7 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conditional/targetpropertyname/ConditionalMethodInMapperWithAllOptions.java @@ -0,0 +1,52 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.conditional.targetpropertyname; + +import org.mapstruct.Condition; +import org.mapstruct.Context; +import org.mapstruct.Mapper; +import org.mapstruct.MappingTarget; +import org.mapstruct.TargetPropertyName; +import org.mapstruct.factory.Mappers; + +import java.util.LinkedHashSet; +import java.util.Set; + +/** + * @author Filip Hrisafov + * @author Nikola Ivačič + */ +@Mapper +public interface ConditionalMethodInMapperWithAllOptions { + + ConditionalMethodInMapperWithAllOptions INSTANCE + = Mappers.getMapper( ConditionalMethodInMapperWithAllOptions.class ); + + class PresenceUtils { + Set visited = new LinkedHashSet<>(); + Set visitedSources = new LinkedHashSet<>(); + Set visitedTargets = new LinkedHashSet<>(); + } + + void map(EmployeeDto employeeDto, + @MappingTarget Employee employee, + @Context PresenceUtils utils); + + @Condition + default boolean isNotBlank(String value, + DomainModel source, + @MappingTarget DomainModel target, + @TargetPropertyName String propName, + @Context PresenceUtils utils) { + utils.visited.add( propName ); + utils.visitedSources.add( source.getClass().getSimpleName() ); + utils.visitedTargets.add( target.getClass().getSimpleName() ); + if ( propName.equalsIgnoreCase( "lastName" ) ) { + return false; + } + return value != null && !value.trim().isEmpty(); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/conditional/targetpropertyname/ConditionalMethodInMapperWithTargetPropertyName.java b/processor/src/test/java/org/mapstruct/ap/test/conditional/targetpropertyname/ConditionalMethodInMapperWithTargetPropertyName.java new file mode 100644 index 000000000..6d27bc903 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conditional/targetpropertyname/ConditionalMethodInMapperWithTargetPropertyName.java @@ -0,0 +1,32 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.conditional.targetpropertyname; + +import org.mapstruct.Condition; +import org.mapstruct.Mapper; +import org.mapstruct.TargetPropertyName; +import org.mapstruct.factory.Mappers; + +/** + * @author Filip Hrisafov + * @author Nikola Ivačič + */ +@Mapper +public interface ConditionalMethodInMapperWithTargetPropertyName { + + ConditionalMethodInMapperWithTargetPropertyName INSTANCE + = Mappers.getMapper( ConditionalMethodInMapperWithTargetPropertyName.class ); + + Employee map(EmployeeDto employee); + + @Condition + default boolean isNotBlank(String value, @TargetPropertyName String propName) { + if ( propName.equalsIgnoreCase( "lastName" ) ) { + return false; + } + return value != null && !value.trim().isEmpty(); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/conditional/targetpropertyname/ConditionalMethodInUsesMapperWithTargetPropertyName.java b/processor/src/test/java/org/mapstruct/ap/test/conditional/targetpropertyname/ConditionalMethodInUsesMapperWithTargetPropertyName.java new file mode 100644 index 000000000..7c526884e --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conditional/targetpropertyname/ConditionalMethodInUsesMapperWithTargetPropertyName.java @@ -0,0 +1,36 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.conditional.targetpropertyname; + +import org.mapstruct.Condition; +import org.mapstruct.Mapper; +import org.mapstruct.TargetPropertyName; +import org.mapstruct.factory.Mappers; + +/** + * @author Filip Hrisafov + * @author Nikola Ivačič + */ +@Mapper(uses = ConditionalMethodInUsesMapperWithTargetPropertyName.PresenceUtils.class) +public interface ConditionalMethodInUsesMapperWithTargetPropertyName { + + ConditionalMethodInUsesMapperWithTargetPropertyName INSTANCE + = Mappers.getMapper( ConditionalMethodInUsesMapperWithTargetPropertyName.class ); + + Employee map(EmployeeDto employee); + + class PresenceUtils { + + @Condition + public boolean isNotBlank(String value, @TargetPropertyName String propName) { + if ( propName.equalsIgnoreCase( "lastName" ) ) { + return false; + } + return value != null && !value.trim().isEmpty(); + } + + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/conditional/targetpropertyname/ConditionalMethodWithTargetPropertyNameInContextMapper.java b/processor/src/test/java/org/mapstruct/ap/test/conditional/targetpropertyname/ConditionalMethodWithTargetPropertyNameInContextMapper.java new file mode 100644 index 000000000..a7cde220c --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conditional/targetpropertyname/ConditionalMethodWithTargetPropertyNameInContextMapper.java @@ -0,0 +1,89 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.conditional.targetpropertyname; + +import org.mapstruct.AfterMapping; +import org.mapstruct.BeforeMapping; +import org.mapstruct.Condition; +import org.mapstruct.Context; +import org.mapstruct.Mapper; +import org.mapstruct.TargetPropertyName; +import org.mapstruct.factory.Mappers; + +import java.util.Deque; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.Set; + +/** + * @author Nikola Ivačič + */ +@Mapper +public interface ConditionalMethodWithTargetPropertyNameInContextMapper { + + ConditionalMethodWithTargetPropertyNameInContextMapper INSTANCE + = Mappers.getMapper( ConditionalMethodWithTargetPropertyNameInContextMapper.class ); + + Employee map(EmployeeDto employee, @Context PresenceUtils utils); + + Address map(AddressDto addressDto, @Context PresenceUtils utils); + + class PresenceUtils { + Set visited = new LinkedHashSet<>(); + + @Condition + public boolean isNotBlank(String value, @TargetPropertyName String propName) { + visited.add( propName ); + return value != null && !value.trim().isEmpty(); + } + } + + Employee map(EmployeeDto employee, @Context PresenceUtilsAllProps utils); + + Address map(AddressDto addressDto, @Context PresenceUtilsAllProps utils); + + class PresenceUtilsAllProps { + Set visited = new LinkedHashSet<>(); + + @Condition + public boolean collect(@TargetPropertyName String propName) { + visited.add( propName ); + return true; + } + } + + Employee map(EmployeeDto employee, @Context PresenceUtilsAllPropsWithSource utils); + + Address map(AddressDto addressDto, @Context PresenceUtilsAllPropsWithSource utils); + + @BeforeMapping + default void before(DomainModel source, @Context PresenceUtilsAllPropsWithSource utils) { + String lastProp = utils.visitedSegments.peekLast(); + if ( lastProp != null && source != null ) { + utils.path.offerLast( lastProp ); + } + } + + @AfterMapping + default void after(@Context PresenceUtilsAllPropsWithSource utils) { + utils.path.pollLast(); + } + + class PresenceUtilsAllPropsWithSource { + Deque visitedSegments = new LinkedList<>(); + Deque visited = new LinkedList<>(); + Deque path = new LinkedList<>(); + + @Condition + public boolean collect(@TargetPropertyName String propName) { + visitedSegments.offerLast( propName ); + path.offerLast( propName ); + visited.offerLast( String.join( ".", path ) ); + path.pollLast(); + return true; + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/conditional/targetpropertyname/DomainModel.java b/processor/src/test/java/org/mapstruct/ap/test/conditional/targetpropertyname/DomainModel.java new file mode 100644 index 000000000..4d2c716a9 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conditional/targetpropertyname/DomainModel.java @@ -0,0 +1,15 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.conditional.targetpropertyname; + +/** + * Target Property Name test entities + * + * @author Nikola Ivačič + */ +public interface DomainModel { + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/conditional/targetpropertyname/Employee.java b/processor/src/test/java/org/mapstruct/ap/test/conditional/targetpropertyname/Employee.java new file mode 100644 index 000000000..59ee3426e --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conditional/targetpropertyname/Employee.java @@ -0,0 +1,99 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.conditional.targetpropertyname; + +import java.util.List; + +/** + * @author Nikola Ivačič + */ +public class Employee implements DomainModel { + + private String firstName; + private String lastName; + private String title; + private String country; + private boolean active; + private int age; + + private Employee boss; + + private Address primaryAddress; + + private List
    addresses; + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getCountry() { + return country; + } + + public void setCountry(String country) { + this.country = country; + } + + public boolean isActive() { + return active; + } + + public void setActive(boolean active) { + this.active = active; + } + + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } + + public Employee getBoss() { + return boss; + } + + public void setBoss(Employee boss) { + this.boss = boss; + } + + public Address getPrimaryAddress() { + return primaryAddress; + } + + public void setPrimaryAddress(Address primaryAddress) { + this.primaryAddress = primaryAddress; + } + + public List
    getAddresses() { + return addresses; + } + + public void setAddresses(List
    addresses) { + this.addresses = addresses; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/conditional/targetpropertyname/EmployeeDto.java b/processor/src/test/java/org/mapstruct/ap/test/conditional/targetpropertyname/EmployeeDto.java new file mode 100644 index 000000000..5d81a9334 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conditional/targetpropertyname/EmployeeDto.java @@ -0,0 +1,98 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.conditional.targetpropertyname; + +import java.util.List; + +/** + * @author Nikola Ivačič + */ +public class EmployeeDto implements DomainModel { + + private String firstName; + private String lastName; + private String title; + private String country; + private boolean active; + private int age; + + private EmployeeDto boss; + + private AddressDto primaryAddress; + private List addresses; + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getCountry() { + return country; + } + + public void setCountry(String country) { + this.country = country; + } + + public boolean isActive() { + return active; + } + + public void setActive(boolean active) { + this.active = active; + } + + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } + + public EmployeeDto getBoss() { + return boss; + } + + public void setBoss(EmployeeDto boss) { + this.boss = boss; + } + + public AddressDto getPrimaryAddress() { + return primaryAddress; + } + + public void setPrimaryAddress(AddressDto primaryAddress) { + this.primaryAddress = primaryAddress; + } + + public List getAddresses() { + return addresses; + } + + public void setAddresses(List addresses) { + this.addresses = addresses; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/conditional/targetpropertyname/TargetPropertyNameTest.java b/processor/src/test/java/org/mapstruct/ap/test/conditional/targetpropertyname/TargetPropertyNameTest.java new file mode 100644 index 000000000..3d96e6666 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conditional/targetpropertyname/TargetPropertyNameTest.java @@ -0,0 +1,281 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.conditional.targetpropertyname; + +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.runner.GeneratedSource; + +import java.util.Collections; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Filip Hrisafov + * @author Nikola Ivačič + */ +@IssueKey("2051") +@WithClasses({ + Address.class, + AddressDto.class, + Employee.class, + EmployeeDto.class, + DomainModel.class +}) +public class TargetPropertyNameTest { + + @RegisterExtension + final GeneratedSource generatedSource = new GeneratedSource(); + + @ProcessorTest + @WithClasses({ + ConditionalMethodInMapperWithTargetPropertyName.class + }) + public void conditionalMethodInMapperWithTargetPropertyName() { + ConditionalMethodInMapperWithTargetPropertyName mapper + = ConditionalMethodInMapperWithTargetPropertyName.INSTANCE; + + EmployeeDto employeeDto = new EmployeeDto(); + employeeDto.setFirstName( " " ); + employeeDto.setLastName( "Testirovich" ); + employeeDto.setCountry( "US" ); + employeeDto.setAddresses( + Collections.singletonList( new AddressDto( "Testing St. 6" ) ) + ); + + Employee employee = mapper.map( employeeDto ); + assertThat( employee.getLastName() ).isNull(); + assertThat( employee.getFirstName() ).isNull(); + assertThat( employee.getCountry() ).isEqualTo( "US" ); + assertThat( employee.getAddresses() ) + .extracting( Address::getStreet ) + .containsExactly( "Testing St. 6" ); + } + + @ProcessorTest + @WithClasses({ + ConditionalMethodForCollectionMapperWithTargetPropertyName.class + }) + public void conditionalMethodForCollectionMapperWithTargetPropertyName() { + ConditionalMethodForCollectionMapperWithTargetPropertyName mapper + = ConditionalMethodForCollectionMapperWithTargetPropertyName.INSTANCE; + + EmployeeDto employeeDto = new EmployeeDto(); + employeeDto.setFirstName( " " ); + employeeDto.setLastName( "Testirovich" ); + employeeDto.setCountry( "US" ); + employeeDto.setAddresses( + Collections.singletonList( new AddressDto( "Testing St. 6" ) ) + ); + + Employee employee = mapper.map( employeeDto ); + assertThat( employee.getLastName() ).isNull(); + assertThat( employee.getFirstName() ).isNull(); + assertThat( employee.getCountry() ).isEqualTo( "US" ); + assertThat( employee.getAddresses() ).isNull(); + } + + @ProcessorTest + @WithClasses({ + ConditionalMethodInUsesMapperWithTargetPropertyName.class + }) + public void conditionalMethodInUsesMapperWithTargetPropertyName() { + ConditionalMethodInUsesMapperWithTargetPropertyName mapper + = ConditionalMethodInUsesMapperWithTargetPropertyName.INSTANCE; + + EmployeeDto employeeDto = new EmployeeDto(); + employeeDto.setFirstName( " " ); + employeeDto.setLastName( "Testirovich" ); + employeeDto.setCountry( "US" ); + employeeDto.setAddresses( + Collections.singletonList( new AddressDto( "Testing St. 6" ) ) + ); + + Employee employee = mapper.map( employeeDto ); + assertThat( employee.getLastName() ).isNull(); + assertThat( employee.getFirstName() ).isNull(); + assertThat( employee.getCountry() ).isEqualTo( "US" ); + assertThat( employee.getAddresses() ) + .extracting( Address::getStreet ) + .containsExactly( "Testing St. 6" ); + } + + @ProcessorTest + @WithClasses({ + ConditionalMethodInMapperWithAllOptions.class + }) + public void conditionalMethodInMapperWithAllOptions() { + ConditionalMethodInMapperWithAllOptions mapper + = ConditionalMethodInMapperWithAllOptions.INSTANCE; + + ConditionalMethodInMapperWithAllOptions.PresenceUtils utils = + new ConditionalMethodInMapperWithAllOptions.PresenceUtils(); + + EmployeeDto employeeDto = new EmployeeDto(); + employeeDto.setFirstName( " " ); + employeeDto.setLastName( "Testirovich" ); + employeeDto.setCountry( "US" ); + employeeDto.setAddresses( + Collections.singletonList( new AddressDto( "Testing St. 6" ) ) + ); + + Employee employee = new Employee(); + mapper.map( employeeDto, employee, utils ); + assertThat( employee.getLastName() ).isNull(); + assertThat( employee.getFirstName() ).isNull(); + assertThat( employee.getCountry() ).isEqualTo( "US" ); + assertThat( employee.getAddresses() ) + .extracting( Address::getStreet ) + .containsExactly( "Testing St. 6" ); + assertThat( utils.visited ) + .containsExactlyInAnyOrder( "firstName", "lastName", "title", "country" ); + assertThat( utils.visitedSources ).containsExactly( "EmployeeDto" ); + assertThat( utils.visitedTargets ).containsExactly( "Employee" ); + } + + @ProcessorTest + @WithClasses({ + ConditionalMethodInMapperWithAllExceptTarget.class + }) + public void conditionalMethodInMapperWithAllExceptTarget() { + ConditionalMethodInMapperWithAllExceptTarget mapper + = ConditionalMethodInMapperWithAllExceptTarget.INSTANCE; + + ConditionalMethodInMapperWithAllExceptTarget.PresenceUtils utils = + new ConditionalMethodInMapperWithAllExceptTarget.PresenceUtils(); + + EmployeeDto employeeDto = new EmployeeDto(); + employeeDto.setFirstName( " " ); + employeeDto.setLastName( "Testirovich" ); + employeeDto.setCountry( "US" ); + employeeDto.setAddresses( + Collections.singletonList( new AddressDto( "Testing St. 6" ) ) + ); + + Employee employee = mapper.map( employeeDto, utils ); + assertThat( employee.getLastName() ).isEqualTo( "Testirovich" ); + assertThat( employee.getFirstName() ).isEqualTo( " " ); + assertThat( employee.getCountry() ).isEqualTo( "US" ); + assertThat( employee.getAddresses() ) + .extracting( Address::getStreet ) + .containsExactly( "Testing St. 6" ); + assertThat( utils.visited ) + .containsExactlyInAnyOrder( "firstName", "lastName", "title", "country", "street" ); + assertThat( utils.visitedSources ).containsExactlyInAnyOrder( "EmployeeDto", "AddressDto" ); + } + + @ProcessorTest + @WithClasses({ + ConditionalMethodWithTargetPropertyNameInContextMapper.class + }) + public void conditionalMethodWithTargetPropertyNameInUsesContextMapper() { + ConditionalMethodWithTargetPropertyNameInContextMapper mapper + = ConditionalMethodWithTargetPropertyNameInContextMapper.INSTANCE; + + ConditionalMethodWithTargetPropertyNameInContextMapper.PresenceUtils utils = + new ConditionalMethodWithTargetPropertyNameInContextMapper.PresenceUtils(); + + EmployeeDto employeeDto = new EmployeeDto(); + employeeDto.setLastName( " " ); + employeeDto.setCountry( "US" ); + employeeDto.setAddresses( + Collections.singletonList( new AddressDto( "Testing St. 6" ) ) + ); + + Employee employee = mapper.map( employeeDto, utils ); + assertThat( employee.getLastName() ).isNull(); + assertThat( employee.getCountry() ).isEqualTo( "US" ); + assertThat( employee.getAddresses() ) + .extracting( Address::getStreet ) + .containsExactly( "Testing St. 6" ); + assertThat( utils.visited ) + .containsExactlyInAnyOrder( "firstName", "lastName", "title", "country", "street" ); + + ConditionalMethodWithTargetPropertyNameInContextMapper.PresenceUtilsAllProps allPropsUtils = + new ConditionalMethodWithTargetPropertyNameInContextMapper.PresenceUtilsAllProps(); + + employeeDto = new EmployeeDto(); + employeeDto.setLastName( "Tester" ); + employeeDto.setCountry( "US" ); + employeeDto.setAddresses( + Collections.singletonList( new AddressDto( "Testing St. 6" ) ) + ); + + employee = mapper.map( employeeDto, allPropsUtils ); + assertThat( employee.getLastName() ).isEqualTo( "Tester" ); + assertThat( employee.getCountry() ).isEqualTo( "US" ); + assertThat( employee.getAddresses() ) + .extracting( Address::getStreet ) + .containsExactly( "Testing St. 6" ); + assertThat( allPropsUtils.visited ) + .containsExactlyInAnyOrder( + "firstName", + "lastName", + "title", + "country", + "active", + "age", + "boss", + "primaryAddress", + "addresses", + "street" + ); + + ConditionalMethodWithTargetPropertyNameInContextMapper.PresenceUtilsAllPropsWithSource allPropsUtilsWithSource = + new ConditionalMethodWithTargetPropertyNameInContextMapper.PresenceUtilsAllPropsWithSource(); + + EmployeeDto bossEmployeeDto = new EmployeeDto(); + bossEmployeeDto.setLastName( "Boss Tester" ); + bossEmployeeDto.setCountry( "US" ); + bossEmployeeDto.setAddresses( Collections.singletonList( new AddressDto( + "Testing St. 10" ) ) ); + + employeeDto = new EmployeeDto(); + employeeDto.setLastName( "Tester" ); + employeeDto.setCountry( "US" ); + employeeDto.setBoss( bossEmployeeDto ); + employeeDto.setAddresses( + Collections.singletonList( new AddressDto( "Testing St. 6" ) ) + ); + + employee = mapper.map( employeeDto, allPropsUtilsWithSource ); + assertThat( employee.getLastName() ).isEqualTo( "Tester" ); + assertThat( employee.getCountry() ).isEqualTo( "US" ); + assertThat( employee.getAddresses() ).isNotEmpty(); + assertThat( employee.getAddresses().get( 0 ).getStreet() ).isEqualTo( "Testing St. 6" ); + assertThat( employee.getBoss() ).isNotNull(); + assertThat( employee.getBoss().getCountry() ).isEqualTo( "US" ); + assertThat( employee.getBoss().getLastName() ).isEqualTo( "Boss Tester" ); + assertThat( employee.getBoss().getAddresses() ) + .extracting( Address::getStreet ) + .containsExactly( "Testing St. 10" ); + assertThat( allPropsUtilsWithSource.visited ) + .containsExactly( + "firstName", + "lastName", + "title", + "country", + "active", + "age", + "boss", + "boss.firstName", + "boss.lastName", + "boss.title", + "boss.country", + "boss.active", + "boss.age", + "boss.boss", + "boss.primaryAddress", + "boss.addresses", + "boss.addresses.street", + "primaryAddress", + "addresses", + "addresses.street" + ); + } +}