From fd27380185fe83feb6d26308c7d358025783ff8f Mon Sep 17 00:00:00 2001 From: Filip Hrisafov Date: Sun, 13 Nov 2022 14:22:25 +0100 Subject: [PATCH] #2953 Add support for globally defining nullValueMapMappingStrategy and nullValueIterableMappingStrategy --- .../main/asciidoc/chapter-2-set-up.asciidoc | 24 ++++++++++ .../org/mapstruct/ap/MappingProcessor.java | 18 +++++++- .../internal/model/source/DefaultOptions.java | 8 ++++ .../mapstruct/ap/internal/option/Options.java | 20 ++++++++- .../test/nullvaluemapping/CarListMapper.java | 30 +++++++++++++ .../CarListMapperSettingOnMapper.java | 31 +++++++++++++ .../test/nullvaluemapping/CarMapMapper.java | 30 +++++++++++++ .../CarMapMapperSettingOnMapper.java | 31 +++++++++++++ .../NullValueIterableMappingStrategyTest.java | 45 +++++++++++++++++++ .../NullValueMapMappingStrategyTest.java | 45 +++++++++++++++++++ 10 files changed, 279 insertions(+), 3 deletions(-) create mode 100644 processor/src/test/java/org/mapstruct/ap/test/nullvaluemapping/CarListMapper.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/nullvaluemapping/CarListMapperSettingOnMapper.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/nullvaluemapping/CarMapMapper.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/nullvaluemapping/CarMapMapperSettingOnMapper.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/nullvaluemapping/NullValueIterableMappingStrategyTest.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/nullvaluemapping/NullValueMapMappingStrategyTest.java diff --git a/documentation/src/main/asciidoc/chapter-2-set-up.asciidoc b/documentation/src/main/asciidoc/chapter-2-set-up.asciidoc index 601bc42a9..12c399c1f 100644 --- a/documentation/src/main/asciidoc/chapter-2-set-up.asciidoc +++ b/documentation/src/main/asciidoc/chapter-2-set-up.asciidoc @@ -266,6 +266,30 @@ If a policy is given for a specific bean mapping via `@BeanMapping#ignoreUnmappe disableBuilders` |If set to `true`, then MapStruct will not use builder patterns when doing the mapping. This is equivalent to doing `@Mapper( builder = @Builder( disableBuilder = true ) )` for all of your mappers. |`false` + +|`mapstruct.nullValueIterableMappingStrategy` +|The strategy to be applied when `null` is passed as a source value to an iterable mapping. + +Supported values are: + +* `RETURN_NULL`: if `null` is passed as a source value, then `null` will be returned +* `RETURN_DEFAULT`: if `null` is passed then a default value (empty collection) will be returned + +If a strategy is given for a specific mapper via `@Mapper#nullValueIterableMappingStrategy()`, the value from the annotation takes precedence. +If a strategy is given for a specific iterable mapping via `@IterableMapping#nullValueMappingStrategy()`, it takes precedence over both `@Mapper#nullValueIterableMappingStrategy()` and the option. +|`RETURN_NULL` + +|`mapstruct.nullValueMapMappingStrategy` +|The strategy to be applied when `null` is passed as a source value to a map mapping. + +Supported values are: + +* `RETURN_NULL`: if `null` is passed as a source value, then `null` will be returned +* `RETURN_DEFAULT`: if `null` is passed then a default value (empty map) will be returned + +If a strategy is given for a specific mapper via `@Mapper#nullValueMapMappingStrategy()`, the value from the annotation takes precedence. +If a strategy is given for a specific map mapping via `@MapMapping#nullValueMappingStrategy()`, it takes precedence over both `@Mapper#nullValueMapMappingStrategy()` and the option. +|`RETURN_NULL` |=== === Using MapStruct with the Java Module System diff --git a/processor/src/main/java/org/mapstruct/ap/MappingProcessor.java b/processor/src/main/java/org/mapstruct/ap/MappingProcessor.java index 2a4af558f..8c842f007 100644 --- a/processor/src/main/java/org/mapstruct/ap/MappingProcessor.java +++ b/processor/src/main/java/org/mapstruct/ap/MappingProcessor.java @@ -12,6 +12,7 @@ import java.util.Comparator; import java.util.HashSet; import java.util.Iterator; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.ServiceLoader; import java.util.Set; @@ -32,6 +33,7 @@ import javax.lang.model.type.TypeMirror; import javax.lang.model.util.ElementKindVisitor6; import javax.tools.Diagnostic.Kind; +import org.mapstruct.ap.internal.gem.NullValueMappingStrategyGem; import org.mapstruct.ap.internal.model.Mapper; import org.mapstruct.ap.internal.option.Options; import org.mapstruct.ap.internal.gem.MapperGem; @@ -87,7 +89,9 @@ import static javax.lang.model.element.ElementKind.CLASS; MappingProcessor.DEFAULT_COMPONENT_MODEL, MappingProcessor.DEFAULT_INJECTION_STRATEGY, MappingProcessor.DISABLE_BUILDERS, - MappingProcessor.VERBOSE + MappingProcessor.VERBOSE, + MappingProcessor.NULL_VALUE_ITERABLE_MAPPING_STRATEGY, + MappingProcessor.NULL_VALUE_MAP_MAPPING_STRATEGY, }) public class MappingProcessor extends AbstractProcessor { @@ -106,6 +110,8 @@ public class MappingProcessor extends AbstractProcessor { protected static final String ALWAYS_GENERATE_SERVICE_FILE = "mapstruct.alwaysGenerateServicesFile"; protected static final String DISABLE_BUILDERS = "mapstruct.disableBuilders"; protected static final String VERBOSE = "mapstruct.verbose"; + protected static final String NULL_VALUE_ITERABLE_MAPPING_STRATEGY = "mapstruct.nullValueIterableMappingStrategy"; + protected static final String NULL_VALUE_MAP_MAPPING_STRATEGY = "mapstruct.nullValueMapMappingStrategy"; private Options options; @@ -139,6 +145,9 @@ public class MappingProcessor extends AbstractProcessor { private Options createOptions() { String unmappedTargetPolicy = processingEnv.getOptions().get( UNMAPPED_TARGET_POLICY ); String unmappedSourcePolicy = processingEnv.getOptions().get( UNMAPPED_SOURCE_POLICY ); + String nullValueIterableMappingStrategy = processingEnv.getOptions() + .get( NULL_VALUE_ITERABLE_MAPPING_STRATEGY ); + String nullValueMapMappingStrategy = processingEnv.getOptions().get( NULL_VALUE_MAP_MAPPING_STRATEGY ); return new Options( Boolean.parseBoolean( processingEnv.getOptions().get( SUPPRESS_GENERATOR_TIMESTAMP ) ), @@ -149,7 +158,12 @@ public class MappingProcessor extends AbstractProcessor { processingEnv.getOptions().get( DEFAULT_INJECTION_STRATEGY ), Boolean.parseBoolean( processingEnv.getOptions().get( ALWAYS_GENERATE_SERVICE_FILE ) ), Boolean.parseBoolean( processingEnv.getOptions().get( DISABLE_BUILDERS ) ), - Boolean.parseBoolean( processingEnv.getOptions().get( VERBOSE ) ) + Boolean.parseBoolean( processingEnv.getOptions().get( VERBOSE ) ), + nullValueIterableMappingStrategy != null ? + NullValueMappingStrategyGem.valueOf( nullValueIterableMappingStrategy.toUpperCase( Locale.ROOT ) ) : + null, + nullValueMapMappingStrategy != null ? + NullValueMappingStrategyGem.valueOf( nullValueMapMappingStrategy.toUpperCase( Locale.ROOT ) ) : null ); } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/source/DefaultOptions.java b/processor/src/main/java/org/mapstruct/ap/internal/model/source/DefaultOptions.java index 5c76e7fac..c754d3c39 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/source/DefaultOptions.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/source/DefaultOptions.java @@ -132,10 +132,18 @@ public class DefaultOptions extends DelegatingOptions { } public NullValueMappingStrategyGem getNullValueIterableMappingStrategy() { + NullValueMappingStrategyGem nullValueIterableMappingStrategy = options.getNullValueIterableMappingStrategy(); + if ( nullValueIterableMappingStrategy != null ) { + return nullValueIterableMappingStrategy; + } return NullValueMappingStrategyGem.valueOf( mapper.nullValueIterableMappingStrategy().getDefaultValue() ); } public NullValueMappingStrategyGem getNullValueMapMappingStrategy() { + NullValueMappingStrategyGem nullValueMapMappingStrategy = options.getNullValueMapMappingStrategy(); + if ( nullValueMapMappingStrategy != null ) { + return nullValueMapMappingStrategy; + } return NullValueMappingStrategyGem.valueOf( mapper.nullValueMapMappingStrategy().getDefaultValue() ); } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/option/Options.java b/processor/src/main/java/org/mapstruct/ap/internal/option/Options.java index 65089cc13..a544374c0 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/option/Options.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/option/Options.java @@ -5,6 +5,7 @@ */ package org.mapstruct.ap.internal.option; +import org.mapstruct.ap.internal.gem.NullValueMappingStrategyGem; import org.mapstruct.ap.internal.gem.ReportingPolicyGem; /** @@ -23,14 +24,21 @@ public class Options { private final String defaultInjectionStrategy; private final boolean disableBuilders; private final boolean verbose; + private final NullValueMappingStrategyGem nullValueIterableMappingStrategy; + private final NullValueMappingStrategyGem nullValueMapMappingStrategy; + //CHECKSTYLE:OFF public Options(boolean suppressGeneratorTimestamp, boolean suppressGeneratorVersionComment, ReportingPolicyGem unmappedTargetPolicy, ReportingPolicyGem unmappedSourcePolicy, String defaultComponentModel, String defaultInjectionStrategy, boolean alwaysGenerateSpi, boolean disableBuilders, - boolean verbose) { + boolean verbose, + NullValueMappingStrategyGem nullValueIterableMappingStrategy, + NullValueMappingStrategyGem nullValueMapMappingStrategy + ) { + //CHECKSTYLE:ON this.suppressGeneratorTimestamp = suppressGeneratorTimestamp; this.suppressGeneratorVersionComment = suppressGeneratorVersionComment; this.unmappedTargetPolicy = unmappedTargetPolicy; @@ -40,6 +48,8 @@ public class Options { this.alwaysGenerateSpi = alwaysGenerateSpi; this.disableBuilders = disableBuilders; this.verbose = verbose; + this.nullValueIterableMappingStrategy = nullValueIterableMappingStrategy; + this.nullValueMapMappingStrategy = nullValueMapMappingStrategy; } public boolean isSuppressGeneratorTimestamp() { @@ -77,4 +87,12 @@ public class Options { public boolean isVerbose() { return verbose; } + + public NullValueMappingStrategyGem getNullValueIterableMappingStrategy() { + return nullValueIterableMappingStrategy; + } + + public NullValueMappingStrategyGem getNullValueMapMappingStrategy() { + return nullValueMapMappingStrategy; + } } diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullvaluemapping/CarListMapper.java b/processor/src/test/java/org/mapstruct/ap/test/nullvaluemapping/CarListMapper.java new file mode 100644 index 000000000..d239f9ace --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullvaluemapping/CarListMapper.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.nullvaluemapping; + +import java.util.List; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.ap.test.nullvaluemapping._target.CarDto; +import org.mapstruct.ap.test.nullvaluemapping.source.Car; +import org.mapstruct.factory.Mappers; + +/** + * @author Filip Hrisafov + */ +@Mapper +public interface CarListMapper { + + CarListMapper INSTANCE = Mappers.getMapper( CarListMapper.class ); + + @Mapping(target = "seatCount", ignore = true) + @Mapping(target = "model", ignore = true) + @Mapping(target = "catalogId", ignore = true) + CarDto map(Car car); + + List carsToCarDtoList(List cars); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullvaluemapping/CarListMapperSettingOnMapper.java b/processor/src/test/java/org/mapstruct/ap/test/nullvaluemapping/CarListMapperSettingOnMapper.java new file mode 100644 index 000000000..860b17654 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullvaluemapping/CarListMapperSettingOnMapper.java @@ -0,0 +1,31 @@ +/* + * 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.nullvaluemapping; + +import java.util.List; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.NullValueMappingStrategy; +import org.mapstruct.ap.test.nullvaluemapping._target.CarDto; +import org.mapstruct.ap.test.nullvaluemapping.source.Car; +import org.mapstruct.factory.Mappers; + +/** + * @author Filip Hrisafov + */ +@Mapper(nullValueIterableMappingStrategy = NullValueMappingStrategy.RETURN_NULL) +public interface CarListMapperSettingOnMapper { + + CarListMapperSettingOnMapper INSTANCE = Mappers.getMapper( CarListMapperSettingOnMapper.class ); + + @Mapping(target = "seatCount", ignore = true) + @Mapping(target = "model", ignore = true) + @Mapping(target = "catalogId", ignore = true) + CarDto map(Car car); + + List carsToCarDtoList(List cars); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullvaluemapping/CarMapMapper.java b/processor/src/test/java/org/mapstruct/ap/test/nullvaluemapping/CarMapMapper.java new file mode 100644 index 000000000..0227949f4 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullvaluemapping/CarMapMapper.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.nullvaluemapping; + +import java.util.Map; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.ap.test.nullvaluemapping._target.CarDto; +import org.mapstruct.ap.test.nullvaluemapping.source.Car; +import org.mapstruct.factory.Mappers; + +/** + * @author Filip Hrisafov + */ +@Mapper +public interface CarMapMapper { + + CarMapMapper INSTANCE = Mappers.getMapper( CarMapMapper.class ); + + @Mapping(target = "seatCount", ignore = true) + @Mapping(target = "model", ignore = true) + @Mapping(target = "catalogId", ignore = true) + CarDto map(Car car); + + Map carsToCarDtoMap(Map cars); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullvaluemapping/CarMapMapperSettingOnMapper.java b/processor/src/test/java/org/mapstruct/ap/test/nullvaluemapping/CarMapMapperSettingOnMapper.java new file mode 100644 index 000000000..adb99887b --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullvaluemapping/CarMapMapperSettingOnMapper.java @@ -0,0 +1,31 @@ +/* + * 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.nullvaluemapping; + +import java.util.Map; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.NullValueMappingStrategy; +import org.mapstruct.ap.test.nullvaluemapping._target.CarDto; +import org.mapstruct.ap.test.nullvaluemapping.source.Car; +import org.mapstruct.factory.Mappers; + +/** + * @author Filip Hrisafov + */ +@Mapper(nullValueMapMappingStrategy = NullValueMappingStrategy.RETURN_NULL) +public interface CarMapMapperSettingOnMapper { + + CarMapMapperSettingOnMapper INSTANCE = Mappers.getMapper( CarMapMapperSettingOnMapper.class ); + + @Mapping(target = "seatCount", ignore = true) + @Mapping(target = "model", ignore = true) + @Mapping(target = "catalogId", ignore = true) + CarDto map(Car car); + + Map carsToCarDtoMap(Map cars); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullvaluemapping/NullValueIterableMappingStrategyTest.java b/processor/src/test/java/org/mapstruct/ap/test/nullvaluemapping/NullValueIterableMappingStrategyTest.java new file mode 100644 index 000000000..c4acecd64 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullvaluemapping/NullValueIterableMappingStrategyTest.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.nullvaluemapping; + +import org.mapstruct.ap.test.nullvaluemapping._target.CarDto; +import org.mapstruct.ap.test.nullvaluemapping.source.Car; +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.compilation.annotation.ProcessorOption; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Filip Hrisafov + */ +@WithClasses({ + CarDto.class, + Car.class +}) +@IssueKey("2953") +public class NullValueIterableMappingStrategyTest { + + @ProcessorTest + @ProcessorOption(name = "mapstruct.nullValueIterableMappingStrategy", value = "return_default") + @WithClasses({ + CarListMapper.class + }) + void globalNullIterableMappingStrategy() { + assertThat( CarListMapper.INSTANCE.carsToCarDtoList( null ) ).isEmpty(); + } + + @ProcessorTest + @ProcessorOption(name = "mapstruct.nullValueIterableMappingStrategy", value = "return_default") + @WithClasses({ + CarListMapperSettingOnMapper.class + }) + void globalNullMapMappingStrategyWithOverrideInMapper() { + // Explicit definition in @Mapper should override global + assertThat( CarListMapperSettingOnMapper.INSTANCE.carsToCarDtoList( null ) ).isNull(); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullvaluemapping/NullValueMapMappingStrategyTest.java b/processor/src/test/java/org/mapstruct/ap/test/nullvaluemapping/NullValueMapMappingStrategyTest.java new file mode 100644 index 000000000..33b5942f8 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullvaluemapping/NullValueMapMappingStrategyTest.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.nullvaluemapping; + +import org.mapstruct.ap.test.nullvaluemapping._target.CarDto; +import org.mapstruct.ap.test.nullvaluemapping.source.Car; +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.compilation.annotation.ProcessorOption; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Filip Hrisafov + */ +@WithClasses({ + CarDto.class, + Car.class +}) +@IssueKey("2953") +public class NullValueMapMappingStrategyTest { + + @ProcessorTest + @ProcessorOption(name = "mapstruct.nullValueMapMappingStrategy", value = "return_default") + @WithClasses({ + CarMapMapper.class + }) + void globalNullMapMappingStrategy() { + assertThat( CarMapMapper.INSTANCE.carsToCarDtoMap( null ) ).isEmpty(); + } + + @ProcessorTest + @ProcessorOption(name = "mapstruct.nullValueMapMappingStrategy", value = "return_default") + @WithClasses({ + CarMapMapperSettingOnMapper.class + }) + void globalNullMapMappingStrategyWithOverrideInMapper() { + // Explicit definition in @Mapper should override global + assertThat( CarMapMapperSettingOnMapper.INSTANCE.carsToCarDtoMap( null ) ).isNull(); + } +}