From c23592a7fe30bf4b4a779caadfeb9edd4578c82c Mon Sep 17 00:00:00 2001 From: Filip Hrisafov Date: Mon, 25 May 2020 21:31:29 +0200 Subject: [PATCH] Add EnumNamingStrategy SPI (#2100) Add a new EnumNamingStrategy SPI which can be used for customising the way enums are matched by name. It is similar to the AccessorNamingStrategy such that it allows implementors to provide a custom way of defining a property. Related to #796, #1220, #1789 and #1667 --- .../chapter-13-using-mapstruct-spi.asciidoc | 146 ++++++++++++++++++ .../internal/model/MappingBuilderContext.java | 8 + .../ap/internal/model/ValueMappingMethod.java | 77 ++++++--- .../DefaultModelElementProcessorContext.java | 6 + .../processor/MapperCreationProcessor.java | 1 + .../processor/ModelElementProcessor.java | 3 + .../util/AnnotationProcessorContext.java | 17 ++ .../mapstruct/ap/internal/util/Message.java | 1 + .../ap/spi/DefaultEnumNamingStrategy.java | 35 +++++ .../mapstruct/ap/spi/EnumNamingStrategy.java | 48 ++++++ .../ap/test/value/spi/CheeseType.java | 15 ++ .../ap/test/value/spi/CustomCheeseMapper.java | 34 ++++ .../ap/test/value/spi/CustomCheeseType.java | 17 ++ .../ap/test/value/spi/CustomEnumMarker.java | 12 ++ .../value/spi/CustomEnumNamingStrategy.java | 54 +++++++ .../spi/CustomEnumNamingStrategyTest.java | 123 +++++++++++++++ .../CustomErroneousEnumNamingStrategy.java | 54 +++++++ ...CustomErroneousEnumNamingStrategyTest.java | 102 ++++++++++++ .../spi/OverridesCustomCheeseMapper.java | 45 ++++++ .../value/spi/CustomCheeseMapperImpl.java | 138 +++++++++++++++++ 20 files changed, 918 insertions(+), 18 deletions(-) create mode 100644 processor/src/main/java/org/mapstruct/ap/spi/DefaultEnumNamingStrategy.java create mode 100644 processor/src/main/java/org/mapstruct/ap/spi/EnumNamingStrategy.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/value/spi/CheeseType.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/value/spi/CustomCheeseMapper.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/value/spi/CustomCheeseType.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/value/spi/CustomEnumMarker.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/value/spi/CustomEnumNamingStrategy.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/value/spi/CustomEnumNamingStrategyTest.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/value/spi/CustomErroneousEnumNamingStrategy.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/value/spi/CustomErroneousEnumNamingStrategyTest.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/value/spi/OverridesCustomCheeseMapper.java create mode 100644 processor/src/test/resources/fixtures/org/mapstruct/ap/test/value/spi/CustomCheeseMapperImpl.java 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 ddf8327f7..bc9cf7646 100644 --- a/documentation/src/main/asciidoc/chapter-13-using-mapstruct-spi.asciidoc +++ b/documentation/src/main/asciidoc/chapter-13-using-mapstruct-spi.asciidoc @@ -199,6 +199,152 @@ include::{processor-ap-main}/spi/NoOpBuilderProvider.java[tag=documentation] ---- ==== +[[custom-enum-naming-strategy]] +=== Custom Enum Naming Strategy + +MapStruct offers the possibility to override the `EnumNamingStrategy` via the Service Provider Interface (SPI). +This can be used when you have certain enums that follow some conventions within your organization. +For example all enums which implement an interface named `CustomEnumMarker` are prefixed with `CUSTOM_` +and the default value for them when mapping from `null` is `UNSPECIFIED` + +.Normal Enum +==== +[source, java, linenums] +[subs="verbatim,attributes"] +---- +public enum CheeseType { + BRIE, + ROQUEFORT; +} +---- +==== + +.Custom marker enum +==== +[source, java, linenums] +[subs="verbatim,attributes"] +---- +public enum CustomCheeseType implements CustomEnumMarker { + + UNSPECIFIED, + CUSTOM_BRIE, + CUSTOM_ROQUEFORT; +} +---- +==== + +We want `CheeseType` and `CustomCheeseType` to be mapped without the need to manually define the value mappings: + +.Custom enum mapping +==== +[source, java, linenums] +[subs="verbatim,attributes"] +---- +@Mapper +public interface CheeseTypeMapper { + + CheeseType map(CustomCheeseType cheese); + + CustomCheeseType map(CheeseType cheese); +} +---- +==== + +This can be achieved with implementing the SPI `org.mapstruct.ap.spi.EnumNamingStrategy` as in the following example. +Here’s an implemented `org.mapstruct.ap.spi.EnumNamingStrategy`: + +.Custom enum naming strategy +==== +[source, java, linenums] +[subs="verbatim,attributes"] +---- +public class CustomEnumNamingStrategy extends DefaultEnumNamingStrategy { + + @Override + public String getDefaultNullEnumConstant(TypeElement enumType) { + if ( isCustomEnum( enumType ) ) { + return "UNSPECIFIED"; + } + + return super.getDefaultNullEnumConstant( enumType ); + } + + @Override + public String getEnumConstant(TypeElement enumType, String enumConstant) { + if ( isCustomEnum( enumType ) ) { + return getCustomEnumConstant( enumConstant ); + } + return super.getEnumConstant( enumType, enumConstant ); + } + protected String getCustomEnumConstant(String enumConstant) { + if ( "UNSPECIFIED".equals( enumConstant ) ) { + return MappingConstantsGem.NULL; + } + return enumConstant.replace( "CUSTOM_", "" ); + } + protected boolean isCustomEnum(TypeElement enumType) { + for ( TypeMirror enumTypeInterface : enumType.getInterfaces() ) { + if ( typeUtils.asElement( enumTypeInterface ).getSimpleName().contentEquals( "CustomEnumMarker" ) ) { + return true; + } + } + return false; + } +} +---- +==== + +The generated code then for the `CheeseMapper` looks like: + +.Generated CheeseTypeMapper +==== +[source, java, linenums] +[subs="verbatim,attributes"] +---- +public class CheeseTypeMapperImpl implements CheeseTypeMapper { + + @Override + public CheeseType map(CustomCheeseType cheese) { + if ( cheese == null ) { + return null; + } + + CheeseType cheeseType; + + switch ( cheese ) { + case UNRECOGNIZED: cheeseType = null; + break; + case CUSTOM_BRIE: cheeseType = CheeseType.BRIE; + break; + case CUSTOM_ROQUEFORT: cheeseType = CheeseType.ROQUEFORT; + break; + default: throw new IllegalArgumentException( "Unexpected enum constant: " + cheese ); + } + + return cheeseType; + } + + @Override + public CustomCheeseType map(CheeseType cheese) { + if ( cheese == null ) { + return CustomCheeseType.UNSPECIFIED; + } + + CustomCheeseType customCheeseType; + + switch ( cheese ) { + case BRIE: customCheeseType = CustomCheeseType.CUSTOM_BRIE; + break; + case ROQUEFORT: customCheeseType = CustomCheeseType.CUSTOM_ROQUEFORT; + break; + default: throw new IllegalArgumentException( "Unexpected enum constant: " + cheese ); + } + + return customCheeseType; + } +} +---- +==== [[custom-enum-transformation-strategy]] === Custom Enum Transformation Strategy 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 e8d649783..53317aefa 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.EnumNamingStrategy; import org.mapstruct.ap.spi.EnumTransformationStrategy; import org.mapstruct.ap.spi.MappingExclusionProvider; @@ -105,6 +106,7 @@ public class MappingBuilderContext { private final Types typeUtils; private final FormattingMessager messager; private final AccessorNamingUtils accessorNaming; + private final EnumNamingStrategy enumNamingStrategy; private final Map enumTransformationStrategies; private final Options options; private final TypeElement mapperTypeElement; @@ -121,6 +123,7 @@ public class MappingBuilderContext { Types typeUtils, FormattingMessager messager, AccessorNamingUtils accessorNaming, + EnumNamingStrategy enumNamingStrategy, Map enumTransformationStrategies, Options options, MappingResolver mappingResolver, @@ -132,6 +135,7 @@ public class MappingBuilderContext { this.typeUtils = typeUtils; this.messager = messager; this.accessorNaming = accessorNaming; + this.enumNamingStrategy = enumNamingStrategy; this.enumTransformationStrategies = enumTransformationStrategies; this.options = options; this.mappingResolver = mappingResolver; @@ -186,6 +190,10 @@ public class MappingBuilderContext { return accessorNaming; } + public EnumNamingStrategy getEnumNamingStrategy() { + return enumNamingStrategy; + } + public Map getEnumTransformationStrategies() { return enumTransformationStrategies; } 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 218c754e3..adfa5779e 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 @@ -8,9 +8,11 @@ package org.mapstruct.ap.internal.model; import java.util.ArrayList; import java.util.HashSet; import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; +import javax.lang.model.element.TypeElement; import javax.lang.model.type.TypeMirror; import javax.lang.model.util.Types; @@ -86,6 +88,12 @@ public class ValueMappingMethod extends MappingMethod { Type sourceType = first( method.getSourceParameters() ).getType(); Type targetType = method.getResultType(); + if ( targetType.isEnumType() && valueMappings.nullTarget == null ) { + // If null target is not set it means that the user has not explicitly defined a mapping for null + valueMappings.nullValueTarget = ctx.getEnumNamingStrategy() + .getDefaultNullEnumConstant( targetType.getTypeElement() ); + } + // enum-to-enum if ( sourceType.isEnumType() && targetType.isEnumType() ) { mappingEntries.addAll( enumToEnumMapping( method, sourceType, targetType ) ); @@ -145,9 +153,7 @@ public class ValueMappingMethod extends MappingMethod { // Start to fill the mappings with the defined value mappings for ( ValueMappingOptions valueMapping : valueMappings.regularValueMappings ) { - String target = - NULL.equals( valueMapping.getTarget() ) ? null : valueMapping.getTarget(); - mappings.add( new MappingEntry( valueMapping.getSource(), target ) ); + mappings.add( new MappingEntry( valueMapping.getSource(), valueMapping.getTarget() ) ); unmappedSourceConstants.remove( valueMapping.getSource() ); } @@ -160,32 +166,40 @@ public class ValueMappingMethod extends MappingMethod { Map targetConstants = new LinkedHashMap<>(); boolean enumMappingInverse = enumMapping.isInverse(); + TypeElement targetTypeElement = method.getReturnType().getTypeElement(); for ( String targetEnumConstant : method.getReturnType().getEnumConstants() ) { + String targetNameEnum = getEnumConstant( targetTypeElement, targetEnumConstant ); if ( enumMappingInverse ) { // If the mapping is inverse we have to change the target enum constant targetConstants.put( - enumTransformationInvoker.transform( targetEnumConstant ), + enumTransformationInvoker.transform( targetNameEnum ), targetEnumConstant ); } else { - targetConstants.put( targetEnumConstant, targetEnumConstant ); + targetConstants.put( targetNameEnum, targetEnumConstant ); } } + TypeElement sourceTypeElement = sourceType.getTypeElement(); for ( String sourceConstant : new ArrayList<>( unmappedSourceConstants ) ) { + String sourceNameConstant = getEnumConstant( sourceTypeElement, sourceConstant ); String targetConstant; if ( !enumMappingInverse ) { - targetConstant = enumTransformationInvoker.transform( sourceConstant ); + targetConstant = enumTransformationInvoker.transform( sourceNameConstant ); } else { - targetConstant = sourceConstant; + targetConstant = sourceNameConstant; } if ( targetConstants.containsKey( targetConstant ) ) { mappings.add( new MappingEntry( sourceConstant, targetConstants.get( targetConstant ) ) ); unmappedSourceConstants.remove( sourceConstant ); } + else if ( NULL.equals( targetConstant ) ) { + mappings.add( new MappingEntry( sourceConstant, null ) ); + unmappedSourceConstants.remove( sourceConstant ); + } } if ( valueMappings.defaultTarget == null && !unmappedSourceConstants.isEmpty() ) { @@ -223,18 +237,18 @@ public class ValueMappingMethod extends MappingMethod { // Start to fill the mappings with the defined valuemappings for ( ValueMappingOptions valueMapping : valueMappings.regularValueMappings ) { - String target = - NULL.equals( valueMapping.getTarget() ) ? null : valueMapping.getTarget(); - mappings.add( new MappingEntry( valueMapping.getSource(), target ) ); + mappings.add( new MappingEntry( valueMapping.getSource(), valueMapping.getTarget() ) ); unmappedSourceConstants.remove( valueMapping.getSource() ); } // add mappings based on name if ( !valueMappings.hasMapAnyUnmapped ) { + TypeElement sourceTypeElement = sourceType.getTypeElement(); // all remaining constants are mapped for ( String sourceConstant : unmappedSourceConstants ) { - String targetConstant = enumTransformationInvoker.transform( sourceConstant ); + String sourceNameConstant = getEnumConstant( sourceTypeElement, sourceConstant ); + String targetConstant = enumTransformationInvoker.transform( sourceNameConstant ); mappings.add( new MappingEntry( sourceConstant, targetConstant ) ); } } @@ -250,27 +264,35 @@ public class ValueMappingMethod extends MappingMethod { if ( sourceErrorOccurred || mandatoryMissing ) { return mappings; } + Set mappedSources = new LinkedHashSet<>(); // Start to fill the mappings with the defined valuemappings for ( ValueMappingOptions valueMapping : valueMappings.regularValueMappings ) { - String target = - NULL.equals( valueMapping.getTarget() ) ? null : valueMapping.getTarget(); - mappings.add( new MappingEntry( valueMapping.getSource(), target ) ); + mappedSources.add( valueMapping.getSource() ); + mappings.add( new MappingEntry( valueMapping.getSource(), valueMapping.getTarget() ) ); unmappedSourceConstants.remove( valueMapping.getSource() ); } // add mappings based on name if ( !valueMappings.hasMapAnyUnmapped ) { - + mappedSources.add( NULL ); + TypeElement targetTypeElement = targetType.getTypeElement(); // all remaining constants are mapped for ( String sourceConstant : unmappedSourceConstants ) { - String stringConstant = enumTransformationInvoker.transform( sourceConstant ); - mappings.add( new MappingEntry( stringConstant, sourceConstant ) ); + String sourceNameConstant = getEnumConstant( targetTypeElement, sourceConstant ); + String stringConstant = enumTransformationInvoker.transform( sourceNameConstant ); + if ( !mappedSources.contains( stringConstant ) ) { + mappings.add( new MappingEntry( stringConstant, sourceConstant ) ); + } } } return mappings; } + private String getEnumConstant(TypeElement typeElement, String enumConstant) { + return ctx.getEnumNamingStrategy().getEnumConstant( typeElement, enumConstant ); + } + private SelectionParameters getSelectionParameters(Method method, Types typeUtils) { BeanMappingGem beanMapping = BeanMappingGem.instanceOn( method.getExecutable() ); if ( beanMapping != null ) { @@ -377,6 +399,18 @@ public class ValueMappingMethod extends MappingMethod { ); foundIncorrectMapping = true; } + else if ( valueMappings.nullTarget == null && valueMappings.nullValueTarget != null + && !targetEnumConstants.contains( valueMappings.nullValueTarget ) ) { + // if there is no nullTarget, but nullValueTarget has a value it means that there was an SPI + // which returned an enum for the target enum + ctx.getMessager().printMessage( + method.getExecutable(), + Message.VALUEMAPPING_NON_EXISTING_CONSTANT_FROM_SPI, + valueMappings.nullValueTarget, + method.getReturnType(), + ctx.getEnumNamingStrategy() + ); + } return !foundIncorrectMapping; } @@ -417,6 +451,7 @@ public class ValueMappingMethod extends MappingMethod { boolean hasMapAnyUnmapped = false; boolean hasMapAnyRemaining = false; boolean hasDefaultValue = false; + boolean hasNullValue = false; ValueMappings(List valueMappings) { @@ -436,6 +471,7 @@ public class ValueMappingMethod extends MappingMethod { else if ( NULL.equals( valueMapping.getSource() ) ) { nullTarget = valueMapping; nullValueTarget = getValue( nullTarget ); + hasNullValue = true; } else { regularValueMappings.add( valueMapping ); @@ -489,7 +525,12 @@ public class ValueMappingMethod extends MappingMethod { MappingEntry( String source, String target ) { this.source = source; - this.target = target; + if ( !NULL.equals( target ) ) { + this.target = target; + } + else { + this.target = null; + } } public String getSource() { 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 adb48d087..3b5cb1b03 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.EnumNamingStrategy; import org.mapstruct.ap.spi.EnumTransformationStrategy; /** @@ -98,6 +99,11 @@ public class DefaultModelElementProcessorContext implements ProcessorContext { return roundContext.getAnnotationProcessorContext().getEnumTransformationStrategies(); } + @Override + public EnumNamingStrategy getEnumNamingStrategy() { + return roundContext.getAnnotationProcessorContext().getEnumNamingStrategy(); + } + @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 6882fba64..c22377f98 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 { Map getEnumTransformationStrategies(); + EnumNamingStrategy getEnumNamingStrategy(); + 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 28db59abb..4b70bd60a 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 @@ -24,6 +24,8 @@ 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.DefaultEnumNamingStrategy; +import org.mapstruct.ap.spi.EnumNamingStrategy; import org.mapstruct.ap.spi.EnumTransformationStrategy; import org.mapstruct.ap.spi.FreeBuilderAccessorNamingStrategy; import org.mapstruct.ap.spi.ImmutablesAccessorNamingStrategy; @@ -41,6 +43,7 @@ public class AnnotationProcessorContext implements MapStructProcessingEnvironmen private BuilderProvider builderProvider; private AccessorNamingStrategy accessorNamingStrategy; + private EnumNamingStrategy enumNamingStrategy; private boolean initialized; private Map enumTransformationStrategies; @@ -110,6 +113,15 @@ public class AnnotationProcessorContext implements MapStructProcessingEnvironmen } this.accessorNaming = new AccessorNamingUtils( this.accessorNamingStrategy ); + this.enumNamingStrategy = Services.get( EnumNamingStrategy.class, new DefaultEnumNamingStrategy() ); + this.enumNamingStrategy.init( this ); + if ( verbose ) { + messager.printMessage( + Diagnostic.Kind.NOTE, + "MapStruct: Using enum naming strategy: " + + this.enumNamingStrategy.getClass().getCanonicalName() + ); + } this.enumTransformationStrategies = new LinkedHashMap<>(); ServiceLoader transformationStrategiesLoader = ServiceLoader.load( @@ -238,6 +250,11 @@ public class AnnotationProcessorContext implements MapStructProcessingEnvironmen return accessorNamingStrategy; } + public EnumNamingStrategy getEnumNamingStrategy() { + initialize(); + return enumNamingStrategy; + } + public BuilderProvider getBuilderProvider() { initialize(); return builderProvider; 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 a26166a7e..4c46c0991 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 @@ -158,6 +158,7 @@ public enum Message { VALUEMAPPING_UNMAPPED_SOURCES( "The following constants from the %s enum have no corresponding constant in the %s enum and must be be mapped via adding additional mappings: %s." ), VALUEMAPPING_ANY_REMAINING_FOR_NON_ENUM( "Source = \"\" can only be used on targets of type enum and not for %s." ), VALUEMAPPING_ANY_REMAINING_OR_UNMAPPED_MISSING( "Source = \"\" or \"\" is advisable for mapping of type String to an enum type.", Diagnostic.Kind.WARNING ), + VALUEMAPPING_NON_EXISTING_CONSTANT_FROM_SPI( "Constant %s doesn't exist in enum type %s. Constant was returned from EnumNamingStrategy: %s"), VALUEMAPPING_NON_EXISTING_CONSTANT( "Constant %s doesn't exist in enum type %s." ); // CHECKSTYLE:ON diff --git a/processor/src/main/java/org/mapstruct/ap/spi/DefaultEnumNamingStrategy.java b/processor/src/main/java/org/mapstruct/ap/spi/DefaultEnumNamingStrategy.java new file mode 100644 index 000000000..526d0a24f --- /dev/null +++ b/processor/src/main/java/org/mapstruct/ap/spi/DefaultEnumNamingStrategy.java @@ -0,0 +1,35 @@ +/* + * 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 javax.lang.model.element.TypeElement; +import javax.lang.model.util.Elements; +import javax.lang.model.util.Types; + +/** + * @author Filip Hrisafov + */ +public class DefaultEnumNamingStrategy implements EnumNamingStrategy { + + protected Elements elementUtils; + protected Types typeUtils; + + @Override + public void init(MapStructProcessingEnvironment processingEnvironment) { + this.elementUtils = processingEnvironment.getElementUtils(); + this.typeUtils = processingEnvironment.getTypeUtils(); + } + + @Override + public String getDefaultNullEnumConstant(TypeElement enumType) { + return null; + } + + @Override + public String getEnumConstant(TypeElement enumType, String enumConstant) { + return enumConstant; + } +} diff --git a/processor/src/main/java/org/mapstruct/ap/spi/EnumNamingStrategy.java b/processor/src/main/java/org/mapstruct/ap/spi/EnumNamingStrategy.java new file mode 100644 index 000000000..c135cc9ab --- /dev/null +++ b/processor/src/main/java/org/mapstruct/ap/spi/EnumNamingStrategy.java @@ -0,0 +1,48 @@ +/* + * 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 javax.lang.model.element.TypeElement; + +import org.mapstruct.util.Experimental; + +/** + * A service provider interface for the mapping between different enum constants + * + * @author Arne Seime + */ +@Experimental("This SPI can have it's signature changed in subsequent releases") +public interface EnumNamingStrategy { + + /** + * Initializes the enum value mapping strategy + * + * @param processingEnvironment environment for facilities + */ + default void init(MapStructProcessingEnvironment processingEnvironment) { + + } + + /** + * Return the default enum constant to use if the source is null. + * + * @param enumType the enum + * @return enum value or null if there is no designated enum constant + */ + String getDefaultNullEnumConstant(TypeElement enumType); + + /** + * Map the enum constant to the value use for matching. + * In case you want this enum constant to match to null return {@link org.mapstruct.MappingConstants#NULL} + * + * @param enumType the enum this constant belongs to + * @param enumConstant constant to transform + * + * @return the transformed constant - or the original value from the parameter if no transformation is needed. + * never return null + */ + String getEnumConstant(TypeElement enumType, String enumConstant); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/value/spi/CheeseType.java b/processor/src/test/java/org/mapstruct/ap/test/value/spi/CheeseType.java new file mode 100644 index 000000000..71bde4415 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/value/spi/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.spi; + +/** + * @author Filip Hrisafov + */ +public enum CheeseType { + + BRIE, + ROQUEFORT +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/value/spi/CustomCheeseMapper.java b/processor/src/test/java/org/mapstruct/ap/test/value/spi/CustomCheeseMapper.java new file mode 100644 index 000000000..6524f67c3 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/value/spi/CustomCheeseMapper.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.spi; + +import org.mapstruct.Mapper; +import org.mapstruct.MappingConstants; +import org.mapstruct.ValueMapping; +import org.mapstruct.factory.Mappers; + +/** + * @author Filip Hrisafov + */ +@Mapper +public interface CustomCheeseMapper { + + CustomCheeseMapper INSTANCE = Mappers.getMapper( CustomCheeseMapper.class ); + + CheeseType map(CustomCheeseType cheese); + + CustomCheeseType map(CheeseType cheese); + + String mapToString(CustomCheeseType cheeseType); + + String mapToString(CheeseType cheeseType); + + @ValueMapping(source = MappingConstants.ANY_REMAINING, target = "CUSTOM_BRIE") + CustomCheeseType mapStringToCustom(String cheese); + + @ValueMapping(source = MappingConstants.ANY_REMAINING, target = "BRIE") + CheeseType mapStringToCheese(String cheese); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/value/spi/CustomCheeseType.java b/processor/src/test/java/org/mapstruct/ap/test/value/spi/CustomCheeseType.java new file mode 100644 index 000000000..384df9b3d --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/value/spi/CustomCheeseType.java @@ -0,0 +1,17 @@ +/* + * 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.spi; + +/** + * @author Filip Hrisafov + */ +public enum CustomCheeseType implements CustomEnumMarker { + + UNSPECIFIED, + CUSTOM_BRIE, + CUSTOM_ROQUEFORT, + UNRECOGNIZED, +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/value/spi/CustomEnumMarker.java b/processor/src/test/java/org/mapstruct/ap/test/value/spi/CustomEnumMarker.java new file mode 100644 index 000000000..b12715e25 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/value/spi/CustomEnumMarker.java @@ -0,0 +1,12 @@ +/* + * 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.spi; + +/** + * @author Filip Hrisafov + */ +public interface CustomEnumMarker { +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/value/spi/CustomEnumNamingStrategy.java b/processor/src/test/java/org/mapstruct/ap/test/value/spi/CustomEnumNamingStrategy.java new file mode 100644 index 000000000..f84ebaf7b --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/value/spi/CustomEnumNamingStrategy.java @@ -0,0 +1,54 @@ +/* + * 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.spi; + +import javax.lang.model.element.TypeElement; +import javax.lang.model.type.TypeMirror; + +import org.mapstruct.ap.internal.gem.MappingConstantsGem; +import org.mapstruct.ap.spi.DefaultEnumNamingStrategy; +import org.mapstruct.ap.spi.EnumNamingStrategy; + +/** + * @author Filip Hrisafov + */ +public class CustomEnumNamingStrategy extends DefaultEnumNamingStrategy implements EnumNamingStrategy { + + @Override + public String getDefaultNullEnumConstant(TypeElement enumType) { + if ( isCustomEnum( enumType ) ) { + return "UNSPECIFIED"; + } + + return super.getDefaultNullEnumConstant( enumType ); + } + + @Override + public String getEnumConstant(TypeElement enumType, String enumConstant) { + if ( isCustomEnum( enumType ) ) { + return getCustomEnumConstant( enumConstant ); + } + return super.getEnumConstant( enumType, enumConstant ); + } + + protected String getCustomEnumConstant(String enumConstant) { + if ( "UNRECOGNIZED".equals( enumConstant ) || "UNSPECIFIED".equals( enumConstant ) ) { + return MappingConstantsGem.NULL; + } + + return enumConstant.replace( "CUSTOM_", "" ); + } + + protected boolean isCustomEnum(TypeElement enumType) { + for ( TypeMirror enumTypeInterface : enumType.getInterfaces() ) { + if ( typeUtils.asElement( enumTypeInterface ).getSimpleName().contentEquals( "CustomEnumMarker" ) ) { + return true; + } + } + + return false; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/value/spi/CustomEnumNamingStrategyTest.java b/processor/src/test/java/org/mapstruct/ap/test/value/spi/CustomEnumNamingStrategyTest.java new file mode 100644 index 000000000..47a0a5d64 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/value/spi/CustomEnumNamingStrategyTest.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.ap.test.value.spi; + +import org.junit.Rule; +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.runner.AnnotationProcessorTestRunner; +import org.mapstruct.ap.testutil.runner.GeneratedSource; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Filip Hrisafov + */ +@RunWith(AnnotationProcessorTestRunner.class) +@WithClasses({ + CheeseType.class, + CustomCheeseType.class, + CustomEnumMarker.class, +}) +@WithServiceImplementation(CustomEnumNamingStrategy.class) +public class CustomEnumNamingStrategyTest { + + @Rule + public final GeneratedSource generatedSource = new GeneratedSource(); + + @Test + @WithClasses({ + CustomCheeseMapper.class + }) + public void shouldApplyCustomEnumNamingStrategy() { + generatedSource.addComparisonToFixtureFor( CustomCheeseMapper.class ); + CustomCheeseMapper mapper = CustomCheeseMapper.INSTANCE; + + // CheeseType -> CustomCheeseType + assertThat( mapper.map( (CheeseType) null ) ).isEqualTo( CustomCheeseType.UNSPECIFIED ); + assertThat( mapper.map( CheeseType.BRIE ) ).isEqualTo( CustomCheeseType.CUSTOM_BRIE ); + assertThat( mapper.map( CheeseType.ROQUEFORT ) ).isEqualTo( CustomCheeseType.CUSTOM_ROQUEFORT ); + + // CustomCheeseType -> CheeseType + assertThat( mapper.map( (CustomCheeseType) null ) ).isNull(); + assertThat( mapper.map( CustomCheeseType.UNSPECIFIED ) ).isNull(); + assertThat( mapper.map( CustomCheeseType.CUSTOM_BRIE ) ).isEqualTo( CheeseType.BRIE ); + assertThat( mapper.map( CustomCheeseType.CUSTOM_ROQUEFORT ) ).isEqualTo( CheeseType.ROQUEFORT ); + assertThat( mapper.map( CustomCheeseType.UNRECOGNIZED ) ).isNull(); + + // CheeseType -> String + assertThat( mapper.mapToString( (CheeseType) null ) ).isNull(); + assertThat( mapper.mapToString( CheeseType.BRIE ) ).isEqualTo( "BRIE" ); + assertThat( mapper.mapToString( CheeseType.ROQUEFORT ) ).isEqualTo( "ROQUEFORT" ); + + // CustomCheeseType -> String + assertThat( mapper.mapToString( (CustomCheeseType) null ) ).isNull(); + assertThat( mapper.mapToString( CustomCheeseType.UNSPECIFIED ) ).isNull(); + assertThat( mapper.mapToString( CustomCheeseType.CUSTOM_BRIE ) ).isEqualTo( "BRIE" ); + assertThat( mapper.mapToString( CustomCheeseType.CUSTOM_ROQUEFORT ) ).isEqualTo( "ROQUEFORT" ); + assertThat( mapper.mapToString( CustomCheeseType.UNRECOGNIZED ) ).isNull(); + + // String - > CheeseType + assertThat( mapper.mapStringToCheese( null ) ).isNull(); + assertThat( mapper.mapStringToCheese( "BRIE" ) ).isEqualTo( CheeseType.BRIE ); + assertThat( mapper.mapStringToCheese( "ROQUEFORT" ) ).isEqualTo( CheeseType.ROQUEFORT ); + assertThat( mapper.mapStringToCheese( "UNKNOWN" ) ).isEqualTo( CheeseType.BRIE ); + + // CustomCheeseType -> String + assertThat( mapper.mapStringToCustom( null ) ).isEqualTo( CustomCheeseType.UNSPECIFIED ); + assertThat( mapper.mapStringToCustom( "UNRECOGNIZED" ) ).isEqualTo( CustomCheeseType.CUSTOM_BRIE ); + assertThat( mapper.mapStringToCustom( "BRIE" ) ).isEqualTo( CustomCheeseType.CUSTOM_BRIE ); + assertThat( mapper.mapStringToCustom( "ROQUEFORT" ) ).isEqualTo( CustomCheeseType.CUSTOM_ROQUEFORT ); + assertThat( mapper.mapStringToCustom( "UNKNOWN" ) ).isEqualTo( CustomCheeseType.CUSTOM_BRIE ); + } + + @Test + @WithClasses({ + OverridesCustomCheeseMapper.class + }) + public void shouldApplyDefinedMappingsInsteadOfCustomEnumNamingStrategy() { + OverridesCustomCheeseMapper mapper = OverridesCustomCheeseMapper.INSTANCE; + + // CheeseType -> CustomCheeseType + assertThat( mapper.map( (CheeseType) null ) ).isEqualTo( CustomCheeseType.CUSTOM_ROQUEFORT ); + assertThat( mapper.map( CheeseType.BRIE ) ).isEqualTo( CustomCheeseType.CUSTOM_ROQUEFORT ); + assertThat( mapper.map( CheeseType.ROQUEFORT ) ).isEqualTo( CustomCheeseType.CUSTOM_ROQUEFORT ); + + // CustomCheeseType -> CheeseType + assertThat( mapper.map( (CustomCheeseType) null ) ).isEqualTo( CheeseType.ROQUEFORT ); + assertThat( mapper.map( CustomCheeseType.UNSPECIFIED ) ).isNull(); + assertThat( mapper.map( CustomCheeseType.CUSTOM_BRIE ) ).isEqualTo( CheeseType.ROQUEFORT ); + assertThat( mapper.map( CustomCheeseType.CUSTOM_ROQUEFORT ) ).isEqualTo( CheeseType.ROQUEFORT ); + assertThat( mapper.map( CustomCheeseType.UNRECOGNIZED ) ).isNull(); + + // CheeseType -> String + assertThat( mapper.mapToString( (CheeseType) null ) ).isNull(); + assertThat( mapper.mapToString( CheeseType.BRIE ) ).isEqualTo( "BRIE" ); + assertThat( mapper.mapToString( CheeseType.ROQUEFORT ) ).isEqualTo( "BRIE" ); + + // CustomCheeseType -> String + assertThat( mapper.mapToString( (CustomCheeseType) null ) ).isEqualTo( "ROQUEFORT" ); + assertThat( mapper.mapToString( CustomCheeseType.UNSPECIFIED ) ).isNull(); + assertThat( mapper.mapToString( CustomCheeseType.CUSTOM_BRIE ) ).isEqualTo( "BRIE" ); + assertThat( mapper.mapToString( CustomCheeseType.CUSTOM_ROQUEFORT ) ).isEqualTo( "BRIE" ); + assertThat( mapper.mapToString( CustomCheeseType.UNRECOGNIZED ) ).isNull(); + + // String - > CheeseType + assertThat( mapper.mapStringToCheese( null ) ).isEqualTo( CheeseType.ROQUEFORT ); + assertThat( mapper.mapStringToCheese( "BRIE" ) ).isEqualTo( CheeseType.ROQUEFORT ); + assertThat( mapper.mapStringToCheese( "ROQUEFORT" ) ).isEqualTo( CheeseType.ROQUEFORT ); + assertThat( mapper.mapStringToCheese( "UNKNOWN" ) ).isEqualTo( CheeseType.BRIE ); + + // CustomCheeseType -> String + assertThat( mapper.mapStringToCustom( null ) ).isEqualTo( CustomCheeseType.CUSTOM_ROQUEFORT ); + assertThat( mapper.mapStringToCustom( "UNRECOGNIZED" ) ).isEqualTo( CustomCheeseType.CUSTOM_BRIE ); + assertThat( mapper.mapStringToCustom( "BRIE" ) ).isEqualTo( CustomCheeseType.CUSTOM_ROQUEFORT ); + assertThat( mapper.mapStringToCustom( "ROQUEFORT" ) ).isEqualTo( CustomCheeseType.CUSTOM_ROQUEFORT ); + assertThat( mapper.mapStringToCustom( "UNKNOWN" ) ).isEqualTo( CustomCheeseType.CUSTOM_BRIE ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/value/spi/CustomErroneousEnumNamingStrategy.java b/processor/src/test/java/org/mapstruct/ap/test/value/spi/CustomErroneousEnumNamingStrategy.java new file mode 100644 index 000000000..671ad45c0 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/value/spi/CustomErroneousEnumNamingStrategy.java @@ -0,0 +1,54 @@ +/* + * 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.spi; + +import javax.lang.model.element.TypeElement; +import javax.lang.model.type.TypeMirror; + +import org.mapstruct.ap.internal.gem.MappingConstantsGem; +import org.mapstruct.ap.spi.DefaultEnumNamingStrategy; +import org.mapstruct.ap.spi.EnumNamingStrategy; + +/** + * @author Filip Hrisafov + */ +public class CustomErroneousEnumNamingStrategy extends DefaultEnumNamingStrategy implements EnumNamingStrategy { + + @Override + public String getDefaultNullEnumConstant(TypeElement enumType) { + if ( isCustomEnum( enumType ) ) { + return "INCORRECT"; + } + + return super.getDefaultNullEnumConstant( enumType ); + } + + @Override + public String getEnumConstant(TypeElement enumType, String enumConstant) { + if ( isCustomEnum( enumType ) ) { + return getCustomEnumConstant( enumConstant ); + } + return super.getEnumConstant( enumType, enumConstant ); + } + + protected String getCustomEnumConstant(String enumConstant) { + if ( "UNRECOGNIZED".equals( enumConstant ) || "UNSPECIFIED".equals( enumConstant ) ) { + return MappingConstantsGem.NULL; + } + + return enumConstant.replace( "CUSTOM_", "" ); + } + + protected boolean isCustomEnum(TypeElement enumType) { + for ( TypeMirror enumTypeInterface : enumType.getInterfaces() ) { + if ( typeUtils.asElement( enumTypeInterface ).getSimpleName().contentEquals( "CustomEnumMarker" ) ) { + return true; + } + } + + return false; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/value/spi/CustomErroneousEnumNamingStrategyTest.java b/processor/src/test/java/org/mapstruct/ap/test/value/spi/CustomErroneousEnumNamingStrategyTest.java new file mode 100644 index 000000000..82d6be4c2 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/value/spi/CustomErroneousEnumNamingStrategyTest.java @@ -0,0 +1,102 @@ +/* + * 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.spi; + +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, + CustomCheeseType.class, + CustomEnumMarker.class, +}) +@WithServiceImplementation(CustomErroneousEnumNamingStrategy.class) +public class CustomErroneousEnumNamingStrategyTest { + + @Test + @WithClasses({ + CustomCheeseMapper.class + }) + @ExpectedCompilationOutcome(value = CompilationResult.FAILED, + diagnostics = { + @Diagnostic( + type = CustomCheeseMapper.class, + kind = javax.tools.Diagnostic.Kind.ERROR, + line = 23, + messageRegExp = "Constant INCORRECT doesn't exist in enum type " + + "org\\.mapstruct\\.ap\\.test\\.value\\.spi\\.CustomCheeseType." + + " Constant was returned from EnumNamingStrategy: .*CustomErroneousEnumNamingStrategy@.*" + ), + @Diagnostic( + type = CustomCheeseMapper.class, + kind = javax.tools.Diagnostic.Kind.ERROR, + line = 30, + messageRegExp = "Constant INCORRECT doesn't exist in enum type " + + "org\\.mapstruct\\.ap\\.test\\.value\\.spi\\.CustomCheeseType." + + " Constant was returned from EnumNamingStrategy: .*CustomErroneousEnumNamingStrategy@.*" + ) + } + ) + public void shouldThrowCompileErrorWhenDefaultEnumDoesNotExist() { + } + + @Test + @WithClasses({ + OverridesCustomCheeseMapper.class + }) + public void shouldApplyDefinedMappingsInsteadOfCustomEnumNamingStrategy() { + OverridesCustomCheeseMapper mapper = OverridesCustomCheeseMapper.INSTANCE; + + // CheeseType -> CustomCheeseType + assertThat( mapper.map( (CheeseType) null ) ).isEqualTo( CustomCheeseType.CUSTOM_ROQUEFORT ); + assertThat( mapper.map( CheeseType.BRIE ) ).isEqualTo( CustomCheeseType.CUSTOM_ROQUEFORT ); + assertThat( mapper.map( CheeseType.ROQUEFORT ) ).isEqualTo( CustomCheeseType.CUSTOM_ROQUEFORT ); + + // CustomCheeseType -> CheeseType + assertThat( mapper.map( (CustomCheeseType) null ) ).isEqualTo( CheeseType.ROQUEFORT ); + assertThat( mapper.map( CustomCheeseType.UNSPECIFIED ) ).isNull(); + assertThat( mapper.map( CustomCheeseType.CUSTOM_BRIE ) ).isEqualTo( CheeseType.ROQUEFORT ); + assertThat( mapper.map( CustomCheeseType.CUSTOM_ROQUEFORT ) ).isEqualTo( CheeseType.ROQUEFORT ); + assertThat( mapper.map( CustomCheeseType.UNRECOGNIZED ) ).isNull(); + + // CheeseType -> String + assertThat( mapper.mapToString( (CheeseType) null ) ).isNull(); + assertThat( mapper.mapToString( CheeseType.BRIE ) ).isEqualTo( "BRIE" ); + assertThat( mapper.mapToString( CheeseType.ROQUEFORT ) ).isEqualTo( "BRIE" ); + + // CustomCheeseType -> String + assertThat( mapper.mapToString( (CustomCheeseType) null ) ).isEqualTo( "ROQUEFORT" ); + assertThat( mapper.mapToString( CustomCheeseType.UNSPECIFIED ) ).isNull(); + assertThat( mapper.mapToString( CustomCheeseType.CUSTOM_BRIE ) ).isEqualTo( "BRIE" ); + assertThat( mapper.mapToString( CustomCheeseType.CUSTOM_ROQUEFORT ) ).isEqualTo( "BRIE" ); + assertThat( mapper.mapToString( CustomCheeseType.UNRECOGNIZED ) ).isNull(); + + // String - > CheeseType + assertThat( mapper.mapStringToCheese( null ) ).isEqualTo( CheeseType.ROQUEFORT ); + assertThat( mapper.mapStringToCheese( "BRIE" ) ).isEqualTo( CheeseType.ROQUEFORT ); + assertThat( mapper.mapStringToCheese( "ROQUEFORT" ) ).isEqualTo( CheeseType.ROQUEFORT ); + assertThat( mapper.mapStringToCheese( "UNKNOWN" ) ).isEqualTo( CheeseType.BRIE ); + + // CustomCheeseType -> String + assertThat( mapper.mapStringToCustom( null ) ).isEqualTo( CustomCheeseType.CUSTOM_ROQUEFORT ); + assertThat( mapper.mapStringToCustom( "UNRECOGNIZED" ) ).isEqualTo( CustomCheeseType.CUSTOM_BRIE ); + assertThat( mapper.mapStringToCustom( "BRIE" ) ).isEqualTo( CustomCheeseType.CUSTOM_ROQUEFORT ); + assertThat( mapper.mapStringToCustom( "ROQUEFORT" ) ).isEqualTo( CustomCheeseType.CUSTOM_ROQUEFORT ); + assertThat( mapper.mapStringToCustom( "UNKNOWN" ) ).isEqualTo( CustomCheeseType.CUSTOM_BRIE ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/value/spi/OverridesCustomCheeseMapper.java b/processor/src/test/java/org/mapstruct/ap/test/value/spi/OverridesCustomCheeseMapper.java new file mode 100644 index 000000000..ae40d2d98 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/value/spi/OverridesCustomCheeseMapper.java @@ -0,0 +1,45 @@ +/* + * 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.spi; + +import org.mapstruct.Mapper; +import org.mapstruct.MappingConstants; +import org.mapstruct.ValueMapping; +import org.mapstruct.factory.Mappers; + +/** + * @author Filip Hrisafov + */ +@Mapper +public interface OverridesCustomCheeseMapper { + + OverridesCustomCheeseMapper INSTANCE = Mappers.getMapper( OverridesCustomCheeseMapper.class ); + + @ValueMapping(source = "CUSTOM_BRIE", target = "ROQUEFORT") + @ValueMapping(source = MappingConstants.NULL, target = "ROQUEFORT") + CheeseType map(CustomCheeseType cheese); + + @ValueMapping(source = "BRIE", target = "CUSTOM_ROQUEFORT") + @ValueMapping(source = MappingConstants.NULL, target = "CUSTOM_ROQUEFORT") + CustomCheeseType map(CheeseType cheese); + + @ValueMapping(source = "CUSTOM_ROQUEFORT", target = "BRIE") + @ValueMapping(source = MappingConstants.NULL, target = "ROQUEFORT") + String mapToString(CustomCheeseType cheeseType); + + @ValueMapping(source = "ROQUEFORT", target = "BRIE") + String mapToString(CheeseType cheeseType); + + @ValueMapping(source = MappingConstants.ANY_REMAINING, target = "CUSTOM_BRIE") + @ValueMapping(source = "BRIE", target = "CUSTOM_ROQUEFORT") + @ValueMapping(source = MappingConstants.NULL, target = "CUSTOM_ROQUEFORT") + CustomCheeseType mapStringToCustom(String cheese); + + @ValueMapping(source = MappingConstants.ANY_REMAINING, target = "BRIE") + @ValueMapping(source = "BRIE", target = "ROQUEFORT") + @ValueMapping(source = MappingConstants.NULL, target = "ROQUEFORT") + CheeseType mapStringToCheese(String cheese); +} diff --git a/processor/src/test/resources/fixtures/org/mapstruct/ap/test/value/spi/CustomCheeseMapperImpl.java b/processor/src/test/resources/fixtures/org/mapstruct/ap/test/value/spi/CustomCheeseMapperImpl.java new file mode 100644 index 000000000..9798c4159 --- /dev/null +++ b/processor/src/test/resources/fixtures/org/mapstruct/ap/test/value/spi/CustomCheeseMapperImpl.java @@ -0,0 +1,138 @@ +/* + * 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.spi; + +import javax.annotation.processing.Generated; + +@Generated( + value = "org.mapstruct.ap.MappingProcessor", + date = "2020-05-16T12:53:12+0200", + comments = "version: , compiler: javac, environment: Java 14.0.1 (Oracle Corporation)" +) +public class CustomCheeseMapperImpl implements CustomCheeseMapper { + + @Override + public CheeseType map(CustomCheeseType cheese) { + if ( cheese == null ) { + return null; + } + + CheeseType cheeseType; + + switch ( cheese ) { + case UNSPECIFIED: cheeseType = null; + break; + case CUSTOM_BRIE: cheeseType = CheeseType.BRIE; + break; + case CUSTOM_ROQUEFORT: cheeseType = CheeseType.ROQUEFORT; + break; + case UNRECOGNIZED: cheeseType = null; + break; + default: throw new IllegalArgumentException( "Unexpected enum constant: " + cheese ); + } + + return cheeseType; + } + + @Override + public CustomCheeseType map(CheeseType cheese) { + if ( cheese == null ) { + return CustomCheeseType.UNSPECIFIED; + } + + CustomCheeseType customCheeseType; + + switch ( cheese ) { + case BRIE: customCheeseType = CustomCheeseType.CUSTOM_BRIE; + break; + case ROQUEFORT: customCheeseType = CustomCheeseType.CUSTOM_ROQUEFORT; + break; + default: throw new IllegalArgumentException( "Unexpected enum constant: " + cheese ); + } + + return customCheeseType; + } + + @Override + public String mapToString(CustomCheeseType cheeseType) { + if ( cheeseType == null ) { + return null; + } + + String string; + + switch ( cheeseType ) { + case UNSPECIFIED: string = null; + break; + case CUSTOM_BRIE: string = "BRIE"; + break; + case CUSTOM_ROQUEFORT: string = "ROQUEFORT"; + break; + case UNRECOGNIZED: string = null; + break; + default: throw new IllegalArgumentException( "Unexpected enum constant: " + cheeseType ); + } + + return string; + } + + @Override + public String mapToString(CheeseType cheeseType) { + if ( cheeseType == null ) { + return null; + } + + String string; + + switch ( cheeseType ) { + case BRIE: string = "BRIE"; + break; + case ROQUEFORT: string = "ROQUEFORT"; + break; + default: throw new IllegalArgumentException( "Unexpected enum constant: " + cheeseType ); + } + + return string; + } + + @Override + public CustomCheeseType mapStringToCustom(String cheese) { + if ( cheese == null ) { + return CustomCheeseType.UNSPECIFIED; + } + + CustomCheeseType customCheeseType; + + switch ( cheese ) { + case "BRIE": customCheeseType = CustomCheeseType.CUSTOM_BRIE; + break; + case "ROQUEFORT": customCheeseType = CustomCheeseType.CUSTOM_ROQUEFORT; + break; + default: customCheeseType = CustomCheeseType.CUSTOM_BRIE; + } + + return customCheeseType; + } + + @Override + public CheeseType mapStringToCheese(String cheese) { + if ( cheese == null ) { + return null; + } + + CheeseType cheeseType; + + switch ( cheese ) { + case "BRIE": cheeseType = CheeseType.BRIE; + break; + case "ROQUEFORT": cheeseType = CheeseType.ROQUEFORT; + break; + default: cheeseType = CheeseType.BRIE; + } + + return cheeseType; + } +}