#157 introduction of conversionY( methodX ( parameter ) )

This commit is contained in:
sjaakd 2014-03-23 00:30:21 +01:00
parent 23f3d4a3c1
commit 01e918b3e0
7 changed files with 353 additions and 299 deletions

View File

@ -38,6 +38,12 @@ public class TypeConversion extends ModelElement {
private final String sourceReference; private final String sourceReference;
private final String openExpression; private final String openExpression;
private final String closeExpression; private final String closeExpression;
/**
* A reference to mapping method in case this is a two-step mapping, e.g. from
* {@code JAXBElement<Bar>} to {@code Foo} to for which a nested method call will be generated:
* {@code setFoo(barToFoo( jaxbElemToValue( bar) ) )}
*/
private MethodReference methodRefChild;
public TypeConversion( Set<Type> importTypes, public TypeConversion( Set<Type> importTypes,
List<Type> exceptionTypes, List<Type> exceptionTypes,
@ -72,4 +78,12 @@ public class TypeConversion extends ModelElement {
public String getCloseExpression() { public String getCloseExpression() {
return closeExpression; return closeExpression;
} }
public void setMethodRefChild( MethodReference methodRefChild ) {
this.methodRefChild = methodRefChild;
}
public MethodReference getMethodRefChild() {
return methodRefChild;
}
} }

View File

@ -111,7 +111,7 @@ public class MappingResolver {
* <li>null, no assignment found</li> * <li>null, no assignment found</li>
* </ol> * </ol>
*/ */
public TargetAssignment getTargetAssignment( SourceMethod mappingMethod, public TargetAssignment getTargetAssignment( SourceMethod mappingMethod,
String mappedElement, String mappedElement,
List<MapperReference> mapperReferences, List<MapperReference> mapperReferences,
List<SourceMethod> methods, List<SourceMethod> methods,
@ -121,282 +121,17 @@ public class MappingResolver {
String dateFormat, String dateFormat,
String sourceReference ) { String sourceReference ) {
MethodReference mappingMethodReference = resolveViaMethod( ResolvingAttempt attempt = new ResolvingAttempt( mappingMethod,
mappingMethod,
mappedElement, mappedElement,
mapperReferences, mapperReferences,
methods, methods,
sourceType,
targetType,
targetPropertyName, targetPropertyName,
dateFormat dateFormat,
sourceReference,
this
); );
TargetAssignment assignment = null; return attempt.getTargetAssignment( sourceType, targetType );
if (mappingMethodReference != null ) {
assignment = new TargetAssignment(mappingMethodReference );
}
else if (sourceType.isAssignableTo( targetType ) ) {
assignment = new TargetAssignment();
}
else {
TypeConversion conversion = resolveViaConversion( sourceType, targetType, dateFormat, sourceReference );
if ( conversion != null ) {
assignment = new TargetAssignment(conversion );
}
else {
mappingMethodReference = resolveViaMethodAndMethod(
mappingMethod,
mappedElement,
mapperReferences,
methods,
sourceType,
targetType,
targetPropertyName,
dateFormat
);
if ( mappingMethodReference != null ) {
assignment = new TargetAssignment( mappingMethodReference );
}
else {
mappingMethodReference = resolveViaConversionAndMethod(
mappingMethod,
mappedElement,
mapperReferences,
methods,
sourceType,
targetType,
targetPropertyName,
dateFormat,
sourceReference );
}
if ( mappingMethodReference != null ) {
assignment = new TargetAssignment( mappingMethodReference );
}
}
}
return assignment;
}
private TypeConversion resolveViaConversion( 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 resolveViaMethod( 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 ) )
*/
private MethodReference resolveViaMethodAndMethod( 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 = resolveViaMethod(
mappingMethod,
mappedElement,
mapperReferences,
methods,
methodYCandidate.getSourceParameters().get( 0 ).getType(),
targetType,
targetPropertyName,
dateFormat
);
if ( methodRefY != null ) {
MethodReference methodRefX = resolveViaMethod(
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;
}
/**
* 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 MethodReference resolveViaConversionAndMethod( SourceMethod mappingMethod,
String mappedElement,
List<MapperReference> mapperReferences,
List<SourceMethod> methods,
Type sourceType,
Type targetType,
String targetPropertyName,
String dateFormat,
String sourceReference ) {
List<Method> methodYCandidates = new ArrayList<Method>( methods );
methodYCandidates.addAll( builtInMethods.getBuiltInMethods() );
MethodReference methodRefY = null;
for ( Method methodYCandidate : methodYCandidates ) {
if ( methodYCandidate.getSourceParameters().size() == 1 ) {
methodRefY = resolveViaMethod(
mappingMethod,
mappedElement,
mapperReferences,
methods,
methodYCandidate.getSourceParameters().get( 0 ).getType(),
targetType,
targetPropertyName,
dateFormat
);
if ( methodRefY != null ) {
TypeConversion conversionXRef = resolveViaConversion(
sourceType,
methodYCandidate.getSourceParameters().get( 0 ).getType(),
dateFormat,
sourceReference
);
if ( conversionXRef != null ) {
methodRefY.setTypeConversionChild( conversionXRef );
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> getVirtualMethodsToGenerate() { public Set<VirtualMappingMethod> getVirtualMethodsToGenerate() {
@ -404,32 +139,309 @@ public class MappingResolver {
} }
private MethodReference getMappingMethodReference( SourceMethod method, private static class ResolvingAttempt {
List<MapperReference> mapperReferences,
Type targetType ) {
MapperReference mapperReference = findMapperReference( mapperReferences, method );
return new MethodReference( private final SourceMethod mappingMethod;
method, private final String mappedElement;
mapperReference, private final List<MapperReference> mapperReferences;
SourceMethod.containsTargetTypeParameter( method.getParameters() ) ? targetType : null private final List<SourceMethod> methods;
); private final String targetPropertyName;
} private final String dateFormat;
private final String sourceReference;
private final MappingResolver context;
private MapperReference findMapperReference(List<MapperReference> mapperReferences, SourceMethod method) { // resolving via 2 steps creates the possibillity of wrong matches, first builtin method matches,
for ( MapperReference ref : mapperReferences ) { // second doesn't. In that case, the first builtin method should not lead to a virtual method
if ( ref.getType().equals( method.getDeclaringMapper() ) ) { // so this set must be cleared.
return ref; private final Set<VirtualMappingMethod> virtualMethodCandidates;
}
private ResolvingAttempt( SourceMethod mappingMethod,
String mappedElement,
List<MapperReference> mapperReferences,
List<SourceMethod> methods,
String targetPropertyName,
String dateFormat,
String sourceReference,
MappingResolver context ) {
this.mappingMethod = mappingMethod;
this.mappedElement = mappedElement;
this.mapperReferences = mapperReferences;
this.methods = methods;
this.targetPropertyName = targetPropertyName;
this.dateFormat = dateFormat;
this.sourceReference = sourceReference;
this.context = context;
this.virtualMethodCandidates = new HashSet<VirtualMappingMethod>();
}
private TargetAssignment getTargetAssignment( Type sourceType, Type targetType ) {
// first simpele mapping method
MethodReference mappingMethodReference = resolveViaMethod( sourceType, targetType );
if ( mappingMethodReference != null ) {
context.virtualMethods.addAll( virtualMethodCandidates );
return new TargetAssignment( mappingMethodReference );
}
// then direct assignable
if ( sourceType.isAssignableTo( targetType ) ) {
return new TargetAssignment();
}
// then type conversion
TypeConversion conversion = resolveViaConversion( sourceType, targetType );
if ( conversion != null ) {
return new TargetAssignment( conversion );
}
// 2 step method, first: method(method(souurce))
mappingMethodReference = resolveViaMethodAndMethod( sourceType, targetType );
if ( mappingMethodReference != null ) {
context.virtualMethods.addAll( virtualMethodCandidates );
return new TargetAssignment( mappingMethodReference );
}
// 2 step method, then: method(conversion(souurce))
mappingMethodReference = resolveViaConversionAndMethod( sourceType, targetType );
if ( mappingMethodReference != null ) {
context.virtualMethods.addAll( virtualMethodCandidates );
return new TargetAssignment( mappingMethodReference );
}
// 2 step method, finally: conversion(method(souurce))
conversion = resolveViaMethodAndConversion( sourceType, targetType );
if ( conversion != null ) {
context.virtualMethods.addAll( virtualMethodCandidates );
return new TargetAssignment( conversion );
}
// if nothing works, alas, the result is null
return null;
}
private TypeConversion resolveViaConversion( Type sourceType, Type targetType ) {
ConversionProvider conversionProvider = context.conversions.getConversion( sourceType, targetType );
if ( conversionProvider == null ) {
return null;
}
return conversionProvider.to(
sourceReference,
new DefaultConversionContext( context.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 resolveViaMethod( Type sourceType, Type targetType ) {
// first try to find a matching source method
SourceMethod matchingSourceMethod = getBestMatch( methods, sourceType, targetType );
if ( matchingSourceMethod != null ) {
return getMappingMethodReference( matchingSourceMethod, mapperReferences, targetType );
}
// then a matching built-in method
BuiltInMethod matchingBuiltInMethod =
getBestMatch( context.builtInMethods.getBuiltInMethods(), sourceType, targetType );
if ( matchingBuiltInMethod != null ) {
virtualMethodCandidates.add( new VirtualMappingMethod( matchingBuiltInMethod ) );
ConversionContext ctx = new DefaultConversionContext( context.typeFactory, targetType, dateFormat );
return new MethodReference( matchingBuiltInMethod, ctx );
}
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 MethodReference resolveViaMethodAndMethod( Type sourceType, Type targetType ) {
List<Method> methodYCandidates = new ArrayList<Method>( methods );
methodYCandidates.addAll( context.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 = resolveViaMethod( methodYCandidate.getSourceParameters().get( 0 ).getType(),
targetType );
if ( methodRefY != null ) {
MethodReference methodRefX = resolveViaMethod(
sourceType,
methodYCandidate.getSourceParameters().get( 0 ).getType()
);
if ( methodRefX != null ) {
methodRefY.setMethodRefChild( methodRefX );
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 MethodReference resolveViaConversionAndMethod( Type sourceType, Type targetType ) {
List<Method> methodYCandidates = new ArrayList<Method>( methods );
methodYCandidates.addAll( context.builtInMethods.getBuiltInMethods() );
MethodReference methodRefY = null;
for ( Method methodYCandidate : methodYCandidates ) {
if ( methodYCandidate.getSourceParameters().size() == 1 ) {
methodRefY = resolveViaMethod(
methodYCandidate.getSourceParameters().get( 0 ).getType(),
targetType
);
if ( methodRefY != null ) {
TypeConversion conversionXRef = resolveViaConversion(
sourceType,
methodYCandidate.getSourceParameters().get( 0 ).getType()
);
if ( conversionXRef != null ) {
methodRefY.setTypeConversionChild( conversionXRef );
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 TypeConversion resolveViaMethodAndConversion( Type sourceType, Type targetType ) {
List<Method> methodXCandidates = new ArrayList<Method>( methods );
methodXCandidates.addAll( context.builtInMethods.getBuiltInMethods() );
TypeConversion conversionYRef = null;
// search the other way arround
for ( Method methodXCandidate : methodXCandidates ) {
if ( methodXCandidate.getSourceParameters().size() == 1 ) {
MethodReference methodRefX = resolveViaMethod(
sourceType,
methodXCandidate.getReturnType()
);
if ( methodRefX != null ) {
conversionYRef = resolveViaConversion(
methodXCandidate.getReturnType(),
targetType
);
if ( conversionYRef != null ) {
conversionYRef.setMethodRefChild( methodRefX );
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 = context.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 ) {
context.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() ) ) {
return ref;
}
}
return null;
} }
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 );
}
} }

View File

@ -32,7 +32,7 @@
<@includeModel object=methodRefChild source=ext.source targetType=singleSourceParameterType.name/> <@includeModel object=methodRefChild source=ext.source targetType=singleSourceParameterType.name/>
<#elseif typeConversion??> <#elseif typeConversion??>
<#-- the nested case: a type conversion --> <#-- the nested case: a type conversion -->
<@includeModel object=typeConversion/> <@includeModel object=typeConversion source=ext.source targetType=singleSourceParameterType.name/>
<#else> <#else>
<#-- the non nested case --> <#-- the non nested case -->
${ext.source} ${ext.source}

View File

@ -52,7 +52,7 @@
<#compress> <#compress>
<#switch assignmentType> <#switch assignmentType>
<#case "TYPE_CONVERSION"> <#case "TYPE_CONVERSION">
<@includeModel object=typeConversion/> <@includeModel object=typeConversion source="${ext.source}" targetType=ext.targetType/>
<#break> <#break>
<#case "METHOD_REFERENCE"> <#case "METHOD_REFERENCE">
<@includeModel object=methodReference source="${ext.source}" targetType=ext.targetType raw=ext.raw/> <@includeModel object=methodReference source="${ext.source}" targetType=ext.targetType raw=ext.raw/>

View File

@ -18,4 +18,10 @@
limitations under the License. limitations under the License.
--> -->
${openExpression}${sourceReference}${closeExpression} <#if methodRefChild??>
<#-- the nested case: mapping method -->
${openExpression}<@includeModel object=methodRefChild source=ext.source targetType=ext.targetType/>${closeExpression}
<#else>
<#-- the non nested case: a type conversion -->
${openExpression}${sourceReference}${closeExpression}
</#if>

View File

@ -83,6 +83,22 @@ public class NestedMappingMethodInvocationTest {
assertThat( target.getOrderDetails().getDescription() ).containsExactly( "elem1", "elem2" ); assertThat( target.getOrderDetails().getDescription() ).containsExactly( "elem1", "elem2" );
} }
@Test
@WithClasses( {
SourceTypeTargetDtoMapper.class,
SourceType.class,
ObjectFactory.class,
TargetDto.class
} )
public void shouldMapViaMethodAndConversion() throws DatatypeConfigurationException {
SourceTypeTargetDtoMapper instance = SourceTypeTargetDtoMapper.INSTANCE;
TargetDto target = instance.sourceToTarget( createSource() );
assertThat( target ).isNotNull();
assertThat( target.getDate() ).isEqualTo( new GregorianCalendar( 2013, 6, 6 ).getTime() );
}
@Test @Test
@WithClasses( { @WithClasses( {
SourceTypeTargetDtoMapper.class, SourceTypeTargetDtoMapper.class,
@ -143,6 +159,12 @@ public class NestedMappingMethodInvocationTest {
.newXMLGregorianCalendarDate( year, month, day, DatatypeConstants.FIELD_UNDEFINED ); .newXMLGregorianCalendarDate( year, month, day, DatatypeConstants.FIELD_UNDEFINED );
} }
private SourceType createSource() {
SourceType source = new SourceType();
source.setDate( new JAXBElement<String>( QNAME, String.class, "06.07.2013" ) );
return source;
}
private TargetDto createTarget() { private TargetDto createTarget() {
TargetDto target = new TargetDto(); TargetDto target = new TargetDto();
target.setDate( new GregorianCalendar( 2013, 6, 6 ).getTime() ); target.setDate( new GregorianCalendar( 2013, 6, 6 ).getTime() );

View File

@ -32,7 +32,7 @@ public interface SourceTypeTargetDtoMapper {
SourceTypeTargetDtoMapper INSTANCE = Mappers.getMapper( SourceTypeTargetDtoMapper.class ); SourceTypeTargetDtoMapper INSTANCE = Mappers.getMapper( SourceTypeTargetDtoMapper.class );
@Mapping(source = "date", target = "date", dateFormat = "dd.MM.yyyy") @Mapping(source = "date", target = "date", dateFormat = "dd.MM.yyyy")
TargetDto sourceToTarget(SourceType source);
SourceType targetToSource( TargetDto source ); SourceType targetToSource( TargetDto source );
// TargetDto sourceToTarget(SourceType source);
} }