graphql.kickstart.tools.MethodFieldResolver.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of graphql-java-tools Show documentation
Show all versions of graphql-java-tools Show documentation
Tools to help map a GraphQL schema to existing Java objects.
package graphql.kickstart.tools
import com.esotericsoftware.reflectasm.MethodAccess
import com.fasterxml.jackson.core.type.TypeReference
import graphql.TrivialDataFetcher
import graphql.execution.batched.Batched
import graphql.kickstart.tools.SchemaParserOptions.GenericWrapper
import graphql.language.*
import graphql.schema.DataFetcher
import graphql.schema.DataFetchingEnvironment
import graphql.schema.GraphQLTypeUtil.isScalar
import kotlinx.coroutines.future.future
import java.lang.reflect.Method
import java.lang.reflect.ParameterizedType
import java.lang.reflect.WildcardType
import java.util.*
import kotlin.coroutines.intrinsics.suspendCoroutineUninterceptedOrReturn
import kotlin.reflect.full.valueParameters
import kotlin.reflect.jvm.javaType
import kotlin.reflect.jvm.kotlinFunction
/**
* @author Andrew Potter
*/
internal class MethodFieldResolver(field: FieldDefinition, search: FieldResolverScanner.Search, options: SchemaParserOptions, val method: Method) : FieldResolver(field, search, options, search.type) {
companion object {
fun isBatched(method: Method, search: FieldResolverScanner.Search): Boolean {
if (method.getAnnotation(Batched::class.java) != null) {
if (!search.allowBatched) {
throw ResolverError("The @Batched annotation is only allowed on non-root resolver methods, but it was found on ${search.type.unwrap().name}#${method.name}!")
}
return true
}
return false
}
}
private val additionalLastArgument =
try {
method.kotlinFunction?.valueParameters?.size ?: method.parameterCount == (field.inputValueDefinitions.size + getIndexOffset() + 1)
} catch (e: InternalError) {
method.parameterCount == (field.inputValueDefinitions.size + getIndexOffset() + 1)
}
override fun createDataFetcher(): DataFetcher<*> {
val batched = isBatched(method, search)
val args = mutableListOf()
val mapper = options.objectMapperProvider.provide(field)
// Add source argument if this is a resolver (but not a root resolver)
if (this.search.requiredFirstParameterType != null) {
val expectedType = if (batched) Iterable::class.java else this.search.requiredFirstParameterType
args.add { environment ->
val source = environment.getSource()
if (!(expectedType.isAssignableFrom(source.javaClass))) {
throw ResolverError("Source type (${source.javaClass.name}) is not expected type (${expectedType.name})!")
}
source
}
}
// Add an argument for each argument defined in the GraphQL schema
this.field.inputValueDefinitions.forEachIndexed { index, definition ->
val genericParameterType = this.getJavaMethodParameterType(index)
?: 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(genericParameterType) == Optional::class.java
val typeReference = object : TypeReference() {
override fun getType() = genericParameterType
}
args.add { environment ->
val value = environment.arguments[definition.name] ?: if (isNonNull) {
throw ResolverError("Missing required argument with name '${definition.name}', this is most likely a bug with graphql-java-tools")
} else {
null
}
if (value == null && isOptional) {
if (environment.containsArgument(definition.name)) {
return@add Optional.empty()
} else {
return@add null
}
}
if (value != null
&& genericParameterType.unwrap().isAssignableFrom(value.javaClass)
&& isScalarType(environment, definition.type, genericParameterType)) {
return@add value
}
return@add mapper.convertValue(value, typeReference)
}
}
// Add DataFetchingEnvironment/Context argument
if (this.additionalLastArgument) {
when (this.method.parameterTypes.last()) {
null -> throw ResolverError("Expected at least one argument but got none, this is most likely a bug with graphql-java-tools")
options.contextClass -> args.add { environment -> environment.getContext() }
else -> args.add { environment -> environment }
}
}
return if (batched) {
BatchedMethodFieldResolverDataFetcher(getSourceResolver(), this.method, args, options)
} else {
if (args.isEmpty() && isTrivialDataFetcher(this.method)) {
TrivialMethodFieldResolverDataFetcher(getSourceResolver(), this.method, args, options)
} else {
MethodFieldResolverDataFetcher(getSourceResolver(), this.method, args, options)
}
}
}
private fun isScalarType(environment: DataFetchingEnvironment, type: Type<*>, genericParameterType: JavaType): Boolean =
when (type) {
is ListType -> List::class.java.isAssignableFrom(this.genericType.getRawClass(genericParameterType))
&& isScalarType(environment, type.type, this.genericType.unwrapGenericType(genericParameterType))
is TypeName -> environment.graphQLSchema?.getType(type.name)?.let { isScalar(it) && !isJavaLanguageType(genericParameterType) } ?: false
is NonNullType -> isScalarType(environment, type.type, genericParameterType)
else -> false
}
private fun isJavaLanguageType(type: JavaType): Boolean =
when (type) {
is ParameterizedType -> isJavaLanguageType(type.actualTypeArguments[0])
is WildcardType -> isJavaLanguageType(type.upperBounds[0])
else -> genericType.getRawClass(type).`package`.name == "java.lang"
}
override fun scanForMatches(): List {
val batched = isBatched(method, search)
val unwrappedGenericType = genericType.unwrapGenericType(try {
method.kotlinFunction?.returnType?.javaType ?: method.genericReturnType
} catch (e: InternalError) {
method.genericReturnType
})
val returnValueMatch = TypeClassMatcher.PotentialMatch.returnValue(field.type, unwrappedGenericType, genericType, SchemaClassScanner.ReturnValueReference(method), batched)
return field.inputValueDefinitions.mapIndexed { i, inputDefinition ->
TypeClassMatcher.PotentialMatch.parameterType(inputDefinition.type, getJavaMethodParameterType(i)!!, genericType, SchemaClassScanner.MethodParameterReference(method, i), batched)
} + listOf(returnValueMatch)
}
private fun getIndexOffset(): Int {
return if (resolverInfo is DataClassTypeResolverInfo && !method.declaringClass.isAssignableFrom(resolverInfo.dataClassType)) {
1
} else {
0
}
}
private fun getJavaMethodParameterIndex(index: Int) = index + getIndexOffset()
private fun getJavaMethodParameterType(index: Int): JavaType? {
val methodIndex = getJavaMethodParameterIndex(index)
val parameters = method.parameterTypes
return if (parameters.size > methodIndex) {
method.genericParameterTypes[getJavaMethodParameterIndex(index)]
} else {
null
}
}
override fun toString() = "MethodFieldResolver{method=$method}"
}
open class MethodFieldResolverDataFetcher(private val sourceResolver: SourceResolver, method: Method, private val args: List, private val options: SchemaParserOptions) : DataFetcher {
// Convert to reflactasm reflection
private val methodAccess = MethodAccess.get(method.declaringClass)!!
private val methodIndex = methodAccess.getIndex(method.name, *method.parameterTypes)
private val isSuspendFunction = try {
method.kotlinFunction?.isSuspend == true
} catch (e: InternalError) {
false
}
private class CompareGenericWrappers {
companion object : Comparator {
override fun compare(w1: GenericWrapper, w2: GenericWrapper): Int = when {
w1.type.isAssignableFrom(w2.type) -> 1
else -> -1
}
}
}
override fun get(environment: DataFetchingEnvironment): Any? {
val source = sourceResolver(environment)
val args = this.args.map { it(environment) }.toTypedArray()
return if (isSuspendFunction) {
environment.coroutineScope().future(options.coroutineContextProvider.provide()) {
methodAccess.invokeSuspend(source, methodIndex, args)?.transformWithGenericWrapper(environment)
}
} else {
methodAccess.invoke(source, methodIndex, *args)?.transformWithGenericWrapper(environment)
}
}
private fun Any.transformWithGenericWrapper(environment: DataFetchingEnvironment): Any? {
return options.genericWrappers
.asSequence()
.filter { it.type.isInstance(this) }
.sortedWith(CompareGenericWrappers)
.firstOrNull()
?.transformer?.invoke(this, environment) ?: this
}
/**
* Function that return the object used to fetch the data
* It can be a DataFetcher or an entity
*/
@Suppress("unused")
open fun getWrappedFetchingObject(environment: DataFetchingEnvironment): Any {
return sourceResolver(environment)
}
}
open class TrivialMethodFieldResolverDataFetcher(sourceResolver: SourceResolver, method: Method, args: List, options: SchemaParserOptions) : MethodFieldResolverDataFetcher(sourceResolver, method, args, options), TrivialDataFetcher {
}
private suspend inline fun MethodAccess.invokeSuspend(target: Any, methodIndex: Int, args: Array): Any? {
return suspendCoroutineUninterceptedOrReturn { continuation ->
invoke(target, methodIndex, *args + continuation)
}
}
class BatchedMethodFieldResolverDataFetcher(sourceResolver: SourceResolver, method: Method, args: List, options: SchemaParserOptions) : MethodFieldResolverDataFetcher(sourceResolver, method, args, options) {
@Batched
override fun get(environment: DataFetchingEnvironment) = super.get(environment)
}
internal typealias ArgumentPlaceholder = (DataFetchingEnvironment) -> Any?