#157 refactoring: moving all mapping method resolving stuff to class with own responsibility

This commit is contained in:
sjaakd 2014-03-10 21:33:04 +01:00
parent 81c936190e
commit 59719dc6cb
2 changed files with 320 additions and 218 deletions

View File

@ -36,8 +36,6 @@ import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;
import javax.tools.Diagnostic.Kind;
import org.mapstruct.ap.conversion.ConversionProvider;
import org.mapstruct.ap.conversion.Conversions;
import org.mapstruct.ap.model.BeanMappingMethod;
import org.mapstruct.ap.model.Decorator;
import org.mapstruct.ap.model.DefaultMapperReference;
@ -51,18 +49,12 @@ import org.mapstruct.ap.model.MappingMethod;
import org.mapstruct.ap.model.MethodReference;
import org.mapstruct.ap.model.PropertyMapping;
import org.mapstruct.ap.model.TypeConversion;
import org.mapstruct.ap.model.VirtualMappingMethod;
import org.mapstruct.ap.model.common.ConversionContext;
import org.mapstruct.ap.model.common.DefaultConversionContext;
import org.mapstruct.ap.model.common.Parameter;
import org.mapstruct.ap.model.common.Type;
import org.mapstruct.ap.model.common.TypeFactory;
import org.mapstruct.ap.model.source.EnumMapping;
import org.mapstruct.ap.model.source.Mapping;
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.option.Options;
import org.mapstruct.ap.option.ReportingPolicy;
@ -86,16 +78,10 @@ public class MapperCreationProcessor implements ModelElementProcessor<List<Sourc
private Options options;
private TypeFactory typeFactory;
private Conversions conversions;
private BuiltInMappingMethods builtInMethods;
private MethodSelectors methodSelectors;
private MappingMethodResolver mappingMethodResolver;
/**
* Private methods which are not present in the original mapper interface and are added to map certain property
* types.
*/
private Set<VirtualMappingMethod> virtualMethods;
@Override
public Mapper process(ProcessorContext context, TypeElement mapperTypeElement, List<SourceMethod> sourceModel) {
@ -105,11 +91,9 @@ public class MapperCreationProcessor implements ModelElementProcessor<List<Sourc
this.options = context.getOptions();
this.typeFactory = context.getTypeFactory();
this.conversions = new Conversions( elementUtils, typeFactory );
this.builtInMethods = new BuiltInMappingMethods( typeFactory );
this.virtualMethods = new HashSet<VirtualMappingMethod>();
this.methodSelectors = new MethodSelectors( typeUtils, typeFactory );
this.mappingMethodResolver = new MappingMethodResolver(messager, typeFactory, elementUtils, typeUtils );
return getMapper( mapperTypeElement, sourceModel );
}
@ -123,7 +107,7 @@ public class MapperCreationProcessor implements ModelElementProcessor<List<Sourc
ReportingPolicy unmappedTargetPolicy = getEffectiveUnmappedTargetPolicy( element );
List<MapperReference> mapperReferences = getReferencedMappers( element );
List<MappingMethod> mappingMethods = getMappingMethods( mapperReferences, methods, unmappedTargetPolicy );
mappingMethods.addAll( virtualMethods );
mappingMethods.addAll( mappingMethodResolver.getVirtualMethodsToCreate() );
Mapper mapper = new Mapper.Builder()
.element( element )
@ -657,7 +641,7 @@ public class MapperCreationProcessor implements ModelElementProcessor<List<Sourc
String targetPropertyName = Executables.getPropertyName( targetAcessor );
String mappedElement = "property '" + Executables.getPropertyName( sourceAccessor ) + "'";
MethodReference mappingMethodReference = getMappingMethodReferenceBasedOnMethod(
MethodReference mappingMethodReference = mappingMethodResolver.getMappingMethodReferenceBasedOnMethod(
method,
mappedElement,
mapperReferences,
@ -668,7 +652,7 @@ public class MapperCreationProcessor implements ModelElementProcessor<List<Sourc
dateFormat
);
TypeConversion conversion = getConversion(
TypeConversion conversion = mappingMethodResolver.getConversion(
sourceType,
targetType,
dateFormat,
@ -690,7 +674,7 @@ public class MapperCreationProcessor implements ModelElementProcessor<List<Sourc
if ( !isPropertyMappable( property ) ) {
// when not mappable, try again with another property mapping method based on parameter only.
mappingMethodReference = getMappingMethodReferenceBasedOnParameter(
mappingMethodReference = mappingMethodResolver.getMappingMethodReferenceBasedOnParameter(
method,
"property '" + Executables.getPropertyName( sourceAccessor ) + "'",
mapperReferences,
@ -737,7 +721,7 @@ public class MapperCreationProcessor implements ModelElementProcessor<List<Sourc
Type targetElementType = method.getResultType().getTypeParameters().get( 0 );
String dateFormat = method.getIterableMapping() != null ? method.getIterableMapping().getDateFormat() : null;
TypeConversion conversion = getConversion(
TypeConversion conversion = mappingMethodResolver.getConversion(
sourceElementType,
targetElementType,
dateFormat,
@ -747,7 +731,7 @@ public class MapperCreationProcessor implements ModelElementProcessor<List<Sourc
)
);
MethodReference mappingMethodReference = getMappingMethodReferenceBasedOnMethod(
MethodReference mappingMethodReference = mappingMethodResolver.getMappingMethodReferenceBasedOnMethod(
method,
"collection element",
mapperReferences,
@ -761,7 +745,7 @@ public class MapperCreationProcessor implements ModelElementProcessor<List<Sourc
if ( !sourceElementType.isAssignableTo( targetElementType ) && conversion == null &&
mappingMethodReference == null ) {
// when no conversion is found and no mapping method try match based on parameter only
mappingMethodReference = getMappingMethodReferenceBasedOnParameter(
mappingMethodReference = mappingMethodResolver.getMappingMethodReferenceBasedOnParameter(
method,
"collection element",
mapperReferences,
@ -805,7 +789,7 @@ public class MapperCreationProcessor implements ModelElementProcessor<List<Sourc
Type keyTargetType = resultTypeParams.get( 0 );
String keyDateFormat = method.getMapMapping() != null ? method.getMapMapping().getKeyFormat() : null;
MethodReference keyMappingMethod = getMappingMethodReferenceBasedOnMethod(
MethodReference keyMappingMethod = mappingMethodResolver.getMappingMethodReferenceBasedOnMethod(
method,
"map key",
mapperReferences,
@ -815,11 +799,12 @@ public class MapperCreationProcessor implements ModelElementProcessor<List<Sourc
null, // there is no targetPropertyName
keyDateFormat
);
TypeConversion keyConversion = getConversion( keySourceType, keyTargetType, keyDateFormat, "entry.getKey()" );
TypeConversion keyConversion = mappingMethodResolver.getConversion( keySourceType, keyTargetType,
keyDateFormat, "entry.getKey()" );
if ( !keySourceType.isAssignableTo( keyTargetType ) && keyConversion == null && keyMappingMethod == null ) {
// when no conversion is found and no mapping method try match based on parameter only
keyMappingMethod = getMappingMethodReferenceBasedOnParameter(
keyMappingMethod = mappingMethodResolver.getMappingMethodReferenceBasedOnParameter(
method,
"map key",
mapperReferences,
@ -848,7 +833,7 @@ public class MapperCreationProcessor implements ModelElementProcessor<List<Sourc
Type valueTargetType = resultTypeParams.get( 1 );
String valueDateFormat = method.getMapMapping() != null ? method.getMapMapping().getValueFormat() : null;
MethodReference valueMappingMethod = getMappingMethodReferenceBasedOnMethod(
MethodReference valueMappingMethod = mappingMethodResolver.getMappingMethodReferenceBasedOnMethod(
method,
"map value",
mapperReferences,
@ -858,7 +843,7 @@ public class MapperCreationProcessor implements ModelElementProcessor<List<Sourc
null, // there is no targetPropertyName
valueDateFormat
);
TypeConversion valueConversion = getConversion(
TypeConversion valueConversion = mappingMethodResolver.getConversion(
valueSourceType,
valueTargetType,
valueDateFormat,
@ -868,7 +853,7 @@ public class MapperCreationProcessor implements ModelElementProcessor<List<Sourc
if ( !valueSourceType.isAssignableTo( valueTargetType ) && valueConversion == null &&
valueMappingMethod == null ) {
// when no conversion is found and no mapping method try match based on parameter only
keyMappingMethod = getMappingMethodReferenceBasedOnParameter(
keyMappingMethod = mappingMethodResolver.getMappingMethodReferenceBasedOnParameter(
method,
"map value",
mapperReferences,
@ -1037,185 +1022,6 @@ public class MapperCreationProcessor implements ModelElementProcessor<List<Sourc
return unmappedSourceEnumConstants.isEmpty();
}
private TypeConversion getConversion(Type sourceType, Type targetType, String dateFormat, String sourceReference) {
ConversionProvider conversionProvider = conversions.getConversion( sourceType, targetType );
if ( conversionProvider == null ) {
return null;
}
return conversionProvider.to(
sourceReference,
new DefaultConversionContext( typeFactory, targetType, dateFormat )
);
}
/**
* Returns a reference to a method mapping the given source type to the given target type, if such a method exists.
*/
private MethodReference getMappingMethodReferenceBasedOnMethod(SourceMethod method,
String mappedElement,
List<MapperReference> mapperReferences,
List<SourceMethod> methods,
Type sourceType,
Type targetType,
String targetPropertyName,
String dateFormat) {
// first try to find a matching source method
SourceMethod matchingSourceMethod = getBestMatch(
method,
mappedElement,
methods,
sourceType,
targetType,
targetPropertyName
);
if ( matchingSourceMethod != null ) {
return getMappingMethodReference( matchingSourceMethod, mapperReferences, targetType );
}
// then a matching built-in method
BuiltInMethod matchingBuiltInMethod = getBestMatch(
method,
mappedElement,
builtInMethods.getBuiltInMethods(),
sourceType,
targetType,
targetPropertyName
);
return matchingBuiltInMethod != null ?
getMappingMethodReference( matchingBuiltInMethod, targetType, dateFormat ) : 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 ) )
*
* @param mappingMethod target mapping method
* @param mappedElement used for error messages
* @param mapperReferences list of references to mapper
* @param methods list of candidate methods
* @param parameterType parameter to match
* @param returnType return type to match
* @param dateFormat used for formatting dates in build in methods that need context information
*
* @return a method reference.
*/
private MethodReference getMappingMethodReferenceBasedOnParameter(SourceMethod mappingMethod,
String mappedElement,
List<MapperReference>
mapperReferences,
List<SourceMethod> methods,
Type parameterType,
Type returnType,
String targetPropertyName,
String dateFormat) {
List<Method> methodYCandidates = new ArrayList<Method>( methods );
methodYCandidates.addAll( builtInMethods.getBuiltInMethods() );
MethodReference 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 = getMappingMethodReferenceBasedOnMethod(
mappingMethod,
mappedElement,
mapperReferences,
methods,
methodYCandidate.getSourceParameters().get( 0 ).getType(),
returnType,
targetPropertyName,
dateFormat
);
if ( methodRefY != null ) {
MethodReference methodRefX = getMappingMethodReferenceBasedOnMethod(
mappingMethod,
mappedElement,
mapperReferences,
methods,
parameterType,
methodYCandidate.getSourceParameters().get( 0 ).getType(),
targetPropertyName,
dateFormat
);
if ( methodRefX != null ) {
methodRefY.setMethodRefChild( methodRefX );
break;
}
else {
// both should match;
methodRefY = null;
}
}
}
}
return methodRefY;
}
private <T extends Method> T getBestMatch(SourceMethod mappingMethod,
String mappedElement,
List<T> methods,
Type sourceType,
Type returnType,
String targetPropertyName) {
List<T> candidates = methodSelectors.getMatchingMethods(
mappingMethod,
methods,
sourceType,
returnType,
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 ) {
messager.printMessage(
Kind.ERROR,
String.format(
"Ambiguous mapping methods found for mapping " + mappedElement + " from %s to %s: %s.",
sourceType,
returnType,
Strings.join( candidates, ", " )
),
mappingMethod.getExecutable()
);
}
if ( !candidates.isEmpty() ) {
return candidates.get( 0 );
}
return null;
}
private MethodReference getMappingMethodReference(SourceMethod method, List<MapperReference> mapperReferences,
Type targetType) {
MapperReference mapperReference = findMapperReference( mapperReferences, method );
return new MethodReference(
method,
mapperReference,
SourceMethod.containsTargetTypeParameter( method.getParameters() ) ? targetType : null
);
}
private MapperReference findMapperReference(List<MapperReference> mapperReferences, SourceMethod method) {
for ( MapperReference ref : mapperReferences ) {
if ( ref.getType().equals( method.getDeclaringMapper() ) ) {
@ -1225,12 +1031,6 @@ public class MapperCreationProcessor implements ModelElementProcessor<List<Sourc
return null;
}
private MethodReference getMappingMethodReference(BuiltInMethod method, Type returnType, String dateFormat) {
virtualMethods.add( new VirtualMappingMethod( method ) );
ConversionContext ctx = new DefaultConversionContext( typeFactory, returnType, dateFormat );
return new MethodReference( method, ctx );
}
/**
* 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:

View File

@ -0,0 +1,302 @@
/**
* 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;
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.util.Elements;
import javax.lang.model.util.Types;
import javax.tools.Diagnostic.Kind;
import org.mapstruct.ap.conversion.ConversionProvider;
import org.mapstruct.ap.conversion.Conversions;
import org.mapstruct.ap.model.MapperReference;
import org.mapstruct.ap.model.MethodReference;
import org.mapstruct.ap.model.TypeConversion;
import org.mapstruct.ap.model.VirtualMappingMethod;
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;
/**
* Resolves class is responsible for resolving the most suitable way to resolve a mapping from source to target.
*
* There are 2 basic types of mappings:
* <ul>
* <li>conversions</li>
* <li>methods</li>
* </ul>
*
* conversions are essentially one line mappings, such as String to Integer and Integer to Long
* methods come in some varieties:
* <ul>
* <li>referenced mapping methods, these are methods implemented (or referenced) by the user. Sometimes indicated
* with the 'uses' in the mapping annotations or part of the abstract mapper class</li>
* <li>generated mapping methods (by means of MapStruct)</li>
* <li>built in methods</li>
* </ul>
*
* @author Sjaak Derksen
*/
public class MappingMethodResolver {
private final Messager messager;
private final TypeFactory typeFactory;
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> virtualMethods;
public MappingMethodResolver(Messager messager, TypeFactory typeFactory, Elements elementUtils, Types typeUtils) {
this.messager = messager;
this.typeFactory = typeFactory;
this.conversions = new Conversions( elementUtils, typeFactory );
this.builtInMethods = new BuiltInMappingMethods( typeFactory );
this.virtualMethods = new HashSet<VirtualMappingMethod>();
this.methodSelectors = new MethodSelectors( typeUtils, typeFactory );
}
public TypeConversion getConversion(Type sourceType, Type targetType, String dateFormat, String sourceReference) {
ConversionProvider conversionProvider = conversions.getConversion( sourceType, targetType );
if ( conversionProvider == null ) {
return null;
}
return conversionProvider.to(
sourceReference,
new DefaultConversionContext( typeFactory, targetType, dateFormat )
);
}
/**
* Returns a reference to a method mapping the given source type to the given target type, if such a method exists.
*
* @param mappingMethod target mapping method
* @param mappedElement used for error messages
* @param mapperReferences list of references to mapper
* @param methods list of candidate methods
* @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
*
* @return a method reference.
*/
public MethodReference getMappingMethodReferenceBasedOnMethod(SourceMethod mappingMethod,
String mappedElement,
List<MapperReference> mapperReferences,
List<SourceMethod> methods,
Type sourceType,
Type targetType,
String targetPropertyName,
String dateFormat) {
// first try to find a matching source method
SourceMethod matchingSourceMethod = getBestMatch(
mappingMethod,
mappedElement,
methods,
sourceType,
targetType,
targetPropertyName
);
if ( matchingSourceMethod != null ) {
return getMappingMethodReference( matchingSourceMethod, mapperReferences, targetType );
}
// then a matching built-in method
BuiltInMethod matchingBuiltInMethod = getBestMatch(
mappingMethod,
mappedElement,
builtInMethods.getBuiltInMethods(),
sourceType,
targetType,
targetPropertyName
);
return matchingBuiltInMethod != null ?
getMappingMethodReference( matchingBuiltInMethod, targetType, dateFormat ) : 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 ) )
*
* @param mappingMethod target mapping method
* @param mappedElement used for error messages
* @param mapperReferences list of references to mapper
* @param methods list of candidate methods
* @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
*
* @return a method reference.
*/
public MethodReference getMappingMethodReferenceBasedOnParameter(SourceMethod mappingMethod,
String mappedElement,
List<MapperReference> mapperReferences,
List<SourceMethod> methods,
Type sourceType,
Type targetType,
String targetPropertyName,
String dateFormat) {
List<Method> methodYCandidates = new ArrayList<Method>( methods );
methodYCandidates.addAll( builtInMethods.getBuiltInMethods() );
MethodReference 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 = getMappingMethodReferenceBasedOnMethod(
mappingMethod,
mappedElement,
mapperReferences,
methods,
methodYCandidate.getSourceParameters().get( 0 ).getType(),
targetType,
targetPropertyName,
dateFormat
);
if ( methodRefY != null ) {
MethodReference methodRefX = getMappingMethodReferenceBasedOnMethod(
mappingMethod,
mappedElement,
mapperReferences,
methods,
sourceType,
methodYCandidate.getSourceParameters().get( 0 ).getType(),
targetPropertyName,
dateFormat
);
if ( methodRefX != null ) {
methodRefY.setMethodRefChild( methodRefX );
break;
}
else {
// both should match;
methodRefY = null;
}
}
}
}
return methodRefY;
}
private <T extends Method> T getBestMatch(SourceMethod mappingMethod,
String mappedElement,
List<T> methods,
Type sourceType,
Type returnType,
String targetPropertyName) {
List<T> candidates = methodSelectors.getMatchingMethods(
mappingMethod,
methods,
sourceType,
returnType,
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 ) {
messager.printMessage(
Kind.ERROR,
String.format(
"Ambiguous mapping methods found for mapping " + mappedElement + " from %s to %s: %s.",
sourceType,
returnType,
Strings.join( candidates, ", " )
),
mappingMethod.getExecutable()
);
}
if ( !candidates.isEmpty() ) {
return candidates.get( 0 );
}
return null;
}
public Set<VirtualMappingMethod> getVirtualMethodsToCreate() {
return virtualMethods;
}
private MethodReference getMappingMethodReference(SourceMethod method, List<MapperReference> mapperReferences,
Type targetType) {
MapperReference mapperReference = findMapperReference( mapperReferences, method );
return new MethodReference(
method,
mapperReference,
SourceMethod.containsTargetTypeParameter( method.getParameters() ) ? targetType : null
);
}
private MapperReference findMapperReference(List<MapperReference> mapperReferences, SourceMethod method) {
for ( MapperReference ref : mapperReferences ) {
if ( ref.getType().equals( method.getDeclaringMapper() ) ) {
return ref;
}
}
return null;
}
private MethodReference getMappingMethodReference(BuiltInMethod method, Type returnType, String dateFormat) {
virtualMethods.add( new VirtualMappingMethod( method ) );
ConversionContext ctx = new DefaultConversionContext( typeFactory, returnType, dateFormat );
return new MethodReference( method, ctx );
}
}