From 31a2e1df92f2108d612dcae8aed2f55801c634d4 Mon Sep 17 00:00:00 2001 From: thunderhook <8238759+thunderhook@users.noreply.github.com> Date: Mon, 25 Dec 2023 00:22:41 +0100 Subject: [PATCH] #3306 use escape character \ for periods inside map mappings and add nested tests --- .../model/beanmapping/SourceReference.java | 49 +++++++----- .../ap/test/frommap/FromMapMappingTest.java | 74 ++++++++++++++++- .../test/frommap/MapToBeanDefinedMapper.java | 2 +- ...BeanFromMapWithKeyContainingDotMapper.java | 4 +- .../frommap/MapToBeanFromMapWithSource.java | 51 ++++++++++++ .../MapToBeanFromMapWithSourceDeepNested.java | 79 +++++++++++++++++++ .../MapToBeanUsingMappingMethodMapper.java | 2 +- .../frommap/MapToBeanWithDefaultMapper.java | 2 +- 8 files changed, 236 insertions(+), 27 deletions(-) create mode 100644 processor/src/test/java/org/mapstruct/ap/test/frommap/MapToBeanFromMapWithSource.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/frommap/MapToBeanFromMapWithSourceDeepNested.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 f4431c6e5..9594ba42e 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,7 +11,6 @@ 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; @@ -121,7 +120,7 @@ public class SourceReference extends AbstractReference { ); } - String[] segments = sourceNameTrimmed.split( "\\." ); + String[] segments = splitEscapedTextIntoSegments( sourceNameTrimmed ); // start with an invalid source reference SourceReference result = new SourceReference( null, new ArrayList<>( ), false ); @@ -153,18 +152,15 @@ public class SourceReference extends AbstractReference { private SourceReference buildFromSingleSourceParameters(String[] segments, Parameter parameter) { boolean foundEntryMatch; - 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 ) }; + 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; List entries = matchWithSourceAccessorTypes( parameter.getType(), @@ -210,9 +206,6 @@ 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 ); } @@ -228,12 +221,6 @@ 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 @@ -366,6 +353,26 @@ public class SourceReference extends AbstractReference { } } + /** + * Splits a value into segments separated by a period if it is not escaped with a leading "\" escape character. + * Also removes the escape character from the result: + * + *
+     * "a.b.c.d" -> ["a", "b", "c", "d"]
+     * "a.b\\.c.d" -> ["a", "b.c", "d"]
+     * 
+ * @param value the possibly escaped text to split into segments + * @return array containing segments without escape characters + */ + private static String[] splitEscapedTextIntoSegments(String value) { + + String[] segments = value.split( "(? source = new HashMap<>(); @@ -304,6 +304,7 @@ class FromMapMappingTest { } @ProcessorTest + @WithClasses(MapToBeanFromMapWithKeyContainingDotMapper.class) void shouldMapToBeanFromMapWithKeyContainingDotLeadingParameterName() { Map source = new HashMap<>(); @@ -316,6 +317,77 @@ class FromMapMappingTest { assertThat( target.getSomeValue() ).isEqualTo( "value" ); } + @ProcessorTest + @WithClasses(MapToBeanFromMapWithSource.class) + void shouldMapFromMapWithSource() { + Map sourceMap = new HashMap<>(); + MapToBeanFromMapWithSource.Source sourceA = new MapToBeanFromMapWithSource.Source(); + sourceA.setName( "value" ); + sourceMap.put( "sourceA", sourceA ); + sourceMap.put( "sourceB", new MapToBeanFromMapWithSource.Source() ); + + MapToBeanFromMapWithSource.Target target = MapToBeanFromMapWithSource.INSTANCE.toTarget( sourceMap ); + + assertThat( target.getTargetName() ).isEqualTo( "value" ); + } + + @ProcessorTest + @WithClasses(MapToBeanFromMapWithSource.class) + void shouldMapFromMapWithSourceWithLeadingParameterName() { + Map sourceMap = new HashMap<>(); + MapToBeanFromMapWithSource.Source sourceA = new MapToBeanFromMapWithSource.Source(); + sourceA.setName( "value" ); + sourceMap.put( "sourceA", sourceA ); + sourceMap.put( "sourceB", new MapToBeanFromMapWithSource.Source() ); + + MapToBeanFromMapWithSource.Target target = + MapToBeanFromMapWithSource.INSTANCE.toTargetWithLeadingParameterName( sourceMap ); + + assertThat( target.getTargetName() ).isEqualTo( "value" ); + } + + @ProcessorTest + @WithClasses(MapToBeanFromMapWithSourceDeepNested.class) + void shouldMapFromMapWithSourceDeepNested() { + MapToBeanFromMapWithSourceDeepNested.Source source = + new MapToBeanFromMapWithSourceDeepNested.Source(); + HashMap innerMap = new HashMap<>(); + MapToBeanFromMapWithSourceDeepNested.SourceEntry johnDoeEntry = + new MapToBeanFromMapWithSourceDeepNested.SourceEntry(); + MapToBeanFromMapWithSourceDeepNested.Person johnDoePerson = + new MapToBeanFromMapWithSourceDeepNested.Person(); + johnDoePerson.setFirstName( "John" ); + johnDoeEntry.setPerson( johnDoePerson ); + innerMap.put( "john.doe", johnDoeEntry ); + source.setInnerMap( innerMap ); + + MapToBeanFromMapWithSourceDeepNested.Target target = + MapToBeanFromMapWithSourceDeepNested.INSTANCE.toTarget( source ); + + assertThat( target.getTargetName() ).isEqualTo( "John" ); + } + + @ProcessorTest + @WithClasses(MapToBeanFromMapWithSourceDeepNested.class) + void shouldMapFromMapWithSourceDeepNestedWithLeadingParameterName() { + MapToBeanFromMapWithSourceDeepNested.Source source = + new MapToBeanFromMapWithSourceDeepNested.Source(); + HashMap innerMap = new HashMap<>(); + MapToBeanFromMapWithSourceDeepNested.SourceEntry johnDoeEntry = + new MapToBeanFromMapWithSourceDeepNested.SourceEntry(); + MapToBeanFromMapWithSourceDeepNested.Person johnDoePerson = + new MapToBeanFromMapWithSourceDeepNested.Person(); + johnDoePerson.setFirstName( "John" ); + johnDoeEntry.setPerson( johnDoePerson ); + innerMap.put( "john.doe", johnDoeEntry ); + source.setInnerMap( innerMap ); + + MapToBeanFromMapWithSourceDeepNested.Target target = + MapToBeanFromMapWithSourceDeepNested.INSTANCE.toTargetWithLeadingParameterName( source ); + + assertThat( target.getTargetName() ).isEqualTo( "John" ); + } + } @ProcessorTest 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 496ff9dbb..6584eaa5c 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,7 +20,7 @@ public interface MapToBeanDefinedMapper { MapToBeanDefinedMapper INSTANCE = Mappers.getMapper( MapToBeanDefinedMapper.class ); @Mapping(target = "normalInt", source = "number") - @Mapping(target = "normalIntWithDots", source = "number.with.dots") + @Mapping(target = "normalIntWithDots", source = "number\\.with\\.dots") Target toTarget(Map source); class Target { 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 index 8fbf492f1..562313079 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/frommap/MapToBeanFromMapWithKeyContainingDotMapper.java +++ b/processor/src/test/java/org/mapstruct/ap/test/frommap/MapToBeanFromMapWithKeyContainingDotMapper.java @@ -20,10 +20,10 @@ public interface MapToBeanFromMapWithKeyContainingDotMapper { MapToBeanFromMapWithKeyContainingDotMapper INSTANCE = Mappers.getMapper( MapToBeanFromMapWithKeyContainingDotMapper.class ); - @Mapping(target = "someValue", source = "some.value") + @Mapping(target = "someValue", source = "some\\.value") Target toTargetDirect(Map source); - @Mapping(target = "someValue", source = "source.some.value") + @Mapping(target = "someValue", source = "source.some\\.value") Target toTargetWithLeadingParameterName(Map source); class Target { diff --git a/processor/src/test/java/org/mapstruct/ap/test/frommap/MapToBeanFromMapWithSource.java b/processor/src/test/java/org/mapstruct/ap/test/frommap/MapToBeanFromMapWithSource.java new file mode 100644 index 000000000..76de2c437 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/frommap/MapToBeanFromMapWithSource.java @@ -0,0 +1,51 @@ +/* + * 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; + +@Mapper +public interface MapToBeanFromMapWithSource { + + MapToBeanFromMapWithSource INSTANCE = Mappers.getMapper( MapToBeanFromMapWithSource.class ); + + @Mapping(target = "targetName", source = "sourceA.name") + Target toTarget(Map source); + + @Mapping(target = "targetName", source = "source.sourceA.name") + Target toTargetWithLeadingParameterName(Map source); + + class Source { + + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } + + class Target { + + String targetName; + + public String getTargetName() { + return targetName; + } + + public void setTargetName(String targetName) { + this.targetName = targetName; + } + } + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/frommap/MapToBeanFromMapWithSourceDeepNested.java b/processor/src/test/java/org/mapstruct/ap/test/frommap/MapToBeanFromMapWithSourceDeepNested.java new file mode 100644 index 000000000..af9f9e9ef --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/frommap/MapToBeanFromMapWithSourceDeepNested.java @@ -0,0 +1,79 @@ +/* + * 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; + +@Mapper +public interface MapToBeanFromMapWithSourceDeepNested { + + MapToBeanFromMapWithSourceDeepNested INSTANCE = Mappers.getMapper( MapToBeanFromMapWithSourceDeepNested.class ); + + @Mapping(target = "targetName", source = "innerMap.john\\.doe.person.firstName") + Target toTarget(Source source); + + @Mapping(target = "targetName", source = "source.innerMap.john\\.doe.person.firstName") + Target toTargetWithLeadingParameterName(Source source); + + class Source { + + private Map innerMap; + + public Map getInnerMap() { + return innerMap; + } + + public void setInnerMap( + Map innerMap) { + this.innerMap = innerMap; + } + } + + class SourceEntry { + + private Person person; + + public Person getPerson() { + return person; + } + + public void setPerson(Person person) { + this.person = person; + } + + } + + class Person { + + private String firstName; + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + } + + class Target { + + String targetName; + + public String getTargetName() { + return targetName; + } + + public void setTargetName(String targetName) { + this.targetName = targetName; + } + } + +} 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 3b5dee197..c1e62f034 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,7 +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") + @Mapping(target = "normalIntWithDots", source = "source.number\\.with\\.dots") Target toTarget(Map source); default String mapIntegerToString( Integer input ) { 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 9f05abe39..24ea83088 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,7 +20,7 @@ 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") + @Mapping(target = "normalIntWithDots", source = "number\\.with\\.dots", defaultValue = "999") Target toTarget(Map source); class Target {