From 5e81c4daf7363aa345629c14667dca47c156fc8e Mon Sep 17 00:00:00 2001 From: hduelme Date: Sun, 7 Jun 2026 22:41:20 +0200 Subject: [PATCH] introduce gem for enums --- .../org/mapstruct/tools/gem/GemValue.java | 21 + .../org/mapstruct/tools/gem/RegisterGem.java | 25 + .../org/mapstruct/tools/gem/GemValueTest.java | 429 +++++++++++++++++- .../tools/gem/processor/GemEnum.java | 47 ++ .../tools/gem/processor/GemInfo.java | 4 +- .../tools/gem/processor/GemProcessor.java | 161 +++++-- .../tools/gem/processor/GemValueType.java | 21 +- .../mapstruct/tools/gem/processor/Enum.ftl | 18 + .../org/mapstruct/tools/gem/processor/Gem.ftl | 8 + .../tools/gem/processor/ProcessorTest.java | 241 +++++++++- .../gem/processor/EnumAnnotationGem.java | 231 ++++++++++ .../processor/EnumAnnotationPackageGem.java | 232 ++++++++++ .../processor/EnumAnnotationRegisterGem.java | 231 ++++++++++ .../tools/gem/processor/SimpleEnumGem.java | 10 + .../gem/processor/SomeAnnotationGem.java | 58 ++- .../java/org/mapstruct/tools/gem/Tester.java | 3 +- .../tools/gem/test/SomeAnnotation.java | 6 +- .../gem/test/enummapping/EnumAnnotation.java | 20 + .../gem/test/enummapping/SimpleEnum.java | 10 + 19 files changed, 1719 insertions(+), 57 deletions(-) create mode 100644 api/src/main/java/org/mapstruct/tools/gem/RegisterGem.java create mode 100644 processor/src/main/java/org/mapstruct/tools/gem/processor/GemEnum.java create mode 100644 processor/src/main/resources/org/mapstruct/tools/gem/processor/Enum.ftl create mode 100644 processor/src/test/resources/fixtures/org/mapstruct/tools/gem/processor/EnumAnnotationGem.java create mode 100644 processor/src/test/resources/fixtures/org/mapstruct/tools/gem/processor/EnumAnnotationPackageGem.java create mode 100644 processor/src/test/resources/fixtures/org/mapstruct/tools/gem/processor/EnumAnnotationRegisterGem.java create mode 100644 processor/src/test/resources/fixtures/org/mapstruct/tools/gem/processor/SimpleEnumGem.java create mode 100644 test/src/main/java/org/mapstruct/tools/gem/test/enummapping/EnumAnnotation.java create mode 100644 test/src/main/java/org/mapstruct/tools/gem/test/enummapping/SimpleEnum.java diff --git a/api/src/main/java/org/mapstruct/tools/gem/GemValue.java b/api/src/main/java/org/mapstruct/tools/gem/GemValue.java index 7652cac..23dc3ef 100644 --- a/api/src/main/java/org/mapstruct/tools/gem/GemValue.java +++ b/api/src/main/java/org/mapstruct/tools/gem/GemValue.java @@ -48,6 +48,16 @@ public static GemValue createEnum(AnnotationValue annotationValue, return new GemValue<>( value, defaultValue, annotationValue ); } + public static > GemValue createEnum(AnnotationValue annotationValue, + AnnotationValue annotationDefaultValue, + Class enumClass) { + ValueAnnotationValueVisitor visitor = new ValueAnnotationValueVisitor<>( + variableElement -> Enum.valueOf( enumClass, variableElement.getSimpleName().toString() ) ); + E value = visit( annotationValue, visitor, VariableElement.class ); + E defaultValue = visit( annotationDefaultValue, visitor, VariableElement.class ); + return new GemValue<>( value, defaultValue, annotationValue ); + } + public static GemValue> createEnumArray(AnnotationValue annotationValue, AnnotationValue annotationDefaultValue) { ValueAnnotationValueListVisitor visitor = new ValueAnnotationValueListVisitor<>( @@ -58,6 +68,17 @@ public static GemValue> createEnumArray(AnnotationValue annotationV return new GemValue<>( value, defaultValue, annotationValue ); } + public static > GemValue> createEnumArray(AnnotationValue annotationValue, + AnnotationValue annotationDefaultValue, + Class enumClass) { + ValueAnnotationValueListVisitor visitor = new ValueAnnotationValueListVisitor<>( + variableElement -> Enum.valueOf( enumClass, variableElement.getSimpleName().toString() ) ); + List value = visitList( annotationValue, visitor, VariableElement.class ); + List defaultValue = visitList( annotationDefaultValue, visitor, VariableElement.class ); + + return new GemValue<>( value, defaultValue, annotationValue ); + } + public static GemValue create(AnnotationValue annotationValue, AnnotationValue annotationDefaultValue, Function creator) { ValueAnnotationValueVisitor visitor = new ValueAnnotationValueVisitor<>( creator ); diff --git a/api/src/main/java/org/mapstruct/tools/gem/RegisterGem.java b/api/src/main/java/org/mapstruct/tools/gem/RegisterGem.java new file mode 100644 index 0000000..f56b102 --- /dev/null +++ b/api/src/main/java/org/mapstruct/tools/gem/RegisterGem.java @@ -0,0 +1,25 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at https://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.tools.gem; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Registers the annotated {@code enum} as the gem enum for {@code value()}. + * The annotated enum must contain exactly all constants of the {@code enum} specified in {@code value()}. + */ +@Retention(RetentionPolicy.SOURCE) +@Target({ ElementType.TYPE }) +public @interface RegisterGem { + + /** + * @return the {@code enum} which should be represented by the annotated class. + */ + Class> value(); +} diff --git a/api/src/test/java/org/mapstruct/tools/gem/GemValueTest.java b/api/src/test/java/org/mapstruct/tools/gem/GemValueTest.java index b366cde..4ab2734 100644 --- a/api/src/test/java/org/mapstruct/tools/gem/GemValueTest.java +++ b/api/src/test/java/org/mapstruct/tools/gem/GemValueTest.java @@ -5,18 +5,28 @@ */ package org.mapstruct.tools.gem; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; - -import javax.lang.model.element.AnnotationValue; -import javax.lang.model.element.AnnotationValueVisitor; - +import java.lang.annotation.Annotation; import java.util.ArrayList; import java.util.Collections; +import java.util.HashSet; import java.util.List; +import java.util.Set; +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.AnnotationValue; +import javax.lang.model.element.AnnotationValueVisitor; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ElementVisitor; +import javax.lang.model.element.Modifier; +import javax.lang.model.element.Name; +import javax.lang.model.element.VariableElement; +import javax.lang.model.type.TypeMirror; import static org.assertj.core.api.Assertions.assertThat; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + class GemValueTest { static class SimpleAnnotationValue implements AnnotationValue { @@ -124,6 +134,25 @@ public R accept(AnnotationValueVisitor v, P p) { } } + static class GenericArrayAnnotationValue implements AnnotationValue { + + private final List value; + + GenericArrayAnnotationValue(AnnotationValue... values) { + this.value = java.util.Arrays.asList( values ); + } + + @Override + public Object getValue() { + return value; + } + + @Override + public R accept(AnnotationValueVisitor v, P p) { + return v.visitArray( value, p ); + } + } + @Nested class CreateArrayTest { @@ -192,4 +221,392 @@ void createArrayValueInvalid() { .as( "getValueOrElseGet should return other" ).isEqualTo( Collections.singletonList( 3 ) ); } } + + @Nested + class CreateEnumAsStringValueTest { + + @Test + void createSimpleValue() { + EnumAnnotationValue annotationValue = new EnumAnnotationValue( TestEnum.A ); + GemValue gemValue = GemValue.createEnum( annotationValue, new EnumAnnotationValue( TestEnum.B ) ); + assertThat( gemValue ).isNotNull(); + assertThat( gemValue.isValid() ).isTrue(); + assertThat( gemValue.hasValue() ).isTrue(); + assertThat( gemValue.getValue() ).isEqualTo( "A" ); + assertThat( gemValue.getDefaultValue() ).isEqualTo( "B" ); + assertThat( gemValue.get() ).as( "get should return value" ).isEqualTo( "A" ); + assertThat( gemValue.getAnnotationValue() ).isEqualTo( annotationValue ); + assertThat( gemValue.getValueOrElseGet( () -> "C" ) ) + .as( "getValueOrElseGet should return value" ).isEqualTo( "A" ); + } + + @Test + void createSimpleValueWithoutAnnotationValue() { + GemValue gemValue = GemValue.createEnum( null, new EnumAnnotationValue( TestEnum.B ) ); + assertThat( gemValue ).isNotNull(); + assertThat( gemValue.isValid() ).isTrue(); + assertThat( gemValue.hasValue() ).isFalse(); + assertThat( gemValue.getValue() ).isNull(); + assertThat( gemValue.getDefaultValue() ).isEqualTo( "B" ); + assertThat( gemValue.get() ).as( "get should return defaultValue" ).isEqualTo( "B" ); + assertThat( gemValue.getAnnotationValue() ).isNull(); + assertThat( gemValue.getValueOrElseGet( () -> "C" ) ) + .as( "getValueOrElseGet should return other" ).isEqualTo( "C" ); + } + + @Test + void createSimpleValueWithoutAnnotationDefaultValue() { + EnumAnnotationValue annotationValue = new EnumAnnotationValue( TestEnum.A ); + GemValue gemValue = GemValue.createEnum( annotationValue, null ); + assertThat( gemValue ).isNotNull(); + assertThat( gemValue.isValid() ).isTrue(); + assertThat( gemValue.hasValue() ).isTrue(); + assertThat( gemValue.getValue() ).isEqualTo( "A" ); + assertThat( gemValue.getDefaultValue() ).isNull(); + assertThat( gemValue.get() ).as( "get should return value" ).isEqualTo( "A" ); + assertThat( gemValue.getAnnotationValue() ).isEqualTo( annotationValue ); + assertThat( gemValue.getValueOrElseGet( () -> "C" ) ) + .as( "getValueOrElseGet should return value" ).isEqualTo( "A" ); + } + + @Test + void createSimpleValueInvalid() { + GemValue gemValue = GemValue.createEnum( null, null ); + assertThat( gemValue ).isNotNull(); + assertThat( gemValue.isValid() ).isFalse(); + assertThat( gemValue.hasValue() ).isFalse(); + assertThat( gemValue.getValue() ).isNull(); + assertThat( gemValue.getDefaultValue() ).isNull(); + assertThat( gemValue.get() ).as( "get should return null" ).isNull(); + assertThat( gemValue.getAnnotationValue() ).isNull(); + assertThat( gemValue.getValueOrElseGet( () -> "C" ) ) + .as( "getValueOrElseGet should return other" ).isEqualTo( "C" ); + } + } + + @Nested + class CreateEnumArrayTest { + + @Test + void createEnumArrayValue() { + EnumAnnotationValue annotationValue = new EnumAnnotationValue( TestEnum.A ); + EnumAnnotationValue defaultAnnotationValue = new EnumAnnotationValue( TestEnum.B ); + + GenericArrayAnnotationValue arrayValue = new GenericArrayAnnotationValue( annotationValue ); + GenericArrayAnnotationValue defaultArrayValue = new GenericArrayAnnotationValue( defaultAnnotationValue ); + + GemValue> gemValue = GemValue.createEnumArray( arrayValue, defaultArrayValue ); + + assertThat( gemValue ).isNotNull(); + assertThat( gemValue.isValid() ).isTrue(); + assertThat( gemValue.hasValue() ).isTrue(); + assertThat( gemValue.getValue() ).isEqualTo( Collections.singletonList( "A" ) ); + assertThat( gemValue.getDefaultValue() ).isEqualTo( Collections.singletonList( "B" ) ); + assertThat( gemValue.get() ).as( "get should return value" ).isEqualTo( Collections.singletonList( "A" ) ); + assertThat( gemValue.getAnnotationValue() ).isEqualTo( arrayValue ); + assertThat( gemValue.getValueOrElseGet( Collections::emptyList ) ) + .as( "getValueOrElseGet should return value" ).isEqualTo( Collections.singletonList( "A" ) ); + } + + @Test + void createEnumArrayValueWithoutAnnotationValue() { + EnumAnnotationValue defaultAnnotationValue = new EnumAnnotationValue( TestEnum.B ); + GenericArrayAnnotationValue defaultArrayValue = new GenericArrayAnnotationValue( defaultAnnotationValue ); + + GemValue> gemValue = GemValue.createEnumArray( null, defaultArrayValue ); + + assertThat( gemValue ).isNotNull(); + assertThat( gemValue.isValid() ).isTrue(); + assertThat( gemValue.hasValue() ).isFalse(); + assertThat( gemValue.getValue() ).isNull(); + assertThat( gemValue.getDefaultValue() ).isEqualTo( Collections.singletonList( "B" ) ); + assertThat( gemValue.get() ).as( "get should return defaultValue" ) + .isEqualTo( Collections.singletonList( "B" ) ); + assertThat( gemValue.getAnnotationValue() ).isNull(); + assertThat( gemValue.getValueOrElseGet( Collections::emptyList ) ) + .as( "getValueOrElseGet should return other" ).isEqualTo( Collections.emptyList() ); + } + + @Test + void createEnumArrayValueWithoutAnnotationDefaultValue() { + EnumAnnotationValue annotationValue = new EnumAnnotationValue( TestEnum.A ); + GenericArrayAnnotationValue arrayValue = new GenericArrayAnnotationValue( annotationValue ); + + GemValue> gemValue = GemValue.createEnumArray( arrayValue, null ); + + assertThat( gemValue ).isNotNull(); + assertThat( gemValue.isValid() ).isTrue(); + assertThat( gemValue.hasValue() ).isTrue(); + assertThat( gemValue.getValue() ).isEqualTo( Collections.singletonList( "A" ) ); + assertThat( gemValue.getDefaultValue() ).isNull(); + assertThat( gemValue.get() ).as( "get should return value" ).isEqualTo( Collections.singletonList( "A" ) ); + assertThat( gemValue.getAnnotationValue() ).isEqualTo( arrayValue ); + assertThat( gemValue.getValueOrElseGet( Collections::emptyList ) ) + .as( "getValueOrElseGet should return value" ).isEqualTo( Collections.singletonList( "A" ) ); + } + + @Test + void createEnumArrayValueInvalid() { + GemValue> gemValue = GemValue.createEnumArray( null, null ); + + assertThat( gemValue ).isNotNull(); + assertThat( gemValue.isValid() ).isFalse(); + assertThat( gemValue.hasValue() ).isFalse(); + assertThat( gemValue.getValue() ).isNull(); + assertThat( gemValue.getDefaultValue() ).isNull(); + assertThat( gemValue.get() ).as( "get should return null" ).isNull(); + assertThat( gemValue.getAnnotationValue() ).isNull(); + assertThat( gemValue.getValueOrElseGet( Collections::emptyList ) ) + .as( "getValueOrElseGet should return other" ).isEqualTo( Collections.emptyList() ); + } + } + + @Nested + class CreateEnumAsEnumTest { + + @Test + void createSimpleValue() { + EnumAnnotationValue annotationValue = new EnumAnnotationValue( TestEnum.A ); + GemValue gemValue = GemValue.createEnum( annotationValue, new EnumAnnotationValue( TestEnum.B ), + TestEnum.class ); + assertThat( gemValue ).isNotNull(); + assertThat( gemValue.isValid() ).isTrue(); + assertThat( gemValue.hasValue() ).isTrue(); + assertThat( gemValue.getValue() ).isEqualTo( TestEnum.A ); + assertThat( gemValue.getDefaultValue() ).isEqualTo( TestEnum.B ); + assertThat( gemValue.get() ).as( "get should return value" ).isEqualTo( TestEnum.A ); + assertThat( gemValue.getAnnotationValue() ).isEqualTo( annotationValue ); + assertThat( gemValue.getValueOrElseGet( () -> TestEnum.C ) ) + .as( "getValueOrElseGet should return value" ).isEqualTo( TestEnum.A ); + } + + @Test + void createSimpleValueWithoutAnnotationValue() { + GemValue gemValue = GemValue.createEnum( null, new EnumAnnotationValue( TestEnum.B ), + TestEnum.class ); + assertThat( gemValue ).isNotNull(); + assertThat( gemValue.isValid() ).isTrue(); + assertThat( gemValue.hasValue() ).isFalse(); + assertThat( gemValue.getValue() ).isNull(); + assertThat( gemValue.getDefaultValue() ).isEqualTo( TestEnum.B ); + assertThat( gemValue.get() ).as( "get should return defaultValue" ).isEqualTo( TestEnum.B ); + assertThat( gemValue.getAnnotationValue() ).isNull(); + assertThat( gemValue.getValueOrElseGet( () -> TestEnum.C ) ) + .as( "getValueOrElseGet should return other" ).isEqualTo( TestEnum.C ); + } + + @Test + void createSimpleValueWithoutAnnotationDefaultValue() { + EnumAnnotationValue annotationValue = new EnumAnnotationValue( TestEnum.A ); + GemValue gemValue = GemValue.createEnum( annotationValue, null, TestEnum.class ); + assertThat( gemValue ).isNotNull(); + assertThat( gemValue.isValid() ).isTrue(); + assertThat( gemValue.hasValue() ).isTrue(); + assertThat( gemValue.getValue() ).isEqualTo( TestEnum.A ); + assertThat( gemValue.getDefaultValue() ).isNull(); + assertThat( gemValue.get() ).as( "get should return value" ).isEqualTo( TestEnum.A ); + assertThat( gemValue.getAnnotationValue() ).isEqualTo( annotationValue ); + assertThat( gemValue.getValueOrElseGet( () -> TestEnum.C ) ) + .as( "getValueOrElseGet should return value" ).isEqualTo( TestEnum.A ); + } + + @Test + void createSimpleValueInvalid() { + GemValue gemValue = GemValue.createEnum( null, null, TestEnum.class ); + assertThat( gemValue ).isNotNull(); + assertThat( gemValue.isValid() ).isFalse(); + assertThat( gemValue.hasValue() ).isFalse(); + assertThat( gemValue.getValue() ).isNull(); + assertThat( gemValue.getDefaultValue() ).isNull(); + assertThat( gemValue.get() ).as( "get should return null" ).isNull(); + assertThat( gemValue.getAnnotationValue() ).isNull(); + assertThat( gemValue.getValueOrElseGet( () -> TestEnum.C ) ) + .as( "getValueOrElseGet should return other" ).isEqualTo( TestEnum.C ); + } + } + + @Nested + class CreateEnumArrayAsEnumTest { + + @Test + void createEnumArrayValue() { + EnumAnnotationValue annotationValue = new EnumAnnotationValue( TestEnum.A ); + EnumAnnotationValue defaultAnnotationValue = new EnumAnnotationValue( TestEnum.B ); + + GenericArrayAnnotationValue arrayValue = new GenericArrayAnnotationValue( annotationValue ); + GenericArrayAnnotationValue defaultArrayValue = new GenericArrayAnnotationValue( defaultAnnotationValue ); + + GemValue> gemValue = GemValue.createEnumArray( arrayValue, defaultArrayValue, + TestEnum.class ); + + assertThat( gemValue ).isNotNull(); + assertThat( gemValue.isValid() ).isTrue(); + assertThat( gemValue.hasValue() ).isTrue(); + assertThat( gemValue.getValue() ).isEqualTo( Collections.singletonList( TestEnum.A ) ); + assertThat( gemValue.getDefaultValue() ).isEqualTo( Collections.singletonList( TestEnum.B ) ); + assertThat( gemValue.get() ).as( "get should return value" ) + .isEqualTo( Collections.singletonList( TestEnum.A ) ); + assertThat( gemValue.getAnnotationValue() ).isEqualTo( arrayValue ); + assertThat( gemValue.getValueOrElseGet( Collections::emptyList ) ) + .as( "getValueOrElseGet should return value" ).isEqualTo( Collections.singletonList( TestEnum.A ) ); + } + + @Test + void createEnumArrayValueWithoutAnnotationValue() { + EnumAnnotationValue defaultAnnotationValue = new EnumAnnotationValue( TestEnum.B ); + GenericArrayAnnotationValue defaultArrayValue = new GenericArrayAnnotationValue( defaultAnnotationValue ); + + GemValue> gemValue = GemValue.createEnumArray( null, defaultArrayValue, TestEnum.class ); + + assertThat( gemValue ).isNotNull(); + assertThat( gemValue.isValid() ).isTrue(); + assertThat( gemValue.hasValue() ).isFalse(); + assertThat( gemValue.getValue() ).isNull(); + assertThat( gemValue.getDefaultValue() ).isEqualTo( Collections.singletonList( TestEnum.B ) ); + assertThat( gemValue.get() ).as( "get should return defaultValue" ) + .isEqualTo( Collections.singletonList( TestEnum.B ) ); + assertThat( gemValue.getAnnotationValue() ).isNull(); + assertThat( gemValue.getValueOrElseGet( Collections::emptyList ) ) + .as( "getValueOrElseGet should return other" ).isEqualTo( Collections.emptyList() ); + } + + @Test + void createEnumArrayValueWithoutAnnotationDefaultValue() { + EnumAnnotationValue annotationValue = new EnumAnnotationValue( TestEnum.A ); + GenericArrayAnnotationValue arrayValue = new GenericArrayAnnotationValue( annotationValue ); + + GemValue> gemValue = GemValue.createEnumArray( arrayValue, null, TestEnum.class ); + + assertThat( gemValue ).isNotNull(); + assertThat( gemValue.isValid() ).isTrue(); + assertThat( gemValue.hasValue() ).isTrue(); + assertThat( gemValue.getValue() ).isEqualTo( Collections.singletonList( TestEnum.A ) ); + assertThat( gemValue.getDefaultValue() ).isNull(); + assertThat( gemValue.get() ).as( "get should return value" ) + .isEqualTo( Collections.singletonList( TestEnum.A ) ); + assertThat( gemValue.getAnnotationValue() ).isEqualTo( arrayValue ); + assertThat( gemValue.getValueOrElseGet( Collections::emptyList ) ) + .as( "getValueOrElseGet should return value" ).isEqualTo( Collections.singletonList( TestEnum.A ) ); + } + + @Test + void createEnumArrayValueInvalid() { + GemValue> gemValue = GemValue.createEnumArray( null, null, TestEnum.class ); + + assertThat( gemValue ).isNotNull(); + assertThat( gemValue.isValid() ).isFalse(); + assertThat( gemValue.hasValue() ).isFalse(); + assertThat( gemValue.getValue() ).isNull(); + assertThat( gemValue.getDefaultValue() ).isNull(); + assertThat( gemValue.get() ).as( "get should return null" ).isNull(); + assertThat( gemValue.getAnnotationValue() ).isNull(); + assertThat( gemValue.getValueOrElseGet( Collections::emptyList ) ) + .as( "getValueOrElseGet should return other" ).isEqualTo( Collections.emptyList() ); + } + } + + + enum TestEnum { + A, B, C + } + + static class EnumAnnotationValue implements AnnotationValue { + + private final String value; + + EnumAnnotationValue(TestEnum value) { + this.value = value.toString(); + } + + @Override + public Object getValue() { + return value; + } + + @Override + public R accept(AnnotationValueVisitor v, P p) { + VariableElement variableElement = new VariableElement() { + @Override + public TypeMirror asType() { + return null; + } + + @Override + public Object getConstantValue() { + return null; + } + + @Override + public Name getSimpleName() { + return new Name() { + @Override + public boolean contentEquals(CharSequence cs) { + return cs.toString().equals( value ); + } + + @Override + public int length() { + return value.length(); + } + + @Override + public char charAt(int index) { + return value.charAt( index ); + } + + @Override + public CharSequence subSequence(int start, int end) { + return value.subSequence( start, end ); + } + + @Override + public String toString() { + return value; + } + }; + } + + @Override + public Element getEnclosingElement() { + return null; + } + + @Override + public ElementKind getKind() { + return null; + } + + @Override + public Set getModifiers() { + return new HashSet<>(); + } + + @Override + public List getEnclosedElements() { + return Collections.emptyList(); + } + + @Override + public List getAnnotationMirrors() { + return Collections.emptyList(); + } + + @Override + public A getAnnotation(Class annotationType) { + return null; + } + + @Override + public A[] getAnnotationsByType(Class annotationType) { + return null; + } + + @Override + public S accept(ElementVisitor v, T p) { + return null; + } + }; + return v.visitEnumConstant( variableElement, p ); + } + } } diff --git a/processor/src/main/java/org/mapstruct/tools/gem/processor/GemEnum.java b/processor/src/main/java/org/mapstruct/tools/gem/processor/GemEnum.java new file mode 100644 index 0000000..56a21a6 --- /dev/null +++ b/processor/src/main/java/org/mapstruct/tools/gem/processor/GemEnum.java @@ -0,0 +1,47 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at https://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.tools.gem.processor; + +import java.util.Arrays; +import java.util.List; +import javax.lang.model.element.Element; + +public class GemEnum { + private final String gemPackageName; + private final String gemName; + private final String originalEnumFullName; + private final List enumConstants; + private final Element[] originatingElements; + + public GemEnum(String gemPackageName, String gemName, String originalEnumFullName, + List values, Element... originatingElements) { + this.gemPackageName = gemPackageName; + this.gemName = gemName; + this.originalEnumFullName = originalEnumFullName; + this.enumConstants = values; + this.originatingElements = Arrays.copyOf( originatingElements, originatingElements.length ); + } + + public String getGemPackageName() { + return gemPackageName; + } + + public String getGemName() { + return gemName; + } + + public String getOriginalEnumFullName() { + return originalEnumFullName; + } + + public List getEnumConstants() { + return enumConstants; + } + + public Element[] getOriginatingElements() { + return originatingElements; + } +} diff --git a/processor/src/main/java/org/mapstruct/tools/gem/processor/GemInfo.java b/processor/src/main/java/org/mapstruct/tools/gem/processor/GemInfo.java index c965e51..6d7b45e 100644 --- a/processor/src/main/java/org/mapstruct/tools/gem/processor/GemInfo.java +++ b/processor/src/main/java/org/mapstruct/tools/gem/processor/GemInfo.java @@ -81,11 +81,11 @@ public Element[] getOriginatingElements() { } private boolean isNotSamePackage(GemValueType valueType ) { - return !valueType.getPacakage().equals( gemPackageName ); + return !valueType.getPackageName().equals( gemPackageName ); } private boolean isNotJavaLang( GemValueType valueType ) { - return !"java.lang".equals( valueType.getPacakage() ); + return !"java.lang".equals( valueType.getPackageName() ); } private boolean isNotTypeMirror( GemValueType valueType ) { diff --git a/processor/src/main/java/org/mapstruct/tools/gem/processor/GemProcessor.java b/processor/src/main/java/org/mapstruct/tools/gem/processor/GemProcessor.java index 9b57c2e..1bca337 100644 --- a/processor/src/main/java/org/mapstruct/tools/gem/processor/GemProcessor.java +++ b/processor/src/main/java/org/mapstruct/tools/gem/processor/GemProcessor.java @@ -11,6 +11,7 @@ import java.io.Writer; import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -21,7 +22,9 @@ import javax.lang.model.SourceVersion; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.Name; import javax.lang.model.element.PackageElement; import javax.lang.model.element.TypeElement; import javax.lang.model.type.ArrayType; @@ -34,16 +37,19 @@ import freemarker.template.Configuration; import freemarker.template.Template; import freemarker.template.TemplateException; -import freemarker.template.Version; /** * @author sjaakd */ -@SupportedAnnotationTypes( {"org.mapstruct.tools.gem.GemDefinitions", "org.mapstruct.tools.gem.GemDefinition"} ) +@SupportedAnnotationTypes( {"org.mapstruct.tools.gem.GemDefinitions", "org.mapstruct.tools.gem.GemDefinition", + "org.mapstruct.tools.gem.RegisterGem"} ) public class GemProcessor extends AbstractProcessor { private Util util; - private List gemInfos = new ArrayList<>( 10 ); + private final List gemInfos = new ArrayList<>( 10 ); + private final List gemEnums = new ArrayList<>(); + private final Map gemEnumMap = new HashMap<>(); + private boolean noErrors = true; @Override public SourceVersion getSupportedSourceVersion() { @@ -74,13 +80,18 @@ public boolean process(Set annotationTypes, RoundEnvironm ); gemDefinitionMirrors.forEach( m -> addGemInfo( m, definingElement ) ); } + else if ( "org.mapstruct.tools.gem.RegisterGem".equals( annotationName ) ) { + addEnumMapping( gemDefinitionsMirror, definingElement ); + } else { addGemInfo( gemDefinitionsMirror, definingElement ); } } } - postProcessGemInfo(); - write(); + if ( noErrors ) { + postProcessGemInfo(); + write(); + } } catch ( RuntimeException ex ) { StringWriter sw = new StringWriter(); @@ -95,27 +106,51 @@ private void addGemInfo(AnnotationMirror gemDefinitionMirror, Element definingEl // create gem info DeclaredType gemDeclaredType = util.getAnnotationValue( gemDefinitionMirror, "value", DeclaredType.class ); + Element element = gemDeclaredType.asElement(); + ElementKind kind = element.getKind(); String annotationName = util.getSimpleName( gemDeclaredType ); String gemName = getGemName( gemDefinitionMirror, annotationName ); PackageElement pkg = processingEnv.getElementUtils().getPackageOf( definingElement ); String gemFqn = util.getFullyQualifiedName( gemDeclaredType ); String gemPackage = pkg.getQualifiedName().toString(); + if ( kind == ElementKind.ENUM ) { + List enumConstants = getEnumConstants( element ); + GemEnum gemEnum = new GemEnum(gemPackage, gemName, gemFqn, enumConstants, definingElement, element); + gemEnums.add( gemEnum ); + if ( gemEnumMap.put( gemFqn, gemEnum ) != null ) { + processingEnv.getMessager().printMessage( Diagnostic.Kind.ERROR, + "Enum gem " + gemFqn + " can only be registered once" ); + noErrors = false; + } + } + else if ( kind == ElementKind.ANNOTATION_TYPE ) { + + // collect value info + List methods = ElementFilter.methodsIn( element.getEnclosedElements() ); + List gemValueInfos = methods.stream() + .map( e -> new GemValueInfo( e.getSimpleName().toString(), e.getReturnType() ) ) + .collect( Collectors.toList() ); + GemInfo gemInfo = new GemInfo( + gemPackage, + gemName, + annotationName, + gemFqn, + gemValueInfos, + definingElement, + element + ); + gemInfos.add( gemInfo ); + } + else { + throw new IllegalArgumentException(); + } - // collect value info - List methods = ElementFilter.methodsIn( gemDeclaredType.asElement().getEnclosedElements() ); - List gemValueInfos = methods.stream() - .map( e -> new GemValueInfo( e.getSimpleName().toString(), e.getReturnType() ) ) - .collect( Collectors.toList() ); - GemInfo gemInfo = new GemInfo( - gemPackage, - gemName, - annotationName, - gemFqn, - gemValueInfos, - definingElement, - gemDeclaredType.asElement() - ); - gemInfos.add( gemInfo ); + } + + private List getEnumConstants(Element element) { + return element.getEnclosedElements().stream() + .filter( e -> e.getKind() == ElementKind.ENUM_CONSTANT ) + .map( Element::getSimpleName ).map( Name::toString ).collect( Collectors.toList() ); } private String getGemName(AnnotationMirror gemDefinitionMirror, String annotationName) { @@ -148,7 +183,13 @@ private GemValueType getGemValueType(TypeMirror type, boolean isArray) { DeclaredType declaredType = (DeclaredType) type; String fqn = util.getFullyQualifiedName( declaredType ); if ( util.isEnumeration( declaredType ) ) { - valueType = new GemValueType( String.class, true, isArray ); + GemEnum gemEnum = gemEnumMap.get( fqn ); + if (gemEnum != null) { + valueType = new GemValueType( gemEnum, isArray ); + } + else { + valueType = new GemValueType( String.class, true, isArray ); + } } else if ( Class.class.getName().equals( fqn ) ) { valueType = new GemValueType( TypeMirror.class, false, isArray ); @@ -157,16 +198,11 @@ else if (String.class.getName().equals( fqn ) ) { valueType = new GemValueType( String.class, false, isArray ); } else { - GemInfo usedGem = gemInfos.stream() + valueType = gemInfos.stream() .filter( g -> fqn.equals( g.getAnnotationFqn() ) ) .findFirst() - .orElse( null ); - if ( usedGem != null ) { - valueType = new GemValueType( usedGem, isArray ); - } - else { - valueType = new GemValueType( TypeMirror.class, false, isArray ); - } + .map( usedGem -> new GemValueType( usedGem, isArray ) ) + .orElseGet( () -> new GemValueType( TypeMirror.class, false, isArray ) ); } break; case BOOLEAN: @@ -199,17 +235,74 @@ else if (String.class.getName().equals( fqn ) ) { return valueType; } + private void addEnumMapping(AnnotationMirror gemDefinitionsMirror, Element definingElement) { + if ( definingElement.getKind() != ElementKind.ENUM) { + throw new IllegalArgumentException(); + } + String packageName = processingEnv.getElementUtils().getPackageOf( definingElement ) + .getQualifiedName().toString(); + DeclaredType original = util.getAnnotationValue( gemDefinitionsMirror, "value", DeclaredType.class ); + String originalFullName = util.getFullyQualifiedName( original ); + List originalElements = getEnumConstants( original.asElement() ); + Set originalValues = new HashSet<>( originalElements ); + List enumConstants = getEnumConstants( definingElement ); + Set definedValues = new HashSet<>( enumConstants ); + List missingOriginals = originalValues.stream() + .filter( value -> !definedValues.remove( value ) ).collect( Collectors.toList() ); + + if ( !missingOriginals.isEmpty() ) { + processingEnv.getMessager().printMessage( Diagnostic.Kind.ERROR, "Enum constants " + missingOriginals + + " are missing in " + packageName + "." + definingElement.getSimpleName() + ". A enum gem of " + + originalFullName + " should exactly contain " + originalElements ); + noErrors = false; + } + if ( !definedValues.isEmpty() ) { + processingEnv.getMessager().printMessage( Diagnostic.Kind.ERROR, "Enum constants " + definedValues + + " are only present in " + packageName + "." + definingElement.getSimpleName() + ". A enum gem of " + + originalFullName + " should exactly contain " + originalElements ); + noErrors = false; + } + if ( gemEnumMap.put( originalFullName, new GemEnum( + packageName, + definingElement.getSimpleName().toString(), + originalFullName, + enumConstants + ) ) != null ) { + processingEnv.getMessager().printMessage( Diagnostic.Kind.ERROR, + "Enum gem " + originalFullName + " can only be registered once" ); + noErrors = false; + } + } + private void write( ) { + Configuration cfg = new Configuration(Configuration.VERSION_2_3_29); + cfg.setClassForTemplateLoading( GemProcessor.class, "/" ); + cfg.setDefaultEncoding( "UTF-8" ); + for (GemEnum gemEnum : gemEnums) { + try (Writer writer = processingEnv.getFiler() + .createSourceFile( + gemEnum.getGemPackageName() + "." + gemEnum.getGemName(), + gemEnum.getOriginatingElements() + ) + .openWriter() ) { + Map templateData = new HashMap<>(); + + templateData.put( "gemEnum", gemEnum ); + Template template = cfg.getTemplate( "org/mapstruct/tools/gem/processor/Enum.ftl" ); + template.process( templateData, writer ); + writer.flush(); + } + catch (TemplateException | IOException ex) { + throw new IllegalStateException(ex); + } + } for ( GemInfo gemInfo : gemInfos ) { try (Writer writer = processingEnv.getFiler() .createSourceFile( gemInfo.getGemPackageName() + "." + gemInfo.getGemName(), gemInfo.getOriginatingElements() ) - .openWriter()) { - Configuration cfg = new Configuration( new Version( "2.3.21" ) ); - cfg.setClassForTemplateLoading( GemProcessor.class, "/" ); - cfg.setDefaultEncoding( "UTF-8" ); + .openWriter() ) { Map templateData = new HashMap<>(); @@ -223,5 +316,7 @@ private void write( ) { } // handled all info, clear gemInfos.clear(); + gemEnums.clear(); + gemEnumMap.clear(); } } diff --git a/processor/src/main/java/org/mapstruct/tools/gem/processor/GemValueType.java b/processor/src/main/java/org/mapstruct/tools/gem/processor/GemValueType.java index 61e73a2..172d331 100644 --- a/processor/src/main/java/org/mapstruct/tools/gem/processor/GemValueType.java +++ b/processor/src/main/java/org/mapstruct/tools/gem/processor/GemValueType.java @@ -9,7 +9,7 @@ public class GemValueType { private final String name; private final String fqn; - private final String pacakage; + private final String packageName; private final boolean isEnum; private final boolean isArray; private final boolean isGem; @@ -17,12 +17,23 @@ public class GemValueType { private String gemName; private String elementName; + public GemValueType(GemEnum gemEnum, boolean isArray) { + this.elementName = gemEnum.getGemName(); + this.name = isArray ? "List<" + elementName + ">" : elementName; + this.fqn = gemEnum.getGemPackageName() + "." + gemEnum.getGemName(); + this.gemName = gemEnum.getGemName(); + this.packageName = gemEnum.getGemPackageName(); + this.isEnum = true; + this.isArray = isArray; + this.isGem = false; + } + public GemValueType(GemInfo gemInfo, boolean isArray) { this.elementName = gemInfo.getGemName(); this.name = isArray ? "List<" + elementName + ">" : elementName; this.fqn = gemInfo.getGemPackageName() + "." + gemInfo.getGemName(); this.gemName = gemInfo.getGemName(); - this.pacakage = gemInfo.getGemPackageName(); + this.packageName = gemInfo.getGemPackageName(); this.isEnum = false; this.isArray = isArray; this.isGem = true; @@ -32,7 +43,7 @@ public GemValueType(Class clazz, boolean isEnum, boolean isArray) { this.elementName = clazz.getSimpleName(); this.name = isArray ? "List<" + clazz.getSimpleName() + ">" : clazz.getSimpleName(); this.fqn = clazz.getName(); - this.pacakage = clazz.getPackage().getName(); + this.packageName = clazz.getPackage().getName(); this.isEnum = isEnum; this.isArray = isArray; this.isGem = false; @@ -42,8 +53,8 @@ public String getFqn() { return fqn; } - public String getPacakage() { - return pacakage; + public String getPackageName() { + return packageName; } public String getName() { diff --git a/processor/src/main/resources/org/mapstruct/tools/gem/processor/Enum.ftl b/processor/src/main/resources/org/mapstruct/tools/gem/processor/Enum.ftl new file mode 100644 index 0000000..c9f4522 --- /dev/null +++ b/processor/src/main/resources/org/mapstruct/tools/gem/processor/Enum.ftl @@ -0,0 +1,18 @@ +<#-- + + Copyright MapStruct Authors. + + Licensed under the Apache License version 2.0, available at https://www.apache.org/licenses/LICENSE-2.0 + +--> +<#-- @ftlvariable name="gemEnum" type="org.mapstruct.tools.gem.processor.GemEnum" --> +package ${gemEnum.gemPackageName}; + +/** + * Gem for the enum {@link ${gemEnum.originalEnumFullName}} +*/ +public enum ${gemEnum.gemName} { +<#list gemEnum.enumConstants as enumConstant> + ${enumConstant}<#if enumConstant?has_next>, + +} \ No newline at end of file diff --git a/processor/src/main/resources/org/mapstruct/tools/gem/processor/Gem.ftl b/processor/src/main/resources/org/mapstruct/tools/gem/processor/Gem.ftl index b110ce5..e563778 100644 --- a/processor/src/main/resources/org/mapstruct/tools/gem/processor/Gem.ftl +++ b/processor/src/main/resources/org/mapstruct/tools/gem/processor/Gem.ftl @@ -213,9 +213,17 @@ public class ${gemInfo.gemName} implements Gem { <#elseif gemValueInfo.valueType.enum> <#if gemValueInfo.valueType.array> + <#if gemValueInfo.valueType.elementName == "String"> GemValue.createEnumArray( value, defaultValue ) + <#else> + GemValue.createEnumArray( value, defaultValue, ${gemValueInfo.valueType.elementName}.class ) + <#else> + <#if gemValueInfo.valueType.elementName == "String"> GemValue.createEnum( value, defaultValue ) + <#else> + GemValue.createEnum( value, defaultValue, ${gemValueInfo.valueType.elementName}.class ) + <#else> <#if gemValueInfo.valueType.array> diff --git a/processor/src/test/java/org/mapstruct/tools/gem/processor/ProcessorTest.java b/processor/src/test/java/org/mapstruct/tools/gem/processor/ProcessorTest.java index 3bd937c..121ee10 100644 --- a/processor/src/test/java/org/mapstruct/tools/gem/processor/ProcessorTest.java +++ b/processor/src/test/java/org/mapstruct/tools/gem/processor/ProcessorTest.java @@ -6,14 +6,15 @@ package org.mapstruct.tools.gem.processor; import java.io.File; -import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.UncheckedIOException; import java.net.URI; +import java.nio.file.Files; import java.nio.file.Path; import java.util.Arrays; import java.util.Collections; +import java.util.List; import javax.annotation.processing.Processor; import javax.tools.Diagnostic; import javax.tools.DiagnosticCollector; @@ -24,11 +25,11 @@ import javax.tools.StandardLocation; import javax.tools.ToolProvider; +import static org.assertj.core.api.Assertions.assertThat; + import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; -import static org.assertj.core.api.Assertions.assertThat; - class ProcessorTest { @TempDir @@ -65,10 +66,219 @@ void singleAnnotation() throws IOException { assertGeneratedFileContent( "BuilderGem", generatedDir ); } + @Test + void generateEnum() throws IOException { + StringJavaFileObject src = new StringJavaFileObject( + "org.mapstruct.annotations.processor.GemGenerator", + "package org.mapstruct.tools.gem.processor;\n" + + "\n" + + "import org.mapstruct.tools.gem.GemDefinition;\n" + + "import org.mapstruct.tools.gem.test.enummapping.EnumAnnotation;\n" + + "import org.mapstruct.tools.gem.test.enummapping.SimpleEnum;\n" + + "\n" + + "@GemDefinition(value = SimpleEnum.class)\n" + + "@GemDefinition(value = EnumAnnotation.class)\n" + + "public class GemGenerator {\n" + + "}" + ); + File generatedDir = compile( new GemProcessor(), src ); + assertGeneratedFileContent( "SimpleEnumGem", generatedDir ); + assertGeneratedFileContent( "EnumAnnotationGem", generatedDir ); + } + + @Test + void generateEnumWithRegisterGem() throws IOException { + StringJavaFileObject src = new StringJavaFileObject( + "org.mapstruct.annotations.processor.GemGenerator", + "package org.mapstruct.tools.gem.processor;\n" + + "\n" + + "import org.mapstruct.tools.gem.GemDefinition;\n" + + "import org.mapstruct.tools.gem.test.enummapping.EnumAnnotation;\n" + + "\n" + + "@GemDefinition(value = EnumAnnotation.class," + + " implementationName = \"RegisterGem\")\n" + + "public class GemGenerator {\n" + + "}" + ); + File generatedDir = compile( new GemProcessor(), src, getManuelGemOfEnum() ); + assertGeneratedFileContent( "EnumAnnotationRegisterGem", generatedDir ); + } + + @Test + void generateEnumWithRegisterOtherPackageGem() throws IOException { + StringJavaFileObject src = new StringJavaFileObject( + "org.mapstruct.annotations.processor.GemGenerator", + "package org.mapstruct.tools.gem.processor;\n" + + "\n" + + "import org.mapstruct.tools.gem.GemDefinition;\n" + + "import org.mapstruct.tools.gem.test.enummapping.EnumAnnotation;\n" + + "\n" + + "@GemDefinition(value = EnumAnnotation.class," + + " implementationName = \"PackageGem\")\n" + + "public class GemGenerator {\n" + + "}" + ); + StringJavaFileObject enumSr = new StringJavaFileObject( + "org.mapstruct.annotations.processor.other.MySimpleEnumGem", + "package org.mapstruct.tools.gem.processor.other;\n" + + "\n" + + "import org.mapstruct.tools.gem.RegisterGem;\n" + + "import org.mapstruct.tools.gem.test.enummapping.SimpleEnum;\n" + + "\n" + + "@RegisterGem(value = SimpleEnum.class)\n" + + "public enum MySimpleEnumGem {\n" + + "A,B,C;\n" + + "}" + ); + File generatedDir = compile( new GemProcessor(), enumSr, src ); + assertGeneratedFileContent( "EnumAnnotationPackageGem", generatedDir ); + } + + @Test + void registerGem() throws IOException { + compile( new GemProcessor(), getManuelGemOfEnum() ); + } + + @Test + void registerGemMissingEnumConstants() throws IOException { + StringJavaFileObject src = new StringJavaFileObject( + "org.mapstruct.annotations.processor.MySimpleEnumGem", + "package org.mapstruct.tools.gem.processor;\n" + + "\n" + + "import org.mapstruct.tools.gem.RegisterGem;\n" + + "import org.mapstruct.tools.gem.test.enummapping.SimpleEnum;\n" + + "\n" + + "@RegisterGem(value = SimpleEnum.class)\n" + + "public enum MySimpleEnumGem {\n" + + "C;\n" + + "}" + ); + List> diagnostics = failCompile( new GemProcessor(), src ); + assertThat( diagnostics ).singleElement() + .extracting( + Diagnostic::getKind, + d -> d.getMessage( null ) + ) + .containsExactly( + Diagnostic.Kind.ERROR, + "Enum constants [A, B] are missing in org.mapstruct.tools.gem.processor.MySimpleEnumGem." + + " A enum gem of org.mapstruct.tools.gem.test.enummapping.SimpleEnum should exactly" + + " contain [A, B, C]" ); + } + + @Test + void registerGemMoreEnumConstants() throws IOException { + StringJavaFileObject src = new StringJavaFileObject( + "org.mapstruct.annotations.processor.MySimpleEnumGem", + "package org.mapstruct.tools.gem.processor;\n" + + "\n" + + "import org.mapstruct.tools.gem.RegisterGem;\n" + + "import org.mapstruct.tools.gem.test.enummapping.SimpleEnum;\n" + + "\n" + + "@RegisterGem(value = SimpleEnum.class)\n" + + "public enum MySimpleEnumGem {\n" + + "A,B,C,D,E;\n" + + "}" + ); + List> diagnostics = failCompile( new GemProcessor(), src ); + assertThat( diagnostics ).singleElement() + .extracting( + Diagnostic::getKind, + d -> d.getMessage( null ) + ) + .containsExactly( + Diagnostic.Kind.ERROR, + "Enum constants [D, E] are only present in org.mapstruct.tools.gem.processor.MySimpleEnumGem." + + " A enum gem of org.mapstruct.tools.gem.test.enummapping.SimpleEnum should exactly" + + " contain [A, B, C]" + ); + } + + @Test + void registerEnumTwiceGeneratorFirst() throws IOException { + StringJavaFileObject src = new StringJavaFileObject( + "org.mapstruct.annotations.processor.GemGenerator", + "package org.mapstruct.tools.gem.processor;\n" + + "\n" + + "import org.mapstruct.tools.gem.GemDefinition;\n" + + "import org.mapstruct.tools.gem.test.enummapping.SimpleEnum;\n" + + "\n" + + "@GemDefinition(value = SimpleEnum.class)\n" + + "public class GemGenerator {\n" + + "}" + ); + List> diagnostics = failCompile( new GemProcessor(), + src, getManuelGemOfEnum() ); + assertThat( diagnostics ).singleElement() + .extracting( + Diagnostic::getKind, + d -> d.getMessage( null ) + ) + .containsExactly( + Diagnostic.Kind.ERROR, + "Enum gem org.mapstruct.tools.gem.test.enummapping.SimpleEnum can only be registered once" + ); + } + + @Test + void registerEnumTwiceGemDefinitionFirst() throws IOException { + StringJavaFileObject src = new StringJavaFileObject( + "org.mapstruct.annotations.processor.GemGenerator", + "package org.mapstruct.tools.gem.processor;\n" + + "\n" + + "import org.mapstruct.tools.gem.GemDefinition;\n" + + "import org.mapstruct.tools.gem.test.enummapping.SimpleEnum;\n" + + "\n" + + "@GemDefinition(value = SimpleEnum.class)\n" + + "public class GemGenerator {\n" + + "}" + ); + List> diagnostics = failCompile( new GemProcessor(), + getManuelGemOfEnum(), src ); + assertThat( diagnostics ).singleElement() + .extracting( + Diagnostic::getKind, + d -> d.getMessage( null ) + ) + .containsExactly( + Diagnostic.Kind.ERROR, + "Enum gem org.mapstruct.tools.gem.test.enummapping.SimpleEnum can only be registered once" + ); + } + + private List> failCompile(Processor processor, + JavaFileObject... compilationUnits) + throws IOException { + JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); + + DiagnosticCollector diagnostics = new DiagnosticCollector<>(); + StandardJavaFileManager fileManager = compiler.getStandardFileManager( diagnostics, null, null ); + File classesDir = newFolder( tempDir, "classes" ); + fileManager.setLocation( StandardLocation.CLASS_OUTPUT, Collections.singletonList( classesDir ) ); + File generatedDir = newFolder( tempDir, "generated" ); + fileManager.setLocation( StandardLocation.SOURCE_OUTPUT, Collections.singletonList( generatedDir ) ); + + JavaCompiler.CompilationTask task = compiler.getTask( + null, + fileManager, + diagnostics, + null, + null, + Arrays.asList( compilationUnits ) + ); + + task.setProcessors( Collections.singletonList( + processor + ) ); + + assertThat( task.call() ).isFalse(); + return diagnostics.getDiagnostics(); + } + private File compile(Processor processor, JavaFileObject... compilationUnits) throws IOException { JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); - DiagnosticCollector diagnostics = new DiagnosticCollector(); + DiagnosticCollector diagnostics = new DiagnosticCollector<>(); StandardJavaFileManager fileManager = compiler.getStandardFileManager( diagnostics, null, null ); File classesDir = newFolder( tempDir, "classes" ); fileManager.setLocation( StandardLocation.CLASS_OUTPUT, Collections.singletonList( classesDir ) ); @@ -84,8 +294,8 @@ private File compile(Processor processor, JavaFileObject... compilationUnits) th Arrays.asList( compilationUnits ) ); - task.setProcessors( Arrays.asList( - processor + task.setProcessors( Collections.singletonList( + processor ) ); boolean success = task.call(); @@ -102,7 +312,7 @@ protected void assertGeneratedFileContent(String gemName, File generatedDir) { .as( gemName ) .exists(); - try (InputStream generatedGemStream = new FileInputStream( gemPath.toFile() ); + try (InputStream generatedGemStream = Files.newInputStream( gemPath.toFile().toPath() ); InputStream expectedGemStream = getClass().getClassLoader() .getResourceAsStream( "fixtures/org/mapstruct/tools/gem/processor/" + gemName + ".java" ) ) { @@ -128,7 +338,7 @@ private static class StringJavaFileObject extends SimpleJavaFileObject { } @Override - public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException { + public CharSequence getCharContent(boolean ignoreEncodingErrors) { return code; } } @@ -151,6 +361,21 @@ private String getSource() { "}"; } + private static StringJavaFileObject getManuelGemOfEnum() { + return new StringJavaFileObject( + "org.mapstruct.annotations.processor.MySimpleEnumGem", + "package org.mapstruct.tools.gem.processor;\n" + + "\n" + + "import org.mapstruct.tools.gem.RegisterGem;\n" + + "import org.mapstruct.tools.gem.test.enummapping.SimpleEnum;\n" + + "\n" + + "@RegisterGem(value = SimpleEnum.class)\n" + + "public enum MySimpleEnumGem {\n" + + "A,B,C;\n" + + "}" + ); + } + private static File newFolder(File root, String folder) throws IOException { File result = new File(root, folder); if (!result.mkdir()) { diff --git a/processor/src/test/resources/fixtures/org/mapstruct/tools/gem/processor/EnumAnnotationGem.java b/processor/src/test/resources/fixtures/org/mapstruct/tools/gem/processor/EnumAnnotationGem.java new file mode 100644 index 0000000..7617d90 --- /dev/null +++ b/processor/src/test/resources/fixtures/org/mapstruct/tools/gem/processor/EnumAnnotationGem.java @@ -0,0 +1,231 @@ +package org.mapstruct.tools.gem.processor; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.AnnotationValue; +import javax.lang.model.element.Element; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.TypeElement; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.util.ElementFilter; +import org.mapstruct.tools.gem.Gem; +import org.mapstruct.tools.gem.GemValue; + + +public class EnumAnnotationGem implements Gem { + + private final GemValue mySimpleEnumWithDefault; + private final GemValue> mySimpleEnumArrayWithDefault; + private final GemValue mySimpleEnum; + private final GemValue> mySimpleEnumArray; + private final boolean isValid; + private final AnnotationMirror mirror; + + private EnumAnnotationGem( BuilderImpl builder ) { + this.mySimpleEnumWithDefault = builder.mySimpleEnumWithDefault; + this.mySimpleEnumArrayWithDefault = builder.mySimpleEnumArrayWithDefault; + this.mySimpleEnum = builder.mySimpleEnum; + this.mySimpleEnumArray = builder.mySimpleEnumArray; + isValid = ( this.mySimpleEnumWithDefault != null && this.mySimpleEnumWithDefault.isValid() ) + && ( this.mySimpleEnumArrayWithDefault != null && this.mySimpleEnumArrayWithDefault.isValid() ) + && ( this.mySimpleEnum != null && this.mySimpleEnum.isValid() ) + && ( this.mySimpleEnumArray != null && this.mySimpleEnumArray.isValid() ); + mirror = builder.mirror; + } + + /** + * accessor + * + * @return the {@link GemValue} for {@link EnumAnnotationGem#mySimpleEnumWithDefault} + */ + public GemValue mySimpleEnumWithDefault( ) { + return mySimpleEnumWithDefault; + } + + /** + * accessor + * + * @return the {@link GemValue} for {@link EnumAnnotationGem#mySimpleEnumArrayWithDefault} + */ + public GemValue> mySimpleEnumArrayWithDefault( ) { + return mySimpleEnumArrayWithDefault; + } + + /** + * accessor + * + * @return the {@link GemValue} for {@link EnumAnnotationGem#mySimpleEnum} + */ + public GemValue mySimpleEnum( ) { + return mySimpleEnum; + } + + /** + * accessor + * + * @return the {@link GemValue} for {@link EnumAnnotationGem#mySimpleEnumArray} + */ + public GemValue> mySimpleEnumArray( ) { + return mySimpleEnumArray; + } + + @Override + public AnnotationMirror mirror( ) { + return mirror; + } + + @Override + public boolean isValid( ) { + return isValid; + } + + public static EnumAnnotationGem instanceOn(Element element) { + return build( element, new BuilderImpl() ); + } + + public static EnumAnnotationGem instanceOn(AnnotationMirror mirror ) { + return build( mirror, new BuilderImpl() ); + } + + public static T build(Element element, Builder builder) { + AnnotationMirror mirror = element.getAnnotationMirrors().stream() + .filter( a -> "org.mapstruct.tools.gem.test.enummapping.EnumAnnotation".contentEquals( ( ( TypeElement )a.getAnnotationType().asElement() ).getQualifiedName() ) ) + .findAny() + .orElse( null ); + return build( mirror, builder ); + } + + public static T build(AnnotationMirror mirror, Builder builder ) { + + // return fast + if ( mirror == null || builder == null ) { + return null; + } + + // fetch defaults from all defined values in the annotation type + List enclosed = ElementFilter.methodsIn( mirror.getAnnotationType().asElement().getEnclosedElements() ); + Map defaultValues = new HashMap<>( enclosed.size() ); + enclosed.forEach( e -> defaultValues.put( e.getSimpleName().toString(), e.getDefaultValue() ) ); + + // fetch all explicitely set annotation values in the annotation instance + Map values = new HashMap<>( enclosed.size() ); + mirror.getElementValues().forEach( (key, value) -> values.put( key.getSimpleName().toString(), value ) ); + + // iterate and populate builder + for ( Map.Entry defaultMethod : defaultValues.entrySet() ) { + String methodName = defaultMethod.getKey(); + AnnotationValue defaultValue = defaultMethod.getValue(); + AnnotationValue value = values.get( methodName ); + switch ( methodName ) { + case "mySimpleEnumWithDefault": + builder.setMysimpleenumwithdefault( GemValue.createEnum( value, defaultValue, SimpleEnumGem.class ) ); + break; + case "mySimpleEnumArrayWithDefault": + builder.setMysimpleenumarraywithdefault( GemValue.createEnumArray( value, defaultValue, SimpleEnumGem.class ) ); + break; + case "mySimpleEnum": + builder.setMysimpleenum( GemValue.createEnum( value, defaultValue, SimpleEnumGem.class ) ); + break; + case "mySimpleEnumArray": + builder.setMysimpleenumarray( GemValue.createEnumArray( value, defaultValue, SimpleEnumGem.class ) ); + break; + } + } + builder.setMirror( mirror ); + return builder.build(); + } + + /** + * A builder that can be implemented by the user to define custom logic e.g. in the + * build method, prior to creating the annotation gem. + */ + public interface Builder { + + /** + * Sets the {@link GemValue} for {@link EnumAnnotationGem#mySimpleEnumWithDefault} + * + * @return the {@link Builder} for this gem, representing {@link EnumAnnotationGem} + */ + Builder setMysimpleenumwithdefault(GemValue methodName ); + + /** + * Sets the {@link GemValue} for {@link EnumAnnotationGem#mySimpleEnumArrayWithDefault} + * + * @return the {@link Builder} for this gem, representing {@link EnumAnnotationGem} + */ + Builder setMysimpleenumarraywithdefault(GemValue> methodName ); + + /** + * Sets the {@link GemValue} for {@link EnumAnnotationGem#mySimpleEnum} + * + * @return the {@link Builder} for this gem, representing {@link EnumAnnotationGem} + */ + Builder setMysimpleenum(GemValue methodName ); + + /** + * Sets the {@link GemValue} for {@link EnumAnnotationGem#mySimpleEnumArray} + * + * @return the {@link Builder} for this gem, representing {@link EnumAnnotationGem} + */ + Builder setMysimpleenumarray(GemValue> methodName ); + + /** + * Sets the annotation mirror + * + * @param mirror the mirror which this gem represents + * + * @return the {@link Builder} for this gem, representing {@link EnumAnnotationGem} + */ + Builder setMirror( AnnotationMirror mirror ); + + /** + * The build method can be overriden in a custom custom implementation, which allows + * the user to define his own custom validation on the annotation. + * + * @return the representation of the annotation + */ + T build(); + } + + private static class BuilderImpl implements Builder { + + private GemValue mySimpleEnumWithDefault; + private GemValue> mySimpleEnumArrayWithDefault; + private GemValue mySimpleEnum; + private GemValue> mySimpleEnumArray; + private AnnotationMirror mirror; + + public Builder setMysimpleenumwithdefault(GemValue mySimpleEnumWithDefault ) { + this.mySimpleEnumWithDefault = mySimpleEnumWithDefault; + return this; + } + + public Builder setMysimpleenumarraywithdefault(GemValue> mySimpleEnumArrayWithDefault ) { + this.mySimpleEnumArrayWithDefault = mySimpleEnumArrayWithDefault; + return this; + } + + public Builder setMysimpleenum(GemValue mySimpleEnum ) { + this.mySimpleEnum = mySimpleEnum; + return this; + } + + public Builder setMysimpleenumarray(GemValue> mySimpleEnumArray ) { + this.mySimpleEnumArray = mySimpleEnumArray; + return this; + } + + public Builder setMirror( AnnotationMirror mirror ) { + this.mirror = mirror; + return this; + } + + public EnumAnnotationGem build() { + return new EnumAnnotationGem( this ); + } + } + +} diff --git a/processor/src/test/resources/fixtures/org/mapstruct/tools/gem/processor/EnumAnnotationPackageGem.java b/processor/src/test/resources/fixtures/org/mapstruct/tools/gem/processor/EnumAnnotationPackageGem.java new file mode 100644 index 0000000..8174704 --- /dev/null +++ b/processor/src/test/resources/fixtures/org/mapstruct/tools/gem/processor/EnumAnnotationPackageGem.java @@ -0,0 +1,232 @@ +package org.mapstruct.tools.gem.processor; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.AnnotationValue; +import javax.lang.model.element.Element; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.TypeElement; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.util.ElementFilter; +import org.mapstruct.tools.gem.Gem; +import org.mapstruct.tools.gem.GemValue; + +import org.mapstruct.tools.gem.processor.other.MySimpleEnumGem; + +public class EnumAnnotationPackageGem implements Gem { + + private final GemValue mySimpleEnumWithDefault; + private final GemValue> mySimpleEnumArrayWithDefault; + private final GemValue mySimpleEnum; + private final GemValue> mySimpleEnumArray; + private final boolean isValid; + private final AnnotationMirror mirror; + + private EnumAnnotationPackageGem( BuilderImpl builder ) { + this.mySimpleEnumWithDefault = builder.mySimpleEnumWithDefault; + this.mySimpleEnumArrayWithDefault = builder.mySimpleEnumArrayWithDefault; + this.mySimpleEnum = builder.mySimpleEnum; + this.mySimpleEnumArray = builder.mySimpleEnumArray; + isValid = ( this.mySimpleEnumWithDefault != null && this.mySimpleEnumWithDefault.isValid() ) + && ( this.mySimpleEnumArrayWithDefault != null && this.mySimpleEnumArrayWithDefault.isValid() ) + && ( this.mySimpleEnum != null && this.mySimpleEnum.isValid() ) + && ( this.mySimpleEnumArray != null && this.mySimpleEnumArray.isValid() ); + mirror = builder.mirror; + } + + /** + * accessor + * + * @return the {@link GemValue} for {@link EnumAnnotationPackageGem#mySimpleEnumWithDefault} + */ + public GemValue mySimpleEnumWithDefault( ) { + return mySimpleEnumWithDefault; + } + + /** + * accessor + * + * @return the {@link GemValue} for {@link EnumAnnotationPackageGem#mySimpleEnumArrayWithDefault} + */ + public GemValue> mySimpleEnumArrayWithDefault( ) { + return mySimpleEnumArrayWithDefault; + } + + /** + * accessor + * + * @return the {@link GemValue} for {@link EnumAnnotationPackageGem#mySimpleEnum} + */ + public GemValue mySimpleEnum( ) { + return mySimpleEnum; + } + + /** + * accessor + * + * @return the {@link GemValue} for {@link EnumAnnotationPackageGem#mySimpleEnumArray} + */ + public GemValue> mySimpleEnumArray( ) { + return mySimpleEnumArray; + } + + @Override + public AnnotationMirror mirror( ) { + return mirror; + } + + @Override + public boolean isValid( ) { + return isValid; + } + + public static EnumAnnotationPackageGem instanceOn(Element element) { + return build( element, new BuilderImpl() ); + } + + public static EnumAnnotationPackageGem instanceOn(AnnotationMirror mirror ) { + return build( mirror, new BuilderImpl() ); + } + + public static T build(Element element, Builder builder) { + AnnotationMirror mirror = element.getAnnotationMirrors().stream() + .filter( a -> "org.mapstruct.tools.gem.test.enummapping.EnumAnnotation".contentEquals( ( ( TypeElement )a.getAnnotationType().asElement() ).getQualifiedName() ) ) + .findAny() + .orElse( null ); + return build( mirror, builder ); + } + + public static T build(AnnotationMirror mirror, Builder builder ) { + + // return fast + if ( mirror == null || builder == null ) { + return null; + } + + // fetch defaults from all defined values in the annotation type + List enclosed = ElementFilter.methodsIn( mirror.getAnnotationType().asElement().getEnclosedElements() ); + Map defaultValues = new HashMap<>( enclosed.size() ); + enclosed.forEach( e -> defaultValues.put( e.getSimpleName().toString(), e.getDefaultValue() ) ); + + // fetch all explicitely set annotation values in the annotation instance + Map values = new HashMap<>( enclosed.size() ); + mirror.getElementValues().forEach( (key, value) -> values.put( key.getSimpleName().toString(), value ) ); + + // iterate and populate builder + for ( Map.Entry defaultMethod : defaultValues.entrySet() ) { + String methodName = defaultMethod.getKey(); + AnnotationValue defaultValue = defaultMethod.getValue(); + AnnotationValue value = values.get( methodName ); + switch ( methodName ) { + case "mySimpleEnumWithDefault": + builder.setMysimpleenumwithdefault( GemValue.createEnum( value, defaultValue, MySimpleEnumGem.class ) ); + break; + case "mySimpleEnumArrayWithDefault": + builder.setMysimpleenumarraywithdefault( GemValue.createEnumArray( value, defaultValue, MySimpleEnumGem.class ) ); + break; + case "mySimpleEnum": + builder.setMysimpleenum( GemValue.createEnum( value, defaultValue, MySimpleEnumGem.class ) ); + break; + case "mySimpleEnumArray": + builder.setMysimpleenumarray( GemValue.createEnumArray( value, defaultValue, MySimpleEnumGem.class ) ); + break; + } + } + builder.setMirror( mirror ); + return builder.build(); + } + + /** + * A builder that can be implemented by the user to define custom logic e.g. in the + * build method, prior to creating the annotation gem. + */ + public interface Builder { + + /** + * Sets the {@link GemValue} for {@link EnumAnnotationPackageGem#mySimpleEnumWithDefault} + * + * @return the {@link Builder} for this gem, representing {@link EnumAnnotationPackageGem} + */ + Builder setMysimpleenumwithdefault(GemValue methodName ); + + /** + * Sets the {@link GemValue} for {@link EnumAnnotationPackageGem#mySimpleEnumArrayWithDefault} + * + * @return the {@link Builder} for this gem, representing {@link EnumAnnotationPackageGem} + */ + Builder setMysimpleenumarraywithdefault(GemValue> methodName ); + + /** + * Sets the {@link GemValue} for {@link EnumAnnotationPackageGem#mySimpleEnum} + * + * @return the {@link Builder} for this gem, representing {@link EnumAnnotationPackageGem} + */ + Builder setMysimpleenum(GemValue methodName ); + + /** + * Sets the {@link GemValue} for {@link EnumAnnotationPackageGem#mySimpleEnumArray} + * + * @return the {@link Builder} for this gem, representing {@link EnumAnnotationPackageGem} + */ + Builder setMysimpleenumarray(GemValue> methodName ); + + /** + * Sets the annotation mirror + * + * @param mirror the mirror which this gem represents + * + * @return the {@link Builder} for this gem, representing {@link EnumAnnotationPackageGem} + */ + Builder setMirror( AnnotationMirror mirror ); + + /** + * The build method can be overriden in a custom custom implementation, which allows + * the user to define his own custom validation on the annotation. + * + * @return the representation of the annotation + */ + T build(); + } + + private static class BuilderImpl implements Builder { + + private GemValue mySimpleEnumWithDefault; + private GemValue> mySimpleEnumArrayWithDefault; + private GemValue mySimpleEnum; + private GemValue> mySimpleEnumArray; + private AnnotationMirror mirror; + + public Builder setMysimpleenumwithdefault(GemValue mySimpleEnumWithDefault ) { + this.mySimpleEnumWithDefault = mySimpleEnumWithDefault; + return this; + } + + public Builder setMysimpleenumarraywithdefault(GemValue> mySimpleEnumArrayWithDefault ) { + this.mySimpleEnumArrayWithDefault = mySimpleEnumArrayWithDefault; + return this; + } + + public Builder setMysimpleenum(GemValue mySimpleEnum ) { + this.mySimpleEnum = mySimpleEnum; + return this; + } + + public Builder setMysimpleenumarray(GemValue> mySimpleEnumArray ) { + this.mySimpleEnumArray = mySimpleEnumArray; + return this; + } + + public Builder setMirror( AnnotationMirror mirror ) { + this.mirror = mirror; + return this; + } + + public EnumAnnotationPackageGem build() { + return new EnumAnnotationPackageGem( this ); + } + } + +} diff --git a/processor/src/test/resources/fixtures/org/mapstruct/tools/gem/processor/EnumAnnotationRegisterGem.java b/processor/src/test/resources/fixtures/org/mapstruct/tools/gem/processor/EnumAnnotationRegisterGem.java new file mode 100644 index 0000000..6e71e68 --- /dev/null +++ b/processor/src/test/resources/fixtures/org/mapstruct/tools/gem/processor/EnumAnnotationRegisterGem.java @@ -0,0 +1,231 @@ +package org.mapstruct.tools.gem.processor; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.AnnotationValue; +import javax.lang.model.element.Element; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.TypeElement; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.util.ElementFilter; +import org.mapstruct.tools.gem.Gem; +import org.mapstruct.tools.gem.GemValue; + + +public class EnumAnnotationRegisterGem implements Gem { + + private final GemValue mySimpleEnumWithDefault; + private final GemValue> mySimpleEnumArrayWithDefault; + private final GemValue mySimpleEnum; + private final GemValue> mySimpleEnumArray; + private final boolean isValid; + private final AnnotationMirror mirror; + + private EnumAnnotationRegisterGem( BuilderImpl builder ) { + this.mySimpleEnumWithDefault = builder.mySimpleEnumWithDefault; + this.mySimpleEnumArrayWithDefault = builder.mySimpleEnumArrayWithDefault; + this.mySimpleEnum = builder.mySimpleEnum; + this.mySimpleEnumArray = builder.mySimpleEnumArray; + isValid = ( this.mySimpleEnumWithDefault != null && this.mySimpleEnumWithDefault.isValid() ) + && ( this.mySimpleEnumArrayWithDefault != null && this.mySimpleEnumArrayWithDefault.isValid() ) + && ( this.mySimpleEnum != null && this.mySimpleEnum.isValid() ) + && ( this.mySimpleEnumArray != null && this.mySimpleEnumArray.isValid() ); + mirror = builder.mirror; + } + + /** + * accessor + * + * @return the {@link GemValue} for {@link EnumAnnotationRegisterGem#mySimpleEnumWithDefault} + */ + public GemValue mySimpleEnumWithDefault( ) { + return mySimpleEnumWithDefault; + } + + /** + * accessor + * + * @return the {@link GemValue} for {@link EnumAnnotationRegisterGem#mySimpleEnumArrayWithDefault} + */ + public GemValue> mySimpleEnumArrayWithDefault( ) { + return mySimpleEnumArrayWithDefault; + } + + /** + * accessor + * + * @return the {@link GemValue} for {@link EnumAnnotationRegisterGem#mySimpleEnum} + */ + public GemValue mySimpleEnum( ) { + return mySimpleEnum; + } + + /** + * accessor + * + * @return the {@link GemValue} for {@link EnumAnnotationRegisterGem#mySimpleEnumArray} + */ + public GemValue> mySimpleEnumArray( ) { + return mySimpleEnumArray; + } + + @Override + public AnnotationMirror mirror( ) { + return mirror; + } + + @Override + public boolean isValid( ) { + return isValid; + } + + public static EnumAnnotationRegisterGem instanceOn(Element element) { + return build( element, new BuilderImpl() ); + } + + public static EnumAnnotationRegisterGem instanceOn(AnnotationMirror mirror ) { + return build( mirror, new BuilderImpl() ); + } + + public static T build(Element element, Builder builder) { + AnnotationMirror mirror = element.getAnnotationMirrors().stream() + .filter( a -> "org.mapstruct.tools.gem.test.enummapping.EnumAnnotation".contentEquals( ( ( TypeElement )a.getAnnotationType().asElement() ).getQualifiedName() ) ) + .findAny() + .orElse( null ); + return build( mirror, builder ); + } + + public static T build(AnnotationMirror mirror, Builder builder ) { + + // return fast + if ( mirror == null || builder == null ) { + return null; + } + + // fetch defaults from all defined values in the annotation type + List enclosed = ElementFilter.methodsIn( mirror.getAnnotationType().asElement().getEnclosedElements() ); + Map defaultValues = new HashMap<>( enclosed.size() ); + enclosed.forEach( e -> defaultValues.put( e.getSimpleName().toString(), e.getDefaultValue() ) ); + + // fetch all explicitely set annotation values in the annotation instance + Map values = new HashMap<>( enclosed.size() ); + mirror.getElementValues().forEach( (key, value) -> values.put( key.getSimpleName().toString(), value ) ); + + // iterate and populate builder + for ( Map.Entry defaultMethod : defaultValues.entrySet() ) { + String methodName = defaultMethod.getKey(); + AnnotationValue defaultValue = defaultMethod.getValue(); + AnnotationValue value = values.get( methodName ); + switch ( methodName ) { + case "mySimpleEnumWithDefault": + builder.setMysimpleenumwithdefault( GemValue.createEnum( value, defaultValue, MySimpleEnumGem.class ) ); + break; + case "mySimpleEnumArrayWithDefault": + builder.setMysimpleenumarraywithdefault( GemValue.createEnumArray( value, defaultValue, MySimpleEnumGem.class ) ); + break; + case "mySimpleEnum": + builder.setMysimpleenum( GemValue.createEnum( value, defaultValue, MySimpleEnumGem.class ) ); + break; + case "mySimpleEnumArray": + builder.setMysimpleenumarray( GemValue.createEnumArray( value, defaultValue, MySimpleEnumGem.class ) ); + break; + } + } + builder.setMirror( mirror ); + return builder.build(); + } + + /** + * A builder that can be implemented by the user to define custom logic e.g. in the + * build method, prior to creating the annotation gem. + */ + public interface Builder { + + /** + * Sets the {@link GemValue} for {@link EnumAnnotationRegisterGem#mySimpleEnumWithDefault} + * + * @return the {@link Builder} for this gem, representing {@link EnumAnnotationRegisterGem} + */ + Builder setMysimpleenumwithdefault(GemValue methodName ); + + /** + * Sets the {@link GemValue} for {@link EnumAnnotationRegisterGem#mySimpleEnumArrayWithDefault} + * + * @return the {@link Builder} for this gem, representing {@link EnumAnnotationRegisterGem} + */ + Builder setMysimpleenumarraywithdefault(GemValue> methodName ); + + /** + * Sets the {@link GemValue} for {@link EnumAnnotationRegisterGem#mySimpleEnum} + * + * @return the {@link Builder} for this gem, representing {@link EnumAnnotationRegisterGem} + */ + Builder setMysimpleenum(GemValue methodName ); + + /** + * Sets the {@link GemValue} for {@link EnumAnnotationRegisterGem#mySimpleEnumArray} + * + * @return the {@link Builder} for this gem, representing {@link EnumAnnotationRegisterGem} + */ + Builder setMysimpleenumarray(GemValue> methodName ); + + /** + * Sets the annotation mirror + * + * @param mirror the mirror which this gem represents + * + * @return the {@link Builder} for this gem, representing {@link EnumAnnotationRegisterGem} + */ + Builder setMirror( AnnotationMirror mirror ); + + /** + * The build method can be overriden in a custom custom implementation, which allows + * the user to define his own custom validation on the annotation. + * + * @return the representation of the annotation + */ + T build(); + } + + private static class BuilderImpl implements Builder { + + private GemValue mySimpleEnumWithDefault; + private GemValue> mySimpleEnumArrayWithDefault; + private GemValue mySimpleEnum; + private GemValue> mySimpleEnumArray; + private AnnotationMirror mirror; + + public Builder setMysimpleenumwithdefault(GemValue mySimpleEnumWithDefault ) { + this.mySimpleEnumWithDefault = mySimpleEnumWithDefault; + return this; + } + + public Builder setMysimpleenumarraywithdefault(GemValue> mySimpleEnumArrayWithDefault ) { + this.mySimpleEnumArrayWithDefault = mySimpleEnumArrayWithDefault; + return this; + } + + public Builder setMysimpleenum(GemValue mySimpleEnum ) { + this.mySimpleEnum = mySimpleEnum; + return this; + } + + public Builder setMysimpleenumarray(GemValue> mySimpleEnumArray ) { + this.mySimpleEnumArray = mySimpleEnumArray; + return this; + } + + public Builder setMirror( AnnotationMirror mirror ) { + this.mirror = mirror; + return this; + } + + public EnumAnnotationRegisterGem build() { + return new EnumAnnotationRegisterGem( this ); + } + } + +} diff --git a/processor/src/test/resources/fixtures/org/mapstruct/tools/gem/processor/SimpleEnumGem.java b/processor/src/test/resources/fixtures/org/mapstruct/tools/gem/processor/SimpleEnumGem.java new file mode 100644 index 0000000..350c77a --- /dev/null +++ b/processor/src/test/resources/fixtures/org/mapstruct/tools/gem/processor/SimpleEnumGem.java @@ -0,0 +1,10 @@ +package org.mapstruct.tools.gem.processor; + +/** + * Gem for the enum {@link org.mapstruct.tools.gem.test.enummapping.SimpleEnum} +*/ +public enum SimpleEnumGem { + A, + B, + C +} \ No newline at end of file diff --git a/processor/src/test/resources/fixtures/org/mapstruct/tools/gem/processor/SomeAnnotationGem.java b/processor/src/test/resources/fixtures/org/mapstruct/tools/gem/processor/SomeAnnotationGem.java index e86267e..7762f75 100644 --- a/processor/src/test/resources/fixtures/org/mapstruct/tools/gem/processor/SomeAnnotationGem.java +++ b/processor/src/test/resources/fixtures/org/mapstruct/tools/gem/processor/SomeAnnotationGem.java @@ -28,6 +28,7 @@ public class SomeAnnotationGem implements Gem { private final GemValue myDoubleWithDefault; private final GemValue myStringWithDefault; private final GemValue myEnumWithDefault; + private final GemValue> myEnumArrayWithDefault; private final GemValue myClass; private final GemValue myBoolean; private final GemValue myChar; @@ -39,6 +40,7 @@ public class SomeAnnotationGem implements Gem { private final GemValue myDouble; private final GemValue myString; private final GemValue myEnum; + private final GemValue> myEnumArray; private final boolean isValid; private final AnnotationMirror mirror; @@ -54,6 +56,7 @@ private SomeAnnotationGem( BuilderImpl builder ) { this.myDoubleWithDefault = builder.myDoubleWithDefault; this.myStringWithDefault = builder.myStringWithDefault; this.myEnumWithDefault = builder.myEnumWithDefault; + this.myEnumArrayWithDefault = builder.myEnumArrayWithDefault; this.myClass = builder.myClass; this.myBoolean = builder.myBoolean; this.myChar = builder.myChar; @@ -65,6 +68,7 @@ private SomeAnnotationGem( BuilderImpl builder ) { this.myDouble = builder.myDouble; this.myString = builder.myString; this.myEnum = builder.myEnum; + this.myEnumArray = builder.myEnumArray; isValid = ( this.myClassWithDefault != null && this.myClassWithDefault.isValid() ) && ( this.myBooleanWithDefault != null && this.myBooleanWithDefault.isValid() ) && ( this.myCharWithDefault != null && this.myCharWithDefault.isValid() ) @@ -76,6 +80,7 @@ private SomeAnnotationGem( BuilderImpl builder ) { && ( this.myDoubleWithDefault != null && this.myDoubleWithDefault.isValid() ) && ( this.myStringWithDefault != null && this.myStringWithDefault.isValid() ) && ( this.myEnumWithDefault != null && this.myEnumWithDefault.isValid() ) + && ( this.myEnumArrayWithDefault != null && this.myEnumArrayWithDefault.isValid() ) && ( this.myClass != null && this.myClass.isValid() ) && ( this.myBoolean != null && this.myBoolean.isValid() ) && ( this.myChar != null && this.myChar.isValid() ) @@ -86,7 +91,8 @@ private SomeAnnotationGem( BuilderImpl builder ) { && ( this.myFloat != null && this.myFloat.isValid() ) && ( this.myDouble != null && this.myDouble.isValid() ) && ( this.myString != null && this.myString.isValid() ) - && ( this.myEnum != null && this.myEnum.isValid() ); + && ( this.myEnum != null && this.myEnum.isValid() ) + && ( this.myEnumArray != null && this.myEnumArray.isValid() ); mirror = builder.mirror; } @@ -189,6 +195,15 @@ public GemValue myEnumWithDefault( ) { return myEnumWithDefault; } + /** + * accessor + * + * @return the {@link GemValue} for {@link SomeAnnotationGem#myEnumArrayWithDefault} + */ + public GemValue> myEnumArrayWithDefault( ) { + return myEnumArrayWithDefault; + } + /** * accessor * @@ -288,6 +303,15 @@ public GemValue myEnum( ) { return myEnum; } + /** + * accessor + * + * @return the {@link GemValue} for {@link SomeAnnotationGem#myEnumArray} + */ + public GemValue> myEnumArray( ) { + return myEnumArray; + } + @Override public AnnotationMirror mirror( ) { return mirror; @@ -369,6 +393,9 @@ public static T build(AnnotationMirror mirror, Builder builder ) { case "myEnumWithDefault": builder.setMyenumwithdefault( GemValue.createEnum( value, defaultValue ) ); break; + case "myEnumArrayWithDefault": + builder.setMyenumarraywithdefault( GemValue.createEnumArray( value, defaultValue ) ); + break; case "myClass": builder.setMyclass( GemValue.create( value, defaultValue, TypeMirror.class ) ); break; @@ -402,6 +429,9 @@ public static T build(AnnotationMirror mirror, Builder builder ) { case "myEnum": builder.setMyenum( GemValue.createEnum( value, defaultValue ) ); break; + case "myEnumArray": + builder.setMyenumarray( GemValue.createEnumArray( value, defaultValue ) ); + break; } } builder.setMirror( mirror ); @@ -491,6 +521,13 @@ public interface Builder { */ Builder setMyenumwithdefault(GemValue methodName ); + /** + * Sets the {@link GemValue} for {@link SomeAnnotationGem#myEnumArrayWithDefault} + * + * @return the {@link Builder} for this gem, representing {@link SomeAnnotationGem} + */ + Builder setMyenumarraywithdefault(GemValue> methodName ); + /** * Sets the {@link GemValue} for {@link SomeAnnotationGem#myClass} * @@ -568,6 +605,13 @@ public interface Builder { */ Builder setMyenum(GemValue methodName ); + /** + * Sets the {@link GemValue} for {@link SomeAnnotationGem#myEnumArray} + * + * @return the {@link Builder} for this gem, representing {@link SomeAnnotationGem} + */ + Builder setMyenumarray(GemValue> methodName ); + /** * Sets the annotation mirror * @@ -599,6 +643,7 @@ private static class BuilderImpl implements Builder { private GemValue myDoubleWithDefault; private GemValue myStringWithDefault; private GemValue myEnumWithDefault; + private GemValue> myEnumArrayWithDefault; private GemValue myClass; private GemValue myBoolean; private GemValue myChar; @@ -610,6 +655,7 @@ private static class BuilderImpl implements Builder { private GemValue myDouble; private GemValue myString; private GemValue myEnum; + private GemValue> myEnumArray; private AnnotationMirror mirror; public Builder setMyclasswithdefault(GemValue myClassWithDefault ) { @@ -667,6 +713,11 @@ public Builder setMyenumwithdefault(GemValue myEnumWi return this; } + public Builder setMyenumarraywithdefault(GemValue> myEnumArrayWithDefault ) { + this.myEnumArrayWithDefault = myEnumArrayWithDefault; + return this; + } + public Builder setMyclass(GemValue myClass ) { this.myClass = myClass; return this; @@ -722,6 +773,11 @@ public Builder setMyenum(GemValue myEnum ) { return this; } + public Builder setMyenumarray(GemValue> myEnumArray ) { + this.myEnumArray = myEnumArray; + return this; + } + public Builder setMirror( AnnotationMirror mirror ) { this.mirror = mirror; return this; diff --git a/test/src/main/java/org/mapstruct/tools/gem/Tester.java b/test/src/main/java/org/mapstruct/tools/gem/Tester.java index ff47e8b..dfdc5ca 100644 --- a/test/src/main/java/org/mapstruct/tools/gem/Tester.java +++ b/test/src/main/java/org/mapstruct/tools/gem/Tester.java @@ -18,7 +18,8 @@ myFloat = 99.3f, myDouble = 5033.19d, myString = "some", - myEnum = SomeAnnotation.TEST.B + myEnum = SomeAnnotation.TEST.B, + myEnumArray = {SomeAnnotation.TEST.B} ) public class Tester { } diff --git a/test/src/main/java/org/mapstruct/tools/gem/test/SomeAnnotation.java b/test/src/main/java/org/mapstruct/tools/gem/test/SomeAnnotation.java index 3dd9ba6..4371185 100644 --- a/test/src/main/java/org/mapstruct/tools/gem/test/SomeAnnotation.java +++ b/test/src/main/java/org/mapstruct/tools/gem/test/SomeAnnotation.java @@ -19,7 +19,7 @@ @Target(ElementType.TYPE ) public @interface SomeAnnotation { - enum TEST { A, B }; + enum TEST { A, B } Class myClassWithDefault() default SomeAnnotation.class; @@ -43,6 +43,8 @@ enum TEST { A, B }; TEST myEnumWithDefault() default TEST.A; + TEST[] myEnumArrayWithDefault() default {TEST.A, TEST.B}; + Class myClass(); boolean myBoolean(); @@ -65,4 +67,6 @@ enum TEST { A, B }; TEST myEnum(); + TEST[] myEnumArray(); + } diff --git a/test/src/main/java/org/mapstruct/tools/gem/test/enummapping/EnumAnnotation.java b/test/src/main/java/org/mapstruct/tools/gem/test/enummapping/EnumAnnotation.java new file mode 100644 index 0000000..9a9b3bf --- /dev/null +++ b/test/src/main/java/org/mapstruct/tools/gem/test/enummapping/EnumAnnotation.java @@ -0,0 +1,20 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at https://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.tools.gem.test.enummapping; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.SOURCE) +@Target(ElementType.TYPE ) +public @interface EnumAnnotation { + SimpleEnum mySimpleEnumWithDefault() default SimpleEnum.C; + SimpleEnum[] mySimpleEnumArrayWithDefault() default {SimpleEnum.A, SimpleEnum.B}; + SimpleEnum mySimpleEnum(); + SimpleEnum[] mySimpleEnumArray(); +} diff --git a/test/src/main/java/org/mapstruct/tools/gem/test/enummapping/SimpleEnum.java b/test/src/main/java/org/mapstruct/tools/gem/test/enummapping/SimpleEnum.java new file mode 100644 index 0000000..9d01d81 --- /dev/null +++ b/test/src/main/java/org/mapstruct/tools/gem/test/enummapping/SimpleEnum.java @@ -0,0 +1,10 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at https://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.tools.gem.test.enummapping; + +public enum SimpleEnum { + A,B,C +}