Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions core/src/main/java/org/mapstruct/Condition.java
Original file line number Diff line number Diff line change
@@ -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 {

}
68 changes: 68 additions & 0 deletions core/src/main/java/org/mapstruct/Mapping.java
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,74 @@
*/
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.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
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;
Expand Down Expand Up @@ -61,6 +62,7 @@
@GemDefinition(ValueMappings.class)
@GemDefinition(Context.class)
@GemDefinition(Builder.class)
@GemDefinition(Condition.class)

@GemDefinition(MappingControl.class)
@GemDefinition(MappingControls.class)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1155,6 +1155,7 @@ else if ( mapping.getJavaExpression() != null ) {
.dependsOn( mapping.getDependsOn() )
.defaultValue( mapping.getDefaultValue() )
.defaultJavaExpression( mapping.getDefaultJavaExpression() )
.conditionJavaExpression( mapping.getConditionJavaExpression() )
.mirror( mapping.getMirror() )
.options( mapping )
.build();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import org.mapstruct.ap.internal.model.common.ModelElement;
import org.mapstruct.ap.internal.model.common.Parameter;
import org.mapstruct.ap.internal.model.common.ParameterBinding;
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.builtin.BuiltInMethod;
Expand Down Expand Up @@ -197,7 +198,7 @@ public String getSourceReference() {
}

@Override
public String getSourcePresenceCheckerReference() {
public PresenceCheck getSourcePresenceCheckerReference() {
return assignment.getSourcePresenceCheckerReference();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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 );
}
}
Loading