#3306 Support for map keys that contain dots when maps are mapped

This commit is contained in:
thunderhook 2023-12-12 22:56:41 +01:00
parent 6d99f7b8f3
commit 01305e7d0e
6 changed files with 139 additions and 10 deletions

View File

@ -11,6 +11,7 @@ import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.AnnotationValue;
import javax.lang.model.type.DeclaredType;
@ -152,15 +153,19 @@ public class SourceReference extends AbstractReference {
private SourceReference buildFromSingleSourceParameters(String[] segments, Parameter parameter) {
boolean foundEntryMatch;
boolean allowedMapToBean = false;
if ( segments.length > 0 ) {
if ( parameter.getType().isMapType() ) {
// When the parameter type is a map and the parameter name matches the first segment
// then the first segment should not be treated as a property of the map
allowedMapToBean = !segments[0].equals( parameter.getName() );
}
}
String[] propertyNames = segments;
boolean allowedMapToBean = false;
if ( segments.length > 0 && parameter.getType().isMapType() ) {
// When the parameter type is a map and the parameter name matches the first segment
// then the first segment should not be treated as a property of the map
boolean firstSegmentIsPathParameterName = segments[0].equals( parameter.getName() );
// do not allow map mapping to bean when the parameter name is the map itself
allowedMapToBean = !( firstSegmentIsPathParameterName && segments.length == 1 );
int segmentsToSkip = firstSegmentIsPathParameterName ? 1 : 0;
propertyNames = new String[] { joinSegmentsToDottedString( segments, segmentsToSkip ) };
}
List<PropertyEntry> entries = matchWithSourceAccessorTypes(
parameter.getType(),
propertyNames,
@ -205,6 +210,9 @@ public class SourceReference extends AbstractReference {
if ( segments.length > 1 && parameter != null ) {
propertyNames = Arrays.copyOfRange( segments, 1, segments.length );
if (parameter.getType().isMapType() ) {
propertyNames = new String[] { joinSegmentsToDottedString( segments, 1 ) };
}
entries = matchWithSourceAccessorTypes( parameter.getType(), propertyNames, true );
foundEntryMatch = ( entries.size() == propertyNames.length );
}
@ -220,6 +228,12 @@ public class SourceReference extends AbstractReference {
return new SourceReference( parameter, entries, foundEntryMatch );
}
private static String joinSegmentsToDottedString(String[] segments, int skip) {
return Arrays.stream( segments )
.skip( skip )
.collect( Collectors.joining( "." ) );
}
/**
* When there are more than one source parameters, the first segment name of the property
* needs to match the parameter name to avoid ambiguity

View File

@ -59,6 +59,7 @@ class FromMapMappingTest {
map.put( "name", "Jacket" );
map.put( "price", "25.5" );
map.put( "shipmentDate", "2021-06-15" );
map.put( "this.will.be.ignored", "..." );
StringMapToBeanMapper.Order order = StringMapToBeanMapper.INSTANCE.fromMap( map );
assertThat( order ).isNotNull();
@ -92,7 +93,7 @@ class FromMapMappingTest {
Map<String, String> map = Collections.singletonMap( "orderDate", "" );
assertThatThrownBy( () -> StringMapToBeanMapper.INSTANCE.fromMap( map ) )
.isInstanceOf( RuntimeException.class )
.getCause()
.cause()
.isInstanceOf( ParseException.class );
}
@ -116,6 +117,7 @@ class FromMapMappingTest {
map.put( "name", "Jacket" );
map.put( "price", "25.5" );
map.put( "shipmentDate", "2021-06-15" );
map.put( "this.will.be.ignored", "..." );
StringMapToBeanWithCustomPresenceCheckMapper.Order order =
StringMapToBeanWithCustomPresenceCheckMapper.INSTANCE.fromMap( map );
@ -133,6 +135,7 @@ class FromMapMappingTest {
map.put( "price", "" );
map.put( "orderDate", "" );
map.put( "shipmentDate", "" );
map.put( "this.will.be.ignored", "" );
StringMapToBeanWithCustomPresenceCheckMapper.Order order =
StringMapToBeanWithCustomPresenceCheckMapper.INSTANCE.fromMap( map );
@ -150,18 +153,21 @@ class FromMapMappingTest {
void shouldMapWithDefinedMapping() {
Map<String, Integer> sourceMap = new HashMap<>();
sourceMap.put( "number", 44 );
sourceMap.put( "number.with.dots", 55 );
MapToBeanDefinedMapper.Target target = MapToBeanDefinedMapper.INSTANCE.toTarget( sourceMap );
assertThat( target ).isNotNull();
assertThat( target.getNormalInt() ).isEqualTo( "44" );
assertThat( target.getNormalIntWithDots() ).isEqualTo( "55" );
}
@ProcessorTest
@WithClasses(MapToBeanImplicitMapper.class)
void shouldMapWithImpicitMapping() {
void shouldMapWithImplicitMapping() {
Map<String, String> sourceMap = new HashMap<>();
sourceMap.put( "name", "mapstruct" );
sourceMap.put( "name.with.dots", "will.be.ignored" );
MapToBeanImplicitMapper.Target target = MapToBeanImplicitMapper.INSTANCE.toTarget( sourceMap );
@ -174,6 +180,7 @@ class FromMapMappingTest {
void shouldMapToExistingTargetWithImplicitMapping() {
Map<String, Integer> sourceMap = new HashMap<>();
sourceMap.put( "rating", 5 );
sourceMap.put( "rating.with.dots", -1 );
MapToBeanUpdateImplicitMapper.Target existingTarget = new MapToBeanUpdateImplicitMapper.Target();
existingTarget.setRating( 4 );
@ -197,6 +204,7 @@ class FromMapMappingTest {
assertThat( target ).isNotNull();
assertThat( target.getNormalInt() ).isEqualTo( "4711" );
assertThat( target.getNormalIntWithDots() ).isEqualTo( "999" );
}
@ProcessorTest
@ -204,12 +212,14 @@ class FromMapMappingTest {
void shouldMapUsingMappingMethod() {
Map<String, Integer> sourceMap = new HashMap<>();
sourceMap.put( "number", 23 );
sourceMap.put( "number.with.dots", 45 );
MapToBeanUsingMappingMethodMapper.Target target = MapToBeanUsingMappingMethodMapper.INSTANCE
.toTarget( sourceMap );
assertThat( target ).isNotNull();
assertThat( target.getNormalInt() ).isEqualTo( "converted_23" );
assertThat( target.getNormalIntWithDots() ).isEqualTo( "converted_45" );
}
@ProcessorTest
@ -275,6 +285,39 @@ class FromMapMappingTest {
assertThat( target.getNested() ).isEqualTo( "valueFromNestedMap" );
}
@IssueKey("3066")
@Nested
@WithClasses(MapToBeanFromMapWithKeyContainingDotMapper.class)
class MapToBeanWithKeyContainingDot {
@ProcessorTest
void shouldMapToBeanFromMapWithKeyContainingDotDirect() {
Map<String, String> source = new HashMap<>();
source.put( "some.value", "value" );
MapToBeanFromMapWithKeyContainingDotMapper.Target target =
MapToBeanFromMapWithKeyContainingDotMapper.INSTANCE.toTargetDirect( source );
assertThat( target ).isNotNull();
assertThat( target.getSomeValue() ).isEqualTo( "value" );
}
@ProcessorTest
void shouldMapToBeanFromMapWithKeyContainingDotLeadingParameterName() {
Map<String, String> source = new HashMap<>();
source.put( "some.value", "value" );
MapToBeanFromMapWithKeyContainingDotMapper.Target target =
MapToBeanFromMapWithKeyContainingDotMapper.INSTANCE.toTargetWithLeadingParameterName( source );
assertThat( target ).isNotNull();
assertThat( target.getSomeValue() ).isEqualTo( "value" );
}
}
@ProcessorTest
@WithClasses(ObjectMapToBeanWithQualifierMapper.class)
void shouldUseObjectQualifiedMethod() {

View File

@ -20,11 +20,13 @@ public interface MapToBeanDefinedMapper {
MapToBeanDefinedMapper INSTANCE = Mappers.getMapper( MapToBeanDefinedMapper.class );
@Mapping(target = "normalInt", source = "number")
@Mapping(target = "normalIntWithDots", source = "number.with.dots")
Target toTarget(Map<String, Integer> source);
class Target {
private String normalInt;
private String normalIntWithDots;
public String getNormalInt() {
return normalInt;
@ -33,6 +35,14 @@ public interface MapToBeanDefinedMapper {
public void setNormalInt(String normalInt) {
this.normalInt = normalInt;
}
public String getNormalIntWithDots() {
return normalIntWithDots;
}
public void setNormalIntWithDots(String normalIntWithDots) {
this.normalIntWithDots = normalIntWithDots;
}
}
}

View File

@ -0,0 +1,42 @@
/*
* 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.frommap;
import java.util.Map;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;
/**
* @author Christian Kosmowski
*/
@Mapper
public interface MapToBeanFromMapWithKeyContainingDotMapper {
MapToBeanFromMapWithKeyContainingDotMapper INSTANCE =
Mappers.getMapper( MapToBeanFromMapWithKeyContainingDotMapper.class );
@Mapping(target = "someValue", source = "some.value")
Target toTargetDirect(Map<String, String> source);
@Mapping(target = "someValue", source = "source.some.value")
Target toTargetWithLeadingParameterName(Map<String, String> source);
class Target {
private String someValue;
public String getSomeValue() {
return someValue;
}
public void setSomeValue(String someValue) {
this.someValue = someValue;
}
}
}

View File

@ -20,6 +20,7 @@ public interface MapToBeanUsingMappingMethodMapper {
MapToBeanUsingMappingMethodMapper INSTANCE = Mappers.getMapper( MapToBeanUsingMappingMethodMapper.class );
@Mapping(target = "normalInt", source = "source.number")
@Mapping(target = "normalIntWithDots", source = "source.number.with.dots")
Target toTarget(Map<String, Integer> source);
default String mapIntegerToString( Integer input ) {
@ -29,6 +30,7 @@ public interface MapToBeanUsingMappingMethodMapper {
class Target {
private String normalInt;
private String normalIntWithDots;
public String getNormalInt() {
return normalInt;
@ -37,6 +39,14 @@ public interface MapToBeanUsingMappingMethodMapper {
public void setNormalInt(String normalInt) {
this.normalInt = normalInt;
}
public String getNormalIntWithDots() {
return normalIntWithDots;
}
public void setNormalIntWithDots(String normalIntWithDots) {
this.normalIntWithDots = normalIntWithDots;
}
}
}

View File

@ -20,11 +20,13 @@ public interface MapToBeanWithDefaultMapper {
MapToBeanWithDefaultMapper INSTANCE = Mappers.getMapper( MapToBeanWithDefaultMapper.class );
@Mapping(target = "normalInt", source = "number", defaultValue = "4711")
@Mapping(target = "normalIntWithDots", source = "number.with.dots", defaultValue = "999")
Target toTarget(Map<String, Integer> source);
class Target {
private String normalInt;
private String normalIntWithDots;
public String getNormalInt() {
return normalInt;
@ -33,6 +35,14 @@ public interface MapToBeanWithDefaultMapper {
public void setNormalInt(String normalInt) {
this.normalInt = normalInt;
}
public String getNormalIntWithDots() {
return normalIntWithDots;
}
public void setNormalIntWithDots(String normalIntWithDots) {
this.normalIntWithDots = normalIntWithDots;
}
}
}