#348 Mapping parameters based on name when other mappings fail

This commit is contained in:
sjaakd 2014-11-16 21:18:22 +01:00 committed by Gunnar Morling
parent 7d1c5c2f24
commit 7948acf630
8 changed files with 134 additions and 3 deletions

View File

@ -63,6 +63,7 @@ public @interface Mapping {
* This may either be a simple property name (e.g. "address") or a dot-separated property path (e.g. "address.city"
* or "address.city.name"). In case the annotated method has several source parameters, the property name must
* qualified with the parameter name, e.g. "addressParam.city".</li>
* <li>When no matching property is found, MapStruct looks for a matching parameter name instead.</li>
* <li>When used to map an enum constant, the name of the constant member is to be given.</li>
* </ol>
* Either this attribute or {@link #constant()} or {@link #expression()} may be specified for a given mapping.

View File

@ -61,6 +61,7 @@ public @interface Mapping {
* This may either be a simple property name (e.g. "address") or a dot-separated property path (e.g. "address.city"
* or "address.city.name"). In case the annotated method has several source parameters, the property name must
* qualified with the parameter name, e.g. "addressParam.city".</li>
* <li>When no matching property is found, MapStruct looks for a matching parameter name instead.</li>
* <li>When used to map an enum constant, the name of the constant member is to be given.</li>
* </ol>
* Either this attribute or {@link #constant()} or {@link #expression()} may be specified for a given mapping.

View File

@ -65,6 +65,7 @@ public class BeanMappingMethod extends MappingMethod {
private SourceMethod method;
private Map<String, ExecutableElement> unprocessedTargetProperties;
private final List<PropertyMapping> propertyMappings = new ArrayList<PropertyMapping>();
private final Set<Parameter> unprocessedSourceParameters = new HashSet<Parameter>();
public Builder mappingContext(MappingBuilderContext mappingContext) {
this.ctx = mappingContext;
@ -74,6 +75,9 @@ public class BeanMappingMethod extends MappingMethod {
public Builder souceMethod(SourceMethod sourceMethod) {
this.method = sourceMethod;
this.unprocessedTargetProperties = initTargetPropertyAccessors();
for ( Parameter sourceParameter : method.getSourceParameters() ) {
unprocessedSourceParameters.add( sourceParameter );
}
return this;
}
@ -87,6 +91,9 @@ public class BeanMappingMethod extends MappingMethod {
// map properties without a mapping
applyPropertyNameBasedMapping();
// map parameters without a mapping
applyParameterNameBasedMapping();
// report errors on unmapped properties
reportErrorForUnmappedTargetPropertiesIfRequired();
@ -207,6 +214,7 @@ public class BeanMappingMethod extends MappingMethod {
.dateFormat( mapping.getDateFormat() )
.build();
handledTargets.add( mapping.getTargetName() );
unprocessedSourceParameters.remove( sourceRef.getParameter() );
}
}
else {
@ -279,6 +287,10 @@ public class BeanMappingMethod extends MappingMethod {
for ( Parameter sourceParameter : method.getSourceParameters() ) {
if ( sourceParameter.getType().isPrimitive() ) {
continue;
}
for ( ExecutableElement sourceAccessor : sourceParameter.getType().getGetters() ) {
String sourcePropertyName = Executables.getPropertyName( sourceAccessor );
@ -309,6 +321,8 @@ public class BeanMappingMethod extends MappingMethod {
.dateFormat( mapping != null ? mapping.getDateFormat() : null )
.build();
unprocessedSourceParameters.remove( sourceParameter );
}
// candidates are handled
candidates.clear();
@ -337,6 +351,46 @@ public class BeanMappingMethod extends MappingMethod {
}
}
private void applyParameterNameBasedMapping() {
Iterator<Entry<String, ExecutableElement>> targetProperties =
unprocessedTargetProperties.entrySet().iterator();
while ( targetProperties.hasNext() ) {
Entry<String, ExecutableElement> targetProperty = targetProperties.next();
Iterator<Parameter> sourceParameters = unprocessedSourceParameters.iterator();
while ( sourceParameters.hasNext() ) {
Parameter sourceParameter = sourceParameters.next();
if ( sourceParameter.getName().equals( targetProperty.getKey() ) ) {
Mapping mapping = method.getSingleMappingByTargetPropertyName( targetProperty.getKey() );
SourceReference sourceRef = new SourceReference.BuilderFromProperty()
.sourceParameter( sourceParameter )
.name( targetProperty.getKey() )
.build();
PropertyMapping propertyMapping = new PropertyMappingBuilder()
.mappingContext( ctx )
.souceMethod( method )
.targetAccessor( targetProperty.getValue() )
.targetPropertyName( targetProperty.getKey() )
.sourceReference( sourceRef )
.qualifiers( mapping != null ? mapping.getQualifiers() : null )
.dateFormat( mapping != null ? mapping.getDateFormat() : null )
.build();
propertyMappings.add( propertyMapping );
targetProperties.remove();
sourceParameters.remove();
}
}
}
}
private ExecutableElement getSourceAccessor(String sourcePropertyName, List<ExecutableElement> candidates) {
if ( candidates.isEmpty() ) {
return null;
@ -462,6 +516,28 @@ public class BeanMappingMethod extends MappingMethod {
return types;
}
public List<Parameter> getSourceParametersExcludingPrimitives() {
List<Parameter> sourceParameters = new ArrayList<Parameter>();
for ( Parameter sourceParam : getSourceParameters() ) {
if (!sourceParam.getType().isPrimitive()) {
sourceParameters.add( sourceParam );
}
}
return sourceParameters;
}
public List<Parameter> getSourcePrimitiveParameters() {
List<Parameter> sourceParameters = new ArrayList<Parameter>();
for ( Parameter sourceParam : getSourceParameters() ) {
if (sourceParam.getType().isPrimitive()) {
sourceParameters.add( sourceParam );
}
}
return sourceParameters;
}
public MethodReference getFactoryMethod() {
return this.factoryMethod;
}

View File

@ -71,4 +71,27 @@ public class Parameter extends ModelElement {
public boolean isTargetType() {
return targetType;
}
@Override
public int hashCode() {
int hash = 5;
hash = 23 * hash + (this.name != null ? this.name.hashCode() : 0);
return hash;
}
@Override
public boolean equals(Object obj) {
if ( obj == null ) {
return false;
}
if ( getClass() != obj.getClass() ) {
return false;
}
final Parameter other = (Parameter) obj;
if ( (this.name == null) ? (other.name != null) : !this.name.equals( other.name ) ) {
return false;
}
return true;
}
}

View File

@ -234,7 +234,10 @@ public class SourceReference {
}
public SourceReference build() {
List<PropertyEntry> sourcePropertyEntries = Arrays.asList( new PropertyEntry( name, accessor, type ) );
List<PropertyEntry> sourcePropertyEntries = new ArrayList<PropertyEntry>();
if ( accessor != null ) {
sourcePropertyEntries.add( new PropertyEntry( name, accessor, type ) );
}
return new SourceReference( sourceParameter, sourcePropertyEntries, true );
}
}

View File

@ -20,13 +20,13 @@
-->
@Override
<#lt>${accessibility.keyword} <@includeModel object=returnType/> ${name}(<#list parameters as param><@includeModel object=param/><#if param_has_next>, </#if></#list>) <@throws/> {
if ( <#list sourceParameters as sourceParam>${sourceParam.name} == null<#if sourceParam_has_next> && </#if></#list> ) {
if ( <#list sourceParametersExcludingPrimitives as sourceParam>${sourceParam.name} == null<#if sourceParam_has_next> && </#if></#list> ) {
return<#if returnType.name != "void"> null</#if>;
}
<#if !existingInstanceMapping><@includeModel object=resultType/> ${resultName} = <#if factoryMethod??><@includeModel object=factoryMethod targetType=resultType raw=true/><#else>new <@includeModel object=resultType/>()</#if>;</#if>
<#if (sourceParameters?size > 1)>
<#list sourceParameters as sourceParam>
<#list sourceParametersExcludingPrimitives as sourceParam>
<#if (propertyMappingsByParameter[sourceParam.name]?size > 0)>
if ( ${sourceParam.name} != null ) {
<#list propertyMappingsByParameter[sourceParam.name] as propertyMapping>
@ -35,6 +35,13 @@
}
</#if>
</#list>
<#list sourcePrimitiveParameters as sourceParam>
<#if (propertyMappingsByParameter[sourceParam.name]?size > 0)>
<#list propertyMappingsByParameter[sourceParam.name] as propertyMapping>
<@includeModel object=propertyMapping targetBeanName=resultName existingInstanceMapping=existingInstanceMapping/>
</#list>
</#if>
</#list>
<#else>
<#list propertyMappingsByParameter[sourceParameters[0].name] as propertyMapping>
<@includeModel object=propertyMapping targetBeanName=resultName existingInstanceMapping=existingInstanceMapping/>

View File

@ -98,6 +98,21 @@ public class SeveralSourceParametersTest {
assertThat( deliveryAddress ).isNull();
}
@Test
@WithClasses({ Person.class, Address.class, DeliveryAddress.class, SourceTargetMapper.class })
public void shouldMapSeveralSourceAttributesAndParameters() {
Person person = new Person( "Bob", "Garner", 181, "An actor" );
DeliveryAddress deliveryAddress =
SourceTargetMapper.INSTANCE.personAndAddressToDeliveryAddress( person, 42, 12345, "Main street" );
assertThat( deliveryAddress.getLastName() ).isEqualTo( "Garner" );
assertThat( deliveryAddress.getZipCode() ).isEqualTo( 12345 );
assertThat( deliveryAddress.getHouseNumber() ).isEqualTo( 42 );
assertThat( deliveryAddress.getDescription() ).isEqualTo( "An actor" );
assertThat( deliveryAddress.getStreet()).isEqualTo( "Main street" );
}
@Test
@WithClasses({ ErroneousSourceTargetMapper.class, Address.class, DeliveryAddress.class })
@ProcessorOption(name = "mapstruct.unmappedTargetPolicy", value = "IGNORE")

View File

@ -41,4 +41,9 @@ public interface SourceTargetMapper {
})
void personAndAddressToDeliveryAddress(Person person, Address address,
@MappingTarget DeliveryAddress deliveryAddress);
@Mapping( target = "description", source = "person.description")
DeliveryAddress personAndAddressToDeliveryAddress(Person person, Integer houseNumber, int zipCode,
String street);
}