/**
* 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.model;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import javax.lang.model.element.ExecutableElement;
import javax.tools.Diagnostic;
import org.mapstruct.CollectionMappingStrategy;
import org.mapstruct.ap.model.PropertyMapping.ConstantMappingBuilder;
import org.mapstruct.ap.model.PropertyMapping.JavaExpressionMappingBuilder;
import org.mapstruct.ap.model.PropertyMapping.PropertyMappingBuilder;
import org.mapstruct.ap.model.common.Parameter;
import org.mapstruct.ap.model.common.Type;
import org.mapstruct.ap.model.source.Mapping;
import org.mapstruct.ap.model.source.SourceMethod;
import org.mapstruct.ap.model.source.SourceReference;
import org.mapstruct.ap.option.ReportingPolicy;
import org.mapstruct.ap.util.Executables;
import org.mapstruct.ap.util.MapperConfig;
import org.mapstruct.ap.util.Strings;
/**
* A {@link MappingMethod} implemented by a {@link Mapper} class which maps one bean type to another, optionally
* configured by one or more {@link PropertyMapping}s.
*
* @author Gunnar Morling
* @author Sjaak Derksen
*/
public class BeanMappingMethod extends MappingMethod {
private final List<PropertyMapping> propertyMappings;
private final Map<String, List<PropertyMapping>> mappingsByParameter;
private final List<PropertyMapping> constantMappings;
private final MethodReference factoryMethod;
public static class Builder {
private MappingBuilderContext ctx;
private SourceMethod method;
private Map<String, ExecutableElement> unprocessedTargetProperties;
private final List<PropertyMapping> propertyMappings = new ArrayList<PropertyMapping>();
public Builder mappingContext(MappingBuilderContext mappingContext) {
this.ctx = mappingContext;
return this;
}
public Builder souceMethod(SourceMethod sourceMethod) {
this.method = sourceMethod;
this.unprocessedTargetProperties = initTargetPropertyAccessors();
return this;
}
public BeanMappingMethod build() {
// map properties with mapping
boolean mappingErrorOccured = handleDefinedSourceMappings();
if ( mappingErrorOccured ) {
return null;
}
// map properties without a mapping
applyPropertyNameBasedMapping();
// report errors on unmapped properties
reportErrorForUnmappedTargetPropertiesIfRequired();
MethodReference factoryMethod = AssignmentFactory.createFactoryMethod( method.getReturnType(), ctx );
return new BeanMappingMethod( method, propertyMappings, factoryMethod );
}
/**
* This method builds the list of target accessors.
*/
private Map<String, ExecutableElement> initTargetPropertyAccessors() {
// fetch settings from element to implement
CollectionMappingStrategy cmStrategy = getEffectiveCollectionMappingStrategy();
// collect all candidate target accessors
List<ExecutableElement> candidates = new ArrayList<ExecutableElement>();
candidates.addAll( method.getResultType().getSetters() );
candidates.addAll( method.getResultType().getAlternativeTargetAccessors() );
Map<String, ExecutableElement> targetProperties = new HashMap<String, ExecutableElement>();
for ( ExecutableElement candidate : candidates ) {
String targetPropertyName = Executables.getPropertyName( candidate );
// A target access is in general a setter method on the target object. However, in case of collections,
// the current target accessor can also be a getter method.
// The following if block, checks if the target accessor should be overruled by an add method.
if ( cmStrategy == CollectionMappingStrategy.SETTER_PREFERRED
|| cmStrategy == CollectionMappingStrategy.ADDER_PREFERRED ) {
// first check if there's a setter method.
ExecutableElement adderMethod = null;
if ( Executables.isSetterMethod( candidate ) ) {
Type targetType = ctx.getTypeFactory().getSingleParameter( candidate ).getType();
// ok, the current accessor is a setter. So now the strategy determines what to use
if ( cmStrategy == CollectionMappingStrategy.ADDER_PREFERRED ) {
adderMethod = method.getResultType().getAdderForType( targetType, targetPropertyName );
}
}
else if ( Executables.isGetterMethod( candidate ) ) {
// the current accessor is a getter (no setter available). But still, an add method is according
// to the above strategy (SETTER_PREFERRED || ADDER_PREFERRED) preferred over the getter.
Type targetType = ctx.getTypeFactory().getReturnType( candidate );
adderMethod = method.getResultType().getAdderForType( targetType, targetPropertyName );
}
if ( adderMethod != null ) {
// an adder has been found (according strategy) so overrule current choice.
candidate = adderMethod;
}
}
targetProperties.put( targetPropertyName, candidate );
}
return targetProperties;
}
/**
* Iterates over all defined mapping methods ({@code @Mapping(s)}), either directly given or inherited from the
* inverse mapping method.
* <p>
* If a match is found between a defined source (constant, expression, ignore or source) the mapping is removed
* from the remaining target properties.
* <p>
* It is furthermore checked whether the given mappings are correct. When an error occurs, the method continues
* in search of more problems.
*/
private boolean handleDefinedSourceMappings() {
boolean errorOccurred = false;
Set<String> handledTargets = new HashSet<String>();
for ( Map.Entry<String, List<Mapping>> entry : method.getMappings().entrySet() ) {
for ( Mapping mapping : entry.getValue() ) {
PropertyMapping propertyMapping = null;
// fetch the target property
ExecutableElement targetProperty = unprocessedTargetProperties.get( mapping.getTargetName() );
if ( targetProperty == null ) {
ctx.getMessager().printMessage(
Diagnostic.Kind.ERROR,
String.format( "Unknown property \"%s\" in return type.",
mapping.getTargetName()
),
method.getExecutable(),
mapping.getMirror(),
mapping.getSourceAnnotationValue()
);
errorOccurred = true;
}
// check the mapping options
// its an ignored property mapping
if ( mapping.isIgnored() ) {
propertyMapping = null;
handledTargets.add( mapping.getTargetName() );
}
// its a plain-old property mapping
else if ( mapping.getSourceName() != null ) {
// determine source parameter
SourceReference sourceRef = mapping.getSourceReference();
if ( sourceRef.isValid() ) {
if ( targetProperty != null ) {
// targetProperty == null can occur: we arrived here because we want as many errors
// as possible before we stop analysing
propertyMapping = new PropertyMappingBuilder()
.mappingContext( ctx )
.souceMethod( method )
.targetAccessor( targetProperty )
.targetPropertyName( mapping.getTargetName() )
.sourceReference( sourceRef )
.qualifiers( mapping.getQualifiers() )
.dateFormat( mapping.getDateFormat() )
.build();
handledTargets.add( mapping.getTargetName() );
}
}
else {
errorOccurred = true;
}
}
// its a constant
else if ( mapping.getConstant() != null && targetProperty != null ) {
propertyMapping = new ConstantMappingBuilder()
.mappingContext( ctx )
.sourceMethod( method )
.constantExpression( "\"" + mapping.getConstant() + "\"" )
.targetAccessor( targetProperty )
.dateFormat( mapping.getDateFormat() )
.qualifiers( mapping.getQualifiers() )
.build();
handledTargets.add( mapping.getTargetName() );
}
// its an expression
else if ( mapping.getJavaExpression() != null && targetProperty != null ) {
propertyMapping = new JavaExpressionMappingBuilder()
.mappingContext( ctx )
.souceMethod( method )
.javaExpression( mapping.getJavaExpression() )
.targetAccessor( targetProperty )
.build();
handledTargets.add( mapping.getTargetName() );
}
// remaining are the mappings without a 'source' so, 'only' a date format or qualifiers
if ( propertyMapping != null ) {
propertyMappings.add( propertyMapping );
}
}
}
for ( String handledTarget : handledTargets ) {
// In order to avoid: "Unknown property foo in return type" in case of duplicate
// target mappings
unprocessedTargetProperties.remove( handledTarget );
}
return errorOccurred;
}
/**
* Iterates over all target properties and all source parameters.
* <p>
* When a property name match occurs, the remainder will be checked for duplicates. Matches will be removed from
* the set of remaining target properties.
*/
private void applyPropertyNameBasedMapping() {
Iterator<Entry<String, ExecutableElement>> targetProperties =
unprocessedTargetProperties.entrySet().iterator();
// usually there should be only one getter; only for Boolean there may be two: isFoo() and getFoo()
List<ExecutableElement> candidates = new ArrayList<ExecutableElement>( 2 );
while ( targetProperties.hasNext() ) {
Entry<String, ExecutableElement> targetProperty = targetProperties.next();
PropertyMapping propertyMapping = null;
if ( propertyMapping == null ) {
for ( Parameter sourceParameter : method.getSourceParameters() ) {
for ( ExecutableElement sourceAccessor : sourceParameter.getType().getGetters() ) {
String sourcePropertyName = Executables.getPropertyName( sourceAccessor );
if ( sourcePropertyName.equals( targetProperty.getKey() ) ) {
candidates.add( sourceAccessor );
}
}
PropertyMapping newPropertyMapping = null;
ExecutableElement sourceAccessor = getSourceAccessor( targetProperty.getKey(), candidates );
if ( sourceAccessor != null ) {
Mapping mapping = method.getSingleMappingByTargetPropertyName( targetProperty.getKey() );
SourceReference sourceRef = new SourceReference.BuilderFromProperty()
.sourceParameter( sourceParameter )
.type( ctx.getTypeFactory().getReturnType( sourceAccessor ) )
.accessor( sourceAccessor )
.name( targetProperty.getKey() )
.build();
newPropertyMapping = 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();
// candidates are handled
candidates.clear();
}
if ( propertyMapping != null && newPropertyMapping != null ) {
// TODO improve error message
ctx.getMessager().printMessage(
Diagnostic.Kind.ERROR,
"Several possible source properties for target property \""
+ targetProperty.getKey()
+ "\".",
method.getExecutable()
);
break;
}
else if ( newPropertyMapping != null ) {
propertyMapping = newPropertyMapping;
}
}
}
if ( propertyMapping != null ) {
propertyMappings.add( propertyMapping );
targetProperties.remove();
}
}
}
private ExecutableElement getSourceAccessor(String sourcePropertyName, List<ExecutableElement> candidates) {
if ( candidates.isEmpty() ) {
return null;
}
else if ( candidates.size() == 1 ) {
return candidates.get( 0 );
}
// can only be the case for Booleans: isFoo() and getFoo(); The latter is preferred then
else if ( candidates.size() == 2 ) {
if ( candidates.get( 0 ).getSimpleName().toString().startsWith( "get" ) ) {
return candidates.get( 0 );
}
else {
return candidates.get( 1 );
}
}
// Should never really happen
else {
ctx.getMessager().printMessage(
Diagnostic.Kind.ERROR,
String.format( "Found several matching getters for property \"%s\"", sourcePropertyName ),
method.getExecutable()
);
return null;
}
}
/**
* Returns the effective policy for reporting unmapped getReturnType properties. If explicitly set via
* {@code Mapper}, this value will be returned. Otherwise the value from the corresponding processor option will
* be returned. If that is not set either, the default value from {@code Mapper#unmappedTargetPolicy()} will be
* returned.
*
* @param element The type declaring the generated mapper type
*
* @return The effective policy for reporting unmapped target properties.
*/
private ReportingPolicy getEffectiveUnmappedTargetPolicy() {
MapperConfig mapperSettings = MapperConfig.getInstanceOn( ctx.getMapperTypeElement() );
boolean setViaAnnotation = mapperSettings.isSetUnmappedTargetPolicy();
ReportingPolicy annotationValue = ReportingPolicy.valueOf( mapperSettings.unmappedTargetPolicy() );
if ( setViaAnnotation
|| ctx.getOptions().getUnmappedTargetPolicy() == null ) {
return annotationValue;
}
else {
return ctx.getOptions().getUnmappedTargetPolicy();
}
}
private CollectionMappingStrategy getEffectiveCollectionMappingStrategy() {
MapperConfig mapperSettings = MapperConfig.getInstanceOn( ctx.getMapperTypeElement() );
return mapperSettings.getCollectionMappingStrategy();
}
private void reportErrorForUnmappedTargetPropertiesIfRequired( ) {
// fetch settings from element to implement
ReportingPolicy unmappedTargetPolicy = getEffectiveUnmappedTargetPolicy();
if ( !unprocessedTargetProperties.isEmpty() && unmappedTargetPolicy.requiresReport() ) {
ctx.getMessager().printMessage(
unmappedTargetPolicy.getDiagnosticKind(),
MessageFormat.format(
"Unmapped target {0,choice,1#property|1<properties}: \"{1}\"",
unprocessedTargetProperties.size(),
Strings.join( unprocessedTargetProperties.keySet(), ", " )
),
method.getExecutable()
);
}
}
}
private BeanMappingMethod(SourceMethod method,
List<PropertyMapping> propertyMappings,
MethodReference factoryMethod) {
super( method );
this.propertyMappings = propertyMappings;
// intialize constant mappings as all mappings, but take out the ones that can be contributed to a
// parameter mapping.
this.mappingsByParameter = new HashMap<String, List<PropertyMapping>>();
this.constantMappings = new ArrayList<PropertyMapping>( propertyMappings );
for ( Parameter sourceParameter : getSourceParameters() ) {
ArrayList<PropertyMapping> mappingsOfParameter = new ArrayList<PropertyMapping>();
mappingsByParameter.put( sourceParameter.getName(), mappingsOfParameter );
for ( PropertyMapping mapping : propertyMappings ) {
if ( sourceParameter.getName().equals( mapping.getSourceBeanName() ) ) {
mappingsOfParameter.add( mapping );
constantMappings.remove( mapping );
}
}
}
this.factoryMethod = factoryMethod;
}
public List<PropertyMapping> getPropertyMappings() {
return propertyMappings;
}
public List<PropertyMapping> getConstantMappings() {
return constantMappings;
}
public Map<String, List<PropertyMapping>> getPropertyMappingsByParameter() {
return mappingsByParameter;
}
@Override
public Set<Type> getImportTypes() {
Set<Type> types = super.getImportTypes();
for ( PropertyMapping propertyMapping : propertyMappings ) {
types.addAll( propertyMapping.getImportTypes() );
}
return types;
}
public MethodReference getFactoryMethod() {
return this.factoryMethod;
}
}