diff --git a/documentation/src/main/asciidoc/chapter-3-defining-a-mapper.asciidoc b/documentation/src/main/asciidoc/chapter-3-defining-a-mapper.asciidoc index f679d4494a..2d2b930114 100644 --- a/documentation/src/main/asciidoc/chapter-3-defining-a-mapper.asciidoc +++ b/documentation/src/main/asciidoc/chapter-3-defining-a-mapper.asciidoc @@ -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` 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 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 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` 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. +The warning is not generated if the map itself is mapped into some other target property directly as is. +==== 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 2dba4637fd..09cc20a6ed 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 @@ -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 readAccessors = sourceParameter.getType().getPropertyReadAccessors(); for ( Entry entry : readAccessors.entrySet() ) { @@ -276,6 +278,7 @@ else if ( !method.isUpdateMethod() ) { // map parameters without a mapping applyParameterNameBasedMapping(); + } // Process the unprocessed defined targets @@ -288,6 +291,7 @@ else if ( !method.isUpdateMethod() ) { reportErrorForUnmappedTargetPropertiesIfRequired(); reportErrorForUnmappedSourcePropertiesIfRequired(); reportErrorForMissingIgnoredSourceProperties(); + reportErrorForUnusedSourceParameters(); // mapNullToDefault boolean mapNullToDefault = method.getOptions() @@ -1364,6 +1368,16 @@ private SourceReference getSourceRefByTargetName(Parameter sourceParameter, Stri return sourceRef; } + if ( sourceParameter.getType().isMapType() ) { + List 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 ) { @@ -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 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 { 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 b53b8f90ec..82d2dd53fa 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 @@ -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; @@ -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() ) ) { @@ -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 presenceChecks = new ArrayList<>(); presenceChecks.add( new SourceReferenceMethodPresenceCheck( sourceParam.getName(), @@ -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 ) { 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 4f2d0957e7..15b10b5782 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,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; @@ -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; @@ -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 entries = Collections.singletonList( + PropertyEntry.forSourceReference( segments, mapValueAccessor, mapValuePresenceChecker, valueType ) + ); + return new SourceReference( parameter, entries, true ); + } + /** * Builds a {@link SourceReference} from an {@code @Mappping}. */ @@ -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; @@ -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]; @@ -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 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 diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/presence/SourceReferenceContainsKeyPresenceCheck.java b/processor/src/main/java/org/mapstruct/ap/internal/model/presence/SourceReferenceContainsKeyPresenceCheck.java new file mode 100644 index 0000000000..d4c011a051 --- /dev/null +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/presence/SourceReferenceContainsKeyPresenceCheck.java @@ -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 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 ); + } +} 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 4bacef6a07..f2226ab224 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 @@ -181,7 +181,11 @@ public enum Message { 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_THROW_EXCEPTION_SOURCE( "Source = \"\" is not allowed. Target = \"\" can only be used." ); + VALUEMAPPING_THROW_EXCEPTION_SOURCE( "Source = \"\" is not allowed. Target = \"\" can only be used." ), + + MAPTOBEANMAPPING_WRONG_KEY_TYPE( "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_RAW_MAP( "The Map parameter \"%s\" cannot be used for property mapping. It must be typed with Map but it was raw.", 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..3f72ed86c1 --- /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.element; + } + + @Override + public AccessorType getAccessorType() { + return AccessorType.MAP_CONTAINS; + } +} diff --git a/processor/src/main/resources/org/mapstruct/ap/internal/model/presence/SourceReferenceContainsKeyPresenceCheck.ftl b/processor/src/main/resources/org/mapstruct/ap/internal/model/presence/SourceReferenceContainsKeyPresenceCheck.ftl new file mode 100644 index 0000000000..5cffc481a1 --- /dev/null +++ b/processor/src/main/resources/org/mapstruct/ap/internal/model/presence/SourceReferenceContainsKeyPresenceCheck.ftl @@ -0,0 +1,9 @@ +<#-- + + Copyright MapStruct Authors. + + Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + +--> +<#-- @ftlvariable name="" type="org.mapstruct.ap.internal.model.presence.SourceReferenceContainsKeyPresenceCheck" --> +${sourceReference}.containsKey( "${propertyName}" ) \ No newline at end of file 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..548aecc1ec --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/frommap/FromMapMappingTest.java @@ -0,0 +1,371 @@ +/* + * 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.text.ParseException; +import java.time.LocalDate; +import java.time.Month; +import java.time.format.DateTimeParseException; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.junit.jupiter.api.Nested; +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +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 static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assertions.entry; + +/** + * @author Christian Kosmowski + */ +@IssueKey("1075") +class FromMapMappingTest { + + @Nested + @WithClasses({ + StringMapToBeanMapper.class + }) + class StringMapToBeanTests { + + @ProcessorTest + void fromNullMap() { + assertThat( StringMapToBeanMapper.INSTANCE.fromMap( null ) ).isNull(); + } + + @ProcessorTest + void fromEmptyMap() { + StringMapToBeanMapper.Order order = StringMapToBeanMapper.INSTANCE.fromMap( Collections.emptyMap() ); + + assertThat( order ).isNotNull(); + assertThat( order.getName() ).isNull(); + assertThat( order.getPrice() ).isEqualTo( 0.0 ); + assertThat( order.getOrderDate() ).isNull(); + assertThat( order.getShipmentDate() ).isNull(); + } + + @ProcessorTest + void fromFullMap() { + Map map = new HashMap<>(); + map.put( "name", "Jacket" ); + map.put( "price", "25.5" ); + map.put( "shipmentDate", "2021-06-15" ); + StringMapToBeanMapper.Order order = StringMapToBeanMapper.INSTANCE.fromMap( map ); + + assertThat( order ).isNotNull(); + assertThat( order.getName() ).isEqualTo( "Jacket" ); + assertThat( order.getPrice() ).isEqualTo( 25.5 ); + assertThat( order.getOrderDate() ).isNull(); + assertThat( order.getShipmentDate() ).isEqualTo( LocalDate.of( 2021, Month.JUNE, 15 ) ); + } + + @ProcessorTest + void fromMapWithEmptyValuesForString() { + Map map = Collections.singletonMap( "name", "" ); + StringMapToBeanMapper.Order order = StringMapToBeanMapper.INSTANCE.fromMap( map ); + + assertThat( order ).isNotNull(); + assertThat( order.getName() ).isEqualTo( "" ); + assertThat( order.getPrice() ).isEqualTo( 0 ); + assertThat( order.getOrderDate() ).isNull(); + assertThat( order.getShipmentDate() ).isNull(); + } + + @ProcessorTest + void fromMapWithEmptyValuesForDouble() { + Map map = Collections.singletonMap( "price", "" ); + assertThatThrownBy( () -> StringMapToBeanMapper.INSTANCE.fromMap( map ) ) + .isInstanceOf( NumberFormatException.class ); + } + + @ProcessorTest + void fromMapWithEmptyValuesForDate() { + Map map = Collections.singletonMap( "orderDate", "" ); + assertThatThrownBy( () -> StringMapToBeanMapper.INSTANCE.fromMap( map ) ) + .isInstanceOf( RuntimeException.class ) + .getCause() + .isInstanceOf( ParseException.class ); + } + + @ProcessorTest + void fromMapWithEmptyValuesForLocalDate() { + Map map = Collections.singletonMap( "shipmentDate", "" ); + assertThatThrownBy( () -> StringMapToBeanMapper.INSTANCE.fromMap( map ) ) + .isInstanceOf( DateTimeParseException.class ); + } + } + + @Nested + @WithClasses({ + StringMapToBeanWithCustomPresenceCheckMapper.class + }) + class StringMapToBeanWithCustomPresenceCheckTests { + + @ProcessorTest + void fromFullMap() { + Map map = new HashMap<>(); + map.put( "name", "Jacket" ); + map.put( "price", "25.5" ); + map.put( "shipmentDate", "2021-06-15" ); + StringMapToBeanWithCustomPresenceCheckMapper.Order order = + StringMapToBeanWithCustomPresenceCheckMapper.INSTANCE.fromMap( map ); + + assertThat( order ).isNotNull(); + assertThat( order.getName() ).isEqualTo( "Jacket" ); + assertThat( order.getPrice() ).isEqualTo( 25.5 ); + assertThat( order.getOrderDate() ).isNull(); + assertThat( order.getShipmentDate() ).isEqualTo( LocalDate.of( 2021, Month.JUNE, 15 ) ); + } + + @ProcessorTest + void fromMapWithEmptyValuesForString() { + Map map = new HashMap<>(); + map.put( "name", "" ); + map.put( "price", "" ); + map.put( "orderDate", "" ); + map.put( "shipmentDate", "" ); + StringMapToBeanWithCustomPresenceCheckMapper.Order order = + StringMapToBeanWithCustomPresenceCheckMapper.INSTANCE.fromMap( map ); + + assertThat( order ).isNotNull(); + assertThat( order.getName() ).isNull(); + assertThat( order.getPrice() ).isEqualTo( 0 ); + assertThat( order.getOrderDate() ).isNull(); + assertThat( order.getShipmentDate() ).isNull(); + } + + } + + @ProcessorTest + @WithClasses(MapToBeanDefinedMapper.class) + 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" ); + } + + @ProcessorTest + @WithClasses(MapToBeanImplicitMapper.class) + 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" ); + } + + @ProcessorTest + @WithClasses(MapToBeanUpdateImplicitMapper.class) + void shouldMapToExistingTargetWithImplicitMapping() { + 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 ); + } + + @ProcessorTest + @WithClasses(MapToBeanWithDefaultMapper.class) + void shouldMapWithDefaultValue() { + Map sourceMap = new HashMap<>(); + + MapToBeanWithDefaultMapper.Target target = MapToBeanWithDefaultMapper.INSTANCE + .toTarget( sourceMap ); + + assertThat( target ).isNotNull(); + assertThat( target.getNormalInt() ).isEqualTo( "4711" ); + } + + @ProcessorTest + @WithClasses(MapToBeanUsingMappingMethodMapper.class) + 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" ); + } + + @ProcessorTest + @WithClasses(MapToBeanFromMultipleSources.class) + 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" ); + } + + @ProcessorTest + @WithClasses(MapToBeanFromMapAndNestedSource.class) + 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" ); + } + + @ProcessorTest + @WithClasses(MapToBeanFromMapAndNestedMap.class) + 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" ); + } + + @ProcessorTest + @WithClasses(ObjectMapToBeanWithQualifierMapper.class) + void shouldUseObjectQualifiedMethod() { + Map vehicles = new HashMap<>(); + vehicles.put( "car", new ObjectMapToBeanWithQualifierMapper.Car( "Tesla" ) ); + + ObjectMapToBeanWithQualifierMapper.VehiclesDto dto = + ObjectMapToBeanWithQualifierMapper.INSTANCE.map( vehicles ); + + assertThat( dto.getCar() ).isNotNull(); + assertThat( dto.getCar() ).isInstanceOf( ObjectMapToBeanWithQualifierMapper.CarDto.class ); + assertThat( dto.getCar().getBrand() ).isEqualTo( "Tesla" ); + } + + @ProcessorTest + @WithClasses(MapToBeanTypeCheckMapper.class) + @ExpectedCompilationOutcome( + value = CompilationResult.SUCCEEDED, + diagnostics = { + @Diagnostic( + type = MapToBeanTypeCheckMapper.class, + kind = javax.tools.Diagnostic.Kind.WARNING, + line = 21, + message = "Unmapped target property: \"value\"." + ), + @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." + ), + } + ) + void shouldWarnAboutWrongMapTypes() { + + Map upsideDownMap = new HashMap<>(); + upsideDownMap.put( 23, "number" ); + + MapToBeanTypeCheckMapper.Target target = MapToBeanTypeCheckMapper.INSTANCE + .toTarget( upsideDownMap ); + + assertThat( target ).isNotNull(); + assertThat( target.getValue() ).isNull(); + } + + @ProcessorTest + @WithClasses(MapToBeanRawMapMapper.class) + @ExpectedCompilationOutcome( + value = CompilationResult.SUCCEEDED, + diagnostics = { + @Diagnostic( + type = MapToBeanRawMapMapper.class, + kind = javax.tools.Diagnostic.Kind.WARNING, + line = 21, + message = "Unmapped target property: \"value\"." + ), + @Diagnostic( + type = MapToBeanRawMapMapper.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 raw." + ), + } + ) + void shouldWarnAboutRawMapTypes() { + + Map rawMap = new HashMap<>(); + rawMap.put( "value", "number" ); + + MapToBeanRawMapMapper.Target target = MapToBeanRawMapMapper.INSTANCE + .toTarget( rawMap ); + + assertThat( target ).isNotNull(); + assertThat( target.getValue() ).isNull(); + } + + @ProcessorTest + @WithClasses({ + MapToBeanNonStringMapAsMultiSourceMapper.class + }) + void shouldNotWarnIfMappedIsUsedAsSourceParameter() { + MapToBeanNonStringMapAsMultiSourceMapper.Target target = MapToBeanNonStringMapAsMultiSourceMapper.INSTANCE + .toTarget( + new MapToBeanNonStringMapAsMultiSourceMapper.Source( "test" ), + Collections.singletonMap( 10, "value" ) + ); + + assertThat( target.getValue() ).isEqualTo( "test" ); + assertThat( target.getMap() ) + .containsOnly( entry( "10", "value" ) ); + } + + @ProcessorTest + @WithClasses(MapToBeanImplicitUnmappedSourcePolicyMapper.class) + void shouldNotReportUnmappedSourcePropertiesWithMap() { + Map sourceMap = new HashMap<>(); + sourceMap.put( "name", "mapstruct" ); + + MapToBeanImplicitUnmappedSourcePolicyMapper.Target target = + MapToBeanImplicitUnmappedSourcePolicyMapper.INSTANCE.toTarget( sourceMap ); + + assertThat( target ).isNotNull(); + assertThat( target.getName() ).isEqualTo( "mapstruct" ); + } + +} 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..eef8c285b3 --- /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(target = "normalInt", source = "number") + 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..f1b21263a4 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/frommap/MapToBeanFromMapAndNestedMap.java @@ -0,0 +1,67 @@ +/* + * 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); + + 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..c66080332e --- /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(target = "integer", source = "integers.number") + @Mapping(target = "stringFromNestedSource", source = "source.nestedSource.nestedString") + 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..c5c9b2e665 --- /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(target = "integer", source = "integers.number") + @Mapping(target = "string", source = "strings.string") + @Mapping(target = "stringFromBean", source = "bean.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/MapToBeanImplicitUnmappedSourcePolicyMapper.java b/processor/src/test/java/org/mapstruct/ap/test/frommap/MapToBeanImplicitUnmappedSourcePolicyMapper.java new file mode 100644 index 0000000000..2c72dc7b70 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/frommap/MapToBeanImplicitUnmappedSourcePolicyMapper.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.ReportingPolicy; +import org.mapstruct.factory.Mappers; + +/** + * @author Christian Kosmowski + */ +@Mapper(unmappedSourcePolicy = ReportingPolicy.ERROR) +public interface MapToBeanImplicitUnmappedSourcePolicyMapper { + + MapToBeanImplicitUnmappedSourcePolicyMapper INSTANCE = + Mappers.getMapper( MapToBeanImplicitUnmappedSourcePolicyMapper.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/MapToBeanNonStringMapAsMultiSourceMapper.java b/processor/src/test/java/org/mapstruct/ap/test/frommap/MapToBeanNonStringMapAsMultiSourceMapper.java new file mode 100644 index 0000000000..2606a9a037 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/frommap/MapToBeanNonStringMapAsMultiSourceMapper.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.test.frommap; + +import java.util.Map; + +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +/** + * @author Christian Kosmowski + */ +@Mapper +public interface MapToBeanNonStringMapAsMultiSourceMapper { + + MapToBeanNonStringMapAsMultiSourceMapper INSTANCE = + Mappers.getMapper( MapToBeanNonStringMapAsMultiSourceMapper.class ); + + Target toTarget(Source source, Map map); + + class Source { + private final String value; + + public Source(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + } + + class Target { + + private final String value; + private final Map map; + + public Target(String value, Map map) { + this.value = value; + this.map = map; + } + + public String getValue() { + return value; + } + + public Map getMap() { + return map; + } + } + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/frommap/MapToBeanRawMapMapper.java b/processor/src/test/java/org/mapstruct/ap/test/frommap/MapToBeanRawMapMapper.java new file mode 100644 index 0000000000..40215ab183 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/frommap/MapToBeanRawMapMapper.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 MapToBeanRawMapMapper { + + MapToBeanRawMapMapper INSTANCE = Mappers.getMapper( MapToBeanRawMapMapper.class ); + + Target toTarget(Map source); + + class Target { + + private final String value; + + public Target(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + } + +} 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..346c153d71 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/frommap/MapToBeanTypeCheckMapper.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 MapToBeanTypeCheckMapper { + + MapToBeanTypeCheckMapper INSTANCE = Mappers.getMapper( MapToBeanTypeCheckMapper.class ); + + Target toTarget(Map source); + + class Target { + + private final String value; + + public Target(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + } + +} 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..9a6344ff15 --- /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(target = "normalInt", source = "number") + 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..104d34bd0a --- /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(target = "normalInt", source = "number", defaultValue = "4711") + 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/ObjectMapToBeanWithQualifierMapper.java b/processor/src/test/java/org/mapstruct/ap/test/frommap/ObjectMapToBeanWithQualifierMapper.java new file mode 100644 index 0000000000..54ddb0e42a --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/frommap/ObjectMapToBeanWithQualifierMapper.java @@ -0,0 +1,74 @@ +/* + * 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.Named; +import org.mapstruct.factory.Mappers; + +/** + * @author Filip Hrisafov + */ +@Mapper +public interface ObjectMapToBeanWithQualifierMapper { + + ObjectMapToBeanWithQualifierMapper INSTANCE = Mappers.getMapper( ObjectMapToBeanWithQualifierMapper.class ); + + @Mapping( target = "car", qualifiedByName = "objectToCar") + VehiclesDto map(Map vehicles); + + @Named("objectToCar") + default CarDto objectToCar(Object object) { + CarDto car = new CarDto(); + + if ( object instanceof Car ) { + car.setBrand( ( (Car) object ).brand ); + } + + return car; + } + + class VehiclesDto { + + private VehicleDto car; + + public VehicleDto getCar() { + return car; + } + + public void setCar(VehicleDto car) { + this.car = car; + } + } + + class VehicleDto { + private String brand; + + public String getBrand() { + return brand; + } + + public void setBrand(String brand) { + this.brand = brand; + } + } + + class CarDto extends VehicleDto { + + } + + class Car { + + private final String brand; + + public Car(String brand) { + this.brand = brand; + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/frommap/StringMapToBeanMapper.java b/processor/src/test/java/org/mapstruct/ap/test/frommap/StringMapToBeanMapper.java new file mode 100644 index 0000000000..95b0f72597 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/frommap/StringMapToBeanMapper.java @@ -0,0 +1,67 @@ +/* + * 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.time.LocalDate; +import java.util.Date; +import java.util.Map; + +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +/** + * @author Filip Hrisafov + */ +@Mapper +public interface StringMapToBeanMapper { + StringMapToBeanMapper INSTANCE = Mappers.getMapper( StringMapToBeanMapper.class ); + + Order fromMap(Map map); + + class Order { + private String name; + private double price; + private Date orderDate; + private LocalDate shipmentDate; + + public Order() { + } + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + public double getPrice() { + return this.price; + } + + public void setPrice(double price) { + this.price = price; + } + + public Date getOrderDate() { + return this.orderDate; + } + + public void setOrderDate(Date orderDate) { + this.orderDate = orderDate; + } + + public LocalDate getShipmentDate() { + return this.shipmentDate; + } + + public void setShipmentDate(LocalDate shipmentDate) { + this.shipmentDate = shipmentDate; + } + } + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/frommap/StringMapToBeanWithCustomPresenceCheckMapper.java b/processor/src/test/java/org/mapstruct/ap/test/frommap/StringMapToBeanWithCustomPresenceCheckMapper.java new file mode 100644 index 0000000000..62c105599f --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/frommap/StringMapToBeanWithCustomPresenceCheckMapper.java @@ -0,0 +1,74 @@ +/* + * 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.time.LocalDate; +import java.util.Date; +import java.util.Map; + +import org.mapstruct.Condition; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +/** + * @author Filip Hrisafov + */ +@Mapper +public interface StringMapToBeanWithCustomPresenceCheckMapper { + StringMapToBeanWithCustomPresenceCheckMapper INSTANCE = + Mappers.getMapper( StringMapToBeanWithCustomPresenceCheckMapper.class ); + + Order fromMap(Map map); + + @Condition + default boolean isNotEmpty(String value) { + return value != null && !value.isEmpty(); + } + + class Order { + private String name; + private double price; + private Date orderDate; + private LocalDate shipmentDate; + + public Order() { + } + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + public double getPrice() { + return this.price; + } + + public void setPrice(double price) { + this.price = price; + } + + public Date getOrderDate() { + return this.orderDate; + } + + public void setOrderDate(Date orderDate) { + this.orderDate = orderDate; + } + + public LocalDate getShipmentDate() { + return this.shipmentDate; + } + + public void setShipmentDate(LocalDate shipmentDate) { + this.shipmentDate = shipmentDate; + } + } + +}