#2688: Support accessing to the target property name

This commit is contained in:
Nikola Ivačič 2022-05-30 12:33:47 +02:00 committed by Filip Hrisafov
parent 62e73464b2
commit 8fa286fe4c
28 changed files with 993 additions and 13 deletions

View File

@ -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</li>
* <li>The mapping source parameter</li>
* <li>{@code @}{@link Context} parameter</li>
* <li>{@code @}{@link TargetPropertyName} parameter</li>
* </ul>
*
* <strong>Note:</strong> The usage of this annotation is <em>mandatory</em>

View File

@ -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 <em>presence check method</em> parameter as a property name parameter.
* <p>
* 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.
* </p>
* @author Nikola Ivačič
* @since 1.6
*/
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.CLASS)
public @interface TargetPropertyName {
}

View File

@ -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.
====
<<selection-based-on-qualifiers>> is also valid for `@Condition` methods.

View File

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

View File

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

View File

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

View File

@ -47,6 +47,7 @@ public class SourceMethod implements Method {
private final List<Parameter> 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

View File

@ -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<ParameterBinding> availableParams, Type targetType) {
private void addTargetRelevantBindings(List<ParameterBinding> 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 <T extends Method> SelectedMethod<T> 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 );
}
}

View File

@ -44,6 +44,8 @@
<#if ext.targetBeanName??>${ext.targetBeanName}<#else>${param.variableName}</#if><#if ext.targetReadAccessorName??>.${ext.targetReadAccessorName}</#if><#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/>
</#macro>

View File

@ -7,4 +7,5 @@
-->
<#-- @ftlvariable name="" type="org.mapstruct.ap.internal.model.MethodReferencePresenceCheck" -->
<@includeModel object=methodReference
targetPropertyName=ext.targetPropertyName
targetType=ext.targetType/>

View File

@ -11,5 +11,6 @@
existingInstanceMapping=ext.existingInstanceMapping
targetReadAccessorName=targetReadAccessorName
targetWriteAccessorName=targetWriteAccessorName
targetPropertyName=name
targetType=targetType
defaultValueAssignment=defaultValueAssignment />

View File

@ -14,6 +14,7 @@ ${openExpression}<@_assignment/>${closeExpression}
existingInstanceMapping=ext.existingInstanceMapping
targetReadAccessorName=ext.targetReadAccessorName
targetWriteAccessorName=ext.targetWriteAccessorName
targetPropertyName=ext.targetPropertyName
targetType=ext.targetType/>
</#macro>
</@compress>

View File

@ -34,5 +34,6 @@
existingInstanceMapping=ext.existingInstanceMapping
targetReadAccessorName=ext.targetReadAccessorName
targetWriteAccessorName=ext.targetWriteAccessorName
targetPropertyName=ext.targetPropertyName
targetType=ext.targetType/>
</#macro>

View File

@ -25,5 +25,6 @@
existingInstanceMapping=ext.existingInstanceMapping
targetReadAccessorName=ext.targetReadAccessorName
targetWriteAccessorName=ext.targetWriteAccessorName
targetPropertyName=ext.targetPropertyName
targetType=ext.targetType/>
</#macro>

View File

@ -13,5 +13,6 @@ return <@_assignment/>;
existingInstanceMapping=ext.existingInstanceMapping
targetReadAccessorName=ext.targetReadAccessorName
targetWriteAccessorName=ext.targetWriteAccessorName
targetPropertyName=ext.targetPropertyName
targetType=ext.targetType/>
</#macro>

View File

@ -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/>
</#macro>
<#--
@ -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/>
</#macro>

View File

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

View File

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

View File

@ -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 <T> boolean isNotEmpty(Collection<T> 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();
}
}

View File

@ -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<String> visited = new LinkedHashSet<>();
Set<String> 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();
}
}

View File

@ -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<String> visited = new LinkedHashSet<>();
Set<String> visitedSources = new LinkedHashSet<>();
Set<String> 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();
}
}

View File

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

View File

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

View File

@ -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<String> 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<String> 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<String> visitedSegments = new LinkedList<>();
Deque<String> visited = new LinkedList<>();
Deque<String> 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;
}
}
}

View File

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

View File

@ -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<Address> 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<Address> getAddresses() {
return addresses;
}
public void setAddresses(List<Address> addresses) {
this.addresses = addresses;
}
}

View File

@ -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<AddressDto> 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<AddressDto> getAddresses() {
return addresses;
}
public void setAddresses(List<AddressDto> addresses) {
this.addresses = addresses;
}
}

View File

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