Skip to content

Commit 77b7cd2

Browse files
committed
Generates ConversionServiceBridge class.
1 parent 5fdf93a commit 77b7cd2

File tree

18 files changed

+355
-22
lines changed

18 files changed

+355
-22
lines changed

additions/build.gradle

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
1+
plugins {
2+
id "io.freefair.lombok" version "5.0.0-rc6"
3+
}
4+
15
dependencies {
2-
// This dependency is used internally, and not exposed to consumers on their own compile classpath.
36
implementation 'org.mapstruct:mapstruct:1.3.1.Final'
47
implementation 'org.springframework:spring-core:5.0.0.RELEASE'
8+
implementation 'com.squareup:javapoet:1.12.1'
9+
implementation 'org.apache.commons:commons-lang3:3.10'
10+
11+
testImplementation 'org.assertj:assertj-core:3.15.0'
12+
testImplementation 'commons-io:commons-io:2.6'
513
}

additions/lombok.config

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# This file is generated by the 'io.freefair.lombok' Gradle plugin
2+
config.stopBubbling = true
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package uk.co.kleindelao.mapstruct.spring.converter;
2+
3+
import com.squareup.javapoet.ClassName;
4+
import lombok.Builder;
5+
import lombok.Singular;
6+
import lombok.Value;
7+
import org.apache.commons.lang3.tuple.Pair;
8+
9+
import java.util.List;
10+
11+
@Value
12+
@Builder
13+
public class ConversionServiceBridgeDescriptor {
14+
ClassName bridgeClassName;
15+
@Singular
16+
List<Pair<ClassName, ClassName>> fromToMappings;
17+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package uk.co.kleindelao.mapstruct.spring.converter;
2+
3+
import com.squareup.javapoet.*;
4+
import lombok.Value;
5+
import org.springframework.core.convert.ConversionService;
6+
7+
import java.io.IOException;
8+
import java.io.UncheckedIOException;
9+
import java.io.Writer;
10+
import java.time.Clock;
11+
import java.time.ZonedDateTime;
12+
import java.time.format.DateTimeFormatter;
13+
14+
import static java.util.stream.Collectors.toList;
15+
import static javax.lang.model.element.Modifier.*;
16+
17+
@Value
18+
public class ConversionServiceBridgeGenerator {
19+
Clock clock;
20+
21+
public void writeConversionServiceBridge(
22+
ConversionServiceBridgeDescriptor descriptor, Writer out) {
23+
try {
24+
JavaFile.builder(
25+
descriptor.getBridgeClassName().packageName(),
26+
createConversionServiceTypeSpec(descriptor))
27+
.build()
28+
.writeTo(out);
29+
} catch (IOException e) {
30+
throw new UncheckedIOException(e);
31+
}
32+
}
33+
34+
private TypeSpec createConversionServiceTypeSpec(
35+
final ConversionServiceBridgeDescriptor descriptor) {
36+
final FieldSpec injectedConversionServiceFieldSpec = buildInjectedConversionServiceFieldSpec();
37+
return TypeSpec.classBuilder(descriptor.getBridgeClassName())
38+
.addModifiers(PUBLIC)
39+
.addAnnotation(buildGeneratedAnnotationSpec())
40+
.addAnnotation(ClassName.get("org.springframework.stereotype", "Component"))
41+
.addField(injectedConversionServiceFieldSpec)
42+
.addMethods(buildMappingMethods(descriptor, injectedConversionServiceFieldSpec))
43+
.build();
44+
}
45+
46+
private static Iterable<MethodSpec> buildMappingMethods(
47+
final ConversionServiceBridgeDescriptor descriptor,
48+
final FieldSpec injectedConversionServiceFieldSpec) {
49+
return descriptor.getFromToMappings().stream()
50+
.map(
51+
sourceTargetPair -> {
52+
final ParameterSpec sourceParameterSpec =
53+
buildSourceParameterSpec(sourceTargetPair.getLeft());
54+
return MethodSpec.methodBuilder(
55+
"map"
56+
+ sourceTargetPair.getLeft().simpleName()
57+
+ "To"
58+
+ sourceTargetPair.getRight().simpleName())
59+
.addParameter(sourceParameterSpec)
60+
.addModifiers(PUBLIC)
61+
.returns(sourceTargetPair.getRight())
62+
.addStatement(
63+
"return $N.convert($N, $T.class)",
64+
injectedConversionServiceFieldSpec,
65+
sourceParameterSpec,
66+
sourceTargetPair.getRight())
67+
.build();
68+
})
69+
.collect(toList());
70+
}
71+
72+
private static ParameterSpec buildSourceParameterSpec(final TypeName sourceClassName) {
73+
return ParameterSpec.builder(sourceClassName, "source", FINAL).build();
74+
}
75+
76+
private static FieldSpec buildInjectedConversionServiceFieldSpec() {
77+
return FieldSpec.builder(ConversionService.class, "conversionService", PRIVATE)
78+
.addAnnotation(ClassName.get("org.springframework.beans.factory.annotation", "Autowired"))
79+
.build();
80+
}
81+
82+
private AnnotationSpec buildGeneratedAnnotationSpec() {
83+
return AnnotationSpec.builder(ClassName.get("javax.annotation", "Generated"))
84+
.addMember("value", "$S", ConversionServiceBridgeGenerator.class.getName())
85+
.addMember("date", "$S", DateTimeFormatter.ISO_INSTANT.format(ZonedDateTime.now(clock)))
86+
.build();
87+
}
88+
}

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

Lines changed: 43 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1-
package uk.co.kleindelao.mapstruct.spring;
1+
package uk.co.kleindelao.mapstruct.spring.converter;
22

3+
import com.squareup.javapoet.ClassName;
4+
import org.apache.commons.lang3.tuple.Pair;
35
import org.springframework.core.convert.converter.Converter;
6+
import uk.co.kleindelao.mapstruct.spring.converter.ConversionServiceBridgeDescriptor.ConversionServiceBridgeDescriptorBuilder;
47

58
import javax.annotation.processing.AbstractProcessor;
69
import javax.annotation.processing.RoundEnvironment;
@@ -11,26 +14,37 @@
1114
import javax.lang.model.type.DeclaredType;
1215
import javax.lang.model.type.TypeMirror;
1316
import javax.lang.model.util.Types;
17+
import java.io.IOException;
18+
import java.io.Writer;
19+
import java.time.Clock;
1420
import java.util.Optional;
1521
import java.util.Set;
16-
import java.util.stream.Collectors;
1722

1823
import static javax.lang.model.element.ElementKind.METHOD;
1924
import static javax.lang.model.element.Modifier.PUBLIC;
2025
import static javax.lang.model.type.TypeKind.DECLARED;
21-
import static javax.tools.Diagnostic.Kind.NOTE;
26+
import static javax.tools.Diagnostic.Kind.ERROR;
2227

2328
@SupportedAnnotationTypes(ConverterMapperProcessor.ORG_MAPSTRUCT_MAPPER)
2429
public class ConverterMapperProcessor extends AbstractProcessor {
2530
protected static final String ORG_MAPSTRUCT_MAPPER = "org.mapstruct.Mapper";
2631

32+
private final ConversionServiceBridgeGenerator bridgeGenerator =
33+
new ConversionServiceBridgeGenerator(Clock.systemUTC());
34+
2735
@Override
2836
public boolean process(
2937
final Set<? extends TypeElement> annotations, final RoundEnvironment roundEnv) {
3038
final Types typeUtils = processingEnv.getTypeUtils();
39+
final ConversionServiceBridgeDescriptorBuilder descriptorBuilder =
40+
ConversionServiceBridgeDescriptor.builder()
41+
.bridgeClassName(
42+
ClassName.get(
43+
ConverterMapperProcessor.class.getPackage().getName(),
44+
"ConversionServiceBridge"));
3145
for (final TypeElement annotation : annotations) {
3246
if (ORG_MAPSTRUCT_MAPPER.contentEquals(annotation.getQualifiedName())) {
33-
roundEnv.getElementsAnnotatedWith(annotation).stream()
47+
roundEnv.getElementsAnnotatedWith(annotation).stream()
3448
.filter(mapper -> mapper.asType().getKind() == DECLARED)
3549
.filter(mapper -> getConverterSupertype(mapper).isPresent())
3650
.forEach(
@@ -48,17 +62,31 @@ public boolean process(
4862
getFirstTypeArgument(getConverterSupertype(mapper).get())))
4963
.forEach(
5064
convert ->
51-
processingEnv
52-
.getMessager()
53-
.printMessage(
54-
NOTE,
55-
"Found Mapper '"
56-
+ mapper
57-
+ "' with convert method mapping from '"
58-
+ ((ExecutableElement) convert).getParameters().stream().map(Element::asType).map(TypeMirror::toString).collect(Collectors.joining())
59-
+ "' to '"
60-
+ ((ExecutableElement) convert).getReturnType()
61-
+ "'.")));
65+
descriptorBuilder.fromToMapping(
66+
Pair.of(
67+
(ClassName)
68+
((ExecutableElement) convert)
69+
.getParameters().stream()
70+
.map(Element::asType)
71+
.map(ClassName::get)
72+
.findFirst()
73+
.get(),
74+
(ClassName)
75+
ClassName.get(
76+
((ExecutableElement) convert).getReturnType())))));
77+
try (final Writer outputWriter =
78+
processingEnv
79+
.getFiler()
80+
.createSourceFile(
81+
ConverterMapperProcessor.class.getPackage().getName() + ".ConversionServiceBridge")
82+
.openWriter()) {
83+
bridgeGenerator.writeConversionServiceBridge(descriptorBuilder.build(), outputWriter);
84+
} catch (IOException e) {
85+
processingEnv
86+
.getMessager()
87+
.printMessage(
88+
ERROR, "Error while opening ConversionServiceBridge output file: " + e.getMessage());
89+
}
6290
}
6391
}
6492
return false;
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
uk.co.kleindelao.mapstruct.spring.ConverterMapperProcessor
1+
uk.co.kleindelao.mapstruct.spring.converter.ConverterMapperProcessor
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package uk.co.kleindelao.mapstruct.spring.converter;
2+
3+
import com.squareup.javapoet.ClassName;
4+
import org.apache.commons.lang3.tuple.Pair;
5+
import org.junit.jupiter.api.Test;
6+
7+
import java.io.IOException;
8+
import java.io.StringWriter;
9+
import java.time.Clock;
10+
import java.time.ZoneId;
11+
import java.time.ZonedDateTime;
12+
13+
import static java.nio.charset.StandardCharsets.UTF_8;
14+
import static org.apache.commons.io.IOUtils.resourceToString;
15+
import static org.assertj.core.api.BDDAssertions.then;
16+
17+
class ConversionServiceBridgeGeneratorTest {
18+
private static final Clock FIXED_CLOCK =
19+
Clock.fixed(
20+
ZonedDateTime.of(2020, 3, 29, 15, 21, 34, (int) (236 * Math.pow(10, 6)), ZoneId.of("Z")).toInstant(),
21+
ZoneId.of("Z"));
22+
private final ConversionServiceBridgeGenerator generator =
23+
new ConversionServiceBridgeGenerator(FIXED_CLOCK);
24+
25+
@Test
26+
void shouldGenerateMatchingOutput() throws IOException {
27+
// Given
28+
final ConversionServiceBridgeDescriptor descriptor =
29+
ConversionServiceBridgeDescriptor.builder()
30+
.bridgeClassName(
31+
ClassName.get(
32+
ConversionServiceBridgeGeneratorTest.class.getPackage().getName(),
33+
"ConversionServiceBridge"))
34+
.fromToMapping(Pair.of(ClassName.get("test", "Car"), ClassName.get("test", "CarDto")))
35+
.build();
36+
final StringWriter outputWriter = new StringWriter();
37+
38+
// When
39+
generator.writeConversionServiceBridge(descriptor, outputWriter);
40+
41+
// Then
42+
then(outputWriter.toString())
43+
.isEqualToIgnoringWhitespace(resourceToString("/ConversionServiceBridge.java", UTF_8));
44+
}
45+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package uk.co.kleindelao.mapstruct.spring.converter;
2+
3+
import org.springframework.beans.factory.annotation.Autowired;
4+
import org.springframework.core.convert.ConversionService;
5+
import org.springframework.stereotype.Component;
6+
import test.Car;
7+
import test.CarDto;
8+
9+
import javax.annotation.Generated;
10+
11+
@Generated(
12+
value = "uk.co.kleindelao.mapstruct.spring.converter.ConversionServiceBridgeGenerator",
13+
date = "2020-03-29T15:21:34.236Z"
14+
)
15+
@Component
16+
public class ConversionServiceBridge {
17+
@Autowired
18+
private ConversionService conversionService;
19+
20+
public CarDto mapCarToCarDto(final Car source) {
21+
return conversionService.convert(source, CarDto.class);
22+
}
23+
}

tests/build.gradle

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,23 @@
11
plugins {
2-
id "io.freefair.lombok" version "5.0.0-rc6"
2+
id 'io.freefair.lombok' version '5.0.0-rc6'
3+
id 'org.assertj.generator' version '0.0.6b'
34
}
45

56
dependencies {
67
annotationProcessor project(":additions")
78
// This dependency is used internally, and not exposed to consumers on their own compile classpath.
9+
compileOnly 'javax.annotation:jsr250-api:1.0'
810
implementation 'org.mapstruct:mapstruct:1.3.1.Final'
911
annotationProcessor 'org.mapstruct:mapstruct-processor:1.3.1.Final'
1012
implementation 'org.springframework:spring-core:5.0.0.RELEASE'
1113
implementation 'org.springframework:spring-context:5.0.0.RELEASE'
14+
testImplementation 'org.springframework:spring-test:5.0.0.RELEASE'
15+
testImplementation 'org.assertj:assertj-core:3.15.0'
1216
}
17+
18+
sourceSets {
19+
main {
20+
// must specify assertJ block to have it applied
21+
assertJ {}
22+
}
23+
}
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
package uk.co.kleindelao.mapstruct.spring.example;
22

33
import lombok.Data;
4+
import lombok.experimental.Accessors;
45

56
@Data
7+
@Accessors(chain = true)
68
public class Car {
79
private String make;
8-
private int numberOfSeats;
10+
private SeatConfiguration seatConfiguration;
911
private CarType type;
1012
}

0 commit comments

Comments
 (0)