All Downloads are FREE. Search and download functionalities are using the official Maven repository.

graphql.kickstart.tools.MethodFieldResolver.kt Maven / Gradle / Ivy

There is a newer version: 14.0.0
Show newest version
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?





© 2015 - 2024 Weber Informatics LLC | Privacy Policy