/** * 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.model.source; import static org.mapstruct.ap.internal.util.Collections.first; import java.util.ArrayList; import java.util.Collections; 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.util.Types; import org.mapstruct.ap.internal.model.common.Accessibility; 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.prism.ObjectFactoryPrism; 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.Strings; /** * Represents a mapping method with source and target type and the mappings between the properties of source and target * type. * <p> * A method can either be configured by itself or by another method for the inverse mapping direction (the appropriate * setter on {@link MappingOptions} will be called in this case). * * @author Gunnar Morling */ public class SourceMethod implements Method { private final Types typeUtils; private final TypeFactory typeFactory; private final Type declaringMapper; private final ExecutableElement executable; private final List<Parameter> parameters; private final Parameter mappingTargetParameter; private final Parameter targetTypeParameter; private final boolean isObjectFactory; private final Type returnType; private final Accessibility accessibility; private final List<Type> exceptionTypes; private final MapperConfiguration config; private final MappingOptions mappingOptions; private final List<SourceMethod> prototypeMethods; private final Type mapperToImplement; private final List<Parameter> sourceParameters; private final List<Parameter> contextParameters; private final ParameterProvidedMethods contextProvidedMethods; private List<String> parameterNames; private List<SourceMethod> applicablePrototypeMethods; private List<SourceMethod> applicableReversePrototypeMethods; private Boolean isBeanMapping; private Boolean isEnumMapping; private Boolean isValueMapping; private Boolean isIterableMapping; private Boolean isMapMapping; private Boolean isStreamMapping; public static class Builder { private Type declaringMapper = null; private Type definingType = null; private ExecutableElement executable; private List<Parameter> parameters; private Type returnType = null; private List<Type> exceptionTypes; private Map<String, List<Mapping>> mappings; private IterableMapping iterableMapping = null; private MapMapping mapMapping = null; private BeanMapping beanMapping = null; private Types typeUtils; private TypeFactory typeFactory = null; private FormattingMessager messager = null; private MapperConfiguration mapperConfig = null; private List<SourceMethod> prototypeMethods = Collections.emptyList(); private List<ValueMapping> valueMappings; private ParameterProvidedMethods contextProvidedMethods; public Builder setDeclaringMapper(Type declaringMapper) { this.declaringMapper = declaringMapper; return this; } public Builder setExecutable(ExecutableElement executable) { this.executable = executable; return this; } public Builder setParameters(List<Parameter> parameters) { this.parameters = parameters; return this; } public Builder setReturnType(Type returnType) { this.returnType = returnType; return this; } public Builder setExceptionTypes(List<Type> exceptionTypes) { this.exceptionTypes = exceptionTypes; return this; } public Builder setMappings(Map<String, List<Mapping>> mappings) { this.mappings = mappings; return this; } public Builder setIterableMapping(IterableMapping iterableMapping) { this.iterableMapping = iterableMapping; return this; } public Builder setMapMapping(MapMapping mapMapping) { this.mapMapping = mapMapping; return this; } public Builder setBeanMapping(BeanMapping beanMapping) { this.beanMapping = beanMapping; return this; } public Builder setValueMappings(List<ValueMapping> valueMappings) { this.valueMappings = valueMappings; return this; } public Builder setTypeUtils(Types typeUtils) { this.typeUtils = typeUtils; return this; } public Builder setTypeFactory(TypeFactory typeFactory) { this.typeFactory = typeFactory; return this; } public Builder setMessager(FormattingMessager messager) { this.messager = messager; return this; } public Builder setMapperConfiguration(MapperConfiguration mapperConfig) { this.mapperConfig = mapperConfig; return this; } public Builder setPrototypeMethods(List<SourceMethod> prototypeMethods) { this.prototypeMethods = prototypeMethods; return this; } public Builder setDefininingType(Type definingType) { this.definingType = definingType; return this; } public Builder setContextProvidedMethods(ParameterProvidedMethods contextProvidedMethods) { this.contextProvidedMethods = contextProvidedMethods; return this; } public SourceMethod build() { MappingOptions mappingOptions = new MappingOptions( mappings, iterableMapping, mapMapping, beanMapping, valueMappings, false ); SourceMethod sourceMethod = new SourceMethod( this, mappingOptions ); if ( mappings != null ) { for ( Map.Entry<String, List<Mapping>> entry : mappings.entrySet() ) { for ( Mapping mapping : entry.getValue() ) { mapping.init( sourceMethod, messager, typeFactory ); } } } return sourceMethod; } } private SourceMethod(Builder builder, MappingOptions mappingOptions) { this.declaringMapper = builder.declaringMapper; this.executable = builder.executable; this.parameters = builder.parameters; this.returnType = builder.returnType; this.exceptionTypes = builder.exceptionTypes; this.accessibility = Accessibility.fromModifiers( builder.executable.getModifiers() ); this.mappingOptions = mappingOptions; this.sourceParameters = Parameter.getSourceParameters( parameters ); this.contextParameters = Parameter.getContextParameters( parameters ); this.contextProvidedMethods = builder.contextProvidedMethods; this.mappingTargetParameter = Parameter.getMappingTargetParameter( parameters ); this.targetTypeParameter = Parameter.getTargetTypeParameter( parameters ); this.isObjectFactory = determineIfIsObjectFactory( executable ); this.typeUtils = builder.typeUtils; this.typeFactory = builder.typeFactory; this.config = builder.mapperConfig; this.prototypeMethods = builder.prototypeMethods; this.mapperToImplement = builder.definingType; } private boolean determineIfIsObjectFactory(ExecutableElement executable) { boolean hasFactoryAnnotation = ObjectFactoryPrism.getInstanceOn( executable ) != null; boolean hasNoSourceParameters = getSourceParameters().isEmpty(); boolean hasNoMappingTargetParam = getMappingTargetParameter() == null; return !isLifecycleCallbackMethod() && !returnType.isVoid() && hasNoMappingTargetParam && ( hasFactoryAnnotation || hasNoSourceParameters ); } @Override public Type getDeclaringMapper() { return declaringMapper; } @Override public ExecutableElement getExecutable() { return executable; } @Override public String getName() { return executable.getSimpleName().toString(); } @Override public List<Parameter> getParameters() { return parameters; } @Override public List<Parameter> getSourceParameters() { return sourceParameters; } @Override public List<Parameter> getContextParameters() { return contextParameters; } @Override public ParameterProvidedMethods getContextProvidedMethods() { return contextProvidedMethods; } @Override public List<String> getParameterNames() { if ( parameterNames == null ) { List<String> names = new ArrayList<String>( parameters.size() ); for ( Parameter parameter : parameters ) { names.add( parameter.getName() ); } parameterNames = Collections.unmodifiableList( names ); } return parameterNames; } @Override public Type getResultType() { return mappingTargetParameter != null ? mappingTargetParameter.getType() : returnType; } @Override public Type getReturnType() { return returnType; } @Override public Accessibility getAccessibility() { return accessibility; } public Mapping getSingleMappingByTargetPropertyName(String targetPropertyName) { List<Mapping> all = mappingOptions.getMappings().get( targetPropertyName ); return all != null ? first( all ) : null; } public boolean reverses(SourceMethod method) { return method.getDeclaringMapper() == null && method.isAbstract() && getSourceParameters().size() == 1 && method.getSourceParameters().size() == 1 && first( getSourceParameters() ).getType().isAssignableTo( method.getResultType() ) && getResultType().isAssignableTo( first( method.getSourceParameters() ).getType() ); } public boolean isSame(SourceMethod method) { return getSourceParameters().size() == 1 && method.getSourceParameters().size() == 1 && equals( first( getSourceParameters() ).getType(), first( method.getSourceParameters() ).getType() ) && equals( getResultType(), method.getResultType() ); } public boolean canInheritFrom(SourceMethod method) { return method.getDeclaringMapper() == null && method.isAbstract() && isMapMapping() == method.isMapMapping() && isIterableMapping() == method.isIterableMapping() && isEnumMapping() == method.isEnumMapping() && getResultType().isAssignableTo( method.getResultType() ) && allParametersAreAssignable( getSourceParameters(), method.getSourceParameters() ); } @Override public Parameter getMappingTargetParameter() { return mappingTargetParameter; } @Override public boolean isObjectFactory() { return isObjectFactory; } @Override public Parameter getTargetTypeParameter() { return targetTypeParameter; } public boolean isIterableMapping() { if ( isIterableMapping == null ) { isIterableMapping = getSourceParameters().size() == 1 && first( getSourceParameters() ).getType().isIterableType() && getResultType().isIterableType(); } return isIterableMapping; } 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() ); } return isStreamMapping; } public boolean isMapMapping() { if ( isMapMapping == null ) { isMapMapping = getSourceParameters().size() == 1 && first( getSourceParameters() ).getType().isMapType() && getResultType().isMapType(); } return isMapMapping; } public boolean isEnumMapping() { if ( isEnumMapping == null ) { isEnumMapping = MappingMethodUtils.isEnumMapping( this ); } return isEnumMapping; } public boolean isBeanMapping() { if ( isBeanMapping == null ) { isBeanMapping = !isIterableMapping() && !isMapMapping() && !isEnumMapping() && !isValueMapping() && !isStreamMapping(); } return isBeanMapping; } /** * The default enum mapping (no mappings specified) will from now on be handled as a value mapping. If there * are any @Mapping / @Mappings defined on the method, then the deprecated enum behavior should be executed. * * @return whether (true) or not (false) to execute value mappings */ public boolean isValueMapping() { if ( isValueMapping == null ) { isValueMapping = isEnumMapping() && mappingOptions.getMappings().isEmpty(); } return isValueMapping; } private boolean equals(Object o1, Object o2) { return (o1 == null && o2 == null) || (o1 != null) && o1.equals( o2 ); } @Override public String toString() { StringBuilder sb = new StringBuilder( returnType.toString() ); sb.append( " " ); if ( declaringMapper != null ) { sb.append( declaringMapper ).append( "." ); } sb.append( getName() ).append( "(" ).append( Strings.join( parameters, ", " ) ).append( ")" ); return sb.toString(); } /** * Returns the {@link Mapping}s for the given source property. * * @param sourcePropertyName the source property name * @return list of mappings */ public List<Mapping> getMappingBySourcePropertyName(String sourcePropertyName) { List<Mapping> mappingsOfSourceProperty = new ArrayList<Mapping>(); for ( List<Mapping> mappingOfProperty : mappingOptions.getMappings().values() ) { for ( Mapping mapping : mappingOfProperty ) { if ( isEnumMapping() ) { if ( mapping.getSourceName().equals( sourcePropertyName ) ) { mappingsOfSourceProperty.add( mapping ); } } else { List<PropertyEntry> sourceEntries = mapping.getSourceReference().getPropertyEntries(); // there can only be a mapping if there's only one entry for a source property, so: param.property. // There can be no mapping if there are more entries. So: param.property.property2 if ( sourceEntries.size() == 1 && sourcePropertyName.equals( first( sourceEntries ).getName() ) ) { mappingsOfSourceProperty.add( mapping ); } } } } return mappingsOfSourceProperty; } public Parameter getSourceParameter(String sourceParameterName) { for ( Parameter parameter : getSourceParameters() ) { if ( parameter.getName().equals( sourceParameterName ) ) { return parameter; } } return null; } public List<SourceMethod> getApplicablePrototypeMethods() { if ( applicablePrototypeMethods == null ) { applicablePrototypeMethods = new ArrayList<SourceMethod>(); for ( SourceMethod prototype : prototypeMethods ) { if ( canInheritFrom( prototype ) ) { applicablePrototypeMethods.add( prototype ); } } } return applicablePrototypeMethods; } public List<SourceMethod> getApplicableReversePrototypeMethods() { if ( applicableReversePrototypeMethods == null ) { applicableReversePrototypeMethods = new ArrayList<SourceMethod>(); for ( SourceMethod prototype : prototypeMethods ) { if ( reverses( prototype ) ) { applicableReversePrototypeMethods.add( prototype ); } } } return applicableReversePrototypeMethods; } private static boolean allParametersAreAssignable(List<Parameter> fromParams, List<Parameter> toParams) { if ( fromParams.size() == toParams.size() ) { Set<Parameter> unaccountedToParams = new HashSet<Parameter>( toParams ); for ( Parameter fromParam : fromParams ) { // each fromParam needs at least one match, and all toParam need to be accounted for at the end boolean hasMatch = false; for ( Parameter toParam : toParams ) { if ( fromParam.getType().isAssignableTo( toParam.getType() ) ) { unaccountedToParams.remove( toParam ); hasMatch = true; } } if ( !hasMatch ) { return false; } } return unaccountedToParams.isEmpty(); } return false; } /** * Whether an implementation of this method must be generated or not. * * @return true when an implementation is required */ @Override public boolean overridesMethod() { return declaringMapper == null && executable.getModifiers().contains( Modifier.ABSTRACT ); } @Override public boolean matches(List<Type> sourceTypes, Type targetType) { MethodMatcher matcher = new MethodMatcher( typeUtils, typeFactory, this ); return matcher.matches( sourceTypes, targetType ); } /** * @param parameters the parameter list to check * * @return {@code true} if the parameter list contains a parameter annotated with {@code @TargetType} */ public static boolean containsTargetTypeParameter(List<Parameter> parameters) { for ( Parameter param : parameters ) { if ( param.isTargetType() ) { return true; } } return false; } @Override public List<Type> getThrownTypes() { return exceptionTypes; } @Override public MappingOptions getMappingOptions() { return mappingOptions; } @Override public boolean isStatic() { return executable.getModifiers().contains( Modifier.STATIC ); } @Override public boolean isDefault() { return Executables.isDefaultMethod( executable ); } @Override public Type getDefiningType() { return mapperToImplement; } @Override public MapperConfiguration getMapperConfiguration() { return config; } @Override public boolean isLifecycleCallbackMethod() { return Executables.isLifecycleCallbackMethod( getExecutable() ); } public boolean isAfterMappingMethod() { return Executables.isAfterMappingMethod( getExecutable() ); } public boolean isBeforeMappingMethod() { return Executables.isBeforeMappingMethod( getExecutable() ); } /** * @return returns true for interface methods (see jls 9.4) lacking a default or static modifier and for abstract * methods */ public boolean isAbstract() { return executable.getModifiers().contains( Modifier.ABSTRACT ); } @Override public boolean isUpdateMethod() { return getMappingTargetParameter() != null; } }