Skip to content

Commit

Permalink
88 generate additional delegating mappers for inheritinverseconfigura…
Browse files Browse the repository at this point in the history
…tion (#94)

* WIP: Working quite well, but for some reason the example now generates two separate converter adapters in different packages.

* WIP: Unified into single Processor; however, we are still getting a separate adapter class for the second round.

We need some type of marker mechanism in order to prevent generating a second (meaningless) adapter in the second round.

* WIP: Seems to work now by using Filer.createResource instead of Filer.createSourceFile. Still requires polishing, integration tests and documentation.

* WIP: Switched to generating full class instead of abstract class, because the implementation generation was suppressed. Generated converters are still in the adapter. Requires polishing, integration tests and documentation.

* WIP: Resource generation meant that there were no class files, but this now works as long as the generated clas is NOT annotated as a @Mapper so the adapter generation does not get retriggered. Requires polishing, integration tests and documentation.

* Documentation completed

* Self-review: No need to change mapstruct core to implementation scope.

* Self-review: No new processor after all
  • Loading branch information
Chessray authored Sep 23, 2023
1 parent 80ac8a5 commit 0de9825
Show file tree
Hide file tree
Showing 37 changed files with 1,211 additions and 325 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package org.mapstruct.extensions.spring;

import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.SOURCE;

import java.lang.annotation.Retention;
import java.lang.annotation.Target;

/**
* Indicates that the system should generate a delegating {@link
* org.springframework.core.convert.converter.Converter} that will call the annotated method in its
* own {@link org.springframework.core.convert.converter.Converter#convert(Object)}.
*/
@Target(METHOD)
@Retention(SOURCE)
public @interface DelegatingConverter {}
62 changes: 62 additions & 0 deletions docs/src/docs/asciidoc/chapter-6-delegating-converters.asciidoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
[[delegatingConverters]]
== Delegating Converters

Applying MapStruct's https://mapstruct.org/documentation/stable/reference/html/#inverse-mappings[inverse mappings] requires a second mapping method inside a `@Mapper`-annotated interface or abstract class.
Since this is not how Spring sees `Converter` s, this inverted mapping method will be "invisible" to the `ConversionService`.
Adding the annotation `@DelegatingConverter` to the same method will lead to the generation of a separate class which implements `Converter` and does nothing more than call the annotated method in its own `convert`.
This class will have a method counterpart in the generated Adapter just like the ones annotated with `@Mapper`.

Take this `Converter` for example:

====
[source,java,linenums]
[subs="verbatim,attributes"]
----
@Mapper(config = MapperSpringConfig.class)
public interface CarMapper extends Converter<Car, CarDto> {
@Mapping(target = "seats", source = "seatConfiguration")
CarDto convert(Car car);
@InheritInverseConfiguration
@DelegatingConverter
Car invertConvert(CarDto carDto);
}
----
====

Notice the combination of `@InheritInverseConfiguration` and `@DelegatingConverter` on the `invertConvert` method.
The `@DelegatingConverter` will lead to a class like this:

====
[source,java,linenums]
[subs="verbatim,attributes"]
----
@Component
public class CarDtoToCarConverter implements Converter<CarDto, Car> {
private CarMapper delegateMapper;
public CarDtoToCarConverter(@Autowired final CarMapper delegateMapper) {
this.delegateMapper = delegateMapper;
}
@Override
public Car convert(final CarDto source) {
return delegateMapper.invertConvert(source);
}
}
----
====

The generated Adapter will contain a method counterpart like this:

====
[source,java,linenums]
[subs="verbatim,attributes"]
----
public Car mapCarDtoToCar(final CarDto source) {
return (Car) conversionService.convert(source, TypeDescriptor.valueOf(CarDto.class), TypeDescriptor.valueOf(Car.class));
}
----
====

Please note: The behaviour of `@DelegatingConverter` is _not_ tied to `@InheritInverseConfiguration`; however, this is the only use case we are aware of where it provides meaningful value.
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,6 @@ include::chapter-3-mapper-as-converter.asciidoc[]

include::chapter-4-external-conversions.asciidoc[]

include::chapter-5-test-extensions.asciidoc[]
include::chapter-5-test-extensions.asciidoc[]

include::chapter-6-delegating-converters.asciidoc[]
14 changes: 14 additions & 0 deletions examples/delegatingconverter/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
dependencies {
annotationProcessor project(":extensions")
implementation projects.examples.model
implementation projects.annotations

testImplementation projects.testExtensions
testImplementation libs.assertj
testImplementation libs.bundles.junit.jupiter
implementation libs.mapstruct.core
annotationProcessor libs.mapstruct.processor
implementation libs.spring.context
implementation libs.spring.core
testImplementation libs.spring.test
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package org.mapstruct.extensions.spring.example.delegatingconverter;

import org.mapstruct.InheritInverseConfiguration;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.extensions.spring.DelegatingConverter;
import org.mapstruct.extensions.spring.example.Car;
import org.mapstruct.extensions.spring.example.CarDto;
import org.springframework.core.convert.converter.Converter;

@Mapper(config = MapperSpringConfig.class)
public interface CarMapper extends Converter<Car, CarDto> {
@Mapping(target = "seats", source = "seatConfiguration")
CarDto convert(Car car);

@InheritInverseConfiguration
@DelegatingConverter
Car invertConvert(CarDto carDto);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package org.mapstruct.extensions.spring.example.delegatingconverter;

import org.mapstruct.MapperConfig;
import org.mapstruct.extensions.spring.SpringMapperConfig;

@MapperConfig(componentModel = "spring", uses = ConversionServiceAdapter.class)
@SpringMapperConfig
public interface MapperSpringConfig {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package org.mapstruct.extensions.spring.example.delegatingconverter;

import org.mapstruct.InheritInverseConfiguration;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.extensions.spring.DelegatingConverter;
import org.mapstruct.extensions.spring.example.SeatConfiguration;
import org.mapstruct.extensions.spring.example.SeatConfigurationDto;
import org.springframework.core.convert.converter.Converter;

@Mapper(config = MapperSpringConfig.class)
public interface SeatConfigurationMapper extends Converter<SeatConfiguration, SeatConfigurationDto> {
@Mapping(target = "seatCount", source = "numberOfSeats")
@Mapping(target = "material", source = "seatMaterial")
SeatConfigurationDto convert(SeatConfiguration seatConfiguration);

@InheritInverseConfiguration
@DelegatingConverter
SeatConfiguration invertConvert(SeatConfigurationDto seatConfigurationDto);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package org.mapstruct.extensions.spring.example.delegatingconverter;

import org.mapstruct.InheritInverseConfiguration;
import org.mapstruct.Mapper;
import org.mapstruct.extensions.spring.AdapterMethodName;
import org.mapstruct.extensions.spring.DelegatingConverter;
import org.mapstruct.extensions.spring.example.Wheel;
import org.mapstruct.extensions.spring.example.WheelDto;
import org.springframework.core.convert.converter.Converter;

@Mapper(config = MapperSpringConfig.class)
@AdapterMethodName("toDto")
public interface WheelMapper extends Converter<Wheel, WheelDto> {
@Override
WheelDto convert(Wheel source);

@InheritInverseConfiguration
@DelegatingConverter
Wheel invertConvert(WheelDto wheelDto);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package org.mapstruct.extensions.spring.example.delegatingconverter;

import java.util.List;
import org.mapstruct.Mapper;
import org.mapstruct.extensions.spring.example.WheelDto;
import org.mapstruct.extensions.spring.example.Wheels;
import org.springframework.core.convert.converter.Converter;

@Mapper(config = MapperSpringConfig.class)
public interface WheelsDtoListMapper extends Converter<List<WheelDto>, Wheels> {
@Override
Wheels convert(List<WheelDto> source);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package org.mapstruct.extensions.spring.example.delegatingconverter;

import java.util.List;
import org.mapstruct.Mapper;
import org.mapstruct.extensions.spring.example.Wheel;
import org.mapstruct.extensions.spring.example.WheelDto;
import org.mapstruct.extensions.spring.example.Wheels;
import org.springframework.core.convert.converter.Converter;

@Mapper(config = MapperSpringConfig.class, imports = Wheel.class)
public interface WheelsMapper extends Converter<Wheels, List<WheelDto>> {
@Override
List<WheelDto> convert(Wheels source);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package org.mapstruct.extensions.spring.example.delegatingconverter;

import static org.assertj.core.api.BDDAssertions.then;
import static org.mapstruct.extensions.spring.example.CarType.OTHER;
import static org.mapstruct.extensions.spring.example.SeatMaterial.LEATHER;
import static org.mapstruct.extensions.spring.example.WheelPosition.RIGHT_FRONT;

import java.util.ArrayList;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mapstruct.extensions.spring.example.*;
import org.mapstruct.extensions.spring.test.ConverterScan;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.ConversionService;
import org.springframework.test.context.junit.jupiter.SpringExtension;

@ExtendWith(SpringExtension.class)
public class ConversionServiceAdapterIntegrationTest {
private static final String TEST_MAKE = "Volvo";
private static final CarType TEST_CAR_TYPE = OTHER;
private static final int TEST_NUMBER_OF_SEATS = 5;
private static final SeatMaterial TEST_SEAT_MATERIAL = LEATHER;
private static final int TEST_DIAMETER = 20;
private static final WheelPosition TEST_WHEEL_POSITION = RIGHT_FRONT;

@Autowired private ConversionService conversionService;

@Configuration
@ConverterScan(basePackageClasses = MapperSpringConfig.class)
static class ScanConfiguration{}

@Test
void shouldKnowDelegatingMappers() {
then(conversionService.canConvert(CarDto.class, Car.class)).isTrue();
then(conversionService.canConvert(SeatConfigurationDto.class, SeatConfiguration.class))
.isTrue();
then(conversionService.canConvert(WheelDto.class, Wheel.class)).isTrue();
}

@Test
void shouldMapAllAttributes() {
// Given
final var carDto = new CarDto();
carDto.setMake(TEST_MAKE);
carDto.setType(TEST_CAR_TYPE.name());
final var seatConfigurationDto = new SeatConfigurationDto();
seatConfigurationDto.setMaterial(TEST_SEAT_MATERIAL.name());
seatConfigurationDto.setSeatCount(TEST_NUMBER_OF_SEATS);
carDto.setSeats(seatConfigurationDto);
final var wheelsList = new ArrayList<WheelDto>();
final var wheel = new WheelDto();
wheel.setDiameter(TEST_DIAMETER);
wheel.setPosition(TEST_WHEEL_POSITION.name());
wheelsList.add(wheel);
carDto.setWheels(wheelsList);

// When
final var mappedCar = conversionService.convert(carDto, Car.class);

// Then
then(mappedCar).isNotNull();
then(mappedCar.getMake()).isEqualTo(TEST_MAKE);
then(mappedCar.getType()).isEqualTo(TEST_CAR_TYPE);
final var mappedCarSeatConfiguration = mappedCar.getSeatConfiguration();
then(mappedCarSeatConfiguration).isNotNull();
then(mappedCarSeatConfiguration.getNumberOfSeats()).isEqualTo(TEST_NUMBER_OF_SEATS);
then(mappedCarSeatConfiguration.getSeatMaterial()).isEqualTo(TEST_SEAT_MATERIAL);
final var expectedWheel = new Wheel();
expectedWheel.setPosition(TEST_WHEEL_POSITION);
expectedWheel.setDiameter(TEST_DIAMETER);
then(mappedCar.getWheels()).hasSize(1).containsExactly(expectedWheel);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package org.mapstruct.extensions.spring.converter;

import com.squareup.javapoet.TypeSpec;

import java.io.Writer;
import java.time.Clock;

public abstract class AdapterRelatedGenerator extends Generator {
protected AdapterRelatedGenerator(Clock clock) {
super(clock);
}

public final void writeGeneratedCodeToOutput(
final ConversionServiceAdapterDescriptor descriptor, final Writer out) {
writeGeneratedCodeToOutput(
() -> descriptor.getAdapterClassName().packageName(),
() -> createMainTypeSpec(descriptor),
out);
}

protected abstract TypeSpec createMainTypeSpec(ConversionServiceAdapterDescriptor descriptor);
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,14 @@
import static java.util.stream.Collectors.toList;
import static java.util.stream.Stream.concat;
import static javax.lang.model.element.Modifier.*;
import static javax.tools.Diagnostic.Kind.WARNING;
import static org.mapstruct.extensions.spring.converter.TypeNameUtils.rawType;

import com.squareup.javapoet.*;
import java.time.Clock;
import java.util.Collection;
import java.util.Optional;
import java.util.stream.Stream;

public class ConversionServiceAdapterGenerator extends Generator {
public class ConversionServiceAdapterGenerator extends AdapterRelatedGenerator {
private static final ClassName CONVERSION_SERVICE_CLASS_NAME =
ClassName.get("org.springframework.core.convert", "ConversionService");
private static final String CONVERSION_SERVICE_FIELD_NAME = "conversionService";
Expand Down Expand Up @@ -83,68 +82,6 @@ private static AnnotationSpec buildLazyAnnotation() {
return AnnotationSpec.builder(LAZY_ANNOTATION_CLASS_NAME).build();
}

private String collectionOfMethodName(final ParameterizedTypeName parameterizedTypeName) {
if (isCollectionWithGenericParameter(parameterizedTypeName)) {
return simpleName(parameterizedTypeName)
+ "Of"
+ collectionOfNameIfApplicable(parameterizedTypeName.typeArguments.iterator().next());
}

return simpleName(parameterizedTypeName);
}

private boolean isCollectionWithGenericParameter(
final ParameterizedTypeName parameterizedTypeName) {
return parameterizedTypeName.typeArguments != null
&& !parameterizedTypeName.typeArguments.isEmpty()
&& isCollection(parameterizedTypeName);
}

private boolean isCollection(final ParameterizedTypeName parameterizedTypeName) {
try {
return Collection.class.isAssignableFrom(
Class.forName(parameterizedTypeName.rawType.canonicalName()));
} catch (ClassNotFoundException e) {
getProcessingEnvironment()
.getMessager()
.printMessage(
WARNING,
"Caught ClassNotFoundException when trying to resolve parameterized type: "
+ e.getMessage());
return false;
}
}

private String collectionOfNameIfApplicable(final TypeName typeName) {
if (typeName instanceof ParameterizedTypeName) {
return collectionOfMethodName((ParameterizedTypeName) typeName);
}
return simpleName(typeName);
}

private static String simpleName(final TypeName typeName) {
final TypeName rawType = rawType(typeName);
if (rawType instanceof ArrayTypeName) {
return arraySimpleName((ArrayTypeName) rawType);
} else if (rawType instanceof ClassName) {
return ((ClassName) rawType).simpleName();
} else return String.valueOf(typeName);
}

private static String arraySimpleName(final ArrayTypeName arrayTypeName) {
return "ArrayOf"
+ (arrayTypeName.componentType instanceof ArrayTypeName
? arraySimpleName((ArrayTypeName) arrayTypeName.componentType)
: arrayTypeName.componentType);
}

private static TypeName rawType(final TypeName typeName) {
if (typeName instanceof ParameterizedTypeName) {
return ((ParameterizedTypeName) typeName).rawType;
}
return typeName;
}

private Iterable<MethodSpec> buildMappingMethods(
final ConversionServiceAdapterDescriptor descriptor,
final FieldSpec injectedConversionServiceFieldSpec) {
Expand Down Expand Up @@ -224,5 +161,4 @@ private static FieldSpec buildConversionServiceFieldSpec() {
CONVERSION_SERVICE_CLASS_NAME, CONVERSION_SERVICE_FIELD_NAME, PRIVATE, FINAL)
.build();
}

}
Loading

0 comments on commit 0de9825

Please sign in to comment.