/**
* 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.HashMap;
import java.util.List;
import java.util.Map;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.TypeParameterElement;
import javax.lang.model.type.ArrayType;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.PrimitiveType;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.type.TypeVariable;
import javax.lang.model.type.WildcardType;
import javax.lang.model.util.SimpleTypeVisitor6;
import javax.lang.model.util.Types;
import org.mapstruct.ap.internal.model.common.Parameter;
import org.mapstruct.ap.internal.model.common.Type;
import org.mapstruct.ap.internal.model.common.TypeFactory;
/**
* SourceMethodMatcher $8.4 of the JavaLanguage specification describes a method body as such:
*
* <pre>
* SourceMethodDeclaration: SourceMethodHeader SourceMethodBody
* SourceMethodHeader: SourceMethodModifiers TypeParameters Result SourceMethodDeclarator Throws
* SourceMethodDeclarator: Identifier ( FormalParameterList )
*
* example <T extends String & Serializable> T getResult(? extends T) throws Exception
* \-------------------------------/ \-/ \---------/
* TypeParameters Result ParameterList
* </pre>
*
* Matches a given method with given ParameterList and Result type obeying the constraints in the TypeParameters block.
* <p>
* For more info on java-generics: http://www.javacodegeeks.com/2011/04/java-generics-quick-tutorial.html
* http://www.angelikalanger.com/GenericsFAQ/FAQSections/ParameterizedTypes.html
* <p>
* The following situations is not supported / tested:
* <ol>
* <li>Multiple bounds were the bound itself is again a generic type.</li>
* </ol>
*
* @author Sjaak Derksen
*/
public class MethodMatcher {
private final SourceMethod candidateMethod;
private final Types typeUtils;
private final TypeFactory typeFactory;
MethodMatcher(Types typeUtils, TypeFactory typeFactory, SourceMethod candidateMethod) {
this.typeUtils = typeUtils;
this.candidateMethod = candidateMethod;
this.typeFactory = typeFactory;
}
/**
* Whether the given source and target types are matched by this matcher's candidate method.
*
* @param sourceTypes the source types
* @param resultType the target type
* @return {@code true} when both, source type and target types match the signature of this matcher's method;
* {@code false} otherwise.
*/
boolean matches(List<Type> sourceTypes, Type resultType) {
// check & collect generic types.
Map<TypeVariable, TypeMirror> genericTypesMap = new HashMap<TypeVariable, TypeMirror>();
if ( candidateMethod.getParameters().size() == sourceTypes.size() ) {
int i = 0;
for ( Parameter candidateParam : candidateMethod.getParameters() ) {
Type sourceType = sourceTypes.get( i++ );
if ( sourceType == null
|| !matchSourceType( sourceType, candidateParam.getType(), genericTypesMap ) ) {
return false;
}
}
}
else {
return false;
}
// check if the method matches the proper result type to construct
Parameter targetTypeParameter = candidateMethod.getTargetTypeParameter();
if ( targetTypeParameter != null ) {
Type returnClassType = typeFactory.classTypeOf( resultType );
if ( !matchSourceType( returnClassType, targetTypeParameter.getType(), genericTypesMap ) ) {
return false;
}
}
// check result type
if ( !matchResultType( resultType, genericTypesMap ) ) {
return false;
}
// check if all type parameters are indeed mapped
if ( candidateMethod.getExecutable().getTypeParameters().size() != genericTypesMap.size() ) {
return false;
}
// check if all entries are in the bounds
for ( Map.Entry<TypeVariable, TypeMirror> entry : genericTypesMap.entrySet() ) {
if ( !isWithinBounds( entry.getValue(), getTypeParamFromCandidate( entry.getKey() ) ) ) {
// checks if the found Type is in bounds of the TypeParameters bounds.
return false;
}
}
return true;
}
private boolean matchSourceType(Type sourceType,
Type candidateSourceType,
Map<TypeVariable, TypeMirror> genericTypesMap) {
if ( !isJavaLangObject( candidateSourceType.getTypeMirror() ) ) {
TypeMatcher parameterMatcher = new TypeMatcher( Assignability.VISITED_ASSIGNABLE_FROM, genericTypesMap );
if ( !parameterMatcher.visit( candidateSourceType.getTypeMirror(), sourceType.getTypeMirror() ) ) {
if ( sourceType.isPrimitive() ) {
// the candidate source is primitive, so promote to its boxed type and check again (autobox)
TypeMirror boxedType = typeUtils.boxedClass( (PrimitiveType) sourceType.getTypeMirror() ).asType();
if ( !parameterMatcher.visit( candidateSourceType.getTypeMirror(), boxedType ) ) {
return false;
}
}
else {
// NOTE: unboxing is deliberately not considered here. This should be handled via type-conversion
// (for NPE safety).
return false;
}
}
}
return true;
}
private boolean matchResultType(Type resultType, Map<TypeVariable, TypeMirror> genericTypesMap) {
Type candidateResultType = candidateMethod.getResultType();
if ( !isJavaLangObject( candidateResultType.getTypeMirror() ) && !candidateResultType.isVoid() ) {
final Assignability visitedAssignability;
if ( candidateMethod.getReturnType().isVoid() ) {
// for void-methods, the result-type of the candidate needs to be assignable from the given result type
visitedAssignability = Assignability.VISITED_ASSIGNABLE_FROM;
}
else {
// for non-void methods, the result-type of the candidate needs to be assignable to the given result
// type
visitedAssignability = Assignability.VISITED_ASSIGNABLE_TO;
}
TypeMatcher returnTypeMatcher = new TypeMatcher( visitedAssignability, genericTypesMap );
if ( !returnTypeMatcher.visit( candidateResultType.getTypeMirror(), resultType.getTypeMirror() ) ) {
if ( resultType.isPrimitive() ) {
TypeMirror boxedType = typeUtils.boxedClass( (PrimitiveType) resultType.getTypeMirror() ).asType();
TypeMatcher boxedReturnTypeMatcher =
new TypeMatcher( visitedAssignability, genericTypesMap );
if ( !boxedReturnTypeMatcher.visit( candidateResultType.getTypeMirror(), boxedType ) ) {
return false;
}
}
else if ( candidateResultType.getTypeMirror().getKind().isPrimitive() ) {
TypeMirror boxedCandidateReturnType =
typeUtils.boxedClass( (PrimitiveType) candidateResultType.getTypeMirror() ).asType();
TypeMatcher boxedReturnTypeMatcher =
new TypeMatcher( visitedAssignability, genericTypesMap );
if ( !boxedReturnTypeMatcher.visit( boxedCandidateReturnType, resultType.getTypeMirror() ) ) {
return false;
}
}
else {
return false;
}
}
}
return true;
}
/**
* @param type the type
* @return {@code true}, if the type represents java.lang.Object
*/
private boolean isJavaLangObject(TypeMirror type) {
return type.getKind() == TypeKind.DECLARED
&& ( (TypeElement) ( (DeclaredType) type ).asElement() ).getQualifiedName().contentEquals(
Object.class.getName() );
}
private enum Assignability {
VISITED_ASSIGNABLE_FROM, VISITED_ASSIGNABLE_TO;
Assignability invert() {
return this == VISITED_ASSIGNABLE_FROM
? VISITED_ASSIGNABLE_TO
: VISITED_ASSIGNABLE_FROM;
}
}
private class TypeMatcher extends SimpleTypeVisitor6<Boolean, TypeMirror> {
private final Assignability assignability;
private final Map<TypeVariable, TypeMirror> genericTypesMap;
private final TypeMatcher inverse;
TypeMatcher(Assignability assignability, Map<TypeVariable, TypeMirror> genericTypesMap) {
super( Boolean.FALSE ); // default value
this.assignability = assignability;
this.genericTypesMap = genericTypesMap;
this.inverse = new TypeMatcher( this, genericTypesMap );
}
TypeMatcher(TypeMatcher inverse, Map<TypeVariable, TypeMirror> genericTypesMap) {
super( Boolean.FALSE ); // default value
this.assignability = inverse.assignability.invert();
this.genericTypesMap = genericTypesMap;
this.inverse = inverse;
}
@Override
public Boolean visitPrimitive(PrimitiveType t, TypeMirror p) {
return typeUtils.isSameType( t, p );
}
@Override
public Boolean visitArray(ArrayType t, TypeMirror p) {
if ( p.getKind().equals( TypeKind.ARRAY ) ) {
return t.getComponentType().accept( this, ( (ArrayType) p ).getComponentType() );
}
else {
return Boolean.FALSE;
}
}
@Override
public Boolean visitDeclared(DeclaredType t, TypeMirror p) {
// its a match when: 1) same kind of type, name is equals, nr of type args are the same
// (type args are checked later).
if ( p.getKind() == TypeKind.DECLARED ) {
DeclaredType t1 = (DeclaredType) p;
if ( assignabilityMatches( t, t1 )
&& t.getTypeArguments().size() == t1.getTypeArguments().size() ) {
for ( int i = 0; i < t.getTypeArguments().size(); i++ ) {
if ( !visit( t.getTypeArguments().get( i ), t1.getTypeArguments().get( i ) ) ) {
return Boolean.FALSE;
}
}
return Boolean.TRUE;
}
else {
return Boolean.FALSE;
}
}
else if ( p.getKind() == TypeKind.WILDCARD ) {
return inverse.visit( p, t ); // inverse, as we switch the params
}
else {
return Boolean.FALSE;
}
}
private boolean assignabilityMatches(DeclaredType visited, DeclaredType param) {
if ( assignability == Assignability.VISITED_ASSIGNABLE_TO ) {
return typeUtils.isAssignable( toRawType( visited ), toRawType( param ) );
}
else {
return typeUtils.isAssignable( toRawType( param ), toRawType( visited ) );
}
}
private DeclaredType toRawType(DeclaredType t) {
return typeUtils.getDeclaredType( (TypeElement) t.asElement() );
}
@Override
public Boolean visitTypeVariable(TypeVariable t, TypeMirror p) {
if ( genericTypesMap.containsKey( t ) ) {
// when already found, the same mapping should apply
TypeMirror p1 = genericTypesMap.get( t );
return typeUtils.isSameType( p, p1 );
}
else {
// check if types are in bound
TypeMirror lowerBound = t.getLowerBound();
TypeMirror upperBound = t.getUpperBound();
if ( ( isNullType( lowerBound ) || typeUtils.isSubtype( lowerBound, p ) )
&& ( isNullType( upperBound ) || typeUtils.isSubtype( p, upperBound ) ) ) {
genericTypesMap.put( t, p );
return Boolean.TRUE;
}
else {
return Boolean.FALSE;
}
}
}
private boolean isNullType(TypeMirror type) {
return type == null || type.getKind() == TypeKind.NULL;
}
@Override
public Boolean visitWildcard(WildcardType t, TypeMirror p) {
// check extends bound
TypeMirror extendsBound = t.getExtendsBound();
if ( !isNullType( extendsBound ) ) {
switch ( extendsBound.getKind() ) {
case DECLARED:
// for example method: String method(? extends String)
// isSubType checks range [subtype, type], e.g. isSubtype [Object, String]==true
return visit( extendsBound, p );
case TYPEVAR:
// for example method: <T extends String & Serializable> T method(? extends T)
// this can be done the directly by checking: ? extends String & Serializable
// this checks the part? <T extends String & Serializable>
return isWithinBounds( p, getTypeParamFromCandidate( extendsBound ) );
default:
// does this situation occur?
return Boolean.FALSE;
}
}
// check super bound
TypeMirror superBound = t.getSuperBound();
if ( !isNullType( superBound ) ) {
switch ( superBound.getKind() ) {
case DECLARED:
// for example method: String method(? super String)
// to check super type, we can simply reverse the argument, but that would initially yield
// a result: <type, superType] (so type not included) so we need to check sameType also.
return typeUtils.isSubtype( superBound, p ) || typeUtils.isSameType( p, superBound );
case TYPEVAR:
TypeParameterElement typeParameter = getTypeParamFromCandidate( superBound );
// for example method: <T extends String & Serializable> T method(? super T)
if ( !isWithinBounds( p, typeParameter ) ) {
// this checks the part? <T extends String & Serializable>
return Boolean.FALSE;
}
// now, it becomes a bit more hairy. We have the relation (? super T). From T we know that
// it is a subclass of String & Serializable. However, The Java Language Secification,
// Chapter 4.4, states that a bound is either: 'A type variable-', 'A class-' or 'An
// interface-' type followed by further interface types. So we must compare with the first
// argument in the Expression String & Serializable & ..., so, in this case String.
// to check super type, we can simply reverse the argument, but that would initially yield
// a result: <type, superType] (so type not included) so we need to check sameType also.
TypeMirror superBoundAsDeclared = typeParameter.getBounds().get( 0 );
return ( typeUtils.isSubtype( superBoundAsDeclared, p ) || typeUtils.isSameType(
p,
superBoundAsDeclared ) );
default:
// does this situation occur?
return Boolean.FALSE;
}
}
return Boolean.TRUE;
}
}
/**
* Looks through the list of type parameters of the candidate method for a match
*
* @param t type parameter to match
*
* @return matching type parameter
*/
private TypeParameterElement getTypeParamFromCandidate(TypeMirror t) {
for ( TypeParameterElement candidateTypeParam : candidateMethod.getExecutable().getTypeParameters() ) {
if ( candidateTypeParam.asType().equals( t ) ) {
return candidateTypeParam;
}
}
return null;
}
/**
* checks whether a type t is in bounds of the typeParameter tpe
*
* @return true if within bounds
*/
private boolean isWithinBounds(TypeMirror t, TypeParameterElement tpe) {
List<? extends TypeMirror> bounds = tpe != null ? tpe.getBounds() : null;
if ( t != null && bounds != null ) {
for ( TypeMirror bound : bounds ) {
if ( !( bound.getKind() == TypeKind.DECLARED && typeUtils.isSubtype( t, bound ) ) ) {
return false;
}
}
return true;
}
return false;
}
}