mirror of
https://github.com/mapstruct/mapstruct.git
synced 2025-07-12 00:00:08 +08:00
#3306 Support for map keys that contain dots when maps are mapped
This commit is contained in:
parent
6d99f7b8f3
commit
01305e7d0e
@ -11,6 +11,7 @@ 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;
|
||||||
@ -152,15 +153,19 @@ public class SourceReference extends AbstractReference {
|
|||||||
private SourceReference buildFromSingleSourceParameters(String[] segments, Parameter parameter) {
|
private SourceReference buildFromSingleSourceParameters(String[] segments, Parameter parameter) {
|
||||||
boolean foundEntryMatch;
|
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;
|
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(
|
List<PropertyEntry> entries = matchWithSourceAccessorTypes(
|
||||||
parameter.getType(),
|
parameter.getType(),
|
||||||
propertyNames,
|
propertyNames,
|
||||||
@ -205,6 +210,9 @@ 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 );
|
||||||
}
|
}
|
||||||
@ -220,6 +228,12 @@ 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
|
||||||
|
@ -59,6 +59,7 @@ class FromMapMappingTest {
|
|||||||
map.put( "name", "Jacket" );
|
map.put( "name", "Jacket" );
|
||||||
map.put( "price", "25.5" );
|
map.put( "price", "25.5" );
|
||||||
map.put( "shipmentDate", "2021-06-15" );
|
map.put( "shipmentDate", "2021-06-15" );
|
||||||
|
map.put( "this.will.be.ignored", "..." );
|
||||||
StringMapToBeanMapper.Order order = StringMapToBeanMapper.INSTANCE.fromMap( map );
|
StringMapToBeanMapper.Order order = StringMapToBeanMapper.INSTANCE.fromMap( map );
|
||||||
|
|
||||||
assertThat( order ).isNotNull();
|
assertThat( order ).isNotNull();
|
||||||
@ -92,7 +93,7 @@ class FromMapMappingTest {
|
|||||||
Map<String, String> map = Collections.singletonMap( "orderDate", "" );
|
Map<String, String> map = Collections.singletonMap( "orderDate", "" );
|
||||||
assertThatThrownBy( () -> StringMapToBeanMapper.INSTANCE.fromMap( map ) )
|
assertThatThrownBy( () -> StringMapToBeanMapper.INSTANCE.fromMap( map ) )
|
||||||
.isInstanceOf( RuntimeException.class )
|
.isInstanceOf( RuntimeException.class )
|
||||||
.getCause()
|
.cause()
|
||||||
.isInstanceOf( ParseException.class );
|
.isInstanceOf( ParseException.class );
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -116,6 +117,7 @@ class FromMapMappingTest {
|
|||||||
map.put( "name", "Jacket" );
|
map.put( "name", "Jacket" );
|
||||||
map.put( "price", "25.5" );
|
map.put( "price", "25.5" );
|
||||||
map.put( "shipmentDate", "2021-06-15" );
|
map.put( "shipmentDate", "2021-06-15" );
|
||||||
|
map.put( "this.will.be.ignored", "..." );
|
||||||
StringMapToBeanWithCustomPresenceCheckMapper.Order order =
|
StringMapToBeanWithCustomPresenceCheckMapper.Order order =
|
||||||
StringMapToBeanWithCustomPresenceCheckMapper.INSTANCE.fromMap( map );
|
StringMapToBeanWithCustomPresenceCheckMapper.INSTANCE.fromMap( map );
|
||||||
|
|
||||||
@ -133,6 +135,7 @@ class FromMapMappingTest {
|
|||||||
map.put( "price", "" );
|
map.put( "price", "" );
|
||||||
map.put( "orderDate", "" );
|
map.put( "orderDate", "" );
|
||||||
map.put( "shipmentDate", "" );
|
map.put( "shipmentDate", "" );
|
||||||
|
map.put( "this.will.be.ignored", "" );
|
||||||
StringMapToBeanWithCustomPresenceCheckMapper.Order order =
|
StringMapToBeanWithCustomPresenceCheckMapper.Order order =
|
||||||
StringMapToBeanWithCustomPresenceCheckMapper.INSTANCE.fromMap( map );
|
StringMapToBeanWithCustomPresenceCheckMapper.INSTANCE.fromMap( map );
|
||||||
|
|
||||||
@ -150,18 +153,21 @@ class FromMapMappingTest {
|
|||||||
void shouldMapWithDefinedMapping() {
|
void shouldMapWithDefinedMapping() {
|
||||||
Map<String, Integer> sourceMap = new HashMap<>();
|
Map<String, Integer> sourceMap = new HashMap<>();
|
||||||
sourceMap.put( "number", 44 );
|
sourceMap.put( "number", 44 );
|
||||||
|
sourceMap.put( "number.with.dots", 55 );
|
||||||
|
|
||||||
MapToBeanDefinedMapper.Target target = MapToBeanDefinedMapper.INSTANCE.toTarget( sourceMap );
|
MapToBeanDefinedMapper.Target target = MapToBeanDefinedMapper.INSTANCE.toTarget( sourceMap );
|
||||||
|
|
||||||
assertThat( target ).isNotNull();
|
assertThat( target ).isNotNull();
|
||||||
assertThat( target.getNormalInt() ).isEqualTo( "44" );
|
assertThat( target.getNormalInt() ).isEqualTo( "44" );
|
||||||
|
assertThat( target.getNormalIntWithDots() ).isEqualTo( "55" );
|
||||||
}
|
}
|
||||||
|
|
||||||
@ProcessorTest
|
@ProcessorTest
|
||||||
@WithClasses(MapToBeanImplicitMapper.class)
|
@WithClasses(MapToBeanImplicitMapper.class)
|
||||||
void shouldMapWithImpicitMapping() {
|
void shouldMapWithImplicitMapping() {
|
||||||
Map<String, String> sourceMap = new HashMap<>();
|
Map<String, String> sourceMap = new HashMap<>();
|
||||||
sourceMap.put( "name", "mapstruct" );
|
sourceMap.put( "name", "mapstruct" );
|
||||||
|
sourceMap.put( "name.with.dots", "will.be.ignored" );
|
||||||
|
|
||||||
MapToBeanImplicitMapper.Target target = MapToBeanImplicitMapper.INSTANCE.toTarget( sourceMap );
|
MapToBeanImplicitMapper.Target target = MapToBeanImplicitMapper.INSTANCE.toTarget( sourceMap );
|
||||||
|
|
||||||
@ -174,6 +180,7 @@ class FromMapMappingTest {
|
|||||||
void shouldMapToExistingTargetWithImplicitMapping() {
|
void shouldMapToExistingTargetWithImplicitMapping() {
|
||||||
Map<String, Integer> sourceMap = new HashMap<>();
|
Map<String, Integer> sourceMap = new HashMap<>();
|
||||||
sourceMap.put( "rating", 5 );
|
sourceMap.put( "rating", 5 );
|
||||||
|
sourceMap.put( "rating.with.dots", -1 );
|
||||||
|
|
||||||
MapToBeanUpdateImplicitMapper.Target existingTarget = new MapToBeanUpdateImplicitMapper.Target();
|
MapToBeanUpdateImplicitMapper.Target existingTarget = new MapToBeanUpdateImplicitMapper.Target();
|
||||||
existingTarget.setRating( 4 );
|
existingTarget.setRating( 4 );
|
||||||
@ -197,6 +204,7 @@ class FromMapMappingTest {
|
|||||||
|
|
||||||
assertThat( target ).isNotNull();
|
assertThat( target ).isNotNull();
|
||||||
assertThat( target.getNormalInt() ).isEqualTo( "4711" );
|
assertThat( target.getNormalInt() ).isEqualTo( "4711" );
|
||||||
|
assertThat( target.getNormalIntWithDots() ).isEqualTo( "999" );
|
||||||
}
|
}
|
||||||
|
|
||||||
@ProcessorTest
|
@ProcessorTest
|
||||||
@ -204,12 +212,14 @@ class FromMapMappingTest {
|
|||||||
void shouldMapUsingMappingMethod() {
|
void shouldMapUsingMappingMethod() {
|
||||||
Map<String, Integer> sourceMap = new HashMap<>();
|
Map<String, Integer> sourceMap = new HashMap<>();
|
||||||
sourceMap.put( "number", 23 );
|
sourceMap.put( "number", 23 );
|
||||||
|
sourceMap.put( "number.with.dots", 45 );
|
||||||
|
|
||||||
MapToBeanUsingMappingMethodMapper.Target target = MapToBeanUsingMappingMethodMapper.INSTANCE
|
MapToBeanUsingMappingMethodMapper.Target target = MapToBeanUsingMappingMethodMapper.INSTANCE
|
||||||
.toTarget( sourceMap );
|
.toTarget( sourceMap );
|
||||||
|
|
||||||
assertThat( target ).isNotNull();
|
assertThat( target ).isNotNull();
|
||||||
assertThat( target.getNormalInt() ).isEqualTo( "converted_23" );
|
assertThat( target.getNormalInt() ).isEqualTo( "converted_23" );
|
||||||
|
assertThat( target.getNormalIntWithDots() ).isEqualTo( "converted_45" );
|
||||||
}
|
}
|
||||||
|
|
||||||
@ProcessorTest
|
@ProcessorTest
|
||||||
@ -275,6 +285,39 @@ class FromMapMappingTest {
|
|||||||
assertThat( target.getNested() ).isEqualTo( "valueFromNestedMap" );
|
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
|
@ProcessorTest
|
||||||
@WithClasses(ObjectMapToBeanWithQualifierMapper.class)
|
@WithClasses(ObjectMapToBeanWithQualifierMapper.class)
|
||||||
void shouldUseObjectQualifiedMethod() {
|
void shouldUseObjectQualifiedMethod() {
|
||||||
|
@ -20,11 +20,13 @@ 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")
|
||||||
Target toTarget(Map<String, Integer> source);
|
Target toTarget(Map<String, Integer> source);
|
||||||
|
|
||||||
class Target {
|
class Target {
|
||||||
|
|
||||||
private String normalInt;
|
private String normalInt;
|
||||||
|
private String normalIntWithDots;
|
||||||
|
|
||||||
public String getNormalInt() {
|
public String getNormalInt() {
|
||||||
return normalInt;
|
return normalInt;
|
||||||
@ -33,6 +35,14 @@ public interface MapToBeanDefinedMapper {
|
|||||||
public void setNormalInt(String normalInt) {
|
public void setNormalInt(String normalInt) {
|
||||||
this.normalInt = normalInt;
|
this.normalInt = normalInt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getNormalIntWithDots() {
|
||||||
|
return normalIntWithDots;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setNormalIntWithDots(String normalIntWithDots) {
|
||||||
|
this.normalIntWithDots = normalIntWithDots;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -20,6 +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")
|
||||||
Target toTarget(Map<String, Integer> source);
|
Target toTarget(Map<String, Integer> source);
|
||||||
|
|
||||||
default String mapIntegerToString( Integer input ) {
|
default String mapIntegerToString( Integer input ) {
|
||||||
@ -29,6 +30,7 @@ public interface MapToBeanUsingMappingMethodMapper {
|
|||||||
class Target {
|
class Target {
|
||||||
|
|
||||||
private String normalInt;
|
private String normalInt;
|
||||||
|
private String normalIntWithDots;
|
||||||
|
|
||||||
public String getNormalInt() {
|
public String getNormalInt() {
|
||||||
return normalInt;
|
return normalInt;
|
||||||
@ -37,6 +39,14 @@ public interface MapToBeanUsingMappingMethodMapper {
|
|||||||
public void setNormalInt(String normalInt) {
|
public void setNormalInt(String normalInt) {
|
||||||
this.normalInt = normalInt;
|
this.normalInt = normalInt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getNormalIntWithDots() {
|
||||||
|
return normalIntWithDots;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setNormalIntWithDots(String normalIntWithDots) {
|
||||||
|
this.normalIntWithDots = normalIntWithDots;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -20,11 +20,13 @@ 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")
|
||||||
Target toTarget(Map<String, Integer> source);
|
Target toTarget(Map<String, Integer> source);
|
||||||
|
|
||||||
class Target {
|
class Target {
|
||||||
|
|
||||||
private String normalInt;
|
private String normalInt;
|
||||||
|
private String normalIntWithDots;
|
||||||
|
|
||||||
public String getNormalInt() {
|
public String getNormalInt() {
|
||||||
return normalInt;
|
return normalInt;
|
||||||
@ -33,6 +35,14 @@ public interface MapToBeanWithDefaultMapper {
|
|||||||
public void setNormalInt(String normalInt) {
|
public void setNormalInt(String normalInt) {
|
||||||
this.normalInt = normalInt;
|
this.normalInt = normalInt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getNormalIntWithDots() {
|
||||||
|
return normalIntWithDots;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setNormalIntWithDots(String normalIntWithDots) {
|
||||||
|
this.normalIntWithDots = normalIntWithDots;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user