/**
* 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 static org.mapstruct.ap.internal.model.source.PropertyEntry.forSourceReference;
import static org.mapstruct.ap.internal.util.Collections.first;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import javax.lang.model.type.DeclaredType;
import org.mapstruct.ap.internal.model.common.Parameter;
import org.mapstruct.ap.internal.model.common.Type;
import org.mapstruct.ap.internal.model.common.TypeFactory;
import org.mapstruct.ap.internal.util.FormattingMessager;
import org.mapstruct.ap.internal.util.Message;
import org.mapstruct.ap.internal.util.Strings;
import org.mapstruct.ap.internal.util.accessor.Accessor;
import org.mapstruct.ap.internal.util.accessor.ExecutableElementAccessor;
/**
* This class describes the source side of a property mapping.
* <p>
* It contains the source parameter, and all individual (nested) property entries. So consider the following
* mapping method:
*
* <pre>
* @Mapping(source = "in.propA.propB" target = "propC")
* TypeB mappingMethod(TypeA in);
* </pre>
*
* Then:
* <ul>
* <li>{@code parameter} will describe {@code in}</li>
* <li>{@code propertyEntries[0]} will describe {@code propA}</li>
* <li>{@code propertyEntries[1]} will describe {@code propB}</li>
* </ul>
*
* After building, {@link #isValid()} will return true when when no problems are detected during building.
*
* @author Sjaak Derksen
*/
public class SourceReference {
private final Parameter parameter;
private final List<PropertyEntry> propertyEntries;
private final boolean isValid;
/**
* Builds a {@link SourceReference} from an {@code @Mappping}.
*/
public static class BuilderFromMapping {
private Mapping mapping;
private SourceMethod method;
private FormattingMessager messager;
private TypeFactory typeFactory;
public BuilderFromMapping messager(FormattingMessager messager) {
this.messager = messager;
return this;
}
public BuilderFromMapping mapping(Mapping mapping) {
this.mapping = mapping;
return this;
}
public BuilderFromMapping method(SourceMethod method) {
this.method = method;
return this;
}
public BuilderFromMapping typeFactory(TypeFactory typeFactory) {
this.typeFactory = typeFactory;
return this;
}
public SourceReference build() {
String sourceName = mapping.getSourceName();
if ( sourceName == null ) {
return null;
}
boolean isValid = true;
boolean foundEntryMatch;
String[] sourcePropertyNames = new String[0];
String[] segments = sourceName.split( "\\." );
Parameter parameter = null;
List<PropertyEntry> entries = new ArrayList<PropertyEntry>();
if ( method.getSourceParameters().size() > 1 ) {
// parameterName is mandatory for multiple source parameters
if ( segments.length > 0 ) {
String sourceParameterName = segments[0];
parameter = method.getSourceParameter( sourceParameterName );
if ( parameter == null ) {
reportMappingError( Message.PROPERTYMAPPING_INVALID_PARAMETER_NAME, sourceParameterName );
isValid = false;
}
}
if ( segments.length > 1 && parameter != null ) {
sourcePropertyNames = Arrays.copyOfRange( segments, 1, segments.length );
entries = getSourceEntries( parameter.getType(), sourcePropertyNames );
foundEntryMatch = (entries.size() == sourcePropertyNames.length);
}
else {
// its only a parameter, no property
foundEntryMatch = true;
}
}
else {
// parameter name is not mandatory for single source parameter
sourcePropertyNames = segments;
parameter = method.getSourceParameters().get( 0 );
entries = getSourceEntries( parameter.getType(), sourcePropertyNames );
foundEntryMatch = (entries.size() == sourcePropertyNames.length);
if ( !foundEntryMatch ) {
//Lets see if the expression contains the parameterName, so parameterName.propName1.propName2
if ( parameter.getName().equals( segments[0] ) ) {
sourcePropertyNames = Arrays.copyOfRange( segments, 1, segments.length );
entries = getSourceEntries( parameter.getType(), sourcePropertyNames );
foundEntryMatch = (entries.size() == sourcePropertyNames.length);
}
else {
// segment[0] cannot be attributed to the parameter name.
parameter = null;
}
}
}
if ( !foundEntryMatch ) {
if ( parameter != null ) {
reportMappingError( Message.PROPERTYMAPPING_NO_PROPERTY_IN_PARAMETER, parameter.getName(),
Strings.join( Arrays.asList( sourcePropertyNames ), "." ) );
}
else {
reportMappingError( Message.PROPERTYMAPPING_INVALID_PROPERTY_NAME, mapping.getSourceName() );
}
isValid = false;
}
return new SourceReference( parameter, entries, isValid );
}
private List<PropertyEntry> getSourceEntries(Type type, String[] entryNames) {
List<PropertyEntry> sourceEntries = new ArrayList<PropertyEntry>();
Type newType = type;
for ( String entryName : entryNames ) {
boolean matchFound = false;
Map<String, Accessor> sourceReadAccessors = newType.getPropertyReadAccessors();
Map<String, ExecutableElementAccessor> sourcePresenceCheckers = newType.getPropertyPresenceCheckers();
for ( Map.Entry<String, Accessor> getter : sourceReadAccessors.entrySet() ) {
if ( getter.getKey().equals( entryName ) ) {
newType = typeFactory.getReturnType( (DeclaredType) newType.getTypeMirror(),
getter.getValue() );
sourceEntries.add( forSourceReference( entryName, getter.getValue(),
sourcePresenceCheckers.get( entryName ), newType ) );
matchFound = true;
break;
}
}
if ( !matchFound ) {
break;
}
}
return sourceEntries;
}
private void reportMappingError(Message msg, Object... objects) {
messager.printMessage( method.getExecutable(), mapping.getMirror(), mapping.getSourceAnnotationValue(),
msg, objects );
}
}
/**
* Builds a {@link SourceReference} from a property.
*/
public static class BuilderFromProperty {
private String name;
private Accessor readAccessor;
private ExecutableElementAccessor presenceChecker;
private Type type;
private Parameter sourceParameter;
public BuilderFromProperty name(String name) {
this.name = name;
return this;
}
public BuilderFromProperty readAccessor(Accessor readAccessor) {
this.readAccessor = readAccessor;
return this;
}
public BuilderFromProperty presenceChecker(ExecutableElementAccessor presenceChecker) {
this.presenceChecker = presenceChecker;
return this;
}
public BuilderFromProperty type(Type type) {
this.type = type;
return this;
}
public BuilderFromProperty sourceParameter(Parameter sourceParameter) {
this.sourceParameter = sourceParameter;
return this;
}
public SourceReference build() {
List<PropertyEntry> sourcePropertyEntries = new ArrayList<PropertyEntry>();
if ( readAccessor != null ) {
sourcePropertyEntries.add( forSourceReference( name, readAccessor, presenceChecker, type ) );
}
return new SourceReference( sourceParameter, sourcePropertyEntries, true );
}
}
/**
* Builds a {@link SourceReference} from a property.
*/
public static class BuilderFromSourceReference {
private Parameter sourceParameter;
private SourceReference sourceReference;
public BuilderFromSourceReference sourceReference(SourceReference sourceReference) {
this.sourceReference = sourceReference;
return this;
}
public BuilderFromSourceReference sourceParameter(Parameter sourceParameter) {
this.sourceParameter = sourceParameter;
return this;
}
public SourceReference build() {
return new SourceReference( sourceParameter, sourceReference.propertyEntries, true );
}
}
private SourceReference(Parameter sourceParameter, List<PropertyEntry> sourcePropertyEntries, boolean isValid) {
this.parameter = sourceParameter;
this.propertyEntries = sourcePropertyEntries;
this.isValid = isValid;
}
public Parameter getParameter() {
return parameter;
}
public List<PropertyEntry> getPropertyEntries() {
return propertyEntries;
}
public boolean isValid() {
return isValid;
}
public List<String> getElementNames() {
List<String> sourceName = new ArrayList<String>();
sourceName.add( parameter.getName() );
for ( PropertyEntry propertyEntry : propertyEntries ) {
sourceName.add( propertyEntry.getName() );
}
return sourceName;
}
/**
* Creates a copy of this reference, which is adapted to the given method
*
* @param method the method to create the copy for
* @return the copy
*/
public SourceReference copyForInheritanceTo(SourceMethod method) {
List<Parameter> replacementParamCandidates = new ArrayList<Parameter>();
for ( Parameter sourceParam : method.getSourceParameters() ) {
if ( sourceParam.getType().isAssignableTo( parameter.getType() ) ) {
replacementParamCandidates.add( sourceParam );
}
}
Parameter replacement = parameter;
if ( replacementParamCandidates.size() == 1 ) {
replacement = first( replacementParamCandidates );
}
return new SourceReference( replacement, propertyEntries, isValid );
}
public SourceReference pop() {
if ( propertyEntries.size() > 1 ) {
List<PropertyEntry> newPropertyEntries =
new ArrayList<PropertyEntry>( propertyEntries.subList( 1, propertyEntries.size() ) );
return new SourceReference( parameter, newPropertyEntries, isValid );
}
else {
return null;
}
}
@Override
public String toString() {
if ( propertyEntries.isEmpty() ) {
return String.format( "parameter \"%s %s\"", getParameter().getType(), getParameter().getName() );
}
else if ( propertyEntries.size() == 1 ) {
PropertyEntry propertyEntry = propertyEntries.get( 0 );
return String.format( "property \"%s %s\"", propertyEntry.getType(), propertyEntry.getName() );
}
else {
PropertyEntry lastPropertyEntry = propertyEntries.get( propertyEntries.size() - 1 );
return String.format(
"property \"%s %s\"",
lastPropertyEntry.getType(),
Strings.join( getElementNames(), "." )
);
}
}
}