Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -650,3 +650,68 @@ public class PersonMapperImpl implements PersonMapper {
}
----
====

[[mapping-map-to-bean]]
=== Mapping Map to Bean

There are situations when a mapping from a `Map<String, ???>` into a specific bean is needed.
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.
Such a mapping looks like:

.Example classes for mapping map to bean
====
[source, java, linenums]
[subs="verbatim,attributes"]
----
public class Customer {

private Long id;
private String name;

//getters and setter omitted for brevity
}

@Mapper
public interface CustomerMapper {

@Mapping(target = "name", source = "customerName")
Customer toCustomer(Map<String, String> map);

}
----
====

.Generated mapper for mapping map to bean
====
[source, java, linenums]
[subs="verbatim,attributes"]
----
// GENERATED CODE
public class CustomerMapperImpl implements CustomerMapper {

@Override
public Customer toCustomer(Map<String, String> map) {
// ...
if ( map.containsKey( "id" ) ) {
customer.setId( Integer.parseInt( map.get( "id" ) ) );
}
if ( map.containsKey( "customerName" ) ) {
customer.setName( source.get( "customerName" ) );
}
// ...
}
}
----
====

[NOTE]
====
All existing rules about mapping between different types and using other mappers defined with `Mapper#uses` or custom methods in the mappers are applied.
i.e. You can map from `Map<String, Integer>` where for each property a conversion from `Integer` into the respective property will be needed.
====

[WARNING]
====
When a raw map or a map that does not have a String as a key is used, then a warning will be generated.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How is the key interpreted in such case.. guess we need to add that in the doc.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It isn't, I thought that it is clear at the beginning of the section

There are situations when a mapping from a Map<String, ???> into a specific bean is needed.

The warning is not generated if the map itself is mapped into some other target property directly as is.
====
Original file line number Diff line number Diff line change
Expand Up @@ -238,9 +238,11 @@ else if ( !method.isUpdateMethod() ) {
for ( Parameter sourceParameter : method.getSourceParameters() ) {
unprocessedSourceParameters.add( sourceParameter );

if ( sourceParameter.getType().isPrimitive() || sourceParameter.getType().isArrayType() ) {
if ( sourceParameter.getType().isPrimitive() || sourceParameter.getType().isArrayType() ||
sourceParameter.getType().isMapType() ) {
continue;
}

Map<String, Accessor> readAccessors = sourceParameter.getType().getPropertyReadAccessors();

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

// map parameters without a mapping
applyParameterNameBasedMapping();

}

// Process the unprocessed defined targets
Expand All @@ -288,6 +291,7 @@ else if ( !method.isUpdateMethod() ) {
reportErrorForUnmappedTargetPropertiesIfRequired();
reportErrorForUnmappedSourcePropertiesIfRequired();
reportErrorForMissingIgnoredSourceProperties();
reportErrorForUnusedSourceParameters();

// mapNullToDefault
boolean mapNullToDefault = method.getOptions()
Expand Down Expand Up @@ -1364,6 +1368,16 @@ private SourceReference getSourceRefByTargetName(Parameter sourceParameter, Stri
return sourceRef;
}

if ( sourceParameter.getType().isMapType() ) {
List<Type> typeParameters = sourceParameter.getType().getTypeParameters();
if ( typeParameters.size() == 2 && typeParameters.get( 0 ).isString() ) {
return SourceReference.fromMapSource(
new String[] { targetPropertyName },
sourceParameter
);
}
}

Accessor sourceReadAccessor =
sourceParameter.getType().getPropertyReadAccessors().get( targetPropertyName );
if ( sourceReadAccessor != null ) {
Expand Down Expand Up @@ -1534,6 +1548,33 @@ private void reportErrorForMissingIgnoredSourceProperties() {
);
}
}

private void reportErrorForUnusedSourceParameters() {
for ( Parameter sourceParameter : unprocessedSourceParameters ) {
Type parameterType = sourceParameter.getType();
if ( parameterType.isMapType() ) {
// We are only going to output a warning for the source parameter if it was unused
// i.e. the intention of the user was most likely to use it as a mapping from Bean to Map
List<Type> typeParameters = parameterType.getTypeParameters();
if ( typeParameters.size() != 2 || !typeParameters.get( 0 ).isString() ) {
Message message = typeParameters.isEmpty() ?
Message.MAPTOBEANMAPPING_RAW_MAP :
Message.MAPTOBEANMAPPING_WRONG_KEY_TYPE;
ctx.getMessager()
.printMessage(
method.getExecutable(),
message,
sourceParameter.getName(),
String.format(
"Map<%s,%s>",
!typeParameters.isEmpty() ? typeParameters.get( 0 ).describe() : "",
typeParameters.size() > 1 ? typeParameters.get( 1 ).describe() : ""
)
);
}
}
}
}
}

private static class ConstructorAccessor {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
import org.mapstruct.ap.internal.model.presence.AllPresenceChecksPresenceCheck;
import org.mapstruct.ap.internal.model.presence.JavaExpressionPresenceCheck;
import org.mapstruct.ap.internal.model.presence.NullPresenceCheck;
import org.mapstruct.ap.internal.model.presence.SourceReferenceContainsKeyPresenceCheck;
import org.mapstruct.ap.internal.model.presence.SourceReferenceMethodPresenceCheck;
import org.mapstruct.ap.internal.model.source.DelegatingOptions;
import org.mapstruct.ap.internal.model.source.MappingControl;
Expand Down Expand Up @@ -307,6 +308,9 @@ private Assignment forge( ) {
else if ( sourceType.isMapType() && targetType.isMapType() ) {
assignment = forgeMapMapping( sourceType, targetType, rightHandSide );
}
else if ( sourceType.isMapType() && !targetType.isMapType()) {
assignment = forgeMapToBeanMapping( sourceType, targetType, rightHandSide );
}
else if ( ( sourceType.isIterableType() && targetType.isStreamType() )
|| ( sourceType.isStreamType() && targetType.isStreamType() )
|| ( sourceType.isStreamType() && targetType.isIterableType() ) ) {
Expand Down Expand Up @@ -656,6 +660,13 @@ private PresenceCheck getSourcePresenceCheckerRef(SourceReference sourceReferenc
// in the forged method?
PropertyEntry propertyEntry = sourceReference.getShallowestProperty();
if ( propertyEntry.getPresenceChecker() != null ) {
if (propertyEntry.getPresenceChecker().getAccessorType() == AccessorType.MAP_CONTAINS ) {
return new SourceReferenceContainsKeyPresenceCheck(
sourceParam.getName(),
propertyEntry.getPresenceChecker().getSimpleName()
);
}

List<PresenceCheck> presenceChecks = new ArrayList<>();
presenceChecks.add( new SourceReferenceMethodPresenceCheck(
sourceParam.getName(),
Expand Down Expand Up @@ -742,6 +753,19 @@ private Assignment forgeMapMapping(Type sourceType, Type targetType, SourceRHS s
return createForgedAssignment( source, methodRef, mapMappingMethod );
}

private Assignment forgeMapToBeanMapping(Type sourceType, Type targetType, SourceRHS source) {

targetType = targetType.withoutBounds();
ForgedMethod methodRef = prepareForgedMethod( sourceType, targetType, source, "{}" );

BeanMappingMethod.Builder builder = new BeanMappingMethod.Builder();
final BeanMappingMethod mapToBeanMappingMethod = builder.mappingContext( ctx )
.forgedMethod( methodRef )
.build();

return createForgedAssignment( source, methodRef, mapToBeanMappingMethod );
}

private Assignment forgeMapping(SourceRHS sourceRHS) {
Type sourceType;
if ( targetWriteAccessorType == AccessorType.ADDER ) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,16 @@

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.AnnotationValue;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeMirror;

import org.mapstruct.ap.internal.model.common.Parameter;
import org.mapstruct.ap.internal.model.common.Type;
Expand All @@ -24,6 +27,8 @@
import org.mapstruct.ap.internal.util.Message;
import org.mapstruct.ap.internal.util.Strings;
import org.mapstruct.ap.internal.util.accessor.Accessor;
import org.mapstruct.ap.internal.util.accessor.MapValueAccessor;
import org.mapstruct.ap.internal.util.accessor.MapValuePresenceChecker;

import static org.mapstruct.ap.internal.model.beanmapping.PropertyEntry.forSourceReference;
import static org.mapstruct.ap.internal.util.Collections.last;
Expand Down Expand Up @@ -52,6 +57,26 @@
*/
public class SourceReference extends AbstractReference {

public static SourceReference fromMapSource(String[] segments, Parameter parameter) {
Type parameterType = parameter.getType();
Type valueType = parameterType.getTypeParameters().get( 1 );

TypeElement typeElement = parameterType.getTypeElement();
TypeMirror typeMirror = valueType.getTypeMirror();
String simpleName = String.join( ".", segments );

MapValueAccessor mapValueAccessor = new MapValueAccessor( typeElement, typeMirror, simpleName );
MapValuePresenceChecker mapValuePresenceChecker = new MapValuePresenceChecker(
typeElement,
typeMirror,
simpleName
);
List<PropertyEntry> entries = Collections.singletonList(
PropertyEntry.forSourceReference( segments, mapValueAccessor, mapValuePresenceChecker, valueType )
);
return new SourceReference( parameter, entries, true );
}

/**
* Builds a {@link SourceReference} from an {@code @Mappping}.
*/
Expand Down Expand Up @@ -149,6 +174,10 @@ public SourceReference build() {
*/
private SourceReference buildFromSingleSourceParameters(String[] segments, Parameter parameter) {

if ( canBeTreatedAsMapSourceType( parameter.getType() ) ) {
return fromMapSource( segments, parameter );
}

boolean foundEntryMatch;

String[] propertyNames = segments;
Expand Down Expand Up @@ -185,6 +214,14 @@ private SourceReference buildFromSingleSourceParameters(String[] segments, Param
*/
private SourceReference buildFromMultipleSourceParameters(String[] segments, Parameter parameter) {

if (parameter != null && canBeTreatedAsMapSourceType( parameter.getType() )) {
String[] propertyNames = new String[0];
if ( segments.length > 1 ) {
propertyNames = Arrays.copyOfRange( segments, 1, segments.length );
}
return fromMapSource( propertyNames, parameter );
}

boolean foundEntryMatch;

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

private boolean canBeTreatedAsMapSourceType(Type type) {
if ( !type.isMapType() ) {
return false;
}

List<Type> typeParameters = type.getTypeParameters();
return typeParameters.size() == 2 && typeParameters.get( 0 ).isString();
}

/**
* When there are more than one source parameters, the first segment name of the propery
* needs to match the parameter name to avoid ambiguity
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* 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.internal.model.presence;

import java.util.Collections;
import java.util.Objects;
import java.util.Set;

import org.mapstruct.ap.internal.model.common.ModelElement;
import org.mapstruct.ap.internal.model.common.PresenceCheck;
import org.mapstruct.ap.internal.model.common.Type;

/**
* @author Filip Hrisafov
*/
public class SourceReferenceContainsKeyPresenceCheck extends ModelElement implements PresenceCheck {

private final String sourceReference;
private final String propertyName;

public SourceReferenceContainsKeyPresenceCheck(String sourceReference, String propertyName) {
this.sourceReference = sourceReference;
this.propertyName = propertyName;
}

public String getSourceReference() {
return sourceReference;
}

public String getPropertyName() {
return propertyName;
}

@Override
public Set<Type> getImportTypes() {
return Collections.emptySet();
}

@Override
public boolean equals(Object o) {
if ( this == o ) {
return true;
}
if ( o == null || getClass() != o.getClass() ) {
return false;
}
SourceReferenceContainsKeyPresenceCheck that = (SourceReferenceContainsKeyPresenceCheck) o;
return Objects.equals( sourceReference, that.sourceReference ) &&
Objects.equals( propertyName, that.propertyName );
}

@Override
public int hashCode() {
return Objects.hash( sourceReference, propertyName );
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,11 @@ public enum Message {
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 ),
VALUEMAPPING_NON_EXISTING_CONSTANT_FROM_SPI( "Constant %s doesn't exist in enum type %s. Constant was returned from EnumMappingStrategy: %s"),
VALUEMAPPING_NON_EXISTING_CONSTANT( "Constant %s doesn't exist in enum type %s." ),
VALUEMAPPING_THROW_EXCEPTION_SOURCE( "Source = \"<THROW_EXCEPTION>\" is not allowed. Target = \"<THROW_EXCEPTION>\" can only be used." );
VALUEMAPPING_THROW_EXCEPTION_SOURCE( "Source = \"<THROW_EXCEPTION>\" is not allowed. Target = \"<THROW_EXCEPTION>\" can only be used." ),

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 ),
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 ),
;
// CHECKSTYLE:ON


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
package org.mapstruct.ap.internal.util;

import org.mapstruct.ap.internal.util.accessor.Accessor;
import org.mapstruct.ap.internal.util.accessor.AccessorType;

/**
* This a wrapper class which provides the value that needs to be used in the models.
Expand Down Expand Up @@ -45,6 +46,10 @@ public static ValueProvider of(Accessor accessor) {
return null;
}
String value = accessor.getSimpleName();
if (accessor.getAccessorType() == AccessorType.MAP_GET ) {
value = "get( \"" + value + "\" )";
return new ValueProvider( value );
}
if ( !accessor.getAccessorType().isFieldAssignment() ) {
value += "()";
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ public enum AccessorType {
GETTER,
SETTER,
ADDER,
MAP_GET,
MAP_CONTAINS,
PRESENCE_CHECKER;

public boolean isFieldAssignment() {
Expand Down
Loading