From 7b5a54971f9b9695a88b9d95dd9eadec42da5a19 Mon Sep 17 00:00:00 2001 From: Filip Hrisafov Date: Mon, 18 May 2020 07:17:30 +0200 Subject: [PATCH] Add EnumTransformationStrategy SPI (#2089) Add a new custom EnumTransformationStrategy SPI which can be used for providing custom way of name based mapping for enums. Add 4 out of the box transformation strategies: * prefix - add a prefix to the name based enum mapping * stripPrefix - remove a prefix from the name based enum mapping * suffix - add a suffix to the name based enum mapping * stripSuffix - remove a suffix from the name based enum mapping This can be achieved by using the new `EnumMapping` e.g. Add suffix `_TYPE` to all enums: `@EnumMapping(nameTransformationStrategy = "suffix", configuration = "_TYPE")` With this it would be possible to achieve what is needed in #796, #1220, #1789. --- .../main/java/org/mapstruct/EnumMapping.java | 123 ++++++++++++++++++ .../java/org/mapstruct/MappingConstants.java | 30 +++++ .../chapter-13-using-mapstruct-spi.asciidoc | 16 +++ .../chapter-8-mapping-values.asciidoc | 103 +++++++++++++++ .../ap/internal/gem/GemGenerator.java | 2 + .../ap/internal/gem/MappingConstantsGem.java | 8 ++ .../internal/model/AbstractBaseBuilder.java | 1 + .../internal/model/MappingBuilderContext.java | 10 ++ .../ap/internal/model/ValueMappingMethod.java | 111 ++++++++++++++-- .../model/source/EnumMappingOptions.java | 99 ++++++++++++++ .../model/source/MappingMethodOptions.java | 23 ++++ .../internal/model/source/SourceMethod.java | 17 ++- .../DefaultModelElementProcessorContext.java | 8 ++ .../processor/MapperCreationProcessor.java | 7 +- .../processor/MethodRetrievalProcessor.java | 13 ++ .../processor/ModelElementProcessor.java | 4 + .../util/AnnotationProcessorContext.java | 31 +++++ .../mapstruct/ap/internal/util/Message.java | 1 + .../ap/spi/EnumTransformationStrategy.java | 44 +++++++ .../spi/PrefixEnumTransformationStrategy.java | 22 ++++ ...StripPrefixEnumTransformationStrategy.java | 25 ++++ ...StripSuffixEnumTransformationStrategy.java | 25 ++++ .../spi/SuffixEnumTransformationStrategy.java | 22 ++++ ...apstruct.ap.spi.EnumTransformationStrategy | 8 ++ .../mapstruct/ap/test/gem/ConstantTest.java | 6 + .../test/value/enum2string/OrderMapper.java | 2 + .../CheeseEnumToStringPrefixMapper.java | 34 +++++ .../CheeseEnumToStringSuffixMapper.java | 36 +++++ .../CheesePrefixMapper.java | 33 +++++ .../CheeseSuffixMapper.java | 36 +++++ .../value/nametransformation/CheeseType.java | 15 +++ .../CheeseTypeCustomSuffix.java | 15 +++ .../CheeseTypePrefixed.java | 16 +++ .../CheeseTypeSuffixed.java | 16 +++ .../CustomEnumTransformationStrategy.java | 30 +++++ .../EnumNameTransformationStrategyTest.java | 115 ++++++++++++++++ .../ErroneousNameTransformStrategyMapper.java | 22 ++++ 37 files changed, 1115 insertions(+), 14 deletions(-) create mode 100644 core/src/main/java/org/mapstruct/EnumMapping.java create mode 100644 processor/src/main/java/org/mapstruct/ap/internal/model/source/EnumMappingOptions.java create mode 100644 processor/src/main/java/org/mapstruct/ap/spi/EnumTransformationStrategy.java create mode 100644 processor/src/main/java/org/mapstruct/ap/spi/PrefixEnumTransformationStrategy.java create mode 100644 processor/src/main/java/org/mapstruct/ap/spi/StripPrefixEnumTransformationStrategy.java create mode 100644 processor/src/main/java/org/mapstruct/ap/spi/StripSuffixEnumTransformationStrategy.java create mode 100644 processor/src/main/java/org/mapstruct/ap/spi/SuffixEnumTransformationStrategy.java create mode 100644 processor/src/main/resources/META-INF/services/org.mapstruct.ap.spi.EnumTransformationStrategy create mode 100644 processor/src/test/java/org/mapstruct/ap/test/value/nametransformation/CheeseEnumToStringPrefixMapper.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/value/nametransformation/CheeseEnumToStringSuffixMapper.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/value/nametransformation/CheesePrefixMapper.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/value/nametransformation/CheeseSuffixMapper.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/value/nametransformation/CheeseType.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/value/nametransformation/CheeseTypeCustomSuffix.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/value/nametransformation/CheeseTypePrefixed.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/value/nametransformation/CheeseTypeSuffixed.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/value/nametransformation/CustomEnumTransformationStrategy.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/value/nametransformation/EnumNameTransformationStrategyTest.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/value/nametransformation/ErroneousNameTransformStrategyMapper.java diff --git a/core/src/main/java/org/mapstruct/EnumMapping.java b/core/src/main/java/org/mapstruct/EnumMapping.java new file mode 100644 index 000000000..c184448aa --- /dev/null +++ b/core/src/main/java/org/mapstruct/EnumMapping.java @@ -0,0 +1,123 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Configured the mapping between two value types. + *

Example: Using a suffix for enums

+ *

+ * public enum CheeseType {
+ *     BRIE,
+ *     ROQUEFORT
+ * }
+ *
+ * public enum CheeseTypeSuffixed {
+ *     BRIE_TYPE,
+ *     ROQUEFORT_TYPE
+ * }
+ *
+ * @Mapper
+ * public interface CheeseMapper {
+ *
+ *     @EnumMapping(nameTransformationStrategy = "suffix", configuration = "_TYPE")
+ *     CheeseTypeSuffixed map(Cheese cheese);
+ *
+ *     @InheritInverseConfiguration
+ *     Cheese map(CheeseTypeSuffixed cheese);
+ *
+ * }
+ * 
+ *

+ * // generates
+ * public class CheeseMapperImpl implements CheeseMapper {
+ *
+ *     @Override
+ *     public CheeseTypeSuffixed map(Cheese cheese) {
+ *         if ( cheese == null ) {
+ *             return null;
+ *         }
+ *
+ *         CheeseTypeSuffixed cheeseTypeSuffixed;
+ *
+ *         switch ( cheese ) {
+ *             case BRIE:
+ *                 cheeseTypeSuffixed = CheeseTypeSuffixed.BRIE_TYPE;
+ *                 break;
+ *             case ROQUEFORT:
+ *                 cheeseTypeSuffixed = CheeseTypeSuffixed.ROQUEFORT_TYPE;
+ *                 break;
+ *             default:
+ *                 throw new IllegalArgumentException( "Unexpected enum constant: " + cheese );
+ *         }
+ *
+ *         return cheeseTypeSuffixed;
+ *     }
+ *
+ *     @Override
+ *     public Cheese map(CheeseTypeSuffixed cheese) {
+ *         if ( cheese == null ) {
+ *             return null;
+ *         }
+ *
+ *         CheeseType cheeseType;
+ *
+ *         switch ( cheese ) {
+ *             case BRIE_TYPE:
+ *                 cheeseType = CheeseType.BRIE;
+ *                 break;
+ *             case ROQUEFORT_TYPE:
+ *                 cheeseType = CheeseType.ROQUEFORT;
+ *                 break;
+ *             default:
+ *                 throw new IllegalArgumentException( "Unexpected enum constant: " + cheese );
+ *         }
+ *
+ *         return cheeseType;
+ *     }
+ * }
+ * 
+ * + * @author Filip Hrisafov + * @since 1.4 + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.CLASS) +public @interface EnumMapping { + + /** + * Specifies the name transformation strategy that should be used for implicit mapping between enums. + * Known strategies are: + * + * + * It is possible to use custom name transformation strategies by implementing the {@code + * EnumTransformationStrategy} SPI. + * + * @return the name transformation strategy + */ + String nameTransformationStrategy(); + + /** + * The configuration that should be passed on the appropriate name transformation strategy. + * e.g. a suffix that should be applied to the source enum when doing name based mapping. + * + * @return the configuration to use + */ + String configuration(); +} diff --git a/core/src/main/java/org/mapstruct/MappingConstants.java b/core/src/main/java/org/mapstruct/MappingConstants.java index af52d58f2..2c5757038 100644 --- a/core/src/main/java/org/mapstruct/MappingConstants.java +++ b/core/src/main/java/org/mapstruct/MappingConstants.java @@ -36,4 +36,34 @@ public final class MappingConstants { */ public static final String ANY_UNMAPPED = ""; + /** + * In an {@link EnumMapping} this represent the enum transformation strategy that adds a suffix to the source enum. + * + * @since 1.4 + */ + public static final String SUFFIX_TRANSFORMATION = "suffix"; + + /** + * In an {@link EnumMapping} this represent the enum transformation strategy that strips a suffix from the source + * enum. + * + * @since 1.4 + */ + public static final String STRIP_SUFFIX_TRANSFORMATION = "stripSuffix"; + + /** + * In an {@link EnumMapping} this represent the enum transformation strategy that adds a prefix to the source enum. + * + * @since 1.4 + */ + public static final String PREFIX_TRANSFORMATION = "prefix"; + + /** + * In an {@link EnumMapping} this represent the enum transformation strategy that strips a prefix from the source + * enum. + * + * @since 1.4 + */ + public static final String STRIP_PREFIX_TRANSFORMATION = "stripPrefix"; + } diff --git a/documentation/src/main/asciidoc/chapter-13-using-mapstruct-spi.asciidoc b/documentation/src/main/asciidoc/chapter-13-using-mapstruct-spi.asciidoc index b15257047..ddf8327f7 100644 --- a/documentation/src/main/asciidoc/chapter-13-using-mapstruct-spi.asciidoc +++ b/documentation/src/main/asciidoc/chapter-13-using-mapstruct-spi.asciidoc @@ -197,4 +197,20 @@ A nice example is to provide support for a custom builder strategy. ---- include::{processor-ap-main}/spi/NoOpBuilderProvider.java[tag=documentation] ---- +==== + + +[[custom-enum-transformation-strategy]] +=== Custom Enum Transformation Strategy + +MapStruct offers the possibility to other transformations strategies by implementing `EnumTransformationStrategy` via the Service Provider Interface (SPI). +A nice example is to provide support for a custom transformation strategy. + +.Custom Enum Transformation Strategy which lower-cases the value and applies a suffix +==== +[source, java, linenums] +[subs="verbatim,attributes"] +---- +include::{processor-ap-test}/value/nametransformation/CustomEnumTransformationStrategy.java[tag=documentation] +---- ==== \ No newline at end of file diff --git a/documentation/src/main/asciidoc/chapter-8-mapping-values.asciidoc b/documentation/src/main/asciidoc/chapter-8-mapping-values.asciidoc index 7bfd6afa4..000fd0652 100644 --- a/documentation/src/main/asciidoc/chapter-8-mapping-values.asciidoc +++ b/documentation/src/main/asciidoc/chapter-8-mapping-values.asciidoc @@ -159,3 +159,106 @@ MapStruct supports enum to a String mapping along the same lines as is described 2. Similarity: ` stops after handling defined mapping and proceeds to the switch/default clause value. 3. Similarity: `` will create a mapping for each target enum constant and proceed to the switch/default clause value. 4. Difference: A switch/default value needs to be provided to have a determined outcome (enum has a limited set of values, `String` has unlimited options). Failing to specify `` or ` will result in a warning. + +=== Custom name transformation + +When no `@ValueMapping`(s) are defined then each constant from the source enum is mapped to a constant with the same name in the target enum type. +However, there are cases where the source enum needs to be transformed before doing the mapping. +E.g. a suffix needs to be applied to map from the source into the target enum. + +.Enum types +==== +[source, java, linenums] +[subs="verbatim,attributes"] +---- +public enum CheeseType { + + BRIE, + ROQUEFORT +} + +public enum CheeseTypeSuffixed { + + BRIE_TYPE, + ROQUEFORT_TYPE +} +---- +==== + +.Enum mapping method with custom name transformation strategy +==== +[source, java, linenums] +[subs="verbatim,attributes"] +---- +@Mapper +public interface CheeseMapper { + + CheeseMapper INSTANCE = Mappers.getMapper( CheeseMapper.class ); + + @EnumMapping(nameTransformationStrategy = "suffix", configuration = "_TYPE") + CheeseTypeSuffixed map(CheeseType cheese); + + @InheritInverseConfiguration + CheeseType map(CheeseTypeSuffix cheese); +} +---- +==== + +.Enum mapping method with custom name transformation strategy result +==== +[source, java, linenums] +[subs="verbatim,attributes"] +---- +// GENERATED CODE +public class CheeseSuffixMapperImpl implements CheeseSuffixMapper { + + @Override + public CheeseTypeSuffixed map(CheeseType cheese) { + if ( cheese == null ) { + return null; + } + + CheeseTypeSuffixed cheeseTypeSuffixed; + + switch ( cheese ) { + case BRIE: cheeseTypeSuffixed = CheeseTypeSuffixed.BRIE_TYPE; + break; + case ROQUEFORT: cheeseTypeSuffixed = CheeseTypeSuffixed.ROQUEFORT_TYPE; + break; + default: throw new IllegalArgumentException( "Unexpected enum constant: " + cheese ); + } + + return cheeseTypeSuffixed; + } + + @Override + public CheeseType map(CheeseTypeSuffixed cheese) { + if ( cheese == null ) { + return null; + } + + CheeseType cheeseType; + + switch ( cheese ) { + case BRIE_TYPE: cheeseType = CheeseType.BRIE; + break; + case ROQUEFORT_TYPE: cheeseType = CheeseType.ROQUEFORT; + break; + default: throw new IllegalArgumentException( "Unexpected enum constant: " + cheese ); + } + + return cheeseType; + } +} +---- +==== + +MapStruct provides the following out of the box enum name transformation strategies: + +* _suffix_ - Applies a suffix on the source enum +* _stripSuffix_ - Strips a suffix from the source enum +* _prefix_ - Applies a prefix on the source enum +* _stripPrefix_ - Strips a prefix from the source enum + +It is also possible to register custom strategies. +For more information on how to do that have a look at <> diff --git a/processor/src/main/java/org/mapstruct/ap/internal/gem/GemGenerator.java b/processor/src/main/java/org/mapstruct/ap/internal/gem/GemGenerator.java index b4d7758a1..524ea995d 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/gem/GemGenerator.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/gem/GemGenerator.java @@ -14,6 +14,7 @@ import org.mapstruct.BeforeMapping; import org.mapstruct.Builder; import org.mapstruct.Context; import org.mapstruct.DecoratedWith; +import org.mapstruct.EnumMapping; import org.mapstruct.InheritConfiguration; import org.mapstruct.InheritInverseConfiguration; import org.mapstruct.IterableMapping; @@ -43,6 +44,7 @@ import org.mapstruct.tools.gem.GemDefinition; @GemDefinition(Mappings.class) @GemDefinition(IterableMapping.class) @GemDefinition(BeanMapping.class) +@GemDefinition(EnumMapping.class) @GemDefinition(MapMapping.class) @GemDefinition(TargetType.class) @GemDefinition(MappingTarget.class) diff --git a/processor/src/main/java/org/mapstruct/ap/internal/gem/MappingConstantsGem.java b/processor/src/main/java/org/mapstruct/ap/internal/gem/MappingConstantsGem.java index b988ccde4..685f5a543 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/gem/MappingConstantsGem.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/gem/MappingConstantsGem.java @@ -20,4 +20,12 @@ public final class MappingConstantsGem { public static final String ANY_REMAINING = ""; public static final String ANY_UNMAPPED = ""; + + public static final String SUFFIX_TRANSFORMATION = "suffix"; + + public static final String STRIP_SUFFIX_TRANSFORMATION = "stripSuffix"; + + public static final String PREFIX_TRANSFORMATION = "prefix"; + + public static final String STRIP_PREFIX_TRANSFORMATION = "stripPrefix"; } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/AbstractBaseBuilder.java b/processor/src/main/java/org/mapstruct/ap/internal/model/AbstractBaseBuilder.java index 7bf720de7..f2e657f8e 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/AbstractBaseBuilder.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/AbstractBaseBuilder.java @@ -86,6 +86,7 @@ class AbstractBaseBuilder> { forgedMappingMethod = new ValueMappingMethod.Builder() .method( forgedMethod ) .valueMappings( forgedMethod.getOptions().getValueMappings() ) + .enumMapping( forgedMethod.getOptions().getEnumMappingOptions() ) .mappingContext( ctx ) .build(); } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/MappingBuilderContext.java b/processor/src/main/java/org/mapstruct/ap/internal/model/MappingBuilderContext.java index 2a752b11b..e8d649783 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/MappingBuilderContext.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/MappingBuilderContext.java @@ -30,6 +30,7 @@ import org.mapstruct.ap.internal.option.Options; import org.mapstruct.ap.internal.util.AccessorNamingUtils; import org.mapstruct.ap.internal.util.FormattingMessager; import org.mapstruct.ap.internal.util.Services; +import org.mapstruct.ap.spi.EnumTransformationStrategy; import org.mapstruct.ap.spi.MappingExclusionProvider; /** @@ -104,6 +105,7 @@ public class MappingBuilderContext { private final Types typeUtils; private final FormattingMessager messager; private final AccessorNamingUtils accessorNaming; + private final Map enumTransformationStrategies; private final Options options; private final TypeElement mapperTypeElement; private final List sourceModel; @@ -113,11 +115,13 @@ public class MappingBuilderContext { private final Map forgedMethodsUnderCreation = new HashMap<>(); + //CHECKSTYLE:OFF public MappingBuilderContext(TypeFactory typeFactory, Elements elementUtils, Types typeUtils, FormattingMessager messager, AccessorNamingUtils accessorNaming, + Map enumTransformationStrategies, Options options, MappingResolver mappingResolver, TypeElement mapper, @@ -128,12 +132,14 @@ public class MappingBuilderContext { this.typeUtils = typeUtils; this.messager = messager; this.accessorNaming = accessorNaming; + this.enumTransformationStrategies = enumTransformationStrategies; this.options = options; this.mappingResolver = mappingResolver; this.mapperTypeElement = mapper; this.sourceModel = sourceModel; this.mapperReferences = mapperReferences; } + //CHECKSTYLE:ON /** * Returns a map which is used to track which forged methods are under creation. @@ -180,6 +186,10 @@ public class MappingBuilderContext { return accessorNaming; } + public Map getEnumTransformationStrategies() { + return enumTransformationStrategies; + } + public Options getOptions() { return options; } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/ValueMappingMethod.java b/processor/src/main/java/org/mapstruct/ap/internal/model/ValueMappingMethod.java index 700cf4cd2..218c754e3 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/ValueMappingMethod.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/ValueMappingMethod.java @@ -7,19 +7,23 @@ package org.mapstruct.ap.internal.model; import java.util.ArrayList; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.Set; import javax.lang.model.type.TypeMirror; import javax.lang.model.util.Types; +import org.mapstruct.ap.internal.gem.BeanMappingGem; import org.mapstruct.ap.internal.model.common.Parameter; import org.mapstruct.ap.internal.model.common.Type; +import org.mapstruct.ap.internal.model.source.EnumMappingOptions; import org.mapstruct.ap.internal.model.source.Method; import org.mapstruct.ap.internal.model.source.SelectionParameters; import org.mapstruct.ap.internal.model.source.ValueMappingOptions; -import org.mapstruct.ap.internal.gem.BeanMappingGem; import org.mapstruct.ap.internal.util.Message; import org.mapstruct.ap.internal.util.Strings; +import org.mapstruct.ap.spi.EnumTransformationStrategy; import static org.mapstruct.ap.internal.gem.MappingConstantsGem.ANY_REMAINING; import static org.mapstruct.ap.internal.gem.MappingConstantsGem.ANY_UNMAPPED; @@ -45,6 +49,8 @@ public class ValueMappingMethod extends MappingMethod { private Method method; private MappingBuilderContext ctx; private ValueMappings valueMappings; + private EnumMappingOptions enumMapping; + private EnumTransformationStrategyInvoker enumTransformationInvoker; public Builder mappingContext(MappingBuilderContext mappingContext) { this.ctx = mappingContext; @@ -61,7 +67,18 @@ public class ValueMappingMethod extends MappingMethod { return this; } - public ValueMappingMethod build( ) { + public Builder enumMapping(EnumMappingOptions enumMapping) { + this.enumMapping = enumMapping; + return this; + } + + public ValueMappingMethod build() { + + if ( !enumMapping.isValid() ) { + return null; + } + + initializeEnumTransformationStrategy(); // initialize all relevant parameters List mappingEntries = new ArrayList<>(); @@ -99,6 +116,23 @@ public class ValueMappingMethod extends MappingMethod { ); } + private void initializeEnumTransformationStrategy() { + if ( !enumMapping.hasAnnotation() ) { + enumTransformationInvoker = EnumTransformationStrategyInvoker.DEFAULT; + } + else { + Map enumTransformationStrategies = + ctx.getEnumTransformationStrategies(); + + String nameTransformationStrategy = enumMapping.getNameTransformationStrategy(); + if ( enumTransformationStrategies.containsKey( nameTransformationStrategy ) ) { + enumTransformationInvoker = new EnumTransformationStrategyInvoker( enumTransformationStrategies.get( + nameTransformationStrategy ), enumMapping.getNameTransformationConfiguration() ); + } + } + + } + private List enumToEnumMapping(Method method, Type sourceType, Type targetType ) { List mappings = new ArrayList<>(); @@ -120,11 +154,36 @@ public class ValueMappingMethod extends MappingMethod { // add mappings based on name if ( !valueMappings.hasMapAnyUnmapped ) { - // get all target constants - List targetConstants = method.getReturnType().getEnumConstants(); + // We store the target constants in a map in order to support inherited inverse mapping + // When using a nameTransformationStrategy the transformation should be done on the target enum + // instead of the source enum + Map targetConstants = new LinkedHashMap<>(); + + boolean enumMappingInverse = enumMapping.isInverse(); + for ( String targetEnumConstant : method.getReturnType().getEnumConstants() ) { + if ( enumMappingInverse ) { + // If the mapping is inverse we have to change the target enum constant + targetConstants.put( + enumTransformationInvoker.transform( targetEnumConstant ), + targetEnumConstant + ); + } + else { + targetConstants.put( targetEnumConstant, targetEnumConstant ); + } + } + for ( String sourceConstant : new ArrayList<>( unmappedSourceConstants ) ) { - if ( targetConstants.contains( sourceConstant ) ) { - mappings.add( new MappingEntry( sourceConstant, sourceConstant ) ); + String targetConstant; + if ( !enumMappingInverse ) { + targetConstant = enumTransformationInvoker.transform( sourceConstant ); + } + else { + targetConstant = sourceConstant; + } + + if ( targetConstants.containsKey( targetConstant ) ) { + mappings.add( new MappingEntry( sourceConstant, targetConstants.get( targetConstant ) ) ); unmappedSourceConstants.remove( sourceConstant ); } } @@ -175,7 +234,8 @@ public class ValueMappingMethod extends MappingMethod { // all remaining constants are mapped for ( String sourceConstant : unmappedSourceConstants ) { - mappings.add( new MappingEntry( sourceConstant, sourceConstant ) ); + String targetConstant = enumTransformationInvoker.transform( sourceConstant ); + mappings.add( new MappingEntry( sourceConstant, targetConstant ) ); } } return mappings; @@ -204,7 +264,8 @@ public class ValueMappingMethod extends MappingMethod { // all remaining constants are mapped for ( String sourceConstant : unmappedSourceConstants ) { - mappings.add( new MappingEntry( sourceConstant, sourceConstant ) ); + String stringConstant = enumTransformationInvoker.transform( sourceConstant ); + mappings.add( new MappingEntry( stringConstant, sourceConstant ) ); } } return mappings; @@ -229,7 +290,8 @@ public class ValueMappingMethod extends MappingMethod { for ( ValueMappingOptions mappedConstant : valueMappings.regularValueMappings ) { if ( !sourceEnumConstants.contains( mappedConstant.getSource() ) ) { - ctx.getMessager().printMessage( method.getExecutable(), + ctx.getMessager().printMessage( + method.getExecutable(), mappedConstant.getMirror(), mappedConstant.getSourceAnnotationValue(), Message.VALUEMAPPING_NON_EXISTING_CONSTANT, @@ -279,7 +341,8 @@ public class ValueMappingMethod extends MappingMethod { for ( ValueMappingOptions mappedConstant : valueMappings.regularValueMappings ) { if ( !NULL.equals( mappedConstant.getTarget() ) && !targetEnumConstants.contains( mappedConstant.getTarget() ) ) { - ctx.getMessager().printMessage( method.getExecutable(), + ctx.getMessager().printMessage( + method.getExecutable(), mappedConstant.getMirror(), mappedConstant.getTargetAnnotationValue(), Message.VALUEMAPPING_NON_EXISTING_CONSTANT, @@ -292,7 +355,8 @@ public class ValueMappingMethod extends MappingMethod { if ( valueMappings.defaultTarget != null && !NULL.equals( valueMappings.defaultTarget.getTarget() ) && !targetEnumConstants.contains( valueMappings.defaultTarget.getTarget() ) ) { - ctx.getMessager().printMessage( method.getExecutable(), + ctx.getMessager().printMessage( + method.getExecutable(), valueMappings.defaultTarget.getMirror(), valueMappings.defaultTarget.getTargetAnnotationValue(), Message.VALUEMAPPING_NON_EXISTING_CONSTANT, @@ -318,6 +382,31 @@ public class ValueMappingMethod extends MappingMethod { } } + private static class EnumTransformationStrategyInvoker { + + private static final EnumTransformationStrategyInvoker DEFAULT = new EnumTransformationStrategyInvoker( + null, + null + ); + + private final EnumTransformationStrategy transformationStrategy; + private final String configuration; + + private EnumTransformationStrategyInvoker( + EnumTransformationStrategy transformationStrategy, String configuration) { + this.transformationStrategy = transformationStrategy; + this.configuration = configuration; + } + + private String transform(String source) { + if ( transformationStrategy == null ) { + return source; + } + + return transformationStrategy.transform( source, configuration ); + } + } + private static class ValueMappings { List regularValueMappings = new ArrayList<>(); diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/source/EnumMappingOptions.java b/processor/src/main/java/org/mapstruct/ap/internal/model/source/EnumMappingOptions.java new file mode 100644 index 000000000..576474e68 --- /dev/null +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/source/EnumMappingOptions.java @@ -0,0 +1,99 @@ +/* + * 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.internal.model.source; + +import java.util.Map; +import javax.lang.model.element.ExecutableElement; + +import org.mapstruct.ap.internal.gem.EnumMappingGem; +import org.mapstruct.ap.internal.util.FormattingMessager; +import org.mapstruct.ap.internal.util.Strings; +import org.mapstruct.ap.spi.EnumTransformationStrategy; + +import static org.mapstruct.ap.internal.util.Message.ENUMMAPPING_INCORRECT_TRANSFORMATION_STRATEGY; + +/** + * @author Filip Hrisafov + */ +public class EnumMappingOptions extends DelegatingOptions { + + private final EnumMappingGem enumMapping; + private final boolean inverse; + private final boolean valid; + + private EnumMappingOptions(EnumMappingGem enumMapping, boolean inverse, boolean valid, DelegatingOptions next) { + super( next ); + this.enumMapping = enumMapping; + this.inverse = inverse; + this.valid = valid; + } + + @Override + public boolean hasAnnotation() { + return enumMapping != null; + } + + public boolean isValid() { + return valid; + } + + public String getNameTransformationStrategy() { + return enumMapping.nameTransformationStrategy().get(); + } + + public String getNameTransformationConfiguration() { + return enumMapping.configuration().get(); + } + + public boolean isInverse() { + return inverse; + } + + public EnumMappingOptions inverse() { + return new EnumMappingOptions( enumMapping, true, valid, next() ); + } + + public static EnumMappingOptions getInstanceOn(ExecutableElement method, MapperOptions mapperOptions, + Map enumTransformationStrategies, FormattingMessager messager) { + + EnumMappingGem enumMapping = EnumMappingGem.instanceOn( method ); + if ( enumMapping == null ) { + return new EnumMappingOptions( null, false, true, mapperOptions ); + } + else if ( !isConsistent( enumMapping, method, enumTransformationStrategies, messager ) ) { + return new EnumMappingOptions( null, false, false, mapperOptions ); + } + + return new EnumMappingOptions( + enumMapping, + false, + true, + mapperOptions + ); + } + + private static boolean isConsistent(EnumMappingGem gem, ExecutableElement method, + Map enumTransformationStrategies, FormattingMessager messager) { + + String strategy = gem.nameTransformationStrategy().getValue(); + + if ( !enumTransformationStrategies.containsKey( strategy ) ) { + String registeredStrategies = Strings.join( enumTransformationStrategies.keySet(), ", " ); + messager.printMessage( + method, + gem.mirror(), + gem.nameTransformationStrategy().getAnnotationValue(), + ENUMMAPPING_INCORRECT_TRANSFORMATION_STRATEGY, + strategy, + registeredStrategies + ); + + return false; + } + + return true; + } +} diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/source/MappingMethodOptions.java b/processor/src/main/java/org/mapstruct/ap/internal/model/source/MappingMethodOptions.java index 587dc5acf..6c5cc1d04 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/source/MappingMethodOptions.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/source/MappingMethodOptions.java @@ -31,6 +31,7 @@ public class MappingMethodOptions { null, null, null, + null, Collections.emptyList() ); @@ -39,18 +40,21 @@ public class MappingMethodOptions { private IterableMappingOptions iterableMapping; private MapMappingOptions mapMapping; private BeanMappingOptions beanMapping; + private EnumMappingOptions enumMappingOptions; private List valueMappings; private boolean fullyInitialized; public MappingMethodOptions(MapperOptions mapper, Set mappings, IterableMappingOptions iterableMapping, MapMappingOptions mapMapping, BeanMappingOptions beanMapping, + EnumMappingOptions enumMappingOptions, List valueMappings) { this.mapper = mapper; this.mappings = mappings; this.iterableMapping = iterableMapping; this.mapMapping = mapMapping; this.beanMapping = beanMapping; + this.enumMappingOptions = enumMappingOptions; this.valueMappings = valueMappings; } @@ -83,6 +87,10 @@ public class MappingMethodOptions { return beanMapping; } + public EnumMappingOptions getEnumMappingOptions() { + return enumMappingOptions; + } + public List getValueMappings() { return valueMappings; } @@ -99,6 +107,10 @@ public class MappingMethodOptions { this.beanMapping = beanMapping; } + public void setEnumMappingOptions(EnumMappingOptions enumMappingOptions) { + this.enumMappingOptions = enumMappingOptions; + } + public void setValueMappings(List valueMappings) { this.valueMappings = valueMappings; } @@ -141,6 +153,17 @@ public class MappingMethodOptions { setBeanMapping( BeanMappingOptions.forInheritance( templateOptions.getBeanMapping( ) ) ); } + if ( !getEnumMappingOptions().hasAnnotation() && templateOptions.getEnumMappingOptions().hasAnnotation() ) { + EnumMappingOptions newEnumMappingOptions; + if ( isInverse ) { + newEnumMappingOptions = templateOptions.getEnumMappingOptions().inverse(); + } + else { + newEnumMappingOptions = templateOptions.getEnumMappingOptions(); + } + setEnumMappingOptions( newEnumMappingOptions ); + } + if ( getValueMappings() == null ) { if ( templateOptions.getValueMappings() != null ) { // there were no mappings, so the inherited mappings are the new ones diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/source/SourceMethod.java b/processor/src/main/java/org/mapstruct/ap/internal/model/source/SourceMethod.java index 3c1a5e178..3551db8c5 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/source/SourceMethod.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/source/SourceMethod.java @@ -84,6 +84,7 @@ public class SourceMethod implements Method { private MapperOptions mapper = null; private List prototypeMethods = Collections.emptyList(); private List valueMappings; + private EnumMappingOptions enumMappingOptions; private ParameterProvidedMethods contextProvidedMethods; public Builder setDeclaringMapper(Type declaringMapper) { @@ -136,6 +137,11 @@ public class SourceMethod implements Method { return this; } + public Builder setEnumMappingOptions(EnumMappingOptions enumMappingOptions) { + this.enumMappingOptions = enumMappingOptions; + return this; + } + public Builder setTypeUtils(Types typeUtils) { this.typeUtils = typeUtils; return this; @@ -172,8 +178,15 @@ public class SourceMethod implements Method { mappings = Collections.emptySet(); } - MappingMethodOptions mappingMethodOptions = - new MappingMethodOptions( mapper, mappings, iterableMapping, mapMapping, beanMapping, valueMappings ); + MappingMethodOptions mappingMethodOptions = new MappingMethodOptions( + mapper, + mappings, + iterableMapping, + mapMapping, + beanMapping, + enumMappingOptions, + valueMappings + ); return new SourceMethod( this, mappingMethodOptions ); } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/processor/DefaultModelElementProcessorContext.java b/processor/src/main/java/org/mapstruct/ap/internal/processor/DefaultModelElementProcessorContext.java index a9f9eff4a..adb48d087 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/processor/DefaultModelElementProcessorContext.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/processor/DefaultModelElementProcessorContext.java @@ -26,6 +26,7 @@ import org.mapstruct.ap.internal.util.Message; import org.mapstruct.ap.internal.util.RoundContext; import org.mapstruct.ap.internal.util.workarounds.TypesDecorator; import org.mapstruct.ap.internal.version.VersionInformation; +import org.mapstruct.ap.spi.EnumTransformationStrategy; /** * Default implementation of the processor context. @@ -41,6 +42,7 @@ public class DefaultModelElementProcessorContext implements ProcessorContext { private final VersionInformation versionInformation; private final Types delegatingTypes; private final AccessorNamingUtils accessorNaming; + private final RoundContext roundContext; public DefaultModelElementProcessorContext(ProcessingEnvironment processingEnvironment, Options options, RoundContext roundContext, Map notToBeImported) { @@ -50,6 +52,7 @@ public class DefaultModelElementProcessorContext implements ProcessorContext { this.accessorNaming = roundContext.getAnnotationProcessorContext().getAccessorNaming(); this.versionInformation = DefaultVersionInformation.fromProcessingEnvironment( processingEnvironment ); this.delegatingTypes = new TypesDecorator( processingEnvironment, versionInformation ); + this.roundContext = roundContext; this.typeFactory = new TypeFactory( processingEnvironment.getElementUtils(), delegatingTypes, @@ -90,6 +93,11 @@ public class DefaultModelElementProcessorContext implements ProcessorContext { return accessorNaming; } + @Override + public Map getEnumTransformationStrategies() { + return roundContext.getAnnotationProcessorContext().getEnumTransformationStrategies(); + } + @Override public Options getOptions() { return options; diff --git a/processor/src/main/java/org/mapstruct/ap/internal/processor/MapperCreationProcessor.java b/processor/src/main/java/org/mapstruct/ap/internal/processor/MapperCreationProcessor.java index 7746dd677..6882fba64 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/processor/MapperCreationProcessor.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/processor/MapperCreationProcessor.java @@ -101,6 +101,7 @@ public class MapperCreationProcessor implements ModelElementProcessor enumTransformationStrategies; private Types typeUtils; private Elements elementUtils; @@ -78,6 +82,7 @@ public class MethodRetrievalProcessor implements ModelElementProcessor { AccessorNamingUtils getAccessorNaming(); + Map getEnumTransformationStrategies(); + Options getOptions(); VersionInformation getVersionInformation(); diff --git a/processor/src/main/java/org/mapstruct/ap/internal/util/AnnotationProcessorContext.java b/processor/src/main/java/org/mapstruct/ap/internal/util/AnnotationProcessorContext.java index 8fdc6201e..28db59abb 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/util/AnnotationProcessorContext.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/util/AnnotationProcessorContext.java @@ -9,7 +9,9 @@ import java.io.PrintWriter; import java.io.StringWriter; import java.util.ArrayList; import java.util.Iterator; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.ServiceLoader; import javax.annotation.processing.Messager; @@ -22,6 +24,7 @@ import org.mapstruct.ap.spi.AstModifyingAnnotationProcessor; import org.mapstruct.ap.spi.BuilderProvider; import org.mapstruct.ap.spi.DefaultAccessorNamingStrategy; import org.mapstruct.ap.spi.DefaultBuilderProvider; +import org.mapstruct.ap.spi.EnumTransformationStrategy; import org.mapstruct.ap.spi.FreeBuilderAccessorNamingStrategy; import org.mapstruct.ap.spi.ImmutablesAccessorNamingStrategy; import org.mapstruct.ap.spi.ImmutablesBuilderProvider; @@ -39,6 +42,7 @@ public class AnnotationProcessorContext implements MapStructProcessingEnvironmen private BuilderProvider builderProvider; private AccessorNamingStrategy accessorNamingStrategy; private boolean initialized; + private Map enumTransformationStrategies; private AccessorNamingUtils accessorNaming; private Elements elementUtils; @@ -105,6 +109,28 @@ public class AnnotationProcessorContext implements MapStructProcessingEnvironmen ); } this.accessorNaming = new AccessorNamingUtils( this.accessorNamingStrategy ); + + + this.enumTransformationStrategies = new LinkedHashMap<>(); + ServiceLoader transformationStrategiesLoader = ServiceLoader.load( + EnumTransformationStrategy.class, + AnnotationProcessorContext.class.getClassLoader() + ); + + for ( EnumTransformationStrategy transformationStrategy : transformationStrategiesLoader ) { + String transformationStrategyName = transformationStrategy.getStrategyName(); + if ( enumTransformationStrategies.containsKey( transformationStrategyName ) ) { + throw new IllegalStateException( + "Multiple EnumTransformationStrategies are using the same ma,e. Found: " + + enumTransformationStrategies.get( transformationStrategyName ) + " and " + + transformationStrategy + " for name " + transformationStrategyName ); + } + + transformationStrategy.init( this ); + enumTransformationStrategies.put( transformationStrategyName, transformationStrategy ); + } + + this.initialized = true; } @@ -216,4 +242,9 @@ public class AnnotationProcessorContext implements MapStructProcessingEnvironmen initialize(); return builderProvider; } + + public Map getEnumTransformationStrategies() { + initialize(); + return enumTransformationStrategies; + } } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/util/Message.java b/processor/src/main/java/org/mapstruct/ap/internal/util/Message.java index d2cb9857e..a26166a7e 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/util/Message.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/util/Message.java @@ -100,6 +100,7 @@ public enum Message { ENUMMAPPING_UNDEFINED_TARGET( "A target constant must be specified for mappings of an enum mapping method." ), ENUMMAPPING_UNMAPPED_SOURCES( "The following constants from the source enum have no corresponding constant in the target enum and must be be mapped via adding additional mappings: %s." ), ENUMMAPPING_REMOVED( "Mapping of Enums via @Mapping is removed. Please use @ValueMapping instead!" ), + ENUMMAPPING_INCORRECT_TRANSFORMATION_STRATEGY( "There is no registered EnumTransformationStrategy for '%s'. Registered strategies are: %s." ), LIFECYCLEMETHOD_AMBIGUOUS_PARAMETERS( "Lifecycle method has multiple matching parameters (e. g. same type), in this case please ensure to name the parameters in the lifecycle and mapping method identical. This lifecycle method will not be used for the mapping method '%s'.", Diagnostic.Kind.WARNING), diff --git a/processor/src/main/java/org/mapstruct/ap/spi/EnumTransformationStrategy.java b/processor/src/main/java/org/mapstruct/ap/spi/EnumTransformationStrategy.java new file mode 100644 index 000000000..2c498fc3a --- /dev/null +++ b/processor/src/main/java/org/mapstruct/ap/spi/EnumTransformationStrategy.java @@ -0,0 +1,44 @@ +/* + * 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.spi; + +import org.mapstruct.util.Experimental; + +/** + * A service provider interface for transforming name based value mappings. + * + * @author Filip Hrisafov + * @since 1.4 + */ +@Experimental("This SPI can have it's signature changed in subsequent releases") +public interface EnumTransformationStrategy { + + /** + * Initializes the enum transformation strategy with the MapStruct processing environment. + * + * @param processingEnvironment environment for facilities + */ + default void init(MapStructProcessingEnvironment processingEnvironment) { + + } + + /** + * The name of the strategy. + * + * @return the name of the strategy, never {@code null} + */ + String getStrategyName(); + + /** + * Transform the given value by using the given {@code configuration}. + * + * @param value the value that should be transformed + * @param configuration the configuration that should be used for the transformation + * + * @return the transformed value after applying the configuration + */ + String transform(String value, String configuration); +} diff --git a/processor/src/main/java/org/mapstruct/ap/spi/PrefixEnumTransformationStrategy.java b/processor/src/main/java/org/mapstruct/ap/spi/PrefixEnumTransformationStrategy.java new file mode 100644 index 000000000..eaa7b7551 --- /dev/null +++ b/processor/src/main/java/org/mapstruct/ap/spi/PrefixEnumTransformationStrategy.java @@ -0,0 +1,22 @@ +/* + * 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.spi; + +/** + * @author Filip Hrisafov + */ +public class PrefixEnumTransformationStrategy implements EnumTransformationStrategy { + + @Override + public String getStrategyName() { + return "prefix"; + } + + @Override + public String transform(String value, String configuration) { + return configuration + value; + } +} diff --git a/processor/src/main/java/org/mapstruct/ap/spi/StripPrefixEnumTransformationStrategy.java b/processor/src/main/java/org/mapstruct/ap/spi/StripPrefixEnumTransformationStrategy.java new file mode 100644 index 000000000..d7f03c9b7 --- /dev/null +++ b/processor/src/main/java/org/mapstruct/ap/spi/StripPrefixEnumTransformationStrategy.java @@ -0,0 +1,25 @@ +/* + * 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.spi; + +/** + * @author Filip Hrisafov + */ +public class StripPrefixEnumTransformationStrategy implements EnumTransformationStrategy { + + @Override + public String getStrategyName() { + return "stripPrefix"; + } + + @Override + public String transform(String value, String configuration) { + if ( value.startsWith( configuration ) ) { + return value.substring( configuration.length() ); + } + return value; + } +} diff --git a/processor/src/main/java/org/mapstruct/ap/spi/StripSuffixEnumTransformationStrategy.java b/processor/src/main/java/org/mapstruct/ap/spi/StripSuffixEnumTransformationStrategy.java new file mode 100644 index 000000000..223837924 --- /dev/null +++ b/processor/src/main/java/org/mapstruct/ap/spi/StripSuffixEnumTransformationStrategy.java @@ -0,0 +1,25 @@ +/* + * 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.spi; + +/** + * @author Filip Hrisafov + */ +public class StripSuffixEnumTransformationStrategy implements EnumTransformationStrategy { + + @Override + public String getStrategyName() { + return "stripSuffix"; + } + + @Override + public String transform(String value, String configuration) { + if ( value.endsWith( configuration ) ) { + return value.substring( 0, value.length() - configuration.length() ); + } + return value; + } +} diff --git a/processor/src/main/java/org/mapstruct/ap/spi/SuffixEnumTransformationStrategy.java b/processor/src/main/java/org/mapstruct/ap/spi/SuffixEnumTransformationStrategy.java new file mode 100644 index 000000000..29a109fad --- /dev/null +++ b/processor/src/main/java/org/mapstruct/ap/spi/SuffixEnumTransformationStrategy.java @@ -0,0 +1,22 @@ +/* + * 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.spi; + +/** + * @author Filip Hrisafov + */ +public class SuffixEnumTransformationStrategy implements EnumTransformationStrategy { + + @Override + public String getStrategyName() { + return "suffix"; + } + + @Override + public String transform(String value, String configuration) { + return value + configuration; + } +} diff --git a/processor/src/main/resources/META-INF/services/org.mapstruct.ap.spi.EnumTransformationStrategy b/processor/src/main/resources/META-INF/services/org.mapstruct.ap.spi.EnumTransformationStrategy new file mode 100644 index 000000000..1f52733ad --- /dev/null +++ b/processor/src/main/resources/META-INF/services/org.mapstruct.ap.spi.EnumTransformationStrategy @@ -0,0 +1,8 @@ +# Copyright MapStruct Authors. +# +# Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + +org.mapstruct.ap.spi.PrefixEnumTransformationStrategy +org.mapstruct.ap.spi.StripPrefixEnumTransformationStrategy +org.mapstruct.ap.spi.StripSuffixEnumTransformationStrategy +org.mapstruct.ap.spi.SuffixEnumTransformationStrategy diff --git a/processor/src/test/java/org/mapstruct/ap/test/gem/ConstantTest.java b/processor/src/test/java/org/mapstruct/ap/test/gem/ConstantTest.java index b33f00964..a2a6afefd 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/gem/ConstantTest.java +++ b/processor/src/test/java/org/mapstruct/ap/test/gem/ConstantTest.java @@ -23,5 +23,11 @@ public class ConstantTest { assertThat( MappingConstants.ANY_REMAINING ).isEqualTo( MappingConstantsGem.ANY_REMAINING ); assertThat( MappingConstants.ANY_UNMAPPED ).isEqualTo( MappingConstantsGem.ANY_UNMAPPED ); assertThat( MappingConstants.NULL ).isEqualTo( MappingConstantsGem.NULL ); + assertThat( MappingConstants.SUFFIX_TRANSFORMATION ).isEqualTo( MappingConstantsGem.SUFFIX_TRANSFORMATION ); + assertThat( MappingConstants.STRIP_SUFFIX_TRANSFORMATION ) + .isEqualTo( MappingConstantsGem.STRIP_SUFFIX_TRANSFORMATION ); + assertThat( MappingConstants.PREFIX_TRANSFORMATION ).isEqualTo( MappingConstantsGem.PREFIX_TRANSFORMATION ); + assertThat( MappingConstants.STRIP_PREFIX_TRANSFORMATION ) + .isEqualTo( MappingConstantsGem.STRIP_PREFIX_TRANSFORMATION ); } } diff --git a/processor/src/test/java/org/mapstruct/ap/test/value/enum2string/OrderMapper.java b/processor/src/test/java/org/mapstruct/ap/test/value/enum2string/OrderMapper.java index 35d9f57f9..d7e6f1062 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/value/enum2string/OrderMapper.java +++ b/processor/src/test/java/org/mapstruct/ap/test/value/enum2string/OrderMapper.java @@ -5,6 +5,7 @@ */ package org.mapstruct.ap.test.value.enum2string; +import org.mapstruct.EnumMapping; import org.mapstruct.Mapper; import org.mapstruct.MappingConstants; import org.mapstruct.ValueMapping; @@ -27,6 +28,7 @@ public interface OrderMapper { }) String mapNormal(OrderType orderType); + @EnumMapping(nameTransformationStrategy = "prefix", configuration = "PREFIX_") @ValueMappings({ @ValueMapping( source = MappingConstants.NULL, target = "DEFAULT" ), @ValueMapping( source = "STANDARD", target = MappingConstants.NULL ), diff --git a/processor/src/test/java/org/mapstruct/ap/test/value/nametransformation/CheeseEnumToStringPrefixMapper.java b/processor/src/test/java/org/mapstruct/ap/test/value/nametransformation/CheeseEnumToStringPrefixMapper.java new file mode 100644 index 000000000..aa6d22071 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/value/nametransformation/CheeseEnumToStringPrefixMapper.java @@ -0,0 +1,34 @@ +/* + * 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.value.nametransformation; + +import org.mapstruct.EnumMapping; +import org.mapstruct.InheritInverseConfiguration; +import org.mapstruct.Mapper; +import org.mapstruct.MappingConstants; +import org.mapstruct.ValueMapping; +import org.mapstruct.factory.Mappers; + +/** + * @author Filip Hrisafov + */ +@Mapper +public interface CheeseEnumToStringPrefixMapper { + + CheeseEnumToStringPrefixMapper INSTANCE = Mappers.getMapper( CheeseEnumToStringPrefixMapper.class ); + + @EnumMapping(nameTransformationStrategy = MappingConstants.PREFIX_TRANSFORMATION, configuration = "SWISS_") + String map(CheeseType cheese); + + @InheritInverseConfiguration + @EnumMapping(nameTransformationStrategy = MappingConstants.PREFIX_TRANSFORMATION, configuration = "FRENCH_") + @ValueMapping(source = MappingConstants.ANY_REMAINING, target = MappingConstants.NULL) + CheeseType map(String cheese); + + @EnumMapping(nameTransformationStrategy = MappingConstants.STRIP_PREFIX_TRANSFORMATION, configuration = "SWISS_") + @ValueMapping(source = MappingConstants.ANY_REMAINING, target = MappingConstants.NULL) + CheeseTypePrefixed mapStripPrefix(String cheese); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/value/nametransformation/CheeseEnumToStringSuffixMapper.java b/processor/src/test/java/org/mapstruct/ap/test/value/nametransformation/CheeseEnumToStringSuffixMapper.java new file mode 100644 index 000000000..a6be3ae2d --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/value/nametransformation/CheeseEnumToStringSuffixMapper.java @@ -0,0 +1,36 @@ +/* + * 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.value.nametransformation; + +import org.mapstruct.EnumMapping; +import org.mapstruct.InheritInverseConfiguration; +import org.mapstruct.Mapper; +import org.mapstruct.MappingConstants; +import org.mapstruct.ValueMapping; +import org.mapstruct.factory.Mappers; + +/** + * @author Filip Hrisafov + */ +@Mapper +public interface CheeseEnumToStringSuffixMapper { + + CheeseEnumToStringSuffixMapper INSTANCE = Mappers.getMapper( CheeseEnumToStringSuffixMapper.class ); + + @EnumMapping(nameTransformationStrategy = MappingConstants.SUFFIX_TRANSFORMATION, configuration = "_CHEESE_TYPE") + String map(CheeseType cheese); + + @InheritInverseConfiguration + @ValueMapping(source = MappingConstants.ANY_REMAINING, target = MappingConstants.NULL) + CheeseType map(String cheese); + + @EnumMapping( + nameTransformationStrategy = MappingConstants.STRIP_SUFFIX_TRANSFORMATION, + configuration = "_CHEESE_TYPE" + ) + @ValueMapping(source = MappingConstants.ANY_REMAINING, target = MappingConstants.NULL) + CheeseTypeSuffixed mapStripSuffix(String cheese); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/value/nametransformation/CheesePrefixMapper.java b/processor/src/test/java/org/mapstruct/ap/test/value/nametransformation/CheesePrefixMapper.java new file mode 100644 index 000000000..7cb441bb0 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/value/nametransformation/CheesePrefixMapper.java @@ -0,0 +1,33 @@ +/* + * 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.value.nametransformation; + +import org.mapstruct.EnumMapping; +import org.mapstruct.InheritInverseConfiguration; +import org.mapstruct.Mapper; +import org.mapstruct.MappingConstants; +import org.mapstruct.ValueMapping; +import org.mapstruct.factory.Mappers; + +/** + * @author Filip Hrisafov + */ +@Mapper +public interface CheesePrefixMapper { + + CheesePrefixMapper INSTANCE = Mappers.getMapper( CheesePrefixMapper.class ); + + @EnumMapping(nameTransformationStrategy = MappingConstants.PREFIX_TRANSFORMATION, configuration = "SWISS_") + CheeseTypePrefixed map(CheeseType cheese); + + @InheritInverseConfiguration + @ValueMapping(source = "DEFAULT", target = MappingConstants.NULL) + CheeseType mapInheritInverse(CheeseTypePrefixed cheese); + + @ValueMapping(source = "DEFAULT", target = MappingConstants.NULL) + @EnumMapping(nameTransformationStrategy = MappingConstants.STRIP_PREFIX_TRANSFORMATION, configuration = "SWISS_") + CheeseType mapStripPrefix(CheeseTypePrefixed cheese); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/value/nametransformation/CheeseSuffixMapper.java b/processor/src/test/java/org/mapstruct/ap/test/value/nametransformation/CheeseSuffixMapper.java new file mode 100644 index 000000000..b50ed0acb --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/value/nametransformation/CheeseSuffixMapper.java @@ -0,0 +1,36 @@ +/* + * 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.value.nametransformation; + +import org.mapstruct.EnumMapping; +import org.mapstruct.InheritInverseConfiguration; +import org.mapstruct.Mapper; +import org.mapstruct.MappingConstants; +import org.mapstruct.ValueMapping; +import org.mapstruct.factory.Mappers; + +/** + * @author Filip Hrisafov + */ +@Mapper +public interface CheeseSuffixMapper { + + CheeseSuffixMapper INSTANCE = Mappers.getMapper( CheeseSuffixMapper.class ); + + @EnumMapping(nameTransformationStrategy = MappingConstants.SUFFIX_TRANSFORMATION, configuration = "_CHEESE_TYPE") + CheeseTypeSuffixed map(CheeseType cheese); + + @InheritInverseConfiguration + @ValueMapping(source = "DEFAULT", target = MappingConstants.NULL) + CheeseType mapInheritInverse(CheeseTypeSuffixed cheese); + + @EnumMapping( + nameTransformationStrategy = MappingConstants.STRIP_SUFFIX_TRANSFORMATION, + configuration = "_CHEESE_TYPE" + ) + @ValueMapping(source = "DEFAULT", target = MappingConstants.NULL) + CheeseType mapStripSuffix(CheeseTypeSuffixed cheese); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/value/nametransformation/CheeseType.java b/processor/src/test/java/org/mapstruct/ap/test/value/nametransformation/CheeseType.java new file mode 100644 index 000000000..d7a1d2e28 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/value/nametransformation/CheeseType.java @@ -0,0 +1,15 @@ +/* + * 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.value.nametransformation; + +/** + * @author Filip Hrisafov + */ +public enum CheeseType { + + BRIE, + ROQUEFORT +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/value/nametransformation/CheeseTypeCustomSuffix.java b/processor/src/test/java/org/mapstruct/ap/test/value/nametransformation/CheeseTypeCustomSuffix.java new file mode 100644 index 000000000..b7eccd3d4 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/value/nametransformation/CheeseTypeCustomSuffix.java @@ -0,0 +1,15 @@ +/* + * 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.value.nametransformation; + +/** + * @author Filip Hrisafov + */ +public enum CheeseTypeCustomSuffix { + + brie_TYPE, + roquefort_TYPE, +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/value/nametransformation/CheeseTypePrefixed.java b/processor/src/test/java/org/mapstruct/ap/test/value/nametransformation/CheeseTypePrefixed.java new file mode 100644 index 000000000..5f5b989c7 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/value/nametransformation/CheeseTypePrefixed.java @@ -0,0 +1,16 @@ +/* + * 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.value.nametransformation; + +/** + * @author Filip Hrisafov + */ +public enum CheeseTypePrefixed { + + DEFAULT, + SWISS_BRIE, + SWISS_ROQUEFORT, +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/value/nametransformation/CheeseTypeSuffixed.java b/processor/src/test/java/org/mapstruct/ap/test/value/nametransformation/CheeseTypeSuffixed.java new file mode 100644 index 000000000..b2b264b78 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/value/nametransformation/CheeseTypeSuffixed.java @@ -0,0 +1,16 @@ +/* + * 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.value.nametransformation; + +/** + * @author Filip Hrisafov + */ +public enum CheeseTypeSuffixed { + + DEFAULT, + BRIE_CHEESE_TYPE, + ROQUEFORT_CHEESE_TYPE, +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/value/nametransformation/CustomEnumTransformationStrategy.java b/processor/src/test/java/org/mapstruct/ap/test/value/nametransformation/CustomEnumTransformationStrategy.java new file mode 100644 index 000000000..f7cfe4437 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/value/nametransformation/CustomEnumTransformationStrategy.java @@ -0,0 +1,30 @@ +/* + * 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.value.nametransformation; + +// tag::documentation[] + +import org.mapstruct.ap.spi.EnumTransformationStrategy; + +// end::documentation[] + +/** + * @author Filip Hrisafov + */ +// tag::documentation[] +public class CustomEnumTransformationStrategy implements EnumTransformationStrategy { + + @Override + public String getStrategyName() { + return "custom"; + } + + @Override + public String transform(String value, String configuration) { + return value.toLowerCase() + configuration; + } +} +// end::documentation[] diff --git a/processor/src/test/java/org/mapstruct/ap/test/value/nametransformation/EnumNameTransformationStrategyTest.java b/processor/src/test/java/org/mapstruct/ap/test/value/nametransformation/EnumNameTransformationStrategyTest.java new file mode 100644 index 000000000..d5de5e872 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/value/nametransformation/EnumNameTransformationStrategyTest.java @@ -0,0 +1,115 @@ +/* + * 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.value.nametransformation; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.WithServiceImplementation; +import org.mapstruct.ap.testutil.compilation.annotation.CompilationResult; +import org.mapstruct.ap.testutil.compilation.annotation.Diagnostic; +import org.mapstruct.ap.testutil.compilation.annotation.ExpectedCompilationOutcome; +import org.mapstruct.ap.testutil.runner.AnnotationProcessorTestRunner; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Filip Hrisafov + */ +@RunWith(AnnotationProcessorTestRunner.class) +@WithClasses({ + CheeseType.class, + CheeseTypeSuffixed.class, + CheeseTypePrefixed.class, + CheeseTypeCustomSuffix.class, +}) +public class EnumNameTransformationStrategyTest { + + @Test + @WithClasses({ + CheeseSuffixMapper.class + }) + public void shouldApplySuffixAndStripSuffixOnEnumToEnumMapping() { + CheeseSuffixMapper mapper = CheeseSuffixMapper.INSTANCE; + + assertThat( mapper.map( CheeseType.BRIE ) ) + .isEqualTo( CheeseTypeSuffixed.BRIE_CHEESE_TYPE ); + assertThat( mapper.mapInheritInverse( CheeseTypeSuffixed.BRIE_CHEESE_TYPE ) ) + .isEqualTo( CheeseType.BRIE ); + assertThat( mapper.mapStripSuffix( CheeseTypeSuffixed.BRIE_CHEESE_TYPE ) ) + .isEqualTo( CheeseType.BRIE ); + } + + @Test + @WithClasses({ + CheesePrefixMapper.class + }) + public void shouldApplyPrefixAndStripPrefixOnEnumToEnumMapping() { + CheesePrefixMapper mapper = CheesePrefixMapper.INSTANCE; + + assertThat( mapper.map( CheeseType.BRIE ) ) + .isEqualTo( CheeseTypePrefixed.SWISS_BRIE ); + assertThat( mapper.mapInheritInverse( CheeseTypePrefixed.SWISS_BRIE ) ) + .isEqualTo( CheeseType.BRIE ); + assertThat( mapper.mapStripPrefix( CheeseTypePrefixed.SWISS_BRIE ) ) + .isEqualTo( CheeseType.BRIE ); + } + + @Test + @WithClasses({ + CheeseEnumToStringSuffixMapper.class + }) + public void shouldApplySuffixAndStripSuffixOnEnumToStringMapping() { + CheeseEnumToStringSuffixMapper mapper = CheeseEnumToStringSuffixMapper.INSTANCE; + + assertThat( mapper.map( CheeseType.BRIE ) ).isEqualTo( "BRIE_CHEESE_TYPE" ); + assertThat( mapper.map( "BRIE_CHEESE_TYPE" ) ).isEqualTo( CheeseType.BRIE ); + assertThat( mapper.mapStripSuffix( "BRIE" ) ).isEqualTo( CheeseTypeSuffixed.BRIE_CHEESE_TYPE ); + assertThat( mapper.mapStripSuffix( "DEFAULT" ) ).isEqualTo( CheeseTypeSuffixed.DEFAULT ); + } + + @Test + @WithClasses({ + CheeseEnumToStringPrefixMapper.class + }) + public void shouldApplyPrefixAndStripPrefixOnEnumToStringMapping() { + CheeseEnumToStringPrefixMapper mapper = CheeseEnumToStringPrefixMapper.INSTANCE; + + assertThat( mapper.map( CheeseType.BRIE ) ).isEqualTo( "SWISS_BRIE" ); + assertThat( mapper.map( "FRENCH_BRIE" ) ).isEqualTo( CheeseType.BRIE ); + assertThat( mapper.mapStripPrefix( "BRIE" ) ).isEqualTo( CheeseTypePrefixed.SWISS_BRIE ); + assertThat( mapper.mapStripPrefix( "DEFAULT" ) ).isEqualTo( CheeseTypePrefixed.DEFAULT ); + } + + @Test + @WithClasses({ + ErroneousNameTransformStrategyMapper.class + }) + @ExpectedCompilationOutcome(value = CompilationResult.FAILED, + diagnostics = { + @Diagnostic( + type = ErroneousNameTransformStrategyMapper.class, + kind = javax.tools.Diagnostic.Kind.ERROR, + line = 20, + message = "There is no registered EnumTransformationStrategy for 'custom'. Registered strategies are:" + + " prefix, stripPrefix, stripSuffix, suffix." + ) + } + ) + public void shouldGiveCompileErrorWhenUsingUnknownNameTransformStrategy() { + } + + @Test + @WithClasses({ + ErroneousNameTransformStrategyMapper.class + }) + @WithServiceImplementation(CustomEnumTransformationStrategy.class) + public void shouldUseCustomEnumTransformationStrategy() { + assertThat( ErroneousNameTransformStrategyMapper.INSTANCE.map( CheeseType.BRIE ) ) + .isEqualTo( CheeseTypeCustomSuffix.brie_TYPE ); + } + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/value/nametransformation/ErroneousNameTransformStrategyMapper.java b/processor/src/test/java/org/mapstruct/ap/test/value/nametransformation/ErroneousNameTransformStrategyMapper.java new file mode 100644 index 000000000..cb59089b3 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/value/nametransformation/ErroneousNameTransformStrategyMapper.java @@ -0,0 +1,22 @@ +/* + * 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.value.nametransformation; + +import org.mapstruct.EnumMapping; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +/** + * @author Filip Hrisafov + */ +@Mapper +public interface ErroneousNameTransformStrategyMapper { + + ErroneousNameTransformStrategyMapper INSTANCE = Mappers.getMapper( ErroneousNameTransformStrategyMapper.class ); + + @EnumMapping(nameTransformationStrategy = "custom", configuration = "_TYPE") + CheeseTypeCustomSuffix map(CheeseType cheese); +}