#2610 Add support for conditions on source parameters + fix incorrect use of source parameter in presence check method (#3543)

The new `@SourceParameterCondition` is also going to cover the problems in #3270 and #3459.
The changes in the `MethodFamilySelector` are also fixing #3561
This commit is contained in:
Filip Hrisafov 2024-04-29 08:05:52 +02:00 committed by GitHub
parent 0a935c67a7
commit 0a2a0aa526
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
31 changed files with 1339 additions and 132 deletions

View File

@ -11,26 +11,35 @@ import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* This annotation marks a method as a <em>presence check method</em> to check check for presence in beans.
* This annotation marks a method as a <em>presence check method</em> to check for presence in beans
* or it can be used to define additional check methods for something like source parameters.
* <p>
* By default bean properties are checked against {@code null} or using a presence check method in the source bean.
* By default, bean properties are checked against {@code null} or using a presence check method in the source bean.
* If a presence check method is available then it will be used instead.
* <p>
* Presence check methods have to return {@code boolean}.
* The following parameters are accepted for the presence check methods:
* <ul>
* <li>The parameter with the value of the source property.
* e.g. the value given by calling {@code getName()} for the name property of the source bean</li>
* e.g. the value given by calling {@code getName()} for the name property of the source bean
* - only possible when using the {@link ConditionStrategy#PROPERTIES}
* </li>
* <li>The mapping source parameter</li>
* <li>{@code @}{@link Context} parameter</li>
* <li>{@code @}{@link TargetPropertyName} parameter</li>
* <li>{@code @}{@link SourcePropertyName} parameter</li>
* <li>
* {@code @}{@link TargetPropertyName} parameter -
* only possible when using the {@link ConditionStrategy#PROPERTIES}
* </li>
* <li>
* {@code @}{@link SourcePropertyName} parameter -
* only possible when using the {@link ConditionStrategy#PROPERTIES}
* </li>
* </ul>
*
* <strong>Note:</strong> The usage of this annotation is <em>mandatory</em>
* for a method to be considered as a presence check method.
*
* <pre><code>
* <pre><code class='java'>
* public class PresenceCheckUtils {
*
* &#64;Condition
@ -45,11 +54,10 @@ import java.lang.annotation.Target;
* MovieDto map(Movie movie);
* }
* </code></pre>
*
* <p>
* The following implementation of {@code MovieMapper} will be generated:
*
* <pre>
* <code>
* <pre><code class='java'>
* public class MovieMapperImpl implements MovieMapper {
*
* &#64;Override
@ -67,14 +75,22 @@ import java.lang.annotation.Target;
* return movieDto;
* }
* }
* </code>
* </pre>
* </code></pre>
* <p>
* This annotation can also be used as a meta-annotation to define the condition strategy.
*
* @author Filip Hrisafov
* @see SourceParameterCondition
* @since 1.5
*/
@Target({ ElementType.METHOD })
@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE })
@Retention(RetentionPolicy.CLASS)
public @interface Condition {
/**
* @return the places where the condition should apply to
* @since 1.6
*/
ConditionStrategy[] appliesTo() default ConditionStrategy.PROPERTIES;
}

View File

@ -0,0 +1,23 @@
/*
* Copyright MapStruct Authors.
*
* Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
*/
package org.mapstruct;
/**
* Strategy for defining what to what a condition (check) method is applied to
*
* @author Filip Hrisafov
* @since 1.6
*/
public enum ConditionStrategy {
/**
* The condition method should be applied whether a property should be mapped.
*/
PROPERTIES,
/**
* The condition method should be applied to check if a source parameters should be mapped.
*/
SOURCE_PARAMETERS,
}

View File

@ -0,0 +1,74 @@
/*
* 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 method as a <em>check method</em> to check if a source parameter needs to be mapped.
* <p>
* By default, source parameters are checked against {@code null}, unless they are primitives.
* <p>
* Check methods have to return {@code boolean}.
* The following parameters are accepted for the presence check methods:
* <ul>
* <li>The mapping source parameter</li>
* <li>{@code @}{@link Context} parameter</li>
* </ul>
*
* <strong>Note:</strong> The usage of this annotation is <em>mandatory</em>
* for a method to be considered as a source check method.
*
* <pre><code class='java'>
* public class PresenceCheckUtils {
*
* &#64;SourceParameterCondition
* public static boolean isDefined(Car car) {
* return car != null &#38;&#38; car.getId() != null;
* }
* }
*
* &#64;Mapper(uses = PresenceCheckUtils.class)
* public interface CarMapper {
*
* CarDto map(Car car);
* }
* </code></pre>
*
* The following implementation of {@code CarMapper} will be generated:
*
* <pre><code class='java'>
* public class CarMapperImpl implements CarMapper {
*
* &#64;Override
* public CarDto map(Car car) {
* if ( !PresenceCheckUtils.isDefined( car ) ) {
* return null;
* }
*
* CarDto carDto = new CarDto();
*
* carDto.setId( car.getId() );
* // ...
*
* return carDto;
* }
* }
* </code></pre>
*
* @author Filip Hrisafov
* @since 1.6
* @see Condition @Condition
*/
@Target({ ElementType.METHOD })
@Retention(RetentionPolicy.CLASS)
@Condition(appliesTo = ConditionStrategy.SOURCE_PARAMETERS)
public @interface SourceParameterCondition {
}

View File

@ -303,8 +303,10 @@ null check, regardless the value of the `NullValueCheckStrategy` to avoid additi
Conditional Mapping is a type of <<source-presence-check>>.
The difference is that it allows users to write custom condition methods that will be invoked to check if a property needs to be mapped or not.
Conditional mapping can also be used to check if a source parameter should be mapped or not.
A custom condition method is a method that is annotated with `org.mapstruct.Condition` and returns `boolean`.
A custom condition method for properties is a method that is annotated with `org.mapstruct.Condition` and returns `boolean`.
A custom condition method for source parameters is annotated with `org.mapstruct.SourceParameterCondition`, `org.mapstruct.Condition(appliesTo = org.mapstruct.ConditionStrategy#SOURCE_PARAMETERS)` or meta-annotated with `Condition(appliesTo = ConditionStrategy#SOURCE_PARAMETERS)`
e.g. if you only want to map a String property when it is not `null`, and it is not empty then you can do something like:
@ -484,6 +486,55 @@ Methods annotated with `@Condition` in addition to the value of the source prope
<<selection-based-on-qualifiers>> is also valid for `@Condition` methods.
In order to use a more specific condition method you will need to use one of `Mapping#conditionQualifiedByName` or `Mapping#conditionQualifiedBy`.
If we want to only map cars that have an id provided then we can do something like:
.Mapper using custom condition source parameter check method
====
[source, java, linenums]
[subs="verbatim,attributes"]
----
@Mapper
public interface CarMapper {
CarDto carToCarDto(Car car);
@SourceParameterCondition
default boolean hasCar(Car car) {
return car != null && car.getId() != null;
}
}
----
====
The generated mapper will look like:
.Custom condition source parameter check generated implementation
====
[source, java, linenums]
[subs="verbatim,attributes"]
----
// GENERATED CODE
public class CarMapperImpl implements CarMapper {
@Override
public CarDto carToCarDto(Car car) {
if ( !hasCar( car ) ) {
return null;
}
CarDto carDto = new CarDto();
carDto.setOwner( car.getOwner() );
// Mapping of other properties
return carDto;
}
}
----
====
[[exceptions]]
=== Exceptions

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.internal.gem;
/**
* @author Filip Hrisafov
*/
public enum ConditionStrategyGem {
PROPERTIES,
SOURCE_PARAMETERS
}

View File

@ -411,6 +411,26 @@ public class BeanMappingMethod extends NormalTypeMappingMethod {
removeMappingReferencesWithoutSourceParameters( afterMappingReferencesWithFinalizedReturnType );
}
Map<String, PresenceCheck> presenceChecksByParameter = new LinkedHashMap<>();
for ( Parameter sourceParameter : method.getSourceParameters() ) {
PresenceCheck parameterPresenceCheck = PresenceCheckMethodResolver.getPresenceCheckForSourceParameter(
method,
selectionParameters,
sourceParameter,
ctx
);
if ( parameterPresenceCheck != null ) {
presenceChecksByParameter.put( sourceParameter.getName(), parameterPresenceCheck );
}
else if ( !sourceParameter.getType().isPrimitive() ) {
presenceChecksByParameter.put(
sourceParameter.getName(),
new NullPresenceCheck( sourceParameter.getName() )
);
}
}
return new BeanMappingMethod(
method,
getMethodAnnotations(),
@ -426,7 +446,8 @@ public class BeanMappingMethod extends NormalTypeMappingMethod {
afterMappingReferencesWithFinalizedReturnType,
finalizeMethod,
mappingReferences,
subclasses
subclasses,
presenceChecksByParameter
);
}
@ -1891,7 +1912,8 @@ public class BeanMappingMethod extends NormalTypeMappingMethod {
List<LifecycleCallbackMethodReference> afterMappingReferencesWithFinalizedReturnType,
MethodReference finalizerMethod,
MappingReferences mappingReferences,
List<SubclassMapping> subclassMappings) {
List<SubclassMapping> subclassMappings,
Map<String, PresenceCheck> presenceChecksByParameter) {
super(
method,
annotations,
@ -1923,18 +1945,12 @@ public class BeanMappingMethod extends NormalTypeMappingMethod {
// parameter mapping.
this.mappingsByParameter = new HashMap<>();
this.constantMappings = new ArrayList<>( propertyMappings.size() );
this.presenceChecksByParameter = new LinkedHashMap<>();
this.presenceChecksByParameter = presenceChecksByParameter;
this.constructorMappingsByParameter = new LinkedHashMap<>();
this.constructorConstantMappings = new ArrayList<>();
Set<String> sourceParameterNames = new HashSet<>();
for ( Parameter sourceParameter : getSourceParameters() ) {
sourceParameterNames.add( sourceParameter.getName() );
if ( !sourceParameter.getType().isPrimitive() ) {
presenceChecksByParameter.put(
sourceParameter.getName(),
new NullPresenceCheck( sourceParameter.getName() )
);
}
}
for ( PropertyMapping mapping : propertyMappings ) {
if ( mapping.isConstructorMapping() ) {

View File

@ -9,6 +9,7 @@ import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import org.mapstruct.ap.internal.gem.ConditionStrategyGem;
import org.mapstruct.ap.internal.model.common.Parameter;
import org.mapstruct.ap.internal.model.common.PresenceCheck;
import org.mapstruct.ap.internal.model.source.Method;
@ -18,6 +19,7 @@ import org.mapstruct.ap.internal.model.source.SourceMethod;
import org.mapstruct.ap.internal.model.source.selector.MethodSelectors;
import org.mapstruct.ap.internal.model.source.selector.SelectedMethod;
import org.mapstruct.ap.internal.model.source.selector.SelectionContext;
import org.mapstruct.ap.internal.model.source.selector.SelectionCriteria;
import org.mapstruct.ap.internal.util.Message;
/**
@ -34,38 +36,12 @@ public final class PresenceCheckMethodResolver {
SelectionParameters selectionParameters,
MappingBuilderContext ctx
) {
SelectedMethod<SourceMethod> matchingMethod = findMatchingPresenceCheckMethod(
List<SelectedMethod<SourceMethod>> matchingMethods = findMatchingMethods(
method,
selectionParameters,
SelectionContext.forPresenceCheckMethods( method, selectionParameters, ctx.getTypeFactory() ),
ctx
);
if ( matchingMethod == null ) {
return null;
}
MethodReference methodReference = getPresenceCheckMethodReference( method, matchingMethod, ctx );
return new MethodReferencePresenceCheck( methodReference );
}
private static SelectedMethod<SourceMethod> findMatchingPresenceCheckMethod(
Method method,
SelectionParameters selectionParameters,
MappingBuilderContext ctx
) {
MethodSelectors selectors = new MethodSelectors(
ctx.getTypeUtils(),
ctx.getElementUtils(),
ctx.getMessager()
);
List<SelectedMethod<SourceMethod>> matchingMethods = selectors.getMatchingMethods(
getAllAvailableMethods( method, ctx.getSourceModel() ),
SelectionContext.forPresenceCheckMethods( method, selectionParameters, ctx.getTypeFactory() )
);
if ( matchingMethods.isEmpty() ) {
return null;
}
@ -84,7 +60,72 @@ public final class PresenceCheckMethodResolver {
return null;
}
return matchingMethods.get( 0 );
SelectedMethod<SourceMethod> matchingMethod = matchingMethods.get( 0 );
MethodReference methodReference = getPresenceCheckMethodReference( method, matchingMethod, ctx );
return new MethodReferencePresenceCheck( methodReference );
}
public static PresenceCheck getPresenceCheckForSourceParameter(
Method method,
SelectionParameters selectionParameters,
Parameter sourceParameter,
MappingBuilderContext ctx
) {
List<SelectedMethod<SourceMethod>> matchingMethods = findMatchingMethods(
method,
SelectionContext.forSourceParameterPresenceCheckMethods(
method,
selectionParameters,
sourceParameter,
ctx.getTypeFactory()
),
ctx
);
if ( matchingMethods.isEmpty() ) {
return null;
}
if ( matchingMethods.size() > 1 ) {
ctx.getMessager().printMessage(
method.getExecutable(),
Message.GENERAL_AMBIGUOUS_SOURCE_PARAMETER_CHECK_METHOD,
sourceParameter.getType().describe(),
matchingMethods.stream()
.map( SelectedMethod::getMethod )
.map( Method::describe )
.collect( Collectors.joining( ", " ) )
);
return null;
}
SelectedMethod<SourceMethod> matchingMethod = matchingMethods.get( 0 );
MethodReference methodReference = getPresenceCheckMethodReference( method, matchingMethod, ctx );
return new MethodReferencePresenceCheck( methodReference );
}
private static List<SelectedMethod<SourceMethod>> findMatchingMethods(
Method method,
SelectionContext selectionContext,
MappingBuilderContext ctx
) {
MethodSelectors selectors = new MethodSelectors(
ctx.getTypeUtils(),
ctx.getElementUtils(),
ctx.getMessager()
);
return selectors.getMatchingMethods(
getAllAvailableMethods( method, ctx.getSourceModel(), selectionContext.getSelectionCriteria() ),
selectionContext
);
}
private static MethodReference getPresenceCheckMethodReference(
@ -116,7 +157,8 @@ public final class PresenceCheckMethodResolver {
}
}
private static List<SourceMethod> getAllAvailableMethods(Method method, List<SourceMethod> sourceModelMethods) {
private static List<SourceMethod> getAllAvailableMethods(Method method, List<SourceMethod> sourceModelMethods,
SelectionCriteria selectionCriteria) {
ParameterProvidedMethods contextProvidedMethods = method.getContextProvidedMethods();
if ( contextProvidedMethods.isEmpty() ) {
return sourceModelMethods;
@ -129,9 +171,19 @@ public final class PresenceCheckMethodResolver {
new ArrayList<>( methodsProvidedByParams.size() + sourceModelMethods.size() );
for ( SourceMethod methodProvidedByParams : methodsProvidedByParams ) {
// add only methods from context that do have the @Condition annotation
if ( methodProvidedByParams.isPresenceCheck() ) {
availableMethods.add( methodProvidedByParams );
if ( selectionCriteria.isPresenceCheckRequired() ) {
// add only methods from context that do have the @Condition for properties annotation
if ( methodProvidedByParams.getConditionOptions()
.isStrategyApplicable( ConditionStrategyGem.PROPERTIES ) ) {
availableMethods.add( methodProvidedByParams );
}
}
else if ( selectionCriteria.isSourceParameterCheckRequired() ) {
// add only methods from context that do have the @Condition for source parameters annotation
if ( methodProvidedByParams.getConditionOptions()
.isStrategyApplicable( ConditionStrategyGem.SOURCE_PARAMETERS ) ) {
availableMethods.add( methodProvidedByParams );
}
}
}
availableMethods.addAll( sourceModelMethods );

View File

@ -133,6 +133,14 @@ public class Parameter extends ModelElement {
return varArgs;
}
public boolean isSourceParameter() {
return !isMappingTarget() &&
!isTargetType() &&
!isMappingContext() &&
!isSourcePropertyName() &&
!isTargetPropertyName();
}
@Override
public int hashCode() {
int result = name != null ? name.hashCode() : 0;
@ -224,12 +232,4 @@ public class Parameter extends ModelElement {
return parameters.stream().filter( Parameter::isTargetPropertyName ).findAny().orElse( null );
}
private static boolean isSourceParameter( Parameter parameter ) {
return !parameter.isMappingTarget() &&
!parameter.isTargetType() &&
!parameter.isMappingContext() &&
!parameter.isSourcePropertyName() &&
!parameter.isTargetPropertyName();
}
}

View File

@ -6,7 +6,9 @@
package org.mapstruct.ap.internal.model.common;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
import java.util.Set;
@ -19,23 +21,14 @@ public class ParameterBinding {
private final Type type;
private final String variableName;
private final boolean targetType;
private final boolean mappingTarget;
private final boolean mappingContext;
private final boolean sourcePropertyName;
private final boolean targetPropertyName;
private final SourceRHS sourceRHS;
private final Collection<BindingType> bindingTypes;
private ParameterBinding(Type parameterType, String variableName, boolean mappingTarget, boolean targetType,
boolean mappingContext, boolean sourcePropertyName, boolean targetPropertyName,
private ParameterBinding(Type parameterType, String variableName, Collection<BindingType> bindingTypes,
SourceRHS sourceRHS) {
this.type = parameterType;
this.variableName = variableName;
this.targetType = targetType;
this.mappingTarget = mappingTarget;
this.mappingContext = mappingContext;
this.sourcePropertyName = sourcePropertyName;
this.targetPropertyName = targetPropertyName;
this.bindingTypes = bindingTypes;
this.sourceRHS = sourceRHS;
}
@ -46,39 +39,47 @@ public class ParameterBinding {
return variableName;
}
public boolean isSourceParameter() {
return bindingTypes.contains( BindingType.PARAMETER );
}
/**
* @return {@code true}, if the parameter being bound is a {@code @TargetType} parameter.
*/
public boolean isTargetType() {
return targetType;
return bindingTypes.contains( BindingType.TARGET_TYPE );
}
/**
* @return {@code true}, if the parameter being bound is a {@code @MappingTarget} parameter.
*/
public boolean isMappingTarget() {
return mappingTarget;
return bindingTypes.contains( BindingType.MAPPING_TARGET );
}
/**
* @return {@code true}, if the parameter being bound is a {@code @MappingContext} parameter.
*/
public boolean isMappingContext() {
return mappingContext;
return bindingTypes.contains( BindingType.CONTEXT );
}
public boolean isForSourceRhs() {
return bindingTypes.contains( BindingType.SOURCE_RHS );
}
/**
* @return {@code true}, if the parameter being bound is a {@code @SourcePropertyName} parameter.
*/
public boolean isSourcePropertyName() {
return sourcePropertyName;
return bindingTypes.contains( BindingType.SOURCE_PROPERTY_NAME );
}
/**
* @return {@code true}, if the parameter being bound is a {@code @TargetPropertyName} parameter.
*/
public boolean isTargetPropertyName() {
return targetPropertyName;
return bindingTypes.contains( BindingType.TARGET_PROPERTY_NAME );
}
/**
@ -96,7 +97,7 @@ public class ParameterBinding {
}
public Set<Type> getImportTypes() {
if ( targetType ) {
if ( isTargetType() ) {
return type.getImportTypes();
}
@ -112,14 +113,31 @@ public class ParameterBinding {
* @return a parameter binding reflecting the given parameter as being used as argument for a method call
*/
public static ParameterBinding fromParameter(Parameter parameter) {
EnumSet<BindingType> bindingTypes = EnumSet.of( BindingType.PARAMETER );
if ( parameter.isMappingTarget() ) {
bindingTypes.add( BindingType.MAPPING_TARGET );
}
if ( parameter.isTargetType() ) {
bindingTypes.add( BindingType.TARGET_TYPE );
}
if ( parameter.isMappingContext() ) {
bindingTypes.add( BindingType.CONTEXT );
}
if ( parameter.isSourcePropertyName() ) {
bindingTypes.add( BindingType.SOURCE_PROPERTY_NAME );
}
if ( parameter.isTargetPropertyName() ) {
bindingTypes.add( BindingType.TARGET_PROPERTY_NAME );
}
return new ParameterBinding(
parameter.getType(),
parameter.getName(),
parameter.isMappingTarget(),
parameter.isTargetType(),
parameter.isMappingContext(),
parameter.isSourcePropertyName(),
parameter.isTargetPropertyName(),
bindingTypes,
null
);
}
@ -136,11 +154,7 @@ public class ParameterBinding {
return new ParameterBinding(
parameterType,
parameterName,
false,
false,
false,
false,
false,
Collections.emptySet(),
null
);
}
@ -150,21 +164,31 @@ 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, false, false, null );
return new ParameterBinding( classTypeOf, null, Collections.singleton( BindingType.TARGET_TYPE ), 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, false, true, null );
return new ParameterBinding(
classTypeOf,
null,
Collections.singleton( BindingType.TARGET_PROPERTY_NAME ),
null
);
}
/**
* @return a parameter binding representing a source property name parameter
*/
public static ParameterBinding forSourcePropertyNameBinding(Type classTypeOf) {
return new ParameterBinding( classTypeOf, null, false, false, false, true, false, null );
return new ParameterBinding(
classTypeOf,
null,
Collections.singleton( BindingType.SOURCE_PROPERTY_NAME ),
null
);
}
/**
@ -172,7 +196,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, false, false, null );
return new ParameterBinding( resultType, null, Collections.singleton( BindingType.MAPPING_TARGET ), null );
}
/**
@ -180,10 +204,27 @@ 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, false, false, null );
return new ParameterBinding( sourceType, null, Collections.singleton( BindingType.SOURCE_TYPE ), null );
}
public static ParameterBinding fromSourceRHS(SourceRHS sourceRHS) {
return new ParameterBinding( sourceRHS.getSourceType(), null, false, false, false, false, false, sourceRHS );
return new ParameterBinding(
sourceRHS.getSourceType(),
null,
Collections.singleton( BindingType.SOURCE_RHS ),
sourceRHS
);
}
enum BindingType {
PARAMETER,
FROM_TYPE_AND_NAME,
TARGET_TYPE,
TARGET_PROPERTY_NAME,
SOURCE_PROPERTY_NAME,
MAPPING_TARGET,
CONTEXT,
SOURCE_TYPE,
SOURCE_RHS
}
}

View File

@ -0,0 +1,45 @@
/*
* 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.internal.model.source;
import java.util.Collection;
import java.util.Collections;
import org.mapstruct.ap.internal.gem.ConditionStrategyGem;
/**
* Encapsulates all options specific for a condition check method.
*
* @author Filip Hrisafov
*/
public class ConditionMethodOptions {
private static final ConditionMethodOptions EMPTY = new ConditionMethodOptions( Collections.emptyList() );
private final Collection<ConditionOptions> conditionOptions;
public ConditionMethodOptions(Collection<ConditionOptions> conditionOptions) {
this.conditionOptions = conditionOptions;
}
public boolean isStrategyApplicable(ConditionStrategyGem strategy) {
for ( ConditionOptions conditionOption : conditionOptions ) {
if ( conditionOption.getConditionStrategies().contains( strategy ) ) {
return true;
}
}
return false;
}
public boolean isAnyStrategyApplicable() {
return !conditionOptions.isEmpty();
}
public static ConditionMethodOptions empty() {
return EMPTY;
}
}

View File

@ -0,0 +1,170 @@
/*
* 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.internal.model.source;
import java.util.Collection;
import java.util.EnumSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import org.mapstruct.ap.internal.gem.ConditionGem;
import org.mapstruct.ap.internal.gem.ConditionStrategyGem;
import org.mapstruct.ap.internal.model.common.Parameter;
import org.mapstruct.ap.internal.util.FormattingMessager;
import org.mapstruct.ap.internal.util.Message;
/**
* @author Filip Hrisafov
*/
public class ConditionOptions {
private final Set<ConditionStrategyGem> conditionStrategies;
private ConditionOptions(Set<ConditionStrategyGem> conditionStrategies) {
this.conditionStrategies = conditionStrategies;
}
public Collection<ConditionStrategyGem> getConditionStrategies() {
return conditionStrategies;
}
public static ConditionOptions getInstanceOn(ConditionGem condition, ExecutableElement method,
List<Parameter> parameters,
FormattingMessager messager) {
if ( condition == null ) {
return null;
}
TypeMirror returnType = method.getReturnType();
TypeKind returnTypeKind = returnType.getKind();
// We only allow methods that return boolean or Boolean to be condition methods
if ( returnTypeKind != TypeKind.BOOLEAN ) {
if ( returnTypeKind != TypeKind.DECLARED ) {
return null;
}
DeclaredType declaredType = (DeclaredType) returnType;
TypeElement returnTypeElement = (TypeElement) declaredType.asElement();
if ( !returnTypeElement.getQualifiedName().contentEquals( Boolean.class.getCanonicalName() ) ) {
return null;
}
}
Set<ConditionStrategyGem> strategies = condition.appliesTo().get()
.stream()
.map( ConditionStrategyGem::valueOf )
.collect( Collectors.toCollection( () -> EnumSet.noneOf( ConditionStrategyGem.class ) ) );
if ( strategies.isEmpty() ) {
messager.printMessage(
method,
condition.mirror(),
condition.appliesTo().getAnnotationValue(),
Message.CONDITION_MISSING_APPLIES_TO_STRATEGY
);
return null;
}
boolean allStrategiesValid = true;
for ( ConditionStrategyGem strategy : strategies ) {
boolean isStrategyValid = isValid( strategy, condition, method, parameters, messager );
allStrategiesValid &= isStrategyValid;
}
return allStrategiesValid ? new ConditionOptions( strategies ) : null;
}
protected static boolean isValid(ConditionStrategyGem strategy, ConditionGem condition,
ExecutableElement method, List<Parameter> parameters,
FormattingMessager messager) {
if ( strategy == ConditionStrategyGem.SOURCE_PARAMETERS ) {
return hasValidStrategyForSourceProperties( condition, method, parameters, messager );
}
else if ( strategy == ConditionStrategyGem.PROPERTIES ) {
return hasValidStrategyForProperties( condition, method, parameters, messager );
}
else {
throw new IllegalStateException( "Invalid condition strategy: " + strategy );
}
}
protected static boolean hasValidStrategyForSourceProperties(ConditionGem condition, ExecutableElement method,
List<Parameter> parameters,
FormattingMessager messager) {
for ( Parameter parameter : parameters ) {
if ( parameter.isSourceParameter() ) {
// source parameter is a valid parameter for a source condition check
continue;
}
if ( parameter.isMappingContext() ) {
// mapping context parameter is a valid parameter for a source condition check
continue;
}
messager.printMessage(
method,
condition.mirror(),
Message.CONDITION_SOURCE_PARAMETERS_INVALID_PARAMETER,
parameter.describe()
);
return false;
}
return true;
}
protected static boolean hasValidStrategyForProperties(ConditionGem condition, ExecutableElement method,
List<Parameter> parameters,
FormattingMessager messager) {
for ( Parameter parameter : parameters ) {
if ( parameter.isSourceParameter() ) {
// source parameter is a valid parameter for a property condition check
continue;
}
if ( parameter.isMappingContext() ) {
// mapping context parameter is a valid parameter for a property condition check
continue;
}
if ( parameter.isTargetType() ) {
// target type parameter is a valid parameter for a property condition check
continue;
}
if ( parameter.isMappingTarget() ) {
// mapping target parameter is a valid parameter for a property condition check
continue;
}
if ( parameter.isSourcePropertyName() ) {
// source property name parameter is a valid parameter for a property condition check
continue;
}
if ( parameter.isTargetPropertyName() ) {
// target property name parameter is a valid parameter for a property condition check
continue;
}
messager.printMessage(
method,
condition.mirror(),
Message.CONDITION_PROPERTIES_INVALID_PARAMETER,
parameter
);
return false;
}
return true;
}
}

View File

@ -90,14 +90,6 @@ public interface Method {
*/
boolean isObjectFactory();
/**
* Returns whether the method is designated as a presence check method
* @return {@code true} if it is a presence check method
*/
default boolean isPresenceCheck() {
return false;
}
/**
* Returns the parameter designated as target type (if present) {@link org.mapstruct.TargetType }
*
@ -187,6 +179,10 @@ public interface Method {
*/
MappingMethodOptions getOptions();
default ConditionMethodOptions getConditionOptions() {
return ConditionMethodOptions.empty();
}
/**
*
* @return true when @MappingTarget annotated parameter is the same type as the return type. The method has

View File

@ -15,7 +15,6 @@ import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import org.mapstruct.ap.internal.gem.ConditionGem;
import org.mapstruct.ap.internal.gem.ObjectFactoryGem;
import org.mapstruct.ap.internal.model.common.Accessibility;
import org.mapstruct.ap.internal.model.common.Parameter;
@ -50,11 +49,11 @@ public class SourceMethod implements Method {
private final Parameter sourcePropertyNameParameter;
private final Parameter targetPropertyNameParameter;
private final boolean isObjectFactory;
private final boolean isPresenceCheck;
private final Type returnType;
private final Accessibility accessibility;
private final List<Type> exceptionTypes;
private final MappingMethodOptions mappingMethodOptions;
private final ConditionMethodOptions conditionMethodOptions;
private final List<SourceMethod> prototypeMethods;
private final Type mapperToImplement;
@ -95,6 +94,7 @@ public class SourceMethod implements Method {
private List<ValueMappingOptions> valueMappings;
private EnumMappingOptions enumMappingOptions;
private ParameterProvidedMethods contextProvidedMethods;
private Set<ConditionOptions> conditionOptions;
private List<Type> typeParameters;
private Set<SubclassMappingOptions> subclassMappings;
@ -196,6 +196,11 @@ public class SourceMethod implements Method {
return this;
}
public Builder setConditionOptions(Set<ConditionOptions> conditionOptions) {
this.conditionOptions = conditionOptions;
return this;
}
public Builder setVerboseLogging(boolean verboseLogging) {
this.verboseLogging = verboseLogging;
return this;
@ -223,17 +228,22 @@ public class SourceMethod implements Method {
subclassValidator
);
ConditionMethodOptions conditionMethodOptions =
conditionOptions != null ? new ConditionMethodOptions( conditionOptions ) :
ConditionMethodOptions.empty();
this.typeParameters = this.executable.getTypeParameters()
.stream()
.map( Element::asType )
.map( typeFactory::getType )
.collect( Collectors.toList() );
return new SourceMethod( this, mappingMethodOptions );
return new SourceMethod( this, mappingMethodOptions, conditionMethodOptions );
}
}
private SourceMethod(Builder builder, MappingMethodOptions mappingMethodOptions) {
private SourceMethod(Builder builder, MappingMethodOptions mappingMethodOptions,
ConditionMethodOptions conditionMethodOptions) {
this.declaringMapper = builder.declaringMapper;
this.executable = builder.executable;
this.parameters = builder.parameters;
@ -242,6 +252,7 @@ public class SourceMethod implements Method {
this.accessibility = Accessibility.fromModifiers( builder.executable.getModifiers() );
this.mappingMethodOptions = mappingMethodOptions;
this.conditionMethodOptions = conditionMethodOptions;
this.sourceParameters = Parameter.getSourceParameters( parameters );
this.contextParameters = Parameter.getContextParameters( parameters );
@ -254,7 +265,6 @@ public class SourceMethod implements Method {
this.targetPropertyNameParameter = Parameter.getTargetPropertyNameParameter( parameters );
this.hasObjectFactoryAnnotation = ObjectFactoryGem.instanceOn( executable ) != null;
this.isObjectFactory = determineIfIsObjectFactory();
this.isPresenceCheck = determineIfIsPresenceCheck();
this.typeUtils = builder.typeUtils;
this.typeFactory = builder.typeFactory;
@ -274,19 +284,6 @@ public class SourceMethod implements Method {
&& ( hasObjectFactoryAnnotation || hasNoSourceParameters );
}
private boolean determineIfIsPresenceCheck() {
if ( returnType.isPrimitive() ) {
if ( !returnType.getName().equals( "boolean" ) ) {
return false;
}
}
else if ( !returnType.getFullyQualifiedName().equals( Boolean.class.getCanonicalName() ) ) {
return false;
}
return ConditionGem.instanceOn( executable ) != null;
}
@Override
public Type getDeclaringMapper() {
return declaringMapper;
@ -547,6 +544,11 @@ public class SourceMethod implements Method {
return mappingMethodOptions;
}
@Override
public ConditionMethodOptions getConditionOptions() {
return conditionMethodOptions;
}
@Override
public boolean isStatic() {
return executable.getModifiers().contains( Modifier.STATIC );
@ -567,11 +569,6 @@ public class SourceMethod implements Method {
return Executables.isLifecycleCallbackMethod( getExecutable() );
}
@Override
public boolean isPresenceCheck() {
return isPresenceCheck;
}
public boolean isAfterMappingMethod() {
return Executables.isAfterMappingMethod( getExecutable() );
}

View File

@ -32,6 +32,7 @@ public class CreateOrUpdateSelector implements MethodSelector {
SelectionContext context) {
SelectionCriteria criteria = context.getSelectionCriteria();
if ( criteria.isLifecycleCallbackRequired() || criteria.isObjectFactoryRequired()
|| criteria.isSourceParameterCheckRequired()
|| criteria.isPresenceCheckRequired() ) {
return methods;
}

View File

@ -8,6 +8,7 @@ package org.mapstruct.ap.internal.model.source.selector;
import java.util.ArrayList;
import java.util.List;
import org.mapstruct.ap.internal.gem.ConditionStrategyGem;
import org.mapstruct.ap.internal.model.source.Method;
/**
@ -25,9 +26,22 @@ public class MethodFamilySelector implements MethodSelector {
List<SelectedMethod<T>> result = new ArrayList<>( methods.size() );
for ( SelectedMethod<T> method : methods ) {
if ( method.getMethod().isObjectFactory() == criteria.isObjectFactoryRequired()
if ( criteria.isPresenceCheckRequired() ) {
if ( method.getMethod()
.getConditionOptions()
.isStrategyApplicable( ConditionStrategyGem.PROPERTIES ) ) {
result.add( method );
}
}
else if ( criteria.isSourceParameterCheckRequired() ) {
if ( method.getMethod()
.getConditionOptions()
.isStrategyApplicable( ConditionStrategyGem.SOURCE_PARAMETERS ) ) {
result.add( method );
}
}
else if ( method.getMethod().isObjectFactory() == criteria.isObjectFactoryRequired()
&& method.getMethod().isLifecycleCallbackMethod() == criteria.isLifecycleCallbackRequired()
&& method.getMethod().isPresenceCheck() == criteria.isPresenceCheckRequired()
) {
result.add( method );

View File

@ -163,6 +163,45 @@ public class SelectionContext {
);
}
public static SelectionContext forSourceParameterPresenceCheckMethods(Method mappingMethod,
SelectionParameters selectionParameters,
Parameter sourceParameter,
TypeFactory typeFactory) {
SelectionCriteria criteria = SelectionCriteria.forSourceParameterCheckMethods( selectionParameters );
Type booleanType = typeFactory.getType( Boolean.class );
return new SelectionContext(
null,
criteria,
mappingMethod,
booleanType,
booleanType,
() -> getParameterBindingsForSourceParameterPresenceCheck(
mappingMethod,
booleanType,
sourceParameter,
typeFactory
)
);
}
private static List<ParameterBinding> getParameterBindingsForSourceParameterPresenceCheck(Method method,
Type targetType,
Parameter sourceParameter,
TypeFactory typeFactory) {
List<ParameterBinding> availableParams = new ArrayList<>( method.getParameters().size() + 3 );
availableParams.add( ParameterBinding.fromParameter( sourceParameter ) );
availableParams.add( ParameterBinding.forTargetTypeBinding( typeFactory.classTypeOf( targetType ) ) );
for ( Parameter parameter : method.getParameters() ) {
if ( !parameter.isSourceParameter( ) ) {
availableParams.add( ParameterBinding.fromParameter( parameter ) );
}
}
return availableParams;
}
private static List<ParameterBinding> getAvailableParameterBindingsFromMethod(Method method, Type targetType,
SourceRHS sourceRHS,
TypeFactory typeFactory) {

View File

@ -97,6 +97,13 @@ public class SelectionCriteria {
return type == Type.PRESENCE_CHECK;
}
/**
* @return {@code true} if source parameter check methods should be selected, {@code false} otherwise
*/
public boolean isSourceParameterCheckRequired() {
return type == Type.SOURCE_PARAMETER_CHECK;
}
public void setIgnoreQualifiers(boolean ignoreQualifiers) {
this.ignoreQualifiers = ignoreQualifiers;
}
@ -177,6 +184,10 @@ public class SelectionCriteria {
return new SelectionCriteria( selectionParameters, null, null, Type.PRESENCE_CHECK );
}
public static SelectionCriteria forSourceParameterCheckMethods(SelectionParameters selectionParameters) {
return new SelectionCriteria( selectionParameters, null, null, Type.SOURCE_PARAMETER_CHECK );
}
public static SelectionCriteria forSubclassMappingMethods(SelectionParameters selectionParameters,
MappingControl mappingControl) {
return new SelectionCriteria( selectionParameters, mappingControl, null, Type.SELF_NOT_ALLOWED );
@ -187,6 +198,7 @@ public class SelectionCriteria {
OBJECT_FACTORY,
LIFECYCLE_CALLBACK,
PRESENCE_CHECK,
SOURCE_PARAMETER_CHECK,
SELF_NOT_ALLOWED,
}
}

View File

@ -36,6 +36,7 @@ import org.mapstruct.ap.internal.model.common.Parameter;
import org.mapstruct.ap.internal.model.common.Type;
import org.mapstruct.ap.internal.model.common.TypeFactory;
import org.mapstruct.ap.internal.model.source.BeanMappingOptions;
import org.mapstruct.ap.internal.model.source.ConditionOptions;
import org.mapstruct.ap.internal.model.source.EnumMappingOptions;
import org.mapstruct.ap.internal.model.source.IterableMappingOptions;
import org.mapstruct.ap.internal.model.source.MapMappingOptions;
@ -53,6 +54,7 @@ import org.mapstruct.ap.internal.util.ElementUtils;
import org.mapstruct.ap.internal.util.Executables;
import org.mapstruct.ap.internal.util.FormattingMessager;
import org.mapstruct.ap.internal.util.Message;
import org.mapstruct.ap.internal.util.MetaAnnotations;
import org.mapstruct.ap.internal.util.RepeatableAnnotations;
import org.mapstruct.ap.internal.util.TypeUtils;
import org.mapstruct.ap.spi.EnumTransformationStrategy;
@ -73,6 +75,7 @@ public class MethodRetrievalProcessor implements ModelElementProcessor<Void, Lis
private static final String SUB_CLASS_MAPPINGS_FQN = "org.mapstruct.SubclassMappings";
private static final String VALUE_MAPPING_FQN = "org.mapstruct.ValueMapping";
private static final String VALUE_MAPPINGS_FQN = "org.mapstruct.ValueMappings";
private static final String CONDITION_FQN = "org.mapstruct.Condition";
private FormattingMessager messager;
private TypeFactory typeFactory;
private AccessorNamingUtils accessorNaming;
@ -358,7 +361,7 @@ public class MethodRetrievalProcessor implements ModelElementProcessor<Void, Lis
List<SourceMethod> contextProvidedMethods = new ArrayList<>( contextParamMethods.size() );
for ( SourceMethod sourceMethod : contextParamMethods ) {
if ( sourceMethod.isLifecycleCallbackMethod() || sourceMethod.isObjectFactory()
|| sourceMethod.isPresenceCheck() ) {
|| sourceMethod.getConditionOptions().isAnyStrategyApplicable() ) {
contextProvidedMethods.add( sourceMethod );
}
}
@ -393,6 +396,7 @@ public class MethodRetrievalProcessor implements ModelElementProcessor<Void, Lis
.setExceptionTypes( exceptionTypes )
.setTypeUtils( typeUtils )
.setTypeFactory( typeFactory )
.setConditionOptions( getConditionOptions( method, parameters ) )
.setVerboseLogging( options.isVerbose() )
.build();
}
@ -633,6 +637,18 @@ public class MethodRetrievalProcessor implements ModelElementProcessor<Void, Lis
.getProcessedAnnotations( method );
}
/**
* Retrieves the conditions configured via {@code @Condition} from the given method.
*
* @param method The method of interest
* @param parameters
* @return The condition options for the given method
*/
private Set<ConditionOptions> getConditionOptions(ExecutableElement method, List<Parameter> parameters) {
return new MetaConditions( parameters ).getProcessedAnnotations( method );
}
private class RepeatableMappings extends RepeatableAnnotations<MappingGem, MappingsGem, MappingOptions> {
private BeanMappingOptions beanMappingOptions;
@ -774,4 +790,32 @@ public class MethodRetrievalProcessor implements ModelElementProcessor<Void, Lis
ValueMappingOptions.fromMappingsGem( gems, (ExecutableElement) source, messager, mappings );
}
}
private class MetaConditions extends MetaAnnotations<ConditionGem, ConditionOptions> {
protected final List<Parameter> parameters;
protected MetaConditions(List<Parameter> parameters) {
super( elementUtils, CONDITION_FQN );
this.parameters = parameters;
}
@Override
protected ConditionGem instanceOn(Element element) {
return ConditionGem.instanceOn( element );
}
@Override
protected void addInstance(ConditionGem gem, Element source, Set<ConditionOptions> values) {
ConditionOptions options = ConditionOptions.getInstanceOn(
gem,
(ExecutableElement) source,
parameters,
messager
);
if ( options != null ) {
values.add( options );
}
}
}
}

View File

@ -470,7 +470,7 @@ public class MappingResolverImpl implements MappingResolver {
}
private boolean isCandidateForMapping(Method methodCandidate) {
if ( methodCandidate.isPresenceCheck() ) {
if ( methodCandidate.getConditionOptions().isAnyStrategyApplicable() ) {
return false;
}
return isCreateMethodForMapping( methodCandidate ) || isUpdateMethodForMapping( methodCandidate );

View File

@ -46,6 +46,10 @@ public enum Message {
BEANMAPPING_UNKNOWN_PROPERTY_IN_DEPENDS_ON( "\"%s\" is no property of the method return type." ),
BEANMAPPING_IGNORE_BY_DEFAULT_WITH_MAPPING_TARGET_THIS( "Using @BeanMapping( ignoreByDefault = true ) with @Mapping( target = \".\", ... ) is not allowed. You'll need to explicitly ignore the target properties that should be ignored instead." ),
CONDITION_MISSING_APPLIES_TO_STRATEGY("'appliesTo' has to have at least one value in @Condition" ),
CONDITION_SOURCE_PARAMETERS_INVALID_PARAMETER("Parameter \"%s\" cannot be used with the ConditionStrategy#SOURCE_PARAMETERS. Only source and @Context parameters are allowed for conditions applicable to source parameters." ),
CONDITION_PROPERTIES_INVALID_PARAMETER("Parameter \"%s\" cannot be used with the ConditionStrategy#PROPERTIES. Only source, @Context, @MappingTarget, @TargetType, @TargetPropertyName and @SourcePropertyName parameters are allowed for conditions applicable to properties." ),
PROPERTYMAPPING_MAPPING_NOTE( "mapping property: %s to: %s.", Diagnostic.Kind.NOTE ),
PROPERTYMAPPING_CREATE_NOTE( "creating property mapping: %s.", Diagnostic.Kind.NOTE ),
PROPERTYMAPPING_SELECT_NOTE( "selecting property mapping: %s.", Diagnostic.Kind.NOTE ),
@ -143,6 +147,7 @@ public enum Message {
GENERAL_AMBIGUOUS_MAPPING_METHOD( "Ambiguous mapping methods found for mapping %s to %s: %s. See " + FAQ_AMBIGUOUS_URL + " for more info." ),
GENERAL_AMBIGUOUS_FACTORY_METHOD( "Ambiguous factory methods found for creating %s: %s. See " + FAQ_AMBIGUOUS_URL + " for more info." ),
GENERAL_AMBIGUOUS_PRESENCE_CHECK_METHOD( "Ambiguous presence check methods found for checking %s: %s. See " + FAQ_AMBIGUOUS_URL + " for more info." ),
GENERAL_AMBIGUOUS_SOURCE_PARAMETER_CHECK_METHOD( "Ambiguous source parameter check methods found for checking %s: %s. See " + FAQ_AMBIGUOUS_URL + " for more info." ),
GENERAL_AMBIGUOUS_CONSTRUCTORS( "Ambiguous constructors found for creating %s: %s. Either declare parameterless constructor or annotate the default constructor with an annotation named @Default." ),
GENERAL_CONSTRUCTOR_PROPERTIES_NOT_MATCHING_PARAMETERS( "Incorrect @ConstructorProperties for %s. The size of the @ConstructorProperties does not match the number of constructor parameters" ),
GENERAL_UNSUPPORTED_DATE_FORMAT_CHECK( "No dateFormat check is supported for types %s, %s" ),

View File

@ -0,0 +1,85 @@
/*
* 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.internal.util;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Set;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.TypeElement;
import org.mapstruct.tools.gem.Gem;
/**
* @author Filip Hrisafov
*/
public abstract class MetaAnnotations<G extends Gem, V> {
private static final String JAVA_LANG_ANNOTATION_PGK = "java.lang.annotation";
private final ElementUtils elementUtils;
private final String annotationFqn;
protected MetaAnnotations(ElementUtils elementUtils, String annotationFqn) {
this.elementUtils = elementUtils;
this.annotationFqn = annotationFqn;
}
/**
* Retrieves the processed annotations.
*
* @param source The source element of interest
* @return The processed annotations for the given element
*/
public Set<V> getProcessedAnnotations(Element source) {
return getValues( source, source, new LinkedHashSet<>(), new HashSet<>() );
}
protected abstract G instanceOn(Element element);
protected abstract void addInstance(G gem, Element source, Set<V> values);
/**
* Retrieves the processed annotations.
*
* @param source The source element of interest
* @param element Element of interest: method, or (meta) annotation
* @param values the set of values found so far
* @param handledElements The collection of already handled elements to handle recursion correctly.
* @return The processed annotations for the given element
*/
private Set<V> getValues(Element source, Element element, Set<V> values, Set<Element> handledElements) {
for ( AnnotationMirror annotationMirror : element.getAnnotationMirrors() ) {
Element annotationElement = annotationMirror.getAnnotationType().asElement();
if ( isAnnotation( annotationElement, annotationFqn ) ) {
G gem = instanceOn( element );
addInstance( gem, source, values );
}
else if ( isNotJavaAnnotation( element ) && !handledElements.contains( annotationElement ) ) {
handledElements.add( annotationElement );
getValues( source, annotationElement, values, handledElements );
}
}
return values;
}
private boolean isNotJavaAnnotation(Element element) {
if ( ElementKind.ANNOTATION_TYPE == element.getKind() ) {
return !elementUtils.getPackageOf( element ).getQualifiedName().contentEquals( JAVA_LANG_ANNOTATION_PGK );
}
return true;
}
private boolean isAnnotation(Element element, String annotationFqn) {
if ( ElementKind.ANNOTATION_TYPE == element.getKind() ) {
return ( (TypeElement) element ).getQualifiedName().contentEquals( annotationFqn );
}
return false;
}
}

View File

@ -208,6 +208,84 @@ public class ConditionalMappingTest {
.containsExactly( "Test", "Test Vol. 2" );
}
@ProcessorTest
@WithClasses({
ConditionalMethodForSourceBeanMapper.class
})
public void conditionalMethodForSourceBean() {
ConditionalMethodForSourceBeanMapper mapper = ConditionalMethodForSourceBeanMapper.INSTANCE;
ConditionalMethodForSourceBeanMapper.Employee employee = mapper.map(
new ConditionalMethodForSourceBeanMapper.EmployeeDto(
"1",
"Tester"
) );
assertThat( employee ).isNotNull();
assertThat( employee.getId() ).isEqualTo( "1" );
assertThat( employee.getName() ).isEqualTo( "Tester" );
employee = mapper.map( null );
assertThat( employee ).isNull();
employee = mapper.map( new ConditionalMethodForSourceBeanMapper.EmployeeDto( null, "Tester" ) );
assertThat( employee ).isNull();
employee = mapper.map( new ConditionalMethodForSourceBeanMapper.EmployeeDto( "test-123", "Tester" ) );
assertThat( employee ).isNotNull();
assertThat( employee.getId() ).isEqualTo( "test-123" );
assertThat( employee.getName() ).isEqualTo( "Tester" );
}
@ProcessorTest
@WithClasses({
ConditionalMethodForSourceParameterAndPropertyMapper.class
})
public void conditionalMethodForSourceParameterAndProperty() {
ConditionalMethodForSourceParameterAndPropertyMapper mapper =
ConditionalMethodForSourceParameterAndPropertyMapper.INSTANCE;
ConditionalMethodForSourceParameterAndPropertyMapper.Employee employee = mapper.map(
new ConditionalMethodForSourceParameterAndPropertyMapper.EmployeeDto(
"1",
"Tester"
) );
assertThat( employee ).isNotNull();
assertThat( employee.getId() ).isEqualTo( "1" );
assertThat( employee.getName() ).isEqualTo( "Tester" );
assertThat( employee.getManager() ).isNull();
employee = mapper.map( null );
assertThat( employee ).isNull();
employee = mapper.map( new ConditionalMethodForSourceParameterAndPropertyMapper.EmployeeDto(
"1",
"Tester",
new ConditionalMethodForSourceParameterAndPropertyMapper.EmployeeDto( null, "Manager" )
) );
assertThat( employee ).isNotNull();
assertThat( employee.getManager() ).isNull();
employee = mapper.map( new ConditionalMethodForSourceParameterAndPropertyMapper.EmployeeDto(
"1",
"Tester",
new ConditionalMethodForSourceParameterAndPropertyMapper.EmployeeDto( "2", "Manager" )
) );
assertThat( employee ).isNotNull();
assertThat( employee.getId() ).isEqualTo( "1" );
assertThat( employee.getName() ).isEqualTo( "Tester" );
assertThat( employee.getManager() ).isNotNull();
assertThat( employee.getManager().getId() ).isEqualTo( "2" );
assertThat( employee.getManager().getName() ).isEqualTo( "Manager" );
}
@ProcessorTest
@WithClasses({
OptionalLikeConditionalMapper.class
@ -244,4 +322,124 @@ public class ConditionalMappingTest {
assertThat( targetEmployee.getName() ).isEqualTo( "CurrentName" );
}
@ProcessorTest
@WithClasses({
ErroneousConditionalWithoutAppliesToMethodMapper.class
})
@ExpectedCompilationOutcome(
value = CompilationResult.FAILED,
diagnostics = {
@Diagnostic(
kind = javax.tools.Diagnostic.Kind.ERROR,
type = ErroneousConditionalWithoutAppliesToMethodMapper.class,
line = 19,
message = "'appliesTo' has to have at least one value in @Condition"
)
}
)
public void emptyConditional() {
}
@ProcessorTest
@WithClasses({
ErroneousSourceParameterConditionalWithMappingTargetMapper.class
})
@ExpectedCompilationOutcome(
value = CompilationResult.FAILED,
diagnostics = {
@Diagnostic(
kind = javax.tools.Diagnostic.Kind.ERROR,
type = ErroneousSourceParameterConditionalWithMappingTargetMapper.class,
line = 21,
message = "Parameter \"@MappingTarget BasicEmployee employee\"" +
" cannot be used with the ConditionStrategy#SOURCE_PARAMETERS." +
" Only source and @Context parameters are allowed for conditions applicable to source parameters."
)
}
)
public void sourceParameterConditionalWithMappingTarget() {
}
@ProcessorTest
@WithClasses({
ErroneousSourceParameterConditionalWithTargetTypeMapper.class
})
@ExpectedCompilationOutcome(
value = CompilationResult.FAILED,
diagnostics = {
@Diagnostic(
kind = javax.tools.Diagnostic.Kind.ERROR,
type = ErroneousSourceParameterConditionalWithTargetTypeMapper.class,
line = 21,
message = "Parameter \"@TargetType Class<?> targetClass\"" +
" cannot be used with the ConditionStrategy#SOURCE_PARAMETERS." +
" Only source and @Context parameters are allowed for conditions applicable to source parameters."
)
}
)
public void sourceParameterConditionalWithTargetType() {
}
@ProcessorTest
@WithClasses({
ErroneousSourceParameterConditionalWithTargetPropertyNameMapper.class
})
@ExpectedCompilationOutcome(
value = CompilationResult.FAILED,
diagnostics = {
@Diagnostic(
kind = javax.tools.Diagnostic.Kind.ERROR,
type = ErroneousSourceParameterConditionalWithTargetPropertyNameMapper.class,
line = 21,
message = "Parameter \"@TargetPropertyName String targetProperty\"" +
" cannot be used with the ConditionStrategy#SOURCE_PARAMETERS." +
" Only source and @Context parameters are allowed for conditions applicable to source parameters."
)
}
)
public void sourceParameterConditionalWithTargetPropertyName() {
}
@ProcessorTest
@WithClasses({
ErroneousSourceParameterConditionalWithSourcePropertyNameMapper.class
})
@ExpectedCompilationOutcome(
value = CompilationResult.FAILED,
diagnostics = {
@Diagnostic(
kind = javax.tools.Diagnostic.Kind.ERROR,
type = ErroneousSourceParameterConditionalWithSourcePropertyNameMapper.class,
line = 21,
message = "Parameter \"@SourcePropertyName String sourceProperty\"" +
" cannot be used with the ConditionStrategy#SOURCE_PARAMETERS." +
" Only source and @Context parameters are allowed for conditions applicable to source parameters."
)
}
)
public void sourceParametersConditionalWithSourcePropertyName() {
}
@ProcessorTest
@WithClasses({
ErroneousAmbiguousSourceParameterConditionalMethodMapper.class
})
@ExpectedCompilationOutcome(
value = CompilationResult.FAILED,
diagnostics = {
@Diagnostic(
kind = javax.tools.Diagnostic.Kind.ERROR,
type = ErroneousAmbiguousSourceParameterConditionalMethodMapper.class,
line = 17,
message = "Ambiguous source parameter check methods found for checking BasicEmployeeDto: " +
"boolean hasName(BasicEmployeeDto value), " +
"boolean hasStrategy(BasicEmployeeDto value). " +
"See https://mapstruct.org/faq/#ambiguous for more info."
)
}
)
public void ambiguousSourceParameterConditionalMethod() {
}
}

View File

@ -0,0 +1,64 @@
/*
* 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.basic;
import org.mapstruct.Mapper;
import org.mapstruct.SourceParameterCondition;
import org.mapstruct.factory.Mappers;
/**
* @author Filip Hrisafov
*/
@Mapper
public interface ConditionalMethodForSourceBeanMapper {
ConditionalMethodForSourceBeanMapper INSTANCE = Mappers.getMapper( ConditionalMethodForSourceBeanMapper.class );
Employee map(EmployeeDto employee);
@SourceParameterCondition
default boolean canMapEmployeeDto(EmployeeDto employee) {
return employee != null && employee.getId() != null;
}
class EmployeeDto {
private final String id;
private final String name;
public EmployeeDto(String id, String name) {
this.id = id;
this.name = name;
}
public String getId() {
return id;
}
public String getName() {
return name;
}
}
class Employee {
private final String id;
private final String name;
public Employee(String id, String name) {
this.id = id;
this.name = name;
}
public String getId() {
return id;
}
public String getName() {
return name;
}
}
}

View File

@ -0,0 +1,85 @@
/*
* 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.basic;
import org.mapstruct.Condition;
import org.mapstruct.ConditionStrategy;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
/**
* @author Filip Hrisafov
*/
@Mapper
public interface ConditionalMethodForSourceParameterAndPropertyMapper {
ConditionalMethodForSourceParameterAndPropertyMapper INSTANCE = Mappers.getMapper(
ConditionalMethodForSourceParameterAndPropertyMapper.class );
Employee map(EmployeeDto employee);
@Condition(appliesTo = {
ConditionStrategy.SOURCE_PARAMETERS,
ConditionStrategy.PROPERTIES
})
default boolean canMapEmployeeDto(EmployeeDto employee) {
return employee != null && employee.getId() != null;
}
class EmployeeDto {
private final String id;
private final String name;
private final EmployeeDto manager;
public EmployeeDto(String id, String name) {
this( id, name, null );
}
public EmployeeDto(String id, String name, EmployeeDto manager) {
this.id = id;
this.name = name;
this.manager = manager;
}
public String getId() {
return id;
}
public String getName() {
return name;
}
public EmployeeDto getManager() {
return manager;
}
}
class Employee {
private final String id;
private final String name;
private final Employee manager;
public Employee(String id, String name, Employee manager) {
this.id = id;
this.name = name;
this.manager = manager;
}
public String getId() {
return id;
}
public String getName() {
return name;
}
public Employee getManager() {
return manager;
}
}
}

View File

@ -0,0 +1,28 @@
/*
* 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.basic;
import org.mapstruct.Mapper;
import org.mapstruct.SourceParameterCondition;
/**
* @author Filip Hrisafov
*/
@Mapper
public interface ErroneousAmbiguousSourceParameterConditionalMethodMapper {
BasicEmployee map(BasicEmployeeDto employee);
@SourceParameterCondition
default boolean hasName(BasicEmployeeDto value) {
return value != null && value.getName() != null;
}
@SourceParameterCondition
default boolean hasStrategy(BasicEmployeeDto value) {
return value != null && value.getStrategy() != null;
}
}

View File

@ -0,0 +1,24 @@
/*
* 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.basic;
import org.mapstruct.Condition;
import org.mapstruct.Mapper;
/**
* @author Filip Hrisafov
*/
@Mapper
public interface ErroneousConditionalWithoutAppliesToMethodMapper {
BasicEmployee map(BasicEmployeeDto employee);
@Condition(appliesTo = {})
default boolean isNotBlank(String value) {
return value != null && !value.trim().isEmpty();
}
}

View File

@ -0,0 +1,26 @@
/*
* 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.basic;
import org.mapstruct.Condition;
import org.mapstruct.ConditionStrategy;
import org.mapstruct.Mapper;
import org.mapstruct.MappingTarget;
/**
* @author Filip Hrisafov
*/
@Mapper
public interface ErroneousSourceParameterConditionalWithMappingTargetMapper {
BasicEmployee map(BasicEmployeeDto employee);
@Condition(appliesTo = ConditionStrategy.SOURCE_PARAMETERS)
default boolean isNotBlank(String value, @MappingTarget BasicEmployee employee) {
return value != null && !value.trim().isEmpty();
}
}

View File

@ -0,0 +1,26 @@
/*
* 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.basic;
import org.mapstruct.Condition;
import org.mapstruct.ConditionStrategy;
import org.mapstruct.Mapper;
import org.mapstruct.SourcePropertyName;
/**
* @author Filip Hrisafov
*/
@Mapper
public interface ErroneousSourceParameterConditionalWithSourcePropertyNameMapper {
BasicEmployee map(BasicEmployeeDto employee);
@Condition(appliesTo = ConditionStrategy.SOURCE_PARAMETERS)
default boolean isNotBlank(String value, @SourcePropertyName String sourceProperty) {
return value != null && !value.trim().isEmpty();
}
}

View File

@ -0,0 +1,26 @@
/*
* 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.basic;
import org.mapstruct.Condition;
import org.mapstruct.ConditionStrategy;
import org.mapstruct.Mapper;
import org.mapstruct.TargetPropertyName;
/**
* @author Filip Hrisafov
*/
@Mapper
public interface ErroneousSourceParameterConditionalWithTargetPropertyNameMapper {
BasicEmployee map(BasicEmployeeDto employee);
@Condition(appliesTo = ConditionStrategy.SOURCE_PARAMETERS)
default boolean isNotBlank(String value, @TargetPropertyName String targetProperty) {
return value != null && !value.trim().isEmpty();
}
}

View File

@ -0,0 +1,26 @@
/*
* 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.basic;
import org.mapstruct.Condition;
import org.mapstruct.ConditionStrategy;
import org.mapstruct.Mapper;
import org.mapstruct.TargetType;
/**
* @author Filip Hrisafov
*/
@Mapper
public interface ErroneousSourceParameterConditionalWithTargetTypeMapper {
BasicEmployee map(BasicEmployeeDto employee);
@Condition(appliesTo = ConditionStrategy.SOURCE_PARAMETERS)
default boolean isNotBlank(String value, @TargetType Class<?> targetClass) {
return value != null && !value.trim().isEmpty();
}
}

View File

@ -11,12 +11,14 @@ import java.util.stream.Stream;
import org.junit.jupiter.api.Test;
import org.mapstruct.CollectionMappingStrategy;
import org.mapstruct.ConditionStrategy;
import org.mapstruct.InjectionStrategy;
import org.mapstruct.MappingInheritanceStrategy;
import org.mapstruct.NullValueCheckStrategy;
import org.mapstruct.NullValueMappingStrategy;
import org.mapstruct.ReportingPolicy;
import org.mapstruct.ap.internal.gem.CollectionMappingStrategyGem;
import org.mapstruct.ap.internal.gem.ConditionStrategyGem;
import org.mapstruct.ap.internal.gem.InjectionStrategyGem;
import org.mapstruct.ap.internal.gem.MappingInheritanceStrategyGem;
import org.mapstruct.ap.internal.gem.NullValueCheckStrategyGem;
@ -67,6 +69,12 @@ public class EnumGemsTest {
namesOf( InjectionStrategyGem.values() ) );
}
@Test
public void conditionStrategyGemIsCorrect() {
assertThat( namesOf( ConditionStrategy.values() ) ).isEqualTo(
namesOf( ConditionStrategyGem.values() ) );
}
private static List<String> namesOf(Enum<?>[] values) {
return Stream.of( values )
.map( Enum::name )