Skip to content

Commit 02a33e2

Browse files
authored
65 map lists (#73)
* Fixes 65
1 parent e14522e commit 02a33e2

File tree

16 files changed

+318
-117
lines changed

16 files changed

+318
-117
lines changed

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ This allows using the Mapper indirectly via the `ConversionService`:
2929
----
3030
====
3131

32-
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.
32+
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` is designed 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:
3535

@@ -61,12 +61,12 @@ public class ConversionServiceAdapter {
6161
}
6262
6363
public CarDto mapCarToCarDto(final Car source) {
64-
return conversionService.convert(source, CarDto.class);
64+
return (CarDto) conversionService.convert(source, TypeDescriptor.valueOf(Car.class), TypeDescriptor.valueOf(CarDto.class));
6565
}
6666
6767
public SeatConfigurationDto mapSeatConfigurationToSeatConfigurationDto(
6868
final SeatConfiguration source) {
69-
return conversionService.convert(source, SeatConfigurationDto.class);
69+
return (SeatConfigurationDto) conversionService.convert(source, TypeDescriptor.valueOf(SeatConfiguration.class), TypeDescriptor.valueOf(SeatConfigurationDto.class));
7070
}
7171
}
7272
----
@@ -165,7 +165,7 @@ public class ConversionServiceAdapter {
165165
}
166166
167167
public Locale mapStringToLocale(final String source) {
168-
return conversionService.convert(source, Locale.class);
168+
return (Locale) conversionService.convert(source, TypeDescriptor.valueOf(String.class), TypeDescriptor.valueOf(Locale.class));
169169
}
170170
}
171171
----

examples/arrays/src/test/java/org/mapstruct/extensions/spring/converter/ConversionServiceAdapterTest.java

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
package org.mapstruct.extensions.spring.converter;
22

3+
import static org.mockito.BDDMockito.then;
4+
import static org.mockito.Mockito.mock;
5+
6+
import java.sql.Blob;
37
import org.junit.jupiter.api.Test;
48
import org.junit.jupiter.api.extension.ExtendWith;
59
import org.mockito.InjectMocks;
610
import org.mockito.Mock;
711
import org.mockito.junit.jupiter.MockitoExtension;
812
import org.springframework.core.convert.ConversionService;
9-
10-
import java.sql.Blob;
11-
12-
import static org.mockito.BDDMockito.then;
13-
import static org.mockito.Mockito.mock;
13+
import org.springframework.core.convert.TypeDescriptor;
1414

1515
@ExtendWith(MockitoExtension.class)
1616
class ConversionServiceAdapterTest {
@@ -29,6 +29,6 @@ void shouldMapViaConversionServiceInGeneratedMethod() {
2929
conversionServiceAdapter.mapBlobToArrayOfbyte(blob);
3030

3131
// Then
32-
then(conversionService).should().convert(blob, byte[].class);
32+
then(conversionService).should().convert(blob, TypeDescriptor.valueOf(Blob.class), TypeDescriptor.valueOf(byte[].class));
3333
}
3434
}

examples/classname/src/test/java/org/mapstruct/extensions/spring/example/ConversionServiceAdapterIntegrationTest.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import org.springframework.beans.factory.annotation.Autowired;
88
import org.springframework.context.annotation.Bean;
99
import org.springframework.context.annotation.ComponentScan;
10+
import org.springframework.core.convert.TypeDescriptor;
1011
import org.springframework.core.convert.support.ConfigurableConversionService;
1112
import org.springframework.core.convert.support.DefaultConversionService;
1213
import org.springframework.stereotype.Component;
@@ -64,6 +65,10 @@ void shouldKnowAllMappers() {
6465
then(conversionService.canConvert(Wheel.class, WheelDto.class)).isTrue();
6566
then(conversionService.canConvert(Wheels.class, List.class)).isTrue();
6667
then(conversionService.canConvert(List.class, Wheels.class)).isTrue();
68+
then(conversionService.canConvert(
69+
TypeDescriptor.collection(List.class, TypeDescriptor.valueOf(WheelDto.class)),
70+
TypeDescriptor.valueOf((Wheels.class))))
71+
.isTrue();
6772
}
6873

6974
@Test

examples/custom-conversion-service-bean/src/test/java/org/mapstruct/extensions/spring/example/ConversionServiceAdapterIntegrationTest.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import org.springframework.beans.factory.annotation.Qualifier;
99
import org.springframework.context.annotation.Bean;
1010
import org.springframework.context.annotation.ComponentScan;
11+
import org.springframework.core.convert.TypeDescriptor;
1112
import org.springframework.core.convert.support.ConfigurableConversionService;
1213
import org.springframework.core.convert.support.DefaultConversionService;
1314
import org.springframework.stereotype.Component;
@@ -73,6 +74,10 @@ void shouldKnowAllMappers() {
7374
then(conversionService.canConvert(Wheel.class, WheelDto.class)).isTrue();
7475
then(conversionService.canConvert(Wheels.class, List.class)).isTrue();
7576
then(conversionService.canConvert(List.class, Wheels.class)).isTrue();
77+
then(conversionService.canConvert(
78+
TypeDescriptor.collection(List.class, TypeDescriptor.valueOf(WheelDto.class)),
79+
TypeDescriptor.valueOf((Wheels.class))))
80+
.isTrue();
7681
}
7782

7883
@Test

examples/noconfig/src/test/java/org/mapstruct/extensions/spring/example/ConversionServiceAdapterIntegrationTest.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import org.springframework.beans.factory.annotation.Autowired;
88
import org.springframework.context.annotation.Bean;
99
import org.springframework.context.annotation.ComponentScan;
10+
import org.springframework.core.convert.TypeDescriptor;
1011
import org.springframework.core.convert.support.ConfigurableConversionService;
1112
import org.springframework.core.convert.support.DefaultConversionService;
1213
import org.springframework.stereotype.Component;
@@ -64,6 +65,10 @@ void shouldKnowAllMappers() {
6465
then(conversionService.canConvert(Wheel.class, WheelDto.class)).isTrue();
6566
then(conversionService.canConvert(Wheels.class, List.class)).isTrue();
6667
then(conversionService.canConvert(List.class, Wheels.class)).isTrue();
68+
then(conversionService.canConvert(
69+
TypeDescriptor.collection(List.class, TypeDescriptor.valueOf(WheelDto.class)),
70+
TypeDescriptor.valueOf((Wheels.class))))
71+
.isTrue();
6772
}
6873

6974
@Test

examples/packagename-and-classname/src/test/java/org/mapstruct/extensions/spring/example/ConversionServiceAdapterIntegrationTest.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import org.springframework.beans.factory.annotation.Autowired;
88
import org.springframework.context.annotation.Bean;
99
import org.springframework.context.annotation.ComponentScan;
10+
import org.springframework.core.convert.TypeDescriptor;
1011
import org.springframework.core.convert.support.ConfigurableConversionService;
1112
import org.springframework.core.convert.support.DefaultConversionService;
1213
import org.springframework.stereotype.Component;
@@ -64,6 +65,10 @@ void shouldKnowAllMappers() {
6465
then(conversionService.canConvert(Wheel.class, WheelDto.class)).isTrue();
6566
then(conversionService.canConvert(Wheels.class, List.class)).isTrue();
6667
then(conversionService.canConvert(List.class, Wheels.class)).isTrue();
68+
then(conversionService.canConvert(
69+
TypeDescriptor.collection(List.class, TypeDescriptor.valueOf(WheelDto.class)),
70+
TypeDescriptor.valueOf((Wheels.class))))
71+
.isTrue();
6772
}
6873

6974
@Test

examples/packagename/src/test/java/org/mapstruct/extensions/spring/example/ConversionServiceAdapterIntegrationTest.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import org.springframework.beans.factory.annotation.Autowired;
88
import org.springframework.context.annotation.Bean;
99
import org.springframework.context.annotation.ComponentScan;
10+
import org.springframework.core.convert.TypeDescriptor;
1011
import org.springframework.core.convert.support.ConfigurableConversionService;
1112
import org.springframework.core.convert.support.DefaultConversionService;
1213
import org.springframework.stereotype.Component;
@@ -60,10 +61,15 @@ void addMappersToConversionService() {
6061
@Test
6162
void shouldKnowAllMappers() {
6263
then(conversionService.canConvert(Car.class, CarDto.class)).isTrue();
63-
then(conversionService.canConvert(SeatConfiguration.class, SeatConfigurationDto.class)).isTrue();
64+
then(conversionService.canConvert(SeatConfiguration.class, SeatConfigurationDto.class))
65+
.isTrue();
6466
then(conversionService.canConvert(Wheel.class, WheelDto.class)).isTrue();
6567
then(conversionService.canConvert(Wheels.class, List.class)).isTrue();
6668
then(conversionService.canConvert(List.class, Wheels.class)).isTrue();
69+
then(conversionService.canConvert(
70+
TypeDescriptor.collection(List.class, TypeDescriptor.valueOf(WheelDto.class)),
71+
TypeDescriptor.valueOf((Wheels.class))))
72+
.isTrue();
6773
}
6874

6975
@Test

extensions/src/main/java/org/mapstruct/extensions/spring/converter/ConversionServiceAdapterGenerator.java

Lines changed: 119 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,24 @@
11
package org.mapstruct.extensions.spring.converter;
22

3-
import com.squareup.javapoet.*;
4-
import org.apache.commons.lang3.StringUtils;
5-
import org.apache.commons.lang3.tuple.Pair;
3+
import static java.time.format.DateTimeFormatter.ISO_INSTANT;
4+
import static java.util.stream.Collectors.toList;
5+
import static javax.lang.model.element.Modifier.*;
6+
import static javax.tools.Diagnostic.Kind.WARNING;
67

8+
import com.squareup.javapoet.*;
79
import java.io.IOException;
810
import java.io.UncheckedIOException;
911
import java.io.Writer;
1012
import java.time.Clock;
1113
import java.time.ZonedDateTime;
14+
import java.util.ArrayList;
15+
import java.util.Collection;
16+
import java.util.List;
1217
import java.util.Optional;
13-
14-
import static java.time.format.DateTimeFormatter.ISO_INSTANT;
15-
import static java.util.stream.Collectors.toList;
16-
import static javax.lang.model.element.Modifier.*;
18+
import java.util.concurrent.atomic.AtomicReference;
19+
import javax.annotation.processing.ProcessingEnvironment;
20+
import org.apache.commons.lang3.StringUtils;
21+
import org.apache.commons.lang3.tuple.Pair;
1722

1823
public class ConversionServiceAdapterGenerator {
1924
private static final String CONVERSION_SERVICE_PACKAGE_NAME = "org.springframework.core.convert";
@@ -25,13 +30,22 @@ public class ConversionServiceAdapterGenerator {
2530
private static final String LAZY_ANNOTATION_PACKAGE_NAME =
2631
"org.springframework.context.annotation";
2732
private static final String LAZY_ANNOTATION_CLASS_NAME = "Lazy";
33+
private static final ClassName TYPE_DESCRIPTOR_CLASS_NAME = ClassName.get("org.springframework.core.convert", "TypeDescriptor");
2834

2935
private final Clock clock;
3036

37+
private final AtomicReference<ProcessingEnvironment> processingEnvironment;
38+
3139
public ConversionServiceAdapterGenerator(final Clock clock) {
3240
this.clock = clock;
41+
processingEnvironment = new AtomicReference<>();
3342
}
3443

44+
45+
ProcessingEnvironment getProcessingEnvironment() {
46+
return processingEnvironment.get();
47+
}
48+
3549
public void writeConversionServiceAdapter(
3650
final ConversionServiceAdapterDescriptor descriptor, final Writer out) {
3751
try {
@@ -101,6 +115,45 @@ private static AnnotationSpec buildLazyAnnotation() {
101115
.build();
102116
}
103117

118+
private String collectionOfMethodName(final ParameterizedTypeName parameterizedTypeName) {
119+
if (isCollectionWithGenericParameter(parameterizedTypeName)) {
120+
return simpleName(parameterizedTypeName)
121+
+ "Of"
122+
+ collectionOfNameIfApplicable(parameterizedTypeName.typeArguments.iterator().next());
123+
}
124+
125+
return simpleName(parameterizedTypeName);
126+
}
127+
128+
private boolean isCollectionWithGenericParameter(final ParameterizedTypeName parameterizedTypeName) {
129+
return parameterizedTypeName.typeArguments != null
130+
&& parameterizedTypeName.typeArguments.size() > 0
131+
&& isCollection(parameterizedTypeName);
132+
}
133+
134+
private boolean isCollection(final ParameterizedTypeName parameterizedTypeName) {
135+
try {
136+
return Collection.class.isAssignableFrom(
137+
Class.forName(parameterizedTypeName.rawType.canonicalName()));
138+
} catch (ClassNotFoundException e) {
139+
processingEnvironment
140+
.get()
141+
.getMessager()
142+
.printMessage(
143+
WARNING,
144+
"Caught ClassNotFoundException when trying to resolve parameterized type: "
145+
+ e.getMessage());
146+
return false;
147+
}
148+
}
149+
150+
private String collectionOfNameIfApplicable(final TypeName typeName) {
151+
if (typeName instanceof ParameterizedTypeName) {
152+
return collectionOfMethodName((ParameterizedTypeName) typeName);
153+
}
154+
return simpleName(typeName);
155+
}
156+
104157
private static String simpleName(final TypeName typeName) {
105158
final TypeName rawType = rawType(typeName);
106159
if (rawType instanceof ArrayTypeName) {
@@ -124,35 +177,76 @@ private static TypeName rawType(final TypeName typeName) {
124177
return typeName;
125178
}
126179

127-
private static Iterable<MethodSpec> buildMappingMethods(
128-
final ConversionServiceAdapterDescriptor descriptor,
129-
final FieldSpec injectedConversionServiceFieldSpec) {
180+
private Iterable<MethodSpec> buildMappingMethods(
181+
final ConversionServiceAdapterDescriptor descriptor,
182+
final FieldSpec injectedConversionServiceFieldSpec) {
130183
return descriptor.getFromToMappings().stream()
131184
.map(
132185
sourceTargetPair ->
133186
toMappingMethodSpec(injectedConversionServiceFieldSpec, sourceTargetPair))
134187
.collect(toList());
135188
}
136189

137-
private static MethodSpec toMappingMethodSpec(
138-
final FieldSpec injectedConversionServiceFieldSpec,
139-
final Pair<TypeName, TypeName> sourceTargetPair) {
190+
private MethodSpec toMappingMethodSpec(
191+
final FieldSpec injectedConversionServiceFieldSpec,
192+
final Pair<TypeName, TypeName> sourceTargetPair) {
140193
final ParameterSpec sourceParameterSpec = buildSourceParameterSpec(sourceTargetPair.getLeft());
141194
return MethodSpec.methodBuilder(
142195
String.format(
143196
"map%sTo%s",
144-
simpleName(sourceTargetPair.getLeft()), simpleName(sourceTargetPair.getRight())))
197+
collectionOfNameIfApplicable(sourceTargetPair.getLeft()),
198+
collectionOfNameIfApplicable(sourceTargetPair.getRight())))
145199
.addParameter(sourceParameterSpec)
146200
.addModifiers(PUBLIC)
147201
.returns(sourceTargetPair.getRight())
148202
.addStatement(
149-
"return $N.convert($N, $T.class)",
150-
injectedConversionServiceFieldSpec,
151-
sourceParameterSpec,
152-
rawType(sourceTargetPair.getRight()))
203+
String.format(
204+
"return ($T) $N.convert($N, %s, %s)",
205+
typeDescriptorFormat(sourceTargetPair.getLeft()),
206+
typeDescriptorFormat(sourceTargetPair.getRight())),
207+
allTypeDescriptorArguments(injectedConversionServiceFieldSpec, sourceParameterSpec, sourceTargetPair))
153208
.build();
154209
}
155210

211+
private Object[] allTypeDescriptorArguments(
212+
final FieldSpec injectedConversionServiceFieldSpec,
213+
final ParameterSpec sourceParameterSpec,
214+
final Pair<TypeName, TypeName> sourceTargetPair) {
215+
final var arguments = new ArrayList<>();
216+
arguments.add(sourceTargetPair.getRight());
217+
arguments.add(injectedConversionServiceFieldSpec);
218+
arguments.add(sourceParameterSpec);
219+
arguments.addAll(typeDescriptorArguments(sourceTargetPair.getLeft()));
220+
arguments.addAll(typeDescriptorArguments(sourceTargetPair.getRight()));
221+
return arguments.toArray();
222+
}
223+
224+
private String typeDescriptorFormat(final TypeName typeName) {
225+
if (typeName instanceof ParameterizedTypeName
226+
&& isCollectionWithGenericParameter((ParameterizedTypeName) typeName)) {
227+
return String.format(
228+
"$T.collection($T.class, %s)",
229+
typeDescriptorFormat(((ParameterizedTypeName) typeName).typeArguments.iterator().next()));
230+
}
231+
return "$T.valueOf($T.class)";
232+
}
233+
234+
private List<Object> typeDescriptorArguments(final TypeName typeName) {
235+
final List<Object> allArguments;
236+
if (typeName instanceof ParameterizedTypeName
237+
&& isCollectionWithGenericParameter((ParameterizedTypeName) typeName)) {
238+
allArguments = new ArrayList<>();
239+
allArguments.add(TYPE_DESCRIPTOR_CLASS_NAME);
240+
allArguments.add(((ParameterizedTypeName) typeName).rawType);
241+
allArguments.addAll(
242+
typeDescriptorArguments(
243+
((ParameterizedTypeName) typeName).typeArguments.iterator().next()));
244+
} else {
245+
allArguments = List.of(TYPE_DESCRIPTOR_CLASS_NAME, rawType(typeName));
246+
}
247+
return allArguments;
248+
}
249+
156250
private static ParameterSpec buildSourceParameterSpec(final TypeName sourceClassName) {
157251
return ParameterSpec.builder(sourceClassName, "source", FINAL).build();
158252
}
@@ -167,7 +261,7 @@ private static FieldSpec buildConversionServiceFieldSpec() {
167261
}
168262

169263
private AnnotationSpec buildGeneratedAnnotationSpec(
170-
ConversionServiceAdapterDescriptor descriptor) {
264+
final ConversionServiceAdapterDescriptor descriptor) {
171265
final AnnotationSpec.Builder builder;
172266
if (descriptor.isSourceVersionAtLeast9()
173267
&& isTypeAvailable(descriptor, "javax.annotation.processing.Generated")) {
@@ -190,4 +284,10 @@ private static boolean isTypeAvailable(
190284
final ConversionServiceAdapterDescriptor descriptor, final String name) {
191285
return descriptor.getElementUtils().getTypeElement(name) != null;
192286
}
287+
288+
public void init(final ProcessingEnvironment processingEnv) {
289+
if (!this.processingEnvironment.compareAndSet(null, processingEnv)) {
290+
throw new IllegalStateException("ProcessingEnvironment already set.");
291+
}
292+
}
193293
}

extensions/src/main/java/org/mapstruct/extensions/spring/converter/ConverterMapperProcessor.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import org.mapstruct.extensions.spring.SpringMapperConfig;
99

1010
import javax.annotation.processing.AbstractProcessor;
11+
import javax.annotation.processing.ProcessingEnvironment;
1112
import javax.annotation.processing.RoundEnvironment;
1213
import javax.annotation.processing.SupportedAnnotationTypes;
1314
import javax.lang.model.SourceVersion;
@@ -50,6 +51,12 @@ public ConverterMapperProcessor() {
5051
this.adapterGenerator = adapterGenerator;
5152
}
5253

54+
@Override
55+
public synchronized void init(final ProcessingEnvironment processingEnv) {
56+
super.init(processingEnv);
57+
adapterGenerator.init(processingEnv);
58+
}
59+
5360
@Override
5461
public SourceVersion getSupportedSourceVersion() {
5562
return SourceVersion.latestSupported();

0 commit comments

Comments
 (0)