Skip to content

Commit 5df6b7a

Browse files
authored
mapstruct#131, mapstruct#2438, mapstruct#366 Add support for Type-Refinement (Downcast) Mapping (mapstruct#2512)
Add new `@SubclassMapping` for creating Downcast mapping. When a parent mapping method is annotated with `@SubclassMapping` it will now generate an instanceof check inside the parent mapping and generate the subclass mappings if they are not manually defined. There is also `SubclassExhaustiveStrategy` for controlling what MapStruct should do in case the target type is abstract and there is no suitable way to create it.
1 parent f167e7a commit 5df6b7a

78 files changed

Lines changed: 2505 additions & 72 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.

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import org.mapstruct.control.MappingControl;
1515

1616
import static org.mapstruct.NullValueCheckStrategy.ON_IMPLICIT_CONVERSION;
17+
import static org.mapstruct.SubclassExhaustiveStrategy.COMPILE_ERROR;
1718

1819
/**
1920
* Configures the mapping between two bean types.
@@ -116,6 +117,17 @@ NullValuePropertyMappingStrategy nullValuePropertyMappingStrategy()
116117
*/
117118
NullValueCheckStrategy nullValueCheckStrategy() default ON_IMPLICIT_CONVERSION;
118119

120+
/**
121+
* Determines how to handle missing implementation for super classes when using the {@link SubclassMapping}.
122+
*
123+
* Overrides the setting on {@link MapperConfig} and {@link Mapper}.
124+
*
125+
* @return strategy to handle missing implementation combined with {@link SubclassMappings}.
126+
*
127+
* @since 1.5
128+
*/
129+
SubclassExhaustiveStrategy subclassExhaustiveStrategy() default COMPILE_ERROR;
130+
119131
/**
120132
* Default ignore all mappings. All mappings have to be defined manually. No automatic mapping will take place. No
121133
* warning will be issued on missing source or target properties.

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import org.mapstruct.factory.Mappers;
1616

1717
import static org.mapstruct.NullValueCheckStrategy.ON_IMPLICIT_CONVERSION;
18+
import static org.mapstruct.SubclassExhaustiveStrategy.COMPILE_ERROR;
1819

1920
/**
2021
* Marks an interface or abstract class as a mapper and activates the generation of a implementation of that type via
@@ -237,6 +238,17 @@ NullValuePropertyMappingStrategy nullValuePropertyMappingStrategy() default
237238
*/
238239
NullValueCheckStrategy nullValueCheckStrategy() default ON_IMPLICIT_CONVERSION;
239240

241+
/**
242+
* Determines how to handle missing implementation for super classes when using the {@link SubclassMapping}.
243+
*
244+
* Can be overridden by the one on {@link BeanMapping}, but overrides {@link MapperConfig}.
245+
*
246+
* @return strategy to handle missing implementation combined with {@link SubclassMappings}.
247+
*
248+
* @since 1.5
249+
*/
250+
SubclassExhaustiveStrategy subclassExhaustiveStrategy() default COMPILE_ERROR;
251+
240252
/**
241253
* Determines whether to use field or constructor injection. This is only used on annotated based component models
242254
* such as CDI, Spring and JSR 330.

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import org.mapstruct.factory.Mappers;
1616

1717
import static org.mapstruct.NullValueCheckStrategy.ON_IMPLICIT_CONVERSION;
18+
import static org.mapstruct.SubclassExhaustiveStrategy.COMPILE_ERROR;
1819

1920
/**
2021
* Marks a class or interface as configuration source for generated mappers. This allows to share common configurations
@@ -210,6 +211,17 @@ MappingInheritanceStrategy mappingInheritanceStrategy()
210211
*/
211212
NullValueCheckStrategy nullValueCheckStrategy() default ON_IMPLICIT_CONVERSION;
212213

214+
/**
215+
* Determines how to handle missing implementation for super classes when using the {@link SubclassMapping}.
216+
*
217+
* Can be overridden by the one on {@link BeanMapping} or {@link Mapper}.
218+
*
219+
* @return strategy to handle missing implementation combined with {@link SubclassMappings}.
220+
*
221+
* @since 1.5
222+
*/
223+
SubclassExhaustiveStrategy subclassExhaustiveStrategy() default COMPILE_ERROR;
224+
213225
/**
214226
* Determines whether to use field or constructor injection. This is only used on annotated based component models
215227
* such as CDI, Spring and JSR 330.
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
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+
/**
9+
* Strategy for dealing with subclassMapping annotated methods.
10+
*
11+
* @since 1.5
12+
* @author Ben Zegveld
13+
*/
14+
public enum SubclassExhaustiveStrategy {
15+
16+
/**
17+
* If there is no valid constructor or known method to create the return value of a with `@SubclassMapping`
18+
* annotated mapping then a compilation error will be thrown.
19+
*/
20+
COMPILE_ERROR,
21+
22+
/**
23+
* If there is no valid constructor or known method to create the return value of a with `@SubclassMapping`
24+
* annotated mapping then an {@link IllegalArgumentException} will be thrown if a call is made with a type for which
25+
* there is no {@link SubclassMapping} available.
26+
*/
27+
RUNTIME_EXCEPTION;
28+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
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.Repeatable;
10+
import java.lang.annotation.Retention;
11+
import java.lang.annotation.RetentionPolicy;
12+
import java.lang.annotation.Target;
13+
14+
import org.mapstruct.util.Experimental;
15+
16+
/**
17+
* Configures the mapping to handle hierarchy of the source type.
18+
* <p>
19+
* The subclass to be mapped is to be specified via {@link #source()}.
20+
* The subclass to map to is to be specified via {@link #target()}.
21+
* </p>
22+
* <p>
23+
* This annotation can be combined with &#64;Mapping annotations.
24+
* </p>
25+
*
26+
* <pre><code class='java'>
27+
* &#64;Mapper
28+
* public interface MyMapper {
29+
* &#64;SubclassMapping (target = TargetSubclass.class, source = SourceSubclass.class)
30+
* TargetParent mapParent(SourceParent parent);
31+
*
32+
* TargetSubclass mapSubclass(SourceSubclass subInstant);
33+
* }
34+
* </code></pre>
35+
* Below follow examples of the implementation for the mapParent method.
36+
* <strong>Example 1:</strong> For parents that cannot be created. (e.g. abstract classes or interfaces)
37+
* <pre><code class='java'>
38+
* // generates
39+
* &#64;Override
40+
* public TargetParent mapParent(SourceParent parent) {
41+
* if (parent instanceof SourceSubclass) {
42+
* return mapSubclass( (SourceSubclass) parent );
43+
* }
44+
* else {
45+
* throw new IllegalArgumentException("Not all subclasses are supported for this mapping. Missing for "
46+
* + parent.getClass());
47+
* }
48+
* }
49+
* </code></pre>
50+
* <strong>Example 2:</strong> For parents that can be created. (e.g. normal classes or interfaces with
51+
* &#64;Mappper( uses = ObjectFactory.class ) )
52+
* <pre><code class='java'>
53+
* // generates
54+
* &#64;Override
55+
* public TargetParent mapParent(SourceParent parent) {
56+
* TargetParent targetParent1;
57+
* if (parent instanceof SourceSubclass) {
58+
* targetParent1 = mapSubclass( (SourceSubclass) parent );
59+
* }
60+
* else {
61+
* targetParent1 = new TargetParent();
62+
* // ...
63+
* }
64+
* }
65+
* </code></pre>
66+
*
67+
* @author Ben Zegveld
68+
* @since 1.5
69+
*/
70+
@Repeatable(value = SubclassMappings.class)
71+
@Retention(RetentionPolicy.CLASS)
72+
@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE })
73+
@Experimental
74+
public @interface SubclassMapping {
75+
/**
76+
* @return the source subclass to check for before using the default mapping as fallback.
77+
*/
78+
Class<?> source();
79+
80+
/**
81+
* @return the target subclass to map the source to.
82+
*/
83+
Class<?> target();
84+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
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+
import org.mapstruct.util.Experimental;
14+
15+
/**
16+
* Configures the SubclassMappings of several subclasses.
17+
* <p>
18+
* <strong>TIP: When using java 8 or later, you can omit the @SubclassMappings
19+
* Wrapper annotation and directly specify several @SubclassMapping annotations
20+
* on one method.</strong>
21+
* </p>
22+
* <p>These two examples are equal.
23+
* </p>
24+
* <pre><code class='java'>
25+
* // before java 8
26+
* &#64;Mapper
27+
* public interface MyMapper {
28+
* &#64;SubclassMappings({
29+
* &#64;SubclassMapping(source = FirstSub.class, target = FirstTargetSub.class),
30+
* &#64;SubclassMapping(source = SecondSub.class, target = SecondTargetSub.class)
31+
* })
32+
* ParentTarget toParentTarget(Parent parent);
33+
* }
34+
* </code></pre>
35+
* <pre><code class='java'>
36+
* // java 8 and later
37+
* &#64;Mapper
38+
* public interface MyMapper {
39+
* &#64;SubclassMapping(source = First.class, target = FirstTargetSub.class),
40+
* &#64;SubclassMapping(source = SecondSub.class, target = SecondTargetSub.class)
41+
* ParentTarget toParentTarget(Parent parent);
42+
* }
43+
* </code></pre>
44+
*
45+
* @author Ben Zegveld
46+
* @since 1.5
47+
*/
48+
@Target(ElementType.METHOD)
49+
@Retention(RetentionPolicy.CLASS)
50+
@Experimental
51+
public @interface SubclassMappings {
52+
53+
/**
54+
* @return the subclassMappings to apply.
55+
*/
56+
SubclassMapping[] value();
57+
58+
}

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

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,49 @@ public interface SourceTargetMapper {
110110

111111
The example demonstrates how to use defaultExpression to set an `ID` field if the source field is null, this could be used to take the existing `sourceId` from the source object if it is set, or create a new `Id` if it isn't. Please note that the fully qualified package name is specified because MapStruct does not take care of the import of the `UUID` class (unless it’s used otherwise explicitly in the `SourceTargetMapper`). This can be resolved by defining imports on the @Mapper annotation (see <<expressions>>).
112112

113+
[[sub-class-mappings]]
114+
=== Subclass Mapping
115+
116+
When both input and result types have an inheritance relation, you would want the correct specialization be mapped to the matching specialization.
117+
Suppose an `Apple` and a `Banana`, which are both specializations of `Fruit`.
118+
119+
.Specifying the sub class mappings of a fruit mapping
120+
====
121+
[source, java, linenums]
122+
[subs="verbatim,attributes"]
123+
----
124+
@Mapper
125+
public interface FruitMapper {
126+
127+
@SubclassMapping( source = AppleDto.class, target = Apple.class )
128+
@SubclassMapping( source = BananaDto.class, target = Banana.class )
129+
Fruit map( FruitDto source );
130+
131+
}
132+
----
133+
====
134+
135+
If you would just use a normal mapping both the `AppleDto` and the `BananaDto` would be made into a `Fruit` object, instead of an `Apple` and a `Banana` object.
136+
By using the subclass mapping an `AppleDtoToApple` mapping will be used for `AppleDto` objects, and an `BananaDtoToBanana` mapping will be used for `BananaDto` objects.
137+
If you try to map a `GrapeDto` it would still turn it into a `Fruit`.
138+
139+
In the case that the `Fruit` is an abstract class or an interface, you would get a compile error.
140+
141+
To allow mappings for abstract classes or interfaces you need to set the `subclassExhaustiveStrategy` to `RUNTIME_EXCEPTION`, you can do this at the `@MapperConfig`, `@Mapper` or `@BeanMapping` annotations. If you then pass a `GrapeDto` an `IllegalArgumentException` will be thrown because it is unknown how to map a `GrapeDto`.
142+
Adding the missing (`@SubclassMapping`) for it will fix that.
143+
144+
[TIP]
145+
====
146+
If the mapping method for the subclasses does not exist it will be created and any other annotations on the fruit mapping method will be inherited by the newly generated mappings.
147+
====
148+
149+
[NOTE]
150+
====
151+
Combining `@SubclassMapping` with update methods is not supported.
152+
If you try to use subclass mappings there will be a compile error.
153+
The same issue exists for the `@Context` and `@TargetType` parameters.
154+
====
155+
113156
[[determining-result-type]]
114157
=== Determining the result type
115158

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828
import org.mapstruct.Named;
2929
import org.mapstruct.ObjectFactory;
3030
import org.mapstruct.Qualifier;
31+
import org.mapstruct.SubclassMapping;
32+
import org.mapstruct.SubclassMappings;
3133
import org.mapstruct.TargetType;
3234
import org.mapstruct.ValueMapping;
3335
import org.mapstruct.ValueMappings;
@@ -47,6 +49,8 @@
4749
@GemDefinition(BeanMapping.class)
4850
@GemDefinition(EnumMapping.class)
4951
@GemDefinition(MapMapping.class)
52+
@GemDefinition(SubclassMapping.class)
53+
@GemDefinition(SubclassMappings.class)
5054
@GemDefinition(TargetType.class)
5155
@GemDefinition(MappingTarget.class)
5256
@GemDefinition(DecoratedWith.class)
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
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.gem;
7+
8+
/**
9+
* Gem for the enum {@link org.mapstruct.SubclassExhaustiveStrategy}
10+
*
11+
* @author Ben Zegveld
12+
*/
13+
public enum SubclassExhaustiveStrategyGem {
14+
15+
COMPILE_ERROR( false ),
16+
RUNTIME_EXCEPTION( true );
17+
18+
private final boolean abstractReturnTypeAllowed;
19+
20+
SubclassExhaustiveStrategyGem(boolean abstractReturnTypeAllowed) {
21+
this.abstractReturnTypeAllowed = abstractReturnTypeAllowed;
22+
}
23+
24+
public boolean isAbstractReturnTypeAllowed() {
25+
return abstractReturnTypeAllowed;
26+
}
27+
}

0 commit comments

Comments
 (0)