#2150 Change the rules for how a constructor for mapping is picked

New rules:

1. Constructor annotated with @Default (from any package) has highest precedence
2. If there is a single public constructor then it would be used to construct the object
3. If a parameterless constructor exists then it would be used to construct the object, and the other constructors will be ignored
This commit is contained in:
Filip Hrisafov 2020-07-18 18:53:32 +02:00 committed by GitHub
parent 6aa39ff428
commit cb432fa61b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 334 additions and 29 deletions

View File

@ -537,8 +537,52 @@ When doing a mapping MapStruct checks if there is a builder for the type being m
If there is no builder, then MapStruct looks for a single accessible constructor.
When there are multiple constructors then the following is done to pick the one which should be used:
* If a parameterless constructor exists then it would be used to construct the object, and the other constructors will be ignored
* If there are multiple constructors then the one annotated with annotation named `@Default` (from any package) will be used
* If a constructor is annotated with an annotation named `@Default` (from any package) it will be used.
* If a single public constructor exists then it will be used to construct the object, and the other non public constructors will be ignored.
* If a parameterless constructor exists then it will be used to construct the object, and the other constructors will be ignored.
* If there are multiple eligible constructors then there will be a compilation error due to ambigious constructors. In order to break the ambiquity an annotation named `@Default` (from any package) can used.
.Deciding which constructor to use
====
[source, java, linenums]
[subs="verbatim,attributes"]
----
public class Vehicle {
protected Vehicle() { }
// MapStruct will use this constructor, because it is a single public constructor
public Vehicle(String color) { }
}
public class Car {
// MapStruct will use this constructor, because it is a parameterless empty constructor
public Car() { }
public Car(String make, String color) { }
}
public class Truck {
public Truck() { }
// MapStruct will use this constructor, because it is annotated with @Default
@Default
public Truck(String make, String color) { }
}
public class Van {
// There will be a compilation error when using this class because MapStruct cannot pick a constructor
public Van(String make) { }
public Van(String make, String color) { }
}
----
====
When using a constructor then the names of the parameters of the constructor will be used and matched to the target properties.
When the constructor has an annotation named `@ConstructorProperties` (from any package) then this annotation will be used to get the names of the parameters.

View File

@ -603,23 +603,57 @@ public class BeanMappingMethod extends NormalTypeMappingMethod {
List<ExecutableElement> constructors = ElementFilter.constructorsIn( type.getTypeElement()
.getEnclosedElements() );
ExecutableElement defaultConstructor = null;
// The rules for picking a constructor are the following:
// 1. Constructor annotated with @Default (from any package) has highest precedence
// 2. If there is a single public constructor then it would be used to construct the object
// 3. If a parameterless constructor exists then it would be used to construct the object, and the other
// constructors will be ignored
ExecutableElement defaultAnnotatedConstructor = null;
ExecutableElement parameterLessConstructor = null;
List<ExecutableElement> accessibleConstructors = new ArrayList<>( constructors.size() );
List<ExecutableElement> publicConstructors = new ArrayList<>( );
for ( ExecutableElement constructor : constructors ) {
if ( constructor.getModifiers().contains( Modifier.PRIVATE ) ) {
continue;
}
if ( hasDefaultAnnotationFromAnyPackage( constructor ) ) {
// We found a constructor annotated with @Default everything else is irrelevant
defaultAnnotatedConstructor = constructor;
break;
}
if ( constructor.getParameters().isEmpty() ) {
defaultConstructor = constructor;
parameterLessConstructor = constructor;
}
else {
accessibleConstructors.add( constructor );
}
if ( constructor.getModifiers().contains( Modifier.PUBLIC ) ) {
publicConstructors.add( constructor );
}
}
if ( defaultConstructor != null ) {
if ( defaultAnnotatedConstructor != null ) {
// If a default annotated constructor exists it will be used, it has highest precedence
return getConstructorAccessor( type, defaultAnnotatedConstructor );
}
if ( publicConstructors.size() == 1 ) {
// If there is a single public constructor then use that one
ExecutableElement publicConstructor = publicConstructors.get( 0 );
if ( publicConstructor.getParameters().isEmpty() ) {
// The public parameterless constructor
return null;
}
return getConstructorAccessor( type, publicConstructor );
}
if ( parameterLessConstructor != null ) {
// If there is a constructor without parameters use it
return null;
}
@ -627,39 +661,26 @@ public class BeanMappingMethod extends NormalTypeMappingMethod {
return null;
}
ExecutableElement constructor = null;
if ( accessibleConstructors.size() > 1 ) {
for ( ExecutableElement accessibleConstructor : accessibleConstructors ) {
for ( AnnotationMirror annotationMirror : accessibleConstructor.getAnnotationMirrors() ) {
if ( annotationMirror.getAnnotationType()
.asElement()
.getSimpleName()
.contentEquals( "Default" ) ) {
constructor = accessibleConstructor;
break;
}
}
}
if ( constructor == null ) {
ctx.getMessager().printMessage(
method.getExecutable(),
GENERAL_AMBIGIOUS_CONSTRUCTORS,
type,
Strings.join( constructors, ", " )
);
return null;
}
ctx.getMessager().printMessage(
method.getExecutable(),
GENERAL_AMBIGIOUS_CONSTRUCTORS,
type,
Strings.join( constructors, ", " )
);
return null;
}
else {
constructor = accessibleConstructors.get( 0 );
return getConstructorAccessor( type, accessibleConstructors.get( 0 ) );
}
}
private ConstructorAccessor getConstructorAccessor(Type type, ExecutableElement constructor) {
List<Parameter> constructorParameters = ctx.getTypeFactory()
.getParameters( (DeclaredType) type.getTypeMirror(), constructor );
List<String> constructorProperties = null;
for ( AnnotationMirror annotationMirror : constructor.getAnnotationMirrors() ) {
if ( annotationMirror.getAnnotationType()
@ -736,6 +757,19 @@ public class BeanMappingMethod extends NormalTypeMappingMethod {
return new ParameterElementAccessor( element, safeParameterName );
}
private boolean hasDefaultAnnotationFromAnyPackage(Element element) {
for ( AnnotationMirror annotationMirror : element.getAnnotationMirrors() ) {
if ( annotationMirror.getAnnotationType()
.asElement()
.getSimpleName()
.contentEquals( "Default" ) ) {
return true;
}
}
return false;
}
private List<String> getArrayValues(AnnotationValue av) {
if ( av.getValue() instanceof List ) {

View File

@ -0,0 +1,79 @@
/*
* 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.constructor.visibility;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mapstruct.ap.test.constructor.Default;
import org.mapstruct.ap.test.constructor.PersonDto;
import org.mapstruct.ap.testutil.IssueKey;
import org.mapstruct.ap.testutil.WithClasses;
import org.mapstruct.ap.testutil.runner.AnnotationProcessorTestRunner;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Filip Hrisafov
*/
@IssueKey("2150")
@RunWith(AnnotationProcessorTestRunner.class)
@WithClasses({
PersonDto.class,
Default.class,
})
public class ConstructorVisibilityTest {
@Test
@WithClasses({
SimpleWithPublicConstructorMapper.class
})
public void shouldUseSinglePublicConstructorAlways() {
PersonDto source = new PersonDto();
source.setName( "Bob" );
source.setAge( 30 );
SimpleWithPublicConstructorMapper.Person target =
SimpleWithPublicConstructorMapper.INSTANCE.map( source );
assertThat( target ).isNotNull();
assertThat( target.getName() ).isEqualTo( "Bob" );
assertThat( target.getAge() ).isEqualTo( 30 );
}
@Test
@WithClasses({
SimpleWithPublicParameterlessConstructorMapper.class
})
public void shouldUsePublicParameterConstructorIfPresent() {
PersonDto source = new PersonDto();
source.setName( "Bob" );
source.setAge( 30 );
SimpleWithPublicParameterlessConstructorMapper.Person target =
SimpleWithPublicParameterlessConstructorMapper.INSTANCE.map( source );
assertThat( target ).isNotNull();
assertThat( target.getName() ).isEqualTo( "From Constructor" );
assertThat( target.getAge() ).isEqualTo( -1 );
}
@Test
@WithClasses({
SimpleWithPublicParameterlessConstructorAndDefaultAnnotatedMapper.class
})
public void shouldUseDefaultAnnotatedConstructorAlways() {
PersonDto source = new PersonDto();
source.setName( "Bob" );
source.setAge( 30 );
SimpleWithPublicParameterlessConstructorAndDefaultAnnotatedMapper.Person target =
SimpleWithPublicParameterlessConstructorAndDefaultAnnotatedMapper.INSTANCE.map( source );
assertThat( target ).isNotNull();
assertThat( target.getName() ).isEqualTo( "Bob" );
assertThat( target.getAge() ).isEqualTo( 30 );
}
}

View File

@ -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.test.constructor.visibility;
import org.mapstruct.Mapper;
import org.mapstruct.ap.test.constructor.PersonDto;
import org.mapstruct.factory.Mappers;
/**
* @author Filip Hrisafov
*/
@Mapper
public interface SimpleWithPublicConstructorMapper {
SimpleWithPublicConstructorMapper INSTANCE = Mappers.getMapper( SimpleWithPublicConstructorMapper.class );
Person map(PersonDto dto);
class Person {
private final String name;
private final int age;
protected Person() {
this( "From Constructor", -1 );
}
protected Person(String name) {
this( name, -1 );
}
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
}
}

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.constructor.visibility;
import org.mapstruct.Mapper;
import org.mapstruct.ap.test.constructor.Default;
import org.mapstruct.ap.test.constructor.PersonDto;
import org.mapstruct.factory.Mappers;
/**
* @author Filip Hrisafov
*/
@Mapper
public interface SimpleWithPublicParameterlessConstructorAndDefaultAnnotatedMapper {
SimpleWithPublicParameterlessConstructorAndDefaultAnnotatedMapper INSTANCE = Mappers.getMapper(
SimpleWithPublicParameterlessConstructorAndDefaultAnnotatedMapper.class );
Person map(PersonDto dto);
class Person {
private final String name;
private final int age;
protected Person() {
this( "From Constructor", -1 );
}
protected Person(String name) {
this( name, -1 );
}
@Default
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
}
}

View File

@ -0,0 +1,49 @@
/*
* 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.constructor.visibility;
import org.mapstruct.Mapper;
import org.mapstruct.ap.test.constructor.PersonDto;
import org.mapstruct.factory.Mappers;
/**
* @author Filip Hrisafov
*/
@Mapper
public interface SimpleWithPublicParameterlessConstructorMapper {
SimpleWithPublicParameterlessConstructorMapper INSTANCE = Mappers.getMapper(
SimpleWithPublicParameterlessConstructorMapper.class );
Person map(PersonDto dto);
class Person {
private final String name;
private final int age;
public Person() {
this( "From Constructor", -1 );
}
protected Person(String name) {
this( name, -1 );
}
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
}
}