Skip to content

Commit 1623913

Browse files
Add ability to specify conversion service bean name
1 parent 51de316 commit 1623913

File tree

13 files changed

+267
-3
lines changed

13 files changed

+267
-3
lines changed

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,11 @@
3030
* @return The class name for the generated Adapter.
3131
*/
3232
String conversionServiceAdapterClassName() default "ConversionServiceAdapter";
33+
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 "";
3340
}
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 project(":examples:model")
4+
implementation project(":annotations")
5+
// This dependency is used internally, and not exposed to consumers on their own compile classpath.
6+
compileOnly 'javax.annotation:jsr250-api:1.0'
7+
implementation 'org.mapstruct:mapstruct:1.4.0.Final'
8+
annotationProcessor 'org.mapstruct:mapstruct-processor:1.4.0.Final'
9+
implementation 'org.springframework:spring-core:5.0.0.RELEASE'
10+
implementation 'org.springframework:spring-context:5.0.0.RELEASE'
11+
testImplementation 'org.springframework:spring-test:5.0.0.RELEASE'
12+
testImplementation 'org.assertj:assertj-core:3.15.0'
13+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package org.mapstruct.extensions.spring.example.custombean;
2+
3+
import org.mapstruct.Mapper;
4+
import org.mapstruct.Mapping;
5+
import org.springframework.core.convert.converter.Converter;
6+
import org.mapstruct.extensions.spring.example.Car;
7+
import org.mapstruct.extensions.spring.example.CarDto;
8+
9+
@Mapper(config = MapperSpringConfig.class)
10+
public interface CarMapper extends Converter<Car, CarDto> {
11+
@Mapping(target = "seats", source = "seatConfiguration")
12+
CarDto convert(Car car);
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package org.mapstruct.extensions.spring.example.custombean;
2+
3+
import org.mapstruct.MapperConfig;
4+
import org.mapstruct.extensions.spring.SpringMapperConfig;
5+
6+
@MapperConfig(componentModel = "spring", uses = ConversionServiceAdapter.class)
7+
@SpringMapperConfig(conversionServiceBeanName = "myConversionService")
8+
public interface MapperSpringConfig {
9+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package org.mapstruct.extensions.spring.example.custombean;
2+
3+
import org.mapstruct.Mapper;
4+
import org.mapstruct.Mapping;
5+
import org.springframework.core.convert.converter.Converter;
6+
import org.mapstruct.extensions.spring.example.SeatConfiguration;
7+
import org.mapstruct.extensions.spring.example.SeatConfigurationDto;
8+
9+
@Mapper(config = MapperSpringConfig.class)
10+
public interface SeatConfigurationMapper extends Converter<SeatConfiguration, SeatConfigurationDto> {
11+
@Mapping(target = "seatCount", source = "numberOfSeats")
12+
@Mapping(target = "material", source = "seatMaterial")
13+
SeatConfigurationDto convert(SeatConfiguration seatConfiguration);
14+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package org.mapstruct.extensions.spring.example;
2+
3+
import static org.assertj.core.api.BDDAssertions.then;
4+
import static org.mapstruct.extensions.spring.example.CarType.OTHER;
5+
import static org.mapstruct.extensions.spring.example.SeatMaterial.LEATHER;
6+
7+
import org.junit.jupiter.api.BeforeEach;
8+
import org.junit.jupiter.api.Test;
9+
import org.junit.jupiter.api.extension.ExtendWith;
10+
import org.springframework.beans.factory.annotation.Autowired;
11+
import org.springframework.beans.factory.annotation.Qualifier;
12+
import org.springframework.context.annotation.Bean;
13+
import org.springframework.context.annotation.ComponentScan;
14+
import org.springframework.core.convert.support.ConfigurableConversionService;
15+
import org.springframework.core.convert.support.DefaultConversionService;
16+
import org.springframework.stereotype.Component;
17+
import org.springframework.test.context.ContextConfiguration;
18+
import org.springframework.test.context.junit.jupiter.SpringExtension;
19+
import org.mapstruct.extensions.spring.example.custombean.CarMapper;
20+
import org.mapstruct.extensions.spring.example.custombean.SeatConfigurationMapper;
21+
22+
@ExtendWith(SpringExtension.class)
23+
@ContextConfiguration(
24+
classes = {ConversionServiceAdapterIntegrationTest.AdditionalBeanConfiguration.class})
25+
public class ConversionServiceAdapterIntegrationTest {
26+
private static final String TEST_MAKE = "Volvo";
27+
private static final CarType TEST_CAR_TYPE = OTHER;
28+
protected static final int TEST_NUMBER_OF_SEATS = 5;
29+
protected static final SeatMaterial TEST_SEAT_MATERIAL = LEATHER;
30+
31+
@Autowired private CarMapper carMapper;
32+
@Autowired private SeatConfigurationMapper seatConfigurationMapper;
33+
@Autowired @Qualifier("myConversionService")
34+
private ConfigurableConversionService conversionService;
35+
36+
@ComponentScan("org.mapstruct.extensions.spring")
37+
@Component
38+
static class AdditionalBeanConfiguration {
39+
@Bean
40+
ConfigurableConversionService getConversionService() {
41+
return new DefaultConversionService();
42+
}
43+
44+
@Bean
45+
ConfigurableConversionService myConversionService() {
46+
return new DefaultConversionService();
47+
}
48+
}
49+
50+
@BeforeEach
51+
void addMappersToConversionService() {
52+
conversionService.addConverter(carMapper);
53+
conversionService.addConverter(seatConfigurationMapper);
54+
}
55+
56+
@Test
57+
void shouldKnowAllMappers() {
58+
then(conversionService.canConvert(Car.class, CarDto.class)).isTrue();
59+
then(conversionService.canConvert(SeatConfiguration.class, SeatConfigurationDto.class))
60+
.isTrue();
61+
}
62+
63+
@Test
64+
void shouldMapAllAttributes() {
65+
// Given
66+
final Car car = new Car();
67+
car.setMake(TEST_MAKE);
68+
car.setType(TEST_CAR_TYPE);
69+
final SeatConfiguration seatConfiguration = new SeatConfiguration();
70+
seatConfiguration.setSeatMaterial(TEST_SEAT_MATERIAL);
71+
seatConfiguration.setNumberOfSeats(TEST_NUMBER_OF_SEATS);
72+
car.setSeatConfiguration(seatConfiguration);
73+
74+
// When
75+
final CarDto mappedCar = conversionService.convert(car, CarDto.class);
76+
77+
// Then
78+
then(mappedCar).isNotNull();
79+
then(mappedCar.getMake()).isEqualTo(TEST_MAKE);
80+
then(mappedCar.getType()).isEqualTo(String.valueOf(TEST_CAR_TYPE));
81+
final SeatConfigurationDto mappedCarSeats = mappedCar.getSeats();
82+
then(mappedCarSeats).isNotNull();
83+
then(mappedCarSeats.getSeatCount()).isEqualTo(TEST_NUMBER_OF_SEATS);
84+
then(mappedCarSeats.getMaterial()).isEqualTo(String.valueOf(TEST_SEAT_MATERIAL));
85+
}
86+
}

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
public class ConversionServiceAdapterDescriptor {
88
private ClassName adapterClassName;
9+
private String conversionServiceBeanName;
910
private List<Pair<ClassName, ClassName>> fromToMappings;
1011

1112
public ClassName getAdapterClassName() {
@@ -16,6 +17,14 @@ public void setAdapterClassName(final ClassName adapterClassName) {
1617
this.adapterClassName = adapterClassName;
1718
}
1819

20+
public String getConversionServiceBeanName() {
21+
return conversionServiceBeanName;
22+
}
23+
24+
public void setConversionServiceBeanName(String conversionServiceBeanName) {
25+
this.conversionServiceBeanName = conversionServiceBeanName;
26+
}
27+
1928
public List<Pair<ClassName, ClassName>> getFromToMappings() {
2029
return fromToMappings;
2130
}

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

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,16 @@
44
import static javax.lang.model.element.Modifier.*;
55

66
import com.squareup.javapoet.*;
7+
import org.apache.commons.lang3.StringUtils;
8+
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;
1214
import java.time.format.DateTimeFormatter;
15+
import java.util.ArrayList;
16+
import java.util.List;
1317

1418
public class ConversionServiceAdapterGenerator {
1519
private final Clock clock;
@@ -33,7 +37,7 @@ public void writeConversionServiceAdapter(
3337

3438
private TypeSpec createConversionServiceTypeSpec(
3539
final ConversionServiceAdapterDescriptor descriptor) {
36-
final FieldSpec injectedConversionServiceFieldSpec = buildInjectedConversionServiceFieldSpec();
40+
final FieldSpec injectedConversionServiceFieldSpec = buildInjectedConversionServiceFieldSpec(descriptor);
3741
return TypeSpec.classBuilder(descriptor.getAdapterClassName())
3842
.addModifiers(PUBLIC)
3943
.addAnnotation(buildGeneratedAnnotationSpec())
@@ -73,10 +77,21 @@ private static ParameterSpec buildSourceParameterSpec(final TypeName sourceClass
7377
return ParameterSpec.builder(sourceClassName, "source", FINAL).build();
7478
}
7579

76-
private static FieldSpec buildInjectedConversionServiceFieldSpec() {
80+
private static FieldSpec buildInjectedConversionServiceFieldSpec(ConversionServiceAdapterDescriptor descriptor) {
81+
List<AnnotationSpec> annotations = new ArrayList<>();
82+
annotations.add(AnnotationSpec.builder(ClassName
83+
.get("org.springframework.beans.factory.annotation", "Autowired"))
84+
.build());
85+
if (StringUtils.isNotEmpty(descriptor.getConversionServiceBeanName())) {
86+
annotations.add(AnnotationSpec
87+
.builder(ClassName.get("org.springframework.beans.factory.annotation", "Qualifier"))
88+
.addMember("value", "$S", descriptor.getConversionServiceBeanName())
89+
.build());
90+
}
91+
7792
return FieldSpec.builder(ClassName.get("org.springframework.core.convert","ConversionService"),
7893
"conversionService", PRIVATE)
79-
.addAnnotation(ClassName.get("org.springframework.beans.factory.annotation", "Autowired"))
94+
.addAnnotations(annotations)
8095
.build();
8196
}
8297

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import java.util.Objects;
1515
import java.util.Optional;
1616
import java.util.Set;
17+
import java.util.concurrent.atomic.AtomicReference;
1718
import javax.annotation.processing.AbstractProcessor;
1819
import javax.annotation.processing.RoundEnvironment;
1920
import javax.annotation.processing.SupportedAnnotationTypes;
@@ -63,6 +64,7 @@ public boolean process(
6364
getAdapterPackageAndClassName(annotations, roundEnv);
6465
descriptor.setAdapterClassName(
6566
ClassName.get(adapterPackageAndClass.getLeft(), adapterPackageAndClass.getRight()));
67+
descriptor.setConversionServiceBeanName(getConversionServiceName(annotations, roundEnv));
6668
annotations.stream()
6769
.filter(annotation -> MAPPER.contentEquals(annotation.getQualifiedName()))
6870
.forEach(
@@ -166,6 +168,23 @@ private void updateFromDeclaration(
166168
adapterPackageAndClass.setRight(springMapperConfig.conversionServiceAdapterClassName());
167169
}
168170

171+
private String getConversionServiceName(
172+
final Set<? extends TypeElement> annotations, final RoundEnvironment roundEnv) {
173+
AtomicReference<String> beanName = new AtomicReference<>();
174+
for (final TypeElement annotation : annotations) {
175+
if (SPRING_MAPPER_CONFIG.contentEquals(annotation.getQualifiedName())) {
176+
roundEnv
177+
.getElementsAnnotatedWith(annotation)
178+
.stream().findFirst()
179+
.ifPresent(element -> {
180+
final SpringMapperConfig springMapperConfig = element.getAnnotation(SpringMapperConfig.class);
181+
beanName.set(springMapperConfig.conversionServiceBeanName());
182+
});
183+
}
184+
}
185+
return beanName.get();
186+
}
187+
169188
private Optional<? extends TypeMirror> getConverterSupertype(final Element mapper) {
170189
final Types typeUtils = processingEnv.getTypeUtils();
171190
return typeUtils.directSupertypes(mapper.asType()).stream()

extensions/src/test/java/org/mapstruct/extensions/spring/converter/ConversionServiceAdapterGeneratorTest.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,4 +42,25 @@ void shouldGenerateMatchingOutput() throws IOException {
4242
then(outputWriter.toString())
4343
.isEqualToIgnoringWhitespace(resourceToString("/ConversionServiceAdapter.java", UTF_8));
4444
}
45+
46+
@Test
47+
void shouldGenerateMatchingOutputWhenUsingCustomConversionService() throws IOException {
48+
// Given
49+
final ConversionServiceAdapterDescriptor descriptor = new ConversionServiceAdapterDescriptor();
50+
descriptor.setAdapterClassName(
51+
ClassName.get(
52+
ConversionServiceAdapterGeneratorTest.class.getPackage().getName(),
53+
"ConversionServiceAdapter"));
54+
descriptor.setConversionServiceBeanName("myConversionService");
55+
descriptor.setFromToMappings(
56+
singletonList(Pair.of(ClassName.get("test", "Car"), ClassName.get("test", "CarDto"))));
57+
final StringWriter outputWriter = new StringWriter();
58+
59+
// When
60+
generator.writeConversionServiceAdapter(descriptor, outputWriter);
61+
62+
// Then
63+
then(outputWriter.toString())
64+
.isEqualToIgnoringWhitespace(resourceToString("/ConversionServiceAdapterCustomBean.java", UTF_8));
65+
}
4566
}

0 commit comments

Comments
 (0)