bug: graphql-dgs-spring-boot-micrometer uses a Timer.Sampler API not supported in micrometer 1.6

## Expected behavior

Developers should be able to use the`graphql-dgs-spring-boot-micrometer` module with Spring Boot 2.3 and Spring Boot 2.4. The first recommends micrometer 1.5 while the latter 1.6.

## Actual behavior

There's an API breakage in micrometer regarding the `Timer.Sampler` between micrometer-core 1.5 and 1.6.
The following call pattern is not supported in 1.6:

```
 timerSampler.stop(registry, Timer.builder("some.key"))
```

Ref.
* https://javadoc.io/doc/io.micrometer/micrometer-core/1.5.12/io/micrometer/core/instrument/Timer.Sample.html

---
Thanks @paulbakker for finding this!

diff --git a/graphql-dgs-spring-boot-micrometer/src/main/kotlin/com/netflix/graphql/dgs/metrics/micrometer/DgsGraphQLMetricsInstrumentation.kt b/graphql-dgs-spring-boot-micrometer/src/main/kotlin/com/netflix/graphql/dgs/metrics/micrometer/DgsGraphQLMetricsInstrumentation.kt
index 29cd8052..56da986a 100644
--- a/graphql-dgs-spring-boot-micrometer/src/main/kotlin/com/netflix/graphql/dgs/metrics/micrometer/DgsGraphQLMetricsInstrumentation.kt
+++ b/graphql-dgs-spring-boot-micrometer/src/main/kotlin/com/netflix/graphql/dgs/metrics/micrometer/DgsGraphQLMetricsInstrumentation.kt
@@ -110,11 +110,7 @@ class DgsGraphQLMetricsInstrumentation(
         baseTags: Iterable<Tag>
     ) {
         val recordedTags = Tags.of(baseTags).and(tagsProvider.getFieldFetchTags(parameters, error))
-
-        timerSampler.stop(
-            registry,
-            Timer.builder(GqlMetric.RESOLVER.key).tags(recordedTags)
-        )
+        timerSampler.stop(Timer.builder(GqlMetric.RESOLVER.key).tags(recordedTags).register(registry))
     }

     class MetricsInstrumentationState(private val registry: MeterRegistry) : InstrumentationState {
diff --git a/graphql-dgs-spring-boot-micrometer/src/main/kotlin/com/netflix/graphql/dgs/metrics/micrometer/dataloader/BatchLoaderInterceptor.kt b/graphql-dgs-spring-boot-micrometer/src/main/kotlin/com/netflix/graphql/dgs/metrics/micrometer/dataloader/BatchLoaderInterceptor.kt
index 6a180adf..14fe146b 100644
--- a/graphql-dgs-spring-boot-micrometer/src/main/kotlin/com/netflix/graphql/dgs/metrics/micrometer/dataloader/BatchLoaderInterceptor.kt
+++ b/graphql-dgs-spring-boot-micrometer/src/main/kotlin/com/netflix/graphql/dgs/metrics/micrometer/dataloader/BatchLoaderInterceptor.kt
@@ -29,14 +29,13 @@ internal class BatchLoaderInterceptor(
             pipe.to(batchLoader).whenComplete { result, _ ->
                 logger.debug("Finished timer[{}] for BatchLoader.", ID)
                 timerSampler.stop(
-                    registry,
                     Timer.builder(ID)
                         .tags(
                             Tags.of(
                                 Tag.of(GqlTag.LOADER_NAME.key, name),
                                 Tag.of(GqlTag.LOADER_BATCH_SIZE.key, result.size.toString())
                             )
-                        )
+                        ).register(registry)
                 )
             }
         } catch (exception: Exception) {
diff --git a/graphql-dgs-spring-boot-micrometer/src/main/kotlin/com/netflix/graphql/dgs/metrics/micrometer/dataloader/BatchLoaderWithContextInterceptor.kt b/graphql-dgs-spring-boot-micrometer/src/main/kotlin/com/netflix/graphql/dgs/metrics/micrometer/dataloader/BatchLoaderWithContextInterceptor.kt
index 3c7fd593..f2be6704 100644
--- a/graphql-dgs-spring-boot-micrometer/src/main/kotlin/com/netflix/graphql/dgs/metrics/micrometer/dataloader/BatchLoaderWithContextInterceptor.kt
+++ b/graphql-dgs-spring-boot-micrometer/src/main/kotlin/com/netflix/graphql/dgs/metrics/micrometer/dataloader/BatchLoaderWithContextInterceptor.kt
@@ -28,14 +28,13 @@ internal class BatchLoaderWithContextInterceptor(
             pipe.to(batchLoader).whenComplete { result, _ ->
                 logger.debug("Stopping timer[{}] for {}", ID, javaClass.simpleName)
                 timerSampler.stop(
-                    registry,
                     Timer.builder(ID)
                         .tags(
                             Tags.of(
                                 Tag.of(GqlTag.LOADER_NAME.key, name),
                                 Tag.of(GqlTag.LOADER_BATCH_SIZE.key, result.size.toString())
                             )
-                        )
+                        ).register(registry)
                 )
             }
         } catch (exception: Exception) {
diff --git a/graphql-dgs-spring-boot-micrometer/src/main/kotlin/com/netflix/graphql/dgs/metrics/micrometer/dataloader/MappedBatchLoaderInterceptor.kt b/graphql-dgs-spring-boot-micrometer/src/main/kotlin/com/netflix/graphql/dgs/metrics/micrometer/dataloader/MappedBatchLoaderInterceptor.kt
index 6d998244..b8c9ac13 100644
--- a/graphql-dgs-spring-boot-micrometer/src/main/kotlin/com/netflix/graphql/dgs/metrics/micrometer/dataloader/MappedBatchLoaderInterceptor.kt
+++ b/graphql-dgs-spring-boot-micrometer/src/main/kotlin/com/netflix/graphql/dgs/metrics/micrometer/dataloader/MappedBatchLoaderInterceptor.kt
@@ -28,14 +28,13 @@ internal class MappedBatchLoaderInterceptor(
             pipe.to(batchLoader).whenComplete { result, _ ->
                 logger.debug("Stopping timer[{}] for {}", ID, javaClass.simpleName)
                 timerSampler.stop(
-                    registry,
                     Timer.builder(ID)
                         .tags(
                             Tags.of(
                                 Tag.of(GqlTag.LOADER_NAME.key, name),
                                 Tag.of(GqlTag.LOADER_BATCH_SIZE.key, result.size.toString())
                             )
-                        )
+                        ).register(registry)
                 )
             }
         } catch (exception: Exception) {
diff --git a/graphql-dgs-spring-boot-micrometer/src/main/kotlin/com/netflix/graphql/dgs/metrics/micrometer/dataloader/MappedBatchLoaderWithContextInterceptor.kt b/graphql-dgs-spring-boot-micrometer/src/main/kotlin/com/netflix/graphql/dgs/metrics/micrometer/dataloader/MappedBatchLoaderWithContextInterceptor.kt
index cb7f8585..e09e8fd2 100644
--- a/graphql-dgs-spring-boot-micrometer/src/main/kotlin/com/netflix/graphql/dgs/metrics/micrometer/dataloader/MappedBatchLoaderWithContextInterceptor.kt
+++ b/graphql-dgs-spring-boot-micrometer/src/main/kotlin/com/netflix/graphql/dgs/metrics/micrometer/dataloader/MappedBatchLoaderWithContextInterceptor.kt
@@ -29,14 +29,13 @@ internal class MappedBatchLoaderWithContextInterceptor(
             pipe.to(batchLoader).whenComplete { result, _ ->
                 logger.debug("Stopping timer[{}] for {}", ID, javaClass.simpleName)
                 timerSampler.stop(
-                    registry,
                     Timer.builder(ID)
                         .tags(
                             Tags.of(
                                 Tag.of(GqlTag.LOADER_NAME.key, name),
                                 Tag.of(GqlTag.LOADER_BATCH_SIZE.key, result.size.toString())
                             )
-                        )
+                        ).register(registry)
                 )
             }
         } catch (exception: Exception) {

bug: graphql-dgs-spring-boot-micrometer uses a Timer.Sampler API not supported in micrometer 1.6
## Expected behavior

Developers should be able to use the`graphql-dgs-spring-boot-micrometer` module with Spring Boot 2.3 and Spring Boot 2.4. The first recommends micrometer 1.5 while the latter 1.6.

## Actual behavior

There's an API breakage in micrometer regarding the `Timer.Sampler` between micrometer-core 1.5 and 1.6.
The following call pattern is not supported in 1.6:

```
 timerSampler.stop(registry, Timer.builder("some.key"))
```

Ref.
* https://javadoc.io/doc/io.micrometer/micrometer-core/1.5.12/io/micrometer/core/instrument/Timer.Sample.html

---
Thanks @paulbakker for finding this!

/*
 * Copyright 2021 Netflix, Inc.
 *
 * 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.
 */

object Versions {
    const val KOTLIN_VERSION = "1.4.32"
    const val SPRING_VERSION = "5.2.13.RELEASE"
    const val SPRING_BOOT_VERSION = "2.3.9.RELEASE"
    const val SPRING_SECURITY_VERSION = "5.3.9.RELEASE"
    const val SPRING_CLOUD_VERSION = "Hoxton.SR10"
    const val GRAPHQL_JAVA = "16.2"
    const val GRAPHQL_JAVA_FEDERATION = "0.6.3"
    const val JACKSON_BOM = "2.12.3"
}

/*
 * Copyright 2021 Netflix, Inc.
 *
 * 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 com.netflix.graphql.mocking

import graphql.schema.*
import java.util.*

class DgsSchemaTransformer {

    fun transformSchemaWithMockProviders(schema: GraphQLSchema, mockProviders: Set<MockProvider>): GraphQLSchema {

        val mockConfig = HashMap<String, Any>()

        mockProviders.forEach { p -> mockConfig.putAll(p.provide()) }

        return transformSchema(schema, mockConfig)
    }

    fun transformSchema(schema: GraphQLSchema, mockConfig: Map<String, *>): GraphQLSchema {
        val mockFetchers = HashMap<FieldCoordinates, DataFetcher<*>>()
        val graphQLTypeVisitorStub = MockGraphQLVisitor(mockConfig, mockFetchers)
        SchemaTraverser().depthFirst(graphQLTypeVisitorStub, schema.getType("Query"))

        return schema.transform { b ->
            val newCodeRegistry = GraphQLCodeRegistry.newCodeRegistry(schema.codeRegistry)
            mockFetchers.forEach { (coordinates, dataFetcher) -> newCodeRegistry.dataFetcher(coordinates, dataFetcher) }

            b.codeRegistry(
                newCodeRegistry
                    .build()
            )
        }
    }
}

/*
 * Copyright 2021 Netflix, Inc.
 *
 * 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 com.netflix.graphql.mocking

import com.github.javafaker.Faker
import graphql.schema.*
import graphql.util.TraversalControl
import graphql.util.TraverserContext
import org.slf4j.Logger
import org.slf4j.LoggerFactory

class MockGraphQLVisitor(private val mockConfig: Map<String, Any?>, private val mockFetchers: MutableMap<FieldCoordinates, DataFetcher<*>>) : GraphQLTypeVisitorStub() {
    private val logger: Logger = LoggerFactory.getLogger(this.javaClass)
    private val providedRoots: MutableList<String?> = mutableListOf()

    override fun visitGraphQLFieldDefinition(node: GraphQLFieldDefinition?, context: TraverserContext<GraphQLSchemaElement>?): TraversalControl {
        val pathForNode = getPathForNode(context?.parentNodes, node)

        if (mockConfig.keys.any { pathForNode?.startsWith(it) == true } && !providedRoots.any { pathForNode?.startsWith(it!!) == true && pathForNode.count { c -> c == '.' } != it?.count { c -> c == '.' } }) {
            val type = if (node?.type is GraphQLNonNull) (node.type as GraphQLNonNull).wrappedType else node?.type

            val dataFetcher: DataFetcher<*> = if (mockConfig[pathForNode] != null) {
                logger.info("Returning provided mock data for {}", pathForNode)
                providedRoots.add(pathForNode)
                getProvidedMockData(pathForNode)
            } else {
                logger.info("Generating mock data for {}", pathForNode)
                when (type) {
                    is GraphQLScalarType -> {
                        DataFetcher { generateDataForScalar(type.name) }
                    }
                    is GraphQLList -> {
                        val wrappedType = when (type.wrappedType) {
                            is GraphQLNamedType -> (type.wrappedType as GraphQLNamedType).name
                            else -> return super.visitGraphQLFieldDefinition(node, context)
                        }

                        val mockedValues = (0..Faker().number().numberBetween(0, 10))
                            .map { generateDataForScalar(wrappedType) }
                            .toList()
                        DataFetcher { mockedValues }
                    }
                    else -> DataFetcher { Object() }
                }
            }

            mockFetchers[FieldCoordinates.coordinates((context?.parentNode as GraphQLObjectType).name, node?.name)] = dataFetcher
        }

        return super.visitGraphQLFieldDefinition(node, context)
    }

    private fun generateDataForScalar(type: String): Any {
        return when (type) {
            "String" -> Faker().book().title()
            "Boolean" -> Faker().bool().bool()
            "Int" -> Faker().number().randomDigit()
            "Float" -> Faker().number().randomDouble(2, 0, 100000)
            "ID" -> Faker().number().digit()
            else -> Object()
        }
    }

    private fun getProvidedMockData(pathForNode: String?): DataFetcher<*> {

        return when (val provided = mockConfig[pathForNode]) {
            is DataFetcher<*> -> provided
            else -> DataFetcher { provided }
        }
    }

    private val getPathForNode = { parents: List<GraphQLSchemaElement>?, node: GraphQLFieldDefinition? -> parents?.filterIsInstance<GraphQLFieldDefinition>()?.map { it.name }?.fold(node?.name) { n, result -> "$result.$n" } }
}

/*
 * Copyright 2020 Netflix, Inc.
 *
 * 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 com.netflix.graphql.mocking

/**
 * Interface used by the Spring integration for the GraphQL mock library to Autowire mock provider implementations.
 */
interface MockProvider {
    fun provide(): Map<String, Any>
}

/*
 * Copyright 2021 Netflix, Inc.
 *
 * 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 com.netflix.graphql.dgs

import org.dataloader.BatchLoader
import org.dataloader.BatchLoaderWithContext
import org.dataloader.MappedBatchLoader
import org.dataloader.MappedBatchLoaderWithContext

interface DataLoaderInstrumentationExtensionProvider {
    fun provide(original: BatchLoader<*, *>, name: String): BatchLoader<*, *>
    fun provide(original: BatchLoaderWithContext<*, *>, name: String): BatchLoaderWithContext<*, *>
    fun provide(original: MappedBatchLoader<*, *>, name: String): MappedBatchLoader<*, *>
    fun provide(original: MappedBatchLoaderWithContext<*, *>, name: String): MappedBatchLoaderWithContext<*, *>
}

/*
 * Copyright 2021 Netflix, Inc.
 *
 * 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 com.netflix.graphql.dgs

import com.netflix.graphql.dgs.context.DgsContext
import com.netflix.graphql.dgs.internal.DgsRequestData

/**
 * A DgsContext is created for each request to hold state during that request.
 * This builder abstraction is used to plugin implementations with more or less functionality.
 * Typically not implemented by users. See [com.netflix.graphql.dgs.context.DgsCustomContextBuilder] instead for adding custom state to the context.
 */
@Deprecated("It is not recommended to create a custom DgsContextBuilder. Use a DgsCustomContextBuilder or DgsCustomContextBuilderWithRequest to add custom state to the context. This interface will be removed in the next major version.")
interface DgsContextBuilder {
    fun build(): DgsContext

    @JvmDefault
    fun build(dgsRequestData: DgsRequestData): DgsContext = build()
}

/*
 * Copyright 2021 Netflix, Inc.
 *
 * 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 com.netflix.graphql.dgs

import com.netflix.graphql.dgs.context.DgsContext
import com.netflix.graphql.dgs.exceptions.MultipleDataLoadersDefinedException
import com.netflix.graphql.dgs.exceptions.NoDataLoaderFoundException
import graphql.cachecontrol.CacheControl
import graphql.execution.ExecutionId
import graphql.execution.ExecutionStepInfo
import graphql.execution.MergedField
import graphql.execution.directives.QueryDirectives
import graphql.language.Document
import graphql.language.Field
import graphql.language.FragmentDefinition
import graphql.language.OperationDefinition
import graphql.schema.*
import org.dataloader.DataLoader
import org.dataloader.DataLoaderRegistry
import java.util.*

class DgsDataFetchingEnvironment(private val dfe: DataFetchingEnvironment) : DataFetchingEnvironment {

    fun getDgsContext(): DgsContext {
        val context = dfe.getContext<Any>()
        if (context is DgsContext) {
            return context
        } else {
            throw RuntimeException("""Context object of type '${context::class.java.name}' is not a DgsContext. This method does not work if you have a custom implementation of DgsContextBuilder""")
        }
    }

    fun <K, V> getDataLoader(loaderClass: Class<*>): DataLoader<K, V> {
        val annotation = loaderClass.getAnnotation(DgsDataLoader::class.java)
        return if (annotation != null) {
            dfe.getDataLoader(annotation.name)
        } else {
            val loaders = loaderClass.fields.filter { it.isAnnotationPresent(DgsDataLoader::class.java) }
            if (loaders.size > 1) throw MultipleDataLoadersDefinedException(loaderClass)
            val loaderName = loaders
                .firstOrNull()?.getAnnotation(DgsDataLoader::class.java)?.name
                ?: throw NoDataLoaderFoundException(loaderClass)
            dfe.getDataLoader(loaderName)
        }
    }

    override fun <T : Any?> getSource(): T {
        return dfe.getSource()
    }

    override fun getArguments(): MutableMap<String, Any> {
        return dfe.arguments
    }

    override fun containsArgument(name: String?): Boolean {
        return dfe.containsArgument(name)
    }

    override fun <T : Any?> getArgument(name: String?): T {
        return dfe.getArgument(name)
    }

    override fun <T : Any?> getArgumentOrDefault(name: String?, defaultValue: T): T {
        return dfe.getArgumentOrDefault(name, defaultValue)
    }

    override fun <T : Any?> getContext(): T {
        return dfe.getContext()
    }

    override fun <T : Any?> getLocalContext(): T {
        return dfe.getLocalContext()
    }

    override fun <T : Any?> getRoot(): T {
        return dfe.getRoot()
    }

    override fun getFieldDefinition(): GraphQLFieldDefinition {
        return dfe.fieldDefinition
    }

    @Deprecated("Use getMergedField()")
    override fun getFields(): MutableList<Field> {
        @Suppress("DEPRECATION")
        return dfe.fields
    }

    override fun getMergedField(): MergedField {
        return dfe.mergedField
    }

    override fun getField(): Field {
        return dfe.field
    }

    override fun getFieldType(): GraphQLOutputType {
        return dfe.fieldType
    }

    override fun getExecutionStepInfo(): ExecutionStepInfo {
        return dfe.executionStepInfo
    }

    override fun getParentType(): GraphQLType {
        return dfe.parentType
    }

    override fun getGraphQLSchema(): GraphQLSchema {
        return dfe.graphQLSchema
    }

    override fun getFragmentsByName(): MutableMap<String, FragmentDefinition> {
        return dfe.fragmentsByName
    }

    override fun getExecutionId(): ExecutionId {
        return dfe.executionId
    }

    override fun getSelectionSet(): DataFetchingFieldSelectionSet {
        return dfe.selectionSet
    }

    override fun getQueryDirectives(): QueryDirectives {
        return dfe.queryDirectives
    }

    override fun <K : Any?, V : Any?> getDataLoader(dataLoaderName: String?): DataLoader<K, V> {
        return dfe.getDataLoader(dataLoaderName)
    }

    override fun getDataLoaderRegistry(): DataLoaderRegistry {
        return dfe.dataLoaderRegistry
    }

    override fun getCacheControl(): CacheControl {
        return dfe.cacheControl
    }

    override fun getLocale(): Locale {
        return dfe.locale
    }

    override fun getOperationDefinition(): OperationDefinition {
        return dfe.operationDefinition
    }

    override fun getDocument(): Document {
        return dfe.document
    }

    override fun getVariables(): MutableMap<String, Any> {
        return dfe.variables
    }
}

/*
 * Copyright 2021 Netflix, Inc.
 *
 * 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 com.netflix.graphql.dgs

import graphql.schema.DataFetcher
import graphql.schema.TypeResolver

/**
 * Required only when federation is used.
 * For federation the frameworks needs a mapping from __typename to an actual type which is done by the entitiesFetcher.
 * The typeResolver takes an instance of a concrete type, and returns its type name as defined in the schema.
 */
interface DgsFederationResolver {
    fun entitiesFetcher(): DataFetcher<Any?>
    fun typeResolver(): TypeResolver
}

/*
 * Copyright 2021 Netflix, Inc.
 *
 * 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 com.netflix.graphql.dgs

import com.jayway.jsonpath.DocumentContext
import com.jayway.jsonpath.TypeRef
import graphql.ExecutionResult
import org.springframework.http.HttpHeaders
import org.springframework.web.context.request.WebRequest

/**
 * Represents the core query executing capability of the framework.
 * Use this interface to easily execute GraphQL queries, without using the HTTP endpoint.
 * This is meant to be used in tests, and is also used internally in the framework.
 *
 * The executeAnd* methods use the [JsonPath library](https://github.com/json-path/JsonPath) library to easily get specific fields out of a nested Json structure.
 * The [executeAndGetDocumentContext] method sets up a DocumentContext, which can then be reused to get multiple fields.
 *
 * See https://netflix.github.io/dgs/query-execution-testing/
 */
interface DgsQueryExecutor {
    fun execute(query: String): ExecutionResult = execute(query = query, variables = emptyMap())
    fun execute(query: String, variables: Map<String, Any>): ExecutionResult = execute(query = query, variables = variables, operationName = null)
    fun execute(query: String, variables: Map<String, Any> = mutableMapOf(), operationName: String? = null): ExecutionResult

    fun execute(query: String, variables: Map<String, Any>, extensions: Map<String, Any>?, headers: HttpHeaders): ExecutionResult =
        execute(query = query, variables = variables, extensions = extensions, headers = headers, operationName = null)

    fun execute(
        query: String,
        variables: Map<String, Any>,
        extensions: Map<String, Any>?,
        headers: HttpHeaders?,
        operationName: String? = null,
        webRequest: WebRequest? = null
    ): ExecutionResult

    fun <T> executeAndExtractJsonPath(query: String, jsonPath: String): T
    fun <T> executeAndExtractJsonPath(query: String, jsonPath: String, variables: Map<String, Any>): T

    fun executeAndGetDocumentContext(query: String): DocumentContext
    fun executeAndGetDocumentContext(query: String, variables: Map<String, Any>): DocumentContext

    fun <T> executeAndExtractJsonPathAsObject(query: String, jsonPath: String, clazz: Class<T>): T
    fun <T> executeAndExtractJsonPathAsObject(query: String, jsonPath: String, variables: Map<String, Any>, clazz: Class<T>): T
    fun <T> executeAndExtractJsonPathAsObject(query: String, jsonPath: String, typeRef: TypeRef<T>): T
    fun <T> executeAndExtractJsonPathAsObject(query: String, jsonPath: String, variables: Map<String, Any>, typeRef: TypeRef<T>): T
}

/*
 * Copyright 2021 Netflix, Inc.
 *
 * 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 com.netflix.graphql.dgs.client

import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.KotlinModule
import com.fasterxml.jackson.module.kotlin.registerKotlinModule
import reactor.core.publisher.Mono

/**
 * Default GraphQLClient implementation. Use this class to execute GraphQL queries against a standalone DGS or the gateway.
 * The value of this client is in it's JSON parsing of responses.
 * The client is not tied to any particular HTTP client library. The actual HTTP request code is provided by the user.
 *
 * Example:
 *
 *     @Autowired
 *     RestTemplate restTemplate;
 *
 *     DefaultGraphQLClient graphQLClient = new DefaultGraphQLClient("/graphql");
 *       return graphQLClient.executeQuery(query, Collections.emptyMap(), (url, headers, body) -> {
 *       HttpHeaders httpHeaders = new HttpHeaders();
 *       headers.forEach(httpHeaders::addAll);
 *
 *       ResponseEntity<String> exchange = restTemplate.exchange(url, HttpMethod.POST, new HttpEntity<String>(body, httpHeaders), String.class);
 *       return new HttpResponse(exchange.getStatusCodeValue(), exchange.getBody());
 *    });
 */
class DefaultGraphQLClient(private val url: String) : GraphQLClient, MonoGraphQLClient {

    companion object {
        private val objectMapper: ObjectMapper = try {
            Class.forName("com.fasterxml.jackson.module.kotlin.KotlinModule\$Builder")
            ObjectMapper().registerModule(KotlinModule.Builder().nullIsSameAsDefault(true).build())
        } catch (ex: ClassNotFoundException) {
            ObjectMapper().registerKotlinModule()
        }
        private val defaultHeaders = mapOf(
            "Accept" to listOf("application/json"),
            "Content-type" to listOf("application/json")
        )

        private data class Request(val query: String, val variables: Map<String, Any>, val operationName: String?)
    }

    /**
     * Executes a query and returns a GraphQLResponse.
     * The actual HTTP request is done by an implementation of RequestExecutor, which is user provided.
     * The RequestExecutor is typically provided as a lambda expression.
     * The Accept and Content-Type headers are set. Additional headers can be set in the RequestExecutor.
     * @param query The Query as a String
     * @param variables Query variables. May be empty
     * @param operationName optional operation name
     * @param requestExecutor The code that does the actual HTTP request. Typically provided as a lambda expression.
     * @return GraphQLResponse
     * @throws GraphQLClientException when the HTTP response code is not 2xx.
     */
    override fun executeQuery(
        query: String,
        variables: Map<String, Any>,
        operationName: String?,
        requestExecutor: RequestExecutor
    ): GraphQLResponse {
        val serializedRequest = objectMapper.writeValueAsString(Request(query, variables, operationName))

        val response = requestExecutor.execute(url, defaultHeaders, serializedRequest)
        return handleResponse(response, serializedRequest)
    }

    /**
     * Executes a query and returns a GraphQLResponse.
     * The actual HTTP request is done by an implementation of RequestExecutor, which is user provided.
     * The RequestExecutor is typically provided as a lambda expression.
     * The Accept and Content-Type headers are set. Additional headers can be set in the RequestExecutor.
     * @param query The Query as a String
     * @param variables Query variables. May be empty
     * @param requestExecutor The code that does the actual HTTP request. Typically provided as a lambda expression.
     * @return GraphQLResponse
     * @throws GraphQLClientException when the HTTP response code is not 2xx.
     */
    override fun executeQuery(
        query: String,
        variables: Map<String, Any>,
        requestExecutor: RequestExecutor
    ): GraphQLResponse {
        return executeQuery(query, variables, null, requestExecutor)
    }

    /**
     * Executes a query and returns a reactive Mono<GraphQLResponse>.
     * The actual HTTP request is done by an implementation of RequestExecutor, which is user provided.
     * The RequestExecutor is typically provided as a lambda expression.
     * The Accept and Content-Type headers are set. Additional headers can be set in the RequestExecutor.
     * @param query The Query as a String
     * @param variables Query variables. May be empty
     * @param requestExecutor The code that does the actual HTTP request. Typically provided as a lambda expression.
     * @return Mono<GraphQLResponse>
     * @throws GraphQLClientException when the HTTP response code is not 2xx.
     */
    override fun reactiveExecuteQuery(
        query: String,
        variables: Map<String, Any>,
        requestExecutor: MonoRequestExecutor
    ): Mono<GraphQLResponse> {
        return reactiveExecuteQuery(query, variables, null, requestExecutor)
    }

    /**
     * Executes a query and returns a reactive Mono<GraphQLResponse>.
     * The actual HTTP request is done by an implementation of RequestExecutor, which is user provided.
     * The RequestExecutor is typically provided as a lambda expression.
     * The Accept and Content-Type headers are set. Additional headers can be set in the RequestExecutor.
     * @param query The Query as a String
     * @param variables Query variables. May be empty
     * @param operationName optional operation name
     * @param requestExecutor The code that does the actual HTTP request. Typically provided as a lambda expression.
     * @return Mono<GraphQLResponse>
     * @throws GraphQLClientException when the HTTP response code is not 2xx.
     */
    override fun reactiveExecuteQuery(
        query: String,
        variables: Map<String, Any>,
        operationName: String?,
        requestExecutor: MonoRequestExecutor
    ): Mono<GraphQLResponse> {
        val serializedRequest = objectMapper.writeValueAsString(Request(query, variables, operationName))

        return requestExecutor.execute(url, defaultHeaders, serializedRequest).map { response ->
            handleResponse(response, serializedRequest)
        }
    }

    private fun handleResponse(response: HttpResponse, requestBody: String): GraphQLResponse {
        val (statusCode, body) = response
        val headers = response.headers
        if (statusCode !in 200..299) {
            throw GraphQLClientException(statusCode, url, body ?: "", requestBody)
        }

        return GraphQLResponse(body ?: "", headers)
    }
}

/*
 * Copyright 2021 Netflix, Inc.
 *
 * 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 com.netflix.graphql.dgs.client

import reactor.core.publisher.Mono

interface GraphQLClient {
    fun executeQuery(query: String, variables: Map<String, Any>, requestExecutor: RequestExecutor): GraphQLResponse
    fun executeQuery(
        query: String,
        variables: Map<String, Any>,
        operationName: String?,
        requestExecutor: RequestExecutor
    ): GraphQLResponse
}

interface MonoGraphQLClient {
    fun reactiveExecuteQuery(
        query: String,
        variables: Map<String, Any>,
        requestExecutor: MonoRequestExecutor
    ): Mono<GraphQLResponse>

    fun reactiveExecuteQuery(
        query: String,
        variables: Map<String, Any>,
        operationName: String?,
        requestExecutor: MonoRequestExecutor
    ): Mono<GraphQLResponse>
}

@FunctionalInterface
/**
 * Code responsible for executing the HTTP request for a GraphQL query.
 * Typically provided as a lambda.
 * @param url The URL the client was configured with
 * @param headers A map of headers. The client sets some default headers such as Accept and Content-Type.
 * @param body The request body
 * @returns HttpResponse which is a representation of the HTTP status code and the response body as a String.
 */
fun interface RequestExecutor {
    fun execute(url: String, headers: Map<String, List<String>>, body: String): HttpResponse
}

data class HttpResponse(val statusCode: Int, val body: String?, val headers: Map<String, List<String>>) {
    constructor(statusCode: Int, body: String?) : this(statusCode, body, emptyMap())
}

@FunctionalInterface
/**
 * Code responsible for executing the HTTP request for a GraphQL query.
 * Typically provided as a lambda.  Reactive version (Mono)
 * @param url The URL the client was configured with
 * @param headers A map of headers. The client sets some default headers such as Accept and Content-Type.
 * @param body The request body
 * @returns Mono<HttpResponse> which is a representation of the HTTP status code and the response body as a String.
 */
fun interface MonoRequestExecutor {
    fun execute(url: String, headers: Map<String, List<String>>, body: String): Mono<HttpResponse>
}

/**
 * A transport level exception (e.g. a failed connection). This does *not* represent successful GraphQL responses that contain errors.
 */
class GraphQLClientException(statusCode: Int, url: String, response: String, request: String) :
    RuntimeException("GraphQL server $url responded with status code $statusCode: '$response'. The request sent to the server was \n$request")

/*
 * Copyright 2021 Netflix, Inc.
 *
 * 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 com.netflix.graphql.dgs.client

import com.fasterxml.jackson.annotation.JsonEnumDefaultValue
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import com.fasterxml.jackson.annotation.JsonProperty

/**
 * A representation of a GraphQL error, following the format used by a DGS and Gateway.
 */
@JsonIgnoreProperties(ignoreUnknown = true)
data class GraphQLError(
    @JsonProperty val message: String = "",
    @JsonProperty val path: List<Any> = emptyList(),
    @JsonProperty val locations: List<Any> = emptyList(),
    @JsonProperty val extensions: GraphQLErrorExtensions?
) {
    val pathAsString = path.joinToString(".")
}

@JsonIgnoreProperties(ignoreUnknown = true)
data class GraphQLErrorExtensions(
    @JsonProperty val errorType: ErrorType? = null,
    @JsonProperty val errorDetail: String? = null,
    @JsonProperty val origin: String = "",
    @JsonProperty val debugInfo: GraphQLErrorDebugInfo = GraphQLErrorDebugInfo(),
    @JsonProperty val classification: String = ""
)

@JsonIgnoreProperties(ignoreUnknown = true)
data class GraphQLErrorDebugInfo(
    @JsonProperty val subquery: String = "",
    @JsonProperty val variables: Map<String, Any> = emptyMap()
)

/**
The value of the errorType field is an enumeration of error types.
An errorType is a fairly coarse characterization of an error that should be sufficient for client side branching logic.
The enumeration of error types should remain mostly static.
More specific errors may be specified by the errorDetail field.

Http mappings are provided only as documentation.
These are rough mappings intended to provide a quick explanation of the semantics by analogy with HTTP.

@See https://docs.google.com/document/d/1FX5K0C1pyySayFmRt53FptUQ8vCf__y_WduaKT8HsbM
 */
enum class ErrorType {
    @JsonEnumDefaultValue
    /**
     Unknown error.

     For example, this error may be returned when
     an error code received from another address space belongs to
     an error space that is not known in this address space.  Also
     errors raised by APIs that do not return enough error information
     may be converted to this error.

     If a client sees an unknown errorType, it will be interpreted as UNKNOWN.
     Unknown errors MUST NOT trigger any special behavior. These MAY be treated
     by an implementation as being equivalent to INTERNAL.

     When possible, a more specific error should be provided.

     HTTP Mapping: 520 Unknown Error
     */
    UNKNOWN,

    /**
     Internal error.

     An unexpected internal error was encountered. This means that some
     invariants expected by the underlying system have been broken.
     This error code is reserved for serious errors.

     HTTP Mapping: 500 Internal Server Error
     */
    INTERNAL,

    /**
     The requested entity was not found.

     This could apply to a resource that has never existed (e.g. bad resource id),
     or a resource that no longer exists (e.g. cache expired.)

     Note to server developers: if a request is denied for an entire class
     of users, such as gradual feature rollout or undocumented whitelist,
     `NOT_FOUND` may be used. If a request is denied for some users within
     a class of users, such as user-based access control, `PERMISSION_DENIED`
     must be used.

     HTTP Mapping: 404 Not Found
     */
    NOT_FOUND,

    /**
     The request does not have valid authentication credentials.

     This is intended to be returned only for routes that require
     authentication.

     HTTP Mapping: 401 Unauthorized
     */
    UNAUTHENTICATED,

    /**
     The caller does not have permission to execute the specified
     operation.

     `PERMISSION_DENIED` must not be used for rejections
     caused by exhausting some resource or quota.
     `PERMISSION_DENIED` must not be used if the caller
     cannot be identified (use `UNAUTHENTICATED`
     instead for those errors).

     This error Type does not imply the
     request is valid or the requested entity exists or satisfies
     other pre-conditions.

     HTTP Mapping: 403 Forbidden
     */
    PERMISSION_DENIED,

    /**
     Bad Request.

     There is a problem with the request.
     Retrying the same request is not likely to succeed.
     An example would be a query or argument that cannot be deserialized.

     HTTP Mapping: 400 Bad Request
     */
    BAD_REQUEST,

    /**
     Currently Unavailable.

     The service is currently unavailable.  This is most likely a
     transient condition, which can be corrected by retrying with
     a backoff.

     HTTP Mapping: 503 Unavailable
     */
    UNAVAILABLE,

    /**
     The operation was rejected because the system is not in a state
     required for the operation's execution.  For example, the directory
     to be deleted is non-empty, an rmdir operation is applied to
     a non-directory, etc.

     Service implementers can use the following guidelines to decide
     between `FAILED_PRECONDITION` and `UNAVAILABLE`:

     - Use `UNAVAILABLE` if the client can retry just the failing call.
     - Use `FAILED_PRECONDITION` if the client should not retry until
     the system state has been explicitly fixed.  E.g., if an "rmdir"
     fails because the directory is non-empty, `FAILED_PRECONDITION`
     should be returned since the client should not retry unless
     the files are deleted from the directory.

     HTTP Mapping: 400 Bad Request or 500 Internal Server Error
     */
    FAILED_PRECONDITION
}

/*
 * Copyright 2021 Netflix, Inc.
 *
 * 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 com.netflix.graphql.dgs.client

import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.jayway.jsonpath.*
import com.jayway.jsonpath.spi.json.JacksonJsonProvider
import com.jayway.jsonpath.spi.mapper.JacksonMappingProvider
import org.slf4j.LoggerFactory

/**
 * Representation of a GraphQL response, which may contain GraphQL errors.
 * This class gives convenient JSON parsing methods to get data out of the response.
 */
data class GraphQLResponse(val json: String, val headers: Map<String, List<String>>) {

    companion object {
        private val mapper: ObjectMapper = jacksonObjectMapper()
            .registerModule(JavaTimeModule())
            .enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE)
        private val jsonPathConfig: Configuration = Configuration.builder()
            .jsonProvider(JacksonJsonProvider(mapper))
            .mappingProvider(JacksonMappingProvider(mapper)).build()
            .addOptions(Option.DEFAULT_PATH_LEAF_TO_NULL)
    }

    /**
     * A JsonPath DocumentContext. Typically only used internally.
     */
    val parsed: DocumentContext = JsonPath.using(jsonPathConfig).parse(json)

    /**
     * Map representation of data
     */

    val data: Map<String, Any> = parsed.read("data") ?: emptyMap()
    val errors: List<GraphQLError> = parsed.read("errors", object : TypeRef<List<GraphQLError>>() {}) ?: emptyList()
    private val logger = LoggerFactory.getLogger(GraphQLResponse::class.java)

    constructor(json: String) : this(json, emptyMap())

    /**
     * Deserialize data into the given class.
     * The class may need Jackson annotations for correct mapping.
     */
    fun <T> dataAsObject(clazz: Class<T>): T {
        return mapper.convertValue(data, clazz)
    }

    /**
     * Extract values given a JsonPath. The return type will be whatever type you expect.
     * Although this looks type safe, it really isn't. Make sure values map to the expected type.
     * For JSON objects, a Map is returned. If you want to deserialize to a class, use #extractValueAsObject instead.
     */
    fun <T> extractValue(path: String): T {
        val dataPath = getDataPath(path)

        try {
            return parsed.read(dataPath)
        } catch (ex: Exception) {
            logger.error("Error extracting path '$path' from data: '$data'")
            throw ex
        }
    }

    /**
     * Extract values given a JsonPath and deserialize into the given class.
     */
    fun <T> extractValueAsObject(path: String, clazz: Class<T>): T {
        val dataPath = getDataPath(path)

        try {
            return parsed.read(dataPath, clazz)
        } catch (ex: Exception) {
            logger.error("Error extracting path '$path' from data: '$data'")
            throw ex
        }
    }

    /**
     * Extract values given a JsonPath and deserialize into the given TypeRef.
     * Use this for Lists of a specific type.
     */
    fun <T> extractValueAsObject(path: String, typeRef: TypeRef<T>): T {
        val dataPath = getDataPath(path)

        try {
            return parsed.read(dataPath, typeRef)
        } catch (ex: Exception) {
            logger.error("Error extracting path '$path' from data: '$data'")
            throw ex
        }
    }

    /**
     * Extracts RequestDetails from the response if available.
     * Returns null otherwise.
     */
    fun getRequestDetails(): RequestDetails {
        return extractValueAsObject("gatewayRequestDetails", RequestDetails::class.java)
    }

    private fun getDataPath(path: String) = if (!path.startsWith("data")) "data.$path" else path

    fun hasErrors(): Boolean = errors.isNotEmpty()
}

@JsonIgnoreProperties(ignoreUnknown = true)
data class RequestDetails(val requestId: String?, val edgarLink: String?)

/*
 * Copyright 2021 Netflix, Inc.
 *
 * 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 com.netflix.graphql.dgs.autoconfig

import com.netflix.graphql.dgs.DgsComponent
import com.netflix.graphql.dgs.DgsRuntimeWiring
import graphql.scalars.ExtendedScalars
import graphql.schema.GraphQLScalarType
import graphql.schema.idl.RuntimeWiring
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration

@ConditionalOnClass(graphql.scalars.ExtendedScalars::class)
@ConditionalOnProperty(
    prefix = "dgs.graphql.extensions.scalars",
    name = ["enabled"],
    havingValue = "true",
    matchIfMissing = true
)
@Configuration(proxyBeanMethods = false)
open class DgsExtendedScalarsAutoConfiguration {

    @ConditionalOnProperty(
        prefix = "dgs.graphql.extensions.scalars.time-dates",
        name = ["enabled"],
        havingValue = "true",
        matchIfMissing = true
    )
    @Configuration(proxyBeanMethods = false)
    open class TimeExtendedScalarsAutoConfiguration {
        @Bean
        open fun timesExtendedScalarsRegistrar(): ExtendedScalarRegistrar {
            return object : AbstractExtendedScalarRegistrar() {
                override fun getScalars(): List<GraphQLScalarType> {
                    return listOf(
                        ExtendedScalars.DateTime,
                        ExtendedScalars.Date,
                        ExtendedScalars.Time
                    )
                }
            }
        }
    }

    @ConditionalOnProperty(
        prefix = "dgs.graphql.extensions.scalars.objects",
        name = ["enabled"],
        havingValue = "true",
        matchIfMissing = true
    )
    @Configuration(proxyBeanMethods = false)
    open class ObjectsExtendedScalarsAutoConfiguration {
        @Bean
        open fun objectsExtendedScalarsRegistrar(): ExtendedScalarRegistrar {
            return object : AbstractExtendedScalarRegistrar() {
                override fun getScalars(): List<GraphQLScalarType> {
                    return listOf(
                        ExtendedScalars.Object,
                        ExtendedScalars.Json,
                        ExtendedScalars.Url,
                        ExtendedScalars.Locale,
                    )
                }
            }
        }
    }

    @ConditionalOnProperty(
        prefix = "dgs.graphql.extensions.scalars.numbers",
        name = ["enabled"],
        havingValue = "true",
        matchIfMissing = true
    )
    @Configuration(proxyBeanMethods = false)
    open class NumbersExtendedScalarsAutoConfiguration {
        @Bean
        open fun numbersExtendedScalarsRegistrar(): ExtendedScalarRegistrar {
            return object : AbstractExtendedScalarRegistrar() {
                override fun getScalars(): List<GraphQLScalarType> {
                    return listOf(
                        // Integers
                        ExtendedScalars.PositiveInt,
                        ExtendedScalars.NegativeInt,
                        ExtendedScalars.NonNegativeInt,
                        ExtendedScalars.NonPositiveInt,
                        // Floats
                        ExtendedScalars.PositiveFloat,
                        ExtendedScalars.NegativeFloat,
                        ExtendedScalars.NonNegativeFloat,
                        ExtendedScalars.NonPositiveFloat,
                        // Others
                        ExtendedScalars.GraphQLLong,
                        ExtendedScalars.GraphQLShort,
                        ExtendedScalars.GraphQLByte,
                        ExtendedScalars.GraphQLBigDecimal,
                        ExtendedScalars.GraphQLBigInteger
                    )
                }
            }
        }
    }

    @ConditionalOnProperty(
        prefix = "dgs.graphql.extensions.scalars.chars",
        name = ["enabled"],
        havingValue = "true",
        matchIfMissing = true
    )
    @Configuration(proxyBeanMethods = false)
    open class CharsExtendedScalarsAutoConfiguration {
        @Bean
        open fun charsExtendedScalarsRegistrar(): ExtendedScalarRegistrar {
            return object : AbstractExtendedScalarRegistrar() {
                override fun getScalars(): List<GraphQLScalarType> {
                    return listOf(ExtendedScalars.GraphQLChar)
                }
            }
        }
    }

    @DgsComponent
    @FunctionalInterface
    fun interface ExtendedScalarRegistrar {
        fun getScalars(): List<GraphQLScalarType>
    }

    abstract class AbstractExtendedScalarRegistrar : ExtendedScalarRegistrar {

        @DgsRuntimeWiring
        fun addScalar(builder: RuntimeWiring.Builder): RuntimeWiring.Builder {
            return getScalars().foldRight(builder) { a, acc -> acc.scalar(a) }
        }
    }
}

/*
 * Copyright 2021 Netflix, Inc.
 *
 * 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 com.netflix.graphql.dgs.metrics

import io.micrometer.core.instrument.Tag

object DgsMetrics {

    /** Defines the GQL Metrics emitted by the framework. */
    enum class GqlMetric(val key: String) {
        /** _Timer_ that captures the elapsed time of a GraphQL query execution..*/
        QUERY("gql.query"),

        /** _Counter_ that captures the number of GraphQL errors encountered during query execution. */
        ERROR("gql.error"),

        /**
         * _Timer_ that captures the elapsed time for each data fetcher invocation.
         * This is useful if you want to find data fetchers that might be responsible for poor query performance.
         * Note that this metric is not available if used with a batch loader.
         */
        RESOLVER("gql.resolver"),

        /**
         * _Timer_ that captures the elapse time for a data loader invocation for batch of queries.
         * This is useful if you want to find data loaders that might be responsible for poor query performance.
         */
        DATA_LOADER("gql.dataLoader"),
    }

    /** Defines the tags applied to the [GqlMetric] emitted by the framework. */
    enum class GqlTag(val key: String) {
        /** The sanitized query path. */
        PATH("gql.path"),

        /** The GraphQL error code, such as VALIDATION, INTERNAL, etc. */
        ERROR_CODE("gql.errorCode"),

        /** Optional flag containing additional details, if present. */
        ERROR_DETAIL("gql.errorDetail"),

        /** Name of the data fetcher. This has the ${parentType}.${field} format as specified in the @DgsData annotation. */
        FIELD("gql.field"),

        /** The number of queries executed in the batch. */
        LOADER_BATCH_SIZE("gql.loaderBatchSize"),

        /** The name of the data loader, may or may not be the same as the type of entity. */
        LOADER_NAME("gql.loaderName"),

        /** Used to capture the result of an action, e.g. `ERROR` or `SUCCESS`.*/
        OUTCOME("outcome")
    }

    enum class GqlTagValue(val owner: GqlTag, val value: String) {
        /** Value used to reflect as successful  outcome.*/
        SUCCESS(GqlTag.OUTCOME, "success"),
        /** Value used to reflect a general failure.*/
        FAILURE(GqlTag.OUTCOME, "failure");

        val tag: Tag = Tag.of(owner.key, value)
    }
}

/*
 * Copyright 2021 Netflix, Inc.
 *
 * 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 com.netflix.graphql.dgs.autoconfig

import com.netflix.graphql.dgs.DgsContextBuilder
import com.netflix.graphql.dgs.DgsFederationResolver
import com.netflix.graphql.dgs.DgsQueryExecutor
import com.netflix.graphql.dgs.context.DgsCustomContextBuilder
import com.netflix.graphql.dgs.context.DgsCustomContextBuilderWithRequest
import com.netflix.graphql.dgs.exceptions.DefaultDataFetcherExceptionHandler
import com.netflix.graphql.dgs.internal.DefaultDgsGraphQLContextBuilder
import com.netflix.graphql.dgs.internal.DefaultDgsQueryExecutor
import com.netflix.graphql.dgs.internal.DefaultDgsQueryExecutor.ReloadSchemaIndicator
import com.netflix.graphql.dgs.internal.DgsDataLoaderProvider
import com.netflix.graphql.dgs.internal.DgsSchemaProvider
import com.netflix.graphql.dgs.scalars.UploadScalar
import com.netflix.graphql.mocking.MockProvider
import graphql.execution.*
import graphql.execution.instrumentation.ChainedInstrumentation
import graphql.execution.instrumentation.Instrumentation
import graphql.schema.GraphQLCodeRegistry
import graphql.schema.GraphQLSchema
import graphql.schema.idl.TypeDefinitionRegistry
import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.boot.autoconfigure.ImportAutoConfiguration
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration
import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.context.ApplicationContext
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.core.env.Environment
import java.util.*

/**
 * Framework auto configuration based on open source Spring only, without Netflix integrations.
 * This does NOT have logging, tracing, metrics and security integration.
 */
@Configuration
@EnableConfigurationProperties(DgsConfigurationProperties::class)
@ImportAutoConfiguration(classes = [JacksonAutoConfiguration::class])
open class DgsAutoConfiguration(
    private val configProps: DgsConfigurationProperties
) {

    @Bean
    open fun dgsQueryExecutor(
        applicationContext: ApplicationContext,
        schema: GraphQLSchema,
        schemaProvider: DgsSchemaProvider,
        dgsDataLoaderProvider: DgsDataLoaderProvider,
        dgsContextBuilder: DgsContextBuilder,
        dataFetcherExceptionHandler: DataFetcherExceptionHandler,
        chainedInstrumentation: ChainedInstrumentation,
        environment: Environment,
        @Qualifier("query") providedQueryExecutionStrategy: Optional<ExecutionStrategy>,
        @Qualifier("mutation") providedMutationExecutionStrategy: Optional<ExecutionStrategy>,
        idProvider: Optional<ExecutionIdProvider>,
        reloadSchemaIndicator: ReloadSchemaIndicator
    ): DgsQueryExecutor {

        val queryExecutionStrategy = providedQueryExecutionStrategy.orElse(AsyncExecutionStrategy(dataFetcherExceptionHandler))
        val mutationExecutionStrategy = providedMutationExecutionStrategy.orElse(AsyncSerialExecutionStrategy(dataFetcherExceptionHandler))
        return DefaultDgsQueryExecutor(
            schema,
            schemaProvider,
            dgsDataLoaderProvider,
            dgsContextBuilder,
            chainedInstrumentation,
            queryExecutionStrategy,
            mutationExecutionStrategy,
            idProvider,
            reloadSchemaIndicator
        )
    }

    @Bean
    open fun dgsDataLoaderProvider(applicationContext: ApplicationContext): DgsDataLoaderProvider {
        return DgsDataLoaderProvider(applicationContext)
    }

    @Bean
    open fun dgsInstrumentation(instrumentation: Optional<List<Instrumentation>>): ChainedInstrumentation {
        val listOfInstrumentations = instrumentation.orElse(emptyList())
        return ChainedInstrumentation(listOfInstrumentations)
    }

    /**
     * Used by the [DefaultDgsQueryExecutor], it controls if, and when, such executor should reload the schema.
     * This implementation will return either the boolean value of the `dgs.reload` flag
     * or `true` if the `laptop` profile is an active Spring Boot profiles.
     * <p>
     * You can provide a bean of type [ReloadSchemaIndicator] if you want to control when the
     * [DefaultDgsQueryExecutor] should reload the schema.
     *
     * @implSpec the implementation of such bean should be thread-safe.
     */
    @Bean
    @ConditionalOnMissingBean
    open fun defaultReloadSchemaIndicator(environment: Environment): ReloadSchemaIndicator {
        val isLaptopProfile = environment.activeProfiles.contains("laptop")
        val hotReloadSetting = environment.getProperty("dgs.reload", Boolean::class.java, isLaptopProfile)

        return ReloadSchemaIndicator {
            hotReloadSetting
        }
    }

    @Bean
    @ConditionalOnMissingBean
    open fun dgsSchemaProvider(
        applicationContext: ApplicationContext,
        federationResolver: Optional<DgsFederationResolver>,
        dataFetcherExceptionHandler: DataFetcherExceptionHandler,
        existingTypeDefinitionFactory: Optional<TypeDefinitionRegistry>,
        existingCodeRegistry: Optional<GraphQLCodeRegistry>,
        mockProviders: Optional<Set<MockProvider>>
    ): DgsSchemaProvider {
        return DgsSchemaProvider(
            applicationContext,
            federationResolver,
            existingTypeDefinitionFactory,
            mockProviders,
            configProps.schemaLocations
        )
    }

    @Bean
    @ConditionalOnMissingBean
    open fun dataFetcherExceptionHandler(): DataFetcherExceptionHandler {
        return DefaultDataFetcherExceptionHandler()
    }

    @Bean
    @ConditionalOnMissingBean
    open fun schema(dgsSchemaProvider: DgsSchemaProvider): GraphQLSchema {
        return dgsSchemaProvider.schema()
    }

    @Bean
    @ConditionalOnMissingBean
    open fun graphQLContextBuilder(
        dgsCustomContextBuilder: Optional<DgsCustomContextBuilder<*>>,
        dgsCustomContextBuilderWithRequest: Optional<DgsCustomContextBuilderWithRequest<*>>
    ): DgsContextBuilder {
        return DefaultDgsGraphQLContextBuilder(dgsCustomContextBuilder, dgsCustomContextBuilderWithRequest)
    }

    @Bean
    open fun uploadScalar(): UploadScalar {
        return UploadScalar()
    }
}

/*
 * Copyright 2021 Netflix, Inc.
 *
 * 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 com.netflix.graphql.dgs.autoconfig

import com.netflix.graphql.dgs.internal.DgsSchemaProvider.Companion.DEFAULT_SCHEMA_LOCATION
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.boot.context.properties.ConstructorBinding
import org.springframework.boot.context.properties.bind.DefaultValue

/**
 * Configuration properties for DGS framework.
 */
@ConstructorBinding
@ConfigurationProperties(prefix = "dgs.graphql")
@Suppress("ConfigurationProperties")
data class DgsConfigurationProperties(
    /** Location of the GraphQL schema files. */
    @DefaultValue(DEFAULT_SCHEMA_LOCATION) val schemaLocations: List<String>
)

/*
 * Copyright 2021 Netflix, Inc.
 *
 * 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 com.netflix.graphql.dgs.diagnostics

import graphql.schema.idl.errors.SchemaProblem
import org.springframework.boot.diagnostics.AbstractFailureAnalyzer
import org.springframework.boot.diagnostics.FailureAnalysis

/**
 * Spring failure analyzer that reports schema problems at startup in a more readable way.
 */
class SchemaFailureAnalyzer : AbstractFailureAnalyzer<SchemaProblem>() {
    override fun analyze(rootFailure: Throwable, cause: SchemaProblem): FailureAnalysis {
        return FailureAnalysis(
            "There are problems with the GraphQL Schema:\n" +
                cause.errors.joinToString(
                    separator = "\n\t * ",
                    prefix = "\t * ",
                    postfix = "\n"
                ),
            null,
            cause
        )
    }
}

/*
 * Copyright 2021 Netflix, Inc.
 *
 * 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 com.netflix.graphql.dgs.mvc

import com.fasterxml.jackson.core.JsonParseException
import com.fasterxml.jackson.databind.exc.InvalidDefinitionException
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import com.netflix.graphql.dgs.DgsQueryExecutor
import com.netflix.graphql.dgs.internal.utils.MultipartVariableMapper
import com.netflix.graphql.dgs.internal.utils.TimeTracer
import graphql.ExecutionResultImpl
import graphql.GraphqlErrorBuilder
import graphql.execution.reactive.CompletionStageMappingPublisher
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.http.HttpHeaders
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.*
import org.springframework.web.context.request.WebRequest
import org.springframework.web.multipart.MultipartFile

/**
 * HTTP entrypoint for the framework. Functionality in this class should be limited, so that as much code as possible
 * is reused between different transport protocols and the testing framework.
 *
 * In addition to regular graphql queries, this method also handles multipart POST requests containing files for upload.
 * This is usually a POST request that  has Content type set to multipart/form-data. Here is an example command.
 *
 * metatron curl -a somedgs https://localhost:8443/graphql -F operations='{ "query": "mutation ($input: FileUploadInput!) { uploadFile(input: $input) }", "variables": { "input": { "description": "test", "files": [null, null] } } }' \
 * -F map='{ "0": ["variables.input.files.0"], "1": ["variables.input.files.1"] }' \
 * -F '0=@file1.txt' \
 * -F '1=@file2.txt'
 *
 * Each part in a multipart request is identified by the -F and is identified by the part name - "operations, map etc."
 * The "operations" part is the graphql query containing the mutation for the file upload, with variables for files set to null.
 * The "map" part and the subsequent parts specify the path of the file in the variables of the query, and will get mapped to
 * construct the graphql query that looks like this:
 *
 * {"query": "mutation ($input: FileUploadInput!) { uploadFile(input: $input) }",
 * "variables": { "input": { "description": "test", "files": [file1.txt, file2.txt] } }
 *
 * where files map to one or more MultipartFile(s)
 *
 * The remaining parts in the request contain the mapping of file name to file path, i.e. a map of MultipartFile(s)
 * The format of a multipart request is also described here:
 * https://github.com/jaydenseric/graphql-multipart-request-spec
 */

@RestController
class DgsRestController(private val dgsQueryExecutor: DgsQueryExecutor) {

    val logger: Logger = LoggerFactory.getLogger(DgsRestController::class.java)

    // The @ConfigurationProperties bean name is <prefix>-<fqn>
    @RequestMapping(
        "#{@'dgs.graphql-com.netflix.graphql.dgs.webmvc.autoconfigure.DgsWebMvcConfigurationProperties'.path}",
        produces = ["application/json"]
    )
    fun graphql(
        @RequestBody body: String?,
        @RequestParam fileParams: Map<String, MultipartFile>?,
        @RequestParam(name = "operations") operation: String?,
        @RequestParam(name = "map") mapParam: String?,
        @RequestHeader headers: HttpHeaders,
        webRequest: WebRequest
    ): ResponseEntity<String> {

        logger.debug("Starting /graphql handling")

        val mapper = jacksonObjectMapper()
        val inputQuery: Map<String, Any>
        val queryVariables: Map<String, Any>
        val extensions: Map<String, Any>
        if (body != null) {
            logger.debug("Reading input value: '{}'", body)

            if ("application/graphql" == headers.getFirst("Content-Type")) {
                inputQuery = mapOf(Pair("query", body))
                queryVariables = emptyMap()
                extensions = emptyMap()
            } else {
                try {
                    inputQuery = body.let { mapper.readValue(it) }
                } catch (ex: JsonParseException) {
                    return ResponseEntity.badRequest()
                        .body(ex.message ?: "Error parsing query - no details found in the error message")
                }

                queryVariables = if (inputQuery.get("variables") != null) {
                    @Suppress("UNCHECKED_CAST")
                    inputQuery["variables"] as Map<String, String>
                } else {
                    emptyMap()
                }

                extensions = if (inputQuery.get("extensions") != null) {
                    @Suppress("UNCHECKED_CAST")
                    inputQuery["extensions"] as Map<String, Any>
                } else {
                    emptyMap()
                }

                logger.debug("Parsed variables: {}", queryVariables)
            }
        } else if (fileParams != null && mapParam != null && operation != null) {
            inputQuery = operation.let { mapper.readValue(it) }

            queryVariables = if (inputQuery.get("variables") != null) {
                @Suppress("UNCHECKED_CAST")
                inputQuery["variables"] as Map<String, Any>
            } else {
                emptyMap()
            }

            extensions = if (inputQuery.get("extensions") != null) {
                @Suppress("UNCHECKED_CAST")
                inputQuery["extensions"] as Map<String, Any>
            } else {
                emptyMap()
            }

            // parse the '-F map' of MultipartFile(s) containing object paths
            val fileMapInput: Map<String, List<String>> = mapParam.let { mapper.readValue(it) }
            fileMapInput.forEach { (fileKey, objectPaths) ->
                val file = fileParams[fileKey]
                if (file != null) {
                    // the variable mapper takes each multipart file and replaces the null portion of the query variables with the file
                    objectPaths.forEach { objectPath ->
                        MultipartVariableMapper.mapVariable(
                            objectPath,
                            queryVariables,
                            file
                        )
                    }
                }
            }
        } else {
            return ResponseEntity.badRequest().body("Invalid GraphQL request - no request body was provided")
        }

        val opName = inputQuery["operationName"]
        val gqlOperationName = if (opName is String?) {
            opName
        } else {
            return ResponseEntity.badRequest().body("Invalid GraphQL request - operationName must be a String")
        }

        val executionResult = TimeTracer.logTime(
            {
                dgsQueryExecutor.execute(
                    inputQuery["query"] as String,
                    queryVariables,
                    extensions,
                    headers,
                    operationName = gqlOperationName,
                    webRequest
                )
            },
            logger, "Executed query in {}ms"
        )
        logger.debug(
            "Execution result - Contains data: '{}' - Number of errors: {}",
            executionResult.isDataPresent,
            executionResult.errors.size
        )

        if (executionResult.isDataPresent && executionResult.getData<Any>() is CompletionStageMappingPublisher<*, *>) {
            return ResponseEntity.badRequest()
                .body("Trying to execute subscription on /graphql. Use /subscriptions instead!")
        }

        val result = try {
            TimeTracer.logTime(
                { mapper.writeValueAsString(executionResult.toSpecification()) },
                logger,
                "Serialized JSON result in {}ms"
            )
        } catch (ex: InvalidDefinitionException) {
            val errorMessage = "Error serializing response: ${ex.message}"
            val errorResponse = ExecutionResultImpl(GraphqlErrorBuilder.newError().message(errorMessage).build())
            logger.error(errorMessage, ex)
            mapper.writeValueAsString(errorResponse.toSpecification())
        }

        return ResponseEntity.ok(result)
    }
}

/*
 * Copyright 2021 Netflix, Inc.
 *
 * 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 com.netflix.graphql.dgs.mvc

import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.netflix.graphql.dgs.internal.DgsSchemaProvider
import graphql.ExecutionInput
import graphql.ExecutionResult
import graphql.GraphQL
import graphql.introspection.IntrospectionQuery
import graphql.schema.GraphQLSchema
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController

/**
 * Provides an HTTP endpoint to retrieve the available schema.
 */
@RestController
class DgsRestSchemaJsonController(private val schemaProvider: DgsSchemaProvider) {

    // The @ConfigurationProperties bean name is <prefix>-<fqn>
    @RequestMapping("#{@'dgs.graphql-com.netflix.graphql.dgs.webmvc.autoconfigure.DgsWebMvcConfigurationProperties'.schemaJson.path}", produces = ["application/json"])
    fun schema(): String {
        val mapper = jacksonObjectMapper()

        val graphQLSchema: GraphQLSchema = schemaProvider.schema()
        val graphQL = GraphQL.newGraphQL(graphQLSchema).build()

        val executionInput: ExecutionInput = ExecutionInput.newExecutionInput().query(IntrospectionQuery.INTROSPECTION_QUERY)
            .build()
        val execute: ExecutionResult = graphQL.execute(executionInput)

        return mapper.writeValueAsString(execute.toSpecification())
    }
}

/*
 * Copyright 2021 Netflix, Inc.
 *
 * 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 com.netflix.graphql.dgs.context

import com.netflix.graphql.dgs.internal.DgsRequestData
import graphql.schema.DataFetchingEnvironment
import org.dataloader.BatchLoaderEnvironment

/**
 * Context class that is created per request, and is added to both DataFetchingEnvironment and BatchLoaderEnvironment.
 * Custom data can be added by providing a [DgsCustomContextBuilder].
 */
open class DgsContext(val customContext: Any?, val requestData: DgsRequestData?) {

    companion object {
        @JvmStatic
        fun <T> getCustomContext(dgsContext: Any): T {
            @Suppress("UNCHECKED_CAST")
            return when (dgsContext) {
                is DgsContext -> dgsContext.customContext as T
                else -> throw RuntimeException("The context object passed to getCustomContext is not a DgsContext. It is a ${dgsContext::class.java.name} instead.")
            }
        }

        @JvmStatic
        fun <T> getCustomContext(dataFetchingEnvironment: DataFetchingEnvironment): T {
            val dgsContext = dataFetchingEnvironment.getContext<DgsContext>()
            return getCustomContext(dgsContext)
        }

        @JvmStatic
        fun <T> getCustomContext(batchLoaderEnvironment: BatchLoaderEnvironment): T {
            val dgsContext = batchLoaderEnvironment.getContext<DgsContext>()
            return getCustomContext(dgsContext)
        }

        @JvmStatic
        fun getRequestData(dataFetchingEnvironment: DataFetchingEnvironment): DgsRequestData? {
            val dgsContext = dataFetchingEnvironment.getContext<DgsContext>()
            return dgsContext.requestData
        }

        @JvmStatic
        fun getRequestData(batchLoaderEnvironment: BatchLoaderEnvironment): DgsRequestData? {
            val dgsContext = batchLoaderEnvironment.getContext<DgsContext>()
            return dgsContext.requestData
        }
    }
}

/*
 * Copyright 2020 Netflix, Inc.
 *
 * 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 com.netflix.graphql.dgs.context

/**
 * When a bean implementing this interface is found, the framework will call the [build] method for every request.
 * The result of the [build] method is placed on the [DgsContext] and can be retrieved with [DgsContext.customContext]
 * or with one of the static methods on [DgsContext] given a DataFetchingEnvironment or batchLoaderEnvironment.
 */
interface DgsCustomContextBuilder<T> {
    fun build(): T
}

/*
 * Copyright 2021 Netflix, Inc.
 *
 * 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 com.netflix.graphql.dgs.context

import org.springframework.http.HttpHeaders
import org.springframework.web.context.request.WebRequest

/**
 * When a bean implementing this interface is found, the framework will call the [build] method for every request.
 * The result of the [build] method is placed on the [DgsContext] and can be retrieved with [DgsContext.customContext]
 * or with one of the static methods on [DgsContext] given a DataFetchingEnvironment or batchLoaderEnvironment.
 */
interface DgsCustomContextBuilderWithRequest<T> {
    fun build(extensions: Map<String, Any>?, headers: HttpHeaders?, webRequest: WebRequest?): T
}

/*
 * Copyright 2021 Netflix, Inc.
 *
 * 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 com.netflix.graphql.dgs.exceptions

import com.netflix.graphql.types.errors.TypedGraphQLError
import graphql.execution.DataFetcherExceptionHandler
import graphql.execution.DataFetcherExceptionHandlerParameters
import graphql.execution.DataFetcherExceptionHandlerResult
import org.slf4j.Logger
import org.slf4j.LoggerFactory

/**
 * Default DataFetcherExceptionHandler used by the framework, can be replaced with a custom implementation.
 * The default implementation uses the Common Errors library to return GraphQL errors.
 */
class DefaultDataFetcherExceptionHandler : DataFetcherExceptionHandler {
    private val logger: Logger = LoggerFactory.getLogger(DefaultDataFetcherExceptionHandler::class.java)

    override fun onException(handlerParameters: DataFetcherExceptionHandlerParameters?): DataFetcherExceptionHandlerResult {

        val exception = handlerParameters!!.exception
        logger.error("Exception while executing data fetcher for ${handlerParameters.path}: ${exception.message}", exception)

        val springSecurityAvailable = try {
            Class.forName("org.springframework.security.access.AccessDeniedException")
            true
        } catch (ex: ClassNotFoundException) {
            false
        }

        val graphqlError = if (springSecurityAvailable && exception is org.springframework.security.access.AccessDeniedException) {
            TypedGraphQLError.newPermissionDeniedBuilder()
                .message("%s: %s", exception::class.java.name, exception.message)
                .path(handlerParameters.path).build()
        } else if (exception is DgsEntityNotFoundException) {
            TypedGraphQLError.newNotFoundBuilder().message("%s: %s", exception::class.java.name, exception.message)
                .path(handlerParameters.path).build()
        } else if (exception is DgsBadRequestException) {
            TypedGraphQLError.newBadRequestBuilder().message("%s: %s", exception::class.java.name, exception.message)
                .path(handlerParameters.path).build()
        } else {
            TypedGraphQLError.newInternalErrorBuilder().message("%s: %s", exception::class.java.name, exception.message)
                .path(handlerParameters.path).build()
        }

        return DataFetcherExceptionHandlerResult.newResult()
            .error(graphqlError)
            .build()
    }
}

/*
 * Copyright 2021 Netflix, Inc.
 *
 * 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 com.netflix.graphql.dgs.exceptions

class DgsBadRequestException(override val message: String = "Malformed or incorrect request") : RuntimeException(message)

/*
 * Copyright 2021 Netflix, Inc.
 *
 * 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 com.netflix.graphql.dgs.exceptions

class DgsEntityNotFoundException(override val message: String = "Requested entity not found") : RuntimeException(message)

/*
 * Copyright 2021 Netflix, Inc.
 *
 * 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 com.netflix.graphql.dgs.exceptions

class DgsInvalidInputArgumentException(override val message: String, override val cause: Exception? = null) : RuntimeException(message, cause)

/*
 * Copyright 2021 Netflix, Inc.
 *
 * 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 com.netflix.graphql.dgs.exceptions

import com.jayway.jsonpath.TypeRef
import com.jayway.jsonpath.spi.mapper.MappingException

data class DgsQueryExecutionDataExtractionException(val ex: Exception, val jsonResult: String, val jsonPath: String, val targetClass: String) : RuntimeException(String.format("Error deserializing data from '%s' with JsonPath '%s' and target class %s", jsonResult, jsonPath, targetClass), ex) {
    constructor(ex: MappingException, jsonResult: String, jsonPath: String, targetClass: TypeRef<*>) : this(ex, jsonResult, jsonPath, targetClass.type.typeName)
    constructor(ex: MappingException, jsonResult: String, jsonPath: String, targetClass: Class<*>) : this(ex, jsonResult, jsonPath, targetClass.name)
}

/*
 * Copyright 2021 Netflix, Inc.
 *
 * 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 com.netflix.graphql.dgs.exceptions

class InvalidDataLoaderTypeException(clazz: Class<*>) : RuntimeException("@DgsDataLoader found that doesn't implement BatchLoader: ${clazz.name}.")

/*
 * Copyright 2021 Netflix, Inc.
 *
 * 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 com.netflix.graphql.dgs.exceptions

class InvalidDgsConfigurationException(message: String) : RuntimeException(message)

/*
 * Copyright 2021 Netflix, Inc.
 *
 * 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 com.netflix.graphql.dgs.exceptions

class InvalidDgsEntityFetcher(message: String) : RuntimeException(message)

/*
 * Copyright 2021 Netflix, Inc.
 *
 * 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 com.netflix.graphql.dgs.exceptions

class InvalidTypeResolverException(msg: String) : RuntimeException(msg)

/*
 * Copyright 2021 Netflix, Inc.
 *
 * 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 com.netflix.graphql.dgs.exceptions

class MissingDgsEntityFetcherException(type: String) : RuntimeException("Missing @DgsEntityFetcher for type $type")

/*
 * Copyright 2021 Netflix, Inc.
 *
 * 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 com.netflix.graphql.dgs.exceptions

class MultipleDataLoadersDefinedException(clazz: Class<*>) : RuntimeException("Multiple data loaders found, unable to disambiguate for ${clazz.name}.")

/*
 * Copyright 2021 Netflix, Inc.
 *
 * 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 com.netflix.graphql.dgs.exceptions

class NoDataLoaderFoundException(clazz: Class<*>) : RuntimeException("No data loader found. Missing @DgsDataLoader for ${clazz.name}.")

/*
 * Copyright 2021 Netflix, Inc.
 *
 * 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 com.netflix.graphql.dgs.exceptions

class NoDgsFederationResolverFoundException : RuntimeException("@key directive was used in schema, but could not find DgsComponent implementing DgsFederationResolver.")

/*
 * Copyright 2021 Netflix, Inc.
 *
 * 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 com.netflix.graphql.dgs.exceptions

class NoSchemaFoundException : RuntimeException("No schema files found. Define schemas in src/main/resources/schema/*.graphqls")

/*
 * Copyright 2021 Netflix, Inc.
 *
 * 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 com.netflix.graphql.dgs.exceptions

import graphql.GraphQLError

class QueryException(val errors: List<GraphQLError>) : RuntimeException(errors.joinToString(separator = ", ") { it.message })

/*
 * Copyright 2021 Netflix, Inc.
 *
 * 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 com.netflix.graphql.dgs.exceptions

class UnsupportedSecuredDataLoaderException(clazz: Class<*>) : RuntimeException("Field level @DgsDataLoader is not supported on classes that use @Secured. Move your @DgsDataLoader to its own class. The offending field is in: ${clazz.name}")

/*
 * Copyright 2021 Netflix, Inc.
 *
 * 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 com.netflix.graphql.dgs.federation

import com.apollographql.federation.graphqljava._Entity
import com.netflix.graphql.dgs.DgsComponent
import com.netflix.graphql.dgs.DgsDataFetchingEnvironment
import com.netflix.graphql.dgs.DgsFederationResolver
import com.netflix.graphql.dgs.exceptions.InvalidDgsEntityFetcher
import com.netflix.graphql.dgs.exceptions.MissingDgsEntityFetcherException
import com.netflix.graphql.dgs.internal.DgsSchemaProvider
import graphql.schema.DataFetcher
import graphql.schema.DataFetchingEnvironment
import graphql.schema.TypeResolver
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired
import java.lang.IllegalArgumentException
import java.lang.reflect.InvocationTargetException
import java.util.concurrent.CompletableFuture
import java.util.concurrent.CompletionStage

@DgsComponent
open class DefaultDgsFederationResolver() :
    DgsFederationResolver {

    /**
     * This constructor is used by DgsSchemaProvider when no custom DgsFederationResolver is provided.
     * This is the most common use case.
     * The default constructor is used to extend the DefaultDgsFederationResolver. In that case injection is used to provide the schemaProvider.
     */
    constructor(providedDgsSchemaProvider: DgsSchemaProvider) : this() {
        dgsSchemaProvider = providedDgsSchemaProvider
    }

    /**
     * Used when the DefaultDgsFederationResolver is extended.
     */
    @Suppress("JoinDeclarationAndAssignment")
    @Autowired
    lateinit var dgsSchemaProvider: DgsSchemaProvider

    val logger: Logger = LoggerFactory.getLogger(DefaultDgsFederationResolver::class.java)

    override fun entitiesFetcher(): DataFetcher<Any?> {
        return DataFetcher { env ->
            dgsEntityFetchers(env)
        }
    }

    private fun dgsEntityFetchers(env: DataFetchingEnvironment): CompletableFuture<List<Any?>> {
        val resultList = env.getArgument<List<Map<String, Any>>>(_Entity.argumentName)
            .map { values ->
                val typename = values["__typename"] ?: throw RuntimeException("__typename missing from arguments in federated query")
                val fetcher = dgsSchemaProvider.entityFetchers[typename]
                    ?: throw MissingDgsEntityFetcherException(typename.toString())

                if (!fetcher.second.parameterTypes.any { it.isAssignableFrom(Map::class.java) }) {
                    throw InvalidDgsEntityFetcher("@DgsEntityFetcher ${fetcher.first::class.java.name}.${fetcher.second.name} is invalid. A DgsEntityFetcher must accept an argument of type Map<String, Object>")
                }
                try {
                    val result =
                        if (fetcher.second.parameterTypes.any { it.isAssignableFrom(DgsDataFetchingEnvironment::class.java) }) {
                            fetcher.second.invoke(fetcher.first, values, DgsDataFetchingEnvironment(env))
                        } else {
                            fetcher.second.invoke(fetcher.first, values)
                        }

                    if (result == null) {
                        throw MissingDgsEntityFetcherException(typename.toString())
                    }

                    if (result is CompletionStage<*>) {
                        result.toCompletableFuture()
                    } else {
                        CompletableFuture.completedFuture(result)
                    }
                } catch (e: Exception) {
                    if (e is InvocationTargetException && e.targetException != null) {
                        throw e.targetException
                    } else if (e is IllegalArgumentException) {
                        throw InvalidDgsEntityFetcher("@DgsEntityFetcher ${fetcher.first::class.java.name}.${fetcher.second.name} has an invalid argument. A DgsEntityFetcher must accept an argument of type Map<String, Object> and optionally, a DgsDataFetchingEnvironment")
                    } else {
                        throw e
                    }
                }
            }

        return CompletableFuture.allOf(*resultList.toTypedArray()).thenApply {
            resultList.asSequence()
                .map { cf -> cf.join() }
                .flatMap { r -> if (r is Collection<*>) r.asSequence() else sequenceOf(r) }
                .toList()
        }
    }

    open fun typeMapping(): Map<Class<*>, String> {
        return emptyMap()
    }

    override fun typeResolver(): TypeResolver {
        return TypeResolver { env ->
            val src: Any = env.getObject()

            val typeName =
                if (typeMapping().containsKey(src::class.java)) typeMapping()[src::class.java] else src::class.java.simpleName
            val type = env.schema.getObjectType(typeName)
            if (type == null) {
                logger.warn(
                    "No type definition found for {}. You probably need to provide either a type mapping, or override DefaultDgsFederationResolver.typeResolver(). Alternatively make sure the type name in the schema and your Java model match",
                    src::class.java.name
                )
            }

            type
        }
    }
}

/*
 * Copyright 2021 Netflix, Inc.
 *
 * 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 com.netflix.graphql.dgs.internal

import com.netflix.graphql.dgs.DgsContextBuilder
import com.netflix.graphql.dgs.context.DgsContext
import com.netflix.graphql.dgs.context.DgsCustomContextBuilder
import com.netflix.graphql.dgs.context.DgsCustomContextBuilderWithRequest
import com.netflix.graphql.dgs.internal.utils.TimeTracer
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.http.HttpHeaders
import org.springframework.web.context.request.WebRequest
import java.util.*

open class DefaultDgsGraphQLContextBuilder(
    private val dgsCustomContextBuilder: Optional<DgsCustomContextBuilder<*>>,
    private val dgsCustomContextBuilderWithRequest: Optional<DgsCustomContextBuilderWithRequest<*>> = Optional.empty()
) : DgsContextBuilder {
    val logger: Logger = LoggerFactory.getLogger(DefaultDgsGraphQLContextBuilder::class.java)

    override fun build(): DgsContext {
        return TimeTracer.logTime({ buildDgsContext(null) }, logger, "Created DGS context in {}ms")
    }

    override fun build(dgsRequestData: DgsRequestData): DgsContext {
        return TimeTracer.logTime({ buildDgsContext(dgsRequestData) }, logger, "Created DGS context in {}ms")
    }

    private fun buildDgsContext(dgsRequestData: DgsRequestData?): DgsContext {
        @Suppress("DEPRECATION") val customContext = when {
            dgsCustomContextBuilderWithRequest.isPresent -> dgsCustomContextBuilderWithRequest.get().build(
                dgsRequestData?.extensions ?: mapOf(),
                HttpHeaders.readOnlyHttpHeaders(
                    dgsRequestData?.headers
                        ?: HttpHeaders()
                ),
                dgsRequestData?.webRequest
            )
            dgsCustomContextBuilder.isPresent -> dgsCustomContextBuilder.get().build()
            else
            // This is for backwards compatibility - we previously made DefaultRequestData the custom context if no custom context was provided.
            -> dgsRequestData
        }

        return DgsContext(
            customContext,
            dgsRequestData
        )
    }
}

@Deprecated("Use DgsContext.requestData instead")
data class DefaultRequestData(
    @Deprecated("Use DgsContext.requestData instead") val extensions: Map<String, Any>,
    @Deprecated("Use DgsContext.requestData instead") val headers: HttpHeaders
)

data class DgsRequestData(
    val extensions: Map<String, Any>? = emptyMap(),
    val headers: HttpHeaders? = HttpHeaders.readOnlyHttpHeaders(HttpHeaders()),
    val webRequest: WebRequest? = null
)

/*
 * Copyright 2021 Netflix, Inc.
 *
 * 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 com.netflix.graphql.dgs.internal

import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.jayway.jsonpath.*
import com.jayway.jsonpath.spi.json.JacksonJsonProvider
import com.jayway.jsonpath.spi.mapper.JacksonMappingProvider
import com.jayway.jsonpath.spi.mapper.MappingException
import com.netflix.graphql.dgs.DgsContextBuilder
import com.netflix.graphql.dgs.DgsQueryExecutor
import com.netflix.graphql.dgs.exceptions.DgsQueryExecutionDataExtractionException
import com.netflix.graphql.dgs.exceptions.QueryException
import com.netflix.graphql.dgs.internal.DefaultDgsQueryExecutor.ReloadSchemaIndicator
import graphql.*
import graphql.execution.ExecutionIdProvider
import graphql.execution.ExecutionStrategy
import graphql.execution.NonNullableFieldWasNullError
import graphql.execution.SubscriptionExecutionStrategy
import graphql.execution.instrumentation.ChainedInstrumentation
import graphql.schema.GraphQLSchema
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.http.HttpHeaders
import org.springframework.web.context.request.WebRequest
import java.util.*
import java.util.concurrent.atomic.AtomicReference

/**
 * Main Query executing functionality. This should be reused between different transport protocols and the testing framework.
 */
class DefaultDgsQueryExecutor(
    defaultSchema: GraphQLSchema,
    private val schemaProvider: DgsSchemaProvider,
    private val dataLoaderProvider: DgsDataLoaderProvider,
    private val contextBuilder: DgsContextBuilder,
    private val chainedInstrumentation: ChainedInstrumentation,
    private val queryExecutionStrategy: ExecutionStrategy,
    private val mutationExecutionStrategy: ExecutionStrategy,
    private val idProvider: Optional<ExecutionIdProvider>,
    private val reloadIndicator: ReloadSchemaIndicator = ReloadSchemaIndicator { false }
) : DgsQueryExecutor {

    private val parseContext: ParseContext =
        JsonPath.using(
            Configuration.builder()
                .jsonProvider(JacksonJsonProvider(jacksonObjectMapper()))
                .mappingProvider(
                    JacksonMappingProvider(
                        jacksonObjectMapper()
                            .enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE)
                            .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
                    )
                ).build()
                .addOptions(Option.DEFAULT_PATH_LEAF_TO_NULL)
        )

    val logger: Logger = LoggerFactory.getLogger(DefaultDgsQueryExecutor::class.java)

    val schema = AtomicReference(defaultSchema)

    override fun execute(
        query: String,
        variables: Map<String, Any>,
        extensions: Map<String, Any>?,
        headers: HttpHeaders?,
        operationName: String?,
        webRequest: WebRequest?
    ): ExecutionResult {
        val graphQLSchema: GraphQLSchema =
            if (reloadIndicator.reloadSchema())
                schema.updateAndGet { schemaProvider.schema() }
            else
                schema.get()

        val graphQLBuilder =
            GraphQL.newGraphQL(graphQLSchema)
                .instrumentation(chainedInstrumentation)
                .queryExecutionStrategy(queryExecutionStrategy)
                .mutationExecutionStrategy(mutationExecutionStrategy)
                .subscriptionExecutionStrategy(SubscriptionExecutionStrategy())
        if (idProvider.isPresent) {
            graphQLBuilder.executionIdProvider(idProvider.get())
        }
        val graphQL = graphQLBuilder.build()

        val dgsContext = contextBuilder.build(DgsRequestData(extensions, headers, webRequest))
        val dataLoaderRegistry = dataLoaderProvider.buildRegistryWithContextSupplier({ dgsContext })
        val executionInput: ExecutionInput = ExecutionInput.newExecutionInput()
            .query(query)
            .dataLoaderRegistry(dataLoaderRegistry)
            .variables(variables)
            .operationName(operationName)
            .context(dgsContext)
            .build()

        val executionResult = try {
            graphQL.execute(executionInput)
        } catch (e: Exception) {
            logger.error("Encountered an exception while handling query $query", e)
            val errors: List<GraphQLError> = if (e is GraphQLError) listOf(e) else emptyList()
            ExecutionResultImpl(null, errors)
        }

        // Check for NonNullableFieldWasNull errors, and log them explicitly because they don't run through the exception handlers.
        if (executionResult.errors.size > 0) {
            val nullValueError = executionResult.errors.find { it is NonNullableFieldWasNullError }
            if (nullValueError != null) {
                logger.error(nullValueError.message)
            }
        }

        return executionResult
    }

    override fun execute(query: String, variables: Map<String, Any>, operationName: String?): ExecutionResult {
        return execute(
            query = query,
            variables = variables,
            operationName = operationName,
            extensions = null,
            headers = null,
            webRequest = null
        )
    }

    override fun <T> executeAndExtractJsonPath(query: String, jsonPath: String): T {
        return JsonPath.read(getJsonResult(query, emptyMap()), jsonPath)
    }

    override fun <T> executeAndExtractJsonPath(query: String, jsonPath: String, variables: Map<String, Any>): T {
        return JsonPath.read(getJsonResult(query, variables), jsonPath)
    }

    override fun <T> executeAndExtractJsonPathAsObject(query: String, jsonPath: String, clazz: Class<T>): T {
        val jsonResult = getJsonResult(query, emptyMap())
        return try {
            parseContext.parse(jsonResult).read(jsonPath, clazz)
        } catch (ex: MappingException) {
            throw DgsQueryExecutionDataExtractionException(ex, jsonResult, jsonPath, clazz)
        }
    }

    override fun <T> executeAndExtractJsonPathAsObject(query: String, jsonPath: String, variables: Map<String, Any>, clazz: Class<T>): T {
        val jsonResult = getJsonResult(query, variables)
        return try {
            parseContext.parse(jsonResult).read(jsonPath, clazz)
        } catch (ex: MappingException) {
            throw DgsQueryExecutionDataExtractionException(ex, jsonResult, jsonPath, clazz)
        }
    }

    override fun <T> executeAndExtractJsonPathAsObject(query: String, jsonPath: String, typeRef: TypeRef<T>): T {
        val jsonResult = getJsonResult(query, emptyMap())
        return try {
            parseContext.parse(jsonResult).read(jsonPath, typeRef)
        } catch (ex: MappingException) {
            throw DgsQueryExecutionDataExtractionException(ex, jsonResult, jsonPath, typeRef)
        }
    }

    override fun <T> executeAndExtractJsonPathAsObject(query: String, jsonPath: String, variables: Map<String, Any>, typeRef: TypeRef<T>): T {
        val jsonResult = getJsonResult(query, variables)
        return try {
            parseContext.parse(jsonResult).read(jsonPath, typeRef)
        } catch (ex: MappingException) {
            throw DgsQueryExecutionDataExtractionException(ex, jsonResult, jsonPath, typeRef)
        }
    }

    override fun executeAndGetDocumentContext(query: String): DocumentContext {
        return parseContext.parse(getJsonResult(query, emptyMap()))
    }

    override fun executeAndGetDocumentContext(query: String, variables: Map<String, Any>): DocumentContext {
        return parseContext.parse(getJsonResult(query, variables))
    }

    private fun getJsonResult(query: String, variables: Map<String, Any>): String {
        val executionResult = execute(query, variables)

        if (executionResult.errors.size > 0) {
            throw QueryException(executionResult.errors)
        }

        val objectMapper = ObjectMapper()
        return objectMapper.writeValueAsString(executionResult.toSpecification())
    }

    /**
     * Provides the means to identify if executor should reload the [GraphQLSchema] from the given [DgsSchemaProvider].
     * If `true` the schema will be reloaded, else the default schema, provided in the cunstructor of the [DefaultDgsQueryExecutor],
     * will be used.
     *
     * @implSpec The implementation should be thread-safe.
     */
    @FunctionalInterface
    fun interface ReloadSchemaIndicator {
        fun reloadSchema(): Boolean
    }
}

/*
 * Copyright 2021 Netflix, Inc.
 *
 * 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 com.netflix.graphql.dgs.internal

import com.netflix.graphql.dgs.DataLoaderInstrumentationExtensionProvider
import com.netflix.graphql.dgs.DgsComponent
import com.netflix.graphql.dgs.DgsDataLoader
import com.netflix.graphql.dgs.exceptions.InvalidDataLoaderTypeException
import com.netflix.graphql.dgs.exceptions.UnsupportedSecuredDataLoaderException
import org.dataloader.*
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.aop.support.AopUtils
import org.springframework.beans.factory.NoSuchBeanDefinitionException
import org.springframework.context.ApplicationContext
import org.springframework.util.ReflectionUtils
import java.util.function.Supplier
import javax.annotation.PostConstruct

/**
 * Framework implementation class responsible for finding and configuring data loaders.
 */
class DgsDataLoaderProvider(private val applicationContext: ApplicationContext) {
    val logger: Logger = LoggerFactory.getLogger(DgsDataLoaderProvider::class.java)

    private val batchLoaders = mutableListOf<Pair<BatchLoader<*, *>, DgsDataLoader>>()
    private val batchLoadersWithContext = mutableListOf<Pair<BatchLoaderWithContext<*, *>, DgsDataLoader>>()
    private val mappedBatchLoaders = mutableListOf<Pair<MappedBatchLoader<*, *>, DgsDataLoader>>()
    private val mappedBatchLoadersWithContext = mutableListOf<Pair<MappedBatchLoaderWithContext<*, *>, DgsDataLoader>>()

    fun buildRegistry(): DataLoaderRegistry {
        return buildRegistryWithContextSupplier { null }
    }

    fun <T> buildRegistryWithContextSupplier(contextSupplier: Supplier<T>): DataLoaderRegistry {
        val startTime = System.currentTimeMillis()

        val dataLoaderRegistry = DataLoaderRegistry()
        batchLoaders.forEach { dataLoaderRegistry.register(it.second.name, createDataLoader(it.first, it.second)) }
        mappedBatchLoaders.forEach {
            dataLoaderRegistry.register(
                it.second.name,
                createDataLoader(it.first, it.second)
            )
        }
        batchLoadersWithContext.forEach {
            dataLoaderRegistry.register(
                it.second.name,
                createDataLoader(it.first, it.second, contextSupplier)
            )
        }
        mappedBatchLoadersWithContext.forEach {
            dataLoaderRegistry.register(
                it.second.name,
                createDataLoader(it.first, it.second, contextSupplier)
            )
        }

        val endTime = System.currentTimeMillis()
        val totalTime = endTime - startTime
        logger.debug("Created DGS dataloader registry in {}ms", totalTime)

        return dataLoaderRegistry
    }

    @PostConstruct
    internal fun findDataLoaders() {
        addDataLoaderComponents()
        addDataLoaderFields()
    }

    private fun addDataLoaderFields() {
        applicationContext.getBeansWithAnnotation(DgsComponent::class.java).values.forEach { dgsComponent ->
            val javaClass = AopUtils.getTargetClass(dgsComponent)

            javaClass.declaredFields.asSequence().filter { it.isAnnotationPresent(DgsDataLoader::class.java) }.forEach { field ->
                if (AopUtils.isAopProxy(dgsComponent)) {
                    throw UnsupportedSecuredDataLoaderException(dgsComponent::class.java)
                }

                val annotation = field.getAnnotation(DgsDataLoader::class.java)
                ReflectionUtils.makeAccessible(field)

                when (val get = field.get(dgsComponent)) {
                    is BatchLoader<*, *> ->
                        batchLoaders.add(get to annotation)
                    is BatchLoaderWithContext<*, *> ->
                        batchLoadersWithContext.add(get to annotation)
                    is MappedBatchLoader<*, *> ->
                        mappedBatchLoaders.add(get to annotation)
                    is MappedBatchLoaderWithContext<*, *> ->
                        mappedBatchLoadersWithContext.add(get to annotation)
                    else -> throw InvalidDataLoaderTypeException(dgsComponent::class.java)
                }
            }
        }
    }

    private fun addDataLoaderComponents() {
        val dataLoaders = applicationContext.getBeansWithAnnotation(DgsDataLoader::class.java)
        dataLoaders.values.forEach { dgsComponent ->
            val javaClass = AopUtils.getTargetClass(dgsComponent)
            val annotation = javaClass.getAnnotation(DgsDataLoader::class.java)
            when (dgsComponent) {
                is BatchLoader<*, *> ->
                    batchLoaders.add(Pair(dgsComponent, annotation))
                is BatchLoaderWithContext<*, *> ->
                    batchLoadersWithContext.add(Pair(dgsComponent, annotation))
                is MappedBatchLoader<*, *> ->
                    mappedBatchLoaders.add(Pair(dgsComponent, annotation))
                is MappedBatchLoaderWithContext<*, *> ->
                    mappedBatchLoadersWithContext.add(Pair(dgsComponent, annotation))
                else -> throw InvalidDataLoaderTypeException(dgsComponent::class.java)
            }
        }
    }

    private fun createDataLoader(
        batchLoader: BatchLoader<*, *>,
        dgsDataLoader: DgsDataLoader
    ): DataLoader<out Any, out Any>? {
        val options = DataLoaderOptions.newOptions()
            .setBatchingEnabled(dgsDataLoader.batching)
            .setCachingEnabled(dgsDataLoader.caching)
        if (dgsDataLoader.maxBatchSize > 0) {
            options.setMaxBatchSize(dgsDataLoader.maxBatchSize)
        }

        val extendedBatchLoader = wrappedDataLoader(batchLoader, dgsDataLoader.name)
        return DataLoader.newDataLoader(extendedBatchLoader, options)
    }

    private fun createDataLoader(
        batchLoader: MappedBatchLoader<*, *>,
        dgsDataLoader: DgsDataLoader
    ): DataLoader<out Any, out Any>? {
        val options = DataLoaderOptions.newOptions()
            .setBatchingEnabled(dgsDataLoader.batching)
            .setCachingEnabled(dgsDataLoader.caching)
        if (dgsDataLoader.maxBatchSize > 0) {
            options.setMaxBatchSize(dgsDataLoader.maxBatchSize)
        }

        val extendedBatchLoader = wrappedDataLoader(batchLoader, dgsDataLoader.name)
        return DataLoader.newMappedDataLoader(extendedBatchLoader, options)
    }

    private fun <T> createDataLoader(
        batchLoader: BatchLoaderWithContext<*, *>,
        dgsDataLoader: DgsDataLoader,
        supplier: Supplier<T>
    ): DataLoader<out Any, out Any>? {
        val options = DataLoaderOptions.newOptions()
            .setBatchingEnabled(dgsDataLoader.batching)
            .setBatchLoaderContextProvider(supplier::get)
            .setCachingEnabled(dgsDataLoader.caching)

        if (dgsDataLoader.maxBatchSize > 0) {
            options.setMaxBatchSize(dgsDataLoader.maxBatchSize)
        }

        val extendedBatchLoader = wrappedDataLoader(batchLoader, dgsDataLoader.name)
        return DataLoader.newDataLoader(extendedBatchLoader, options)
    }

    private fun <T> createDataLoader(
        batchLoader: MappedBatchLoaderWithContext<*, *>,
        dgsDataLoader: DgsDataLoader,
        supplier: Supplier<T>
    ): DataLoader<out Any, out Any>? {
        val options = DataLoaderOptions.newOptions()
            .setBatchingEnabled(dgsDataLoader.batching)
            .setBatchLoaderContextProvider(supplier::get)
            .setCachingEnabled(dgsDataLoader.caching)

        if (dgsDataLoader.maxBatchSize > 0) {
            options.setMaxBatchSize(dgsDataLoader.maxBatchSize)
        }

        val extendedBatchLoader = wrappedDataLoader(batchLoader, dgsDataLoader.name)
        return DataLoader.newMappedDataLoader(extendedBatchLoader, options)
    }

    private inline fun <reified T> wrappedDataLoader(loader: T, name: String): T {
        try {
            val stream = applicationContext
                .getBeanProvider(DataLoaderInstrumentationExtensionProvider::class.java)
                .orderedStream()

            when (loader) {
                is BatchLoader<*, *> -> {
                    var wrappedBatchLoader: BatchLoader<*, *> = loader
                    stream.forEach { wrappedBatchLoader = it.provide(wrappedBatchLoader, name) }
                    return wrappedBatchLoader as T
                }
                is BatchLoaderWithContext<*, *> -> {
                    var wrappedBatchLoader: BatchLoaderWithContext<*, *> = loader
                    stream.forEach { wrappedBatchLoader = it.provide(wrappedBatchLoader, name) }
                    return wrappedBatchLoader as T
                }
                is MappedBatchLoader<*, *> -> {
                    var wrappedBatchLoader: MappedBatchLoader<*, *> = loader
                    stream.forEach { wrappedBatchLoader = it.provide(wrappedBatchLoader, name) }
                    return wrappedBatchLoader as T
                }
                is MappedBatchLoaderWithContext<*, *> -> {
                    var wrappedBatchLoader: MappedBatchLoaderWithContext<*, *> = loader
                    stream.forEach { wrappedBatchLoader = it.provide(wrappedBatchLoader, name) }
                    return wrappedBatchLoader as T
                }
            }
        } catch (ex: NoSuchBeanDefinitionException) {
            logger.debug("Unable to wrap the [{} : {}]", name, loader, ex)
        }
        return loader
    }
}

/*
 * Copyright 2021 Netflix, Inc.
 *
 * 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 com.netflix.graphql.dgs.internal

import com.apollographql.federation.graphqljava.Federation
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.netflix.graphql.dgs.*
import com.netflix.graphql.dgs.context.DgsContext
import com.netflix.graphql.dgs.exceptions.DgsInvalidInputArgumentException
import com.netflix.graphql.dgs.exceptions.InvalidDgsConfigurationException
import com.netflix.graphql.dgs.exceptions.InvalidTypeResolverException
import com.netflix.graphql.dgs.exceptions.NoSchemaFoundException
import com.netflix.graphql.dgs.federation.DefaultDgsFederationResolver
import com.netflix.graphql.mocking.DgsSchemaTransformer
import com.netflix.graphql.mocking.MockProvider
import graphql.TypeResolutionEnvironment
import graphql.language.InterfaceTypeDefinition
import graphql.language.TypeName
import graphql.language.UnionTypeDefinition
import graphql.schema.*
import graphql.schema.idl.RuntimeWiring
import graphql.schema.idl.SchemaParser
import graphql.schema.idl.TypeDefinitionRegistry
import graphql.schema.idl.TypeRuntimeWiring
import org.slf4j.LoggerFactory
import org.springframework.aop.support.AopUtils
import org.springframework.context.ApplicationContext
import org.springframework.core.DefaultParameterNameDiscoverer
import org.springframework.core.annotation.AnnotationUtils
import org.springframework.core.annotation.MergedAnnotations
import org.springframework.core.io.Resource
import org.springframework.core.io.support.PathMatchingResourcePatternResolver
import org.springframework.util.ReflectionUtils
import org.springframework.web.bind.annotation.RequestHeader
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.ValueConstants
import org.springframework.web.multipart.MultipartFile
import java.io.InputStreamReader
import java.lang.reflect.Method
import java.lang.reflect.Parameter
import java.nio.charset.StandardCharsets
import java.util.*
import java.util.concurrent.CompletableFuture
import java.util.concurrent.CompletionStage

/**
 * Main framework class that scans for components and configures a runtime executable schema.
 */
class DgsSchemaProvider(
    private val applicationContext: ApplicationContext,
    private val federationResolver: Optional<DgsFederationResolver>,
    private val existingTypeDefinitionRegistry: Optional<TypeDefinitionRegistry>,
    private val mockProviders: Optional<Set<MockProvider>>,
    private val schemaLocations: List<String> = listOf(DEFAULT_SCHEMA_LOCATION)
) {

    companion object {
        const val DEFAULT_SCHEMA_LOCATION = "classpath*:schema/**/*.graphql*"
    }

    val dataFetcherInstrumentationEnabled = mutableMapOf<String, Boolean>()
    val entityFetchers = mutableMapOf<String, Pair<Any, Method>>()

    private val defaultParameterNameDiscoverer = DefaultParameterNameDiscoverer()
    private val logger = LoggerFactory.getLogger(DgsSchemaProvider::class.java)

    private val objectMapper = jacksonObjectMapper().registerModule(JavaTimeModule())

    fun schema(schema: String? = null): GraphQLSchema {
        val startTime = System.currentTimeMillis()
        val dgsComponents = applicationContext.getBeansWithAnnotation(DgsComponent::class.java)
        val hasDynamicTypeRegistry =
            dgsComponents.values.any { it.javaClass.methods.any { m -> m.isAnnotationPresent(DgsTypeDefinitionRegistry::class.java) } }

        var mergedRegistry = if (schema == null) {
            findSchemaFiles(hasDynamicTypeRegistry = hasDynamicTypeRegistry).map {
                InputStreamReader(it.inputStream, StandardCharsets.UTF_8).use { reader -> SchemaParser().parse(reader) }
            }.fold(TypeDefinitionRegistry()) { a, b -> a.merge(b) }
        } else {
            SchemaParser().parse(schema)
        }

        if (existingTypeDefinitionRegistry.isPresent) {
            mergedRegistry = mergedRegistry.merge(existingTypeDefinitionRegistry.get())
        }

        DefaultDgsFederationResolver(this)

        val federationResolverInstance = federationResolver.orElseGet { DefaultDgsFederationResolver(this) }

        val entityFetcher = federationResolverInstance.entitiesFetcher()
        val typeResolver = federationResolverInstance.typeResolver()
        val codeRegistryBuilder = GraphQLCodeRegistry.newCodeRegistry()
        val runtimeWiringBuilder = RuntimeWiring.newRuntimeWiring()

        dgsComponents.values.mapNotNull { dgsComponent -> invokeDgsTypeDefinitionRegistry(dgsComponent) }
            .fold(mergedRegistry) { a, b -> a.merge(b) }
        findScalars(applicationContext, runtimeWiringBuilder)
        findDataFetchers(dgsComponents, codeRegistryBuilder, mergedRegistry)
        findTypeResolvers(dgsComponents, runtimeWiringBuilder, mergedRegistry)
        findEntityFetchers(dgsComponents)

        dgsComponents.values.forEach { dgsComponent ->
            invokeDgsCodeRegistry(
                dgsComponent,
                codeRegistryBuilder,
                mergedRegistry
            )
        }

        runtimeWiringBuilder.codeRegistry(codeRegistryBuilder.build())

        dgsComponents.values.forEach { dgsComponent -> invokeDgsRuntimeWiring(dgsComponent, runtimeWiringBuilder) }

        val graphQLSchema =
            Federation.transform(mergedRegistry, runtimeWiringBuilder.build()).fetchEntities(entityFetcher)
                .resolveEntityType(typeResolver).build()

        val endTime = System.currentTimeMillis()
        val totalTime = endTime - startTime
        logger.debug("DGS initialized schema in {}ms", totalTime)

        return if (mockProviders.isPresent) {
            DgsSchemaTransformer().transformSchemaWithMockProviders(graphQLSchema, mockProviders.get())
        } else {
            graphQLSchema
        }
    }

    private fun invokeDgsTypeDefinitionRegistry(dgsComponent: Any): TypeDefinitionRegistry? {
        return dgsComponent.javaClass.methods.filter { it.isAnnotationPresent(DgsTypeDefinitionRegistry::class.java) }
            .map { method ->
                if (method.returnType != TypeDefinitionRegistry::class.java) {
                    throw InvalidDgsConfigurationException("Method annotated with @DgsTypeDefinitionRegistry must have return type TypeDefinitionRegistry")
                }

                method.invoke(dgsComponent) as TypeDefinitionRegistry
            }.reduceOrNull { a, b -> a.merge(b) }
    }

    private fun invokeDgsCodeRegistry(
        dgsComponent: Any,
        codeRegistryBuilder: GraphQLCodeRegistry.Builder,
        registry: TypeDefinitionRegistry
    ) {
        dgsComponent.javaClass.methods.filter { it.isAnnotationPresent(DgsCodeRegistry::class.java) }
            .forEach { method ->
                if (method.returnType != GraphQLCodeRegistry.Builder::class.java) {
                    throw InvalidDgsConfigurationException("Method annotated with @DgsCodeRegistry must have return type GraphQLCodeRegistry.Builder")
                }

                if (method.parameterCount != 2 || method.parameterTypes[0] != GraphQLCodeRegistry.Builder::class.java || method.parameterTypes[1] != TypeDefinitionRegistry::class.java) {
                    throw InvalidDgsConfigurationException("Method annotated with @DgsCodeRegistry must accept the following arguments: GraphQLCodeRegistry.Builder, TypeDefinitionRegistry. ${dgsComponent.javaClass.name}.${method.name} has the following arguments: ${method.parameterTypes.joinToString()}")
                }

                method.invoke(dgsComponent, codeRegistryBuilder, registry)
            }
    }

    private fun invokeDgsRuntimeWiring(dgsComponent: Any, runtimeWiringBuilder: RuntimeWiring.Builder) {
        dgsComponent.javaClass.methods.filter { it.isAnnotationPresent(DgsRuntimeWiring::class.java) }
            .forEach { method ->
                if (method.returnType != RuntimeWiring.Builder::class.java) {
                    throw InvalidDgsConfigurationException("Method annotated with @DgsRuntimeWiring must have return type RuntimeWiring.Builder")
                }

                if (method.parameterCount != 1 || method.parameterTypes[0] != RuntimeWiring.Builder::class.java) {
                    throw InvalidDgsConfigurationException("Method annotated with @DgsRuntimeWiring must accept an argument of type RuntimeWiring.Builder. ${dgsComponent.javaClass.name}.${method.name} has the following arguments: ${method.parameterTypes.joinToString()}")
                }

                method.invoke(dgsComponent, runtimeWiringBuilder)
            }
    }

    private fun findDataFetchers(
        dgsComponents: Map<String, Any>,
        codeRegistryBuilder: GraphQLCodeRegistry.Builder,
        typeDefinitionRegistry: TypeDefinitionRegistry
    ) {
        dgsComponents.values.forEach { dgsComponent ->
            val javaClass = AopUtils.getTargetClass(dgsComponent)

            javaClass.methods.asSequence()
                .filter { method -> MergedAnnotations.from(method).isPresent(DgsData::class.java) }
                .forEach { method ->

                    val dgsDataAnnotation = MergedAnnotations.from(method).get(DgsData::class.java)
                    val field = dgsDataAnnotation.getString("field").ifEmpty { method.name }
                    val parentType = dgsDataAnnotation.getString("parentType")

                    val enableInstrumentation =
                        if (method.isAnnotationPresent(DgsEnableDataFetcherInstrumentation::class.java)) {
                            val dgsEnableDataFetcherInstrumentation =
                                method.getAnnotation(DgsEnableDataFetcherInstrumentation::class.java)
                            dgsEnableDataFetcherInstrumentation.value
                        } else {
                            method.returnType != CompletionStage::class.java && method.returnType != CompletableFuture::class.java
                        }

                    dataFetcherInstrumentationEnabled["$parentType.$field"] = enableInstrumentation

                    try {
                        if (!typeDefinitionRegistry.getType(parentType).isPresent) {
                            logger.error("Parent type $parentType not found, but it was referenced in ${javaClass.name} in @DgsData annotation for field $field")
                            throw InvalidDgsConfigurationException("Parent type $parentType not found, but it was referenced on ${javaClass.name} in @DgsData annotation for field $field")
                        }
                        val type = typeDefinitionRegistry.getType(parentType).get()
                        if (type is InterfaceTypeDefinition) {
                            val implementationsOf = typeDefinitionRegistry.getImplementationsOf(type)
                            implementationsOf.forEach { implType ->
                                val dataFetcher = createBasicDataFetcher(method, dgsComponent)
                                codeRegistryBuilder.dataFetcher(
                                    FieldCoordinates.coordinates(implType.name, field),
                                    dataFetcher
                                )
                                dataFetcherInstrumentationEnabled["${implType.name}.$field"] = enableInstrumentation
                            }
                        } else if (type is UnionTypeDefinition) {
                            type.memberTypes.filterIsInstance<TypeName>().forEach { memberType ->
                                val dataFetcher = createBasicDataFetcher(method, dgsComponent)
                                codeRegistryBuilder.dataFetcher(
                                    FieldCoordinates.coordinates(memberType.name, field),
                                    dataFetcher
                                )
                                dataFetcherInstrumentationEnabled["${memberType.name}.$field"] = enableInstrumentation
                            }
                        } else {
                            val dataFetcher = createBasicDataFetcher(method, dgsComponent)
                            codeRegistryBuilder.dataFetcher(
                                FieldCoordinates.coordinates(parentType, field),
                                dataFetcher
                            )
                        }
                    } catch (ex: Exception) {
                        logger.error("Invalid parent type $parentType")
                        throw ex
                    }
                }
        }
    }

    private fun findEntityFetchers(dgsComponents: Map<String, Any>) {
        dgsComponents.values.forEach { dgsComponent ->
            val javaClass = AopUtils.getTargetClass(dgsComponent)

            ReflectionUtils.getDeclaredMethods(javaClass).asSequence()
                .filter { it.isAnnotationPresent(DgsEntityFetcher::class.java) }
                .forEach { method ->
                    val dgsEntityFetcherAnnotation = method.getAnnotation(DgsEntityFetcher::class.java)

                    val enableInstrumentation =
                        method.getAnnotation(DgsEnableDataFetcherInstrumentation::class.java)?.value
                            ?: false
                    dataFetcherInstrumentationEnabled["${"__entities"}.${dgsEntityFetcherAnnotation.name}"] =
                        enableInstrumentation

                    entityFetchers[dgsEntityFetcherAnnotation.name] = dgsComponent to method
                }
        }
    }

    private fun createBasicDataFetcher(method: Method, dgsComponent: Any): DataFetcher<Any?> {
        return DataFetcher<Any?> { environment ->
            val result = invokeDataFetcher(method, dgsComponent, DgsDataFetchingEnvironment(environment))
            result
        }
    }

    private fun invokeDataFetcher(method: Method, dgsComponent: Any, environment: DataFetchingEnvironment): Any? {
        val args = mutableListOf<Any?>()
        val parameterNames = defaultParameterNameDiscoverer.getParameterNames(method) ?: emptyArray()
        method.parameters.forEachIndexed { idx, parameter ->

            when {
                parameter.isAnnotationPresent(InputArgument::class.java) -> {
                    val annotation = AnnotationUtils.getAnnotation(parameter, InputArgument::class.java)!!
                    val name: String = AnnotationUtils.getAnnotationAttributes(annotation)["name"] as String

                    val parameterName = name.ifBlank { parameterNames[idx] }
                    val collectionType = annotation.collectionType.java
                    val parameterValue: Any? = environment.getArgument(parameterName)

                    val convertValue: Any? = if (parameterValue is List<*> && collectionType != Object::class.java) {
                        try {
                            // Return a list of elements that are converted to their collection type, e.e.g. List<Person>, List<String> etc.
                            parameterValue.map { item -> objectMapper.convertValue(item, collectionType) }.toList()
                        } catch (ex: Exception) {
                            throw DgsInvalidInputArgumentException(
                                "Specified type '$collectionType' is invalid for $parameterName.",
                                ex
                            )
                        }
                    } else if (parameterValue is List<*>) {
                        // Return as is for all other types of Lists, i.e. custom scalars e.g. List<UUID>, and other scalar types like List<Integer> etc.
                        parameterValue
                    } else if (parameterValue is MultipartFile) {
                        parameterValue
                    } else if (environment.fieldDefinition.arguments.find { it.name == parameterName }?.type is GraphQLScalarType) {
                        // Return the value with it's type for scalars
                        parameterValue
                    } else {
                        // Return the converted value mapped to the defined type
                        if (parameter.type.isAssignableFrom(Optional::class.java)) {
                            if (collectionType != Object::class.java) {
                                objectMapper.convertValue(parameterValue, collectionType)
                            } else {
                                throw DgsInvalidInputArgumentException("When Optional<T> is used, the type must be specified using the collectionType argument of the @InputArgument annotation.")
                            }
                        } else {
                            objectMapper.convertValue(parameterValue, parameter.type)
                        }
                    }

                    val paramType = parameter.type
                    val optionalValue = getValueAsOptional(convertValue, parameter)

                    if (optionalValue != null && !paramType.isPrimitive && !paramType.isAssignableFrom(optionalValue.javaClass)) {
                        throw DgsInvalidInputArgumentException("Specified type '${parameter.type}' is invalid. Found ${parameterValue?.javaClass?.name} instead.")
                    }

                    if (convertValue == null && environment.fieldDefinition.arguments.none { it.name == parameterName }) {
                        logger.warn("Unknown argument '$parameterName' on data fetcher ${dgsComponent.javaClass.name}.${method.name}")
                    }

                    args.add(optionalValue)
                }

                parameter.isAnnotationPresent(RequestHeader::class.java) -> {
                    val annotation = AnnotationUtils.getAnnotation(parameter, RequestHeader::class.java)!!
                    val name: String = AnnotationUtils.getAnnotationAttributes(annotation)["name"] as String
                    val parameterName = name.ifBlank { parameterNames[idx] }
                    val value = DgsContext.getRequestData(environment)?.headers?.get(parameterName)?.let {
                        if (parameter.type.isAssignableFrom(List::class.java)) {
                            it
                        } else {
                            it.joinToString()
                        }
                    } ?: if (annotation.defaultValue != ValueConstants.DEFAULT_NONE) annotation.defaultValue else null

                    if (value == null && annotation.required) {
                        throw DgsInvalidInputArgumentException("Required header '$parameterName' was not provided")
                    }

                    val optionalValue = getValueAsOptional(value, parameter)
                    args.add(optionalValue)
                }

                parameter.isAnnotationPresent(RequestParam::class.java) -> {
                    val annotation = AnnotationUtils.getAnnotation(parameter, RequestParam::class.java)!!
                    val name: String = AnnotationUtils.getAnnotationAttributes(annotation)["name"] as String
                    val parameterName = name.ifBlank { parameterNames[idx] }
                    val value: Any? =
                        DgsContext.getRequestData(environment)?.webRequest?.parameterMap?.get(parameterName)?.let {
                            if (parameter.type.isAssignableFrom(List::class.java)) {
                                it
                            } else {
                                it.joinToString()
                            }
                        }
                            ?: if (annotation.defaultValue != ValueConstants.DEFAULT_NONE) annotation.defaultValue else null

                    if (value == null && annotation.required) {
                        throw DgsInvalidInputArgumentException("Required request parameter '$parameterName' was not provided")
                    }

                    val optionalValue = getValueAsOptional(value, parameter)
                    args.add(optionalValue)
                }

                environment.containsArgument(parameterNames[idx]) -> {
                    val parameterValue: Any = environment.getArgument(parameterNames[idx])
                    val convertValue = objectMapper.convertValue(parameterValue, parameter.type)
                    args.add(convertValue)
                }

                parameter.type == DataFetchingEnvironment::class.java || parameter.type == DgsDataFetchingEnvironment::class.java -> {
                    args.add(environment)
                }
                else -> {
                    logger.warn("Unknown argument '${parameterNames[idx]}' on data fetcher ${dgsComponent.javaClass.name}.${method.name}")
                    // This might cause an exception, but parameter's the best effort we can do
                    args.add(null)
                }
            }
        }

        return ReflectionUtils.invokeMethod(method, dgsComponent, *args.toTypedArray())
    }

    private fun getValueAsOptional(value: Any?, parameter: Parameter) =
        if (value == null && parameter.type.isAssignableFrom(Optional::class.java)) {
            Optional.empty<Any>()
        } else if (parameter.type.isAssignableFrom(Optional::class.java)) {
            Optional.of(value!!)
        } else {
            value
        }

    private fun findTypeResolvers(
        dgsComponents: Map<String, Any>,
        runtimeWiringBuilder: RuntimeWiring.Builder,
        mergedRegistry: TypeDefinitionRegistry
    ) {
        val registeredTypeResolvers = mutableSetOf<String>()

        dgsComponents.values.forEach { dgsComponent ->
            dgsComponent.javaClass.methods.filter { it.isAnnotationPresent(DgsTypeResolver::class.java) }
                .forEach { method ->
                    val annotation = method.getAnnotation(DgsTypeResolver::class.java)

                    if (method.returnType != String::class.java) {
                        throw InvalidTypeResolverException("@DgsTypeResolvers must return String")
                    }

                    if (method.parameterCount != 1) {
                        throw InvalidTypeResolverException("@DgsTypeResolvers must take exactly one parameter")
                    }

                    if (!mergedRegistry.hasType(TypeName(annotation.name))) {
                        throw InvalidTypeResolverException("could not find type name '${annotation.name}' in schema")
                    }

                    var overrideTypeResolver = false
                    val defaultTypeResolver = method.getAnnotation(DgsDefaultTypeResolver::class.java)
                    if (defaultTypeResolver != null) {
                        overrideTypeResolver = dgsComponents.values.any { component ->
                            component.javaClass.methods.any { method ->
                                method.isAnnotationPresent(DgsTypeResolver::class.java) &&
                                    method.getAnnotation(DgsTypeResolver::class.java).name == annotation.name &&
                                    component != dgsComponent
                            }
                        }
                    }
                    // do not add the default resolver if another resolver with the same name is present
                    if (defaultTypeResolver == null || !overrideTypeResolver) {
                        registeredTypeResolvers.add(annotation.name)

                        runtimeWiringBuilder.type(
                            TypeRuntimeWiring.newTypeWiring(annotation.name)
                                .typeResolver { env: TypeResolutionEnvironment ->
                                    val typeName =
                                        ReflectionUtils.invokeMethod(method, dgsComponent, env.getObject()) as String
                                    env.schema.getObjectType(typeName)
                                }
                        )
                    }
                }
        }

        // Add a fallback type resolver for types that don't have a type resolver registered.
        // This works when the Java type has the same name as the GraphQL type.
        val unregisteredTypes = mergedRegistry.types()
            .filterValues { it is InterfaceTypeDefinition || it is UnionTypeDefinition }
            .map { it.key }
            .filter { !registeredTypeResolvers.contains(it) }
        unregisteredTypes.forEach {
            runtimeWiringBuilder.type(
                TypeRuntimeWiring.newTypeWiring(it)
                    .typeResolver { env: TypeResolutionEnvironment ->
                        val instance = env.getObject<Any>()
                        val resolvedType = env.schema.getObjectType(instance::class.java.simpleName)
                        resolvedType
                            ?: throw InvalidTypeResolverException("The default type resolver could not find a suitable Java type for GraphQL type `${instance::class.java.simpleName}. Provide a @DgsTypeResolver.`")
                    }
            )
        }
    }

    internal fun findScalars(applicationContext: ApplicationContext, runtimeWiringBuilder: RuntimeWiring.Builder) {
        applicationContext.getBeansWithAnnotation(DgsScalar::class.java).forEach {
            val scalarComponent = it.value
            val annotation = scalarComponent::class.java.getAnnotation(DgsScalar::class.java)
            when (scalarComponent) {
                is Coercing<*, *> -> runtimeWiringBuilder.scalar(
                    GraphQLScalarType.newScalar().name(annotation.name).coercing(scalarComponent).build()
                )
                else -> throw RuntimeException("Invalid @DgsScalar type: the class must implement graphql.schema.Coercing")
            }
        }
    }

    internal fun findSchemaFiles(hasDynamicTypeRegistry: Boolean = false): Array<Resource> {
        val cl = Thread.currentThread().contextClassLoader

        val resolver = PathMatchingResourcePatternResolver(cl)
        val schemas = try {
            val resources = schemaLocations.flatMap { resolver.getResources(it).toList() }.distinct().toTypedArray()
            if (resources.isEmpty()) {
                throw NoSchemaFoundException()
            }
            resources
        } catch (ex: Exception) {
            if (existingTypeDefinitionRegistry.isPresent || hasDynamicTypeRegistry) {
                logger.info("No schema files found, but a schema was provided as an TypeDefinitionRegistry")
                arrayOf()
            } else {
                logger.error("No schema files found in $schemaLocations. Define schema locations with property dgs.graphql.schema-locations")
                throw NoSchemaFoundException()
            }
        }

        val metaInfSchemas = try {
            resolver.getResources("classpath*:META-INF/schema/**/*.graphql*")
        } catch (ex: Exception) {
            arrayOf<Resource>()
        }

        return schemas + metaInfSchemas
    }
}

/*
 * Copyright 2021 Netflix, Inc.
 *
 * 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 com.netflix.graphql.dgs.logging

data class LogEvent(
    var gqlQuery: String = "",
    var gqlQueryVariables: String = "",
    var gqlResponse: String = "",
    var requestId: String = "",
    var traceId: String = "",
    var spanId: String = "",
    var authCallerType: String = "",
    var authUser: String = "",
    var callingApp: String = "",
    var callingRegion: String = "",
    var callingStack: String = "",
    var callingInstance: String = "",
    var callingAsg: String = "",
    var hostRegion: String = "",
    var hostEnvironment: String = "",
    var hostStack: String = "",
    var hostAsg: String = "",
    var hostCluster: String = "",
    var instanceId: String = "",
    var clientId: String = "",
    var mapleLegacyCaller: String = "",
    var mapleLegacyCallerAuthzAllowed: String = ""
)

/*
 * Copyright 2021 Netflix, Inc.
 *
 * 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 com.netflix.graphql.dgs.scalars

import com.netflix.graphql.dgs.DgsComponent
import com.netflix.graphql.dgs.DgsRuntimeWiring
import graphql.schema.*
import graphql.schema.idl.RuntimeWiring
import org.springframework.web.multipart.MultipartFile

@DgsComponent
class UploadScalar {
    val upload: GraphQLScalarType = GraphQLScalarType.newScalar().name("Upload")
        .description("A custom scalar that represents files")
        .coercing(MultipartFileCoercing).build()

    object MultipartFileCoercing : Coercing<MultipartFile, Void> {
        @Throws(CoercingSerializeException::class)
        override fun serialize(dataFetcherResult: Any): Void {
            throw CoercingSerializeException("Upload is an input-only type")
        }

        @Throws(CoercingParseValueException::class)
        override fun parseValue(input: Any): MultipartFile? {
            return if (input is MultipartFile) {
                input
            } else run {
                throw CoercingParseValueException(
                    "Expected type " +
                        MultipartFile::class.java.name +
                        " but was " +
                        input.javaClass.name
                )
            }
        }

        override fun parseLiteral(input: Any): MultipartFile {
            throw CoercingParseLiteralException(
                "Must use variables to specify Upload values"
            )
        }
    }

    // add the scalar manually since we can't use @DgsScalar in the framework
    @DgsRuntimeWiring
    fun addScalar(builder: RuntimeWiring.Builder): RuntimeWiring.Builder {
        return builder.scalar(upload)
    }
}

/*
 * Copyright 2020 Netflix, Inc.
 *
 * 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 com.netflix.graphql.dgs.client.codegen

import java.util.*

abstract class BaseProjectionNode {
    val fields: MutableMap<String, Any?> = LinkedHashMap()
    val fragments: MutableList<BaseSubProjectionNode<*, *>> = LinkedList()

    override fun toString(): String {
        if (fields.isEmpty() && fragments.isEmpty()) {
            return ""
        }

        val joiner = StringJoiner(" ", "{ ", " }")
        fields.forEach { (key, value) ->
            joiner.add(key)
            if (value != null) {
                joiner.add(" ").add(value.toString())
            }
        }

        fragments.forEach { joiner.add(it.toString()) }

        return joiner.toString()
    }
}

/*
 * Copyright 2021 Netflix, Inc.
 *
 * 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 com.netflix.graphql.dgs.client.codegen

abstract class BaseSubProjectionNode<T, R>(val parent: T, val root: R) : BaseProjectionNode() {

    fun parent(): T {
        return parent
    }

    fun root(): R {
        return root
    }
}

/*
 * Copyright 2020 Netflix, Inc.
 *
 * 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 com.netflix.graphql.dgs.client.codegen

import com.fasterxml.jackson.databind.ObjectMapper
import java.util.*

class EntitiesGraphQLQuery : GraphQLQuery {
    val variables: MutableMap<String, Any> = LinkedHashMap()

    constructor(representations: List<Any>?) {
        variables["representations"] = representations!!
    }

    constructor()

    override fun getOperationType(): String {
        return "query(\$representations: [_Any!]!)"
    }

    override fun getOperationName(): String {
        return "_entities(representations: \$representations)"
    }

    class Builder {
        private val representations: MutableList<Any> = ArrayList()
        val mapper = ObjectMapper()

        fun build(): EntitiesGraphQLQuery {
            return EntitiesGraphQLQuery(representations)
        }

        fun addRepresentationAsVariable(representation: Any): Builder {
            representations.add(mapper.convertValue(representation, HashMap::class.java))
            return this
        }
    }

    companion object {
        fun newRequest(): Builder {
            return Builder()
        }
    }
}

/*
 * Copyright 2020 Netflix, Inc.
 *
 * 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 com.netflix.graphql.dgs.client.codegen

import java.util.*

abstract class GraphQLQuery(val operation: String, val name: String?) {
    val input: MutableMap<String, Any> = LinkedHashMap()

    constructor(operation: String) : this(operation, null)
    constructor() : this("query")

    open fun getOperationType(): String? {
        return operation
    }

    abstract fun getOperationName(): String
}

/*
 * Copyright 2021 Netflix, Inc.
 *
 * 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 com.netflix.graphql.dgs.client.codegen

class GraphQLQueryRequest(private val query: GraphQLQuery, private val projection: BaseProjectionNode?) {

    constructor(query: GraphQLQuery) : this(query, null)

    fun serialize(): String {
        val builder = StringBuilder()
        builder.append(query.getOperationType())
        if (query.name != null) {
            builder.append(" ").append(query.name)
        }
        builder.append(" {").append(query.getOperationName())
        val input: Map<String, Any?> = query.input
        if (input.isNotEmpty()) {
            builder.append("(")
            val inputEntryIterator = input.entries.iterator()
            while (inputEntryIterator.hasNext()) {
                val (key, value) = inputEntryIterator.next()
                if (value != null) {
                    builder.append(key)
                    builder.append(": ")
                    if (value is String) {
                        builder.append("\"")
                        builder.append(value.toString())
                        builder.append("\"")
                    } else if (value is List<*>) {
                        if (value.isNotEmpty() && value[0] is String) {
                            builder.append("[")
                            val result = value.joinToString(separator = "\", \"", prefix = "\"", postfix = "\"")
                            builder.append(result)
                            builder.append("]")
                        } else {
                            builder.append(value.toString())
                        }
                    } else {
                        builder.append(value.toString())
                    }
                }
                if (inputEntryIterator.hasNext()) {
                    builder.append(", ")
                }
            }
            builder.append(")")
        }

        if (projection is BaseSubProjectionNode<*, *>) {
            builder.append(projection.root().toString())
        } else if (projection != null) {
            builder.append(projection.toString())
        }

        builder.append(" }")
        return builder.toString()
    }
}

/*
 * Copyright 2021 Netflix, Inc.
 *
 * 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 com.netflix.graphql.dgs.client.exceptions

class NoDataPresentException : RuntimeException("No data available in response")

/*
 * Copyright 2021 Netflix, Inc.
 *
 * 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 com.netflix.graphql.dgs.metrics.micrometer

import com.netflix.graphql.dgs.metrics.micrometer.tagging.DgsContextualTagCustomizer
import com.netflix.graphql.dgs.metrics.micrometer.tagging.DgsExecutionTagCustomizer
import com.netflix.graphql.dgs.metrics.micrometer.tagging.DgsFieldFetchTagCustomizer
import com.netflix.graphql.dgs.metrics.micrometer.tagging.DgsGraphQLMetricsTagsProvider
import graphql.ExecutionResult
import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters
import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters
import io.micrometer.core.instrument.Tag
import io.micrometer.core.instrument.Tags

internal class DgsGraphQLCollatedMetricsTagsProvider(
    private val contextualTagCustomizer: Collection<DgsContextualTagCustomizer>,
    private val executionTagCustomizer: Collection<DgsExecutionTagCustomizer>,
    private val fieldFetchTagCustomizer: Collection<DgsFieldFetchTagCustomizer>
) : DgsGraphQLMetricsTagsProvider {

    override fun getContextualTags(): Iterable<Tag> {
        return Tags.of(contextualTagCustomizer.flatMap { it.getContextualTags() })
    }

    override fun getExecutionTags(
        parameters: InstrumentationExecutionParameters,
        result: ExecutionResult,
        exception: Throwable?
    ): Iterable<Tag> {
        return Tags.of(executionTagCustomizer.flatMap { it.getExecutionTags(parameters, result, exception) })
    }

    override fun getFieldFetchTags(parameters: InstrumentationFieldFetchParameters, exception: Throwable?): Iterable<Tag> {
        return Tags.of(fieldFetchTagCustomizer.flatMap { it.getFieldFetchTags(parameters, exception) })
    }
}

package com.netflix.graphql.dgs.metrics.micrometer

import com.netflix.graphql.dgs.metrics.DgsMetrics.GqlMetric
import com.netflix.graphql.dgs.metrics.DgsMetrics.GqlTag
import com.netflix.graphql.dgs.metrics.micrometer.DgsGraphQLMetricsInstrumentationUtils.resolveDataFetcherTagValue
import com.netflix.graphql.dgs.metrics.micrometer.DgsGraphQLMetricsInstrumentationUtils.sanitizeErrorPaths
import com.netflix.graphql.dgs.metrics.micrometer.DgsGraphQLMetricsInstrumentationUtils.shouldIgnoreTag
import com.netflix.graphql.dgs.metrics.micrometer.tagging.DgsGraphQLMetricsTagsProvider
import graphql.ExecutionResult
import graphql.execution.instrumentation.InstrumentationContext
import graphql.execution.instrumentation.InstrumentationState
import graphql.execution.instrumentation.SimpleInstrumentation
import graphql.execution.instrumentation.SimpleInstrumentationContext
import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters
import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters
import graphql.schema.DataFetcher
import io.micrometer.core.instrument.MeterRegistry
import io.micrometer.core.instrument.Tag
import io.micrometer.core.instrument.Tags
import io.micrometer.core.instrument.Timer
import org.springframework.boot.actuate.metrics.AutoTimer
import java.util.*
import java.util.concurrent.CompletableFuture
import java.util.concurrent.CompletionStage

class DgsGraphQLMetricsInstrumentation(
    private val registrySupplier: DgsMeterRegistrySupplier,
    private val tagsProvider: DgsGraphQLMetricsTagsProvider,
    private val autoTimer: AutoTimer
) : SimpleInstrumentation() {

    override fun createState(): InstrumentationState {
        return MetricsInstrumentationState(registrySupplier.get())
    }

    override fun beginExecution(parameters: InstrumentationExecutionParameters): InstrumentationContext<ExecutionResult> {
        val state: MetricsInstrumentationState = parameters.getInstrumentationState()
        state.startTimer()

        return object : SimpleInstrumentationContext<ExecutionResult>() {
            override fun onCompleted(result: ExecutionResult, exc: Throwable?) {

                state.stopTimer(
                    autoTimer.builder(GqlMetric.QUERY.key)
                        .tags(tagsProvider.getContextualTags())
                        .tags(tagsProvider.getExecutionTags(parameters, result, exc))
                )
            }
        }
    }

    override fun instrumentExecutionResult(
        executionResult: ExecutionResult,
        parameters: InstrumentationExecutionParameters
    ): CompletableFuture<ExecutionResult> {

        val tags =
            Tags.empty()
                .and(tagsProvider.getContextualTags())
                .and(tagsProvider.getExecutionTags(parameters, executionResult, null))

        sanitizeErrorPaths(executionResult).forEach {
            registrySupplier
                .get()
                .counter(
                    GqlMetric.ERROR.key,
                    tags.and(GqlTag.PATH.key, it.path)
                        .and(GqlTag.ERROR_CODE.key, it.type)
                        .and(GqlTag.ERROR_DETAIL.key, it.detail)
                ).increment()
        }

        return CompletableFuture.completedFuture(executionResult)
    }

    override fun instrumentDataFetcher(
        dataFetcher: DataFetcher<*>,
        parameters: InstrumentationFieldFetchParameters
    ): DataFetcher<*> {
        val gqlField = resolveDataFetcherTagValue(parameters)
        if (parameters.isTrivialDataFetcher || shouldIgnoreTag(gqlField)) {
            return dataFetcher
        }

        return DataFetcher { environment ->
            val registry = registrySupplier.get()
            val baseTags = Tags.of(GqlTag.FIELD.key, gqlField).and(tagsProvider.getContextualTags())

            val sampler = Timer.start(registry)
            try {
                val result = dataFetcher.get(environment)
                if (result is CompletionStage<*>) {
                    result.whenComplete { _, error -> recordDataFetcherMetrics(registry, sampler, parameters, error, baseTags) }
                } else {
                    recordDataFetcherMetrics(registry, sampler, parameters, null, baseTags)
                }
                result
            } catch (throwable: Throwable) {
                recordDataFetcherMetrics(registry, sampler, parameters, throwable, baseTags)
                throw throwable
            }
        }
    }

    private fun recordDataFetcherMetrics(
        registry: MeterRegistry,
        timerSampler: Timer.Sample,
        parameters: InstrumentationFieldFetchParameters,
        error: Throwable?,
        baseTags: Iterable<Tag>
    ) {
        val recordedTags = Tags.of(baseTags).and(tagsProvider.getFieldFetchTags(parameters, error))

        timerSampler.stop(
            registry,
            Timer.builder(GqlMetric.RESOLVER.key).tags(recordedTags)
        )
    }

    class MetricsInstrumentationState(private val registry: MeterRegistry) : InstrumentationState {
        private var timerSample: Optional<Timer.Sample> = Optional.empty()

        fun startTimer() {
            this.timerSample = Optional.of(Timer.start(this.registry))
        }

        fun stopTimer(timer: Timer.Builder) {
            this.timerSample.ifPresent { it.stop(timer.register(this.registry)) }
        }
    }
}

/*
 * Copyright 2021 Netflix, Inc.
 *
 * 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 com.netflix.graphql.dgs.metrics.micrometer

import graphql.ExecutionResult
import graphql.GraphQLError
import graphql.InvalidSyntaxError
import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters
import graphql.schema.GraphQLNonNull
import graphql.schema.GraphQLObjectType
import graphql.validation.ValidationError

internal object DgsGraphQLMetricsInstrumentationUtils {
    private val instrumentationIgnores = setOf("__typename", "__Schema", "__Type")
    private const val TAG_VALUE_UNKNOWN = "unknown"
    private const val TAG_VALUE_NONE = "none"

    fun resolveDataFetcherTagValue(parameters: InstrumentationFieldFetchParameters): String {
        val type = parameters.executionStepInfo.parent.type
        val parentType = if (type is GraphQLNonNull) {
            type.wrappedType as GraphQLObjectType
        } else {
            type as GraphQLObjectType
        }

        return "${parentType.name}.${parameters.executionStepInfo.field.singleField.name}"
    }

    fun shouldIgnoreTag(tag: String): Boolean {
        return instrumentationIgnores.find { tag.contains(it) } != null
    }

    fun sanitizeErrorPaths(executionResult: ExecutionResult): Collection<ErrorTagValues> {
        var dedupeErrorPaths: Map<String, ErrorTagValues> = emptyMap()
        executionResult.errors.forEach { error ->
            val errorPath: List<Any>
            val errorType: String
            val errorDetail = errorDetailExtension(error)
            when (error) {
                is ValidationError -> {
                    errorPath = error.queryPath ?: emptyList()
                    errorType = errorType(error)
                }
                is InvalidSyntaxError -> {
                    errorPath = emptyList()
                    errorType = errorType(error)
                }
                else -> {
                    errorPath = error.path ?: emptyList()
                    errorType = errorTypeExtension(error)
                }
            }
            val sanitizedPath = errorPath.map { iter ->
                if (iter.toString().toIntOrNull() != null) "number"
                else iter.toString()
            }.toString()
            // in case of batch loaders, eliminate duplicate instances of the same error at different indices
            if (!dedupeErrorPaths.contains(sanitizedPath)) {
                dedupeErrorPaths = dedupeErrorPaths
                    .plus(Pair(sanitizedPath, ErrorTagValues(sanitizedPath, errorType, errorDetail)))
            }
        }
        return dedupeErrorPaths.values
    }

    private fun <T : GraphQLError> errorType(error: T): String {
        return error.errorType?.toString() ?: TAG_VALUE_UNKNOWN
    }

    private fun <T : GraphQLError> errorTypeExtension(error: T): String {
        return extension(error, "errorType", TAG_VALUE_UNKNOWN)
    }

    private fun <T : GraphQLError> errorDetailExtension(error: T): String {
        return extension(error, "errorDetail", TAG_VALUE_NONE)
    }

    private fun <T : GraphQLError> extension(error: T, key: String, default: String): String {
        return error.extensions?.get(key)?.toString() ?: default
    }

    internal data class ErrorTagValues(val path: String, val type: String, val detail: String)
}

package com.netflix.graphql.dgs.metrics.micrometer

import org.springframework.boot.actuate.autoconfigure.metrics.AutoTimeProperties
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.boot.context.properties.NestedConfigurationProperty

@ConfigurationProperties("management.metrics.dgs-graphql")
class DgsGraphQLMetricsProperties {

    /** Auto-timed queries settings. */
    @NestedConfigurationProperty
    val autotime = AutoTimeProperties()
}

package com.netflix.graphql.dgs.metrics.micrometer

import com.netflix.graphql.dgs.metrics.micrometer.DgsGraphQLMicrometerAutoConfiguration.Companion.AUTO_CONF_PREFIX
import com.netflix.graphql.dgs.metrics.micrometer.dataloader.DgsDataLoaderInstrumentationProvider
import com.netflix.graphql.dgs.metrics.micrometer.tagging.DgsContextualTagCustomizer
import com.netflix.graphql.dgs.metrics.micrometer.tagging.DgsExecutionTagCustomizer
import com.netflix.graphql.dgs.metrics.micrometer.tagging.DgsFieldFetchTagCustomizer
import com.netflix.graphql.dgs.metrics.micrometer.tagging.DgsGraphQLMetricsTagsProvider
import com.netflix.graphql.dgs.metrics.micrometer.tagging.SimpleGqlOutcomeTagCustomizer
import io.micrometer.core.instrument.MeterRegistry
import io.micrometer.core.instrument.simple.SimpleMeterRegistry
import org.springframework.beans.factory.ObjectProvider
import org.springframework.boot.actuate.autoconfigure.metrics.CompositeMeterRegistryAutoConfiguration
import org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration
import org.springframework.boot.autoconfigure.AutoConfigureAfter
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration

/**
 * [Auto-configuration][org.springframework.boot.autoconfigure.EnableAutoConfiguration] for instrumentation of Spring GraphQL
 * endpoints.
 */
@AutoConfigureAfter(CompositeMeterRegistryAutoConfiguration::class)
@ConditionalOnClass(MetricsAutoConfiguration::class, MeterRegistry::class)
@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(prefix = AUTO_CONF_PREFIX, name = ["enabled"], havingValue = "true", matchIfMissing = true)
@EnableConfigurationProperties(DgsGraphQLMetricsProperties::class)
open class DgsGraphQLMicrometerAutoConfiguration {

    companion object {
        const val AUTO_CONF_PREFIX = "management.metrics.dgs-graphql"
    }

    @Bean
    @ConditionalOnProperty(
        prefix = "$AUTO_CONF_PREFIX.tag-customizers.outcome",
        name = ["enabled"], havingValue = "true", matchIfMissing = true
    )
    open fun simpleGqlOutcomeTagCustomizer(): SimpleGqlOutcomeTagCustomizer {
        return SimpleGqlOutcomeTagCustomizer()
    }

    @Bean
    @ConditionalOnProperty(
        prefix = "$AUTO_CONF_PREFIX.instrumentation",
        name = ["enabled"], havingValue = "true", matchIfMissing = true
    )
    open fun metricsInstrumentation(
        meterRegistrySupplier: DgsMeterRegistrySupplier,
        tagsProvider: DgsGraphQLMetricsTagsProvider,
        properties: DgsGraphQLMetricsProperties
    ): DgsGraphQLMetricsInstrumentation {
        return DgsGraphQLMetricsInstrumentation(
            meterRegistrySupplier,
            tagsProvider,
            properties.autotime
        )
    }

    @Bean
    @ConditionalOnProperty(
        prefix = "$AUTO_CONF_PREFIX.data-loader-instrumentation",
        name = ["enabled"], havingValue = "true", matchIfMissing = true
    )
    open fun dataLoaderInstrumentationProvider(
        meterRegistrySupplier: DgsMeterRegistrySupplier
    ): DgsDataLoaderInstrumentationProvider {
        return DgsDataLoaderInstrumentationProvider(meterRegistrySupplier)
    }

    @Bean
    open fun collatedMetricsTagsProvider(
        contextualTagCustomizer: Collection<DgsContextualTagCustomizer>,
        executionTagCustomizer: Collection<DgsExecutionTagCustomizer>,
        fieldFetchTagCustomizer: Collection<DgsFieldFetchTagCustomizer>
    ): DgsGraphQLMetricsTagsProvider {
        return DgsGraphQLCollatedMetricsTagsProvider(
            contextualTagCustomizer,
            executionTagCustomizer,
            fieldFetchTagCustomizer
        )
    }

    @Bean
    @ConditionalOnMissingBean
    open fun meterRegistrySupplier(meterRegistryProvider: ObjectProvider<MeterRegistry>): DgsMeterRegistrySupplier {
        return DefaultMeterRegistrySupplier(meterRegistryProvider)
    }

    internal class DefaultMeterRegistrySupplier(
        val meterRegistryProvider: ObjectProvider<MeterRegistry>
    ) : DgsMeterRegistrySupplier {

        companion object {
            /** Fallback Micrometer [MeterRegistry] used in case the [ObjectProvider] doesn't define one. */
            private val DEFAULT_METER_REGISTRY = SimpleMeterRegistry()
        }
        override fun get(): MeterRegistry {
            return meterRegistryProvider.ifAvailable ?: DEFAULT_METER_REGISTRY
        }
    }
}

package com.netflix.graphql.dgs.metrics.micrometer

import io.micrometer.core.instrument.MeterRegistry

/**
 * A supplier of a [MeterRegistry] that should guarantee a none-null reference is returned.
 * [Bean][Beans] should access the registry via the supplier and not depend on a [MeterRegistry] when constructed.
 * This is will avoid _eager initialization_ of a _MeterRegistry_ which has been observed as problematic in some cases.
 * */
@FunctionalInterface
fun interface DgsMeterRegistrySupplier {
    fun get(): MeterRegistry
}

/*
 * Copyright 2020 Netflix, Inc.
 *
 * 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 com.netflix.graphql.dgs.webmvc.autoconfigure

import com.netflix.graphql.dgs.DgsQueryExecutor
import com.netflix.graphql.dgs.internal.DgsSchemaProvider
import com.netflix.graphql.dgs.mvc.DgsRestController
import com.netflix.graphql.dgs.mvc.DgsRestSchemaJsonController
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication
import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.Import
import org.springframework.web.servlet.DispatcherServlet

@Configuration
@ConditionalOnWebApplication
@EnableConfigurationProperties(DgsWebMvcConfigurationProperties::class)
open class DgsWebMvcAutoConfiguration {
    @Bean
    open fun dgsRestController(dgsQueryExecutor: DgsQueryExecutor): DgsRestController {
        return DgsRestController(dgsQueryExecutor)
    }

    @Configuration
    @ConditionalOnClass(DispatcherServlet::class)
    @ConditionalOnProperty(name = ["dgs.graphql.graphiql.enabled"], havingValue = "true", matchIfMissing = true)
    @Import(GraphiQLConfigurer::class)
    open class DgsGraphiQLConfiguration

    @Configuration
    @ConditionalOnProperty(name = ["dgs.graphql.schema-json.enabled"], havingValue = "true", matchIfMissing = true)
    open class DgsWebMvcSchemaJsonConfiguration {
        @Bean
        open fun dgsRestSchemaJsonController(dgsSchemaProvider: DgsSchemaProvider): DgsRestSchemaJsonController {
            return DgsRestSchemaJsonController(dgsSchemaProvider)
        }
    }
}

/*
 * Copyright 2021 Netflix, Inc.
 *
 * 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 com.netflix.graphql.dgs.webmvc.autoconfigure

import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.boot.context.properties.ConstructorBinding
import org.springframework.boot.context.properties.bind.DefaultValue
import javax.annotation.PostConstruct

/**
 * Configuration properties for DGS web controllers.
 */
@ConstructorBinding
@ConfigurationProperties(prefix = "dgs.graphql")
@Suppress("ConfigurationProperties")
data class DgsWebMvcConfigurationProperties(
    /** Path to the GraphQL endpoint without trailing slash. */
    @DefaultValue("/graphql") val path: String,
    @DefaultValue val graphiql: DgsGraphiQLConfigurationProperties,
    @DefaultValue val schemaJson: DgsSchemaJsonConfigurationProperties
) {
    /**
     * Configuration properties for the schema-json endpoint.
     */
    data class DgsGraphiQLConfigurationProperties(
        /** Path to the GraphiQL endpoint without trailing slash. */
        @DefaultValue("/graphiql") val path: String
    )
    /**
     * Configuration properties for the schema-json endpoint.
     */
    data class DgsSchemaJsonConfigurationProperties(
        /** Path to the schema-json endpoint without trailing slash. */
        @DefaultValue("/schema.json") val path: String
    )

    @PostConstruct
    fun validatePaths() {
        validatePath(this.path, "dgs.graphql.path")
        validatePath(this.graphiql.path, "dgs.graphql.graphiql.path")
        validatePath(this.schemaJson.path, "dgs.graphql.schema-json.path")
    }

    private fun validatePath(path: String, pathProperty: String) {
        if (!path.startsWith("/") || path.endsWith("/")) {
            throw IllegalArgumentException("$pathProperty must start with '/' and not end with '/' but was '$path'")
        }
    }
}

/*
 * Copyright 2021 Netflix, Inc.
 *
 * 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 com.netflix.graphql.dgs.webmvc.autoconfigure

import com.netflix.graphql.dgs.webmvc.autoconfigure.GraphiQLConfigurer.Constants.PATH_TO_GRAPHIQL_INDEX_HTML
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.context.annotation.Configuration
import org.springframework.core.io.Resource
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer
import org.springframework.web.servlet.resource.PathResourceResolver
import org.springframework.web.servlet.resource.ResourceTransformer
import org.springframework.web.servlet.resource.ResourceTransformerChain
import org.springframework.web.servlet.resource.TransformedResource
import java.io.BufferedReader
import java.io.IOException
import java.nio.charset.StandardCharsets.UTF_8
import javax.servlet.ServletContext
import javax.servlet.http.HttpServletRequest

@Configuration
@EnableConfigurationProperties(DgsWebMvcConfigurationProperties::class)
@Suppress("SpringJavaInjectionPointsAutowiringInspection")
open class GraphiQLConfigurer(
    private val configProps: DgsWebMvcConfigurationProperties,
    private val servletContext: ServletContext
) : WebMvcConfigurer {

    val logger: Logger = LoggerFactory.getLogger(GraphiQLConfigurer::class.java)

    object Constants {
        const val PATH_TO_GRAPHIQL_INDEX_HTML = "/graphiql/index.html"
    }

    override fun addViewControllers(registry: ViewControllerRegistry) {
        registry.addViewController(configProps.graphiql.path).setViewName("forward:$PATH_TO_GRAPHIQL_INDEX_HTML")
        registry.addViewController("${configProps.graphiql.path}/").setViewName("forward:$PATH_TO_GRAPHIQL_INDEX_HTML")
    }

    override fun addResourceHandlers(registry: ResourceHandlerRegistry) {
        val graphqlPath = servletContext.contextPath + configProps.path
        logger.info("Configuring GraphiQL to use GraphQL endpoint at '{}'", graphqlPath)
        registry
            .addResourceHandler("/graphiql/**")
            .addResourceLocations("classpath:/static/graphiql/")
            .setCachePeriod(3600)
            .resourceChain(true)
            .addResolver(PathResourceResolver())
            .addTransformer(TokenReplacingTransformer("<DGS_GRAPHQL_PATH>", graphqlPath))
    }

    class TokenReplacingTransformer(private val replaceToken: String, private val replaceValue: String) :
        ResourceTransformer {
        @Throws(IOException::class)

        override fun transform(
            request: HttpServletRequest?,
            resource: Resource,
            transformerChain: ResourceTransformerChain?
        ): Resource {
            if (request?.requestURI?.endsWith(PATH_TO_GRAPHIQL_INDEX_HTML) == true) {
                val content = resource.inputStream.bufferedReader().use(BufferedReader::readText)
                return TransformedResource(resource, content.replace(replaceToken, replaceValue).toByteArray(UTF_8))
            }
            return resource
        }
    }
}

/*
 * Copyright 2020 Netflix, Inc.
 *
 * 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 com.netflix.graphql.dgs.subscriptions.sse

import com.netflix.graphql.dgs.DgsQueryExecutor
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.web.servlet.DispatcherServlet

@Configuration
@ConditionalOnWebApplication
@ConditionalOnClass(DispatcherServlet::class)
open class DgsSSEAutoConfig {
    @Bean
    open fun DgsSSEController(dgsQueryExecutor: DgsQueryExecutor): DgsSSESubscriptionHandler {
        return DgsSSESubscriptionHandler(dgsQueryExecutor)
    }
}

/*
 * Copyright 2020 Netflix, Inc.
 *
 * 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 com.netflix.graphql.dgs.subscriptions.sse

import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.netflix.graphql.dgs.DgsQueryExecutor
import graphql.ExecutionResult
import graphql.validation.ValidationError
import org.reactivestreams.Publisher
import org.reactivestreams.Subscriber
import org.reactivestreams.Subscription
import org.slf4j.LoggerFactory
import org.springframework.http.MediaType
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter
import java.nio.charset.StandardCharsets
import java.util.*

@RestController
class DgsSSESubscriptionHandler(private val dgsQueryExecutor: DgsQueryExecutor) {
    private val logger = LoggerFactory.getLogger(DgsSSESubscriptionHandler::class.java)

    @RequestMapping("/subscriptions", produces = ["text/event-stream"])
    fun subscriptionWithId(@RequestParam("query") queryBase64: String): ResponseEntity<SseEmitter> {
        val emitter = SseEmitter(-1)
        val mapper = jacksonObjectMapper()
        val sessionId = UUID.randomUUID().toString()
        val query = try {
            String(Base64.getDecoder().decode(queryBase64), StandardCharsets.UTF_8)
        } catch (ex: IllegalArgumentException) {
            emitter.send("Error decoding base64 encoded query")
            emitter.complete()
            return ResponseEntity.badRequest().body(emitter)
        }

        val queryPayload = try {
            mapper.readValue(query, QueryPayload::class.java)
        } catch (ex: Exception) {
            emitter.send("Error parsing query: ${ex.message}")
            emitter.complete()
            return ResponseEntity.badRequest().body(emitter)
        }

        val executionResult: ExecutionResult = dgsQueryExecutor.execute(queryPayload.query, queryPayload.variables)
        if (executionResult.errors.isNotEmpty()) {
            return if (executionResult.errors.filterIsInstance<ValidationError>().isNotEmpty()) {
                val errorMessage = "Subscription query failed to validate: ${executionResult.errors.joinToString(", ")}"
                emitter.send(errorMessage)
                emitter.complete()
                ResponseEntity.badRequest().body(emitter)
            } else {
                val errorMessage = "Error executing subscription query: ${executionResult.errors.joinToString(", ")}"
                logger.error(errorMessage)
                emitter.send(errorMessage)
                emitter.complete()
                ResponseEntity.status(500).body(emitter)
            }
        }

        val subscriber = object : Subscriber<ExecutionResult> {
            lateinit var subscription: Subscription

            override fun onSubscribe(s: Subscription) {
                logger.info("Started subscription with id {} for request {}", sessionId, queryPayload)
                subscription = s
                s.request(1)
            }

            override fun onNext(t: ExecutionResult) {

                val event = SseEmitter.event()
                    .data(mapper.writeValueAsString(SubscriptionData(t.getData(), sessionId)), MediaType.APPLICATION_JSON)
                    .id(UUID.randomUUID().toString())
                emitter.send(event)

                subscription.request(1)
            }

            override fun onError(t: Throwable) {
                logger.error("Error on subscription {}", sessionId, t)
                emitter.completeWithError(t)
            }

            override fun onComplete() {
                emitter.complete()
            }
        }

        emitter.onError {
            logger.warn("Subscription {} had a connection error", sessionId)
            subscriber.subscription.cancel()
        }

        emitter.onTimeout {
            logger.warn("Subscription {} timed out", sessionId)
            subscriber.subscription.cancel()
        }

        val publisher = try {
            executionResult.getData<Publisher<ExecutionResult>>()
        } catch (ex: ClassCastException) {
            return if (query.contains("subscription")) {
                logger.error("Invalid return type for subscription datafetcher. A subscription datafetcher must return a Publisher<ExecutionResult>. The query was $query", ex)
                emitter.send("Invalid return type for subscription datafetcher. Was a non-subscription query send to the subscription endpoint?")
                emitter.complete()
                ResponseEntity.status(500).body(emitter)
            } else {
                logger.warn("Invalid return type for subscription datafetcher. The query sent doesn't appear to be a subscription query: $query", ex)
                emitter.send("Invalid return type for subscription datafetcher. Was a non-subscription query send to the subscription endpoint?")
                emitter.complete()
                ResponseEntity.badRequest().body(emitter)
            }
        }

        publisher.subscribe(subscriber)

        return ResponseEntity.ok(emitter)
    }

    data class QueryPayload(@JsonProperty("variables") val variables: Map<String, Any> = emptyMap(), @JsonProperty("extensions") val extensions: Map<String, Any> = emptyMap(), @JsonProperty("operationName") val operationName: String?, @JsonProperty("query") val query: String)
    data class SubscriptionData(val data: Any, val subId: String, val type: String = "SUBSCRIPTION_DATA")
}

/*
 * Copyright 2020 Netflix, Inc.
 *
 * 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 com.netflix.graphql.dgs.subscriptions.websockets

import com.netflix.graphql.dgs.DgsQueryExecutor
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.web.socket.WebSocketHandler
import org.springframework.web.socket.config.annotation.EnableWebSocket
import org.springframework.web.socket.config.annotation.WebSocketConfigurer
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry
import org.springframework.web.socket.server.support.DefaultHandshakeHandler

@Configuration
@ConditionalOnWebApplication
open class DgsWebSocketAutoConfig {
    @Bean
    open fun webSocketHandler(@Suppress("SpringJavaInjectionPointsAutowiringInspection") dgsQueryExecutor: DgsQueryExecutor): WebSocketHandler {
        return DgsWebSocketHandler(dgsQueryExecutor)
    }

    @Configuration
    @EnableWebSocket
    internal open class WebSocketConfig(@Suppress("SpringJavaInjectionPointsAutowiringInspection") private val webSocketHandler: WebSocketHandler) : WebSocketConfigurer {

        override fun registerWebSocketHandlers(registry: WebSocketHandlerRegistry) {
            val defaultHandshakeHandler = DefaultHandshakeHandler()
            defaultHandshakeHandler.setSupportedProtocols("graphql-ws")
            registry.addHandler(webSocketHandler, "/subscriptions").setHandshakeHandler(defaultHandshakeHandler).setAllowedOrigins("*")
        }
    }
}

/*
 * Copyright 2020 Netflix, Inc.
 *
 * 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 com.netflix.graphql.dgs.subscriptions.websockets

import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.netflix.graphql.dgs.DgsQueryExecutor
import graphql.ExecutionResult
import org.reactivestreams.Publisher
import org.reactivestreams.Subscriber
import org.reactivestreams.Subscription
import org.slf4j.LoggerFactory
import org.springframework.web.socket.TextMessage
import org.springframework.web.socket.WebSocketSession
import org.springframework.web.socket.handler.TextWebSocketHandler
import java.util.*
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.CopyOnWriteArrayList
import javax.annotation.PostConstruct

class DgsWebSocketHandler(private val dgsQueryExecutor: DgsQueryExecutor) : TextWebSocketHandler() {
    private val logger = LoggerFactory.getLogger(DgsWebSocketHandler::class.java)
    internal val subscriptions = ConcurrentHashMap<String, MutableMap<String, Subscription>>()
    internal val sessions = CopyOnWriteArrayList<WebSocketSession>()

    @PostConstruct
    fun setupCleanup() {
        val timerTask = object : TimerTask() {
            override fun run() {
                sessions.filter { !it.isOpen }.forEach(this@DgsWebSocketHandler::cleanupSubscriptionsForSession)
            }
        }

        val timer = Timer(true)
        timer.scheduleAtFixedRate(timerTask, 0, 5000)
    }

    public override fun handleTextMessage(session: WebSocketSession, message: TextMessage) {
        val (type, payload, id) = jacksonObjectMapper().readValue(message.payload, OperationMessage::class.java)
        when (type) {
            GQL_CONNECTION_INIT -> {
                logger.info("Initialized connection for {}", session.id)
                sessions.add(session)
                session.sendMessage(
                    TextMessage(
                        jacksonObjectMapper().writeValueAsBytes(
                            OperationMessage(
                                GQL_CONNECTION_ACK
                            )
                        )
                    )
                )
            }
            GQL_START -> {
                val queryPayload = jacksonObjectMapper().convertValue(payload, QueryPayload::class.java)
                handleSubscription(id!!, queryPayload, session)
            }
            GQL_STOP -> {
                subscriptions[session.id]?.get(id)?.cancel()
                subscriptions.remove(id)
            }
            GQL_CONNECTION_TERMINATE -> {
                logger.info("Terminated session " + session.id)
                cleanupSubscriptionsForSession(session)
                subscriptions.remove(session.id)
                session.close()
            }
            else -> session.sendMessage(TextMessage(jacksonObjectMapper().writeValueAsBytes(OperationMessage("error"))))
        }
    }

    private fun cleanupSubscriptionsForSession(session: WebSocketSession) {
        logger.info("Cleaning up for session {}", session.id)
        subscriptions[session.id]?.values?.forEach { it.cancel() }
        sessions.remove(session)
    }

    private fun handleSubscription(id: String, payload: QueryPayload, session: WebSocketSession) {
        val executionResult: ExecutionResult = dgsQueryExecutor.execute(payload.query)
        val subscriptionStream: Publisher<ExecutionResult> = executionResult.getData()

        subscriptionStream.subscribe(object : Subscriber<ExecutionResult> {
            override fun onSubscribe(s: Subscription) {
                logger.info("Subscription started for {}", id)
                subscriptions[session.id] = mutableMapOf(Pair(id, s))

                s.request(1)
            }

            override fun onNext(er: ExecutionResult) {
                val message = OperationMessage(GQL_DATA, DataPayload(er.getData()), id)
                val jsonMessage = TextMessage(jacksonObjectMapper().writeValueAsBytes(message))
                logger.debug("Sending subscription data: {}", jsonMessage)

                if (session.isOpen) {
                    session.sendMessage(jsonMessage)
                    subscriptions[session.id]?.get(id)?.request(1)
                }
            }

            override fun onError(t: Throwable) {
                logger.error("Error on subscription {}", id, t)
                val message = OperationMessage(GQL_ERROR, DataPayload(null, listOf(t.message!!)), id)
                val jsonMessage = TextMessage(jacksonObjectMapper().writeValueAsBytes(message))
                logger.debug("Sending subscription error: {}", jsonMessage)

                if (session.isOpen) {
                    session.sendMessage(jsonMessage)
                }
            }

            override fun onComplete() {
                logger.info("Subscription completed for {}", id)
                val message = OperationMessage(GQL_TYPE, null, id)
                val jsonMessage = TextMessage(jacksonObjectMapper().writeValueAsBytes(message))

                if (session.isOpen) {
                    session.sendMessage(jsonMessage)
                }

                subscriptions.remove(id)
            }
        })
    }
}

const val GQL_CONNECTION_INIT = "connection_init"
const val GQL_CONNECTION_ACK = "connection_ack"
const val GQL_START = "start"
const val GQL_STOP = "stop"
const val GQL_DATA = "data"
const val GQL_ERROR = "error"
const val GQL_TYPE = "complete"
const val GQL_CONNECTION_TERMINATE = "connection_terminate"

data class DataPayload(val data: Any?, val errors: List<Any>? = emptyList())
data class OperationMessage(@JsonProperty("type") val type: String, @JsonProperty("payload") val payload: Any? = null, @JsonProperty("id", required = false) val id: String? = "")
data class QueryPayload(@JsonProperty("variables") val variables: Map<String, Any> = emptyMap(), @JsonProperty("extensions") val extensions: Map<String, Any> = emptyMap(), @JsonProperty("operationName") val operationName: String?, @JsonProperty("query") val query: String)

/*
 * Copyright 2020 Netflix, Inc.
 *
 * 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 com.netflix.graphql.dgs.internal.utils

import com.fasterxml.jackson.core.JsonGenerator
import com.fasterxml.jackson.core.JsonProcessingException
import com.fasterxml.jackson.databind.SerializerProvider
import com.fasterxml.jackson.databind.ser.std.StdSerializer
import org.springframework.web.multipart.MultipartFile
import java.io.IOException

/**
 * This class is used only for logging purposes since we cannot serialize a MultipartFile to json otherwise.
 */
class MultipartFileSerializer @JvmOverloads constructor(t: Class<MultipartFile>? = null) : StdSerializer<MultipartFile>(t) {

    @Throws(IOException::class, JsonProcessingException::class)
    override fun serialize(
        value: MultipartFile,
        jgen: JsonGenerator,
        provider: SerializerProvider
    ) {
        jgen.writeStartObject()
        jgen.writeStringField("name", value.originalFilename)
        jgen.writeEndObject()
    }
}

/*
 * Copyright 2021 Netflix, Inc.
 *
 * 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 com.netflix.graphql.dgs.internal.utils

import org.springframework.web.multipart.MultipartFile
import java.util.regex.Pattern

/**
 * This implementation has borrowed heavily from graphql-servlet-java implementation of the variable mapper.
 * It handles populating the query variables with the files specified by object paths in the multi-part request.
 * Specifically, it takes each entry here '-F map='{ "0": ["variables.input.files.0"], "1": ["variables.input.files.1"] }',
 * and uses the object path, e.g., variables.input.files.0, to navigate to the appropriate path in the query variables, i.e.,
 * "variables": { "input": { "description": "test", "files": [null, null] } } }' and sets it to the file specified as
 * -F '0=@file1.txt' -F '1=@file2.txt'
 *
 * The resulting map of populated query variables is the output.
 * Original => "variables": { "input": { "description": "test", "files": [null, null] } }
 * Transformed => "variables": { "input": { "description": "test", "files": [file1.txt, file2.txt] } }
 */
object MultipartVariableMapper {
    private val PERIOD = Pattern.compile("\\.")

    private val MAP_MAPPER = object : Mapper<MutableMap<String, Any>> {
        override operator fun set(location: MutableMap<String, Any>, target: String, value: MultipartFile): Any? {
            return location.put(target, value)
        }

        override fun recurse(location: MutableMap<String, Any>, target: String): Any {
            return location[target] ?: error("")
        }
    }

    private val LIST_MAPPER = object : Mapper<MutableList<Any>> {
        override operator fun set(location: MutableList<Any>, target: String, value: MultipartFile): Any {
            return location.set(Integer.parseInt(target), value)
        }

        override fun recurse(location: MutableList<Any>, target: String): Any {
            return location[Integer.parseInt(target)]
        }
    }

    internal interface Mapper<T> {
        operator fun set(location: T, target: String, value: MultipartFile): Any?
        fun recurse(location: T, target: String): Any
    }

    @Suppress("UNCHECKED_CAST")
    fun mapVariable(objectPath: String, variables: Map<String, Any>, part: MultipartFile) {
        val segments = PERIOD.split(objectPath)

        if (segments.size < 2) {
            throw RuntimeException("object-path in map must have at least two segments")
        } else if ("variables" != segments[0]) {
            throw RuntimeException("can only map into variables")
        }

        var currentLocation: Any = variables
        for (i in 1 until segments.size) {
            val segmentName = segments[i]
            if (i == segments.size - 1) {
                if (currentLocation is Map<*, *>) {
                    if (null != MAP_MAPPER.set(currentLocation as MutableMap<String, Any>, segmentName, part)) {
                        throw RuntimeException("expected null value when mapping $objectPath")
                    }
                } else {
                    if (null != LIST_MAPPER.set(currentLocation as MutableList<Any>, segmentName, part)) {
                        throw RuntimeException("expected null value when mapping $objectPath")
                    }
                }
            } else {
                if (currentLocation is Map<*, *>) {
                    currentLocation = MAP_MAPPER.recurse(currentLocation as MutableMap<String, Any>, segmentName)
                } else {
                    currentLocation = LIST_MAPPER.recurse(currentLocation as MutableList<Any>, segmentName)
                }
                if (null == currentLocation) {
                    throw RuntimeException("found null intermediate value when trying to map $objectPath")
                }
            }
        }
    }
}

/*
 * Copyright 2020 Netflix, Inc.
 *
 * 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 com.netflix.graphql.dgs.internal.utils

import org.slf4j.Logger

object TimeTracer {
    fun <R> logTime(action: () -> R, logger: Logger, message: String): R {
        val startTime = System.currentTimeMillis()
        val result = action()
        val endTime = System.currentTimeMillis()
        val totalTime = endTime - startTime

        logger.debug(message, totalTime)

        return result
    }
}

package com.netflix.graphql.dgs.metrics.micrometer.dataloader

import com.netflix.graphql.dgs.metrics.DgsMetrics.GqlMetric
import com.netflix.graphql.dgs.metrics.DgsMetrics.GqlTag
import io.micrometer.core.instrument.MeterRegistry
import io.micrometer.core.instrument.Tag
import io.micrometer.core.instrument.Tags
import io.micrometer.core.instrument.Timer
import net.bytebuddy.implementation.bind.annotation.Pipe
import org.dataloader.BatchLoader
import org.slf4j.LoggerFactory
import java.util.concurrent.CompletionStage

internal class BatchLoaderInterceptor(
    private val batchLoader: BatchLoader<*, *>,
    private val name: String,
    private val registry: MeterRegistry
) {

    companion object {
        private val ID = GqlMetric.DATA_LOADER.key
        private val logger = LoggerFactory.getLogger(BatchLoaderInterceptor::class.java)
    }

    fun load(@Pipe pipe: Forwarder<CompletionStage<List<*>>, BatchLoader<*, *>>): CompletionStage<List<*>> {
        logger.debug("Starting metered timer[{}] for BatchLoader.", ID)
        val timerSampler = Timer.start(registry)
        return try {
            pipe.to(batchLoader).whenComplete { result, _ ->
                logger.debug("Finished timer[{}] for BatchLoader.", ID)
                timerSampler.stop(
                    registry,
                    Timer.builder(ID)
                        .tags(
                            Tags.of(
                                Tag.of(GqlTag.LOADER_NAME.key, name),
                                Tag.of(GqlTag.LOADER_BATCH_SIZE.key, result.size.toString())
                            )
                        )
                )
            }
        } catch (exception: Exception) {
            logger.warn("Error creating BatchLoader metric interceptor '{}'", ID)
            pipe.to(batchLoader)
        }
    }
}

package com.netflix.graphql.dgs.metrics.micrometer.dataloader

import com.netflix.graphql.dgs.metrics.DgsMetrics.GqlMetric
import com.netflix.graphql.dgs.metrics.DgsMetrics.GqlTag
import io.micrometer.core.instrument.MeterRegistry
import io.micrometer.core.instrument.Tag
import io.micrometer.core.instrument.Tags
import io.micrometer.core.instrument.Timer
import net.bytebuddy.implementation.bind.annotation.Pipe
import org.dataloader.BatchLoaderWithContext
import org.slf4j.LoggerFactory
import java.util.concurrent.CompletionStage

internal class BatchLoaderWithContextInterceptor(
    private val batchLoader: BatchLoaderWithContext<*, *>,
    private val name: String,
    private val registry: MeterRegistry
) {
    companion object {
        private val ID = GqlMetric.DATA_LOADER.key
        private val logger = LoggerFactory.getLogger(BatchLoaderWithContextInterceptor::class.java)
    }

    fun load(@Pipe pipe: Forwarder<CompletionStage<List<*>>, BatchLoaderWithContext<*, *>>): CompletionStage<List<*>> {
        logger.debug("Starting metered timer[{}] for {}.", ID, javaClass.simpleName)
        val timerSampler = Timer.start(registry)
        return try {
            pipe.to(batchLoader).whenComplete { result, _ ->
                logger.debug("Stopping timer[{}] for {}", ID, javaClass.simpleName)
                timerSampler.stop(
                    registry,
                    Timer.builder(ID)
                        .tags(
                            Tags.of(
                                Tag.of(GqlTag.LOADER_NAME.key, name),
                                Tag.of(GqlTag.LOADER_BATCH_SIZE.key, result.size.toString())
                            )
                        )
                )
            }
        } catch (exception: Exception) {
            logger.warn("Error creating timer interceptor '{}' for {}", ID, javaClass.simpleName)
            pipe.to(batchLoader)
        }
    }
}

package com.netflix.graphql.dgs.metrics.micrometer.dataloader

import com.netflix.graphql.dgs.DataLoaderInstrumentationExtensionProvider
import com.netflix.graphql.dgs.metrics.micrometer.DgsMeterRegistrySupplier
import net.bytebuddy.ByteBuddy
import net.bytebuddy.implementation.MethodDelegation
import net.bytebuddy.implementation.bind.annotation.Pipe
import net.bytebuddy.matcher.ElementMatchers
import org.dataloader.BatchLoader
import org.dataloader.BatchLoaderWithContext
import org.dataloader.MappedBatchLoader
import org.dataloader.MappedBatchLoaderWithContext

class DgsDataLoaderInstrumentationProvider(
    private val meterRegistrySupplier: DgsMeterRegistrySupplier,
) : DataLoaderInstrumentationExtensionProvider {

    private val batchLoaderClasses = mutableMapOf<String, Class<out BatchLoader<*, *>>>()
    private val batchLoaderWithContextClasses = mutableMapOf<String, Class<out BatchLoaderWithContext<*, *>>>()
    private val mappedBatchLoaderClasses = mutableMapOf<String, Class<out MappedBatchLoader<*, *>>>()
    private val mappedBatchLoaderWithContextClasses = mutableMapOf<String, Class<out MappedBatchLoaderWithContext<*, *>>>()

    override fun provide(original: BatchLoader<*, *>, name: String): BatchLoader<*, *> {
        return batchLoaderClasses.getOrPut(name) {
            val withBinders =
                MethodDelegation
                    .withDefaultConfiguration()
                    .withBinders(Pipe.Binder.install(Forwarder::class.java))
                    .to(BatchLoaderInterceptor(original, name, meterRegistrySupplier.get()))
            ByteBuddy()
                .subclass(BatchLoader::class.java)
                .method(ElementMatchers.named("load")).intercept(withBinders)
                .make()
                .load(javaClass.classLoader)
                .loaded
        }.newInstance()
    }

    override fun provide(original: BatchLoaderWithContext<*, *>, name: String): BatchLoaderWithContext<*, *> {
        return batchLoaderWithContextClasses.getOrPut(name) {
            val withBinders = MethodDelegation.withDefaultConfiguration()
                .withBinders(Pipe.Binder.install(Forwarder::class.java))
                .to(BatchLoaderWithContextInterceptor(original, name, meterRegistrySupplier.get()))

            ByteBuddy()
                .subclass(BatchLoaderWithContext::class.java)
                .method(ElementMatchers.named("load")).intercept(withBinders)
                .make()
                .load(javaClass.classLoader)
                .loaded
        }.newInstance()
    }

    override fun provide(original: MappedBatchLoader<*, *>, name: String): MappedBatchLoader<*, *> {
        return mappedBatchLoaderClasses.getOrPut(name) {
            val withBinders = MethodDelegation.withDefaultConfiguration()
                .withBinders(Pipe.Binder.install(Forwarder::class.java)).to(
                    MappedBatchLoaderInterceptor(original, name, meterRegistrySupplier.get())
                )

            ByteBuddy()
                .subclass(MappedBatchLoader::class.java)
                .method(ElementMatchers.named("load")).intercept(withBinders)
                .make()
                .load(javaClass.classLoader)
                .loaded
        }.newInstance()
    }

    override fun provide(
        original: MappedBatchLoaderWithContext<*, *>,
        name: String
    ): MappedBatchLoaderWithContext<*, *> {
        return mappedBatchLoaderWithContextClasses.getOrPut(name) {
            val withBinders = MethodDelegation.withDefaultConfiguration()
                .withBinders(Pipe.Binder.install(Forwarder::class.java))
                .to(MappedBatchLoaderWithContextInterceptor(original, name, meterRegistrySupplier.get()))
            ByteBuddy()
                .subclass(MappedBatchLoaderWithContext::class.java)
                .method(ElementMatchers.named("load")).intercept(withBinders)
                .make()
                .load(javaClass.classLoader)
                .loaded
        }.newInstance()
    }
}

package com.netflix.graphql.dgs.metrics.micrometer.dataloader

internal interface Forwarder<T, S> {
    fun to(target: S): T
}

package com.netflix.graphql.dgs.metrics.micrometer.dataloader

import com.netflix.graphql.dgs.metrics.DgsMetrics.GqlMetric
import com.netflix.graphql.dgs.metrics.DgsMetrics.GqlTag
import io.micrometer.core.instrument.MeterRegistry
import io.micrometer.core.instrument.Tag
import io.micrometer.core.instrument.Tags
import io.micrometer.core.instrument.Timer
import net.bytebuddy.implementation.bind.annotation.Pipe
import org.dataloader.MappedBatchLoader
import org.slf4j.LoggerFactory
import java.util.concurrent.CompletionStage

internal class MappedBatchLoaderInterceptor(
    private val batchLoader: MappedBatchLoader<*, *>,
    private val name: String,
    private val registry: MeterRegistry
) {
    companion object {
        private val ID = GqlMetric.DATA_LOADER.key
        private val logger = LoggerFactory.getLogger(MappedBatchLoaderInterceptor::class.java)
    }

    fun load(@Pipe pipe: Forwarder<CompletionStage<Map<*, *>>, MappedBatchLoader<*, *>>): CompletionStage<Map<*, *>> {
        logger.debug("Starting metered timer[{}] for {}.", ID, javaClass.simpleName)
        val timerSampler = Timer.start(registry)
        return try {
            pipe.to(batchLoader).whenComplete { result, _ ->
                logger.debug("Stopping timer[{}] for {}", ID, javaClass.simpleName)
                timerSampler.stop(
                    registry,
                    Timer.builder(ID)
                        .tags(
                            Tags.of(
                                Tag.of(GqlTag.LOADER_NAME.key, name),
                                Tag.of(GqlTag.LOADER_BATCH_SIZE.key, result.size.toString())
                            )
                        )
                )
            }
        } catch (exception: Exception) {
            logger.warn("Error creating timer interceptor '{}' for {}", ID, javaClass.simpleName)
            pipe.to(batchLoader)
        }
    }
}

package com.netflix.graphql.dgs.metrics.micrometer.dataloader

import com.netflix.graphql.dgs.metrics.DgsMetrics.GqlMetric
import com.netflix.graphql.dgs.metrics.DgsMetrics.GqlTag
import io.micrometer.core.instrument.MeterRegistry
import io.micrometer.core.instrument.Tag
import io.micrometer.core.instrument.Tags
import io.micrometer.core.instrument.Timer
import net.bytebuddy.implementation.bind.annotation.Pipe
import org.dataloader.MappedBatchLoaderWithContext
import org.slf4j.LoggerFactory
import java.util.concurrent.CompletionStage

internal class MappedBatchLoaderWithContextInterceptor(
    private val batchLoader: MappedBatchLoaderWithContext<*, *>,
    private val name: String,
    private val registry: MeterRegistry
) {

    companion object {
        private val ID = GqlMetric.DATA_LOADER.key
        private val logger = LoggerFactory.getLogger(MappedBatchLoaderWithContextInterceptor::class.java)
    }

    fun load(@Pipe pipe: Forwarder<CompletionStage<Map<*, *>>, MappedBatchLoaderWithContext<*, *>>): CompletionStage<Map<*, *>> {
        logger.debug("Starting metered timer[{}] for {}.", ID, javaClass.simpleName)
        val timerSampler = Timer.start(registry)
        return try {
            pipe.to(batchLoader).whenComplete { result, _ ->
                logger.debug("Stopping timer[{}] for {}", ID, javaClass.simpleName)
                timerSampler.stop(
                    registry,
                    Timer.builder(ID)
                        .tags(
                            Tags.of(
                                Tag.of(GqlTag.LOADER_NAME.key, name),
                                Tag.of(GqlTag.LOADER_BATCH_SIZE.key, result.size.toString())
                            )
                        )
                )
            }
        } catch (exception: Exception) {
            logger.warn("Error creating timer interceptor '{}' for {}", ID, javaClass.simpleName)
            pipe.to(batchLoader)
        }
    }
}

/*
 * Copyright 2021 Netflix, Inc.
 *
 * 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 com.netflix.graphql.dgs.metrics.micrometer.tagging

import io.micrometer.core.instrument.Tag

@FunctionalInterface
fun interface DgsContextualTagCustomizer {

    fun getContextualTags(): Iterable<Tag>
}

/*
 * Copyright 2021 Netflix, Inc.
 *
 * 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 com.netflix.graphql.dgs.metrics.micrometer.tagging

import graphql.ExecutionResult
import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters
import io.micrometer.core.instrument.Tag

@FunctionalInterface
fun interface DgsExecutionTagCustomizer {

    fun getExecutionTags(
        parameters: InstrumentationExecutionParameters,
        result: ExecutionResult,
        exception: Throwable?
    ): Iterable<Tag>
}

/*
 * Copyright 2021 Netflix, Inc.
 *
 * 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 com.netflix.graphql.dgs.metrics.micrometer.tagging

import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters
import io.micrometer.core.instrument.Tag

@FunctionalInterface
fun interface DgsFieldFetchTagCustomizer {

    fun getFieldFetchTags(parameters: InstrumentationFieldFetchParameters, error: Throwable?): Iterable<Tag>
}

/*
 * Copyright 2021 Netflix, Inc.
 *
 * 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 com.netflix.graphql.dgs.metrics.micrometer.tagging

import graphql.ExecutionResult
import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters
import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters
import io.micrometer.core.instrument.Tag
import io.micrometer.core.instrument.Tags

interface DgsGraphQLMetricsTagsProvider {

    fun getContextualTags(): Iterable<Tag> = Tags.empty()

    fun getExecutionTags(
        parameters: InstrumentationExecutionParameters,
        result: ExecutionResult,
        exception: Throwable?
    ): Iterable<Tag> = Tags.empty()

    fun getFieldFetchTags(parameters: InstrumentationFieldFetchParameters, exception: Throwable?): Iterable<Tag> = Tags.empty()
}

/*
 * Copyright 2021 Netflix, Inc.
 *
 * 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 com.netflix.graphql.dgs.metrics.micrometer.tagging

import com.netflix.graphql.dgs.metrics.DgsMetrics.GqlTagValue
import graphql.ExecutionResult
import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters
import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters
import io.micrometer.core.instrument.Tag
import io.micrometer.core.instrument.Tags

class SimpleGqlOutcomeTagCustomizer : DgsExecutionTagCustomizer, DgsFieldFetchTagCustomizer {

    override fun getExecutionTags(
        parameters: InstrumentationExecutionParameters,
        result: ExecutionResult,
        exception: Throwable?
    ): Iterable<Tag> {
        return if (result.errors.isNotEmpty() || exception != null) {
            Tags.of(GqlTagValue.FAILURE.tag)
        } else {
            Tags.of(GqlTagValue.SUCCESS.tag)
        }
    }

    override fun getFieldFetchTags(
        parameters: InstrumentationFieldFetchParameters,
        error: Throwable?
    ): Iterable<Tag> {
        return if (error == null) {
            Tags.of(GqlTagValue.SUCCESS.tag)
        } else {
            Tags.of(GqlTagValue.FAILURE.tag)
        }
    }
}