From 01305e7d0ec181d410116f4277d7d2259d35af9a Mon Sep 17 00:00:00 2001 From: thunderhook <8238759+thunderhook@users.noreply.github.com> Date: Tue, 12 Dec 2023 22:56:41 +0100 Subject: [PATCH] #3306 Support for map keys that contain dots when maps are mapped --- .../model/beanmapping/SourceReference.java | 30 ++++++++---- .../ap/test/frommap/FromMapMappingTest.java | 47 ++++++++++++++++++- .../test/frommap/MapToBeanDefinedMapper.java | 10 ++++ ...BeanFromMapWithKeyContainingDotMapper.java | 42 +++++++++++++++++ .../MapToBeanUsingMappingMethodMapper.java | 10 ++++ .../frommap/MapToBeanWithDefaultMapper.java | 10 ++++ 6 files changed, 139 insertions(+), 10 deletions(-) create mode 100644 processor/src/test/java/org/mapstruct/ap/test/frommap/MapToBeanFromMapWithKeyContainingDotMapper.java diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/beanmapping/SourceReference.java b/processor/src/main/java/org/mapstruct/ap/internal/model/beanmapping/SourceReference.java index 73ec84b6b..f4431c6e5 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/beanmapping/SourceReference.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/beanmapping/SourceReference.java @@ -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 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 diff --git a/processor/src/test/java/org/mapstruct/ap/test/frommap/FromMapMappingTest.java b/processor/src/test/java/org/mapstruct/ap/test/frommap/FromMapMappingTest.java index bcb78d8dc..870630572 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/frommap/FromMapMappingTest.java +++ b/processor/src/test/java/org/mapstruct/ap/test/frommap/FromMapMappingTest.java @@ -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 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 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 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 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 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 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 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() { diff --git a/processor/src/test/java/org/mapstruct/ap/test/frommap/MapToBeanDefinedMapper.java b/processor/src/test/java/org/mapstruct/ap/test/frommap/MapToBeanDefinedMapper.java index eef8c285b..496ff9dbb 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/frommap/MapToBeanDefinedMapper.java +++ b/processor/src/test/java/org/mapstruct/ap/test/frommap/MapToBeanDefinedMapper.java @@ -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 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; + } } } diff --git a/processor/src/test/java/org/mapstruct/ap/test/frommap/MapToBeanFromMapWithKeyContainingDotMapper.java b/processor/src/test/java/org/mapstruct/ap/test/frommap/MapToBeanFromMapWithKeyContainingDotMapper.java new file mode 100644 index 000000000..8fbf492f1 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/frommap/MapToBeanFromMapWithKeyContainingDotMapper.java @@ -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 source); + + @Mapping(target = "someValue", source = "source.some.value") + Target toTargetWithLeadingParameterName(Map source); + + class Target { + + private String someValue; + + public String getSomeValue() { + return someValue; + } + + public void setSomeValue(String someValue) { + this.someValue = someValue; + } + } + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/frommap/MapToBeanUsingMappingMethodMapper.java b/processor/src/test/java/org/mapstruct/ap/test/frommap/MapToBeanUsingMappingMethodMapper.java index 225ca00ad..3b5dee197 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/frommap/MapToBeanUsingMappingMethodMapper.java +++ b/processor/src/test/java/org/mapstruct/ap/test/frommap/MapToBeanUsingMappingMethodMapper.java @@ -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 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; + } } } diff --git a/processor/src/test/java/org/mapstruct/ap/test/frommap/MapToBeanWithDefaultMapper.java b/processor/src/test/java/org/mapstruct/ap/test/frommap/MapToBeanWithDefaultMapper.java index 104d34bd0..9f05abe39 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/frommap/MapToBeanWithDefaultMapper.java +++ b/processor/src/test/java/org/mapstruct/ap/test/frommap/MapToBeanWithDefaultMapper.java @@ -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 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; + } } }