#2636: defaultValue combined with qualified should not convert if not needed (#2637)

This commit is contained in:
Zegveld 2021-11-14 20:11:05 +01:00 committed by GitHub
parent 735a5bef6a
commit 72e6b1feb5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 259 additions and 0 deletions

View File

@ -218,6 +218,10 @@ public @interface Mapping {
* </li>
* </ol>
* <p>
* 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})
* </p>
* <p>
* 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.
* <p>
* 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.
* <p>
* Note that {@link #defaultValue()} usage will also be converted using this qualifier.
*
* @return One or more qualifier name(s)
* @see #qualifiedBy()

View File

@ -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
}
}
----
====

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -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( "#" );
}
}