mirror of
https://github.com/mapstruct/mapstruct.git
synced 2025-07-12 00:00:08 +08:00
#1126 Add new CollectionMappingStrategy TARGET_IMMUTABLE
This commit is contained in:
parent
cab7596a47
commit
e154452d53
@ -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;
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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(),
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -27,5 +27,6 @@ public enum CollectionMappingStrategyPrism {
|
||||
|
||||
ACCESSOR_ONLY,
|
||||
SETTER_PREFERRED,
|
||||
ADDER_PREFERRED;
|
||||
ADDER_PREFERRED,
|
||||
TARGET_IMMUTABLE;
|
||||
}
|
||||
|
@ -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." ),
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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 );
|
||||
}
|
@ -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 );
|
||||
}
|
@ -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() {
|
||||
}
|
||||
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user