#1126 Add new CollectionMappingStrategy TARGET_IMMUTABLE

This commit is contained in:
sjaakd 2017-03-11 11:23:49 +01:00
parent cab7596a47
commit e154452d53
14 changed files with 312 additions and 12 deletions

View File

@ -30,7 +30,8 @@ public enum CollectionMappingStrategy {
* {@code orderDto.setOrderLines(order.getOrderLines)}.
* <p>
* If no setter is available but a getter method, this will be used, under the assumption it has been initialized:
* {@code orderDto.getOrderLines().addAll(order.getOrderLines)}.
* {@code orderDto.getOrderLines().addAll(order.getOrderLines)}. This will also be the case when using
* {@link MappingTarget} (updating existing instances).
*/
ACCESSOR_ONLY,
@ -51,5 +52,13 @@ public enum CollectionMappingStrategy {
* Identical to {@link #SETTER_PREFERRED}, only that adder methods will be preferred over setter methods, if both
* are present for a given collection-typed property.
*/
ADDER_PREFERRED;
ADDER_PREFERRED,
/**
* Identical to {@link #SETTER_PREFERRED}, however the target collection will not be cleared and accessed via
* addAll in case of updating existing bean instances, see: {@link MappingTarget}.
*
* Instead the target accessor (e.g. set) will be used on the target bean to set the collection.
*/
TARGET_IMMUTABLE;
}

View File

@ -466,7 +466,7 @@ Specifying the parameter in which the property resides is mandatory when using t
Mapping methods with several source parameters will return `null` in case all the source parameters are `null`. Otherwise the target object will be instantiated and all properties from the provided parameters will be propagated.
====
MapStruct also offers the possibility to directly refer to a source parameter.
MapStruct also offers the possibility to directly refer to a source parameter.
.Mapping method directly referring to a source parameter
====
@ -1338,7 +1338,7 @@ public Map<Long, Date> stringStringMapToLongDateMap(Map<String, String> source)
[[collection-mapping-strategies]]
=== Collection mapping strategies
MapStruct has a `CollectionMappingStrategy`, with the possible values: `ACCESSOR_ONLY`, `SETTER_PREFERRED` and `ADDER_PREFERRED`.
MapStruct has a `CollectionMappingStrategy`, with the possible values: `ACCESSOR_ONLY`, `SETTER_PREFERRED`, `ADDER_PREFERRED` and `TARGET_IMMUTABLE`.
In the table below, the dash `-` indicates a property name. Next, the trailing `s` indicates the plural form. The table explains the options and how they are apply to the presence/absense of a `set-s`, `add-` and / or `get-s` method on the target object:
@ -1366,6 +1366,13 @@ In the table below, the dash `-` indicates a property name. Next, the trailing `
|add-
|get-s
|get-s
|`TARGET_IMMUTABLE`
|set-s
|exception
|set-s
|exception
|set-s
|===
Some background: An `adder` method is typically used in case of http://www.eclipse.org/webtools/dali/[generated (JPA) entities], to add a single element (entity) to an underlying collection. Invoking the adder establishes a parent-child relation between parent - the bean (entity) on which the adder is invoked - and its child(ren), the elements (entities) in the collection. To find the appropriate `adder`, MapStruct will try to make a match between the generic parameter type of the underlying collection and the single argument of a candidate `adder`. When there are more candidates, the plural `setter` / `getter` name is converted to singular and will be used in addition to make a match.

View File

@ -52,6 +52,7 @@ import org.mapstruct.ap.internal.model.source.ParameterProvidedMethods;
import org.mapstruct.ap.internal.model.source.PropertyEntry;
import org.mapstruct.ap.internal.model.source.SelectionParameters;
import org.mapstruct.ap.internal.model.source.SourceReference;
import org.mapstruct.ap.internal.prism.CollectionMappingStrategyPrism;
import org.mapstruct.ap.internal.prism.NullValueCheckStrategyPrism;
import org.mapstruct.ap.internal.util.Executables;
import org.mapstruct.ap.internal.util.MapperConfiguration;
@ -400,10 +401,15 @@ public class PropertyMapping extends ModelElement {
Assignment result = rhs;
if ( targetAccessorType == TargetWriteAccessorType.SETTER ||
targetAccessorType == TargetWriteAccessorType.FIELD ) {
CollectionMappingStrategyPrism cms = method.getMapperConfiguration().getCollectionMappingStrategy();
boolean targetImmutable = cms == CollectionMappingStrategyPrism.TARGET_IMMUTABLE;
if ( targetAccessorType == TargetWriteAccessorType.SETTER ||
targetAccessorType == TargetWriteAccessorType.FIELD ) {
if ( result.isCallingUpdateMethod() && !targetImmutable) {
if ( result.isCallingUpdateMethod() ) {
// call to an update method
if ( targetReadAccessor == null ) {
ctx.getMessager().printMessage(
@ -423,6 +429,7 @@ public class PropertyMapping extends ModelElement {
);
}
else {
// target accessor is setter, so wrap the setter in setter map/ collection handling
result = new SetterWrapperForCollectionsAndMaps(
result,
@ -430,11 +437,20 @@ public class PropertyMapping extends ModelElement {
targetType,
method.getMapperConfiguration().getNullValueCheckStrategy(),
ctx.getTypeFactory(),
targetWriteAccessorType == TargetWriteAccessorType.FIELD
targetWriteAccessorType == TargetWriteAccessorType.FIELD,
targetImmutable
);
}
}
else {
if ( targetImmutable ) {
ctx.getMessager().printMessage(
method.getExecutable(),
Message.PROPERTYMAPPING_NO_WRITE_ACCESSOR_FOR_TARGET_TYPE,
targetPropertyName
);
}
// target accessor is getter, so wrap the setter in getter map/ collection handling
result = new GetterWrapperForCollectionsAndMaps( result,
method.getThrownTypes(),

View File

@ -47,13 +47,15 @@ public class SetterWrapperForCollectionsAndMaps extends WrapperForCollectionsAnd
private final boolean includeSourceNullCheck;
private final Type targetType;
private final TypeFactory typeFactory;
private final boolean targetImmutable;
public SetterWrapperForCollectionsAndMaps(Assignment decoratedAssignment,
List<Type> thrownTypesToExclude,
Type targetType,
NullValueCheckStrategyPrism nvms,
TypeFactory typeFactory,
boolean fieldAssignment) {
boolean fieldAssignment,
boolean targetImmutable ) {
super(
decoratedAssignment,
@ -64,6 +66,7 @@ public class SetterWrapperForCollectionsAndMaps extends WrapperForCollectionsAnd
this.includeSourceNullCheck = ALWAYS == nvms;
this.targetType = targetType;
this.typeFactory = typeFactory;
this.targetImmutable = targetImmutable;
}
@Override
@ -96,4 +99,8 @@ public class SetterWrapperForCollectionsAndMaps extends WrapperForCollectionsAnd
return "java.util.EnumSet".equals( targetType.getFullyQualifiedName() );
}
public boolean isTargetImmutable() {
return targetImmutable;
}
}

View File

@ -469,7 +469,8 @@ public class Type extends ModelElement implements Comparable<Type> {
// the current target accessor can also be a getter method.
// The following if block, checks if the target accessor should be overruled by an add method.
if ( cmStrategy == CollectionMappingStrategyPrism.SETTER_PREFERRED
|| cmStrategy == CollectionMappingStrategyPrism.ADDER_PREFERRED ) {
|| cmStrategy == CollectionMappingStrategyPrism.ADDER_PREFERRED
|| cmStrategy == CollectionMappingStrategyPrism.TARGET_IMMUTABLE ) {
// first check if there's a setter method.
Accessor adderMethod = null;

View File

@ -27,5 +27,6 @@ public enum CollectionMappingStrategyPrism {
ACCESSOR_ONLY,
SETTER_PREFERRED,
ADDER_PREFERRED;
ADDER_PREFERRED,
TARGET_IMMUTABLE;
}

View File

@ -55,6 +55,7 @@ public enum Message {
PROPERTYMAPPING_INVALID_PROPERTY_NAME( "No property named \"%s\" exists in source parameter(s)." ),
PROPERTYMAPPING_NO_PRESENCE_CHECKER_FOR_SOURCE_TYPE( "Using custom source value presence checking strategy, but no presence checker found for %s in source type." ),
PROPERTYMAPPING_NO_READ_ACCESSOR_FOR_TARGET_TYPE( "No read accessor found for property \"%s\" in target type." ),
PROPERTYMAPPING_NO_WRITE_ACCESSOR_FOR_TARGET_TYPE( "No write accessor found for property \"%s\" in target type." ),
CONSTANTMAPPING_MAPPING_NOT_FOUND( "Can't map \"%s %s\" to \"%s %s\"." ),
CONSTANTMAPPING_NO_READ_ACCESSOR_FOR_TARGET_TYPE( "No read accessor found for property \"%s\" in target type." ),

View File

@ -22,7 +22,7 @@
<#import "../macro/CommonMacros.ftl" as lib>
<@lib.sourceLocalVarAssignment/>
<@lib.handleExceptions>
<#if ext.existingInstanceMapping>
<#if ext.existingInstanceMapping && !targetImmutable>
if ( ${ext.targetBeanName}.${ext.targetReadAccessorName} != null ) {
<@lib.handleLocalVarNullCheck>
${ext.targetBeanName}.${ext.targetReadAccessorName}.clear();

View File

@ -0,0 +1,38 @@
/**
* Copyright 2012-2017 Gunnar Morling (http://www.gunnarmorling.de/)
* and/or other contributors as indicated by the @authors tag. See the
* copyright.txt file in the distribution for a full listing of all
* contributors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.mapstruct.ap.test.collection.immutabletarget;
import java.util.List;
/**
*
* @author Sjaak Derksen
*/
public class CupboardDto {
private List<String> content;
public List<String> getContent() {
return content;
}
public void setContent(List<String> content) {
this.content = content;
}
}

View File

@ -0,0 +1,38 @@
/**
* Copyright 2012-2017 Gunnar Morling (http://www.gunnarmorling.de/)
* and/or other contributors as indicated by the @authors tag. See the
* copyright.txt file in the distribution for a full listing of all
* contributors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.mapstruct.ap.test.collection.immutabletarget;
import java.util.List;
/**
*
* @author Sjaak Derksen
*/
public class CupboardEntity {
private List<String> content;
public List<String> getContent() {
return content;
}
public void setContent(List<String> content) {
this.content = content;
}
}

View File

@ -0,0 +1,35 @@
/**
* Copyright 2012-2017 Gunnar Morling (http://www.gunnarmorling.de/)
* and/or other contributors as indicated by the @authors tag. See the
* copyright.txt file in the distribution for a full listing of all
* contributors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.mapstruct.ap.test.collection.immutabletarget;
import java.util.List;
/**
*
* @author Sjaak Derksen
*/
public class CupboardEntityOnlyGetter {
private List<String> content;
public List<String> getContent() {
return content;
}
}

View File

@ -0,0 +1,36 @@
/**
* Copyright 2012-2017 Gunnar Morling (http://www.gunnarmorling.de/)
* and/or other contributors as indicated by the @authors tag. See the
* copyright.txt file in the distribution for a full listing of all
* contributors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.mapstruct.ap.test.collection.immutabletarget;
import org.mapstruct.CollectionMappingStrategy;
import org.mapstruct.Mapper;
import org.mapstruct.MappingTarget;
import org.mapstruct.factory.Mappers;
/**
*
* @author Sjaak Derksen
*/
@Mapper( collectionMappingStrategy = CollectionMappingStrategy.TARGET_IMMUTABLE )
public interface CupboardMapper {
CupboardMapper INSTANCE = Mappers.getMapper( CupboardMapper.class );
void map( CupboardDto in, @MappingTarget CupboardEntity out );
}

View File

@ -0,0 +1,36 @@
/**
* Copyright 2012-2017 Gunnar Morling (http://www.gunnarmorling.de/)
* and/or other contributors as indicated by the @authors tag. See the
* copyright.txt file in the distribution for a full listing of all
* contributors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.mapstruct.ap.test.collection.immutabletarget;
import org.mapstruct.CollectionMappingStrategy;
import org.mapstruct.Mapper;
import org.mapstruct.MappingTarget;
import org.mapstruct.factory.Mappers;
/**
*
* @author Sjaak Derksen
*/
@Mapper( collectionMappingStrategy = CollectionMappingStrategy.TARGET_IMMUTABLE )
public interface ErroneousCupboardMapper {
ErroneousCupboardMapper INSTANCE = Mappers.getMapper( ErroneousCupboardMapper.class );
void map( CupboardDto in, @MappingTarget CupboardEntityOnlyGetter out );
}

View File

@ -0,0 +1,75 @@
/**
* Copyright 2012-2017 Gunnar Morling (http://www.gunnarmorling.de/)
* and/or other contributors as indicated by the @authors tag. See the
* copyright.txt file in the distribution for a full listing of all
* contributors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.mapstruct.ap.test.collection.immutabletarget;
import static org.assertj.core.api.Assertions.assertThat;
import java.util.Arrays;
import java.util.Collections;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mapstruct.ap.testutil.IssueKey;
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;
import org.mapstruct.ap.testutil.runner.AnnotationProcessorTestRunner;
/**
*
* @author Sjaak Derksen
*/
@RunWith(AnnotationProcessorTestRunner.class)
@WithClasses({CupboardDto.class, CupboardEntity.class, CupboardMapper.class})
@IssueKey( "1126" )
public class ImmutableTargetTest {
@Test
public void shouldHandleImmutableTarget() {
CupboardDto in = new CupboardDto();
in.setContent( Arrays.asList( "cups", "soucers" ) );
CupboardEntity out = new CupboardEntity();
out.setContent( Collections.<String>emptyList() );
CupboardMapper.INSTANCE.map( in, out );
assertThat( out.getContent() ).isNotNull();
assertThat( out.getContent() ).containsExactly( "cups", "soucers" );
}
@Test
@WithClasses({
ErroneousCupboardMapper.class,
CupboardEntityOnlyGetter.class
})
@ExpectedCompilationOutcome(
value = CompilationResult.FAILED,
diagnostics = {
@Diagnostic(type = ErroneousCupboardMapper.class,
kind = javax.tools.Diagnostic.Kind.ERROR,
line = 35,
messageRegExp = "No write accessor found for property \"content\" in target type.")
}
)
public void testShouldFailOnPropertyMappingNoPropertySetterOnlyGetter() {
}
}