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.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
|
||||
|
@ -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() {
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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 );
|
||||
|
||||
@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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user