Skip to content

Commit

Permalink
90 name collision with same class name in different packages (#92)
Browse files Browse the repository at this point in the history
* Prepared for specifying non default adapter method name.

* Fixes #90
  • Loading branch information
Chessray authored Aug 29, 2023
1 parent 12b1fb5 commit 80ac8a5
Show file tree
Hide file tree
Showing 24 changed files with 354 additions and 88 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package org.mapstruct.extensions.spring;

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

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

/**
* Overrides the default method name generated in the Adapter class. To be used exclusively on a
* {@link org.springframework.core.convert.converter.Converter} annotated as {@code @Mapper}.
*/
@Target(TYPE)
@Retention(SOURCE)
public @interface AdapterMethodName {
/**
* The method name to be used instead of the default.
*
* @return The method name to be used instead of the default.
*/
String value();
}
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
package org.mapstruct.extensions.spring;

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

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

/**
* Allows the specification of a conversion that is available via the {@link
* org.springframework.core.convert.ConversionService ConversionService}, but is <em>not</em>
* declared as a MapStruct mapper within the scope of the {@link SpringMapperConfig}.
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
@Target(TYPE)
@Retention(SOURCE)
public @interface ExternalConversion {
Class<?> sourceType();

Class<?> targetType();
String adapterMethodName() default "";
}
46 changes: 44 additions & 2 deletions docs/src/docs/asciidoc/chapter-3-mapper-as-converter.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -142,10 +142,12 @@ public interface MapperSpringConfig {

When the `conversionServiceBeanName` property is set, the built-in <<converterScan>> cannot be used in tests as it does not pick up this property.
However, setting the property `generateConverterScan` to `true` will create an alternative inside the project.
Important to note: This version will _not_ create a `ConversionService` with the given bean name, but merely register all Mappers with the bean identified by the given name. This leads to two practical differences:
Important to note: This version will _not_ create a `ConversionService` with the given bean name, but merely register all Mappers with the bean identified by the given name.
This leads to two practical differences:

- Unlike its <<testExtensions>> counterpart, this version is perfectly suited to be used in production code.
- In a test, the developer will still have to provide a `ConfigurableConversionService` themselves, e.g.:

====
[source,java,linenums]
[subs="verbatim,attributes"]
Expand All @@ -166,4 +168,44 @@ public class ConversionServiceAdapterIntegrationTest {
private ConversionService conversionService;
}
----
====
====

[[adapterMethodName]]
=== Modifying the name for the generated adapter method

By default, the adapter class will contain method names of the form `map<SourceTypeName>To<targetTypeName>`.
If you wish to change this, you can do so on a per-Mapper basis by applying the annotation `@AdapterMethodName`:

====
[source,java,linenums]
[subs="verbatim,attributes"]
----
@Mapper(config = MapperSpringConfig.class)
@AdapterMethodName("toDto")
public interface WheelMapper extends Converter<Wheel, WheelDto> {
@Override
WheelDto convert(Wheel source);
}
----
====

This changes the generated method name to be the annotation's `value` attribute:

====
[source,java,linenums]
[subs="verbatim,attributes"]
----
@Component
public class ConversionServiceAdapter {
private final ConversionService conversionService;
public ConversionServiceAdapter(@Lazy final ConversionService conversionService) {
this.conversionService = conversionService;
}
public WheelDto toDto(final Wheel source) {
return (WheelDto) conversionService.convert(source, TypeDescriptor.valueOf(Wheel.class), TypeDescriptor.valueOf(WheelDto.class));
}
}
----
====
40 changes: 39 additions & 1 deletion docs/src/docs/asciidoc/chapter-4-external-conversions.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,42 @@ public class ConversionServiceAdapter {
}
}
----
====
====

[[externalAdapterMethodName]]
=== Modifying the name for the generated adapter method

By default, the adapter class will contain method names of the form `map<SourceTypeName>To<targetTypeName>`.
If you wish to change this, you can do so on a per-conversion basis by setting the property `adapterMethodName`:

====
[source,java,linenums]
[subs="verbatim,attributes"]
----
@MapperConfig(componentModel = "spring")
@SpringMapperConfig(
externalConversions = @ExternalConversion(sourceType = Blob.class, targetType = byte[].class, adapterMethodName = "blob2Bytes"))
public interface MapstructConfig {}
----
====

This changes the generated method name to be the property's value:

====
[source,java,linenums]
[subs="verbatim,attributes"]
----
@Component
public class ConversionServiceAdapter {
private final ConversionService conversionService;
public ConversionServiceAdapter(@Lazy final ConversionService conversionService) {
this.conversionService = conversionService;
}
public byte[] blob2Bytes(final Blob source) {
return (byte[]) conversionService.convert(source, TypeDescriptor.valueOf(Blob.class), TypeDescriptor.valueOf(byte[].class));
}
}
----
====
1 change: 1 addition & 0 deletions examples/external-conversions/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@ dependencies {
annotationProcessor libs.mapstruct.processor
implementation libs.spring.context
implementation libs.spring.core
testImplementation libs.mockito
testImplementation libs.spring.test
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,6 @@
@SpringMapperConfig(
externalConversions = {
@ExternalConversion(sourceType = String.class, targetType = Locale.class),
@ExternalConversion(sourceType = Blob.class, targetType = byte[].class)
@ExternalConversion(sourceType = Blob.class, targetType = byte[].class, adapterMethodName = "blob2Bytes")
})
public interface MapstructConfig {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package org.mapstruct.extensions.spring.example.externalconversions;

import static java.nio.charset.StandardCharsets.UTF_8;
import static org.assertj.core.api.BDDAssertions.then;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;

import java.sql.Blob;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.TypeDescriptor;

@ExtendWith(MockitoExtension.class)
class ConversionServiceAdapterTest {
@Mock private ConversionService conversionService;

@InjectMocks private ConversionServiceAdapter conversionServiceAdapter;

@Test
void shouldCallConversionServiceFromGeneratedMethodWithOverriddenMethodName() {
final var blob = mock(Blob.class);
final var expectedBytes = "Hello World!".getBytes(UTF_8);
given(
conversionService.convert(
blob, TypeDescriptor.valueOf(Blob.class), TypeDescriptor.valueOf(byte[].class)))
.willReturn(expectedBytes);

final var actualBytes = conversionServiceAdapter.blob2Bytes(blob);

then(actualBytes).isSameAs(expectedBytes);
}
}
2 changes: 2 additions & 0 deletions examples/noconfig/build.gradle
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
dependencies {
annotationProcessor project(":extensions")
implementation projects.examples.model
implementation projects.annotations

testImplementation projects.testExtensions
testImplementation libs.assertj
Expand All @@ -10,5 +11,6 @@ dependencies {
annotationProcessor libs.mapstruct.processor
implementation libs.spring.context
implementation libs.spring.core
testImplementation libs.mockito
testImplementation libs.spring.test
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package org.mapstruct.extensions.spring.example.noconfig;

import org.mapstruct.Mapper;
import org.mapstruct.extensions.spring.AdapterMethodName;
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);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package org.mapstruct.extensions.spring.example;

import static org.assertj.core.api.BDDAssertions.then;
import static org.mapstruct.extensions.spring.example.WheelPosition.RIGHT_FRONT;
import static org.mockito.BDDMockito.given;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mapstruct.extensions.spring.converter.ConversionServiceAdapter;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.TypeDescriptor;

@ExtendWith(MockitoExtension.class)
class ConversionServiceAdapterTest {
@Mock private ConversionService conversionService;

@InjectMocks private ConversionServiceAdapter conversionServiceAdapter;

@Test
void shouldCallConversionServiceFromGeneratedMethodWithOverriddenMethodName() {
final var wheel = new Wheel();
wheel.setPosition(RIGHT_FRONT);
wheel.setDiameter(16);
final var expectedDto = new WheelDto();
expectedDto.setDiameter(16);
expectedDto.setPosition("RIGHT_FRONT");
given(
conversionService.convert(
wheel, TypeDescriptor.valueOf(Wheel.class), TypeDescriptor.valueOf(WheelDto.class)))
.willReturn(expectedDto);

final var actualDto = conversionServiceAdapter.toDto(wheel);

then(actualDto).isSameAs(expectedDto);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,12 @@
import static org.apache.commons.lang3.StringUtils.isNotEmpty;

import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.TypeName;
import java.util.List;
import org.apache.commons.lang3.tuple.Pair;

public class ConversionServiceAdapterDescriptor {
private ClassName adapterClassName;
private String conversionServiceBeanName;
private List<Pair<TypeName, TypeName>> fromToMappings;
private List<FromToMapping> fromToMappings;
private boolean lazyAnnotatedConversionServiceBean;

private boolean generateConverterScan;
Expand Down Expand Up @@ -38,12 +36,12 @@ public ConversionServiceAdapterDescriptor conversionServiceBeanName(
return this;
}

public List<Pair<TypeName, TypeName>> getFromToMappings() {
public List<FromToMapping> getFromToMappings() {
return fromToMappings;
}

public ConversionServiceAdapterDescriptor fromToMappings(
final List<Pair<TypeName, TypeName>> fromToMappings) {
final List<FromToMapping> fromToMappings) {
this.fromToMappings = fromToMappings;
return this;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
import java.util.Collection;
import java.util.Optional;
import java.util.stream.Stream;
import org.apache.commons.lang3.tuple.Pair;

public class ConversionServiceAdapterGenerator extends Generator {
private static final ClassName CONVERSION_SERVICE_CLASS_NAME =
Expand Down Expand Up @@ -151,45 +150,48 @@ private Iterable<MethodSpec> buildMappingMethods(
final FieldSpec injectedConversionServiceFieldSpec) {
return descriptor.getFromToMappings().stream()
.map(
sourceTargetPair ->
toMappingMethodSpec(injectedConversionServiceFieldSpec, sourceTargetPair))
fromToMapping ->
toMappingMethodSpec(injectedConversionServiceFieldSpec, fromToMapping))
.collect(toList());
}

private MethodSpec toMappingMethodSpec(
final FieldSpec injectedConversionServiceFieldSpec,
final Pair<TypeName, TypeName> sourceTargetPair) {
final ParameterSpec sourceParameterSpec = buildSourceParameterSpec(sourceTargetPair.getLeft());
final FromToMapping fromToMapping) {
final ParameterSpec sourceParameterSpec = buildSourceParameterSpec(fromToMapping.getSource());
return MethodSpec.methodBuilder(
String.format(
"map%sTo%s",
collectionOfNameIfApplicable(sourceTargetPair.getLeft()),
collectionOfNameIfApplicable(sourceTargetPair.getRight())))
fromToMapping
.getAdapterMethodName()
.orElse(
String.format(
"map%sTo%s",
collectionOfNameIfApplicable(fromToMapping.getSource()),
collectionOfNameIfApplicable(fromToMapping.getTarget()))))
.addParameter(sourceParameterSpec)
.addModifiers(PUBLIC)
.returns(sourceTargetPair.getRight())
.returns(fromToMapping.getTarget())
.addStatement(
String.format(
"return ($T) $N.convert($N, %s, %s)",
typeDescriptorFormat(sourceTargetPair.getLeft()),
typeDescriptorFormat(sourceTargetPair.getRight())),
typeDescriptorFormat(fromToMapping.getSource()),
typeDescriptorFormat(fromToMapping.getTarget())),
allTypeDescriptorArguments(
injectedConversionServiceFieldSpec, sourceParameterSpec, sourceTargetPair))
injectedConversionServiceFieldSpec, sourceParameterSpec, fromToMapping))
.build();
}

private Object[] allTypeDescriptorArguments(
final FieldSpec injectedConversionServiceFieldSpec,
final ParameterSpec sourceParameterSpec,
final Pair<TypeName, TypeName> sourceTargetPair) {
final FromToMapping fromToMapping) {
return concat(
concat(
Stream.of(
sourceTargetPair.getRight(),
fromToMapping.getTarget(),
injectedConversionServiceFieldSpec,
sourceParameterSpec),
typeDescriptorArguments(sourceTargetPair.getLeft())),
typeDescriptorArguments(sourceTargetPair.getRight()))
typeDescriptorArguments(fromToMapping.getSource())),
typeDescriptorArguments(fromToMapping.getTarget()))
.toArray();
}

Expand Down
Loading

0 comments on commit 80ac8a5

Please sign in to comment.