Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
147 changes: 0 additions & 147 deletions src/main/kotlin/graphql/kickstart/tools/GenericType.kt

This file was deleted.

92 changes: 92 additions & 0 deletions src/main/kotlin/graphql/kickstart/tools/GenericTypeResolver.kt
Original file line number Diff line number Diff line change
@@ -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<T>.getId()` becomes `Long` for `class Item : AbstractItem<Long>()`
* - generic wrappers are replaced by their wrapped type, e.g. `CompletableFuture<Foo>` 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<TypeVariable<*>> = 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<T>),
// 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<Owner>.Entry)
.firstNotNullOfOrNull { TypeUtils.getTypeArguments(it, genericDeclaration)?.get(type) }
// keep the full type argument (e.g. List<Foo>) rather than its raw class so nested generics aren't lost
?.let { resolveTypeVariables(it, resolving + type) }
?: type
else -> type
}
}
else -> type
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
33 changes: 14 additions & 19 deletions src/main/kotlin/graphql/kickstart/tools/TypeClassMatcher.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -13,10 +14,6 @@ import java.util.*
*/
internal class TypeClassMatcher(private val definitionsByName: Map<String, TypeDefinition<*>>) {

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 {
Expand All @@ -25,33 +22,33 @@ internal class TypeClassMatcher(private val definitionsByName: Map<String, TypeD

private fun match(potentialMatch: PotentialMatch, graphQLType: GraphQLLangType, javaType: JavaType, root: Boolean = false): Match {

var realType = potentialMatch.generic.unwrapGenericType(javaType)
var realType = potentialMatch.typeResolver.resolve(javaType)

if (realType is ParameterizedType && potentialMatch.generic.isTypeAssignableFromRawClass(realType, DataFetcherResult::class.java)) {
if (realType is ParameterizedType && realType.isSubtypeOf(DataFetcherResult::class.java)) {
if (potentialMatch.location != Location.RETURN_TYPE) {
throw error(potentialMatch, "${DataFetcherResult::class.java.name} can only be used as a return type")
}

realType = potentialMatch.generic.unwrapGenericType(realType.actualTypeArguments.first())
realType = potentialMatch.typeResolver.resolve(realType.actualTypeArguments.first())

if (realType is ParameterizedType && potentialMatch.generic.isTypeAssignableFromRawClass(realType, DataFetcherResult::class.java)) {
if (realType is ParameterizedType && realType.isSubtypeOf(DataFetcherResult::class.java)) {
throw error(potentialMatch, "${DataFetcherResult::class.java.name} cannot be nested within itself")
}
}

var optional = false

// Handle jdk8 Optionals
if (realType is ParameterizedType && potentialMatch.generic.isTypeAssignableFromRawClass(realType, Optional::class.java)) {
if (realType is ParameterizedType && realType.isSubtypeOf(Optional::class.java)) {
optional = true

if (potentialMatch.location == Location.RETURN_TYPE && !root) {
throw error(potentialMatch, "${Optional::class.java.name} can only be used at the top level of a return type")
}

realType = potentialMatch.generic.unwrapGenericType(realType.actualTypeArguments.first())
realType = potentialMatch.typeResolver.resolve(realType.actualTypeArguments.first())

if (realType is ParameterizedType && potentialMatch.generic.isTypeAssignableFromRawClass(realType, Optional::class.java)) {
if (realType is ParameterizedType && realType.isSubtypeOf(Optional::class.java)) {
throw error(potentialMatch, "${Optional::class.java.name} cannot be nested within itself")
}
}
Expand All @@ -66,7 +63,7 @@ internal class TypeClassMatcher(private val definitionsByName: Map<String, TypeD
}

is ListType -> 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)
Expand All @@ -93,8 +90,6 @@ internal class TypeClassMatcher(private val definitionsByName: Map<String, TypeD
}
}

private fun isListType(realType: ParameterizedType, potentialMatch: PotentialMatch) = isListType(realType, potentialMatch.generic)

internal sealed interface Match

internal data class ScalarMatch(val type: ScalarTypeDefinition) : Match
Expand All @@ -109,16 +104,16 @@ internal class TypeClassMatcher(private val definitionsByName: Map<String, TypeD
internal data class PotentialMatch(
val graphQLType: GraphQLLangType,
val javaType: JavaType,
val generic: GenericType.RelativeTo,
val typeResolver: GenericTypeResolver,
val reference: SchemaClassScanner.Reference,
val location: Location
) {
companion object {
fun returnValue(graphQLType: GraphQLLangType, javaType: JavaType, generic: GenericType.RelativeTo, reference: SchemaClassScanner.Reference) =
PotentialMatch(graphQLType, javaType, generic, reference, Location.RETURN_TYPE)
fun returnValue(graphQLType: GraphQLLangType, javaType: JavaType, typeResolver: GenericTypeResolver, reference: SchemaClassScanner.Reference) =
PotentialMatch(graphQLType, javaType, typeResolver, reference, Location.RETURN_TYPE)

fun parameterType(graphQLType: GraphQLLangType, javaType: JavaType, generic: GenericType.RelativeTo, reference: SchemaClassScanner.Reference) =
PotentialMatch(graphQLType, javaType, generic, reference, Location.PARAMETER_TYPE)
fun parameterType(graphQLType: GraphQLLangType, javaType: JavaType, typeResolver: GenericTypeResolver, reference: SchemaClassScanner.Reference) =
PotentialMatch(graphQLType, javaType, typeResolver, reference, Location.PARAMETER_TYPE)
}
}
}
Loading
Loading