diff --git a/processor/src/main/java/org/mapstruct/ap/internal/processor/SpringComponentProcessor.java b/processor/src/main/java/org/mapstruct/ap/internal/processor/SpringComponentProcessor.java index 4efc537c3..84a672bcc 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/processor/SpringComponentProcessor.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/processor/SpringComponentProcessor.java @@ -8,7 +8,9 @@ package org.mapstruct.ap.internal.processor; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.HashSet; import java.util.List; +import java.util.Set; import org.mapstruct.ap.internal.gem.MappingConstantsGem; import org.mapstruct.ap.internal.model.Annotation; @@ -16,6 +18,13 @@ import org.mapstruct.ap.internal.model.Mapper; import org.mapstruct.ap.internal.model.annotation.AnnotationElement; import org.mapstruct.ap.internal.model.annotation.AnnotationElement.AnnotationElementType; +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.Element; +import javax.lang.model.element.PackageElement; +import javax.lang.model.element.TypeElement; + +import static javax.lang.model.element.ElementKind.PACKAGE; + /** * A {@link ModelElementProcessor} which converts the given {@link Mapper} * object into a Spring bean in case Spring is configured as the @@ -25,6 +34,7 @@ import org.mapstruct.ap.internal.model.annotation.AnnotationElement.AnnotationEl * @author Andreas Gudian */ public class SpringComponentProcessor extends AnnotationBasedComponentModelProcessor { + @Override protected String getComponentModelIdentifier() { return MappingConstantsGem.ComponentModelGem.SPRING; @@ -33,7 +43,9 @@ public class SpringComponentProcessor extends AnnotationBasedComponentModelProce @Override protected List getTypeAnnotations(Mapper mapper) { List typeAnnotations = new ArrayList<>(); - typeAnnotations.add( component() ); + if ( !isAlreadyAnnotatedAsSpringStereotype( mapper ) ) { + typeAnnotations.add( component() ); + } if ( mapper.getDecorator() != null ) { typeAnnotations.add( qualifierDelegate() ); @@ -91,4 +103,57 @@ public class SpringComponentProcessor extends AnnotationBasedComponentModelProce private Annotation component() { return new Annotation( getTypeFactory().getType( "org.springframework.stereotype.Component" ) ); } + + private boolean isAlreadyAnnotatedAsSpringStereotype(Mapper mapper) { + Set handledElements = new HashSet<>(); + return mapper.getAnnotations() + .stream() + .anyMatch( + annotation -> isOrIncludesComponentAnnotation( annotation, handledElements ) + ); + } + + private boolean isOrIncludesComponentAnnotation(Annotation annotation, Set handledElements) { + return isOrIncludesComponentAnnotation( + annotation.getType().getTypeElement(), handledElements + ); + } + + private boolean isOrIncludesComponentAnnotation(Element element, Set handledElements) { + if ( "org.springframework.stereotype.Component".equals( + ( (TypeElement) element ).getQualifiedName().toString() + )) { + return true; + } + + for ( AnnotationMirror annotationMirror : element.getAnnotationMirrors() ) { + Element annotationMirrorElement = annotationMirror.getAnnotationType().asElement(); + // Bypass java lang annotations to improve performance avoiding unnecessary checks + if ( !isAnnotationInPackage( annotationMirrorElement, "java.lang.annotation" ) && + !handledElements.contains( annotationMirrorElement ) ) { + handledElements.add( annotationMirrorElement ); + boolean isOrIncludesComponentAnnotation = isOrIncludesComponentAnnotation( + annotationMirrorElement, handledElements + ); + + if ( isOrIncludesComponentAnnotation ) { + return true; + } + } + } + + return false; + } + + private PackageElement getPackageOf( Element element ) { + while ( element.getKind() != PACKAGE ) { + element = element.getEnclosingElement(); + } + + return (PackageElement) element; + } + + private boolean isAnnotationInPackage(Element element, String packageFQN) { + return packageFQN.equals( getPackageOf( element ).getQualifiedName().toString() ); + } } diff --git a/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/spring/annotateWith/CustomStereotype.java b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/spring/annotateWith/CustomStereotype.java new file mode 100644 index 000000000..ccc196c5d --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/spring/annotateWith/CustomStereotype.java @@ -0,0 +1,21 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.injectionstrategy.spring.annotateWith; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import org.springframework.stereotype.Component; + +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Component +public @interface CustomStereotype { + String value() default ""; +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/spring/annotateWith/CustomerSpringComponentQualifiedMapper.java b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/spring/annotateWith/CustomerSpringComponentQualifiedMapper.java new file mode 100644 index 000000000..be08ff0b1 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/spring/annotateWith/CustomerSpringComponentQualifiedMapper.java @@ -0,0 +1,20 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.injectionstrategy.spring.annotateWith; + +import org.springframework.stereotype.Component; + +import org.mapstruct.AnnotateWith; +import org.mapstruct.Mapper; +import org.mapstruct.MappingConstants; + +/** + * @author Ben Zegveld + */ +@AnnotateWith( value = Component.class, elements = @AnnotateWith.Element( strings = "AnnotateWithComponent" ) ) +@Mapper( componentModel = MappingConstants.ComponentModel.SPRING ) +public interface CustomerSpringComponentQualifiedMapper { +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/spring/annotateWith/CustomerSpringControllerQualifiedMapper.java b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/spring/annotateWith/CustomerSpringControllerQualifiedMapper.java new file mode 100644 index 000000000..7203fad7a --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/spring/annotateWith/CustomerSpringControllerQualifiedMapper.java @@ -0,0 +1,19 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.injectionstrategy.spring.annotateWith; + +import org.mapstruct.AnnotateWith; +import org.mapstruct.Mapper; +import org.mapstruct.MappingConstants; +import org.springframework.stereotype.Controller; + +/** + * @author Jose Carlos Campanero Ortiz + */ +@AnnotateWith( value = Controller.class, elements = @AnnotateWith.Element( strings = "AnnotateWithController" ) ) +@Mapper( componentModel = MappingConstants.ComponentModel.SPRING ) +public interface CustomerSpringControllerQualifiedMapper { +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/spring/annotateWith/CustomerSpringCustomStereotypeQualifiedMapper.java b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/spring/annotateWith/CustomerSpringCustomStereotypeQualifiedMapper.java new file mode 100644 index 000000000..18a062497 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/spring/annotateWith/CustomerSpringCustomStereotypeQualifiedMapper.java @@ -0,0 +1,21 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.injectionstrategy.spring.annotateWith; + +import org.mapstruct.AnnotateWith; +import org.mapstruct.Mapper; +import org.mapstruct.MappingConstants; + +/** + * @author Jose Carlos Campanero Ortiz + */ +@AnnotateWith( + value = CustomStereotype.class, + elements = @AnnotateWith.Element( strings = "AnnotateWithCustomStereotype" ) +) +@Mapper( componentModel = MappingConstants.ComponentModel.SPRING ) +public interface CustomerSpringCustomStereotypeQualifiedMapper { +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/spring/annotateWith/CustomerSpringRepositoryQualifiedMapper.java b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/spring/annotateWith/CustomerSpringRepositoryQualifiedMapper.java new file mode 100644 index 000000000..7bbefbee2 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/spring/annotateWith/CustomerSpringRepositoryQualifiedMapper.java @@ -0,0 +1,19 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.injectionstrategy.spring.annotateWith; + +import org.mapstruct.AnnotateWith; +import org.mapstruct.Mapper; +import org.mapstruct.MappingConstants; +import org.springframework.stereotype.Repository; + +/** + * @author Jose Carlos Campanero Ortiz + */ +@AnnotateWith( value = Repository.class, elements = @AnnotateWith.Element( strings = "AnnotateWithRepository" ) ) +@Mapper( componentModel = MappingConstants.ComponentModel.SPRING ) +public interface CustomerSpringRepositoryQualifiedMapper { +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/spring/annotateWith/CustomerSpringServiceQualifiedMapper.java b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/spring/annotateWith/CustomerSpringServiceQualifiedMapper.java new file mode 100644 index 000000000..52dff8ef8 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/spring/annotateWith/CustomerSpringServiceQualifiedMapper.java @@ -0,0 +1,19 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.injectionstrategy.spring.annotateWith; + +import org.mapstruct.AnnotateWith; +import org.mapstruct.Mapper; +import org.mapstruct.MappingConstants; +import org.springframework.stereotype.Service; + +/** + * @author Jose Carlos Campanero Ortiz + */ +@AnnotateWith( value = Service.class, elements = @AnnotateWith.Element( strings = "AnnotateWithService" ) ) +@Mapper( componentModel = MappingConstants.ComponentModel.SPRING ) +public interface CustomerSpringServiceQualifiedMapper { +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/spring/annotateWith/SpringAnnotateWithMapperTest.java b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/spring/annotateWith/SpringAnnotateWithMapperTest.java new file mode 100644 index 000000000..cdc741814 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/spring/annotateWith/SpringAnnotateWithMapperTest.java @@ -0,0 +1,91 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.injectionstrategy.spring.annotateWith; + +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.WithSpring; +import org.mapstruct.ap.testutil.runner.GeneratedSource; + +/** + * Test field injection for component model spring. + * + * @author Filip Hrisafov + * @author Jose Carlos Campanero Ortiz + */ +@WithClasses({ + CustomerSpringComponentQualifiedMapper.class, + CustomerSpringControllerQualifiedMapper.class, + CustomerSpringServiceQualifiedMapper.class, + CustomerSpringRepositoryQualifiedMapper.class, + CustomStereotype.class, + CustomerSpringCustomStereotypeQualifiedMapper.class +}) +@IssueKey( "1427" ) +@WithSpring +public class SpringAnnotateWithMapperTest { + + @RegisterExtension + final GeneratedSource generatedSource = new GeneratedSource(); + + @ProcessorTest + public void shouldHaveComponentAnnotatedQualifiedMapper() { + + // then + generatedSource.forMapper( CustomerSpringComponentQualifiedMapper.class ) + .content() + .contains( "@Component(value = \"AnnotateWithComponent\")" ) + .doesNotContain( "@Component" + System.lineSeparator() ); + + } + + @ProcessorTest + public void shouldHaveControllerAnnotatedQualifiedMapper() { + + // then + generatedSource.forMapper( CustomerSpringControllerQualifiedMapper.class ) + .content() + .contains( "@Controller(value = \"AnnotateWithController\")" ) + .doesNotContain( "@Component" ); + + } + + @ProcessorTest + public void shouldHaveServiceAnnotatedQualifiedMapper() { + + // then + generatedSource.forMapper( CustomerSpringServiceQualifiedMapper.class ) + .content() + .contains( "@Service(value = \"AnnotateWithService\")" ) + .doesNotContain( "@Component" ); + + } + + @ProcessorTest + public void shouldHaveRepositoryAnnotatedQualifiedMapper() { + + // then + generatedSource.forMapper( CustomerSpringRepositoryQualifiedMapper.class ) + .content() + .contains( "@Repository(value = \"AnnotateWithRepository\")" ) + .doesNotContain( "@Component" ); + + } + + @ProcessorTest + public void shouldHaveCustomStereotypeAnnotatedQualifiedMapper() { + + // then + generatedSource.forMapper( CustomerSpringCustomStereotypeQualifiedMapper.class ) + .content() + .contains( "@CustomStereotype(value = \"AnnotateWithCustomStereotype\")" ) + .doesNotContain( "@Component" ); + + } + +}