From 399458b2dd7c9d5d00e6dccc5ebf0adb488fcb90 Mon Sep 17 00:00:00 2001 From: Yang Date: Tue, 17 Jun 2025 21:37:29 +0800 Subject: [PATCH 1/3] feat: add @MappingSource annotation for implicit mapping control Support: 1. enabling Map-to-Bean implicit mapping in multi-source 2.disabling implicit mapping for bean --- .../java/org/mapstruct/MappingSource.java | 73 ++++++++++++ .../ap/internal/gem/GemGenerator.java | 6 +- .../ap/internal/model/BeanMappingMethod.java | 10 +- .../ap/internal/model/common/Parameter.java | 34 +++++- .../MappingSourceImplicitMappingMapper.java | 101 ++++++++++++++++ .../MappingSourceImplicitMappingTest.java | 63 ++++++++++ .../MappingSourceWithContextMapper.java | 111 ++++++++++++++++++ .../MappingSourceWithContextTest.java | 49 ++++++++ 8 files changed, 436 insertions(+), 11 deletions(-) create mode 100644 core/src/main/java/org/mapstruct/MappingSource.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/mappingsource/MappingSourceImplicitMappingMapper.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/mappingsource/MappingSourceImplicitMappingTest.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/mappingsource/MappingSourceWithContextMapper.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/mappingsource/MappingSourceWithContextTest.java diff --git a/core/src/main/java/org/mapstruct/MappingSource.java b/core/src/main/java/org/mapstruct/MappingSource.java new file mode 100644 index 0000000000..4e93412cce --- /dev/null +++ b/core/src/main/java/org/mapstruct/MappingSource.java @@ -0,0 +1,73 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Marks a parameter with specific handling behavior during mapping. + * This annotation controls how source parameters are handled in multi-source mapping scenarios. + *

+ * By default,: + * - all parameters have their properties automatically used for implicit mapping + * - Map parameters in multi-source scenarios don't have their entries automatically used for implicit mapping + *

+ * The implicitMapping attribute allows overriding these defaults: + * - For Bean parameters: setting implicitMapping=false prevents automatic property expansion + * - For Map parameters: setting implicitMapping=true enables using Map entries for implicit mapping + *

+ * This annotation is primarily useful in multi-source parameter scenarios. + * + *


+ * // Preventing a bean parameter's properties from being used in implicit mapping
+ * {@literal @}Mapper
+ * public interface MultiSourceMapper {
+ *     {@literal @}Mapping(target = "name", source = "otherSource.name")
+ *     TargetDto map(
+ *         {@literal @}MappingSource(implicitMapping = false) UserEntity user,
+ *          OtherSource otherSource);
+ * }
+ *
+ * // Enabling Map entries for implicit mapping in multi-source scenarios
+ * {@literal @}Mapper
+ * public interface MultiSourceMapper {
+ *     {@literal @}Mapping(target = "id", source = "entity.id")
+ *     TargetDto map({@literal @}MappingSource(implicitMapping = true)
+ *     Map<String, Object> sourceMap, Entity entity);
+ * }
+ * 
+ * + * @since 1.6.0 + */ +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.CLASS) +public @interface MappingSource { + + /** + * Controls whether this parameter participates in implicit mapping. + * The effect depends on the parameter type: + *

+ * - For Bean parameters: + * true (default): Properties are automatically used for implicit mapping + * false: Properties are not automatically used for implicit mapping + *

+ * - For Map parameters (in multi-source scenarios): + * true: Map entries are used for implicit mapping (similar to single-source behavior) + * false (default): Map entries are not automatically used for implicit mapping + *

+ * - For other types (Collection, Path, etc.): + * This setting has no effect as these types are never automatically expanded + *

+ * Note: This setting only affects implicit mapping. Explicit mappings defined with + * {@literal @}Mapping annotations are always processed regardless of this setting. + * + * @return whether implicit mapping should be enabled for this parameter + */ + boolean implicitMapping() default true; +} diff --git a/processor/src/main/java/org/mapstruct/ap/internal/gem/GemGenerator.java b/processor/src/main/java/org/mapstruct/ap/internal/gem/GemGenerator.java index a8b62babe9..60dfbf15e7 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/gem/GemGenerator.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/gem/GemGenerator.java @@ -18,6 +18,8 @@ import org.mapstruct.Context; import org.mapstruct.DecoratedWith; import org.mapstruct.EnumMapping; +import org.mapstruct.Ignored; +import org.mapstruct.IgnoredList; import org.mapstruct.InheritConfiguration; import org.mapstruct.InheritInverseConfiguration; import org.mapstruct.IterableMapping; @@ -26,8 +28,7 @@ import org.mapstruct.Mapper; import org.mapstruct.MapperConfig; import org.mapstruct.Mapping; -import org.mapstruct.Ignored; -import org.mapstruct.IgnoredList; +import org.mapstruct.MappingSource; import org.mapstruct.MappingTarget; import org.mapstruct.Mappings; import org.mapstruct.Named; @@ -68,6 +69,7 @@ @GemDefinition(TargetType.class) @GemDefinition(TargetPropertyName.class) @GemDefinition(MappingTarget.class) +@GemDefinition(MappingSource.class) @GemDefinition(DecoratedWith.class) @GemDefinition(MapperConfig.class) @GemDefinition(InheritConfiguration.class) 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 b5208d302b..cd568ae88b 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 @@ -1696,12 +1696,16 @@ private SourceReference getSourceRefByTargetName(Parameter sourceParameter, Stri SourceReference sourceRef = null; - if ( sourceParameter.getType().isPrimitive() || sourceParameter.getType().isArrayType() ) { - return sourceRef; + if ( ( sourceParameter.isMappingSource() && !sourceParameter.isImplicitMapping() ) + || sourceParameter.getType().isPrimitive() + || sourceParameter.getType().isArrayType() ) { + return null; } + boolean allowedMapToBean = + method.getSourceParameters().size() == 1 || ( sourceParameter.isImplicitMapping() ); ReadAccessor sourceReadAccessor = sourceParameter.getType() - .getReadAccessor( targetPropertyName, method.getSourceParameters().size() == 1 ); + .getReadAccessor( targetPropertyName, allowedMapToBean ); if ( sourceReadAccessor != null ) { // property mapping PresenceCheckAccessor sourcePresenceChecker = diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/common/Parameter.java b/processor/src/main/java/org/mapstruct/ap/internal/model/common/Parameter.java index aaab7f46ca..901a323145 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/common/Parameter.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/common/Parameter.java @@ -13,6 +13,7 @@ import javax.lang.model.element.VariableElement; import org.mapstruct.ap.internal.gem.ContextGem; +import org.mapstruct.ap.internal.gem.MappingSourceGem; import org.mapstruct.ap.internal.gem.MappingTargetGem; import org.mapstruct.ap.internal.gem.SourcePropertyNameGem; import org.mapstruct.ap.internal.gem.TargetPropertyNameGem; @@ -35,6 +36,8 @@ public class Parameter extends ModelElement { private final boolean mappingContext; private final boolean sourcePropertyName; private final boolean targetPropertyName; + private final boolean mappingSource; + private final boolean implicitMapping; private final boolean varArgs; @@ -48,12 +51,18 @@ private Parameter(Element element, Type type, boolean varArgs) { this.mappingContext = ContextGem.instanceOn( element ) != null; this.sourcePropertyName = SourcePropertyNameGem.instanceOn( element ) != null; this.targetPropertyName = TargetPropertyNameGem.instanceOn( element ) != null; + + // Handle MappingSource annotation + MappingSourceGem mappingSourceGem = MappingSourceGem.instanceOn( element ); + this.mappingSource = mappingSourceGem != null; + this.implicitMapping = this.mappingSource && mappingSourceGem.implicitMapping().get(); + this.varArgs = varArgs; } private Parameter(String name, Type type, boolean mappingTarget, boolean targetType, boolean mappingContext, - boolean sourcePropertyName, boolean targetPropertyName, - boolean varArgs) { + boolean sourcePropertyName, boolean targetPropertyName, boolean mappingSource, + boolean implicitMapping, boolean varArgs) { this.element = null; this.name = name; this.originalName = name; @@ -63,11 +72,13 @@ private Parameter(String name, Type type, boolean mappingTarget, boolean targetT this.mappingContext = mappingContext; this.sourcePropertyName = sourcePropertyName; this.targetPropertyName = targetPropertyName; + this.mappingSource = mappingSource; + this.implicitMapping = implicitMapping; this.varArgs = varArgs; } public Parameter(String name, Type type) { - this( name, type, false, false, false, false, false, false ); + this( name, type, false, false, false, false, false, false, false, false ); } public Element getElement() { @@ -105,7 +116,8 @@ private String format() { + ( mappingContext ? "@Context " : "" ) + ( sourcePropertyName ? "@SourcePropertyName " : "" ) + ( targetPropertyName ? "@TargetPropertyName " : "" ) - + "%s " + name; + + ( mappingSource ? "@MappingSource " : "" ) + + "%s " + name; } @Override @@ -129,6 +141,14 @@ public boolean isSourcePropertyName() { return sourcePropertyName; } + public boolean isMappingSource() { + return mappingSource; + } + + public boolean isImplicitMapping() { + return implicitMapping; + } + public boolean isVarArgs() { return varArgs; } @@ -136,9 +156,9 @@ public boolean isVarArgs() { public boolean isSourceParameter() { return !isMappingTarget() && !isTargetType() && - !isMappingContext() && !isSourcePropertyName() && - !isTargetPropertyName(); + !isTargetPropertyName() && + ( !isMappingContext() || isMappingSource() ); } @Override @@ -183,6 +203,8 @@ public static Parameter forForgedMappingTarget(Type parameterType) { false, false, false, + false, + false, false ); } diff --git a/processor/src/test/java/org/mapstruct/ap/test/mappingsource/MappingSourceImplicitMappingMapper.java b/processor/src/test/java/org/mapstruct/ap/test/mappingsource/MappingSourceImplicitMappingMapper.java new file mode 100644 index 0000000000..87e10e5717 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/mappingsource/MappingSourceImplicitMappingMapper.java @@ -0,0 +1,101 @@ +/* + * 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.mappingsource; + +import java.util.Map; + +import org.mapstruct.Mapper; +import org.mapstruct.MappingSource; +import org.mapstruct.ReportingPolicy; +import org.mapstruct.factory.Mappers; + +@Mapper(unmappedTargetPolicy = ReportingPolicy.IGNORE) +public interface MappingSourceImplicitMappingMapper { + + MappingSourceImplicitMappingMapper INSTANCE = Mappers.getMapper( MappingSourceImplicitMappingMapper.class ); + + MapTarget multiSourceWithImplicitMap(@MappingSource Map mapSource, OtherSource otherSource); + + BeanTarget multiSourceWithImplicitBean(@MappingSource(implicitMapping = false) BeanSource beanSource, + OtherSource otherSource); + + MapTarget singleWithImplicitMap(@MappingSource(implicitMapping = false) Map map); + + class MapTarget { + private String mapId; + private String mapName; + + public String getMapId() { + return mapId; + } + + public void setMapId(String mapId) { + this.mapId = mapId; + } + + public String getMapName() { + return mapName; + } + + public void setMapName(String mapName) { + this.mapName = mapName; + } + } + + class BeanTarget { + private Integer beanId; + private String beanName; + + public Integer getBeanId() { + return beanId; + } + + public void setBeanId(Integer beanId) { + this.beanId = beanId; + } + + public String getBeanName() { + return beanName; + } + + public void setBeanName(String beanName) { + this.beanName = beanName; + } + } + + class BeanSource { + private Integer beanId; + private String beanName; + + public Integer getBeanId() { + return beanId; + } + + public void setBeanId(Integer beanId) { + this.beanId = beanId; + } + + public String getBeanName() { + return beanName; + } + + public void setBeanName(String beanName) { + this.beanName = beanName; + } + } + + class OtherSource { + private final Long id; + + public OtherSource(Long id) { + this.id = id; + } + + public Long getId() { + return id; + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/mappingsource/MappingSourceImplicitMappingTest.java b/processor/src/test/java/org/mapstruct/ap/test/mappingsource/MappingSourceImplicitMappingTest.java new file mode 100644 index 0000000000..5449ec48d1 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/mappingsource/MappingSourceImplicitMappingTest.java @@ -0,0 +1,63 @@ +/* + * 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.mappingsource; + +import java.util.HashMap; +import java.util.Map; + +import org.mapstruct.ap.test.mappingsource.MappingSourceImplicitMappingMapper.BeanSource; +import org.mapstruct.ap.test.mappingsource.MappingSourceImplicitMappingMapper.BeanTarget; +import org.mapstruct.ap.test.mappingsource.MappingSourceImplicitMappingMapper.MapTarget; +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; + +import static org.assertj.core.api.Assertions.assertThat; + +@IssueKey("2559") +@WithClasses(MappingSourceImplicitMappingMapper.class) +public class MappingSourceImplicitMappingTest { + + @ProcessorTest + public void testMultiSourceWithImplicitMapping() { + Map mapSource = new HashMap<>(); + mapSource.put( "mapId", "1" ); + mapSource.put( "mapName", "MapTest" ); + + BeanSource beanSource = new MappingSourceImplicitMappingMapper.BeanSource(); + beanSource.setBeanId( 2 ); + beanSource.setBeanName( "BeanTest" ); + + MapTarget mapTarget = MappingSourceImplicitMappingMapper.INSTANCE.multiSourceWithImplicitMap( + mapSource, + null + ); + + assertThat( mapTarget ).isNotNull(); + assertThat( mapTarget.getMapId() ).isEqualTo( "1" ); + assertThat( mapTarget.getMapName() ).isEqualTo( "MapTest" ); + + BeanTarget beanTarget = MappingSourceImplicitMappingMapper.INSTANCE.multiSourceWithImplicitBean( + beanSource, + null + ); + assertThat( beanTarget ).isNotNull(); + assertThat( beanTarget.getBeanId() ).isNull(); + assertThat( beanTarget.getBeanName() ).isNull(); + } + + @ProcessorTest + public void shouldDisableImplicitMappingForSingleMapSource() { + Map mapSource = Map.of( "mapId", "1", "mapName", "MapTest" ); + + MapTarget mapTarget = MappingSourceImplicitMappingMapper.INSTANCE.singleWithImplicitMap( mapSource ); + + assertThat( mapTarget ).isNotNull(); + assertThat( mapTarget.getMapId() ).isNull(); + assertThat( mapTarget.getMapName() ).isNull(); + } +} + diff --git a/processor/src/test/java/org/mapstruct/ap/test/mappingsource/MappingSourceWithContextMapper.java b/processor/src/test/java/org/mapstruct/ap/test/mappingsource/MappingSourceWithContextMapper.java new file mode 100644 index 0000000000..41d3d2057c --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/mappingsource/MappingSourceWithContextMapper.java @@ -0,0 +1,111 @@ +/* + * 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.mappingsource; + +import java.util.List; +import java.util.stream.Collectors; + +import org.mapstruct.Context; +import org.mapstruct.Mapper; +import org.mapstruct.MappingSource; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface MappingSourceWithContextMapper { + + MappingSourceWithContextMapper INSTANCE = Mappers.getMapper( MappingSourceWithContextMapper.class ); + + default List toDto(Model model) { + return model + .getItems() + .stream() + .map( item -> this.toDto( item, model.getModelId() ) ) + .collect( Collectors.toList() ); + } + + ItemDTO toDto(Item item, @MappingSource @Context String modelId); + + class Model { + private List items; + private String modelId; + + public Model(String modelId) { + this.modelId = modelId; + } + + public List getItems() { + return items; + } + + public void setItems(List items) { + this.items = items; + } + + public String getModelId() { + return modelId; + } + + public void setModelId(String modelId) { + this.modelId = modelId; + } + } + + class Item { + private String itemId; + private String name; + + public Item(String itemId, String name) { + this.itemId = itemId; + this.name = name; + } + + public String getItemId() { + return itemId; + } + + public void setItemId(String itemId) { + this.itemId = itemId; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } + + class ItemDTO { + private String itemId; + private String name; + private String modelId; // This comes from @Context parameter + + public String getItemId() { + return itemId; + } + + public void setItemId(String itemId) { + this.itemId = itemId; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getModelId() { + return modelId; + } + + public void setModelId(String modelId) { + this.modelId = modelId; + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/mappingsource/MappingSourceWithContextTest.java b/processor/src/test/java/org/mapstruct/ap/test/mappingsource/MappingSourceWithContextTest.java new file mode 100644 index 0000000000..d9c4d43e56 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/mappingsource/MappingSourceWithContextTest.java @@ -0,0 +1,49 @@ +/* + * 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.mappingsource; + +import java.util.Arrays; +import java.util.List; + +import org.mapstruct.ap.test.mappingsource.MappingSourceWithContextMapper.Item; +import org.mapstruct.ap.test.mappingsource.MappingSourceWithContextMapper.ItemDTO; +import org.mapstruct.ap.test.mappingsource.MappingSourceWithContextMapper.Model; +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; + +import static org.assertj.core.api.Assertions.assertThat; + +@IssueKey("3665") +@WithClasses(MappingSourceWithContextMapper.class) +public class MappingSourceWithContextTest { + + @ProcessorTest + @WithClasses(MappingSourceWithContextMapper.class) + public void testMappingSourceWithContext() { + Model model = new Model( "MODEL-123" ); + List items = Arrays.asList( + new Item( "ITEM-1", "First Item" ), + new Item( "ITEM-2", "Second Item" ) + ); + model.setItems( items ); + + List result = MappingSourceWithContextMapper.INSTANCE.toDto( model ); + + assertThat( result ).isNotNull(); + assertThat( result ).hasSize( 2 ); + + ItemDTO dto1 = result.getFirst(); + assertThat( dto1.getItemId() ).isEqualTo( "ITEM-1" ); + assertThat( dto1.getName() ).isEqualTo( "First Item" ); + assertThat( dto1.getModelId() ).isEqualTo( "MODEL-123" ); + + ItemDTO dto2 = result.get( 1 ); + assertThat( dto2.getItemId() ).isEqualTo( "ITEM-2" ); + assertThat( dto2.getName() ).isEqualTo( "Second Item" ); + assertThat( dto2.getModelId() ).isEqualTo( "MODEL-123" ); + } +} From 3ebce8b71d2961c6a20ae4e47ab7819af3d886c9 Mon Sep 17 00:00:00 2001 From: Yang Date: Wed, 18 Jun 2025 00:07:48 +0800 Subject: [PATCH 2/3] feat: Support using @MappingSource to mark @Context parameters as source parameters --- .../MappingSourceWithContextMapper.java | 111 ++++++++++++++++++ .../MappingSourceWithContextTest.java | 49 ++++++++ 2 files changed, 160 insertions(+) create mode 100644 distribution/processor/src/test/java/org/mapstruct/ap/test/mappingsource/MappingSourceWithContextMapper.java create mode 100644 distribution/processor/src/test/java/org/mapstruct/ap/test/mappingsource/MappingSourceWithContextTest.java diff --git a/distribution/processor/src/test/java/org/mapstruct/ap/test/mappingsource/MappingSourceWithContextMapper.java b/distribution/processor/src/test/java/org/mapstruct/ap/test/mappingsource/MappingSourceWithContextMapper.java new file mode 100644 index 0000000000..41d3d2057c --- /dev/null +++ b/distribution/processor/src/test/java/org/mapstruct/ap/test/mappingsource/MappingSourceWithContextMapper.java @@ -0,0 +1,111 @@ +/* + * 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.mappingsource; + +import java.util.List; +import java.util.stream.Collectors; + +import org.mapstruct.Context; +import org.mapstruct.Mapper; +import org.mapstruct.MappingSource; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface MappingSourceWithContextMapper { + + MappingSourceWithContextMapper INSTANCE = Mappers.getMapper( MappingSourceWithContextMapper.class ); + + default List toDto(Model model) { + return model + .getItems() + .stream() + .map( item -> this.toDto( item, model.getModelId() ) ) + .collect( Collectors.toList() ); + } + + ItemDTO toDto(Item item, @MappingSource @Context String modelId); + + class Model { + private List items; + private String modelId; + + public Model(String modelId) { + this.modelId = modelId; + } + + public List getItems() { + return items; + } + + public void setItems(List items) { + this.items = items; + } + + public String getModelId() { + return modelId; + } + + public void setModelId(String modelId) { + this.modelId = modelId; + } + } + + class Item { + private String itemId; + private String name; + + public Item(String itemId, String name) { + this.itemId = itemId; + this.name = name; + } + + public String getItemId() { + return itemId; + } + + public void setItemId(String itemId) { + this.itemId = itemId; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } + + class ItemDTO { + private String itemId; + private String name; + private String modelId; // This comes from @Context parameter + + public String getItemId() { + return itemId; + } + + public void setItemId(String itemId) { + this.itemId = itemId; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getModelId() { + return modelId; + } + + public void setModelId(String modelId) { + this.modelId = modelId; + } + } +} diff --git a/distribution/processor/src/test/java/org/mapstruct/ap/test/mappingsource/MappingSourceWithContextTest.java b/distribution/processor/src/test/java/org/mapstruct/ap/test/mappingsource/MappingSourceWithContextTest.java new file mode 100644 index 0000000000..d9c4d43e56 --- /dev/null +++ b/distribution/processor/src/test/java/org/mapstruct/ap/test/mappingsource/MappingSourceWithContextTest.java @@ -0,0 +1,49 @@ +/* + * 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.mappingsource; + +import java.util.Arrays; +import java.util.List; + +import org.mapstruct.ap.test.mappingsource.MappingSourceWithContextMapper.Item; +import org.mapstruct.ap.test.mappingsource.MappingSourceWithContextMapper.ItemDTO; +import org.mapstruct.ap.test.mappingsource.MappingSourceWithContextMapper.Model; +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; + +import static org.assertj.core.api.Assertions.assertThat; + +@IssueKey("3665") +@WithClasses(MappingSourceWithContextMapper.class) +public class MappingSourceWithContextTest { + + @ProcessorTest + @WithClasses(MappingSourceWithContextMapper.class) + public void testMappingSourceWithContext() { + Model model = new Model( "MODEL-123" ); + List items = Arrays.asList( + new Item( "ITEM-1", "First Item" ), + new Item( "ITEM-2", "Second Item" ) + ); + model.setItems( items ); + + List result = MappingSourceWithContextMapper.INSTANCE.toDto( model ); + + assertThat( result ).isNotNull(); + assertThat( result ).hasSize( 2 ); + + ItemDTO dto1 = result.getFirst(); + assertThat( dto1.getItemId() ).isEqualTo( "ITEM-1" ); + assertThat( dto1.getName() ).isEqualTo( "First Item" ); + assertThat( dto1.getModelId() ).isEqualTo( "MODEL-123" ); + + ItemDTO dto2 = result.get( 1 ); + assertThat( dto2.getItemId() ).isEqualTo( "ITEM-2" ); + assertThat( dto2.getName() ).isEqualTo( "Second Item" ); + assertThat( dto2.getModelId() ).isEqualTo( "MODEL-123" ); + } +} From 1b6582431f7a82c970af82e5d8036cc14cd8ca7a Mon Sep 17 00:00:00 2001 From: Yang Tang Date: Wed, 18 Jun 2025 17:23:23 +0800 Subject: [PATCH 3/3] feat: Enhance @MappingSource annotation to support marking parameters as primary parameters and handling conflicts in multi-source mapping - Add primary attribute to mark primary parameters and resolve attribute conflicts in multi-source mapping. Signed-off-by: Yang Tang --- .../java/org/mapstruct/MappingSource.java | 94 ++++++----- .../ap/internal/model/BeanMappingMethod.java | 145 ++++++++++++----- .../ap/internal/model/common/Parameter.java | 19 ++- .../MappingSourceWithContextMapper.java | 111 ------------- .../MappingSourceWithContextTest.java | 49 ------ .../MappingSourceWithContextMapper.java | 2 +- .../MappingSourceWithContextTest.java | 8 +- .../MappingSourceImplicitMappingMapper.java | 2 +- .../MappingSourceImplicitMappingTest.java | 8 +- ...MappingSourcePrimaryConflictingMapper.java | 90 +++++++++++ .../primary/MappingSourcePrimaryMapper.java | 151 ++++++++++++++++++ .../primary/MappingSourcePrimaryTest.java | 87 ++++++++++ 12 files changed, 514 insertions(+), 252 deletions(-) delete mode 100644 processor/src/test/java/org/mapstruct/ap/test/mappingsource/MappingSourceWithContextMapper.java delete mode 100644 processor/src/test/java/org/mapstruct/ap/test/mappingsource/MappingSourceWithContextTest.java rename {distribution/processor/src/test/java/org/mapstruct/ap/test/mappingsource => processor/src/test/java/org/mapstruct/ap/test/mappingsource/context}/MappingSourceWithContextMapper.java (97%) rename {distribution/processor/src/test/java/org/mapstruct/ap/test/mappingsource => processor/src/test/java/org/mapstruct/ap/test/mappingsource/context}/MappingSourceWithContextTest.java (82%) rename processor/src/test/java/org/mapstruct/ap/test/mappingsource/{ => implicitmapping}/MappingSourceImplicitMappingMapper.java (97%) rename processor/src/test/java/org/mapstruct/ap/test/mappingsource/{ => implicitmapping}/MappingSourceImplicitMappingTest.java (84%) create mode 100644 processor/src/test/java/org/mapstruct/ap/test/mappingsource/primary/ErroneousMappingSourcePrimaryConflictingMapper.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/mappingsource/primary/MappingSourcePrimaryMapper.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/mappingsource/primary/MappingSourcePrimaryTest.java diff --git a/core/src/main/java/org/mapstruct/MappingSource.java b/core/src/main/java/org/mapstruct/MappingSource.java index 4e93412cce..aaeb88349a 100644 --- a/core/src/main/java/org/mapstruct/MappingSource.java +++ b/core/src/main/java/org/mapstruct/MappingSource.java @@ -11,37 +11,23 @@ import java.lang.annotation.Target; /** - * Marks a parameter with specific handling behavior during mapping. - * This annotation controls how source parameters are handled in multi-source mapping scenarios. + * Marks a parameter as a source parameter, providing fine-grained control over mapping behavior. + * This annotation controls how parameters are handled during the mapping process. *

- * By default,: - * - all parameters have their properties automatically used for implicit mapping - * - Map parameters in multi-source scenarios don't have their entries automatically used for implicit mapping + * Key features: + *

*

- * The implicitMapping attribute allows overriding these defaults: - * - For Bean parameters: setting implicitMapping=false prevents automatic property expansion - * - For Map parameters: setting implicitMapping=true enables using Map entries for implicit mapping - *

- * This annotation is primarily useful in multi-source parameter scenarios. - * - *


- * // Preventing a bean parameter's properties from being used in implicit mapping
- * {@literal @}Mapper
- * public interface MultiSourceMapper {
- *     {@literal @}Mapping(target = "name", source = "otherSource.name")
- *     TargetDto map(
- *         {@literal @}MappingSource(implicitMapping = false) UserEntity user,
- *          OtherSource otherSource);
- * }
- *
- * // Enabling Map entries for implicit mapping in multi-source scenarios
- * {@literal @}Mapper
- * public interface MultiSourceMapper {
- *     {@literal @}Mapping(target = "id", source = "entity.id")
- *     TargetDto map({@literal @}MappingSource(implicitMapping = true)
- *     Map<String, Object> sourceMap, Entity entity);
- * }
- * 
+ * Standard mapping behavior (without this annotation): + * * * @since 1.6.0 */ @@ -53,16 +39,26 @@ * Controls whether this parameter participates in implicit mapping. * The effect depends on the parameter type: *

- * - For Bean parameters: - * true (default): Properties are automatically used for implicit mapping - * false: Properties are not automatically used for implicit mapping + * Bean/Collection parameters: + *

*

- * - For Map parameters (in multi-source scenarios): - * true: Map entries are used for implicit mapping (similar to single-source behavior) - * false (default): Map entries are not automatically used for implicit mapping - *

- * - For other types (Collection, Path, etc.): - * This setting has no effect as these types are never automatically expanded + * Map parameters: + *

*

* Note: This setting only affects implicit mapping. Explicit mappings defined with * {@literal @}Mapping annotations are always processed regardless of this setting. @@ -70,4 +66,26 @@ * @return whether implicit mapping should be enabled for this parameter */ boolean implicitMapping() default true; + + /** + * Marks this parameter as primary in multi-source mapping scenarios. + *

+ * When multiple source parameters contain properties with the same name that could be mapped to + * a target property, MapStruct normally reports an error due to the ambiguity. When a parameter + * is marked as primary: + *

+ *

+ *

+ * Note: This setting affects both implicit and explicit mappings when resolving conflicts. + * Explicit {@literal @}Mapping annotations always take precedence over primary parameter selection. + * + * @return whether this parameter should be considered primary when resolving conflicts + * @since 1.7.0 + */ + boolean primary() default false; } 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 cd568ae88b..d4cc61f089 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 @@ -1420,31 +1420,15 @@ else if ( mapping.getJavaExpression() != null ) { // When we implicitly map we first do property name based mapping // i.e. look for matching properties in the source types // and then do parameter name based mapping - for ( Parameter sourceParameter : method.getSourceParameters() ) { - SourceReference matchingSourceRef = getSourceRefByTargetName( - sourceParameter, - targetPropertyName - ); - if ( matchingSourceRef != null ) { - if ( sourceRef != null ) { - errorOccured = true; - // This can only happen when the target property matches multiple properties - // within the different source parameters - ctx.getMessager() - .printMessage( - method.getExecutable(), - mappingRef.getMapping().getMirror(), - Message.BEANMAPPING_SEVERAL_POSSIBLE_SOURCES, - targetPropertyName - ); - break; - } - // We can't break here since it is possible that the same property exists in multiple - // source parameters - sourceRef = matchingSourceRef; - } + SourceReferenceResult matchingSourceRefResult = findSourceReferenceForTargetProperty( + method.getSourceParameters(), + targetPropertyName, + mappingRef.getMapping().getMirror() + ); + sourceRef = matchingSourceRefResult.getSourceReference(); + if (matchingSourceRefResult.isErrorOccurred() ) { + errorOccured = true; } - } if ( sourceRef == null ) { @@ -1568,34 +1552,103 @@ private void applyTargetThisMapping() { } /** - * Iterates over all target properties and all source parameters. + * Iterates over all target properties and all source parameters to find property name matches. *

- * When a property name match occurs, the remainder will be checked for duplicates. Matches will be removed from - * the set of remaining target properties. + * For each target property, the method attempts to find a matching source property + * using {@link #findSourceReferenceForTargetProperty}. + *

+ * When a match is found, it's added to the list of source references for further processing. + * Primary parameters take precedence when multiple source parameters have properties with the same name. */ private void applyPropertyNameBasedMapping() { List sourceReferences = new ArrayList<>(); + for ( String targetPropertyName : unprocessedTargetProperties.keySet() ) { - for ( Parameter sourceParameter : method.getSourceParameters() ) { - SourceReference sourceRef = getSourceRefByTargetName( sourceParameter, targetPropertyName ); + SourceReferenceResult matchingSourceRefResult = findSourceReferenceForTargetProperty( + method.getSourceParameters(), + targetPropertyName, + null + ); + if ( matchingSourceRefResult.getSourceReference() != null ) { + sourceReferences.add( matchingSourceRefResult.getSourceReference() ); + } + } + applyPropertyNameBasedMapping( sourceReferences ); + } + + /** + * Finds a source reference for a target property name, handling potential conflicts. + *

+ * This method iterates through source parameters to find properties matching the target property name, + * applying the following rules: + *

+ * + * @param sourceParameters the source parameters to search through + * @param targetPropertyName the target property name to match + * @param positionHint annotation mirror used for error reporting position, can be null + * @return a SourceReferenceResult containing the selected source reference and error status + */ + private SourceReferenceResult findSourceReferenceForTargetProperty(List sourceParameters, + String targetPropertyName, + AnnotationMirror positionHint) { + List sortedSourceParameters = + sourceParameters + .stream() + .sorted( Comparator.comparing( Parameter::isPrimary ).reversed() ) + .collect( Collectors.toList() ); + + SourceReference sourceRef = null; + boolean errorOccurred = false; + for ( Parameter sourceParameter : sortedSourceParameters ) { + SourceReference matchingSourceRef = getSourceRefByTargetName( sourceParameter, targetPropertyName ); + if ( matchingSourceRef != null ) { if ( sourceRef != null ) { - sourceReferences.add( sourceRef ); + if ( sourceRef.getParameter().isPrimary() == matchingSourceRef.getParameter().isPrimary() ) { + // Conflict detected - both parameters have the same primary status + // Either: + // 1. Both parameters are marked with @MappingSource(primary = true) + // 2. Neither parameter has primary status + errorOccurred = true; + ctx.getMessager() + .printMessage( + method.getExecutable(), + positionHint, + Message.BEANMAPPING_SEVERAL_POSSIBLE_SOURCES, + targetPropertyName + ); + } + break; } + // We can't break here since it is possible that the same property exists in multiple + // source parameters + sourceRef = matchingSourceRef; } } - applyPropertyNameBasedMapping( sourceReferences ); + return new SourceReferenceResult( sourceRef, errorOccurred ); } /** - * Iterates over all target properties and all source parameters. + * Processes a list of source references to create property mappings. + *

+ * Each source reference is used to create a property mapping for its target property. + * The referenced target property is removed from the set of unprocessed properties. *

- * When a property name match occurs, the remainder will be checked for duplicates. Matches will be removed from - * the set of remaining target properties. + * Note: This method assumes that conflicts between multiple source references for the same target property + * have already been resolved by {@link #findSourceReferenceForTargetProperty}. + * + * @param sourceReferences the list of source references to process */ private void applyPropertyNameBasedMapping(List sourceReferences) { - for ( SourceReference sourceRef : sourceReferences ) { - String targetPropertyName = sourceRef.getDeepestPropertyName(); Accessor targetPropertyWriteAccessor = unprocessedTargetProperties.remove( targetPropertyName ); unprocessedConstructorProperties.remove( targetPropertyName ); @@ -1703,7 +1756,7 @@ private SourceReference getSourceRefByTargetName(Parameter sourceParameter, Stri } boolean allowedMapToBean = - method.getSourceParameters().size() == 1 || ( sourceParameter.isImplicitMapping() ); + method.getSourceParameters().size() == 1 || sourceParameter.isImplicitMapping(); ReadAccessor sourceReadAccessor = sourceParameter.getType() .getReadAccessor( targetPropertyName, allowedMapToBean ); if ( sourceReadAccessor != null ) { @@ -1930,6 +1983,24 @@ private void reportErrorForUnusedSourceParameters() { } } + private static class SourceReferenceResult { + private final SourceReference sourceReference; + private final boolean errorOccurred; + + private SourceReferenceResult(SourceReference sourceReference, boolean errorOccurred) { + this.sourceReference = sourceReference; + this.errorOccurred = errorOccurred; + } + + SourceReference getSourceReference() { + return sourceReference; + } + + boolean isErrorOccurred() { + return errorOccurred; + } + } + private static class ConstructorAccessor { private final boolean hasError; private final List parameterBindings; diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/common/Parameter.java b/processor/src/main/java/org/mapstruct/ap/internal/model/common/Parameter.java index 901a323145..184764d177 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/common/Parameter.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/common/Parameter.java @@ -38,6 +38,7 @@ public class Parameter extends ModelElement { private final boolean targetPropertyName; private final boolean mappingSource; private final boolean implicitMapping; + private final boolean primary; private final boolean varArgs; @@ -56,13 +57,14 @@ private Parameter(Element element, Type type, boolean varArgs) { MappingSourceGem mappingSourceGem = MappingSourceGem.instanceOn( element ); this.mappingSource = mappingSourceGem != null; this.implicitMapping = this.mappingSource && mappingSourceGem.implicitMapping().get(); + this.primary = this.mappingSource && mappingSourceGem.primary().get(); this.varArgs = varArgs; } private Parameter(String name, Type type, boolean mappingTarget, boolean targetType, boolean mappingContext, - boolean sourcePropertyName, boolean targetPropertyName, boolean mappingSource, - boolean implicitMapping, boolean varArgs) { + boolean sourcePropertyName, boolean targetPropertyName, + boolean varArgs) { this.element = null; this.name = name; this.originalName = name; @@ -72,13 +74,14 @@ private Parameter(String name, Type type, boolean mappingTarget, boolean targetT this.mappingContext = mappingContext; this.sourcePropertyName = sourcePropertyName; this.targetPropertyName = targetPropertyName; - this.mappingSource = mappingSource; - this.implicitMapping = implicitMapping; this.varArgs = varArgs; + this.mappingSource = false; + this.implicitMapping = false; + this.primary = false; } public Parameter(String name, Type type) { - this( name, type, false, false, false, false, false, false, false, false ); + this( name, type, false, false, false, false, false, false ); } public Element getElement() { @@ -149,6 +152,10 @@ public boolean isImplicitMapping() { return implicitMapping; } + public boolean isPrimary() { + return primary; + } + public boolean isVarArgs() { return varArgs; } @@ -203,8 +210,6 @@ public static Parameter forForgedMappingTarget(Type parameterType) { false, false, false, - false, - false, false ); } diff --git a/processor/src/test/java/org/mapstruct/ap/test/mappingsource/MappingSourceWithContextMapper.java b/processor/src/test/java/org/mapstruct/ap/test/mappingsource/MappingSourceWithContextMapper.java deleted file mode 100644 index 41d3d2057c..0000000000 --- a/processor/src/test/java/org/mapstruct/ap/test/mappingsource/MappingSourceWithContextMapper.java +++ /dev/null @@ -1,111 +0,0 @@ -/* - * 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.mappingsource; - -import java.util.List; -import java.util.stream.Collectors; - -import org.mapstruct.Context; -import org.mapstruct.Mapper; -import org.mapstruct.MappingSource; -import org.mapstruct.factory.Mappers; - -@Mapper -public interface MappingSourceWithContextMapper { - - MappingSourceWithContextMapper INSTANCE = Mappers.getMapper( MappingSourceWithContextMapper.class ); - - default List toDto(Model model) { - return model - .getItems() - .stream() - .map( item -> this.toDto( item, model.getModelId() ) ) - .collect( Collectors.toList() ); - } - - ItemDTO toDto(Item item, @MappingSource @Context String modelId); - - class Model { - private List items; - private String modelId; - - public Model(String modelId) { - this.modelId = modelId; - } - - public List getItems() { - return items; - } - - public void setItems(List items) { - this.items = items; - } - - public String getModelId() { - return modelId; - } - - public void setModelId(String modelId) { - this.modelId = modelId; - } - } - - class Item { - private String itemId; - private String name; - - public Item(String itemId, String name) { - this.itemId = itemId; - this.name = name; - } - - public String getItemId() { - return itemId; - } - - public void setItemId(String itemId) { - this.itemId = itemId; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - } - - class ItemDTO { - private String itemId; - private String name; - private String modelId; // This comes from @Context parameter - - public String getItemId() { - return itemId; - } - - public void setItemId(String itemId) { - this.itemId = itemId; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public String getModelId() { - return modelId; - } - - public void setModelId(String modelId) { - this.modelId = modelId; - } - } -} diff --git a/processor/src/test/java/org/mapstruct/ap/test/mappingsource/MappingSourceWithContextTest.java b/processor/src/test/java/org/mapstruct/ap/test/mappingsource/MappingSourceWithContextTest.java deleted file mode 100644 index d9c4d43e56..0000000000 --- a/processor/src/test/java/org/mapstruct/ap/test/mappingsource/MappingSourceWithContextTest.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * 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.mappingsource; - -import java.util.Arrays; -import java.util.List; - -import org.mapstruct.ap.test.mappingsource.MappingSourceWithContextMapper.Item; -import org.mapstruct.ap.test.mappingsource.MappingSourceWithContextMapper.ItemDTO; -import org.mapstruct.ap.test.mappingsource.MappingSourceWithContextMapper.Model; -import org.mapstruct.ap.testutil.IssueKey; -import org.mapstruct.ap.testutil.ProcessorTest; -import org.mapstruct.ap.testutil.WithClasses; - -import static org.assertj.core.api.Assertions.assertThat; - -@IssueKey("3665") -@WithClasses(MappingSourceWithContextMapper.class) -public class MappingSourceWithContextTest { - - @ProcessorTest - @WithClasses(MappingSourceWithContextMapper.class) - public void testMappingSourceWithContext() { - Model model = new Model( "MODEL-123" ); - List items = Arrays.asList( - new Item( "ITEM-1", "First Item" ), - new Item( "ITEM-2", "Second Item" ) - ); - model.setItems( items ); - - List result = MappingSourceWithContextMapper.INSTANCE.toDto( model ); - - assertThat( result ).isNotNull(); - assertThat( result ).hasSize( 2 ); - - ItemDTO dto1 = result.getFirst(); - assertThat( dto1.getItemId() ).isEqualTo( "ITEM-1" ); - assertThat( dto1.getName() ).isEqualTo( "First Item" ); - assertThat( dto1.getModelId() ).isEqualTo( "MODEL-123" ); - - ItemDTO dto2 = result.get( 1 ); - assertThat( dto2.getItemId() ).isEqualTo( "ITEM-2" ); - assertThat( dto2.getName() ).isEqualTo( "Second Item" ); - assertThat( dto2.getModelId() ).isEqualTo( "MODEL-123" ); - } -} diff --git a/distribution/processor/src/test/java/org/mapstruct/ap/test/mappingsource/MappingSourceWithContextMapper.java b/processor/src/test/java/org/mapstruct/ap/test/mappingsource/context/MappingSourceWithContextMapper.java similarity index 97% rename from distribution/processor/src/test/java/org/mapstruct/ap/test/mappingsource/MappingSourceWithContextMapper.java rename to processor/src/test/java/org/mapstruct/ap/test/mappingsource/context/MappingSourceWithContextMapper.java index 41d3d2057c..f2bd5da64d 100644 --- a/distribution/processor/src/test/java/org/mapstruct/ap/test/mappingsource/MappingSourceWithContextMapper.java +++ b/processor/src/test/java/org/mapstruct/ap/test/mappingsource/context/MappingSourceWithContextMapper.java @@ -3,7 +3,7 @@ * * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 */ -package org.mapstruct.ap.test.mappingsource; +package org.mapstruct.ap.test.mappingsource.context; import java.util.List; import java.util.stream.Collectors; diff --git a/distribution/processor/src/test/java/org/mapstruct/ap/test/mappingsource/MappingSourceWithContextTest.java b/processor/src/test/java/org/mapstruct/ap/test/mappingsource/context/MappingSourceWithContextTest.java similarity index 82% rename from distribution/processor/src/test/java/org/mapstruct/ap/test/mappingsource/MappingSourceWithContextTest.java rename to processor/src/test/java/org/mapstruct/ap/test/mappingsource/context/MappingSourceWithContextTest.java index d9c4d43e56..c585b84e86 100644 --- a/distribution/processor/src/test/java/org/mapstruct/ap/test/mappingsource/MappingSourceWithContextTest.java +++ b/processor/src/test/java/org/mapstruct/ap/test/mappingsource/context/MappingSourceWithContextTest.java @@ -3,14 +3,14 @@ * * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 */ -package org.mapstruct.ap.test.mappingsource; +package org.mapstruct.ap.test.mappingsource.context; import java.util.Arrays; import java.util.List; -import org.mapstruct.ap.test.mappingsource.MappingSourceWithContextMapper.Item; -import org.mapstruct.ap.test.mappingsource.MappingSourceWithContextMapper.ItemDTO; -import org.mapstruct.ap.test.mappingsource.MappingSourceWithContextMapper.Model; +import org.mapstruct.ap.test.mappingsource.context.MappingSourceWithContextMapper.Item; +import org.mapstruct.ap.test.mappingsource.context.MappingSourceWithContextMapper.ItemDTO; +import org.mapstruct.ap.test.mappingsource.context.MappingSourceWithContextMapper.Model; import org.mapstruct.ap.testutil.IssueKey; import org.mapstruct.ap.testutil.ProcessorTest; import org.mapstruct.ap.testutil.WithClasses; diff --git a/processor/src/test/java/org/mapstruct/ap/test/mappingsource/MappingSourceImplicitMappingMapper.java b/processor/src/test/java/org/mapstruct/ap/test/mappingsource/implicitmapping/MappingSourceImplicitMappingMapper.java similarity index 97% rename from processor/src/test/java/org/mapstruct/ap/test/mappingsource/MappingSourceImplicitMappingMapper.java rename to processor/src/test/java/org/mapstruct/ap/test/mappingsource/implicitmapping/MappingSourceImplicitMappingMapper.java index 87e10e5717..288a984766 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/mappingsource/MappingSourceImplicitMappingMapper.java +++ b/processor/src/test/java/org/mapstruct/ap/test/mappingsource/implicitmapping/MappingSourceImplicitMappingMapper.java @@ -3,7 +3,7 @@ * * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 */ -package org.mapstruct.ap.test.mappingsource; +package org.mapstruct.ap.test.mappingsource.implicitmapping; import java.util.Map; diff --git a/processor/src/test/java/org/mapstruct/ap/test/mappingsource/MappingSourceImplicitMappingTest.java b/processor/src/test/java/org/mapstruct/ap/test/mappingsource/implicitmapping/MappingSourceImplicitMappingTest.java similarity index 84% rename from processor/src/test/java/org/mapstruct/ap/test/mappingsource/MappingSourceImplicitMappingTest.java rename to processor/src/test/java/org/mapstruct/ap/test/mappingsource/implicitmapping/MappingSourceImplicitMappingTest.java index 5449ec48d1..53a8ab4ab8 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/mappingsource/MappingSourceImplicitMappingTest.java +++ b/processor/src/test/java/org/mapstruct/ap/test/mappingsource/implicitmapping/MappingSourceImplicitMappingTest.java @@ -3,14 +3,14 @@ * * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 */ -package org.mapstruct.ap.test.mappingsource; +package org.mapstruct.ap.test.mappingsource.implicitmapping; import java.util.HashMap; import java.util.Map; -import org.mapstruct.ap.test.mappingsource.MappingSourceImplicitMappingMapper.BeanSource; -import org.mapstruct.ap.test.mappingsource.MappingSourceImplicitMappingMapper.BeanTarget; -import org.mapstruct.ap.test.mappingsource.MappingSourceImplicitMappingMapper.MapTarget; +import org.mapstruct.ap.test.mappingsource.implicitmapping.MappingSourceImplicitMappingMapper.BeanSource; +import org.mapstruct.ap.test.mappingsource.implicitmapping.MappingSourceImplicitMappingMapper.BeanTarget; +import org.mapstruct.ap.test.mappingsource.implicitmapping.MappingSourceImplicitMappingMapper.MapTarget; import org.mapstruct.ap.testutil.IssueKey; import org.mapstruct.ap.testutil.ProcessorTest; import org.mapstruct.ap.testutil.WithClasses; diff --git a/processor/src/test/java/org/mapstruct/ap/test/mappingsource/primary/ErroneousMappingSourcePrimaryConflictingMapper.java b/processor/src/test/java/org/mapstruct/ap/test/mappingsource/primary/ErroneousMappingSourcePrimaryConflictingMapper.java new file mode 100644 index 0000000000..cc84ea6ea1 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/mappingsource/primary/ErroneousMappingSourcePrimaryConflictingMapper.java @@ -0,0 +1,90 @@ +/* + * 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.mappingsource.primary; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.MappingSource; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface ErroneousMappingSourcePrimaryConflictingMapper { + + ErroneousMappingSourcePrimaryConflictingMapper INSTANCE = Mappers.getMapper( + ErroneousMappingSourcePrimaryConflictingMapper.class ); + + @Mapping(target = "value", ignore = true) + Target mapFromMultiplePrimarySources(@MappingSource(primary = true) Source1 source1, + @MappingSource(primary = true) Source2 source2); + + @Mapping(target = "name", ignore = true) + @Mapping(target = "value") + Target mapWithExplicitTargetMultiplePrimary(@MappingSource(primary = true) Source1 source1, + @MappingSource(primary = true) Source2 source2); + + class Source1 { + private String name; + private String value; + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } + + class Source2 { + private String name; + private String value; + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } + + class Target { + private String name; + private String value; + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/mappingsource/primary/MappingSourcePrimaryMapper.java b/processor/src/test/java/org/mapstruct/ap/test/mappingsource/primary/MappingSourcePrimaryMapper.java new file mode 100644 index 0000000000..1e7647edb1 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/mappingsource/primary/MappingSourcePrimaryMapper.java @@ -0,0 +1,151 @@ +/* + * 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.mappingsource.primary; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.MappingSource; +import org.mapstruct.ReportingPolicy; +import org.mapstruct.factory.Mappers; + +@Mapper(unmappedTargetPolicy = ReportingPolicy.IGNORE) +public interface MappingSourcePrimaryMapper { + + MappingSourcePrimaryMapper INSTANCE = Mappers.getMapper( MappingSourcePrimaryMapper.class ); + + Target mapWithPrimarySource(Source1 source1, @MappingSource(primary = true) Source2 source2); + + Target mapWithPrimaryMissingProperty(Source1 source1, @MappingSource(primary = true) Source3 source3); + + @Mapping(target = "name") + Target mapExplicitTargetWithPrimary(Source1 source1, @MappingSource(primary = true) Source2 source2); + + class Source1 { + private String name; + private Integer value; + + public Source1(String name, Integer value) { + this.name = name; + this.value = value; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Integer getValue() { + return value; + } + + public void setValue(Integer value) { + this.value = value; + } + } + + class Source2 { + private String name; + private Long count; + + public Source2(String name, Long count) { + this.name = name; + this.count = count; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Long getCount() { + return count; + } + + public void setCount(Long count) { + this.count = count; + } + } + + class Source3 { + private Integer id; + private Boolean active; + + public Source3(Integer id, Boolean active) { + this.id = id; + this.active = active; + } + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public Boolean getActive() { + return active; + } + + public void setActive(Boolean active) { + this.active = active; + } + } + + class Target { + private String name; + private Integer value; + private Long count; + private Integer id; + private Boolean active; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Integer getValue() { + return value; + } + + public void setValue(Integer value) { + this.value = value; + } + + public Long getCount() { + return count; + } + + public void setCount(Long count) { + this.count = count; + } + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public Boolean getActive() { + return active; + } + + public void setActive(Boolean active) { + this.active = active; + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/mappingsource/primary/MappingSourcePrimaryTest.java b/processor/src/test/java/org/mapstruct/ap/test/mappingsource/primary/MappingSourcePrimaryTest.java new file mode 100644 index 0000000000..2ff4dd6ac1 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/mappingsource/primary/MappingSourcePrimaryTest.java @@ -0,0 +1,87 @@ +/* + * 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.mappingsource.primary; + +import org.mapstruct.ap.test.mappingsource.primary.MappingSourcePrimaryMapper.Source1; +import org.mapstruct.ap.test.mappingsource.primary.MappingSourcePrimaryMapper.Source2; +import org.mapstruct.ap.test.mappingsource.primary.MappingSourcePrimaryMapper.Source3; +import org.mapstruct.ap.test.mappingsource.primary.MappingSourcePrimaryMapper.Target; +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 org.mapstruct.ap.testutil.runner.Compiler; + +import static org.assertj.core.api.Assertions.assertThat; + +@IssueKey("3136") +public class MappingSourcePrimaryTest { + + // The error message is too long and causes line wrapping. + // Eclipse has some bugs and cannot obtain the correct error message. + @ProcessorTest(Compiler.JDK) + @WithClasses(ErroneousMappingSourcePrimaryConflictingMapper.class) + @ExpectedCompilationOutcome( + value = CompilationResult.FAILED, + diagnostics = { + @Diagnostic(type = ErroneousMappingSourcePrimaryConflictingMapper.class, + kind = javax.tools.Diagnostic.Kind.ERROR, + line = 20, + message = "Several possible source properties for target property \"name\"."), + @Diagnostic(type = ErroneousMappingSourcePrimaryConflictingMapper.class, + kind = javax.tools.Diagnostic.Kind.ERROR, + line = 25, + message = "Several possible source properties for target property \"value\".") + } + ) + public void shouldFailOnMultiplePrimarySourcesWithConflictingProperties() { + } + + @ProcessorTest + @WithClasses(MappingSourcePrimaryMapper.class) + public void shouldUsePropertyFromPrimarySourceInImplicitMapping() { + Source1 source1 = new Source1( "source1Name", 42 ); + Source2 source2 = new Source2( "source2Name", 100L ); + + Target target = MappingSourcePrimaryMapper.INSTANCE.mapWithPrimarySource( source1, source2 ); + + assertThat( target ).isNotNull(); + assertThat( target.getName() ).isEqualTo( "source2Name" ); + assertThat( target.getValue() ).isEqualTo( 42 ); + assertThat( target.getCount() ).isEqualTo( 100L ); + } + + @ProcessorTest + @WithClasses(MappingSourcePrimaryMapper.class) + public void shouldUseNonPrimarySourceWhenPropertyIsMissingInPrimary() { + Source1 source1 = new Source1( "source1Name", 42 ); + Source3 source3 = new Source3( 123, true ); + + Target target = MappingSourcePrimaryMapper.INSTANCE.mapWithPrimaryMissingProperty( source1, source3 ); + + assertThat( target ).isNotNull(); + assertThat( target.getName() ).isEqualTo( "source1Name" ); + assertThat( target.getValue() ).isEqualTo( 42 ); + assertThat( target.getId() ).isEqualTo( 123 ); + assertThat( target.getActive() ).isEqualTo( true ); + } + + @ProcessorTest + @WithClasses(MappingSourcePrimaryMapper.class) + public void shouldUsePropertyFromPrimarySourceInExplicitTargetOnlyMapping() { + Source1 source1 = new Source1( "source1Name", 42 ); + Source2 source2 = new Source2( "source2Name", 100L ); + + Target target = MappingSourcePrimaryMapper.INSTANCE.mapExplicitTargetWithPrimary( source1, source2 ); + + assertThat( target ).isNotNull(); + assertThat( target.getName() ).isEqualTo( "source2Name" ); + assertThat( target.getValue() ).isEqualTo( 42 ); + assertThat( target.getCount() ).isEqualTo( 100L ); + } +}