diff --git a/src/main/kotlin/graphql/kickstart/tools/GenericType.kt b/src/main/kotlin/graphql/kickstart/tools/GenericType.kt deleted file mode 100644 index a616ee2f..00000000 --- a/src/main/kotlin/graphql/kickstart/tools/GenericType.kt +++ /dev/null @@ -1,147 +0,0 @@ -package graphql.kickstart.tools - -import com.fasterxml.classmate.ResolvedType -import graphql.kickstart.tools.util.JavaType -import graphql.kickstart.tools.util.ParameterizedTypeImpl -import graphql.kickstart.tools.util.Primitives -import org.apache.commons.lang3.reflect.TypeUtils -import java.lang.reflect.ParameterizedType -import java.lang.reflect.TypeVariable -import java.lang.reflect.WildcardType - -/** - * @author Andrew Potter - */ -internal open class GenericType(protected val mostSpecificType: JavaType, protected val options: SchemaParserOptions) { - - fun isTypeAssignableFromRawClass(type: ParameterizedType, clazz: Class<*>) = - clazz.isAssignableFrom(getRawClass(type.rawType)) - - fun getRawClass() = getRawClass(mostSpecificType) - - fun getRawClass(type: JavaType): Class<*> = TypeUtils.getRawType(type, mostSpecificType) - - fun isAssignableFrom(type: JavaType) = TypeUtils.isAssignable(type, mostSpecificType) - - fun relativeToPotentialParent(declaringType: JavaType): RelativeTo { - if (declaringType !is Class<*> || declaringType.isInterface) { - return relativeToType(declaringType) - } - - val type = getGenericSuperType(mostSpecificType, declaringType) - if (type == null) { - error("Unable to find generic type of class ${TypeUtils.toString(declaringType)} relative to ${TypeUtils.toString(mostSpecificType)}") - } else { - return relativeToType(type) - } - } - - fun relativeToType(declaringType: JavaType) = RelativeTo(declaringType, mostSpecificType, options) - - fun getGenericInterface(targetInterface: Class<*>) = getGenericInterface(mostSpecificType, targetInterface) - - private fun getGenericInterface(type: JavaType?, targetInterface: Class<*>): JavaType? { - if (type == null) { - return null - } - - val raw = type as? Class<*> ?: getRawClass(type) - - if (raw == targetInterface) { - return type - } - - val possibleSubInterface = raw.genericInterfaces.find { genericInterface -> - TypeUtils.isAssignable(genericInterface, targetInterface) - } ?: raw.interfaces.find { iface -> - TypeUtils.isAssignable(iface, targetInterface) - } ?: getGenericInterface(raw.genericSuperclass, targetInterface) ?: return null - - return getGenericInterface(possibleSubInterface, targetInterface) - } - - fun getGenericSuperType(targetSuperClass: Class<*>) = getGenericSuperType(mostSpecificType, targetSuperClass) - - private fun getGenericSuperType(type: JavaType?, targetSuperClass: Class<*>): JavaType? { - if (type == null) { - return null - } - - val raw = type as? Class<*> ?: TypeUtils.getRawType(type, type) - - if (raw == targetSuperClass) { - return type - } - - return getGenericSuperType(raw.genericSuperclass, targetSuperClass) - } - - class RelativeTo(private val declaringType: JavaType, mostSpecificType: JavaType, options: SchemaParserOptions) : GenericType(mostSpecificType, options) { - - /** - * Unwrap certain Java types to find the "real" class. - */ - fun unwrapGenericType(javaType: JavaType): JavaType { - return when (val type = replaceTypeVariable(javaType)) { - is ParameterizedType -> { - val rawType = type.rawType - val genericType = options.genericWrappers.find { it.type == rawType } - ?: return type - - val typeArguments = type.actualTypeArguments - if (typeArguments.size <= genericType.index) { - throw IndexOutOfBoundsException("Generic type '${TypeUtils.toString(type)}' does not have a type argument at index ${genericType.index}!") - } - - val unwrapsTo = genericType.schemaWrapper.invoke(typeArguments[genericType.index]) - unwrapGenericType(unwrapsTo) - } - is TypeVariable<*> -> error("Could not resolve type variable '${TypeUtils.toLongString(type)}' of ${TypeUtils.toString(declaringType)} relative to ${TypeUtils.toString(mostSpecificType)}") - is WildcardType -> type.upperBounds.firstOrNull() - ?: error("Unable to unwrap type, wildcard has no upper bound: $type") - is Class<*> -> if (type.isPrimitive) Primitives.wrap(type) else type - else -> error("Unable to unwrap type: $type") - } - } - - private fun replaceTypeVariable(type: JavaType, resolving: Set> = emptySet()): JavaType { - return when (type) { - is ParameterizedType -> { - val actualTypeArguments = type.actualTypeArguments.map { replaceTypeVariable(it, resolving) }.toTypedArray() - ParameterizedTypeImpl(type.rawType as Class<*>, actualTypeArguments, type.ownerType?.let { replaceTypeVariable(it, resolving) }) - } - is WildcardType -> TypeUtils.wildcardType() - .withUpperBounds(*type.upperBounds.map { replaceTypeVariable(it, resolving) }.toTypedArray()) - .withLowerBounds(*type.lowerBounds.map { replaceTypeVariable(it, resolving) }.toTypedArray()) - .build() - is ResolvedType -> { - if (type.typeParameters.isEmpty()) { - type.erasedType - } else { - val actualTypeArguments = type.typeParameters.map { replaceTypeVariable(it) }.toTypedArray() - ParameterizedTypeImpl(type.erasedType, actualTypeArguments, null) - } - } - is TypeVariable<*> -> { - val genericDeclaration = type.genericDeclaration - when { - // only a variable leaked from a raw type can be bound to a type containing itself (e.g. T -> List), - // erase it like the raw type does instead of expanding it forever - type in resolving -> TypeUtils.getRawType(type.bounds.first(), null) ?: Any::class.java - // the most specific type binds the variables of all its supertypes - genericDeclaration is Class<*> -> generateSequence(mostSpecificType) { (it as? ParameterizedType)?.ownerType } - // an inner class can also use the variables of its outer class, those are bound by its owner type (e.g. Connection.Entry) - .firstNotNullOfOrNull { TypeUtils.getTypeArguments(it, genericDeclaration)?.get(type) } - // keep the full type argument (e.g. List) rather than its raw class so nested generics aren't lost - ?.let { replaceTypeVariable(it, resolving + type) } - ?: type - else -> type - } - } - else -> { - type - } - } - } - } -} diff --git a/src/main/kotlin/graphql/kickstart/tools/GenericTypeResolver.kt b/src/main/kotlin/graphql/kickstart/tools/GenericTypeResolver.kt new file mode 100644 index 00000000..66a8762c --- /dev/null +++ b/src/main/kotlin/graphql/kickstart/tools/GenericTypeResolver.kt @@ -0,0 +1,92 @@ +package graphql.kickstart.tools + +import com.fasterxml.classmate.ResolvedType +import graphql.kickstart.tools.util.JavaType +import graphql.kickstart.tools.util.ParameterizedTypeImpl +import graphql.kickstart.tools.util.Primitives +import org.apache.commons.lang3.reflect.TypeUtils +import java.lang.reflect.ParameterizedType +import java.lang.reflect.TypeVariable +import java.lang.reflect.WildcardType + +/** + * @author Andrew Potter + */ +internal class GenericTypeResolver(private val containingType: JavaType, private val options: SchemaParserOptions) { + + fun getRawClass() = getRawClass(containingType) + + fun getRawClass(type: JavaType): Class<*> = TypeUtils.getRawType(type, containingType) + + fun isAssignableFrom(type: JavaType) = TypeUtils.isAssignable(type, containingType) + + /** + * Turns a type as declared in the source (a method's return or parameter type, or a field's type) into the type + * the GraphQL type is matched against: + * - type variables are resolved relative to the containing type, + * e.g. `T` of `AbstractItem.getId()` becomes `Long` for `class Item : AbstractItem()` + * - generic wrappers are replaced by their wrapped type, e.g. `CompletableFuture` becomes `Foo` + * - wildcards are replaced by their upper bound, e.g. `? extends Foo` becomes `Foo` + * - primitives are replaced by their boxed class, e.g. `int` becomes `Integer` + */ + fun resolve(javaType: JavaType): JavaType { + return when (val type = resolveTypeVariables(javaType)) { + is ParameterizedType -> { + val rawType = type.rawType + val wrapper = options.genericWrappers.find { it.type == rawType } + ?: return type + + val typeArguments = type.actualTypeArguments + if (typeArguments.size <= wrapper.index) { + throw IndexOutOfBoundsException("Generic type '${TypeUtils.toString(type)}' does not have a type argument at index ${wrapper.index}!") + } + + val unwrapsTo = wrapper.schemaWrapper.invoke(typeArguments[wrapper.index]) + resolve(unwrapsTo) + } + is TypeVariable<*> -> error("Could not resolve type variable '${TypeUtils.toLongString(type)}' relative to ${TypeUtils.toString(containingType)}") + is WildcardType -> type.upperBounds.firstOrNull() + ?: error("Unable to unwrap type, wildcard has no upper bound: $type") + is Class<*> -> if (type.isPrimitive) Primitives.wrap(type) else type + else -> error("Unable to unwrap type: $type") + } + } + + private fun resolveTypeVariables(type: JavaType, resolving: Set> = emptySet()): JavaType { + return when (type) { + is ParameterizedType -> { + val actualTypeArguments = type.actualTypeArguments.map { resolveTypeVariables(it, resolving) }.toTypedArray() + ParameterizedTypeImpl(type.rawType as Class<*>, actualTypeArguments, type.ownerType?.let { resolveTypeVariables(it, resolving) }) + } + is WildcardType -> TypeUtils.wildcardType() + .withUpperBounds(*type.upperBounds.map { resolveTypeVariables(it, resolving) }.toTypedArray()) + .withLowerBounds(*type.lowerBounds.map { resolveTypeVariables(it, resolving) }.toTypedArray()) + .build() + is ResolvedType -> { + if (type.typeParameters.isEmpty()) { + type.erasedType + } else { + val actualTypeArguments = type.typeParameters.map { resolveTypeVariables(it) }.toTypedArray() + ParameterizedTypeImpl(type.erasedType, actualTypeArguments, null) + } + } + is TypeVariable<*> -> { + val genericDeclaration = type.genericDeclaration + when { + // only a variable leaked from a raw type can be bound to a type containing itself (e.g. T -> List), + // erase it like the raw type does instead of expanding it forever + type in resolving -> TypeUtils.getRawType(type.bounds.first(), null) ?: Any::class.java + // the containing type binds the variables of all its supertypes + genericDeclaration is Class<*> -> generateSequence(containingType) { (it as? ParameterizedType)?.ownerType } + // an inner class can also use the variables of its outer class, those are bound by its owner type (e.g. Connection.Entry) + .firstNotNullOfOrNull { TypeUtils.getTypeArguments(it, genericDeclaration)?.get(type) } + // keep the full type argument (e.g. List) rather than its raw class so nested generics aren't lost + ?.let { resolveTypeVariables(it, resolving + type) } + ?: type + else -> type + } + } + else -> type + } + } +} diff --git a/src/main/kotlin/graphql/kickstart/tools/SchemaClassScanner.kt b/src/main/kotlin/graphql/kickstart/tools/SchemaClassScanner.kt index bea11ace..b48195fe 100644 --- a/src/main/kotlin/graphql/kickstart/tools/SchemaClassScanner.kt +++ b/src/main/kotlin/graphql/kickstart/tools/SchemaClassScanner.kt @@ -396,7 +396,7 @@ internal class SchemaClassScanner( handleFoundType(typeClassMatcher.match(TypeClassMatcher.PotentialMatch.parameterType( inputValueDefinition.type, inputValueJavaType, - GenericType(javaType, options).relativeToType(javaType), + GenericTypeResolver(javaType, options), InputObjectReference(inputValueDefinition) ))) } else { diff --git a/src/main/kotlin/graphql/kickstart/tools/TypeClassMatcher.kt b/src/main/kotlin/graphql/kickstart/tools/TypeClassMatcher.kt index b505b16d..b6e4096e 100644 --- a/src/main/kotlin/graphql/kickstart/tools/TypeClassMatcher.kt +++ b/src/main/kotlin/graphql/kickstart/tools/TypeClassMatcher.kt @@ -3,6 +3,7 @@ package graphql.kickstart.tools import graphql.execution.DataFetcherResult import graphql.kickstart.tools.util.GraphQLLangType import graphql.kickstart.tools.util.JavaType +import graphql.kickstart.tools.util.isSubtypeOf import graphql.language.* import graphql.schema.idl.ScalarInfo import java.lang.reflect.ParameterizedType @@ -13,10 +14,6 @@ import java.util.* */ internal class TypeClassMatcher(private val definitionsByName: Map>) { - companion object { - fun isListType(realType: ParameterizedType, generic: GenericType) = generic.isTypeAssignableFromRawClass(realType, Iterable::class.java) - } - private fun error(potentialMatch: PotentialMatch, msg: String) = SchemaClassScannerError("Unable to match type definition (${potentialMatch.graphQLType}) for reference ${potentialMatch.reference} with java type (${potentialMatch.javaType}): $msg") fun match(potentialMatch: PotentialMatch): Match { @@ -25,16 +22,16 @@ internal class TypeClassMatcher(private val definitionsByName: Map when { - realType is ParameterizedType && isListType(realType, potentialMatch) -> + realType is ParameterizedType && realType.isSubtypeOf(Iterable::class.java) -> match(potentialMatch, graphQLType.type, realType.actualTypeArguments.first()) realType is Class<*> && realType.isArray -> match(potentialMatch, graphQLType.type, realType.componentType) @@ -93,8 +90,6 @@ internal class TypeClassMatcher(private val definitionsByName: Map @@ -35,8 +33,8 @@ internal abstract class FieldResolver( ?: environment?.getSource() ?: throw ResolverError("Expected DataFetchingEnvironment and source object to not be null!") - if (!this.genericType.isAssignableFrom(source.javaClass)) { - throw ResolverError("Expected source object to be an instance of '${this.genericType.getRawClass().name}' but instead got '${source.javaClass.name}'") + if (!this.typeResolver.isAssignableFrom(source.javaClass)) { + throw ResolverError("Expected source object to be an instance of '${this.typeResolver.getRawClass().name}' but instead got '${source.javaClass.name}'") } source diff --git a/src/main/kotlin/graphql/kickstart/tools/resolver/MapFieldResolver.kt b/src/main/kotlin/graphql/kickstart/tools/resolver/MapFieldResolver.kt index e2b635be..70c6d41b 100644 --- a/src/main/kotlin/graphql/kickstart/tools/resolver/MapFieldResolver.kt +++ b/src/main/kotlin/graphql/kickstart/tools/resolver/MapFieldResolver.kt @@ -21,10 +21,10 @@ internal class MapFieldResolver( field: FieldDefinition, search: FieldResolverScanner.Search, options: SchemaParserOptions, - relativeTo: JavaType -) : FieldResolver(field, search, options, relativeTo) { + mapClass: JavaType +) : FieldResolver(field, search, options) { - private var mapGenericValue: JavaType = getMapGenericType(relativeTo) + private var mapGenericValue: JavaType = getMapGenericType(mapClass) /** * Takes a type which implements Map and tries to find the @@ -44,7 +44,7 @@ internal class MapFieldResolver( } override fun scanForMatches(): List { - return listOf(TypeClassMatcher.PotentialMatch.returnValue(field.type, mapGenericValue, genericType, SchemaClassScanner.FieldTypeReference(field.name))) + return listOf(TypeClassMatcher.PotentialMatch.returnValue(field.type, mapGenericValue, typeResolver, SchemaClassScanner.FieldTypeReference(field.name))) } override fun toString() = "MapFieldResolver{key=${field.name}}" diff --git a/src/main/kotlin/graphql/kickstart/tools/resolver/MethodFieldResolver.kt b/src/main/kotlin/graphql/kickstart/tools/resolver/MethodFieldResolver.kt index 360382b8..c65f6ed6 100644 --- a/src/main/kotlin/graphql/kickstart/tools/resolver/MethodFieldResolver.kt +++ b/src/main/kotlin/graphql/kickstart/tools/resolver/MethodFieldResolver.kt @@ -41,7 +41,7 @@ internal class MethodFieldResolver( search: FieldResolverScanner.Search, options: SchemaParserOptions, val method: Method -) : FieldResolver(field, search, options, search.type) { +) : FieldResolver(field, search, options) { private val log = LoggerFactory.getLogger(javaClass) @@ -72,11 +72,11 @@ internal class MethodFieldResolver( this.field.inputValueDefinitions.forEachIndexed { index, definition -> val parameterType = this.getMethodParameterType(index) - ?.apply { genericType.getRawClass(this) } + ?.apply { typeResolver.getRawClass(this) } ?: throw ResolverError("Missing method type at position ${this.getJavaMethodParameterIndex(index)}, this is most likely a bug with graphql-java-tools") val isNonNull = definition.type is NonNullType - val isOptional = this.genericType.getRawClass(parameterType) == Optional::class.java + val isOptional = this.typeResolver.getRawClass(parameterType) == Optional::class.java args.add { environment -> val argumentPresent = environment.arguments.containsKey(definition.name) @@ -130,7 +130,7 @@ internal class MethodFieldResolver( // the class the return type unwraps to, so a value that happens to implement Future isn't mistaken for a wrapper. // Object (e.g. for a union) can't tell them apart, so then every future of a method declared to return Future is checked - val valueClass = (TypeUtils.getRawType(getUnwrappedReturnType(), null) ?: Any::class.java) + val valueClass = (TypeUtils.getRawType(getResolvedReturnType(), null) ?: Any::class.java) .takeUnless { it == Any::class.java && TypeUtils.getRawType(getReturnType(), null) == Future::class.java } return if (numberOfParameters > 0 || isSuspendFunction) { @@ -153,8 +153,8 @@ internal class MethodFieldResolver( */ private fun isConcreteScalarType(environment: DataFetchingEnvironment, type: Type<*>, genericParameterType: JavaType): Boolean { return when (type) { - is ListType -> List::class.java.isAssignableFrom(this.genericType.getRawClass(genericParameterType)) - && isConcreteScalarType(environment, type.type, this.genericType.unwrapGenericType(genericParameterType)) + is ListType -> List::class.java.isAssignableFrom(this.typeResolver.getRawClass(genericParameterType)) + && isConcreteScalarType(environment, type.type, this.typeResolver.resolve(genericParameterType)) is TypeName -> environment.graphQLSchema?.getType(type.name!!)?.let { isScalar(it) && type.name != "ID" } ?: false @@ -165,10 +165,10 @@ internal class MethodFieldResolver( } override fun scanForMatches(): List { - val returnValueMatch = TypeClassMatcher.PotentialMatch.returnValue(field.type, getUnwrappedReturnType(), genericType, SchemaClassScanner.ReturnValueReference(method)) + val returnValueMatch = TypeClassMatcher.PotentialMatch.returnValue(field.type, getResolvedReturnType(), typeResolver, SchemaClassScanner.ReturnValueReference(method)) return field.inputValueDefinitions.mapIndexed { i, inputDefinition -> - TypeClassMatcher.PotentialMatch.parameterType(inputDefinition.type, getMethodParameterType(i)!!, genericType, SchemaClassScanner.MethodParameterReference(method, i)) + TypeClassMatcher.PotentialMatch.parameterType(inputDefinition.type, getMethodParameterType(i)!!, typeResolver, SchemaClassScanner.MethodParameterReference(method, i)) } + listOf(returnValueMatch) } @@ -186,8 +186,8 @@ internal class MethodFieldResolver( method.genericReturnType } - private fun getUnwrappedReturnType(): JavaType = - getReturnType().let { genericType.unwrapGenericType(if (search.isSubscription) it.asPublisher() else it) } + private fun getResolvedReturnType(): JavaType = + getReturnType().let { typeResolver.resolve(if (search.isSubscription) it.asPublisher() else it) } private fun getIndexOffset(): Int { return if (resolverInfo is DataClassTypeResolverInfo && !method.declaringClass.isAssignableFrom(resolverInfo.dataClassType)) { diff --git a/src/main/kotlin/graphql/kickstart/tools/resolver/MissingFieldResolver.kt b/src/main/kotlin/graphql/kickstart/tools/resolver/MissingFieldResolver.kt index 74ed5a75..0061b811 100644 --- a/src/main/kotlin/graphql/kickstart/tools/resolver/MissingFieldResolver.kt +++ b/src/main/kotlin/graphql/kickstart/tools/resolver/MissingFieldResolver.kt @@ -9,7 +9,7 @@ import graphql.schema.DataFetcher internal class MissingFieldResolver( field: FieldDefinition, options: SchemaParserOptions -) : FieldResolver(field, FieldResolverScanner.Search(Any::class.java, MissingResolverInfo(), null), options, Any::class.java) { +) : FieldResolver(field, FieldResolverScanner.Search(Any::class.java, MissingResolverInfo(), null), options) { private val missingResolverDataFetcherProvider: MissingResolverDataFetcherProvider = options.missingResolverDataFetcherProvider ?: MissingResolverDataFetcherProvider { diff --git a/src/main/kotlin/graphql/kickstart/tools/resolver/PropertyFieldResolver.kt b/src/main/kotlin/graphql/kickstart/tools/resolver/PropertyFieldResolver.kt index 6151181a..b2b498ce 100644 --- a/src/main/kotlin/graphql/kickstart/tools/resolver/PropertyFieldResolver.kt +++ b/src/main/kotlin/graphql/kickstart/tools/resolver/PropertyFieldResolver.kt @@ -19,7 +19,7 @@ internal class PropertyFieldResolver( search: FieldResolverScanner.Search, options: SchemaParserOptions, private val property: Field -) : FieldResolver(field, search, options, property.declaringClass) { +) : FieldResolver(field, search, options) { override fun createDataFetcher(): DataFetcher<*> { return PropertyFieldResolverDataFetcher(createSourceResolver(), property) @@ -30,7 +30,7 @@ internal class PropertyFieldResolver( TypeClassMatcher.PotentialMatch.returnValue( field.type, property.genericType, - genericType, + typeResolver, SchemaClassScanner.FieldTypeReference(property.toString()) ) ) diff --git a/src/main/kotlin/graphql/kickstart/tools/util/Utils.kt b/src/main/kotlin/graphql/kickstart/tools/util/Utils.kt index 24a1e8f6..8c67f589 100644 --- a/src/main/kotlin/graphql/kickstart/tools/util/Utils.kt +++ b/src/main/kotlin/graphql/kickstart/tools/util/Utils.kt @@ -41,6 +41,8 @@ internal fun JavaType.unwrap(): Class = this as Class<*> } +internal fun ParameterizedType.isSubtypeOf(type: Class<*>): Boolean = type.isAssignableFrom(unwrap()) + /** * Replaces a parameterized type whose type arguments are all unbounded wildcards, e.g. Kotlin's Page<*>, by its raw type. */