Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
91 changes: 91 additions & 0 deletions core/src/main/java/org/mapstruct/MappingSource.java
Original file line number Diff line number Diff line change
@@ -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.
* <p>
* 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>
* 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
*/
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.CLASS)
public @interface MappingSource {

/**
* Controls whether this parameter participates in implicit mapping.
* The effect depends on the parameter type:
* <p>
* <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>
* <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 @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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)
Expand Down
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 @@ -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 =
Expand Down Expand Up @@ -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<ParameterBinding> parameterBindings;
Expand Down
Loading