Skip to content

Commit 985ca2f

Browse files
ckosmowskifiliphr
andcommitted
mapstruct#1075 Support for Mapping from Map<String, ???> to Bean
Co-authored-by: Filip Hrisafov <filip.hrisafov@gmail.com>
1 parent fb9c7a3 commit 985ca2f

27 files changed

+1535
-2
lines changed

documentation/src/main/asciidoc/chapter-3-defining-a-mapper.asciidoc

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -650,3 +650,68 @@ public class PersonMapperImpl implements PersonMapper {
650650
}
651651
----
652652
====
653+
654+
[[mapping-map-to-bean]]
655+
=== Mapping Map to Bean
656+
657+
There are situations when a mapping from a `Map<String, ???>` into a specific bean is needed.
658+
MapStruct offers a transparent way of doing such a mapping by using the target bean properties (or defined through `Mapping#source`) to extract the values from the map.
659+
Such a mapping looks like:
660+
661+
.Example classes for mapping map to bean
662+
====
663+
[source, java, linenums]
664+
[subs="verbatim,attributes"]
665+
----
666+
public class Customer {
667+
668+
private Long id;
669+
private String name;
670+
671+
//getters and setter omitted for brevity
672+
}
673+
674+
@Mapper
675+
public interface CustomerMapper {
676+
677+
@Mapping(target = "name", source = "customerName")
678+
Customer toCustomer(Map<String, String> map);
679+
680+
}
681+
----
682+
====
683+
684+
.Generated mapper for mapping map to bean
685+
====
686+
[source, java, linenums]
687+
[subs="verbatim,attributes"]
688+
----
689+
// GENERATED CODE
690+
public class CustomerMapperImpl implements CustomerMapper {
691+
692+
@Override
693+
public Customer toCustomer(Map<String, String> map) {
694+
// ...
695+
if ( map.containsKey( "id" ) ) {
696+
customer.setId( Integer.parseInt( map.get( "id" ) ) );
697+
}
698+
if ( map.containsKey( "customerName" ) ) {
699+
customer.setName( source.get( "customerName" ) );
700+
}
701+
// ...
702+
}
703+
}
704+
----
705+
====
706+
707+
[NOTE]
708+
====
709+
All existing rules about mapping between different types and using other mappers defined with `Mapper#uses` or custom methods in the mappers are applied.
710+
i.e. You can map from `Map<String, Integer>` where for each property a conversion from `Integer` into the respective property will be needed.
711+
====
712+
713+
[WARNING]
714+
====
715+
When a raw map or a map that does not have a String as a key is used, then a warning will be generated.
716+
The warning is not generated if the map itself is mapped into some other target property directly as is.
717+
====

processor/src/main/java/org/mapstruct/ap/internal/model/BeanMappingMethod.java

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -238,9 +238,11 @@ else if ( !method.isUpdateMethod() ) {
238238
for ( Parameter sourceParameter : method.getSourceParameters() ) {
239239
unprocessedSourceParameters.add( sourceParameter );
240240

241-
if ( sourceParameter.getType().isPrimitive() || sourceParameter.getType().isArrayType() ) {
241+
if ( sourceParameter.getType().isPrimitive() || sourceParameter.getType().isArrayType() ||
242+
sourceParameter.getType().isMapType() ) {
242243
continue;
243244
}
245+
244246
Map<String, Accessor> readAccessors = sourceParameter.getType().getPropertyReadAccessors();
245247

246248
for ( Entry<String, Accessor> entry : readAccessors.entrySet() ) {
@@ -276,6 +278,7 @@ else if ( !method.isUpdateMethod() ) {
276278

277279
// map parameters without a mapping
278280
applyParameterNameBasedMapping();
281+
279282
}
280283

281284
// Process the unprocessed defined targets
@@ -288,6 +291,7 @@ else if ( !method.isUpdateMethod() ) {
288291
reportErrorForUnmappedTargetPropertiesIfRequired();
289292
reportErrorForUnmappedSourcePropertiesIfRequired();
290293
reportErrorForMissingIgnoredSourceProperties();
294+
reportErrorForUnusedSourceParameters();
291295

292296
// mapNullToDefault
293297
boolean mapNullToDefault = method.getOptions()
@@ -1364,6 +1368,16 @@ private SourceReference getSourceRefByTargetName(Parameter sourceParameter, Stri
13641368
return sourceRef;
13651369
}
13661370

1371+
if ( sourceParameter.getType().isMapType() ) {
1372+
List<Type> typeParameters = sourceParameter.getType().getTypeParameters();
1373+
if ( typeParameters.size() == 2 && typeParameters.get( 0 ).isString() ) {
1374+
return SourceReference.fromMapSource(
1375+
new String[] { targetPropertyName },
1376+
sourceParameter
1377+
);
1378+
}
1379+
}
1380+
13671381
Accessor sourceReadAccessor =
13681382
sourceParameter.getType().getPropertyReadAccessors().get( targetPropertyName );
13691383
if ( sourceReadAccessor != null ) {
@@ -1534,6 +1548,33 @@ private void reportErrorForMissingIgnoredSourceProperties() {
15341548
);
15351549
}
15361550
}
1551+
1552+
private void reportErrorForUnusedSourceParameters() {
1553+
for ( Parameter sourceParameter : unprocessedSourceParameters ) {
1554+
Type parameterType = sourceParameter.getType();
1555+
if ( parameterType.isMapType() ) {
1556+
// We are only going to output a warning for the source parameter if it was unused
1557+
// i.e. the intention of the user was most likely to use it as a mapping from Bean to Map
1558+
List<Type> typeParameters = parameterType.getTypeParameters();
1559+
if ( typeParameters.size() != 2 || !typeParameters.get( 0 ).isString() ) {
1560+
Message message = typeParameters.isEmpty() ?
1561+
Message.MAPTOBEANMAPPING_RAW_MAP :
1562+
Message.MAPTOBEANMAPPING_WRONG_KEY_TYPE;
1563+
ctx.getMessager()
1564+
.printMessage(
1565+
method.getExecutable(),
1566+
message,
1567+
sourceParameter.getName(),
1568+
String.format(
1569+
"Map<%s,%s>",
1570+
!typeParameters.isEmpty() ? typeParameters.get( 0 ).describe() : "",
1571+
typeParameters.size() > 1 ? typeParameters.get( 1 ).describe() : ""
1572+
)
1573+
);
1574+
}
1575+
}
1576+
}
1577+
}
15371578
}
15381579

15391580
private static class ConstructorAccessor {

processor/src/main/java/org/mapstruct/ap/internal/model/PropertyMapping.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
import org.mapstruct.ap.internal.model.presence.AllPresenceChecksPresenceCheck;
3838
import org.mapstruct.ap.internal.model.presence.JavaExpressionPresenceCheck;
3939
import org.mapstruct.ap.internal.model.presence.NullPresenceCheck;
40+
import org.mapstruct.ap.internal.model.presence.SourceReferenceContainsKeyPresenceCheck;
4041
import org.mapstruct.ap.internal.model.presence.SourceReferenceMethodPresenceCheck;
4142
import org.mapstruct.ap.internal.model.source.DelegatingOptions;
4243
import org.mapstruct.ap.internal.model.source.MappingControl;
@@ -307,6 +308,9 @@ private Assignment forge( ) {
307308
else if ( sourceType.isMapType() && targetType.isMapType() ) {
308309
assignment = forgeMapMapping( sourceType, targetType, rightHandSide );
309310
}
311+
else if ( sourceType.isMapType() && !targetType.isMapType()) {
312+
assignment = forgeMapToBeanMapping( sourceType, targetType, rightHandSide );
313+
}
310314
else if ( ( sourceType.isIterableType() && targetType.isStreamType() )
311315
|| ( sourceType.isStreamType() && targetType.isStreamType() )
312316
|| ( sourceType.isStreamType() && targetType.isIterableType() ) ) {
@@ -656,6 +660,13 @@ private PresenceCheck getSourcePresenceCheckerRef(SourceReference sourceReferenc
656660
// in the forged method?
657661
PropertyEntry propertyEntry = sourceReference.getShallowestProperty();
658662
if ( propertyEntry.getPresenceChecker() != null ) {
663+
if (propertyEntry.getPresenceChecker().getAccessorType() == AccessorType.MAP_CONTAINS ) {
664+
return new SourceReferenceContainsKeyPresenceCheck(
665+
sourceParam.getName(),
666+
propertyEntry.getPresenceChecker().getSimpleName()
667+
);
668+
}
669+
659670
List<PresenceCheck> presenceChecks = new ArrayList<>();
660671
presenceChecks.add( new SourceReferenceMethodPresenceCheck(
661672
sourceParam.getName(),
@@ -742,6 +753,19 @@ private Assignment forgeMapMapping(Type sourceType, Type targetType, SourceRHS s
742753
return createForgedAssignment( source, methodRef, mapMappingMethod );
743754
}
744755

756+
private Assignment forgeMapToBeanMapping(Type sourceType, Type targetType, SourceRHS source) {
757+
758+
targetType = targetType.withoutBounds();
759+
ForgedMethod methodRef = prepareForgedMethod( sourceType, targetType, source, "{}" );
760+
761+
BeanMappingMethod.Builder builder = new BeanMappingMethod.Builder();
762+
final BeanMappingMethod mapToBeanMappingMethod = builder.mappingContext( ctx )
763+
.forgedMethod( methodRef )
764+
.build();
765+
766+
return createForgedAssignment( source, methodRef, mapToBeanMappingMethod );
767+
}
768+
745769
private Assignment forgeMapping(SourceRHS sourceRHS) {
746770
Type sourceType;
747771
if ( targetWriteAccessorType == AccessorType.ADDER ) {

processor/src/main/java/org/mapstruct/ap/internal/model/beanmapping/SourceReference.java

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,16 @@
77

88
import java.util.ArrayList;
99
import java.util.Arrays;
10+
import java.util.Collections;
1011
import java.util.List;
1112
import java.util.Map;
1213
import java.util.Objects;
1314
import java.util.Set;
1415
import javax.lang.model.element.AnnotationMirror;
1516
import javax.lang.model.element.AnnotationValue;
17+
import javax.lang.model.element.TypeElement;
1618
import javax.lang.model.type.DeclaredType;
19+
import javax.lang.model.type.TypeMirror;
1720

1821
import org.mapstruct.ap.internal.model.common.Parameter;
1922
import org.mapstruct.ap.internal.model.common.Type;
@@ -24,6 +27,8 @@
2427
import org.mapstruct.ap.internal.util.Message;
2528
import org.mapstruct.ap.internal.util.Strings;
2629
import org.mapstruct.ap.internal.util.accessor.Accessor;
30+
import org.mapstruct.ap.internal.util.accessor.MapValueAccessor;
31+
import org.mapstruct.ap.internal.util.accessor.MapValuePresenceChecker;
2732

2833
import static org.mapstruct.ap.internal.model.beanmapping.PropertyEntry.forSourceReference;
2934
import static org.mapstruct.ap.internal.util.Collections.last;
@@ -52,6 +57,26 @@
5257
*/
5358
public class SourceReference extends AbstractReference {
5459

60+
public static SourceReference fromMapSource(String[] segments, Parameter parameter) {
61+
Type parameterType = parameter.getType();
62+
Type valueType = parameterType.getTypeParameters().get( 1 );
63+
64+
TypeElement typeElement = parameterType.getTypeElement();
65+
TypeMirror typeMirror = valueType.getTypeMirror();
66+
String simpleName = String.join( ".", segments );
67+
68+
MapValueAccessor mapValueAccessor = new MapValueAccessor( typeElement, typeMirror, simpleName );
69+
MapValuePresenceChecker mapValuePresenceChecker = new MapValuePresenceChecker(
70+
typeElement,
71+
typeMirror,
72+
simpleName
73+
);
74+
List<PropertyEntry> entries = Collections.singletonList(
75+
PropertyEntry.forSourceReference( segments, mapValueAccessor, mapValuePresenceChecker, valueType )
76+
);
77+
return new SourceReference( parameter, entries, true );
78+
}
79+
5580
/**
5681
* Builds a {@link SourceReference} from an {@code @Mappping}.
5782
*/
@@ -149,6 +174,10 @@ public SourceReference build() {
149174
*/
150175
private SourceReference buildFromSingleSourceParameters(String[] segments, Parameter parameter) {
151176

177+
if ( canBeTreatedAsMapSourceType( parameter.getType() ) ) {
178+
return fromMapSource( segments, parameter );
179+
}
180+
152181
boolean foundEntryMatch;
153182

154183
String[] propertyNames = segments;
@@ -185,6 +214,14 @@ private SourceReference buildFromSingleSourceParameters(String[] segments, Param
185214
*/
186215
private SourceReference buildFromMultipleSourceParameters(String[] segments, Parameter parameter) {
187216

217+
if (parameter != null && canBeTreatedAsMapSourceType( parameter.getType() )) {
218+
String[] propertyNames = new String[0];
219+
if ( segments.length > 1 ) {
220+
propertyNames = Arrays.copyOfRange( segments, 1, segments.length );
221+
}
222+
return fromMapSource( propertyNames, parameter );
223+
}
224+
188225
boolean foundEntryMatch;
189226

190227
String[] propertyNames = new String[0];
@@ -207,6 +244,15 @@ private SourceReference buildFromMultipleSourceParameters(String[] segments, Par
207244
return new SourceReference( parameter, entries, foundEntryMatch );
208245
}
209246

247+
private boolean canBeTreatedAsMapSourceType(Type type) {
248+
if ( !type.isMapType() ) {
249+
return false;
250+
}
251+
252+
List<Type> typeParameters = type.getTypeParameters();
253+
return typeParameters.size() == 2 && typeParameters.get( 0 ).isString();
254+
}
255+
210256
/**
211257
* When there are more than one source parameters, the first segment name of the propery
212258
* needs to match the parameter name to avoid ambiguity
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/*
2+
* Copyright MapStruct Authors.
3+
*
4+
* Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
5+
*/
6+
package org.mapstruct.ap.internal.model.presence;
7+
8+
import java.util.Collections;
9+
import java.util.Objects;
10+
import java.util.Set;
11+
12+
import org.mapstruct.ap.internal.model.common.ModelElement;
13+
import org.mapstruct.ap.internal.model.common.PresenceCheck;
14+
import org.mapstruct.ap.internal.model.common.Type;
15+
16+
/**
17+
* @author Filip Hrisafov
18+
*/
19+
public class SourceReferenceContainsKeyPresenceCheck extends ModelElement implements PresenceCheck {
20+
21+
private final String sourceReference;
22+
private final String propertyName;
23+
24+
public SourceReferenceContainsKeyPresenceCheck(String sourceReference, String propertyName) {
25+
this.sourceReference = sourceReference;
26+
this.propertyName = propertyName;
27+
}
28+
29+
public String getSourceReference() {
30+
return sourceReference;
31+
}
32+
33+
public String getPropertyName() {
34+
return propertyName;
35+
}
36+
37+
@Override
38+
public Set<Type> getImportTypes() {
39+
return Collections.emptySet();
40+
}
41+
42+
@Override
43+
public boolean equals(Object o) {
44+
if ( this == o ) {
45+
return true;
46+
}
47+
if ( o == null || getClass() != o.getClass() ) {
48+
return false;
49+
}
50+
SourceReferenceContainsKeyPresenceCheck that = (SourceReferenceContainsKeyPresenceCheck) o;
51+
return Objects.equals( sourceReference, that.sourceReference ) &&
52+
Objects.equals( propertyName, that.propertyName );
53+
}
54+
55+
@Override
56+
public int hashCode() {
57+
return Objects.hash( sourceReference, propertyName );
58+
}
59+
}

processor/src/main/java/org/mapstruct/ap/internal/util/Message.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,11 @@ public enum Message {
181181
VALUEMAPPING_ANY_REMAINING_OR_UNMAPPED_MISSING( "Source = \"<ANY_REMAINING>\" or \"<ANY_UNMAPPED>\" is advisable for mapping of type String to an enum type.", Diagnostic.Kind.WARNING ),
182182
VALUEMAPPING_NON_EXISTING_CONSTANT_FROM_SPI( "Constant %s doesn't exist in enum type %s. Constant was returned from EnumMappingStrategy: %s"),
183183
VALUEMAPPING_NON_EXISTING_CONSTANT( "Constant %s doesn't exist in enum type %s." ),
184-
VALUEMAPPING_THROW_EXCEPTION_SOURCE( "Source = \"<THROW_EXCEPTION>\" is not allowed. Target = \"<THROW_EXCEPTION>\" can only be used." );
184+
VALUEMAPPING_THROW_EXCEPTION_SOURCE( "Source = \"<THROW_EXCEPTION>\" is not allowed. Target = \"<THROW_EXCEPTION>\" can only be used." ),
185+
186+
MAPTOBEANMAPPING_WRONG_KEY_TYPE( "The Map parameter \"%s\" cannot be used for property mapping. It must be typed with Map<String, ???> but it was typed with %s.", Diagnostic.Kind.WARNING ),
187+
MAPTOBEANMAPPING_RAW_MAP( "The Map parameter \"%s\" cannot be used for property mapping. It must be typed with Map<String, ???> but it was raw.", Diagnostic.Kind.WARNING ),
188+
;
185189
// CHECKSTYLE:ON
186190

187191

processor/src/main/java/org/mapstruct/ap/internal/util/ValueProvider.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
package org.mapstruct.ap.internal.util;
77

88
import org.mapstruct.ap.internal.util.accessor.Accessor;
9+
import org.mapstruct.ap.internal.util.accessor.AccessorType;
910

1011
/**
1112
* This a wrapper class which provides the value that needs to be used in the models.
@@ -45,6 +46,10 @@ public static ValueProvider of(Accessor accessor) {
4546
return null;
4647
}
4748
String value = accessor.getSimpleName();
49+
if (accessor.getAccessorType() == AccessorType.MAP_GET ) {
50+
value = "get( \"" + value + "\" )";
51+
return new ValueProvider( value );
52+
}
4853
if ( !accessor.getAccessorType().isFieldAssignment() ) {
4954
value += "()";
5055
}

processor/src/main/java/org/mapstruct/ap/internal/util/accessor/AccessorType.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ public enum AccessorType {
1212
GETTER,
1313
SETTER,
1414
ADDER,
15+
MAP_GET,
16+
MAP_CONTAINS,
1517
PRESENCE_CHECKER;
1618

1719
public boolean isFieldAssignment() {

0 commit comments

Comments
 (0)