#2668: Added support for collections and maps with a no-args constructor (#2671)

#2668: Added support for collections and maps with a no-args constructor. Added a compiler error in case of a collection or map without either a no-arg constructor or a copy constructor.
This commit is contained in:
Zegveld 2022-01-29 00:37:24 +01:00 committed by GitHub
parent b22efd9ad7
commit 9105041522
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 438 additions and 8 deletions

View File

@ -5,19 +5,27 @@
*/
package org.mapstruct.ap.internal.model;
import java.util.function.Predicate;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import org.mapstruct.ap.internal.gem.CollectionMappingStrategyGem;
import org.mapstruct.ap.internal.gem.NullValueCheckStrategyGem;
import org.mapstruct.ap.internal.gem.NullValuePropertyMappingStrategyGem;
import org.mapstruct.ap.internal.model.assignment.ExistingInstanceSetterWrapperForCollectionsAndMaps;
import org.mapstruct.ap.internal.model.assignment.GetterWrapperForCollectionsAndMaps;
import org.mapstruct.ap.internal.model.assignment.NewInstanceSetterWrapperForCollectionsAndMaps;
import org.mapstruct.ap.internal.model.assignment.SetterWrapperForCollectionsAndMaps;
import org.mapstruct.ap.internal.model.assignment.SetterWrapperForCollectionsAndMapsWithNullCheck;
import org.mapstruct.ap.internal.model.assignment.UpdateWrapper;
import org.mapstruct.ap.internal.model.common.Assignment;
import org.mapstruct.ap.internal.model.common.Assignment.AssignmentType;
import org.mapstruct.ap.internal.model.common.SourceRHS;
import org.mapstruct.ap.internal.model.common.Type;
import org.mapstruct.ap.internal.model.source.Method;
import org.mapstruct.ap.internal.model.source.SelectionParameters;
import org.mapstruct.ap.internal.gem.CollectionMappingStrategyGem;
import org.mapstruct.ap.internal.gem.NullValueCheckStrategyGem;
import org.mapstruct.ap.internal.gem.NullValuePropertyMappingStrategyGem;
import org.mapstruct.ap.internal.util.Message;
import org.mapstruct.ap.internal.util.accessor.Accessor;
import org.mapstruct.ap.internal.util.accessor.AccessorType;
@ -169,7 +177,8 @@ public class CollectionAssignmentBuilder {
targetAccessorType.isFieldAssignment()
);
}
else if ( setterWrapperNeedsSourceNullCheck( result ) ) {
else if ( setterWrapperNeedsSourceNullCheck( result )
&& canBeMappedOrDirectlyAssigned( result ) ) {
result = new SetterWrapperForCollectionsAndMapsWithNullCheck(
result,
@ -179,7 +188,7 @@ public class CollectionAssignmentBuilder {
targetAccessorType.isFieldAssignment()
);
}
else {
else if ( canBeMappedOrDirectlyAssigned( result ) ) {
//TODO init default value
// target accessor is setter, so wrap the setter in setter map/ collection handling
@ -190,6 +199,21 @@ public class CollectionAssignmentBuilder {
targetAccessorType.isFieldAssignment()
);
}
else if ( hasNoArgsConstructor() ) {
result = new NewInstanceSetterWrapperForCollectionsAndMaps(
result,
method.getThrownTypes(),
targetType,
ctx.getTypeFactory(),
targetAccessorType.isFieldAssignment() );
}
else {
ctx.getMessager().printMessage(
method.getExecutable(),
Message.PROPERTYMAPPING_NO_SUITABLE_COLLECTION_OR_MAP_CONSTRUCTOR,
targetType
);
}
}
else {
if ( targetImmutable ) {
@ -212,6 +236,12 @@ public class CollectionAssignmentBuilder {
return result;
}
private boolean canBeMappedOrDirectlyAssigned(Assignment result) {
return result.getType() != AssignmentType.DIRECT
|| hasCopyConstructor()
|| targetType.isEnumSet();
}
/**
* Checks whether the setter wrapper should include a null / presence check or not
*
@ -236,4 +266,48 @@ public class CollectionAssignmentBuilder {
return false;
}
private boolean hasCopyConstructor() {
return checkConstructorForPredicate( this::hasCopyConstructor );
}
private boolean hasNoArgsConstructor() {
return checkConstructorForPredicate( this::hasNoArgsConstructor );
}
private boolean checkConstructorForPredicate(Predicate<Element> predicate) {
if ( targetType.isCollectionOrMapType() ) {
if ( "java.util".equals( targetType.getPackageName() ) ) {
return true;
}
else {
Element sourceElement = targetType.getImplementationType() != null
? targetType.getImplementationType().getTypeElement()
: targetType.getTypeElement();
if ( sourceElement != null ) {
for ( Element element : sourceElement.getEnclosedElements() ) {
if ( element.getKind() == ElementKind.CONSTRUCTOR
&& element.getModifiers().contains( Modifier.PUBLIC ) ) {
if ( predicate.test( element ) ) {
return true;
}
}
}
}
}
}
return false;
}
private boolean hasNoArgsConstructor(Element element) {
return ( (ExecutableElement) element ).getParameters().isEmpty();
}
private boolean hasCopyConstructor(Element element) {
if ( element instanceof ExecutableElement ) {
ExecutableElement ee = (ExecutableElement) element;
return ee.getParameters().size() == 1
&& ctx.getTypeUtils().isAssignable( targetType.getTypeMirror(), ee.getParameters().get( 0 ).asType() );
}
return false;
}
}

View File

@ -0,0 +1,43 @@
/*
* 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.internal.model.assignment;
import java.util.List;
import org.mapstruct.ap.internal.model.common.Assignment;
import org.mapstruct.ap.internal.model.common.Type;
import org.mapstruct.ap.internal.model.common.TypeFactory;
/**
* This wrapper handles the situation where an assignment is done via the setter, while creating the collection or map
* using a no-args constructor.
*
* @author Ben Zegveld
*/
public class NewInstanceSetterWrapperForCollectionsAndMaps extends SetterWrapperForCollectionsAndMapsWithNullCheck {
private String instanceVar;
public NewInstanceSetterWrapperForCollectionsAndMaps(Assignment decoratedAssignment,
List<Type> thrownTypesToExclude,
Type targetType,
TypeFactory typeFactory,
boolean fieldAssignment) {
super(
decoratedAssignment,
thrownTypesToExclude,
targetType,
typeFactory,
fieldAssignment
);
this.instanceVar = decoratedAssignment.createUniqueVarName( targetType.getName() );
}
public String getInstanceVar() {
return instanceVar;
}
}

View File

@ -68,7 +68,7 @@ public class SetterWrapperForCollectionsAndMapsWithNullCheck extends WrapperForC
}
public boolean isEnumSet() {
return "java.util.EnumSet".equals( targetType.getFullyQualifiedName() );
return targetType.isEnumSet();
}
}

View File

@ -19,7 +19,6 @@ import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
@ -113,7 +112,6 @@ public class Type extends ModelElement implements Comparable<Type> {
private List<Accessor> setters = null;
private List<Accessor> adders = null;
private List<Accessor> alternativeTargetAccessors = null;
private Map<String, Accessor> constructorAccessors = null;
private Type boundingBase = null;
@ -1603,4 +1601,8 @@ public class Type extends ModelElement implements Comparable<Type> {
return parent == null ? null : typeFactory.getType( parent.asType() );
}
public boolean isEnumSet() {
return "java.util.EnumSet".equals( getFullyQualifiedName() );
}
}

View File

@ -80,6 +80,7 @@ public enum Message {
PROPERTYMAPPING_WHITESPACE_TRIMMED( "The property named \"%s\" has whitespaces, using trimmed property \"%s\" instead.", Diagnostic.Kind.WARNING ),
PROPERTYMAPPING_CANNOT_DETERMINE_SOURCE_PROPERTY_FROM_TARGET("The type of parameter \"%s\" has no property named \"%s\". Please define the source property explicitly."),
PROPERTYMAPPING_CANNOT_DETERMINE_SOURCE_PARAMETER_FROM_TARGET("No property named \"%s\" exists in source parameter(s). Please define the source explicitly."),
PROPERTYMAPPING_NO_SUITABLE_COLLECTION_OR_MAP_CONSTRUCTOR( "%s does not have an accessible copy or no-args constructor." ),
CONVERSION_LOSSY_WARNING( "%s has a possibly lossy conversion from %s to %s.", Diagnostic.Kind.WARNING ),
CONVERSION_LOSSY_ERROR( "Can't map %s. It has a possibly lossy conversion from %s to %s." ),

View File

@ -0,0 +1,23 @@
<#--
Copyright MapStruct Authors.
Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
-->
<#-- @ftlvariable name="" type="org.mapstruct.ap.internal.model.assignment.SetterWrapperForCollectionsAndMapsWithNullCheck" -->
<#import "../macro/CommonMacros.ftl" as lib>
<@lib.sourceLocalVarAssignment/>
<@lib.handleExceptions>
<@callTargetWriteAccessor/>
</@lib.handleExceptions>
<#--
assigns the target via the regular target write accessor (usually the setter)
-->
<#macro callTargetWriteAccessor>
<@lib.handleLocalVarNullCheck needs_explicit_local_var=directAssignment>
<#if ext.targetType.implementationType??><@includeModel object=ext.targetType.implementationType/><#else><@includeModel object=ext.targetType/></#if> ${instanceVar} = new <#if ext.targetType.implementationType??><@includeModel object=ext.targetType.implementationType/><#else><@includeModel object=ext.targetType/></#if>();
${instanceVar}.<#if ext.targetType.collectionType>addAll<#else>putAll</#if>( ${nullCheckLocalVarName} );
<#if ext.targetBeanName?has_content>${ext.targetBeanName}.</#if>${ext.targetWriteAccessorName}<@lib.handleWrite>${instanceVar}</@lib.handleWrite>;
</@lib.handleLocalVarNullCheck>
</#macro>

View File

@ -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.bugs._2668;
import java.util.ArrayList;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
/**
* @author Ben Zegveld
*/
@Mapper
public interface Erroneous2668ListMapper {
Erroneous2668ListMapper INSTANCE = Mappers.getMapper( Erroneous2668ListMapper.class );
CollectionTarget map(CollectionSource source);
class CollectionTarget {
MyArrayList list;
public MyArrayList getList() {
return list;
}
public void setList(MyArrayList list) {
this.list = list;
}
}
class CollectionSource {
MyArrayList list;
public MyArrayList getList() {
return list;
}
public void setList(MyArrayList list) {
this.list = list;
}
}
class MyArrayList extends ArrayList<String> {
private String unusable;
public MyArrayList(String unusable) {
this.unusable = unusable;
}
}
}

View File

@ -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.bugs._2668;
import java.util.HashMap;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
/**
* @author Ben Zegveld
*/
@Mapper
public interface Erroneous2668MapMapper {
Erroneous2668MapMapper INSTANCE = Mappers.getMapper( Erroneous2668MapMapper.class );
CollectionTarget map(CollectionSource source);
class CollectionTarget {
MyHashMap map;
public MyHashMap getMap() {
return map;
}
public void setMap(MyHashMap map) {
this.map = map;
}
}
class CollectionSource {
MyHashMap map;
public MyHashMap getMap() {
return map;
}
public void setMap(MyHashMap map) {
this.map = map;
}
}
class MyHashMap extends HashMap<String, String> {
private String unusable;
public MyHashMap(String unusable) {
this.unusable = unusable;
}
}
}

View File

@ -0,0 +1,120 @@
/*
* 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.bugs._2668;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
/**
* @author Ben Zegveld
*/
@Mapper
public interface Issue2668Mapper {
Issue2668Mapper INSTANCE = Mappers.getMapper( Issue2668Mapper.class );
CollectionTarget map(CollectionSource source);
class CollectionTarget {
MyArrayList list;
MyHashMap map;
MyCopyArrayList copyList;
MyCopyHashMap copyMap;
public MyArrayList getList() {
return list;
}
public MyHashMap getMap() {
return map;
}
public void setList(MyArrayList list) {
this.list = list;
}
public void setMap(MyHashMap map) {
this.map = map;
}
public MyCopyArrayList getCopyList() {
return copyList;
}
public MyCopyHashMap getCopyMap() {
return copyMap;
}
public void setCopyList(MyCopyArrayList copyList) {
this.copyList = copyList;
}
public void setCopyMap(MyCopyHashMap copyMap) {
this.copyMap = copyMap;
}
}
class CollectionSource {
MyArrayList list;
MyHashMap map;
MyCopyArrayList copyList;
MyCopyHashMap copyMap;
public MyArrayList getList() {
return list;
}
public MyHashMap getMap() {
return map;
}
public void setList(MyArrayList list) {
this.list = list;
}
public void setMap(MyHashMap map) {
this.map = map;
}
public MyCopyArrayList getCopyList() {
return copyList;
}
public MyCopyHashMap getCopyMap() {
return copyMap;
}
public void setCopyList(MyCopyArrayList copyList) {
this.copyList = copyList;
}
public void setCopyMap(MyCopyHashMap copyMap) {
this.copyMap = copyMap;
}
}
class MyArrayList extends ArrayList<String> {
}
class MyHashMap extends HashMap<String, String> {
}
class MyCopyArrayList extends ArrayList<String> {
public MyCopyArrayList(Collection<String> copy) {
super( copy );
}
}
class MyCopyHashMap extends HashMap<String, String> {
public MyCopyHashMap(HashMap<String, String> copy) {
super( copy );
}
}
}

View File

@ -0,0 +1,59 @@
/*
* 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.bugs._2668;
import javax.tools.Diagnostic.Kind;
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.CompilationResult;
import org.mapstruct.ap.testutil.compilation.annotation.Diagnostic;
import org.mapstruct.ap.testutil.compilation.annotation.ExpectedCompilationOutcome;
/**
* @author Ben Zegveld
*/
@IssueKey( "2668" )
class Issue2668Test {
@ProcessorTest
@WithClasses( Issue2668Mapper.class )
void shouldCompileCorrectlyWithAvailableConstructors() {
}
@ProcessorTest
@WithClasses( Erroneous2668ListMapper.class )
@ExpectedCompilationOutcome(
value = CompilationResult.FAILED,
diagnostics = {
@Diagnostic(
kind = Kind.ERROR,
line = 21,
message = "org.mapstruct.ap.test.bugs._2668.Erroneous2668ListMapper.MyArrayList"
+ " does not have an accessible copy or no-args constructor."
)
}
)
void errorExpectedBecauseCollectionIsNotUsable() {
}
@ProcessorTest
@WithClasses( Erroneous2668MapMapper.class )
@ExpectedCompilationOutcome(
value = CompilationResult.FAILED,
diagnostics = {
@Diagnostic(
kind = Kind.ERROR,
line = 21,
message = "org.mapstruct.ap.test.bugs._2668.Erroneous2668MapMapper.MyHashMap"
+ " does not have an accessible copy or no-args constructor."
)
}
)
void errorExpectedBecauseMapIsNotUsable() {
}
}