Package org.mapstruct.ap.processor.creation

Source Code of org.mapstruct.ap.processor.creation.MappingResolverImpl$ResolvingAttempt

/**
*  Copyright 2012-2014 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.processor.creation;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import javax.annotation.processing.Messager;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;
import javax.tools.Diagnostic;

import org.mapstruct.ap.conversion.ConversionProvider;
import org.mapstruct.ap.conversion.Conversions;
import org.mapstruct.ap.model.AssignmentFactory;
import org.mapstruct.ap.model.MapperReference;
import org.mapstruct.ap.model.MappingBuilderContext.MappingResolver;
import org.mapstruct.ap.model.VirtualMappingMethod;
import org.mapstruct.ap.model.assignment.Assignment;
import org.mapstruct.ap.model.common.ConversionContext;
import org.mapstruct.ap.model.common.DefaultConversionContext;
import org.mapstruct.ap.model.common.Type;
import org.mapstruct.ap.model.common.TypeFactory;
import org.mapstruct.ap.model.source.Method;
import org.mapstruct.ap.model.source.SourceMethod;
import org.mapstruct.ap.model.source.builtin.BuiltInMappingMethods;
import org.mapstruct.ap.model.source.builtin.BuiltInMethod;
import org.mapstruct.ap.model.source.selector.MethodSelectors;
import org.mapstruct.ap.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 Messager messager;
    private final Types typeUtils;
    private final TypeFactory typeFactory;

    private final List<SourceMethod> 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(Messager messager, Elements elementUtils, Types typeUtils, TypeFactory typeFactory,
                               List<SourceMethod> 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
        );
    }

    /**
     * returns a parameter assignment
     *
     * @param mappingMethod target mapping method
     * @param mappedElement used for error messages
     * @param sourceType parameter to match
     * @param targetType return type to match
     * @param targetPropertyName name of the target property
     * @param dateFormat used for formatting dates in build in methods that need context information
     * @param qualifiers used for further select the appropriate mapping method based on class and name
     * @param sourceReference call to source type as string
     *
     * @return an assignment to a method parameter, which can either be:
     * <ol>
     * <li>MethodReference</li>
     * <li>TypeConversion</li>
     * <li>Direct Assignment (empty TargetAssignment)</li>
     * <li>null, no assignment found</li>
     * </ol>
     */
    @Override
    public Assignment getTargetAssignment(
        Method mappingMethod,
        String mappedElement,
        Type sourceType,
        Type targetType,
        String targetPropertyName,
        String dateFormat,
        List<TypeMirror> qualifiers,
        String sourceReference) {

        ResolvingAttempt attempt = new ResolvingAttempt(
            sourceModel,
            mappingMethod,
            mappedElement,
            targetPropertyName,
            dateFormat,
            qualifiers,
            sourceReference
        );

        return attempt.getTargetAssignment( sourceType, targetType );
    }

    @Override
    public Set<VirtualMappingMethod> getUsedVirtualMappings() {
        return usedVirtualMappings;
    }

    private class ResolvingAttempt {

        private final Method mappingMethod;
        private final String mappedElement;
        private final List<SourceMethod> methods;
        private final String targetPropertyName;
        private final String dateFormat;
        private final List<TypeMirror> qualifiers;
        private final String sourceReference;

        // 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<SourceMethod> sourceModel,
                                 Method mappingMethod,
                                 String mappedElement,
                                 String targetPropertyName,
                                 String dateFormat,
                                 List<TypeMirror> qualifiers,
                                 String sourceReference) {
            this.mappingMethod = mappingMethod;
            this.mappedElement = mappedElement;
            this.methods = sourceModel;
            this.targetPropertyName = targetPropertyName;
            this.dateFormat = dateFormat;
            this.qualifiers = qualifiers;
            this.sourceReference = sourceReference;
            this.virtualMethodCandidates = new HashSet<VirtualMappingMethod>();
        }

        private Assignment getTargetAssignment(Type sourceType, Type targetType) {

            // first simple mapping method
            Assignment referencedMethod = resolveViaMethod( sourceType, targetType, false );
            if ( referencedMethod != null ) {
                referencedMethod.setAssignment( AssignmentFactory.createDirect( sourceReference ) );
                return referencedMethod;
            }

            // then direct assignable
            if ( sourceType.isAssignableTo( targetType ) || isPropertyMappable( sourceType, targetType ) ) {
                Assignment simpleAssignment = AssignmentFactory.createDirect( sourceReference );
                return simpleAssignment;
            }

            // then type conversion
            Assignment conversion = resolveViaConversion( sourceType, targetType );
            if ( conversion != null ) {
                conversion.setAssignment( AssignmentFactory.createDirect( sourceReference ) );
                return conversion;
            }

            // check for a built-in method
            Assignment builtInMethod = resolveViaBuiltInMethod( sourceType, targetType );
            if ( builtInMethod != null ) {
                builtInMethod.setAssignment( AssignmentFactory.createDirect( sourceReference ) );
                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;
            }

            // 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, targetType, dateFormat );
            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
            SourceMethod 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) {
            BuiltInMethod matchingBuiltInMethod =
                getBestMatch( builtInMethods.getBuiltInMethods(), sourceType, targetType );

            if ( matchingBuiltInMethod != null ) {
                virtualMethodCandidates.add( new VirtualMappingMethod( matchingBuiltInMethod ) );
                ConversionContext ctx = new DefaultConversionContext( typeFactory, targetType, dateFormat );
                Assignment methodReference = AssignmentFactory.createMethodReference( matchingBuiltInMethod, ctx );
                methodReference.setAssignment( AssignmentFactory.createDirect( sourceReference ) );
                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 ) {
                if ( methodYCandidate.getSourceParameters().size() == 1 ) {
                    methodRefY = resolveViaMethod(
                        methodYCandidate.getSourceParameters().get( 0 ).getType(),
                        targetType,
                        true
                    );
                    if ( methodRefY != null ) {
                        Assignment methodRefX = resolveViaMethod(
                            sourceType,
                            methodYCandidate.getSourceParameters().get( 0 ).getType(),
                            true
                        );
                        if ( methodRefX != null ) {
                            methodRefY.setAssignment( methodRefX );
                            methodRefX.setAssignment( AssignmentFactory.createDirect( sourceReference ) );
                            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 ) {
                if ( methodYCandidate.getSourceParameters().size() == 1 ) {
                    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( AssignmentFactory.createDirect( sourceReference ) );
                            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.getSourceParameters().size() == 1 ) {
                    Assignment methodRefX = resolveViaMethod(
                        sourceType,
                        methodXCandidate.getReturnType(),
                        true
                    );
                    if ( methodRefX != null ) {
                        conversionYRef = resolveViaConversion( methodXCandidate.getReturnType(), targetType );
                        if ( conversionYRef != null ) {
                            conversionYRef.setAssignment( methodRefX );
                            methodRefX.setAssignment( AssignmentFactory.createDirect( sourceReference ) );
                            break;
                        }
                        else {
                            // both should match;
                            virtualMethodCandidates.clear();
                            conversionYRef = null;
                        }
                    }
                }
            }
            return conversionYRef;
        }

        private <T extends Method> T getBestMatch(List<T> methods, Type sourceType, Type returnType) {

            List<T> candidates = methodSelectors.getMatchingMethods(
                mappingMethod,
                methods,
                sourceType,
                returnType,
                qualifiers,
                targetPropertyName
            );

            // 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 ) {

                String errorMsg = String.format(
                    "Ambiguous mapping methods found for mapping " + mappedElement + " from %s to %s: %s.",
                    sourceType,
                    returnType,
                    Strings.join( candidates, ", " )
                );

                messager.printMessage( Diagnostic.Kind.ERROR, errorMsg, mappingMethod.getExecutable() );
            }

            if ( !candidates.isEmpty() ) {
                return candidates.get( 0 );
            }

            return null;
        }

        private Assignment getMappingMethodReference(SourceMethod method,
                                                     Type targetType) {
            MapperReference mapperReference = findMapperReference( method );

            return AssignmentFactory.createMethodReference(
                method,
                mapperReference,
                SourceMethod.containsTargetTypeParameter( method.getParameters() ) ? targetType : null
            );
        }

        private MapperReference findMapperReference(SourceMethod method) {
            for ( MapperReference ref : mapperReferences ) {
                if ( ref.getType().equals( method.getDeclaringMapper() ) ) {
                    return ref;
                }
            }
            return null;
        }

        /**
         * Whether the specified property can be mapped from source to target or not. A mapping if possible if one of
         * the following conditions is true:
         * <ul>
         * <li>the source type is assignable to the target type</li>
         * <li>a mapping method exists</li>
         * <li>a built-in conversion exists</li>
         * <li>the property is of a collection or map type and the constructor of the target type (either itself or its
         * implementation type) accepts the source type.</li>
         * </ul>
         *
         * @param property The property mapping to check.
         *
         * @return {@code true} if the specified property can be mapped, {@code false} otherwise.
         */
        private boolean isPropertyMappable(Type sourceType, Type targetType) {
            boolean collectionOrMapTargetTypeHasCompatibleConstructor = false;

            if ( sourceType.isCollectionType() && targetType.isCollectionType() ) {
                collectionOrMapTargetTypeHasCompatibleConstructor = collectionTypeHasCompatibleConstructor(
                    sourceType,
                    targetType.getImplementationType() != null
                        ? targetType.getImplementationType() : targetType
                );
            }

            if ( sourceType.isMapType() && targetType.isMapType() ) {
                collectionOrMapTargetTypeHasCompatibleConstructor = mapTypeHasCompatibleConstructor(
                    sourceType,
                    targetType.getImplementationType() != null
                        ? targetType.getImplementationType() : targetType
                );
            }

            if ( ( ( targetType.isCollectionType() || targetType.isMapType() )
                && collectionOrMapTargetTypeHasCompatibleConstructor ) ) {
                return true;
            }

            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 collectionTypeHasCompatibleConstructor(Type sourceType, Type targetType) {
            // note (issue #127): actually this should check for the presence of a matching constructor, with help of
            // Types#asMemberOf(); but this method seems to not work correctly in the Eclipse implementation, so instead
            // we just check whether the target type is parameterized in a way that it implicitly should have a
            // constructor which accepts the source type

            TypeMirror sourceElementType = sourceType.getTypeParameters().isEmpty()
                ? typeFactory.getType( Object.class ).getTypeMirror()
                : sourceType.getTypeParameters().get( 0 ).getTypeMirror();

            TypeMirror targetElementType = targetType.getTypeParameters().isEmpty()
                ? typeFactory.getType( Object.class ).getTypeMirror()
                : targetType.getTypeParameters().get( 0 ).getTypeMirror();

            return typeUtils.isAssignable( sourceElementType, targetElementType );
        }

        /**
         * 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 mapTypeHasCompatibleConstructor(Type sourceType, Type targetType) {
            // note (issue #127): actually this should check for the presence of a matching constructor, with help of
            // Types#asMemberOf(); but this method seems to not work correctly in the Eclipse implementation, so instead
            // we just check whether the target type is parameterized in a way that it implicitly should have a
            // constructor which accepts the source type

            TypeMirror sourceKeyType;
            TypeMirror targetKeyType;
            TypeMirror sourceValueType;
            TypeMirror targetValueType;

            if ( sourceType.getTypeParameters().isEmpty() ) {
                sourceKeyType = typeFactory.getType( Object.class ).getTypeMirror();
                sourceValueType = typeFactory.getType( Object.class ).getTypeMirror();
            }
            else {
                sourceKeyType = sourceType.getTypeParameters().get( 0 ).getTypeMirror();
                sourceValueType = sourceType.getTypeParameters().get( 1 ).getTypeMirror();
            }

            if ( targetType.getTypeParameters().isEmpty() ) {
                targetKeyType = typeFactory.getType( Object.class ).getTypeMirror();
                targetValueType = typeFactory.getType( Object.class ).getTypeMirror();
            }
            else {
                targetKeyType = targetType.getTypeParameters().get( 0 ).getTypeMirror();
                targetValueType = targetType.getTypeParameters().get( 1 ).getTypeMirror();
            }

            return typeUtils.isAssignable( sourceKeyType, targetKeyType )
                && typeUtils.isAssignable( sourceValueType, targetValueType );
        }
    }
}
TOP

Related Classes of org.mapstruct.ap.processor.creation.MappingResolverImpl$ResolvingAttempt

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.