From 72e6b1feb56c228eae5dfb91ee0a6b6c7acc9b42 Mon Sep 17 00:00:00 2001 From: Zegveld <41897697+Zegveld@users.noreply.github.com> Date: Sun, 14 Nov 2021 20:11:05 +0100 Subject: [PATCH] #2636: defaultValue combined with qualified should not convert if not needed (#2637) --- core/src/main/java/org/mapstruct/Mapping.java | 8 +++ .../chapter-5-data-type-conversions.asciidoc | 70 +++++++++++++++++++ .../DefaultExpressionUsageMapper.java | 27 +++++++ .../defaults/DefaultValueUsageMapper.java | 27 +++++++ .../qualifier/defaults/DirectoryNode.java | 21 ++++++ .../FaultyDefaultValueUsageMapper.java | 26 +++++++ .../selection/qualifier/defaults/Folder.java | 29 ++++++++ .../defaults/QualifierWithDefaultsTest.java | 51 ++++++++++++++ 8 files changed, 259 insertions(+) create mode 100644 processor/src/test/java/org/mapstruct/ap/test/selection/qualifier/defaults/DefaultExpressionUsageMapper.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/selection/qualifier/defaults/DefaultValueUsageMapper.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/selection/qualifier/defaults/DirectoryNode.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/selection/qualifier/defaults/FaultyDefaultValueUsageMapper.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/selection/qualifier/defaults/Folder.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/selection/qualifier/defaults/QualifierWithDefaultsTest.java diff --git a/core/src/main/java/org/mapstruct/Mapping.java b/core/src/main/java/org/mapstruct/Mapping.java index 7ca637f0c..92ae29eb5 100644 --- a/core/src/main/java/org/mapstruct/Mapping.java +++ b/core/src/main/java/org/mapstruct/Mapping.java @@ -218,6 +218,10 @@ public @interface Mapping { * * *

+ * You can use {@link #qualifiedBy()} or {@link #qualifiedByName()} to force the use of a conversion method + * even when one would not apply. (e.g. {@code String} to {@code String}) + *

+ *

* This attribute can not be used together with {@link #source()}, {@link #defaultValue()}, * {@link #defaultExpression()} or {@link #expression()}. * @@ -295,6 +299,8 @@ public @interface Mapping { * A qualifier can be specified to aid the selection process of a suitable mapper. This is useful in case multiple * mapping methods (hand written or generated) qualify and thus would result in an 'Ambiguous mapping methods found' * error. A qualifier is a custom annotation and can be placed on a hand written mapper class or a method. + *

+ * Note that {@link #defaultValue()} usage will also be converted using this qualifier. * * @return the qualifiers * @see Qualifier @@ -309,6 +315,8 @@ public @interface Mapping { * Note that annotation-based qualifiers are generally preferable as they allow more easily to find references and * are safe for refactorings, but name-based qualifiers can be a less verbose alternative when requiring a large * number of qualifiers as no custom annotation types are needed. + *

+ * Note that {@link #defaultValue()} usage will also be converted using this qualifier. * * @return One or more qualifier name(s) * @see #qualifiedBy() diff --git a/documentation/src/main/asciidoc/chapter-5-data-type-conversions.asciidoc b/documentation/src/main/asciidoc/chapter-5-data-type-conversions.asciidoc index 91f4afb1c..e456af01b 100644 --- a/documentation/src/main/asciidoc/chapter-5-data-type-conversions.asciidoc +++ b/documentation/src/main/asciidoc/chapter-5-data-type-conversions.asciidoc @@ -703,3 +703,73 @@ public interface MovieMapper { ==== Although the used mechanism is the same, the user has to be a bit more careful. Refactoring the name of a defined qualifier in an IDE will neatly refactor all other occurrences as well. This is obviously not the case for changing a name. ==== + +=== Combining qualifiers with defaults +Please note that the `Mapping#defaultValue` is in essence a `String`, which needs to be converted to the `Mapping#target`. Providing a `Mapping#qualifiedByName` or `Mapping#qualifiedBy` will force MapStruct to use that method. If you want different behavior for the `Mapping#defaultValue`, then please provide an appropriate mapping method. This mapping method needs to transforms a `String` into the desired type of `Mapping#target` and also be annotated so that it can be found by the `Mapping#qualifiedByName` or `Mapping#qualifiedBy`. + +.Mapper using defaultValue +==== +[source, java, linenums] +[subs="verbatim,attributes"] +---- +@Mapper +public interface MovieMapper { + + @Mapping( target = "category", qualifiedByName = "CategoryToString", defaultValue = "DEFAULT" ) + GermanRelease toGerman( OriginalRelease movies ); + + @Named("CategoryToString") + default String defaultValueForQualifier(Category cat) { + // some mapping logic + } +} +---- +==== + +In the above example in case that category is null, the method `CategoryToString( Enum.valueOf( Category.class, "DEFAULT" ) )` will be called and the result will be set to the category field. + +.Mapper using defaultValue and default method. +==== +[source, java, linenums] +[subs="verbatim,attributes"] +---- +@Mapper +public interface MovieMapper { + + @Mapping( target = "category", qualifiedByName = "CategoryToString", defaultValue = "Unknown" ) + GermanRelease toGerman( OriginalRelease movies ); + + @Named("CategoryToString") + default String defaultValueForQualifier(Category cat) { + // some mapping logic + } + + @Named("CategoryToString") + default String defaultValueForQualifier(String value) { + return value; + } +} +---- +==== +In the above example in case that category is null, the method `defaultValueForQualifier( "Unknown" )` will be called and the result will be set to the category field. + +If the above mentioned methods do not work there is the option to use `defaultExpression` to set the default value. + +.Mapper using defaultExpression +==== +[source, java, linenums] +[subs="verbatim,attributes"] +---- +@Mapper +public interface MovieMapper { + + @Mapping( target = "category", qualifiedByName = "CategoryToString", defaultExpression = "java(\"Unknown\")" ) + GermanRelease toGerman( OriginalRelease movies ); + + @Named("CategoryToString") + default String defaultValueForQualifier(Category cat) { + // some mapping logic + } +} +---- +==== diff --git a/processor/src/test/java/org/mapstruct/ap/test/selection/qualifier/defaults/DefaultExpressionUsageMapper.java b/processor/src/test/java/org/mapstruct/ap/test/selection/qualifier/defaults/DefaultExpressionUsageMapper.java new file mode 100644 index 000000000..69c025983 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/selection/qualifier/defaults/DefaultExpressionUsageMapper.java @@ -0,0 +1,27 @@ +/* + * 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.selection.qualifier.defaults; + +import java.util.UUID; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.Named; + +/** + * @author Ben Zegveld + */ +@Mapper +public interface DefaultExpressionUsageMapper { + @Mapping( source = "folder.ancestor.id", target = "parent", + defaultExpression = "java(\"#\")", qualifiedByName = "uuidToString" ) + DirectoryNode convert(Folder folder); + + @Named( "uuidToString" ) + default String uuidToString(UUID id) { + return id.toString(); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/selection/qualifier/defaults/DefaultValueUsageMapper.java b/processor/src/test/java/org/mapstruct/ap/test/selection/qualifier/defaults/DefaultValueUsageMapper.java new file mode 100644 index 000000000..622cd9193 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/selection/qualifier/defaults/DefaultValueUsageMapper.java @@ -0,0 +1,27 @@ +/* + * 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.selection.qualifier.defaults; + +import java.util.UUID; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.Named; + +/** + * @author Ben Zegveld + */ +@Mapper +public interface DefaultValueUsageMapper { + @Mapping( source = "folder.ancestor.id", target = "parent", + defaultValue = "00000000-0000-4000-0000-000000000000", qualifiedByName = "uuidToString" ) + DirectoryNode convert(Folder folder); + + @Named( "uuidToString" ) + default String uuidToString(UUID id) { + return id.toString(); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/selection/qualifier/defaults/DirectoryNode.java b/processor/src/test/java/org/mapstruct/ap/test/selection/qualifier/defaults/DirectoryNode.java new file mode 100644 index 000000000..5f52f0727 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/selection/qualifier/defaults/DirectoryNode.java @@ -0,0 +1,21 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.selection.qualifier.defaults; + +/** + * @author Ben Zegveld + */ +class DirectoryNode { + private String parent; + + public void setParent(String parent) { + this.parent = parent; + } + + public String getParent() { + return parent; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/selection/qualifier/defaults/FaultyDefaultValueUsageMapper.java b/processor/src/test/java/org/mapstruct/ap/test/selection/qualifier/defaults/FaultyDefaultValueUsageMapper.java new file mode 100644 index 000000000..e394536da --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/selection/qualifier/defaults/FaultyDefaultValueUsageMapper.java @@ -0,0 +1,26 @@ +/* + * 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.selection.qualifier.defaults; + +import java.util.UUID; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.Named; + +/** + * @author Ben Zegveld + */ +@Mapper +public interface FaultyDefaultValueUsageMapper { + @Mapping( source = "folder.ancestor.id", target = "parent", defaultValue = "#", qualifiedByName = "uuidToString" ) + DirectoryNode convert(Folder folder); + + @Named( "uuidToString" ) + default String uuidToString(UUID id) { + return id.toString(); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/selection/qualifier/defaults/Folder.java b/processor/src/test/java/org/mapstruct/ap/test/selection/qualifier/defaults/Folder.java new file mode 100644 index 000000000..a6e7b5a9b --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/selection/qualifier/defaults/Folder.java @@ -0,0 +1,29 @@ +/* + * 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.selection.qualifier.defaults; + +import java.util.UUID; + +/** + * @author Ben Zegveld + */ +class Folder { + private UUID id; + private Folder ancestor; + + Folder(UUID id, Folder ancestor) { + this.id = id; + this.ancestor = ancestor; + } + + public Folder getAncestor() { + return ancestor; + } + + public UUID getId() { + return id; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/selection/qualifier/defaults/QualifierWithDefaultsTest.java b/processor/src/test/java/org/mapstruct/ap/test/selection/qualifier/defaults/QualifierWithDefaultsTest.java new file mode 100644 index 000000000..63af9911f --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/selection/qualifier/defaults/QualifierWithDefaultsTest.java @@ -0,0 +1,51 @@ +/* + * 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.selection.qualifier.defaults; + +import java.util.UUID; +import org.assertj.core.api.Assertions; + +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.factory.Mappers; + +/** + * @author Ben Zegveld + */ +@WithClasses( { DirectoryNode.class, Folder.class } ) +public class QualifierWithDefaultsTest { + + @ProcessorTest + @WithClasses( FaultyDefaultValueUsageMapper.class ) + void defaultValueHasInvalidValue() { + Folder rootFolder = new Folder( UUID.randomUUID(), null ); + FaultyDefaultValueUsageMapper faultyMapper = Mappers.getMapper( FaultyDefaultValueUsageMapper.class ); + + Assertions + .assertThatThrownBy( () -> faultyMapper.convert( rootFolder ) ) + .isInstanceOf( IllegalArgumentException.class ); // UUID.valueOf should throw this. + } + + @ProcessorTest + @WithClasses( DefaultValueUsageMapper.class ) + void defaultValuehasUsableValue() { + Folder rootFolder = new Folder( UUID.randomUUID(), null ); + + DirectoryNode node = Mappers.getMapper( DefaultValueUsageMapper.class ).convert( rootFolder ); + + Assertions.assertThat( node.getParent() ).isEqualTo( "00000000-0000-4000-0000-000000000000" ); + } + + @ProcessorTest + @WithClasses( DefaultExpressionUsageMapper.class ) + void defaultExpressionDoesNotGetConverted() { + Folder rootFolder = new Folder( UUID.randomUUID(), null ); + + DirectoryNode node = Mappers.getMapper( DefaultExpressionUsageMapper.class ).convert( rootFolder ); + + Assertions.assertThat( node.getParent() ).isEqualTo( "#" ); + } +}