diff --git a/build.gradle b/build.gradle index d25800cf..60e4869a 100644 --- a/build.gradle +++ b/build.gradle @@ -1,212 +1,221 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2016 oEmbedler Inc. and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit + * persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING + * BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + buildscript { repositories { - jcenter() + mavenLocal() mavenCentral() + jcenter() + maven { url "https://dl.bintray.com/graphql-java-kickstart/releases" } + maven { url "https://plugins.gradle.org/m2/" } + maven { url 'https://repo.spring.io/plugins-release' } } dependencies { - classpath 'biz.aQute.bnd:biz.aQute.bnd.gradle:3.1.0' + classpath "com.jfrog.bintray.gradle:gradle-bintray-plugin:1.+" + classpath 'net.researchgate:gradle-release:2.7.0' } } + plugins { - id "com.jfrog.bintray" version "1.8.4" - id "com.jfrog.artifactory" version "4.8.1" id 'net.researchgate.release' version '2.7.0' + id 'io.franzbecker.gradle-lombok' version '3.2.0' apply false + id "com.jfrog.artifactory" version "4.11.0" apply false + id "biz.aQute.bnd" version "4.3.1" apply false } -apply plugin: 'java' -sourceCompatibility = 1.8 -targetCompatibility = 1.8 +subprojects { + apply plugin: 'idea' + apply plugin: 'java' + apply plugin: 'maven-publish' + apply plugin: "com.jfrog.bintray" + apply plugin: 'io.franzbecker.gradle-lombok' + apply plugin: 'com.jfrog.artifactory' -// Tests -apply plugin: 'groovy' + repositories { + mavenLocal() + mavenCentral() + jcenter() + maven { url "https://dl.bintray.com/graphql-java-kickstart/releases" } + maven { url "https://oss.jfrog.org/artifactory/oss-snapshot-local" } + maven { url "https://repo.spring.io/libs-milestone" } + } -repositories { - mavenLocal() - mavenCentral() -} + idea { + module { + downloadJavadoc = true + downloadSources = true + } + } -configurations.all { - exclude group:"org.projectlombok", module: "lombok" -} + compileJava { + sourceCompatibility = SOURCE_COMPATIBILITY + targetCompatibility = TARGET_COMPATIBILITY + } -dependencies { - compile 'org.slf4j:slf4j-api:1.7.21' - - // Useful utilities - compile 'com.google.guava:guava:24.1.1-jre' - - // Unit testing - testCompile "org.codehaus.groovy:groovy-all:2.4.1" - testCompile "org.spockframework:spock-core:1.1-groovy-2.4-rc-3" - testRuntime "cglib:cglib-nodep:3.2.4" - testRuntime "org.objenesis:objenesis:2.5.1" - testCompile 'org.slf4j:slf4j-simple:1.7.24' - testCompile 'org.springframework:spring-test:4.3.7.RELEASE' - testRuntime 'org.springframework:spring-web:4.3.7.RELEASE' - - // OSGi - compileOnly 'org.osgi:org.osgi.core:6.0.0' - compileOnly 'org.osgi:org.osgi.service.cm:1.5.0' - compileOnly 'org.osgi:org.osgi.service.component:1.3.0' - compileOnly 'biz.aQute.bnd:biz.aQute.bndlib:3.1.0' - - // Servlet - compile 'javax.servlet:javax.servlet-api:3.1.0' - compile 'javax.websocket:javax.websocket-api:1.1' - - // GraphQL - compile "com.graphql-java:graphql-java:$LIB_GRAPHQL_JAVA_VER" - - testCompile 'io.github.graphql-java:graphql-java-annotations:5.2' - - // JSON - compile "com.fasterxml.jackson.core:jackson-core:$LIB_JACKSON_VER" - compile "com.fasterxml.jackson.core:jackson-annotations:$LIB_JACKSON_VER" - compile "com.fasterxml.jackson.core:jackson-databind:$LIB_JACKSON_VER" - compile "com.fasterxml.jackson.datatype:jackson-datatype-jdk8:$LIB_JACKSON_VER" -} + compileJava.dependsOn(processResources) -apply plugin: 'osgi' -apply plugin: 'java-library-distribution' -apply plugin: 'biz.aQute.bnd.builder' -apply plugin: 'com.jfrog.bintray' -apply plugin: 'maven-publish' -apply plugin: 'idea' -apply plugin: 'maven' - -jar { - manifest { - instruction 'Require-Capability', 'osgi.extender' + lombok { + version = "1.18.4" + sha256 = "" } -} -// custom tasks for creating source/javadoc jars -task sourcesJar(type: Jar, dependsOn: classes) { - classifier = 'sources' - from sourceSets.main.allSource -} -task javadocJar(type: Jar, dependsOn: javadoc) { - classifier = 'javadoc' - from javadoc.destinationDir -} + if (!it.name.startsWith('example')) { -publishing { - publications { - maven(MavenPublication) { - from components.java - groupId 'com.graphql-java-kickstart' - artifactId project.name - version project.version - - artifact sourcesJar - artifact javadocJar - - pom.withXml { - asNode().children().last() + { - resolveStrategy = Closure.DELEGATE_FIRST - name 'graphql-java-servlet' - description 'relay.js-compatible GraphQL servlet' - url 'https://github.com/graphql-java-kickstart/graphql-java-servlet' - inceptionYear '2016' - - scm { - url 'https://github.com/graphql-java-kickstart/graphql-java-servlet' - connection 'scm:https://github.com/graphql-java-kickstart/graphql-java-servlet.git' - developerConnection 'scm:git://github.com/graphql-java-kickstart/graphql-java-servlet.git' - } + jar { + from "LICENSE.md" + } - licenses { - license { - name 'The Apache Software License, Version 2.0' - url 'http://www.apache.org/licenses/LICENSE-2.0.txt' - distribution 'repo' - } + task sourcesJar(type: Jar) { + dependsOn classes + classifier 'sources' + from sourceSets.main.allSource + } + + task javadocJar(type: Jar, dependsOn: javadoc) { + classifier = 'javadoc' + from javadoc.destinationDir + } + + artifacts { + archives sourcesJar + archives javadocJar + } + + publishing { + publications { + mainProjectPublication(MavenPublication) { + version version + from components.java + + artifact sourcesJar { + classifier "sources" + } + artifact javadocJar { + classifier "javadoc" } - developers { - developer { - id 'yrashk' - name 'Yurii Rashkovskii' - email 'yrashk@gmail.com' - } - developer { - id 'apottere' - name 'Andrew Potter' - email 'apottere@gmail.com' + pom.withXml { + asNode().children().last() + { + resolveStrategy = Closure.DELEGATE_FIRST + name 'graphql-java-servlet' + description 'relay.js-compatible GraphQL servlet' + url 'https://github.com/graphql-java-kickstart/graphql-java-servlet' + inceptionYear '2016' + + scm { + url 'https://github.com/graphql-java-kickstart/graphql-java-servlet' + connection 'scm:https://github.com/graphql-java-kickstart/graphql-java-servlet.git' + developerConnection 'scm:git://github.com/graphql-java-kickstart/graphql-java-servlet.git' + } + + licenses { + license { + name 'The Apache Software License, Version 2.0' + url 'http://www.apache.org/licenses/LICENSE-2.0.txt' + distribution 'repo' + } + } + + developers { + developer { + id 'yrashk' + name 'Yurii Rashkovskii' + email 'yrashk@gmail.com' + } + developer { + id 'apottere' + name 'Andrew Potter' + email 'apottere@gmail.com' + } + } } + // https://discuss.gradle.org/t/maven-publish-plugin-generated-pom-making-dependency-scope-runtime/7494/10 + asNode().dependencies.'*'.findAll() { + it.scope.text() == 'runtime' && project.configurations.compile.allDependencies.find { dep -> + dep.name == it.artifactId.text() + } + }.each { it.scope*.value = 'compile'} } } - // https://discuss.gradle.org/t/maven-publish-plugin-generated-pom-making-dependency-scope-runtime/7494/10 - asNode().dependencies.'*'.findAll() { - it.scope.text() == 'runtime' && project.configurations.compile.allDependencies.find { dep -> - dep.name == it.artifactId.text() - } - }.each { it.scope*.value = 'compile'} } } - } -} - -release { - tagTemplate = 'v${version}' - failOnPublishNeeded = false - ignoredSnapshotDependencies = ['com.graphql-java-kickstart:graphql-java-servlet'] -} -afterReleaseBuild.dependsOn bintrayUpload - -bintray { - user = System.env.BINTRAY_USER ?: project.findProperty('BINTRAY_USER') ?: '' - key = System.env.BINTRAY_PASS ?: project.findProperty('BINTRAY_PASS') ?: '' - publications = ['maven'] - publish = true - pkg { - repo = 'releases' - name = project.name - licenses = ['Apache-2.0'] - vcsUrl = 'https://github.com/graphql-java-kickstart/graphql-java-servlet' - userOrg = 'graphql-java-kickstart' - version { - name = project.version - mavenCentralSync { - close = '1' + bintray { + user = System.env.BINTRAY_USER ?: project.findProperty('BINTRAY_USER') ?: '' + key = System.env.BINTRAY_PASS ?: project.findProperty('BINTRAY_PASS') ?: '' + publications = ['mainProjectPublication'] + publish = true + pkg { + repo = 'releases' + name = PROJECT_NAME + desc = PROJECT_DESC + licenses = [PROJECT_LICENSE] + vcsUrl = PROJECT_GIT_REPO_URL + userOrg = 'graphql-java-kickstart' + version { + name = project.version + mavenCentralSync { + close = '1' + } + } } } - } -} -artifactory { - contextUrl = 'https://oss.jfrog.org' - publish { - repository { - repoKey = 'oss-snapshot-local' + artifactory { + contextUrl = 'https://oss.jfrog.org' + publish { + repository { + repoKey = 'oss-snapshot-local' - username = System.env.BINTRAY_USER ?: System.getProperty('BINTRAY_USER') - password = System.env.BINTRAY_PASS ?: System.getProperty('BINTRAY_PASS') + username = System.env.BINTRAY_USER ?: System.getProperty('BINTRAY_USER') + password = System.env.BINTRAY_PASS ?: System.getProperty('BINTRAY_PASS') - maven = true - } - defaults { - publications 'maven' - publishArtifacts = true - publishPom = true - } - } - resolve { - repository { - repoKey = 'jcenter' + maven = true + } + defaults { + publications 'maven' + publishArtifacts = true + publishPom = true + } + } + resolve { + repository { + repoKey = 'jcenter' + } + } } } } -idea { - project { - languageLevel = '11' - vcs = 'Git' - } +release { + tagTemplate = 'v${version}' + failOnPublishNeeded = false + ignoredSnapshotDependencies = ['com.graphql-java-kickstart:graphql-java-servlet'] +} + +task build { + dependsOn subprojects.findResults { it.tasks.findByName('assemble') } + dependsOn subprojects.findResults { it.tasks.findByName('check') } + dependsOn subprojects.findResults { it.tasks.findByName('bintray') } } wrapper { - gradleVersion = '4.10.3' + gradleVersion = "${GRADLE_WRAPPER_VER}" } diff --git a/gradle.properties b/gradle.properties index c38b93db..425ddf04 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,18 @@ -version = 8.1.0-SNAPSHOT +version = 9.0.0-SNAPSHOT group = com.graphql-java-kickstart +PROJECT_NAME = graphql-java-kickstart +PROJECT_DESC = GraphQL Java Kickstart +PROJECT_GIT_REPO_URL = https://github.com/graphql-java-kickstart/graphql-spring-boot +PROJECT_LICENSE = MIT +PROJECT_LICENSE_URL = https://github.com/graphql-java-kickstart/spring-boot-graphql/blob/master/LICENSE.md +PROJECT_DEV_ID = apottere +PROJECT_DEV_NAME = Andrew Potter + LIB_GRAPHQL_JAVA_VER = 13.0 -LIB_JACKSON_VER = 2.9.9 +LIB_JACKSON_VER = 2.10.0 + +SOURCE_COMPATIBILITY = 1.8 +TARGET_COMPATIBILITY = 1.8 + +GRADLE_WRAPPER_VER = 6.0.1 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 310a46a5..5f8ff30a 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Thu Jun 20 12:32:36 CEST 2019 +#Thu Nov 14 18:53:34 CET 2019 +distributionUrl=https\://services.gradle.org/distributions/gradle-6.0.1-all.zip distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.3-all.zip +zipStoreBase=GRADLE_USER_HOME diff --git a/graphql-java-kickstart/build.gradle b/graphql-java-kickstart/build.gradle new file mode 100644 index 00000000..cf44bc07 --- /dev/null +++ b/graphql-java-kickstart/build.gradle @@ -0,0 +1,10 @@ +dependencies { + // GraphQL + compile "com.graphql-java:graphql-java:$LIB_GRAPHQL_JAVA_VER" + + // JSON + compile "com.fasterxml.jackson.core:jackson-core:$LIB_JACKSON_VER" + compile "com.fasterxml.jackson.core:jackson-annotations:$LIB_JACKSON_VER" + compile "com.fasterxml.jackson.core:jackson-databind:$LIB_JACKSON_VER" + compile "com.fasterxml.jackson.datatype:jackson-datatype-jdk8:$LIB_JACKSON_VER" +} diff --git a/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/BatchedDataLoaderGraphQLBuilder.java b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/BatchedDataLoaderGraphQLBuilder.java new file mode 100644 index 00000000..ba66dcdc --- /dev/null +++ b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/BatchedDataLoaderGraphQLBuilder.java @@ -0,0 +1,42 @@ +package graphql.kickstart.execution; + +import graphql.ExecutionInput; +import graphql.GraphQL; +import graphql.execution.instrumentation.Instrumentation; +import graphql.execution.instrumentation.dataloader.DataLoaderDispatcherInstrumentationOptions; +import graphql.kickstart.execution.config.GraphQLBuilder; +import graphql.kickstart.execution.input.GraphQLBatchedInvocationInput; +import graphql.kickstart.execution.input.GraphQLSingleInvocationInput; +import java.util.List; +import java.util.function.Supplier; + +public class BatchedDataLoaderGraphQLBuilder { + + private final Supplier optionsSupplier; + + public BatchedDataLoaderGraphQLBuilder(Supplier optionsSupplier) { + if (optionsSupplier != null) { + this.optionsSupplier = optionsSupplier; + } else { + this.optionsSupplier = DataLoaderDispatcherInstrumentationOptions::newOptions; + } + } + + GraphQL newGraphQL(GraphQLBatchedInvocationInput invocationInput, GraphQLBuilder graphQLBuilder) { + Supplier supplier = augment(invocationInput, graphQLBuilder.getInstrumentationSupplier()); + return invocationInput.getInvocationInputs().stream().findFirst() + .map(GraphQLSingleInvocationInput::getSchema) + .map(schema -> graphQLBuilder.build(schema, supplier)) + .orElseThrow(() -> new IllegalArgumentException("Batched invocation input must contain at least one query")); + } + + private Supplier augment( + GraphQLBatchedInvocationInput batchedInvocationInput, + Supplier instrumentationSupplier + ) { + List executionInputs = batchedInvocationInput.getExecutionInputs(); + return batchedInvocationInput.getContextSetting() + .configureInstrumentationForContext(instrumentationSupplier, executionInputs, optionsSupplier.get()); + } + +} diff --git a/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/DecoratedExecutionResult.java b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/DecoratedExecutionResult.java new file mode 100644 index 00000000..149f3657 --- /dev/null +++ b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/DecoratedExecutionResult.java @@ -0,0 +1,49 @@ +package graphql.kickstart.execution; + +import graphql.ExecutionResult; +import graphql.GraphQL; +import graphql.GraphQLError; +import java.util.List; +import java.util.Map; +import lombok.RequiredArgsConstructor; +import org.reactivestreams.Publisher; + +@RequiredArgsConstructor +class DecoratedExecutionResult implements ExecutionResult { + + private final ExecutionResult result; + + boolean isAsynchronous() { + return result.getData() instanceof Publisher || isDeferred(); + } + + private boolean isDeferred() { + return result.getExtensions() != null && result.getExtensions().containsKey(GraphQL.DEFERRED_RESULTS); + } + + @Override + public List getErrors() { + return result.getErrors(); + } + + @Override + public T getData() { + return result.getData(); + } + + @Override + public boolean isDataPresent() { + return result.isDataPresent(); + } + + @Override + public Map getExtensions() { + return result.getExtensions(); + } + + @Override + public Map toSpecification() { + return result.toSpecification(); + } + +} diff --git a/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/DefaultGraphQLRootObjectBuilder.java b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/DefaultGraphQLRootObjectBuilder.java new file mode 100644 index 00000000..b913e37b --- /dev/null +++ b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/DefaultGraphQLRootObjectBuilder.java @@ -0,0 +1,9 @@ +package graphql.kickstart.execution; + +public class DefaultGraphQLRootObjectBuilder extends StaticGraphQLRootObjectBuilder { + + public DefaultGraphQLRootObjectBuilder() { + super(new Object()); + } + +} diff --git a/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/GraphQLBatchedQueryResult.java b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/GraphQLBatchedQueryResult.java new file mode 100644 index 00000000..20b97800 --- /dev/null +++ b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/GraphQLBatchedQueryResult.java @@ -0,0 +1,24 @@ +package graphql.kickstart.execution; + +import graphql.ExecutionResult; +import java.util.List; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +class GraphQLBatchedQueryResult implements GraphQLQueryResult { + + @Getter + private final List results; + + @Override + public boolean isBatched() { + return true; + } + + @Override + public boolean isAsynchronous() { + return false; + } + +} diff --git a/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/GraphQLErrorQueryResult.java b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/GraphQLErrorQueryResult.java new file mode 100644 index 00000000..bf1fc9d8 --- /dev/null +++ b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/GraphQLErrorQueryResult.java @@ -0,0 +1,27 @@ +package graphql.kickstart.execution; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +class GraphQLErrorQueryResult implements GraphQLQueryResult { + + private final int statusCode; + private final String message; + + @Override + public boolean isBatched() { + return false; + } + + @Override + public boolean isAsynchronous() { + return false; + } + + @Override + public boolean isError() { + return true; + } +} diff --git a/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/GraphQLInvoker.java b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/GraphQLInvoker.java new file mode 100644 index 00000000..27e2f08e --- /dev/null +++ b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/GraphQLInvoker.java @@ -0,0 +1,51 @@ +package graphql.kickstart.execution; + +import static java.util.stream.Collectors.toList; + +import graphql.ExecutionResult; +import graphql.GraphQL; +import graphql.kickstart.execution.config.GraphQLBuilder; +import graphql.kickstart.execution.input.GraphQLBatchedInvocationInput; +import graphql.kickstart.execution.input.GraphQLInvocationInput; +import graphql.kickstart.execution.input.GraphQLSingleInvocationInput; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import lombok.AllArgsConstructor; +import lombok.RequiredArgsConstructor; + +@AllArgsConstructor +@RequiredArgsConstructor +public class GraphQLInvoker { + + private final GraphQLBuilder graphQLBuilder; + private final BatchedDataLoaderGraphQLBuilder batchedDataLoaderGraphQLBuilder; + private GraphQLInvokerProxy proxy = GraphQL::executeAsync; + + public CompletableFuture executeAsync(GraphQLSingleInvocationInput invocationInput) { + GraphQL graphQL = graphQLBuilder.build(invocationInput.getSchema()); + return proxy.executeAsync(graphQL, invocationInput.getExecutionInput()); + } + + public GraphQLQueryResult query(GraphQLInvocationInput invocationInput) { + if (invocationInput instanceof GraphQLSingleInvocationInput) { + return GraphQLQueryResult.create(query((GraphQLSingleInvocationInput) invocationInput)); + } + GraphQLBatchedInvocationInput batchedInvocationInput = (GraphQLBatchedInvocationInput) invocationInput; + return GraphQLQueryResult.create(query(batchedInvocationInput)); + } + + private ExecutionResult query(GraphQLSingleInvocationInput singleInvocationInput) { + return executeAsync(singleInvocationInput).join(); + } + + private List query(GraphQLBatchedInvocationInput batchedInvocationInput) { + GraphQL graphQL = batchedDataLoaderGraphQLBuilder.newGraphQL(batchedInvocationInput, graphQLBuilder); + return batchedInvocationInput.getExecutionInputs().stream() + .map(executionInput -> proxy.executeAsync(graphQL, executionInput)) + .collect(toList()) + .stream() + .map(CompletableFuture::join) + .collect(toList()); + } +} + diff --git a/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/GraphQLInvokerProxy.java b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/GraphQLInvokerProxy.java new file mode 100644 index 00000000..a09e1681 --- /dev/null +++ b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/GraphQLInvokerProxy.java @@ -0,0 +1,12 @@ +package graphql.kickstart.execution; + +import graphql.ExecutionInput; +import graphql.ExecutionResult; +import graphql.GraphQL; +import java.util.concurrent.CompletableFuture; + +public interface GraphQLInvokerProxy { + + CompletableFuture executeAsync(GraphQL graphQL, ExecutionInput executionInput); + +} diff --git a/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/GraphQLInvokerSubjectProxy.java b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/GraphQLInvokerSubjectProxy.java new file mode 100644 index 00000000..5995adab --- /dev/null +++ b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/GraphQLInvokerSubjectProxy.java @@ -0,0 +1,30 @@ +package graphql.kickstart.execution; + +import graphql.ExecutionInput; +import graphql.ExecutionResult; +import graphql.GraphQL; +import graphql.kickstart.execution.context.GraphQLContext; +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.util.concurrent.CompletableFuture; +import javax.security.auth.Subject; + +public class GraphQLInvokerSubjectProxy implements GraphQLInvokerProxy { + + @Override + public CompletableFuture executeAsync(GraphQL graphQL, ExecutionInput executionInput) { + GraphQLContext context = (GraphQLContext) executionInput.getContext(); + if (Subject.getSubject(AccessController.getContext()) == null && context.getSubject().isPresent()) { + return Subject + .doAs(context.getSubject().get(), (PrivilegedAction>) () -> { + try { + return graphQL.executeAsync(executionInput); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + } + return graphQL.executeAsync(executionInput); + } + +} diff --git a/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/GraphQLObjectMapper.java b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/GraphQLObjectMapper.java new file mode 100644 index 00000000..a7fc3408 --- /dev/null +++ b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/GraphQLObjectMapper.java @@ -0,0 +1,222 @@ +package graphql.kickstart.execution; + +import static java.util.stream.Collectors.toList; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.MappingIterator; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectReader; +import graphql.DeferredExecutionResult; +import graphql.DeferredExecutionResultImpl; +import graphql.ExecutionResult; +import graphql.ExecutionResultImpl; +import graphql.GraphQLError; +import graphql.execution.ExecutionPath; +import graphql.kickstart.execution.config.ConfiguringObjectMapperProvider; +import graphql.kickstart.execution.config.ObjectMapperConfigurer; +import graphql.kickstart.execution.config.ObjectMapperProvider; +import graphql.kickstart.execution.error.DefaultGraphQLErrorHandler; +import graphql.kickstart.execution.error.GraphQLErrorHandler; +import java.io.IOException; +import java.io.InputStream; +import java.io.Writer; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Supplier; + +/** + * @author Andrew Potter + */ +public class GraphQLObjectMapper { + + private static final TypeReference>> + MULTIPART_MAP_TYPE_REFERENCE = new TypeReference>>() { + }; + private final ObjectMapperProvider objectMapperProvider; + private final Supplier graphQLErrorHandlerSupplier; + + private volatile ObjectMapper mapper; + + protected GraphQLObjectMapper(ObjectMapperProvider objectMapperProvider, + Supplier graphQLErrorHandlerSupplier) { + this.objectMapperProvider = objectMapperProvider; + this.graphQLErrorHandlerSupplier = graphQLErrorHandlerSupplier; + } + + public static Builder newBuilder() { + return new Builder(); + } + + // Double-check idiom for lazy initialization of instance fields. + public ObjectMapper getJacksonMapper() { + ObjectMapper result = mapper; + if (result == null) { // First check (no locking) + synchronized (this) { + result = mapper; + if (result == null) { // Second check (with locking) + mapper = result = objectMapperProvider.provide(); + } + } + } + + return result; + } + + /** + * @return an {@link ObjectReader} for deserializing {@link GraphQLRequest} + */ + public ObjectReader getGraphQLRequestMapper() { + return getJacksonMapper().reader().forType(GraphQLRequest.class); + } + + public GraphQLRequest readGraphQLRequest(InputStream inputStream) throws IOException { + return getGraphQLRequestMapper().readValue(inputStream); + } + + public GraphQLRequest readGraphQLRequest(String text) throws IOException { + return getGraphQLRequestMapper().readValue(text); + } + + public List readBatchedGraphQLRequest(InputStream inputStream) throws IOException { + MappingIterator iterator = getGraphQLRequestMapper().readValues(inputStream); + List requests = new ArrayList<>(); + + while (iterator.hasNext()) { + requests.add(iterator.next()); + } + + return requests; + } + + public List readBatchedGraphQLRequest(String query) throws IOException { + MappingIterator iterator = getGraphQLRequestMapper().readValues(query); + List requests = new ArrayList<>(); + + while (iterator.hasNext()) { + requests.add(iterator.next()); + } + + return requests; + } + + public String serializeResultAsJson(ExecutionResult executionResult) { + try { + return getJacksonMapper().writeValueAsString(createResultFromExecutionResult(executionResult)); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + + public void serializeResultAsJson(Writer writer, ExecutionResult executionResult) throws IOException { + getJacksonMapper().writeValue(writer, createResultFromExecutionResult(executionResult)); + } + + public boolean areErrorsPresent(ExecutionResult executionResult) { + return graphQLErrorHandlerSupplier.get().errorsPresent(executionResult.getErrors()); + } + + public ExecutionResult sanitizeErrors(ExecutionResult executionResult) { + Object data = executionResult.getData(); + Map extensions = executionResult.getExtensions(); + List errors = executionResult.getErrors(); + + GraphQLErrorHandler errorHandler = graphQLErrorHandlerSupplier.get(); + if (errorHandler.errorsPresent(errors)) { + errors = errorHandler.processErrors(errors); + } else { + errors = null; + } + return new ExecutionResultImpl(data, errors, extensions); + } + + public Map createResultFromExecutionResult(ExecutionResult executionResult) { + ExecutionResult sanitizedExecutionResult = sanitizeErrors(executionResult); + if (executionResult instanceof DeferredExecutionResult) { + sanitizedExecutionResult = DeferredExecutionResultImpl + .newDeferredExecutionResult() + .from(executionResult) + .path(ExecutionPath.fromList(((DeferredExecutionResult) executionResult).getPath())) + .build(); + } + return convertSanitizedExecutionResult(sanitizedExecutionResult); + } + + public Map convertSanitizedExecutionResult(ExecutionResult executionResult) { + return convertSanitizedExecutionResult(executionResult, true); + } + + public Map convertSanitizedExecutionResult(ExecutionResult executionResult, boolean includeData) { + final Map result = new LinkedHashMap<>(); + + if (areErrorsPresent(executionResult)) { + result.put("errors", executionResult.getErrors().stream().map(GraphQLError::toSpecification).collect(toList())); + } + + if (executionResult.getExtensions() != null && !executionResult.getExtensions().isEmpty()) { + result.put("extensions", executionResult.getExtensions()); + } + + if (includeData) { + result.put("data", executionResult.getData()); + } + + if (executionResult instanceof DeferredExecutionResult) { + result.put("path", ((DeferredExecutionResult) executionResult).getPath()); + } + + return result; + } + + public Map deserializeVariables(String variables) { + try { + return VariablesDeserializer + .deserializeVariablesObject(getJacksonMapper().readValue(variables, Object.class), getJacksonMapper()); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public Map> deserializeMultipartMap(InputStream inputStream) { + try { + return getJacksonMapper().readValue(inputStream, MULTIPART_MAP_TYPE_REFERENCE); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public static class Builder { + + private ObjectMapperProvider objectMapperProvider = new ConfiguringObjectMapperProvider(); + private Supplier graphQLErrorHandler = DefaultGraphQLErrorHandler::new; + + public Builder withObjectMapperConfigurer(ObjectMapperConfigurer objectMapperConfigurer) { + return withObjectMapperConfigurer(() -> objectMapperConfigurer); + } + + public Builder withObjectMapperConfigurer(Supplier objectMapperConfigurer) { + this.objectMapperProvider = new ConfiguringObjectMapperProvider(objectMapperConfigurer.get()); + return this; + } + + public Builder withObjectMapperProvider(ObjectMapperProvider objectMapperProvider) { + this.objectMapperProvider = objectMapperProvider; + return this; + } + + public Builder withGraphQLErrorHandler(GraphQLErrorHandler graphQLErrorHandler) { + return withGraphQLErrorHandler(() -> graphQLErrorHandler); + } + + public Builder withGraphQLErrorHandler(Supplier graphQLErrorHandler) { + this.graphQLErrorHandler = graphQLErrorHandler; + return this; + } + + public GraphQLObjectMapper build() { + return new GraphQLObjectMapper(objectMapperProvider, graphQLErrorHandler); + } + } +} diff --git a/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/GraphQLQueryInvoker.java b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/GraphQLQueryInvoker.java new file mode 100644 index 00000000..1d3f6a59 --- /dev/null +++ b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/GraphQLQueryInvoker.java @@ -0,0 +1,109 @@ +package graphql.kickstart.execution; + +import graphql.execution.instrumentation.ChainedInstrumentation; +import graphql.execution.instrumentation.Instrumentation; +import graphql.execution.instrumentation.SimpleInstrumentation; +import graphql.execution.instrumentation.dataloader.DataLoaderDispatcherInstrumentationOptions; +import graphql.execution.preparsed.NoOpPreparsedDocumentProvider; +import graphql.execution.preparsed.PreparsedDocumentProvider; +import graphql.kickstart.execution.config.DefaultExecutionStrategyProvider; +import graphql.kickstart.execution.config.ExecutionStrategyProvider; +import graphql.kickstart.execution.config.GraphQLBuilder; +import java.util.List; +import java.util.function.Supplier; + +/** + * @author Andrew Potter + */ +public class GraphQLQueryInvoker { + + private final Supplier getExecutionStrategyProvider; + private final Supplier getInstrumentation; + private final Supplier getPreparsedDocumentProvider; + private final Supplier optionsSupplier; + + protected GraphQLQueryInvoker(Supplier getExecutionStrategyProvider, + Supplier getInstrumentation, + Supplier getPreparsedDocumentProvider, + Supplier optionsSupplier) { + this.getExecutionStrategyProvider = getExecutionStrategyProvider; + this.getInstrumentation = getInstrumentation; + this.getPreparsedDocumentProvider = getPreparsedDocumentProvider; + this.optionsSupplier = optionsSupplier; + } + + public static Builder newBuilder() { + return new Builder(); + } + + public GraphQLInvoker toGraphQLInvoker() { + GraphQLBuilder graphQLBuilder = new GraphQLBuilder() + .executionStrategyProvider(getExecutionStrategyProvider) + .instrumentation(getInstrumentation) + .preparsedDocumentProvider(getPreparsedDocumentProvider); + return new GraphQLInvoker(graphQLBuilder, new BatchedDataLoaderGraphQLBuilder(optionsSupplier)); + } + + public static class Builder { + + private Supplier getExecutionStrategyProvider = DefaultExecutionStrategyProvider::new; + private Supplier getInstrumentation = () -> SimpleInstrumentation.INSTANCE; + private Supplier getPreparsedDocumentProvider = () -> NoOpPreparsedDocumentProvider.INSTANCE; + private Supplier dataLoaderDispatcherInstrumentationOptionsSupplier = DataLoaderDispatcherInstrumentationOptions::newOptions; + + + public Builder withExecutionStrategyProvider(ExecutionStrategyProvider provider) { + return withExecutionStrategyProvider(() -> provider); + } + + public Builder withExecutionStrategyProvider(Supplier supplier) { + this.getExecutionStrategyProvider = supplier; + return this; + } + + public Builder withInstrumentation(Instrumentation instrumentation) { + return withInstrumentation(() -> instrumentation); + } + + public Builder withInstrumentation(Supplier supplier) { + this.getInstrumentation = supplier; + return this; + } + + public Builder with(List instrumentations) { + if (instrumentations.isEmpty()) { + return this; + } + if (instrumentations.size() == 1) { + withInstrumentation(instrumentations.get(0)); + } else { + withInstrumentation(new ChainedInstrumentation(instrumentations)); + } + return this; + } + + public Builder withPreparsedDocumentProvider(PreparsedDocumentProvider provider) { + return withPreparsedDocumentProvider(() -> provider); + } + + public Builder withPreparsedDocumentProvider(Supplier supplier) { + this.getPreparsedDocumentProvider = supplier; + return this; + } + + public Builder withDataLoaderDispatcherInstrumentationOptions(DataLoaderDispatcherInstrumentationOptions options) { + return withDataLoaderDispatcherInstrumentationOptions(() -> options); + } + + public Builder withDataLoaderDispatcherInstrumentationOptions( + Supplier supplier) { + this.dataLoaderDispatcherInstrumentationOptionsSupplier = supplier; + return this; + } + + public GraphQLQueryInvoker build() { + return new GraphQLQueryInvoker(getExecutionStrategyProvider, getInstrumentation, getPreparsedDocumentProvider, + dataLoaderDispatcherInstrumentationOptionsSupplier); + } + } +} diff --git a/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/GraphQLQueryResult.java b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/GraphQLQueryResult.java new file mode 100644 index 00000000..29139aad --- /dev/null +++ b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/GraphQLQueryResult.java @@ -0,0 +1,43 @@ +package graphql.kickstart.execution; + +import static java.util.Collections.emptyList; + +import graphql.ExecutionResult; +import java.util.List; + +public interface GraphQLQueryResult { + + static GraphQLSingleQueryResult create(ExecutionResult result) { + return new GraphQLSingleQueryResult(new DecoratedExecutionResult(result)); + } + + static GraphQLBatchedQueryResult create(List results) { + return new GraphQLBatchedQueryResult(results); + } + + static GraphQLErrorQueryResult createError(int statusCode, String message) { + return new GraphQLErrorQueryResult(statusCode, message); + } + + boolean isBatched(); + + boolean isAsynchronous(); + + default DecoratedExecutionResult getResult() { + return null; + } + + default List getResults() { + return emptyList(); + } + + default boolean isError() { return false; } + + default int getStatusCode() { + return 200; + } + + default String getMessage() { + return null; + } +} diff --git a/src/main/java/graphql/servlet/core/internal/GraphQLRequest.java b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/GraphQLRequest.java similarity index 78% rename from src/main/java/graphql/servlet/core/internal/GraphQLRequest.java rename to graphql-java-kickstart/src/main/java/graphql/kickstart/execution/GraphQLRequest.java index 41a3958f..caf093d3 100644 --- a/src/main/java/graphql/servlet/core/internal/GraphQLRequest.java +++ b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/GraphQLRequest.java @@ -1,8 +1,9 @@ -package graphql.servlet.core.internal; +package graphql.kickstart.execution; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import graphql.introspection.IntrospectionQuery; import java.util.HashMap; import java.util.Map; @@ -27,6 +28,14 @@ public GraphQLRequest(String query, Map variables, String operat } } + public static GraphQLRequest createIntrospectionRequest() { + return new GraphQLRequest(IntrospectionQuery.INTROSPECTION_QUERY, new HashMap<>(), null); + } + + public static GraphQLRequest createQueryOnlyRequest(String query) { + return new GraphQLRequest(query, new HashMap<>(), null); + } + public String getQuery() { return query; } diff --git a/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/GraphQLRootObjectBuilder.java b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/GraphQLRootObjectBuilder.java new file mode 100644 index 00000000..55e7c763 --- /dev/null +++ b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/GraphQLRootObjectBuilder.java @@ -0,0 +1,10 @@ +package graphql.kickstart.execution; + +public interface GraphQLRootObjectBuilder { + + /** + * @return the graphql root object + */ + Object build(); + +} diff --git a/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/GraphQLSingleQueryResult.java b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/GraphQLSingleQueryResult.java new file mode 100644 index 00000000..20f1083a --- /dev/null +++ b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/GraphQLSingleQueryResult.java @@ -0,0 +1,22 @@ +package graphql.kickstart.execution; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +class GraphQLSingleQueryResult implements GraphQLQueryResult { + + @Getter + private final DecoratedExecutionResult result; + + @Override + public boolean isBatched() { + return false; + } + + @Override + public boolean isAsynchronous() { + return result.isAsynchronous(); + } + +} diff --git a/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/StaticGraphQLRootObjectBuilder.java b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/StaticGraphQLRootObjectBuilder.java new file mode 100644 index 00000000..a780a02c --- /dev/null +++ b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/StaticGraphQLRootObjectBuilder.java @@ -0,0 +1,20 @@ +package graphql.kickstart.execution; + +public class StaticGraphQLRootObjectBuilder implements GraphQLRootObjectBuilder { + + private final Object rootObject; + + public StaticGraphQLRootObjectBuilder(Object rootObject) { + this.rootObject = rootObject; + } + + @Override + public Object build() { + return rootObject; + } + + protected Object getRootObject() { + return rootObject; + } + +} diff --git a/src/main/java/graphql/servlet/core/internal/VariablesDeserializer.java b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/VariablesDeserializer.java similarity index 97% rename from src/main/java/graphql/servlet/core/internal/VariablesDeserializer.java rename to graphql-java-kickstart/src/main/java/graphql/kickstart/execution/VariablesDeserializer.java index c7bc1925..fdd3a3ff 100644 --- a/src/main/java/graphql/servlet/core/internal/VariablesDeserializer.java +++ b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/VariablesDeserializer.java @@ -1,4 +1,4 @@ -package graphql.servlet.core.internal; +package graphql.kickstart.execution; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.ObjectCodec; diff --git a/src/main/java/graphql/servlet/config/ConfiguringObjectMapperProvider.java b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/config/ConfiguringObjectMapperProvider.java similarity index 85% rename from src/main/java/graphql/servlet/config/ConfiguringObjectMapperProvider.java rename to graphql-java-kickstart/src/main/java/graphql/kickstart/execution/config/ConfiguringObjectMapperProvider.java index 521465e5..491bd5f1 100644 --- a/src/main/java/graphql/servlet/config/ConfiguringObjectMapperProvider.java +++ b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/config/ConfiguringObjectMapperProvider.java @@ -1,9 +1,7 @@ -package graphql.servlet.config; +package graphql.kickstart.execution.config; import com.fasterxml.jackson.databind.ObjectMapper; -import graphql.servlet.config.ObjectMapperConfigurer; -import graphql.servlet.config.ObjectMapperProvider; -import graphql.servlet.core.DefaultObjectMapperConfigurer; +import graphql.kickstart.execution.error.DefaultObjectMapperConfigurer; public class ConfiguringObjectMapperProvider implements ObjectMapperProvider { diff --git a/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/config/DefaultExecutionStrategyProvider.java b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/config/DefaultExecutionStrategyProvider.java new file mode 100644 index 00000000..47aa6211 --- /dev/null +++ b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/config/DefaultExecutionStrategyProvider.java @@ -0,0 +1,51 @@ +package graphql.kickstart.execution.config; + +import graphql.execution.AsyncExecutionStrategy; +import graphql.execution.ExecutionStrategy; +import graphql.execution.SubscriptionExecutionStrategy; + +/** + * @author Andrew Potter + */ +public class DefaultExecutionStrategyProvider implements ExecutionStrategyProvider { + + private final ExecutionStrategy queryExecutionStrategy; + private final ExecutionStrategy mutationExecutionStrategy; + private final ExecutionStrategy subscriptionExecutionStrategy; + + public DefaultExecutionStrategyProvider() { + this(null); + } + + public DefaultExecutionStrategyProvider(ExecutionStrategy executionStrategy) { + this(executionStrategy, null, null); + } + + public DefaultExecutionStrategyProvider(ExecutionStrategy queryExecutionStrategy, + ExecutionStrategy mutationExecutionStrategy, ExecutionStrategy subscriptionExecutionStrategy) { + this.queryExecutionStrategy = defaultIfNull(queryExecutionStrategy, new AsyncExecutionStrategy()); + this.mutationExecutionStrategy = defaultIfNull(mutationExecutionStrategy, this.queryExecutionStrategy); + this.subscriptionExecutionStrategy = defaultIfNull(subscriptionExecutionStrategy, + new SubscriptionExecutionStrategy()); + } + + private ExecutionStrategy defaultIfNull(ExecutionStrategy executionStrategy, ExecutionStrategy defaultStrategy) { + return executionStrategy != null ? executionStrategy : defaultStrategy; + } + + @Override + public ExecutionStrategy getQueryExecutionStrategy() { + return queryExecutionStrategy; + } + + @Override + public ExecutionStrategy getMutationExecutionStrategy() { + return mutationExecutionStrategy; + } + + @Override + public ExecutionStrategy getSubscriptionExecutionStrategy() { + return subscriptionExecutionStrategy; + } + +} diff --git a/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/config/DefaultGraphQLSchemaProvider.java b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/config/DefaultGraphQLSchemaProvider.java new file mode 100644 index 00000000..eca85768 --- /dev/null +++ b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/config/DefaultGraphQLSchemaProvider.java @@ -0,0 +1,32 @@ +package graphql.kickstart.execution.config; + +import graphql.schema.GraphQLSchema; + +/** + * @author Andrew Potter + */ +public class DefaultGraphQLSchemaProvider implements GraphQLSchemaProvider { + + private final GraphQLSchema schema; + private final GraphQLSchema readOnlySchema; + + public DefaultGraphQLSchemaProvider(GraphQLSchema schema) { + this(schema, GraphQLSchemaProvider.copyReadOnly(schema)); + } + + public DefaultGraphQLSchemaProvider(GraphQLSchema schema, GraphQLSchema readOnlySchema) { + this.schema = schema; + this.readOnlySchema = readOnlySchema; + } + + @Override + public GraphQLSchema getSchema() { + return schema; + } + + @Override + public GraphQLSchema getReadOnlySchema() { + return readOnlySchema; + } + +} diff --git a/src/main/java/graphql/servlet/config/ExecutionStrategyProvider.java b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/config/ExecutionStrategyProvider.java similarity index 85% rename from src/main/java/graphql/servlet/config/ExecutionStrategyProvider.java rename to graphql-java-kickstart/src/main/java/graphql/kickstart/execution/config/ExecutionStrategyProvider.java index d7ab70b8..dc2977a4 100644 --- a/src/main/java/graphql/servlet/config/ExecutionStrategyProvider.java +++ b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/config/ExecutionStrategyProvider.java @@ -1,4 +1,4 @@ -package graphql.servlet.config; +package graphql.kickstart.execution.config; import graphql.execution.ExecutionStrategy; diff --git a/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/config/GraphQLBuilder.java b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/config/GraphQLBuilder.java new file mode 100644 index 00000000..e8411249 --- /dev/null +++ b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/config/GraphQLBuilder.java @@ -0,0 +1,73 @@ +package graphql.kickstart.execution.config; + +import graphql.GraphQL; +import graphql.execution.instrumentation.ChainedInstrumentation; +import graphql.execution.instrumentation.Instrumentation; +import graphql.execution.instrumentation.SimpleInstrumentation; +import graphql.execution.instrumentation.dataloader.DataLoaderDispatcherInstrumentation; +import graphql.execution.preparsed.NoOpPreparsedDocumentProvider; +import graphql.execution.preparsed.PreparsedDocumentProvider; +import graphql.schema.GraphQLSchema; +import java.util.function.Supplier; +import lombok.Getter; + +public class GraphQLBuilder { + + private Supplier executionStrategyProviderSupplier = DefaultExecutionStrategyProvider::new; + private Supplier preparsedDocumentProviderSupplier = () -> NoOpPreparsedDocumentProvider.INSTANCE; + @Getter + private Supplier instrumentationSupplier = () -> SimpleInstrumentation.INSTANCE; + + public GraphQLBuilder executionStrategyProvider(Supplier supplier) { + if (supplier != null) { + executionStrategyProviderSupplier = supplier; + } + return this; + } + + public GraphQLBuilder preparsedDocumentProvider(Supplier supplier) { + if (supplier != null) { + preparsedDocumentProviderSupplier = supplier; + } + return this; + } + + public GraphQLBuilder instrumentation(Supplier supplier) { + if (supplier != null) { + instrumentationSupplier = supplier; + } + return this; + } + + public GraphQL build(GraphQLSchemaProvider schemaProvider) { + return build(schemaProvider.getSchema()); + } + + public GraphQL build(GraphQLSchema schema) { + return build(schema, instrumentationSupplier); + } + + public GraphQL build(GraphQLSchema schema, Supplier configuredInstrumentationSupplier) { + ExecutionStrategyProvider executionStrategyProvider = executionStrategyProviderSupplier.get(); + GraphQL.Builder builder = GraphQL.newGraphQL(schema) + .queryExecutionStrategy(executionStrategyProvider.getQueryExecutionStrategy()) + .mutationExecutionStrategy(executionStrategyProvider.getMutationExecutionStrategy()) + .subscriptionExecutionStrategy(executionStrategyProvider.getSubscriptionExecutionStrategy()) + .preparsedDocumentProvider(preparsedDocumentProviderSupplier.get()); + Instrumentation instrumentation = configuredInstrumentationSupplier.get(); + builder.instrumentation(instrumentation); + if (containsDispatchInstrumentation(instrumentation)) { + builder.doNotAddDefaultInstrumentations(); + } + return builder.build(); + } + + private boolean containsDispatchInstrumentation(Instrumentation instrumentation) { + if (instrumentation instanceof ChainedInstrumentation) { + return ((ChainedInstrumentation) instrumentation).getInstrumentations().stream() + .anyMatch(this::containsDispatchInstrumentation); + } + return instrumentation instanceof DataLoaderDispatcherInstrumentation; + } + +} diff --git a/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/config/GraphQLSchemaProvider.java b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/config/GraphQLSchemaProvider.java new file mode 100644 index 00000000..e812adfd --- /dev/null +++ b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/config/GraphQLSchemaProvider.java @@ -0,0 +1,21 @@ +package graphql.kickstart.execution.config; + +import graphql.schema.GraphQLObjectType; +import graphql.schema.GraphQLSchema; + +public interface GraphQLSchemaProvider { + + static GraphQLSchema copyReadOnly(GraphQLSchema schema) { + return GraphQLSchema.newSchema(schema) + .mutation((GraphQLObjectType) null) + .build(); + } + + /** + * @return a schema for handling mbean calls. + */ + GraphQLSchema getSchema(); + + GraphQLSchema getReadOnlySchema(); + +} diff --git a/src/main/java/graphql/servlet/config/InstrumentationProvider.java b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/config/InstrumentationProvider.java similarity index 76% rename from src/main/java/graphql/servlet/config/InstrumentationProvider.java rename to graphql-java-kickstart/src/main/java/graphql/kickstart/execution/config/InstrumentationProvider.java index 5d6990fd..a48e1fac 100644 --- a/src/main/java/graphql/servlet/config/InstrumentationProvider.java +++ b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/config/InstrumentationProvider.java @@ -1,4 +1,4 @@ -package graphql.servlet.config; +package graphql.kickstart.execution.config; import graphql.execution.instrumentation.Instrumentation; diff --git a/src/main/java/graphql/servlet/config/ObjectMapperConfigurer.java b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/config/ObjectMapperConfigurer.java similarity index 79% rename from src/main/java/graphql/servlet/config/ObjectMapperConfigurer.java rename to graphql-java-kickstart/src/main/java/graphql/kickstart/execution/config/ObjectMapperConfigurer.java index 1e13637b..fd514119 100644 --- a/src/main/java/graphql/servlet/config/ObjectMapperConfigurer.java +++ b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/config/ObjectMapperConfigurer.java @@ -1,4 +1,4 @@ -package graphql.servlet.config; +package graphql.kickstart.execution.config; import com.fasterxml.jackson.databind.ObjectMapper; diff --git a/src/main/java/graphql/servlet/config/ObjectMapperProvider.java b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/config/ObjectMapperProvider.java similarity index 73% rename from src/main/java/graphql/servlet/config/ObjectMapperProvider.java rename to graphql-java-kickstart/src/main/java/graphql/kickstart/execution/config/ObjectMapperProvider.java index bb052045..097d10c0 100644 --- a/src/main/java/graphql/servlet/config/ObjectMapperProvider.java +++ b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/config/ObjectMapperProvider.java @@ -1,4 +1,4 @@ -package graphql.servlet.config; +package graphql.kickstart.execution.config; import com.fasterxml.jackson.databind.ObjectMapper; diff --git a/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/context/ContextSetting.java b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/context/ContextSetting.java new file mode 100644 index 00000000..fdb56c64 --- /dev/null +++ b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/context/ContextSetting.java @@ -0,0 +1,98 @@ +package graphql.kickstart.execution.context; + +import graphql.ExecutionInput; +import graphql.execution.ExecutionId; +import graphql.execution.instrumentation.ChainedInstrumentation; +import graphql.execution.instrumentation.Instrumentation; +import graphql.execution.instrumentation.dataloader.DataLoaderDispatcherInstrumentationOptions; +import graphql.kickstart.execution.GraphQLRequest; +import graphql.kickstart.execution.input.GraphQLBatchedInvocationInput; +import graphql.kickstart.execution.input.PerQueryBatchedInvocationInput; +import graphql.kickstart.execution.input.PerRequestBatchedInvocationInput; +import graphql.kickstart.execution.instrumentation.ConfigurableDispatchInstrumentation; +import graphql.kickstart.execution.instrumentation.FieldLevelTrackingApproach; +import graphql.kickstart.execution.instrumentation.RequestLevelTrackingApproach; +import graphql.schema.GraphQLSchema; +import java.util.Arrays; +import java.util.List; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import org.dataloader.DataLoaderRegistry; + +/** + * An enum representing possible context settings. These are modeled after Apollo's link settings. + */ +public enum ContextSetting { + + /** + * A context object, and therefor dataloader registry and subject, should be shared between all GraphQL executions in + * a http request. + */ + PER_REQUEST_WITH_INSTRUMENTATION, + PER_REQUEST_WITHOUT_INSTRUMENTATION, + /** + * Each GraphQL execution should always have its own context. + */ + PER_QUERY_WITH_INSTRUMENTATION, + PER_QUERY_WITHOUT_INSTRUMENTATION; + + /** + * Creates a set of inputs with the correct context based on the setting. + * + * @param requests the GraphQL requests to execute. + * @param schema the GraphQL schema to execute the requests against. + * @param contextSupplier method that returns the context to use for each execution or for the request as a whole. + * @param root the root object to use for each execution. + * @return a configured batch input. + */ + public GraphQLBatchedInvocationInput getBatch(List requests, GraphQLSchema schema, + Supplier contextSupplier, Object root) { + switch (this) { + case PER_QUERY_WITH_INSTRUMENTATION: + //Intentional fallthrough + case PER_QUERY_WITHOUT_INSTRUMENTATION: + return new PerQueryBatchedInvocationInput(requests, schema, contextSupplier, root, this); + case PER_REQUEST_WITHOUT_INSTRUMENTATION: + //Intentional fallthrough + case PER_REQUEST_WITH_INSTRUMENTATION: + return new PerRequestBatchedInvocationInput(requests, schema, contextSupplier, root, this); + default: + throw new RuntimeException("Unconfigured context setting type"); + } + } + + /** + * Augments the provided instrumentation supplier to also supply the correct dispatching instrumentation. + * + * @param instrumentation the instrumentation supplier to augment + * @param executionInputs the inputs that will be dispatched by the instrumentation + * @param options the DataLoader dispatching instrumentation options that will be used. + * @return augmented instrumentation supplier. + */ + public Supplier configureInstrumentationForContext(Supplier instrumentation, + List executionInputs, + DataLoaderDispatcherInstrumentationOptions options) { + ConfigurableDispatchInstrumentation dispatchInstrumentation; + switch (this) { + case PER_REQUEST_WITH_INSTRUMENTATION: + DataLoaderRegistry registry = executionInputs.stream().findFirst().map(ExecutionInput::getDataLoaderRegistry) + .orElseThrow(IllegalArgumentException::new); + List executionIds = executionInputs.stream().map(ExecutionInput::getExecutionId) + .collect(Collectors.toList()); + RequestLevelTrackingApproach requestTrackingApproach = new RequestLevelTrackingApproach(executionIds, registry); + dispatchInstrumentation = new ConfigurableDispatchInstrumentation(options, + (dataLoaderRegistry -> requestTrackingApproach)); + break; + case PER_QUERY_WITH_INSTRUMENTATION: + dispatchInstrumentation = new ConfigurableDispatchInstrumentation(options, FieldLevelTrackingApproach::new); + break; + case PER_REQUEST_WITHOUT_INSTRUMENTATION: + //Intentional fallthrough + case PER_QUERY_WITHOUT_INSTRUMENTATION: + return instrumentation; + default: + throw new RuntimeException("Unconfigured context setting type"); + } + return () -> new ChainedInstrumentation(Arrays.asList(dispatchInstrumentation, instrumentation.get())); + } +} diff --git a/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/context/DefaultGraphQLContext.java b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/context/DefaultGraphQLContext.java new file mode 100644 index 00000000..06242ede --- /dev/null +++ b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/context/DefaultGraphQLContext.java @@ -0,0 +1,35 @@ +package graphql.kickstart.execution.context; + +import java.util.Optional; +import javax.security.auth.Subject; +import org.dataloader.DataLoaderRegistry; + +/** + * An object for the DefaultGraphQLContextBuilder to return. Can be extended to include more context. + */ +public class DefaultGraphQLContext implements GraphQLContext { + + private final Subject subject; + + private final DataLoaderRegistry dataLoaderRegistry; + + public DefaultGraphQLContext(DataLoaderRegistry dataLoaderRegistry, Subject subject) { + this.dataLoaderRegistry = dataLoaderRegistry; + this.subject = subject; + } + + public DefaultGraphQLContext() { + this(new DataLoaderRegistry(), null); + } + + @Override + public Optional getSubject() { + return Optional.ofNullable(subject); + } + + @Override + public Optional getDataLoaderRegistry() { + return Optional.ofNullable(dataLoaderRegistry); + } + +} diff --git a/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/context/DefaultGraphQLContextBuilder.java b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/context/DefaultGraphQLContextBuilder.java new file mode 100644 index 00000000..5f5c7a6f --- /dev/null +++ b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/context/DefaultGraphQLContextBuilder.java @@ -0,0 +1,13 @@ +package graphql.kickstart.execution.context; + +/** + * Returns an empty context. + */ +public class DefaultGraphQLContextBuilder implements GraphQLContextBuilder { + + @Override + public GraphQLContext build() { + return new DefaultGraphQLContext(); + } + +} diff --git a/src/main/java/graphql/servlet/context/GraphQLContext.java b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/context/GraphQLContext.java similarity index 91% rename from src/main/java/graphql/servlet/context/GraphQLContext.java rename to graphql-java-kickstart/src/main/java/graphql/kickstart/execution/context/GraphQLContext.java index 25d16ee9..5fc9546a 100644 --- a/src/main/java/graphql/servlet/context/GraphQLContext.java +++ b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/context/GraphQLContext.java @@ -1,4 +1,4 @@ -package graphql.servlet.context; +package graphql.kickstart.execution.context; import org.dataloader.DataLoaderRegistry; diff --git a/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/context/GraphQLContextBuilder.java b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/context/GraphQLContextBuilder.java new file mode 100644 index 00000000..d62b25ed --- /dev/null +++ b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/context/GraphQLContextBuilder.java @@ -0,0 +1,10 @@ +package graphql.kickstart.execution.context; + +public interface GraphQLContextBuilder { + + /** + * @return the graphql context + */ + GraphQLContext build(); + +} diff --git a/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/error/DefaultGraphQLErrorHandler.java b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/error/DefaultGraphQLErrorHandler.java new file mode 100644 index 00000000..9769ae2c --- /dev/null +++ b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/error/DefaultGraphQLErrorHandler.java @@ -0,0 +1,63 @@ +package graphql.kickstart.execution.error; + +import graphql.ExceptionWhileDataFetching; +import graphql.GraphQLError; +import graphql.execution.NonNullableFieldWasNullError; +import java.util.List; +import java.util.stream.Collectors; +import lombok.extern.slf4j.Slf4j; + +/** + * @author Andrew Potter + */ +@Slf4j +public class DefaultGraphQLErrorHandler implements GraphQLErrorHandler { + + @Override + public List processErrors(List errors) { + final List clientErrors = filterGraphQLErrors(errors); + if (clientErrors.size() < errors.size()) { + + // Some errors were filtered out to hide implementation - put a generic error in place. + clientErrors.add(new GenericGraphQLError("Internal Server Error(s) while executing query")); + + errors.stream() + .filter(error -> !isClientError(error)) + .forEach(this::logError); + } + + return clientErrors; + } + + protected void logError(GraphQLError error) { + if (error instanceof Throwable) { + log.error("Error executing query!", (Throwable) error); + } else if (error instanceof ExceptionWhileDataFetching) { + log.error("Error executing query {}", error.getMessage(), ((ExceptionWhileDataFetching) error).getException()); + } else { + log.error("Error executing query ({}): {}", error.getClass().getSimpleName(), error.getMessage()); + } + } + + protected List filterGraphQLErrors(List errors) { + return errors.stream() + .filter(this::isClientError) + .map(this::replaceNonNullableFieldWasNullError) + .collect(Collectors.toList()); + } + + protected boolean isClientError(GraphQLError error) { + if (error instanceof ExceptionWhileDataFetching) { + return ((ExceptionWhileDataFetching) error).getException() instanceof GraphQLError; + } + return true; + } + + private GraphQLError replaceNonNullableFieldWasNullError(GraphQLError error) { + if (error instanceof NonNullableFieldWasNullError) { + return new RenderableNonNullableFieldWasNullError((NonNullableFieldWasNullError) error); + } else { + return error; + } + } +} diff --git a/src/main/java/graphql/servlet/core/DefaultObjectMapperConfigurer.java b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/error/DefaultObjectMapperConfigurer.java similarity index 85% rename from src/main/java/graphql/servlet/core/DefaultObjectMapperConfigurer.java rename to graphql-java-kickstart/src/main/java/graphql/kickstart/execution/error/DefaultObjectMapperConfigurer.java index 57b5fca8..d7c5b029 100644 --- a/src/main/java/graphql/servlet/core/DefaultObjectMapperConfigurer.java +++ b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/error/DefaultObjectMapperConfigurer.java @@ -1,10 +1,10 @@ -package graphql.servlet.core; +package graphql.kickstart.execution.error; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; -import graphql.servlet.config.ObjectMapperConfigurer; +import graphql.kickstart.execution.config.ObjectMapperConfigurer; /** * @author Andrew Potter diff --git a/src/main/java/graphql/servlet/core/GenericGraphQLError.java b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/error/GenericGraphQLError.java similarity index 93% rename from src/main/java/graphql/servlet/core/GenericGraphQLError.java rename to graphql-java-kickstart/src/main/java/graphql/kickstart/execution/error/GenericGraphQLError.java index c50dc6e8..569a9533 100644 --- a/src/main/java/graphql/servlet/core/GenericGraphQLError.java +++ b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/error/GenericGraphQLError.java @@ -1,4 +1,4 @@ -package graphql.servlet.core; +package graphql.kickstart.execution.error; import com.fasterxml.jackson.annotation.JsonIgnore; import graphql.ErrorType; diff --git a/src/main/java/graphql/servlet/core/GraphQLErrorHandler.java b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/error/GraphQLErrorHandler.java similarity index 88% rename from src/main/java/graphql/servlet/core/GraphQLErrorHandler.java rename to graphql-java-kickstart/src/main/java/graphql/kickstart/execution/error/GraphQLErrorHandler.java index 666f12a9..3cad4b51 100644 --- a/src/main/java/graphql/servlet/core/GraphQLErrorHandler.java +++ b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/error/GraphQLErrorHandler.java @@ -1,4 +1,4 @@ -package graphql.servlet.core; +package graphql.kickstart.execution.error; import graphql.GraphQLError; diff --git a/src/main/java/graphql/servlet/core/RenderableNonNullableFieldWasNullError.java b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/error/RenderableNonNullableFieldWasNullError.java similarity index 96% rename from src/main/java/graphql/servlet/core/RenderableNonNullableFieldWasNullError.java rename to graphql-java-kickstart/src/main/java/graphql/kickstart/execution/error/RenderableNonNullableFieldWasNullError.java index 6522a3d4..f63a717e 100644 --- a/src/main/java/graphql/servlet/core/RenderableNonNullableFieldWasNullError.java +++ b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/error/RenderableNonNullableFieldWasNullError.java @@ -1,4 +1,4 @@ -package graphql.servlet.core; +package graphql.kickstart.execution.error; import com.fasterxml.jackson.annotation.JsonInclude; import graphql.ErrorType; diff --git a/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/input/GraphQLBatchedInvocationInput.java b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/input/GraphQLBatchedInvocationInput.java new file mode 100644 index 00000000..4fc8b70d --- /dev/null +++ b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/input/GraphQLBatchedInvocationInput.java @@ -0,0 +1,27 @@ +package graphql.kickstart.execution.input; + +import static java.util.stream.Collectors.toList; + +import graphql.ExecutionInput; +import graphql.kickstart.execution.context.ContextSetting; +import java.util.List; + +/** + * Interface representing a batched input. + */ +public interface GraphQLBatchedInvocationInput extends GraphQLInvocationInput { + + /** + * @return each individual input in the batch, configured with a context. + */ + List getInvocationInputs(); + + default List getExecutionInputs() { + return getInvocationInputs().stream() + .map(GraphQLSingleInvocationInput::getExecutionInput) + .collect(toList()); + } + + ContextSetting getContextSetting(); + +} diff --git a/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/input/GraphQLInvocationInput.java b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/input/GraphQLInvocationInput.java new file mode 100644 index 00000000..2f90eab0 --- /dev/null +++ b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/input/GraphQLInvocationInput.java @@ -0,0 +1,5 @@ +package graphql.kickstart.execution.input; + +public interface GraphQLInvocationInput { + +} diff --git a/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/input/GraphQLSingleInvocationInput.java b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/input/GraphQLSingleInvocationInput.java new file mode 100644 index 00000000..7159e176 --- /dev/null +++ b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/input/GraphQLSingleInvocationInput.java @@ -0,0 +1,59 @@ +package graphql.kickstart.execution.input; + +import graphql.ExecutionInput; +import graphql.execution.ExecutionId; +import graphql.kickstart.execution.GraphQLRequest; +import graphql.kickstart.execution.context.GraphQLContext; +import graphql.schema.GraphQLSchema; +import java.util.Optional; +import javax.security.auth.Subject; +import org.dataloader.DataLoaderRegistry; + +/** + * Represents a single GraphQL execution. + */ +public class GraphQLSingleInvocationInput implements GraphQLInvocationInput { + + private final GraphQLSchema schema; + + private final ExecutionInput executionInput; + + private final Subject subject; + + public GraphQLSingleInvocationInput(GraphQLRequest request, GraphQLSchema schema, GraphQLContext context, + Object root) { + this.schema = schema; + this.executionInput = createExecutionInput(request, context, root); + subject = context.getSubject().orElse(null); + } + + /** + * @return the schema to use to execute this query. + */ + public GraphQLSchema getSchema() { + return schema; + } + + /** + * @return a subject to execute the query as. + */ + public Optional getSubject() { + return Optional.ofNullable(subject); + } + + private ExecutionInput createExecutionInput(GraphQLRequest graphQLRequest, GraphQLContext context, Object root) { + return ExecutionInput.newExecutionInput() + .query(graphQLRequest.getQuery()) + .operationName(graphQLRequest.getOperationName()) + .context(context) + .root(root) + .variables(graphQLRequest.getVariables()) + .dataLoaderRegistry(context.getDataLoaderRegistry().orElse(new DataLoaderRegistry())) + .executionId(ExecutionId.generate()) + .build(); + } + + public ExecutionInput getExecutionInput() { + return executionInput; + } +} diff --git a/src/main/java/graphql/servlet/input/PerQueryBatchedInvocationInput.java b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/input/PerQueryBatchedInvocationInput.java similarity index 53% rename from src/main/java/graphql/servlet/input/PerQueryBatchedInvocationInput.java rename to graphql-java-kickstart/src/main/java/graphql/kickstart/execution/input/PerQueryBatchedInvocationInput.java index 2f396e6d..3cea428b 100644 --- a/src/main/java/graphql/servlet/input/PerQueryBatchedInvocationInput.java +++ b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/input/PerQueryBatchedInvocationInput.java @@ -1,26 +1,28 @@ -package graphql.servlet.input; +package graphql.kickstart.execution.input; +import graphql.kickstart.execution.GraphQLRequest; import graphql.schema.GraphQLSchema; -import graphql.servlet.context.GraphQLContext; -import graphql.servlet.core.internal.GraphQLRequest; +import graphql.kickstart.execution.context.ContextSetting; +import graphql.kickstart.execution.context.GraphQLContext; import java.util.List; import java.util.function.Supplier; import java.util.stream.Collectors; +import lombok.Getter; /** * A Collection of GraphQLSingleInvocationInput that each have a unique context object. */ +@Getter public class PerQueryBatchedInvocationInput implements GraphQLBatchedInvocationInput { - private final List inputs; - public PerQueryBatchedInvocationInput(List requests, GraphQLSchema schema, Supplier contextSupplier, Object root) { - inputs = requests.stream() + private final List invocationInputs; + private final ContextSetting contextSetting; + + public PerQueryBatchedInvocationInput(List requests, GraphQLSchema schema, Supplier contextSupplier, Object root, ContextSetting contextSetting) { + invocationInputs = requests.stream() .map(request -> new GraphQLSingleInvocationInput(request, schema, contextSupplier.get(), root)).collect(Collectors.toList()); + this.contextSetting = contextSetting; } - @Override - public List getExecutionInputs() { - return inputs; - } } diff --git a/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/input/PerRequestBatchedInvocationInput.java b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/input/PerRequestBatchedInvocationInput.java new file mode 100644 index 00000000..a50e0cdf --- /dev/null +++ b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/input/PerRequestBatchedInvocationInput.java @@ -0,0 +1,29 @@ +package graphql.kickstart.execution.input; + +import graphql.kickstart.execution.GraphQLRequest; +import graphql.kickstart.execution.context.ContextSetting; +import graphql.kickstart.execution.context.GraphQLContext; +import graphql.schema.GraphQLSchema; +import java.util.List; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import lombok.Getter; + +/** + * A collection of GraphQLSingleInvocationInputs that share a context object. + */ +@Getter +public class PerRequestBatchedInvocationInput implements GraphQLBatchedInvocationInput { + + private final List invocationInputs; + private final ContextSetting contextSetting; + + public PerRequestBatchedInvocationInput(List requests, GraphQLSchema schema, + Supplier contextSupplier, Object root, ContextSetting contextSetting) { + GraphQLContext context = contextSupplier.get(); + invocationInputs = requests.stream().map(request -> new GraphQLSingleInvocationInput(request, schema, context, root)) + .collect(Collectors.toList()); + this.contextSetting = contextSetting; + } + +} diff --git a/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/instrumentation/AbstractTrackingApproach.java b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/instrumentation/AbstractTrackingApproach.java new file mode 100644 index 00000000..71478b3c --- /dev/null +++ b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/instrumentation/AbstractTrackingApproach.java @@ -0,0 +1,220 @@ +package graphql.kickstart.execution.instrumentation; + +import graphql.ExecutionResult; +import graphql.execution.ExecutionId; +import graphql.execution.ExecutionPath; +import graphql.execution.FieldValueInfo; +import graphql.execution.MergedField; +import graphql.execution.instrumentation.DeferredFieldInstrumentationContext; +import graphql.execution.instrumentation.ExecutionStrategyInstrumentationContext; +import graphql.execution.instrumentation.InstrumentationContext; +import graphql.execution.instrumentation.parameters.InstrumentationDeferredFieldParameters; +import graphql.execution.instrumentation.parameters.InstrumentationExecutionStrategyParameters; +import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import lombok.extern.slf4j.Slf4j; +import org.dataloader.DataLoaderRegistry; + +/** + * Handles logic common to tracking approaches. + */ +@Slf4j +public abstract class AbstractTrackingApproach implements TrackingApproach { + + private final DataLoaderRegistry dataLoaderRegistry; + + private final RequestStack stack = new RequestStack(); + + public AbstractTrackingApproach(DataLoaderRegistry dataLoaderRegistry) { + this.dataLoaderRegistry = dataLoaderRegistry; + } + + /** + * @return allows extending classes to modify the stack. + */ + protected RequestStack getStack() { + return stack; + } + + @Override + public ExecutionStrategyInstrumentationContext beginExecutionStrategy( + InstrumentationExecutionStrategyParameters parameters) { + ExecutionId executionId = parameters.getExecutionContext().getExecutionId(); + ExecutionPath path = parameters.getExecutionStrategyParameters().getPath(); + int parentLevel = path.getLevel(); + int curLevel = parentLevel + 1; + int fieldCount = parameters.getExecutionStrategyParameters().getFields().size(); + synchronized (stack) { + stack.increaseExpectedFetchCount(executionId, curLevel, fieldCount); + stack.increaseHappenedStrategyCalls(executionId, curLevel); + } + + return new ExecutionStrategyInstrumentationContext() { + @Override + public void onDispatched(CompletableFuture result) { + + } + + @Override + public void onCompleted(ExecutionResult result, Throwable t) { + + } + + @Override + public void onFieldValuesInfo(List fieldValueInfoList) { + synchronized (stack) { + stack.setStatus(executionId, handleOnFieldValuesInfo(fieldValueInfoList, stack, executionId, curLevel)); + if (stack.allReady()) { + dispatchWithoutLocking(); + } + } + } + + @Override + public void onDeferredField(MergedField field) { + // fake fetch count for this field + synchronized (stack) { + stack.increaseFetchCount(executionId, curLevel); + stack.setStatus(executionId, dispatchIfNeeded(stack, executionId, curLevel)); + if (stack.allReady()) { + dispatchWithoutLocking(); + } + } + } + }; + } + + // + // thread safety : called with synchronised(stack) + // + private boolean handleOnFieldValuesInfo(List fieldValueInfoList, RequestStack stack, + ExecutionId executionId, int curLevel) { + stack.increaseHappenedOnFieldValueCalls(executionId, curLevel); + int expectedStrategyCalls = 0; + for (FieldValueInfo fieldValueInfo : fieldValueInfoList) { + if (fieldValueInfo.getCompleteValueType() == FieldValueInfo.CompleteValueType.OBJECT) { + expectedStrategyCalls++; + } else if (fieldValueInfo.getCompleteValueType() == FieldValueInfo.CompleteValueType.LIST) { + expectedStrategyCalls += getCountForList(fieldValueInfo); + } + } + stack.increaseExpectedStrategyCalls(executionId, curLevel + 1, expectedStrategyCalls); + return dispatchIfNeeded(stack, executionId, curLevel + 1); + } + + private int getCountForList(FieldValueInfo fieldValueInfo) { + int result = 0; + for (FieldValueInfo cvi : fieldValueInfo.getFieldValueInfos()) { + if (cvi.getCompleteValueType() == FieldValueInfo.CompleteValueType.OBJECT) { + result++; + } else if (cvi.getCompleteValueType() == FieldValueInfo.CompleteValueType.LIST) { + result += getCountForList(cvi); + } + } + return result; + } + + @Override + public DeferredFieldInstrumentationContext beginDeferredField(InstrumentationDeferredFieldParameters parameters) { + ExecutionId executionId = parameters.getExecutionContext().getExecutionId(); + int level = parameters.getExecutionStrategyParameters().getPath().getLevel(); + synchronized (stack) { + stack.clearAndMarkCurrentLevelAsReady(executionId, level); + } + + return new DeferredFieldInstrumentationContext() { + @Override + public void onDispatched(CompletableFuture result) { + + } + + @Override + public void onCompleted(ExecutionResult result, Throwable t) { + } + + @Override + public void onFieldValueInfo(FieldValueInfo fieldValueInfo) { + synchronized (stack) { + stack.setStatus(executionId, + handleOnFieldValuesInfo(Collections.singletonList(fieldValueInfo), stack, executionId, level)); + if (stack.allReady()) { + dispatchWithoutLocking(); + } + } + } + }; + } + + @Override + public InstrumentationContext beginFieldFetch(InstrumentationFieldFetchParameters parameters) { + ExecutionId executionId = parameters.getExecutionContext().getExecutionId(); + ExecutionPath path = parameters.getEnvironment().getExecutionStepInfo().getPath(); + int level = path.getLevel(); + return new InstrumentationContext() { + + @Override + public void onDispatched(CompletableFuture result) { + synchronized (stack) { + stack.increaseFetchCount(executionId, level); + stack.setStatus(executionId, dispatchIfNeeded(stack, executionId, level)); + + if (stack.allReady()) { + dispatchWithoutLocking(); + } + } + } + + @Override + public void onCompleted(Object result, Throwable t) { + } + }; + } + + @Override + public void removeTracking(ExecutionId executionId) { + synchronized (stack) { + stack.removeExecution(executionId); + if (stack.allReady()) { + dispatchWithoutLocking(); + } + } + } + + + // + // thread safety : called with synchronised(stack) + // + private boolean dispatchIfNeeded(RequestStack stack, ExecutionId executionId, int level) { + if (levelReady(stack, executionId, level)) { + return stack.dispatchIfNotDispatchedBefore(executionId, level); + } + return false; + } + + // + // thread safety : called with synchronised(stack) + // + private boolean levelReady(RequestStack stack, ExecutionId executionId, int level) { + if (level == 1) { + // level 1 is special: there is only one strategy call and that's it + return stack.allFetchesHappened(executionId, 1); + } + return (levelReady(stack, executionId, level - 1) && stack.allOnFieldCallsHappened(executionId, level - 1) + && stack.allStrategyCallsHappened(executionId, level) && stack.allFetchesHappened(executionId, level)); + } + + @Override + public void dispatch() { + synchronized (stack) { + dispatchWithoutLocking(); + } + } + + private void dispatchWithoutLocking() { + log.debug("Dispatching data loaders ({})", dataLoaderRegistry.getKeys()); + dataLoaderRegistry.dispatchAll(); + stack.allReset(); + } +} diff --git a/src/main/java/graphql/servlet/instrumentation/ConfigurableDispatchInstrumentation.java b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/instrumentation/ConfigurableDispatchInstrumentation.java similarity index 99% rename from src/main/java/graphql/servlet/instrumentation/ConfigurableDispatchInstrumentation.java rename to graphql-java-kickstart/src/main/java/graphql/kickstart/execution/instrumentation/ConfigurableDispatchInstrumentation.java index d2c05164..0f22c0cb 100644 --- a/src/main/java/graphql/servlet/instrumentation/ConfigurableDispatchInstrumentation.java +++ b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/instrumentation/ConfigurableDispatchInstrumentation.java @@ -1,4 +1,4 @@ -package graphql.servlet.instrumentation; +package graphql.kickstart.execution.instrumentation; import graphql.ExecutionResult; import graphql.ExecutionResultImpl; diff --git a/src/main/java/graphql/servlet/instrumentation/DataLoaderDispatcherInstrumentationState.java b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/instrumentation/DataLoaderDispatcherInstrumentationState.java similarity index 96% rename from src/main/java/graphql/servlet/instrumentation/DataLoaderDispatcherInstrumentationState.java rename to graphql-java-kickstart/src/main/java/graphql/kickstart/execution/instrumentation/DataLoaderDispatcherInstrumentationState.java index c50c5d86..efc56687 100644 --- a/src/main/java/graphql/servlet/instrumentation/DataLoaderDispatcherInstrumentationState.java +++ b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/instrumentation/DataLoaderDispatcherInstrumentationState.java @@ -1,4 +1,4 @@ -package graphql.servlet.instrumentation; +package graphql.kickstart.execution.instrumentation; import graphql.execution.ExecutionId; import graphql.execution.instrumentation.InstrumentationState; diff --git a/src/main/java/graphql/servlet/instrumentation/FieldLevelTrackingApproach.java b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/instrumentation/FieldLevelTrackingApproach.java similarity index 94% rename from src/main/java/graphql/servlet/instrumentation/FieldLevelTrackingApproach.java rename to graphql-java-kickstart/src/main/java/graphql/kickstart/execution/instrumentation/FieldLevelTrackingApproach.java index c2a6101a..7b9e644e 100644 --- a/src/main/java/graphql/servlet/instrumentation/FieldLevelTrackingApproach.java +++ b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/instrumentation/FieldLevelTrackingApproach.java @@ -1,4 +1,4 @@ -package graphql.servlet.instrumentation; +package graphql.kickstart.execution.instrumentation; import graphql.Internal; import graphql.execution.ExecutionId; diff --git a/src/main/java/graphql/servlet/instrumentation/NoOpInstrumentationProvider.java b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/instrumentation/NoOpInstrumentationProvider.java similarity index 72% rename from src/main/java/graphql/servlet/instrumentation/NoOpInstrumentationProvider.java rename to graphql-java-kickstart/src/main/java/graphql/kickstart/execution/instrumentation/NoOpInstrumentationProvider.java index 7f09ff7c..17b635ff 100644 --- a/src/main/java/graphql/servlet/instrumentation/NoOpInstrumentationProvider.java +++ b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/instrumentation/NoOpInstrumentationProvider.java @@ -1,8 +1,8 @@ -package graphql.servlet.instrumentation; +package graphql.kickstart.execution.instrumentation; import graphql.execution.instrumentation.Instrumentation; import graphql.execution.instrumentation.SimpleInstrumentation; -import graphql.servlet.config.InstrumentationProvider; +import graphql.kickstart.execution.config.InstrumentationProvider; public class NoOpInstrumentationProvider implements InstrumentationProvider { diff --git a/src/main/java/graphql/servlet/instrumentation/RequestLevelTrackingApproach.java b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/instrumentation/RequestLevelTrackingApproach.java similarity index 94% rename from src/main/java/graphql/servlet/instrumentation/RequestLevelTrackingApproach.java rename to graphql-java-kickstart/src/main/java/graphql/kickstart/execution/instrumentation/RequestLevelTrackingApproach.java index e6ae0fd2..53893d05 100644 --- a/src/main/java/graphql/servlet/instrumentation/RequestLevelTrackingApproach.java +++ b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/instrumentation/RequestLevelTrackingApproach.java @@ -1,4 +1,4 @@ -package graphql.servlet.instrumentation; +package graphql.kickstart.execution.instrumentation; import graphql.execution.ExecutionId; import graphql.execution.instrumentation.InstrumentationState; @@ -25,4 +25,4 @@ public InstrumentationState createState(ExecutionId executionId) { return null; } -} \ No newline at end of file +} diff --git a/src/main/java/graphql/servlet/instrumentation/RequestStack.java b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/instrumentation/RequestStack.java similarity index 99% rename from src/main/java/graphql/servlet/instrumentation/RequestStack.java rename to graphql-java-kickstart/src/main/java/graphql/kickstart/execution/instrumentation/RequestStack.java index 0b459dd1..800d13fd 100644 --- a/src/main/java/graphql/servlet/instrumentation/RequestStack.java +++ b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/instrumentation/RequestStack.java @@ -1,4 +1,4 @@ -package graphql.servlet.instrumentation; +package graphql.kickstart.execution.instrumentation; import graphql.Assert; import graphql.execution.ExecutionId; @@ -302,4 +302,4 @@ public void clearAndMarkCurrentLevelAsReady(ExecutionId executionId, int level) } activeRequests.get(executionId).clearAndMarkCurrentLevelAsReady(level); } -} \ No newline at end of file +} diff --git a/src/main/java/graphql/servlet/instrumentation/TrackingApproach.java b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/instrumentation/TrackingApproach.java similarity index 97% rename from src/main/java/graphql/servlet/instrumentation/TrackingApproach.java rename to graphql-java-kickstart/src/main/java/graphql/kickstart/execution/instrumentation/TrackingApproach.java index 5d359caf..d91e4c32 100644 --- a/src/main/java/graphql/servlet/instrumentation/TrackingApproach.java +++ b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/instrumentation/TrackingApproach.java @@ -1,4 +1,4 @@ -package graphql.servlet.instrumentation; +package graphql.kickstart.execution.instrumentation; import graphql.execution.ExecutionId; import graphql.execution.instrumentation.DeferredFieldInstrumentationContext; diff --git a/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/AtomicSubscriptionSubscription.java b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/AtomicSubscriptionSubscription.java new file mode 100644 index 00000000..f60faac8 --- /dev/null +++ b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/AtomicSubscriptionSubscription.java @@ -0,0 +1,27 @@ +package graphql.kickstart.execution.subscriptions; + +import java.util.concurrent.atomic.AtomicReference; +import org.reactivestreams.Subscription; + +public class AtomicSubscriptionSubscription { + + private final AtomicReference reference = new AtomicReference<>(null); + + public void set(Subscription subscription) { + if (reference.get() != null) { + throw new IllegalStateException("Cannot overwrite subscription!"); + } + + reference.set(subscription); + } + + public Subscription get() { + Subscription subscription = reference.get(); + if (subscription == null) { + throw new IllegalStateException("Subscription has not been initialized yet!"); + } + + return subscription; + } + +} diff --git a/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/DefaultSubscriptionSession.java b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/DefaultSubscriptionSession.java new file mode 100644 index 00000000..c8f17cf2 --- /dev/null +++ b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/DefaultSubscriptionSession.java @@ -0,0 +1,107 @@ +package graphql.kickstart.execution.subscriptions; + +import graphql.ExecutionResult; +import graphql.execution.reactive.SingleSubscriberPublisher; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscription; + +@Slf4j +@RequiredArgsConstructor +public class DefaultSubscriptionSession implements SubscriptionSession { + + @Getter + private final GraphQLSubscriptionMapper mapper; + private SingleSubscriberPublisher publisher = new SingleSubscriberPublisher<>(); + private SessionSubscriptions subscriptions = new SessionSubscriptions(); + + @Override + public void send(String message) { + Objects.requireNonNull(message, "message is required"); + publisher.offer(message); + } + + @Override + public void sendMessage(Object payload) { + Objects.requireNonNull(payload, "payload is required"); + send(mapper.serialize(payload)); + } + + @Override + public void subscribe(String id, Publisher dataPublisher) { + dataPublisher.subscribe(new SessionSubscriber(this, id)); + } + + @Override + public void add(String id, Subscription subscription) { + subscriptions.add(id, subscription); + } + + @Override + public void unsubscribe(String id) { + subscriptions.cancel(id); + } + + @Override + public void sendDataMessage(String id, Object payload) { + send(mapper.serialize(payload)); + } + + @Override + public void sendErrorMessage(String id) { + + } + + @Override + public void sendCompleteMessage(String id) { + + } + + @Override + public void close(String reason) { + log.debug("Closing subscription session {}", getId()); + subscriptions.close(); + publisher.noMoreData(); + } + + @Override + public Map getUserProperties() { + return new HashMap<>(); + } + + @Override + public boolean isOpen() { + return true; + } + + @Override + public String getId() { + return null; + } + + @Override + public SessionSubscriptions getSubscriptions() { + return subscriptions; + } + + @Override + public Object unwrap() { + throw new UnsupportedOperationException(); + } + + @Override + public Publisher getPublisher() { + return publisher; + } + + @Override + public String toString() { + return getId(); + } + +} diff --git a/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/GraphQLSubscriptionInvocationInputFactory.java b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/GraphQLSubscriptionInvocationInputFactory.java new file mode 100644 index 00000000..89d83a76 --- /dev/null +++ b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/GraphQLSubscriptionInvocationInputFactory.java @@ -0,0 +1,10 @@ +package graphql.kickstart.execution.subscriptions; + +import graphql.kickstart.execution.GraphQLRequest; +import graphql.kickstart.execution.input.GraphQLSingleInvocationInput; + +public interface GraphQLSubscriptionInvocationInputFactory { + + GraphQLSingleInvocationInput create(GraphQLRequest graphQLRequest, SubscriptionSession session); + +} diff --git a/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/GraphQLSubscriptionMapper.java b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/GraphQLSubscriptionMapper.java new file mode 100644 index 00000000..1e4c8459 --- /dev/null +++ b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/GraphQLSubscriptionMapper.java @@ -0,0 +1,39 @@ +package graphql.kickstart.execution.subscriptions; + +import com.fasterxml.jackson.core.JsonProcessingException; +import graphql.ExecutionResult; +import graphql.kickstart.execution.GraphQLObjectMapper; +import graphql.kickstart.execution.GraphQLRequest; +import java.util.Map; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public class GraphQLSubscriptionMapper { + + private final GraphQLObjectMapper graphQLObjectMapper; + + public GraphQLRequest readGraphQLRequest(Object payload) { + return graphQLObjectMapper.getJacksonMapper().convertValue(payload, GraphQLRequest.class); + } + + public ExecutionResult sanitizeErrors(ExecutionResult executionResult) { + return graphQLObjectMapper.sanitizeErrors(executionResult); + } + + public boolean areErrorsPresent(ExecutionResult executionResult) { + return graphQLObjectMapper.areErrorsPresent(executionResult); + } + + public Map convertSanitizedExecutionResult(ExecutionResult executionResult) { + return graphQLObjectMapper.convertSanitizedExecutionResult(executionResult, false); + } + + public String serialize(Object payload) { + try { + return graphQLObjectMapper.getJacksonMapper().writeValueAsString(payload); + } catch (JsonProcessingException e) { + return e.getMessage(); + } + } + +} diff --git a/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/SessionSubscriber.java b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/SessionSubscriber.java new file mode 100644 index 00000000..12cfa157 --- /dev/null +++ b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/SessionSubscriber.java @@ -0,0 +1,49 @@ +package graphql.kickstart.execution.subscriptions; + +import graphql.ExecutionResult; +import java.util.HashMap; +import java.util.Map; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; + +@Slf4j +@RequiredArgsConstructor +class SessionSubscriber implements Subscriber { + + private final SubscriptionSession session; + private final String id; + private AtomicSubscriptionSubscription subscriptionReference = new AtomicSubscriptionSubscription(); + + @Override + public void onSubscribe(Subscription subscription) { + log.debug("Subscribe to execution result: {}", subscription); + subscriptionReference.set(subscription); + subscriptionReference.get().request(1); + + session.add(id, subscriptionReference.get()); + } + + @Override + public void onNext(ExecutionResult executionResult) { + Map result = new HashMap<>(); + result.put("data", executionResult.getData()); + session.sendDataMessage(id, result); + subscriptionReference.get().request(1); + } + + @Override + public void onError(Throwable throwable) { + log.error("Subscription error", throwable); + session.unsubscribe(id); + session.sendErrorMessage(id); + } + + @Override + public void onComplete() { + session.unsubscribe(id); + session.sendCompleteMessage(id); + } + +} diff --git a/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/SessionSubscriptions.java b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/SessionSubscriptions.java new file mode 100644 index 00000000..72d67953 --- /dev/null +++ b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/SessionSubscriptions.java @@ -0,0 +1,56 @@ +package graphql.kickstart.execution.subscriptions; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import org.reactivestreams.Subscription; + +/** + * @author Andrew Potter + */ +public class SessionSubscriptions { + + private final Object lock = new Object(); + + private boolean closed = false; + private Map subscriptions = new ConcurrentHashMap<>(); + + public void add(Subscription subscription) { + add(getImplicitId(subscription), subscription); + } + + public void add(String id, Subscription subscription) { + synchronized (lock) { + if (closed) { + throw new IllegalStateException("Websocket was already closed!"); + } + subscriptions.put(id, subscription); + } + } + + public void cancel(Subscription subscription) { + cancel(getImplicitId(subscription)); + } + + public void cancel(String id) { + Subscription subscription = subscriptions.remove(id); + if (subscription != null) { + subscription.cancel(); + } + } + + public void close() { + synchronized (lock) { + closed = true; + subscriptions.forEach((k, v) -> v.cancel()); + subscriptions.clear(); + } + } + + private String getImplicitId(Subscription subscription) { + return String.valueOf(subscription.hashCode()); + } + + public int getSubscriptionCount() { + return subscriptions.size(); + } +} diff --git a/src/main/java/graphql/servlet/core/SubscriptionConnectionListener.java b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/SubscriptionConnectionListener.java similarity index 61% rename from src/main/java/graphql/servlet/core/SubscriptionConnectionListener.java rename to graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/SubscriptionConnectionListener.java index 8325d1de..f806e169 100644 --- a/src/main/java/graphql/servlet/core/SubscriptionConnectionListener.java +++ b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/SubscriptionConnectionListener.java @@ -1,4 +1,4 @@ -package graphql.servlet.core; +package graphql.kickstart.execution.subscriptions; /** * Marker interface diff --git a/src/main/java/graphql/servlet/core/SubscriptionException.java b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/SubscriptionException.java similarity index 84% rename from src/main/java/graphql/servlet/core/SubscriptionException.java rename to graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/SubscriptionException.java index c1ee9d29..05040d69 100644 --- a/src/main/java/graphql/servlet/core/SubscriptionException.java +++ b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/SubscriptionException.java @@ -1,4 +1,4 @@ -package graphql.servlet.core; +package graphql.kickstart.execution.subscriptions; public class SubscriptionException extends Exception { diff --git a/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/SubscriptionHandler.java b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/SubscriptionHandler.java new file mode 100644 index 00000000..0847ad27 --- /dev/null +++ b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/SubscriptionHandler.java @@ -0,0 +1,5 @@ +package graphql.kickstart.execution.subscriptions; + +public class SubscriptionHandler { + +} diff --git a/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/SubscriptionProtocolFactory.java b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/SubscriptionProtocolFactory.java new file mode 100644 index 00000000..c1789ad2 --- /dev/null +++ b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/SubscriptionProtocolFactory.java @@ -0,0 +1,22 @@ +package graphql.kickstart.execution.subscriptions; + +import java.util.function.Consumer; + +/** + * @author Andrew Potter + */ +public abstract class SubscriptionProtocolFactory { + + private final String protocol; + + public SubscriptionProtocolFactory(String protocol) { + this.protocol = protocol; + } + + public String getProtocol() { + return protocol; + } + + public abstract Consumer createConsumer(SubscriptionSession session); + +} diff --git a/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/SubscriptionSession.java b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/SubscriptionSession.java new file mode 100644 index 00000000..6b45e51a --- /dev/null +++ b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/SubscriptionSession.java @@ -0,0 +1,49 @@ +package graphql.kickstart.execution.subscriptions; + +import graphql.ExecutionResult; +import java.util.Map; +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscription; + +public interface SubscriptionSession { + + void subscribe(String id, Publisher data); + + void add(String id, Subscription subscription); + + void unsubscribe(String id); + + void send(String message); + + void sendMessage(Object payload); + + void sendDataMessage(String id, Object payload); + + void sendErrorMessage(String id); + + void sendCompleteMessage(String id); + + void close(String reason); + + /** + * While the session is open, this method returns a Map that the developer may use to store application specific + * information relating to this session instance. The developer may retrieve information from this Map at any time + * between the opening of the session and during the onClose() method. But outside that time, any information stored + * using this Map may no longer be kept by the container. Web socket applications running on distributed + * implementations of the web container should make any application specific objects stored here java.io.Serializable, + * or the object may not be recreated after a failover. + * + * @return an editable Map of application data. + */ + Map getUserProperties(); + + boolean isOpen(); + + String getId(); + + SessionSubscriptions getSubscriptions(); + + Object unwrap(); + + Publisher getPublisher(); +} diff --git a/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/apollo/ApolloCommandProvider.java b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/apollo/ApolloCommandProvider.java new file mode 100644 index 00000000..29993119 --- /dev/null +++ b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/apollo/ApolloCommandProvider.java @@ -0,0 +1,34 @@ +package graphql.kickstart.execution.subscriptions.apollo; + +import graphql.kickstart.execution.GraphQLInvoker; +import graphql.kickstart.execution.subscriptions.GraphQLSubscriptionInvocationInputFactory; +import graphql.kickstart.execution.subscriptions.GraphQLSubscriptionMapper; +import graphql.kickstart.execution.subscriptions.apollo.OperationMessage.Type; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +public class ApolloCommandProvider { + + private final Map commands = new HashMap<>(); + + public ApolloCommandProvider( + GraphQLSubscriptionMapper mapper, + GraphQLSubscriptionInvocationInputFactory invocationInputFactory, + GraphQLInvoker graphQLInvoker, + Collection connectionListeners + ) { + commands.put(Type.GQL_CONNECTION_INIT, new SubscriptionConnectionInitCommand(connectionListeners)); + commands.put(Type.GQL_START, new SubscriptionStartCommand(mapper, invocationInputFactory, graphQLInvoker, connectionListeners)); + commands.put(Type.GQL_STOP, new SubscriptionStopCommand(connectionListeners)); + commands.put(Type.GQL_CONNECTION_TERMINATE, new SubscriptionConnectionTerminateCommand(connectionListeners)); + } + + public SubscriptionCommand getByType(Type type) { + if (commands.containsKey(type)) { + return commands.get(type); + } + throw new IllegalStateException("No command found for type " + type); + } + +} diff --git a/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/apollo/ApolloSubscriptionConnectionListener.java b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/apollo/ApolloSubscriptionConnectionListener.java new file mode 100644 index 00000000..57bf1ce0 --- /dev/null +++ b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/apollo/ApolloSubscriptionConnectionListener.java @@ -0,0 +1,24 @@ +package graphql.kickstart.execution.subscriptions.apollo; + +import graphql.kickstart.execution.subscriptions.SubscriptionConnectionListener; +import graphql.kickstart.execution.subscriptions.SubscriptionSession; + +public interface ApolloSubscriptionConnectionListener extends SubscriptionConnectionListener { + + default void onConnect(SubscriptionSession session, OperationMessage message) { + // do nothing + } + + default void onStart(SubscriptionSession session, OperationMessage message) { + // do nothing + } + + default void onStop(SubscriptionSession session, OperationMessage message) { + // do nothing + } + + default void onTerminate(SubscriptionSession session, OperationMessage message) { + // do nothing + } + +} diff --git a/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/apollo/ApolloSubscriptionConsumer.java b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/apollo/ApolloSubscriptionConsumer.java new file mode 100644 index 00000000..a7860494 --- /dev/null +++ b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/apollo/ApolloSubscriptionConsumer.java @@ -0,0 +1,31 @@ +package graphql.kickstart.execution.subscriptions.apollo; + +import com.fasterxml.jackson.core.JsonProcessingException; +import graphql.kickstart.execution.GraphQLObjectMapper; +import graphql.kickstart.execution.subscriptions.SubscriptionSession; +import graphql.kickstart.execution.subscriptions.apollo.OperationMessage.Type; +import java.util.function.Consumer; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@RequiredArgsConstructor +public class ApolloSubscriptionConsumer implements Consumer { + + private final SubscriptionSession session; + private final GraphQLObjectMapper objectMapper; + private final ApolloCommandProvider commandProvider; + + @Override + public void accept(String request) { + try { + OperationMessage message = objectMapper.getJacksonMapper().readValue(request, OperationMessage.class); + SubscriptionCommand command = commandProvider.getByType(message.getType()); + command.apply(session, message); + } catch (JsonProcessingException e) { + log.error("Cannot read subscription command '{}'", request, e); + session.sendMessage(new OperationMessage(Type.GQL_CONNECTION_ERROR, null, e.getMessage())); + } + } + +} diff --git a/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/apollo/ApolloSubscriptionKeepAliveRunner.java b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/apollo/ApolloSubscriptionKeepAliveRunner.java new file mode 100644 index 00000000..c78e9076 --- /dev/null +++ b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/apollo/ApolloSubscriptionKeepAliveRunner.java @@ -0,0 +1,58 @@ +package graphql.kickstart.execution.subscriptions.apollo; + +import graphql.kickstart.execution.subscriptions.SubscriptionSession; +import java.time.Duration; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +class ApolloSubscriptionKeepAliveRunner { + + private static final int EXECUTOR_POOL_SIZE = 10; + + private final ScheduledExecutorService executor; + private final OperationMessage keepAliveMessage; + private final Map> futures; + private final long keepAliveIntervalSeconds; + + ApolloSubscriptionKeepAliveRunner(Duration keepAliveInterval) { + this.keepAliveMessage = OperationMessage.newKeepAliveMessage(); + this.executor = Executors.newScheduledThreadPool(EXECUTOR_POOL_SIZE); + this.futures = new ConcurrentHashMap<>(); + this.keepAliveIntervalSeconds = keepAliveInterval.getSeconds(); + } + + void keepAlive(SubscriptionSession session) { + futures.computeIfAbsent(session, this::startKeepAlive); + } + + private ScheduledFuture startKeepAlive(SubscriptionSession session) { + return executor.scheduleAtFixedRate(() -> { + try { + if (session.isOpen()) { + session.sendMessage(keepAliveMessage); + } else { + log.debug("Session {} appears to be closed. Aborting keep alive", session.getId()); + abort(session); + } + } catch (Throwable t) { + log.error("Cannot send keep alive message to session {}. Aborting keep alive", session.getId(), t); + abort(session); + } + }, 0, keepAliveIntervalSeconds, TimeUnit.SECONDS); + } + + void abort(SubscriptionSession session) { + Future future = futures.remove(session); + if (future != null) { + future.cancel(true); + } + } + +} diff --git a/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/apollo/ApolloSubscriptionProtocolFactory.java b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/apollo/ApolloSubscriptionProtocolFactory.java new file mode 100644 index 00000000..02af1fcf --- /dev/null +++ b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/apollo/ApolloSubscriptionProtocolFactory.java @@ -0,0 +1,84 @@ +package graphql.kickstart.execution.subscriptions.apollo; + +import graphql.kickstart.execution.GraphQLInvoker; +import graphql.kickstart.execution.GraphQLObjectMapper; +import graphql.kickstart.execution.subscriptions.GraphQLSubscriptionInvocationInputFactory; +import graphql.kickstart.execution.subscriptions.GraphQLSubscriptionMapper; +import graphql.kickstart.execution.subscriptions.SubscriptionProtocolFactory; +import graphql.kickstart.execution.subscriptions.SubscriptionSession; +import java.time.Duration; +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; +import java.util.function.Consumer; +import lombok.Getter; + +/** + * @author Andrew Potter + */ +public class ApolloSubscriptionProtocolFactory extends SubscriptionProtocolFactory { + + public static final int KEEP_ALIVE_INTERVAL = 15; + @Getter + private final GraphQLObjectMapper objectMapper; + private final ApolloCommandProvider commandProvider; + + public ApolloSubscriptionProtocolFactory( + GraphQLObjectMapper objectMapper, + GraphQLSubscriptionInvocationInputFactory invocationInputFactory, + GraphQLInvoker graphQLInvoker + ) { + this(objectMapper, invocationInputFactory, graphQLInvoker, Duration.ofSeconds(KEEP_ALIVE_INTERVAL)); + } + + public ApolloSubscriptionProtocolFactory( + GraphQLObjectMapper objectMapper, + GraphQLSubscriptionInvocationInputFactory invocationInputFactory, + GraphQLInvoker graphQLInvoker, + Duration keepAliveInterval) { + this(objectMapper, invocationInputFactory, graphQLInvoker, null, keepAliveInterval); + } + + public ApolloSubscriptionProtocolFactory( + GraphQLObjectMapper objectMapper, + GraphQLSubscriptionInvocationInputFactory invocationInputFactory, + GraphQLInvoker graphQLInvoker, + Collection connectionListeners) { + this( + objectMapper, + invocationInputFactory, + graphQLInvoker, + connectionListeners, + Duration.ofSeconds(KEEP_ALIVE_INTERVAL) + ); + } + + public ApolloSubscriptionProtocolFactory( + GraphQLObjectMapper objectMapper, + GraphQLSubscriptionInvocationInputFactory invocationInputFactory, + GraphQLInvoker graphQLInvoker, + Collection connectionListeners, + Duration keepAliveInterval) { + super("graphql-ws"); + this.objectMapper = objectMapper; + Set listeners = new HashSet<>(); + if (connectionListeners != null) { + listeners.addAll(connectionListeners); + } + if (keepAliveInterval != null && + listeners.stream().noneMatch(KeepAliveSubscriptionConnectionListener.class::isInstance)) { + listeners.add(new KeepAliveSubscriptionConnectionListener(keepAliveInterval)); + } + commandProvider = new ApolloCommandProvider( + new GraphQLSubscriptionMapper(objectMapper), + invocationInputFactory, + graphQLInvoker, + listeners); + } + + @Override + public Consumer createConsumer(SubscriptionSession session) { + return new ApolloSubscriptionConsumer(session, objectMapper, commandProvider); + } + +} diff --git a/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/apollo/ApolloSubscriptionSession.java b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/apollo/ApolloSubscriptionSession.java new file mode 100644 index 00000000..db1918d6 --- /dev/null +++ b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/apollo/ApolloSubscriptionSession.java @@ -0,0 +1,30 @@ +package graphql.kickstart.execution.subscriptions.apollo; + +import graphql.kickstart.execution.subscriptions.DefaultSubscriptionSession; +import graphql.kickstart.execution.subscriptions.GraphQLSubscriptionMapper; +import graphql.kickstart.execution.subscriptions.apollo.OperationMessage.Type; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class ApolloSubscriptionSession extends DefaultSubscriptionSession { + + public ApolloSubscriptionSession(GraphQLSubscriptionMapper mapper) { + super(mapper); + } + + @Override + public void sendDataMessage(String id, Object payload) { + sendMessage(new OperationMessage(Type.GQL_DATA, id, payload)); + } + + @Override + public void sendErrorMessage(String id) { + sendMessage(new OperationMessage(Type.GQL_ERROR, id, null)); + } + + @Override + public void sendCompleteMessage(String id) { + sendMessage(new OperationMessage(Type.GQL_COMPLETE, id, null)); + } + +} diff --git a/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/apollo/KeepAliveSubscriptionConnectionListener.java b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/apollo/KeepAliveSubscriptionConnectionListener.java new file mode 100644 index 00000000..9f03e85c --- /dev/null +++ b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/apollo/KeepAliveSubscriptionConnectionListener.java @@ -0,0 +1,38 @@ +package graphql.kickstart.execution.subscriptions.apollo; + +import graphql.kickstart.execution.subscriptions.SubscriptionSession; +import java.time.Duration; + +public class KeepAliveSubscriptionConnectionListener implements ApolloSubscriptionConnectionListener { + + private final ApolloSubscriptionKeepAliveRunner keepAliveRunner; + + public KeepAliveSubscriptionConnectionListener() { + this(Duration.ofSeconds(15)); + } + + public KeepAliveSubscriptionConnectionListener(Duration keepAliveInterval) { + keepAliveRunner = new ApolloSubscriptionKeepAliveRunner(keepAliveInterval); + } + + @Override + public void onConnect(SubscriptionSession session, OperationMessage message) { + keepAliveRunner.keepAlive(session); + } + + @Override + public void onStart(SubscriptionSession session, OperationMessage message) { + // do nothing + } + + @Override + public void onStop(SubscriptionSession session, OperationMessage message) { + // do nothing + } + + @Override + public void onTerminate(SubscriptionSession session, OperationMessage message) { + keepAliveRunner.abort(session); + } + +} diff --git a/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/apollo/OperationMessage.java b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/apollo/OperationMessage.java new file mode 100644 index 00000000..79bad5fa --- /dev/null +++ b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/apollo/OperationMessage.java @@ -0,0 +1,77 @@ +package graphql.kickstart.execution.subscriptions.apollo; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonValue; +import java.util.HashMap; +import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.NoArgsConstructor; + +@NoArgsConstructor +@AllArgsConstructor +@JsonInclude(JsonInclude.Include.NON_NULL) +public class OperationMessage { + + private Type type; + private String id; + private Object payload; + + public static OperationMessage newKeepAliveMessage() { + return new OperationMessage(Type.GQL_CONNECTION_KEEP_ALIVE, null, null); + } + + public Type getType() { + return type; + } + + public String getId() { + return id; + } + + public Object getPayload() { + return payload; + } + + public enum Type { + + // Server Messages + GQL_CONNECTION_ACK("connection_ack"), + GQL_CONNECTION_ERROR("connection_error"), + GQL_CONNECTION_KEEP_ALIVE("ka"), + GQL_DATA("data"), + GQL_ERROR("error"), + GQL_COMPLETE("complete"), + + // Client Messages + GQL_CONNECTION_INIT("connection_init"), + GQL_CONNECTION_TERMINATE("connection_terminate"), + GQL_START("start"), + GQL_STOP("stop"); + + private static final Map reverseLookup = new HashMap<>(); + + static { + for (Type type : Type.values()) { + reverseLookup.put(type.getType(), type); + } + } + + private final String type; + + Type(String type) { + this.type = type; + } + + @JsonCreator + public static Type findType(String type) { + return reverseLookup.get(type); + } + + @JsonValue + public String getType() { + return type; + } + + } +} diff --git a/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/apollo/SubscriptionCommand.java b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/apollo/SubscriptionCommand.java new file mode 100644 index 00000000..64195f76 --- /dev/null +++ b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/apollo/SubscriptionCommand.java @@ -0,0 +1,9 @@ +package graphql.kickstart.execution.subscriptions.apollo; + +import graphql.kickstart.execution.subscriptions.SubscriptionSession; + +interface SubscriptionCommand { + + void apply(SubscriptionSession session, OperationMessage message); + +} diff --git a/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/apollo/SubscriptionConnectionInitCommand.java b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/apollo/SubscriptionConnectionInitCommand.java new file mode 100644 index 00000000..3d666ec7 --- /dev/null +++ b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/apollo/SubscriptionConnectionInitCommand.java @@ -0,0 +1,26 @@ +package graphql.kickstart.execution.subscriptions.apollo; + +import graphql.kickstart.execution.subscriptions.SubscriptionSession; +import graphql.kickstart.execution.subscriptions.apollo.OperationMessage.Type; +import java.util.Collection; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@RequiredArgsConstructor +class SubscriptionConnectionInitCommand implements SubscriptionCommand { + + private final Collection connectionListeners; + + @Override + public void apply(SubscriptionSession session, OperationMessage message) { + log.debug("Apollo subscription connection init: {}", session); + try { + connectionListeners.forEach(it -> it.onConnect(session, message)); + session.sendMessage(new OperationMessage(Type.GQL_CONNECTION_ACK, message.getId(), null)); + } catch (Throwable t) { + session.sendMessage(new OperationMessage(Type.GQL_CONNECTION_ERROR, message.getId(), t.getMessage())); + } + } + +} diff --git a/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/apollo/SubscriptionConnectionTerminateCommand.java b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/apollo/SubscriptionConnectionTerminateCommand.java new file mode 100644 index 00000000..16255c5f --- /dev/null +++ b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/apollo/SubscriptionConnectionTerminateCommand.java @@ -0,0 +1,22 @@ +package graphql.kickstart.execution.subscriptions.apollo; + +import static graphql.kickstart.execution.subscriptions.apollo.OperationMessage.Type.GQL_CONNECTION_TERMINATE; + +import graphql.kickstart.execution.subscriptions.SubscriptionSession; +import java.util.Collection; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@RequiredArgsConstructor +class SubscriptionConnectionTerminateCommand implements SubscriptionCommand { + + private final Collection connectionListeners; + + @Override + public void apply(SubscriptionSession session, OperationMessage message) { + connectionListeners.forEach(it -> it.onTerminate(session, message)); + session.close("client requested " + GQL_CONNECTION_TERMINATE.getType()); + } + +} diff --git a/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/apollo/SubscriptionStartCommand.java b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/apollo/SubscriptionStartCommand.java new file mode 100644 index 00000000..e7d0db3f --- /dev/null +++ b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/apollo/SubscriptionStartCommand.java @@ -0,0 +1,53 @@ +package graphql.kickstart.execution.subscriptions.apollo; + +import static graphql.kickstart.execution.subscriptions.apollo.OperationMessage.Type.GQL_ERROR; + +import graphql.ExecutionResult; +import graphql.kickstart.execution.GraphQLInvoker; +import graphql.kickstart.execution.GraphQLRequest; +import graphql.kickstart.execution.input.GraphQLSingleInvocationInput; +import graphql.kickstart.execution.subscriptions.GraphQLSubscriptionInvocationInputFactory; +import graphql.kickstart.execution.subscriptions.GraphQLSubscriptionMapper; +import graphql.kickstart.execution.subscriptions.SubscriptionSession; +import java.util.Collection; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@RequiredArgsConstructor +class SubscriptionStartCommand implements SubscriptionCommand { + + private final GraphQLSubscriptionMapper mapper; + private final GraphQLSubscriptionInvocationInputFactory invocationInputFactory; + private final GraphQLInvoker graphQLInvoker; + private final Collection connectionListeners; + + @Override + public void apply(SubscriptionSession session, OperationMessage message) { + log.debug("Apollo subscription start: {} --> {}", session, message.getPayload()); + connectionListeners.forEach(it -> it.onStart(session, message)); + CompletableFuture executionResult = executeAsync(message.getPayload(), session); + executionResult.thenAccept(result -> handleSubscriptionStart(session, message.getId(), result)); + } + + private CompletableFuture executeAsync(Object payload, SubscriptionSession session) { + Objects.requireNonNull(payload, "Payload is required"); + GraphQLRequest graphQLRequest = mapper.readGraphQLRequest(payload); + + GraphQLSingleInvocationInput invocationInput = invocationInputFactory.create(graphQLRequest, session); + return graphQLInvoker.executeAsync(invocationInput); + } + + private void handleSubscriptionStart(SubscriptionSession session, String id, ExecutionResult executionResult) { + ExecutionResult sanitizedExecutionResult = mapper.sanitizeErrors(executionResult); + if (!mapper.areErrorsPresent(sanitizedExecutionResult)) { + session.subscribe(id, sanitizedExecutionResult.getData()); + } else { + Object payload = mapper.convertSanitizedExecutionResult(sanitizedExecutionResult); + session.sendMessage(new OperationMessage(GQL_ERROR, id, payload)); + } + } + +} diff --git a/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/apollo/SubscriptionStopCommand.java b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/apollo/SubscriptionStopCommand.java new file mode 100644 index 00000000..71de4be5 --- /dev/null +++ b/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/apollo/SubscriptionStopCommand.java @@ -0,0 +1,18 @@ +package graphql.kickstart.execution.subscriptions.apollo; + +import graphql.kickstart.execution.subscriptions.SubscriptionSession; +import java.util.Collection; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +class SubscriptionStopCommand implements SubscriptionCommand { + + private final Collection connectionListeners; + + @Override + public void apply(SubscriptionSession session, OperationMessage message) { + connectionListeners.forEach(it -> it.onStop(session, message)); + session.unsubscribe(message.getId()); + } + +} diff --git a/graphql-java-servlet/build.gradle b/graphql-java-servlet/build.gradle new file mode 100644 index 00000000..b7bd4fc6 --- /dev/null +++ b/graphql-java-servlet/build.gradle @@ -0,0 +1,42 @@ +buildscript { + repositories { + jcenter() + mavenCentral() + } +} + +apply plugin: 'groovy' +apply plugin: 'java-library-distribution' +apply plugin: 'biz.aQute.bnd.builder' + +jar { + bnd ('Require-Capability': 'osgi.extender') +} + +dependencies { + api(project(':graphql-java-kickstart')) + + // Useful utilities + compile 'com.google.guava:guava:24.1.1-jre' + + // Servlet + compile 'javax.servlet:javax.servlet-api:3.1.0' + compile 'javax.websocket:javax.websocket-api:1.1' + + // OSGi + compileOnly 'org.osgi:org.osgi.core:6.0.0' + compileOnly 'org.osgi:org.osgi.service.cm:1.5.0' + compileOnly 'org.osgi:org.osgi.service.component:1.3.0' + compileOnly 'biz.aQute.bnd:biz.aQute.bndlib:4.3.1' + + testCompile 'io.github.graphql-java:graphql-java-annotations:5.2' + + // Unit testing + testCompile "org.codehaus.groovy:groovy-all:2.4.1" + testCompile "org.spockframework:spock-core:1.1-groovy-2.4-rc-3" + testRuntime "cglib:cglib-nodep:3.2.4" + testRuntime "org.objenesis:objenesis:2.5.1" + testCompile 'org.slf4j:slf4j-simple:1.7.24' + testCompile 'org.springframework:spring-test:4.3.7.RELEASE' + testRuntime 'org.springframework:spring-web:4.3.7.RELEASE' +} diff --git a/graphql-java-servlet/src/main/java/graphql/servlet/AbstractGraphQLHttpServlet.java b/graphql-java-servlet/src/main/java/graphql/servlet/AbstractGraphQLHttpServlet.java new file mode 100644 index 00000000..19523bde --- /dev/null +++ b/graphql-java-servlet/src/main/java/graphql/servlet/AbstractGraphQLHttpServlet.java @@ -0,0 +1,196 @@ +package graphql.servlet; + +import static graphql.kickstart.execution.GraphQLRequest.createQueryOnlyRequest; + +import graphql.ExecutionResult; +import graphql.kickstart.execution.GraphQLObjectMapper; +import graphql.kickstart.execution.GraphQLQueryInvoker; +import graphql.kickstart.execution.GraphQLRequest; +import graphql.kickstart.execution.input.GraphQLSingleInvocationInput; +import graphql.schema.GraphQLFieldDefinition; +import graphql.servlet.core.GraphQLMBean; +import graphql.servlet.core.GraphQLServletListener; +import graphql.servlet.input.GraphQLInvocationInputFactory; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.stream.Collectors; +import javax.servlet.AsyncContext; +import javax.servlet.Servlet; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; + +/** + * @author Andrew Potter + */ +@Slf4j +public abstract class AbstractGraphQLHttpServlet extends HttpServlet implements Servlet, GraphQLMBean { + + /** + * @deprecated use {@link #getConfiguration()} instead + */ + @Deprecated + private final List listeners; + private GraphQLConfiguration configuration; + private HttpRequestHandler requestHandler; + + public AbstractGraphQLHttpServlet() { + this(null); + } + + public AbstractGraphQLHttpServlet(List listeners) { + this.listeners = listeners != null ? new ArrayList<>(listeners) : new ArrayList<>(); + } + + /** + * @deprecated override {@link #getConfiguration()} instead + */ + @Deprecated + protected abstract GraphQLQueryInvoker getQueryInvoker(); + + /** + * @deprecated override {@link #getConfiguration()} instead + */ + @Deprecated + protected abstract GraphQLInvocationInputFactory getInvocationInputFactory(); + + /** + * @deprecated override {@link #getConfiguration()} instead + */ + @Deprecated + protected abstract GraphQLObjectMapper getGraphQLObjectMapper(); + + /** + * @deprecated override {@link #getConfiguration()} instead + */ + @Deprecated + protected abstract boolean isAsyncServletMode(); + + protected GraphQLConfiguration getConfiguration() { + return GraphQLConfiguration.with(getInvocationInputFactory()) + .with(getQueryInvoker()) + .with(getGraphQLObjectMapper()) + .with(isAsyncServletMode()) + .with(listeners) + .build(); + } + + @Override + public void init() { + if (configuration == null) { + this.configuration = getConfiguration(); + this.requestHandler = new HttpRequestHandlerImpl(configuration); + } + } + + public void addListener(GraphQLServletListener servletListener) { + if (configuration != null) { + configuration.add(servletListener); + } else { + listeners.add(servletListener); + } + } + + public void removeListener(GraphQLServletListener servletListener) { + if (configuration != null) { + configuration.remove(servletListener); + } else { + listeners.remove(servletListener); + } + } + + @Override + public String[] getQueries() { + return configuration.getInvocationInputFactory().getSchemaProvider().getSchema().getQueryType() + .getFieldDefinitions().stream().map(GraphQLFieldDefinition::getName).toArray(String[]::new); + } + + @Override + public String[] getMutations() { + return configuration.getInvocationInputFactory().getSchemaProvider().getSchema().getMutationType() + .getFieldDefinitions().stream().map(GraphQLFieldDefinition::getName).toArray(String[]::new); + } + + @Override + public String executeQuery(String query) { + try { + GraphQLRequest graphQLRequest = createQueryOnlyRequest(query); + GraphQLSingleInvocationInput invocationInput = configuration.getInvocationInputFactory().create(graphQLRequest); + ExecutionResult result = configuration.getGraphQLInvoker().query(invocationInput).getResult(); + return configuration.getObjectMapper().serializeResultAsJson(result); + } catch (Exception e) { + return e.getMessage(); + } + } + + private void doRequestAsync(HttpServletRequest request, HttpServletResponse response, HttpRequestHandler handler) { + if (configuration.isAsyncServletModeEnabled()) { + AsyncContext asyncContext = request.startAsync(request, response); + HttpServletRequest asyncRequest = (HttpServletRequest) asyncContext.getRequest(); + HttpServletResponse asyncResponse = (HttpServletResponse) asyncContext.getResponse(); + configuration.getAsyncExecutor().execute(() -> doRequest(asyncRequest, asyncResponse, handler, asyncContext)); + } else { + doRequest(request, response, handler, null); + } + } + + private void doRequest(HttpServletRequest request, HttpServletResponse response, HttpRequestHandler handler, + AsyncContext asyncContext) { + + List requestCallbacks = runListeners(l -> l.onRequest(request, response)); + + try { + handler.handle(request, response); + runCallbacks(requestCallbacks, c -> c.onSuccess(request, response)); + } catch (Throwable t) { + log.error("Error executing GraphQL request!", t); + runCallbacks(requestCallbacks, c -> c.onError(request, response, t)); + } finally { + runCallbacks(requestCallbacks, c -> c.onFinally(request, response)); + if (asyncContext != null) { + asyncContext.complete(); + } + } + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) { + init(); + doRequestAsync(req, resp, requestHandler); + } + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) { + init(); + doRequestAsync(req, resp, requestHandler); + } + + private List runListeners(Function action) { + return configuration.getListeners().stream() + .map(listener -> { + try { + return action.apply(listener); + } catch (Throwable t) { + log.error("Error running listener: {}", listener, t); + return null; + } + }) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + } + + private void runCallbacks(List callbacks, Consumer action) { + callbacks.forEach(callback -> { + try { + action.accept(callback); + } catch (Throwable t) { + log.error("Error running callback: {}", callback, t); + } + }); + } + +} diff --git a/graphql-java-servlet/src/main/java/graphql/servlet/AbstractGraphQLInvocationInputParser.java b/graphql-java-servlet/src/main/java/graphql/servlet/AbstractGraphQLInvocationInputParser.java new file mode 100644 index 00000000..aa703d0e --- /dev/null +++ b/graphql-java-servlet/src/main/java/graphql/servlet/AbstractGraphQLInvocationInputParser.java @@ -0,0 +1,23 @@ +package graphql.servlet; + +import graphql.kickstart.execution.context.ContextSetting; +import graphql.kickstart.execution.GraphQLObjectMapper; +import graphql.servlet.input.GraphQLInvocationInputFactory; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +abstract class AbstractGraphQLInvocationInputParser implements GraphQLInvocationInputParser { + + final GraphQLInvocationInputFactory invocationInputFactory; + final GraphQLObjectMapper graphQLObjectMapper; + final ContextSetting contextSetting; + + boolean isSingleQuery(String query) { + return query != null && !query.trim().isEmpty() && !query.trim().startsWith("["); + } + + boolean isBatchedQuery(String query) { + return query != null && !query.trim().isEmpty() && query.trim().startsWith("["); + } + +} diff --git a/graphql-java-servlet/src/main/java/graphql/servlet/BatchedQueryResponseWriter.java b/graphql-java-servlet/src/main/java/graphql/servlet/BatchedQueryResponseWriter.java new file mode 100644 index 00000000..c69d4244 --- /dev/null +++ b/graphql-java-servlet/src/main/java/graphql/servlet/BatchedQueryResponseWriter.java @@ -0,0 +1,42 @@ +package graphql.servlet; + +import static graphql.servlet.HttpRequestHandler.APPLICATION_JSON_UTF8; +import static graphql.servlet.HttpRequestHandler.STATUS_OK; + +import graphql.ExecutionResult; +import graphql.kickstart.execution.GraphQLObjectMapper; +import java.io.IOException; +import java.util.Iterator; +import java.util.List; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +class BatchedQueryResponseWriter implements QueryResponseWriter { + + private final List results; + private final GraphQLObjectMapper graphQLObjectMapper; + + @Override + public void write(HttpServletRequest request, HttpServletResponse response) throws IOException { + response.setContentType(APPLICATION_JSON_UTF8); + response.setStatus(STATUS_OK); + + Iterator executionInputIterator = results.iterator(); + StringBuilder responseBuilder = new StringBuilder(); + responseBuilder.append('['); + while (executionInputIterator.hasNext()) { + responseBuilder.append(graphQLObjectMapper.serializeResultAsJson(executionInputIterator.next())); + if (executionInputIterator.hasNext()) { + responseBuilder.append(','); + } + } + responseBuilder.append(']'); + + String responseContent = responseBuilder.toString(); + response.setContentLength(responseContent.length()); + response.getWriter().write(responseContent); + } + +} diff --git a/src/main/java/graphql/servlet/ConfiguredGraphQLHttpServlet.java b/graphql-java-servlet/src/main/java/graphql/servlet/ConfiguredGraphQLHttpServlet.java similarity index 89% rename from src/main/java/graphql/servlet/ConfiguredGraphQLHttpServlet.java rename to graphql-java-servlet/src/main/java/graphql/servlet/ConfiguredGraphQLHttpServlet.java index 9e46f152..c00fdf1c 100644 --- a/src/main/java/graphql/servlet/ConfiguredGraphQLHttpServlet.java +++ b/graphql-java-servlet/src/main/java/graphql/servlet/ConfiguredGraphQLHttpServlet.java @@ -1,7 +1,5 @@ package graphql.servlet; -import graphql.servlet.config.GraphQLConfiguration; - import java.util.Objects; class ConfiguredGraphQLHttpServlet extends GraphQLHttpServlet { diff --git a/src/main/java/graphql/servlet/DefaultGraphQLServlet.java b/graphql-java-servlet/src/main/java/graphql/servlet/DefaultGraphQLServlet.java similarity index 85% rename from src/main/java/graphql/servlet/DefaultGraphQLServlet.java rename to graphql-java-servlet/src/main/java/graphql/servlet/DefaultGraphQLServlet.java index 3be92e86..bc0370e4 100644 --- a/src/main/java/graphql/servlet/DefaultGraphQLServlet.java +++ b/graphql-java-servlet/src/main/java/graphql/servlet/DefaultGraphQLServlet.java @@ -1,7 +1,7 @@ package graphql.servlet; -import graphql.servlet.core.GraphQLObjectMapper; -import graphql.servlet.core.GraphQLQueryInvoker; +import graphql.kickstart.execution.GraphQLObjectMapper; +import graphql.kickstart.execution.GraphQLQueryInvoker; import graphql.servlet.input.GraphQLInvocationInputFactory; public class DefaultGraphQLServlet extends AbstractGraphQLHttpServlet { diff --git a/graphql-java-servlet/src/main/java/graphql/servlet/ErrorQueryResponseWriter.java b/graphql-java-servlet/src/main/java/graphql/servlet/ErrorQueryResponseWriter.java new file mode 100644 index 00000000..9cd91a5a --- /dev/null +++ b/graphql-java-servlet/src/main/java/graphql/servlet/ErrorQueryResponseWriter.java @@ -0,0 +1,19 @@ +package graphql.servlet; + +import java.io.IOException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +class ErrorQueryResponseWriter implements QueryResponseWriter { + + private final int statusCode; + private final String message; + + @Override + public void write(HttpServletRequest request, HttpServletResponse response) throws IOException { + response.sendError(statusCode, message); + } + +} diff --git a/graphql-java-servlet/src/main/java/graphql/servlet/ExecutionResultSubscriber.java b/graphql-java-servlet/src/main/java/graphql/servlet/ExecutionResultSubscriber.java new file mode 100644 index 00000000..d6342ddf --- /dev/null +++ b/graphql-java-servlet/src/main/java/graphql/servlet/ExecutionResultSubscriber.java @@ -0,0 +1,62 @@ +package graphql.servlet; + +import graphql.ExecutionResult; +import graphql.kickstart.execution.GraphQLObjectMapper; +import java.io.IOException; +import java.io.Writer; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicReference; +import javax.servlet.AsyncContext; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; + +class ExecutionResultSubscriber implements Subscriber { + + private final AtomicReference subscriptionRef; + private final AsyncContext asyncContext; + private final GraphQLObjectMapper graphQLObjectMapper; + private final CountDownLatch completedLatch = new CountDownLatch(1); + + ExecutionResultSubscriber(AtomicReference subscriptionRef, AsyncContext asyncContext, + GraphQLObjectMapper graphQLObjectMapper) { + this.subscriptionRef = subscriptionRef; + this.asyncContext = asyncContext; + this.graphQLObjectMapper = graphQLObjectMapper; + } + + @Override + public void onSubscribe(Subscription subscription) { + subscriptionRef.set(subscription); + subscriptionRef.get().request(1); + } + + @Override + public void onNext(ExecutionResult executionResult) { + try { + Writer writer = asyncContext.getResponse().getWriter(); + writer.write("data: "); + writer.write(graphQLObjectMapper.serializeResultAsJson(executionResult)); + writer.write("\n\n"); + writer.flush(); + subscriptionRef.get().request(1); + } catch (IOException ignored) { + } + } + + @Override + public void onError(Throwable t) { + asyncContext.complete(); + completedLatch.countDown(); + } + + @Override + public void onComplete() { + asyncContext.complete(); + completedLatch.countDown(); + } + + void await() throws InterruptedException { + completedLatch.await(); + } + +} diff --git a/graphql-java-servlet/src/main/java/graphql/servlet/GraphQLConfiguration.java b/graphql-java-servlet/src/main/java/graphql/servlet/GraphQLConfiguration.java new file mode 100644 index 00000000..1f7a0530 --- /dev/null +++ b/graphql-java-servlet/src/main/java/graphql/servlet/GraphQLConfiguration.java @@ -0,0 +1,217 @@ +package graphql.servlet; + +import graphql.kickstart.execution.GraphQLInvoker; +import graphql.kickstart.execution.GraphQLObjectMapper; +import graphql.kickstart.execution.GraphQLQueryInvoker; +import graphql.kickstart.execution.context.ContextSetting; +import graphql.schema.GraphQLSchema; +import graphql.servlet.config.DefaultGraphQLSchemaServletProvider; +import graphql.servlet.config.GraphQLSchemaServletProvider; +import graphql.servlet.context.GraphQLServletContextBuilder; +import graphql.servlet.core.GraphQLServletListener; +import graphql.servlet.core.GraphQLServletRootObjectBuilder; +import graphql.servlet.core.internal.GraphQLThreadFactory; +import graphql.servlet.input.BatchInputPreProcessor; +import graphql.servlet.input.GraphQLInvocationInputFactory; +import graphql.servlet.input.NoOpBatchInputPreProcessor; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.function.Supplier; + +public class GraphQLConfiguration { + + private final GraphQLInvocationInputFactory invocationInputFactory; + private final Supplier batchInputPreProcessor; + private final GraphQLQueryInvoker queryInvoker; + private final GraphQLInvoker graphQLInvoker; + private final GraphQLObjectMapper objectMapper; + private final List listeners; + private final boolean asyncServletModeEnabled; + private final Executor asyncExecutor; + private final long subscriptionTimeout; + private final ContextSetting contextSetting; + + private GraphQLConfiguration(GraphQLInvocationInputFactory invocationInputFactory, + GraphQLQueryInvoker queryInvoker, + GraphQLObjectMapper objectMapper, List listeners, boolean asyncServletModeEnabled, + Executor asyncExecutor, long subscriptionTimeout, ContextSetting contextSetting, + Supplier batchInputPreProcessor) { + this.invocationInputFactory = invocationInputFactory; + this.queryInvoker = queryInvoker; + this.graphQLInvoker = queryInvoker.toGraphQLInvoker(); + this.objectMapper = objectMapper; + this.listeners = listeners; + this.asyncServletModeEnabled = asyncServletModeEnabled; + this.asyncExecutor = asyncExecutor; + this.subscriptionTimeout = subscriptionTimeout; + this.contextSetting = contextSetting; + this.batchInputPreProcessor = batchInputPreProcessor; + } + + public static GraphQLConfiguration.Builder with(GraphQLSchema schema) { + return with(new DefaultGraphQLSchemaServletProvider(schema)); + } + + public static GraphQLConfiguration.Builder with(GraphQLSchemaServletProvider schemaProvider) { + return new Builder(GraphQLInvocationInputFactory.newBuilder(schemaProvider)); + } + + public static GraphQLConfiguration.Builder with(GraphQLInvocationInputFactory invocationInputFactory) { + return new Builder(invocationInputFactory); + } + + public GraphQLInvocationInputFactory getInvocationInputFactory() { + return invocationInputFactory; + } + + public GraphQLQueryInvoker getQueryInvoker() { return queryInvoker; } + + public GraphQLInvoker getGraphQLInvoker() { + return graphQLInvoker; + } + + public GraphQLObjectMapper getObjectMapper() { + return objectMapper; + } + + public List getListeners() { + return new ArrayList<>(listeners); + } + + public boolean isAsyncServletModeEnabled() { + return asyncServletModeEnabled; + } + + public Executor getAsyncExecutor() { + return asyncExecutor; + } + + public void add(GraphQLServletListener listener) { + listeners.add(listener); + } + + public boolean remove(GraphQLServletListener listener) { + return listeners.remove(listener); + } + + public long getSubscriptionTimeout() { + return subscriptionTimeout; + } + + public ContextSetting getContextSetting() { + return contextSetting; + } + + public BatchInputPreProcessor getBatchInputPreProcessor() { + return batchInputPreProcessor.get(); + } + + public static class Builder { + + private GraphQLInvocationInputFactory.Builder invocationInputFactoryBuilder; + private GraphQLInvocationInputFactory invocationInputFactory; + private GraphQLQueryInvoker queryInvoker = GraphQLQueryInvoker.newBuilder().build(); + private GraphQLObjectMapper objectMapper = GraphQLObjectMapper.newBuilder().build(); + private List listeners = new ArrayList<>(); + private boolean asyncServletModeEnabled = false; + private Executor asyncExecutor = Executors.newCachedThreadPool(new GraphQLThreadFactory()); + private long subscriptionTimeout = 0; + private ContextSetting contextSetting = ContextSetting.PER_QUERY_WITH_INSTRUMENTATION; + private Supplier batchInputPreProcessorSupplier = () -> new NoOpBatchInputPreProcessor(); + + private Builder(GraphQLInvocationInputFactory.Builder invocationInputFactoryBuilder) { + this.invocationInputFactoryBuilder = invocationInputFactoryBuilder; + } + + private Builder(GraphQLInvocationInputFactory invocationInputFactory) { + this.invocationInputFactory = invocationInputFactory; + } + + public Builder with(GraphQLQueryInvoker queryInvoker) { + if (queryInvoker != null) { + this.queryInvoker = queryInvoker; + } + return this; + } + + public Builder with(GraphQLObjectMapper objectMapper) { + if (objectMapper != null) { + this.objectMapper = objectMapper; + } + return this; + } + + public Builder with(List listeners) { + if (listeners != null) { + this.listeners = listeners; + } + return this; + } + + public Builder with(boolean asyncServletModeEnabled) { + this.asyncServletModeEnabled = asyncServletModeEnabled; + return this; + } + + public Builder with(Executor asyncExecutor) { + if (asyncExecutor != null) { + this.asyncExecutor = asyncExecutor; + } + return this; + } + + public Builder with(GraphQLServletContextBuilder contextBuilder) { + this.invocationInputFactoryBuilder.withGraphQLContextBuilder(contextBuilder); + return this; + } + + public Builder with(GraphQLServletRootObjectBuilder rootObjectBuilder) { + this.invocationInputFactoryBuilder.withGraphQLRootObjectBuilder(rootObjectBuilder); + return this; + } + + public Builder with(long subscriptionTimeout) { + this.subscriptionTimeout = subscriptionTimeout; + return this; + } + + public Builder with(ContextSetting contextSetting) { + if (contextSetting != null) { + this.contextSetting = contextSetting; + } + return this; + } + + public Builder with(BatchInputPreProcessor batchInputPreProcessor) { + if (batchInputPreProcessor != null) { + this.batchInputPreProcessorSupplier = () -> batchInputPreProcessor; + } + return this; + } + + public Builder with(Supplier batchInputPreProcessor) { + if (batchInputPreProcessor != null) { + this.batchInputPreProcessorSupplier = batchInputPreProcessor; + } + return this; + } + + public GraphQLConfiguration build() { + return new GraphQLConfiguration( + this.invocationInputFactory != null ? this.invocationInputFactory : invocationInputFactoryBuilder.build(), + queryInvoker, + objectMapper, + listeners, + asyncServletModeEnabled, + asyncExecutor, + subscriptionTimeout, + contextSetting, + batchInputPreProcessorSupplier + ); + } + + } + +} diff --git a/graphql-java-servlet/src/main/java/graphql/servlet/GraphQLGetInvocationInputParser.java b/graphql-java-servlet/src/main/java/graphql/servlet/GraphQLGetInvocationInputParser.java new file mode 100644 index 00000000..f8a53599 --- /dev/null +++ b/graphql-java-servlet/src/main/java/graphql/servlet/GraphQLGetInvocationInputParser.java @@ -0,0 +1,61 @@ +package graphql.servlet; + +import graphql.GraphQLException; +import graphql.kickstart.execution.context.ContextSetting; +import graphql.kickstart.execution.GraphQLObjectMapper; +import graphql.kickstart.execution.GraphQLRequest; +import graphql.kickstart.execution.input.GraphQLInvocationInput; +import graphql.servlet.input.GraphQLInvocationInputFactory; +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +class GraphQLGetInvocationInputParser extends AbstractGraphQLInvocationInputParser { + + GraphQLGetInvocationInputParser(GraphQLInvocationInputFactory invocationInputFactory, + GraphQLObjectMapper graphQLObjectMapper, ContextSetting contextSetting) { + super(invocationInputFactory, graphQLObjectMapper, contextSetting); + } + + public GraphQLInvocationInput getGraphQLInvocationInput(HttpServletRequest request, HttpServletResponse response) + throws IOException { + if (isIntrospectionQuery(request)) { + GraphQLRequest graphqlRequest = GraphQLRequest.createIntrospectionRequest(); + return invocationInputFactory.create(graphqlRequest, request, response); + } + + String query = request.getParameter("query"); + if (query == null) { + throw new GraphQLException("Query parameter not found in GET request"); + } + + if (isSingleQuery(query)) { + Map variables = getVariables(request); + String operationName = request.getParameter("operationName"); + GraphQLRequest graphqlRequest = new GraphQLRequest(query, variables, operationName); + return invocationInputFactory.createReadOnly(graphqlRequest, request, response); + } + + List graphqlRequests = graphQLObjectMapper.readBatchedGraphQLRequest(query); + return invocationInputFactory.createReadOnly(contextSetting, graphqlRequests, request, response); + } + + private boolean isIntrospectionQuery(HttpServletRequest request) { + String path = Optional.ofNullable(request.getPathInfo()).orElseGet(request::getServletPath).toLowerCase(); + return path.contentEquals("/schema.json"); + } + + private Map getVariables(HttpServletRequest request) { + return Optional.ofNullable(request.getParameter("variables")) + .map(graphQLObjectMapper::deserializeVariables) + .map(HashMap::new) + .orElseGet(HashMap::new); + } + +} diff --git a/src/main/java/graphql/servlet/GraphQLHttpServlet.java b/graphql-java-servlet/src/main/java/graphql/servlet/GraphQLHttpServlet.java similarity index 88% rename from src/main/java/graphql/servlet/GraphQLHttpServlet.java rename to graphql-java-servlet/src/main/java/graphql/servlet/GraphQLHttpServlet.java index b16c072b..161f6db8 100644 --- a/src/main/java/graphql/servlet/GraphQLHttpServlet.java +++ b/graphql-java-servlet/src/main/java/graphql/servlet/GraphQLHttpServlet.java @@ -1,9 +1,8 @@ package graphql.servlet; import graphql.schema.GraphQLSchema; -import graphql.servlet.core.GraphQLObjectMapper; -import graphql.servlet.core.GraphQLQueryInvoker; -import graphql.servlet.config.GraphQLConfiguration; +import graphql.kickstart.execution.GraphQLObjectMapper; +import graphql.kickstart.execution.GraphQLQueryInvoker; import graphql.servlet.input.GraphQLInvocationInputFactory; /** diff --git a/graphql-java-servlet/src/main/java/graphql/servlet/GraphQLInvocationInputParser.java b/graphql-java-servlet/src/main/java/graphql/servlet/GraphQLInvocationInputParser.java new file mode 100644 index 00000000..32292f83 --- /dev/null +++ b/graphql-java-servlet/src/main/java/graphql/servlet/GraphQLInvocationInputParser.java @@ -0,0 +1,39 @@ +package graphql.servlet; + +import graphql.kickstart.execution.context.ContextSetting; +import graphql.kickstart.execution.GraphQLObjectMapper; +import graphql.kickstart.execution.input.GraphQLInvocationInput; +import graphql.servlet.input.GraphQLInvocationInputFactory; +import java.io.IOException; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +interface GraphQLInvocationInputParser { + + static GraphQLInvocationInputParser create(HttpServletRequest request, + GraphQLInvocationInputFactory invocationInputFactory, + GraphQLObjectMapper graphQLObjectMapper, + ContextSetting contextSetting + ) throws IOException { + if ("GET".equalsIgnoreCase(request.getMethod())) { + return new GraphQLGetInvocationInputParser(invocationInputFactory, graphQLObjectMapper, contextSetting); + } + + try { + boolean notMultipartRequest =request.getContentType() == null || + !request.getContentType().startsWith("multipart/form-data") || + request.getParts().isEmpty(); + if (notMultipartRequest) { + return new GraphQLPostInvocationInputParser(invocationInputFactory, graphQLObjectMapper, contextSetting); + } + return new GraphQLMultipartInvocationInputParser(invocationInputFactory, graphQLObjectMapper, contextSetting); + } catch (ServletException e) { + throw new IOException("Cannot get parts of request", e); + } + } + + GraphQLInvocationInput getGraphQLInvocationInput(HttpServletRequest request, HttpServletResponse response) + throws IOException; + +} diff --git a/graphql-java-servlet/src/main/java/graphql/servlet/GraphQLMultipartInvocationInputParser.java b/graphql-java-servlet/src/main/java/graphql/servlet/GraphQLMultipartInvocationInputParser.java new file mode 100644 index 00000000..99b4f967 --- /dev/null +++ b/graphql-java-servlet/src/main/java/graphql/servlet/GraphQLMultipartInvocationInputParser.java @@ -0,0 +1,139 @@ +package graphql.servlet; + +import static java.util.stream.Collectors.joining; + +import graphql.GraphQLException; +import graphql.kickstart.execution.context.ContextSetting; +import graphql.kickstart.execution.GraphQLObjectMapper; +import graphql.kickstart.execution.GraphQLRequest; +import graphql.servlet.core.internal.VariableMapper; +import graphql.kickstart.execution.input.GraphQLInvocationInput; +import graphql.servlet.input.GraphQLInvocationInputFactory; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.Part; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +class GraphQLMultipartInvocationInputParser extends AbstractGraphQLInvocationInputParser { + + private static final String[] MULTIPART_KEYS = new String[]{"operations", "graphql", "query"}; + + GraphQLMultipartInvocationInputParser(GraphQLInvocationInputFactory invocationInputFactory, + GraphQLObjectMapper graphQLObjectMapper, ContextSetting contextSetting) { + super(invocationInputFactory, graphQLObjectMapper, contextSetting); + } + + @Override + public GraphQLInvocationInput getGraphQLInvocationInput(HttpServletRequest request, HttpServletResponse response) + throws IOException { + try { + final Map> parts = request.getParts() + .stream() + .collect(Collectors.groupingBy(Part::getName)); + + for (String key : MULTIPART_KEYS) { + // Check to see if there is a part under the key we seek + if (!parts.containsKey(key)) { + continue; + } + + final Optional queryItem = getPart(parts, key); + if (!queryItem.isPresent()) { + // If there is a part, but we don't see an item, then break and return BAD_REQUEST + break; + } + + InputStream inputStream = queryItem.get().getInputStream(); + + final Optional>> variablesMap = + getPart(parts, "map") + .map(part -> { + try (InputStream is = part.getInputStream()) { + return graphQLObjectMapper.deserializeMultipartMap(is); + } catch (IOException e) { + throw new RuntimeException("Unable to read input stream from part", e); + } + }); + + String query = read(inputStream); + if ("query".equals(key) && isSingleQuery(query)) { + GraphQLRequest graphqlRequest = buildRequestFromQuery(query, graphQLObjectMapper, parts); + variablesMap.ifPresent(m -> mapMultipartVariables(graphqlRequest, m, parts)); + return invocationInputFactory.create(graphqlRequest, request, response); + } else { + if (isSingleQuery(query)) { + GraphQLRequest graphqlRequest = graphQLObjectMapper.readGraphQLRequest(query); + variablesMap.ifPresent(m -> mapMultipartVariables(graphqlRequest, m, parts)); + return invocationInputFactory.create(graphqlRequest, request, response); + } else { + List graphqlRequests = graphQLObjectMapper.readBatchedGraphQLRequest(query); + variablesMap.ifPresent(map -> graphqlRequests.forEach(r -> mapMultipartVariables(r, map, parts))); + return invocationInputFactory.create(contextSetting, graphqlRequests, request, response); + } + } + } + + log.info("Bad POST multipart request: no part named {}", Arrays.toString(MULTIPART_KEYS)); + throw new GraphQLException("Bad POST multipart request: no part named " + Arrays.toString(MULTIPART_KEYS)); + } catch (ServletException e) { + throw new IOException("Cannot get parts from request", e); + } + } + + private Optional getPart(Map> parts, String name) { + return Optional.ofNullable(parts.get(name)).filter(list -> !list.isEmpty()).map(list -> list.get(0)); + } + + private void mapMultipartVariables(GraphQLRequest request, + Map> variablesMap, + Map> fileItems) { + Map variables = request.getVariables(); + + variablesMap.forEach((partName, objectPaths) -> { + Part part = getPart(fileItems, partName) + .orElseThrow(() -> new RuntimeException("unable to find part name " + + partName + + " as referenced in the variables map")); + + objectPaths.forEach(objectPath -> VariableMapper.mapVariable(objectPath, variables, part)); + }); + } + + private GraphQLRequest buildRequestFromQuery(String query, + GraphQLObjectMapper graphQLObjectMapper, + Map> parts) throws IOException { + Map variables = null; + final Optional variablesItem = getPart(parts, "variables"); + if (variablesItem.isPresent()) { + variables = graphQLObjectMapper + .deserializeVariables(read(variablesItem.get().getInputStream())); + } + + String operationName = null; + final Optional operationNameItem = getPart(parts, "operationName"); + if (operationNameItem.isPresent()) { + operationName = read(operationNameItem.get().getInputStream()).trim(); + } + + return new GraphQLRequest(query, variables, operationName); + } + + private String read(InputStream inputStream) throws IOException { + try (InputStreamReader streamReader = new InputStreamReader(inputStream); + BufferedReader reader = new BufferedReader(streamReader)) { + return reader.lines().collect(joining()); + } + } + +} diff --git a/graphql-java-servlet/src/main/java/graphql/servlet/GraphQLPostInvocationInputParser.java b/graphql-java-servlet/src/main/java/graphql/servlet/GraphQLPostInvocationInputParser.java new file mode 100644 index 00000000..cea0a2e7 --- /dev/null +++ b/graphql-java-servlet/src/main/java/graphql/servlet/GraphQLPostInvocationInputParser.java @@ -0,0 +1,47 @@ +package graphql.servlet; + +import static java.util.stream.Collectors.joining; + +import graphql.GraphQLException; +import graphql.kickstart.execution.GraphQLRequest; +import graphql.kickstart.execution.context.ContextSetting; +import graphql.kickstart.execution.input.GraphQLInvocationInput; +import graphql.kickstart.execution.GraphQLObjectMapper; +import graphql.servlet.input.GraphQLInvocationInputFactory; +import java.io.IOException; +import java.util.List; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +class GraphQLPostInvocationInputParser extends AbstractGraphQLInvocationInputParser { + + private static final String APPLICATION_GRAPHQL = "application/graphql"; + + GraphQLPostInvocationInputParser(GraphQLInvocationInputFactory invocationInputFactory, + GraphQLObjectMapper graphQLObjectMapper, ContextSetting contextSetting) { + super(invocationInputFactory, graphQLObjectMapper, contextSetting); + } + + public GraphQLInvocationInput getGraphQLInvocationInput(HttpServletRequest request, HttpServletResponse response) + throws IOException { + if (APPLICATION_GRAPHQL.equals(request.getContentType())) { + String query = request.getReader().lines().collect(joining()); + GraphQLRequest graphqlRequest = GraphQLRequest.createQueryOnlyRequest(query); + return invocationInputFactory.create(graphqlRequest, request, response); + } + + String body = request.getReader().lines().collect(joining()); + if (isSingleQuery(body)) { + GraphQLRequest graphqlRequest = graphQLObjectMapper.readGraphQLRequest(body); + return invocationInputFactory.create(graphqlRequest, request, response); + } + + if (isBatchedQuery(body)) { + List requests = graphQLObjectMapper.readBatchedGraphQLRequest(body); + return invocationInputFactory.create(contextSetting, requests, request, response); + } + + throw new GraphQLException("No valid query found in request"); + } + +} diff --git a/graphql-java-servlet/src/main/java/graphql/servlet/GraphQLWebsocketServlet.java b/graphql-java-servlet/src/main/java/graphql/servlet/GraphQLWebsocketServlet.java new file mode 100644 index 00000000..acf404ce --- /dev/null +++ b/graphql-java-servlet/src/main/java/graphql/servlet/GraphQLWebsocketServlet.java @@ -0,0 +1,243 @@ +package graphql.servlet; + +import static java.util.Collections.singletonList; +import static java.util.stream.Collectors.toList; + +import graphql.kickstart.execution.GraphQLInvoker; +import graphql.kickstart.execution.GraphQLObjectMapper; +import graphql.kickstart.execution.subscriptions.GraphQLSubscriptionInvocationInputFactory; +import graphql.kickstart.execution.subscriptions.GraphQLSubscriptionMapper; +import graphql.kickstart.execution.subscriptions.SessionSubscriptions; +import graphql.kickstart.execution.subscriptions.SubscriptionConnectionListener; +import graphql.kickstart.execution.subscriptions.SubscriptionProtocolFactory; +import graphql.kickstart.execution.subscriptions.SubscriptionSession; +import graphql.kickstart.execution.subscriptions.apollo.ApolloSubscriptionConnectionListener; +import graphql.servlet.apollo.ApolloWebSocketSubscriptionProtocolFactory; +import graphql.servlet.subscriptions.FallbackSubscriptionProtocolFactory; +import graphql.servlet.subscriptions.WebSocketSendSubscriber; +import graphql.servlet.subscriptions.WebSocketSubscriptionProtocolFactory; +import java.io.EOFException; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; +import java.util.stream.Stream; +import javax.websocket.CloseReason; +import javax.websocket.Endpoint; +import javax.websocket.EndpointConfig; +import javax.websocket.HandshakeResponse; +import javax.websocket.MessageHandler; +import javax.websocket.Session; +import javax.websocket.server.HandshakeRequest; +import javax.websocket.server.ServerEndpointConfig; +import lombok.extern.slf4j.Slf4j; + +/** + * Must be used with {@link #modifyHandshake(ServerEndpointConfig, HandshakeRequest, HandshakeResponse)} + * + * @author Andrew Potter + */ +@Slf4j +public class GraphQLWebsocketServlet extends Endpoint { + + private static final String HANDSHAKE_REQUEST_KEY = HandshakeRequest.class.getName(); + private static final String PROTOCOL_FACTORY_REQUEST_KEY = SubscriptionProtocolFactory.class.getName(); + private static final CloseReason ERROR_CLOSE_REASON = new CloseReason(CloseReason.CloseCodes.UNEXPECTED_CONDITION, + "Internal Server Error"); + private static final CloseReason SHUTDOWN_CLOSE_REASON = new CloseReason(CloseReason.CloseCodes.UNEXPECTED_CONDITION, + "Server Shut Down"); + + private final List subscriptionProtocolFactories; + private final SubscriptionProtocolFactory fallbackSubscriptionProtocolFactory; + private final List allSubscriptionProtocols; + + private final Map sessionSubscriptionCache = new ConcurrentHashMap<>(); + private final AtomicBoolean isShuttingDown = new AtomicBoolean(false); + private final AtomicBoolean isShutDown = new AtomicBoolean(false); + private final Object cacheLock = new Object(); + + public GraphQLWebsocketServlet( + GraphQLInvoker graphQLInvoker, + GraphQLSubscriptionInvocationInputFactory invocationInputFactory, + GraphQLObjectMapper graphQLObjectMapper) { + this(graphQLInvoker, invocationInputFactory, graphQLObjectMapper, null); + } + + public GraphQLWebsocketServlet( + GraphQLInvoker graphQLInvoker, + GraphQLSubscriptionInvocationInputFactory invocationInputFactory, + GraphQLObjectMapper graphQLObjectMapper, + Collection connectionListeners) { + List listeners = new ArrayList<>(); + if (connectionListeners != null) { + connectionListeners.stream() + .filter(ApolloSubscriptionConnectionListener.class::isInstance) + .map(ApolloSubscriptionConnectionListener.class::cast) + .forEach(listeners::add); + } + subscriptionProtocolFactories = singletonList(new ApolloWebSocketSubscriptionProtocolFactory( + graphQLObjectMapper, + invocationInputFactory, + graphQLInvoker, + listeners + )); + fallbackSubscriptionProtocolFactory = new FallbackSubscriptionProtocolFactory( + new GraphQLSubscriptionMapper(graphQLObjectMapper), + invocationInputFactory, + graphQLInvoker + ); + allSubscriptionProtocols = Stream + .concat(subscriptionProtocolFactories.stream(), Stream.of(fallbackSubscriptionProtocolFactory)) + .map(SubscriptionProtocolFactory::getProtocol) + .collect(toList()); + } + + @Override + public void onOpen(Session session, EndpointConfig endpointConfig) { + final WebSocketSubscriptionProtocolFactory subscriptionProtocolFactory = + (WebSocketSubscriptionProtocolFactory) endpointConfig.getUserProperties().get(PROTOCOL_FACTORY_REQUEST_KEY); + + // todo: create apollo version of it through SubscriptionProtocolFactory + SubscriptionSession subscriptionSession = subscriptionProtocolFactory.createSession(session); + synchronized (cacheLock) { + if (isShuttingDown.get()) { + throw new IllegalStateException("Server is shutting down!"); + } + + sessionSubscriptionCache.put(session, subscriptionSession.getSubscriptions()); + } + + subscriptionSession.getPublisher().subscribe(new WebSocketSendSubscriber(session)); + + log.debug("Session opened: {}, {}", session.getId(), endpointConfig); + Consumer consumer = subscriptionProtocolFactory.createConsumer(subscriptionSession); + + // This *cannot* be a lambda because of the way undertow checks the class... + session.addMessageHandler(new MessageHandler.Whole() { + @Override + public void onMessage(String text) { + try { + consumer.accept(text); + } catch (Throwable t) { + log.error("Error executing websocket query for session: {}", session.getId(), t); + closeUnexpectedly(session, t); + } + } + }); + } + + @Override + public void onClose(Session session, CloseReason closeReason) { + log.debug("Session closed: {}, {}", session.getId(), closeReason); + SessionSubscriptions subscriptions; + synchronized (cacheLock) { + subscriptions = sessionSubscriptionCache.remove(session); + } + if (subscriptions != null) { + subscriptions.close(); + } + } + + @Override + public void onError(Session session, Throwable thr) { + if (thr instanceof EOFException) { + log.warn("Session {} was killed abruptly without calling onClose. Cleaning up session", session.getId()); + onClose(session, ERROR_CLOSE_REASON); + } else { + log.error("Error in websocket session: {}", session.getId(), thr); + closeUnexpectedly(session, thr); + } + } + + private void closeUnexpectedly(Session session, Throwable t) { + try { + session.close(ERROR_CLOSE_REASON); + } catch (IOException e) { + log.error("Error closing websocket session for session: {}", session.getId(), t); + } + } + + public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) { + sec.getUserProperties().put(HANDSHAKE_REQUEST_KEY, request); + + List protocol = request.getHeaders().get(HandshakeRequest.SEC_WEBSOCKET_PROTOCOL); + if (protocol == null) { + protocol = Collections.emptyList(); + } + + SubscriptionProtocolFactory subscriptionProtocolFactory = getSubscriptionProtocolFactory(protocol); + sec.getUserProperties().put(PROTOCOL_FACTORY_REQUEST_KEY, subscriptionProtocolFactory); + + if (request.getHeaders().get(HandshakeResponse.SEC_WEBSOCKET_ACCEPT) != null) { + response.getHeaders().put(HandshakeResponse.SEC_WEBSOCKET_ACCEPT, allSubscriptionProtocols); + } + if (!protocol.isEmpty()) { + response.getHeaders().put(HandshakeRequest.SEC_WEBSOCKET_PROTOCOL, + singletonList(subscriptionProtocolFactory.getProtocol())); + } + } + + /** + * Stops accepting connections and closes all existing connections + */ + public void beginShutDown() { + synchronized (cacheLock) { + isShuttingDown.set(true); + Map copy = new HashMap<>(sessionSubscriptionCache); + + // Prevent comodification exception since #onClose() is called during session.close(), but we can't necessarily rely on that happening so we close subscriptions here anyway. + copy.forEach((session, wsSessionSubscriptions) -> { + wsSessionSubscriptions.close(); + try { + session.close(SHUTDOWN_CLOSE_REASON); + } catch (IOException e) { + log.error("Error closing websocket session!", e); + } + }); + + copy.clear(); + + if (!sessionSubscriptionCache.isEmpty()) { + log.error("GraphQLWebsocketServlet did not shut down cleanly!"); + sessionSubscriptionCache.clear(); + } + } + + isShutDown.set(true); + } + + /** + * @return true when shutdown is complete + */ + public boolean isShutDown() { + return isShutDown.get(); + } + + private SubscriptionProtocolFactory getSubscriptionProtocolFactory(List accept) { + for (String protocol : accept) { + for (SubscriptionProtocolFactory subscriptionProtocolFactory : subscriptionProtocolFactories) { + if (subscriptionProtocolFactory.getProtocol().equals(protocol)) { + return subscriptionProtocolFactory; + } + } + } + + return fallbackSubscriptionProtocolFactory; + } + + public int getSessionCount() { + return sessionSubscriptionCache.size(); + } + + public int getSubscriptionCount() { + return sessionSubscriptionCache.values().stream() + .mapToInt(SessionSubscriptions::getSubscriptionCount) + .sum(); + } +} diff --git a/graphql-java-servlet/src/main/java/graphql/servlet/HttpRequestHandler.java b/graphql-java-servlet/src/main/java/graphql/servlet/HttpRequestHandler.java new file mode 100644 index 00000000..3e0bb385 --- /dev/null +++ b/graphql-java-servlet/src/main/java/graphql/servlet/HttpRequestHandler.java @@ -0,0 +1,16 @@ +package graphql.servlet; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +interface HttpRequestHandler { + + String APPLICATION_JSON_UTF8 = "application/json;charset=UTF-8"; + String APPLICATION_EVENT_STREAM_UTF8 = "text/event-stream;charset=UTF-8"; + + int STATUS_OK = 200; + int STATUS_BAD_REQUEST = 400; + + void handle(HttpServletRequest request, HttpServletResponse response) throws Exception; + +} diff --git a/graphql-java-servlet/src/main/java/graphql/servlet/HttpRequestHandlerImpl.java b/graphql-java-servlet/src/main/java/graphql/servlet/HttpRequestHandlerImpl.java new file mode 100644 index 00000000..170d2295 --- /dev/null +++ b/graphql-java-servlet/src/main/java/graphql/servlet/HttpRequestHandlerImpl.java @@ -0,0 +1,85 @@ +package graphql.servlet; + +import static graphql.servlet.QueryResponseWriter.createWriter; + +import graphql.GraphQLException; +import graphql.kickstart.execution.GraphQLInvoker; +import graphql.kickstart.execution.GraphQLQueryResult; +import graphql.servlet.input.BatchInputPreProcessResult; +import graphql.servlet.input.BatchInputPreProcessor; +import graphql.kickstart.execution.input.GraphQLBatchedInvocationInput; +import graphql.kickstart.execution.input.GraphQLInvocationInput; +import graphql.kickstart.execution.input.GraphQLSingleInvocationInput; +import java.io.IOException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +class HttpRequestHandlerImpl implements HttpRequestHandler { + + private final GraphQLConfiguration configuration; + private final GraphQLInvoker graphQLInvoker; + + HttpRequestHandlerImpl(GraphQLConfiguration configuration) { + this.configuration = configuration; + graphQLInvoker = configuration.getGraphQLInvoker(); + } + + @Override + public void handle(HttpServletRequest request, HttpServletResponse response) throws IOException { + try { + GraphQLInvocationInputParser invocationInputParser = GraphQLInvocationInputParser.create( + request, + configuration.getInvocationInputFactory(), + configuration.getObjectMapper(), + configuration.getContextSetting() + ); + GraphQLInvocationInput invocationInput = invocationInputParser.getGraphQLInvocationInput(request, response); + execute(invocationInput, request, response); + } catch (GraphQLException e) { + response.setStatus(STATUS_BAD_REQUEST); + log.info("Bad request: cannot create invocation input parser", e); + throw e; + } catch (Throwable t) { + response.setStatus(500); + log.info("Bad request: cannot create invocation input parser", t); + throw t; + } + } + + private void execute(GraphQLInvocationInput invocationInput, HttpServletRequest request, + HttpServletResponse response) { + try { + GraphQLQueryResult queryResult = invoke(invocationInput, request, response); + + QueryResponseWriter queryResponseWriter = createWriter(queryResult, configuration.getObjectMapper(), + configuration.getSubscriptionTimeout()); + queryResponseWriter.write(request, response); + } catch (Throwable t) { + response.setStatus(STATUS_BAD_REQUEST); + log.info("Bad GET request: path was not \"/schema.json\" or no query variable named \"query\" given"); + } + } + + private GraphQLQueryResult invoke(GraphQLInvocationInput invocationInput, HttpServletRequest request, + HttpServletResponse response) { + if (invocationInput instanceof GraphQLSingleInvocationInput) { + return graphQLInvoker.query(invocationInput); + } + return invokeBatched((GraphQLBatchedInvocationInput) invocationInput, request, response); + } + + private GraphQLQueryResult invokeBatched(GraphQLBatchedInvocationInput batchedInvocationInput, + HttpServletRequest request, + HttpServletResponse response) { + BatchInputPreProcessor preprocessor = configuration.getBatchInputPreProcessor(); + BatchInputPreProcessResult result = preprocessor.preProcessBatch(batchedInvocationInput, request, response); + if (result.isExecutable()) { + return graphQLInvoker.query(result.getBatchedInvocationInput()); + } + + return GraphQLQueryResult.createError(result.getStatusCode(), result.getStatusMessage()); + } + +} diff --git a/graphql-java-servlet/src/main/java/graphql/servlet/OsgiGraphQLHttpServlet.java b/graphql-java-servlet/src/main/java/graphql/servlet/OsgiGraphQLHttpServlet.java new file mode 100644 index 00000000..29ae4bb9 --- /dev/null +++ b/graphql-java-servlet/src/main/java/graphql/servlet/OsgiGraphQLHttpServlet.java @@ -0,0 +1,390 @@ +package graphql.servlet; + +import static graphql.schema.GraphQLObjectType.newObject; +import static graphql.schema.GraphQLSchema.newSchema; + +import aQute.bnd.component.annotations.Activate; +import aQute.bnd.component.annotations.Component; +import aQute.bnd.component.annotations.Deactivate; +import aQute.bnd.component.annotations.Reference; +import aQute.bnd.component.annotations.ReferenceCardinality; +import aQute.bnd.component.annotations.ReferencePolicy; +import aQute.bnd.component.annotations.ReferencePolicyOption; +import graphql.execution.preparsed.NoOpPreparsedDocumentProvider; +import graphql.execution.preparsed.PreparsedDocumentProvider; +import graphql.schema.GraphQLCodeRegistry; +import graphql.schema.GraphQLObjectType; +import graphql.schema.GraphQLType; +import graphql.kickstart.execution.config.DefaultExecutionStrategyProvider; +import graphql.servlet.config.DefaultGraphQLSchemaServletProvider; +import graphql.kickstart.execution.config.ExecutionStrategyProvider; +import graphql.servlet.config.GraphQLSchemaServletProvider; +import graphql.servlet.core.GraphQLServletRootObjectBuilder; +import graphql.servlet.osgi.GraphQLCodeRegistryProvider; +import graphql.servlet.osgi.GraphQLMutationProvider; +import graphql.servlet.osgi.GraphQLProvider; +import graphql.servlet.osgi.GraphQLQueryProvider; +import graphql.servlet.osgi.GraphQLSubscriptionProvider; +import graphql.servlet.osgi.GraphQLTypesProvider; +import graphql.kickstart.execution.config.InstrumentationProvider; +import graphql.servlet.context.DefaultGraphQLServletContextBuilder; +import graphql.servlet.context.GraphQLServletContextBuilder; +import graphql.kickstart.execution.error.DefaultGraphQLErrorHandler; +import graphql.servlet.core.DefaultGraphQLRootObjectBuilder; +import graphql.kickstart.execution.error.GraphQLErrorHandler; +import graphql.kickstart.execution.GraphQLObjectMapper; +import graphql.kickstart.execution.GraphQLQueryInvoker; +import graphql.kickstart.execution.GraphQLRootObjectBuilder; +import graphql.servlet.core.GraphQLServletListener; +import graphql.servlet.input.GraphQLInvocationInputFactory; +import graphql.kickstart.execution.instrumentation.NoOpInstrumentationProvider; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +@Component( + service = {javax.servlet.http.HttpServlet.class, javax.servlet.Servlet.class}, + property = {"alias=/graphql", "jmx.objectname=graphql.servlet:type=graphql"} +) +public class OsgiGraphQLHttpServlet extends AbstractGraphQLHttpServlet { + + private final List queryProviders = new ArrayList<>(); + private final List mutationProviders = new ArrayList<>(); + private final List subscriptionProviders = new ArrayList<>(); + private final List typesProviders = new ArrayList<>(); + + private final GraphQLQueryInvoker queryInvoker; + private final GraphQLInvocationInputFactory invocationInputFactory; + private final GraphQLObjectMapper graphQLObjectMapper; + + private GraphQLServletContextBuilder contextBuilder = new DefaultGraphQLServletContextBuilder(); + private GraphQLServletRootObjectBuilder rootObjectBuilder = new DefaultGraphQLRootObjectBuilder(); + private ExecutionStrategyProvider executionStrategyProvider = new DefaultExecutionStrategyProvider(); + private InstrumentationProvider instrumentationProvider = new NoOpInstrumentationProvider(); + private GraphQLErrorHandler errorHandler = new DefaultGraphQLErrorHandler(); + private PreparsedDocumentProvider preparsedDocumentProvider = NoOpPreparsedDocumentProvider.INSTANCE; + private GraphQLCodeRegistryProvider codeRegistryProvider = () -> GraphQLCodeRegistry.newCodeRegistry().build(); + + private GraphQLSchemaServletProvider schemaProvider; + + private ScheduledExecutorService executor; + private ScheduledFuture updateFuture; + private int schemaUpdateDelay; + + public OsgiGraphQLHttpServlet() { + updateSchema(); + + this.queryInvoker = GraphQLQueryInvoker.newBuilder() + .withPreparsedDocumentProvider(this::getPreparsedDocumentProvider) + .withInstrumentation(() -> this.getInstrumentationProvider().getInstrumentation()) + .withExecutionStrategyProvider(this::getExecutionStrategyProvider).build(); + + this.invocationInputFactory = GraphQLInvocationInputFactory.newBuilder(this::getSchemaProvider) + .withGraphQLContextBuilder(this::getContextBuilder) + .withGraphQLRootObjectBuilder(this::getRootObjectBuilder) + .build(); + + this.graphQLObjectMapper = GraphQLObjectMapper.newBuilder() + .withGraphQLErrorHandler(this::getErrorHandler) + .build(); + } + + @Activate + public void activate(Config config) { + this.schemaUpdateDelay = config.schema_update_delay(); + if (schemaUpdateDelay != 0) { + executor = Executors.newSingleThreadScheduledExecutor(); + } + } + + @Deactivate + public void deactivate() { + if (executor != null) { + executor.shutdown(); + } + } + + @Override + protected GraphQLQueryInvoker getQueryInvoker() { + return queryInvoker; + } + + @Override + protected GraphQLInvocationInputFactory getInvocationInputFactory() { + return invocationInputFactory; + } + + @Override + protected GraphQLObjectMapper getGraphQLObjectMapper() { + return graphQLObjectMapper; + } + + @Override + protected boolean isAsyncServletMode() { + return false; + } + + protected void updateSchema() { + if (schemaUpdateDelay == 0) { + doUpdateSchema(); + } else { + if (updateFuture != null) { + updateFuture.cancel(true); + } + + updateFuture = executor.schedule(new Runnable() { + @Override + public void run() { + doUpdateSchema(); + } + }, schemaUpdateDelay, TimeUnit.MILLISECONDS); + } + } + + private void doUpdateSchema() { + final GraphQLObjectType.Builder queryTypeBuilder = newObject().name("Query").description("Root query type"); + + for (GraphQLQueryProvider provider : queryProviders) { + if (provider.getQueries() != null && !provider.getQueries().isEmpty()) { + provider.getQueries().forEach(queryTypeBuilder::field); + } + } + + final Set types = new HashSet<>(); + for (GraphQLTypesProvider typesProvider : typesProviders) { + types.addAll(typesProvider.getTypes()); + } + + GraphQLObjectType mutationType = null; + + if (!mutationProviders.isEmpty()) { + final GraphQLObjectType.Builder mutationTypeBuilder = newObject().name("Mutation") + .description("Root mutation type"); + + for (GraphQLMutationProvider provider : mutationProviders) { + provider.getMutations().forEach(mutationTypeBuilder::field); + } + + if (!mutationTypeBuilder.build().getFieldDefinitions().isEmpty()) { + mutationType = mutationTypeBuilder.build(); + } + } + + GraphQLObjectType subscriptionType = null; + + if (!subscriptionProviders.isEmpty()) { + final GraphQLObjectType.Builder subscriptionTypeBuilder = newObject().name("Subscription") + .description("Root subscription type"); + + for (GraphQLSubscriptionProvider provider : subscriptionProviders) { + provider.getSubscriptions().forEach(subscriptionTypeBuilder::field); + } + + if (!subscriptionTypeBuilder.build().getFieldDefinitions().isEmpty()) { + subscriptionType = subscriptionTypeBuilder.build(); + } + } + + this.schemaProvider = new DefaultGraphQLSchemaServletProvider(newSchema().query(queryTypeBuilder.build()) + .mutation(mutationType) + .subscription(subscriptionType) + .additionalTypes(types) + .codeRegistry(codeRegistryProvider.getCodeRegistry()) + .build()); + } + + @Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC) + public void bindProvider(GraphQLProvider provider) { + if (provider instanceof GraphQLQueryProvider) { + queryProviders.add((GraphQLQueryProvider) provider); + } + if (provider instanceof GraphQLMutationProvider) { + mutationProviders.add((GraphQLMutationProvider) provider); + } + if (provider instanceof GraphQLSubscriptionProvider) { + subscriptionProviders.add((GraphQLSubscriptionProvider) provider); + } + if (provider instanceof GraphQLTypesProvider) { + typesProviders.add((GraphQLTypesProvider) provider); + } + if (provider instanceof GraphQLCodeRegistryProvider) { + codeRegistryProvider = (GraphQLCodeRegistryProvider) provider; + } + updateSchema(); + } + + public void unbindProvider(GraphQLProvider provider) { + if (provider instanceof GraphQLQueryProvider) { + queryProviders.remove(provider); + } + if (provider instanceof GraphQLMutationProvider) { + mutationProviders.remove(provider); + } + if (provider instanceof GraphQLSubscriptionProvider) { + subscriptionProviders.remove(provider); + } + if (provider instanceof GraphQLTypesProvider) { + typesProviders.remove(provider); + } + if (provider instanceof GraphQLCodeRegistryProvider) { + codeRegistryProvider = () -> GraphQLCodeRegistry.newCodeRegistry().build(); + } + updateSchema(); + } + + @Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC) + public void bindQueryProvider(GraphQLQueryProvider queryProvider) { + queryProviders.add(queryProvider); + updateSchema(); + } + + public void unbindQueryProvider(GraphQLQueryProvider queryProvider) { + queryProviders.remove(queryProvider); + updateSchema(); + } + + @Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC) + public void bindMutationProvider(GraphQLMutationProvider mutationProvider) { + mutationProviders.add(mutationProvider); + updateSchema(); + } + + public void unbindMutationProvider(GraphQLMutationProvider mutationProvider) { + mutationProviders.remove(mutationProvider); + updateSchema(); + } + + @Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC) + public void bindSubscriptionProvider(GraphQLSubscriptionProvider subscriptionProvider) { + subscriptionProviders.add(subscriptionProvider); + updateSchema(); + } + + public void unbindSubscriptionProvider(GraphQLSubscriptionProvider subscriptionProvider) { + subscriptionProviders.remove(subscriptionProvider); + updateSchema(); + } + + @Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC) + public void bindTypesProvider(GraphQLTypesProvider typesProvider) { + typesProviders.add(typesProvider); + updateSchema(); + } + + public void unbindTypesProvider(GraphQLTypesProvider typesProvider) { + typesProviders.remove(typesProvider); + updateSchema(); + } + + @Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC) + public void bindServletListener(GraphQLServletListener listener) { + this.addListener(listener); + } + + public void unbindServletListener(GraphQLServletListener listener) { + this.removeListener(listener); + } + + @Reference(cardinality = ReferenceCardinality.OPTIONAL, policy = ReferencePolicy.DYNAMIC) + public void setContextProvider(GraphQLServletContextBuilder contextBuilder) { + this.contextBuilder = contextBuilder; + } + + public void unsetContextProvider(GraphQLServletContextBuilder contextBuilder) { + this.contextBuilder = new DefaultGraphQLServletContextBuilder(); + } + + public void unsetRootObjectBuilder(GraphQLRootObjectBuilder rootObjectBuilder) { + this.rootObjectBuilder = new DefaultGraphQLRootObjectBuilder(); + } + + public void unsetExecutionStrategyProvider(ExecutionStrategyProvider provider) { + executionStrategyProvider = new DefaultExecutionStrategyProvider(); + } + + public void unsetInstrumentationProvider(InstrumentationProvider provider) { + instrumentationProvider = new NoOpInstrumentationProvider(); + } + + public void unsetErrorHandler(GraphQLErrorHandler errorHandler) { + this.errorHandler = new DefaultGraphQLErrorHandler(); + } + + public void unsetPreparsedDocumentProvider(PreparsedDocumentProvider preparsedDocumentProvider) { + this.preparsedDocumentProvider = NoOpPreparsedDocumentProvider.INSTANCE; + } + + @Reference(cardinality = ReferenceCardinality.OPTIONAL, policy = ReferencePolicy.DYNAMIC, policyOption = ReferencePolicyOption.GREEDY) + public void bindCodeRegistryProvider(GraphQLCodeRegistryProvider graphQLCodeRegistryProvider) { + this.codeRegistryProvider = graphQLCodeRegistryProvider; + updateSchema(); + } + + public void unbindCodeRegistryProvider(GraphQLCodeRegistryProvider graphQLCodeRegistryProvider) { + this.codeRegistryProvider = () -> GraphQLCodeRegistry.newCodeRegistry().build(); + updateSchema(); + } + + public GraphQLServletContextBuilder getContextBuilder() { + return contextBuilder; + } + + public GraphQLServletRootObjectBuilder getRootObjectBuilder() { + return rootObjectBuilder; + } + + @Reference(cardinality = ReferenceCardinality.OPTIONAL, policy = ReferencePolicy.DYNAMIC) + public void setRootObjectBuilder(GraphQLServletRootObjectBuilder rootObjectBuilder) { + this.rootObjectBuilder = rootObjectBuilder; + } + + public ExecutionStrategyProvider getExecutionStrategyProvider() { + return executionStrategyProvider; + } + + @Reference(cardinality = ReferenceCardinality.OPTIONAL, policy = ReferencePolicy.DYNAMIC, policyOption = ReferencePolicyOption.GREEDY) + public void setExecutionStrategyProvider(ExecutionStrategyProvider provider) { + executionStrategyProvider = provider; + } + + public InstrumentationProvider getInstrumentationProvider() { + return instrumentationProvider; + } + + @Reference(cardinality = ReferenceCardinality.OPTIONAL, policy = ReferencePolicy.DYNAMIC, policyOption = ReferencePolicyOption.GREEDY) + public void setInstrumentationProvider(InstrumentationProvider provider) { + instrumentationProvider = provider; + } + + public GraphQLErrorHandler getErrorHandler() { + return errorHandler; + } + + @Reference(cardinality = ReferenceCardinality.OPTIONAL, policy = ReferencePolicy.DYNAMIC, policyOption = ReferencePolicyOption.GREEDY) + public void setErrorHandler(GraphQLErrorHandler errorHandler) { + this.errorHandler = errorHandler; + } + + public PreparsedDocumentProvider getPreparsedDocumentProvider() { + return preparsedDocumentProvider; + } + + @Reference(cardinality = ReferenceCardinality.OPTIONAL, policy = ReferencePolicy.DYNAMIC, policyOption = ReferencePolicyOption.GREEDY) + public void setPreparsedDocumentProvider(PreparsedDocumentProvider preparsedDocumentProvider) { + this.preparsedDocumentProvider = preparsedDocumentProvider; + } + + public GraphQLSchemaServletProvider getSchemaProvider() { + return schemaProvider; + } + + @interface Config { + + int schema_update_delay() default 0; + } +} diff --git a/graphql-java-servlet/src/main/java/graphql/servlet/QueryResponseWriter.java b/graphql-java-servlet/src/main/java/graphql/servlet/QueryResponseWriter.java new file mode 100644 index 00000000..907c1a02 --- /dev/null +++ b/graphql-java-servlet/src/main/java/graphql/servlet/QueryResponseWriter.java @@ -0,0 +1,31 @@ +package graphql.servlet; + +import graphql.kickstart.execution.GraphQLQueryResult; +import graphql.kickstart.execution.GraphQLObjectMapper; +import java.io.IOException; +import java.util.Objects; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +interface QueryResponseWriter { + + static QueryResponseWriter createWriter( + GraphQLQueryResult result, + GraphQLObjectMapper graphQLObjectMapper, + long subscriptionTimeout + ) { + Objects.requireNonNull(result, "GraphQL query result cannot be null"); + + if (result.isBatched()) { + return new BatchedQueryResponseWriter(result.getResults(), graphQLObjectMapper); + } else if (result.isAsynchronous()) { + return new SingleAsynchronousQueryResponseWriter(result.getResult(), graphQLObjectMapper, subscriptionTimeout); + } else if (result.isError()) { + return new ErrorQueryResponseWriter(result.getStatusCode(), result.getMessage()); + } + return new SingleQueryResponseWriter(result.getResult(), graphQLObjectMapper); + } + + void write(HttpServletRequest request, HttpServletResponse response) throws IOException; + +} diff --git a/graphql-java-servlet/src/main/java/graphql/servlet/SimpleGraphQLHttpServlet.java b/graphql-java-servlet/src/main/java/graphql/servlet/SimpleGraphQLHttpServlet.java new file mode 100644 index 00000000..009642df --- /dev/null +++ b/graphql-java-servlet/src/main/java/graphql/servlet/SimpleGraphQLHttpServlet.java @@ -0,0 +1,147 @@ +package graphql.servlet; + +import graphql.kickstart.execution.GraphQLObjectMapper; +import graphql.kickstart.execution.GraphQLQueryInvoker; +import graphql.schema.GraphQLSchema; +import graphql.servlet.config.GraphQLSchemaServletProvider; +import graphql.servlet.core.GraphQLServletListener; +import graphql.servlet.input.GraphQLInvocationInputFactory; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** + * @author Andrew Potter + */ +public class SimpleGraphQLHttpServlet extends AbstractGraphQLHttpServlet { + + private GraphQLConfiguration configuration; + + public SimpleGraphQLHttpServlet() { + } + + /** + * @deprecated use {@link GraphQLHttpServlet} instead + */ + @Deprecated + public SimpleGraphQLHttpServlet(GraphQLInvocationInputFactory invocationInputFactory, + GraphQLQueryInvoker queryInvoker, GraphQLObjectMapper graphQLObjectMapper, List listeners, + boolean asyncServletMode) { + super(listeners); + this.configuration = GraphQLConfiguration.with(invocationInputFactory) + .with(queryInvoker) + .with(graphQLObjectMapper) + .with(listeners != null ? listeners : new ArrayList<>()) + .with(asyncServletMode) + .build(); + } + + /** + * @deprecated use {@link GraphQLHttpServlet} instead + */ + @Deprecated + public SimpleGraphQLHttpServlet(GraphQLInvocationInputFactory invocationInputFactory, + GraphQLQueryInvoker queryInvoker, GraphQLObjectMapper graphQLObjectMapper, List listeners, + boolean asyncServletMode, long subscriptionTimeout) { + super(listeners); + this.configuration = GraphQLConfiguration.with(invocationInputFactory) + .with(queryInvoker) + .with(graphQLObjectMapper) + .with(listeners != null ? listeners : new ArrayList<>()) + .with(asyncServletMode) + .with(subscriptionTimeout) + .build(); + } + + private SimpleGraphQLHttpServlet(GraphQLConfiguration configuration) { + this.configuration = Objects.requireNonNull(configuration, "configuration is required"); + } + + public static Builder newBuilder(GraphQLSchema schema) { + return new Builder(GraphQLInvocationInputFactory.newBuilder(schema).build()); + } + + public static Builder newBuilder(GraphQLSchemaServletProvider schemaProvider) { + return new Builder(GraphQLInvocationInputFactory.newBuilder(schemaProvider).build()); + } + + public static Builder newBuilder(GraphQLInvocationInputFactory invocationInputFactory) { + return new Builder(invocationInputFactory); + } + + @Override + protected GraphQLConfiguration getConfiguration() { + return configuration; + } + + @Override + protected GraphQLQueryInvoker getQueryInvoker() { + return configuration.getQueryInvoker(); + } + + @Override + protected GraphQLInvocationInputFactory getInvocationInputFactory() { + return configuration.getInvocationInputFactory(); + } + + @Override + protected GraphQLObjectMapper getGraphQLObjectMapper() { + return configuration.getObjectMapper(); + } + + @Override + protected boolean isAsyncServletMode() { + return configuration.isAsyncServletModeEnabled(); + } + + public static class Builder { + + private final GraphQLInvocationInputFactory invocationInputFactory; + private GraphQLQueryInvoker queryInvoker = GraphQLQueryInvoker.newBuilder().build(); + private GraphQLObjectMapper graphQLObjectMapper = GraphQLObjectMapper.newBuilder().build(); + private List listeners; + private boolean asyncServletMode; + private long subscriptionTimeout; + + Builder(GraphQLInvocationInputFactory invocationInputFactory) { + this.invocationInputFactory = invocationInputFactory; + } + + public Builder withQueryInvoker(GraphQLQueryInvoker queryInvoker) { + this.queryInvoker = queryInvoker; + return this; + } + + public Builder withObjectMapper(GraphQLObjectMapper objectMapper) { + this.graphQLObjectMapper = objectMapper; + return this; + } + + public Builder withAsyncServletMode(boolean asyncServletMode) { + this.asyncServletMode = asyncServletMode; + return this; + } + + public Builder withListeners(List listeners) { + this.listeners = listeners; + return this; + } + + public Builder withSubscriptionTimeout(long subscriptionTimeout) { + this.subscriptionTimeout = subscriptionTimeout; + return this; + } + + @Deprecated + public SimpleGraphQLHttpServlet build() { + GraphQLConfiguration configuration = GraphQLConfiguration.with(invocationInputFactory) + .with(queryInvoker) + .with(graphQLObjectMapper) + .with(listeners != null ? listeners : new ArrayList<>()) + .with(asyncServletMode) + .with(subscriptionTimeout) + .build(); + return new SimpleGraphQLHttpServlet(configuration); + } + } +} diff --git a/graphql-java-servlet/src/main/java/graphql/servlet/SingleAsynchronousQueryResponseWriter.java b/graphql-java-servlet/src/main/java/graphql/servlet/SingleAsynchronousQueryResponseWriter.java new file mode 100644 index 00000000..da30d0d0 --- /dev/null +++ b/graphql-java-servlet/src/main/java/graphql/servlet/SingleAsynchronousQueryResponseWriter.java @@ -0,0 +1,63 @@ +package graphql.servlet; + +import static graphql.servlet.HttpRequestHandler.APPLICATION_EVENT_STREAM_UTF8; +import static graphql.servlet.HttpRequestHandler.STATUS_OK; + +import graphql.ExecutionResult; +import graphql.GraphQL; +import graphql.kickstart.execution.GraphQLObjectMapper; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicReference; +import javax.servlet.AsyncContext; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscription; + +@RequiredArgsConstructor +class SingleAsynchronousQueryResponseWriter implements QueryResponseWriter { + + @Getter + private final ExecutionResult result; + private final GraphQLObjectMapper graphQLObjectMapper; + private final long subscriptionTimeout; + + @Override + public void write(HttpServletRequest request, HttpServletResponse response) { + Objects.requireNonNull(request, "Http servlet request cannot be null"); + response.setContentType(APPLICATION_EVENT_STREAM_UTF8); + response.setStatus(STATUS_OK); + + boolean isInAsyncThread = request.isAsyncStarted(); + AsyncContext asyncContext = isInAsyncThread ? request.getAsyncContext() : request.startAsync(request, response); + asyncContext.setTimeout(subscriptionTimeout); + AtomicReference subscriptionRef = new AtomicReference<>(); + asyncContext.addListener(new SubscriptionAsyncListener(subscriptionRef)); + ExecutionResultSubscriber subscriber = new ExecutionResultSubscriber(subscriptionRef, asyncContext, + graphQLObjectMapper); + List> publishers = new ArrayList<>(); + if (result.getData() instanceof Publisher) { + publishers.add(result.getData()); + } else { + publishers.add(new StaticDataPublisher<>(result)); + final Publisher deferredResultsPublisher = (Publisher) result.getExtensions() + .get(GraphQL.DEFERRED_RESULTS); + publishers.add(deferredResultsPublisher); + } + publishers.forEach(it -> it.subscribe(subscriber)); + + if (isInAsyncThread) { + // We need to delay the completion of async context until after the subscription has terminated, otherwise the AsyncContext is prematurely closed. + try { + subscriber.await(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + } + +} diff --git a/graphql-java-servlet/src/main/java/graphql/servlet/SingleQueryResponseWriter.java b/graphql-java-servlet/src/main/java/graphql/servlet/SingleQueryResponseWriter.java new file mode 100644 index 00000000..33475a11 --- /dev/null +++ b/graphql-java-servlet/src/main/java/graphql/servlet/SingleQueryResponseWriter.java @@ -0,0 +1,28 @@ +package graphql.servlet; + +import static graphql.servlet.HttpRequestHandler.APPLICATION_JSON_UTF8; +import static graphql.servlet.HttpRequestHandler.STATUS_OK; + +import graphql.ExecutionResult; +import graphql.kickstart.execution.GraphQLObjectMapper; +import java.io.IOException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +class SingleQueryResponseWriter implements QueryResponseWriter { + + private final ExecutionResult result; + private final GraphQLObjectMapper graphQLObjectMapper; + + @Override + public void write(HttpServletRequest request, HttpServletResponse response) throws IOException { + response.setContentType(APPLICATION_JSON_UTF8); + response.setStatus(STATUS_OK); + String responseContent = graphQLObjectMapper.serializeResultAsJson(result); + response.setContentLength(responseContent.length()); + response.getWriter().write(responseContent); + } + +} diff --git a/graphql-java-servlet/src/main/java/graphql/servlet/StaticDataPublisher.java b/graphql-java-servlet/src/main/java/graphql/servlet/StaticDataPublisher.java new file mode 100644 index 00000000..bccfa7ba --- /dev/null +++ b/graphql-java-servlet/src/main/java/graphql/servlet/StaticDataPublisher.java @@ -0,0 +1,14 @@ +package graphql.servlet; + +import graphql.execution.reactive.SingleSubscriberPublisher; +import org.reactivestreams.Publisher; + +class StaticDataPublisher extends SingleSubscriberPublisher implements Publisher { + + StaticDataPublisher(T data) { + super(); + offer(data); + noMoreData(); + } + +} diff --git a/graphql-java-servlet/src/main/java/graphql/servlet/SubscriptionAsyncListener.java b/graphql-java-servlet/src/main/java/graphql/servlet/SubscriptionAsyncListener.java new file mode 100644 index 00000000..7be88fad --- /dev/null +++ b/graphql-java-servlet/src/main/java/graphql/servlet/SubscriptionAsyncListener.java @@ -0,0 +1,33 @@ +package graphql.servlet; + +import java.util.concurrent.atomic.AtomicReference; +import javax.servlet.AsyncEvent; +import javax.servlet.AsyncListener; +import lombok.RequiredArgsConstructor; +import org.reactivestreams.Subscription; + +@RequiredArgsConstructor +class SubscriptionAsyncListener implements AsyncListener { + + private final AtomicReference subscriptionRef; + + @Override + public void onComplete(AsyncEvent event) { + subscriptionRef.get().cancel(); + } + + @Override + public void onTimeout(AsyncEvent event) { + subscriptionRef.get().cancel(); + } + + @Override + public void onError(AsyncEvent event) { + subscriptionRef.get().cancel(); + } + + @Override + public void onStartAsync(AsyncEvent event) { + } + +} diff --git a/graphql-java-servlet/src/main/java/graphql/servlet/apollo/ApolloScalars.java b/graphql-java-servlet/src/main/java/graphql/servlet/apollo/ApolloScalars.java new file mode 100644 index 00000000..4cc34ce1 --- /dev/null +++ b/graphql-java-servlet/src/main/java/graphql/servlet/apollo/ApolloScalars.java @@ -0,0 +1,44 @@ +package graphql.servlet.apollo; + +import graphql.schema.Coercing; +import graphql.schema.CoercingParseLiteralException; +import graphql.schema.CoercingParseValueException; +import graphql.schema.CoercingSerializeException; +import graphql.schema.GraphQLScalarType; +import javax.servlet.http.Part; + +public class ApolloScalars { + + public static final GraphQLScalarType Upload = + GraphQLScalarType.newScalar() + .name("Upload") + .description("A file part in a multipart request") + .coercing(new Coercing() { + @Override + public Void serialize(Object dataFetcherResult) { + throw new CoercingSerializeException("Upload is an input-only type"); + } + + @Override + public Part parseValue(Object input) { + if (input instanceof Part) { + return (Part) input; + } else if (null == input) { + return null; + } else { + throw new CoercingParseValueException("Expected type " + + Part.class.getName() + + " but was " + + input.getClass().getName()); + } + } + + @Override + public Part parseLiteral(Object input) { + throw new CoercingParseLiteralException( + "Must use variables to specify Upload values"); + } + }) + .build(); + +} diff --git a/graphql-java-servlet/src/main/java/graphql/servlet/apollo/ApolloWebSocketSubscriptionProtocolFactory.java b/graphql-java-servlet/src/main/java/graphql/servlet/apollo/ApolloWebSocketSubscriptionProtocolFactory.java new file mode 100644 index 00000000..ebfe7272 --- /dev/null +++ b/graphql-java-servlet/src/main/java/graphql/servlet/apollo/ApolloWebSocketSubscriptionProtocolFactory.java @@ -0,0 +1,50 @@ +package graphql.servlet.apollo; + +import graphql.kickstart.execution.GraphQLInvoker; +import graphql.kickstart.execution.GraphQLObjectMapper; +import graphql.kickstart.execution.subscriptions.GraphQLSubscriptionInvocationInputFactory; +import graphql.kickstart.execution.subscriptions.GraphQLSubscriptionMapper; +import graphql.kickstart.execution.subscriptions.SubscriptionSession; +import graphql.kickstart.execution.subscriptions.apollo.ApolloSubscriptionConnectionListener; +import graphql.kickstart.execution.subscriptions.apollo.ApolloSubscriptionProtocolFactory; +import graphql.servlet.subscriptions.WebSocketSubscriptionProtocolFactory; +import java.time.Duration; +import java.util.Collection; +import javax.websocket.Session; + +public class ApolloWebSocketSubscriptionProtocolFactory + extends ApolloSubscriptionProtocolFactory + implements WebSocketSubscriptionProtocolFactory { + + public ApolloWebSocketSubscriptionProtocolFactory(GraphQLObjectMapper objectMapper, + GraphQLSubscriptionInvocationInputFactory invocationInputFactory, + GraphQLInvoker graphQLInvoker) { + super(objectMapper, invocationInputFactory, graphQLInvoker); + } + + public ApolloWebSocketSubscriptionProtocolFactory(GraphQLObjectMapper objectMapper, + GraphQLSubscriptionInvocationInputFactory invocationInputFactory, + GraphQLInvoker graphQLInvoker, Duration keepAliveInterval) { + super(objectMapper, invocationInputFactory, graphQLInvoker, keepAliveInterval); + } + + public ApolloWebSocketSubscriptionProtocolFactory(GraphQLObjectMapper objectMapper, + GraphQLSubscriptionInvocationInputFactory invocationInputFactory, + GraphQLInvoker graphQLInvoker, + Collection connectionListeners) { + super(objectMapper, invocationInputFactory, graphQLInvoker, connectionListeners); + } + + public ApolloWebSocketSubscriptionProtocolFactory(GraphQLObjectMapper objectMapper, + GraphQLSubscriptionInvocationInputFactory invocationInputFactory, + GraphQLInvoker graphQLInvoker, + Collection connectionListeners, Duration keepAliveInterval) { + super(objectMapper, invocationInputFactory, graphQLInvoker, connectionListeners, keepAliveInterval); + } + + @Override + public SubscriptionSession createSession(Session session) { + return new ApolloWebSocketSubscriptionSession(new GraphQLSubscriptionMapper(getObjectMapper()), session); + } + +} diff --git a/graphql-java-servlet/src/main/java/graphql/servlet/apollo/ApolloWebSocketSubscriptionSession.java b/graphql-java-servlet/src/main/java/graphql/servlet/apollo/ApolloWebSocketSubscriptionSession.java new file mode 100644 index 00000000..e745b7da --- /dev/null +++ b/graphql-java-servlet/src/main/java/graphql/servlet/apollo/ApolloWebSocketSubscriptionSession.java @@ -0,0 +1,38 @@ +package graphql.servlet.apollo; + +import graphql.kickstart.execution.subscriptions.GraphQLSubscriptionMapper; +import graphql.kickstart.execution.subscriptions.apollo.ApolloSubscriptionSession; +import graphql.servlet.subscriptions.WebSocketSubscriptionSession; +import java.util.Map; +import javax.websocket.Session; + +public class ApolloWebSocketSubscriptionSession extends ApolloSubscriptionSession { + + private final WebSocketSubscriptionSession webSocketSubscriptionSession; + + public ApolloWebSocketSubscriptionSession(GraphQLSubscriptionMapper mapper, Session session) { + super(mapper); + webSocketSubscriptionSession = new WebSocketSubscriptionSession(mapper, session); + } + + @Override + public boolean isOpen() { + return webSocketSubscriptionSession.isOpen(); + } + + @Override + public Map getUserProperties() { + return webSocketSubscriptionSession.getUserProperties(); + } + + @Override + public String getId() { + return webSocketSubscriptionSession.getId(); + } + + @Override + public Session unwrap() { + return webSocketSubscriptionSession.unwrap(); + } + +} diff --git a/graphql-java-servlet/src/main/java/graphql/servlet/config/DefaultGraphQLSchemaServletProvider.java b/graphql-java-servlet/src/main/java/graphql/servlet/config/DefaultGraphQLSchemaServletProvider.java new file mode 100644 index 00000000..1dc9f4ed --- /dev/null +++ b/graphql-java-servlet/src/main/java/graphql/servlet/config/DefaultGraphQLSchemaServletProvider.java @@ -0,0 +1,33 @@ +package graphql.servlet.config; + +import graphql.kickstart.execution.config.DefaultGraphQLSchemaProvider; +import graphql.schema.GraphQLSchema; +import javax.servlet.http.HttpServletRequest; +import javax.websocket.server.HandshakeRequest; + +/** + * @author Andrew Potter + */ +public class DefaultGraphQLSchemaServletProvider extends DefaultGraphQLSchemaProvider implements + GraphQLSchemaServletProvider { + + public DefaultGraphQLSchemaServletProvider(GraphQLSchema schema) { + super(schema); + } + + @Override + public GraphQLSchema getSchema(HttpServletRequest request) { + return getSchema(); + } + + @Override + public GraphQLSchema getSchema(HandshakeRequest request) { + return getSchema(); + } + + @Override + public GraphQLSchema getReadOnlySchema(HttpServletRequest request) { + return getReadOnlySchema(); + } + +} diff --git a/graphql-java-servlet/src/main/java/graphql/servlet/config/GraphQLSchemaServletProvider.java b/graphql-java-servlet/src/main/java/graphql/servlet/config/GraphQLSchemaServletProvider.java new file mode 100644 index 00000000..305bf34e --- /dev/null +++ b/graphql-java-servlet/src/main/java/graphql/servlet/config/GraphQLSchemaServletProvider.java @@ -0,0 +1,28 @@ +package graphql.servlet.config; + +import graphql.kickstart.execution.config.GraphQLSchemaProvider; +import graphql.schema.GraphQLSchema; +import javax.servlet.http.HttpServletRequest; +import javax.websocket.server.HandshakeRequest; + +public interface GraphQLSchemaServletProvider extends GraphQLSchemaProvider { + + /** + * @param request the http request + * @return a schema based on the request (auth, etc). + */ + GraphQLSchema getSchema(HttpServletRequest request); + + /** + * @param request the http request used to create a websocket + * @return a schema based on the request (auth, etc). + */ + GraphQLSchema getSchema(HandshakeRequest request); + + /** + * @param request the http request + * @return a read-only schema based on the request (auth, etc). Should return the same schema (query/subscription-only version) as {@link #getSchema(HttpServletRequest)} for a given request. + */ + GraphQLSchema getReadOnlySchema(HttpServletRequest request); + +} diff --git a/src/main/java/graphql/servlet/context/DefaultGraphQLServletContext.java b/graphql-java-servlet/src/main/java/graphql/servlet/context/DefaultGraphQLServletContext.java similarity index 98% rename from src/main/java/graphql/servlet/context/DefaultGraphQLServletContext.java rename to graphql-java-servlet/src/main/java/graphql/servlet/context/DefaultGraphQLServletContext.java index 03ef9463..f396f71e 100644 --- a/src/main/java/graphql/servlet/context/DefaultGraphQLServletContext.java +++ b/graphql-java-servlet/src/main/java/graphql/servlet/context/DefaultGraphQLServletContext.java @@ -1,5 +1,6 @@ package graphql.servlet.context; +import graphql.kickstart.execution.context.DefaultGraphQLContext; import org.dataloader.DataLoaderRegistry; import javax.security.auth.Subject; diff --git a/graphql-java-servlet/src/main/java/graphql/servlet/context/DefaultGraphQLServletContextBuilder.java b/graphql-java-servlet/src/main/java/graphql/servlet/context/DefaultGraphQLServletContextBuilder.java new file mode 100644 index 00000000..5270b062 --- /dev/null +++ b/graphql-java-servlet/src/main/java/graphql/servlet/context/DefaultGraphQLServletContextBuilder.java @@ -0,0 +1,26 @@ +package graphql.servlet.context; + +import graphql.kickstart.execution.context.DefaultGraphQLContextBuilder; +import graphql.kickstart.execution.context.GraphQLContext; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.websocket.Session; +import javax.websocket.server.HandshakeRequest; + +/** + * Returns an empty context. + */ +public class DefaultGraphQLServletContextBuilder extends DefaultGraphQLContextBuilder implements + GraphQLServletContextBuilder { + + @Override + public GraphQLContext build(HttpServletRequest request, HttpServletResponse response) { + return DefaultGraphQLServletContext.createServletContext().with(request).with(response).build(); + } + + @Override + public GraphQLContext build(Session session, HandshakeRequest handshakeRequest) { + return DefaultGraphQLWebSocketContext.createWebSocketContext().with(session).with(handshakeRequest).build(); + } + +} diff --git a/graphql-java-servlet/src/main/java/graphql/servlet/context/DefaultGraphQLWebSocketContext.java b/graphql-java-servlet/src/main/java/graphql/servlet/context/DefaultGraphQLWebSocketContext.java new file mode 100644 index 00000000..fdad98f7 --- /dev/null +++ b/graphql-java-servlet/src/main/java/graphql/servlet/context/DefaultGraphQLWebSocketContext.java @@ -0,0 +1,76 @@ +package graphql.servlet.context; + +import graphql.kickstart.execution.context.DefaultGraphQLContext; +import javax.security.auth.Subject; +import javax.websocket.Session; +import javax.websocket.server.HandshakeRequest; +import org.dataloader.DataLoaderRegistry; + +public class DefaultGraphQLWebSocketContext extends DefaultGraphQLContext implements GraphQLWebSocketContext { + + private final Session session; + private final HandshakeRequest handshakeRequest; + + private DefaultGraphQLWebSocketContext(DataLoaderRegistry dataLoaderRegistry, Subject subject, + Session session, HandshakeRequest handshakeRequest) { + super(dataLoaderRegistry, subject); + this.session = session; + this.handshakeRequest = handshakeRequest; + } + + public static Builder createWebSocketContext(DataLoaderRegistry registry, Subject subject) { + return new Builder(registry, subject); + } + + public static Builder createWebSocketContext() { + return new Builder(new DataLoaderRegistry(), null); + } + + @Override + public Session getSession() { + return session; + } + + @Override + public HandshakeRequest getHandshakeRequest() { + return handshakeRequest; + } + + public static class Builder { + + private Session session; + private HandshakeRequest handshakeRequest; + private DataLoaderRegistry dataLoaderRegistry; + private Subject subject; + + private Builder(DataLoaderRegistry dataLoaderRegistry, Subject subject) { + this.dataLoaderRegistry = dataLoaderRegistry; + this.subject = subject; + } + + public DefaultGraphQLWebSocketContext build() { + return new DefaultGraphQLWebSocketContext(dataLoaderRegistry, subject, session, handshakeRequest); + } + + public Builder with(Session session) { + this.session = session; + return this; + } + + public Builder with(HandshakeRequest handshakeRequest) { + this.handshakeRequest = handshakeRequest; + return this; + } + + public Builder with(DataLoaderRegistry dataLoaderRegistry) { + this.dataLoaderRegistry = dataLoaderRegistry; + return this; + } + + public Builder with(Subject subject) { + this.subject = subject; + return this; + } + } + +} diff --git a/src/main/java/graphql/servlet/context/GraphQLServletContext.java b/graphql-java-servlet/src/main/java/graphql/servlet/context/GraphQLServletContext.java similarity index 80% rename from src/main/java/graphql/servlet/context/GraphQLServletContext.java rename to graphql-java-servlet/src/main/java/graphql/servlet/context/GraphQLServletContext.java index 58af94da..73ff70fd 100644 --- a/src/main/java/graphql/servlet/context/GraphQLServletContext.java +++ b/graphql-java-servlet/src/main/java/graphql/servlet/context/GraphQLServletContext.java @@ -1,13 +1,11 @@ package graphql.servlet.context; +import graphql.kickstart.execution.context.GraphQLContext; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.Part; -import javax.websocket.Session; -import javax.websocket.server.HandshakeRequest; import java.util.List; import java.util.Map; -import java.util.Optional; public interface GraphQLServletContext extends GraphQLContext { diff --git a/src/main/java/graphql/servlet/context/GraphQLContextBuilder.java b/graphql-java-servlet/src/main/java/graphql/servlet/context/GraphQLServletContextBuilder.java similarity index 66% rename from src/main/java/graphql/servlet/context/GraphQLContextBuilder.java rename to graphql-java-servlet/src/main/java/graphql/servlet/context/GraphQLServletContextBuilder.java index 4ca7916a..5c319460 100644 --- a/src/main/java/graphql/servlet/context/GraphQLContextBuilder.java +++ b/graphql-java-servlet/src/main/java/graphql/servlet/context/GraphQLServletContextBuilder.java @@ -1,19 +1,16 @@ package graphql.servlet.context; +import graphql.kickstart.execution.context.GraphQLContext; +import graphql.kickstart.execution.context.GraphQLContextBuilder; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.websocket.Session; import javax.websocket.server.HandshakeRequest; -public interface GraphQLContextBuilder { +public interface GraphQLServletContextBuilder extends GraphQLContextBuilder { GraphQLContext build(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse); GraphQLContext build(Session session, HandshakeRequest handshakeRequest); - /** - * Only used for MBean calls. - * @return the graphql context - */ - GraphQLContext build(); } diff --git a/src/main/java/graphql/servlet/context/GraphQLWebSocketContext.java b/graphql-java-servlet/src/main/java/graphql/servlet/context/GraphQLWebSocketContext.java similarity index 82% rename from src/main/java/graphql/servlet/context/GraphQLWebSocketContext.java rename to graphql-java-servlet/src/main/java/graphql/servlet/context/GraphQLWebSocketContext.java index fbe29dfb..4ab956a3 100644 --- a/src/main/java/graphql/servlet/context/GraphQLWebSocketContext.java +++ b/graphql-java-servlet/src/main/java/graphql/servlet/context/GraphQLWebSocketContext.java @@ -1,5 +1,6 @@ package graphql.servlet.context; +import graphql.kickstart.execution.context.GraphQLContext; import javax.websocket.Session; import javax.websocket.server.HandshakeRequest; import java.util.Optional; @@ -8,8 +9,6 @@ public interface GraphQLWebSocketContext extends GraphQLContext { Session getSession(); - Optional getConnectResult(); - HandshakeRequest getHandshakeRequest(); } diff --git a/graphql-java-servlet/src/main/java/graphql/servlet/core/DefaultGraphQLRootObjectBuilder.java b/graphql-java-servlet/src/main/java/graphql/servlet/core/DefaultGraphQLRootObjectBuilder.java new file mode 100644 index 00000000..b07a3b89 --- /dev/null +++ b/graphql-java-servlet/src/main/java/graphql/servlet/core/DefaultGraphQLRootObjectBuilder.java @@ -0,0 +1,23 @@ +package graphql.servlet.core; + +import graphql.kickstart.execution.StaticGraphQLRootObjectBuilder; +import javax.servlet.http.HttpServletRequest; +import javax.websocket.server.HandshakeRequest; + +public class DefaultGraphQLRootObjectBuilder extends StaticGraphQLRootObjectBuilder implements GraphQLServletRootObjectBuilder { + + public DefaultGraphQLRootObjectBuilder() { + super(new Object()); + } + + @Override + public Object build(HttpServletRequest req) { + return getRootObject(); + } + + @Override + public Object build(HandshakeRequest req) { + return getRootObject(); + } + +} diff --git a/src/main/java/graphql/servlet/core/GraphQLMBean.java b/graphql-java-servlet/src/main/java/graphql/servlet/core/GraphQLMBean.java similarity index 100% rename from src/main/java/graphql/servlet/core/GraphQLMBean.java rename to graphql-java-servlet/src/main/java/graphql/servlet/core/GraphQLMBean.java diff --git a/src/main/java/graphql/servlet/core/GraphQLServletListener.java b/graphql-java-servlet/src/main/java/graphql/servlet/core/GraphQLServletListener.java similarity index 100% rename from src/main/java/graphql/servlet/core/GraphQLServletListener.java rename to graphql-java-servlet/src/main/java/graphql/servlet/core/GraphQLServletListener.java diff --git a/graphql-java-servlet/src/main/java/graphql/servlet/core/GraphQLServletRootObjectBuilder.java b/graphql-java-servlet/src/main/java/graphql/servlet/core/GraphQLServletRootObjectBuilder.java new file mode 100644 index 00000000..5d350647 --- /dev/null +++ b/graphql-java-servlet/src/main/java/graphql/servlet/core/GraphQLServletRootObjectBuilder.java @@ -0,0 +1,13 @@ +package graphql.servlet.core; + +import graphql.kickstart.execution.GraphQLRootObjectBuilder; +import javax.servlet.http.HttpServletRequest; +import javax.websocket.server.HandshakeRequest; + +public interface GraphQLServletRootObjectBuilder extends GraphQLRootObjectBuilder { + + Object build(HttpServletRequest req); + + Object build(HandshakeRequest req); + +} diff --git a/src/main/java/graphql/servlet/core/internal/GraphQLThreadFactory.java b/graphql-java-servlet/src/main/java/graphql/servlet/core/internal/GraphQLThreadFactory.java similarity index 100% rename from src/main/java/graphql/servlet/core/internal/GraphQLThreadFactory.java rename to graphql-java-servlet/src/main/java/graphql/servlet/core/internal/GraphQLThreadFactory.java diff --git a/graphql-java-servlet/src/main/java/graphql/servlet/core/internal/VariableMapper.java b/graphql-java-servlet/src/main/java/graphql/servlet/core/internal/VariableMapper.java new file mode 100644 index 00000000..23764047 --- /dev/null +++ b/graphql-java-servlet/src/main/java/graphql/servlet/core/internal/VariableMapper.java @@ -0,0 +1,78 @@ +package graphql.servlet.core.internal; + +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; +import javax.servlet.http.Part; + +public class VariableMapper { + + private static final Pattern PERIOD = Pattern.compile("\\."); + + private static final Mapper> MAP_MAPPER = new Mapper>() { + @Override + public Object set(Map location, String target, Part value) { + return location.put(target, value); + } + + @Override + public Object recurse(Map location, String target) { + return location.get(target); + } + }; + private static final Mapper> LIST_MAPPER = new Mapper>() { + @Override + public Object set(List location, String target, Part value) { + return location.set(Integer.parseInt(target), value); + } + + @Override + public Object recurse(List location, String target) { + return location.get(Integer.parseInt(target)); + } + }; + + public static void mapVariable(String objectPath, Map variables, Part part) { + String[] segments = PERIOD.split(objectPath); + + if (segments.length < 2) { + throw new RuntimeException("object-path in map must have at least two segments"); + } else if (!"variables".equals(segments[0])) { + throw new RuntimeException("can only map into variables"); + } + + Object currentLocation = variables; + for (int i = 1; i < segments.length; i++) { + String segmentName = segments[i]; + Mapper mapper = determineMapper(currentLocation, objectPath, segmentName); + + if (i == segments.length - 1) { + if (null != mapper.set(currentLocation, segmentName, part)) { + throw new RuntimeException("expected null value when mapping " + objectPath); + } + } else { + currentLocation = mapper.recurse(currentLocation, segmentName); + if (null == currentLocation) { + throw new RuntimeException("found null intermediate value when trying to map " + objectPath); + } + } + } + } + + private static Mapper determineMapper(Object currentLocation, String objectPath, String segmentName) { + if (currentLocation instanceof Map) { + return MAP_MAPPER; + } else if (currentLocation instanceof List) { + return LIST_MAPPER; + } + + throw new RuntimeException("expected a map or list at " + segmentName + " when trying to map " + objectPath); + } + + interface Mapper { + + Object set(T location, String target, Part value); + + Object recurse(T location, String target); + } +} diff --git a/src/main/java/graphql/servlet/input/BatchInputPreProcessResult.java b/graphql-java-servlet/src/main/java/graphql/servlet/input/BatchInputPreProcessResult.java similarity index 91% rename from src/main/java/graphql/servlet/input/BatchInputPreProcessResult.java rename to graphql-java-servlet/src/main/java/graphql/servlet/input/BatchInputPreProcessResult.java index 8b86f7e8..d60bda3a 100644 --- a/src/main/java/graphql/servlet/input/BatchInputPreProcessResult.java +++ b/graphql-java-servlet/src/main/java/graphql/servlet/input/BatchInputPreProcessResult.java @@ -1,7 +1,9 @@ package graphql.servlet.input; +import graphql.kickstart.execution.input.GraphQLBatchedInvocationInput; + /** - * Wraps the result of pre processing a batch. Allows customization of the response code and message if the batch isn't to be executed. + * Wraps the result of pre processing a batch. Allows customization of the response code and message if the batch isn't to be executed. */ public class BatchInputPreProcessResult { diff --git a/src/main/java/graphql/servlet/input/BatchInputPreProcessor.java b/graphql-java-servlet/src/main/java/graphql/servlet/input/BatchInputPreProcessor.java similarity index 90% rename from src/main/java/graphql/servlet/input/BatchInputPreProcessor.java rename to graphql-java-servlet/src/main/java/graphql/servlet/input/BatchInputPreProcessor.java index 39d7c50a..cacfc4fe 100644 --- a/src/main/java/graphql/servlet/input/BatchInputPreProcessor.java +++ b/graphql-java-servlet/src/main/java/graphql/servlet/input/BatchInputPreProcessor.java @@ -1,5 +1,6 @@ package graphql.servlet.input; +import graphql.kickstart.execution.input.GraphQLBatchedInvocationInput; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; diff --git a/graphql-java-servlet/src/main/java/graphql/servlet/input/GraphQLInvocationInputFactory.java b/graphql-java-servlet/src/main/java/graphql/servlet/input/GraphQLInvocationInputFactory.java new file mode 100644 index 00000000..cb9dc80d --- /dev/null +++ b/graphql-java-servlet/src/main/java/graphql/servlet/input/GraphQLInvocationInputFactory.java @@ -0,0 +1,170 @@ +package graphql.servlet.input; + +import graphql.kickstart.execution.GraphQLRequest; +import graphql.kickstart.execution.config.GraphQLSchemaProvider; +import graphql.kickstart.execution.context.ContextSetting; +import graphql.kickstart.execution.input.GraphQLBatchedInvocationInput; +import graphql.kickstart.execution.input.GraphQLSingleInvocationInput; +import graphql.kickstart.execution.subscriptions.GraphQLSubscriptionInvocationInputFactory; +import graphql.kickstart.execution.subscriptions.SubscriptionSession; +import graphql.schema.GraphQLSchema; +import graphql.servlet.config.DefaultGraphQLSchemaServletProvider; +import graphql.servlet.config.GraphQLSchemaServletProvider; +import graphql.servlet.context.DefaultGraphQLServletContextBuilder; +import graphql.servlet.context.GraphQLServletContextBuilder; +import graphql.servlet.core.DefaultGraphQLRootObjectBuilder; +import graphql.servlet.core.GraphQLServletRootObjectBuilder; +import java.util.List; +import java.util.function.Supplier; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.websocket.Session; +import javax.websocket.server.HandshakeRequest; + +/** + * @author Andrew Potter + */ +public class GraphQLInvocationInputFactory implements GraphQLSubscriptionInvocationInputFactory { + + private final Supplier schemaProviderSupplier; + private final Supplier contextBuilderSupplier; + private final Supplier rootObjectBuilderSupplier; + + protected GraphQLInvocationInputFactory(Supplier schemaProviderSupplier, + Supplier contextBuilderSupplier, + Supplier rootObjectBuilderSupplier) { + this.schemaProviderSupplier = schemaProviderSupplier; + this.contextBuilderSupplier = contextBuilderSupplier; + this.rootObjectBuilderSupplier = rootObjectBuilderSupplier; + } + + public static Builder newBuilder(GraphQLSchema schema) { + return new Builder(new DefaultGraphQLSchemaServletProvider(schema)); + } + + public static Builder newBuilder(GraphQLSchemaServletProvider schemaProvider) { + return new Builder(schemaProvider); + } + + public static Builder newBuilder(Supplier schemaProviderSupplier) { + return new Builder(schemaProviderSupplier); + } + + public GraphQLSchemaProvider getSchemaProvider() { + return schemaProviderSupplier.get(); + } + + public GraphQLSingleInvocationInput create(GraphQLRequest graphQLRequest, HttpServletRequest request, + HttpServletResponse response) { + return create(graphQLRequest, request, response, false); + } + + public GraphQLBatchedInvocationInput create(ContextSetting contextSetting, List graphQLRequests, + HttpServletRequest request, + HttpServletResponse response) { + return create(contextSetting, graphQLRequests, request, response, false); + } + + public GraphQLSingleInvocationInput createReadOnly(GraphQLRequest graphQLRequest, HttpServletRequest request, + HttpServletResponse response) { + return create(graphQLRequest, request, response, true); + } + + public GraphQLBatchedInvocationInput createReadOnly(ContextSetting contextSetting, + List graphQLRequests, HttpServletRequest request, HttpServletResponse response) { + return create(contextSetting, graphQLRequests, request, response, true); + } + + public GraphQLSingleInvocationInput create(GraphQLRequest graphQLRequest) { + return new GraphQLSingleInvocationInput( + graphQLRequest, + schemaProviderSupplier.get().getSchema(), + contextBuilderSupplier.get().build(), + rootObjectBuilderSupplier.get().build() + ); + } + + private GraphQLSingleInvocationInput create(GraphQLRequest graphQLRequest, HttpServletRequest request, + HttpServletResponse response, + boolean readOnly) { + return new GraphQLSingleInvocationInput( + graphQLRequest, + readOnly ? schemaProviderSupplier.get().getReadOnlySchema(request) + : schemaProviderSupplier.get().getSchema(request), + contextBuilderSupplier.get().build(request, response), + rootObjectBuilderSupplier.get().build(request) + ); + } + + private GraphQLBatchedInvocationInput create(ContextSetting contextSetting, List graphQLRequests, + HttpServletRequest request, + HttpServletResponse response, boolean readOnly) { + return contextSetting.getBatch( + graphQLRequests, + readOnly ? schemaProviderSupplier.get().getReadOnlySchema(request) + : schemaProviderSupplier.get().getSchema(request), + () -> contextBuilderSupplier.get().build(request, response), + rootObjectBuilderSupplier.get().build(request) + ); + } + + @Override + public GraphQLSingleInvocationInput create(GraphQLRequest graphQLRequest, SubscriptionSession session) { + HandshakeRequest request = (HandshakeRequest) session.getUserProperties().get(HandshakeRequest.class.getName()); + return new GraphQLSingleInvocationInput( + graphQLRequest, + schemaProviderSupplier.get().getSchema(request), + contextBuilderSupplier.get().build((Session) session.unwrap(), request), + rootObjectBuilderSupplier.get().build(request) + ); + } + + public GraphQLBatchedInvocationInput create(ContextSetting contextSetting, List graphQLRequest, + Session session) { + HandshakeRequest request = (HandshakeRequest) session.getUserProperties().get(HandshakeRequest.class.getName()); + return contextSetting.getBatch( + graphQLRequest, + schemaProviderSupplier.get().getSchema(request), + () -> contextBuilderSupplier.get().build(session, request), + rootObjectBuilderSupplier.get().build(request) + ); + } + + public static class Builder { + + private final Supplier schemaProviderSupplier; + private Supplier contextBuilderSupplier = DefaultGraphQLServletContextBuilder::new; + private Supplier rootObjectBuilderSupplier = DefaultGraphQLRootObjectBuilder::new; + + public Builder(GraphQLSchemaServletProvider schemaProvider) { + this(() -> schemaProvider); + } + + public Builder(Supplier schemaProviderSupplier) { + this.schemaProviderSupplier = schemaProviderSupplier; + } + + public Builder withGraphQLContextBuilder(GraphQLServletContextBuilder contextBuilder) { + return withGraphQLContextBuilder(() -> contextBuilder); + } + + public Builder withGraphQLContextBuilder(Supplier contextBuilderSupplier) { + this.contextBuilderSupplier = contextBuilderSupplier; + return this; + } + + public Builder withGraphQLRootObjectBuilder(GraphQLServletRootObjectBuilder rootObjectBuilder) { + return withGraphQLRootObjectBuilder(() -> rootObjectBuilder); + } + + public Builder withGraphQLRootObjectBuilder(Supplier rootObjectBuilderSupplier) { + this.rootObjectBuilderSupplier = rootObjectBuilderSupplier; + return this; + } + + public GraphQLInvocationInputFactory build() { + return new GraphQLInvocationInputFactory(schemaProviderSupplier, contextBuilderSupplier, + rootObjectBuilderSupplier); + } + } +} diff --git a/src/main/java/graphql/servlet/input/NoOpBatchInputPreProcessor.java b/graphql-java-servlet/src/main/java/graphql/servlet/input/NoOpBatchInputPreProcessor.java similarity index 76% rename from src/main/java/graphql/servlet/input/NoOpBatchInputPreProcessor.java rename to graphql-java-servlet/src/main/java/graphql/servlet/input/NoOpBatchInputPreProcessor.java index 6b19d93b..e645d413 100644 --- a/src/main/java/graphql/servlet/input/NoOpBatchInputPreProcessor.java +++ b/graphql-java-servlet/src/main/java/graphql/servlet/input/NoOpBatchInputPreProcessor.java @@ -1,5 +1,8 @@ package graphql.servlet.input; +import graphql.kickstart.execution.input.GraphQLBatchedInvocationInput; +import graphql.servlet.input.BatchInputPreProcessResult; +import graphql.servlet.input.BatchInputPreProcessor; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; diff --git a/src/main/java/graphql/servlet/config/GraphQLCodeRegistryProvider.java b/graphql-java-servlet/src/main/java/graphql/servlet/osgi/GraphQLCodeRegistryProvider.java similarity index 68% rename from src/main/java/graphql/servlet/config/GraphQLCodeRegistryProvider.java rename to graphql-java-servlet/src/main/java/graphql/servlet/osgi/GraphQLCodeRegistryProvider.java index 51e46528..cbfab2e8 100644 --- a/src/main/java/graphql/servlet/config/GraphQLCodeRegistryProvider.java +++ b/graphql-java-servlet/src/main/java/graphql/servlet/osgi/GraphQLCodeRegistryProvider.java @@ -1,6 +1,7 @@ -package graphql.servlet.config; +package graphql.servlet.osgi; import graphql.schema.GraphQLCodeRegistry; +import graphql.servlet.osgi.GraphQLProvider; public interface GraphQLCodeRegistryProvider extends GraphQLProvider { GraphQLCodeRegistry getCodeRegistry(); diff --git a/src/main/java/graphql/servlet/config/GraphQLMutationProvider.java b/graphql-java-servlet/src/main/java/graphql/servlet/osgi/GraphQLMutationProvider.java similarity index 52% rename from src/main/java/graphql/servlet/config/GraphQLMutationProvider.java rename to graphql-java-servlet/src/main/java/graphql/servlet/osgi/GraphQLMutationProvider.java index 60e0ddff..93b1a2c6 100644 --- a/src/main/java/graphql/servlet/config/GraphQLMutationProvider.java +++ b/graphql-java-servlet/src/main/java/graphql/servlet/osgi/GraphQLMutationProvider.java @@ -1,10 +1,10 @@ -package graphql.servlet.config; +package graphql.servlet.osgi; import graphql.schema.GraphQLFieldDefinition; -import graphql.servlet.config.GraphQLProvider; - import java.util.Collection; public interface GraphQLMutationProvider extends GraphQLProvider { - Collection getMutations(); + + Collection getMutations(); + } diff --git a/src/main/java/graphql/servlet/config/GraphQLProvider.java b/graphql-java-servlet/src/main/java/graphql/servlet/osgi/GraphQLProvider.java similarity index 54% rename from src/main/java/graphql/servlet/config/GraphQLProvider.java rename to graphql-java-servlet/src/main/java/graphql/servlet/osgi/GraphQLProvider.java index 54535a9c..3168abb4 100644 --- a/src/main/java/graphql/servlet/config/GraphQLProvider.java +++ b/graphql-java-servlet/src/main/java/graphql/servlet/osgi/GraphQLProvider.java @@ -1,4 +1,4 @@ -package graphql.servlet.config; +package graphql.servlet.osgi; public interface GraphQLProvider { } diff --git a/graphql-java-servlet/src/main/java/graphql/servlet/osgi/GraphQLQueryProvider.java b/graphql-java-servlet/src/main/java/graphql/servlet/osgi/GraphQLQueryProvider.java new file mode 100644 index 00000000..646263b9 --- /dev/null +++ b/graphql-java-servlet/src/main/java/graphql/servlet/osgi/GraphQLQueryProvider.java @@ -0,0 +1,16 @@ +package graphql.servlet.osgi; + +import graphql.schema.GraphQLFieldDefinition; +import java.util.Collection; + +/** + * This interface is used by OSGi bundles to plugin new field into the root query type + */ +public interface GraphQLQueryProvider extends GraphQLProvider { + + /** + * @return a collection of field definitions that will be added to the root query type. + */ + Collection getQueries(); + +} diff --git a/src/main/java/graphql/servlet/config/GraphQLSubscriptionProvider.java b/graphql-java-servlet/src/main/java/graphql/servlet/osgi/GraphQLSubscriptionProvider.java similarity index 72% rename from src/main/java/graphql/servlet/config/GraphQLSubscriptionProvider.java rename to graphql-java-servlet/src/main/java/graphql/servlet/osgi/GraphQLSubscriptionProvider.java index 046dd123..75f398f4 100644 --- a/src/main/java/graphql/servlet/config/GraphQLSubscriptionProvider.java +++ b/graphql-java-servlet/src/main/java/graphql/servlet/osgi/GraphQLSubscriptionProvider.java @@ -1,7 +1,7 @@ -package graphql.servlet.config; +package graphql.servlet.osgi; import graphql.schema.GraphQLFieldDefinition; -import graphql.servlet.config.GraphQLProvider; +import graphql.servlet.osgi.GraphQLProvider; import java.util.Collection; diff --git a/src/main/java/graphql/servlet/config/GraphQLTypesProvider.java b/graphql-java-servlet/src/main/java/graphql/servlet/osgi/GraphQLTypesProvider.java similarity index 52% rename from src/main/java/graphql/servlet/config/GraphQLTypesProvider.java rename to graphql-java-servlet/src/main/java/graphql/servlet/osgi/GraphQLTypesProvider.java index 61ff7065..22f9006e 100644 --- a/src/main/java/graphql/servlet/config/GraphQLTypesProvider.java +++ b/graphql-java-servlet/src/main/java/graphql/servlet/osgi/GraphQLTypesProvider.java @@ -1,10 +1,9 @@ -package graphql.servlet.config; +package graphql.servlet.osgi; import graphql.schema.GraphQLType; -import graphql.servlet.config.GraphQLProvider; - import java.util.Collection; public interface GraphQLTypesProvider extends GraphQLProvider { - Collection getTypes(); + + Collection getTypes(); } diff --git a/graphql-java-servlet/src/main/java/graphql/servlet/subscriptions/FallbackSubscriptionConsumer.java b/graphql-java-servlet/src/main/java/graphql/servlet/subscriptions/FallbackSubscriptionConsumer.java new file mode 100644 index 00000000..e65dc209 --- /dev/null +++ b/graphql-java-servlet/src/main/java/graphql/servlet/subscriptions/FallbackSubscriptionConsumer.java @@ -0,0 +1,51 @@ +package graphql.servlet.subscriptions; + +import graphql.ExecutionResult; +import graphql.kickstart.execution.GraphQLInvoker; +import graphql.kickstart.execution.GraphQLRequest; +import graphql.kickstart.execution.input.GraphQLSingleInvocationInput; +import graphql.kickstart.execution.subscriptions.GraphQLSubscriptionInvocationInputFactory; +import graphql.kickstart.execution.subscriptions.GraphQLSubscriptionMapper; +import graphql.kickstart.execution.subscriptions.SubscriptionSession; +import java.util.Objects; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; +import lombok.RequiredArgsConstructor; + +/** + * @author Andrew Potter + */ +@RequiredArgsConstructor +public class FallbackSubscriptionConsumer implements Consumer { + + private final SubscriptionSession session; + private final GraphQLSubscriptionMapper mapper; + private final GraphQLSubscriptionInvocationInputFactory invocationInputFactory; + private final GraphQLInvoker graphQLInvoker; + + @Override + public void accept(String text) { + CompletableFuture executionResult = executeAsync(text, session); + executionResult.thenAccept(result -> handleSubscriptionStart(session, UUID.randomUUID().toString(), result)); + } + + private CompletableFuture executeAsync(Object payload, SubscriptionSession session) { + Objects.requireNonNull(payload, "Payload is required"); + GraphQLRequest graphQLRequest = mapper.readGraphQLRequest(payload); + + GraphQLSingleInvocationInput invocationInput = invocationInputFactory.create(graphQLRequest, session); + return graphQLInvoker.executeAsync(invocationInput); + } + + private void handleSubscriptionStart(SubscriptionSession session, String id, ExecutionResult executionResult) { + ExecutionResult sanitizedExecutionResult = mapper.sanitizeErrors(executionResult); + if (!mapper.areErrorsPresent(sanitizedExecutionResult)) { + session.subscribe(id, sanitizedExecutionResult.getData()); + } else { + Object payload = mapper.convertSanitizedExecutionResult(sanitizedExecutionResult); + session.sendDataMessage(id, payload); + } + } + +} diff --git a/graphql-java-servlet/src/main/java/graphql/servlet/subscriptions/FallbackSubscriptionProtocolFactory.java b/graphql-java-servlet/src/main/java/graphql/servlet/subscriptions/FallbackSubscriptionProtocolFactory.java new file mode 100644 index 00000000..471f8f8f --- /dev/null +++ b/graphql-java-servlet/src/main/java/graphql/servlet/subscriptions/FallbackSubscriptionProtocolFactory.java @@ -0,0 +1,40 @@ +package graphql.servlet.subscriptions; + +import graphql.kickstart.execution.GraphQLInvoker; +import graphql.kickstart.execution.subscriptions.GraphQLSubscriptionInvocationInputFactory; +import graphql.kickstart.execution.subscriptions.GraphQLSubscriptionMapper; +import graphql.kickstart.execution.subscriptions.SubscriptionProtocolFactory; +import graphql.kickstart.execution.subscriptions.SubscriptionSession; +import java.util.function.Consumer; +import javax.websocket.Session; + +/** + * @author Andrew Potter + */ +public class FallbackSubscriptionProtocolFactory extends SubscriptionProtocolFactory implements + WebSocketSubscriptionProtocolFactory { + + private final GraphQLSubscriptionMapper mapper; + private final GraphQLSubscriptionInvocationInputFactory invocationInputFactory; + private final GraphQLInvoker graphQLInvoker; + + public FallbackSubscriptionProtocolFactory( + GraphQLSubscriptionMapper mapper, + GraphQLSubscriptionInvocationInputFactory invocationInputFactory, + GraphQLInvoker graphQLInvoker) { + super(""); + this.mapper = mapper; + this.invocationInputFactory = invocationInputFactory; + this.graphQLInvoker = graphQLInvoker; + } + + @Override + public Consumer createConsumer(SubscriptionSession session) { + return new FallbackSubscriptionConsumer(session, mapper, invocationInputFactory, graphQLInvoker); + } + + @Override + public SubscriptionSession createSession(Session session) { + return new WebSocketSubscriptionSession(mapper, session); + } +} diff --git a/graphql-java-servlet/src/main/java/graphql/servlet/subscriptions/WebSocketSendSubscriber.java b/graphql-java-servlet/src/main/java/graphql/servlet/subscriptions/WebSocketSendSubscriber.java new file mode 100644 index 00000000..99774e47 --- /dev/null +++ b/graphql-java-servlet/src/main/java/graphql/servlet/subscriptions/WebSocketSendSubscriber.java @@ -0,0 +1,55 @@ +package graphql.servlet.subscriptions; + +import java.io.IOException; +import java.util.concurrent.atomic.AtomicReference; +import javax.websocket.Session; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; + +@Slf4j +@RequiredArgsConstructor +public class WebSocketSendSubscriber implements Subscriber { + + private final Session session; + private AtomicReference subscriptionRef = new AtomicReference<>(); + + @Override + public void onSubscribe(Subscription subscription) { + subscriptionRef.set(subscription); + subscriptionRef.get().request(1); + } + + @Override + public void onNext(String message) { + subscriptionRef.get().request(1); + if (session.isOpen()) { + try { + session.getBasicRemote().sendText(message); + } catch (IOException e) { + log.error("Cannot send message {}", message, e); + } + } + } + + @Override + public void onError(Throwable t) { + + } + + @Override + public void onComplete() { + subscriptionRef.get().request(1); + if (session.isOpen()) { + try { + log.debug("Closing session"); + session.close(); + } catch (IOException e) { + log.error("Cannot close session", e); + } + } + subscriptionRef.get().cancel(); + } + +} diff --git a/graphql-java-servlet/src/main/java/graphql/servlet/subscriptions/WebSocketSubscriptionProtocolFactory.java b/graphql-java-servlet/src/main/java/graphql/servlet/subscriptions/WebSocketSubscriptionProtocolFactory.java new file mode 100644 index 00000000..224757aa --- /dev/null +++ b/graphql-java-servlet/src/main/java/graphql/servlet/subscriptions/WebSocketSubscriptionProtocolFactory.java @@ -0,0 +1,13 @@ +package graphql.servlet.subscriptions; + +import graphql.kickstart.execution.subscriptions.SubscriptionSession; +import java.util.function.Consumer; +import javax.websocket.Session; + +public interface WebSocketSubscriptionProtocolFactory { + + Consumer createConsumer(SubscriptionSession session); + + SubscriptionSession createSession(Session session); + +} diff --git a/graphql-java-servlet/src/main/java/graphql/servlet/subscriptions/WebSocketSubscriptionSession.java b/graphql-java-servlet/src/main/java/graphql/servlet/subscriptions/WebSocketSubscriptionSession.java new file mode 100644 index 00000000..4f6eacfa --- /dev/null +++ b/graphql-java-servlet/src/main/java/graphql/servlet/subscriptions/WebSocketSubscriptionSession.java @@ -0,0 +1,34 @@ +package graphql.servlet.subscriptions; + +import graphql.kickstart.execution.subscriptions.DefaultSubscriptionSession; +import graphql.kickstart.execution.subscriptions.GraphQLSubscriptionMapper; +import java.util.Map; +import javax.websocket.Session; + +public class WebSocketSubscriptionSession extends DefaultSubscriptionSession { + + private final Session session; + + public WebSocketSubscriptionSession(GraphQLSubscriptionMapper mapper, Session session) { + super(mapper); + this.session = session; + } + + public boolean isOpen() { + return session.isOpen(); + } + + public Map getUserProperties() { + return session.getUserProperties(); + } + + public String getId() { + return session.getId(); + } + + @Override + public Session unwrap() { + return session; + } + +} diff --git a/src/test/groovy/graphql/servlet/AbstractGraphQLHttpServletSpec.groovy b/graphql-java-servlet/src/test/groovy/graphql/servlet/AbstractGraphQLHttpServletSpec.groovy similarity index 96% rename from src/test/groovy/graphql/servlet/AbstractGraphQLHttpServletSpec.groovy rename to graphql-java-servlet/src/test/groovy/graphql/servlet/AbstractGraphQLHttpServletSpec.groovy index 89e4c3a4..72e43aa3 100644 --- a/src/test/groovy/graphql/servlet/AbstractGraphQLHttpServletSpec.groovy +++ b/graphql-java-servlet/src/test/groovy/graphql/servlet/AbstractGraphQLHttpServletSpec.groovy @@ -3,17 +3,16 @@ package graphql.servlet import com.fasterxml.jackson.databind.ObjectMapper import graphql.Scalars import graphql.execution.ExecutionStepInfo +import graphql.execution.MergedField import graphql.execution.reactive.SingleSubscriberPublisher +import graphql.language.Field import graphql.schema.GraphQLNonNull import graphql.servlet.input.GraphQLInvocationInputFactory import org.springframework.mock.web.MockHttpServletRequest import org.springframework.mock.web.MockHttpServletResponse -import spock.lang.Ignore import spock.lang.Shared import spock.lang.Specification -import javax.servlet.ServletInputStream -import javax.servlet.http.HttpServletRequest import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicReference @@ -54,6 +53,7 @@ class AbstractGraphQLHttpServletSpec extends Specification { request = new MockHttpServletRequest() request.setAsyncSupported(true) request.asyncSupported = true + request.setMethod("GET") response = new MockHttpServletResponse() } @@ -108,13 +108,13 @@ class AbstractGraphQLHttpServletSpec extends Specification { then: response.getStatus() == STATUS_OK response.getContentType() == CONTENT_TYPE_JSON_UTF8 + response.getContentLength() == mapper.writeValueAsString(["data": ["echo": "test"]]).length() getResponseContent().data.echo == "test" } - @Ignore def "async query over HTTP GET starts async request"() { setup: - servlet = TestUtils.createDefaultServlet({ env -> env.arguments.arg },{ env -> env.arguments.arg }, { env -> + servlet = TestUtils.createDefaultServlet({ env -> env.arguments.arg }, { env -> env.arguments.arg }, { env -> AtomicReference> publisherRef = new AtomicReference<>(); publisherRef.set(new SingleSubscriberPublisher<>({ subscription -> publisherRef.get().offer(env.arguments.arg) @@ -283,7 +283,6 @@ class AbstractGraphQLHttpServletSpec extends Specification { getBatchedResponseContent()[1].data.echo == "test" } - @Ignore def "deferred query over HTTP GET"() { setup: request.addParameter('query', 'query { echo(arg:"test") @defer }') @@ -368,12 +367,11 @@ class AbstractGraphQLHttpServletSpec extends Specification { getBatchedResponseContent()[1].errors.size() == 1 } - @Ignore def "subscription query over HTTP GET with variables as string returns data"() { setup: request.addParameter('query', 'subscription Subscription($arg: String!) { echo(arg: $arg) }') request.addParameter('operationName', 'Subscription') - request.addParameter( 'variables', '{"arg": "test"}') + request.addParameter('variables', '{"arg": "test"}') request.setAsyncSupported(true) when: @@ -391,6 +389,7 @@ class AbstractGraphQLHttpServletSpec extends Specification { def "query over HTTP POST without part or body returns bad request"() { when: + request.setMethod("POST") servlet.doPost(request, response) then: @@ -402,6 +401,7 @@ class AbstractGraphQLHttpServletSpec extends Specification { request.setContent(mapper.writeValueAsBytes([ query: 'query { echo(arg:"test") }' ])) + request.setMethod("POST") when: servlet.doPost(request, response) @@ -414,7 +414,7 @@ class AbstractGraphQLHttpServletSpec extends Specification { def "async query over HTTP POST starts async request"() { setup: - servlet = TestUtils.createDefaultServlet({ env -> env.arguments.arg },{ env -> env.arguments.arg }, { env -> + servlet = TestUtils.createDefaultServlet({ env -> env.arguments.arg }, { env -> env.arguments.arg }, { env -> AtomicReference> publisherRef = new AtomicReference<>(); publisherRef.set(new SingleSubscriberPublisher<>({ subscription -> publisherRef.get().offer(env.arguments.arg) @@ -422,9 +422,9 @@ class AbstractGraphQLHttpServletSpec extends Specification { })) return publisherRef.get() }, true) - request.setContent(mapper.writeValueAsBytes([ - query: 'query { echo(arg:"test") }' - ])) + request.setContent(mapper.writeValueAsBytes([ + query: 'query { echo(arg:"test") }' + ])) when: servlet.doPost(request, response) @@ -437,6 +437,7 @@ class AbstractGraphQLHttpServletSpec extends Specification { setup: request.addHeader("Content-Type", "application/graphql") request.setContent('query { echo(arg:"test") }'.getBytes("UTF-8")) + request.setMethod("POST") when: servlet.doPost(request, response) @@ -453,6 +454,7 @@ class AbstractGraphQLHttpServletSpec extends Specification { query : 'query Echo($arg: String) { echo(arg:$arg) }', variables: '{"arg": "test"}' ])) + request.setMethod("POST") when: servlet.doPost(request, response) @@ -469,6 +471,7 @@ class AbstractGraphQLHttpServletSpec extends Specification { query : 'query one{ echoOne: echo(arg:"test-one") } query two{ echoTwo: echo(arg:"test-two") }', operationName: 'two' ])) + request.setMethod("POST") when: servlet.doPost(request, response) @@ -486,6 +489,7 @@ class AbstractGraphQLHttpServletSpec extends Specification { query : 'query echo{ echo: echo(arg:"test") }', operationName: '' ])) + request.setMethod("POST") when: servlet.doPost(request, response) @@ -502,6 +506,7 @@ class AbstractGraphQLHttpServletSpec extends Specification { query: 'query { echo(arg:"test") }', test : 'test' ])) + request.setMethod("POST") when: servlet.doPost(request, response) @@ -723,6 +728,7 @@ class AbstractGraphQLHttpServletSpec extends Specification { def "batched query over HTTP POST body returns data"() { setup: request.setContent('[{ "query": "query { echo(arg:\\"test\\") }" }, { "query": "query { echo(arg:\\"test\\") }" }]'.bytes) + request.setMethod("POST") when: servlet.doPost(request, response) @@ -730,6 +736,7 @@ class AbstractGraphQLHttpServletSpec extends Specification { then: response.getStatus() == STATUS_OK response.getContentType() == CONTENT_TYPE_JSON_UTF8 + response.getContentLength() == mapper.writeValueAsString([["data": ["echo": "test"]], ["data": ["echo": "test"]]]).length() getBatchedResponseContent()[0].data.echo == "test" getBatchedResponseContent()[1].data.echo == "test" } @@ -737,6 +744,7 @@ class AbstractGraphQLHttpServletSpec extends Specification { def "batched query over HTTP POST body with variables returns data"() { setup: request.setContent('[{ "query": "query { echo(arg:\\"test\\") }", "variables": { "arg": "test" } }, { "query": "query { echo(arg:\\"test\\") }", "variables": { "arg": "test" } }]'.bytes) + request.setMethod("POST") when: servlet.doPost(request, response) @@ -751,6 +759,7 @@ class AbstractGraphQLHttpServletSpec extends Specification { def "batched query over HTTP POST body with operationName returns data"() { setup: request.setContent('[{ "query": "query one{ echoOne: echo(arg:\\"test-one\\") } query two{ echoTwo: echo(arg:\\"test-two\\") }", "operationName": "one" }, { "query": "query one{ echoOne: echo(arg:\\"test-one\\") } query two{ echoTwo: echo(arg:\\"test-two\\") }", "operationName": "two" }]'.bytes) + request.setMethod("POST") when: servlet.doPost(request, response) @@ -767,6 +776,7 @@ class AbstractGraphQLHttpServletSpec extends Specification { def "batched query over HTTP POST body with empty non-null operationName returns data"() { setup: request.setContent('[{ "query": "query echo{ echo: echo(arg:\\"test\\") }", "operationName": "" }, { "query": "query echo{ echo: echo(arg:\\"test\\") }", "operationName": "" }]'.bytes) + request.setMethod("POST") when: servlet.doPost(request, response) @@ -781,6 +791,7 @@ class AbstractGraphQLHttpServletSpec extends Specification { def "batched query over HTTP POST body with unknown property 'test' returns data"() { setup: request.setContent('[{ "query": "query { echo(arg:\\"test\\") }", "test": "test" }, { "query": "query { echo(arg:\\"test\\") }", "test": "test" }]'.bytes) + request.setMethod("POST") when: servlet.doPost(request, response) @@ -997,6 +1008,7 @@ class AbstractGraphQLHttpServletSpec extends Specification { request.setContent(mapper.writeValueAsBytes([ query: 'mutation { echo(arg:"test") }' ])) + request.setMethod("POST") when: servlet.doPost(request, response) @@ -1010,6 +1022,7 @@ class AbstractGraphQLHttpServletSpec extends Specification { def "batched mutation over HTTP POST body returns data"() { setup: request.setContent('[{ "query": "mutation { echo(arg:\\"test\\") }" }, { "query": "mutation { echo(arg:\\"test\\") }" }]'.bytes) + request.setMethod("POST") when: servlet.doPost(request, response) @@ -1024,6 +1037,7 @@ class AbstractGraphQLHttpServletSpec extends Specification { def "batched mutation over HTTP POST body with unknown property 'test' returns data"() { setup: request.setContent('[{ "query": "mutation { echo(arg:\\"test\\") }", "test": "test" }, { "query": "mutation { echo(arg:\\"test\\") }", "test": "test" }]'.bytes) + request.setMethod("POST") when: servlet.doPost(request, response) @@ -1035,11 +1049,11 @@ class AbstractGraphQLHttpServletSpec extends Specification { getBatchedResponseContent()[1].data.echo == "test" } - @Ignore def "subscription query over HTTP POST with variables as string returns data"() { setup: request.setContent('{"query": "subscription Subscription($arg: String!) { echo(arg: $arg) }", "operationName": "Subscription", "variables": {"arg": "test"}}'.bytes) request.setAsyncSupported(true) + request.setMethod("POST") when: servlet.doPost(request, response) @@ -1054,11 +1068,11 @@ class AbstractGraphQLHttpServletSpec extends Specification { getSubscriptionResponseContent()[1].data.echo == "Second\n\ntest" } - @Ignore def "defer query over HTTP POST"() { setup: request.setContent('{"query": "subscription Subscription($arg: String!) { echo(arg: $arg) }", "operationName": "Subscription", "variables": {"arg": "test"}}'.bytes) request.setAsyncSupported(true) + request.setMethod("POST") when: servlet.doPost(request, response) @@ -1073,7 +1087,6 @@ class AbstractGraphQLHttpServletSpec extends Specification { getSubscriptionResponseContent()[1].data.echo == "Second\n\ntest" } - @Ignore def "deferred query that takes longer than initial results, should still be sent second"() { setup: servlet = TestUtils.createDefaultServlet({ env -> @@ -1093,6 +1106,7 @@ class AbstractGraphQLHttpServletSpec extends Specification { ''' ])) request.setAsyncSupported(true) + request.setMethod("POST") when: servlet.doPost(request, response) @@ -1214,33 +1228,13 @@ class AbstractGraphQLHttpServletSpec extends Specification { resp[1].errors != null } - @Ignore def "typeInfo is serialized correctly"() { - expect: - servlet.getConfiguration().getObjectMapper().getJacksonMapper().writeValueAsString(ExecutionStepInfo.newExecutionStepInfo().type(new GraphQLNonNull(Scalars.GraphQLString)).build()) != "{}" - } - - @Ignore - def "isBatchedQuery check uses buffer length as read limit"() { setup: - HttpServletRequest mockRequest = Mock() - ServletInputStream mockInputStream = Mock() - - mockInputStream.markSupported() >> true - mockRequest.getInputStream() >> mockInputStream - mockRequest.getMethod() >> "POST" - mockRequest.getParts() >> Collections.emptyList() + MergedField field = MergedField.newMergedField().addField(new Field("test")).build() + ExecutionStepInfo stepInfo = ExecutionStepInfo.newExecutionStepInfo().field(field).type(new GraphQLNonNull(Scalars.GraphQLString)).build() - when: - servlet.doPost(mockRequest, response) - - then: - 1 * mockInputStream.mark(128) - - then: - 1 * mockInputStream.read({ it.length == 128 }) >> -1 - - then: - 1 * mockInputStream.reset() + expect: + servlet.getConfiguration().getObjectMapper().getJacksonMapper().writeValueAsString(stepInfo) != "{}" } + } diff --git a/src/test/groovy/graphql/servlet/DataLoaderDispatchingSpec.groovy b/graphql-java-servlet/src/test/groovy/graphql/servlet/DataLoaderDispatchingSpec.groovy similarity index 91% rename from src/test/groovy/graphql/servlet/DataLoaderDispatchingSpec.groovy rename to graphql-java-servlet/src/test/groovy/graphql/servlet/DataLoaderDispatchingSpec.groovy index 423f3198..6278d428 100644 --- a/src/test/groovy/graphql/servlet/DataLoaderDispatchingSpec.groovy +++ b/graphql-java-servlet/src/test/groovy/graphql/servlet/DataLoaderDispatchingSpec.groovy @@ -8,11 +8,11 @@ import graphql.execution.instrumentation.SimpleInstrumentation import graphql.execution.instrumentation.dataloader.DataLoaderDispatcherInstrumentationOptions import graphql.schema.DataFetcher import graphql.schema.DataFetchingEnvironment -import graphql.servlet.context.DefaultGraphQLContext -import graphql.servlet.context.GraphQLContext -import graphql.servlet.context.GraphQLContextBuilder -import graphql.servlet.context.ContextSetting -import graphql.servlet.instrumentation.ConfigurableDispatchInstrumentation +import graphql.kickstart.execution.context.ContextSetting +import graphql.kickstart.execution.context.DefaultGraphQLContext +import graphql.kickstart.execution.context.GraphQLContext +import graphql.servlet.context.GraphQLServletContextBuilder +import graphql.kickstart.execution.instrumentation.ConfigurableDispatchInstrumentation import org.dataloader.BatchLoader import org.dataloader.DataLoader import org.dataloader.DataLoaderRegistry @@ -23,7 +23,6 @@ import spock.lang.Specification import javax.servlet.http.HttpServletRequest import javax.servlet.http.HttpServletResponse -import javax.servlet.http.Part import javax.websocket.Session import javax.websocket.server.HandshakeRequest import java.util.concurrent.CompletableFuture @@ -84,8 +83,8 @@ class DataLoaderDispatchingSpec extends Specification { } } - def contextBuilder () { - return new GraphQLContextBuilder() { + def contextBuilder() { + return new GraphQLServletContextBuilder() { @Override GraphQLContext build(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) { new DefaultGraphQLContext(registry(), null) @@ -123,8 +122,8 @@ class DataLoaderDispatchingSpec extends Specification { Instrumentation simpleInstrumentation = new SimpleInstrumentation() ChainedInstrumentation chainedInstrumentation = new ChainedInstrumentation(Collections.singletonList(simpleInstrumentation)) - def simpleSupplier = {simpleInstrumentation} - def chainedSupplier = {chainedInstrumentation} + def simpleSupplier = { simpleInstrumentation } + def chainedSupplier = { chainedInstrumentation } def "batched query with per query context does not batch loads together"() { setup: @@ -132,6 +131,7 @@ class DataLoaderDispatchingSpec extends Specification { request.addParameter('query', '[{ "query": "query { query(arg:\\"test\\") { echo(arg:\\"test\\") { echo(arg:\\"test\\") } }}" }, { "query": "query{query(arg:\\"test\\") { echo (arg:\\"test\\") { echo(arg:\\"test\\")} }}" },' + ' { "query": "query{queryTwo(arg:\\"test\\") { echo (arg:\\"test\\")}}" }, { "query": "query{queryTwo(arg:\\"test\\") { echo (arg:\\"test\\")}}" }]') resetCounters() + request.setMethod("GET") when: servlet.doGet(request, response) @@ -157,6 +157,7 @@ class DataLoaderDispatchingSpec extends Specification { request.addParameter('query', '[{ "query": "query { query(arg:\\"test\\") { echo(arg:\\"test\\") { echo(arg:\\"test\\") } }}" }, { "query": "query{query(arg:\\"test\\") { echo (arg:\\"test\\") { echo(arg:\\"test\\")} }}" },' + ' { "query": "query{queryTwo(arg:\\"test\\") { echo (arg:\\"test\\")}}" }, { "query": "query{queryTwo(arg:\\"test\\") { echo (arg:\\"test\\")}}" }]') resetCounters() + request.setMethod("GET") when: servlet.doGet(request, response) @@ -181,7 +182,7 @@ class DataLoaderDispatchingSpec extends Specification { return Collections.singletonList(instrumentation) } else { List instrumentations = new ArrayList<>() - for (Instrumentation current : ((ChainedInstrumentation)instrumentation).getInstrumentations()) { + for (Instrumentation current : ((ChainedInstrumentation) instrumentation).getInstrumentations()) { if (current instanceof ChainedInstrumentation) { instrumentations.addAll(unwrapChainedInstrumentations(current)) } else { @@ -225,10 +226,10 @@ class DataLoaderDispatchingSpec extends Specification { then: fromSimple.size() == 2 fromSimple.contains(simpleInstrumentation) - fromSimple.stream().anyMatch({inst -> inst instanceof ConfigurableDispatchInstrumentation}) + fromSimple.stream().anyMatch({ inst -> inst instanceof ConfigurableDispatchInstrumentation }) fromChained.size() == 2 fromChained.contains(simpleInstrumentation) - fromChained.stream().anyMatch({inst -> inst instanceof ConfigurableDispatchInstrumentation}) + fromChained.stream().anyMatch({ inst -> inst instanceof ConfigurableDispatchInstrumentation }) } def "PER_REQUEST_WITH_INSTRUMENTATION adds instrumentation"() { @@ -244,9 +245,9 @@ class DataLoaderDispatchingSpec extends Specification { then: fromSimple.size() == 2 fromSimple.contains(simpleInstrumentation) - fromSimple.stream().anyMatch({inst -> inst instanceof ConfigurableDispatchInstrumentation}) + fromSimple.stream().anyMatch({ inst -> inst instanceof ConfigurableDispatchInstrumentation }) fromChained.size() == 2 fromChained.contains(simpleInstrumentation) - fromChained.stream().anyMatch({inst -> inst instanceof ConfigurableDispatchInstrumentation}) + fromChained.stream().anyMatch({ inst -> inst instanceof ConfigurableDispatchInstrumentation }) } -} \ No newline at end of file +} diff --git a/src/test/groovy/graphql/servlet/OsgiGraphQLHttpServletSpec.groovy b/graphql-java-servlet/src/test/groovy/graphql/servlet/OsgiGraphQLHttpServletSpec.groovy similarity index 96% rename from src/test/groovy/graphql/servlet/OsgiGraphQLHttpServletSpec.groovy rename to graphql-java-servlet/src/test/groovy/graphql/servlet/OsgiGraphQLHttpServletSpec.groovy index 1b111ab2..ab32d145 100644 --- a/src/test/groovy/graphql/servlet/OsgiGraphQLHttpServletSpec.groovy +++ b/graphql-java-servlet/src/test/groovy/graphql/servlet/OsgiGraphQLHttpServletSpec.groovy @@ -7,10 +7,10 @@ import graphql.annotations.processor.GraphQLAnnotations import graphql.schema.GraphQLCodeRegistry import graphql.schema.GraphQLFieldDefinition import graphql.schema.GraphQLInterfaceType -import graphql.servlet.config.GraphQLCodeRegistryProvider -import graphql.servlet.config.GraphQLMutationProvider -import graphql.servlet.config.GraphQLQueryProvider -import graphql.servlet.config.GraphQLSubscriptionProvider +import graphql.servlet.osgi.GraphQLCodeRegistryProvider +import graphql.servlet.osgi.GraphQLMutationProvider +import graphql.servlet.osgi.GraphQLQueryProvider +import graphql.servlet.osgi.GraphQLSubscriptionProvider import spock.lang.Ignore import spock.lang.Specification diff --git a/graphql-java-servlet/src/test/groovy/graphql/servlet/TestBatchInputPreProcessor.java b/graphql-java-servlet/src/test/groovy/graphql/servlet/TestBatchInputPreProcessor.java new file mode 100644 index 00000000..66c9108f --- /dev/null +++ b/graphql-java-servlet/src/test/groovy/graphql/servlet/TestBatchInputPreProcessor.java @@ -0,0 +1,25 @@ +package graphql.servlet; + +import graphql.kickstart.execution.input.GraphQLBatchedInvocationInput; +import graphql.servlet.input.BatchInputPreProcessResult; +import graphql.servlet.input.BatchInputPreProcessor; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +public class TestBatchInputPreProcessor implements BatchInputPreProcessor { + + public static String BATCH_ERROR_MESSAGE = "Batch limit exceeded"; + + @Override + public BatchInputPreProcessResult preProcessBatch(GraphQLBatchedInvocationInput batchedInvocationInput, + HttpServletRequest request, + HttpServletResponse response) { + BatchInputPreProcessResult preProcessResult; + if (batchedInvocationInput.getExecutionInputs().size() > 2) { + preProcessResult = new BatchInputPreProcessResult(400, BATCH_ERROR_MESSAGE); + } else { + preProcessResult = new BatchInputPreProcessResult(batchedInvocationInput); + } + return preProcessResult; + } +} diff --git a/src/test/groovy/graphql/servlet/TestException.groovy b/graphql-java-servlet/src/test/groovy/graphql/servlet/TestException.groovy similarity index 100% rename from src/test/groovy/graphql/servlet/TestException.groovy rename to graphql-java-servlet/src/test/groovy/graphql/servlet/TestException.groovy diff --git a/src/test/groovy/graphql/servlet/TestGraphQLErrorException.groovy b/graphql-java-servlet/src/test/groovy/graphql/servlet/TestGraphQLErrorException.groovy similarity index 100% rename from src/test/groovy/graphql/servlet/TestGraphQLErrorException.groovy rename to graphql-java-servlet/src/test/groovy/graphql/servlet/TestGraphQLErrorException.groovy diff --git a/src/test/groovy/graphql/servlet/TestMultipartPart.groovy b/graphql-java-servlet/src/test/groovy/graphql/servlet/TestMultipartPart.groovy similarity index 100% rename from src/test/groovy/graphql/servlet/TestMultipartPart.groovy rename to graphql-java-servlet/src/test/groovy/graphql/servlet/TestMultipartPart.groovy diff --git a/src/test/groovy/graphql/servlet/TestUtils.groovy b/graphql-java-servlet/src/test/groovy/graphql/servlet/TestUtils.groovy similarity index 97% rename from src/test/groovy/graphql/servlet/TestUtils.groovy rename to graphql-java-servlet/src/test/groovy/graphql/servlet/TestUtils.groovy index ec79d188..71d7258b 100644 --- a/src/test/groovy/graphql/servlet/TestUtils.groovy +++ b/graphql-java-servlet/src/test/groovy/graphql/servlet/TestUtils.groovy @@ -10,13 +10,11 @@ import graphql.schema.idl.SchemaGenerator import graphql.schema.idl.SchemaParser import graphql.schema.idl.TypeRuntimeWiring import graphql.schema.idl.errors.SchemaProblem -import graphql.servlet.context.GraphQLContextBuilder -import graphql.servlet.config.GraphQLConfiguration -import graphql.servlet.core.ApolloScalars +import graphql.servlet.context.GraphQLServletContextBuilder +import graphql.servlet.apollo.ApolloScalars import graphql.servlet.input.BatchInputPreProcessor -import graphql.servlet.context.ContextSetting +import graphql.kickstart.execution.context.ContextSetting -import java.util.concurrent.CompletableFuture import java.util.concurrent.atomic.AtomicReference class TestUtils { @@ -51,7 +49,7 @@ class TestUtils { DataFetcher fieldDataFetcher = { env -> env.arguments.arg }, DataFetcher otherDataFetcher, boolean asyncServletModeEnabled = false, ContextSetting contextSetting, - GraphQLContextBuilder contextBuilder) { + GraphQLServletContextBuilder contextBuilder) { GraphQLSchema schema = createGraphQlSchemaWithTwoLevels(queryDataFetcher, fieldDataFetcher, otherDataFetcher) GraphQLHttpServlet servlet = GraphQLHttpServlet.with(GraphQLConfiguration .with(schema) diff --git a/settings.gradle b/settings.gradle index f96cebb6..df6580aa 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1,4 @@ rootProject.name = 'graphql-java-servlet' + +include ':graphql-java-kickstart' +include ':graphql-java-servlet' diff --git a/src/main/java/graphql/servlet/AbstractGraphQLHttpServlet.java b/src/main/java/graphql/servlet/AbstractGraphQLHttpServlet.java deleted file mode 100644 index f59fa38e..00000000 --- a/src/main/java/graphql/servlet/AbstractGraphQLHttpServlet.java +++ /dev/null @@ -1,596 +0,0 @@ -package graphql.servlet; - -import com.google.common.io.ByteStreams; -import com.google.common.io.CharStreams; -import graphql.ExecutionResult; -import graphql.GraphQL; -import graphql.execution.reactive.SingleSubscriberPublisher; -import graphql.introspection.IntrospectionQuery; -import graphql.schema.GraphQLFieldDefinition; -import graphql.servlet.config.GraphQLConfiguration; -import graphql.servlet.context.ContextSetting; -import graphql.servlet.core.GraphQLMBean; -import graphql.servlet.core.GraphQLObjectMapper; -import graphql.servlet.core.GraphQLQueryInvoker; -import graphql.servlet.core.GraphQLServletListener; -import graphql.servlet.core.internal.GraphQLRequest; -import graphql.servlet.core.internal.VariableMapper; -import graphql.servlet.input.*; -import org.reactivestreams.Publisher; -import org.reactivestreams.Subscriber; -import org.reactivestreams.Subscription; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.servlet.AsyncContext; -import javax.servlet.AsyncEvent; -import javax.servlet.AsyncListener; -import javax.servlet.Servlet; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import javax.servlet.http.Part; -import java.io.*; -import java.util.*; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.atomic.AtomicReference; -import java.util.function.BiConsumer; -import java.util.function.Consumer; -import java.util.function.Function; -import java.util.stream.Collectors; - -/** - * @author Andrew Potter - */ -public abstract class AbstractGraphQLHttpServlet extends HttpServlet implements Servlet, GraphQLMBean { - - private static final Logger log = LoggerFactory.getLogger(AbstractGraphQLHttpServlet.class); - - private static final String APPLICATION_JSON_UTF8 = "application/json;charset=UTF-8"; - private static final String APPLICATION_EVENT_STREAM_UTF8 = "text/event-stream;charset=UTF-8"; - private static final String APPLICATION_GRAPHQL = "application/graphql"; - private static final int STATUS_OK = 200; - private static final int STATUS_BAD_REQUEST = 400; - - private static final GraphQLRequest INTROSPECTION_REQUEST = new GraphQLRequest(IntrospectionQuery.INTROSPECTION_QUERY, new HashMap<>(), null); - private static final String[] MULTIPART_KEYS = new String[]{"operations", "graphql", "query"}; - - private GraphQLConfiguration configuration; - - /** - * @deprecated override {@link #getConfiguration()} instead - */ - @Deprecated - protected abstract GraphQLQueryInvoker getQueryInvoker(); - - /** - * @deprecated override {@link #getConfiguration()} instead - */ - @Deprecated - protected abstract GraphQLInvocationInputFactory getInvocationInputFactory(); - - /** - * @deprecated override {@link #getConfiguration()} instead - */ - @Deprecated - protected abstract GraphQLObjectMapper getGraphQLObjectMapper(); - - /** - * @deprecated override {@link #getConfiguration()} instead - */ - @Deprecated - protected abstract boolean isAsyncServletMode(); - - protected GraphQLConfiguration getConfiguration() { - return GraphQLConfiguration.with(getInvocationInputFactory()) - .with(getQueryInvoker()) - .with(getGraphQLObjectMapper()) - .with(isAsyncServletMode()) - .with(listeners) - .build(); - } - - /** - * @deprecated use {@link #getConfiguration()} instead - */ - @Deprecated - private final List listeners; - - private HttpRequestHandler getHandler; - private HttpRequestHandler postHandler; - - public AbstractGraphQLHttpServlet() { - this(null); - } - - public AbstractGraphQLHttpServlet(List listeners) { - this.listeners = listeners != null ? new ArrayList<>(listeners) : new ArrayList<>(); - } - - @Override - public void init() { - this.configuration = getConfiguration(); - - this.getHandler = (request, response) -> { - GraphQLInvocationInputFactory invocationInputFactory = configuration.getInvocationInputFactory(); - GraphQLObjectMapper graphQLObjectMapper = configuration.getObjectMapper(); - GraphQLQueryInvoker queryInvoker = configuration.getQueryInvoker(); - - String path = request.getPathInfo(); - if (path == null) { - path = request.getServletPath(); - } - if (path.contentEquals("/schema.json")) { - query(queryInvoker, graphQLObjectMapper, invocationInputFactory.create(INTROSPECTION_REQUEST, request, response), - request, response); - } else { - String query = request.getParameter("query"); - if (query != null) { - - if (isBatchedQuery(query)) { - List requests = graphQLObjectMapper.readBatchedGraphQLRequest(query); - GraphQLBatchedInvocationInput batchedInvocationInput = - invocationInputFactory.createReadOnly(configuration.getContextSetting(), requests, request, response); - queryBatched(queryInvoker, batchedInvocationInput, request, response, configuration); - } else { - final Map variables = new HashMap<>(); - if (request.getParameter("variables") != null) { - variables.putAll(graphQLObjectMapper.deserializeVariables(request.getParameter("variables"))); - } - - String operationName = request.getParameter("operationName"); - - query(queryInvoker, graphQLObjectMapper, - invocationInputFactory.createReadOnly(new GraphQLRequest(query, variables, operationName), request, response), - request, response); - } - } else { - response.setStatus(STATUS_BAD_REQUEST); - log.info("Bad GET request: path was not \"/schema.json\" or no query variable named \"query\" given"); - } - } - }; - - this.postHandler = (request, response) -> { - GraphQLInvocationInputFactory invocationInputFactory = configuration.getInvocationInputFactory(); - GraphQLObjectMapper graphQLObjectMapper = configuration.getObjectMapper(); - GraphQLQueryInvoker queryInvoker = configuration.getQueryInvoker(); - - try { - if (APPLICATION_GRAPHQL.equals(request.getContentType())) { - String query = CharStreams.toString(request.getReader()); - query(queryInvoker, graphQLObjectMapper, - invocationInputFactory.create(new GraphQLRequest(query, null, null), request, response), - request, response); - } else if (request.getContentType() != null && request.getContentType().startsWith("multipart/form-data") && !request.getParts().isEmpty()) { - final Map> fileItems = request.getParts() - .stream() - .collect(Collectors.groupingBy(Part::getName)); - - for (String key : MULTIPART_KEYS) { - // Check to see if there is a part under the key we seek - if (!fileItems.containsKey(key)) { - continue; - } - - final Optional queryItem = getFileItem(fileItems, key); - if (!queryItem.isPresent()) { - // If there is a part, but we don't see an item, then break and return BAD_REQUEST - break; - } - - InputStream inputStream = asMarkableInputStream(queryItem.get().getInputStream()); - - final Optional>> variablesMap = - getFileItem(fileItems, "map").map(graphQLObjectMapper::deserializeMultipartMap); - - if (isBatchedQuery(inputStream)) { - List graphQLRequests = - graphQLObjectMapper.readBatchedGraphQLRequest(inputStream); - variablesMap.ifPresent(map -> graphQLRequests.forEach(r -> mapMultipartVariables(r, map, fileItems))); - GraphQLBatchedInvocationInput batchedInvocationInput = invocationInputFactory.create(configuration.getContextSetting(), - graphQLRequests, request, response); - queryBatched(queryInvoker, batchedInvocationInput, request, response, configuration); - return; - } else { - GraphQLRequest graphQLRequest; - if ("query".equals(key)) { - graphQLRequest = buildRequestFromQuery(inputStream, graphQLObjectMapper, fileItems); - } else { - graphQLRequest = graphQLObjectMapper.readGraphQLRequest(inputStream); - } - - variablesMap.ifPresent(m -> mapMultipartVariables(graphQLRequest, m, fileItems)); - GraphQLSingleInvocationInput invocationInput = - invocationInputFactory.create(graphQLRequest, request, response); - query(queryInvoker, graphQLObjectMapper, invocationInput, request, response); - return; - } - } - - response.setStatus(STATUS_BAD_REQUEST); - log.info("Bad POST multipart request: no part named " + Arrays.toString(MULTIPART_KEYS)); - } else { - // this is not a multipart request - InputStream inputStream = asMarkableInputStream(request.getInputStream()); - - if (isBatchedQuery(inputStream)) { - List requests = graphQLObjectMapper.readBatchedGraphQLRequest(inputStream); - GraphQLBatchedInvocationInput batchedInvocationInput = - invocationInputFactory.create(configuration.getContextSetting(), requests, request, response); - queryBatched(queryInvoker, batchedInvocationInput, request, response, configuration); - } else { - query(queryInvoker, graphQLObjectMapper, invocationInputFactory.create(graphQLObjectMapper.readGraphQLRequest(inputStream), request, response), request, response); - } - } - } catch (Exception e) { - log.info("Bad POST request: parsing failed", e); - response.setStatus(STATUS_BAD_REQUEST); - } - }; - } - - private InputStream asMarkableInputStream(InputStream inputStream) { - if (!inputStream.markSupported()) { - return new BufferedInputStream(inputStream); - } - return inputStream; - } - - private GraphQLRequest buildRequestFromQuery(InputStream inputStream, - GraphQLObjectMapper graphQLObjectMapper, - Map> fileItems) throws IOException { - GraphQLRequest graphQLRequest; - String query = new String(ByteStreams.toByteArray(inputStream)); - - Map variables = null; - final Optional variablesItem = getFileItem(fileItems, "variables"); - if (variablesItem.isPresent()) { - variables = graphQLObjectMapper.deserializeVariables(new String(ByteStreams.toByteArray(variablesItem.get().getInputStream()))); - } - - String operationName = null; - final Optional operationNameItem = getFileItem(fileItems, "operationName"); - if (operationNameItem.isPresent()) { - operationName = new String(ByteStreams.toByteArray(operationNameItem.get().getInputStream())).trim(); - } - - graphQLRequest = new GraphQLRequest(query, variables, operationName); - return graphQLRequest; - } - - private void mapMultipartVariables(GraphQLRequest request, - Map> variablesMap, - Map> fileItems) { - Map variables = request.getVariables(); - - variablesMap.forEach((partName, objectPaths) -> { - Part part = getFileItem(fileItems, partName) - .orElseThrow(() -> new RuntimeException("unable to find part name " + - partName + - " as referenced in the variables map")); - - objectPaths.forEach(objectPath -> VariableMapper.mapVariable(objectPath, variables, part)); - }); - } - - public void addListener(GraphQLServletListener servletListener) { - if (configuration != null) { - configuration.add(servletListener); - } else { - listeners.add(servletListener); - } - } - - public void removeListener(GraphQLServletListener servletListener) { - if (configuration != null) { - configuration.remove(servletListener); - } else { - listeners.remove(servletListener); - } - } - - @Override - public String[] getQueries() { - return configuration.getInvocationInputFactory().getSchemaProvider().getSchema().getQueryType().getFieldDefinitions().stream().map(GraphQLFieldDefinition::getName).toArray(String[]::new); - } - - @Override - public String[] getMutations() { - return configuration.getInvocationInputFactory().getSchemaProvider().getSchema().getMutationType().getFieldDefinitions().stream().map(GraphQLFieldDefinition::getName).toArray(String[]::new); - } - - @Override - public String executeQuery(String query) { - try { - return configuration.getObjectMapper().serializeResultAsJson(configuration.getQueryInvoker().query(configuration.getInvocationInputFactory().create(new GraphQLRequest(query, new HashMap<>(), null)))); - } catch (Exception e) { - return e.getMessage(); - } - } - - private void doRequestAsync(HttpServletRequest request, HttpServletResponse response, HttpRequestHandler handler) { - if (configuration.isAsyncServletModeEnabled()) { - AsyncContext asyncContext = request.startAsync(request, response); - HttpServletRequest asyncRequest = (HttpServletRequest) asyncContext.getRequest(); - HttpServletResponse asyncResponse = (HttpServletResponse) asyncContext.getResponse(); - configuration.getAsyncExecutor().execute(() -> doRequest(asyncRequest, asyncResponse, handler, asyncContext)); - } else { - doRequest(request, response, handler, null); - } - } - - private void doRequest(HttpServletRequest request, HttpServletResponse response, HttpRequestHandler handler, AsyncContext asyncContext) { - - List requestCallbacks = runListeners(l -> l.onRequest(request, response)); - - try { - handler.handle(request, response); - runCallbacks(requestCallbacks, c -> c.onSuccess(request, response)); - } catch (Throwable t) { - response.setStatus(500); - log.error("Error executing GraphQL request!", t); - runCallbacks(requestCallbacks, c -> c.onError(request, response, t)); - } finally { - runCallbacks(requestCallbacks, c -> c.onFinally(request, response)); - if (asyncContext != null) { - asyncContext.complete(); - } - } - } - - @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) { - init(); - doRequestAsync(req, resp, getHandler); - } - - @Override - protected void doPost(HttpServletRequest req, HttpServletResponse resp) { - init(); - doRequestAsync(req, resp, postHandler); - } - - private Optional getFileItem(Map> fileItems, String name) { - return Optional.ofNullable(fileItems.get(name)).filter(list -> !list.isEmpty()).map(list -> list.get(0)); - } - - private void query(GraphQLQueryInvoker queryInvoker, GraphQLObjectMapper graphQLObjectMapper, GraphQLSingleInvocationInput invocationInput, - HttpServletRequest req, HttpServletResponse resp) throws IOException { - ExecutionResult result = queryInvoker.query(invocationInput); - - boolean isDeferred = Objects.nonNull(result.getExtensions()) && result.getExtensions().containsKey(GraphQL.DEFERRED_RESULTS); - - if (!(result.getData() instanceof Publisher || isDeferred)) { - resp.setContentType(APPLICATION_JSON_UTF8); - resp.setStatus(STATUS_OK); - graphQLObjectMapper.serializeResultAsJson(resp.getWriter(), result); - } else { - if (req == null) { - throw new IllegalStateException("Http servlet request can not be null"); - } - resp.setContentType(APPLICATION_EVENT_STREAM_UTF8); - resp.setStatus(STATUS_OK); - - boolean isInAsyncThread = req.isAsyncStarted(); - AsyncContext asyncContext = isInAsyncThread ? req.getAsyncContext() : req.startAsync(req, resp); - asyncContext.setTimeout(configuration.getSubscriptionTimeout()); - AtomicReference subscriptionRef = new AtomicReference<>(); - asyncContext.addListener(new SubscriptionAsyncListener(subscriptionRef)); - ExecutionResultSubscriber subscriber = new ExecutionResultSubscriber(subscriptionRef, asyncContext, graphQLObjectMapper); - List> publishers = new ArrayList<>(); - if (result.getData() instanceof Publisher) { - publishers.add(result.getData()); - } else { - publishers.add(new StaticDataPublisher<>(result)); - final Publisher deferredResultsPublisher = (Publisher) result.getExtensions().get(GraphQL.DEFERRED_RESULTS); - publishers.add(deferredResultsPublisher); - } - publishers.forEach(it -> it.subscribe(subscriber)); - - if (isInAsyncThread) { - // We need to delay the completion of async context until after the subscription has terminated, otherwise the AsyncContext is prematurely closed. - try { - subscriber.await(); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - } - } - } - - private void queryBatched(GraphQLQueryInvoker queryInvoker, GraphQLBatchedInvocationInput batchedInvocationInput, HttpServletRequest request, - HttpServletResponse response, GraphQLConfiguration configuration) throws IOException { - BatchInputPreProcessor batchInputPreProcessor = configuration.getBatchInputPreProcessor(); - ContextSetting contextSetting = configuration.getContextSetting(); - BatchInputPreProcessResult batchInputPreProcessResult = batchInputPreProcessor.preProcessBatch(batchedInvocationInput, request, response); - if (batchInputPreProcessResult.isExecutable()) { - List results = queryInvoker.query(batchInputPreProcessResult.getBatchedInvocationInput().getExecutionInputs(), - contextSetting); - response.setContentType(AbstractGraphQLHttpServlet.APPLICATION_JSON_UTF8); - response.setStatus(AbstractGraphQLHttpServlet.STATUS_OK); - Writer writer = response.getWriter(); - Iterator executionInputIterator = results.iterator(); - writer.write("["); - GraphQLObjectMapper graphQLObjectMapper = configuration.getObjectMapper(); - while (executionInputIterator.hasNext()) { - String result = graphQLObjectMapper.serializeResultAsJson(executionInputIterator.next()); - writer.write(result); - if (executionInputIterator.hasNext()) { - writer.write(","); - } - } - writer.write("]"); - } else { - response.sendError(batchInputPreProcessResult.getStatusCode(), batchInputPreProcessResult.getStatusMessage()); - } - } - - private List runListeners(Function action) { - return configuration.getListeners().stream() - .map(listener -> { - try { - return action.apply(listener); - } catch (Throwable t) { - log.error("Error running listener: {}", listener, t); - return null; - } - }) - .filter(Objects::nonNull) - .collect(Collectors.toList()); - } - - private void runCallbacks(List callbacks, Consumer action) { - callbacks.forEach(callback -> { - try { - action.accept(callback); - } catch (Throwable t) { - log.error("Error running callback: {}", callback, t); - } - }); - } - - private boolean isBatchedQuery(InputStream inputStream) throws IOException { - if (inputStream == null) { - return false; - } - - final int BUFFER_SIZE = 128; - ByteArrayOutputStream result = new ByteArrayOutputStream(); - byte[] buffer = new byte[BUFFER_SIZE]; - int length; - - inputStream.mark(BUFFER_SIZE); - while ((length = inputStream.read(buffer)) != -1) { - result.write(buffer, 0, length); - String chunk = result.toString(); - Boolean isArrayStart = isArrayStart(chunk); - if (isArrayStart != null) { - inputStream.reset(); - return isArrayStart; - } - } - - inputStream.reset(); - return false; - } - - private boolean isBatchedQuery(String query) { - if (query == null) { - return false; - } - - Boolean isArrayStart = isArrayStart(query); - return isArrayStart != null && isArrayStart; - } - - // return true if the first non whitespace character is the beginning of an array - private Boolean isArrayStart(String s) { - for (int i = 0; i < s.length(); i++) { - char ch = s.charAt(i); - if (!Character.isWhitespace(ch)) { - return ch == '['; - } - } - - return null; - } - - protected interface HttpRequestHandler extends BiConsumer { - @Override - default void accept(HttpServletRequest request, HttpServletResponse response) { - try { - handle(request, response); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - void handle(HttpServletRequest request, HttpServletResponse response) throws Exception; - } - - private static class SubscriptionAsyncListener implements AsyncListener { - private final AtomicReference subscriptionRef; - - public SubscriptionAsyncListener(AtomicReference subscriptionRef) { - this.subscriptionRef = subscriptionRef; - } - - @Override - public void onComplete(AsyncEvent event) { - subscriptionRef.get().cancel(); - } - - @Override - public void onTimeout(AsyncEvent event) { - subscriptionRef.get().cancel(); - } - - @Override - public void onError(AsyncEvent event) { - subscriptionRef.get().cancel(); - } - - @Override - public void onStartAsync(AsyncEvent event) { - } - } - - private static class ExecutionResultSubscriber implements Subscriber { - - private final AtomicReference subscriptionRef; - private final AsyncContext asyncContext; - private final GraphQLObjectMapper graphQLObjectMapper; - private final CountDownLatch completedLatch = new CountDownLatch(1); - - public ExecutionResultSubscriber(AtomicReference subscriptionRef, AsyncContext asyncContext, GraphQLObjectMapper graphQLObjectMapper) { - this.subscriptionRef = subscriptionRef; - this.asyncContext = asyncContext; - this.graphQLObjectMapper = graphQLObjectMapper; - } - - @Override - public void onSubscribe(Subscription subscription) { - subscriptionRef.set(subscription); - subscriptionRef.get().request(1); - } - - @Override - public void onNext(ExecutionResult executionResult) { - try { - Writer writer = asyncContext.getResponse().getWriter(); - writer.write("data: "); - graphQLObjectMapper.serializeResultAsJson(writer, executionResult); - writer.write("\n\n"); - writer.flush(); - subscriptionRef.get().request(1); - } catch (IOException ignored) { - } - } - - @Override - public void onError(Throwable t) { - asyncContext.complete(); - completedLatch.countDown(); - } - - @Override - public void onComplete() { - asyncContext.complete(); - completedLatch.countDown(); - } - - public void await() throws InterruptedException { - completedLatch.await(); - } - } - - private static class StaticDataPublisher extends SingleSubscriberPublisher implements Publisher { - StaticDataPublisher(T data) { - super(); - super.offer(data); - super.noMoreData(); - } - } - -} diff --git a/src/main/java/graphql/servlet/GraphQLWebsocketServlet.java b/src/main/java/graphql/servlet/GraphQLWebsocketServlet.java deleted file mode 100644 index ba358b31..00000000 --- a/src/main/java/graphql/servlet/GraphQLWebsocketServlet.java +++ /dev/null @@ -1,200 +0,0 @@ -package graphql.servlet; - -import graphql.servlet.core.GraphQLObjectMapper; -import graphql.servlet.core.GraphQLQueryInvoker; -import graphql.servlet.core.SubscriptionConnectionListener; -import graphql.servlet.input.GraphQLInvocationInputFactory; -import graphql.servlet.core.internal.*; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.websocket.*; -import javax.websocket.server.HandshakeRequest; -import javax.websocket.server.ServerEndpointConfig; -import java.io.EOFException; -import java.io.IOException; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -/** - * Must be used with {@link #modifyHandshake(ServerEndpointConfig, HandshakeRequest, HandshakeResponse)} - * - * @author Andrew Potter - */ -public class GraphQLWebsocketServlet extends Endpoint { - - private static final Logger log = LoggerFactory.getLogger(GraphQLWebsocketServlet.class); - - private static final String HANDSHAKE_REQUEST_KEY = HandshakeRequest.class.getName(); - private static final String PROTOCOL_HANDLER_REQUEST_KEY = SubscriptionProtocolHandler.class.getName(); - private static final CloseReason ERROR_CLOSE_REASON = new CloseReason(CloseReason.CloseCodes.UNEXPECTED_CONDITION, "Internal Server Error"); - private static final CloseReason SHUTDOWN_CLOSE_REASON = new CloseReason(CloseReason.CloseCodes.UNEXPECTED_CONDITION, "Server Shut Down"); - - private final List subscriptionProtocolFactories; - private final SubscriptionProtocolFactory fallbackSubscriptionProtocolFactory; - private final List allSubscriptionProtocols; - - private final Map sessionSubscriptionCache = new ConcurrentHashMap<>(); - private final SubscriptionHandlerInput subscriptionHandlerInput; - private final AtomicBoolean isShuttingDown = new AtomicBoolean(false); - private final AtomicBoolean isShutDown = new AtomicBoolean(false); - private final Object cacheLock = new Object(); - - public GraphQLWebsocketServlet(GraphQLQueryInvoker queryInvoker, GraphQLInvocationInputFactory invocationInputFactory, GraphQLObjectMapper graphQLObjectMapper) { - this(queryInvoker, invocationInputFactory, graphQLObjectMapper, null); - } - - public GraphQLWebsocketServlet(GraphQLQueryInvoker queryInvoker, GraphQLInvocationInputFactory invocationInputFactory, GraphQLObjectMapper graphQLObjectMapper, SubscriptionConnectionListener subscriptionConnectionListener) { - this.subscriptionHandlerInput = new SubscriptionHandlerInput(invocationInputFactory, queryInvoker, graphQLObjectMapper, subscriptionConnectionListener); - - subscriptionProtocolFactories = Collections.singletonList(new ApolloSubscriptionProtocolFactory(subscriptionHandlerInput)); - fallbackSubscriptionProtocolFactory = new FallbackSubscriptionProtocolFactory(subscriptionHandlerInput); - allSubscriptionProtocols = Stream.concat(subscriptionProtocolFactories.stream(), Stream.of(fallbackSubscriptionProtocolFactory)) - .map(SubscriptionProtocolFactory::getProtocol) - .collect(Collectors.toList()); - } - - @Override - public void onOpen(Session session, EndpointConfig endpointConfig) { - final WsSessionSubscriptions subscriptions = new WsSessionSubscriptions(); - final HandshakeRequest request = (HandshakeRequest) endpointConfig.getUserProperties().get(HANDSHAKE_REQUEST_KEY); - final SubscriptionProtocolHandler subscriptionProtocolHandler = (SubscriptionProtocolHandler) endpointConfig.getUserProperties().get(PROTOCOL_HANDLER_REQUEST_KEY); - - synchronized (cacheLock) { - if (isShuttingDown.get()) { - throw new IllegalStateException("Server is shutting down!"); - } - - sessionSubscriptionCache.put(session, subscriptions); - } - - log.debug("Session opened: {}, {}", session.getId(), endpointConfig); - - // This *cannot* be a lambda because of the way undertow checks the class... - session.addMessageHandler(new MessageHandler.Whole() { - @Override - public void onMessage(String text) { - try { - subscriptionProtocolHandler.onMessage(request, session, subscriptions, text); - } catch (Throwable t) { - log.error("Error executing websocket query for session: {}", session.getId(), t); - closeUnexpectedly(session, t); - } - } - }); - } - - @Override - public void onClose(Session session, CloseReason closeReason) { - log.debug("Session closed: {}, {}", session.getId(), closeReason); - WsSessionSubscriptions subscriptions; - synchronized (cacheLock) { - subscriptions = sessionSubscriptionCache.remove(session); - } - if (subscriptions != null) { - subscriptions.close(); - } - } - - @Override - public void onError(Session session, Throwable thr) { - if (thr instanceof EOFException) { - log.warn("Session {} was killed abruptly without calling onClose. Cleaning up session", session.getId()); - onClose(session, ERROR_CLOSE_REASON); - } else { - log.error("Error in websocket session: {}", session.getId(), thr); - closeUnexpectedly(session, thr); - } - } - - private void closeUnexpectedly(Session session, Throwable t) { - try { - session.close(ERROR_CLOSE_REASON); - } catch (IOException e) { - log.error("Error closing websocket session for session: {}", session.getId(), t); - } - } - - public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) { - sec.getUserProperties().put(HANDSHAKE_REQUEST_KEY, request); - - List protocol = request.getHeaders().get(HandshakeRequest.SEC_WEBSOCKET_PROTOCOL); - if (protocol == null) { - protocol = Collections.emptyList(); - } - - SubscriptionProtocolFactory subscriptionProtocolFactory = getSubscriptionProtocolFactory(protocol); - sec.getUserProperties().put(PROTOCOL_HANDLER_REQUEST_KEY, subscriptionProtocolFactory.createHandler()); - - if (request.getHeaders().get(HandshakeResponse.SEC_WEBSOCKET_ACCEPT) != null) { - response.getHeaders().put(HandshakeResponse.SEC_WEBSOCKET_ACCEPT, allSubscriptionProtocols); - } - if (!protocol.isEmpty()) { - response.getHeaders().put(HandshakeRequest.SEC_WEBSOCKET_PROTOCOL, Collections.singletonList(subscriptionProtocolFactory.getProtocol())); - } - } - - /** - * Stops accepting connections and closes all existing connections - */ - public void beginShutDown() { - synchronized (cacheLock) { - isShuttingDown.set(true); - Map copy = new HashMap<>(sessionSubscriptionCache); - - // Prevent comodification exception since #onClose() is called during session.close(), but we can't necessarily rely on that happening so we close subscriptions here anyway. - copy.forEach((session, wsSessionSubscriptions) -> { - wsSessionSubscriptions.close(); - try { - session.close(SHUTDOWN_CLOSE_REASON); - } catch (IOException e) { - log.error("Error closing websocket session!", e); - } - }); - - copy.clear(); - - if(!sessionSubscriptionCache.isEmpty()) { - log.error("GraphQLWebsocketServlet did not shut down cleanly!"); - sessionSubscriptionCache.clear(); - } - } - - isShutDown.set(true); - } - - /** - * @return true when shutdown is complete - */ - public boolean isShutDown() { - return isShutDown.get(); - } - - private SubscriptionProtocolFactory getSubscriptionProtocolFactory(List accept) { - for (String protocol : accept) { - for (SubscriptionProtocolFactory subscriptionProtocolFactory : subscriptionProtocolFactories) { - if (subscriptionProtocolFactory.getProtocol().equals(protocol)) { - return subscriptionProtocolFactory; - } - } - } - - return fallbackSubscriptionProtocolFactory; - } - - public int getSessionCount() { - return sessionSubscriptionCache.size(); - } - - public int getSubscriptionCount() { - return sessionSubscriptionCache.values().stream() - .mapToInt(WsSessionSubscriptions::getSubscriptionCount) - .sum(); - } -} diff --git a/src/main/java/graphql/servlet/OsgiGraphQLHttpServlet.java b/src/main/java/graphql/servlet/OsgiGraphQLHttpServlet.java deleted file mode 100644 index 80d37490..00000000 --- a/src/main/java/graphql/servlet/OsgiGraphQLHttpServlet.java +++ /dev/null @@ -1,372 +0,0 @@ -package graphql.servlet; - -import static graphql.schema.GraphQLObjectType.newObject; -import static graphql.schema.GraphQLSchema.newSchema; - -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; - -import graphql.servlet.config.DefaultExecutionStrategyProvider; -import graphql.servlet.context.DefaultGraphQLContextBuilder; -import graphql.servlet.core.DefaultGraphQLErrorHandler; -import graphql.servlet.core.DefaultGraphQLRootObjectBuilder; -import graphql.servlet.config.DefaultGraphQLSchemaProvider; -import graphql.servlet.config.ExecutionStrategyProvider; -import graphql.servlet.config.GraphQLCodeRegistryProvider; -import graphql.servlet.context.GraphQLContextBuilder; -import graphql.servlet.core.GraphQLErrorHandler; -import graphql.servlet.config.GraphQLMutationProvider; -import graphql.servlet.core.GraphQLObjectMapper; -import graphql.servlet.config.GraphQLProvider; -import graphql.servlet.core.GraphQLQueryInvoker; -import graphql.servlet.config.GraphQLQueryProvider; -import graphql.servlet.core.GraphQLRootObjectBuilder; -import graphql.servlet.config.GraphQLSchemaProvider; -import graphql.servlet.core.GraphQLServletListener; -import graphql.servlet.config.GraphQLSubscriptionProvider; -import graphql.servlet.config.GraphQLTypesProvider; -import graphql.servlet.config.InstrumentationProvider; -import graphql.servlet.instrumentation.NoOpInstrumentationProvider; -import graphql.servlet.input.GraphQLInvocationInputFactory; -import org.osgi.service.component.annotations.Activate; -import org.osgi.service.component.annotations.Component; -import org.osgi.service.component.annotations.Deactivate; -import org.osgi.service.component.annotations.Reference; -import org.osgi.service.component.annotations.ReferenceCardinality; -import org.osgi.service.component.annotations.ReferencePolicy; -import org.osgi.service.component.annotations.ReferencePolicyOption; - -import graphql.execution.preparsed.NoOpPreparsedDocumentProvider; -import graphql.execution.preparsed.PreparsedDocumentProvider; -import graphql.schema.GraphQLObjectType; -import graphql.schema.GraphQLType; -import graphql.schema.GraphQLCodeRegistry; - -@Component( - service={javax.servlet.http.HttpServlet.class,javax.servlet.Servlet.class}, - property = {"alias=/graphql", "jmx.objectname=graphql.servlet:type=graphql"} -) -public class OsgiGraphQLHttpServlet extends AbstractGraphQLHttpServlet { - - private final List queryProviders = new ArrayList<>(); - private final List mutationProviders = new ArrayList<>(); - private final List subscriptionProviders = new ArrayList<>(); - private final List typesProviders = new ArrayList<>(); - - private final GraphQLQueryInvoker queryInvoker; - private final GraphQLInvocationInputFactory invocationInputFactory; - private final GraphQLObjectMapper graphQLObjectMapper; - - private GraphQLContextBuilder contextBuilder = new DefaultGraphQLContextBuilder(); - private GraphQLRootObjectBuilder rootObjectBuilder = new DefaultGraphQLRootObjectBuilder(); - private ExecutionStrategyProvider executionStrategyProvider = new DefaultExecutionStrategyProvider(); - private InstrumentationProvider instrumentationProvider = new NoOpInstrumentationProvider(); - private GraphQLErrorHandler errorHandler = new DefaultGraphQLErrorHandler(); - private PreparsedDocumentProvider preparsedDocumentProvider = NoOpPreparsedDocumentProvider.INSTANCE; - private GraphQLCodeRegistryProvider codeRegistryProvider = () -> GraphQLCodeRegistry.newCodeRegistry().build(); - - private GraphQLSchemaProvider schemaProvider; - - private ScheduledExecutorService executor; - private ScheduledFuture updateFuture; - private int schemaUpdateDelay; - - @interface Config { - int schema_update_delay() default 0; - } - - @Activate - public void activate(Config config) { - this.schemaUpdateDelay = config.schema_update_delay(); - if (schemaUpdateDelay!=0) - executor = Executors.newSingleThreadScheduledExecutor(); - } - - @Deactivate - public void deactivate() { - if (executor!=null) executor.shutdown(); - } - - @Override - protected GraphQLQueryInvoker getQueryInvoker() { - return queryInvoker; - } - - @Override - protected GraphQLInvocationInputFactory getInvocationInputFactory() { - return invocationInputFactory; - } - - @Override - protected GraphQLObjectMapper getGraphQLObjectMapper() { - return graphQLObjectMapper; - } - - @Override - protected boolean isAsyncServletMode() { - return false; - } - - public OsgiGraphQLHttpServlet() { - updateSchema(); - - this.queryInvoker = GraphQLQueryInvoker.newBuilder() - .withPreparsedDocumentProvider(this::getPreparsedDocumentProvider) - .withInstrumentation(() -> this.getInstrumentationProvider().getInstrumentation()) - .withExecutionStrategyProvider(this::getExecutionStrategyProvider).build(); - - this.invocationInputFactory = GraphQLInvocationInputFactory.newBuilder(this::getSchemaProvider) - .withGraphQLContextBuilder(this::getContextBuilder) - .withGraphQLRootObjectBuilder(this::getRootObjectBuilder) - .build(); - - this.graphQLObjectMapper = GraphQLObjectMapper.newBuilder() - .withGraphQLErrorHandler(this::getErrorHandler) - .build(); - } - - protected void updateSchema() { - if (schemaUpdateDelay==0) { - doUpdateSchema(); - } - else { - if (updateFuture!=null) - updateFuture.cancel(true); - - updateFuture = executor.schedule(new Runnable() { - @Override - public void run() { - doUpdateSchema(); - } - }, schemaUpdateDelay, TimeUnit.MILLISECONDS); - } - } - - private void doUpdateSchema() { - final GraphQLObjectType.Builder queryTypeBuilder = newObject().name("Query").description("Root query type"); - - for (GraphQLQueryProvider provider : queryProviders) { - if (provider.getQueries() != null && !provider.getQueries().isEmpty()) { - provider.getQueries().forEach(queryTypeBuilder::field); - } - } - - final Set types = new HashSet<>(); - for (GraphQLTypesProvider typesProvider : typesProviders) { - types.addAll(typesProvider.getTypes()); - } - - GraphQLObjectType mutationType = null; - - if (!mutationProviders.isEmpty()) { - final GraphQLObjectType.Builder mutationTypeBuilder = newObject().name("Mutation").description("Root mutation type"); - - for (GraphQLMutationProvider provider : mutationProviders) { - provider.getMutations().forEach(mutationTypeBuilder::field); - } - - if (!mutationTypeBuilder.build().getFieldDefinitions().isEmpty()) { - mutationType = mutationTypeBuilder.build(); - } - } - - GraphQLObjectType subscriptionType = null; - - if (!subscriptionProviders.isEmpty()) { - final GraphQLObjectType.Builder subscriptionTypeBuilder = newObject().name("Subscription").description("Root subscription type"); - - for (GraphQLSubscriptionProvider provider : subscriptionProviders) { - provider.getSubscriptions().forEach(subscriptionTypeBuilder::field); - } - - if (!subscriptionTypeBuilder.build().getFieldDefinitions().isEmpty()) { - subscriptionType = subscriptionTypeBuilder.build(); - } - } - - this.schemaProvider = new DefaultGraphQLSchemaProvider(newSchema().query(queryTypeBuilder.build()) - .mutation(mutationType) - .subscription(subscriptionType) - .additionalTypes(types) - .codeRegistry(codeRegistryProvider.getCodeRegistry()) - .build()); - } - - @Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC) - public void bindProvider(GraphQLProvider provider) { - if (provider instanceof GraphQLQueryProvider) { - queryProviders.add((GraphQLQueryProvider) provider); - } - if (provider instanceof GraphQLMutationProvider) { - mutationProviders.add((GraphQLMutationProvider) provider); - } - if (provider instanceof GraphQLSubscriptionProvider) { - subscriptionProviders.add((GraphQLSubscriptionProvider) provider); - } - if (provider instanceof GraphQLTypesProvider) { - typesProviders.add((GraphQLTypesProvider) provider); - } - if (provider instanceof GraphQLCodeRegistryProvider) { - codeRegistryProvider = (GraphQLCodeRegistryProvider) provider; - } - updateSchema(); - } - public void unbindProvider(GraphQLProvider provider) { - if (provider instanceof GraphQLQueryProvider) { - queryProviders.remove(provider); - } - if (provider instanceof GraphQLMutationProvider) { - mutationProviders.remove(provider); - } - if (provider instanceof GraphQLSubscriptionProvider) { - subscriptionProviders.remove(provider); - } - if (provider instanceof GraphQLTypesProvider) { - typesProviders.remove(provider); - } - if (provider instanceof GraphQLCodeRegistryProvider) { - codeRegistryProvider = () -> GraphQLCodeRegistry.newCodeRegistry().build(); - } - updateSchema(); - } - - @Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC) - public void bindQueryProvider(GraphQLQueryProvider queryProvider) { - queryProviders.add(queryProvider); - updateSchema(); - } - public void unbindQueryProvider(GraphQLQueryProvider queryProvider) { - queryProviders.remove(queryProvider); - updateSchema(); - } - - @Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC) - public void bindMutationProvider(GraphQLMutationProvider mutationProvider) { - mutationProviders.add(mutationProvider); - updateSchema(); - } - public void unbindMutationProvider(GraphQLMutationProvider mutationProvider) { - mutationProviders.remove(mutationProvider); - updateSchema(); - } - - @Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC) - public void bindSubscriptionProvider(GraphQLSubscriptionProvider subscriptionProvider) { - subscriptionProviders.add(subscriptionProvider); - updateSchema(); - } - public void unbindSubscriptionProvider(GraphQLSubscriptionProvider subscriptionProvider) { - subscriptionProviders.remove(subscriptionProvider); - updateSchema(); - } - - @Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC) - public void bindTypesProvider(GraphQLTypesProvider typesProvider) { - typesProviders.add(typesProvider); - updateSchema(); - } - public void unbindTypesProvider(GraphQLTypesProvider typesProvider) { - typesProviders.remove(typesProvider); - updateSchema(); - } - - @Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC) - public void bindServletListener(GraphQLServletListener listener) { - this.addListener(listener); - } - public void unbindServletListener(GraphQLServletListener listener) { - this.removeListener(listener); - } - - @Reference(cardinality = ReferenceCardinality.OPTIONAL, policy = ReferencePolicy.DYNAMIC) - public void setContextProvider(GraphQLContextBuilder contextBuilder) { - this.contextBuilder = contextBuilder; - } - public void unsetContextProvider(GraphQLContextBuilder contextBuilder) { - this.contextBuilder = new DefaultGraphQLContextBuilder(); - } - - @Reference(cardinality = ReferenceCardinality.OPTIONAL, policy = ReferencePolicy.DYNAMIC) - public void setRootObjectBuilder(GraphQLRootObjectBuilder rootObjectBuilder) { - this.rootObjectBuilder = rootObjectBuilder; - } - public void unsetRootObjectBuilder(GraphQLRootObjectBuilder rootObjectBuilder) { - this.rootObjectBuilder = new DefaultGraphQLRootObjectBuilder(); - } - - @Reference(cardinality = ReferenceCardinality.OPTIONAL, policy= ReferencePolicy.DYNAMIC, policyOption = ReferencePolicyOption.GREEDY) - public void setExecutionStrategyProvider(ExecutionStrategyProvider provider) { - executionStrategyProvider = provider; - } - public void unsetExecutionStrategyProvider(ExecutionStrategyProvider provider) { - executionStrategyProvider = new DefaultExecutionStrategyProvider(); - } - - @Reference(cardinality = ReferenceCardinality.OPTIONAL, policy= ReferencePolicy.DYNAMIC, policyOption = ReferencePolicyOption.GREEDY) - public void setInstrumentationProvider(InstrumentationProvider provider) { - instrumentationProvider = provider; - } - public void unsetInstrumentationProvider(InstrumentationProvider provider) { - instrumentationProvider = new NoOpInstrumentationProvider(); - } - - @Reference(cardinality = ReferenceCardinality.OPTIONAL, policy= ReferencePolicy.DYNAMIC, policyOption = ReferencePolicyOption.GREEDY) - public void setErrorHandler(GraphQLErrorHandler errorHandler) { - this.errorHandler = errorHandler; - } - public void unsetErrorHandler(GraphQLErrorHandler errorHandler) { - this.errorHandler = new DefaultGraphQLErrorHandler(); - } - - @Reference(cardinality = ReferenceCardinality.OPTIONAL, policy= ReferencePolicy.DYNAMIC, policyOption = ReferencePolicyOption.GREEDY) - public void setPreparsedDocumentProvider(PreparsedDocumentProvider preparsedDocumentProvider) { - this.preparsedDocumentProvider = preparsedDocumentProvider; - } - public void unsetPreparsedDocumentProvider(PreparsedDocumentProvider preparsedDocumentProvider) { - this.preparsedDocumentProvider = NoOpPreparsedDocumentProvider.INSTANCE; - } - - @Reference(cardinality = ReferenceCardinality.OPTIONAL, policy= ReferencePolicy.DYNAMIC, policyOption = ReferencePolicyOption.GREEDY) - public void bindCodeRegistryProvider(GraphQLCodeRegistryProvider graphQLCodeRegistryProvider) { - this.codeRegistryProvider = graphQLCodeRegistryProvider; - updateSchema(); - } - public void unbindCodeRegistryProvider(GraphQLCodeRegistryProvider graphQLCodeRegistryProvider) { - this.codeRegistryProvider = () -> GraphQLCodeRegistry.newCodeRegistry().build(); - updateSchema(); - } - - public GraphQLContextBuilder getContextBuilder() { - return contextBuilder; - } - - public GraphQLRootObjectBuilder getRootObjectBuilder() { - return rootObjectBuilder; - } - - public ExecutionStrategyProvider getExecutionStrategyProvider() { - return executionStrategyProvider; - } - - public InstrumentationProvider getInstrumentationProvider() { - return instrumentationProvider; - } - - public GraphQLErrorHandler getErrorHandler() { - return errorHandler; - } - - public PreparsedDocumentProvider getPreparsedDocumentProvider() { - return preparsedDocumentProvider; - } - - public GraphQLSchemaProvider getSchemaProvider() { - return schemaProvider; - } -} diff --git a/src/main/java/graphql/servlet/SimpleGraphQLHttpServlet.java b/src/main/java/graphql/servlet/SimpleGraphQLHttpServlet.java deleted file mode 100644 index bfa7d5f7..00000000 --- a/src/main/java/graphql/servlet/SimpleGraphQLHttpServlet.java +++ /dev/null @@ -1,144 +0,0 @@ -package graphql.servlet; - -import graphql.schema.GraphQLSchema; -import graphql.servlet.core.GraphQLObjectMapper; -import graphql.servlet.core.GraphQLQueryInvoker; -import graphql.servlet.config.GraphQLSchemaProvider; -import graphql.servlet.core.GraphQLServletListener; -import graphql.servlet.config.GraphQLConfiguration; -import graphql.servlet.input.GraphQLInvocationInputFactory; - -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; - -/** - * @author Andrew Potter - */ -public class SimpleGraphQLHttpServlet extends AbstractGraphQLHttpServlet { - - private GraphQLConfiguration configuration; - - public SimpleGraphQLHttpServlet() { - } - - /** - * @deprecated use {@link GraphQLHttpServlet} instead - */ - @Deprecated - public SimpleGraphQLHttpServlet(GraphQLInvocationInputFactory invocationInputFactory, GraphQLQueryInvoker queryInvoker, GraphQLObjectMapper graphQLObjectMapper, List listeners, boolean asyncServletMode) { - super(listeners); - this.configuration = GraphQLConfiguration.with(invocationInputFactory) - .with(queryInvoker) - .with(graphQLObjectMapper) - .with(listeners != null ? listeners : new ArrayList<>()) - .with(asyncServletMode) - .build(); - } - - /** - * @deprecated use {@link GraphQLHttpServlet} instead - */ - @Deprecated - public SimpleGraphQLHttpServlet(GraphQLInvocationInputFactory invocationInputFactory, GraphQLQueryInvoker queryInvoker, GraphQLObjectMapper graphQLObjectMapper, List listeners, boolean asyncServletMode, long subscriptionTimeout) { - super(listeners); - this.configuration = GraphQLConfiguration.with(invocationInputFactory) - .with(queryInvoker) - .with(graphQLObjectMapper) - .with(listeners != null ? listeners : new ArrayList<>()) - .with(asyncServletMode) - .with(subscriptionTimeout) - .build(); - } - - private SimpleGraphQLHttpServlet(GraphQLConfiguration configuration) { - this.configuration = Objects.requireNonNull(configuration, "configuration is required"); - } - - @Override - protected GraphQLConfiguration getConfiguration() { - return configuration; - } - - @Override - protected GraphQLQueryInvoker getQueryInvoker() { - return configuration.getQueryInvoker(); - } - - @Override - protected GraphQLInvocationInputFactory getInvocationInputFactory() { - return configuration.getInvocationInputFactory(); - } - - @Override - protected GraphQLObjectMapper getGraphQLObjectMapper() { - return configuration.getObjectMapper(); - } - - @Override - protected boolean isAsyncServletMode() { - return configuration.isAsyncServletModeEnabled(); - } - - public static Builder newBuilder(GraphQLSchema schema) { - return new Builder(GraphQLInvocationInputFactory.newBuilder(schema).build()); - } - - public static Builder newBuilder(GraphQLSchemaProvider schemaProvider) { - return new Builder(GraphQLInvocationInputFactory.newBuilder(schemaProvider).build()); - } - - public static Builder newBuilder(GraphQLInvocationInputFactory invocationInputFactory) { - return new Builder(invocationInputFactory); - } - - public static class Builder { - private final GraphQLInvocationInputFactory invocationInputFactory; - private GraphQLQueryInvoker queryInvoker = GraphQLQueryInvoker.newBuilder().build(); - private GraphQLObjectMapper graphQLObjectMapper = GraphQLObjectMapper.newBuilder().build(); - private List listeners; - private boolean asyncServletMode; - private long subscriptionTimeout; - - Builder(GraphQLInvocationInputFactory invocationInputFactory) { - this.invocationInputFactory = invocationInputFactory; - } - - public Builder withQueryInvoker(GraphQLQueryInvoker queryInvoker) { - this.queryInvoker = queryInvoker; - return this; - } - - public Builder withObjectMapper(GraphQLObjectMapper objectMapper) { - this.graphQLObjectMapper = objectMapper; - return this; - } - - public Builder withAsyncServletMode(boolean asyncServletMode) { - this.asyncServletMode = asyncServletMode; - return this; - } - - public Builder withListeners(List listeners) { - this.listeners = listeners; - return this; - } - - public Builder withSubscriptionTimeout(long subscriptionTimeout) { - this.subscriptionTimeout = subscriptionTimeout; - return this; - } - - @Deprecated - public SimpleGraphQLHttpServlet build() { - GraphQLConfiguration configuration = GraphQLConfiguration.with(invocationInputFactory) - .with(queryInvoker) - .with(graphQLObjectMapper) - .with(listeners != null ? listeners : new ArrayList<>()) - .with(asyncServletMode) - .with(subscriptionTimeout) - .build(); - return new SimpleGraphQLHttpServlet(configuration); - } - } -} diff --git a/src/main/java/graphql/servlet/config/DefaultExecutionStrategyProvider.java b/src/main/java/graphql/servlet/config/DefaultExecutionStrategyProvider.java deleted file mode 100644 index d70a8d97..00000000 --- a/src/main/java/graphql/servlet/config/DefaultExecutionStrategyProvider.java +++ /dev/null @@ -1,50 +0,0 @@ -package graphql.servlet.config; - -import graphql.execution.AsyncExecutionStrategy; -import graphql.execution.ExecutionStrategy; -import graphql.execution.SubscriptionExecutionStrategy; -import graphql.servlet.config.ExecutionStrategyProvider; - -/** - * @author Andrew Potter - */ -public class DefaultExecutionStrategyProvider implements ExecutionStrategyProvider { - - private final ExecutionStrategy queryExecutionStrategy; - private final ExecutionStrategy mutationExecutionStrategy; - private final ExecutionStrategy subscriptionExecutionStrategy; - - public DefaultExecutionStrategyProvider() { - this(null); - } - - public DefaultExecutionStrategyProvider(ExecutionStrategy executionStrategy) { - this(executionStrategy, null, null); - } - - public DefaultExecutionStrategyProvider(ExecutionStrategy queryExecutionStrategy, ExecutionStrategy mutationExecutionStrategy, ExecutionStrategy subscriptionExecutionStrategy) { - this.queryExecutionStrategy = defaultIfNull(queryExecutionStrategy, new AsyncExecutionStrategy()); - this.mutationExecutionStrategy = defaultIfNull(mutationExecutionStrategy, this.queryExecutionStrategy); - this.subscriptionExecutionStrategy = defaultIfNull(subscriptionExecutionStrategy, new SubscriptionExecutionStrategy()); - } - - private ExecutionStrategy defaultIfNull(ExecutionStrategy executionStrategy, ExecutionStrategy defaultStrategy) { - return executionStrategy != null ? executionStrategy : defaultStrategy; - } - - @Override - public ExecutionStrategy getQueryExecutionStrategy() { - return queryExecutionStrategy; - } - - @Override - public ExecutionStrategy getMutationExecutionStrategy() { - return mutationExecutionStrategy; - } - - @Override - public ExecutionStrategy getSubscriptionExecutionStrategy() { - return subscriptionExecutionStrategy; - } - -} diff --git a/src/main/java/graphql/servlet/config/DefaultGraphQLSchemaProvider.java b/src/main/java/graphql/servlet/config/DefaultGraphQLSchemaProvider.java deleted file mode 100644 index bcdb7c4c..00000000 --- a/src/main/java/graphql/servlet/config/DefaultGraphQLSchemaProvider.java +++ /dev/null @@ -1,46 +0,0 @@ -package graphql.servlet.config; - -import graphql.schema.GraphQLSchema; -import graphql.servlet.config.GraphQLSchemaProvider; - -import javax.servlet.http.HttpServletRequest; -import javax.websocket.server.HandshakeRequest; - -/** - * @author Andrew Potter - */ -public class DefaultGraphQLSchemaProvider implements GraphQLSchemaProvider { - - private final GraphQLSchema schema; - private final GraphQLSchema readOnlySchema; - - public DefaultGraphQLSchemaProvider(GraphQLSchema schema) { - this(schema, GraphQLSchemaProvider.copyReadOnly(schema)); - } - - public DefaultGraphQLSchemaProvider(GraphQLSchema schema, GraphQLSchema readOnlySchema) { - this.schema = schema; - this.readOnlySchema = readOnlySchema; - } - - - @Override - public GraphQLSchema getSchema(HttpServletRequest request) { - return getSchema(); - } - - @Override - public GraphQLSchema getSchema(HandshakeRequest request) { - return getSchema(); - } - - @Override - public GraphQLSchema getSchema() { - return schema; - } - - @Override - public GraphQLSchema getReadOnlySchema(HttpServletRequest request) { - return readOnlySchema; - } -} diff --git a/src/main/java/graphql/servlet/config/GraphQLConfiguration.java b/src/main/java/graphql/servlet/config/GraphQLConfiguration.java deleted file mode 100644 index 938230fc..00000000 --- a/src/main/java/graphql/servlet/config/GraphQLConfiguration.java +++ /dev/null @@ -1,210 +0,0 @@ -package graphql.servlet.config; - -import graphql.schema.GraphQLSchema; -import graphql.servlet.context.GraphQLContextBuilder; -import graphql.servlet.core.GraphQLObjectMapper; -import graphql.servlet.core.GraphQLQueryInvoker; -import graphql.servlet.core.GraphQLRootObjectBuilder; -import graphql.servlet.core.GraphQLServletListener; -import graphql.servlet.context.ContextSetting; -import graphql.servlet.input.BatchInputPreProcessor; -import graphql.servlet.input.GraphQLInvocationInputFactory; -import graphql.servlet.core.internal.GraphQLThreadFactory; -import graphql.servlet.input.NoOpBatchInputPreProcessor; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.Executor; -import java.util.concurrent.Executors; -import java.util.function.Supplier; - -public class GraphQLConfiguration { - - private final GraphQLInvocationInputFactory invocationInputFactory; - private final Supplier batchInputPreProcessor; - private final GraphQLQueryInvoker queryInvoker; - private final GraphQLObjectMapper objectMapper; - private final List listeners; - private final boolean asyncServletModeEnabled; - private final Executor asyncExecutor; - private final long subscriptionTimeout; - private final ContextSetting contextSetting; - - public static GraphQLConfiguration.Builder with(GraphQLSchema schema) { - return with(new DefaultGraphQLSchemaProvider(schema)); - } - - public static GraphQLConfiguration.Builder with(GraphQLSchemaProvider schemaProvider) { - return new Builder(GraphQLInvocationInputFactory.newBuilder(schemaProvider)); - } - - public static GraphQLConfiguration.Builder with(GraphQLInvocationInputFactory invocationInputFactory) { - return new Builder(invocationInputFactory); - } - - private GraphQLConfiguration(GraphQLInvocationInputFactory invocationInputFactory, GraphQLQueryInvoker queryInvoker, - GraphQLObjectMapper objectMapper, List listeners, boolean asyncServletModeEnabled, - Executor asyncExecutor, long subscriptionTimeout, ContextSetting contextSetting, - Supplier batchInputPreProcessor) { - this.invocationInputFactory = invocationInputFactory; - this.queryInvoker = queryInvoker; - this.objectMapper = objectMapper; - this.listeners = listeners; - this.asyncServletModeEnabled = asyncServletModeEnabled; - this.asyncExecutor = asyncExecutor; - this.subscriptionTimeout = subscriptionTimeout; - this.contextSetting = contextSetting; - this.batchInputPreProcessor = batchInputPreProcessor; - } - - public GraphQLInvocationInputFactory getInvocationInputFactory() { - return invocationInputFactory; - } - - public GraphQLQueryInvoker getQueryInvoker() { - return queryInvoker; - } - - public GraphQLObjectMapper getObjectMapper() { - return objectMapper; - } - - public List getListeners() { - return new ArrayList<>(listeners); - } - - public boolean isAsyncServletModeEnabled() { - return asyncServletModeEnabled; - } - - public Executor getAsyncExecutor() { - return asyncExecutor; - } - - public void add(GraphQLServletListener listener) { - listeners.add(listener); - } - - public boolean remove(GraphQLServletListener listener) { - return listeners.remove(listener); - } - - public long getSubscriptionTimeout() { - return subscriptionTimeout; - } - - public ContextSetting getContextSetting() { - return contextSetting; - } - - public BatchInputPreProcessor getBatchInputPreProcessor() { - return batchInputPreProcessor.get(); - } - - public static class Builder { - - private GraphQLInvocationInputFactory.Builder invocationInputFactoryBuilder; - private GraphQLInvocationInputFactory invocationInputFactory; - private GraphQLQueryInvoker queryInvoker = GraphQLQueryInvoker.newBuilder().build(); - private GraphQLObjectMapper objectMapper = GraphQLObjectMapper.newBuilder().build(); - private List listeners = new ArrayList<>(); - private boolean asyncServletModeEnabled = false; - private Executor asyncExecutor = Executors.newCachedThreadPool(new GraphQLThreadFactory()); - private long subscriptionTimeout = 0; - private ContextSetting contextSetting = ContextSetting.PER_QUERY_WITH_INSTRUMENTATION; - private Supplier batchInputPreProcessorSupplier = () -> new NoOpBatchInputPreProcessor(); - - private Builder(GraphQLInvocationInputFactory.Builder invocationInputFactoryBuilder) { - this.invocationInputFactoryBuilder = invocationInputFactoryBuilder; - } - - private Builder(GraphQLInvocationInputFactory invocationInputFactory) { - this.invocationInputFactory = invocationInputFactory; - } - - public Builder with(GraphQLQueryInvoker queryInvoker) { - if (queryInvoker != null) { - this.queryInvoker = queryInvoker; - } - return this; - } - - public Builder with(GraphQLObjectMapper objectMapper) { - if (objectMapper != null) { - this.objectMapper = objectMapper; - } - return this; - } - - public Builder with(List listeners) { - if (listeners != null) { - this.listeners = listeners; - } - return this; - } - - public Builder with(boolean asyncServletModeEnabled) { - this.asyncServletModeEnabled = asyncServletModeEnabled; - return this; - } - - public Builder with(Executor asyncExecutor) { - if (asyncExecutor != null) { - this.asyncExecutor = asyncExecutor; - } - return this; - } - - public Builder with(GraphQLContextBuilder contextBuilder) { - this.invocationInputFactoryBuilder.withGraphQLContextBuilder(contextBuilder); - return this; - } - - public Builder with(GraphQLRootObjectBuilder rootObjectBuilder) { - this.invocationInputFactoryBuilder.withGraphQLRootObjectBuilder(rootObjectBuilder); - return this; - } - - public Builder with(long subscriptionTimeout) { - this.subscriptionTimeout = subscriptionTimeout; - return this; - } - - public Builder with(ContextSetting contextSetting) { - if (contextSetting != null) { - this.contextSetting = contextSetting; - } - return this; - } - - public Builder with(BatchInputPreProcessor batchInputPreProcessor) { - if (batchInputPreProcessor != null) { - this.batchInputPreProcessorSupplier = () -> batchInputPreProcessor; - } - return this; - } - - public Builder with(Supplier batchInputPreProcessor) { - if (batchInputPreProcessor != null) { - this.batchInputPreProcessorSupplier = batchInputPreProcessor; - } - return this; - } - - public GraphQLConfiguration build() { - return new GraphQLConfiguration( - this.invocationInputFactory != null ? this.invocationInputFactory : invocationInputFactoryBuilder.build(), - queryInvoker, - objectMapper, - listeners, - asyncServletModeEnabled, - asyncExecutor, - subscriptionTimeout, - contextSetting, - batchInputPreProcessorSupplier - ); - } - - } - -} diff --git a/src/main/java/graphql/servlet/config/GraphQLQueryProvider.java b/src/main/java/graphql/servlet/config/GraphQLQueryProvider.java deleted file mode 100644 index c46952eb..00000000 --- a/src/main/java/graphql/servlet/config/GraphQLQueryProvider.java +++ /dev/null @@ -1,18 +0,0 @@ -package graphql.servlet.config; - -import graphql.schema.GraphQLFieldDefinition; -import graphql.servlet.config.GraphQLProvider; - -import java.util.Collection; - -/** - * This interface is used by OSGi bundles to plugin new field into the root query type - */ -public interface GraphQLQueryProvider extends GraphQLProvider { - - /** - * @return a collection of field definitions that will be added to the root query type. - */ - Collection getQueries(); - -} diff --git a/src/main/java/graphql/servlet/config/GraphQLSchemaProvider.java b/src/main/java/graphql/servlet/config/GraphQLSchemaProvider.java deleted file mode 100644 index 386a0285..00000000 --- a/src/main/java/graphql/servlet/config/GraphQLSchemaProvider.java +++ /dev/null @@ -1,40 +0,0 @@ -package graphql.servlet.config; - -import graphql.schema.GraphQLObjectType; -import graphql.schema.GraphQLSchema; - -import javax.servlet.http.HttpServletRequest; -import javax.websocket.server.HandshakeRequest; - -public interface GraphQLSchemaProvider { - - static GraphQLSchema copyReadOnly(GraphQLSchema schema) { - return GraphQLSchema.newSchema(schema) - .mutation((GraphQLObjectType) null) - .build(); - } - - /** - * @param request the http request - * @return a schema based on the request (auth, etc). - */ - GraphQLSchema getSchema(HttpServletRequest request); - - /** - * @param request the http request used to create a websocket - * @return a schema based on the request (auth, etc). - */ - GraphQLSchema getSchema(HandshakeRequest request); - - /** - * @return a schema for handling mbean calls. - */ - GraphQLSchema getSchema(); - - /** - * @param request the http request - * @return a read-only schema based on the request (auth, etc). Should return the same schema (query/subscription-only version) as {@link #getSchema(HttpServletRequest)} for a given request. - */ - GraphQLSchema getReadOnlySchema(HttpServletRequest request); - -} diff --git a/src/main/java/graphql/servlet/context/ContextSetting.java b/src/main/java/graphql/servlet/context/ContextSetting.java deleted file mode 100644 index b8648cfc..00000000 --- a/src/main/java/graphql/servlet/context/ContextSetting.java +++ /dev/null @@ -1,94 +0,0 @@ -package graphql.servlet.context; - -import graphql.ExecutionInput; -import graphql.execution.ExecutionId; -import graphql.execution.instrumentation.ChainedInstrumentation; -import graphql.execution.instrumentation.Instrumentation; -import graphql.execution.instrumentation.dataloader.DataLoaderDispatcherInstrumentationOptions; -import graphql.schema.GraphQLSchema; -import graphql.servlet.input.GraphQLBatchedInvocationInput; -import graphql.servlet.input.PerQueryBatchedInvocationInput; -import graphql.servlet.input.PerRequestBatchedInvocationInput; -import graphql.servlet.instrumentation.ConfigurableDispatchInstrumentation; -import graphql.servlet.instrumentation.FieldLevelTrackingApproach; -import graphql.servlet.instrumentation.RequestLevelTrackingApproach; -import graphql.servlet.core.internal.GraphQLRequest; -import org.dataloader.DataLoaderRegistry; - -import java.util.Arrays; -import java.util.List; - -import java.util.function.Supplier; -import java.util.stream.Collectors; - -/** - * An enum representing possible context settings. These are modeled after Apollo's link settings. - */ -public enum ContextSetting { - - /** - * A context object, and therefor dataloader registry and subject, should be shared between all GraphQL executions in a http request. - */ - PER_REQUEST_WITH_INSTRUMENTATION, - PER_REQUEST_WITHOUT_INSTRUMENTATION, - /** - * Each GraphQL execution should always have its own context. - */ - PER_QUERY_WITH_INSTRUMENTATION, - PER_QUERY_WITHOUT_INSTRUMENTATION; - - /** - * Creates a set of inputs with the correct context based on the setting. - * @param requests the GraphQL requests to execute. - * @param schema the GraphQL schema to execute the requests against. - * @param contextSupplier method that returns the context to use for each execution or for the request as a whole. - * @param root the root object to use for each execution. - * @return a configured batch input. - */ - public GraphQLBatchedInvocationInput getBatch(List requests, GraphQLSchema schema, Supplier contextSupplier, Object root) { - switch (this) { - case PER_QUERY_WITH_INSTRUMENTATION: - //Intentional fallthrough - case PER_QUERY_WITHOUT_INSTRUMENTATION: - return new PerQueryBatchedInvocationInput(requests, schema, contextSupplier, root); - case PER_REQUEST_WITHOUT_INSTRUMENTATION: - //Intentional fallthrough - case PER_REQUEST_WITH_INSTRUMENTATION: - return new PerRequestBatchedInvocationInput(requests, schema, contextSupplier, root); - default: - throw new RuntimeException("Unconfigured context setting type"); - } - } - - /** - * Augments the provided instrumentation supplier to also supply the correct dispatching instrumentation. - * @param instrumentation the instrumentation supplier to augment - * @param executionInputs the inputs that will be dispatched by the instrumentation - * @param options the DataLoader dispatching instrumentation options that will be used. - * @return augmented instrumentation supplier. - */ - public Supplier configureInstrumentationForContext(Supplier instrumentation, List executionInputs, - DataLoaderDispatcherInstrumentationOptions options) { - ConfigurableDispatchInstrumentation dispatchInstrumentation; - switch (this) { - case PER_REQUEST_WITH_INSTRUMENTATION: - DataLoaderRegistry registry = executionInputs.stream().findFirst().map(ExecutionInput::getDataLoaderRegistry) - .orElseThrow(IllegalArgumentException::new); - List executionIds = executionInputs.stream().map(ExecutionInput::getExecutionId).collect(Collectors.toList()); - RequestLevelTrackingApproach requestTrackingApproach = new RequestLevelTrackingApproach(executionIds, registry); - dispatchInstrumentation = new ConfigurableDispatchInstrumentation(options, - (dataLoaderRegistry -> requestTrackingApproach)); - break; - case PER_QUERY_WITH_INSTRUMENTATION: - dispatchInstrumentation = new ConfigurableDispatchInstrumentation(options, FieldLevelTrackingApproach::new); - break; - case PER_REQUEST_WITHOUT_INSTRUMENTATION: - //Intentional fallthrough - case PER_QUERY_WITHOUT_INSTRUMENTATION: - return instrumentation::get; - default: - throw new RuntimeException("Unconfigured context setting type"); - } - return () -> new ChainedInstrumentation(Arrays.asList(dispatchInstrumentation, instrumentation.get())); - } -} diff --git a/src/main/java/graphql/servlet/context/DefaultGraphQLContext.java b/src/main/java/graphql/servlet/context/DefaultGraphQLContext.java deleted file mode 100644 index 98bc11f6..00000000 --- a/src/main/java/graphql/servlet/context/DefaultGraphQLContext.java +++ /dev/null @@ -1,35 +0,0 @@ -package graphql.servlet.context; - -import org.dataloader.DataLoaderRegistry; - -import javax.security.auth.Subject; -import java.util.Optional; - -/** - * An object for the DefaultGraphQLContextBuilder to return. Can be extended to include more context. - */ -public class DefaultGraphQLContext implements GraphQLContext{ - - private final Subject subject; - - private final DataLoaderRegistry dataLoaderRegistry; - - public DefaultGraphQLContext(DataLoaderRegistry dataLoaderRegistry, Subject subject) { - this.dataLoaderRegistry = dataLoaderRegistry; - this.subject = subject; - } - - public DefaultGraphQLContext() { - this(null, null); - } - - @Override - public Optional getSubject() { - return Optional.ofNullable(subject); - } - - @Override - public Optional getDataLoaderRegistry() { - return Optional.ofNullable(dataLoaderRegistry); - } -} diff --git a/src/main/java/graphql/servlet/context/DefaultGraphQLContextBuilder.java b/src/main/java/graphql/servlet/context/DefaultGraphQLContextBuilder.java deleted file mode 100644 index 48950ca6..00000000 --- a/src/main/java/graphql/servlet/context/DefaultGraphQLContextBuilder.java +++ /dev/null @@ -1,30 +0,0 @@ -package graphql.servlet.context; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import javax.servlet.http.Part; -import javax.websocket.Session; -import javax.websocket.server.HandshakeRequest; -import java.util.List; -import java.util.Map; - -/** - * Returns an empty context. - */ -public class DefaultGraphQLContextBuilder implements GraphQLContextBuilder { - - @Override - public GraphQLContext build(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) { - return DefaultGraphQLServletContext.createServletContext().with(httpServletRequest).with(httpServletResponse).build(); - } - - @Override - public GraphQLContext build(Session session, HandshakeRequest handshakeRequest) { - return DefaultGraphQLWebSocketContext.createWebSocketContext().with(session).with(handshakeRequest).build(); - } - - @Override - public GraphQLContext build() { - return new DefaultGraphQLContext(); - } -} diff --git a/src/main/java/graphql/servlet/context/DefaultGraphQLWebSocketContext.java b/src/main/java/graphql/servlet/context/DefaultGraphQLWebSocketContext.java deleted file mode 100644 index fe1d4d60..00000000 --- a/src/main/java/graphql/servlet/context/DefaultGraphQLWebSocketContext.java +++ /dev/null @@ -1,82 +0,0 @@ -package graphql.servlet.context; - -import graphql.servlet.core.ApolloSubscriptionConnectionListener; -import org.dataloader.DataLoaderRegistry; - -import javax.security.auth.Subject; -import javax.websocket.Session; -import javax.websocket.server.HandshakeRequest; -import java.util.Optional; - -public class DefaultGraphQLWebSocketContext extends DefaultGraphQLContext implements GraphQLWebSocketContext { - - private final Session session; - private final HandshakeRequest handshakeRequest; - - private DefaultGraphQLWebSocketContext(DataLoaderRegistry dataLoaderRegistry, Subject subject, - Session session, HandshakeRequest handshakeRequest) { - super(dataLoaderRegistry, subject); - this.session = session; - this.handshakeRequest = handshakeRequest; - } - - @Override - public Session getSession() { - return session; - } - - @Override - public Optional getConnectResult() { - return Optional.of(session).map(session -> session.getUserProperties().get(ApolloSubscriptionConnectionListener.CONNECT_RESULT_KEY)); - } - - @Override - public HandshakeRequest getHandshakeRequest() { - return handshakeRequest; - } - - public static Builder createWebSocketContext(DataLoaderRegistry registry, Subject subject) { - return new Builder(registry, subject); - } - - public static Builder createWebSocketContext() { - return new Builder(new DataLoaderRegistry(), null); - } - - public static class Builder { - private Session session; - private HandshakeRequest handshakeRequest; - private DataLoaderRegistry dataLoaderRegistry; - private Subject subject; - - private Builder(DataLoaderRegistry dataLoaderRegistry, Subject subject) { - this.dataLoaderRegistry = dataLoaderRegistry; - this.subject = subject; - } - - public DefaultGraphQLWebSocketContext build() { - return new DefaultGraphQLWebSocketContext(dataLoaderRegistry, subject, session, handshakeRequest); - } - - public Builder with(Session session) { - this.session = session; - return this; - } - - public Builder with(HandshakeRequest handshakeRequest) { - this.handshakeRequest = handshakeRequest; - return this; - } - - public Builder with(DataLoaderRegistry dataLoaderRegistry) { - this.dataLoaderRegistry = dataLoaderRegistry; - return this; - } - - public Builder with(Subject subject) { - this.subject = subject; - return this; - } - - } -} diff --git a/src/main/java/graphql/servlet/core/ApolloScalars.java b/src/main/java/graphql/servlet/core/ApolloScalars.java deleted file mode 100644 index 19dbf406..00000000 --- a/src/main/java/graphql/servlet/core/ApolloScalars.java +++ /dev/null @@ -1,41 +0,0 @@ -package graphql.servlet.core; - -import graphql.schema.Coercing; -import graphql.schema.CoercingParseLiteralException; -import graphql.schema.CoercingParseValueException; -import graphql.schema.CoercingSerializeException; -import graphql.schema.GraphQLScalarType; - -import javax.servlet.http.Part; - -public class ApolloScalars { - public static final GraphQLScalarType Upload = - new GraphQLScalarType("Upload", - "A file part in a multipart request", - new Coercing() { - @Override - public Void serialize(Object dataFetcherResult) { - throw new CoercingSerializeException("Upload is an input-only type"); - } - - @Override - public Part parseValue(Object input) { - if (input instanceof Part) { - return (Part) input; - } else if (null == input) { - return null; - } else { - throw new CoercingParseValueException("Expected type " + - Part.class.getName() + - " but was " + - input.getClass().getName()); - } - } - - @Override - public Part parseLiteral(Object input) { - throw new CoercingParseLiteralException( - "Must use variables to specify Upload values"); - } - }); -} diff --git a/src/main/java/graphql/servlet/core/ApolloSubscriptionConnectionListener.java b/src/main/java/graphql/servlet/core/ApolloSubscriptionConnectionListener.java deleted file mode 100644 index ba0eb01d..00000000 --- a/src/main/java/graphql/servlet/core/ApolloSubscriptionConnectionListener.java +++ /dev/null @@ -1,42 +0,0 @@ -package graphql.servlet.core; - -import java.time.Duration; -import java.util.Optional; - -public interface ApolloSubscriptionConnectionListener extends SubscriptionConnectionListener { - - long KEEP_ALIVE_INTERVAL_SEC = 15; - - String CONNECT_RESULT_KEY = "CONNECT_RESULT"; - - default boolean isKeepAliveEnabled() { - return true; - } - - default Optional onConnect(Object payload) throws SubscriptionException { - return Optional.empty(); - } - - default Duration getKeepAliveInterval() { - return Duration.ofSeconds(KEEP_ALIVE_INTERVAL_SEC); - } - - static ApolloSubscriptionConnectionListener createWithKeepAliveDisabled() { - return new ApolloSubscriptionConnectionListener() { - @Override - public boolean isKeepAliveEnabled() { - return false; - } - }; - } - - static ApolloSubscriptionConnectionListener createWithKeepAliveInterval(Duration interval) { - return new ApolloSubscriptionConnectionListener() { - @Override - public Duration getKeepAliveInterval() { - return interval; - } - }; - } - -} diff --git a/src/main/java/graphql/servlet/core/DefaultGraphQLErrorHandler.java b/src/main/java/graphql/servlet/core/DefaultGraphQLErrorHandler.java deleted file mode 100644 index 3d2524e0..00000000 --- a/src/main/java/graphql/servlet/core/DefaultGraphQLErrorHandler.java +++ /dev/null @@ -1,66 +0,0 @@ -package graphql.servlet.core; - -import graphql.ExceptionWhileDataFetching; -import graphql.GraphQLError; -import graphql.execution.NonNullableFieldWasNullError; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.List; -import java.util.stream.Collectors; - -/** - * @author Andrew Potter - */ -public class DefaultGraphQLErrorHandler implements GraphQLErrorHandler { - - private static final Logger log = LoggerFactory.getLogger(DefaultGraphQLErrorHandler.class); - - @Override - public List processErrors(List errors) { - final List clientErrors = filterGraphQLErrors(errors); - if (clientErrors.size() < errors.size()) { - - // Some errors were filtered out to hide implementation - put a generic error in place. - clientErrors.add(new GenericGraphQLError("Internal Server Error(s) while executing query")); - - errors.stream() - .filter(error -> !isClientError(error)) - .forEach(this::logError); - } - - return clientErrors; - } - - protected void logError(GraphQLError error) { - if (error instanceof Throwable) { - log.error("Error executing query!", (Throwable) error); - } else if (error instanceof ExceptionWhileDataFetching) { - log.error("Error executing query {}", error.getMessage(), ((ExceptionWhileDataFetching) error).getException()); - } else { - log.error("Error executing query ({}): {}", error.getClass().getSimpleName(), error.getMessage()); - } - } - - protected List filterGraphQLErrors(List errors) { - return errors.stream() - .filter(this::isClientError) - .map(this::replaceNonNullableFieldWasNullError) - .collect(Collectors.toList()); - } - - protected boolean isClientError(GraphQLError error) { - if (error instanceof ExceptionWhileDataFetching) { - return ((ExceptionWhileDataFetching) error).getException() instanceof GraphQLError; - } - return true; - } - - private GraphQLError replaceNonNullableFieldWasNullError(GraphQLError error) { - if (error instanceof NonNullableFieldWasNullError) { - return new RenderableNonNullableFieldWasNullError((NonNullableFieldWasNullError) error); - } else { - return error; - } - } -} diff --git a/src/main/java/graphql/servlet/core/DefaultGraphQLRootObjectBuilder.java b/src/main/java/graphql/servlet/core/DefaultGraphQLRootObjectBuilder.java deleted file mode 100644 index 2c5e3c8b..00000000 --- a/src/main/java/graphql/servlet/core/DefaultGraphQLRootObjectBuilder.java +++ /dev/null @@ -1,7 +0,0 @@ -package graphql.servlet.core; - -public class DefaultGraphQLRootObjectBuilder extends StaticGraphQLRootObjectBuilder { - public DefaultGraphQLRootObjectBuilder() { - super(new Object()); - } -} diff --git a/src/main/java/graphql/servlet/core/GraphQLObjectMapper.java b/src/main/java/graphql/servlet/core/GraphQLObjectMapper.java deleted file mode 100644 index 1ca26dcb..00000000 --- a/src/main/java/graphql/servlet/core/GraphQLObjectMapper.java +++ /dev/null @@ -1,216 +0,0 @@ -package graphql.servlet.core; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.MappingIterator; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.ObjectReader; -import graphql.*; -import graphql.execution.ExecutionPath; -import graphql.servlet.config.ConfiguringObjectMapperProvider; -import graphql.servlet.config.ObjectMapperConfigurer; -import graphql.servlet.config.ObjectMapperProvider; -import graphql.servlet.core.internal.GraphQLRequest; -import graphql.servlet.core.internal.VariablesDeserializer; - -import javax.servlet.http.Part; -import java.io.IOException; -import java.io.InputStream; -import java.io.Writer; -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.function.Supplier; - -import static java.util.stream.Collectors.toList; - -/** - * @author Andrew Potter - */ -public class GraphQLObjectMapper { - private static final TypeReference>> - MULTIPART_MAP_TYPE_REFERENCE = new TypeReference>>() { - }; - private final ObjectMapperProvider objectMapperProvider; - private final Supplier graphQLErrorHandlerSupplier; - - private volatile ObjectMapper mapper; - - protected GraphQLObjectMapper(ObjectMapperProvider objectMapperProvider, Supplier graphQLErrorHandlerSupplier) { - this.objectMapperProvider = objectMapperProvider; - this.graphQLErrorHandlerSupplier = graphQLErrorHandlerSupplier; - } - - // Double-check idiom for lazy initialization of instance fields. - public ObjectMapper getJacksonMapper() { - ObjectMapper result = mapper; - if (result == null) { // First check (no locking) - synchronized (this) { - result = mapper; - if (result == null) { // Second check (with locking) - mapper = result = objectMapperProvider.provide(); - } - } - } - - return result; - } - - /** - * @return an {@link ObjectReader} for deserializing {@link GraphQLRequest} - */ - public ObjectReader getGraphQLRequestMapper() { - return getJacksonMapper().reader().forType(GraphQLRequest.class); - } - - public GraphQLRequest readGraphQLRequest(InputStream inputStream) throws IOException { - return getGraphQLRequestMapper().readValue(inputStream); - } - - public GraphQLRequest readGraphQLRequest(String text) throws IOException { - return getGraphQLRequestMapper().readValue(text); - } - - public List readBatchedGraphQLRequest(InputStream inputStream) throws IOException { - MappingIterator iterator = getGraphQLRequestMapper().readValues(inputStream); - List requests = new ArrayList<>(); - - while (iterator.hasNext()) { - requests.add(iterator.next()); - } - - return requests; - } - - public List readBatchedGraphQLRequest(String query) throws IOException { - MappingIterator iterator = getGraphQLRequestMapper().readValues(query); - List requests = new ArrayList<>(); - - while (iterator.hasNext()) { - requests.add(iterator.next()); - } - - return requests; - } - - public String serializeResultAsJson(ExecutionResult executionResult) { - try { - return getJacksonMapper().writeValueAsString(createResultFromExecutionResult(executionResult)); - } catch (JsonProcessingException e) { - throw new RuntimeException(e); - } - } - - public void serializeResultAsJson(Writer writer, ExecutionResult executionResult) throws IOException { - getJacksonMapper().writeValue(writer, createResultFromExecutionResult(executionResult)); - } - - public boolean areErrorsPresent(ExecutionResult executionResult) { - return graphQLErrorHandlerSupplier.get().errorsPresent(executionResult.getErrors()); - } - - public ExecutionResult sanitizeErrors(ExecutionResult executionResult) { - Object data = executionResult.getData(); - Map extensions = executionResult.getExtensions(); - List errors = executionResult.getErrors(); - - GraphQLErrorHandler errorHandler = graphQLErrorHandlerSupplier.get(); - if (errorHandler.errorsPresent(errors)) { - errors = errorHandler.processErrors(errors); - } else { - errors = null; - } - return new ExecutionResultImpl(data, errors, extensions); - } - - public Map createResultFromExecutionResult(ExecutionResult executionResult) { - ExecutionResult sanitizedExecutionResult = sanitizeErrors(executionResult); - if (executionResult instanceof DeferredExecutionResult) { - sanitizedExecutionResult = DeferredExecutionResultImpl - .newDeferredExecutionResult() - .from(executionResult) - .path(ExecutionPath.fromList(((DeferredExecutionResult) executionResult).getPath())) - .build(); - } - return convertSanitizedExecutionResult(sanitizedExecutionResult); - } - - public Map convertSanitizedExecutionResult(ExecutionResult executionResult) { - return convertSanitizedExecutionResult(executionResult, true); - } - - public Map convertSanitizedExecutionResult(ExecutionResult executionResult, boolean includeData) { - final Map result = new LinkedHashMap<>(); - - if (areErrorsPresent(executionResult)) { - result.put("errors", executionResult.getErrors().stream().map(GraphQLError::toSpecification).collect(toList())); - } - - if (executionResult.getExtensions() != null && !executionResult.getExtensions().isEmpty()) { - result.put("extensions", executionResult.getExtensions()); - } - - if (includeData) { - result.put("data", executionResult.getData()); - } - - if (executionResult instanceof DeferredExecutionResult) { - result.put("path", ((DeferredExecutionResult) executionResult).getPath()); - } - - return result; - } - - public Map deserializeVariables(String variables) { - try { - return VariablesDeserializer.deserializeVariablesObject(getJacksonMapper().readValue(variables, Object.class), getJacksonMapper()); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - public Map> deserializeMultipartMap(Part part) { - try { - return getJacksonMapper().readValue(part.getInputStream(), MULTIPART_MAP_TYPE_REFERENCE); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - public static Builder newBuilder() { - return new Builder(); - } - - public static class Builder { - private ObjectMapperProvider objectMapperProvider = new ConfiguringObjectMapperProvider(); - private Supplier graphQLErrorHandler = DefaultGraphQLErrorHandler::new; - - public Builder withObjectMapperConfigurer(ObjectMapperConfigurer objectMapperConfigurer) { - return withObjectMapperConfigurer(() -> objectMapperConfigurer); - } - - public Builder withObjectMapperConfigurer(Supplier objectMapperConfigurer) { - this.objectMapperProvider = new ConfiguringObjectMapperProvider(objectMapperConfigurer.get()); - return this; - } - - public Builder withObjectMapperProvider(ObjectMapperProvider objectMapperProvider) { - this.objectMapperProvider = objectMapperProvider; - return this; - } - - public Builder withGraphQLErrorHandler(GraphQLErrorHandler graphQLErrorHandler) { - return withGraphQLErrorHandler(() -> graphQLErrorHandler); - } - - public Builder withGraphQLErrorHandler(Supplier graphQLErrorHandler) { - this.graphQLErrorHandler = graphQLErrorHandler; - return this; - } - - public GraphQLObjectMapper build() { - return new GraphQLObjectMapper(objectMapperProvider, graphQLErrorHandler); - } - } -} diff --git a/src/main/java/graphql/servlet/core/GraphQLQueryInvoker.java b/src/main/java/graphql/servlet/core/GraphQLQueryInvoker.java deleted file mode 100644 index b30dc2e7..00000000 --- a/src/main/java/graphql/servlet/core/GraphQLQueryInvoker.java +++ /dev/null @@ -1,173 +0,0 @@ -package graphql.servlet.core; - -import graphql.ExecutionInput; -import graphql.ExecutionResult; -import graphql.GraphQL; -import graphql.execution.instrumentation.ChainedInstrumentation; -import graphql.execution.instrumentation.Instrumentation; -import graphql.execution.instrumentation.SimpleInstrumentation; -import graphql.execution.instrumentation.dataloader.DataLoaderDispatcherInstrumentation; -import graphql.execution.instrumentation.dataloader.DataLoaderDispatcherInstrumentationOptions; -import graphql.execution.preparsed.NoOpPreparsedDocumentProvider; -import graphql.execution.preparsed.PreparsedDocumentProvider; -import graphql.schema.GraphQLSchema; -import graphql.servlet.config.DefaultExecutionStrategyProvider; -import graphql.servlet.config.ExecutionStrategyProvider; -import graphql.servlet.context.ContextSetting; -import graphql.servlet.input.GraphQLSingleInvocationInput; - -import javax.security.auth.Subject; -import java.security.AccessController; -import java.security.PrivilegedAction; -import java.util.List; -import java.util.concurrent.CompletableFuture; -import java.util.function.Supplier; -import java.util.stream.Collectors; - -/** - * @author Andrew Potter - */ -public class GraphQLQueryInvoker { - - private final Supplier getExecutionStrategyProvider; - private final Supplier getInstrumentation; - private final Supplier getPreparsedDocumentProvider; - private final Supplier optionsSupplier; - - protected GraphQLQueryInvoker(Supplier getExecutionStrategyProvider, Supplier getInstrumentation, - Supplier getPreparsedDocumentProvider, - Supplier optionsSupplier) { - this.getExecutionStrategyProvider = getExecutionStrategyProvider; - this.getInstrumentation = getInstrumentation; - this.getPreparsedDocumentProvider = getPreparsedDocumentProvider; - this.optionsSupplier = optionsSupplier; - } - - public ExecutionResult query(GraphQLSingleInvocationInput singleInvocationInput) { - return queryAsync(singleInvocationInput, getInstrumentation).join(); - } - - private CompletableFuture queryAsync(GraphQLSingleInvocationInput singleInvocationInput, Supplier configuredInstrumentation) { - return query(singleInvocationInput, configuredInstrumentation, singleInvocationInput.getExecutionInput()); - } - - public List query(List batchedInvocationInput, ContextSetting contextSetting) { - List executionIds = batchedInvocationInput.stream() - .map(GraphQLSingleInvocationInput::getExecutionInput) - .collect(Collectors.toList()); - Supplier configuredInstrumentation = contextSetting.configureInstrumentationForContext(getInstrumentation, executionIds, - optionsSupplier.get()); - return batchedInvocationInput.stream() - .map(input -> this.queryAsync(input, configuredInstrumentation)) - //We want eager eval - .collect(Collectors.toList()) - .stream() - .map(CompletableFuture::join) - .collect(Collectors.toList()); - } - - private GraphQL newGraphQL(GraphQLSchema schema, Supplier configuredInstrumentation) { - ExecutionStrategyProvider executionStrategyProvider = getExecutionStrategyProvider.get(); - GraphQL.Builder builder = GraphQL.newGraphQL(schema) - .queryExecutionStrategy(executionStrategyProvider.getQueryExecutionStrategy()) - .mutationExecutionStrategy(executionStrategyProvider.getMutationExecutionStrategy()) - .subscriptionExecutionStrategy(executionStrategyProvider.getSubscriptionExecutionStrategy()) - .preparsedDocumentProvider(getPreparsedDocumentProvider.get()); - Instrumentation instrumentation = configuredInstrumentation.get(); - builder.instrumentation(instrumentation); - if (containsDispatchInstrumentation(instrumentation)) { - builder.doNotAddDefaultInstrumentations(); - } - return builder.build(); - } - - private boolean containsDispatchInstrumentation(Instrumentation instrumentation) { - if (instrumentation instanceof ChainedInstrumentation) { - return ((ChainedInstrumentation)instrumentation).getInstrumentations().stream().anyMatch(this::containsDispatchInstrumentation); - } - return instrumentation instanceof DataLoaderDispatcherInstrumentation; - } - - private CompletableFuture query(GraphQLSingleInvocationInput invocationInput, Supplier configuredInstrumentation, ExecutionInput executionInput) { - if (Subject.getSubject(AccessController.getContext()) == null && invocationInput.getSubject().isPresent()) { - return Subject.doAs(invocationInput.getSubject().get(), (PrivilegedAction>) () -> { - try { - return query(invocationInput.getSchema(), executionInput, configuredInstrumentation); - } catch (Exception e) { - throw new RuntimeException(e); - } - }); - } - - return query(invocationInput.getSchema(), executionInput, configuredInstrumentation); - } - - private CompletableFuture query(GraphQLSchema schema, ExecutionInput executionInput, Supplier configuredInstrumentation) { - return newGraphQL(schema, configuredInstrumentation).executeAsync(executionInput); - } - - public static Builder newBuilder() { - return new Builder(); - } - - public static class Builder { - private Supplier getExecutionStrategyProvider = DefaultExecutionStrategyProvider::new; - private Supplier getInstrumentation = () -> SimpleInstrumentation.INSTANCE; - private Supplier getPreparsedDocumentProvider = () -> NoOpPreparsedDocumentProvider.INSTANCE; - private Supplier dataLoaderDispatcherInstrumentationOptionsSupplier = DataLoaderDispatcherInstrumentationOptions::newOptions; - - - public Builder withExecutionStrategyProvider(ExecutionStrategyProvider provider) { - return withExecutionStrategyProvider(() -> provider); - } - - public Builder withExecutionStrategyProvider(Supplier supplier) { - this.getExecutionStrategyProvider = supplier; - return this; - } - - public Builder withInstrumentation(Instrumentation instrumentation) { - return withInstrumentation(() -> instrumentation); - } - - public Builder withInstrumentation(Supplier supplier) { - this.getInstrumentation = supplier; - return this; - } - - public Builder with(List instrumentations) { - if (instrumentations.isEmpty()) { - return this; - } - if (instrumentations.size() == 1) { - withInstrumentation(instrumentations.get(0)); - } else { - withInstrumentation(new ChainedInstrumentation(instrumentations)); - } - return this; - } - - public Builder withPreparsedDocumentProvider(PreparsedDocumentProvider provider) { - return withPreparsedDocumentProvider(() -> provider); - } - - public Builder withPreparsedDocumentProvider(Supplier supplier) { - this.getPreparsedDocumentProvider = supplier; - return this; - } - - public Builder withDataLoaderDispatcherInstrumentationOptions(DataLoaderDispatcherInstrumentationOptions options) { - return withDataLoaderDispatcherInstrumentationOptions(() -> options); - } - - public Builder withDataLoaderDispatcherInstrumentationOptions(Supplier supplier) { - this.dataLoaderDispatcherInstrumentationOptionsSupplier = supplier; - return this; - } - - public GraphQLQueryInvoker build() { - return new GraphQLQueryInvoker(getExecutionStrategyProvider, getInstrumentation, getPreparsedDocumentProvider, - dataLoaderDispatcherInstrumentationOptionsSupplier); - } - } -} diff --git a/src/main/java/graphql/servlet/core/GraphQLRootObjectBuilder.java b/src/main/java/graphql/servlet/core/GraphQLRootObjectBuilder.java deleted file mode 100644 index e4022730..00000000 --- a/src/main/java/graphql/servlet/core/GraphQLRootObjectBuilder.java +++ /dev/null @@ -1,15 +0,0 @@ -package graphql.servlet.core; - -import javax.servlet.http.HttpServletRequest; -import javax.websocket.server.HandshakeRequest; - -public interface GraphQLRootObjectBuilder { - Object build(HttpServletRequest req); - Object build(HandshakeRequest req); - - /** - * Only used for MBean calls. - * @return the graphql root object - */ - Object build(); -} diff --git a/src/main/java/graphql/servlet/core/StaticGraphQLRootObjectBuilder.java b/src/main/java/graphql/servlet/core/StaticGraphQLRootObjectBuilder.java deleted file mode 100644 index 8790a9a0..00000000 --- a/src/main/java/graphql/servlet/core/StaticGraphQLRootObjectBuilder.java +++ /dev/null @@ -1,28 +0,0 @@ -package graphql.servlet.core; - -import javax.servlet.http.HttpServletRequest; -import javax.websocket.server.HandshakeRequest; - -public class StaticGraphQLRootObjectBuilder implements GraphQLRootObjectBuilder { - - private final Object rootObject; - - public StaticGraphQLRootObjectBuilder(Object rootObject) { - this.rootObject = rootObject; - } - - @Override - public Object build(HttpServletRequest req) { - return rootObject; - } - - @Override - public Object build(HandshakeRequest req) { - return rootObject; - } - - @Override - public Object build() { - return rootObject; - } -} diff --git a/src/main/java/graphql/servlet/core/internal/ApolloSubscriptionKeepAliveRunner.java b/src/main/java/graphql/servlet/core/internal/ApolloSubscriptionKeepAliveRunner.java deleted file mode 100644 index 2c60f9cf..00000000 --- a/src/main/java/graphql/servlet/core/internal/ApolloSubscriptionKeepAliveRunner.java +++ /dev/null @@ -1,64 +0,0 @@ -package graphql.servlet.core.internal; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.websocket.Session; -import java.time.Duration; -import java.util.Map; -import java.util.Objects; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; - -class ApolloSubscriptionKeepAliveRunner { - - private static final Logger LOG = LoggerFactory.getLogger(ApolloSubscriptionKeepAliveRunner.class); - - private static final int EXECUTOR_POOL_SIZE = 10; - - private final ScheduledExecutorService executor; - private final SubscriptionSender sender; - private final ApolloSubscriptionProtocolHandler.OperationMessage keepAliveMessage; - private final Map> futures; - private final long keepAliveIntervalSeconds; - - ApolloSubscriptionKeepAliveRunner(SubscriptionSender sender, Duration keepAliveInterval) { - this.sender = Objects.requireNonNull(sender); - this.keepAliveMessage = ApolloSubscriptionProtocolHandler.OperationMessage.newKeepAliveMessage(); - this.executor = Executors.newScheduledThreadPool(EXECUTOR_POOL_SIZE); - this.futures = new ConcurrentHashMap<>(); - this.keepAliveIntervalSeconds = keepAliveInterval.getSeconds(); - } - - void keepAlive(Session session) { - futures.computeIfAbsent(session, this::startKeepAlive); - } - - private ScheduledFuture startKeepAlive(Session session) { - return executor.scheduleAtFixedRate(() -> { - try { - if (session.isOpen()) { - sender.send(session, keepAliveMessage); - } else { - LOG.debug("Session {} appears to be closed. Aborting keep alive", session.getId()); - abort(session); - } - } catch (Throwable t) { - LOG.error("Cannot send keep alive message to session {}. Aborting keep alive", session.getId(), t); - abort(session); - } - }, 0, keepAliveIntervalSeconds, TimeUnit.SECONDS); - } - - void abort(Session session) { - Future future = futures.remove(session); - if (future != null) { - future.cancel(true); - } - } - -} diff --git a/src/main/java/graphql/servlet/core/internal/ApolloSubscriptionProtocolFactory.java b/src/main/java/graphql/servlet/core/internal/ApolloSubscriptionProtocolFactory.java deleted file mode 100644 index 25b40410..00000000 --- a/src/main/java/graphql/servlet/core/internal/ApolloSubscriptionProtocolFactory.java +++ /dev/null @@ -1,30 +0,0 @@ -package graphql.servlet.core.internal; - -import graphql.servlet.core.ApolloSubscriptionConnectionListener; - -/** - * @author Andrew Potter - */ -public class ApolloSubscriptionProtocolFactory extends SubscriptionProtocolFactory { - private final SubscriptionHandlerInput subscriptionHandlerInput; - private final SubscriptionSender subscriptionSender; - private final ApolloSubscriptionKeepAliveRunner keepAliveRunner; - private final ApolloSubscriptionConnectionListener connectionListener; - - public ApolloSubscriptionProtocolFactory(SubscriptionHandlerInput subscriptionHandlerInput) { - super("graphql-ws"); - this.subscriptionHandlerInput = subscriptionHandlerInput; - this.connectionListener = subscriptionHandlerInput.getSubscriptionConnectionListener() - .filter(ApolloSubscriptionConnectionListener.class::isInstance) - .map(ApolloSubscriptionConnectionListener.class::cast) - .orElse(new ApolloSubscriptionConnectionListener() {}); - subscriptionSender = - new SubscriptionSender(subscriptionHandlerInput.getGraphQLObjectMapper().getJacksonMapper()); - keepAliveRunner = new ApolloSubscriptionKeepAliveRunner(subscriptionSender, connectionListener.getKeepAliveInterval()); - } - - @Override - public SubscriptionProtocolHandler createHandler() { - return new ApolloSubscriptionProtocolHandler(subscriptionHandlerInput, connectionListener, subscriptionSender, keepAliveRunner); - } -} diff --git a/src/main/java/graphql/servlet/core/internal/ApolloSubscriptionProtocolHandler.java b/src/main/java/graphql/servlet/core/internal/ApolloSubscriptionProtocolHandler.java deleted file mode 100644 index 4818c5e1..00000000 --- a/src/main/java/graphql/servlet/core/internal/ApolloSubscriptionProtocolHandler.java +++ /dev/null @@ -1,227 +0,0 @@ -package graphql.servlet.core.internal; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonValue; -import graphql.ExecutionResult; -import graphql.servlet.core.ApolloSubscriptionConnectionListener; -import graphql.servlet.input.GraphQLSingleInvocationInput; -import graphql.servlet.core.SubscriptionException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.websocket.CloseReason; -import javax.websocket.Session; -import javax.websocket.server.HandshakeRequest; -import java.io.IOException; -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; - -import static graphql.servlet.core.internal.ApolloSubscriptionProtocolHandler.OperationMessage.Type.GQL_COMPLETE; -import static graphql.servlet.core.internal.ApolloSubscriptionProtocolHandler.OperationMessage.Type.GQL_CONNECTION_TERMINATE; -import static graphql.servlet.core.internal.ApolloSubscriptionProtocolHandler.OperationMessage.Type.GQL_DATA; -import static graphql.servlet.core.internal.ApolloSubscriptionProtocolHandler.OperationMessage.Type.GQL_ERROR; - -/** - * https://github.com/apollographql/subscriptions-transport-ws/blob/master/PROTOCOL.md - * - * @author Andrew Potter - */ -public class ApolloSubscriptionProtocolHandler extends SubscriptionProtocolHandler { - - private static final Logger log = LoggerFactory.getLogger(ApolloSubscriptionProtocolHandler.class); - private static final CloseReason TERMINATE_CLOSE_REASON = new CloseReason(CloseReason.CloseCodes.NORMAL_CLOSURE, "client requested " + GQL_CONNECTION_TERMINATE.getType()); - - private final SubscriptionHandlerInput input; - private final SubscriptionSender sender; - private final ApolloSubscriptionKeepAliveRunner keepAliveRunner; - private final ApolloSubscriptionConnectionListener connectionListener; - - public ApolloSubscriptionProtocolHandler(SubscriptionHandlerInput subscriptionHandlerInput, - ApolloSubscriptionConnectionListener connectionListener, - SubscriptionSender subscriptionSender, - ApolloSubscriptionKeepAliveRunner keepAliveRunner) { - this.input = subscriptionHandlerInput; - this.connectionListener = connectionListener; - this.sender = subscriptionSender; - this.keepAliveRunner = keepAliveRunner; - } - - @Override - public void onMessage(HandshakeRequest request, Session session, WsSessionSubscriptions subscriptions, String text) { - OperationMessage message; - try { - message = input.getGraphQLObjectMapper().getJacksonMapper().readValue(text, OperationMessage.class); - } catch(Throwable t) { - log.warn("Error parsing message", t); - sendMessage(session, OperationMessage.Type.GQL_CONNECTION_ERROR, null); - return; - } - - switch (message.getType()) { - case GQL_CONNECTION_INIT: - try { - Optional connectionResponse = connectionListener.onConnect(message.getPayload()); - connectionResponse.ifPresent(it -> session.getUserProperties().put(ApolloSubscriptionConnectionListener.CONNECT_RESULT_KEY, it)); - } catch (SubscriptionException e) { - sendMessage(session, OperationMessage.Type.GQL_CONNECTION_ERROR, message.getId(), e.getPayload()); - return; - } - - sendMessage(session, OperationMessage.Type.GQL_CONNECTION_ACK, message.getId()); - - if (connectionListener.isKeepAliveEnabled()) { - keepAliveRunner.keepAlive(session); - } - break; - - case GQL_START: - GraphQLSingleInvocationInput graphQLSingleInvocationInput = createInvocationInput(session, message); - handleSubscriptionStart( - session, - subscriptions, - message.id, - input.getQueryInvoker().query(graphQLSingleInvocationInput) - ); - break; - - case GQL_STOP: - unsubscribe(subscriptions, message.id); - break; - - case GQL_CONNECTION_TERMINATE: - keepAliveRunner.abort(session); - try { - session.close(TERMINATE_CLOSE_REASON); - } catch (IOException e) { - log.error("Error closing websocket session!", e); - } - break; - - default: - throw new IllegalArgumentException("Unknown message type: " + message.getType()); - } - } - - private GraphQLSingleInvocationInput createInvocationInput(Session session, OperationMessage message) { - GraphQLRequest graphQLRequest = input.getGraphQLObjectMapper() - .getJacksonMapper() - .convertValue(message.getPayload(), GraphQLRequest.class); - HandshakeRequest handshakeRequest = (HandshakeRequest) session.getUserProperties() - .get(HandshakeRequest.class.getName()); - - return input.getInvocationInputFactory().create(graphQLRequest, session, handshakeRequest); - } - - @SuppressWarnings("unchecked") - private void handleSubscriptionStart(Session session, WsSessionSubscriptions subscriptions, String id, ExecutionResult executionResult) { - executionResult = input.getGraphQLObjectMapper().sanitizeErrors(executionResult); - - if (input.getGraphQLObjectMapper().areErrorsPresent(executionResult)) { - sendMessage(session, OperationMessage.Type.GQL_ERROR, id, input.getGraphQLObjectMapper().convertSanitizedExecutionResult(executionResult, false)); - return; - } - - subscribe(session, executionResult, subscriptions, id); - } - - @Override - protected void sendDataMessage(Session session, String id, Object payload) { - sendMessage(session, GQL_DATA, id, payload); - } - - @Override - protected void sendErrorMessage(Session session, String id) { - keepAliveRunner.abort(session); - sendMessage(session, GQL_ERROR, id); - } - - @Override - protected void sendCompleteMessage(Session session, String id) { - keepAliveRunner.abort(session); - sendMessage(session, GQL_COMPLETE, id); - } - - private void sendMessage(Session session, OperationMessage.Type type, String id) { - sendMessage(session, type, id, null); - } - - private void sendMessage(Session session, OperationMessage.Type type, String id, Object payload) { - sender.send(session, new OperationMessage(type, id, payload)); - } - - @JsonInclude(JsonInclude.Include.NON_NULL) - public static class OperationMessage { - private Type type; - private String id; - private Object payload; - - public OperationMessage() { - } - - public OperationMessage(Type type, String id, Object payload) { - this.type = type; - this.id = id; - this.payload = payload; - } - - static OperationMessage newKeepAliveMessage() { - return new OperationMessage(Type.GQL_CONNECTION_KEEP_ALIVE, null, null); - } - - public Type getType() { - return type; - } - - public String getId() { - return id; - } - - public Object getPayload() { - return payload; - } - - public enum Type { - - // Server Messages - GQL_CONNECTION_ACK("connection_ack"), - GQL_CONNECTION_ERROR("connection_error"), - GQL_CONNECTION_KEEP_ALIVE("ka"), - GQL_DATA("data"), - GQL_ERROR("error"), - GQL_COMPLETE("complete"), - - // Client Messages - GQL_CONNECTION_INIT("connection_init"), - GQL_CONNECTION_TERMINATE("connection_terminate"), - GQL_START("start"), - GQL_STOP("stop"); - - private static final Map reverseLookup = new HashMap<>(); - - static { - for(Type type: Type.values()) { - reverseLookup.put(type.getType(), type); - } - } - - private final String type; - - Type(String type) { - this.type = type; - } - - @JsonCreator - public static Type findType(String type) { - return reverseLookup.get(type); - } - - @JsonValue - public String getType() { - return type; - } - } - } - -} diff --git a/src/main/java/graphql/servlet/core/internal/FallbackSubscriptionProtocolFactory.java b/src/main/java/graphql/servlet/core/internal/FallbackSubscriptionProtocolFactory.java deleted file mode 100644 index b5566293..00000000 --- a/src/main/java/graphql/servlet/core/internal/FallbackSubscriptionProtocolFactory.java +++ /dev/null @@ -1,18 +0,0 @@ -package graphql.servlet.core.internal; - -/** - * @author Andrew Potter - */ -public class FallbackSubscriptionProtocolFactory extends SubscriptionProtocolFactory { - private final SubscriptionHandlerInput subscriptionHandlerInput; - - public FallbackSubscriptionProtocolFactory(SubscriptionHandlerInput subscriptionHandlerInput) { - super(""); - this.subscriptionHandlerInput = subscriptionHandlerInput; - } - - @Override - public SubscriptionProtocolHandler createHandler() { - return new FallbackSubscriptionProtocolHandler(subscriptionHandlerInput); - } -} diff --git a/src/main/java/graphql/servlet/core/internal/FallbackSubscriptionProtocolHandler.java b/src/main/java/graphql/servlet/core/internal/FallbackSubscriptionProtocolHandler.java deleted file mode 100644 index 909423b2..00000000 --- a/src/main/java/graphql/servlet/core/internal/FallbackSubscriptionProtocolHandler.java +++ /dev/null @@ -1,56 +0,0 @@ -package graphql.servlet.core.internal; - -import graphql.servlet.input.GraphQLSingleInvocationInput; - -import javax.websocket.Session; -import javax.websocket.server.HandshakeRequest; -import java.io.IOException; -import java.util.UUID; - -/** - * @author Andrew Potter - */ -public class FallbackSubscriptionProtocolHandler extends SubscriptionProtocolHandler { - - private final SubscriptionHandlerInput input; - private final SubscriptionSender sender; - - public FallbackSubscriptionProtocolHandler(SubscriptionHandlerInput subscriptionHandlerInput) { - this.input = subscriptionHandlerInput; - sender = new SubscriptionSender(subscriptionHandlerInput.getGraphQLObjectMapper().getJacksonMapper()); - } - - @Override - public void onMessage(HandshakeRequest request, Session session, WsSessionSubscriptions subscriptions, String text) throws Exception { - GraphQLSingleInvocationInput graphQLSingleInvocationInput = createInvocationInput(session, text); - subscribe( - session, - input.getQueryInvoker().query(graphQLSingleInvocationInput), - subscriptions, - UUID.randomUUID().toString() - ); - } - - private GraphQLSingleInvocationInput createInvocationInput(Session session, String text) throws IOException { - GraphQLRequest graphQLRequest = input.getGraphQLObjectMapper().readGraphQLRequest(text); - HandshakeRequest handshakeRequest = (HandshakeRequest) session.getUserProperties() - .get(HandshakeRequest.class.getName()); - - return input.getInvocationInputFactory().create(graphQLRequest, session, handshakeRequest); - } - - @Override - protected void sendDataMessage(Session session, String id, Object payload) { - sender.send(session, payload); - } - - @Override - protected void sendErrorMessage(Session session, String id) { - - } - - @Override - protected void sendCompleteMessage(Session session, String id) { - - } -} diff --git a/src/main/java/graphql/servlet/core/internal/SubscriptionHandlerInput.java b/src/main/java/graphql/servlet/core/internal/SubscriptionHandlerInput.java deleted file mode 100644 index 22b1d850..00000000 --- a/src/main/java/graphql/servlet/core/internal/SubscriptionHandlerInput.java +++ /dev/null @@ -1,39 +0,0 @@ -package graphql.servlet.core.internal; - -import graphql.servlet.input.GraphQLInvocationInputFactory; -import graphql.servlet.core.GraphQLObjectMapper; -import graphql.servlet.core.GraphQLQueryInvoker; -import graphql.servlet.core.SubscriptionConnectionListener; - -import java.util.Optional; - -public class SubscriptionHandlerInput { - - private final GraphQLInvocationInputFactory invocationInputFactory; - private final GraphQLQueryInvoker queryInvoker; - private final GraphQLObjectMapper graphQLObjectMapper; - private final SubscriptionConnectionListener subscriptionConnectionListener; - - public SubscriptionHandlerInput(GraphQLInvocationInputFactory invocationInputFactory, GraphQLQueryInvoker queryInvoker, GraphQLObjectMapper graphQLObjectMapper, SubscriptionConnectionListener subscriptionConnectionListener) { - this.invocationInputFactory = invocationInputFactory; - this.queryInvoker = queryInvoker; - this.graphQLObjectMapper = graphQLObjectMapper; - this.subscriptionConnectionListener = subscriptionConnectionListener; - } - - public GraphQLInvocationInputFactory getInvocationInputFactory() { - return invocationInputFactory; - } - - public GraphQLQueryInvoker getQueryInvoker() { - return queryInvoker; - } - - public GraphQLObjectMapper getGraphQLObjectMapper() { - return graphQLObjectMapper; - } - - public Optional getSubscriptionConnectionListener() { - return Optional.ofNullable(subscriptionConnectionListener); - } -} diff --git a/src/main/java/graphql/servlet/core/internal/SubscriptionProtocolFactory.java b/src/main/java/graphql/servlet/core/internal/SubscriptionProtocolFactory.java deleted file mode 100644 index f82fa912..00000000 --- a/src/main/java/graphql/servlet/core/internal/SubscriptionProtocolFactory.java +++ /dev/null @@ -1,18 +0,0 @@ -package graphql.servlet.core.internal; - -/** - * @author Andrew Potter - */ -public abstract class SubscriptionProtocolFactory { - private final String protocol; - - public SubscriptionProtocolFactory(String protocol) { - this.protocol = protocol; - } - - public String getProtocol() { - return protocol; - } - - public abstract SubscriptionProtocolHandler createHandler(); -} diff --git a/src/main/java/graphql/servlet/core/internal/SubscriptionProtocolHandler.java b/src/main/java/graphql/servlet/core/internal/SubscriptionProtocolHandler.java deleted file mode 100644 index b75babf9..00000000 --- a/src/main/java/graphql/servlet/core/internal/SubscriptionProtocolHandler.java +++ /dev/null @@ -1,95 +0,0 @@ -package graphql.servlet.core.internal; - -import graphql.ExecutionResult; -import org.reactivestreams.Publisher; -import org.reactivestreams.Subscriber; -import org.reactivestreams.Subscription; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.websocket.Session; -import javax.websocket.server.HandshakeRequest; -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.atomic.AtomicReference; - -/** - * @author Andrew Potter - */ -public abstract class SubscriptionProtocolHandler { - - private static final Logger log = LoggerFactory.getLogger(SubscriptionProtocolHandler.class); - - public abstract void onMessage(HandshakeRequest request, Session session, WsSessionSubscriptions subscriptions, String text) throws Exception; - - protected abstract void sendDataMessage(Session session, String id, Object payload); - - protected abstract void sendErrorMessage(Session session, String id); - - protected abstract void sendCompleteMessage(Session session, String id); - - protected void subscribe(Session session, ExecutionResult executionResult, WsSessionSubscriptions subscriptions, String id) { - final Object data = executionResult.getData(); - - if (data instanceof Publisher) { - @SuppressWarnings("unchecked") final Publisher publisher = (Publisher) data; - final AtomicSubscriptionReference subscriptionReference = new AtomicSubscriptionReference(); - - publisher.subscribe(new Subscriber() { - @Override - public void onSubscribe(Subscription subscription) { - subscriptionReference.set(subscription); - subscriptionReference.get().request(1); - - subscriptions.add(id, subscriptionReference.get()); - } - - @Override - public void onNext(ExecutionResult executionResult) { - subscriptionReference.get().request(1); - Map result = new HashMap<>(); - result.put("data", executionResult.getData()); - sendDataMessage(session, id, result); - } - - @Override - public void onError(Throwable throwable) { - log.error("Subscription error", throwable); - unsubscribe(subscriptions, id); - sendErrorMessage(session, id); - } - - @Override - public void onComplete() { - unsubscribe(subscriptions, id); - sendCompleteMessage(session, id); - } - }); - } - } - - protected void unsubscribe(WsSessionSubscriptions subscriptions, String id) { - subscriptions.cancel(id); - } - - static class AtomicSubscriptionReference { - private final AtomicReference reference = new AtomicReference<>(null); - - public void set(Subscription subscription) { - if(reference.get() != null) { - throw new IllegalStateException("Cannot overwrite subscription!"); - } - - reference.set(subscription); - } - - public Subscription get() { - Subscription subscription = reference.get(); - if(subscription == null) { - throw new IllegalStateException("Subscription has not been initialized yet!"); - } - - return subscription; - } - } -} diff --git a/src/main/java/graphql/servlet/core/internal/SubscriptionSender.java b/src/main/java/graphql/servlet/core/internal/SubscriptionSender.java deleted file mode 100644 index 89c5d117..00000000 --- a/src/main/java/graphql/servlet/core/internal/SubscriptionSender.java +++ /dev/null @@ -1,24 +0,0 @@ -package graphql.servlet.core.internal; - -import com.fasterxml.jackson.databind.ObjectMapper; - -import javax.websocket.Session; -import java.io.IOException; -import java.io.UncheckedIOException; - -class SubscriptionSender { - - private final ObjectMapper objectMapper; - - SubscriptionSender(ObjectMapper objectMapper) { - this.objectMapper = objectMapper; - } - - void send(Session session, Object payload) { - try { - session.getBasicRemote().sendText(objectMapper.writeValueAsString(payload)); - } catch (IOException e) { - throw new UncheckedIOException("Error sending subscription response", e); - } - } -} diff --git a/src/main/java/graphql/servlet/core/internal/VariableMapper.java b/src/main/java/graphql/servlet/core/internal/VariableMapper.java deleted file mode 100644 index 1a3b058f..00000000 --- a/src/main/java/graphql/servlet/core/internal/VariableMapper.java +++ /dev/null @@ -1,76 +0,0 @@ -package graphql.servlet.core.internal; - -import javax.servlet.http.Part; -import java.util.List; -import java.util.Map; -import java.util.regex.Pattern; - -public class VariableMapper { - private static final Pattern PERIOD = Pattern.compile("\\."); - - private static final Mapper> MAP_MAPPER = new Mapper>() { - @Override - public Object set(Map location, String target, Part value) { - return location.put(target, value); - } - - @Override - public Object recurse(Map location, String target) { - return location.get(target); - } - }; - private static final Mapper> LIST_MAPPER = new Mapper>() { - @Override - public Object set(List location, String target, Part value) { - return location.set(Integer.parseInt(target), value); - } - - @Override - public Object recurse(List location, String target) { - return location.get(Integer.parseInt(target)); - } - }; - - public static void mapVariable(String objectPath, Map variables, Part part) { - String[] segments = PERIOD.split(objectPath); - - if (segments.length < 2) { - throw new RuntimeException("object-path in map must have at least two segments"); - } else if (!"variables".equals(segments[0])) { - throw new RuntimeException("can only map into variables"); - } - - Object currentLocation = variables; - for (int i = 1; i < segments.length; i++) { - String segmentName = segments[i]; - Mapper mapper = determineMapper(currentLocation, objectPath, segmentName); - - if (i == segments.length - 1) { - if (null != mapper.set(currentLocation, segmentName, part)) { - throw new RuntimeException("expected null value when mapping " + objectPath); - } - } else { - currentLocation = mapper.recurse(currentLocation, segmentName); - if (null == currentLocation) { - throw new RuntimeException("found null intermediate value when trying to map " + objectPath); - } - } - } - } - - private static Mapper determineMapper(Object currentLocation, String objectPath, String segmentName) { - if (currentLocation instanceof Map) { - return MAP_MAPPER; - } else if (currentLocation instanceof List) { - return LIST_MAPPER; - } - - throw new RuntimeException("expected a map or list at " + segmentName + " when trying to map " + objectPath); - } - - interface Mapper { - Object set(T location, String target, Part value); - - Object recurse(T location, String target); - } -} diff --git a/src/main/java/graphql/servlet/core/internal/WsSessionSubscriptions.java b/src/main/java/graphql/servlet/core/internal/WsSessionSubscriptions.java deleted file mode 100644 index 98cf770d..00000000 --- a/src/main/java/graphql/servlet/core/internal/WsSessionSubscriptions.java +++ /dev/null @@ -1,55 +0,0 @@ -package graphql.servlet.core.internal; - -import org.reactivestreams.Subscription; - -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -/** - * @author Andrew Potter - */ -public class WsSessionSubscriptions { - - private final Object lock = new Object(); - - private boolean closed = false; - private Map subscriptions = new ConcurrentHashMap<>(); - - public void add(Subscription subscription) { - add(getImplicitId(subscription), subscription); - } - - public void add(String id, Subscription subscription) { - synchronized (lock) { - if (closed) { - throw new IllegalStateException("Websocket was already closed!"); - } - subscriptions.put(id, subscription); - } - } - - public void cancel(Subscription subscription) { - cancel(getImplicitId(subscription)); - } - - public void cancel(String id) { - Subscription subscription = subscriptions.remove(id); - if(subscription != null) { - subscription.cancel(); - } - } - - public void close() { - synchronized (lock) { - closed = true; - subscriptions.forEach((k, v) -> v.cancel()); - subscriptions.clear(); - } - } - - private String getImplicitId(Subscription subscription) { - return String.valueOf(subscription.hashCode()); - } - - public int getSubscriptionCount() { return subscriptions.size(); } -} diff --git a/src/main/java/graphql/servlet/input/GraphQLBatchedInvocationInput.java b/src/main/java/graphql/servlet/input/GraphQLBatchedInvocationInput.java deleted file mode 100644 index cf41fe1f..00000000 --- a/src/main/java/graphql/servlet/input/GraphQLBatchedInvocationInput.java +++ /dev/null @@ -1,14 +0,0 @@ -package graphql.servlet.input; - -import java.util.List; - -/** - * Interface representing a batched input. - */ -public interface GraphQLBatchedInvocationInput { - - /** - * @return each individual input in the batch, configured with a context. - */ - List getExecutionInputs(); -} diff --git a/src/main/java/graphql/servlet/input/GraphQLInvocationInputFactory.java b/src/main/java/graphql/servlet/input/GraphQLInvocationInputFactory.java deleted file mode 100644 index a5088433..00000000 --- a/src/main/java/graphql/servlet/input/GraphQLInvocationInputFactory.java +++ /dev/null @@ -1,151 +0,0 @@ -package graphql.servlet.input; - -import graphql.schema.GraphQLSchema; -import graphql.servlet.config.DefaultGraphQLSchemaProvider; -import graphql.servlet.config.GraphQLSchemaProvider; -import graphql.servlet.context.ContextSetting; -import graphql.servlet.context.DefaultGraphQLContextBuilder; -import graphql.servlet.context.GraphQLContextBuilder; -import graphql.servlet.core.DefaultGraphQLRootObjectBuilder; -import graphql.servlet.core.GraphQLRootObjectBuilder; -import graphql.servlet.core.internal.GraphQLRequest; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import javax.websocket.Session; -import javax.websocket.server.HandshakeRequest; -import java.util.List; -import java.util.function.Supplier; - -/** - * @author Andrew Potter - */ -public class GraphQLInvocationInputFactory { - - private final Supplier schemaProviderSupplier; - private final Supplier contextBuilderSupplier; - private final Supplier rootObjectBuilderSupplier; - - protected GraphQLInvocationInputFactory(Supplier schemaProviderSupplier, Supplier contextBuilderSupplier, Supplier rootObjectBuilderSupplier) { - this.schemaProviderSupplier = schemaProviderSupplier; - this.contextBuilderSupplier = contextBuilderSupplier; - this.rootObjectBuilderSupplier = rootObjectBuilderSupplier; - } - - public GraphQLSchemaProvider getSchemaProvider() { - return schemaProviderSupplier.get(); - } - - public GraphQLSingleInvocationInput create(GraphQLRequest graphQLRequest, HttpServletRequest request, HttpServletResponse response) { - return create(graphQLRequest, request, response, false); - } - - public GraphQLBatchedInvocationInput create(ContextSetting contextSetting, List graphQLRequests, HttpServletRequest request, - HttpServletResponse response) { - return create(contextSetting, graphQLRequests, request, response, false); - } - - - public GraphQLSingleInvocationInput createReadOnly(GraphQLRequest graphQLRequest, HttpServletRequest request, HttpServletResponse response) { - return create(graphQLRequest, request, response, true); - } - - public GraphQLBatchedInvocationInput createReadOnly(ContextSetting contextSetting, List graphQLRequests, HttpServletRequest request, HttpServletResponse response) { - return create(contextSetting, graphQLRequests, request, response, true); - } - - public GraphQLSingleInvocationInput create(GraphQLRequest graphQLRequest) { - return new GraphQLSingleInvocationInput( - graphQLRequest, - schemaProviderSupplier.get().getSchema(), - contextBuilderSupplier.get().build(), - rootObjectBuilderSupplier.get().build() - ); - } - - private GraphQLSingleInvocationInput create(GraphQLRequest graphQLRequest, HttpServletRequest request, HttpServletResponse response, - boolean readOnly) { - return new GraphQLSingleInvocationInput( - graphQLRequest, - readOnly ? schemaProviderSupplier.get().getReadOnlySchema(request) : schemaProviderSupplier.get().getSchema(request), - contextBuilderSupplier.get().build(request, response), - rootObjectBuilderSupplier.get().build(request) - ); - } - - private GraphQLBatchedInvocationInput create(ContextSetting contextSetting, List graphQLRequests, HttpServletRequest request, - HttpServletResponse response, boolean readOnly) { - return contextSetting.getBatch( - graphQLRequests, - readOnly ? schemaProviderSupplier.get().getReadOnlySchema(request) : schemaProviderSupplier.get().getSchema(request), - () -> contextBuilderSupplier.get().build(request, response), - rootObjectBuilderSupplier.get().build(request) - ); - } - - public GraphQLSingleInvocationInput create(GraphQLRequest graphQLRequest, Session session, HandshakeRequest request) { - return new GraphQLSingleInvocationInput( - graphQLRequest, - schemaProviderSupplier.get().getSchema(request), - contextBuilderSupplier.get().build(session, request), - rootObjectBuilderSupplier.get().build(request) - ); - } - - public GraphQLBatchedInvocationInput create(ContextSetting contextSetting, List graphQLRequest, Session session, HandshakeRequest request) { - return contextSetting.getBatch( - graphQLRequest, - schemaProviderSupplier.get().getSchema(request), - () -> contextBuilderSupplier.get().build(session, request), - rootObjectBuilderSupplier.get().build(request) - ); - } - - public static Builder newBuilder(GraphQLSchema schema) { - return new Builder(new DefaultGraphQLSchemaProvider(schema)); - } - - public static Builder newBuilder(GraphQLSchemaProvider schemaProvider) { - return new Builder(schemaProvider); - } - - public static Builder newBuilder(Supplier schemaProviderSupplier) { - return new Builder(schemaProviderSupplier); - } - - public static class Builder { - private final Supplier schemaProviderSupplier; - private Supplier contextBuilderSupplier = DefaultGraphQLContextBuilder::new; - private Supplier rootObjectBuilderSupplier = DefaultGraphQLRootObjectBuilder::new; - - public Builder(GraphQLSchemaProvider schemaProvider) { - this(() -> schemaProvider); - } - - public Builder(Supplier schemaProviderSupplier) { - this.schemaProviderSupplier = schemaProviderSupplier; - } - - public Builder withGraphQLContextBuilder(GraphQLContextBuilder contextBuilder) { - return withGraphQLContextBuilder(() -> contextBuilder); - } - - public Builder withGraphQLContextBuilder(Supplier contextBuilderSupplier) { - this.contextBuilderSupplier = contextBuilderSupplier; - return this; - } - - public Builder withGraphQLRootObjectBuilder(GraphQLRootObjectBuilder rootObjectBuilder) { - return withGraphQLRootObjectBuilder(() -> rootObjectBuilder); - } - - public Builder withGraphQLRootObjectBuilder(Supplier rootObjectBuilderSupplier) { - this.rootObjectBuilderSupplier = rootObjectBuilderSupplier; - return this; - } - - public GraphQLInvocationInputFactory build() { - return new GraphQLInvocationInputFactory(schemaProviderSupplier, contextBuilderSupplier, rootObjectBuilderSupplier); - } - } -} diff --git a/src/main/java/graphql/servlet/input/GraphQLSingleInvocationInput.java b/src/main/java/graphql/servlet/input/GraphQLSingleInvocationInput.java deleted file mode 100644 index 302155d7..00000000 --- a/src/main/java/graphql/servlet/input/GraphQLSingleInvocationInput.java +++ /dev/null @@ -1,59 +0,0 @@ -package graphql.servlet.input; - -import graphql.ExecutionInput; -import graphql.execution.ExecutionId; -import graphql.schema.GraphQLSchema; -import graphql.servlet.context.GraphQLContext; -import graphql.servlet.core.internal.GraphQLRequest; -import org.dataloader.DataLoaderRegistry; - -import javax.security.auth.Subject; -import java.util.Optional; - -/** - * Represents a single GraphQL execution. - */ -public class GraphQLSingleInvocationInput { - - private final GraphQLSchema schema; - - private final ExecutionInput executionInput; - - private final Optional subject; - - public GraphQLSingleInvocationInput(GraphQLRequest request, GraphQLSchema schema, GraphQLContext context, Object root) { - this.schema = schema; - this.executionInput = createExecutionInput(request, context, root); - subject = context.getSubject(); - } - - /** - * @return the schema to use to execute this query. - */ - public GraphQLSchema getSchema() { - return schema; - } - - /** - * @return a subject to execute the query as. - */ - public Optional getSubject() { - return subject; - } - - private ExecutionInput createExecutionInput(GraphQLRequest graphQLRequest, GraphQLContext context, Object root) { - return ExecutionInput.newExecutionInput() - .query(graphQLRequest.getQuery()) - .operationName(graphQLRequest.getOperationName()) - .context(context) - .root(root) - .variables(graphQLRequest.getVariables()) - .dataLoaderRegistry(context.getDataLoaderRegistry().orElse(new DataLoaderRegistry())) - .executionId(ExecutionId.generate()) - .build(); - } - - public ExecutionInput getExecutionInput() { - return executionInput; - } -} diff --git a/src/main/java/graphql/servlet/input/PerRequestBatchedInvocationInput.java b/src/main/java/graphql/servlet/input/PerRequestBatchedInvocationInput.java deleted file mode 100644 index d68fd0bd..00000000 --- a/src/main/java/graphql/servlet/input/PerRequestBatchedInvocationInput.java +++ /dev/null @@ -1,27 +0,0 @@ -package graphql.servlet.input; - -import graphql.schema.GraphQLSchema; -import graphql.servlet.context.GraphQLContext; -import graphql.servlet.core.internal.GraphQLRequest; - -import java.util.List; -import java.util.function.Supplier; -import java.util.stream.Collectors; - -/** - * A collection of GraphQLSingleInvocationInputs that share a context object. - */ -public class PerRequestBatchedInvocationInput implements GraphQLBatchedInvocationInput { - - private final List inputs; - - public PerRequestBatchedInvocationInput(List requests, GraphQLSchema schema, Supplier contextSupplier, Object root) { - GraphQLContext context = contextSupplier.get(); - inputs = requests.stream().map(request -> new GraphQLSingleInvocationInput(request, schema, context, root)).collect(Collectors.toList()); - } - - @Override - public List getExecutionInputs() { - return inputs; - } -} diff --git a/src/main/java/graphql/servlet/instrumentation/AbstractTrackingApproach.java b/src/main/java/graphql/servlet/instrumentation/AbstractTrackingApproach.java deleted file mode 100644 index 16394254..00000000 --- a/src/main/java/graphql/servlet/instrumentation/AbstractTrackingApproach.java +++ /dev/null @@ -1,225 +0,0 @@ -package graphql.servlet.instrumentation; - -import graphql.ExecutionResult; -import graphql.execution.ExecutionId; -import graphql.execution.ExecutionPath; -import graphql.execution.FieldValueInfo; -import graphql.execution.MergedField; -import graphql.execution.instrumentation.DeferredFieldInstrumentationContext; -import graphql.execution.instrumentation.ExecutionStrategyInstrumentationContext; -import graphql.execution.instrumentation.InstrumentationContext; -import graphql.execution.instrumentation.parameters.InstrumentationDeferredFieldParameters; -import graphql.execution.instrumentation.parameters.InstrumentationExecutionStrategyParameters; -import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters; -import graphql.language.Field; -import graphql.language.Selection; -import graphql.language.SelectionSet; -import graphql.schema.GraphQLOutputType; -import org.dataloader.DataLoaderRegistry; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.Collections; -import java.util.List; -import java.util.Optional; -import java.util.concurrent.CompletableFuture; - -/** - * Handles logic common to tracking approaches. - */ -public abstract class AbstractTrackingApproach implements TrackingApproach { - - private static final Logger log = LoggerFactory.getLogger(AbstractTrackingApproach.class); - - private final DataLoaderRegistry dataLoaderRegistry; - - private final RequestStack stack = new RequestStack(); - - public AbstractTrackingApproach(DataLoaderRegistry dataLoaderRegistry) { - this.dataLoaderRegistry = dataLoaderRegistry; - } - - /** - * @return allows extending classes to modify the stack. - */ - protected RequestStack getStack() { - return stack; - } - - @Override - public ExecutionStrategyInstrumentationContext beginExecutionStrategy(InstrumentationExecutionStrategyParameters parameters) { - ExecutionId executionId = parameters.getExecutionContext().getExecutionId(); - ExecutionPath path = parameters.getExecutionStrategyParameters().getPath(); - int parentLevel = path.getLevel(); - int curLevel = parentLevel + 1; - int fieldCount = parameters.getExecutionStrategyParameters().getFields().size(); - synchronized (stack) { - stack.increaseExpectedFetchCount(executionId, curLevel, fieldCount); - stack.increaseHappenedStrategyCalls(executionId, curLevel); - } - - return new ExecutionStrategyInstrumentationContext() { - @Override - public void onDispatched(CompletableFuture result) { - - } - - @Override - public void onCompleted(ExecutionResult result, Throwable t) { - - } - - @Override - public void onFieldValuesInfo(List fieldValueInfoList) { - synchronized (stack) { - stack.setStatus(executionId, handleOnFieldValuesInfo(fieldValueInfoList, stack, executionId, curLevel)); - if (stack.allReady()) { - dispatchWithoutLocking(); - } - } - } - - @Override - public void onDeferredField(MergedField field) { - // fake fetch count for this field - synchronized (stack) { - stack.increaseFetchCount(executionId, curLevel); - stack.setStatus(executionId, dispatchIfNeeded(stack, executionId, curLevel)); - if (stack.allReady()) { - dispatchWithoutLocking(); - } - } - } - }; - } - - // - // thread safety : called with synchronised(stack) - // - private boolean handleOnFieldValuesInfo(List fieldValueInfoList, RequestStack stack, ExecutionId executionId, int curLevel) { - stack.increaseHappenedOnFieldValueCalls(executionId, curLevel); - int expectedStrategyCalls = 0; - for (FieldValueInfo fieldValueInfo : fieldValueInfoList) { - if (fieldValueInfo.getCompleteValueType() == FieldValueInfo.CompleteValueType.OBJECT) { - expectedStrategyCalls++; - } else if (fieldValueInfo.getCompleteValueType() == FieldValueInfo.CompleteValueType.LIST) { - expectedStrategyCalls += getCountForList(fieldValueInfo); - } - } - stack.increaseExpectedStrategyCalls(executionId, curLevel + 1, expectedStrategyCalls); - return dispatchIfNeeded(stack, executionId, curLevel + 1); - } - - private int getCountForList(FieldValueInfo fieldValueInfo) { - int result = 0; - for (FieldValueInfo cvi : fieldValueInfo.getFieldValueInfos()) { - if (cvi.getCompleteValueType() == FieldValueInfo.CompleteValueType.OBJECT) { - result++; - } else if (cvi.getCompleteValueType() == FieldValueInfo.CompleteValueType.LIST) { - result += getCountForList(cvi); - } - } - return result; - } - - @Override - public DeferredFieldInstrumentationContext beginDeferredField(InstrumentationDeferredFieldParameters parameters) { - ExecutionId executionId = parameters.getExecutionContext().getExecutionId(); - int level = parameters.getExecutionStrategyParameters().getPath().getLevel(); - synchronized (stack) { - stack.clearAndMarkCurrentLevelAsReady(executionId, level); - } - - return new DeferredFieldInstrumentationContext() { - @Override - public void onDispatched(CompletableFuture result) { - - } - - @Override - public void onCompleted(ExecutionResult result, Throwable t) { - } - - @Override - public void onFieldValueInfo(FieldValueInfo fieldValueInfo) { - synchronized (stack) { - stack.setStatus(executionId, handleOnFieldValuesInfo(Collections.singletonList(fieldValueInfo), stack, executionId, level)); - if (stack.allReady()) { - dispatchWithoutLocking(); - } - } - } - }; - } - - @Override - public InstrumentationContext beginFieldFetch(InstrumentationFieldFetchParameters parameters) { - ExecutionId executionId = parameters.getExecutionContext().getExecutionId(); - ExecutionPath path = parameters.getEnvironment().getExecutionStepInfo().getPath(); - int level = path.getLevel(); - return new InstrumentationContext() { - - @Override - public void onDispatched(CompletableFuture result) { - synchronized (stack) { - stack.increaseFetchCount(executionId, level); - stack.setStatus(executionId, dispatchIfNeeded(stack, executionId, level)); - - if (stack.allReady()) { - dispatchWithoutLocking(); - } - } - } - - @Override - public void onCompleted(Object result, Throwable t) { - } - }; - } - - @Override - public void removeTracking(ExecutionId executionId) { - synchronized (stack) { - stack.removeExecution(executionId); - if (stack.allReady()) { - dispatchWithoutLocking(); - } - } - } - - - // - // thread safety : called with synchronised(stack) - // - private boolean dispatchIfNeeded(RequestStack stack, ExecutionId executionId, int level) { - if (levelReady(stack, executionId, level)) { - return stack.dispatchIfNotDispatchedBefore(executionId, level); - } - return false; - } - - // - // thread safety : called with synchronised(stack) - // - private boolean levelReady(RequestStack stack, ExecutionId executionId, int level) { - if (level == 1) { - // level 1 is special: there is only one strategy call and that's it - return stack.allFetchesHappened(executionId, 1); - } - return (levelReady(stack, executionId, level - 1) && stack.allOnFieldCallsHappened(executionId, level - 1) - && stack.allStrategyCallsHappened(executionId, level) && stack.allFetchesHappened(executionId, level)); - } - - @Override - public void dispatch() { - synchronized (stack) { - dispatchWithoutLocking(); - } - } - - private void dispatchWithoutLocking() { - log.debug("Dispatching data loaders ({})", dataLoaderRegistry.getKeys()); - dataLoaderRegistry.dispatchAll(); - stack.allReset(); - } -} diff --git a/src/test/groovy/graphql/servlet/TestBatchInputPreProcessor.java b/src/test/groovy/graphql/servlet/TestBatchInputPreProcessor.java deleted file mode 100644 index ef271181..00000000 --- a/src/test/groovy/graphql/servlet/TestBatchInputPreProcessor.java +++ /dev/null @@ -1,25 +0,0 @@ -package graphql.servlet; - -import graphql.servlet.input.BatchInputPreProcessResult; -import graphql.servlet.input.BatchInputPreProcessor; -import graphql.servlet.input.GraphQLBatchedInvocationInput; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -public class TestBatchInputPreProcessor implements BatchInputPreProcessor { - - public static String BATCH_ERROR_MESSAGE = "Batch limit exceeded"; - - @Override - public BatchInputPreProcessResult preProcessBatch(GraphQLBatchedInvocationInput batchedInvocationInput, HttpServletRequest request, - HttpServletResponse response) { - BatchInputPreProcessResult preProcessResult; - if (batchedInvocationInput.getExecutionInputs().size() > 2) { - preProcessResult = new BatchInputPreProcessResult(400, BATCH_ERROR_MESSAGE); - } else { - preProcessResult = new BatchInputPreProcessResult(batchedInvocationInput); - } - return preProcessResult; - } -}