Skip to content

Commit 51cdbd6

Browse files
committed
mapstruct#2051, mapstruct#2084 Add new @Condition annotation for custom presence check methods
1 parent a2e1404 commit 51cdbd6

49 files changed

Lines changed: 2015 additions & 42 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/*
2+
* Copyright MapStruct Authors.
3+
*
4+
* Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
5+
*/
6+
package org.mapstruct;
7+
8+
import java.lang.annotation.ElementType;
9+
import java.lang.annotation.Retention;
10+
import java.lang.annotation.RetentionPolicy;
11+
import java.lang.annotation.Target;
12+
13+
/**
14+
* This annotation marks a method as a <em>presence check method</em> to check check for presence in beans.
15+
* <p>
16+
* By default bean properties are checked against {@code null} or using a presence check method in the source bean.
17+
* If a presence check method is available then it will be used instead.
18+
* <p>
19+
* Presence check methods have to return {@code boolean}.
20+
* The following parameters are accepted for the presence check methods:
21+
* <ul>
22+
* <li>The parameter with the value of the source property.
23+
* e.g. the value given by calling {@code getName()} for the name property of the source bean</li>
24+
* <li>The mapping source parameter</li>
25+
* <li>{@code @}{@link Context} parameter</li>
26+
* </ul>
27+
*
28+
* <strong>Note:</strong> The usage of this annotation is <em>mandatory</em>
29+
* for a method to be considered as a presence check method.
30+
*
31+
* <pre><code>
32+
* public class PresenceCheckUtils {
33+
*
34+
* &#64;Condition
35+
* public static boolean isNotEmpty(String value) {
36+
* return value != null &#38;&#38; !value.isEmpty();
37+
* }
38+
* }
39+
*
40+
* &#64;Mapper(uses = PresenceCheckUtils.class)
41+
* public interface MovieMapper {
42+
*
43+
* MovieDto map(Movie movie);
44+
* }
45+
* </code></pre>
46+
*
47+
* The following implementation of {@code MovieMapper} will be generated:
48+
*
49+
* <pre>
50+
* <code>
51+
* public class MovieMapperImpl implements MovieMapper {
52+
*
53+
* &#64;Override
54+
* public MovieDto map(Movie movie) {
55+
* if ( movie == null ) {
56+
* return null;
57+
* }
58+
*
59+
* MovieDto movieDto = new MovieDto();
60+
*
61+
* if ( PresenceCheckUtils.isNotEmpty( movie.getTitle() ) ) {
62+
* movieDto.setTitle( movie.getTitle() );
63+
* }
64+
*
65+
* return movieDto;
66+
* }
67+
* }
68+
* </code>
69+
* </pre>
70+
*
71+
* @author Filip Hrisafov
72+
* @since 1.5
73+
*/
74+
@Target({ ElementType.METHOD })
75+
@Retention(RetentionPolicy.CLASS)
76+
public @interface Condition {
77+
78+
}

core/src/main/java/org/mapstruct/Mapping.java

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,74 @@
316316
*/
317317
String[] qualifiedByName() default { };
318318

319+
/**
320+
* A qualifier can be specified to aid the selection process of a suitable presence check method.
321+
* This is useful in case multiple presence check methods qualify and thus would result in an
322+
* 'Ambiguous presence check methods found' error.
323+
* A qualifier is a custom annotation and can be placed on a hand written mapper class or a method.
324+
* This is similar to the {@link #qualifiedBy()}, but it is only applied for {@link Condition} methods.
325+
*
326+
* @return the qualifiers
327+
* @see Qualifier
328+
* @see #qualifiedBy()
329+
* @since 1.5
330+
*/
331+
Class<? extends Annotation>[] conditionQualifiedBy() default { };
332+
333+
/**
334+
* String-based form of qualifiers for condition / presence check methods;
335+
* When looking for a suitable presence check method for a given property, MapStruct will
336+
* only consider those methods carrying directly or indirectly (i.e. on the class-level) a {@link Named} annotation
337+
* for each of the specified qualifier names.
338+
*
339+
* This is similar like {@link #qualifiedByName()} but it is only applied for {@link Condition} methods.
340+
* <p>
341+
* Note that annotation-based qualifiers are generally preferable as they allow more easily to find references and
342+
* are safe for refactorings, but name-based qualifiers can be a less verbose alternative when requiring a large
343+
* number of qualifiers as no custom annotation types are needed.
344+
* </p>
345+
*
346+
*
347+
* @return One or more qualifier name(s)
348+
* @see #conditionQualifiedBy()
349+
* @see #qualifiedByName()
350+
* @see Named
351+
* @since 1.5
352+
*/
353+
String[] conditionQualifiedByName() default { };
354+
355+
/**
356+
* A conditionExpression {@link String} based on which the specified property is to be checked
357+
* whether it is present or not.
358+
* <p>
359+
* Currently, Java is the only supported "expression language" and expressions must be given in form of Java
360+
* expressions using the following format: {@code java(<EXPRESSION>)}. For instance the mapping:
361+
* <pre><code>
362+
* &#64;Mapping(
363+
* target = "someProp",
364+
* conditionExpression = "java(s.getAge() &#60; 18)"
365+
* )
366+
* </code></pre>
367+
* <p>
368+
* will cause the following target property assignment to be generated:
369+
* <pre><code>
370+
* if (s.getAge() &#60; 18) {
371+
* targetBean.setSomeProp( s.getSomeProp() );
372+
* }
373+
* </code></pre>
374+
* <p>
375+
* <p>
376+
* Any types referenced in expressions must be given via their fully-qualified name. Alternatively, types can be
377+
* imported via {@link Mapper#imports()}.
378+
* <p>
379+
* This attribute can not be used together with {@link #expression()} or {@link #constant()}.
380+
*
381+
* @return An expression specifying a condition check for the designated property
382+
*
383+
* @since 1.5
384+
*/
385+
String conditionExpression() default "";
386+
319387
/**
320388
* Specifies the result type of the mapping method to be used in case multiple mapping methods qualify.
321389
*

documentation/src/main/asciidoc/chapter-10-advanced-mapping-options.asciidoc

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,78 @@ The source presence checker name can be changed in the MapStruct service provide
229229
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
230230
null check, regardless the value of the `NullValueCheckStrategy` to avoid addition of `null` to the target collection or map.
231231
====
232+
233+
[[conditional-mapping]]
234+
=== Conditional Mapping
235+
236+
Conditional Mapping is a type of <<source-presence-check>>.
237+
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.
238+
239+
A custom condition method is a method that is annotated with `org.mapstruct.Condition` and returns `boolean`.
240+
241+
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:
242+
243+
.Mapper using custom condition check method
244+
====
245+
[source, java, linenums]
246+
[subs="verbatim,attributes"]
247+
----
248+
@Mapper
249+
public interface CarMapper {
250+
251+
CarDto carToCarDto(Car car);
252+
253+
@Condition
254+
default boolean isNotEmpty(String value) {
255+
return value != null && !value.isEmpty();
256+
}
257+
}
258+
----
259+
====
260+
261+
The generated mapper will look like:
262+
263+
.try-catch block in generated implementation
264+
====
265+
[source, java, linenums]
266+
[subs="verbatim,attributes"]
267+
----
268+
// GENERATED CODE
269+
public class CarMapperImpl implements CarMapper {
270+
271+
@Override
272+
public CarDto carToCarDto(Car car) {
273+
if ( car == null ) {
274+
return null;
275+
}
276+
277+
CarDto carDto = new CarDto();
278+
279+
if ( isNotEmpty( car.getOwner() ) ) {
280+
carDto.setOwner( car.getOwner() );
281+
}
282+
283+
// Mapping of other properties
284+
285+
return carDto;
286+
}
287+
}
288+
----
289+
====
290+
291+
[IMPORTANT]
292+
====
293+
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.
294+
====
295+
296+
[NOTE]
297+
====
298+
Methods annotated with `@Condition` in addition to the value of the source property can also have the source parameter as an input.
299+
====
300+
301+
<<selection-based-on-qualifiers>> is also valid for `@Condition` methods.
302+
In order to use a more specific condition method you will need to use one of `Mapping#conditionQualifiedByName` or `Mapping#conditionQualifiedBy`.
303+
232304
[[exceptions]]
233305
=== Exceptions
234306

processor/src/main/java/org/mapstruct/ap/internal/gem/GemGenerator.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import org.mapstruct.BeanMapping;
1313
import org.mapstruct.BeforeMapping;
1414
import org.mapstruct.Builder;
15+
import org.mapstruct.Condition;
1516
import org.mapstruct.Context;
1617
import org.mapstruct.DecoratedWith;
1718
import org.mapstruct.EnumMapping;
@@ -61,6 +62,7 @@
6162
@GemDefinition(ValueMappings.class)
6263
@GemDefinition(Context.class)
6364
@GemDefinition(Builder.class)
65+
@GemDefinition(Condition.class)
6466

6567
@GemDefinition(MappingControl.class)
6668
@GemDefinition(MappingControls.class)

processor/src/main/java/org/mapstruct/ap/internal/model/BeanMappingMethod.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1155,6 +1155,7 @@ else if ( mapping.getJavaExpression() != null ) {
11551155
.dependsOn( mapping.getDependsOn() )
11561156
.defaultValue( mapping.getDefaultValue() )
11571157
.defaultJavaExpression( mapping.getDefaultJavaExpression() )
1158+
.conditionJavaExpression( mapping.getConditionJavaExpression() )
11581159
.mirror( mapping.getMirror() )
11591160
.options( mapping )
11601161
.build();
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*
2+
* Copyright MapStruct Authors.
3+
*
4+
* Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
5+
*/
6+
package org.mapstruct.ap.internal.model;
7+
8+
import java.util.Objects;
9+
import java.util.Set;
10+
11+
import org.mapstruct.ap.internal.model.common.ModelElement;
12+
import org.mapstruct.ap.internal.model.common.PresenceCheck;
13+
import org.mapstruct.ap.internal.model.common.Type;
14+
15+
/**
16+
* @author Filip Hrisafov
17+
*/
18+
public class MethodReferencePresenceCheck extends ModelElement implements PresenceCheck {
19+
20+
protected final MethodReference methodReference;
21+
22+
public MethodReferencePresenceCheck(MethodReference methodReference) {
23+
this.methodReference = methodReference;
24+
}
25+
26+
@Override
27+
public Set<Type> getImportTypes() {
28+
return methodReference.getImportTypes();
29+
}
30+
31+
public MethodReference getMethodReference() {
32+
return methodReference;
33+
}
34+
35+
@Override
36+
public boolean equals(Object o) {
37+
if ( this == o ) {
38+
return true;
39+
}
40+
if ( o == null || getClass() != o.getClass() ) {
41+
return false;
42+
}
43+
MethodReferencePresenceCheck that = (MethodReferencePresenceCheck) o;
44+
return Objects.equals( methodReference, that.methodReference );
45+
}
46+
47+
@Override
48+
public int hashCode() {
49+
return Objects.hash( methodReference );
50+
}
51+
}

0 commit comments

Comments
 (0)