/**
* Copyright 2012-2017 Gunnar Morling (http://www.gunnarmorling.de/)
* and/or other contributors as indicated by the @authors tag. See the
* copyright.txt file in the distribution for a full listing of all
* contributors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.mapstruct.ap.internal.processor.creation;
import static java.util.Collections.singletonList;
import static org.mapstruct.ap.internal.util.Collections.first;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.ExecutableType;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.ElementFilter;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;
import org.mapstruct.ap.internal.conversion.ConversionProvider;
import org.mapstruct.ap.internal.conversion.Conversions;
import org.mapstruct.ap.internal.model.HelperMethod;
import org.mapstruct.ap.internal.model.MapperReference;
import org.mapstruct.ap.internal.model.MappingBuilderContext.MappingResolver;
import org.mapstruct.ap.internal.model.MethodReference;
import org.mapstruct.ap.internal.model.SourceRHS;
import org.mapstruct.ap.internal.model.VirtualMappingMethod;
import org.mapstruct.ap.internal.model.assignment.Assignment;
import org.mapstruct.ap.internal.model.common.ConversionContext;
import org.mapstruct.ap.internal.model.common.DefaultConversionContext;
import org.mapstruct.ap.internal.model.common.Type;
import org.mapstruct.ap.internal.model.common.TypeFactory;
import org.mapstruct.ap.internal.model.common.FormattingParameters;
import org.mapstruct.ap.internal.model.source.Method;
import org.mapstruct.ap.internal.model.source.SelectionParameters;
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;
import org.mapstruct.ap.internal.model.source.selector.SelectedMethod;
import org.mapstruct.ap.internal.model.source.selector.SelectionCriteria;
import org.mapstruct.ap.internal.util.Collections;
import org.mapstruct.ap.internal.util.FormattingMessager;
import org.mapstruct.ap.internal.util.Message;
import org.mapstruct.ap.internal.util.Strings;
/**
* 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
* which should not be exposed to the {@code model} package.
*
* @author Sjaak Derksen
*/
public class MappingResolverImpl implements MappingResolver {
private final FormattingMessager messager;
private final Types typeUtils;
private final TypeFactory typeFactory;
private final List<Method> sourceModel;
private final List<MapperReference> mapperReferences;
private final Conversions conversions;
private final BuiltInMappingMethods builtInMethods;
private final MethodSelectors methodSelectors;
/**
* Private methods which are not present in the original mapper interface and are added to map certain property
* types.
*/
private final Set<VirtualMappingMethod> usedVirtualMappings = new HashSet<VirtualMappingMethod>();
public MappingResolverImpl(FormattingMessager messager, Elements elementUtils, Types typeUtils,
TypeFactory typeFactory, List<Method> sourceModel,
List<MapperReference> mapperReferences) {
this.messager = messager;
this.typeUtils = typeUtils;
this.typeFactory = typeFactory;
this.sourceModel = sourceModel;
this.mapperReferences = mapperReferences;
this.conversions = new Conversions( elementUtils, typeFactory );
this.builtInMethods = new BuiltInMappingMethods( typeFactory );
this.methodSelectors = new MethodSelectors( typeUtils, elementUtils, typeFactory );
}
@Override
public Assignment getTargetAssignment(Method mappingMethod, Type targetType, String targetPropertyName,
FormattingParameters formattingParameters, SelectionParameters selectionParameters, SourceRHS sourceRHS,
boolean preferUpdateMapping) {
SelectionCriteria criteria =
SelectionCriteria.forMappingMethods( selectionParameters, targetPropertyName, preferUpdateMapping );
ResolvingAttempt attempt = new ResolvingAttempt(
sourceModel,
mappingMethod,
formattingParameters,
sourceRHS,
criteria
);
return attempt.getTargetAssignment( sourceRHS.getSourceTypeForMatching(), targetType );
}
@Override
public Set<VirtualMappingMethod> getUsedVirtualMappings() {
return usedVirtualMappings;
}
@Override
public MethodReference getFactoryMethod(final Method mappingMethod, Type targetType,
SelectionParameters selectionParameters) {
List<SelectedMethod<Method>> matchingFactoryMethods =
methodSelectors.getMatchingMethods(
mappingMethod,
sourceModel,
java.util.Collections.<Type> emptyList(),
targetType,
SelectionCriteria.forFactoryMethods( selectionParameters ) );
if (matchingFactoryMethods.isEmpty()) {
return null;
}
if ( matchingFactoryMethods.size() > 1 ) {
messager.printMessage(
mappingMethod.getExecutable(),
Message.GENERAL_AMBIGIOUS_FACTORY_METHOD,
targetType,
Strings.join( matchingFactoryMethods, ", " ) );
return null;
}
SelectedMethod<Method> matchingFactoryMethod = first( matchingFactoryMethods );
MapperReference ref = findMapperReference( matchingFactoryMethod.getMethod() );
return MethodReference.forMapperReference(
matchingFactoryMethod.getMethod(),
ref,
matchingFactoryMethod.getParameterBindings() );
}
private MapperReference findMapperReference(Method method) {
for ( MapperReference ref : mapperReferences ) {
if ( ref.getType().equals( method.getDeclaringMapper() ) ) {
ref.setUsed( ref.isUsed() || !method.isStatic() );
ref.setTypeRequiresImport( true );
return ref;
}
}
return null;
}
private class ResolvingAttempt {
private final Method mappingMethod;
private final List<Method> methods;
private final SelectionCriteria selectionCriteria;
private final SourceRHS sourceRHS;
private final boolean savedPreferUpdateMapping;
private final FormattingParameters formattingParameters;
// resolving via 2 steps creates the possibillity of wrong matches, first builtin method matches,
// second doesn't. In that case, the first builtin method should not lead to a virtual method
// so this set must be cleared.
private final Set<VirtualMappingMethod> virtualMethodCandidates;
private ResolvingAttempt(List<Method> sourceModel, Method mappingMethod,
FormattingParameters formattingParameters, SourceRHS sourceRHS, SelectionCriteria criteria) {
this.mappingMethod = mappingMethod;
this.methods = filterPossibleCandidateMethods( sourceModel );
this.formattingParameters =
formattingParameters == null ? FormattingParameters.EMPTY : formattingParameters;
this.sourceRHS = sourceRHS;
this.virtualMethodCandidates = new HashSet<VirtualMappingMethod>();
this.selectionCriteria = criteria;
this.savedPreferUpdateMapping = criteria.isPreferUpdateMapping();
}
private <T extends Method> List<T> filterPossibleCandidateMethods(List<T> candidateMethods) {
List<T> result = new ArrayList<T>( candidateMethods.size() );
for ( T candidate : candidateMethods ) {
if ( isCandidateForMapping( candidate ) ) {
result.add( candidate );
}
}
return result;
}
private Assignment getTargetAssignment(Type sourceType, Type targetType) {
// first simple mapping method
Assignment referencedMethod = resolveViaMethod( sourceType, targetType, false );
if ( referencedMethod != null ) {
referencedMethod.setAssignment( sourceRHS );
return referencedMethod;
}
// then direct assignable
if ( sourceType.isAssignableTo( targetType ) ||
isAssignableThroughCollectionCopyConstructor( sourceType, targetType ) ) {
Assignment simpleAssignment = sourceRHS;
return simpleAssignment;
}
// then type conversion
Assignment conversion = resolveViaConversion( sourceType, targetType );
if ( conversion != null ) {
conversion.setAssignment( sourceRHS );
return conversion;
}
// check for a built-in method
Assignment builtInMethod = resolveViaBuiltInMethod( sourceType, targetType );
if ( builtInMethod != null ) {
builtInMethod.setAssignment( sourceRHS );
usedVirtualMappings.addAll( virtualMethodCandidates );
return builtInMethod;
}
// 2 step method, first: method(method(source))
referencedMethod = resolveViaMethodAndMethod( sourceType, targetType );
if ( referencedMethod != null ) {
usedVirtualMappings.addAll( virtualMethodCandidates );
return referencedMethod;
}
// 2 step method, then: method(conversion(source))
referencedMethod = resolveViaConversionAndMethod( sourceType, targetType );
if ( referencedMethod != null ) {
usedVirtualMappings.addAll( virtualMethodCandidates );
return referencedMethod;
}
// stop here when looking for update methods.
selectionCriteria.setPreferUpdateMapping( false );
// 2 step method, finally: conversion(method(source))
conversion = resolveViaMethodAndConversion( sourceType, targetType );
if ( conversion != null ) {
usedVirtualMappings.addAll( virtualMethodCandidates );
return conversion;
}
// if nothing works, alas, the result is null
return null;
}
private Assignment resolveViaConversion(Type sourceType, Type targetType) {
ConversionProvider conversionProvider = conversions.getConversion( sourceType, targetType );
if ( conversionProvider == null ) {
return null;
}
ConversionContext ctx = new DefaultConversionContext(
typeFactory,
messager,
sourceType,
targetType,
formattingParameters
);
// add helper methods required in conversion
for ( HelperMethod helperMethod : conversionProvider.getRequiredHelperMethods( ctx ) ) {
usedVirtualMappings.add( new VirtualMappingMethod( helperMethod ) );
}
return conversionProvider.to( ctx );
}
/**
* 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, targetType );
}
if ( considerBuiltInMethods ) {
return resolveViaBuiltInMethod( sourceType, targetType );
}
return null;
}
private Assignment resolveViaBuiltInMethod(Type sourceType, Type targetType) {
SelectedMethod<BuiltInMethod> matchingBuiltInMethod =
getBestMatch( builtInMethods.getBuiltInMethods(), sourceType, targetType );
if ( matchingBuiltInMethod != null ) {
virtualMethodCandidates.add( new VirtualMappingMethod( matchingBuiltInMethod.getMethod() ) );
ConversionContext ctx = new DefaultConversionContext(
typeFactory,
messager,
sourceType,
targetType,
formattingParameters
);
Assignment methodReference = MethodReference.forBuiltInMethod( matchingBuiltInMethod.getMethod(), ctx );
methodReference.setAssignment( sourceRHS );
return methodReference;
}
return null;
}
/**
* 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<Method>( 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 ) {
methodRefY =
resolveViaMethod( methodYCandidate.getSourceParameters().get( 0 ).getType(), targetType, true );
if ( methodRefY != null ) {
selectionCriteria.setPreferUpdateMapping( false );
Assignment methodRefX =
resolveViaMethod( sourceType, methodYCandidate.getSourceParameters().get( 0 ).getType(), true );
selectionCriteria.setPreferUpdateMapping( savedPreferUpdateMapping );
if ( methodRefX != null ) {
methodRefY.setAssignment( methodRefX );
methodRefX.setAssignment( sourceRHS );
break;
}
else {
// both should match;
virtualMethodCandidates.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 ) )
*/
private Assignment resolveViaConversionAndMethod(Type sourceType, Type targetType) {
List<Method> methodYCandidates = new ArrayList<Method>( methods );
methodYCandidates.addAll( builtInMethods.getBuiltInMethods() );
Assignment methodRefY = null;
for ( Method methodYCandidate : methodYCandidates ) {
methodRefY =
resolveViaMethod( methodYCandidate.getSourceParameters().get( 0 ).getType(), targetType, true );
if ( methodRefY != null ) {
Assignment conversionXRef =
resolveViaConversion( sourceType, methodYCandidate.getSourceParameters().get( 0 ).getType() );
if ( conversionXRef != null ) {
methodRefY.setAssignment( conversionXRef );
conversionXRef.setAssignment( sourceRHS );
break;
}
else {
// both should match
virtualMethodCandidates.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 ) )
*/
private Assignment resolveViaMethodAndConversion(Type sourceType, Type targetType) {
List<Method> methodXCandidates = new ArrayList<Method>( methods );
methodXCandidates.addAll( builtInMethods.getBuiltInMethods() );
Assignment conversionYRef = null;
// search the other way around
for ( Method methodXCandidate : methodXCandidates ) {
if ( methodXCandidate.getMappingTargetParameter() != null ) {
continue;
}
Assignment methodRefX = resolveViaMethod(
sourceType,
methodXCandidate.getReturnType(),
true
);
if ( methodRefX != null ) {
conversionYRef = resolveViaConversion( methodXCandidate.getReturnType(), targetType );
if ( conversionYRef != null ) {
conversionYRef.setAssignment( methodRefX );
methodRefX.setAssignment( sourceRHS );
break;
}
else {
// both should match;
virtualMethodCandidates.clear();
conversionYRef = null;
}
}
}
return conversionYRef;
}
private boolean isCandidateForMapping(Method methodCandidate) {
return isCreateMethodForMapping( methodCandidate ) || isUpdateMethodForMapping( methodCandidate );
}
private boolean isCreateMethodForMapping(Method methodCandidate) {
// a create method may not return void and has no target parameter
return methodCandidate.getSourceParameters().size() == 1
&& !methodCandidate.getReturnType().isVoid()
&& methodCandidate.getMappingTargetParameter() == null
&& !methodCandidate.isLifecycleCallbackMethod();
}
private boolean isUpdateMethodForMapping(Method methodCandidate) {
// an update method may, or may not return void and has a target parameter
return methodCandidate.getSourceParameters().size() == 1
&& methodCandidate.getMappingTargetParameter() != null
&& !methodCandidate.isLifecycleCallbackMethod();
}
private <T extends Method> SelectedMethod<T> getBestMatch(List<T> methods, Type sourceType, Type returnType) {
List<SelectedMethod<T>> candidates = methodSelectors.getMatchingMethods(
mappingMethod,
methods,
singletonList( sourceType ),
returnType,
selectionCriteria
);
// raise an error if more than one mapping method is suitable to map the given source type
// into the target type
if ( candidates.size() > 1 ) {
if ( sourceRHS.getSourceErrorMessagePart() != null ) {
messager.printMessage( mappingMethod.getExecutable(),
Message.GENERAL_AMBIGIOUS_MAPPING_METHOD,
sourceRHS.getSourceErrorMessagePart(),
returnType,
Strings.join( candidates, ", " )
);
}
else {
messager.printMessage( mappingMethod.getExecutable(),
Message.GENERAL_AMBIGIOUS_FACTORY_METHOD,
returnType,
Strings.join( candidates, ", " )
);
}
}
if ( !candidates.isEmpty() ) {
return first( candidates );
}
return null;
}
private Assignment getMappingMethodReference(SelectedMethod<Method> method,
Type targetType) {
MapperReference mapperReference = findMapperReference( method.getMethod() );
return MethodReference.forMapperReference(
method.getMethod(),
mapperReference,
method.getParameterBindings() );
}
/**
* 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.
*/
private boolean isAssignableThroughCollectionCopyConstructor(Type sourceType, Type targetType) {
boolean bothCollectionOrMap = false;
if ( ( sourceType.isCollectionType() && targetType.isCollectionType() ) ||
( sourceType.isMapType() && targetType.isMapType() ) ) {
bothCollectionOrMap = true;
}
if ( bothCollectionOrMap ) {
return hasCompatibleCopyConstructor(
sourceType,
targetType.getImplementationType() != null ? targetType.getImplementationType() : targetType
);
}
return false;
}
/**
* Whether the given target type has a single-argument constructor which accepts the given source type.
*
* @param sourceType the source type
* @param targetType the target type
* @return {@code true} if the target type has a constructor accepting the given source type, {@code false}
* otherwise.
*/
private boolean hasCompatibleCopyConstructor(Type sourceType, Type targetType) {
if ( targetType.isPrimitive() ) {
return false;
}
List<ExecutableElement> targetTypeConstructors = ElementFilter.constructorsIn(
targetType.getTypeElement().getEnclosedElements() );
for ( ExecutableElement constructor : targetTypeConstructors ) {
if ( constructor.getParameters().size() != 1 ) {
continue;
}
// get the constructor resolved against the type arguments of specific target type
ExecutableType typedConstructor = (ExecutableType) typeUtils.asMemberOf(
(DeclaredType) targetType.getTypeMirror(),
constructor );
TypeMirror parameterType = Collections.first( typedConstructor.getParameterTypes() );
if ( parameterType.getKind() == TypeKind.DECLARED ) {
// replace any possible type bounds in the type parameters of the parameter types, as in JDK super
// type bounds in the arguments are returned from asMemberOf with "? extends ? super XX"
//
// It might also be enough to just remove "? super" from type parameters of
// targetType.getTypeMirror() in case we're in JDK. And that would be something that should be
// handled in SpecificCompilerWorkarounds...
DeclaredType p = (DeclaredType) parameterType;
List<TypeMirror> typeArguments = new ArrayList<TypeMirror>( p.getTypeArguments().size() );
for ( TypeMirror tArg : p.getTypeArguments() ) {
typeArguments.add( typeFactory.getTypeBound( tArg ) );
}
parameterType = typeUtils.getDeclaredType(
(TypeElement) p.asElement(),
typeArguments.toArray( new TypeMirror[typeArguments.size()] ) );
}
if ( typeUtils.isAssignable( sourceType.getTypeMirror(), parameterType ) ) {
return true;
}
}
return false;
}
}
}