Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@
MappingProcessor.VERBOSE,
MappingProcessor.NULL_VALUE_ITERABLE_MAPPING_STRATEGY,
MappingProcessor.NULL_VALUE_MAP_MAPPING_STRATEGY,
MappingProcessor.DISABLE_LIFECYCLE_OVERLOAD_DEDUPLICATE_SELECTOR,
})
public class MappingProcessor extends AbstractProcessor {

Expand All @@ -115,6 +116,8 @@ public class MappingProcessor extends AbstractProcessor {
protected static final String VERBOSE = "mapstruct.verbose";
protected static final String NULL_VALUE_ITERABLE_MAPPING_STRATEGY = "mapstruct.nullValueIterableMappingStrategy";
protected static final String NULL_VALUE_MAP_MAPPING_STRATEGY = "mapstruct.nullValueMapMappingStrategy";
protected static final String DISABLE_LIFECYCLE_OVERLOAD_DEDUPLICATE_SELECTOR =
"mapstruct.disableLifecycleOverloadDeduplicateSelector";

private final Set<String> additionalSupportedOptions;
private final String additionalSupportedOptionsError;
Expand Down Expand Up @@ -174,6 +177,8 @@ private Options createOptions() {
String nullValueIterableMappingStrategy = processingEnv.getOptions()
.get( NULL_VALUE_ITERABLE_MAPPING_STRATEGY );
String nullValueMapMappingStrategy = processingEnv.getOptions().get( NULL_VALUE_MAP_MAPPING_STRATEGY );
String disableLifecycleOverloadDeduplicateSelector = processingEnv.getOptions()
.get( DISABLE_LIFECYCLE_OVERLOAD_DEDUPLICATE_SELECTOR );

return new Options(
Boolean.parseBoolean( processingEnv.getOptions().get( SUPPRESS_GENERATOR_TIMESTAMP ) ),
Expand All @@ -189,7 +194,8 @@ private Options createOptions() {
NullValueMappingStrategyGem.valueOf( nullValueIterableMappingStrategy.toUpperCase( Locale.ROOT ) ) :
null,
nullValueMapMappingStrategy != null ?
NullValueMappingStrategyGem.valueOf( nullValueMapMappingStrategy.toUpperCase( Locale.ROOT ) ) : null
NullValueMappingStrategyGem.valueOf( nullValueMapMappingStrategy.toUpperCase( Locale.ROOT ) ) : null,
Boolean.parseBoolean( disableLifecycleOverloadDeduplicateSelector )
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ private static List<LifecycleCallbackMethodReference> collectLifecycleCallbackMe
MappingBuilderContext ctx, Set<String> existingVariableNames) {

MethodSelectors selectors =
new MethodSelectors( ctx.getTypeUtils(), ctx.getElementUtils(), ctx.getMessager() );
new MethodSelectors( ctx.getTypeUtils(), ctx.getElementUtils(), ctx.getMessager(), ctx.getOptions() );

List<SelectedMethod<SourceMethod>> matchingMethods = selectors.getMatchingMethods(
callbackMethods,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,9 @@
*/
package org.mapstruct.ap.internal.model;

import static org.mapstruct.ap.internal.util.Collections.first;

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;

Expand All @@ -26,6 +23,8 @@
import org.mapstruct.ap.internal.model.source.selector.SelectionContext;
import org.mapstruct.ap.internal.util.Message;

import static org.mapstruct.ap.internal.util.Collections.first;

/**
*
* @author Sjaak Derksen
Expand Down Expand Up @@ -126,7 +125,7 @@ public static List<SelectedMethod<SourceMethod>> getMatchingFactoryMethods( Meth
MappingBuilderContext ctx) {

MethodSelectors selectors =
new MethodSelectors( ctx.getTypeUtils(), ctx.getElementUtils(), ctx.getMessager() );
new MethodSelectors( ctx.getTypeUtils(), ctx.getElementUtils(), ctx.getMessager(), null );

return selectors.getMatchingMethods(
getAllAvailableMethods( method, ctx.getSourceModel() ),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,8 @@ private static List<SelectedMethod<SourceMethod>> findMatchingMethods(
MethodSelectors selectors = new MethodSelectors(
ctx.getTypeUtils(),
ctx.getElementUtils(),
ctx.getMessager()
ctx.getMessager(),
null
);

return selectors.getMatchingMethods(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
/*
* 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.internal.model.source.selector;

import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.stream.Collectors;

import org.mapstruct.ap.internal.model.common.Parameter;
import org.mapstruct.ap.internal.model.common.ParameterBinding;
import org.mapstruct.ap.internal.model.source.Method;

/**
* Selector for deduplicating overloaded lifecycle callback methods
* whose parameter signatures differ only by type hierarchy.
* <p>
* In the context of lifecycle callback method selection
* (such as @BeforeMapping or @AfterMapping), it is possible to have multiple overloaded methods
* whose parameter lists are structurally identical except for the specific types,
* where those types are related by inheritance (e.g., one parameter is a superclass or subclass of another).
* <p>
* This selector groups such methods by their effective parameter signature
* (ignoring differences only in type hierarchy), and, within each group,
* retains only the method whose parameter types have the closest inheritance distance
* to the actual invocation types.
* This ensures that, for each group of nearly identical overloads,
* only the most specific and appropriate method is selected.
* <p>
* <b>Example (see Issue3849Test):</b>
*
* <pre>{@code
* @AfterMapping
* default void afterMapping(Parent source, @MappingTarget ParentDto target) { ... }
* @AfterMapping
* default void afterMapping(Parent source, @MappingTarget ChildDto target) { ... }
* }</pre>
* When mapping a Child to a ChildDto,
* only the method with ChildDto is selected, even though both methods match by signature
* except for the target type's inheritance relationship.
*/
public class LifecycleOverloadDeduplicateSelector implements MethodSelector {
@Override
public <T extends Method> List<SelectedMethod<T>> getMatchingMethods(List<SelectedMethod<T>> methods,
SelectionContext context) {
if ( !context.getSelectionCriteria().isLifecycleCallbackRequired() || methods.size() <= 1 ) {
return methods;
}
Collection<List<SelectedMethod<T>>> methodSignatureGroups =
methods.stream()
.collect( Collectors.groupingBy(
LifecycleOverloadDeduplicateSelector::buildSignatureKey,
LinkedHashMap::new,
Collectors.toList()
) )
.values();
List<SelectedMethod<T>> deduplicatedMethods = new ArrayList<>( methods.size() );
for ( List<SelectedMethod<T>> signatureGroup : methodSignatureGroups ) {
if ( signatureGroup.size() == 1 ) {
deduplicatedMethods.add( signatureGroup.get( 0 ) );
continue;
}
SelectedMethod<T> bestInheritanceMethod = signatureGroup.get( 0 );
for ( int i = 1; i < signatureGroup.size(); i++ ) {
SelectedMethod<T> candidateMethod = signatureGroup.get( i );
if ( isInheritanceBetter( candidateMethod, bestInheritanceMethod ) ) {
bestInheritanceMethod = candidateMethod;
}
}
deduplicatedMethods.add( bestInheritanceMethod );
}
return deduplicatedMethods;
}

/**
* Builds a grouping key for a method based on its defining type,
* method name, and a detailed breakdown of each parameter binding.
* <p>
* The key consists of:
* <ul>
* <li>The type that defines the method</li>
* <li>The method name</li>
* <li>parameter bindings</li>
* </ul>
* This ensures that methods are grouped together only if all these aspects match,
* except for differences in type hierarchy, which are handled separately.
*/
private static <T extends Method> List<Object> buildSignatureKey(SelectedMethod<T> method) {
List<Object> key = new ArrayList<>();
key.add( method.getMethod().getDefiningType() );
key.add( method.getMethod().getName() );
for ( ParameterBinding binding : method.getParameterBindings() ) {
key.add( binding.getType() );
key.add( binding.getVariableName() );
}
return key;
}

/**
* Compare the inheritance distance of parameters between two methods to determine if candidateMethod is better.
* Compares parameters in order, returns as soon as a better one is found.
*/
private <T extends Method> boolean isInheritanceBetter(SelectedMethod<T> candidateMethod,
SelectedMethod<T> currentBestMethod) {
List<ParameterBinding> candidateBindings = candidateMethod.getParameterBindings();
List<ParameterBinding> bestBindings = currentBestMethod.getParameterBindings();
List<Parameter> candidateParams = candidateMethod.getMethod().getParameters();
List<Parameter> bestParams = currentBestMethod.getMethod().getParameters();
int paramCount = candidateBindings.size();

for ( int i = 0; i < paramCount; i++ ) {
int candidateDistance = candidateBindings.get( i )
.getType()
.distanceTo( candidateParams.get( i ).getType() );
int bestDistance = bestBindings.get( i ).getType().distanceTo( bestParams.get( i ).getType() );
if ( candidateDistance < bestDistance ) {
return true;
}
else if ( candidateDistance > bestDistance ) {
return false;
}
}
return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import java.util.List;

import org.mapstruct.ap.internal.model.source.Method;
import org.mapstruct.ap.internal.option.Options;
import org.mapstruct.ap.internal.util.ElementUtils;
import org.mapstruct.ap.internal.util.FormattingMessager;
import org.mapstruct.ap.internal.util.TypeUtils;
Expand All @@ -24,20 +25,27 @@ public class MethodSelectors {
private final List<MethodSelector> selectors;

public MethodSelectors(TypeUtils typeUtils, ElementUtils elementUtils,
FormattingMessager messager) {
selectors = Arrays.asList(
FormattingMessager messager, Options options) {
List<MethodSelector> selectorList = new ArrayList<>( Arrays.asList(
new MethodFamilySelector(),
new TypeSelector( messager ),
new QualifierSelector( typeUtils, elementUtils ),
new TargetTypeSelector( typeUtils ),
new JavaxXmlElementDeclSelector( typeUtils ),
new JakartaXmlElementDeclSelector( typeUtils ),
new InheritanceSelector(),
new InheritanceSelector()
) );
if ( options != null && !options.isDisableLifecycleOverloadDeduplicateSelector() ) {
selectorList.add( new LifecycleOverloadDeduplicateSelector() );
}

selectorList.addAll( Arrays.asList(
new CreateOrUpdateSelector(),
new SourceRhsSelector(),
new FactoryParameterSelector(),
new MostSpecificResultTypeSelector()
);
) );
this.selectors = selectorList;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ public class Options {
private final boolean verbose;
private final NullValueMappingStrategyGem nullValueIterableMappingStrategy;
private final NullValueMappingStrategyGem nullValueMapMappingStrategy;
private final boolean disableLifecycleOverloadDeduplicateSelector;

//CHECKSTYLE:OFF
public Options(boolean suppressGeneratorTimestamp, boolean suppressGeneratorVersionComment,
Expand All @@ -36,7 +37,8 @@ public Options(boolean suppressGeneratorTimestamp, boolean suppressGeneratorVers
boolean disableBuilders,
boolean verbose,
NullValueMappingStrategyGem nullValueIterableMappingStrategy,
NullValueMappingStrategyGem nullValueMapMappingStrategy
NullValueMappingStrategyGem nullValueMapMappingStrategy,
boolean disableLifecycleOverloadDeduplicateSelector
) {
//CHECKSTYLE:ON
this.suppressGeneratorTimestamp = suppressGeneratorTimestamp;
Expand All @@ -50,6 +52,7 @@ public Options(boolean suppressGeneratorTimestamp, boolean suppressGeneratorVers
this.verbose = verbose;
this.nullValueIterableMappingStrategy = nullValueIterableMappingStrategy;
this.nullValueMapMappingStrategy = nullValueMapMappingStrategy;
this.disableLifecycleOverloadDeduplicateSelector = disableLifecycleOverloadDeduplicateSelector;
}

public boolean isSuppressGeneratorTimestamp() {
Expand Down Expand Up @@ -95,4 +98,8 @@ public NullValueMappingStrategyGem getNullValueIterableMappingStrategy() {
public NullValueMappingStrategyGem getNullValueMapMappingStrategy() {
return nullValueMapMappingStrategy;
}

public boolean isDisableLifecycleOverloadDeduplicateSelector() {
return disableLifecycleOverloadDeduplicateSelector;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ public MappingResolverImpl(FormattingMessager messager, ElementUtils elementUtil

this.conversions = new Conversions( typeFactory );
this.builtInMethods = new BuiltInMappingMethods( typeFactory );
this.methodSelectors = new MethodSelectors( typeUtils, elementUtils, messager );
this.methodSelectors = new MethodSelectors( typeUtils, elementUtils, messager, null );

this.verboseLogging = verboseLogging;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/*
* 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.bugs._3849;

public class Child extends Parent {
public Child() {
super();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/*
* 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.bugs._3849;

public class ChildDto extends ParentDto {
public ChildDto(String value) {
super( value );
}

public void setValue(String value) {
super.setValue( value );
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
* 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.bugs._3849;

import java.util.ArrayList;
import java.util.List;

import org.mapstruct.AfterMapping;
import org.mapstruct.BeforeMapping;
import org.mapstruct.Context;
import org.mapstruct.Mapper;
import org.mapstruct.MappingTarget;
import org.mapstruct.factory.Mappers;

@Mapper
public interface DeduplicateBySourceMapper {

DeduplicateBySourceMapper INSTANCE = Mappers.getMapper( DeduplicateBySourceMapper.class );
List<String> INVOKED_METHODS = new ArrayList<>();

ParentDto mapParent(Parent source, @Context MappingContext context);

ParentDto mapChild(Child source, @Context MappingContext context);

class MappingContext {
@BeforeMapping
void deduplicateBySourceForBefore(Parent source, @MappingTarget ParentDto target) {
INVOKED_METHODS.add( "beforeMappingParentSourceInOtherClass" );
}

@BeforeMapping
void deduplicateBySourceForBefore(Child sourceChild, @MappingTarget ParentDto target) {
INVOKED_METHODS.add( "beforeMappingChildSourceInOtherClass" );
}

@AfterMapping
void deduplicateBySource(Parent source, @MappingTarget ParentDto target) {
INVOKED_METHODS.add( "afterMappingParentSourceInOtherClass" );
}

@AfterMapping
void deduplicateBySource(Child sourceChild, @MappingTarget ParentDto target) {
INVOKED_METHODS.add( "afterMappingChildSourceInOtherClass" );
}
}

@BeforeMapping
default void deduplicateBySourceForBefore(Parent source, @MappingTarget ParentDto target) {
INVOKED_METHODS.add( "beforeMappingParentSource" );
}

@BeforeMapping
default void deduplicateBySourceForBefore(Child sourceChild, @MappingTarget ParentDto target) {
INVOKED_METHODS.add( "beforeMappingChildSource" );
}

@AfterMapping
default void deduplicateBySource(Parent source, @MappingTarget ParentDto target) {
INVOKED_METHODS.add( "afterMappingParentSource" );
}

@AfterMapping
default void deduplicateBySource(Child sourceChild, @MappingTarget ParentDto target) {
INVOKED_METHODS.add( "afterMappingChildSource" );
}
}
Loading