#134 adding nested method match functionality including UT after merge with #135

This commit is contained in:
sjaakd 2014-02-27 23:11:11 +01:00
parent a9e1438c1e
commit 27c85d8f97
9 changed files with 543 additions and 26 deletions

View File

@ -34,6 +34,7 @@ import org.mapstruct.ap.model.source.builtin.BuiltInMethod;
public class MethodReference extends MappingMethod {
private final MapperReference declaringMapper;
private MethodReference methodRefChild;
/**
* In case this reference targets a built-in method, allows to pass specific context information to the invoked
@ -69,4 +70,21 @@ public class MethodReference extends MappingMethod {
public String getContextParam() {
return contextParam;
}
public void setMethodRefChild( MethodReference methodRefChild ) {
this.methodRefChild = methodRefChild;
}
public MethodReference getMethodRefChild() {
return methodRefChild;
}
@Override
public Set<Type> getImportTypes() {
Set<Type> imported = super.getImportTypes();
if (methodRefChild != null) {
imported.addAll( methodRefChild.getImportTypes() );
}
return imported;
}
}

View File

@ -598,7 +598,7 @@ public class MapperCreationProcessor implements ModelElementProcessor<List<Sourc
String targetPropertyName = Executables.getPropertyName( targetAcessor );
String mappedElement = "property '" + Executables.getPropertyName( sourceAccessor ) + "'";
MethodReference mappingMethodReference = getMappingMethodReference(
MethodReference mappingMethodReference = getMappingMethodReferenceBasedOnMethod(
method,
mappedElement,
mapperReferences,
@ -628,11 +628,45 @@ public class MapperCreationProcessor implements ModelElementProcessor<List<Sourc
conversion
);
reportErrorIfPropertyCanNotBeMapped(
method,
property
);
if (!isPropertyMappable( property ) ) {
// when not mappable, try again with another property mapping method based on parameter only.
mappingMethodReference = getMappingMethodReferenceBasedOnParameter(
method,
"property '" + Executables.getPropertyName( sourceAccessor ) + "'",
mapperReferences,
methods,
sourceType,
targetType,
targetPropertyName,
dateFormat );
property = new PropertyMapping(
parameter.getName(),
Executables.getPropertyName( sourceAccessor ),
sourceAccessor.getSimpleName().toString(),
sourceType,
Executables.getPropertyName( targetAcessor ),
targetAcessor.getSimpleName().toString(),
targetType,
mappingMethodReference,
conversion
);
}
if ( !isPropertyMappable( property ) ) {
messager.printMessage(
Kind.ERROR,
String.format(
"Can't map property \"%s %s\" to \"%s %s\".",
property.getSourceType(),
property.getSourceName(),
property.getTargetType(),
property.getTargetName()
),
method.getExecutable()
);
}
return property;
}
@ -653,7 +687,7 @@ public class MapperCreationProcessor implements ModelElementProcessor<List<Sourc
)
);
MethodReference mappingMethodReference = getMappingMethodReference(
MethodReference mappingMethodReference = getMappingMethodReferenceBasedOnMethod(
method,
"collection element",
mapperReferences,
@ -664,6 +698,20 @@ public class MapperCreationProcessor implements ModelElementProcessor<List<Sourc
dateFormat
);
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(
method,
"collection element",
mapperReferences,
methods,
sourceElementType,
targetElementType,
null, // there is no targetPropertyName
dateFormat );
}
if ( !sourceElementType.isAssignableTo( targetElementType ) && conversion == null &&
mappingMethodReference == null ) {
messager.printMessage(
@ -696,7 +744,7 @@ public class MapperCreationProcessor implements ModelElementProcessor<List<Sourc
Type keyTargetType = resultTypeParams.get( 0 );
String keyDateFormat = method.getMapMapping() != null ? method.getMapMapping().getKeyFormat() : null;
MethodReference keyMappingMethod = getMappingMethodReference(
MethodReference keyMappingMethod = getMappingMethodReferenceBasedOnMethod(
method,
"map key",
mapperReferences,
@ -708,6 +756,19 @@ public class MapperCreationProcessor implements ModelElementProcessor<List<Sourc
);
TypeConversion keyConversion = 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(
method,
"map key",
mapperReferences,
methods,
keySourceType,
keyTargetType,
null, // there is no targetPropertyName
keyDateFormat );
}
if ( !keySourceType.isAssignableTo( keyTargetType ) && keyConversion == null && keyMappingMethod == null ) {
messager.printMessage(
Kind.ERROR,
@ -725,7 +786,7 @@ public class MapperCreationProcessor implements ModelElementProcessor<List<Sourc
Type valueTargetType = resultTypeParams.get( 1 );
String valueDateFormat = method.getMapMapping() != null ? method.getMapMapping().getValueFormat() : null;
MethodReference valueMappingMethod = getMappingMethodReference(
MethodReference valueMappingMethod = getMappingMethodReferenceBasedOnMethod(
method,
"map value",
mapperReferences,
@ -742,6 +803,20 @@ public class MapperCreationProcessor implements ModelElementProcessor<List<Sourc
"entry.getValue()"
);
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(
method,
"map value",
mapperReferences,
methods,
valueSourceType,
valueTargetType,
null, // there is no targetPropertyName
valueDateFormat );
}
if ( !valueSourceType.isAssignableTo( valueTargetType ) && valueConversion == null &&
valueMappingMethod == null ) {
messager.printMessage(
@ -915,7 +990,7 @@ public class MapperCreationProcessor implements ModelElementProcessor<List<Sourc
/**
* Returns a reference to a method mapping the given source type to the given target type, if such a method exists.
*/
private MethodReference getMappingMethodReference(SourceMethod method,
private MethodReference getMappingMethodReferenceBasedOnMethod(SourceMethod method,
String mappedElement,
List<MapperReference> mapperReferences,
List<SourceMethod> methods,
@ -951,6 +1026,80 @@ public class MapperCreationProcessor implements ModelElementProcessor<List<Sourc
getMappingMethodReference( matchingBuiltInMethod, targetType, dateFormat ) : null;
}
/**
* Suppose mapping required from A to C and:
* <ul>
* <li>no direct referenced mapping method either BuiltIn or Referenced is avaliable 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.getParameters().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,
@ -1005,13 +1154,12 @@ public class MapperCreationProcessor implements ModelElementProcessor<List<Sourc
return new MethodReference( method, ctx );
}
/**
* Reports an error if source the property can't be mapped from source to target. A mapping if possible if one of
/**
* Returns false if source the property can't be mapped from source to target. 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 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>
@ -1019,8 +1167,9 @@ public class MapperCreationProcessor implements ModelElementProcessor<List<Sourc
*
* @param method The mapping method owning the property mapping.
* @param property The property mapping to check.
* @return false if property cannot be mapped
*/
private void reportErrorIfPropertyCanNotBeMapped(SourceMethod method, PropertyMapping property) {
private boolean isPropertyMappable(PropertyMapping property) {
boolean collectionOrMapTargetTypeHasCompatibleConstructor = false;
if ( property.getSourceType().isCollectionType() && property.getTargetType().isCollectionType() ) {
@ -1044,20 +1193,10 @@ public class MapperCreationProcessor implements ModelElementProcessor<List<Sourc
property.getConversion() != null ||
( ( property.getTargetType().isCollectionType() || property.getTargetType().isMapType() ) &&
collectionOrMapTargetTypeHasCompatibleConstructor ) ) {
return;
return true;
}
messager.printMessage(
Kind.ERROR,
String.format(
"Can't map property \"%s %s\" to \"%s %s\".",
property.getSourceType(),
property.getSourceName(),
property.getTargetType(),
property.getTargetName()
),
method.getExecutable()
);
return false;
}
/**

View File

@ -18,4 +18,4 @@
limitations under the License.
-->
<#if declaringMapper??>${mapperVariableName}.</#if>${name}<#if ext.input??>( ${ext.input}<#if contextParam??>, ${contextParam}</#if> )<#else>()</#if>
<#if declaringMapper??>${mapperVariableName}.</#if>${name}<#if ext.input??>( <#if methodRefChild??><@includeModel object=methodRefChild input=ext.input/><#if contextParam??>, ${contextParam}</#if><#else>${ext.input}<#if contextParam??>, ${contextParam}</#if></#if> )<#else>()</#if>

View File

@ -0,0 +1,99 @@
/**
* 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.test.nestedmethodcall;
import java.util.ArrayList;
import java.util.List;
import javax.xml.bind.JAXBElement;
import javax.xml.datatype.DatatypeConfigurationException;
import javax.xml.datatype.DatatypeFactory;
import javax.xml.datatype.XMLGregorianCalendar;
import javax.xml.namespace.QName;
import static org.fest.assertions.Assertions.assertThat;
import org.mapstruct.ap.testutil.IssueKey;
import org.mapstruct.ap.testutil.MapperTestBase;
import org.mapstruct.ap.testutil.WithClasses;
import org.testng.annotations.Test;
/**
* @author Sjaak Derksen
*
*/
@IssueKey( "134" )
@WithClasses( {
SourceTargetMapper.class,
OrderDto.class,
OrderDetailsDto.class,
OrderDetailsType.class,
OrderType.class
} )
public class MapperTest extends MapperTestBase {
private static final QName QNAME = new QName("dont-care");
@Test
public void referencedMappersAreInstatiatedCorrectly() throws DatatypeConfigurationException {
SourceTargetMapper instance = SourceTargetMapper.INSTANCE;
OrderDto target = instance.sourceToTarget( createOrderType() );
assertThat( target ).isNotNull();
assertThat( target.getOrderNumber() ).isEqualTo( 5L );
assertThat( target.getDates().size() ).isEqualTo( 2 );
assertThat( target.getDates().get( 0 ) ).isEqualTo( "02.03.1999" );
assertThat( target.getDates().get( 1 ) ).isEqualTo( "28.07.2004" );
assertThat( target.getOrderDetails() ).isNotNull();
assertThat( target.getOrderDetails().getName() ).isEqualTo( "test" );
assertThat( target.getOrderDetails().getDescription() ).isNotNull();
assertThat( target.getOrderDetails().getDescription().size() ).isEqualTo( 2 );
assertThat( target.getOrderDetails().getDescription().get( 0 ) ).isEqualTo( "elem1" );
assertThat( target.getOrderDetails().getDescription().get( 1 ) ).isEqualTo( "elem2" );
}
private OrderType createOrderType() throws DatatypeConfigurationException {
List<JAXBElement<XMLGregorianCalendar>> dates = new ArrayList<JAXBElement<XMLGregorianCalendar>>();
dates.add( new JAXBElement(QNAME, XMLGregorianCalendar.class, createXmlCal( 1999, 3, 2, 1 ) ) );
dates.add( new JAXBElement(QNAME, XMLGregorianCalendar.class, createXmlCal( 2004, 7, 29, 3 ) ) );
List<JAXBElement<String>> description = new ArrayList<JAXBElement<String>>();
description.add( new JAXBElement(QNAME, String.class, "elem1" ) );
description.add( new JAXBElement(QNAME, String.class, "elem2" ) );
OrderType orderType = new OrderType();
orderType.setOrderNumber( new JAXBElement(QNAME, Long.class, 5L ) );
orderType.setOrderDetails( new JAXBElement(QNAME, OrderDetailsType.class, new OrderDetailsType() ) );
orderType.getOrderDetails().getValue().setName( new JAXBElement(QNAME, String.class, "test" ) );
orderType.getOrderDetails().getValue().setDescription( description );
orderType.setDates( dates );
return orderType;
}
private XMLGregorianCalendar createXmlCal( int year, int month, int day, int tz )
throws DatatypeConfigurationException {
return DatatypeFactory.newInstance().newXMLGregorianCalendarDate( year, month, day, tz );
}
}

View File

@ -0,0 +1,48 @@
/**
* 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.test.nestedmethodcall;
import java.util.List;
/**
*
* @author Sjaak Derksen
*/
public class OrderDetailsDto {
private String name;
private List<String> description;
public String getName() {
return name;
}
public void setName( String name ) {
this.name = name;
}
public List<String> getDescription() {
return description;
}
public void setDescription( List<String> description ) {
this.description = description;
}
}

View File

@ -0,0 +1,50 @@
/**
* 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.test.nestedmethodcall;
import java.util.List;
import javax.xml.bind.JAXBElement;
/**
*
* @author Sjaak Derksen
*/
public class OrderDetailsType {
private JAXBElement<String> name;
private List<JAXBElement<String>> description;
public JAXBElement<String> getName() {
return name;
}
public void setName(JAXBElement<String> value) {
this.name = value;
}
public void setDescription( List<JAXBElement<String>> description ) {
this.description = description;
}
public List<JAXBElement<String>> getDescription() {
return description;
}
}

View File

@ -0,0 +1,58 @@
/**
* 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.test.nestedmethodcall;
import java.util.List;
/**
*
* @author Sjaak Derksen
*/
public class OrderDto {
private Long orderNumber;
private OrderDetailsDto orderDetails;
private List<String> dates;
public Long getOrderNumber() {
return orderNumber;
}
public void setOrderNumber( Long orderNumber ) {
this.orderNumber = orderNumber;
}
public OrderDetailsDto getOrderDetails() {
return orderDetails;
}
public void setOrderDetails( OrderDetailsDto orderDetails ) {
this.orderDetails = orderDetails;
}
public List<String> getDates() {
return dates;
}
public void setDates( List<String> dates ) {
this.dates = dates;
}
}

View File

@ -0,0 +1,61 @@
/**
* 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.test.nestedmethodcall;
import java.util.List;
import javax.xml.bind.JAXBElement;
import javax.xml.datatype.XMLGregorianCalendar;
/**
*
* @author Sjaak Derksen
*/
public class OrderType {
private JAXBElement<Long> orderNumber;
private JAXBElement<OrderDetailsType> orderDetails;
private List<JAXBElement<XMLGregorianCalendar>> dates;
public JAXBElement<Long> getOrderNumber() {
return orderNumber;
}
public void setOrderNumber(JAXBElement<Long> value) {
this.orderNumber = value;
}
public JAXBElement<OrderDetailsType> getOrderDetails() {
return orderDetails;
}
public void setOrderDetails(JAXBElement<OrderDetailsType> value) {
this.orderDetails = value;
}
public List<JAXBElement<XMLGregorianCalendar>> getDates() {
return dates;
}
public void setDates( List<JAXBElement<XMLGregorianCalendar>> dates ) {
this.dates = dates;
}
}

View File

@ -0,0 +1,44 @@
/**
* 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.test.nestedmethodcall;
import java.util.List;
import javax.xml.bind.JAXBElement;
import javax.xml.datatype.XMLGregorianCalendar;
import org.mapstruct.IterableMapping;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
/**
*
* @author Sjaak Derksen
*/
@Mapper
public interface SourceTargetMapper {
SourceTargetMapper INSTANCE = Mappers.getMapper( SourceTargetMapper.class );
OrderDto sourceToTarget(OrderType source);
OrderDetailsDto detailsToDto(OrderDetailsType source);
@IterableMapping(dateFormat = "dd.MM.yyyy")
List<String> stringListToDateList(List<JAXBElement<XMLGregorianCalendar>> dates);
}