diff --git a/core/src/main/java/org/mapstruct/Condition.java b/core/src/main/java/org/mapstruct/Condition.java
new file mode 100644
index 000000000..ec1bf06a8
--- /dev/null
+++ b/core/src/main/java/org/mapstruct/Condition.java
@@ -0,0 +1,78 @@
+/*
+ * 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 presence check method to check check for presence in beans.
+ *
+ * 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:
+ *
+ * - 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
+ * - The mapping source parameter
+ * - {@code @}{@link Context} parameter
+ *
+ *
+ * Note: The usage of this annotation is mandatory
+ * for a method to be considered as a presence check method.
+ *
+ *
+ * public class PresenceCheckUtils {
+ *
+ * @Condition
+ * public static boolean isNotEmpty(String value) {
+ * return value != null && !value.isEmpty();
+ * }
+ * }
+ *
+ * @Mapper(uses = PresenceCheckUtils.class)
+ * public interface MovieMapper {
+ *
+ * MovieDto map(Movie movie);
+ * }
+ *
+ *
+ * The following implementation of {@code MovieMapper} will be generated:
+ *
+ *
+ *
+ * public class MovieMapperImpl implements MovieMapper {
+ *
+ * @Override
+ * public MovieDto map(Movie movie) {
+ * if ( movie == null ) {
+ * return null;
+ * }
+ *
+ * MovieDto movieDto = new MovieDto();
+ *
+ * if ( PresenceCheckUtils.isNotEmpty( movie.getTitle() ) ) {
+ * movieDto.setTitle( movie.getTitle() );
+ * }
+ *
+ * return movieDto;
+ * }
+ * }
+ *
+ *
+ *
+ * @author Filip Hrisafov
+ * @since 1.5
+ */
+@Target({ ElementType.METHOD })
+@Retention(RetentionPolicy.CLASS)
+public @interface Condition {
+
+}
diff --git a/core/src/main/java/org/mapstruct/Mapping.java b/core/src/main/java/org/mapstruct/Mapping.java
index b5ce5c7a8..b4bfa30d8 100644
--- a/core/src/main/java/org/mapstruct/Mapping.java
+++ b/core/src/main/java/org/mapstruct/Mapping.java
@@ -316,6 +316,74 @@ public @interface Mapping {
*/
String[] qualifiedByName() default { };
+ /**
+ * A qualifier can be specified to aid the selection process of a suitable presence check method.
+ * This is useful in case multiple presence check methods qualify and thus would result in an
+ * 'Ambiguous presence check methods found' error.
+ * A qualifier is a custom annotation and can be placed on a hand written mapper class or a method.
+ * This is similar to the {@link #qualifiedBy()}, but it is only applied for {@link Condition} methods.
+ *
+ * @return the qualifiers
+ * @see Qualifier
+ * @see #qualifiedBy()
+ * @since 1.5
+ */
+ Class extends Annotation>[] conditionQualifiedBy() default { };
+
+ /**
+ * String-based form of qualifiers for condition / presence check methods;
+ * When looking for a suitable presence check method for a given property, MapStruct will
+ * only consider those methods carrying directly or indirectly (i.e. on the class-level) a {@link Named} annotation
+ * for each of the specified qualifier names.
+ *
+ * This is similar like {@link #qualifiedByName()} but it is only applied for {@link Condition} methods.
+ *
+ * Note that annotation-based qualifiers are generally preferable as they allow more easily to find references and
+ * are safe for refactorings, but name-based qualifiers can be a less verbose alternative when requiring a large
+ * number of qualifiers as no custom annotation types are needed.
+ *
+ *
+ *
+ * @return One or more qualifier name(s)
+ * @see #conditionQualifiedBy()
+ * @see #qualifiedByName()
+ * @see Named
+ * @since 1.5
+ */
+ String[] conditionQualifiedByName() default { };
+
+ /**
+ * A conditionExpression {@link String} based on which the specified property is to be checked
+ * whether it is present or not.
+ *
+ * Currently, Java is the only supported "expression language" and expressions must be given in form of Java
+ * expressions using the following format: {@code java()}. For instance the mapping:
+ *
+ * @Mapping(
+ * target = "someProp",
+ * conditionExpression = "java(s.getAge() < 18)"
+ * )
+ *
+ *
+ * will cause the following target property assignment to be generated:
+ *
+ * if (s.getAge() < 18) {
+ * targetBean.setSomeProp( s.getSomeProp() );
+ * }
+ *
+ *
+ *
+ * Any types referenced in expressions must be given via their fully-qualified name. Alternatively, types can be
+ * imported via {@link Mapper#imports()}.
+ *
+ * This attribute can not be used together with {@link #expression()} or {@link #constant()}.
+ *
+ * @return An expression specifying a condition check for the designated property
+ *
+ * @since 1.5
+ */
+ String conditionExpression() default "";
+
/**
* Specifies the result type of the mapping method to be used in case multiple mapping methods qualify.
*
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 158aa632f..d3ab2424b 100644
--- a/documentation/src/main/asciidoc/chapter-10-advanced-mapping-options.asciidoc
+++ b/documentation/src/main/asciidoc/chapter-10-advanced-mapping-options.asciidoc
@@ -229,6 +229,78 @@ The source presence checker name can be changed in the MapStruct service provide
Some types of mappings (collections, maps), in which MapStruct is instructed to use a getter or adder as target accessor see `CollectionMappingStrategy`, MapStruct will always generate a source property
null check, regardless the value of the `NullValueCheckStrategy` to avoid addition of `null` to the target collection or map.
====
+
+[[conditional-mapping]]
+=== Conditional Mapping
+
+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.
+
+A custom condition method is a method that is annotated with `org.mapstruct.Condition` and returns `boolean`.
+
+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:
+
+.Mapper using custom condition check method
+====
+[source, java, linenums]
+[subs="verbatim,attributes"]
+----
+@Mapper
+public interface CarMapper {
+
+ CarDto carToCarDto(Car car);
+
+ @Condition
+ default boolean isNotEmpty(String value) {
+ return value != null && !value.isEmpty();
+ }
+}
+----
+====
+
+The generated mapper will look like:
+
+.try-catch block in generated implementation
+====
+[source, java, linenums]
+[subs="verbatim,attributes"]
+----
+// GENERATED CODE
+public class CarMapperImpl implements CarMapper {
+
+ @Override
+ public CarDto carToCarDto(Car car) {
+ if ( car == null ) {
+ return null;
+ }
+
+ CarDto carDto = new CarDto();
+
+ if ( isNotEmpty( car.getOwner() ) ) {
+ carDto.setOwner( car.getOwner() );
+ }
+
+ // Mapping of other properties
+
+ return carDto;
+ }
+}
+----
+====
+
+[IMPORTANT]
+====
+If there is a custom `@Condition` method applicable for the property it will have a precedence over a presence check method in the bean itself.
+====
+
+[NOTE]
+====
+Methods annotated with `@Condition` in addition to the value of the source property can also have the source parameter as an input.
+====
+
+<> 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`.
+
[[exceptions]]
=== Exceptions
diff --git a/processor/src/main/java/org/mapstruct/ap/internal/gem/GemGenerator.java b/processor/src/main/java/org/mapstruct/ap/internal/gem/GemGenerator.java
index 524ea995d..f8bdbc539 100644
--- a/processor/src/main/java/org/mapstruct/ap/internal/gem/GemGenerator.java
+++ b/processor/src/main/java/org/mapstruct/ap/internal/gem/GemGenerator.java
@@ -12,6 +12,7 @@ import org.mapstruct.AfterMapping;
import org.mapstruct.BeanMapping;
import org.mapstruct.BeforeMapping;
import org.mapstruct.Builder;
+import org.mapstruct.Condition;
import org.mapstruct.Context;
import org.mapstruct.DecoratedWith;
import org.mapstruct.EnumMapping;
@@ -61,6 +62,7 @@ import org.mapstruct.tools.gem.GemDefinition;
@GemDefinition(ValueMappings.class)
@GemDefinition(Context.class)
@GemDefinition(Builder.class)
+@GemDefinition(Condition.class)
@GemDefinition(MappingControl.class)
@GemDefinition(MappingControls.class)
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 0ce80c993..2e82366f8 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
@@ -1155,6 +1155,7 @@ public class BeanMappingMethod extends NormalTypeMappingMethod {
.dependsOn( mapping.getDependsOn() )
.defaultValue( mapping.getDefaultValue() )
.defaultJavaExpression( mapping.getDefaultJavaExpression() )
+ .conditionJavaExpression( mapping.getConditionJavaExpression() )
.mirror( mapping.getMirror() )
.options( mapping )
.build();
diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/MethodReferencePresenceCheck.java b/processor/src/main/java/org/mapstruct/ap/internal/model/MethodReferencePresenceCheck.java
new file mode 100644
index 000000000..e6adceab0
--- /dev/null
+++ b/processor/src/main/java/org/mapstruct/ap/internal/model/MethodReferencePresenceCheck.java
@@ -0,0 +1,51 @@
+/*
+ * 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;
+
+import java.util.Objects;
+import java.util.Set;
+
+import org.mapstruct.ap.internal.model.common.ModelElement;
+import org.mapstruct.ap.internal.model.common.PresenceCheck;
+import org.mapstruct.ap.internal.model.common.Type;
+
+/**
+ * @author Filip Hrisafov
+ */
+public class MethodReferencePresenceCheck extends ModelElement implements PresenceCheck {
+
+ protected final MethodReference methodReference;
+
+ public MethodReferencePresenceCheck(MethodReference methodReference) {
+ this.methodReference = methodReference;
+ }
+
+ @Override
+ public Set getImportTypes() {
+ return methodReference.getImportTypes();
+ }
+
+ public MethodReference getMethodReference() {
+ return methodReference;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if ( this == o ) {
+ return true;
+ }
+ if ( o == null || getClass() != o.getClass() ) {
+ return false;
+ }
+ MethodReferencePresenceCheck that = (MethodReferencePresenceCheck) o;
+ return Objects.equals( methodReference, that.methodReference );
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash( methodReference );
+ }
+}
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
new file mode 100644
index 000000000..9d4910bdc
--- /dev/null
+++ b/processor/src/main/java/org/mapstruct/ap/internal/model/PresenceCheckMethodResolver.java
@@ -0,0 +1,149 @@
+/*
+ * 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;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.mapstruct.ap.internal.model.common.Parameter;
+import org.mapstruct.ap.internal.model.common.PresenceCheck;
+import org.mapstruct.ap.internal.model.common.Type;
+import org.mapstruct.ap.internal.model.source.Method;
+import org.mapstruct.ap.internal.model.source.ParameterProvidedMethods;
+import org.mapstruct.ap.internal.model.source.SelectionParameters;
+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.SelectionCriteria;
+import org.mapstruct.ap.internal.util.Message;
+
+/**
+ * @author Filip Hrisafov
+ */
+public final class PresenceCheckMethodResolver {
+
+ private PresenceCheckMethodResolver() {
+
+ }
+
+ public static PresenceCheck getPresenceCheck(
+ Method method,
+ SelectionParameters selectionParameters,
+ MappingBuilderContext ctx
+ ) {
+ SelectedMethod matchingMethod = findMatchingPresenceCheckMethod(
+ method,
+ selectionParameters,
+ 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.getTypeFactory(),
+ ctx.getMessager()
+ );
+
+ Type booleanType = ctx.getTypeFactory().getType( Boolean.class );
+ List> matchingMethods = selectors.getMatchingMethods(
+ method,
+ getAllAvailableMethods( method, ctx.getSourceModel() ),
+ Collections.emptyList(),
+ booleanType,
+ booleanType,
+ SelectionCriteria.forPresenceCheckMethods( selectionParameters )
+ );
+
+ if ( matchingMethods.isEmpty() ) {
+ return null;
+ }
+
+ if ( matchingMethods.size() > 1 ) {
+ ctx.getMessager().printMessage(
+ method.getExecutable(),
+ Message.GENERAL_AMBIGUOUS_PRESENCE_CHECK_METHOD,
+ selectionParameters.getSourceRHS().getSourceType().describe(),
+ matchingMethods.stream()
+ .map( SelectedMethod::getMethod )
+ .map( Method::describe )
+ .collect( Collectors.joining( ", " ) )
+ );
+
+ return null;
+ }
+
+ return matchingMethods.get( 0 );
+ }
+
+ private static MethodReference getPresenceCheckMethodReference(
+ Method method,
+ SelectedMethod matchingMethod,
+ MappingBuilderContext ctx
+ ) {
+
+ Parameter providingParameter =
+ method.getContextProvidedMethods().getParameterForProvidedMethod( matchingMethod.getMethod() );
+ if ( providingParameter != null ) {
+ return MethodReference.forParameterProvidedMethod(
+ matchingMethod.getMethod(),
+ providingParameter,
+ matchingMethod.getParameterBindings()
+ );
+ }
+ else {
+ MapperReference ref = MapperReference.findMapperReference(
+ ctx.getMapperReferences(),
+ matchingMethod.getMethod()
+ );
+
+ return MethodReference.forMapperReference(
+ matchingMethod.getMethod(),
+ ref,
+ matchingMethod.getParameterBindings()
+ );
+ }
+ }
+
+ private static List getAllAvailableMethods(Method method, List sourceModelMethods) {
+ ParameterProvidedMethods contextProvidedMethods = method.getContextProvidedMethods();
+ if ( contextProvidedMethods.isEmpty() ) {
+ return sourceModelMethods;
+ }
+
+ List methodsProvidedByParams = contextProvidedMethods
+ .getAllProvidedMethodsInParameterOrder( method.getContextParameters() );
+
+ List availableMethods =
+ 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 );
+ }
+ }
+ availableMethods.addAll( sourceModelMethods );
+
+ return availableMethods;
+ }
+}
diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/PropertyMapping.java b/processor/src/main/java/org/mapstruct/ap/internal/model/PropertyMapping.java
index d1d7eaebd..b53b8f90e 100644
--- a/processor/src/main/java/org/mapstruct/ap/internal/model/PropertyMapping.java
+++ b/processor/src/main/java/org/mapstruct/ap/internal/model/PropertyMapping.java
@@ -13,6 +13,9 @@ import java.util.Objects;
import java.util.Set;
import javax.lang.model.element.AnnotationMirror;
+import org.mapstruct.ap.internal.gem.BuilderGem;
+import org.mapstruct.ap.internal.gem.NullValueCheckStrategyGem;
+import org.mapstruct.ap.internal.gem.NullValuePropertyMappingStrategyGem;
import org.mapstruct.ap.internal.model.assignment.AdderWrapper;
import org.mapstruct.ap.internal.model.assignment.ArrayCopyWrapper;
import org.mapstruct.ap.internal.model.assignment.EnumConstantWrapper;
@@ -32,6 +35,7 @@ import org.mapstruct.ap.internal.model.common.PresenceCheck;
import org.mapstruct.ap.internal.model.common.SourceRHS;
import org.mapstruct.ap.internal.model.common.Type;
import org.mapstruct.ap.internal.model.presence.AllPresenceChecksPresenceCheck;
+import org.mapstruct.ap.internal.model.presence.JavaExpressionPresenceCheck;
import org.mapstruct.ap.internal.model.presence.NullPresenceCheck;
import org.mapstruct.ap.internal.model.presence.SourceReferenceMethodPresenceCheck;
import org.mapstruct.ap.internal.model.source.DelegatingOptions;
@@ -40,9 +44,6 @@ import org.mapstruct.ap.internal.model.source.MappingOptions;
import org.mapstruct.ap.internal.model.source.Method;
import org.mapstruct.ap.internal.model.source.SelectionParameters;
import org.mapstruct.ap.internal.model.source.selector.SelectionCriteria;
-import org.mapstruct.ap.internal.gem.BuilderGem;
-import org.mapstruct.ap.internal.gem.NullValueCheckStrategyGem;
-import org.mapstruct.ap.internal.gem.NullValuePropertyMappingStrategyGem;
import org.mapstruct.ap.internal.util.Message;
import org.mapstruct.ap.internal.util.NativeTypes;
import org.mapstruct.ap.internal.util.Strings;
@@ -52,12 +53,12 @@ import org.mapstruct.ap.internal.util.accessor.AccessorType;
import static org.mapstruct.ap.internal.gem.NullValueCheckStrategyGem.ALWAYS;
import static org.mapstruct.ap.internal.gem.NullValuePropertyMappingStrategyGem.IGNORE;
+import static org.mapstruct.ap.internal.gem.NullValuePropertyMappingStrategyGem.SET_TO_DEFAULT;
+import static org.mapstruct.ap.internal.gem.NullValuePropertyMappingStrategyGem.SET_TO_NULL;
import static org.mapstruct.ap.internal.model.ForgedMethod.forElementMapping;
import static org.mapstruct.ap.internal.model.ForgedMethod.forParameterMapping;
import static org.mapstruct.ap.internal.model.ForgedMethod.forPropertyMapping;
import static org.mapstruct.ap.internal.model.common.Assignment.AssignmentType.DIRECT;
-import static org.mapstruct.ap.internal.gem.NullValuePropertyMappingStrategyGem.SET_TO_DEFAULT;
-import static org.mapstruct.ap.internal.gem.NullValuePropertyMappingStrategyGem.SET_TO_NULL;
/**
* Represents the mapping between a source and target property, e.g. from {@code String Source#foo} to
@@ -142,6 +143,7 @@ public class PropertyMapping extends ModelElement {
// initial properties
private String defaultValue;
private String defaultJavaExpression;
+ private String conditionJavaExpression;
private SourceReference sourceReference;
private SourceRHS rightHandSide;
private FormattingParameters formattingParameters;
@@ -182,6 +184,11 @@ public class PropertyMapping extends ModelElement {
return this;
}
+ public PropertyMappingBuilder conditionJavaExpression(String conditionJavaExpression) {
+ this.conditionJavaExpression = conditionJavaExpression;
+ return this;
+ }
+
public PropertyMappingBuilder forgeMethodWithMappingReferences(MappingReferences mappingReferences) {
this.forgeMethodWithMappingReferences = mappingReferences;
return this;
@@ -557,13 +564,19 @@ public class PropertyMapping extends ModelElement {
// simple property
else if ( !sourceReference.isNested() ) {
String sourceRef = sourceParam.getName() + "." + ValueProvider.of( propertyEntry.getReadAccessor() );
- return new SourceRHS( sourceParam.getName(),
- sourceRef,
- getSourcePresenceCheckerRef( sourceReference ),
- propertyEntry.getType(),
- existingVariableNames,
- sourceReference.toString()
+ SourceRHS sourceRHS = new SourceRHS(
+ sourceParam.getName(),
+ sourceRef,
+ null,
+ propertyEntry.getType(),
+ existingVariableNames,
+ sourceReference.toString()
);
+ sourceRHS.setSourcePresenceCheckerReference( getSourcePresenceCheckerRef(
+ sourceReference,
+ sourceRHS
+ ) );
+ return sourceRHS;
}
// nested property given as dot path
else {
@@ -598,11 +611,15 @@ public class PropertyMapping extends ModelElement {
String sourceRef = forgedName + "( " + sourceParam.getName() + " )";
SourceRHS sourceRhs = new SourceRHS( sourceParam.getName(),
sourceRef,
- getSourcePresenceCheckerRef( sourceReference ),
+ null,
sourceType,
existingVariableNames,
sourceReference.toString()
);
+ sourceRhs.setSourcePresenceCheckerReference( getSourcePresenceCheckerRef(
+ sourceReference,
+ sourceRhs
+ ) );
// create a local variable to which forged method can be assigned.
String desiredName = propertyEntry.getName();
@@ -613,7 +630,25 @@ public class PropertyMapping extends ModelElement {
}
}
- private PresenceCheck getSourcePresenceCheckerRef(SourceReference sourceReference ) {
+ private PresenceCheck getSourcePresenceCheckerRef(SourceReference sourceReference,
+ SourceRHS sourceRHS) {
+
+ if ( conditionJavaExpression != null ) {
+ return new JavaExpressionPresenceCheck( conditionJavaExpression );
+ }
+
+ SelectionParameters selectionParameters = this.selectionParameters != null ?
+ this.selectionParameters.withSourceRHS( sourceRHS ) :
+ SelectionParameters.forSourceRHS( sourceRHS );
+ PresenceCheck presenceCheck = PresenceCheckMethodResolver.getPresenceCheck(
+ method,
+ selectionParameters,
+ ctx
+ );
+ if ( presenceCheck != null ) {
+ return presenceCheck;
+ }
+
PresenceCheck sourcePresenceChecker = null;
if ( !sourceReference.getPropertyEntries().isEmpty() ) {
Parameter sourceParam = sourceReference.getParameter();
diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/common/SourceRHS.java b/processor/src/main/java/org/mapstruct/ap/internal/model/common/SourceRHS.java
index 7861c03f2..b73d1ecf4 100644
--- a/processor/src/main/java/org/mapstruct/ap/internal/model/common/SourceRHS.java
+++ b/processor/src/main/java/org/mapstruct/ap/internal/model/common/SourceRHS.java
@@ -7,7 +7,6 @@ package org.mapstruct.ap.internal.model.common;
import java.util.Collection;
import java.util.Collections;
-import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Stream;
@@ -31,7 +30,7 @@ public class SourceRHS extends ModelElement implements Assignment {
private String sourceLoopVarName;
private final Set existingVariableNames;
private final String sourceErrorMessagePart;
- private final PresenceCheck sourcePresenceCheckerReference;
+ private PresenceCheck sourcePresenceCheckerReference;
private boolean useElementAsSourceTypeForMatching = false;
private final String sourceParameterName;
@@ -65,6 +64,10 @@ public class SourceRHS extends ModelElement implements Assignment {
return sourcePresenceCheckerReference;
}
+ public void setSourcePresenceCheckerReference(PresenceCheck sourcePresenceCheckerReference) {
+ this.sourcePresenceCheckerReference = sourcePresenceCheckerReference;
+ }
+
@Override
public Type getSourceType() {
return sourceType;
diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/presence/AllPresenceChecksPresenceCheck.java b/processor/src/main/java/org/mapstruct/ap/internal/model/presence/AllPresenceChecksPresenceCheck.java
index 4ee62cf54..df8b00803 100644
--- a/processor/src/main/java/org/mapstruct/ap/internal/model/presence/AllPresenceChecksPresenceCheck.java
+++ b/processor/src/main/java/org/mapstruct/ap/internal/model/presence/AllPresenceChecksPresenceCheck.java
@@ -6,7 +6,6 @@
package org.mapstruct.ap.internal.model.presence;
import java.util.Collection;
-import java.util.Collections;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/presence/JavaExpressionPresenceCheck.java b/processor/src/main/java/org/mapstruct/ap/internal/model/presence/JavaExpressionPresenceCheck.java
new file mode 100644
index 000000000..d4f52c9f2
--- /dev/null
+++ b/processor/src/main/java/org/mapstruct/ap/internal/model/presence/JavaExpressionPresenceCheck.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright MapStruct Authors.
+ *
+ * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+package org.mapstruct.ap.internal.model.presence;
+
+import java.util.Collections;
+import java.util.Objects;
+import java.util.Set;
+
+import org.mapstruct.ap.internal.model.common.ModelElement;
+import org.mapstruct.ap.internal.model.common.PresenceCheck;
+import org.mapstruct.ap.internal.model.common.Type;
+
+/**
+ * @author Filip Hrisafov
+ */
+public class JavaExpressionPresenceCheck extends ModelElement implements PresenceCheck {
+
+ private final String javaExpression;
+
+ public JavaExpressionPresenceCheck(String javaExpression) {
+ this.javaExpression = javaExpression;
+ }
+
+ public String getJavaExpression() {
+ return javaExpression;
+ }
+
+ @Override
+ public Set getImportTypes() {
+ return Collections.emptySet();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if ( this == o ) {
+ return true;
+ }
+ if ( o == null || getClass() != o.getClass() ) {
+ return false;
+ }
+ JavaExpressionPresenceCheck that = (JavaExpressionPresenceCheck) o;
+ return Objects.equals( javaExpression, that.javaExpression );
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash( javaExpression );
+ }
+}
diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/source/MappingOptions.java b/processor/src/main/java/org/mapstruct/ap/internal/model/source/MappingOptions.java
index 9db1b1c04..d7d6ad629 100644
--- a/processor/src/main/java/org/mapstruct/ap/internal/model/source/MappingOptions.java
+++ b/processor/src/main/java/org/mapstruct/ap/internal/model/source/MappingOptions.java
@@ -43,6 +43,7 @@ public class MappingOptions extends DelegatingOptions {
private final String constant;
private final String javaExpression;
private final String defaultJavaExpression;
+ private final String conditionJavaExpression;
private final String targetName;
private final String defaultValue;
private final FormattingParameters formattingParameters;
@@ -114,6 +115,7 @@ public class MappingOptions extends DelegatingOptions {
String constant = mapping.constant().getValue();
String expression = getExpression( mapping, method, messager );
String defaultExpression = getDefaultExpression( mapping, method, messager );
+ String conditionExpression = getConditionExpression( mapping, method, messager );
String dateFormat = mapping.dateFormat().getValue();
String numberFormat = mapping.numberFormat().getValue();
String defaultValue = mapping.defaultValue().getValue();
@@ -132,6 +134,8 @@ public class MappingOptions extends DelegatingOptions {
SelectionParameters selectionParams = new SelectionParameters(
mapping.qualifiedBy().get(),
mapping.qualifiedByName().get(),
+ mapping.conditionQualifiedBy().get(),
+ mapping.conditionQualifiedByName().get(),
mapping.resultType().getValue(),
typeUtils
);
@@ -145,6 +149,7 @@ public class MappingOptions extends DelegatingOptions {
constant,
expression,
defaultExpression,
+ conditionExpression,
defaultValue,
mapping.ignore().get(),
formattingParam,
@@ -174,6 +179,7 @@ public class MappingOptions extends DelegatingOptions {
null,
null,
null,
+ null,
true,
null,
null,
@@ -261,6 +267,7 @@ public class MappingOptions extends DelegatingOptions {
String constant,
String javaExpression,
String defaultJavaExpression,
+ String conditionJavaExpression,
String defaultValue,
boolean isIgnored,
FormattingParameters formattingParameters,
@@ -279,6 +286,7 @@ public class MappingOptions extends DelegatingOptions {
this.constant = constant;
this.javaExpression = javaExpression;
this.defaultJavaExpression = defaultJavaExpression;
+ this.conditionJavaExpression = conditionJavaExpression;
this.defaultValue = defaultValue;
this.isIgnored = isIgnored;
this.formattingParameters = formattingParameters;
@@ -330,6 +338,27 @@ public class MappingOptions extends DelegatingOptions {
return javaExpressionMatcher.group( 1 ).trim();
}
+ private static String getConditionExpression(MappingGem mapping, ExecutableElement element,
+ FormattingMessager messager) {
+ if ( !mapping.conditionExpression().hasValue() ) {
+ return null;
+ }
+
+ Matcher javaExpressionMatcher = JAVA_EXPRESSION.matcher( mapping.conditionExpression().get() );
+
+ if ( !javaExpressionMatcher.matches() ) {
+ messager.printMessage(
+ element,
+ mapping.mirror(),
+ mapping.conditionExpression().getAnnotationValue(),
+ Message.PROPERTYMAPPING_INVALID_CONDITION_EXPRESSION
+ );
+ return null;
+ }
+
+ return javaExpressionMatcher.group( 1 ).trim();
+ }
+
public String getTargetName() {
return targetName;
}
@@ -364,6 +393,10 @@ public class MappingOptions extends DelegatingOptions {
return defaultJavaExpression;
}
+ public String getConditionJavaExpression() {
+ return conditionJavaExpression;
+ }
+
public String getDefaultValue() {
return defaultValue;
}
@@ -452,6 +485,7 @@ public class MappingOptions extends DelegatingOptions {
null, // constant
null, // expression
null, // defaultExpression
+ null, // conditionExpression
null,
isIgnored,
formattingParameters,
@@ -481,6 +515,7 @@ public class MappingOptions extends DelegatingOptions {
constant,
javaExpression,
defaultJavaExpression,
+ conditionJavaExpression,
defaultValue,
isIgnored,
formattingParameters,
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 8a38ed2ac..0c60f4136 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,6 +90,14 @@ 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 }
*
diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/source/SelectionParameters.java b/processor/src/main/java/org/mapstruct/ap/internal/model/source/SelectionParameters.java
index a78409582..697f0d9e4 100644
--- a/processor/src/main/java/org/mapstruct/ap/internal/model/source/SelectionParameters.java
+++ b/processor/src/main/java/org/mapstruct/ap/internal/model/source/SelectionParameters.java
@@ -23,6 +23,8 @@ public class SelectionParameters {
private final List qualifiers;
private final List qualifyingNames;
+ private final List conditionQualifiers;
+ private final List conditionQualifyingNames;
private final TypeMirror resultType;
private final TypeUtils typeUtils;
private final SourceRHS sourceRHS;
@@ -39,6 +41,8 @@ public class SelectionParameters {
return new SelectionParameters(
selectionParameters.qualifiers,
selectionParameters.qualifyingNames,
+ selectionParameters.conditionQualifiers,
+ selectionParameters.conditionQualifyingNames,
null,
selectionParameters.typeUtils
);
@@ -46,13 +50,32 @@ public class SelectionParameters {
public SelectionParameters(List qualifiers, List qualifyingNames, TypeMirror resultType,
TypeUtils typeUtils) {
- this( qualifiers, qualifyingNames, resultType, typeUtils, null );
+ this(
+ qualifiers,
+ qualifyingNames,
+ Collections.emptyList(),
+ Collections.emptyList(),
+ resultType,
+ typeUtils,
+ null
+ );
}
- private SelectionParameters(List qualifiers, List qualifyingNames, TypeMirror resultType,
+ public SelectionParameters(List qualifiers, List qualifyingNames,
+ List conditionQualifiers, List conditionQualifyingNames,
+ TypeMirror resultType,
+ TypeUtils typeUtils) {
+ this( qualifiers, qualifyingNames, conditionQualifiers, conditionQualifyingNames, resultType, typeUtils, null );
+ }
+
+ private SelectionParameters(List qualifiers, List qualifyingNames,
+ List conditionQualifiers, List conditionQualifyingNames,
+ TypeMirror resultType,
TypeUtils typeUtils, SourceRHS sourceRHS) {
this.qualifiers = qualifiers;
this.qualifyingNames = qualifyingNames;
+ this.conditionQualifiers = conditionQualifiers;
+ this.conditionQualifyingNames = conditionQualifyingNames;
this.resultType = resultType;
this.typeUtils = typeUtils;
this.sourceRHS = sourceRHS;
@@ -74,6 +97,21 @@ public class SelectionParameters {
return qualifyingNames;
}
+ /**
+ * @return qualifiers used for further select the appropriate presence check method based on class and name
+ */
+ public List getConditionQualifiers() {
+ return conditionQualifiers;
+ }
+
+ /**
+ * @return qualifyingNames, used in combination with with @Named
+ * @see #getConditionQualifiers()
+ */
+ public List getConditionQualifyingNames() {
+ return conditionQualifyingNames;
+ }
+
/**
*
* @return resultType used for further select the appropriate mapping method based on resultType (bean mapping)
@@ -119,6 +157,14 @@ public class SelectionParameters {
return false;
}
+ if ( !Objects.equals( this.conditionQualifiers, other.conditionQualifiers ) ) {
+ return false;
+ }
+
+ if ( !Objects.equals( this.conditionQualifyingNames, other.conditionQualifyingNames ) ) {
+ return false;
+ }
+
if ( !Objects.equals( this.sourceRHS, other.sourceRHS ) ) {
return false;
}
@@ -151,8 +197,22 @@ public class SelectionParameters {
}
}
+ public SelectionParameters withSourceRHS(SourceRHS sourceRHS) {
+ return new SelectionParameters(
+ this.qualifiers,
+ this.qualifyingNames,
+ this.conditionQualifiers,
+ this.conditionQualifyingNames,
+ null,
+ this.typeUtils,
+ sourceRHS
+ );
+ }
+
public static SelectionParameters forSourceRHS(SourceRHS sourceRHS) {
return new SelectionParameters(
+ Collections.emptyList(),
+ Collections.emptyList(),
Collections.emptyList(),
Collections.emptyList(),
null,
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 83898d4b3..86f19618d 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
@@ -14,6 +14,8 @@ import java.util.stream.Collectors;
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.util.TypeUtils;
import org.mapstruct.ap.internal.model.common.Accessibility;
@@ -47,6 +49,7 @@ public class SourceMethod implements Method {
private final Parameter mappingTargetParameter;
private final Parameter targetTypeParameter;
private final boolean isObjectFactory;
+ private final boolean isPresenceCheck;
private final Type returnType;
private final Accessibility accessibility;
private final List exceptionTypes;
@@ -230,6 +233,7 @@ public class SourceMethod implements Method {
this.targetTypeParameter = Parameter.getTargetTypeParameter( parameters );
this.hasObjectFactoryAnnotation = ObjectFactoryGem.instanceOn( executable ) != null;
this.isObjectFactory = determineIfIsObjectFactory();
+ this.isPresenceCheck = determineIfIsPresenceCheck();
this.typeUtils = builder.typeUtils;
this.typeFactory = builder.typeFactory;
@@ -247,6 +251,19 @@ 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;
@@ -519,6 +536,11 @@ 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 0e2477402..ed1c72ab2 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
@@ -36,7 +36,8 @@ public class CreateOrUpdateSelector implements MethodSelector {
Type returnType,
SelectionCriteria criteria) {
- if ( criteria.isLifecycleCallbackRequired() || criteria.isObjectFactoryRequired() ) {
+ if ( criteria.isLifecycleCallbackRequired() || criteria.isObjectFactoryRequired()
+ || 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 7e7925ca0..37502ec56 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
@@ -30,7 +30,9 @@ public class MethodFamilySelector implements MethodSelector {
List> result = new ArrayList<>( methods.size() );
for ( SelectedMethod method : methods ) {
if ( method.getMethod().isObjectFactory() == criteria.isObjectFactoryRequired()
- && method.getMethod().isLifecycleCallbackMethod() == criteria.isLifecycleCallbackRequired() ) {
+ && 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/MethodSelectors.java b/processor/src/main/java/org/mapstruct/ap/internal/model/source/selector/MethodSelectors.java
index b88de4f22..de429174d 100644
--- a/processor/src/main/java/org/mapstruct/ap/internal/model/source/selector/MethodSelectors.java
+++ b/processor/src/main/java/org/mapstruct/ap/internal/model/source/selector/MethodSelectors.java
@@ -35,6 +35,7 @@ public class MethodSelectors {
new XmlElementDeclSelector( typeUtils ),
new InheritanceSelector(),
new CreateOrUpdateSelector(),
+ new SourceRhsSelector(),
new FactoryParameterSelector() );
}
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 23c67b230..b70e94b6d 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
@@ -26,20 +26,23 @@ public class SelectionCriteria {
private final String targetPropertyName;
private final TypeMirror qualifyingResultType;
private final SourceRHS sourceRHS;
- private boolean preferUpdateMapping;
- private final boolean objectFactoryRequired;
- private final boolean lifecycleCallbackRequired;
+ private Type type;
private final boolean allowDirect;
private final boolean allowConversion;
private final boolean allowMappingMethod;
private final boolean allow2Steps;
public SelectionCriteria(SelectionParameters selectionParameters, MappingControl mappingControl,
- String targetPropertyName, boolean preferUpdateMapping, boolean objectFactoryRequired,
- boolean lifecycleCallbackRequired) {
+ String targetPropertyName, Type type) {
if ( selectionParameters != null ) {
- qualifiers.addAll( selectionParameters.getQualifiers() );
- qualifiedByNames.addAll( selectionParameters.getQualifyingNames() );
+ if ( type == Type.PRESENCE_CHECK ) {
+ qualifiers.addAll( selectionParameters.getConditionQualifiers() );
+ qualifiedByNames.addAll( selectionParameters.getConditionQualifyingNames() );
+ }
+ else {
+ qualifiers.addAll( selectionParameters.getQualifiers() );
+ qualifiedByNames.addAll( selectionParameters.getQualifyingNames() );
+ }
qualifyingResultType = selectionParameters.getResultType();
sourceRHS = selectionParameters.getSourceRHS();
}
@@ -60,23 +63,28 @@ public class SelectionCriteria {
this.allow2Steps = true;
}
this.targetPropertyName = targetPropertyName;
- this.preferUpdateMapping = preferUpdateMapping;
- this.objectFactoryRequired = objectFactoryRequired;
- this.lifecycleCallbackRequired = lifecycleCallbackRequired;
+ this.type = type;
}
/**
* @return true if factory methods should be selected, false otherwise.
*/
public boolean isObjectFactoryRequired() {
- return objectFactoryRequired;
+ return type == Type.OBJECT_FACTORY;
}
/**
* @return true if lifecycle callback methods should be selected, false otherwise.
*/
public boolean isLifecycleCallbackRequired() {
- return lifecycleCallbackRequired;
+ return type == Type.LIFECYCLE_CALLBACK;
+ }
+
+ /**
+ * @return {@code true} if presence check methods should be selected, {@code false} otherwise
+ */
+ public boolean isPresenceCheckRequired() {
+ return type == Type.PRESENCE_CHECK;
}
public List getQualifiers() {
@@ -96,7 +104,7 @@ public class SelectionCriteria {
}
public boolean isPreferUpdateMapping() {
- return preferUpdateMapping;
+ return type == Type.PREFER_UPDATE_MAPPING;
}
public SourceRHS getSourceRHS() {
@@ -104,7 +112,7 @@ public class SelectionCriteria {
}
public void setPreferUpdateMapping(boolean preferUpdateMapping) {
- this.preferUpdateMapping = preferUpdateMapping;
+ this.type = preferUpdateMapping ? Type.PREFER_UPDATE_MAPPING : null;
}
public boolean hasQualfiers() {
@@ -135,17 +143,26 @@ public class SelectionCriteria {
selectionParameters,
mappingControl,
targetPropertyName,
- preferUpdateMapping,
- false,
- false
+ preferUpdateMapping ? Type.PREFER_UPDATE_MAPPING : null
);
}
public static SelectionCriteria forFactoryMethods(SelectionParameters selectionParameters) {
- return new SelectionCriteria( selectionParameters, null, null, false, true, false );
+ return new SelectionCriteria( selectionParameters, null, null, Type.OBJECT_FACTORY );
}
public static SelectionCriteria forLifecycleMethods(SelectionParameters selectionParameters) {
- return new SelectionCriteria( selectionParameters, null, null, false, false, true );
+ return new SelectionCriteria( selectionParameters, null, null, Type.LIFECYCLE_CALLBACK );
+ }
+
+ public static SelectionCriteria forPresenceCheckMethods(SelectionParameters selectionParameters) {
+ return new SelectionCriteria( selectionParameters, null, null, Type.PRESENCE_CHECK );
+ }
+
+ public enum Type {
+ PREFER_UPDATE_MAPPING,
+ OBJECT_FACTORY,
+ LIFECYCLE_CALLBACK,
+ PRESENCE_CHECK,
}
}
diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/source/selector/SourceRhsSelector.java b/processor/src/main/java/org/mapstruct/ap/internal/model/source/selector/SourceRhsSelector.java
new file mode 100644
index 000000000..930ce2d40
--- /dev/null
+++ b/processor/src/main/java/org/mapstruct/ap/internal/model/source/selector/SourceRhsSelector.java
@@ -0,0 +1,49 @@
+/*
+ * 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.selector;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.mapstruct.ap.internal.model.common.ParameterBinding;
+import org.mapstruct.ap.internal.model.common.Type;
+import org.mapstruct.ap.internal.model.source.Method;
+
+/**
+ * Selector that tries to resolve an ambiquity between methods that contain source parameters and
+ * {@link org.mapstruct.ap.internal.model.common.SourceRHS SourceRHS} type parameters.
+ * @author Filip Hrisafov
+ */
+public class SourceRhsSelector implements MethodSelector {
+
+ @Override
+ public List> getMatchingMethods(Method mappingMethod,
+ List> candidates,
+ List sourceTypes, Type mappingTargetType,
+ Type returnType, SelectionCriteria criteria) {
+ if ( candidates.size() < 2 || criteria.getSourceRHS() == null ) {
+ return candidates;
+ }
+
+ List> sourceRHSFavoringCandidates = new ArrayList<>();
+
+ for ( SelectedMethod candidate : candidates ) {
+ for ( ParameterBinding parameterBinding : candidate.getParameterBindings() ) {
+ if ( parameterBinding.getSourceRHS() != null ) {
+ sourceRHSFavoringCandidates.add( candidate );
+ break;
+ }
+ }
+
+ }
+
+ if ( !sourceRHSFavoringCandidates.isEmpty() ) {
+ return sourceRHSFavoringCandidates;
+ }
+
+ return candidates;
+ }
+}
diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/source/selector/TypeSelector.java b/processor/src/main/java/org/mapstruct/ap/internal/model/source/selector/TypeSelector.java
index 3d0bd4b9b..74e518c84 100644
--- a/processor/src/main/java/org/mapstruct/ap/internal/model/source/selector/TypeSelector.java
+++ b/processor/src/main/java/org/mapstruct/ap/internal/model/source/selector/TypeSelector.java
@@ -89,7 +89,7 @@ public class TypeSelector implements MethodSelector {
List availableParams = new ArrayList<>( method.getParameters().size() + 3 );
if ( sourceRHS != null ) {
- availableParams.addAll( ParameterBinding.fromParameters( method.getContextParameters() ) );
+ availableParams.addAll( ParameterBinding.fromParameters( method.getParameters() ) );
availableParams.add( ParameterBinding.fromSourceRHS( sourceRHS ) );
}
else {
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 b959a15dc..c6e510d31 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
@@ -23,6 +23,7 @@ import javax.lang.model.type.ExecutableType;
import javax.lang.model.type.TypeKind;
import org.mapstruct.ap.internal.gem.BeanMappingGem;
+import org.mapstruct.ap.internal.gem.ConditionGem;
import org.mapstruct.ap.internal.gem.IterableMappingGem;
import org.mapstruct.ap.internal.gem.MapMappingGem;
import org.mapstruct.ap.internal.gem.MappingGem;
@@ -225,7 +226,8 @@ public class MethodRetrievalProcessor implements ModelElementProcessor contextProvidedMethods = new ArrayList<>( contextParamMethods.size() );
for ( SourceMethod sourceMethod : contextParamMethods ) {
- if ( sourceMethod.isLifecycleCallbackMethod() || sourceMethod.isObjectFactory() ) {
+ if ( sourceMethod.isLifecycleCallbackMethod() || sourceMethod.isObjectFactory()
+ || sourceMethod.isPresenceCheck() ) {
contextProvidedMethods.add( sourceMethod );
}
}
@@ -389,10 +392,22 @@ public class MethodRetrievalProcessor implements ModelElementProcessor parameters) {
int validSourceParameters = 0;
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 e30407ea2..50aeb701d 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
@@ -66,6 +66,7 @@ public enum Message {
PROPERTYMAPPING_EXPRESSION_AND_QUALIFIER_BOTH_DEFINED("Expression and a qualifier both defined in @Mapping, either define an expression or a qualifier."),
PROPERTYMAPPING_INVALID_EXPRESSION( "Value for expression must be given in the form \"java()\"." ),
PROPERTYMAPPING_INVALID_DEFAULT_EXPRESSION( "Value for default expression must be given in the form \"java()\"." ),
+ PROPERTYMAPPING_INVALID_CONDITION_EXPRESSION( "Value for condition expression must be given in the form \"java()\"." ),
PROPERTYMAPPING_INVALID_PARAMETER_NAME( "Method has no source parameter named \"%s\". Method source parameters are: \"%s\"." ),
PROPERTYMAPPING_NO_PROPERTY_IN_PARAMETER( "The type of parameter \"%s\" has no property named \"%s\"." ),
PROPERTYMAPPING_INVALID_PROPERTY_NAME( "No property named \"%s\" exists in source parameter(s). Did you mean \"%s\"?" ),
@@ -120,6 +121,7 @@ public enum Message {
GENERAL_ABSTRACT_RETURN_TYPE( "The return type %s is an abstract class or interface. Provide a non abstract / non interface result type or a factory method." ),
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_CONSTRUCTORS( "Ambiguous constructors found for creating %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/resources/org/mapstruct/ap/internal/model/MethodReferencePresenceCheck.ftl b/processor/src/main/resources/org/mapstruct/ap/internal/model/MethodReferencePresenceCheck.ftl
new file mode 100644
index 000000000..44ec29673
--- /dev/null
+++ b/processor/src/main/resources/org/mapstruct/ap/internal/model/MethodReferencePresenceCheck.ftl
@@ -0,0 +1,9 @@
+<#--
+
+ Copyright MapStruct Authors.
+
+ Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+
+-->
+<#-- @ftlvariable name="" type="org.mapstruct.ap.internal.model.MethodReferencePresenceCheck" -->
+<@includeModel object=methodReference/>
\ No newline at end of file
diff --git a/processor/src/main/resources/org/mapstruct/ap/internal/model/presence/JavaExpressionPresenceCheck.ftl b/processor/src/main/resources/org/mapstruct/ap/internal/model/presence/JavaExpressionPresenceCheck.ftl
new file mode 100644
index 000000000..447fa375c
--- /dev/null
+++ b/processor/src/main/resources/org/mapstruct/ap/internal/model/presence/JavaExpressionPresenceCheck.ftl
@@ -0,0 +1,9 @@
+<#--
+
+ Copyright MapStruct Authors.
+
+ Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+
+-->
+<#-- @ftlvariable name="" type="org.mapstruct.ap.internal.model.presence.JavaExpressionPresenceCheck" -->
+${javaExpression}
\ No newline at end of file
diff --git a/processor/src/test/java/org/mapstruct/ap/test/conditional/Employee.java b/processor/src/test/java/org/mapstruct/ap/test/conditional/Employee.java
new file mode 100644
index 000000000..ab7cdf4ad
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/conditional/Employee.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright MapStruct Authors.
+ *
+ * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+package org.mapstruct.ap.test.conditional;
+
+public class Employee {
+ private String name;
+ private String ssid;
+ private String nin;
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getSsid() {
+ return ssid;
+ }
+
+ public void setSsid(String ssid) {
+ this.ssid = ssid;
+ }
+
+ public String getNin() {
+ return nin;
+ }
+
+ public void setNin(String nin) {
+ this.nin = nin;
+ }
+}
diff --git a/processor/src/test/java/org/mapstruct/ap/test/conditional/EmployeeDto.java b/processor/src/test/java/org/mapstruct/ap/test/conditional/EmployeeDto.java
new file mode 100644
index 000000000..f8b9b319f
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/conditional/EmployeeDto.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright MapStruct Authors.
+ *
+ * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+package org.mapstruct.ap.test.conditional;
+
+public class EmployeeDto {
+ private String name;
+ private String country;
+ private String uniqueIdNumber;
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getCountry() {
+ return country;
+ }
+
+ public void setCountry(String country) {
+ this.country = country;
+ }
+
+ public String getUniqueIdNumber() {
+ return uniqueIdNumber;
+ }
+
+ public void setUniqueIdNumber(String uniqueIdNumber) {
+ this.uniqueIdNumber = uniqueIdNumber;
+ }
+}
diff --git a/processor/src/test/java/org/mapstruct/ap/test/conditional/basic/BasicEmployee.java b/processor/src/test/java/org/mapstruct/ap/test/conditional/basic/BasicEmployee.java
new file mode 100644
index 000000000..9fcaa6e76
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/conditional/basic/BasicEmployee.java
@@ -0,0 +1,22 @@
+/*
+ * 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;
+
+/**
+ * @author Filip Hrisafov
+ */
+public class BasicEmployee {
+
+ private String name;
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+}
diff --git a/processor/src/test/java/org/mapstruct/ap/test/conditional/basic/BasicEmployeeDto.java b/processor/src/test/java/org/mapstruct/ap/test/conditional/basic/BasicEmployeeDto.java
new file mode 100644
index 000000000..24a944137
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/conditional/basic/BasicEmployeeDto.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright MapStruct Authors.
+ *
+ * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+package org.mapstruct.ap.test.conditional.basic;
+
+/**
+ * @author Filip Hrisafov
+ */
+public class BasicEmployeeDto {
+
+ private final String name;
+ private final String strategy;
+
+ public BasicEmployeeDto(String name) {
+ this( name, "default" );
+ }
+
+ public BasicEmployeeDto(String name, String strategy) {
+ this.name = name;
+ this.strategy = strategy;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public String getStrategy() {
+ return strategy;
+ }
+}
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
new file mode 100644
index 000000000..300f97bce
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/conditional/basic/ConditionalMappingTest.java
@@ -0,0 +1,229 @@
+/*
+ * 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 java.util.Arrays;
+import java.util.Collections;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mapstruct.ap.testutil.IssueKey;
+import org.mapstruct.ap.testutil.WithClasses;
+import org.mapstruct.ap.testutil.compilation.annotation.CompilationResult;
+import org.mapstruct.ap.testutil.compilation.annotation.Diagnostic;
+import org.mapstruct.ap.testutil.compilation.annotation.ExpectedCompilationOutcome;
+import org.mapstruct.ap.testutil.runner.AnnotationProcessorTestRunner;
+import org.mapstruct.ap.testutil.runner.GeneratedSource;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @author Filip Hrisafov
+ */
+@IssueKey("2051")
+@WithClasses({
+ BasicEmployee.class,
+ BasicEmployeeDto.class
+})
+@RunWith(AnnotationProcessorTestRunner.class)
+public class ConditionalMappingTest {
+
+ @Rule
+ public final GeneratedSource generatedSource = new GeneratedSource();
+
+ @Test
+ @WithClasses({
+ ConditionalMethodInMapper.class
+ })
+ public void conditionalMethodInMapper() {
+ generatedSource.addComparisonToFixtureFor( ConditionalMethodInMapper.class );
+ ConditionalMethodInMapper mapper = ConditionalMethodInMapper.INSTANCE;
+
+ BasicEmployee employee = mapper.map( new BasicEmployeeDto( "Tester" ) );
+ assertThat( employee.getName() ).isEqualTo( "Tester" );
+
+ employee = mapper.map( new BasicEmployeeDto( "" ) );
+ assertThat( employee.getName() ).isNull();
+
+ employee = mapper.map( new BasicEmployeeDto( " " ) );
+ assertThat( employee.getName() ).isNull();
+ }
+
+ @Test
+ @WithClasses({
+ ConditionalMethodAndBeanPresenceCheckMapper.class
+ })
+ public void conditionalMethodAndBeanPresenceCheckMapper() {
+ ConditionalMethodAndBeanPresenceCheckMapper mapper = ConditionalMethodAndBeanPresenceCheckMapper.INSTANCE;
+
+ BasicEmployee employee = mapper.map( new ConditionalMethodAndBeanPresenceCheckMapper.EmployeeDto( "Tester" ) );
+ assertThat( employee.getName() ).isEqualTo( "Tester" );
+
+ employee = mapper.map( new ConditionalMethodAndBeanPresenceCheckMapper.EmployeeDto( "" ) );
+ assertThat( employee.getName() ).isNull();
+
+ employee = mapper.map( new ConditionalMethodAndBeanPresenceCheckMapper.EmployeeDto( " " ) );
+ assertThat( employee.getName() ).isNull();
+ }
+
+ @Test
+ @WithClasses({
+ ConditionalMethodInUsesMapper.class
+ })
+ public void conditionalMethodInUsesMapper() {
+ ConditionalMethodInUsesMapper mapper = ConditionalMethodInUsesMapper.INSTANCE;
+
+ BasicEmployee employee = mapper.map( new BasicEmployeeDto( "Tester" ) );
+ assertThat( employee.getName() ).isEqualTo( "Tester" );
+
+ employee = mapper.map( new BasicEmployeeDto( "" ) );
+ assertThat( employee.getName() ).isNull();
+
+ employee = mapper.map( new BasicEmployeeDto( " " ) );
+ assertThat( employee.getName() ).isNull();
+ }
+
+ @Test
+ @WithClasses({
+ ConditionalMethodInUsesStaticMapper.class
+ })
+ public void conditionalMethodInUsesStaticMapper() {
+ ConditionalMethodInUsesStaticMapper mapper = ConditionalMethodInUsesStaticMapper.INSTANCE;
+
+ BasicEmployee employee = mapper.map( new BasicEmployeeDto( "Tester" ) );
+ assertThat( employee.getName() ).isEqualTo( "Tester" );
+
+ employee = mapper.map( new BasicEmployeeDto( "" ) );
+ assertThat( employee.getName() ).isNull();
+
+ employee = mapper.map( new BasicEmployeeDto( " " ) );
+ assertThat( employee.getName() ).isNull();
+ }
+
+ @Test
+ @WithClasses({
+ ConditionalMethodInContextMapper.class
+ })
+ public void conditionalMethodInUsesContextMapper() {
+ ConditionalMethodInContextMapper mapper = ConditionalMethodInContextMapper.INSTANCE;
+
+ ConditionalMethodInContextMapper.PresenceUtils utils = new ConditionalMethodInContextMapper.PresenceUtils();
+ BasicEmployee employee = mapper.map( new BasicEmployeeDto( "Tester" ), utils );
+ assertThat( employee.getName() ).isEqualTo( "Tester" );
+
+ employee = mapper.map( new BasicEmployeeDto( "" ), utils );
+ assertThat( employee.getName() ).isNull();
+
+ employee = mapper.map( new BasicEmployeeDto( " " ), utils );
+ assertThat( employee.getName() ).isNull();
+ }
+
+ @Test
+ @WithClasses({
+ ConditionalMethodWithSourceParameterMapper.class
+ })
+ public void conditionalMethodWithSourceParameter() {
+ ConditionalMethodWithSourceParameterMapper mapper = ConditionalMethodWithSourceParameterMapper.INSTANCE;
+
+ BasicEmployee employee = mapper.map( new BasicEmployeeDto( "Tester" ) );
+ assertThat( employee.getName() ).isNull();
+
+ employee = mapper.map( new BasicEmployeeDto( "Tester", "map" ) );
+ assertThat( employee.getName() ).isEqualTo( "Tester" );
+ }
+
+ @Test
+ @WithClasses({
+ ConditionalMethodWithSourceParameterAndValueMapper.class
+ })
+ public void conditionalMethodWithSourceParameterAndValue() {
+ generatedSource.addComparisonToFixtureFor( ConditionalMethodWithSourceParameterAndValueMapper.class );
+ ConditionalMethodWithSourceParameterAndValueMapper mapper =
+ ConditionalMethodWithSourceParameterAndValueMapper.INSTANCE;
+
+ BasicEmployee employee = mapper.map( new BasicEmployeeDto( " ", "empty" ) );
+ assertThat( employee.getName() ).isEqualTo( " " );
+
+ employee = mapper.map( new BasicEmployeeDto( " ", "blank" ) );
+ assertThat( employee.getName() ).isNull();
+
+ employee = mapper.map( new BasicEmployeeDto( "Tester", "blank" ) );
+ assertThat( employee.getName() ).isEqualTo( "Tester" );
+ }
+
+ @Test
+ @WithClasses({
+ ErroneousAmbiguousConditionalMethodMapper.class
+ })
+ @ExpectedCompilationOutcome(
+ value = CompilationResult.FAILED,
+ diagnostics = {
+ @Diagnostic(
+ kind = javax.tools.Diagnostic.Kind.ERROR,
+ type = ErroneousAmbiguousConditionalMethodMapper.class,
+ line = 17,
+ message = "Ambiguous presence check methods found for checking String: " +
+ "boolean isNotBlank(String value), " +
+ "boolean isNotEmpty(String value). " +
+ "See https://mapstruct.org/faq/#ambiguous for more info."
+ )
+ }
+ )
+ public void ambiguousConditionalMethod() {
+
+ }
+
+ @Test
+ @WithClasses({
+ ConditionalMethodForCollectionMapper.class
+ })
+ public void conditionalMethodForCollection() {
+ ConditionalMethodForCollectionMapper mapper = ConditionalMethodForCollectionMapper.INSTANCE;
+
+ ConditionalMethodForCollectionMapper.Author author = new ConditionalMethodForCollectionMapper.Author();
+ ConditionalMethodForCollectionMapper.AuthorDto dto = mapper.map( author );
+
+ assertThat( dto.getBooks() ).isNull();
+
+ author.setBooks( Collections.emptyList() );
+ dto = mapper.map( author );
+
+ assertThat( dto.getBooks() ).isNull();
+
+ author.setBooks( Arrays.asList(
+ new ConditionalMethodForCollectionMapper.Book( "Test" ),
+ new ConditionalMethodForCollectionMapper.Book( "Test Vol. 2" )
+ ) );
+ dto = mapper.map( author );
+
+ assertThat( dto.getBooks() )
+ .extracting( ConditionalMethodForCollectionMapper.BookDto::getName )
+ .containsExactly( "Test", "Test Vol. 2" );
+ }
+
+ @Test
+ @WithClasses({
+ OptionalLikeConditionalMapper.class
+ })
+ @IssueKey("2084")
+ public void optionalLikeConditional() {
+ OptionalLikeConditionalMapper mapper = OptionalLikeConditionalMapper.INSTANCE;
+
+ OptionalLikeConditionalMapper.Target target = mapper.map( new OptionalLikeConditionalMapper.Source(
+ OptionalLikeConditionalMapper.Nullable.ofNullable( "test" ) ) );
+
+ assertThat( target.getValue() ).isEqualTo( "test" );
+
+ target = mapper.map(
+ new OptionalLikeConditionalMapper.Source( OptionalLikeConditionalMapper.Nullable.undefined() )
+ );
+
+ assertThat( target.getValue() ).isEqualTo( "initial" );
+
+ }
+
+}
diff --git a/processor/src/test/java/org/mapstruct/ap/test/conditional/basic/ConditionalMethodAndBeanPresenceCheckMapper.java b/processor/src/test/java/org/mapstruct/ap/test/conditional/basic/ConditionalMethodAndBeanPresenceCheckMapper.java
new file mode 100644
index 000000000..ad37d5832
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/conditional/basic/ConditionalMethodAndBeanPresenceCheckMapper.java
@@ -0,0 +1,44 @@
+/*
+ * 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;
+import org.mapstruct.factory.Mappers;
+
+/**
+ * @author Filip Hrisafov
+ */
+@Mapper
+public interface ConditionalMethodAndBeanPresenceCheckMapper {
+
+ ConditionalMethodAndBeanPresenceCheckMapper INSTANCE = Mappers.getMapper(
+ ConditionalMethodAndBeanPresenceCheckMapper.class );
+
+ BasicEmployee map(EmployeeDto employee);
+
+ @Condition
+ default boolean isNotBlank(String value) {
+ return value != null && !value.trim().isEmpty();
+ }
+
+ class EmployeeDto {
+
+ private final String name;
+
+ public EmployeeDto(String name) {
+ this.name = name;
+ }
+
+ public boolean hasName() {
+ return false;
+ }
+
+ public String getName() {
+ return name;
+ }
+ }
+}
diff --git a/processor/src/test/java/org/mapstruct/ap/test/conditional/basic/ConditionalMethodForCollectionMapper.java b/processor/src/test/java/org/mapstruct/ap/test/conditional/basic/ConditionalMethodForCollectionMapper.java
new file mode 100644
index 000000000..a27a0aaf0
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/conditional/basic/ConditionalMethodForCollectionMapper.java
@@ -0,0 +1,81 @@
+/*
+ * 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 java.util.Collection;
+import java.util.List;
+
+import org.mapstruct.Condition;
+import org.mapstruct.Mapper;
+import org.mapstruct.factory.Mappers;
+
+/**
+ * @author Filip Hrisafov
+ */
+@Mapper
+public interface ConditionalMethodForCollectionMapper {
+
+ ConditionalMethodForCollectionMapper INSTANCE = Mappers.getMapper( ConditionalMethodForCollectionMapper.class );
+
+ AuthorDto map(Author author);
+
+ @Condition
+ default boolean isNotEmpty(Collection collection) {
+ return collection != null && !collection.isEmpty();
+ }
+
+ class Author {
+ private List books;
+
+ public List getBooks() {
+ return books;
+ }
+
+ public boolean hasBooks() {
+ return false;
+ }
+
+ public void setBooks(List books) {
+ this.books = books;
+ }
+ }
+
+ class Book {
+ private final String name;
+
+ public Book(String name) {
+ this.name = name;
+ }
+
+ public String getName() {
+ return name;
+ }
+ }
+
+ class AuthorDto {
+ private List books;
+
+ public List getBooks() {
+ return books;
+ }
+
+ public void setBooks(List books) {
+ this.books = books;
+ }
+ }
+
+ class BookDto {
+ private String name;
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+ }
+}
diff --git a/processor/src/test/java/org/mapstruct/ap/test/conditional/basic/ConditionalMethodInContextMapper.java b/processor/src/test/java/org/mapstruct/ap/test/conditional/basic/ConditionalMethodInContextMapper.java
new file mode 100644
index 000000000..26ad84414
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/conditional/basic/ConditionalMethodInContextMapper.java
@@ -0,0 +1,29 @@
+/*
+ * 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.Context;
+import org.mapstruct.Mapper;
+import org.mapstruct.factory.Mappers;
+
+/**
+ * @author Filip Hrisafov
+ */
+@Mapper
+public interface ConditionalMethodInContextMapper {
+
+ ConditionalMethodInContextMapper INSTANCE = Mappers.getMapper( ConditionalMethodInContextMapper.class );
+
+ BasicEmployee map(BasicEmployeeDto employee, @Context PresenceUtils utils);
+
+ class PresenceUtils {
+ @Condition
+ public boolean isNotBlank(String value) {
+ return value != null && !value.trim().isEmpty();
+ }
+ }
+}
diff --git a/processor/src/test/java/org/mapstruct/ap/test/conditional/basic/ConditionalMethodInMapper.java b/processor/src/test/java/org/mapstruct/ap/test/conditional/basic/ConditionalMethodInMapper.java
new file mode 100644
index 000000000..256419f68
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/conditional/basic/ConditionalMethodInMapper.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.Mapper;
+import org.mapstruct.factory.Mappers;
+
+/**
+ * @author Filip Hrisafov
+ */
+@Mapper
+public interface ConditionalMethodInMapper {
+
+ ConditionalMethodInMapper INSTANCE = Mappers.getMapper( ConditionalMethodInMapper.class );
+
+ BasicEmployee map(BasicEmployeeDto employee);
+
+ @Condition
+ default boolean isNotBlank(String value) {
+ return value != null && !value.trim().isEmpty();
+ }
+}
diff --git a/processor/src/test/java/org/mapstruct/ap/test/conditional/basic/ConditionalMethodInUsesMapper.java b/processor/src/test/java/org/mapstruct/ap/test/conditional/basic/ConditionalMethodInUsesMapper.java
new file mode 100644
index 000000000..fb30d19c8
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/conditional/basic/ConditionalMethodInUsesMapper.java
@@ -0,0 +1,30 @@
+/*
+ * 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;
+import org.mapstruct.factory.Mappers;
+
+/**
+ * @author Filip Hrisafov
+ */
+@Mapper(uses = ConditionalMethodInUsesMapper.PresenceUtils.class)
+public interface ConditionalMethodInUsesMapper {
+
+ ConditionalMethodInUsesMapper INSTANCE = Mappers.getMapper( ConditionalMethodInUsesMapper.class );
+
+ BasicEmployee map(BasicEmployeeDto employee);
+
+ class PresenceUtils {
+
+ @Condition
+ public boolean isNotBlank(String value) {
+ return value != null && !value.trim().isEmpty();
+ }
+
+ }
+}
diff --git a/processor/src/test/java/org/mapstruct/ap/test/conditional/basic/ConditionalMethodInUsesStaticMapper.java b/processor/src/test/java/org/mapstruct/ap/test/conditional/basic/ConditionalMethodInUsesStaticMapper.java
new file mode 100644
index 000000000..5e77d6f8f
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/conditional/basic/ConditionalMethodInUsesStaticMapper.java
@@ -0,0 +1,30 @@
+/*
+ * 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;
+import org.mapstruct.factory.Mappers;
+
+/**
+ * @author Filip Hrisafov
+ */
+@Mapper(uses = ConditionalMethodInUsesStaticMapper.PresenceUtils.class)
+public interface ConditionalMethodInUsesStaticMapper {
+
+ ConditionalMethodInUsesStaticMapper INSTANCE = Mappers.getMapper( ConditionalMethodInUsesStaticMapper.class );
+
+ BasicEmployee map(BasicEmployeeDto employee);
+
+ interface PresenceUtils {
+
+ @Condition
+ static boolean isNotBlank(String value) {
+ return value != null && !value.trim().isEmpty();
+ }
+
+ }
+}
diff --git a/processor/src/test/java/org/mapstruct/ap/test/conditional/basic/ConditionalMethodWithSourceParameterAndValueMapper.java b/processor/src/test/java/org/mapstruct/ap/test/conditional/basic/ConditionalMethodWithSourceParameterAndValueMapper.java
new file mode 100644
index 000000000..e39c5c88e
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/conditional/basic/ConditionalMethodWithSourceParameterAndValueMapper.java
@@ -0,0 +1,38 @@
+/*
+ * 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;
+import org.mapstruct.factory.Mappers;
+
+/**
+ * @author Filip Hrisafov
+ */
+@Mapper
+public interface ConditionalMethodWithSourceParameterAndValueMapper {
+
+ ConditionalMethodWithSourceParameterAndValueMapper INSTANCE = Mappers.getMapper(
+ ConditionalMethodWithSourceParameterAndValueMapper.class );
+
+ BasicEmployee map(BasicEmployeeDto employee);
+
+ @Condition
+ default boolean isPresent(BasicEmployeeDto source, String value) {
+ if ( value == null ) {
+ return false;
+ }
+ switch ( source.getStrategy() ) {
+ case "blank":
+ return !value.trim().isEmpty();
+ case "empty":
+ return !value.isEmpty();
+ default:
+ return true;
+ }
+ }
+
+}
diff --git a/processor/src/test/java/org/mapstruct/ap/test/conditional/basic/ConditionalMethodWithSourceParameterMapper.java b/processor/src/test/java/org/mapstruct/ap/test/conditional/basic/ConditionalMethodWithSourceParameterMapper.java
new file mode 100644
index 000000000..9a499a603
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/conditional/basic/ConditionalMethodWithSourceParameterMapper.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.Condition;
+import org.mapstruct.Mapper;
+import org.mapstruct.factory.Mappers;
+
+/**
+ * @author Filip Hrisafov
+ */
+@Mapper
+public interface ConditionalMethodWithSourceParameterMapper {
+
+ ConditionalMethodWithSourceParameterMapper INSTANCE =
+ Mappers.getMapper( ConditionalMethodWithSourceParameterMapper.class );
+
+ BasicEmployee map(BasicEmployeeDto employee);
+
+ @Condition
+ default boolean shouldMap(BasicEmployeeDto source) {
+ return "map".equals( source.getStrategy() );
+ }
+
+}
diff --git a/processor/src/test/java/org/mapstruct/ap/test/conditional/basic/ErroneousAmbiguousConditionalMethodMapper.java b/processor/src/test/java/org/mapstruct/ap/test/conditional/basic/ErroneousAmbiguousConditionalMethodMapper.java
new file mode 100644
index 000000000..49e354c91
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/conditional/basic/ErroneousAmbiguousConditionalMethodMapper.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.Condition;
+import org.mapstruct.Mapper;
+
+/**
+ * @author Filip Hrisafov
+ */
+@Mapper
+public interface ErroneousAmbiguousConditionalMethodMapper {
+
+ BasicEmployee map(BasicEmployeeDto employee);
+
+ @Condition
+ default boolean isNotBlank(String value) {
+ return value != null && !value.trim().isEmpty();
+ }
+
+ @Condition
+ default boolean isNotEmpty(String value) {
+ return value != null && value.isEmpty();
+ }
+}
diff --git a/processor/src/test/java/org/mapstruct/ap/test/conditional/basic/OptionalLikeConditionalMapper.java b/processor/src/test/java/org/mapstruct/ap/test/conditional/basic/OptionalLikeConditionalMapper.java
new file mode 100644
index 000000000..4d097d7b5
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/conditional/basic/OptionalLikeConditionalMapper.java
@@ -0,0 +1,77 @@
+/*
+ * 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;
+import org.mapstruct.factory.Mappers;
+
+/**
+ * @author Filip Hrisafov
+ */
+@Mapper
+public interface OptionalLikeConditionalMapper {
+
+ OptionalLikeConditionalMapper INSTANCE = Mappers.getMapper( OptionalLikeConditionalMapper.class );
+
+ Target map(Source source);
+
+ default T map(Nullable nullable) {
+ return nullable.value;
+ }
+
+ @Condition
+ default boolean isPresent(Nullable nullable) {
+ return nullable.isPresent();
+ }
+
+ class Nullable {
+
+ private final T value;
+ private final boolean present;
+
+ private Nullable(T value, boolean present) {
+ this.value = value;
+ this.present = present;
+ }
+
+ public boolean isPresent() {
+ return present;
+ }
+
+ public static Nullable undefined() {
+ return new Nullable<>( null, false );
+ }
+
+ public static Nullable ofNullable(T value) {
+ return new Nullable<>( value, true );
+ }
+ }
+
+ class Source {
+ protected final Nullable value;
+
+ public Source(Nullable value) {
+ this.value = value;
+ }
+
+ public Nullable getValue() {
+ return value;
+ }
+ }
+
+ class Target {
+ protected String value = "initial";
+
+ public String getValue() {
+ return value;
+ }
+
+ public void setValue(String value) {
+ this.value = value;
+ }
+ }
+}
diff --git a/processor/src/test/java/org/mapstruct/ap/test/conditional/expression/ConditionalExpressionTest.java b/processor/src/test/java/org/mapstruct/ap/test/conditional/expression/ConditionalExpressionTest.java
new file mode 100644
index 000000000..cf38d85dd
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/conditional/expression/ConditionalExpressionTest.java
@@ -0,0 +1,97 @@
+/*
+ * 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.expression;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mapstruct.ap.test.conditional.Employee;
+import org.mapstruct.ap.test.conditional.EmployeeDto;
+import org.mapstruct.ap.test.conditional.basic.BasicEmployee;
+import org.mapstruct.ap.testutil.IssueKey;
+import org.mapstruct.ap.testutil.WithClasses;
+import org.mapstruct.ap.testutil.compilation.annotation.CompilationResult;
+import org.mapstruct.ap.testutil.compilation.annotation.Diagnostic;
+import org.mapstruct.ap.testutil.compilation.annotation.ExpectedCompilationOutcome;
+import org.mapstruct.ap.testutil.runner.AnnotationProcessorTestRunner;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @author Filip Hrisafov
+ */
+@IssueKey("2051")
+@WithClasses({
+ Employee.class,
+ EmployeeDto.class
+})
+@RunWith(AnnotationProcessorTestRunner.class)
+public class ConditionalExpressionTest {
+
+ @Test
+ @WithClasses({
+ ConditionalMethodsInUtilClassMapper.class
+ })
+ public void conditionalExpressionInStaticClassMethod() {
+ ConditionalMethodsInUtilClassMapper mapper = ConditionalMethodsInUtilClassMapper.INSTANCE;
+
+ EmployeeDto dto = new EmployeeDto();
+ dto.setName( "Tester" );
+ dto.setUniqueIdNumber( "SSID-001" );
+ dto.setCountry( null );
+
+ Employee employee = mapper.map( dto );
+ assertThat( employee.getNin() ).isNull();
+ assertThat( employee.getSsid() ).isNull();
+
+ dto.setCountry( "UK" );
+ employee = mapper.map( dto );
+ assertThat( employee.getNin() ).isEqualTo( "SSID-001" );
+ assertThat( employee.getSsid() ).isNull();
+
+ dto.setCountry( "US" );
+ employee = mapper.map( dto );
+ assertThat( employee.getNin() ).isNull();
+ assertThat( employee.getSsid() ).isEqualTo( "SSID-001" );
+
+ dto.setCountry( "CH" );
+ employee = mapper.map( dto );
+ assertThat( employee.getNin() ).isNull();
+ assertThat( employee.getSsid() ).isNull();
+ }
+
+ @Test
+ @WithClasses({
+ SimpleConditionalExpressionMapper.class
+ })
+ public void conditionalSimpleExpression() {
+ SimpleConditionalExpressionMapper mapper = SimpleConditionalExpressionMapper.INSTANCE;
+
+ SimpleConditionalExpressionMapper.Target target =
+ mapper.map( new SimpleConditionalExpressionMapper.Source( 50 ) );
+ assertThat( target.getValue() ).isEqualTo( 50 );
+
+ target = mapper.map( new SimpleConditionalExpressionMapper.Source( 101 ) );
+ assertThat( target.getValue() ).isEqualTo( 0 );
+ }
+
+ @Test
+ @ExpectedCompilationOutcome(
+ value = CompilationResult.FAILED,
+ diagnostics = {
+ @Diagnostic(type = ErroneousConditionExpressionMapper.class,
+ kind = javax.tools.Diagnostic.Kind.ERROR,
+ line = 19,
+ message = "Value for condition expression must be given in the form \"java()\"."
+ )
+ }
+ )
+ @WithClasses({
+ BasicEmployee.class,
+ ErroneousConditionExpressionMapper.class
+ } )
+ public void invalidJavaConditionExpression() {
+ }
+}
diff --git a/processor/src/test/java/org/mapstruct/ap/test/conditional/expression/ConditionalMethodsInUtilClassMapper.java b/processor/src/test/java/org/mapstruct/ap/test/conditional/expression/ConditionalMethodsInUtilClassMapper.java
new file mode 100644
index 000000000..3c49de5bf
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/conditional/expression/ConditionalMethodsInUtilClassMapper.java
@@ -0,0 +1,38 @@
+/*
+ * 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.expression;
+
+import org.mapstruct.Mapper;
+import org.mapstruct.Mapping;
+import org.mapstruct.ap.test.conditional.Employee;
+import org.mapstruct.ap.test.conditional.EmployeeDto;
+import org.mapstruct.factory.Mappers;
+
+/**
+ * @author Filip Hrisafov
+ */
+@Mapper(imports = ConditionalMethodsInUtilClassMapper.StaticUtil.class)
+public interface ConditionalMethodsInUtilClassMapper {
+
+ ConditionalMethodsInUtilClassMapper INSTANCE = Mappers.getMapper( ConditionalMethodsInUtilClassMapper.class );
+
+ @Mapping(target = "ssid", source = "uniqueIdNumber",
+ conditionExpression = "java(StaticUtil.isAmericanCitizen( employee ))")
+ @Mapping(target = "nin", source = "uniqueIdNumber",
+ conditionExpression = "java(StaticUtil.isBritishCitizen( employee ))")
+ Employee map(EmployeeDto employee);
+
+ interface StaticUtil {
+
+ static boolean isAmericanCitizen(EmployeeDto employeeDto) {
+ return "US".equals( employeeDto.getCountry() );
+ }
+
+ static boolean isBritishCitizen(EmployeeDto employeeDto) {
+ return "UK".equals( employeeDto.getCountry() );
+ }
+ }
+}
diff --git a/processor/src/test/java/org/mapstruct/ap/test/conditional/expression/ErroneousConditionExpressionMapper.java b/processor/src/test/java/org/mapstruct/ap/test/conditional/expression/ErroneousConditionExpressionMapper.java
new file mode 100644
index 000000000..c74c55253
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/conditional/expression/ErroneousConditionExpressionMapper.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright MapStruct Authors.
+ *
+ * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+package org.mapstruct.ap.test.conditional.expression;
+
+import org.mapstruct.Mapper;
+import org.mapstruct.Mapping;
+import org.mapstruct.ap.test.conditional.EmployeeDto;
+import org.mapstruct.ap.test.conditional.basic.BasicEmployee;
+
+/**
+ * @author Filip Hrisafov
+ */
+@Mapper
+public interface ErroneousConditionExpressionMapper {
+
+ @Mapping(target = "name", conditionExpression = "!employee.getName().isEmpty()")
+ BasicEmployee map(EmployeeDto employee);
+}
diff --git a/processor/src/test/java/org/mapstruct/ap/test/conditional/expression/SimpleConditionalExpressionMapper.java b/processor/src/test/java/org/mapstruct/ap/test/conditional/expression/SimpleConditionalExpressionMapper.java
new file mode 100644
index 000000000..861fa0c1e
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/conditional/expression/SimpleConditionalExpressionMapper.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright MapStruct Authors.
+ *
+ * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+package org.mapstruct.ap.test.conditional.expression;
+
+import org.mapstruct.Mapper;
+import org.mapstruct.Mapping;
+import org.mapstruct.factory.Mappers;
+
+/**
+ * @author Filip Hrisafov
+ */
+@Mapper
+public interface SimpleConditionalExpressionMapper {
+
+ SimpleConditionalExpressionMapper INSTANCE = Mappers.getMapper( SimpleConditionalExpressionMapper.class );
+
+ @Mapping(target = "value", conditionExpression = "java(source.getValue() < 100)")
+ Target map(Source source);
+
+ class Source {
+ private final int value;
+
+ public Source(int value) {
+ this.value = value;
+ }
+
+ public int getValue() {
+ return value;
+ }
+ }
+
+ class Target {
+ private int value;
+
+ public int getValue() {
+ return value;
+ }
+
+ public void setValue(int value) {
+ this.value = value;
+ }
+ }
+}
diff --git a/processor/src/test/java/org/mapstruct/ap/test/conditional/qualifier/ConditionalMethodWithClassQualifiersMapper.java b/processor/src/test/java/org/mapstruct/ap/test/conditional/qualifier/ConditionalMethodWithClassQualifiersMapper.java
new file mode 100644
index 000000000..7cbef82a5
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/conditional/qualifier/ConditionalMethodWithClassQualifiersMapper.java
@@ -0,0 +1,63 @@
+/*
+ * 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.qualifier;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import org.mapstruct.Condition;
+import org.mapstruct.Mapper;
+import org.mapstruct.Mapping;
+import org.mapstruct.Named;
+import org.mapstruct.Qualifier;
+import org.mapstruct.ap.test.conditional.Employee;
+import org.mapstruct.ap.test.conditional.EmployeeDto;
+import org.mapstruct.factory.Mappers;
+
+/**
+ * @author Filip Hrisafov
+ */
+@Mapper(uses = ConditionalMethodWithClassQualifiersMapper.StaticUtil.class)
+public interface ConditionalMethodWithClassQualifiersMapper {
+
+ ConditionalMethodWithClassQualifiersMapper INSTANCE =
+ Mappers.getMapper( ConditionalMethodWithClassQualifiersMapper.class );
+
+ @Mapping(target = "ssid", source = "uniqueIdNumber",
+ conditionQualifiedBy = UtilConditions.class, conditionQualifiedByName = "american")
+ @Mapping(target = "nin", source = "uniqueIdNumber",
+ conditionQualifiedBy = UtilConditions.class, conditionQualifiedByName = "british")
+ Employee map(EmployeeDto employee);
+
+ @Condition
+ default boolean isNotBlank(String value) {
+ return value != null && !value.trim().isEmpty();
+ }
+
+ @UtilConditions
+ interface StaticUtil {
+
+ @Condition
+ @Named("american")
+ static boolean isAmericanCitizen(EmployeeDto employerDto) {
+ return "US".equals( employerDto.getCountry() );
+ }
+
+ @Condition
+ @Named("british")
+ static boolean isBritishCitizen(EmployeeDto employeeDto) {
+ return "UK".equals( employeeDto.getCountry() );
+ }
+ }
+
+ @Qualifier
+ @Target(ElementType.TYPE)
+ @Retention(RetentionPolicy.CLASS)
+ @interface UtilConditions {
+ }
+}
diff --git a/processor/src/test/java/org/mapstruct/ap/test/conditional/qualifier/ConditionalMethodWithSourceParameterMapper.java b/processor/src/test/java/org/mapstruct/ap/test/conditional/qualifier/ConditionalMethodWithSourceParameterMapper.java
new file mode 100644
index 000000000..c04269d57
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/conditional/qualifier/ConditionalMethodWithSourceParameterMapper.java
@@ -0,0 +1,48 @@
+/*
+ * 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.qualifier;
+
+import org.mapstruct.Condition;
+import org.mapstruct.Mapper;
+import org.mapstruct.Mapping;
+import org.mapstruct.Named;
+import org.mapstruct.ap.test.conditional.Employee;
+import org.mapstruct.ap.test.conditional.EmployeeDto;
+import org.mapstruct.factory.Mappers;
+
+/**
+ * @author Filip Hrisafov
+ */
+@Mapper(uses = ConditionalMethodWithSourceParameterMapper.StaticUtil.class)
+public interface ConditionalMethodWithSourceParameterMapper {
+
+ ConditionalMethodWithSourceParameterMapper INSTANCE =
+ Mappers.getMapper( ConditionalMethodWithSourceParameterMapper.class );
+
+ @Mapping(target = "ssid", source = "uniqueIdNumber", conditionQualifiedByName = "isAmericanCitizen")
+ @Mapping(target = "nin", source = "uniqueIdNumber", conditionQualifiedByName = "isBritishCitizen")
+ Employee map(EmployeeDto employee);
+
+ @Condition
+ default boolean isNotBlank(String value) {
+ return value != null && !value.trim().isEmpty();
+ }
+
+ @Condition
+ @Named("isAmericanCitizen")
+ default boolean isAmericanCitizen(EmployeeDto employerDto) {
+ return "US".equals( employerDto.getCountry() );
+ }
+
+ interface StaticUtil {
+
+ @Condition
+ @Named("isBritishCitizen")
+ static boolean isBritishCitizen(EmployeeDto employeeDto) {
+ return "UK".equals( employeeDto.getCountry() );
+ }
+ }
+}
diff --git a/processor/src/test/java/org/mapstruct/ap/test/conditional/qualifier/ConditionalQualifierTest.java b/processor/src/test/java/org/mapstruct/ap/test/conditional/qualifier/ConditionalQualifierTest.java
new file mode 100644
index 000000000..3dc6d7c8a
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/conditional/qualifier/ConditionalQualifierTest.java
@@ -0,0 +1,92 @@
+/*
+ * 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.qualifier;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mapstruct.ap.test.conditional.Employee;
+import org.mapstruct.ap.test.conditional.EmployeeDto;
+import org.mapstruct.ap.testutil.IssueKey;
+import org.mapstruct.ap.testutil.WithClasses;
+import org.mapstruct.ap.testutil.runner.AnnotationProcessorTestRunner;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @author Filip Hrisafov
+ */
+@IssueKey("2051")
+@WithClasses({
+ Employee.class,
+ EmployeeDto.class
+})
+@RunWith(AnnotationProcessorTestRunner.class)
+public class ConditionalQualifierTest {
+
+ @Test
+ @WithClasses({
+ ConditionalMethodWithSourceParameterMapper.class
+ })
+ public void conditionalMethodWithSourceParameter() {
+ ConditionalMethodWithSourceParameterMapper mapper = ConditionalMethodWithSourceParameterMapper.INSTANCE;
+
+ EmployeeDto dto = new EmployeeDto();
+ dto.setName( "Tester" );
+ dto.setUniqueIdNumber( "SSID-001" );
+ dto.setCountry( null );
+
+ Employee employee = mapper.map( dto );
+ assertThat( employee.getNin() ).isNull();
+ assertThat( employee.getSsid() ).isNull();
+
+ dto.setCountry( "UK" );
+ employee = mapper.map( dto );
+ assertThat( employee.getNin() ).isEqualTo( "SSID-001" );
+ assertThat( employee.getSsid() ).isNull();
+
+ dto.setCountry( "US" );
+ employee = mapper.map( dto );
+ assertThat( employee.getNin() ).isNull();
+ assertThat( employee.getSsid() ).isEqualTo( "SSID-001" );
+
+ dto.setCountry( "CH" );
+ employee = mapper.map( dto );
+ assertThat( employee.getNin() ).isNull();
+ assertThat( employee.getSsid() ).isNull();
+ }
+
+ @Test
+ @WithClasses({
+ ConditionalMethodWithClassQualifiersMapper.class
+ })
+ public void conditionalClassQualifiers() {
+ ConditionalMethodWithClassQualifiersMapper mapper = ConditionalMethodWithClassQualifiersMapper.INSTANCE;
+
+ EmployeeDto dto = new EmployeeDto();
+ dto.setName( "Tester" );
+ dto.setUniqueIdNumber( "SSID-001" );
+ dto.setCountry( null );
+
+ Employee employee = mapper.map( dto );
+ assertThat( employee.getNin() ).isNull();
+ assertThat( employee.getSsid() ).isNull();
+
+ dto.setCountry( "UK" );
+ employee = mapper.map( dto );
+ assertThat( employee.getNin() ).isEqualTo( "SSID-001" );
+ assertThat( employee.getSsid() ).isNull();
+
+ dto.setCountry( "US" );
+ employee = mapper.map( dto );
+ assertThat( employee.getNin() ).isNull();
+ assertThat( employee.getSsid() ).isEqualTo( "SSID-001" );
+
+ dto.setCountry( "CH" );
+ employee = mapper.map( dto );
+ assertThat( employee.getNin() ).isNull();
+ assertThat( employee.getSsid() ).isNull();
+ }
+}
diff --git a/processor/src/test/resources/fixtures/org/mapstruct/ap/test/conditional/basic/ConditionalMethodInMapperImpl.java b/processor/src/test/resources/fixtures/org/mapstruct/ap/test/conditional/basic/ConditionalMethodInMapperImpl.java
new file mode 100644
index 000000000..8f07ed89b
--- /dev/null
+++ b/processor/src/test/resources/fixtures/org/mapstruct/ap/test/conditional/basic/ConditionalMethodInMapperImpl.java
@@ -0,0 +1,31 @@
+/*
+ * 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 javax.annotation.processing.Generated;
+
+@Generated(
+ value = "org.mapstruct.ap.MappingProcessor",
+ date = "2021-04-19T21:10:40+0200",
+ comments = "version: , compiler: javac, environment: Java 11.0.9.1 (AdoptOpenJDK)"
+)
+public class ConditionalMethodInMapperImpl implements ConditionalMethodInMapper {
+
+ @Override
+ public BasicEmployee map(BasicEmployeeDto employee) {
+ if ( employee == null ) {
+ return null;
+ }
+
+ BasicEmployee basicEmployee = new BasicEmployee();
+
+ if ( isNotBlank( employee.getName() ) ) {
+ basicEmployee.setName( employee.getName() );
+ }
+
+ return basicEmployee;
+ }
+}
diff --git a/processor/src/test/resources/fixtures/org/mapstruct/ap/test/conditional/basic/ConditionalMethodWithSourceParameterAndValueMapperImpl.java b/processor/src/test/resources/fixtures/org/mapstruct/ap/test/conditional/basic/ConditionalMethodWithSourceParameterAndValueMapperImpl.java
new file mode 100644
index 000000000..ee629fb72
--- /dev/null
+++ b/processor/src/test/resources/fixtures/org/mapstruct/ap/test/conditional/basic/ConditionalMethodWithSourceParameterAndValueMapperImpl.java
@@ -0,0 +1,31 @@
+/*
+ * 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 javax.annotation.processing.Generated;
+
+@Generated(
+ value = "org.mapstruct.ap.MappingProcessor",
+ date = "2021-04-19T21:10:38+0200",
+ comments = "version: , compiler: javac, environment: Java 11.0.9.1 (AdoptOpenJDK)"
+)
+public class ConditionalMethodWithSourceParameterAndValueMapperImpl implements ConditionalMethodWithSourceParameterAndValueMapper {
+
+ @Override
+ public BasicEmployee map(BasicEmployeeDto employee) {
+ if ( employee == null ) {
+ return null;
+ }
+
+ BasicEmployee basicEmployee = new BasicEmployee();
+
+ if ( isPresent( employee, employee.getName() ) ) {
+ basicEmployee.setName( employee.getName() );
+ }
+
+ return basicEmployee;
+ }
+}