/**
* Copyright 2012-2017 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.internal.model.source;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.AnnotationValue;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import org.mapstruct.ap.internal.model.common.FormattingParameters;
import org.mapstruct.ap.internal.model.common.Parameter;
import org.mapstruct.ap.internal.model.common.TypeFactory;
import org.mapstruct.ap.internal.prism.CollectionMappingStrategyPrism;
import org.mapstruct.ap.internal.prism.MappingPrism;
import org.mapstruct.ap.internal.prism.MappingsPrism;
import org.mapstruct.ap.internal.util.FormattingMessager;
import org.mapstruct.ap.internal.util.Message;
import org.mapstruct.ap.internal.util.Strings;
/**
* Represents a property mapping as configured via {@code @Mapping}.
*
* @author Gunnar Morling
*/
public class Mapping {
private static final Pattern JAVA_EXPRESSION = Pattern.compile( "^java\\((.*)\\)$" );
private final String sourceName;
private final String constant;
private final String javaExpression;
private final String targetName;
private final String defaultValue;
private final FormattingParameters formattingParameters;
private final SelectionParameters selectionParameters;
private final boolean isIgnored;
private final List<String> dependsOn;
private final AnnotationMirror mirror;
private final AnnotationValue sourceAnnotationValue;
private final AnnotationValue targetAnnotationValue;
private final AnnotationValue dependsOnAnnotationValue;
private SourceReference sourceReference;
private TargetReference targetReference;
public static Map<String, List<Mapping>> fromMappingsPrism(MappingsPrism mappingsAnnotation,
ExecutableElement method,
FormattingMessager messager) {
Map<String, List<Mapping>> mappings = new HashMap<String, List<Mapping>>();
for ( MappingPrism mappingPrism : mappingsAnnotation.value() ) {
Mapping mapping = fromMappingPrism( mappingPrism, method, messager );
if ( mapping != null ) {
List<Mapping> mappingsOfProperty = mappings.get( mappingPrism.target() );
if ( mappingsOfProperty == null ) {
mappingsOfProperty = new ArrayList<Mapping>();
mappings.put( mappingPrism.target(), mappingsOfProperty );
}
mappingsOfProperty.add( mapping );
if ( mappingsOfProperty.size() > 1 && !isEnumType( method.getReturnType() ) ) {
messager.printMessage( method, Message.PROPERTYMAPPING_DUPLICATE_TARGETS, mappingPrism.target() );
}
}
}
return mappings;
}
public static Mapping fromMappingPrism(MappingPrism mappingPrism, ExecutableElement element,
FormattingMessager messager) {
if ( mappingPrism.target().isEmpty() ) {
messager.printMessage(
element,
mappingPrism.mirror,
mappingPrism.values.target(),
Message.PROPERTYMAPPING_EMPTY_TARGET
);
return null;
}
if ( !mappingPrism.source().isEmpty() && mappingPrism.values.constant() != null ) {
messager.printMessage( element, Message.PROPERTYMAPPING_SOURCE_AND_CONSTANT_BOTH_DEFINED );
return null;
}
else if ( !mappingPrism.source().isEmpty() && mappingPrism.values.expression() != null ) {
messager.printMessage( element, Message.PROPERTYMAPPING_SOURCE_AND_EXPRESSION_BOTH_DEFINED );
return null;
}
else if ( mappingPrism.values.expression() != null && mappingPrism.values.constant() != null ) {
messager.printMessage( element, Message.PROPERTYMAPPING_EXPRESSION_AND_CONSTANT_BOTH_DEFINED );
return null;
}
else if ( mappingPrism.values.expression() != null && mappingPrism.values.defaultValue() != null ) {
messager.printMessage( element, Message.PROPERTYMAPPING_EXPRESSION_AND_DEFAULT_VALUE_BOTH_DEFINED );
return null;
}
else if ( mappingPrism.values.constant() != null && mappingPrism.values.defaultValue() != null ) {
messager.printMessage( element, Message.PROPERTYMAPPING_CONSTANT_AND_DEFAULT_VALUE_BOTH_DEFINED );
return null;
}
String source = mappingPrism.source().isEmpty() ? null : mappingPrism.source();
String constant = mappingPrism.values.constant() == null ? null : mappingPrism.constant();
String expression = getExpression( mappingPrism, element, messager );
String dateFormat = mappingPrism.values.dateFormat() == null ? null : mappingPrism.dateFormat();
String numberFormat = mappingPrism.values.numberFormat() == null ? null : mappingPrism.numberFormat();
String defaultValue = mappingPrism.values.defaultValue() == null ? null : mappingPrism.defaultValue();
boolean resultTypeIsDefined = mappingPrism.values.resultType() != null;
List<String> dependsOn =
mappingPrism.dependsOn() != null ? mappingPrism.dependsOn() : Collections.<String>emptyList();
FormattingParameters formattingParam = new FormattingParameters(
dateFormat,
numberFormat,
mappingPrism.mirror,
mappingPrism.values.dateFormat(),
element
);
SelectionParameters selectionParams = new SelectionParameters(
mappingPrism.qualifiedBy(),
mappingPrism.qualifiedByName(),
resultTypeIsDefined ? mappingPrism.resultType() : null);
return new Mapping(
source,
constant,
expression,
mappingPrism.target(),
defaultValue,
mappingPrism.ignore(),
mappingPrism.mirror,
mappingPrism.values.source(),
mappingPrism.values.target(),
formattingParam,
selectionParams,
mappingPrism.values.dependsOn(),
dependsOn
);
}
@SuppressWarnings("checkstyle:parameternumber")
private Mapping( String sourceName, String constant, String javaExpression, String targetName,
String defaultValue, boolean isIgnored, AnnotationMirror mirror,
AnnotationValue sourceAnnotationValue, AnnotationValue targetAnnotationValue,
FormattingParameters formattingParameters, SelectionParameters selectionParameters,
AnnotationValue dependsOnAnnotationValue, List<String> dependsOn ) {
this.sourceName = sourceName;
this.constant = constant;
this.javaExpression = javaExpression;
this.targetName = targetName;
this.defaultValue = defaultValue;
this.isIgnored = isIgnored;
this.mirror = mirror;
this.sourceAnnotationValue = sourceAnnotationValue;
this.targetAnnotationValue = targetAnnotationValue;
this.formattingParameters = formattingParameters;
this.selectionParameters = selectionParameters;
this.dependsOnAnnotationValue = dependsOnAnnotationValue;
this.dependsOn = dependsOn;
}
private Mapping( Mapping mapping, TargetReference targetReference ) {
this.sourceName = mapping.sourceName;
this.constant = mapping.constant;
this.javaExpression = mapping.javaExpression;
this.targetName = Strings.join( targetReference.getElementNames(), "." );
this.defaultValue = mapping.defaultValue;
this.isIgnored = mapping.isIgnored;
this.mirror = mapping.mirror;
this.sourceAnnotationValue = mapping.sourceAnnotationValue;
this.targetAnnotationValue = mapping.targetAnnotationValue;
this.formattingParameters = mapping.formattingParameters;
this.selectionParameters = mapping.selectionParameters;
this.dependsOnAnnotationValue = mapping.dependsOnAnnotationValue;
this.dependsOn = mapping.dependsOn;
this.sourceReference = mapping.sourceReference;
this.targetReference = targetReference;
}
private Mapping( Mapping mapping, SourceReference sourceReference ) {
this.sourceName = Strings.join( sourceReference.getElementNames(), "." );
this.constant = mapping.constant;
this.javaExpression = mapping.javaExpression;
this.targetName = mapping.targetName;
this.defaultValue = mapping.defaultValue;
this.isIgnored = mapping.isIgnored;
this.mirror = mapping.mirror;
this.sourceAnnotationValue = mapping.sourceAnnotationValue;
this.targetAnnotationValue = mapping.targetAnnotationValue;
this.formattingParameters = mapping.formattingParameters;
this.selectionParameters = mapping.selectionParameters;
this.dependsOnAnnotationValue = mapping.dependsOnAnnotationValue;
this.dependsOn = mapping.dependsOn;
this.sourceReference = sourceReference;
this.targetReference = mapping.targetReference;
}
private static String getExpression(MappingPrism mappingPrism, ExecutableElement element,
FormattingMessager messager) {
if ( mappingPrism.expression().isEmpty() ) {
return null;
}
Matcher javaExpressionMatcher = JAVA_EXPRESSION.matcher( mappingPrism.expression() );
if ( !javaExpressionMatcher.matches() ) {
messager.printMessage(
element, mappingPrism.mirror, mappingPrism.values.expression(),
Message.PROPERTYMAPPING_INVALID_EXPRESSION
);
return null;
}
return javaExpressionMatcher.group( 1 ).trim();
}
private static boolean isEnumType(TypeMirror mirror) {
return mirror.getKind() == TypeKind.DECLARED &&
( (DeclaredType) mirror ).asElement().getKind() == ElementKind.ENUM;
}
public void init(SourceMethod method, FormattingMessager messager, TypeFactory typeFactory) {
init( method, messager, typeFactory, false, null );
}
/**
* Initialize the Mapping.
*
* @param method the source method that the mapping belongs to
* @param messager the messager that can be used for outputting messages
* @param typeFactory the type factory
* @param isReverse whether the init is for a reverse mapping
* @param reverseSourceParameter the source parameter from the revers mapping
*/
private void init(SourceMethod method, FormattingMessager messager, TypeFactory typeFactory, boolean isReverse,
Parameter reverseSourceParameter) {
if ( !method.isEnumMapping() ) {
sourceReference = new SourceReference.BuilderFromMapping()
.mapping( this )
.method( method )
.messager( messager )
.typeFactory( typeFactory )
.build();
targetReference = new TargetReference.BuilderFromTargetMapping()
.mapping( this )
.isReverse( isReverse )
.method( method )
.messager( messager )
.typeFactory( typeFactory )
.reverseSourceParameter( reverseSourceParameter )
.build();
}
}
/**
* Initializes the mapping with a new source parameter.
*
* @param sourceParameter sets the source parameter that acts as a basis for this mapping
*/
public void init( Parameter sourceParameter ) {
if ( sourceReference != null ) {
SourceReference oldSourceReference = sourceReference;
sourceReference = new SourceReference.BuilderFromSourceReference()
.sourceParameter( sourceParameter )
.sourceReference( oldSourceReference )
.build();
}
}
/**
* Returns the complete source name of this mapping, either a qualified (e.g. {@code parameter1.foo}) or
* unqualified (e.g. {@code foo}) property reference.
*
* @return The complete source name of this mapping.
*/
public String getSourceName() {
return sourceName;
}
public String getConstant() {
return constant;
}
public String getJavaExpression() {
return javaExpression;
}
public String getTargetName() {
return targetName;
}
public String getDefaultValue() {
return defaultValue;
}
public FormattingParameters getFormattingParameters() {
return formattingParameters;
}
public SelectionParameters getSelectionParameters() {
return selectionParameters;
}
public boolean isIgnored() {
return isIgnored;
}
public AnnotationMirror getMirror() {
return mirror;
}
public AnnotationValue getSourceAnnotationValue() {
return sourceAnnotationValue;
}
public AnnotationValue getTargetAnnotationValue() {
return targetAnnotationValue;
}
public AnnotationValue getDependsOnAnnotationValue() {
return dependsOnAnnotationValue;
}
public SourceReference getSourceReference() {
return sourceReference;
}
public TargetReference getTargetReference() {
return targetReference;
}
public Mapping popTargetReference() {
if ( targetReference != null ) {
TargetReference newTargetReference = targetReference.pop();
if (newTargetReference != null ) {
return new Mapping(this, newTargetReference );
}
}
return null;
}
public Mapping popSourceReference() {
if ( sourceReference != null ) {
SourceReference newSourceReference = sourceReference.pop();
if (newSourceReference != null ) {
return new Mapping(this, newSourceReference );
}
}
return null;
}
public List<String> getDependsOn() {
return dependsOn;
}
private boolean hasPropertyInReverseMethod(String name, SourceMethod method) {
CollectionMappingStrategyPrism cms = method.getMapperConfiguration().getCollectionMappingStrategy();
return method.getResultType().getPropertyWriteAccessors( cms ).containsKey( name );
}
public Mapping reverse(SourceMethod method, FormattingMessager messager, TypeFactory typeFactory) {
// mapping can only be reversed if the source was not a constant nor an expression nor a nested property
if ( constant != null || javaExpression != null ) {
return null;
}
// should only ignore a property when 1) there is a sourceName defined or 2) there's a name match
if ( isIgnored ) {
if ( sourceName == null && !hasPropertyInReverseMethod( targetName, method ) ) {
return null;
}
}
Mapping reverse = new Mapping(
sourceName != null ? targetName : null,
null, // constant
null, // expression
sourceName != null ? sourceName : targetName,
null,
isIgnored,
mirror,
sourceAnnotationValue,
targetAnnotationValue,
formattingParameters,
selectionParameters,
dependsOnAnnotationValue,
Collections.<String>emptyList()
);
reverse.init(
method,
messager,
typeFactory,
true,
sourceReference != null ? sourceReference.getParameter() : null
);
return reverse;
}
/**
* Creates a copy of this mapping, which is adapted to the given method
*
* @param method the method to create the copy for
* @return the copy
*/
public Mapping copyForInheritanceTo(SourceMethod method) {
Mapping mapping = new Mapping(
sourceName,
constant,
javaExpression,
targetName,
defaultValue,
isIgnored,
mirror,
sourceAnnotationValue,
targetAnnotationValue,
formattingParameters,
selectionParameters,
dependsOnAnnotationValue,
dependsOn
);
if ( sourceReference != null ) {
mapping.sourceReference = sourceReference.copyForInheritanceTo( method );
}
// TODO... must something be done here? Andreas?
mapping.targetReference = targetReference;
return mapping;
}
@Override
public String toString() {
return "Mapping {" +
"\n sourceName='" + sourceName + "\'," +
"\n targetName='" + targetName + "\'," +
"\n}";
}
}