diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/BeanMappingMethod.java b/processor/src/main/java/org/mapstruct/ap/internal/model/BeanMappingMethod.java index 7ea17bf27b..c53369f4b5 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/BeanMappingMethod.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/BeanMappingMethod.java @@ -118,7 +118,7 @@ public Builder userDefinedReturnType(Type userDefinedReturnType) { return this; } - public Builder returnTypeBuilder( BuilderType returnTypeBuilder ) { + public Builder returnTypeBuilder(BuilderType returnTypeBuilder) { this.returnTypeBuilder = returnTypeBuilder; return this; } @@ -132,7 +132,7 @@ public Builder forgedMethod(ForgedMethod forgedMethod) { this.method = forgedMethod; mappingReferences = forgedMethod.getMappingReferences(); Parameter sourceParameter = first( Parameter.getSourceParameters( forgedMethod.getParameters() ) ); - for ( MappingReference mappingReference: mappingReferences.getMappingReferences() ) { + for ( MappingReference mappingReference : mappingReferences.getMappingReferences() ) { SourceReference sourceReference = mappingReference.getSourceReference(); if ( sourceReference != null ) { mappingReference.setSourceReference( new SourceReference.BuilderFromSourceReference() @@ -237,7 +237,8 @@ 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 readAccessors = sourceParameter.getType().getPropertyReadAccessors(); @@ -272,6 +273,9 @@ else if ( !method.isUpdateMethod() ) { // map parameters without a mapping applyParameterNameBasedMapping(); + + // map properties without a mapping from map parameters + applyPropertyNameBasedMappingForMapSources(); } // Process the unprocessed defined targets @@ -295,18 +299,18 @@ else if ( !method.isUpdateMethod() ) { // before / after mappings List beforeMappingMethods = LifecycleMethodResolver.beforeMappingMethods( - method, - resultTypeToMap, - selectionParameters, - ctx, - existingVariableNames + method, + resultTypeToMap, + selectionParameters, + ctx, + existingVariableNames ); List afterMappingMethods = LifecycleMethodResolver.afterMappingMethods( - method, - resultTypeToMap, - selectionParameters, - ctx, - existingVariableNames + method, + resultTypeToMap, + selectionParameters, + ctx, + existingVariableNames ); if ( method instanceof ForgedMethod ) { @@ -365,10 +369,10 @@ private void initializeMappingReferencesIfNeeded(Type resultTypeToMap) { */ private boolean isBuilderRequired() { return returnTypeBuilder != null - && ( !method.isUpdateMethod() || !method.isMappingTargetAssignableToReturnType() ); + && ( !method.isUpdateMethod() || !method.isMappingTargetAssignableToReturnType() ); } - private boolean shouldCallFinalizerMethod(Type returnTypeToConstruct ) { + private boolean shouldCallFinalizerMethod(Type returnTypeToConstruct) { if ( returnTypeToConstruct == null ) { return false; } @@ -553,7 +557,8 @@ else if ( !returnType.hasAccessibleConstructor() ) { /** * Find a factory method for a return type or for a builder. - * @param returnTypeImpl the return type implementation to construct + * + * @param returnTypeImpl the return type implementation to construct * @param @selectionParameters * @return */ @@ -634,7 +639,7 @@ private ConstructorAccessor getConstructorAccessor(Type type) { ExecutableElement defaultAnnotatedConstructor = null; ExecutableElement parameterLessConstructor = null; List accessibleConstructors = new ArrayList<>( constructors.size() ); - List publicConstructors = new ArrayList<>( ); + List publicConstructors = new ArrayList<>(); for ( ExecutableElement constructor : constructors ) { if ( constructor.getModifiers().contains( Modifier.PRIVATE ) ) { @@ -894,7 +899,7 @@ private boolean handleDefinedNestedTargetMapping(Set handledTargets, Typ handledTargets.addAll( holder.getHandledTargets() ); // Store all the unprocessed defined targets. for ( Entry> entry : holder.getUnprocessedDefinedTarget() - .entrySet() ) { + .entrySet() ) { if ( entry.getValue().isEmpty() ) { continue; } @@ -904,7 +909,7 @@ private boolean handleDefinedNestedTargetMapping(Set handledTargets, Typ } private boolean handleDefinedMapping(MappingReference mappingRef, Type resultTypeToMap, - Set handledTargets) { + Set handledTargets) { boolean errorOccured = false; PropertyMapping propertyMapping = null; @@ -1069,7 +1074,7 @@ else if ( mapping.getJavaExpression() != null ) { handledTargets.add( targetPropertyName ); } // its a plain-old property mapping - else { + else { SourceReference sourceRef = mappingRef.getSourceReference(); // sourceRef is not defined, check if a source property has the same name @@ -1185,7 +1190,7 @@ else if ( mapping.getJavaExpression() != null ) { *

* When a target property matches its name with the (nested) source property, it is added to the list if and * only if it is an unprocessed target property. - * + *

* duplicates will be handled by {@link #applyPropertyNameBasedMapping(List)} */ private void applyTargetThisMapping() { @@ -1231,6 +1236,54 @@ private void applyPropertyNameBasedMapping() { applyPropertyNameBasedMapping( sourceReferences ); } + /** + * Iterates over all target properties and all source parameters. + *

+ * When a property name match occurs, the remainder will be checked for duplicates. Matches will be removed from + * the set of remaining target properties. + */ + private void applyPropertyNameBasedMappingForMapSources() { + final Type stringType = ctx.getTypeFactory().getType( String.class ); + for ( Parameter sourceParameter : method.getSourceParameters() ) { + if ( !sourceParameter.getType().isMapType() ) { + continue; + } + final List typeParameters = sourceParameter.getType().getTypeParameters(); + if (!(typeParameters.size() == 2 && typeParameters.get( 0 ).equals( stringType ))) { + Message message = typeParameters.isEmpty() ? Message.MAPTOBEANMAPPING_UNTYPED + : Message.MAPTOBEANMAPPING_WRONG_GENERIC_TYPES; + ctx.getMessager() + .printMessage( + method.getExecutable(), + message, + sourceParameter.getName(), + String.format( + "Map<%s,%s>", + !typeParameters.isEmpty() ? typeParameters.get( 0 ).getName() : "", + typeParameters.size() > 1 ? typeParameters.get( 1 ).getName() : "" + ) + ); + } + } + List sourceReferences = new ArrayList<>(); + for ( String targetPropertyName : unprocessedTargetProperties.keySet() ) { + for ( Parameter sourceParameter : method.getSourceParameters() ) { + if ( !sourceParameter.getType().isMapType() ) { + continue; + } + final List typeParameters = sourceParameter.getType().getTypeParameters(); + if (!(typeParameters.size() == 2 && typeParameters.get( 0 ).equals( stringType ))) { + continue; + } + SourceReference sourceRef = getMapSourceRefByTargetName( sourceParameter, targetPropertyName ); + if ( sourceRef != null ) { + sourceReferences.add( sourceRef ); + } + } + } + applyPropertyNameBasedMapping( sourceReferences ); + } + /** * Iterates over all target properties and all source parameters. *

@@ -1247,10 +1300,11 @@ private void applyPropertyNameBasedMapping(List sourceReference if ( targetPropertyWriteAccessor == null ) { // TODO improve error message ctx.getMessager() - .printMessage( method.getExecutable(), - Message.BEANMAPPING_SEVERAL_POSSIBLE_SOURCES, - targetPropertyName - ); + .printMessage( + method.getExecutable(), + Message.BEANMAPPING_SEVERAL_POSSIBLE_SOURCES, + targetPropertyName + ); continue; } @@ -1349,15 +1403,27 @@ private SourceReference getSourceRefByTargetName(Parameter sourceParameter, Stri DeclaredType declaredSourceType = (DeclaredType) sourceParameter.getType().getTypeMirror(); Type returnType = ctx.getTypeFactory().getReturnType( declaredSourceType, sourceReadAccessor ); sourceRef = new SourceReference.BuilderFromProperty().sourceParameter( sourceParameter ) - .type( returnType ) - .readAccessor( sourceReadAccessor ) - .presenceChecker( sourcePresenceChecker ) - .name( targetPropertyName ) - .build(); + .type( returnType ) + .readAccessor( sourceReadAccessor ) + .presenceChecker( sourcePresenceChecker ) + .name( targetPropertyName ) + .build(); } return sourceRef; } + private SourceReference getMapSourceRefByTargetName(Parameter sourceParameter, String targetPropertyName) { + + SourceReference sourceRef = null; + + if ( !sourceParameter.getType().isMapType() ) { + return sourceRef; + } + + Type defaultType = ctx.getTypeFactory().getType( Object.class ); + return SourceReference.fromMapSource( new String[] { targetPropertyName }, sourceParameter, defaultType ); + } + private MappingReferences extractMappingReferences(String targetProperty, boolean restrictToDefinedMappings) { if ( unprocessedDefinedTargets.containsKey( targetProperty ) ) { Set mappings = unprocessedDefinedTargets.get( targetProperty ); @@ -1603,7 +1669,7 @@ public Set getImportTypes() { } } - if ( returnTypeToConstruct != null ) { + if ( returnTypeToConstruct != null ) { types.addAll( returnTypeToConstruct.getImportTypes() ); } if ( returnTypeBuilder != null ) { @@ -1615,20 +1681,20 @@ public Set getImportTypes() { public List getSourceParametersExcludingPrimitives() { return getSourceParameters().stream() - .filter( parameter -> !parameter.getType().isPrimitive() ) - .collect( Collectors.toList() ); + .filter( parameter -> !parameter.getType().isPrimitive() ) + .collect( Collectors.toList() ); } public List getSourceParametersNeedingNullCheck() { return getSourceParameters().stream() - .filter( this::needsNullCheck ) - .collect( Collectors.toList() ); + .filter( this::needsNullCheck ) + .collect( Collectors.toList() ); } public List getSourceParametersNotNeedingNullCheck() { return getSourceParameters().stream() - .filter( parameter -> !needsNullCheck( parameter ) ) - .collect( Collectors.toList() ); + .filter( parameter -> !needsNullCheck( parameter ) ) + .collect( Collectors.toList() ); } private boolean needsNullCheck(Parameter parameter) { diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/PropertyMapping.java b/processor/src/main/java/org/mapstruct/ap/internal/model/PropertyMapping.java index 61e1fca864..7121137e35 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/PropertyMapping.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/PropertyMapping.java @@ -296,6 +296,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() ) ) { @@ -612,6 +615,12 @@ private String getSourcePresenceCheckerRef( SourceReference sourceReference ) { // in the forged method? PropertyEntry propertyEntry = sourceReference.getShallowestProperty(); if ( propertyEntry.getPresenceChecker() != null ) { + + if (propertyEntry.getPresenceChecker().getAccessorType() == AccessorType.MAP_CONTAINS ) { + return sourceParam.getName() + + "." + "containsKey( \"" + propertyEntry.getPresenceChecker().getSimpleName() + "\" )"; + } + sourcePresenceChecker = sourceParam.getName() + "." + propertyEntry.getPresenceChecker().getSimpleName() + "()"; @@ -684,6 +693,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 ) { diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/beanmapping/PropertyEntry.java b/processor/src/main/java/org/mapstruct/ap/internal/model/beanmapping/PropertyEntry.java index 8ac5e46671..5ee79cc1c9 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/beanmapping/PropertyEntry.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/beanmapping/PropertyEntry.java @@ -39,10 +39,10 @@ private PropertyEntry(String[] fullName, Accessor readAccessor, Accessor presenc /** * Constructor used to create {@link SourceReference} property entries from a mapping * - * @param name name of the property (dot separated) - * @param readAccessor its read accessor + * @param name name of the property (dot separated) + * @param readAccessor its read accessor * @param presenceChecker its presence Checker - * @param type type of the property + * @param type type of the property * @return the property entry for given parameters. */ public static PropertyEntry forSourceReference(String[] name, Accessor readAccessor, @@ -67,7 +67,7 @@ public Type getType() { } public String getFullName() { - return Strings.join( Arrays.asList( fullName ), "." ); + return Strings.join( Arrays.asList( fullName ), "." ); } @Override diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/beanmapping/SourceReference.java b/processor/src/main/java/org/mapstruct/ap/internal/model/beanmapping/SourceReference.java index 1d747c190e..3a7638fedb 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/beanmapping/SourceReference.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/beanmapping/SourceReference.java @@ -7,6 +7,7 @@ import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Objects; @@ -23,6 +24,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; @@ -51,6 +54,19 @@ */ public class SourceReference extends AbstractReference { + public static SourceReference fromMapSource(String[] segments, Parameter parameter, Type defaultValueType) { + final List typeParameters = parameter.getType().getTypeParameters(); + Type valueType = typeParameters.size() == 2 ? typeParameters.get( 1 ) : defaultValueType; + MapValueAccessor mapValueAccessor = new MapValueAccessor(parameter.getType().getTypeElement(), + valueType.getTypeMirror(), String.join( ".", segments ) ); + MapValuePresenceChecker mapValuePresenceChecker = new MapValuePresenceChecker( + parameter.getType().getTypeElement(), valueType.getTypeMirror(), String.join( ".", segments ) ); + List entries = Collections.singletonList( + PropertyEntry.forSourceReference( segments, mapValueAccessor, mapValuePresenceChecker, valueType ) + ); + return new SourceReference( parameter, entries, true ); + } + /** * Builds a {@link SourceReference} from an {@code @Mappping}. */ @@ -120,7 +136,7 @@ public SourceReference build() { String[] segments = sourceNameTrimmed.split( "\\." ); // start with an invalid source reference - SourceReference result = new SourceReference( null, new ArrayList<>( ), false ); + SourceReference result = new SourceReference( null, new ArrayList<>( ), false); if ( method.getSourceParameters().size() > 1 ) { Parameter parameter = fetchMatchingParameterFromFirstSegment( segments ); if ( parameter != null ) { @@ -148,6 +164,10 @@ public SourceReference build() { */ private SourceReference buildFromSingleSourceParameters(String[] segments, Parameter parameter) { + if (parameter.getType().isMapType()) { + return fromMapSource( segments, parameter, typeFactory.getType( Object.class ) ); + } + boolean foundEntryMatch; String[] propertyNames = segments; @@ -184,6 +204,14 @@ private SourceReference buildFromSingleSourceParameters(String[] segments, Param */ private SourceReference buildFromMultipleSourceParameters(String[] segments, Parameter parameter) { + if (parameter != null && parameter.getType().isMapType()) { + String[] propertyNames = new String[0]; + if ( segments.length > 1) { + propertyNames = Arrays.copyOfRange( segments, 1, segments.length ); + } + return fromMapSource( propertyNames, parameter, typeFactory.getType( Object.class ) ); + } + boolean foundEntryMatch; String[] propertyNames = new String[0]; diff --git a/processor/src/main/java/org/mapstruct/ap/internal/util/Message.java b/processor/src/main/java/org/mapstruct/ap/internal/util/Message.java index d88791fda1..e232f1211a 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/util/Message.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/util/Message.java @@ -174,7 +174,10 @@ public enum Message { VALUEMAPPING_ANY_REMAINING_FOR_NON_ENUM( "Source = \"\" can only be used on targets of type enum and not for %s." ), VALUEMAPPING_ANY_REMAINING_OR_UNMAPPED_MISSING( "Source = \"\" or \"\" 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_NON_EXISTING_CONSTANT( "Constant %s doesn't exist in enum type %s." ), + + MAPTOBEANMAPPING_WRONG_GENERIC_TYPES( "The Map parameter \"%s\" cannot be used for property mapping. It must be typed with Map but it was typed with %s.", Diagnostic.Kind.WARNING ), + MAPTOBEANMAPPING_UNTYPED( "The Map parameter \"%s\" cannot be used for property mapping. It must be typed with Map but it was untyped.", Diagnostic.Kind.WARNING ); // CHECKSTYLE:ON diff --git a/processor/src/main/java/org/mapstruct/ap/internal/util/ValueProvider.java b/processor/src/main/java/org/mapstruct/ap/internal/util/ValueProvider.java index bbd73943db..16abd074dd 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/util/ValueProvider.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/util/ValueProvider.java @@ -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. @@ -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 += "()"; } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/util/accessor/AccessorType.java b/processor/src/main/java/org/mapstruct/ap/internal/util/accessor/AccessorType.java index 23448a9e98..4e3b7a4776 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/util/accessor/AccessorType.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/util/accessor/AccessorType.java @@ -12,6 +12,8 @@ public enum AccessorType { GETTER, SETTER, ADDER, + MAP_GET, + MAP_CONTAINS, PRESENCE_CHECKER; public boolean isFieldAssignment() { diff --git a/processor/src/main/java/org/mapstruct/ap/internal/util/accessor/MapValueAccessor.java b/processor/src/main/java/org/mapstruct/ap/internal/util/accessor/MapValueAccessor.java new file mode 100644 index 0000000000..ab086d9b0e --- /dev/null +++ b/processor/src/main/java/org/mapstruct/ap/internal/util/accessor/MapValueAccessor.java @@ -0,0 +1,55 @@ +/* + * 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.util.accessor; + +import java.util.Collections; +import java.util.Set; +import javax.lang.model.element.Element; +import javax.lang.model.element.Modifier; +import javax.lang.model.type.TypeMirror; + +/** + * An {@link Accessor} that wraps a Map value. + * + * @author Christian Kosmowski + */ +public class MapValueAccessor implements Accessor { + + private final TypeMirror valueTypeMirror; + private final String simpleName; + private final Element element; + + public MapValueAccessor(Element element, TypeMirror valueTypeMirror, String simpleName) { + this.element = element; + this.valueTypeMirror = valueTypeMirror; + this.simpleName = simpleName; + } + + @Override + public TypeMirror getAccessedType() { + return valueTypeMirror; + } + + @Override + public String getSimpleName() { + return this.simpleName; + } + + @Override + public Set getModifiers() { + return Collections.emptySet(); + } + + @Override + public Element getElement() { + return this.element; + } + + @Override + public AccessorType getAccessorType() { + return AccessorType.MAP_GET; + } +} diff --git a/processor/src/main/java/org/mapstruct/ap/internal/util/accessor/MapValuePresenceChecker.java b/processor/src/main/java/org/mapstruct/ap/internal/util/accessor/MapValuePresenceChecker.java new file mode 100644 index 0000000000..d3d4d735f4 --- /dev/null +++ b/processor/src/main/java/org/mapstruct/ap/internal/util/accessor/MapValuePresenceChecker.java @@ -0,0 +1,55 @@ +/* + * 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.util.accessor; + +import java.util.Collections; +import java.util.Set; +import javax.lang.model.element.Element; +import javax.lang.model.element.Modifier; +import javax.lang.model.type.TypeMirror; + +/** + * An {@link Accessor} that wraps a Map value. + * + * @author Christian Kosmowski + */ +public class MapValuePresenceChecker implements Accessor { + + private final Element element; + private final TypeMirror valueTypeMirror; + private final String simpleName; + + public MapValuePresenceChecker(Element element, TypeMirror valueTypeMirror, String simpleName) { + this.element = element; + this.valueTypeMirror = valueTypeMirror; + this.simpleName = simpleName; + } + + @Override + public TypeMirror getAccessedType() { + return valueTypeMirror; + } + + @Override + public String getSimpleName() { + return this.simpleName; + } + + @Override + public Set getModifiers() { + return Collections.emptySet(); + } + + @Override + public Element getElement() { + return this.getElement(); + } + + @Override + public AccessorType getAccessorType() { + return AccessorType.MAP_CONTAINS; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/frommap/FromMapMappingTest.java b/processor/src/test/java/org/mapstruct/ap/test/frommap/FromMapMappingTest.java new file mode 100644 index 0000000000..d817809f83 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/frommap/FromMapMappingTest.java @@ -0,0 +1,187 @@ +/* + * 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.HashMap; +import java.util.Map; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.compilation.annotation.CompilationResult; +import org.mapstruct.ap.testutil.compilation.annotation.Diagnostic; +import org.mapstruct.ap.testutil.compilation.annotation.ExpectedCompilationOutcome; +import org.mapstruct.ap.testutil.runner.AnnotationProcessorTestRunner; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Christian Kosmowski + */ +@RunWith(AnnotationProcessorTestRunner.class) +@IssueKey("1075") +public class FromMapMappingTest { + + @Test + @WithClasses(MapToBeanDefinedMapper.class) + public void shouldMapWithDefinedMapping() { + Map sourceMap = new HashMap<>(); + sourceMap.put( "number", 44 ); + + MapToBeanDefinedMapper.Target target = MapToBeanDefinedMapper.INSTANCE.toTarget( sourceMap ); + + assertThat( target ).isNotNull(); + assertThat( target.getNormalInt() ).isEqualTo( "44" ); + } + + @Test + @WithClasses(MapToBeanImplicitMapper.class) + public void shouldMapWithImpicitMapping() { + Map sourceMap = new HashMap<>(); + sourceMap.put( "name", "mapstruct" ); + + MapToBeanImplicitMapper.Target target = MapToBeanImplicitMapper.INSTANCE.toTarget( sourceMap ); + + assertThat( target ).isNotNull(); + assertThat( target.getName() ).isEqualTo( "mapstruct" ); + } + + @Test + @WithClasses(MapToBeanUpdateImplicitMapper.class) + public void shouldMapToExistingTargetWithImpicitMapping() { + Map sourceMap = new HashMap<>(); + sourceMap.put( "rating", 5 ); + + MapToBeanUpdateImplicitMapper.Target existingTarget = new MapToBeanUpdateImplicitMapper.Target(); + existingTarget.setRating( 4 ); + existingTarget.setName( "mapstruct" ); + + MapToBeanUpdateImplicitMapper.Target target = MapToBeanUpdateImplicitMapper.INSTANCE + .toTarget( existingTarget, sourceMap ); + + assertThat( target ).isNotNull(); + assertThat( target.getName() ).isEqualTo( "mapstruct" ); + assertThat( target.getRating() ).isEqualTo( 5 ); + } + + @Test + @WithClasses(MapToBeanWithDefaultMapper.class) + public void shouldMapWithDefaultValue() { + Map sourceMap = new HashMap<>(); + + MapToBeanWithDefaultMapper.Target target = MapToBeanWithDefaultMapper.INSTANCE + .toTarget( sourceMap ); + + assertThat( target ).isNotNull(); + assertThat( target.getNormalInt() ).isEqualTo( "4711" ); + } + + @Test + @WithClasses(MapToBeanUsingMappingMethodMapper.class) + public void shouldMapUsingMappingMethod() { + Map sourceMap = new HashMap<>(); + sourceMap.put( "number", 23 ); + + MapToBeanUsingMappingMethodMapper.Target target = MapToBeanUsingMappingMethodMapper.INSTANCE + .toTarget( sourceMap ); + + assertThat( target ).isNotNull(); + assertThat( target.getNormalInt() ).isEqualTo( "converted_23" ); + } + + @Test + @WithClasses(MapToBeanFromMultipleSources.class) + public void shouldMapFromMultipleSources() { + Map integers = new HashMap<>(); + integers.put( "number", 23 ); + + Map strings = new HashMap<>(); + strings.put( "string", "stringFromMap" ); + + MapToBeanFromMultipleSources.Source source = new MapToBeanFromMultipleSources.Source(); + + MapToBeanFromMultipleSources.Target target = MapToBeanFromMultipleSources.INSTANCE + .toTarget( integers, strings, source ); + + assertThat( target ).isNotNull(); + assertThat( target.getInteger() ).isEqualTo( 23 ); + assertThat( target.getString() ).isEqualTo( "stringFromMap" ); + assertThat( target.getStringFromBean() ).isEqualTo( "stringFromBean" ); + } + + @Test + @WithClasses(MapToBeanFromMapAndNestedSource.class) + public void shouldMapFromNestedSource() { + Map integers = new HashMap<>(); + integers.put( "number", 23 ); + + MapToBeanFromMapAndNestedSource.Source source = new MapToBeanFromMapAndNestedSource.Source(); + + MapToBeanFromMapAndNestedSource.Target target = MapToBeanFromMapAndNestedSource.INSTANCE + .toTarget( integers, source ); + + assertThat( target ).isNotNull(); + assertThat( target.getInteger() ).isEqualTo( 23 ); + assertThat( target.getStringFromNestedSource() ).isEqualTo( "nestedString" ); + } + + @Test + @WithClasses(MapToBeanFromMapAndNestedMap.class) + public void shouldMapFromNestedMap() { + + MapToBeanFromMapAndNestedMap.Source source = new MapToBeanFromMapAndNestedMap.Source(); + MapToBeanFromMapAndNestedMap.Target target = MapToBeanFromMapAndNestedMap.INSTANCE + .toTarget( source ); + + assertThat( target ).isNotNull(); + assertThat( target.getNestedTarget() ).isNotNull(); + assertThat( target.getNestedTarget().getStringFromNestedMap() ).isEqualTo( "valueFromNestedMap" ); + } + + @Test + @WithClasses(MapToBeanTypeCheckMapper.class) + @ExpectedCompilationOutcome( + value = CompilationResult.SUCCEEDED, + diagnostics = { + @Diagnostic(type = MapToBeanTypeCheckMapper.class, + kind = javax.tools.Diagnostic.Kind.WARNING, + line = 21, + message = "The Map parameter \"source\" cannot be used for property mapping. " + + "It must be typed with Map but it was typed with Map.") + } + ) + public void shouldWarnAboutWrongMapTypes() { + + Map upsideDownMap = new HashMap<>(); + upsideDownMap.put( 23, "number" ); + + MapToBeanTypeCheckMapper.Target target = MapToBeanTypeCheckMapper.INSTANCE + .toTarget( upsideDownMap ); + } + + @Test + @WithClasses(MapToBeanUntypedMapMapper.class) + @ExpectedCompilationOutcome( + value = CompilationResult.SUCCEEDED, + diagnostics = { + @Diagnostic(type = MapToBeanUntypedMapMapper.class, + kind = javax.tools.Diagnostic.Kind.WARNING, + line = 21, + message = "The Map parameter \"source\" cannot be used for property mapping. " + + "It must be typed with Map but it was untyped.") + } + ) + public void shouldWarnAboutUntypedMapTypes() { + + Map upsideDownMap = new HashMap<>(); + upsideDownMap.put( 23, "number" ); + + MapToBeanUntypedMapMapper.Target target = MapToBeanUntypedMapMapper.INSTANCE + .toTarget( upsideDownMap ); + } + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/frommap/MapToBeanDefinedMapper.java b/processor/src/test/java/org/mapstruct/ap/test/frommap/MapToBeanDefinedMapper.java new file mode 100644 index 0000000000..833f7e5620 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/frommap/MapToBeanDefinedMapper.java @@ -0,0 +1,38 @@ +/* + * 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 MapToBeanDefinedMapper { + + MapToBeanDefinedMapper INSTANCE = Mappers.getMapper( MapToBeanDefinedMapper.class ); + + @Mapping(source = "number", target = "normalInt") + Target toTarget(Map source); + + class Target { + + private String normalInt; + + public String getNormalInt() { + return normalInt; + } + + public void setNormalInt(String normalInt) { + this.normalInt = normalInt; + } + } + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/frommap/MapToBeanFromMapAndNestedMap.java b/processor/src/test/java/org/mapstruct/ap/test/frommap/MapToBeanFromMapAndNestedMap.java new file mode 100644 index 0000000000..da47368e68 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/frommap/MapToBeanFromMapAndNestedMap.java @@ -0,0 +1,69 @@ +/* + * 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.HashMap; +import java.util.Map; + +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +/** + * @author Christian Kosmowski + */ +@Mapper +public interface MapToBeanFromMapAndNestedMap { + + MapToBeanFromMapAndNestedMap INSTANCE = Mappers.getMapper( MapToBeanFromMapAndNestedMap.class ); + + Target toTarget(Source source); + + //NestedTarget toNestedTarget(Map nestedMap); + + class Source { + + private Map nestedTarget = new HashMap<>( ); + + public Map getNestedTarget() { + return nestedTarget; + } + + public void setNestedTarget(Map nestedTarget) { + this.nestedTarget = nestedTarget; + } + + public Source() { + nestedTarget.put( "stringFromNestedMap", "valueFromNestedMap" ); + } + } + + class Target { + + private NestedTarget nestedTarget; + + public NestedTarget getNestedTarget() { + return nestedTarget; + } + + public void setNestedTarget(NestedTarget nestedTarget) { + this.nestedTarget = nestedTarget; + } + + } + + class NestedTarget { + private String stringFromNestedMap; + + public String getStringFromNestedMap() { + return stringFromNestedMap; + } + + public void setStringFromNestedMap(String stringFromNestedMap) { + this.stringFromNestedMap = stringFromNestedMap; + } + } + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/frommap/MapToBeanFromMapAndNestedSource.java b/processor/src/test/java/org/mapstruct/ap/test/frommap/MapToBeanFromMapAndNestedSource.java new file mode 100644 index 0000000000..407e66add2 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/frommap/MapToBeanFromMapAndNestedSource.java @@ -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; + +/** + * @author Christian Kosmowski + */ +@Mapper +public interface MapToBeanFromMapAndNestedSource { + + MapToBeanFromMapAndNestedSource INSTANCE = Mappers.getMapper( MapToBeanFromMapAndNestedSource.class ); + + @Mapping(source = "integers.number", target = "integer") + @Mapping(source = "source.nestedSource.nestedString", target = "stringFromNestedSource") + Target toTarget(Map integers, Source source); + + class Source { + + private String stringFromBean = "stringFromBean"; + private NestedSource nestedSource = new NestedSource(); + + public String getStringFromBean() { + return stringFromBean; + } + + public void setStringFromBean(String stringFromBean) { + this.stringFromBean = stringFromBean; + } + + public NestedSource getNestedSource() { + return nestedSource; + } + + public void setNestedSource(NestedSource nestedSource) { + this.nestedSource = nestedSource; + } + + class NestedSource { + + private String nestedString = "nestedString"; + + public String getNestedString() { + return nestedString; + } + } + } + + class Target { + + private int integer; + private String stringFromNestedSource; + + public int getInteger() { + return integer; + } + + public void setInteger(int integer) { + this.integer = integer; + } + + public String getStringFromNestedSource() { + return stringFromNestedSource; + } + + public void setStringFromNestedSource(String stringFromNestedSource) { + this.stringFromNestedSource = stringFromNestedSource; + } + } + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/frommap/MapToBeanFromMultipleSources.java b/processor/src/test/java/org/mapstruct/ap/test/frommap/MapToBeanFromMultipleSources.java new file mode 100644 index 0000000000..971105acf9 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/frommap/MapToBeanFromMultipleSources.java @@ -0,0 +1,70 @@ +/* + * 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 MapToBeanFromMultipleSources { + + MapToBeanFromMultipleSources INSTANCE = Mappers.getMapper( MapToBeanFromMultipleSources.class ); + + @Mapping(source = "integers.number", target = "integer") + @Mapping(source = "strings.string", target = "string") + @Mapping(source = "bean.stringFromBean", target = "stringFromBean") + Target toTarget(Map integers, Map strings, Source bean); + + class Source { + private String stringFromBean = "stringFromBean"; + + public String getStringFromBean() { + return stringFromBean; + } + + public void setStringFromBean(String stringFromBean) { + this.stringFromBean = stringFromBean; + } + } + + class Target { + + private int integer; + private String string; + private String stringFromBean; + + public int getInteger() { + return integer; + } + + public void setInteger(int integer) { + this.integer = integer; + } + + public String getString() { + return string; + } + + public void setString(String string) { + this.string = string; + } + + public String getStringFromBean() { + return stringFromBean; + } + + public void setStringFromBean(String stringFromBean) { + this.stringFromBean = stringFromBean; + } + } + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/frommap/MapToBeanImplicitMapper.java b/processor/src/test/java/org/mapstruct/ap/test/frommap/MapToBeanImplicitMapper.java new file mode 100644 index 0000000000..ca4b7a3d21 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/frommap/MapToBeanImplicitMapper.java @@ -0,0 +1,36 @@ +/* + * 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.factory.Mappers; + +/** + * @author Christian Kosmowski + */ +@Mapper +public interface MapToBeanImplicitMapper { + + MapToBeanImplicitMapper INSTANCE = Mappers.getMapper( MapToBeanImplicitMapper.class ); + + Target toTarget(Map source); + + class Target { + + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/frommap/MapToBeanTypeCheckMapper.java b/processor/src/test/java/org/mapstruct/ap/test/frommap/MapToBeanTypeCheckMapper.java new file mode 100644 index 0000000000..bdb06be3b8 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/frommap/MapToBeanTypeCheckMapper.java @@ -0,0 +1,26 @@ +/* + * 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.factory.Mappers; + +/** + * @author Christian Kosmowski + */ +@Mapper +public interface MapToBeanTypeCheckMapper { + + MapToBeanTypeCheckMapper INSTANCE = Mappers.getMapper( MapToBeanTypeCheckMapper.class ); + + Target toTarget(Map source); + + class Target { + } + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/frommap/MapToBeanUntypedMapMapper.java b/processor/src/test/java/org/mapstruct/ap/test/frommap/MapToBeanUntypedMapMapper.java new file mode 100644 index 0000000000..442ac18592 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/frommap/MapToBeanUntypedMapMapper.java @@ -0,0 +1,26 @@ +/* + * 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.factory.Mappers; + +/** + * @author Christian Kosmowski + */ +@Mapper +public interface MapToBeanUntypedMapMapper { + + MapToBeanUntypedMapMapper INSTANCE = Mappers.getMapper( MapToBeanUntypedMapMapper.class ); + + Target toTarget(Map source); + + class Target { + } + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/frommap/MapToBeanUpdateImplicitMapper.java b/processor/src/test/java/org/mapstruct/ap/test/frommap/MapToBeanUpdateImplicitMapper.java new file mode 100644 index 0000000000..82fe906bfe --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/frommap/MapToBeanUpdateImplicitMapper.java @@ -0,0 +1,47 @@ +/* + * 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.MappingTarget; +import org.mapstruct.NullValuePropertyMappingStrategy; +import org.mapstruct.factory.Mappers; + +/** + * @author Christian Kosmowski + */ +@Mapper(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE) +public interface MapToBeanUpdateImplicitMapper { + + MapToBeanUpdateImplicitMapper INSTANCE = Mappers.getMapper( MapToBeanUpdateImplicitMapper.class ); + + Target toTarget(@MappingTarget Target target, Map source); + + class Target { + + private String name; + private Integer rating; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Integer getRating() { + return rating; + } + + public void setRating(Integer rating) { + this.rating = rating; + } + } + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/frommap/MapToBeanUsingMappingMethodMapper.java b/processor/src/test/java/org/mapstruct/ap/test/frommap/MapToBeanUsingMappingMethodMapper.java new file mode 100644 index 0000000000..faab37bca8 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/frommap/MapToBeanUsingMappingMethodMapper.java @@ -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 MapToBeanUsingMappingMethodMapper { + + MapToBeanUsingMappingMethodMapper INSTANCE = Mappers.getMapper( MapToBeanUsingMappingMethodMapper.class ); + + @Mapping(source = "number", target = "normalInt") + Target toTarget(Map source); + + default String mapIntegerToString( Integer input ) { + return "converted_" + input; + } + + class Target { + + private String normalInt; + + public String getNormalInt() { + return normalInt; + } + + public void setNormalInt(String normalInt) { + this.normalInt = normalInt; + } + } + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/frommap/MapToBeanWithDefaultMapper.java b/processor/src/test/java/org/mapstruct/ap/test/frommap/MapToBeanWithDefaultMapper.java new file mode 100644 index 0000000000..ad3073ff57 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/frommap/MapToBeanWithDefaultMapper.java @@ -0,0 +1,38 @@ +/* + * 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 MapToBeanWithDefaultMapper { + + MapToBeanWithDefaultMapper INSTANCE = Mappers.getMapper( MapToBeanWithDefaultMapper.class ); + + @Mapping(source = "number", target = "normalInt", defaultValue = "4711") + Target toTarget(Map source); + + class Target { + + private String normalInt; + + public String getNormalInt() { + return normalInt; + } + + public void setNormalInt(String normalInt) { + this.normalInt = normalInt; + } + } + +}