Skip to content

Commit 1b65824

Browse files
committed
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 <tangyang9464@163.com>
1 parent 3ebce8b commit 1b65824

12 files changed

Lines changed: 514 additions & 252 deletions

core/src/main/java/org/mapstruct/MappingSource.java

Lines changed: 56 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -11,37 +11,23 @@
1111
import java.lang.annotation.Target;
1212

1313
/**
14-
* Marks a parameter with specific handling behavior during mapping.
15-
* This annotation controls how source parameters are handled in multi-source mapping scenarios.
14+
* Marks a parameter as a source parameter, providing fine-grained control over mapping behavior.
15+
* This annotation controls how parameters are handled during the mapping process.
1616
* <p>
17-
* By default,:
18-
* - all parameters have their properties automatically used for implicit mapping
19-
* - Map parameters in multi-source scenarios don't have their entries automatically used for implicit mapping
17+
* Key features:
18+
* <ul>
19+
* <li>When combined with {@link Context}, allows the annotated parameter to be used as both
20+
* a context parameter and a source parameter simultaneously</li>
21+
* <li>Precisely controls whether a parameter participates in implicit mapping (whether its properties
22+
* are automatically mapped to the target object)</li>
23+
* <li>Can mark a parameter as primary to automatically resolve property conflicts in multi-source scenarios</li>
24+
* </ul>
2025
* <p>
21-
* The implicitMapping attribute allows overriding these defaults:
22-
* - For Bean parameters: setting implicitMapping=false prevents automatic property expansion
23-
* - For Map parameters: setting implicitMapping=true enables using Map entries for implicit mapping
24-
* <p>
25-
* This annotation is primarily useful in multi-source parameter scenarios.
26-
*
27-
* <pre><code class='java'>
28-
* // Preventing a bean parameter's properties from being used in implicit mapping
29-
* {@literal @}Mapper
30-
* public interface MultiSourceMapper {
31-
* {@literal @}Mapping(target = "name", source = "otherSource.name")
32-
* TargetDto map(
33-
* {@literal @}MappingSource(implicitMapping = false) UserEntity user,
34-
* OtherSource otherSource);
35-
* }
36-
*
37-
* // Enabling Map entries for implicit mapping in multi-source scenarios
38-
* {@literal @}Mapper
39-
* public interface MultiSourceMapper {
40-
* {@literal @}Mapping(target = "id", source = "entity.id")
41-
* TargetDto map({@literal @}MappingSource(implicitMapping = true)
42-
* Map&lt;String, Object&gt; sourceMap, Entity entity);
43-
* }
44-
* </code></pre>
26+
* Standard mapping behavior (without this annotation):
27+
* <ul>
28+
* <li>All Bean-type parameters have their properties automatically used for implicit mapping</li>
29+
* <li>Map parameters in multi-source scenarios don't have their entries automatically used for implicit mapping</li>
30+
* </ul>
4531
*
4632
* @since 1.6.0
4733
*/
@@ -53,21 +39,53 @@
5339
* Controls whether this parameter participates in implicit mapping.
5440
* The effect depends on the parameter type:
5541
* <p>
56-
* - For Bean parameters:
57-
* true (default): Properties are automatically used for implicit mapping
58-
* false: Properties are not automatically used for implicit mapping
42+
* <b>Bean/Collection parameters:</b>
43+
* <ul>
44+
* <li>true : Properties automatically participate in implicit mapping. This maintains the same behavior
45+
* as not applying the annotation at all, since Bean properties are implicitly mapped by default.</li>
46+
* <li>false: Properties do not automatically participate in implicit mapping.
47+
* This is useful for preventing Bean types from
48+
* automatically mapping their properties to the target object, or to prevent properties from one source
49+
* overriding properties from other sources in multi-source scenarios.</li>
50+
* </ul>
5951
* <p>
60-
* - For Map parameters (in multi-source scenarios):
61-
* true: Map entries are used for implicit mapping (similar to single-source behavior)
62-
* false (default): Map entries are not automatically used for implicit mapping
63-
* <p>
64-
* - For other types (Collection, Path, etc.):
65-
* This setting has no effect as these types are never automatically expanded
52+
* <b>Map parameters:</b>
53+
* <ul>
54+
* <li>true: Enables Map entries to participate in implicit mapping.
55+
* This changes the default behavior
56+
* for Maps in multi-source scenarios,
57+
* allowing Map-to-Bean mapping to work similarly to single-source scenarios.</li>
58+
* <li>false : Disables automatic implicit mapping of Map entries. This maintains the same behavior
59+
* as not applying the annotation at all in multi-source scenarios,
60+
* where Map entries are not implicitly mapped by default.</li>
61+
* </ul>
6662
* <p>
6763
* Note: This setting only affects implicit mapping. Explicit mappings defined with
6864
* {@literal @}Mapping annotations are always processed regardless of this setting.
6965
*
7066
* @return whether implicit mapping should be enabled for this parameter
7167
*/
7268
boolean implicitMapping() default true;
69+
70+
/**
71+
* Marks this parameter as primary in multi-source mapping scenarios.
72+
* <p>
73+
* When multiple source parameters contain properties with the same name that could be mapped to
74+
* a target property, MapStruct normally reports an error due to the ambiguity. When a parameter
75+
* is marked as primary:
76+
* <p>
77+
* <ul>
78+
* <li>If conflict occurs between properties from different source parameters, the property from the
79+
* primary-marked parameter will be used</li>
80+
* <li>If multiple parameters are marked as primary and have conflicting properties, MapStruct will
81+
* still report an error</li>
82+
* </ul>
83+
* <p>
84+
* Note: This setting affects both implicit and explicit mappings when resolving conflicts.
85+
* Explicit {@literal @}Mapping annotations always take precedence over primary parameter selection.
86+
*
87+
* @return whether this parameter should be considered primary when resolving conflicts
88+
* @since 1.7.0
89+
*/
90+
boolean primary() default false;
7391
}

processor/src/main/java/org/mapstruct/ap/internal/model/BeanMappingMethod.java

Lines changed: 108 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1420,31 +1420,15 @@ else if ( mapping.getJavaExpression() != null ) {
14201420
// When we implicitly map we first do property name based mapping
14211421
// i.e. look for matching properties in the source types
14221422
// and then do parameter name based mapping
1423-
for ( Parameter sourceParameter : method.getSourceParameters() ) {
1424-
SourceReference matchingSourceRef = getSourceRefByTargetName(
1425-
sourceParameter,
1426-
targetPropertyName
1427-
);
1428-
if ( matchingSourceRef != null ) {
1429-
if ( sourceRef != null ) {
1430-
errorOccured = true;
1431-
// This can only happen when the target property matches multiple properties
1432-
// within the different source parameters
1433-
ctx.getMessager()
1434-
.printMessage(
1435-
method.getExecutable(),
1436-
mappingRef.getMapping().getMirror(),
1437-
Message.BEANMAPPING_SEVERAL_POSSIBLE_SOURCES,
1438-
targetPropertyName
1439-
);
1440-
break;
1441-
}
1442-
// We can't break here since it is possible that the same property exists in multiple
1443-
// source parameters
1444-
sourceRef = matchingSourceRef;
1445-
}
1423+
SourceReferenceResult matchingSourceRefResult = findSourceReferenceForTargetProperty(
1424+
method.getSourceParameters(),
1425+
targetPropertyName,
1426+
mappingRef.getMapping().getMirror()
1427+
);
1428+
sourceRef = matchingSourceRefResult.getSourceReference();
1429+
if (matchingSourceRefResult.isErrorOccurred() ) {
1430+
errorOccured = true;
14461431
}
1447-
14481432
}
14491433

14501434
if ( sourceRef == null ) {
@@ -1568,34 +1552,103 @@ private void applyTargetThisMapping() {
15681552
}
15691553

15701554
/**
1571-
* Iterates over all target properties and all source parameters.
1555+
* Iterates over all target properties and all source parameters to find property name matches.
15721556
* <p>
1573-
* When a property name match occurs, the remainder will be checked for duplicates. Matches will be removed from
1574-
* the set of remaining target properties.
1557+
* For each target property, the method attempts to find a matching source property
1558+
* using {@link #findSourceReferenceForTargetProperty}.
1559+
* <p>
1560+
* When a match is found, it's added to the list of source references for further processing.
1561+
* Primary parameters take precedence when multiple source parameters have properties with the same name.
15751562
*/
15761563
private void applyPropertyNameBasedMapping() {
15771564
List<SourceReference> sourceReferences = new ArrayList<>();
1565+
15781566
for ( String targetPropertyName : unprocessedTargetProperties.keySet() ) {
1579-
for ( Parameter sourceParameter : method.getSourceParameters() ) {
1580-
SourceReference sourceRef = getSourceRefByTargetName( sourceParameter, targetPropertyName );
1567+
SourceReferenceResult matchingSourceRefResult = findSourceReferenceForTargetProperty(
1568+
method.getSourceParameters(),
1569+
targetPropertyName,
1570+
null
1571+
);
1572+
if ( matchingSourceRefResult.getSourceReference() != null ) {
1573+
sourceReferences.add( matchingSourceRefResult.getSourceReference() );
1574+
}
1575+
}
1576+
applyPropertyNameBasedMapping( sourceReferences );
1577+
}
1578+
1579+
/**
1580+
* Finds a source reference for a target property name, handling potential conflicts.
1581+
* <p>
1582+
* This method iterates through source parameters to find properties matching the target property name,
1583+
* applying the following rules:
1584+
* <ul>
1585+
* <li>If only one matching source reference is found, it's returned</li>
1586+
* <li>If multiple matching source references are found, their primary status is checked:
1587+
* <ul>
1588+
* <li>If all matching references have the same primary status (all primary or all non-primary),
1589+
* a conflict error is reported and an error result is returned</li>
1590+
* <li>If they have different primary status, the reference from the primary parameter is preferred</li>
1591+
* </ul>
1592+
* </li>
1593+
* </ul>
1594+
*
1595+
* @param sourceParameters the source parameters to search through
1596+
* @param targetPropertyName the target property name to match
1597+
* @param positionHint annotation mirror used for error reporting position, can be null
1598+
* @return a SourceReferenceResult containing the selected source reference and error status
1599+
*/
1600+
private SourceReferenceResult findSourceReferenceForTargetProperty(List<Parameter> sourceParameters,
1601+
String targetPropertyName,
1602+
AnnotationMirror positionHint) {
1603+
List<Parameter> sortedSourceParameters =
1604+
sourceParameters
1605+
.stream()
1606+
.sorted( Comparator.comparing( Parameter::isPrimary ).reversed() )
1607+
.collect( Collectors.toList() );
1608+
1609+
SourceReference sourceRef = null;
1610+
boolean errorOccurred = false;
1611+
for ( Parameter sourceParameter : sortedSourceParameters ) {
1612+
SourceReference matchingSourceRef = getSourceRefByTargetName( sourceParameter, targetPropertyName );
1613+
if ( matchingSourceRef != null ) {
15811614
if ( sourceRef != null ) {
1582-
sourceReferences.add( sourceRef );
1615+
if ( sourceRef.getParameter().isPrimary() == matchingSourceRef.getParameter().isPrimary() ) {
1616+
// Conflict detected - both parameters have the same primary status
1617+
// Either:
1618+
// 1. Both parameters are marked with @MappingSource(primary = true)
1619+
// 2. Neither parameter has primary status
1620+
errorOccurred = true;
1621+
ctx.getMessager()
1622+
.printMessage(
1623+
method.getExecutable(),
1624+
positionHint,
1625+
Message.BEANMAPPING_SEVERAL_POSSIBLE_SOURCES,
1626+
targetPropertyName
1627+
);
1628+
}
1629+
break;
15831630
}
1631+
// We can't break here since it is possible that the same property exists in multiple
1632+
// source parameters
1633+
sourceRef = matchingSourceRef;
15841634
}
15851635
}
1586-
applyPropertyNameBasedMapping( sourceReferences );
1636+
return new SourceReferenceResult( sourceRef, errorOccurred );
15871637
}
15881638

15891639
/**
1590-
* Iterates over all target properties and all source parameters.
1640+
* Processes a list of source references to create property mappings.
1641+
* <p>
1642+
* Each source reference is used to create a property mapping for its target property.
1643+
* The referenced target property is removed from the set of unprocessed properties.
15911644
* <p>
1592-
* When a property name match occurs, the remainder will be checked for duplicates. Matches will be removed from
1593-
* the set of remaining target properties.
1645+
* Note: This method assumes that conflicts between multiple source references for the same target property
1646+
* have already been resolved by {@link #findSourceReferenceForTargetProperty}.
1647+
*
1648+
* @param sourceReferences the list of source references to process
15941649
*/
15951650
private void applyPropertyNameBasedMapping(List<SourceReference> sourceReferences) {
1596-
15971651
for ( SourceReference sourceRef : sourceReferences ) {
1598-
15991652
String targetPropertyName = sourceRef.getDeepestPropertyName();
16001653
Accessor targetPropertyWriteAccessor = unprocessedTargetProperties.remove( targetPropertyName );
16011654
unprocessedConstructorProperties.remove( targetPropertyName );
@@ -1703,7 +1756,7 @@ private SourceReference getSourceRefByTargetName(Parameter sourceParameter, Stri
17031756
}
17041757

17051758
boolean allowedMapToBean =
1706-
method.getSourceParameters().size() == 1 || ( sourceParameter.isImplicitMapping() );
1759+
method.getSourceParameters().size() == 1 || sourceParameter.isImplicitMapping();
17071760
ReadAccessor sourceReadAccessor = sourceParameter.getType()
17081761
.getReadAccessor( targetPropertyName, allowedMapToBean );
17091762
if ( sourceReadAccessor != null ) {
@@ -1930,6 +1983,24 @@ private void reportErrorForUnusedSourceParameters() {
19301983
}
19311984
}
19321985

1986+
private static class SourceReferenceResult {
1987+
private final SourceReference sourceReference;
1988+
private final boolean errorOccurred;
1989+
1990+
private SourceReferenceResult(SourceReference sourceReference, boolean errorOccurred) {
1991+
this.sourceReference = sourceReference;
1992+
this.errorOccurred = errorOccurred;
1993+
}
1994+
1995+
SourceReference getSourceReference() {
1996+
return sourceReference;
1997+
}
1998+
1999+
boolean isErrorOccurred() {
2000+
return errorOccurred;
2001+
}
2002+
}
2003+
19332004
private static class ConstructorAccessor {
19342005
private final boolean hasError;
19352006
private final List<ParameterBinding> parameterBindings;

processor/src/main/java/org/mapstruct/ap/internal/model/common/Parameter.java

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ public class Parameter extends ModelElement {
3838
private final boolean targetPropertyName;
3939
private final boolean mappingSource;
4040
private final boolean implicitMapping;
41+
private final boolean primary;
4142

4243
private final boolean varArgs;
4344

@@ -56,13 +57,14 @@ private Parameter(Element element, Type type, boolean varArgs) {
5657
MappingSourceGem mappingSourceGem = MappingSourceGem.instanceOn( element );
5758
this.mappingSource = mappingSourceGem != null;
5859
this.implicitMapping = this.mappingSource && mappingSourceGem.implicitMapping().get();
60+
this.primary = this.mappingSource && mappingSourceGem.primary().get();
5961

6062
this.varArgs = varArgs;
6163
}
6264

6365
private Parameter(String name, Type type, boolean mappingTarget, boolean targetType, boolean mappingContext,
64-
boolean sourcePropertyName, boolean targetPropertyName, boolean mappingSource,
65-
boolean implicitMapping, boolean varArgs) {
66+
boolean sourcePropertyName, boolean targetPropertyName,
67+
boolean varArgs) {
6668
this.element = null;
6769
this.name = name;
6870
this.originalName = name;
@@ -72,13 +74,14 @@ private Parameter(String name, Type type, boolean mappingTarget, boolean targetT
7274
this.mappingContext = mappingContext;
7375
this.sourcePropertyName = sourcePropertyName;
7476
this.targetPropertyName = targetPropertyName;
75-
this.mappingSource = mappingSource;
76-
this.implicitMapping = implicitMapping;
7777
this.varArgs = varArgs;
78+
this.mappingSource = false;
79+
this.implicitMapping = false;
80+
this.primary = false;
7881
}
7982

8083
public Parameter(String name, Type type) {
81-
this( name, type, false, false, false, false, false, false, false, false );
84+
this( name, type, false, false, false, false, false, false );
8285
}
8386

8487
public Element getElement() {
@@ -149,6 +152,10 @@ public boolean isImplicitMapping() {
149152
return implicitMapping;
150153
}
151154

155+
public boolean isPrimary() {
156+
return primary;
157+
}
158+
152159
public boolean isVarArgs() {
153160
return varArgs;
154161
}
@@ -203,8 +210,6 @@ public static Parameter forForgedMappingTarget(Type parameterType) {
203210
false,
204211
false,
205212
false,
206-
false,
207-
false,
208213
false
209214
);
210215
}

0 commit comments

Comments
 (0)