mirror of
https://github.com/mapstruct/mapstruct.git
synced 2025-07-12 00:00:08 +08:00
#3306 use escape character \ for periods inside map mappings and add nested tests
This commit is contained in:
parent
01305e7d0e
commit
31a2e1df92
@ -11,7 +11,6 @@ import java.util.List;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.stream.Collectors;
|
|
||||||
import javax.lang.model.element.AnnotationMirror;
|
import javax.lang.model.element.AnnotationMirror;
|
||||||
import javax.lang.model.element.AnnotationValue;
|
import javax.lang.model.element.AnnotationValue;
|
||||||
import javax.lang.model.type.DeclaredType;
|
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
|
// start with an invalid source reference
|
||||||
SourceReference result = new SourceReference( null, new ArrayList<>( ), false );
|
SourceReference result = new SourceReference( null, new ArrayList<>( ), false );
|
||||||
@ -153,18 +152,15 @@ public class SourceReference extends AbstractReference {
|
|||||||
private SourceReference buildFromSingleSourceParameters(String[] segments, Parameter parameter) {
|
private SourceReference buildFromSingleSourceParameters(String[] segments, Parameter parameter) {
|
||||||
boolean foundEntryMatch;
|
boolean foundEntryMatch;
|
||||||
|
|
||||||
String[] propertyNames = segments;
|
|
||||||
boolean allowedMapToBean = false;
|
boolean allowedMapToBean = false;
|
||||||
if ( segments.length > 0 && parameter.getType().isMapType() ) {
|
if ( segments.length > 0 ) {
|
||||||
|
if ( parameter.getType().isMapType() ) {
|
||||||
// When the parameter type is a map and the parameter name matches the first segment
|
// 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
|
// then the first segment should not be treated as a property of the map
|
||||||
boolean firstSegmentIsPathParameterName = segments[0].equals( parameter.getName() );
|
allowedMapToBean = !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 ) };
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
String[] propertyNames = segments;
|
||||||
|
|
||||||
List<PropertyEntry> entries = matchWithSourceAccessorTypes(
|
List<PropertyEntry> entries = matchWithSourceAccessorTypes(
|
||||||
parameter.getType(),
|
parameter.getType(),
|
||||||
@ -210,9 +206,6 @@ public class SourceReference extends AbstractReference {
|
|||||||
|
|
||||||
if ( segments.length > 1 && parameter != null ) {
|
if ( segments.length > 1 && parameter != null ) {
|
||||||
propertyNames = Arrays.copyOfRange( segments, 1, segments.length );
|
propertyNames = Arrays.copyOfRange( segments, 1, segments.length );
|
||||||
if (parameter.getType().isMapType() ) {
|
|
||||||
propertyNames = new String[] { joinSegmentsToDottedString( segments, 1 ) };
|
|
||||||
}
|
|
||||||
entries = matchWithSourceAccessorTypes( parameter.getType(), propertyNames, true );
|
entries = matchWithSourceAccessorTypes( parameter.getType(), propertyNames, true );
|
||||||
foundEntryMatch = ( entries.size() == propertyNames.length );
|
foundEntryMatch = ( entries.size() == propertyNames.length );
|
||||||
}
|
}
|
||||||
@ -228,12 +221,6 @@ public class SourceReference extends AbstractReference {
|
|||||||
return new SourceReference( parameter, entries, foundEntryMatch );
|
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
|
* When there are more than one source parameters, the first segment name of the property
|
||||||
* needs to match the parameter name to avoid ambiguity
|
* 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:
|
||||||
|
*
|
||||||
|
* <pre>
|
||||||
|
* "a.b.c.d" -> ["a", "b", "c", "d"]
|
||||||
|
* "a.b\\.c.d" -> ["a", "b.c", "d"]
|
||||||
|
* </pre>
|
||||||
|
* @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( "(?<!\\\\)\\." );
|
||||||
|
for ( int i = 0; i < segments.length; i++ ) {
|
||||||
|
segments[i] = segments[i].replaceAll( "\\\\", "" );
|
||||||
|
}
|
||||||
|
return segments;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builds a {@link SourceReference} from a property.
|
* Builds a {@link SourceReference} from a property.
|
||||||
*/
|
*/
|
||||||
|
@ -287,10 +287,10 @@ class FromMapMappingTest {
|
|||||||
|
|
||||||
@IssueKey("3066")
|
@IssueKey("3066")
|
||||||
@Nested
|
@Nested
|
||||||
@WithClasses(MapToBeanFromMapWithKeyContainingDotMapper.class)
|
|
||||||
class MapToBeanWithKeyContainingDot {
|
class MapToBeanWithKeyContainingDot {
|
||||||
|
|
||||||
@ProcessorTest
|
@ProcessorTest
|
||||||
|
@WithClasses(MapToBeanFromMapWithKeyContainingDotMapper.class)
|
||||||
void shouldMapToBeanFromMapWithKeyContainingDotDirect() {
|
void shouldMapToBeanFromMapWithKeyContainingDotDirect() {
|
||||||
|
|
||||||
Map<String, String> source = new HashMap<>();
|
Map<String, String> source = new HashMap<>();
|
||||||
@ -304,6 +304,7 @@ class FromMapMappingTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@ProcessorTest
|
@ProcessorTest
|
||||||
|
@WithClasses(MapToBeanFromMapWithKeyContainingDotMapper.class)
|
||||||
void shouldMapToBeanFromMapWithKeyContainingDotLeadingParameterName() {
|
void shouldMapToBeanFromMapWithKeyContainingDotLeadingParameterName() {
|
||||||
|
|
||||||
Map<String, String> source = new HashMap<>();
|
Map<String, String> source = new HashMap<>();
|
||||||
@ -316,6 +317,77 @@ class FromMapMappingTest {
|
|||||||
assertThat( target.getSomeValue() ).isEqualTo( "value" );
|
assertThat( target.getSomeValue() ).isEqualTo( "value" );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ProcessorTest
|
||||||
|
@WithClasses(MapToBeanFromMapWithSource.class)
|
||||||
|
void shouldMapFromMapWithSource() {
|
||||||
|
Map<String, MapToBeanFromMapWithSource.Source> 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<String, MapToBeanFromMapWithSource.Source> 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<String, MapToBeanFromMapWithSourceDeepNested.SourceEntry> 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<String, MapToBeanFromMapWithSourceDeepNested.SourceEntry> 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
|
@ProcessorTest
|
||||||
|
@ -20,7 +20,7 @@ public interface MapToBeanDefinedMapper {
|
|||||||
MapToBeanDefinedMapper INSTANCE = Mappers.getMapper( MapToBeanDefinedMapper.class );
|
MapToBeanDefinedMapper INSTANCE = Mappers.getMapper( MapToBeanDefinedMapper.class );
|
||||||
|
|
||||||
@Mapping(target = "normalInt", source = "number")
|
@Mapping(target = "normalInt", source = "number")
|
||||||
@Mapping(target = "normalIntWithDots", source = "number.with.dots")
|
@Mapping(target = "normalIntWithDots", source = "number\\.with\\.dots")
|
||||||
Target toTarget(Map<String, Integer> source);
|
Target toTarget(Map<String, Integer> source);
|
||||||
|
|
||||||
class Target {
|
class Target {
|
||||||
|
@ -20,10 +20,10 @@ public interface MapToBeanFromMapWithKeyContainingDotMapper {
|
|||||||
MapToBeanFromMapWithKeyContainingDotMapper INSTANCE =
|
MapToBeanFromMapWithKeyContainingDotMapper INSTANCE =
|
||||||
Mappers.getMapper( MapToBeanFromMapWithKeyContainingDotMapper.class );
|
Mappers.getMapper( MapToBeanFromMapWithKeyContainingDotMapper.class );
|
||||||
|
|
||||||
@Mapping(target = "someValue", source = "some.value")
|
@Mapping(target = "someValue", source = "some\\.value")
|
||||||
Target toTargetDirect(Map<String, String> source);
|
Target toTargetDirect(Map<String, String> source);
|
||||||
|
|
||||||
@Mapping(target = "someValue", source = "source.some.value")
|
@Mapping(target = "someValue", source = "source.some\\.value")
|
||||||
Target toTargetWithLeadingParameterName(Map<String, String> source);
|
Target toTargetWithLeadingParameterName(Map<String, String> source);
|
||||||
|
|
||||||
class Target {
|
class Target {
|
||||||
|
@ -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<String, Source> source);
|
||||||
|
|
||||||
|
@Mapping(target = "targetName", source = "source.sourceA.name")
|
||||||
|
Target toTargetWithLeadingParameterName(Map<String, Source> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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<String, SourceEntry> innerMap;
|
||||||
|
|
||||||
|
public Map<String, SourceEntry> getInnerMap() {
|
||||||
|
return innerMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setInnerMap(
|
||||||
|
Map<String, SourceEntry> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -20,7 +20,7 @@ public interface MapToBeanUsingMappingMethodMapper {
|
|||||||
MapToBeanUsingMappingMethodMapper INSTANCE = Mappers.getMapper( MapToBeanUsingMappingMethodMapper.class );
|
MapToBeanUsingMappingMethodMapper INSTANCE = Mappers.getMapper( MapToBeanUsingMappingMethodMapper.class );
|
||||||
|
|
||||||
@Mapping(target = "normalInt", source = "source.number")
|
@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<String, Integer> source);
|
Target toTarget(Map<String, Integer> source);
|
||||||
|
|
||||||
default String mapIntegerToString( Integer input ) {
|
default String mapIntegerToString( Integer input ) {
|
||||||
|
@ -20,7 +20,7 @@ public interface MapToBeanWithDefaultMapper {
|
|||||||
MapToBeanWithDefaultMapper INSTANCE = Mappers.getMapper( MapToBeanWithDefaultMapper.class );
|
MapToBeanWithDefaultMapper INSTANCE = Mappers.getMapper( MapToBeanWithDefaultMapper.class );
|
||||||
|
|
||||||
@Mapping(target = "normalInt", source = "number", defaultValue = "4711")
|
@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<String, Integer> source);
|
Target toTarget(Map<String, Integer> source);
|
||||||
|
|
||||||
class Target {
|
class Target {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user