From 2a849dca12c6547a56de6cd68d49bad1ca53390a Mon Sep 17 00:00:00 2001 From: Sjaak Derksen Date: Thu, 16 Jul 2020 13:49:49 +0200 Subject: [PATCH] #2145 fixing 2 step mapping methods (refactoring) (#2146) --- .../ap/internal/model/common/Type.java | 18 + .../ap/internal/model/source/Method.java | 14 + .../internal/model/source/SourceMethod.java | 12 +- .../model/source/builtin/BuiltInMethod.java | 20 +- .../model/source/builtin/JaxbElemToValue.java | 10 +- .../model/source/selector/SelectedMethod.java | 18 + .../selector/XmlElementDeclSelector.java | 2 +- .../creation/MappingResolverImpl.java | 749 +++++++++++------- .../ap/internal/util/Collections.java | 13 + .../mapstruct/ap/internal/util/Message.java | 4 + .../ap/test/bugs/_2145/Issue2145Mapper.java | 85 ++ .../ap/test/bugs/_2145/Issue2145Test.java | 34 + .../twosteperror/ErroneousMapperCM.java | 56 ++ .../twosteperror/ErroneousMapperMC.java | 52 ++ .../twosteperror/ErroneousMapperMM.java | 74 ++ .../twosteperror/TwoStepMappingTest.java | 79 ++ 16 files changed, 946 insertions(+), 294 deletions(-) create mode 100644 processor/src/test/java/org/mapstruct/ap/test/bugs/_2145/Issue2145Mapper.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/bugs/_2145/Issue2145Test.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/selection/twosteperror/ErroneousMapperCM.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/selection/twosteperror/ErroneousMapperMC.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/selection/twosteperror/ErroneousMapperMM.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/selection/twosteperror/TwoStepMappingTest.java diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/common/Type.java b/processor/src/main/java/org/mapstruct/ap/internal/model/common/Type.java index ea27153cc..a829fa61a 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/common/Type.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/common/Type.java @@ -482,6 +482,24 @@ public class Type extends ModelElement implements Comparable { return typeUtils.isAssignable( typeMirrorToMatch, other.typeMirror ); } + /** + * Whether this type is raw assignable to the given other type. We can't make a verdict on typevars, + * they need to be resolved first. + * + * @param other The other type. + * + * @return {@code true} if and only if this type is assignable to the given other type. + */ + public boolean isRawAssignableTo(Type other) { + if ( isTypeVar() || other.isTypeVar() ) { + return true; + } + if ( equals( other ) ) { + return true; + } + return typeUtils.isAssignable( typeUtils.erasure( typeMirror ), typeUtils.erasure( other.typeMirror ) ); + } + /** * getPropertyReadAccessors * diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/source/Method.java b/processor/src/main/java/org/mapstruct/ap/internal/model/source/Method.java index 3930dc76b..39d827d97 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/source/Method.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/source/Method.java @@ -187,4 +187,18 @@ public interface Method { default boolean isMappingTargetAssignableToReturnType() { return isUpdateMethod() && getResultType().isAssignableTo( getReturnType() ); } + + /** + * @return the first source type, intended for mapping methods from single source to target + */ + default Type getMappingSourceType() { + return getSourceParameters().get( 0 ).getType(); + } + + /** + * @return the short name for error messages + */ + default String shortName() { + return getResultType().getName() + ":" + getName() + "(" + getMappingSourceType().getName() + ")"; + } } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/source/SourceMethod.java b/processor/src/main/java/org/mapstruct/ap/internal/model/source/SourceMethod.java index 3551db8c5..9ed432d3e 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/source/SourceMethod.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/source/SourceMethod.java @@ -294,7 +294,7 @@ public class SourceMethod implements Method { return method.getDeclaringMapper() == null && method.isAbstract() && getSourceParameters().size() == 1 && method.getSourceParameters().size() == 1 - && first( getSourceParameters() ).getType().isAssignableTo( method.getResultType() ) + && getMappingSourceType().isAssignableTo( method.getResultType() ) && getResultType().isAssignableTo( first( method.getSourceParameters() ).getType() ); } @@ -326,7 +326,7 @@ public class SourceMethod implements Method { public boolean isIterableMapping() { if ( isIterableMapping == null ) { isIterableMapping = getSourceParameters().size() == 1 - && first( getSourceParameters() ).getType().isIterableType() + && getMappingSourceType().isIterableType() && getResultType().isIterableType(); } return isIterableMapping; @@ -335,9 +335,9 @@ public class SourceMethod implements Method { public boolean isStreamMapping() { if ( isStreamMapping == null ) { isStreamMapping = getSourceParameters().size() == 1 - && ( first( getSourceParameters() ).getType().isIterableType() && getResultType().isStreamType() - || first( getSourceParameters() ).getType().isStreamType() && getResultType().isIterableType() - || first( getSourceParameters() ).getType().isStreamType() && getResultType().isStreamType() ); + && ( getMappingSourceType().isIterableType() && getResultType().isStreamType() + || getMappingSourceType().isStreamType() && getResultType().isIterableType() + || getMappingSourceType().isStreamType() && getResultType().isStreamType() ); } return isStreamMapping; } @@ -345,7 +345,7 @@ public class SourceMethod implements Method { public boolean isMapMapping() { if ( isMapMapping == null ) { isMapMapping = getSourceParameters().size() == 1 - && first( getSourceParameters() ).getType().isMapType() + && getMappingSourceType().isMapType() && getResultType().isMapType(); } return isMapMapping; diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/source/builtin/BuiltInMethod.java b/processor/src/main/java/org/mapstruct/ap/internal/model/source/builtin/BuiltInMethod.java index 0621a68db..360eb56ba 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/source/builtin/BuiltInMethod.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/source/builtin/BuiltInMethod.java @@ -67,21 +67,13 @@ public abstract class BuiltInMethod implements Method { Type sourceType = first( sourceTypes ); - if ( getReturnType().isAssignableTo( targetType.erasure() ) - && sourceType.erasure().isAssignableTo( getParameter().getType() ) ) { - return doTypeVarsMatch( sourceType, targetType ); + Type returnType = getReturnType().resolveTypeVarToType( sourceType, getParameter().getType() ); + if ( returnType == null ) { + return false; } - if ( getReturnType().getFullyQualifiedName().equals( "java.lang.Object" ) - && sourceType.erasure().isAssignableTo( getParameter().getType() ) ) { - // return type could be a type parameter T - return doTypeVarsMatch( sourceType, targetType ); - } - if ( getReturnType().isAssignableTo( targetType.erasure() ) - && getParameter().getType().getFullyQualifiedName().equals( "java.lang.Object" ) ) { - // parameter type could be a type parameter T - return doTypeVarsMatch( sourceType, targetType ); - } - return false; + + return returnType.isAssignableTo( targetType ) + && sourceType.erasure().isAssignableTo( getParameter().getType() ); } @Override diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/source/builtin/JaxbElemToValue.java b/processor/src/main/java/org/mapstruct/ap/internal/model/source/builtin/JaxbElemToValue.java index 1ae3efe3c..4d116266f 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/source/builtin/JaxbElemToValue.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/source/builtin/JaxbElemToValue.java @@ -5,15 +5,16 @@ */ package org.mapstruct.ap.internal.model.source.builtin; +import static org.mapstruct.ap.internal.util.Collections.asSet; + import java.util.Set; + import javax.xml.bind.JAXBElement; import org.mapstruct.ap.internal.model.common.Parameter; import org.mapstruct.ap.internal.model.common.Type; import org.mapstruct.ap.internal.model.common.TypeFactory; -import static org.mapstruct.ap.internal.util.Collections.asSet; - /** * @author Sjaak Derksen */ @@ -24,8 +25,9 @@ public class JaxbElemToValue extends BuiltInMethod { private final Set importTypes; public JaxbElemToValue(TypeFactory typeFactory) { - this.parameter = new Parameter( "element", typeFactory.getType( JAXBElement.class ) ); - this.returnType = typeFactory.getType( Object.class ); + Type type = typeFactory.getType( JAXBElement.class ); + this.parameter = new Parameter( "element", type ); + this.returnType = type.getTypeParameters().get( 0 ); this.importTypes = asSet( parameter.getType() ); } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/source/selector/SelectedMethod.java b/processor/src/main/java/org/mapstruct/ap/internal/model/source/selector/SelectedMethod.java index bc0696d2b..7acb1af21 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/source/selector/SelectedMethod.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/source/selector/SelectedMethod.java @@ -6,6 +6,7 @@ package org.mapstruct.ap.internal.model.source.selector; import java.util.List; +import java.util.Objects; import org.mapstruct.ap.internal.model.common.ParameterBinding; import org.mapstruct.ap.internal.model.source.Method; @@ -39,4 +40,21 @@ public class SelectedMethod { public String toString() { return method.toString(); } + + @Override + public boolean equals(Object o) { + if ( this == o ) { + return true; + } + if ( o == null || getClass() != o.getClass() ) { + return false; + } + SelectedMethod that = (SelectedMethod) o; + return method.equals( that.method ); + } + + @Override + public int hashCode() { + return Objects.hash( method ); + } } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/source/selector/XmlElementDeclSelector.java b/processor/src/main/java/org/mapstruct/ap/internal/model/source/selector/XmlElementDeclSelector.java index cb9d30917..c3c394aef 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/source/selector/XmlElementDeclSelector.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/source/selector/XmlElementDeclSelector.java @@ -69,7 +69,7 @@ public class XmlElementDeclSelector implements MethodSelector { } String name = xmlElementDecl.name().get(); - TypeMirror scope = xmlElementDecl.scope().get(); + TypeMirror scope = xmlElementDecl.scope().getValue(); boolean nameIsSetAndMatches = name != null && name.equals( xmlElementRefInfo.nameValue() ); boolean scopeIsSetAndMatches = diff --git a/processor/src/main/java/org/mapstruct/ap/internal/processor/creation/MappingResolverImpl.java b/processor/src/main/java/org/mapstruct/ap/internal/processor/creation/MappingResolverImpl.java index 5427ddefa..5edd79294 100755 --- a/processor/src/main/java/org/mapstruct/ap/internal/processor/creation/MappingResolverImpl.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/processor/creation/MappingResolverImpl.java @@ -5,12 +5,23 @@ */ package org.mapstruct.ap.internal.processor.creation; +import static java.util.Collections.singletonList; +import static org.mapstruct.ap.internal.util.Collections.first; +import static org.mapstruct.ap.internal.util.Collections.firstKey; +import static org.mapstruct.ap.internal.util.Collections.firstValue; + import java.util.ArrayList; import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; +import java.util.Objects; import java.util.Set; +import java.util.function.Function; import java.util.function.Supplier; import java.util.stream.Collectors; + import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.Element; import javax.lang.model.element.ExecutableElement; @@ -42,7 +53,6 @@ import org.mapstruct.ap.internal.model.common.SourceRHS; import org.mapstruct.ap.internal.model.common.Type; import org.mapstruct.ap.internal.model.common.TypeFactory; import org.mapstruct.ap.internal.model.source.Method; -import org.mapstruct.ap.internal.model.source.SourceMethod; import org.mapstruct.ap.internal.model.source.builtin.BuiltInMappingMethods; import org.mapstruct.ap.internal.model.source.builtin.BuiltInMethod; import org.mapstruct.ap.internal.model.source.selector.MethodSelectors; @@ -55,9 +65,6 @@ import org.mapstruct.ap.internal.util.MessageConstants; import org.mapstruct.ap.internal.util.NativeTypes; import org.mapstruct.ap.internal.util.Strings; -import static java.util.Collections.singletonList; -import static org.mapstruct.ap.internal.util.Collections.first; - /** * The one and only implementation of {@link MappingResolver}. The class has been split into an interface an * implementation for the sake of avoiding package dependencies. Specifically, this implementation refers to classes @@ -78,6 +85,8 @@ public class MappingResolverImpl implements MappingResolver { private final BuiltInMappingMethods builtInMethods; private final MethodSelectors methodSelectors; + private static final String JL_OBJECT_NAME = Object.class.getName(); + /** * Private methods which are not present in the original mapper interface and are added to map certain property * types. @@ -113,7 +122,9 @@ public class MappingResolverImpl implements MappingResolver { sourceRHS, criteria, positionHint, - forger + forger, + builtInMethods.getBuiltInMethods(), + messager ); return attempt.getTargetAssignment( sourceRHS.getSourceTypeForMatching(), targetType ); @@ -141,10 +152,11 @@ public class MappingResolverImpl implements MappingResolver { private final List methods; private final SelectionCriteria selectionCriteria; private final SourceRHS sourceRHS; - private final boolean savedPreferUpdateMapping; private final FormattingParameters formattingParameters; private final AnnotationMirror positionHint; private final Supplier forger; + private final List builtIns; + private final FormattingMessager messager; // resolving via 2 steps creates the possibility of wrong matches, first builtin method matches, // second doesn't. In that case, the first builtin method should not lead to a supported method @@ -155,7 +167,9 @@ public class MappingResolverImpl implements MappingResolver { FormattingParameters formattingParameters, SourceRHS sourceRHS, SelectionCriteria criteria, AnnotationMirror positionHint, - Supplier forger) { + Supplier forger, + List builtIns, + FormattingMessager messager) { this.mappingMethod = mappingMethod; this.methods = filterPossibleCandidateMethods( sourceModel ); @@ -164,9 +178,10 @@ public class MappingResolverImpl implements MappingResolver { this.sourceRHS = sourceRHS; this.supportingMethodCandidates = new HashSet<>(); this.selectionCriteria = criteria; - this.savedPreferUpdateMapping = criteria.isPreferUpdateMapping(); this.positionHint = positionHint; this.forger = forger; + this.builtIns = builtIns; + this.messager = messager; } private List filterPossibleCandidateMethods(List candidateMethods) { @@ -182,14 +197,16 @@ public class MappingResolverImpl implements MappingResolver { private Assignment getTargetAssignment(Type sourceType, Type targetType) { - Assignment referencedMethod; + Assignment assignment; // first simple mapping method if ( allowMappingMethod() ) { - referencedMethod = resolveViaMethod( sourceType, targetType, false ); - if ( referencedMethod != null ) { - referencedMethod.setAssignment( sourceRHS ); - return referencedMethod; + List> matches = getBestMatch( methods, sourceType, targetType ); + reportErrorWhenAmbigious( matches, targetType ); + if ( !matches.isEmpty() ) { + assignment = toMethodRef( first( matches ) ); + assignment.setAssignment( sourceRHS ); + return assignment; } } @@ -228,38 +245,40 @@ public class MappingResolverImpl implements MappingResolver { // check for a built-in method if ( !hasQualfiers() ) { - Assignment builtInMethod = resolveViaBuiltInMethod( sourceType, targetType ); - if ( builtInMethod != null ) { - builtInMethod.setAssignment( sourceRHS ); + List> matches = getBestMatch( builtIns, sourceType, targetType ); + reportErrorWhenAmbigious( matches, targetType ); + if ( !matches.isEmpty() ) { + assignment = toBuildInRef( first( matches ) ); + assignment.setAssignment( sourceRHS ); usedSupportedMappings.addAll( supportingMethodCandidates ); - return builtInMethod; + return assignment; } } } if ( allow2Steps() ) { // 2 step method, first: method(method(source)) - referencedMethod = resolveViaMethodAndMethod( sourceType, targetType ); - if ( referencedMethod != null ) { + assignment = MethodMethod.getBestMatch( this, sourceType, targetType ); + if ( assignment != null ) { usedSupportedMappings.addAll( supportingMethodCandidates ); - return referencedMethod; + return assignment; } // 2 step method, then: method(conversion(source)) - referencedMethod = resolveViaConversionAndMethod( sourceType, targetType ); - if ( referencedMethod != null ) { + assignment = ConversionMethod.getBestMatch( this, sourceType, targetType ); + if ( assignment != null ) { usedSupportedMappings.addAll( supportingMethodCandidates ); - return referencedMethod; + return assignment; } // stop here when looking for update methods. selectionCriteria.setPreferUpdateMapping( false ); // 2 step method, finally: conversion(method(source)) - ConversionAssignment conversion = resolveViaMethodAndConversion( sourceType, targetType ); - if ( conversion != null ) { + assignment = MethodConversion.getBestMatch( this, sourceType, targetType ); + if ( assignment != null ) { usedSupportedMappings.addAll( supportingMethodCandidates ); - return conversion.getAssignment(); + return assignment; } } @@ -395,232 +414,6 @@ public class MappingResolverImpl implements MappingResolver { return null; } - /** - * Returns a reference to a method mapping the given source type to the given target type, if such a method - * exists. - */ - private Assignment resolveViaMethod(Type sourceType, Type targetType, boolean considerBuiltInMethods) { - - // first try to find a matching source method - SelectedMethod matchingSourceMethod = getBestMatch( methods, sourceType, targetType ); - - if ( matchingSourceMethod != null ) { - return getMappingMethodReference( matchingSourceMethod ); - } - - if ( considerBuiltInMethods ) { - return resolveViaBuiltInMethod( sourceType, targetType ); - } - - return null; - } - - /** - * Applies matching to given method only - * - * @param sourceType the source type - * @param targetType the target type - * @param method the method to match - * @return an assignment if a match, given the criteria could be found. When the method is a - * buildIn method, all the bookkeeping is applied. - */ - private Assignment applyMatching(Type sourceType, Type targetType, Method method) { - - if ( method instanceof SourceMethod ) { - SelectedMethod selectedMethod = - getBestMatch( java.util.Collections.singletonList( method ), sourceType, targetType ); - return selectedMethod != null ? getMappingMethodReference( selectedMethod ) : null; - } - else if ( method instanceof BuiltInMethod ) { - return resolveViaBuiltInMethod( sourceType, targetType ); - } - return null; - } - - private Assignment resolveViaBuiltInMethod(Type sourceType, Type targetType) { - SelectedMethod matchingBuiltInMethod = - getBestMatch( builtInMethods.getBuiltInMethods(), sourceType, targetType ); - - if ( matchingBuiltInMethod != null ) { - return createFromBuiltInMethod( matchingBuiltInMethod.getMethod() ); - } - - return null; - } - - private Assignment createFromBuiltInMethod(BuiltInMethod method) { - Set allUsedFields = new HashSet<>( mapperReferences ); - SupportingField.addAllFieldsIn( supportingMethodCandidates, allUsedFields ); - SupportingMappingMethod supportingMappingMethod = new SupportingMappingMethod( method, allUsedFields ); - supportingMethodCandidates.add( supportingMappingMethod ); - ConversionContext ctx = new DefaultConversionContext( - typeFactory, - messager, - method.getSourceParameters().get( 0 ).getType(), - method.getResultType(), - formattingParameters - ); - Assignment methodReference = MethodReference.forBuiltInMethod( method, ctx ); - methodReference.setAssignment( sourceRHS ); - return methodReference; - } - - /** - * Suppose mapping required from A to C and: - *
    - *
  • no direct referenced mapping method either built-in or referenced is available from A to C
  • - *
  • no conversion is available
  • - *
  • there is a method from A to B, methodX
  • - *
  • there is a method from B to C, methodY
  • - *
- * then this method tries to resolve this combination and make a mapping methodY( methodX ( parameter ) ) - */ - private Assignment resolveViaMethodAndMethod(Type sourceType, Type targetType) { - - List methodYCandidates = new ArrayList<>( methods ); - methodYCandidates.addAll( builtInMethods.getBuiltInMethods() ); - - Assignment methodRefY = null; - - // Iterate over all source methods. Check if the return type matches with the parameter that we need. - // so assume we need a method from A to C we look for a methodX from A to B (all methods in the - // list form such a candidate). - // For each of the candidates, we need to look if there's a methodY, either - // sourceMethod or builtIn that fits the signature B to C. Only then there is a match. If we have a match - // a nested method call can be called. so C = methodY( methodX (A) ) - for ( Method methodYCandidate : methodYCandidates ) { - Type ySourceType = methodYCandidate.getSourceParameters().get( 0 ).getType(); - if ( Object.class.getName().equals( ySourceType.getName() ) ) { - // java.lang.Object as intermediate result - continue; - } - - ySourceType = ySourceType.resolveTypeVarToType( targetType, methodYCandidate.getResultType() ); - - if ( ySourceType != null ) { - methodRefY = applyMatching( ySourceType, targetType, methodYCandidate ); - if ( methodRefY != null ) { - - selectionCriteria.setPreferUpdateMapping( false ); - Assignment methodRefX = resolveViaMethod( sourceType, ySourceType, true ); - selectionCriteria.setPreferUpdateMapping( savedPreferUpdateMapping ); - if ( methodRefX != null ) { - methodRefY.setAssignment( methodRefX ); - methodRefX.setAssignment( sourceRHS ); - break; - } - else { - // both should match; - supportingMethodCandidates.clear(); - methodRefY = null; - } - } - } - } - return methodRefY; - } - - /** - * Suppose mapping required from A to C and: - *
    - *
  • there is a conversion from A to B, conversionX
  • - *
  • there is a method from B to C, methodY
  • - *
- * then this method tries to resolve this combination and make a mapping methodY( conversionX ( parameter ) ) - * - * Instead of directly using a built in method candidate, all the return types as 'B' of all available built-in - * methods are used to resolve a mapping (assignment) from result type to 'B'. If a match is found, an attempt - * is done to find a matching type conversion. - */ - private Assignment resolveViaConversionAndMethod(Type sourceType, Type targetType) { - - List methodYCandidates = new ArrayList<>( methods ); - methodYCandidates.addAll( builtInMethods.getBuiltInMethods() ); - - Assignment methodRefY = null; - - for ( Method methodYCandidate : methodYCandidates ) { - Type ySourceType = methodYCandidate.getSourceParameters().get( 0 ).getType(); - if ( Object.class.getName().equals( ySourceType.getName() ) ) { - // java.lang.Object as intermediate result - continue; - } - - ySourceType = ySourceType.resolveTypeVarToType( targetType, methodYCandidate.getResultType() ); - - if ( ySourceType != null ) { - methodRefY = applyMatching( ySourceType, targetType, methodYCandidate ); - if ( methodRefY != null ) { - ConversionAssignment conversionXRef = resolveViaConversion( sourceType, ySourceType ); - if ( conversionXRef != null ) { - methodRefY.setAssignment( conversionXRef.getAssignment() ); - conversionXRef.getAssignment().setAssignment( sourceRHS ); - conversionXRef.reportMessageWhenNarrowing( messager, this ); - break; - } - else { - // both should match - supportingMethodCandidates.clear(); - methodRefY = null; - } - } - } - } - return methodRefY; - } - - /** - * Suppose mapping required from A to C and: - *
    - *
  • there is a method from A to B, methodX
  • - *
  • there is a conversion from B to C, conversionY
  • - *
- * then this method tries to resolve this combination and make a mapping conversionY( methodX ( parameter ) ) - * - * Instead of directly using a built in method candidate, all the return types as 'B' of all available built-in - * methods are used to resolve a mapping (assignment) from source type to 'B'. If a match is found, an attempt - * is done to find a matching type conversion. - */ - private ConversionAssignment resolveViaMethodAndConversion(Type sourceType, Type targetType) { - - List methodXCandidates = new ArrayList<>( methods ); - methodXCandidates.addAll( builtInMethods.getBuiltInMethods() ); - - ConversionAssignment conversionYRef = null; - - // search the other way around - for ( Method methodXCandidate : methodXCandidates ) { - Type xTargetType = methodXCandidate.getReturnType(); - if ( methodXCandidate.isUpdateMethod() || - Object.class.getName().equals( xTargetType.getFullyQualifiedName() ) ) { - // skip update methods || java.lang.Object as intermediate result - continue; - } - - xTargetType = - xTargetType.resolveTypeVarToType( sourceType, methodXCandidate.getParameters().get( 0 ).getType() ); - - if ( xTargetType != null ) { - Assignment methodRefX = applyMatching( sourceType, xTargetType, methodXCandidate ); - if ( methodRefX != null ) { - conversionYRef = resolveViaConversion( xTargetType, targetType ); - if ( conversionYRef != null ) { - conversionYRef.getAssignment().setAssignment( methodRefX ); - methodRefX.setAssignment( sourceRHS ); - conversionYRef.reportMessageWhenNarrowing( messager, this ); - break; - } - else { - // both should match; - supportingMethodCandidates.clear(); - conversionYRef = null; - } - } - } - } - return conversionYRef; - } - private boolean isCandidateForMapping(Method methodCandidate) { return isCreateMethodForMapping( methodCandidate ) || isUpdateMethodForMapping( methodCandidate ); } @@ -640,15 +433,17 @@ public class MappingResolverImpl implements MappingResolver { && !methodCandidate.isLifecycleCallbackMethod(); } - private SelectedMethod getBestMatch(List methods, Type sourceType, Type returnType) { - - List> candidates = methodSelectors.getMatchingMethods( + private List> getBestMatch(List methods, Type source, Type target) { + return methodSelectors.getMatchingMethods( mappingMethod, methods, - singletonList( sourceType ), - returnType, + singletonList( source ), + target, selectionCriteria ); + } + + private void reportErrorWhenAmbigious(List> candidates, Type target) { // raise an error if more than one mapping method is suitable to map the given source type // into the target type @@ -660,7 +455,7 @@ public class MappingResolverImpl implements MappingResolver { positionHint, Message.GENERAL_AMBIGIOUS_MAPPING_METHOD, sourceRHS.getSourceErrorMessagePart(), - returnType, + target, Strings.join( candidates, ", " ) ); } @@ -669,29 +464,41 @@ public class MappingResolverImpl implements MappingResolver { mappingMethod.getExecutable(), positionHint, Message.GENERAL_AMBIGIOUS_FACTORY_METHOD, - returnType, + target, Strings.join( candidates, ", " ) ); } } - - if ( !candidates.isEmpty() ) { - return first( candidates ); - } - - return null; } - private Assignment getMappingMethodReference(SelectedMethod method) { - MapperReference mapperReference = findMapperReference( method.getMethod() ); + private Assignment toMethodRef(SelectedMethod selectedMethod) { + MapperReference mapperReference = findMapperReference( selectedMethod.getMethod() ); return MethodReference.forMapperReference( - method.getMethod(), + selectedMethod.getMethod(), mapperReference, - method.getParameterBindings() + selectedMethod.getParameterBindings() ); } + private Assignment toBuildInRef(SelectedMethod selectedMethod) { + BuiltInMethod method = selectedMethod.getMethod(); + Set allUsedFields = new HashSet<>( mapperReferences ); + SupportingField.addAllFieldsIn( supportingMethodCandidates, allUsedFields ); + SupportingMappingMethod supportingMappingMethod = new SupportingMappingMethod( method, allUsedFields ); + supportingMethodCandidates.add( supportingMappingMethod ); + ConversionContext ctx = new DefaultConversionContext( + typeFactory, + messager, + method.getMappingSourceType(), + method.getResultType(), + formattingParameters + ); + Assignment methodReference = MethodReference.forBuiltInMethod( method, ctx ); + methodReference.setAssignment( sourceRHS ); + return methodReference; + } + /** * Whether the given source and target type are both a collection type or both a map type and the source value * can be propagated via a copy constructor. @@ -770,6 +577,7 @@ public class MappingResolverImpl implements MappingResolver { return false; } + } private static class ConversionAssignment { @@ -813,5 +621,408 @@ public class MappingResolverImpl implements MappingResolver { ); } + String shortName() { + return sourceType.getName() + "-->" + targetType.getName(); + } + + @Override + public boolean equals(Object o) { + if ( this == o ) { + return true; + } + if ( o == null || getClass() != o.getClass() ) { + return false; + } + ConversionAssignment that = (ConversionAssignment) o; + return sourceType.equals( that.sourceType ) && targetType.equals( that.targetType ); + } + + @Override + public int hashCode() { + return Objects.hash( sourceType, targetType ); + } } + + /** + * Suppose mapping required from A to C and: + *
    + *
  • no direct referenced mapping method either built-in or referenced is available from A to C
  • + *
  • no conversion is available
  • + *
  • there is a method from A to B, methodX
  • + *
  • there is a method from B to C, methodY
  • + *
+ * then this method tries to resolve this combination and make a mapping methodY( methodX ( parameter ) ) + * + * NOTE method X cannot be an update method + */ + private static class MethodMethod { + + private final ResolvingAttempt attempt; + private final List xMethods; + private final List yMethods; + private final Function, Assignment> xCreate; + private final Function, Assignment> yCreate; + + // results + private boolean hasResult = false; + private Assignment result = null; + + static Assignment getBestMatch(ResolvingAttempt att, Type sourceType, Type targetType) { + MethodMethod mmAttempt = + new MethodMethod<>( att, att.methods, att.methods, att::toMethodRef, att::toMethodRef ) + .getBestMatch( sourceType, targetType ); + if ( mmAttempt.hasResult ) { + return mmAttempt.result; + } + MethodMethod mbAttempt = + new MethodMethod<>( att, att.methods, att.builtIns, att::toMethodRef, att::toBuildInRef ) + .getBestMatch( sourceType, targetType ); + if ( mbAttempt.hasResult ) { + return mbAttempt.result; + } + MethodMethod bmAttempt = + new MethodMethod<>( att, att.builtIns, att.methods, att::toBuildInRef, att::toMethodRef ) + .getBestMatch( sourceType, targetType ); + if ( bmAttempt.hasResult ) { + return bmAttempt.result; + } + MethodMethod bbAttempt = + new MethodMethod<>( att, att.builtIns, att.builtIns, att::toBuildInRef, att::toBuildInRef ) + .getBestMatch( sourceType, targetType ); + return bbAttempt.result; + } + + MethodMethod(ResolvingAttempt attempt, List xMethods, List yMethods, + Function, Assignment> xCreate, + Function, Assignment> yCreate) { + this.attempt = attempt; + this.xMethods = xMethods; + this.yMethods = yMethods; + this.xCreate = xCreate; + this.yCreate = yCreate; + } + + private MethodMethod getBestMatch(Type sourceType, Type targetType) { + + Set yCandidates = new HashSet<>(); + Map, List>> xCandidates = new LinkedHashMap<>(); + Map, Type> typesInTheMiddle = new LinkedHashMap<>(); + + // Iterate over all source methods. Check if the return type matches with the parameter that we need. + // so assume we need a method from A to C we look for a methodX from A to B (all methods in the + // list form such a candidate). + // For each of the candidates, we need to look if there's a methodY, either + // sourceMethod or builtIn that fits the signature B to C. Only then there is a match. If we have a match + // a nested method call can be called. so C = methodY( methodX (A) ) + attempt.selectionCriteria.setPreferUpdateMapping( false ); + for ( T2 yCandidate : yMethods ) { + Type ySourceType = yCandidate.getMappingSourceType(); + ySourceType = ySourceType.resolveTypeVarToType( targetType, yCandidate.getResultType() ); + Type yTargetType = yCandidate.getResultType(); + if ( ySourceType == null + || !yTargetType.isRawAssignableTo( targetType ) + || JL_OBJECT_NAME.equals( ySourceType.getFullyQualifiedName() ) ) { + // java.lang.Object as intermediate result + continue; + } + List> xMatches = attempt.getBestMatch( xMethods, sourceType, ySourceType ); + if ( !xMatches.isEmpty() ) { + xMatches.stream().forEach( x -> xCandidates.put( x, new ArrayList<>() ) ); + final Type typeInTheMiddle = ySourceType; + xMatches.stream().forEach( x -> typesInTheMiddle.put( x, typeInTheMiddle ) ); + yCandidates.add( yCandidate ); + } + } + attempt.selectionCriteria.setPreferUpdateMapping( true ); + + // collect all results + List yCandidatesList = new ArrayList<>( yCandidates ); + Iterator, List>>> i = xCandidates.entrySet().iterator(); + while ( i.hasNext() ) { + Map.Entry, List>> entry = i.next(); + Type typeInTheMiddle = typesInTheMiddle.get( entry.getKey() ); + entry.getValue().addAll( attempt.getBestMatch( yCandidatesList, typeInTheMiddle, targetType ) ); + if ( entry.getValue().isEmpty() ) { + i.remove(); + } + } + + // no results left + if ( xCandidates.isEmpty() ) { + return this; + } + hasResult = true; + + // get result, there should be one entry left with only one value + if ( xCandidates.size() == 1 && firstValue( xCandidates ).size() == 1 ) { + Assignment methodRefY = yCreate.apply( first( firstValue( xCandidates ) ) ); + Assignment methodRefX = xCreate.apply( firstKey( xCandidates ) ); + methodRefY.setAssignment( methodRefX ); + methodRefX.setAssignment( attempt.sourceRHS ); + result = methodRefY; + } + else { + reportAmbigiousError( xCandidates, targetType ); + } + return this; + + } + + void reportAmbigiousError(Map, List>> xCandidates, Type target) { + StringBuilder result = new StringBuilder(); + xCandidates.entrySet() + .stream() + .forEach( e -> result.append( "method(s)Y: " ) + .append( e.getValue() + .stream() + .map( v -> v.getMethod().shortName() ) + .collect( Collectors.joining( ", " ) ) ) + .append( ", methodX: " ) + .append( e.getKey().getMethod().shortName() ) + .append( "; " ) ); + attempt.messager.printMessage( + attempt.mappingMethod.getExecutable(), + attempt.positionHint, + Message.GENERAL_AMBIGIOUS_MAPPING_METHODY_METHODX, + attempt.sourceRHS.getSourceType().getName() + " " + attempt.sourceRHS.getSourceParameterName(), + target.getName(), + result.toString() ); + } + } + + /** + * Suppose mapping required from A to C and: + *
    + *
  • there is a conversion from A to B, conversionX
  • + *
  • there is a method from B to C, methodY
  • + *
+ * then this method tries to resolve this combination and make a mapping methodY( conversionX ( parameter ) ) + * + * Instead of directly using a built in method candidate, all the return types as 'B' of all available built-in + * methods are used to resolve a mapping (assignment) from result type to 'B'. If a match is found, an attempt + * is done to find a matching type conversion. + */ + private static class ConversionMethod { + + private final ResolvingAttempt attempt; + private final List methods; + private final Function, Assignment> create; + + // results + private boolean hasResult = false; + private Assignment result = null; + + static Assignment getBestMatch(ResolvingAttempt att, Type sourceType, Type targetType) { + ConversionMethod mAttempt = new ConversionMethod<>( att, att.methods, att::toMethodRef ) + .getBestMatch( sourceType, targetType ); + if ( mAttempt.hasResult ) { + return mAttempt.result; + } + ConversionMethod bAttempt = + new ConversionMethod<>( att, att.builtIns, att::toBuildInRef ) + .getBestMatch( sourceType, targetType ); + return bAttempt.result; + } + + ConversionMethod(ResolvingAttempt attempt, List methods, Function, Assignment> create) { + this.attempt = attempt; + this.methods = methods; + this.create = create; + } + + private ConversionMethod getBestMatch(Type sourceType, Type targetType) { + + List yCandidates = new ArrayList<>(); + Map>> xRefCandidates = new LinkedHashMap<>(); + + for ( T yCandidate : methods ) { + Type ySourceType = yCandidate.getMappingSourceType(); + ySourceType = ySourceType.resolveTypeVarToType( targetType, yCandidate.getResultType() ); + Type yTargetType = yCandidate.getResultType(); + if ( ySourceType == null + || !yTargetType.isRawAssignableTo( targetType ) + || JL_OBJECT_NAME.equals( ySourceType.getFullyQualifiedName() ) ) { + // java.lang.Object as intermediate result + continue; + } + ConversionAssignment xRefCandidate = attempt.resolveViaConversion( sourceType, ySourceType ); + if ( xRefCandidate != null ) { + xRefCandidates.put( xRefCandidate, new ArrayList<>() ); + yCandidates.add( yCandidate ); + } + } + + // collect all results + Iterator>>> i = xRefCandidates.entrySet().iterator(); + while ( i.hasNext() ) { + Map.Entry>> entry = i.next(); + entry.getValue().addAll( attempt.getBestMatch( yCandidates, entry.getKey().targetType, targetType ) ); + if ( entry.getValue().isEmpty() ) { + i.remove(); + } + } + + // no results left + if ( xRefCandidates.isEmpty() ) { + return this; + } + hasResult = true; + + // get result, there should be one entry left with only one value + if ( xRefCandidates.size() == 1 && firstValue( xRefCandidates ).size() == 1 ) { + Assignment methodRefY = create.apply( first( firstValue( xRefCandidates ) ) ); + ConversionAssignment conversionRefX = firstKey( xRefCandidates ); + conversionRefX.reportMessageWhenNarrowing( attempt.messager, attempt ); + methodRefY.setAssignment( conversionRefX.assignment ); + conversionRefX.assignment.setAssignment( attempt.sourceRHS ); + result = methodRefY; + } + else { + reportAmbigiousError( xRefCandidates, targetType ); + } + return this; + + } + + void reportAmbigiousError(Map>> xRefCandidates, Type target) { + StringBuilder result = new StringBuilder(); + xRefCandidates.entrySet() + .stream() + .forEach( e -> result.append( "method(s)Y: " ) + .append( e.getValue() + .stream() + .map( v -> v.getMethod().shortName() ) + .collect( Collectors.joining( ", " ) ) ) + .append( ", conversionX: " ) + .append( e.getKey().shortName() ) + .append( "; " ) ); + attempt.messager.printMessage( + attempt.mappingMethod.getExecutable(), + attempt.positionHint, + Message.GENERAL_AMBIGIOUS_MAPPING_METHODY_CONVERSIONX, + attempt.sourceRHS.getSourceType().getName() + " " + attempt.sourceRHS.getSourceParameterName(), + target.getName(), + result.toString() ); + } + } + + /** + * Suppose mapping required from A to C and: + *
    + *
  • there is a method from A to B, methodX
  • + *
  • there is a conversion from B to C, conversionY
  • + *
+ * then this method tries to resolve this combination and make a mapping conversionY( methodX ( parameter ) ) + * + * Instead of directly using a built in method candidate, all the return types as 'B' of all available built-in + * methods are used to resolve a mapping (assignment) from source type to 'B'. If a match is found, an attempt + * is done to find a matching type conversion. + * + * NOTE methodX cannot be an update method + */ + private static class MethodConversion { + + private final ResolvingAttempt attempt; + private final List methods; + private final Function, Assignment> create; + + // results + private boolean hasResult = false; + private Assignment result = null; + + static Assignment getBestMatch(ResolvingAttempt att, Type sourceType, Type targetType) { + MethodConversion mAttempt = new MethodConversion<>( att, att.methods, att::toMethodRef ) + .getBestMatch( sourceType, targetType ); + if ( mAttempt.hasResult ) { + return mAttempt.result; + } + MethodConversion bAttempt = new MethodConversion<>( att, att.builtIns, att::toBuildInRef ) + .getBestMatch( sourceType, targetType ); + return bAttempt.result; + } + + MethodConversion(ResolvingAttempt attempt, List methods, Function, Assignment> create) { + this.attempt = attempt; + this.methods = methods; + this.create = create; + } + + private MethodConversion getBestMatch(Type sourceType, Type targetType) { + + List xCandidates = new ArrayList<>(); + Map>> yRefCandidates = new LinkedHashMap<>(); + + // search through methods, and select egible candidates + for ( T xCandidate : methods ) { + Type xTargetType = xCandidate.getReturnType(); + Type xSourceType = xCandidate.getMappingSourceType(); + xTargetType = xTargetType.resolveTypeVarToType( sourceType, xSourceType ); + if ( xTargetType == null + || xCandidate.isUpdateMethod() + || !sourceType.isRawAssignableTo( xSourceType ) + || JL_OBJECT_NAME.equals( xTargetType.getFullyQualifiedName() ) ) { + // skip update methods || java.lang.Object as intermediate result + continue; + } + ConversionAssignment yRefCandidate = attempt.resolveViaConversion( xTargetType, targetType ); + if ( yRefCandidate != null ) { + yRefCandidates.put( yRefCandidate, new ArrayList<>() ); + xCandidates.add( xCandidate ); + } + } + + // collect all results + Iterator>>> i = yRefCandidates.entrySet().iterator(); + while ( i.hasNext() ) { + Map.Entry>> entry = i.next(); + entry.getValue().addAll( attempt.getBestMatch( xCandidates, sourceType, entry.getKey().sourceType ) ); + if ( entry.getValue().isEmpty() ) { + i.remove(); + } + } + + // no results left + if ( yRefCandidates.isEmpty() ) { + return this; + } + hasResult = true; + + // get result, there should be one entry left with only one value + if ( yRefCandidates.size() == 1 && firstValue( yRefCandidates ).size() == 1 ) { + Assignment methodRefX = create.apply( first( firstValue( yRefCandidates ) ) ); + ConversionAssignment conversionRefY = firstKey( yRefCandidates ); + conversionRefY.reportMessageWhenNarrowing( attempt.messager, attempt ); + methodRefX.setAssignment( attempt.sourceRHS ); + conversionRefY.assignment.setAssignment( methodRefX ); + result = conversionRefY.assignment; + } + else { + reportAmbigiousError( yRefCandidates, targetType ); + } + return this; + + } + + void reportAmbigiousError(Map>> yRefCandidates, Type target) { + StringBuilder result = new StringBuilder(); + yRefCandidates.entrySet() + .stream() + .forEach( e -> result.append( "conversionY: " ) + .append( e.getKey().shortName() ) + .append( ", method(s)X: " ) + .append( e.getValue() + .stream() + .map( v -> v.getMethod().shortName() ) + .collect( Collectors.joining( ", " ) ) ) + .append( "; " ) ); + attempt.messager.printMessage( + attempt.mappingMethod.getExecutable(), + attempt.positionHint, + Message.GENERAL_AMBIGIOUS_MAPPING_CONVERSIONY_METHODX, + attempt.sourceRHS.getSourceType().getName() + " " + attempt.sourceRHS.getSourceParameterName(), + target.getName(), + result.toString() ); + } + } + } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/util/Collections.java b/processor/src/main/java/org/mapstruct/ap/internal/util/Collections.java index 7f2d5cd8d..365efc56c 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/util/Collections.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/util/Collections.java @@ -9,6 +9,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; /** @@ -63,4 +64,16 @@ public class Collections { return result; } + public static Map.Entry first(Map map) { + return map.entrySet().iterator().next(); + } + + public static V firstValue(Map map) { + return first( map ).getValue(); + } + + public static K firstKey(Map map) { + return first( map ).getKey(); + } + } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/util/Message.java b/processor/src/main/java/org/mapstruct/ap/internal/util/Message.java index ae66d3877..d10faf64b 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/util/Message.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/util/Message.java @@ -125,6 +125,10 @@ public enum Message { GENERAL_NO_QUALIFYING_METHOD_NAMED( "Qualifier error. No method found annotated with @Named#value: [ %s ]. See " + FAQ_QUALIFIER_URL + " for more info." ), GENERAL_NO_QUALIFYING_METHOD_COMBINED( "Qualifier error. No method found annotated with @Named#value: [ %s ], annotated with [ %s ]. See " + FAQ_QUALIFIER_URL + " for more info." ), + GENERAL_AMBIGIOUS_MAPPING_METHODY_METHODX( "Ambiguous 2step methods found, mapping %s to %s. Found methodY( methodX ( parameter ) ): %s." ), + GENERAL_AMBIGIOUS_MAPPING_CONVERSIONY_METHODX( "Ambiguous 2step methods found, mapping %s to %s. Found conversionY( methodX ( parameter ) ): %s." ), + GENERAL_AMBIGIOUS_MAPPING_METHODY_CONVERSIONX( "Ambiguous 2step methods found, mapping %s to %s. Found methodY( conversionX ( parameter ) ): %s." ), + BUILDER_MORE_THAN_ONE_BUILDER_CREATION_METHOD( "More than one builder creation method for \"%s\". Found methods: \"%s\". Builder will not be used. Consider implementing a custom BuilderProvider SPI.", Diagnostic.Kind.WARNING ), BUILDER_NO_BUILD_METHOD_FOUND("No build method \"%s\" found in \"%s\" for \"%s\". Found methods: \"%s\".", Diagnostic.Kind.ERROR ), BUILDER_NO_BUILD_METHOD_FOUND_DEFAULT("No build method \"%s\" found in \"%s\" for \"%s\". Found methods: \"%s\". Consider to add @Builder in order to select the correct build method.", Diagnostic.Kind.ERROR ), diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2145/Issue2145Mapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2145/Issue2145Mapper.java new file mode 100644 index 000000000..1b8969664 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2145/Issue2145Mapper.java @@ -0,0 +1,85 @@ +/* + * 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._2145; + +import javax.xml.bind.JAXBElement; +import javax.xml.bind.annotation.XmlElementDecl; +import javax.xml.namespace.QName; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; + +@Mapper( uses = Issue2145Mapper.ObjectFactory.class ) +public interface Issue2145Mapper { + + Issue2145Mapper INSTANCE = Mappers.getMapper( Issue2145Mapper.class ); + + @Mapping(target = "nested", source = "value") + Target map(Source source); + + default Nested map(String in) { + Nested nested = new Nested(); + nested.setValue( in ); + return nested; + } + + class Target { + + private JAXBElement nested; + + public JAXBElement getNested() { + return nested; + } + + public void setNested(JAXBElement nested) { + this.nested = nested; + } + } + + class Nested { + + private String value; + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + } + + class Source { + + private String value; + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + } + + class ObjectFactory { + + private static final QName Q_NAME = new QName( "http://www.test.com/test", "" ); + private static final QName Q_NAME_NESTED = new QName( "http://www.test.com/test", "nested" ); + + @XmlElementDecl(namespace = "http://www.test.com/test", name = "Nested") + public JAXBElement createNested(Nested value) { + return new JAXBElement( Q_NAME, Nested.class, null, value ); + } + + @XmlElementDecl(namespace = "http://www.test.com/test", name = "nested", scope = Nested.class) + public JAXBElement createNestedInNestedTarget(Nested value) { + return new JAXBElement( Q_NAME_NESTED, Nested.class, Target.class, value ); + } + + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2145/Issue2145Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2145/Issue2145Test.java new file mode 100644 index 000000000..02ad2ba90 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2145/Issue2145Test.java @@ -0,0 +1,34 @@ +/* + * 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._2145; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.runner.AnnotationProcessorTestRunner; + +import static org.assertj.core.api.Assertions.assertThat; + +@IssueKey("2145") +@WithClasses(Issue2145Mapper.class) +@RunWith(AnnotationProcessorTestRunner.class) +public class Issue2145Test { + + @Test + public void test() { + Issue2145Mapper.Source source = new Issue2145Mapper.Source(); + source.setValue( "test" ); + + Issue2145Mapper.Target target = Issue2145Mapper.INSTANCE.map( source ); + + assertThat( target ).isNotNull(); + assertThat( target.getNested() ).isNotNull(); + assertThat( target.getNested().getScope() ).isEqualTo( Issue2145Mapper.Target.class ); + assertThat( target.getNested().getValue() ).isNotNull(); + assertThat( target.getNested().getValue().getValue() ).isEqualTo( "test" ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/selection/twosteperror/ErroneousMapperCM.java b/processor/src/test/java/org/mapstruct/ap/test/selection/twosteperror/ErroneousMapperCM.java new file mode 100644 index 000000000..691005768 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/selection/twosteperror/ErroneousMapperCM.java @@ -0,0 +1,56 @@ +/* + * 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.selection.twosteperror; + +import java.math.BigDecimal; + +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface ErroneousMapperCM { + + ErroneousMapperCM INSTANCE = Mappers.getMapper( ErroneousMapperCM.class ); + + Target map(Source s); + + default TargetType methodY1(String s) { + return new TargetType( s ); + } + + default TargetType methodY2(Double d) { + return new TargetType( d.toString() ); + } + + // CHECKSTYLE:OFF + class Target { + public TargetType t1; + } + + class Source { + public BigDecimal t1; + } + + class TargetType { + public String t1; + + public TargetType(String test) { + this.t1 = test; + } + } + + class TypeInTheMiddleA { + + TypeInTheMiddleA(String t1) { + this.test = t1; + } + + public String test; + } + + // CHECKSTYLE:ON + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/selection/twosteperror/ErroneousMapperMC.java b/processor/src/test/java/org/mapstruct/ap/test/selection/twosteperror/ErroneousMapperMC.java new file mode 100644 index 000000000..4b7dcf90a --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/selection/twosteperror/ErroneousMapperMC.java @@ -0,0 +1,52 @@ +/* + * 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.selection.twosteperror; + +import java.math.BigDecimal; + +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface ErroneousMapperMC { + + ErroneousMapperMC INSTANCE = Mappers.getMapper( ErroneousMapperMC.class ); + + Target map(Source s); + + default BigDecimal methodX1(SourceType s) { + return new BigDecimal( s.t1 ); + } + + default Double methodX2(SourceType s) { + return new Double( s.t1 ); + } + + // CHECKSTYLE:OFF + class Target { + public String t1; + } + + class Source { + public SourceType t1; + } + + class SourceType { + public String t1; + } + + class TargetType { + public String t1; + + public TargetType(String test) { + this.t1 = test; + } + } + + + // CHECKSTYLE:ON + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/selection/twosteperror/ErroneousMapperMM.java b/processor/src/test/java/org/mapstruct/ap/test/selection/twosteperror/ErroneousMapperMM.java new file mode 100644 index 000000000..2f0e25c64 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/selection/twosteperror/ErroneousMapperMM.java @@ -0,0 +1,74 @@ +/* + * 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.selection.twosteperror; + +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface ErroneousMapperMM { + + ErroneousMapperMM INSTANCE = Mappers.getMapper( ErroneousMapperMM.class ); + + Target map( Source s ); + + default TargetType methodY1(TypeInTheMiddleA s) { + return new TargetType( s.test ); + } + + default TypeInTheMiddleA methodX1(SourceType s) { + return new TypeInTheMiddleA( s.t1 ); + } + + default TargetType methodY2(TypeInTheMiddleB s) { + return new TargetType( s.test ); + } + + default TypeInTheMiddleB methodX2(SourceType s) { + return new TypeInTheMiddleB( s.t1 ); + } + + // CHECKSTYLE:OFF + class Target { + public TargetType t1; + } + + class Source { + public SourceType t1; + } + + class SourceType { + public String t1; + } + + class TargetType { + public String t1; + + public TargetType(String test) { + this.t1 = test; + } + } + + class TypeInTheMiddleA { + + TypeInTheMiddleA(String t1) { + this.test = t1; + } + + public String test; + } + + class TypeInTheMiddleB { + + TypeInTheMiddleB(String t1) { + this.test = t1; + } + + public String test; + } + // CHECKSTYLE:ON + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/selection/twosteperror/TwoStepMappingTest.java b/processor/src/test/java/org/mapstruct/ap/test/selection/twosteperror/TwoStepMappingTest.java new file mode 100644 index 000000000..e22150fe3 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/selection/twosteperror/TwoStepMappingTest.java @@ -0,0 +1,79 @@ +/* + * 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.selection.twosteperror; + +import org.junit.Test; +import org.junit.runner.RunWith; +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.AnnotationProcessorTestRunner; + +@RunWith( AnnotationProcessorTestRunner.class ) +public class TwoStepMappingTest { + + @Test + @WithClasses( ErroneousMapperMM.class ) + @ExpectedCompilationOutcome( value = CompilationResult.FAILED, + diagnostics = @Diagnostic( + kind = javax.tools.Diagnostic.Kind.ERROR, + type = ErroneousMapperMM.class, + line = 16, + message = "Ambiguous 2step methods found, mapping SourceType s to TargetType. " + + "Found methodY( methodX ( parameter ) ): " + + "method(s)Y: TargetType:methodY1(TypeInTheMiddleA), methodX: TypeInTheMiddleA:methodX1(SourceType); " + + "method(s)Y: TargetType:methodY2(TypeInTheMiddleB), methodX: TypeInTheMiddleB:methodX2(SourceType); ." + ) ) + public void methodAndMethodTest() { + } + + @Test + @WithClasses( ErroneousMapperCM.class ) + @ExpectedCompilationOutcome( value = CompilationResult.FAILED, + diagnostics = { + @Diagnostic( + kind = javax.tools.Diagnostic.Kind.ERROR, + type = ErroneousMapperCM.class, + line = 18, + message = "Ambiguous 2step methods found, mapping BigDecimal s to TargetType. " + + "Found methodY( conversionX ( parameter ) ): " + + "method(s)Y: TargetType:methodY1(String), conversionX: BigDecimal-->String; " + + "method(s)Y: TargetType:methodY2(Double), conversionX: BigDecimal-->Double; ." + ), + @Diagnostic( + kind = javax.tools.Diagnostic.Kind.ERROR, + type = ErroneousMapperCM.class, + line = 18, + messageRegExp = "Can't map property.*" + ) + } ) + public void conversionAndMethodTest() { + } + + @Test + @WithClasses( ErroneousMapperMC.class ) + @ExpectedCompilationOutcome( value = CompilationResult.FAILED, + diagnostics = { + @Diagnostic( + kind = javax.tools.Diagnostic.Kind.ERROR, + type = ErroneousMapperMC.class, + line = 18, + message = "Ambiguous 2step methods found, mapping SourceType s to String. " + + "Found conversionY( methodX ( parameter ) ): " + + "conversionY: BigDecimal-->String, method(s)X: BigDecimal:methodX1(SourceType); " + + "conversionY: Double-->String, method(s)X: Double:methodX2(SourceType); ." + ), + @Diagnostic( + kind = javax.tools.Diagnostic.Kind.ERROR, + type = ErroneousMapperMC.class, + line = 18, + messageRegExp = "Can't map property.*" + ) + } ) + public void methodAndConversionTest() { + } +}