Skip to content

Commit f91e3a4

Browse files
authored
Issue29 expose external conversions (#32)
* New attribute externalConversions for converters declared outside the scope of this spring config. * Documentation for new attribute
1 parent affbdc4 commit f91e3a4

File tree

14 files changed

+380
-113
lines changed

14 files changed

+380
-113
lines changed
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package org.mapstruct.extensions.spring;
2+
3+
import java.lang.annotation.ElementType;
4+
import java.lang.annotation.Retention;
5+
import java.lang.annotation.RetentionPolicy;
6+
import java.lang.annotation.Target;
7+
8+
/**
9+
* Allows the specification of a conversion that is available via the {@link
10+
* org.springframework.core.convert.ConversionService ConversionService}, but is <em>not</em>
11+
* declared as a MapStruct mapper within the scope of the {@link SpringMapperConfig}.
12+
*/
13+
@Target(ElementType.TYPE)
14+
@Retention(RetentionPolicy.SOURCE)
15+
public @interface ExternalConversion {
16+
Class<?> sourceType();
17+
18+
Class<?> targetType();
19+
}

annotations/src/main/java/org/mapstruct/extensions/spring/SpringMapperConfig.java

Lines changed: 40 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -6,43 +6,53 @@
66
import java.lang.annotation.Target;
77

88
/**
9-
* Marks a class or interface as configuration source for the Spring extension. There can be only <em>one</em>
10-
* annotated type in each compiled module.
9+
* Marks a class or interface as configuration source for the Spring extension. There can be only
10+
* <em>one</em> annotated type in each compiled module.
1111
*
1212
* @author Raimund Klein
1313
*/
1414
@Target(ElementType.TYPE)
1515
@Retention(RetentionPolicy.SOURCE)
1616
public @interface SpringMapperConfig {
17-
/**
18-
* The package name for the generated Adapter between the MapStruct mappers and Spring's
19-
* {@link org.springframework.core.convert.ConversionService}. If omitted or empty, the package name will be the
20-
* same as the one for the annotated type.
21-
*
22-
* @return The package name for the generated Adapter.
23-
*/
24-
String conversionServiceAdapterPackage() default "";
17+
/**
18+
* The package name for the generated Adapter between the MapStruct mappers and Spring's {@link
19+
* org.springframework.core.convert.ConversionService}. If omitted or empty, the package name will
20+
* be the same as the one for the annotated type.
21+
*
22+
* @return The package name for the generated Adapter.
23+
*/
24+
String conversionServiceAdapterPackage() default "";
2525

26-
/**
27-
* The class name for the generated Adapter between the MapStruct mappers and Spring's
28-
* {@link org.springframework.core.convert.ConversionService}.
29-
*
30-
* @return The class name for the generated Adapter.
31-
*/
32-
String conversionServiceAdapterClassName() default "ConversionServiceAdapter";
26+
/**
27+
* The class name for the generated Adapter between the MapStruct mappers and Spring's {@link
28+
* org.springframework.core.convert.ConversionService}.
29+
*
30+
* @return The class name for the generated Adapter.
31+
*/
32+
String conversionServiceAdapterClassName() default "ConversionServiceAdapter";
3333

34-
/**
35-
* The bean name for the Spring {@link org.springframework.core.convert.ConversionService} to use.
36-
*
37-
* @return The bean name for the Spring {@link org.springframework.core.convert.ConversionService}.
38-
*/
39-
String conversionServiceBeanName() default "";
34+
/**
35+
* The bean name for the Spring {@link org.springframework.core.convert.ConversionService} to use.
36+
*
37+
* @return The bean name for the Spring {@link
38+
* org.springframework.core.convert.ConversionService}.
39+
*/
40+
String conversionServiceBeanName() default "";
4041

41-
/**
42-
* To set if the Lazy annotation will be added to the
43-
* ConversionService's usage in the ConversionServiceAdapter. Defaults to true.
44-
*
45-
* @return true to add the Lazy annotation
46-
*/
47-
boolean lazyAnnotatedConversionServiceBean() default true;
42+
/**
43+
* To set if the Lazy annotation will be added to the ConversionService's usage in the
44+
* ConversionServiceAdapter. Defaults to true.
45+
*
46+
* @return true to add the Lazy annotation
47+
*/
48+
boolean lazyAnnotatedConversionServiceBean() default true;
49+
50+
/**
51+
* Additional {@link ExternalConversion conversions} which should be made available through the
52+
* generated Adapter.
53+
*
54+
* @return Additional {@link ExternalConversion conversions} which should be made available
55+
* through the generated Adapter.
56+
*/
57+
ExternalConversion[] externalConversions() default {};
4858
}

build.gradle

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,3 @@
1-
/*
2-
* This file was generated by the Gradle 'init' task.
3-
*
4-
* This generated file contains a sample Java Library project to get you started.
5-
* For more details take a look at the Java Libraries chapter in the Gradle
6-
* User Manual available at https://docs.gradle.org/5.6.2/userguide/java_library_plugin.html
7-
*/
8-
91
plugins {
102
id 'java-library'
113
id 'jacoco'
@@ -14,6 +6,7 @@ plugins {
146
id "com.github.ben-manes.versions" version "0.39.0"
157
id "org.springframework.boot" version "2.5.1" apply false
168
id "io.spring.dependency-management" version "1.0.11.RELEASE" apply false
9+
id "org.asciidoctor.jvm.convert" version "3.3.2" apply false
1710
}
1811

1912
def isNonStable = { String version ->

docs/build.gradle

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
plugins {
2-
id 'org.asciidoctor.jvm.convert' version '3.3.2'
3-
}
1+
apply plugin: "org.asciidoctor.jvm.convert"
42

53
asciidoctor {
64
baseDirFollowsSourceDir()

docs/src/docs/asciidoc/chapter-3-mapper-as-converter.asciidoc

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ This allows using the Mapper indirectly via the `ConversionService`:
3232
All this can be achieved already with MapStruct's core functionality. However, when a Mapper wants to https://mapstruct.org/documentation/stable/reference/html/#invoking-other-mappers[invoke] another one, it can't take the route via the `ConversionService`, because the latter's `convert` method does not match the signature that MapStruct expects for a mapping method. Thus, the developer still has to add every invoked Mapper to the invoking Mapper's `uses` element. This creates (aside from a potentially long list) a tight coupling between Mappers that the `ConversionService` wants to avoid.
3333

3434
This is where MapStruct Spring Extensions can help. Including the two artifacts in your build will generate an Adapter class that _can_ be used by an invoking Mapper. Let's say that the above CarMapper is accompanied by a SeatConfigurationMapper:
35+
3536
====
3637
[source, java, linenums]
3738
[subs="verbatim,attributes"]
@@ -118,4 +119,54 @@ import org.mapstruct.extensions.spring.SpringMapperConfig;
118119
public interface MapperSpringConfig {
119120
}
120121
----
122+
====
123+
[[externalConversions]]
124+
=== External Conversions
125+
Spring ships with a variety of https://github.com/spring-projects/spring-framework/tree/main/spring-core/src/main/java/org/springframework/core/convert/support[builtin conversions], e.g. `String` to `Locale` or `Object` to `Optional`. In order to use these (or your own conversions from another module) in the same fashion, you can add them as `externalConversions` to your `SpringMapperConfig`:
126+
====
127+
[source, java, linenums]
128+
[subs="verbatim,attributes"]
129+
----
130+
import org.mapstruct.MapperConfig;
131+
import org.mapstruct.extensions.spring.ExternalConversion;
132+
import org.mapstruct.extensions.spring.SpringMapperConfig;
133+
134+
import java.util.Locale;
135+
136+
@MapperConfig(componentModel = "spring")
137+
@SpringMapperConfig(
138+
externalConversions = @ExternalConversion(sourceType = String.class, targetType = Locale.class))
139+
public interface MapstructConfig {}
140+
----
141+
====
142+
143+
The processor will add the corresponding methods to the generated adapter so MapStruct can use them in the same fashion as the ones for the Converter Mappers in the same module:
144+
====
145+
[source, java, linenums]
146+
[subs="verbatim,attributes"]
147+
----
148+
import java.lang.String;
149+
import java.util.Locale;
150+
import javax.annotation.Generated;
151+
import org.springframework.context.annotation.Lazy;
152+
import org.springframework.core.convert.ConversionService;
153+
import org.springframework.stereotype.Component;
154+
155+
@Generated(
156+
value = "org.mapstruct.extensions.spring.converter.ConversionServiceAdapterGenerator",
157+
date = "2021-06-25T18:51:21.585Z"
158+
)
159+
@Component
160+
public class ConversionServiceAdapter {
161+
private final ConversionService conversionService;
162+
163+
public ConversionServiceAdapter(@Lazy final ConversionService conversionService) {
164+
this.conversionService = conversionService;
165+
}
166+
167+
public Locale mapStringToLocale(final String source) {
168+
return conversionService.convert(source, Locale.class);
169+
}
170+
}
171+
----
121172
====
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
dependencies {
2+
annotationProcessor project(":extensions")
3+
implementation projects.examples.model
4+
implementation projects.annotations
5+
6+
testImplementation libs.assertj
7+
testImplementation libs.bundles.junit.jupiter
8+
implementation libs.mapstruct.core
9+
annotationProcessor libs.mapstruct.processor
10+
implementation libs.spring.context
11+
implementation libs.spring.core
12+
testImplementation libs.spring.test
13+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package org.mapstruct.extensions.spring.example.externalconversions;
2+
3+
import java.util.Locale;
4+
5+
public class LocaleInfo {
6+
private Locale locale;
7+
8+
public Locale getLocale() {
9+
return locale;
10+
}
11+
12+
public void setLocale(Locale locale) {
13+
this.locale = locale;
14+
}
15+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package org.mapstruct.extensions.spring.example.externalconversions;
2+
3+
public class LocaleInfoDto {
4+
private String locale;
5+
6+
public String getLocale() {
7+
return locale;
8+
}
9+
10+
public void setLocale(String locale) {
11+
this.locale = locale;
12+
}
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package org.mapstruct.extensions.spring.example.externalconversions;
2+
3+
import org.mapstruct.Mapper;
4+
import org.springframework.core.convert.converter.Converter;
5+
6+
@Mapper(config = MapstructConfig.class, uses = ConversionServiceAdapter.class)
7+
public interface LocaleInfoDtoMapper extends Converter<LocaleInfoDto, LocaleInfo> {
8+
@Override
9+
LocaleInfo convert(LocaleInfoDto source);
10+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package org.mapstruct.extensions.spring.example.externalconversions;
2+
3+
import org.mapstruct.MapperConfig;
4+
import org.mapstruct.extensions.spring.ExternalConversion;
5+
import org.mapstruct.extensions.spring.SpringMapperConfig;
6+
7+
import java.util.Locale;
8+
9+
@MapperConfig(componentModel = "spring")
10+
@SpringMapperConfig(
11+
externalConversions = @ExternalConversion(sourceType = String.class, targetType = Locale.class))
12+
public interface MapstructConfig {}

0 commit comments

Comments
 (0)