#2145 fixing 2 step mapping methods (refactoring) (#2146)

This commit is contained in:
Sjaak Derksen 2020-07-16 13:49:49 +02:00 committed by Filip Hrisafov
parent 74f281fa3e
commit 2a849dca12
16 changed files with 946 additions and 294 deletions

View File

@ -482,6 +482,24 @@ public class Type extends ModelElement implements Comparable<Type> {
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
*

View File

@ -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() + ")";
}
}

View File

@ -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;

View File

@ -67,23 +67,15 @@ public abstract class BuiltInMethod implements Method {
Type sourceType = first( sourceTypes );
if ( getReturnType().isAssignableTo( targetType.erasure() )
&& sourceType.erasure().isAssignableTo( getParameter().getType() ) ) {
return doTypeVarsMatch( sourceType, targetType );
}
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 );
}
Type returnType = getReturnType().resolveTypeVarToType( sourceType, getParameter().getType() );
if ( returnType == null ) {
return false;
}
return returnType.isAssignableTo( targetType )
&& sourceType.erasure().isAssignableTo( getParameter().getType() );
}
@Override
public List<Parameter> getSourceParameters() {
return getParameters();

View File

@ -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<Type> 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() );
}

View File

@ -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<T extends Method> {
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 );
}
}

View File

@ -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 =

View File

@ -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<Method> methods;
private final SelectionCriteria selectionCriteria;
private final SourceRHS sourceRHS;
private final boolean savedPreferUpdateMapping;
private final FormattingParameters formattingParameters;
private final AnnotationMirror positionHint;
private final Supplier<Assignment> forger;
private final List<BuiltInMethod> 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<Assignment> forger) {
Supplier<Assignment> forger,
List<BuiltInMethod> 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 <T extends Method> List<T> filterPossibleCandidateMethods(List<T> 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<SelectedMethod<Method>> 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<SelectedMethod<BuiltInMethod>> 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<Method> 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<Method> 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<BuiltInMethod> matchingBuiltInMethod =
getBestMatch( builtInMethods.getBuiltInMethods(), sourceType, targetType );
if ( matchingBuiltInMethod != null ) {
return createFromBuiltInMethod( matchingBuiltInMethod.getMethod() );
}
return null;
}
private Assignment createFromBuiltInMethod(BuiltInMethod method) {
Set<Field> 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:
* <ul>
* <li>no direct referenced mapping method either built-in or referenced is available from A to C</li>
* <li>no conversion is available</li>
* <li>there is a method from A to B, methodX</li>
* <li>there is a method from B to C, methodY</li>
* </ul>
* then this method tries to resolve this combination and make a mapping methodY( methodX ( parameter ) )
*/
private Assignment resolveViaMethodAndMethod(Type sourceType, Type targetType) {
List<Method> 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:
* <ul>
* <li>there is a conversion from A to B, conversionX</li>
* <li>there is a method from B to C, methodY</li>
* </ul>
* 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<Method> 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:
* <ul>
* <li>there is a method from A to B, methodX</li>
* <li>there is a conversion from B to C, conversionY</li>
* </ul>
* 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<Method> 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 <T extends Method> SelectedMethod<T> getBestMatch(List<T> methods, Type sourceType, Type returnType) {
List<SelectedMethod<T>> candidates = methodSelectors.getMatchingMethods(
private <T extends Method> List<SelectedMethod<T>> getBestMatch(List<T> methods, Type source, Type target) {
return methodSelectors.getMatchingMethods(
mappingMethod,
methods,
singletonList( sourceType ),
returnType,
singletonList( source ),
target,
selectionCriteria
);
}
private <T extends Method> void reportErrorWhenAmbigious(List<SelectedMethod<T>> 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> method) {
MapperReference mapperReference = findMapperReference( method.getMethod() );
private Assignment toMethodRef(SelectedMethod<Method> selectedMethod) {
MapperReference mapperReference = findMapperReference( selectedMethod.getMethod() );
return MethodReference.forMapperReference(
method.getMethod(),
selectedMethod.getMethod(),
mapperReference,
method.getParameterBindings()
selectedMethod.getParameterBindings()
);
}
private Assignment toBuildInRef(SelectedMethod<BuiltInMethod> selectedMethod) {
BuiltInMethod method = selectedMethod.getMethod();
Set<Field> 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:
* <ul>
* <li>no direct referenced mapping method either built-in or referenced is available from A to C</li>
* <li>no conversion is available</li>
* <li>there is a method from A to B, methodX</li>
* <li>there is a method from B to C, methodY</li>
* </ul>
* 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<T1 extends Method, T2 extends Method> {
private final ResolvingAttempt attempt;
private final List<T1> xMethods;
private final List<T2> yMethods;
private final Function<SelectedMethod<T1>, Assignment> xCreate;
private final Function<SelectedMethod<T2>, Assignment> yCreate;
// results
private boolean hasResult = false;
private Assignment result = null;
static Assignment getBestMatch(ResolvingAttempt att, Type sourceType, Type targetType) {
MethodMethod<Method, Method> mmAttempt =
new MethodMethod<>( att, att.methods, att.methods, att::toMethodRef, att::toMethodRef )
.getBestMatch( sourceType, targetType );
if ( mmAttempt.hasResult ) {
return mmAttempt.result;
}
MethodMethod<Method, BuiltInMethod> mbAttempt =
new MethodMethod<>( att, att.methods, att.builtIns, att::toMethodRef, att::toBuildInRef )
.getBestMatch( sourceType, targetType );
if ( mbAttempt.hasResult ) {
return mbAttempt.result;
}
MethodMethod<BuiltInMethod, Method> bmAttempt =
new MethodMethod<>( att, att.builtIns, att.methods, att::toBuildInRef, att::toMethodRef )
.getBestMatch( sourceType, targetType );
if ( bmAttempt.hasResult ) {
return bmAttempt.result;
}
MethodMethod<BuiltInMethod, BuiltInMethod> bbAttempt =
new MethodMethod<>( att, att.builtIns, att.builtIns, att::toBuildInRef, att::toBuildInRef )
.getBestMatch( sourceType, targetType );
return bbAttempt.result;
}
MethodMethod(ResolvingAttempt attempt, List<T1> xMethods, List<T2> yMethods,
Function<SelectedMethod<T1>, Assignment> xCreate,
Function<SelectedMethod<T2>, Assignment> yCreate) {
this.attempt = attempt;
this.xMethods = xMethods;
this.yMethods = yMethods;
this.xCreate = xCreate;
this.yCreate = yCreate;
}
private MethodMethod<T1, T2> getBestMatch(Type sourceType, Type targetType) {
Set<T2> yCandidates = new HashSet<>();
Map<SelectedMethod<T1>, List<SelectedMethod<T2>>> xCandidates = new LinkedHashMap<>();
Map<SelectedMethod<T1>, 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<SelectedMethod<T1>> 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<T2> yCandidatesList = new ArrayList<>( yCandidates );
Iterator<Map.Entry<SelectedMethod<T1>, List<SelectedMethod<T2>>>> i = xCandidates.entrySet().iterator();
while ( i.hasNext() ) {
Map.Entry<SelectedMethod<T1>, List<SelectedMethod<T2>>> 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<SelectedMethod<T1>, List<SelectedMethod<T2>>> 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:
* <ul>
* <li>there is a conversion from A to B, conversionX</li>
* <li>there is a method from B to C, methodY</li>
* </ul>
* 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<T extends Method> {
private final ResolvingAttempt attempt;
private final List<T> methods;
private final Function<SelectedMethod<T>, Assignment> create;
// results
private boolean hasResult = false;
private Assignment result = null;
static Assignment getBestMatch(ResolvingAttempt att, Type sourceType, Type targetType) {
ConversionMethod<Method> mAttempt = new ConversionMethod<>( att, att.methods, att::toMethodRef )
.getBestMatch( sourceType, targetType );
if ( mAttempt.hasResult ) {
return mAttempt.result;
}
ConversionMethod<BuiltInMethod> bAttempt =
new ConversionMethod<>( att, att.builtIns, att::toBuildInRef )
.getBestMatch( sourceType, targetType );
return bAttempt.result;
}
ConversionMethod(ResolvingAttempt attempt, List<T> methods, Function<SelectedMethod<T>, Assignment> create) {
this.attempt = attempt;
this.methods = methods;
this.create = create;
}
private ConversionMethod<T> getBestMatch(Type sourceType, Type targetType) {
List<T> yCandidates = new ArrayList<>();
Map<ConversionAssignment, List<SelectedMethod<T>>> 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<Map.Entry<ConversionAssignment, List<SelectedMethod<T>>>> i = xRefCandidates.entrySet().iterator();
while ( i.hasNext() ) {
Map.Entry<ConversionAssignment, List<SelectedMethod<T>>> 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<ConversionAssignment, List<SelectedMethod<T>>> 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:
* <ul>
* <li>there is a method from A to B, methodX</li>
* <li>there is a conversion from B to C, conversionY</li>
* </ul>
* 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<T extends Method> {
private final ResolvingAttempt attempt;
private final List<T> methods;
private final Function<SelectedMethod<T>, Assignment> create;
// results
private boolean hasResult = false;
private Assignment result = null;
static Assignment getBestMatch(ResolvingAttempt att, Type sourceType, Type targetType) {
MethodConversion<Method> mAttempt = new MethodConversion<>( att, att.methods, att::toMethodRef )
.getBestMatch( sourceType, targetType );
if ( mAttempt.hasResult ) {
return mAttempt.result;
}
MethodConversion<BuiltInMethod> bAttempt = new MethodConversion<>( att, att.builtIns, att::toBuildInRef )
.getBestMatch( sourceType, targetType );
return bAttempt.result;
}
MethodConversion(ResolvingAttempt attempt, List<T> methods, Function<SelectedMethod<T>, Assignment> create) {
this.attempt = attempt;
this.methods = methods;
this.create = create;
}
private MethodConversion<T> getBestMatch(Type sourceType, Type targetType) {
List<T> xCandidates = new ArrayList<>();
Map<ConversionAssignment, List<SelectedMethod<T>>> 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<Map.Entry<ConversionAssignment, List<SelectedMethod<T>>>> i = yRefCandidates.entrySet().iterator();
while ( i.hasNext() ) {
Map.Entry<ConversionAssignment, List<SelectedMethod<T>>> 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<ConversionAssignment, List<SelectedMethod<T>>> 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() );
}
}
}

View File

@ -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 <K, V> Map.Entry<K, V> first(Map<K, V> map) {
return map.entrySet().iterator().next();
}
public static <K, V> V firstValue(Map<K, V> map) {
return first( map ).getValue();
}
public static <K, V> K firstKey(Map<K, V> map) {
return first( map ).getKey();
}
}

View File

@ -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 ),

View File

@ -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> nested;
public JAXBElement<Nested> getNested() {
return nested;
}
public void setNested(JAXBElement<Nested> 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<Nested> createNested(Nested value) {
return new JAXBElement<Nested>( Q_NAME, Nested.class, null, value );
}
@XmlElementDecl(namespace = "http://www.test.com/test", name = "nested", scope = Nested.class)
public JAXBElement<Nested> createNestedInNestedTarget(Nested value) {
return new JAXBElement<Nested>( Q_NAME_NESTED, Nested.class, Target.class, value );
}
}
}

View File

@ -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" );
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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() {
}
}