Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
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>
  • Loading branch information
tangyang9464 committed Jun 19, 2025
commit 1b6582431f7a82c970af82e5d8036cc14cd8ca7a
94 changes: 56 additions & 38 deletions core/src/main/java/org/mapstruct/MappingSource.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
* <p>
* 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:
* <ul>
* <li>When combined with {@link Context}, allows the annotated parameter to be used as both
* a context parameter and a source parameter simultaneously</li>
* <li>Precisely controls whether a parameter participates in implicit mapping (whether its properties
* are automatically mapped to the target object)</li>
* <li>Can mark a parameter as primary to automatically resolve property conflicts in multi-source scenarios</li>
* </ul>
* <p>
* 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
* <p>
* This annotation is primarily useful in multi-source parameter scenarios.
*
* <pre><code class='java'>
* // 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&lt;String, Object&gt; sourceMap, Entity entity);
* }
* </code></pre>
* Standard mapping behavior (without this annotation):
* <ul>
* <li>All Bean-type parameters have their properties automatically used for implicit mapping</li>
* <li>Map parameters in multi-source scenarios don't have their entries automatically used for implicit mapping</li>
* </ul>
*
* @since 1.6.0
*/
Expand All @@ -53,21 +39,53 @@
* Controls whether this parameter participates in implicit mapping.
* The effect depends on the parameter type:
* <p>
* - For Bean parameters:
* true (default): Properties are automatically used for implicit mapping
* false: Properties are not automatically used for implicit mapping
* <b>Bean/Collection parameters:</b>
* <ul>
* <li>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.</li>
* <li>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.</li>
* </ul>
* <p>
* - 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
* <p>
* - For other types (Collection, Path, etc.):
* This setting has no effect as these types are never automatically expanded
* <b>Map parameters:</b>
* <ul>
* <li>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.</li>
* <li>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.</li>
* </ul>
* <p>
* 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.
* <p>
* 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:
* <p>
* <ul>
* <li>If conflict occurs between properties from different source parameters, the property from the
* primary-marked parameter will be used</li>
* <li>If multiple parameters are marked as primary and have conflicting properties, MapStruct will
* still report an error</li>
* </ul>
* <p>
* 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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 ) {
Expand Down Expand Up @@ -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.
* <p>
* 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}.
* <p>
* 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<SourceReference> 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.
* <p>
* This method iterates through source parameters to find properties matching the target property name,
* applying the following rules:
* <ul>
* <li>If only one matching source reference is found, it's returned</li>
* <li>If multiple matching source references are found, their primary status is checked:
* <ul>
* <li>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</li>
* <li>If they have different primary status, the reference from the primary parameter is preferred</li>
* </ul>
* </li>
* </ul>
*
* @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<Parameter> sourceParameters,
String targetPropertyName,
AnnotationMirror positionHint) {
List<Parameter> 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.
* <p>
* 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.
* <p>
* 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<SourceReference> sourceReferences) {

for ( SourceReference sourceRef : sourceReferences ) {

String targetPropertyName = sourceRef.getDeepestPropertyName();
Accessor targetPropertyWriteAccessor = unprocessedTargetProperties.remove( targetPropertyName );
unprocessedConstructorProperties.remove( targetPropertyName );
Expand Down Expand Up @@ -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 ) {
Expand Down Expand Up @@ -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<ParameterBinding> parameterBindings;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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;
Expand All @@ -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() {
Expand Down Expand Up @@ -149,6 +152,10 @@ public boolean isImplicitMapping() {
return implicitMapping;
}

public boolean isPrimary() {
return primary;
}

public boolean isVarArgs() {
return varArgs;
}
Expand Down Expand Up @@ -203,8 +210,6 @@ public static Parameter forForgedMappingTarget(Type parameterType) {
false,
false,
false,
false,
false,
false
);
}
Expand Down
Loading