Skip to content

Commit bd7601d

Browse files
committed
Added annotation for allowing different package and class names for generated ConversionServiceBridge.
1 parent 133eaae commit bd7601d

File tree

25 files changed

+525
-76
lines changed

25 files changed

+525
-76
lines changed

additions/build.gradle

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
dependencies {
2-
implementation 'org.mapstruct:mapstruct:1.3.1.Final'
2+
implementation project(":annotations")
3+
implementation 'org.mapstruct:mapstruct:1.4.0-SNAPSHOT'
34
implementation 'org.springframework:spring-core:5.0.0.RELEASE'
45
implementation 'com.squareup:javapoet:1.12.1'
56
implementation 'org.apache.commons:commons-lang3:3.10'
7+
implementation 'org.mapstruct:mapstruct-processor:1.4.0-SNAPSHOT'
68

79
testImplementation 'org.assertj:assertj-core:3.15.0'
810
testImplementation 'commons-io:commons-io:2.6'

additions/src/main/java/uk/co/kleindelao/mapstruct/spring/converter/ConverterMapperProcessor.java

Lines changed: 103 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package uk.co.kleindelao.mapstruct.spring.converter;
22

3+
import static java.util.stream.Collectors.toList;
34
import static javax.lang.model.element.ElementKind.METHOD;
45
import static javax.lang.model.element.Modifier.PUBLIC;
56
import static javax.lang.model.type.TypeKind.DECLARED;
@@ -9,8 +10,8 @@
910
import java.io.IOException;
1011
import java.io.Writer;
1112
import java.time.Clock;
12-
import java.util.ArrayList;
1313
import java.util.List;
14+
import java.util.Objects;
1415
import java.util.Optional;
1516
import java.util.Set;
1617
import javax.annotation.processing.AbstractProcessor;
@@ -22,78 +23,127 @@
2223
import javax.lang.model.type.DeclaredType;
2324
import javax.lang.model.type.TypeMirror;
2425
import javax.lang.model.util.Types;
26+
import org.apache.commons.lang3.StringUtils;
27+
import org.apache.commons.lang3.tuple.MutablePair;
2528
import org.apache.commons.lang3.tuple.Pair;
2629
import org.springframework.core.convert.converter.Converter;
30+
import uk.co.kleindelao.mapstruct.spring.SpringMapperConfig;
2731

28-
@SupportedAnnotationTypes(ConverterMapperProcessor.ORG_MAPSTRUCT_MAPPER)
32+
@SupportedAnnotationTypes({
33+
ConverterMapperProcessor.MAPPER,
34+
ConverterMapperProcessor.SPRING_MAPPER_CONFIG
35+
})
2936
public class ConverterMapperProcessor extends AbstractProcessor {
30-
protected static final String ORG_MAPSTRUCT_MAPPER = "org.mapstruct.Mapper";
37+
protected static final String MAPPER = "org.mapstruct.Mapper";
38+
protected static final String SPRING_MAPPER_CONFIG =
39+
"uk.co.kleindelao.mapstruct.spring.SpringMapperConfig";
3140

3241
private final ConversionServiceBridgeGenerator bridgeGenerator =
3342
new ConversionServiceBridgeGenerator(Clock.systemUTC());
3443

3544
@Override
3645
public boolean process(
3746
final Set<? extends TypeElement> annotations, final RoundEnvironment roundEnv) {
38-
final Types typeUtils = processingEnv.getTypeUtils();
3947
final ConversionServiceBridgeDescriptor descriptor = new ConversionServiceBridgeDescriptor();
48+
final MutablePair<String, String> bridgePackageAndClass =
49+
getBridgePackageAndClassName(annotations, roundEnv);
4050
descriptor.setBridgeClassName(
41-
ClassName.get(
42-
ConverterMapperProcessor.class.getPackage().getName(), "ConversionServiceBridge"));
51+
ClassName.get(bridgePackageAndClass.getLeft(), bridgePackageAndClass.getRight()));
4352
for (final TypeElement annotation : annotations) {
44-
if (ORG_MAPSTRUCT_MAPPER.contentEquals(annotation.getQualifiedName())) {
45-
final List<Pair<ClassName, ClassName>> fromToMappings = new ArrayList<>();
46-
roundEnv.getElementsAnnotatedWith(annotation).stream()
47-
.filter(mapper -> mapper.asType().getKind() == DECLARED)
48-
.filter(mapper -> getConverterSupertype(mapper).isPresent())
49-
.forEach(
50-
mapper ->
51-
mapper.getEnclosedElements().stream()
52-
.filter(element -> element.getKind() == METHOD)
53-
.filter(method -> method.getModifiers().contains(PUBLIC))
54-
.filter(method -> method.getSimpleName().contentEquals("convert"))
55-
.filter(
56-
convert -> ((ExecutableElement) convert).getParameters().size() == 1)
57-
.filter(
58-
convert ->
59-
typeUtils.isSameType(
60-
getFirstParameterType((ExecutableElement) convert),
61-
getFirstTypeArgument(getConverterSupertype(mapper).get())))
62-
.map(
63-
convert ->
64-
Pair.of(
65-
(ClassName)
66-
((ExecutableElement) convert)
67-
.getParameters().stream()
68-
.map(Element::asType)
69-
.map(ClassName::get)
70-
.findFirst()
71-
.get(),
72-
(ClassName)
73-
ClassName.get(
74-
((ExecutableElement) convert).getReturnType())))
75-
.forEach(fromToMappings::add));
53+
if (MAPPER.contentEquals(annotation.getQualifiedName())) {
54+
final List<Pair<ClassName, ClassName>> fromToMappings =
55+
roundEnv.getElementsAnnotatedWith(annotation).stream()
56+
.filter(mapper -> mapper.asType().getKind() == DECLARED)
57+
.filter(mapper -> getConverterSupertype(mapper).isPresent())
58+
.map(this::toConvertMethod)
59+
.filter(Objects::nonNull)
60+
.map(ExecutableElement.class::cast)
61+
.map(this::toFromToMapping)
62+
.collect(toList());
7663
descriptor.setFromToMappings(fromToMappings);
77-
try (final Writer outputWriter =
78-
processingEnv
79-
.getFiler()
80-
.createSourceFile(
81-
ConverterMapperProcessor.class.getPackage().getName()
82-
+ ".ConversionServiceBridge")
83-
.openWriter()) {
84-
bridgeGenerator.writeConversionServiceBridge(descriptor, outputWriter);
85-
} catch (IOException e) {
86-
processingEnv
87-
.getMessager()
88-
.printMessage(
89-
ERROR,
90-
"Error while opening ConversionServiceBridge output file: " + e.getMessage());
91-
}
64+
writeBridgeClassFile(descriptor, bridgePackageAndClass);
9265
}
9366
}
9467
return false;
9568
}
9669

70+
private Pair<ClassName, ClassName> toFromToMapping(final ExecutableElement convert) {
71+
return Pair.of(
72+
(ClassName)
73+
convert.getParameters().stream()
74+
.map(Element::asType)
75+
.map(ClassName::get)
76+
.findFirst()
77+
.get(),
78+
(ClassName) ClassName.get(convert.getReturnType()));
79+
}
80+
81+
private Element toConvertMethod(final Element mapper) {
82+
return mapper.getEnclosedElements().stream()
83+
.filter(element -> element.getKind() == METHOD)
84+
.filter(method -> method.getModifiers().contains(PUBLIC))
85+
.filter(method -> method.getSimpleName().contentEquals("convert"))
86+
.filter(convert -> ((ExecutableElement) convert).getParameters().size() == 1)
87+
.filter(
88+
convert ->
89+
processingEnv
90+
.getTypeUtils()
91+
.isSameType(
92+
getFirstParameterType((ExecutableElement) convert),
93+
getFirstTypeArgument(getConverterSupertype(mapper).get())))
94+
.findFirst()
95+
.orElse(null);
96+
}
97+
98+
private void writeBridgeClassFile(
99+
final ConversionServiceBridgeDescriptor descriptor,
100+
final MutablePair<String, String> bridgePackageAndClass) {
101+
try (final Writer outputWriter =
102+
processingEnv
103+
.getFiler()
104+
.createSourceFile(
105+
bridgePackageAndClass.getLeft() + "." + bridgePackageAndClass.getRight())
106+
.openWriter()) {
107+
bridgeGenerator.writeConversionServiceBridge(descriptor, outputWriter);
108+
} catch (IOException e) {
109+
processingEnv
110+
.getMessager()
111+
.printMessage(
112+
ERROR,
113+
"Error while opening "
114+
+ bridgePackageAndClass.getRight()
115+
+ " output file: "
116+
+ e.getMessage());
117+
}
118+
}
119+
120+
private MutablePair<String, String> getBridgePackageAndClassName(
121+
final Set<? extends TypeElement> annotations, final RoundEnvironment roundEnv) {
122+
final MutablePair<String, String> packageAndClass =
123+
MutablePair.of(
124+
ConverterMapperProcessor.class.getPackage().getName(), "ConversionServiceBridge");
125+
for (final TypeElement annotation : annotations) {
126+
if (SPRING_MAPPER_CONFIG.contentEquals(annotation.getQualifiedName())) {
127+
roundEnv
128+
.getElementsAnnotatedWith(annotation)
129+
.forEach(element -> updateFromDeclaration(element, packageAndClass));
130+
}
131+
}
132+
return packageAndClass;
133+
}
134+
135+
private void updateFromDeclaration(
136+
final Element element, final MutablePair<String, String> bridgePackageAndClass) {
137+
final SpringMapperConfig springMapperConfig = element.getAnnotation(SpringMapperConfig.class);
138+
bridgePackageAndClass.setLeft(
139+
Optional.of(springMapperConfig.conversionServiceBridgePackage())
140+
.filter(StringUtils::isNotBlank)
141+
.orElse(
142+
String.valueOf(
143+
processingEnv.getElementUtils().getPackageOf(element).getQualifiedName())));
144+
bridgePackageAndClass.setRight(springMapperConfig.conversionServiceBridgeClassName());
145+
}
146+
97147
private Optional<? extends TypeMirror> getConverterSupertype(final Element mapper) {
98148
final Types typeUtils = processingEnv.getTypeUtils();
99149
return typeUtils.directSupertypes(mapper.asType()).stream()

annotations/src/main/java/uk/co/kleindelao/mapstruct/spring/SpringMapperConfig.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@
1616
public @interface SpringMapperConfig {
1717
/**
1818
* The package name for the generated &quot;Bridge&quot; between the MapStruct mappers and Spring's
19-
* {@link org.springframework.core.convert.ConversionService}. If omitted, the package name will be the same as
20-
* the one for the annotated type.
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.
2121
*
2222
* @return The package name for the generated &quot;Bridge&quot;.
2323
*/

build.gradle

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,15 @@ version = '0.0.1'
1717

1818
allprojects {
1919
repositories {
20-
// Use jcenter for resolving dependencies.
21-
// You can declare any Maven/Ivy/file repository here.
2220
jcenter()
2321
mavenCentral()
22+
mavenLocal()
2423
}
2524
}
2625

2726
subprojects {
2827
apply plugin: "java-library"
28+
apply plugin: "jacoco"
2929

3030
dependencies {
3131
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.6.1'
@@ -42,10 +42,14 @@ subprojects {
4242
useJUnitPlatform()
4343
}
4444

45+
jacoco {
46+
toolVersion = "0.8.5"
47+
}
48+
4549
jacocoTestReport {
4650
reports {
4751
xml.enabled true
48-
html.enabled false
52+
html.enabled true
4953
}
5054
}
5155

examples/classname/build.gradle

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
dependencies {
2+
annotationProcessor project(":additions")
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-SNAPSHOT'
8+
annotationProcessor 'org.mapstruct:mapstruct-processor:1.4.0-SNAPSHOT'
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 uk.co.kleindelao.mapstruct.spring.example.classname;
2+
3+
import org.mapstruct.Mapper;
4+
import org.mapstruct.Mapping;
5+
import org.springframework.core.convert.converter.Converter;
6+
import uk.co.kleindelao.mapstruct.spring.example.Car;
7+
import uk.co.kleindelao.mapstruct.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+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package uk.co.kleindelao.mapstruct.spring.example.classname;
2+
3+
import org.mapstruct.MapperConfig;
4+
import uk.co.kleindelao.mapstruct.spring.SpringMapperConfig;
5+
6+
@MapperConfig(componentModel = "spring", uses = MyBridge.class)
7+
@SpringMapperConfig(conversionServiceBridgeClassName="MyBridge")
8+
public interface MapperSpringConfig {
9+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package uk.co.kleindelao.mapstruct.spring.example.classname;
2+
3+
import org.mapstruct.Mapper;
4+
import org.mapstruct.Mapping;
5+
import org.springframework.core.convert.converter.Converter;
6+
import uk.co.kleindelao.mapstruct.spring.example.SeatConfiguration;
7+
import uk.co.kleindelao.mapstruct.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+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package uk.co.kleindelao.mapstruct.spring.example;
2+
3+
import static org.assertj.core.api.BDDAssertions.then;
4+
import static uk.co.kleindelao.mapstruct.spring.example.CarType.OTHER;
5+
import static uk.co.kleindelao.mapstruct.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.context.annotation.Bean;
12+
import org.springframework.context.annotation.ComponentScan;
13+
import org.springframework.core.convert.support.ConfigurableConversionService;
14+
import org.springframework.core.convert.support.DefaultConversionService;
15+
import org.springframework.stereotype.Component;
16+
import org.springframework.test.context.ContextConfiguration;
17+
import org.springframework.test.context.junit.jupiter.SpringExtension;
18+
import uk.co.kleindelao.mapstruct.spring.example.classname.CarMapper;
19+
import uk.co.kleindelao.mapstruct.spring.example.classname.SeatConfigurationMapper;
20+
21+
@ExtendWith(SpringExtension.class)
22+
@ContextConfiguration(
23+
classes = {ConversionServiceBridgeIntegrationTest.AdditionalBeanConfiguration.class})
24+
public class ConversionServiceBridgeIntegrationTest {
25+
private static final String TEST_MAKE = "Volvo";
26+
private static final CarType TEST_CAR_TYPE = OTHER;
27+
protected static final int TEST_NUMBER_OF_SEATS = 5;
28+
protected static final SeatMaterial TEST_SEAT_MATERIAL = LEATHER;
29+
30+
@Autowired private CarMapper carMapper;
31+
@Autowired private SeatConfigurationMapper seatConfigurationMapper;
32+
@Autowired private ConfigurableConversionService conversionService;
33+
34+
@ComponentScan("uk.co.kleindelao.mapstruct.spring")
35+
@Component
36+
static class AdditionalBeanConfiguration {
37+
@Bean
38+
ConfigurableConversionService getConversionService() {
39+
return new DefaultConversionService();
40+
}
41+
}
42+
43+
@BeforeEach
44+
void addMappersToConversionService() {
45+
conversionService.addConverter(carMapper);
46+
conversionService.addConverter(seatConfigurationMapper);
47+
}
48+
49+
@Test
50+
void shouldKnowAllMappers() {
51+
then(conversionService.canConvert(Car.class, CarDto.class)).isTrue();
52+
then(conversionService.canConvert(SeatConfiguration.class, SeatConfigurationDto.class))
53+
.isTrue();
54+
}
55+
56+
@Test
57+
void shouldMapAllAttributes() {
58+
// Given
59+
final Car car = new Car();
60+
car.setMake(TEST_MAKE);
61+
car.setType(TEST_CAR_TYPE);
62+
final SeatConfiguration seatConfiguration = new SeatConfiguration();
63+
seatConfiguration.setSeatMaterial(TEST_SEAT_MATERIAL);
64+
seatConfiguration.setNumberOfSeats(TEST_NUMBER_OF_SEATS);
65+
car.setSeatConfiguration(seatConfiguration);
66+
67+
// When
68+
final CarDto mappedCar = conversionService.convert(car, CarDto.class);
69+
70+
// Then
71+
then(mappedCar).isNotNull();
72+
then(mappedCar.getMake()).isEqualTo(TEST_MAKE);
73+
then(mappedCar.getType()).isEqualTo(String.valueOf(TEST_CAR_TYPE));
74+
final SeatConfigurationDto mappedCarSeats = mappedCar.getSeats();
75+
then(mappedCarSeats).isNotNull();
76+
then(mappedCarSeats.getSeatCount()).isEqualTo(TEST_NUMBER_OF_SEATS);
77+
then(mappedCarSeats.getMaterial()).isEqualTo(String.valueOf(TEST_SEAT_MATERIAL));
78+
}
79+
}

examples/noconfig/build.gradle

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ dependencies {
33
implementation project(":examples:model")
44
// This dependency is used internally, and not exposed to consumers on their own compile classpath.
55
compileOnly 'javax.annotation:jsr250-api:1.0'
6-
implementation 'org.mapstruct:mapstruct:1.3.1.Final'
7-
annotationProcessor 'org.mapstruct:mapstruct-processor:1.3.1.Final'
6+
implementation 'org.mapstruct:mapstruct:1.4.0-SNAPSHOT'
7+
annotationProcessor 'org.mapstruct:mapstruct-processor:1.4.0-SNAPSHOT'
88
implementation 'org.springframework:spring-core:5.0.0.RELEASE'
99
implementation 'org.springframework:spring-context:5.0.0.RELEASE'
1010
testImplementation 'org.springframework:spring-test:5.0.0.RELEASE'

0 commit comments

Comments
 (0)