#2051, #2084 Add new @Condition annotation for custom presence check methods

This commit is contained in:
Filip Hrisafov 2021-04-04 19:36:46 +02:00
parent a2e1404b93
commit 51cdbd67e3
49 changed files with 2015 additions and 42 deletions

View File

@ -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 <em>presence check method</em> to check check for presence in beans.
* <p>
* By default bean properties are checked against {@code null} or using a presence check method in the source bean.
* If a presence check method is available then it will be used instead.
* <p>
* Presence check methods have to return {@code boolean}.
* The following parameters are accepted for the presence check methods:
* <ul>
* <li>The parameter with the value of the source property.
* e.g. the value given by calling {@code getName()} for the name property of the source bean</li>
* <li>The mapping source parameter</li>
* <li>{@code @}{@link Context} parameter</li>
* </ul>
*
* <strong>Note:</strong> The usage of this annotation is <em>mandatory</em>
* for a method to be considered as a presence check method.
*
* <pre><code>
* public class PresenceCheckUtils {
*
* &#64;Condition
* public static boolean isNotEmpty(String value) {
* return value != null &#38;&#38; !value.isEmpty();
* }
* }
*
* &#64;Mapper(uses = PresenceCheckUtils.class)
* public interface MovieMapper {
*
* MovieDto map(Movie movie);
* }
* </code></pre>
*
* The following implementation of {@code MovieMapper} will be generated:
*
* <pre>
* <code>
* public class MovieMapperImpl implements MovieMapper {
*
* &#64;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;
* }
* }
* </code>
* </pre>
*
* @author Filip Hrisafov
* @since 1.5
*/
@Target({ ElementType.METHOD })
@Retention(RetentionPolicy.CLASS)
public @interface Condition {
}

View File

@ -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.
* <p>
* 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.
* </p>
*
*
* @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.
* <p>
* Currently, Java is the only supported "expression language" and expressions must be given in form of Java
* expressions using the following format: {@code java(<EXPRESSION>)}. For instance the mapping:
* <pre><code>
* &#64;Mapping(
* target = "someProp",
* conditionExpression = "java(s.getAge() &#60; 18)"
* )
* </code></pre>
* <p>
* will cause the following target property assignment to be generated:
* <pre><code>
* if (s.getAge() &#60; 18) {
* targetBean.setSomeProp( s.getSomeProp() );
* }
* </code></pre>
* <p>
* <p>
* Any types referenced in expressions must be given via their fully-qualified name. Alternatively, types can be
* imported via {@link Mapper#imports()}.
* <p>
* 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.
*

View File

@ -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 <<source-presence-check>>.
The difference is that it allows users to write custom condition methods that will be invoked to check if a property needs to be mapped or not.
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.
====
<<selection-based-on-qualifiers>> is also valid for `@Condition` methods.
In order to use a more specific condition method you will need to use one of `Mapping#conditionQualifiedByName` or `Mapping#conditionQualifiedBy`.
[[exceptions]]
=== Exceptions

View File

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

View File

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

View File

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

View File

@ -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<SourceMethod> matchingMethod = findMatchingPresenceCheckMethod(
method,
selectionParameters,
ctx
);
if ( matchingMethod == null ) {
return null;
}
MethodReference methodReference = getPresenceCheckMethodReference( method, matchingMethod, ctx );
return new MethodReferencePresenceCheck( methodReference );
}
private static SelectedMethod<SourceMethod> findMatchingPresenceCheckMethod(
Method method,
SelectionParameters selectionParameters,
MappingBuilderContext ctx
) {
MethodSelectors selectors = new MethodSelectors(
ctx.getTypeUtils(),
ctx.getElementUtils(),
ctx.getTypeFactory(),
ctx.getMessager()
);
Type booleanType = ctx.getTypeFactory().getType( Boolean.class );
List<SelectedMethod<SourceMethod>> 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<SourceMethod> 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<SourceMethod> getAllAvailableMethods(Method method, List<SourceMethod> sourceModelMethods) {
ParameterProvidedMethods contextProvidedMethods = method.getContextProvidedMethods();
if ( contextProvidedMethods.isEmpty() ) {
return sourceModelMethods;
}
List<SourceMethod> methodsProvidedByParams = contextProvidedMethods
.getAllProvidedMethodsInParameterOrder( method.getContextParameters() );
List<SourceMethod> 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;
}
}

View File

@ -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(),
SourceRHS sourceRHS = new SourceRHS(
sourceParam.getName(),
sourceRef,
getSourcePresenceCheckerRef( sourceReference ),
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();

View File

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

View File

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

View File

@ -0,0 +1,52 @@
/*
* Copyright MapStruct Authors.
*
* Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
*/
package org.mapstruct.ap.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<Type> 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 );
}
}

View File

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

View File

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

View File

@ -23,6 +23,8 @@ public class SelectionParameters {
private final List<TypeMirror> qualifiers;
private final List<String> qualifyingNames;
private final List<TypeMirror> conditionQualifiers;
private final List<String> 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<TypeMirror> qualifiers, List<String> qualifyingNames, TypeMirror resultType,
TypeUtils typeUtils) {
this( qualifiers, qualifyingNames, resultType, typeUtils, null );
this(
qualifiers,
qualifyingNames,
Collections.emptyList(),
Collections.emptyList(),
resultType,
typeUtils,
null
);
}
private SelectionParameters(List<TypeMirror> qualifiers, List<String> qualifyingNames, TypeMirror resultType,
public SelectionParameters(List<TypeMirror> qualifiers, List<String> qualifyingNames,
List<TypeMirror> conditionQualifiers, List<String> conditionQualifyingNames,
TypeMirror resultType,
TypeUtils typeUtils) {
this( qualifiers, qualifyingNames, conditionQualifiers, conditionQualifyingNames, resultType, typeUtils, null );
}
private SelectionParameters(List<TypeMirror> qualifiers, List<String> qualifyingNames,
List<TypeMirror> conditionQualifiers, List<String> 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<TypeMirror> getConditionQualifiers() {
return conditionQualifiers;
}
/**
* @return qualifyingNames, used in combination with with @Named
* @see #getConditionQualifiers()
*/
public List<String> 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,

View File

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

View File

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

View File

@ -30,7 +30,9 @@ public class MethodFamilySelector implements MethodSelector {
List<SelectedMethod<T>> result = new ArrayList<>( methods.size() );
for ( SelectedMethod<T> 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 );
}

View File

@ -35,6 +35,7 @@ public class MethodSelectors {
new XmlElementDeclSelector( typeUtils ),
new InheritanceSelector(),
new CreateOrUpdateSelector(),
new SourceRhsSelector(),
new FactoryParameterSelector() );
}

View File

@ -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 ) {
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<TypeMirror> 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,
}
}

View File

@ -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 <T extends Method> List<SelectedMethod<T>> getMatchingMethods(Method mappingMethod,
List<SelectedMethod<T>> candidates,
List<Type> sourceTypes, Type mappingTargetType,
Type returnType, SelectionCriteria criteria) {
if ( candidates.size() < 2 || criteria.getSourceRHS() == null ) {
return candidates;
}
List<SelectedMethod<T>> sourceRHSFavoringCandidates = new ArrayList<>();
for ( SelectedMethod<T> candidate : candidates ) {
for ( ParameterBinding parameterBinding : candidate.getParameterBindings() ) {
if ( parameterBinding.getSourceRHS() != null ) {
sourceRHSFavoringCandidates.add( candidate );
break;
}
}
}
if ( !sourceRHSFavoringCandidates.isEmpty() ) {
return sourceRHSFavoringCandidates;
}
return candidates;
}
}

View File

@ -89,7 +89,7 @@ public class TypeSelector implements MethodSelector {
List<ParameterBinding> 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 {

View File

@ -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<Void, Lis
}
// otherwise add reference to existing mapper method
else if ( isValidReferencedMethod( parameters ) || isValidFactoryMethod( method, parameters, returnType )
|| isValidLifecycleCallbackMethod( method ) ) {
|| isValidLifecycleCallbackMethod( method )
|| isValidPresenceCheckMethod( method, returnType ) ) {
return getReferencedMethod( usedMapper, methodType, method, mapperToImplement, parameters );
}
else {
@ -333,7 +335,8 @@ public class MethodRetrievalProcessor implements ModelElementProcessor<Void, Lis
List<SourceMethod> 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<Void, Lis
return ObjectFactoryGem.instanceOn( method ) != null;
}
private boolean isValidPresenceCheckMethod(ExecutableElement method, Type returnType) {
return isBoolean( returnType ) && hasConditionAnnotation( method );
}
private boolean hasConditionAnnotation(ExecutableElement method) {
return ConditionGem.instanceOn( method ) != null;
}
private boolean isVoid(Type returnType) {
return returnType.getTypeMirror().getKind() == TypeKind.VOID;
}
private boolean isBoolean(Type returnType) {
return Boolean.class.getCanonicalName().equals( returnType.getBoxedEquivalent().getFullyQualifiedName() );
}
private boolean isValidReferencedOrFactoryMethod(int sourceParamCount, int targetParamCount,
List<Parameter> parameters) {
int validSourceParameters = 0;

View File

@ -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(<EXPRESSION>)\"." ),
PROPERTYMAPPING_INVALID_DEFAULT_EXPRESSION( "Value for default expression must be given in the form \"java(<EXPRESSION>)\"." ),
PROPERTYMAPPING_INVALID_CONDITION_EXPRESSION( "Value for condition expression must be given in the form \"java(<EXPRESSION>)\"." ),
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" ),

View File

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

View File

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

View File

@ -0,0 +1,36 @@
/*
* Copyright MapStruct Authors.
*
* Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
*/
package org.mapstruct.ap.test.conditional;
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;
}
}

View File

@ -0,0 +1,36 @@
/*
* Copyright MapStruct Authors.
*
* Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
*/
package org.mapstruct.ap.test.conditional;
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;
}
}

View File

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

View File

@ -0,0 +1,32 @@
/*
* Copyright MapStruct Authors.
*
* Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
*/
package org.mapstruct.ap.test.conditional.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;
}
}

View File

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

View File

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

View File

@ -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 <T> boolean isNotEmpty(Collection<T> collection) {
return collection != null && !collection.isEmpty();
}
class Author {
private List<Book> books;
public List<Book> getBooks() {
return books;
}
public boolean hasBooks() {
return false;
}
public void setBooks(List<Book> 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<BookDto> books;
public List<BookDto> getBooks() {
return books;
}
public void setBooks(List<BookDto> books) {
this.books = books;
}
}
class BookDto {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
}

View File

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

View File

@ -0,0 +1,26 @@
/*
* Copyright MapStruct Authors.
*
* Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
*/
package org.mapstruct.ap.test.conditional.basic;
import org.mapstruct.Condition;
import org.mapstruct.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();
}
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,28 @@
/*
* Copyright MapStruct Authors.
*
* Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
*/
package org.mapstruct.ap.test.conditional.basic;
import org.mapstruct.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() );
}
}

View File

@ -0,0 +1,28 @@
/*
* Copyright MapStruct Authors.
*
* Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
*/
package org.mapstruct.ap.test.conditional.basic;
import org.mapstruct.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();
}
}

View File

@ -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> T map(Nullable<T> nullable) {
return nullable.value;
}
@Condition
default <T> boolean isPresent(Nullable<T> nullable) {
return nullable.isPresent();
}
class Nullable<T> {
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 <T> Nullable<T> undefined() {
return new Nullable<>( null, false );
}
public static <T> Nullable<T> ofNullable(T value) {
return new Nullable<>( value, true );
}
}
class Source {
protected final Nullable<String> value;
public Source(Nullable<String> value) {
this.value = value;
}
public Nullable<String> getValue() {
return value;
}
}
class Target {
protected String value = "initial";
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
}
}

View File

@ -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(<EXPRESSION>)\"."
)
}
)
@WithClasses({
BasicEmployee.class,
ErroneousConditionExpressionMapper.class
} )
public void invalidJavaConditionExpression() {
}
}

View File

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

View File

@ -0,0 +1,21 @@
/*
* Copyright MapStruct Authors.
*
* Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
*/
package org.mapstruct.ap.test.conditional.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);
}

View File

@ -0,0 +1,46 @@
/*
* Copyright MapStruct Authors.
*
* Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
*/
package org.mapstruct.ap.test.conditional.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;
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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