From 0a2a0aa526fe1c970baba61c4f9dcc23e975c517 Mon Sep 17 00:00:00 2001 From: Filip Hrisafov Date: Mon, 29 Apr 2024 08:05:52 +0200 Subject: [PATCH] #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 --- .../main/java/org/mapstruct/Condition.java | 40 ++-- .../java/org/mapstruct/ConditionStrategy.java | 23 ++ .../mapstruct/SourceParameterCondition.java | 74 +++++++ ...apter-10-advanced-mapping-options.asciidoc | 53 ++++- .../ap/internal/gem/ConditionStrategyGem.java | 15 ++ .../ap/internal/model/BeanMappingMethod.java | 34 ++- .../model/PresenceCheckMethodResolver.java | 118 ++++++++--- .../ap/internal/model/common/Parameter.java | 16 +- .../model/common/ParameterBinding.java | 109 +++++++--- .../model/source/ConditionMethodOptions.java | 45 ++++ .../model/source/ConditionOptions.java | 170 +++++++++++++++ .../ap/internal/model/source/Method.java | 12 +- .../internal/model/source/SourceMethod.java | 43 ++-- .../selector/CreateOrUpdateSelector.java | 1 + .../source/selector/MethodFamilySelector.java | 18 +- .../source/selector/SelectionContext.java | 39 ++++ .../source/selector/SelectionCriteria.java | 12 ++ .../processor/MethodRetrievalProcessor.java | 46 +++- .../creation/MappingResolverImpl.java | 2 +- .../mapstruct/ap/internal/util/Message.java | 5 + .../ap/internal/util/MetaAnnotations.java | 85 ++++++++ .../basic/ConditionalMappingTest.java | 198 ++++++++++++++++++ .../ConditionalMethodForSourceBeanMapper.java | 64 ++++++ ...odForSourceParameterAndPropertyMapper.java | 85 ++++++++ ...ourceParameterConditionalMethodMapper.java | 28 +++ ...nditionalWithoutAppliesToMethodMapper.java | 24 +++ ...terConditionalWithMappingTargetMapper.java | 26 +++ ...nditionalWithSourcePropertyNameMapper.java | 26 +++ ...nditionalWithTargetPropertyNameMapper.java | 26 +++ ...ameterConditionalWithTargetTypeMapper.java | 26 +++ .../mapstruct/ap/test/gem/EnumGemsTest.java | 8 + 31 files changed, 1339 insertions(+), 132 deletions(-) create mode 100644 core/src/main/java/org/mapstruct/ConditionStrategy.java create mode 100644 core/src/main/java/org/mapstruct/SourceParameterCondition.java create mode 100644 processor/src/main/java/org/mapstruct/ap/internal/gem/ConditionStrategyGem.java create mode 100644 processor/src/main/java/org/mapstruct/ap/internal/model/source/ConditionMethodOptions.java create mode 100644 processor/src/main/java/org/mapstruct/ap/internal/model/source/ConditionOptions.java create mode 100644 processor/src/main/java/org/mapstruct/ap/internal/util/MetaAnnotations.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/conditional/basic/ConditionalMethodForSourceBeanMapper.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/conditional/basic/ConditionalMethodForSourceParameterAndPropertyMapper.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/conditional/basic/ErroneousAmbiguousSourceParameterConditionalMethodMapper.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/conditional/basic/ErroneousConditionalWithoutAppliesToMethodMapper.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/conditional/basic/ErroneousSourceParameterConditionalWithMappingTargetMapper.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/conditional/basic/ErroneousSourceParameterConditionalWithSourcePropertyNameMapper.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/conditional/basic/ErroneousSourceParameterConditionalWithTargetPropertyNameMapper.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/conditional/basic/ErroneousSourceParameterConditionalWithTargetTypeMapper.java diff --git a/core/src/main/java/org/mapstruct/Condition.java b/core/src/main/java/org/mapstruct/Condition.java index 37f1553be..1273deab0 100644 --- a/core/src/main/java/org/mapstruct/Condition.java +++ b/core/src/main/java/org/mapstruct/Condition.java @@ -11,26 +11,35 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** - * This annotation marks a method as a presence check method to check check for presence in beans. + * This annotation marks a method as a presence check method to check for presence in beans + * or it can be used to define additional check methods for something like source parameters. *

- * 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. *

* Presence check methods have to return {@code boolean}. * The following parameters are accepted for the presence check methods: *

* * Note: The usage of this annotation is mandatory * for a method to be considered as a presence check method. * - *

+ * 

  * public class PresenceCheckUtils {
  *
  *   @Condition
@@ -45,11 +54,10 @@ import java.lang.annotation.Target;
  *     MovieDto map(Movie movie);
  * }
  * 
- * + *

* The following implementation of {@code MovieMapper} will be generated: * - *

- * 
+ * 

  * public class MovieMapperImpl implements MovieMapper {
  *
  *     @Override
@@ -67,14 +75,22 @@ import java.lang.annotation.Target;
  *         return movieDto;
  *     }
  * }
- * 
- * 
+ *
+ *

+ * 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; + } diff --git a/core/src/main/java/org/mapstruct/ConditionStrategy.java b/core/src/main/java/org/mapstruct/ConditionStrategy.java new file mode 100644 index 000000000..6b042017c --- /dev/null +++ b/core/src/main/java/org/mapstruct/ConditionStrategy.java @@ -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, +} diff --git a/core/src/main/java/org/mapstruct/SourceParameterCondition.java b/core/src/main/java/org/mapstruct/SourceParameterCondition.java new file mode 100644 index 000000000..8bff97abc --- /dev/null +++ b/core/src/main/java/org/mapstruct/SourceParameterCondition.java @@ -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 check method to check if a source parameter needs to be mapped. + *

+ * By default, source parameters are checked against {@code null}, unless they are primitives. + *

+ * Check methods have to return {@code boolean}. + * The following parameters are accepted for the presence check methods: + *

    + *
  • The mapping source parameter
  • + *
  • {@code @}{@link Context} parameter
  • + *
+ * + * Note: The usage of this annotation is mandatory + * for a method to be considered as a source check method. + * + *

+ * public class PresenceCheckUtils {
+ *
+ *   @SourceParameterCondition
+ *   public static boolean isDefined(Car car) {
+ *      return car != null && car.getId() != null;
+ *   }
+ * }
+ *
+ * @Mapper(uses = PresenceCheckUtils.class)
+ * public interface CarMapper {
+ *
+ *     CarDto map(Car car);
+ * }
+ * 
+ * + * The following implementation of {@code CarMapper} will be generated: + * + *

+ * public class CarMapperImpl implements CarMapper {
+ *
+ *     @Override
+ *     public CarDto map(Car car) {
+ *         if ( !PresenceCheckUtils.isDefined( car ) ) {
+ *             return null;
+ *         }
+ *
+ *         CarDto carDto = new CarDto();
+ *
+ *         carDto.setId( car.getId() );
+ *         // ...
+ *
+ *         return carDto;
+ *     }
+ * }
+ * 
+ * + * @author Filip Hrisafov + * @since 1.6 + * @see Condition @Condition + */ +@Target({ ElementType.METHOD }) +@Retention(RetentionPolicy.CLASS) +@Condition(appliesTo = ConditionStrategy.SOURCE_PARAMETERS) +public @interface SourceParameterCondition { + +} diff --git a/documentation/src/main/asciidoc/chapter-10-advanced-mapping-options.asciidoc b/documentation/src/main/asciidoc/chapter-10-advanced-mapping-options.asciidoc index db13c0548..1e2bd133d 100644 --- a/documentation/src/main/asciidoc/chapter-10-advanced-mapping-options.asciidoc +++ b/documentation/src/main/asciidoc/chapter-10-advanced-mapping-options.asciidoc @@ -303,8 +303,10 @@ null check, regardless the value of the `NullValueCheckStrategy` to avoid additi Conditional Mapping is a type of <>. 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 <> 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 diff --git a/processor/src/main/java/org/mapstruct/ap/internal/gem/ConditionStrategyGem.java b/processor/src/main/java/org/mapstruct/ap/internal/gem/ConditionStrategyGem.java new file mode 100644 index 000000000..adea4b4c1 --- /dev/null +++ b/processor/src/main/java/org/mapstruct/ap/internal/gem/ConditionStrategyGem.java @@ -0,0 +1,15 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.internal.gem; + +/** + * @author Filip Hrisafov + */ +public enum ConditionStrategyGem { + + PROPERTIES, + SOURCE_PARAMETERS +} diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/BeanMappingMethod.java b/processor/src/main/java/org/mapstruct/ap/internal/model/BeanMappingMethod.java index dd3a86e2c..cf5179f9c 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/BeanMappingMethod.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/BeanMappingMethod.java @@ -411,6 +411,26 @@ public class BeanMappingMethod extends NormalTypeMappingMethod { removeMappingReferencesWithoutSourceParameters( afterMappingReferencesWithFinalizedReturnType ); } + Map 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 afterMappingReferencesWithFinalizedReturnType, MethodReference finalizerMethod, MappingReferences mappingReferences, - List subclassMappings) { + List subclassMappings, + Map 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 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() ) { diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/PresenceCheckMethodResolver.java b/processor/src/main/java/org/mapstruct/ap/internal/model/PresenceCheckMethodResolver.java index a5c873743..5906db821 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/PresenceCheckMethodResolver.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/PresenceCheckMethodResolver.java @@ -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 matchingMethod = findMatchingPresenceCheckMethod( + List> 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 findMatchingPresenceCheckMethod( - Method method, - SelectionParameters selectionParameters, - MappingBuilderContext ctx - ) { - MethodSelectors selectors = new MethodSelectors( - ctx.getTypeUtils(), - ctx.getElementUtils(), - ctx.getMessager() - ); - - List> 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 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> 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 matchingMethod = matchingMethods.get( 0 ); + + MethodReference methodReference = getPresenceCheckMethodReference( method, matchingMethod, ctx ); + + return new MethodReferencePresenceCheck( methodReference ); + + } + + private static List> 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 getAllAvailableMethods(Method method, List sourceModelMethods) { + private static List getAllAvailableMethods(Method method, List 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 ); diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/common/Parameter.java b/processor/src/main/java/org/mapstruct/ap/internal/model/common/Parameter.java index 44ac0eb7f..aaab7f46c 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/common/Parameter.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/common/Parameter.java @@ -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(); - } - } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/common/ParameterBinding.java b/processor/src/main/java/org/mapstruct/ap/internal/model/common/ParameterBinding.java index 0791ee626..c1a594c73 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/common/ParameterBinding.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/common/ParameterBinding.java @@ -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 bindingTypes; - private ParameterBinding(Type parameterType, String variableName, boolean mappingTarget, boolean targetType, - boolean mappingContext, boolean sourcePropertyName, boolean targetPropertyName, + private ParameterBinding(Type parameterType, String variableName, Collection 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 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 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 } } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/source/ConditionMethodOptions.java b/processor/src/main/java/org/mapstruct/ap/internal/model/source/ConditionMethodOptions.java new file mode 100644 index 000000000..dfb0865ec --- /dev/null +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/source/ConditionMethodOptions.java @@ -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; + + public ConditionMethodOptions(Collection 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; + } +} diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/source/ConditionOptions.java b/processor/src/main/java/org/mapstruct/ap/internal/model/source/ConditionOptions.java new file mode 100644 index 000000000..936d049af --- /dev/null +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/source/ConditionOptions.java @@ -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 conditionStrategies; + + private ConditionOptions(Set conditionStrategies) { + this.conditionStrategies = conditionStrategies; + } + + public Collection getConditionStrategies() { + return conditionStrategies; + } + + public static ConditionOptions getInstanceOn(ConditionGem condition, ExecutableElement method, + List 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 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 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 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 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; + } +} diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/source/Method.java b/processor/src/main/java/org/mapstruct/ap/internal/model/source/Method.java index 0c60f4136..ad2882080 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/source/Method.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/source/Method.java @@ -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 diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/source/SourceMethod.java b/processor/src/main/java/org/mapstruct/ap/internal/model/source/SourceMethod.java index 42c318ca4..7103fb285 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/source/SourceMethod.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/source/SourceMethod.java @@ -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 exceptionTypes; private final MappingMethodOptions mappingMethodOptions; + private final ConditionMethodOptions conditionMethodOptions; private final List prototypeMethods; private final Type mapperToImplement; @@ -95,6 +94,7 @@ public class SourceMethod implements Method { private List valueMappings; private EnumMappingOptions enumMappingOptions; private ParameterProvidedMethods contextProvidedMethods; + private Set conditionOptions; private List typeParameters; private Set subclassMappings; @@ -196,6 +196,11 @@ public class SourceMethod implements Method { return this; } + public Builder setConditionOptions(Set 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() ); } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/source/selector/CreateOrUpdateSelector.java b/processor/src/main/java/org/mapstruct/ap/internal/model/source/selector/CreateOrUpdateSelector.java index 03a671de5..b6e5ca0a5 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/source/selector/CreateOrUpdateSelector.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/source/selector/CreateOrUpdateSelector.java @@ -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; } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/source/selector/MethodFamilySelector.java b/processor/src/main/java/org/mapstruct/ap/internal/model/source/selector/MethodFamilySelector.java index d81269421..691199b49 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/source/selector/MethodFamilySelector.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/source/selector/MethodFamilySelector.java @@ -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> result = new ArrayList<>( methods.size() ); for ( SelectedMethod 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 ); diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/source/selector/SelectionContext.java b/processor/src/main/java/org/mapstruct/ap/internal/model/source/selector/SelectionContext.java index 800278a4c..84bd04d7d 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/source/selector/SelectionContext.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/source/selector/SelectionContext.java @@ -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 getParameterBindingsForSourceParameterPresenceCheck(Method method, + Type targetType, + Parameter sourceParameter, + TypeFactory typeFactory) { + + List 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 getAvailableParameterBindingsFromMethod(Method method, Type targetType, SourceRHS sourceRHS, TypeFactory typeFactory) { diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/source/selector/SelectionCriteria.java b/processor/src/main/java/org/mapstruct/ap/internal/model/source/selector/SelectionCriteria.java index 2d288dd56..fa5e1c29c 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/source/selector/SelectionCriteria.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/source/selector/SelectionCriteria.java @@ -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, } } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/processor/MethodRetrievalProcessor.java b/processor/src/main/java/org/mapstruct/ap/internal/processor/MethodRetrievalProcessor.java index a9099ea17..4bd2ed48a 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/processor/MethodRetrievalProcessor.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/processor/MethodRetrievalProcessor.java @@ -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 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 getConditionOptions(ExecutableElement method, List parameters) { + return new MetaConditions( parameters ).getProcessedAnnotations( method ); + } + private class RepeatableMappings extends RepeatableAnnotations { private BeanMappingOptions beanMappingOptions; @@ -774,4 +790,32 @@ public class MethodRetrievalProcessor implements ModelElementProcessor { + + protected final List parameters; + + protected MetaConditions(List 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 values) { + ConditionOptions options = ConditionOptions.getInstanceOn( + gem, + (ExecutableElement) source, + parameters, + messager + ); + if ( options != null ) { + values.add( options ); + } + } + } } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/processor/creation/MappingResolverImpl.java b/processor/src/main/java/org/mapstruct/ap/internal/processor/creation/MappingResolverImpl.java index e6a26ba04..d84ba974d 100755 --- a/processor/src/main/java/org/mapstruct/ap/internal/processor/creation/MappingResolverImpl.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/processor/creation/MappingResolverImpl.java @@ -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 ); diff --git a/processor/src/main/java/org/mapstruct/ap/internal/util/Message.java b/processor/src/main/java/org/mapstruct/ap/internal/util/Message.java index 68c079cb9..5887ee07f 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/util/Message.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/util/Message.java @@ -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" ), diff --git a/processor/src/main/java/org/mapstruct/ap/internal/util/MetaAnnotations.java b/processor/src/main/java/org/mapstruct/ap/internal/util/MetaAnnotations.java new file mode 100644 index 000000000..f2328a91c --- /dev/null +++ b/processor/src/main/java/org/mapstruct/ap/internal/util/MetaAnnotations.java @@ -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 { + + 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 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 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 getValues(Element source, Element element, Set values, Set 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; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/conditional/basic/ConditionalMappingTest.java b/processor/src/test/java/org/mapstruct/ap/test/conditional/basic/ConditionalMappingTest.java index 57a68a952..ae309988c 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/conditional/basic/ConditionalMappingTest.java +++ b/processor/src/test/java/org/mapstruct/ap/test/conditional/basic/ConditionalMappingTest.java @@ -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() { + + } } diff --git a/processor/src/test/java/org/mapstruct/ap/test/conditional/basic/ConditionalMethodForSourceBeanMapper.java b/processor/src/test/java/org/mapstruct/ap/test/conditional/basic/ConditionalMethodForSourceBeanMapper.java new file mode 100644 index 000000000..dd2fe4ac9 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conditional/basic/ConditionalMethodForSourceBeanMapper.java @@ -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; + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/conditional/basic/ConditionalMethodForSourceParameterAndPropertyMapper.java b/processor/src/test/java/org/mapstruct/ap/test/conditional/basic/ConditionalMethodForSourceParameterAndPropertyMapper.java new file mode 100644 index 000000000..11a97b723 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conditional/basic/ConditionalMethodForSourceParameterAndPropertyMapper.java @@ -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; + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/conditional/basic/ErroneousAmbiguousSourceParameterConditionalMethodMapper.java b/processor/src/test/java/org/mapstruct/ap/test/conditional/basic/ErroneousAmbiguousSourceParameterConditionalMethodMapper.java new file mode 100644 index 000000000..39433e264 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conditional/basic/ErroneousAmbiguousSourceParameterConditionalMethodMapper.java @@ -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; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/conditional/basic/ErroneousConditionalWithoutAppliesToMethodMapper.java b/processor/src/test/java/org/mapstruct/ap/test/conditional/basic/ErroneousConditionalWithoutAppliesToMethodMapper.java new file mode 100644 index 000000000..76f62845d --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conditional/basic/ErroneousConditionalWithoutAppliesToMethodMapper.java @@ -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(); + } + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/conditional/basic/ErroneousSourceParameterConditionalWithMappingTargetMapper.java b/processor/src/test/java/org/mapstruct/ap/test/conditional/basic/ErroneousSourceParameterConditionalWithMappingTargetMapper.java new file mode 100644 index 000000000..b1206798d --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conditional/basic/ErroneousSourceParameterConditionalWithMappingTargetMapper.java @@ -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(); + } + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/conditional/basic/ErroneousSourceParameterConditionalWithSourcePropertyNameMapper.java b/processor/src/test/java/org/mapstruct/ap/test/conditional/basic/ErroneousSourceParameterConditionalWithSourcePropertyNameMapper.java new file mode 100644 index 000000000..1bfd150af --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conditional/basic/ErroneousSourceParameterConditionalWithSourcePropertyNameMapper.java @@ -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(); + } + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/conditional/basic/ErroneousSourceParameterConditionalWithTargetPropertyNameMapper.java b/processor/src/test/java/org/mapstruct/ap/test/conditional/basic/ErroneousSourceParameterConditionalWithTargetPropertyNameMapper.java new file mode 100644 index 000000000..e9dac1ece --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conditional/basic/ErroneousSourceParameterConditionalWithTargetPropertyNameMapper.java @@ -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(); + } + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/conditional/basic/ErroneousSourceParameterConditionalWithTargetTypeMapper.java b/processor/src/test/java/org/mapstruct/ap/test/conditional/basic/ErroneousSourceParameterConditionalWithTargetTypeMapper.java new file mode 100644 index 000000000..9ff55597a --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conditional/basic/ErroneousSourceParameterConditionalWithTargetTypeMapper.java @@ -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(); + } + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/gem/EnumGemsTest.java b/processor/src/test/java/org/mapstruct/ap/test/gem/EnumGemsTest.java index 0c92fc6bb..f46018065 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/gem/EnumGemsTest.java +++ b/processor/src/test/java/org/mapstruct/ap/test/gem/EnumGemsTest.java @@ -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 namesOf(Enum[] values) { return Stream.of( values ) .map( Enum::name )