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..aaeb88349a
--- /dev/null
+++ b/core/src/main/java/org/mapstruct/MappingSource.java
@@ -0,0 +1,91 @@
+/*
+ * 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 as a source parameter, providing fine-grained control over mapping behavior.
+ * This annotation controls how parameters are handled during the mapping process.
+ *
+ * Key features:
+ *
+ * - When combined with {@link Context}, allows the annotated parameter to be used as both
+ * a context parameter and a source parameter simultaneously
+ * - Precisely controls whether a parameter participates in implicit mapping (whether its properties
+ * are automatically mapped to the target object)
+ * - Can mark a parameter as primary to automatically resolve property conflicts in multi-source scenarios
+ *
+ *
+ * Standard mapping behavior (without this annotation):
+ *
+ * - All Bean-type 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
+ *
+ *
+ * @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:
+ *
+ * Bean/Collection parameters:
+ *
+ * - true : Properties automatically participate in implicit mapping. This maintains the same behavior
+ * as not applying the annotation at all, since Bean properties are implicitly mapped by default.
+ * - false: Properties do not automatically participate in implicit mapping.
+ * This is useful for preventing Bean types from
+ * automatically mapping their properties to the target object, or to prevent properties from one source
+ * overriding properties from other sources in multi-source scenarios.
+ *
+ *
+ * Map parameters:
+ *
+ * - true: Enables Map entries to participate in implicit mapping.
+ * This changes the default behavior
+ * for Maps in multi-source scenarios,
+ * allowing Map-to-Bean mapping to work similarly to single-source scenarios.
+ * - false : Disables automatic implicit mapping of Map entries. This maintains the same behavior
+ * as not applying the annotation at all in multi-source scenarios,
+ * where Map entries are not implicitly mapped by default.
+ *
+ *
+ * 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;
+
+ /**
+ * 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:
+ *
+ *
+ * - If conflict occurs between properties from different source parameters, the property from the
+ * primary-marked parameter will be used
+ * - If multiple parameters are marked as primary and have conflicting properties, MapStruct will
+ * still report an error
+ *
+ *
+ * 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/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..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:
+ *
+ * - If only one matching source reference is found, it's returned
+ * - If multiple matching source references are found, their primary status is checked:
+ *
+ * - If all matching references have the same primary status (all primary or all non-primary),
+ * a conflict error is reported and an error result is returned
+ * - If they have different primary status, the reference from the primary parameter is preferred
+ *
+ *
+ *
+ *
+ * @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 );
@@ -1696,12 +1749,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 =
@@ -1926,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 aaab7f46ca..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
@@ -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,9 @@ 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 primary;
private final boolean varArgs;
@@ -48,6 +52,13 @@ 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.primary = this.mappingSource && mappingSourceGem.primary().get();
+
this.varArgs = varArgs;
}
@@ -64,6 +75,9 @@ private Parameter(String name, Type type, boolean mappingTarget, boolean targetT
this.sourcePropertyName = sourcePropertyName;
this.targetPropertyName = targetPropertyName;
this.varArgs = varArgs;
+ this.mappingSource = false;
+ this.implicitMapping = false;
+ this.primary = false;
}
public Parameter(String name, Type type) {
@@ -105,7 +119,8 @@ private String format() {
+ ( mappingContext ? "@Context " : "" )
+ ( sourcePropertyName ? "@SourcePropertyName " : "" )
+ ( targetPropertyName ? "@TargetPropertyName " : "" )
- + "%s " + name;
+ + ( mappingSource ? "@MappingSource " : "" )
+ + "%s " + name;
}
@Override
@@ -129,6 +144,18 @@ public boolean isSourcePropertyName() {
return sourcePropertyName;
}
+ public boolean isMappingSource() {
+ return mappingSource;
+ }
+
+ public boolean isImplicitMapping() {
+ return implicitMapping;
+ }
+
+ public boolean isPrimary() {
+ return primary;
+ }
+
public boolean isVarArgs() {
return varArgs;
}
@@ -136,9 +163,9 @@ public boolean isVarArgs() {
public boolean isSourceParameter() {
return !isMappingTarget() &&
!isTargetType() &&
- !isMappingContext() &&
!isSourcePropertyName() &&
- !isTargetPropertyName();
+ !isTargetPropertyName() &&
+ ( !isMappingContext() || isMappingSource() );
}
@Override
diff --git a/processor/src/test/java/org/mapstruct/ap/test/mappingsource/context/MappingSourceWithContextMapper.java b/processor/src/test/java/org/mapstruct/ap/test/mappingsource/context/MappingSourceWithContextMapper.java
new file mode 100644
index 0000000000..f2bd5da64d
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/mappingsource/context/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.context;
+
+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/context/MappingSourceWithContextTest.java b/processor/src/test/java/org/mapstruct/ap/test/mappingsource/context/MappingSourceWithContextTest.java
new file mode 100644
index 0000000000..c585b84e86
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/mappingsource/context/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.context;
+
+import java.util.Arrays;
+import java.util.List;
+
+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;
+
+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/processor/src/test/java/org/mapstruct/ap/test/mappingsource/implicitmapping/MappingSourceImplicitMappingMapper.java b/processor/src/test/java/org/mapstruct/ap/test/mappingsource/implicitmapping/MappingSourceImplicitMappingMapper.java
new file mode 100644
index 0000000000..288a984766
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/mappingsource/implicitmapping/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.implicitmapping;
+
+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/implicitmapping/MappingSourceImplicitMappingTest.java b/processor/src/test/java/org/mapstruct/ap/test/mappingsource/implicitmapping/MappingSourceImplicitMappingTest.java
new file mode 100644
index 0000000000..53a8ab4ab8
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/mappingsource/implicitmapping/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.implicitmapping;
+
+import java.util.HashMap;
+import java.util.Map;
+
+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;
+
+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/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 );
+ }
+}