/** * 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; import static org.mapstruct.ap.internal.util.Executables.getAllEnclosedExecutableElements; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.Modifier; 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.util.Elements; import javax.lang.model.util.Types; 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 org.mapstruct.ap.internal.model.source.BeanMapping; import org.mapstruct.ap.internal.model.source.IterableMapping; import org.mapstruct.ap.internal.model.source.MapMapping; import org.mapstruct.ap.internal.model.source.Mapping; import org.mapstruct.ap.internal.model.source.ParameterProvidedMethods; import org.mapstruct.ap.internal.model.source.SourceMethod; import org.mapstruct.ap.internal.model.source.ValueMapping; import org.mapstruct.ap.internal.prism.BeanMappingPrism; import org.mapstruct.ap.internal.prism.IterableMappingPrism; import org.mapstruct.ap.internal.prism.MapMappingPrism; import org.mapstruct.ap.internal.prism.MappingPrism; import org.mapstruct.ap.internal.prism.MappingsPrism; import org.mapstruct.ap.internal.prism.ObjectFactoryPrism; import org.mapstruct.ap.internal.prism.ValueMappingPrism; import org.mapstruct.ap.internal.prism.ValueMappingsPrism; import org.mapstruct.ap.internal.util.AnnotationProcessingException; import org.mapstruct.ap.internal.util.Executables; import org.mapstruct.ap.internal.util.FormattingMessager; import org.mapstruct.ap.internal.util.MapperConfiguration; import org.mapstruct.ap.internal.util.Message; /** * A {@link ModelElementProcessor} which retrieves a list of {@link SourceMethod}s * representing all the mapping methods of the given bean mapper type as well as * all referenced mapper methods declared by other mappers referenced by the * current mapper. * * @author Gunnar Morling */ public class MethodRetrievalProcessor implements ModelElementProcessor<Void, List<SourceMethod>> { private FormattingMessager messager; private TypeFactory typeFactory; private Types typeUtils; private Elements elementUtils; @Override public List<SourceMethod> process(ProcessorContext context, TypeElement mapperTypeElement, Void sourceModel) { this.messager = context.getMessager(); this.typeFactory = context.getTypeFactory(); this.typeUtils = context.getTypeUtils(); this.elementUtils = context.getElementUtils(); MapperConfiguration mapperConfig = MapperConfiguration.getInstanceOn( mapperTypeElement ); if ( !mapperConfig.isValid() ) { throw new AnnotationProcessingException( "Couldn't retrieve @Mapper annotation", mapperTypeElement, mapperConfig.getAnnotationMirror() ); } List<SourceMethod> prototypeMethods = retrievePrototypeMethods( mapperTypeElement, mapperConfig ); return retrieveMethods( mapperTypeElement, mapperTypeElement, mapperConfig, prototypeMethods ); } @Override public int getPriority() { return 1; } private List<SourceMethod> retrievePrototypeMethods(TypeElement mapperTypeElement, MapperConfiguration mapperConfig) { if ( mapperConfig.config() == null ) { return Collections.emptyList(); } TypeElement typeElement = asTypeElement( mapperConfig.config() ); List<SourceMethod> methods = new ArrayList<SourceMethod>(); for ( ExecutableElement executable : getAllEnclosedExecutableElements( elementUtils, typeElement ) ) { ExecutableType methodType = typeFactory.getMethodType( mapperConfig.config(), executable ); List<Parameter> parameters = typeFactory.getParameters( methodType, executable ); boolean containsTargetTypeParameter = SourceMethod.containsTargetTypeParameter( parameters ); // prototype methods don't have prototypes themselves List<SourceMethod> prototypeMethods = Collections.emptyList(); SourceMethod method = getMethodRequiringImplementation( methodType, executable, parameters, containsTargetTypeParameter, mapperConfig, prototypeMethods, mapperTypeElement ); if ( method != null ) { methods.add( method ); } } return methods; } /** * Retrieves the mapping methods declared by the given mapper type. * * @param usedMapper The type of interest (either the mapper to implement or a used mapper via @uses annotation) * @param mapperToImplement the top level type (mapper) that requires implementation * @param mapperConfig the mapper config * @param prototypeMethods prototype methods defined in mapper config type * @return All mapping methods declared by the given type */ private List<SourceMethod> retrieveMethods(TypeElement usedMapper, TypeElement mapperToImplement, MapperConfiguration mapperConfig, List<SourceMethod> prototypeMethods) { List<SourceMethod> methods = new ArrayList<SourceMethod>(); for ( ExecutableElement executable : getAllEnclosedExecutableElements( elementUtils, usedMapper ) ) { SourceMethod method = getMethod( usedMapper, executable, mapperToImplement, mapperConfig, prototypeMethods ); if ( method != null ) { methods.add( method ); } } //Add all methods of used mappers in order to reference them in the aggregated model if ( usedMapper.equals( mapperToImplement ) ) { for ( DeclaredType mapper : mapperConfig.uses() ) { methods.addAll( retrieveMethods( asTypeElement( mapper ), mapperToImplement, mapperConfig, prototypeMethods ) ); } } return methods; } private TypeElement asTypeElement(DeclaredType type) { return (TypeElement) type.asElement(); } private SourceMethod getMethod(TypeElement usedMapper, ExecutableElement method, TypeElement mapperToImplement, MapperConfiguration mapperConfig, List<SourceMethod> prototypeMethods) { ExecutableType methodType = typeFactory.getMethodType( (DeclaredType) usedMapper.asType(), method ); List<Parameter> parameters = typeFactory.getParameters( methodType, method ); Type returnType = typeFactory.getReturnType( methodType ); boolean methodRequiresImplementation = method.getModifiers().contains( Modifier.ABSTRACT ); boolean containsTargetTypeParameter = SourceMethod.containsTargetTypeParameter( parameters ); //add method with property mappings if an implementation needs to be generated if ( ( usedMapper.equals( mapperToImplement ) ) && methodRequiresImplementation ) { return getMethodRequiringImplementation( methodType, method, parameters, containsTargetTypeParameter, mapperConfig, prototypeMethods, mapperToImplement ); } // otherwise add reference to existing mapper method else if ( isValidReferencedMethod( parameters ) || isValidFactoryMethod( method, parameters, returnType ) || isValidLifecycleCallbackMethod( method, returnType ) ) { return getReferencedMethod( usedMapper, methodType, method, mapperToImplement, parameters ); } else { return null; } } private SourceMethod getMethodRequiringImplementation(ExecutableType methodType, ExecutableElement method, List<Parameter> parameters, boolean containsTargetTypeParameter, MapperConfiguration mapperConfig, List<SourceMethod> prototypeMethods, TypeElement mapperToImplement) { Type returnType = typeFactory.getReturnType( methodType ); List<Type> exceptionTypes = typeFactory.getThrownTypes( methodType ); List<Parameter> sourceParameters = Parameter.getSourceParameters( parameters ); List<Parameter> contextParameters = Parameter.getContextParameters( parameters ); Parameter targetParameter = extractTargetParameter( parameters ); Type resultType = selectResultType( returnType, targetParameter ); boolean isValid = checkParameterAndReturnType( method, sourceParameters, targetParameter, contextParameters, resultType, returnType, containsTargetTypeParameter ); if ( !isValid ) { return null; } ParameterProvidedMethods contextProvidedMethods = retrieveLifecycleMethodsFromContext( contextParameters, mapperToImplement, mapperConfig ); return new SourceMethod.Builder() .setExecutable( method ) .setParameters( parameters ) .setReturnType( returnType ) .setExceptionTypes( exceptionTypes ) .setMappings( getMappings( method ) ) .setIterableMapping( IterableMapping.fromPrism( IterableMappingPrism.getInstanceOn( method ), method, messager ) ) .setMapMapping( MapMapping.fromPrism( MapMappingPrism.getInstanceOn( method ), method, messager ) ) .setBeanMapping( BeanMapping.fromPrism( BeanMappingPrism.getInstanceOn( method ), method, messager ) ) .setValueMappings( getValueMappings( method ) ) .setTypeUtils( typeUtils ) .setMessager( messager ) .setTypeFactory( typeFactory ) .setMapperConfiguration( mapperConfig ) .setPrototypeMethods( prototypeMethods ) .setContextProvidedMethods( contextProvidedMethods ) .build(); } private ParameterProvidedMethods retrieveLifecycleMethodsFromContext( List<Parameter> contextParameters, TypeElement mapperToImplement, MapperConfiguration mapperConfig) { ParameterProvidedMethods.Builder builder = ParameterProvidedMethods.builder(); for ( Parameter contextParam : contextParameters ) { List<SourceMethod> contextParamMethods = retrieveMethods( contextParam.getType().getTypeElement(), mapperToImplement, mapperConfig, Collections.<SourceMethod> emptyList() ); List<SourceMethod> lifecycleMethods = new ArrayList<SourceMethod>( contextParamMethods.size() ); for ( SourceMethod sourceMethod : contextParamMethods ) { if ( sourceMethod.isLifecycleCallbackMethod() ) { lifecycleMethods.add( sourceMethod ); } } builder.addMethodsForParameter( contextParam, lifecycleMethods ); } return builder.build(); } private SourceMethod getReferencedMethod(TypeElement usedMapper, ExecutableType methodType, ExecutableElement method, TypeElement mapperToImplement, List<Parameter> parameters) { Type returnType = typeFactory.getReturnType( methodType ); List<Type> exceptionTypes = typeFactory.getThrownTypes( methodType ); Type usedMapperAsType = typeFactory.getType( usedMapper ); Type mapperToImplementAsType = typeFactory.getType( mapperToImplement ); if ( !mapperToImplementAsType.canAccess( usedMapperAsType, method ) ) { return null; } Type definingType = typeFactory.getType( method.getEnclosingElement().asType() ); return new SourceMethod.Builder() .setDeclaringMapper( usedMapper.equals( mapperToImplement ) ? null : usedMapperAsType ) .setDefininingType( definingType ) .setExecutable( method ) .setParameters( parameters ) .setReturnType( returnType ) .setExceptionTypes( exceptionTypes ) .setTypeUtils( typeUtils ) .setTypeFactory( typeFactory ) .build(); } private boolean isValidLifecycleCallbackMethod(ExecutableElement method, Type returnType) { return Executables.isLifecycleCallbackMethod( method ); } private boolean isValidReferencedMethod(List<Parameter> parameters) { return isValidReferencedOrFactoryMethod( 1, 1, parameters ); } private boolean isValidFactoryMethod(ExecutableElement method, List<Parameter> parameters, Type returnType) { return !isVoid( returnType ) && ( isValidReferencedOrFactoryMethod( 0, 0, parameters ) || hasFactoryAnnotation( method ) ); } private boolean hasFactoryAnnotation(ExecutableElement method) { return ObjectFactoryPrism.getInstanceOn( method ) != null; } private boolean isVoid(Type returnType) { return returnType.getTypeMirror().getKind() == TypeKind.VOID; } private boolean isValidReferencedOrFactoryMethod(int sourceParamCount, int targetParamCount, List<Parameter> parameters) { int validSourceParameters = 0; int targetParameters = 0; int targetTypeParameters = 0; for ( Parameter param : parameters ) { if ( param.isMappingTarget() ) { targetParameters++; } else if ( param.isTargetType() ) { targetTypeParameters++; } else if ( !param.isMappingContext() ) { validSourceParameters++; } } return validSourceParameters == sourceParamCount && targetParameters <= targetParamCount && targetTypeParameters <= 1; } private Parameter extractTargetParameter(List<Parameter> parameters) { for ( Parameter param : parameters ) { if ( param.isMappingTarget() ) { return param; } } return null; } private Type selectResultType(Type returnType, Parameter targetParameter) { if ( null != targetParameter ) { return targetParameter.getType(); } else { return returnType; } } private boolean checkParameterAndReturnType(ExecutableElement method, List<Parameter> sourceParameters, Parameter targetParameter, List<Parameter> contextParameters, Type resultType, Type returnType, boolean containsTargetTypeParameter) { if ( sourceParameters.isEmpty() ) { messager.printMessage( method, Message.RETRIEVAL_NO_INPUT_ARGS ); return false; } if ( targetParameter != null && ( sourceParameters.size() + contextParameters.size() + 1 != method.getParameters().size() ) ) { messager.printMessage( method, Message.RETRIEVAL_DUPLICATE_MAPPING_TARGETS ); return false; } if ( isVoid( resultType ) ) { messager.printMessage( method, Message.RETRIEVAL_VOID_MAPPING_METHOD ); return false; } if ( returnType.getTypeMirror().getKind() != TypeKind.VOID && !resultType.isAssignableTo( returnType ) ) { messager.printMessage( method, Message.RETRIEVAL_NON_ASSIGNABLE_RESULTTYPE ); return false; } for ( Parameter sourceParameter : sourceParameters ) { if ( sourceParameter.getType().isTypeVar() ) { messager.printMessage( method, Message.RETRIEVAL_TYPE_VAR_SOURCE ); return false; } } Set<Type> contextParameterTypes = new HashSet<Type>(); for ( Parameter contextParameter : contextParameters ) { if ( !contextParameterTypes.add( contextParameter.getType() ) ) { messager.printMessage( method, Message.RETRIEVAL_CONTEXT_PARAMS_WITH_SAME_TYPE ); return false; } } if ( returnType.isTypeVar() || resultType.isTypeVar() ) { messager.printMessage( method, Message.RETRIEVAL_TYPE_VAR_RESULT ); return false; } Type parameterType = sourceParameters.get( 0 ).getType(); if ( parameterType.isIterableOrStreamType() && !resultType.isIterableOrStreamType() ) { messager.printMessage( method, Message.RETRIEVAL_ITERABLE_TO_NON_ITERABLE ); return false; } if ( containsTargetTypeParameter ) { messager.printMessage( method, Message.RETRIEVAL_MAPPING_HAS_TARGET_TYPE_PARAMETER ); return false; } if ( !parameterType.isIterableOrStreamType() && resultType.isIterableOrStreamType() ) { messager.printMessage( method, Message.RETRIEVAL_NON_ITERABLE_TO_ITERABLE ); return false; } if ( parameterType.isPrimitive() ) { messager.printMessage( method, Message.RETRIEVAL_PRIMITIVE_PARAMETER ); return false; } if ( resultType.isPrimitive() ) { messager.printMessage( method, Message.RETRIEVAL_PRIMITIVE_RETURN ); return false; } if ( parameterType.isEnumType() && !resultType.isEnumType() ) { messager.printMessage( method, Message.RETRIEVAL_ENUM_TO_NON_ENUM ); return false; } if ( !parameterType.isEnumType() && resultType.isEnumType() ) { messager.printMessage( method, Message.RETRIEVAL_NON_ENUM_TO_ENUM ); return false; } for ( Type typeParameter : resultType.getTypeParameters() ) { if ( typeParameter.isTypeVar() ) { messager.printMessage( method, Message.RETRIEVAL_TYPE_VAR_RESULT ); return false; } if ( typeParameter.isWildCardExtendsBound() ) { messager.printMessage( method, Message.RETRIEVAL_WILDCARD_EXTENDS_BOUND_RESULT ); return false; } } for ( Type typeParameter : parameterType.getTypeParameters() ) { if ( typeParameter.isWildCardSuperBound() ) { messager.printMessage( method, Message.RETRIEVAL_WILDCARD_SUPER_BOUND_SOURCE ); return false; } if ( typeParameter.isTypeVar() ) { messager.printMessage( method, Message.RETRIEVAL_TYPE_VAR_SOURCE ); return false; } } return true; } /** * Retrieves the mappings configured via {@code @Mapping} from the given * method. * * @param method The method of interest * * @return The mappings for the given method, keyed by target property name */ private Map<String, List<Mapping>> getMappings(ExecutableElement method) { Map<String, List<Mapping>> mappings = new HashMap<String, List<Mapping>>(); MappingPrism mappingAnnotation = MappingPrism.getInstanceOn( method ); MappingsPrism mappingsAnnotation = MappingsPrism.getInstanceOn( method ); if ( mappingAnnotation != null ) { if ( !mappings.containsKey( mappingAnnotation.target() ) ) { mappings.put( mappingAnnotation.target(), new ArrayList<Mapping>() ); } Mapping mapping = Mapping.fromMappingPrism( mappingAnnotation, method, messager ); if ( mapping != null ) { mappings.get( mappingAnnotation.target() ).add( mapping ); } } if ( mappingsAnnotation != null ) { mappings.putAll( Mapping.fromMappingsPrism( mappingsAnnotation, method, messager ) ); } return mappings; } /** * Retrieves the mappings configured via {@code @ValueMapping} from the given * method. * * @param method The method of interest * * @return The mappings for the given method, keyed by target property name */ private List<ValueMapping> getValueMappings(ExecutableElement method) { List<ValueMapping> valueMappings = new ArrayList<ValueMapping>(); ValueMappingPrism mappingAnnotation = ValueMappingPrism.getInstanceOn( method ); ValueMappingsPrism mappingsAnnotation = ValueMappingsPrism.getInstanceOn( method ); if ( mappingAnnotation != null ) { ValueMapping valueMapping = ValueMapping.fromMappingPrism( mappingAnnotation, method, messager ); if ( valueMapping != null ) { valueMappings.add( valueMapping ); } } if ( mappingsAnnotation != null ) { ValueMapping.fromMappingsPrism( mappingsAnnotation, method, messager, valueMappings ); } return valueMappings; } }