├── lombok.config ├── graphql-java-kickstart ├── bnd.bnd ├── src │ └── main │ │ └── java │ │ └── graphql │ │ └── kickstart │ │ └── execution │ │ ├── subscriptions │ │ ├── SubscriptionConnectionListener.java │ │ ├── apollo │ │ │ ├── SubscriptionCommand.java │ │ │ ├── SubscriptionStopCommand.java │ │ │ ├── SubscriptionConnectionTerminateCommand.java │ │ │ ├── ApolloSubscriptionConnectionListener.java │ │ │ ├── ApolloSubscriptionSession.java │ │ │ ├── SubscriptionConnectionInitCommand.java │ │ │ ├── ApolloSubscriptionConsumer.java │ │ │ ├── KeepAliveSubscriptionConnectionListener.java │ │ │ ├── ApolloCommandProvider.java │ │ │ ├── OperationMessage.java │ │ │ ├── ApolloSubscriptionKeepAliveRunner.java │ │ │ └── SubscriptionStartCommand.java │ │ ├── GraphQLSubscriptionInvocationInputFactory.java │ │ ├── SubscriptionException.java │ │ ├── SubscriptionProtocolFactory.java │ │ ├── AtomicSubscriptionSubscription.java │ │ ├── SessionSubscriptions.java │ │ ├── SubscriptionSession.java │ │ ├── GraphQLSubscriptionMapper.java │ │ ├── SessionSubscriber.java │ │ └── DefaultSubscriptionSession.java │ │ ├── GraphQLRootObjectBuilder.java │ │ ├── input │ │ ├── GraphQLInvocationInput.java │ │ ├── GraphQLBatchedInvocationInput.java │ │ ├── PerRequestBatchedInvocationInput.java │ │ ├── PerQueryBatchedInvocationInput.java │ │ └── GraphQLSingleInvocationInput.java │ │ ├── config │ │ ├── GraphQLBuilderConfigurer.java │ │ ├── ObjectMapperProvider.java │ │ ├── InstrumentationProvider.java │ │ ├── GraphQLServletObjectMapperConfigurer.java │ │ ├── ExecutionStrategyProvider.java │ │ ├── GraphQLSchemaProvider.java │ │ ├── DefaultGraphQLSchemaProvider.java │ │ ├── DefaultExecutionStrategyProvider.java │ │ └── ConfiguringObjectMapperProvider.java │ │ ├── context │ │ ├── GraphQLContextBuilder.java │ │ ├── ContextSettingNotConfiguredException.java │ │ ├── DefaultGraphQLContextBuilder.java │ │ ├── GraphQLKickstartContext.java │ │ ├── DefaultGraphQLContext.java │ │ └── ContextSetting.java │ │ ├── DefaultGraphQLRootObjectBuilder.java │ │ ├── instrumentation │ │ ├── TrackingApproachException.java │ │ ├── NoOpInstrumentationProvider.java │ │ ├── RequestLevelTrackingApproach.java │ │ ├── FieldLevelTrackingApproach.java │ │ ├── DataLoaderDispatcherInstrumentationState.java │ │ └── TrackingApproach.java │ │ ├── GraphQLInvokerProxy.java │ │ ├── ObjectMapDeserializationException.java │ │ ├── StringUtils.java │ │ ├── error │ │ ├── GraphQLErrorHandler.java │ │ ├── DefaultGraphQLServletObjectMapperConfigurer.java │ │ ├── GenericGraphQLError.java │ │ ├── RenderableNonNullableFieldWasNullError.java │ │ └── DefaultGraphQLErrorHandler.java │ │ ├── StaticGraphQLRootObjectBuilder.java │ │ ├── GraphQLSingleQueryResult.java │ │ ├── GraphQLBatchedQueryResult.java │ │ ├── GraphQLErrorQueryResult.java │ │ ├── FutureErrorExecutionResult.java │ │ ├── FutureSingleExecutionResult.java │ │ ├── FutureBatchedExecutionResult.java │ │ ├── BatchedDataLoaderGraphQLBuilder.java │ │ ├── VariablesDeserializer.java │ │ ├── ExtensionsDeserializer.java │ │ ├── FutureExecutionResult.java │ │ ├── DecoratedExecutionResult.java │ │ ├── GraphQLQueryResult.java │ │ ├── ObjectMapDeserializeHelper.java │ │ ├── OperationNameExtractor.java │ │ └── GraphQLRequest.java └── build.gradle ├── renovate.json ├── settings.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── graphql-java-servlet ├── src │ ├── main │ │ └── java │ │ │ └── graphql │ │ │ └── kickstart │ │ │ └── servlet │ │ │ ├── osgi │ │ │ ├── GraphQLProvider.java │ │ │ ├── GraphQLCodeRegistryProvider.java │ │ │ ├── GraphQLTypesProvider.java │ │ │ ├── GraphQLFieldProvider.java │ │ │ ├── GraphQLDirectiveProvider.java │ │ │ ├── GraphQLMutationProvider.java │ │ │ ├── GraphQLSubscriptionProvider.java │ │ │ └── GraphQLQueryProvider.java │ │ │ ├── AsyncTaskDecorator.java │ │ │ ├── PartIOException.java │ │ │ ├── core │ │ │ ├── GraphQLMBean.java │ │ │ ├── internal │ │ │ │ ├── VariableMapException.java │ │ │ │ ├── GraphQLThreadFactory.java │ │ │ │ └── VariableMapper.java │ │ │ ├── GraphQLServletRootObjectBuilder.java │ │ │ ├── DefaultGraphQLRootObjectBuilder.java │ │ │ └── GraphQLServletListener.java │ │ │ ├── InvocationInputParseException.java │ │ │ ├── QueryResponseWriter.java │ │ │ ├── StaticDataPublisher.java │ │ │ ├── QueryResponseWriterFactory.java │ │ │ ├── subscriptions │ │ │ ├── WebSocketSubscriptionProtocolFactory.java │ │ │ ├── WebSocketSubscriptionSession.java │ │ │ ├── WebSocketSendSubscriber.java │ │ │ ├── FallbackSubscriptionProtocolFactory.java │ │ │ └── FallbackSubscriptionConsumer.java │ │ │ ├── AsyncTimeoutListener.java │ │ │ ├── HttpRequestInvoker.java │ │ │ ├── context │ │ │ ├── GraphQLWebSocketContext.java │ │ │ ├── GraphQLServletContext.java │ │ │ ├── GraphQLServletContextBuilder.java │ │ │ ├── DefaultGraphQLServletContextBuilder.java │ │ │ └── DefaultGraphQLWebSocketContext.java │ │ │ ├── ConfiguredGraphQLHttpServlet.java │ │ │ ├── ErrorQueryResponseWriter.java │ │ │ ├── HttpRequestHandler.java │ │ │ ├── AsyncTaskExecutor.java │ │ │ ├── GraphQLHttpServlet.java │ │ │ ├── OsgiGraphQLHttpServletConfiguration.java │ │ │ ├── input │ │ │ ├── NoOpBatchInputPreProcessor.java │ │ │ ├── BatchInputPreProcessor.java │ │ │ └── BatchInputPreProcessResult.java │ │ │ ├── AbstractGraphQLInvocationInputParser.java │ │ │ ├── SubscriptionAsyncListener.java │ │ │ ├── config │ │ │ ├── DefaultGraphQLSchemaServletProvider.java │ │ │ └── GraphQLSchemaServletProvider.java │ │ │ ├── SingleQueryResponseWriter.java │ │ │ ├── cache │ │ │ ├── CachingQueryResponseWriterFactory.java │ │ │ ├── GraphQLResponseCacheManager.java │ │ │ ├── CacheReader.java │ │ │ ├── CachingHttpRequestInvoker.java │ │ │ ├── CachedResponse.java │ │ │ └── CachingQueryResponseWriter.java │ │ │ ├── apollo │ │ │ ├── ApolloWebSocketSubscriptionSession.java │ │ │ ├── ApolloScalars.java │ │ │ └── ApolloWebSocketSubscriptionProtocolFactory.java │ │ │ ├── QueryResponseWriterFactoryImpl.java │ │ │ ├── GraphQLInvocationInputParser.java │ │ │ ├── ExecutionResultSubscriber.java │ │ │ ├── GraphQLPostInvocationInputParser.java │ │ │ ├── SingleAsynchronousQueryResponseWriter.java │ │ │ ├── BatchedQueryResponseWriter.java │ │ │ ├── ListenerHandler.java │ │ │ ├── HttpRequestHandlerImpl.java │ │ │ ├── AbstractGraphQLHttpServlet.java │ │ │ └── GraphQLGetInvocationInputParser.java │ └── test │ │ └── groovy │ │ └── graphql │ │ └── kickstart │ │ └── servlet │ │ ├── TestException.groovy │ │ ├── PartIOExceptionTest.groovy │ │ ├── TestGraphQLErrorException.groovy │ │ ├── TestBatchInputPreProcessor.java │ │ ├── SingleAsynchronousQueryResponseWriterTest.groovy │ │ ├── GraphQLServletListenerSpec.groovy │ │ ├── TestMultipartPart.groovy │ │ ├── SingleQueryResponseWriterTest.groovy │ │ ├── BatchedQueryResponseWriterTest.groovy │ │ ├── RequestTester.groovy │ │ └── cache │ │ └── CacheReaderTest.groovy ├── bnd.bnd └── build.gradle ├── .idea └── codeStyles │ └── codeStyleConfig.xml ├── .gitignore ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── feature_request.md │ └── bug_report.md ├── release.sh ├── add-javax-suffix.sh ├── replaceJakartaWithJavax.sh ├── tag-release.sh └── workflows │ └── pull-request.yml ├── examples └── osgi │ ├── buildAndRun.sh │ ├── apache-karaf-feature │ └── src │ │ └── main │ │ └── feature │ │ └── feature.xml │ ├── pom.xml │ └── providers │ ├── src │ └── main │ │ └── java │ │ └── graphql │ │ └── servlet │ │ └── examples │ │ └── osgi │ │ └── ExampleGraphQLProvider.java │ └── pom.xml ├── LICENSE ├── gradle.properties ├── CONTRIBUTING.md └── gradlew.bat /lombok.config: -------------------------------------------------------------------------------- 1 | lombok.addLombokGeneratedAnnotation = true 2 | -------------------------------------------------------------------------------- /graphql-java-kickstart/bnd.bnd: -------------------------------------------------------------------------------- 1 | Export-Package: graphql.kickstart.* 2 | Import-Package: !lombok,* 3 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "github>graphql-java-kickstart/renovate-config" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'graphql-java-servlet' 2 | 3 | include ':graphql-java-kickstart' 4 | include ':graphql-java-servlet' 5 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphql-java-kickstart/graphql-java-servlet/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /graphql-java-servlet/src/main/java/graphql/kickstart/servlet/osgi/GraphQLProvider.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.servlet.osgi; 2 | 3 | public interface GraphQLProvider {} 4 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /graphql-java-servlet/bnd.bnd: -------------------------------------------------------------------------------- 1 | Export-Package: graphql.kickstart.servlet.* 2 | Import-Package: !lombok,* 3 | Require-Capability: osgi.extender; 4 | filter:="(&(osgi.extender=osgi.component)(version>=1.3)(!(version>=2.0)))" 5 | -------------------------------------------------------------------------------- /graphql-java-servlet/src/test/groovy/graphql/kickstart/servlet/TestException.groovy: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.servlet 2 | 3 | /** 4 | * @author Andrew Potter 5 | */ 6 | class TestException extends RuntimeException { 7 | } 8 | -------------------------------------------------------------------------------- /graphql-java-servlet/src/main/java/graphql/kickstart/servlet/AsyncTaskDecorator.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.servlet; 2 | 3 | public interface AsyncTaskDecorator { 4 | 5 | Runnable decorate(Runnable runnable); 6 | 7 | } 8 | -------------------------------------------------------------------------------- /graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/SubscriptionConnectionListener.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.execution.subscriptions; 2 | 3 | /** Marker interface */ 4 | public interface SubscriptionConnectionListener {} 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle/ 2 | build/ 3 | *.iml 4 | *.ipr 5 | *.iws 6 | .idea/* 7 | !.idea/codeStyles/ 8 | target/ 9 | /out/ 10 | .classpath 11 | .project 12 | .settings 13 | bin 14 | .DS_Store 15 | /**/out/ 16 | /projectFilesBackup/.idea/workspace.xml 17 | -------------------------------------------------------------------------------- /graphql-java-kickstart/src/main/java/graphql/kickstart/execution/GraphQLRootObjectBuilder.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.execution; 2 | 3 | public interface GraphQLRootObjectBuilder { 4 | 5 | /** @return the graphql root object */ 6 | Object build(); 7 | } 8 | -------------------------------------------------------------------------------- /graphql-java-kickstart/src/main/java/graphql/kickstart/execution/input/GraphQLInvocationInput.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.execution.input; 2 | 3 | import java.util.List; 4 | 5 | public interface GraphQLInvocationInput { 6 | List getQueries(); 7 | } 8 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Question 4 | url: https://github.com/graphql-java-kickstart/graphql-java-servlet/discussions 5 | about: Anything you are not sure about? Ask the community in Discussions! 6 | -------------------------------------------------------------------------------- /graphql-java-kickstart/src/main/java/graphql/kickstart/execution/config/GraphQLBuilderConfigurer.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.execution.config; 2 | 3 | import graphql.GraphQL; 4 | 5 | public interface GraphQLBuilderConfigurer { 6 | 7 | void configure(GraphQL.Builder builder); 8 | } 9 | -------------------------------------------------------------------------------- /graphql-java-kickstart/src/main/java/graphql/kickstart/execution/context/GraphQLContextBuilder.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.execution.context; 2 | 3 | public interface GraphQLContextBuilder { 4 | 5 | /** @return the graphql context */ 6 | GraphQLKickstartContext build(); 7 | } 8 | -------------------------------------------------------------------------------- /graphql-java-kickstart/src/main/java/graphql/kickstart/execution/config/ObjectMapperProvider.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.execution.config; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | 5 | public interface ObjectMapperProvider { 6 | 7 | ObjectMapper provide(); 8 | } 9 | -------------------------------------------------------------------------------- /graphql-java-servlet/src/main/java/graphql/kickstart/servlet/PartIOException.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.servlet; 2 | 3 | public class PartIOException extends RuntimeException { 4 | 5 | public PartIOException(String message, Throwable cause) { 6 | super(message, cause); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /graphql-java-servlet/src/main/java/graphql/kickstart/servlet/core/GraphQLMBean.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.servlet.core; 2 | 3 | public interface GraphQLMBean { 4 | 5 | String[] getQueries(); 6 | 7 | String[] getMutations(); 8 | 9 | String executeQuery(String query); 10 | } 11 | -------------------------------------------------------------------------------- /graphql-java-servlet/src/main/java/graphql/kickstart/servlet/core/internal/VariableMapException.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.servlet.core.internal; 2 | 3 | public class VariableMapException extends RuntimeException { 4 | 5 | VariableMapException(String message) { 6 | super(message); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /graphql-java-kickstart/src/main/java/graphql/kickstart/execution/config/InstrumentationProvider.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.execution.config; 2 | 3 | import graphql.execution.instrumentation.Instrumentation; 4 | 5 | public interface InstrumentationProvider { 6 | 7 | Instrumentation getInstrumentation(); 8 | } 9 | -------------------------------------------------------------------------------- /examples/osgi/buildAndRun.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | mvn clean install 3 | pushd apache-karaf-package/target || exit 1 4 | tar zxvf graphql-java-servlet-osgi-examples-apache-karaf-package-10.1.0.tar.gz 5 | cd graphql-java-servlet-osgi-examples-apache-karaf-package-10.1.0/bin || exit 1 6 | ./karaf debug 7 | popd || exit 1 8 | -------------------------------------------------------------------------------- /graphql-java-servlet/src/main/java/graphql/kickstart/servlet/osgi/GraphQLCodeRegistryProvider.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.servlet.osgi; 2 | 3 | import graphql.schema.GraphQLCodeRegistry; 4 | 5 | public interface GraphQLCodeRegistryProvider extends GraphQLProvider { 6 | 7 | GraphQLCodeRegistry getCodeRegistry(); 8 | } 9 | -------------------------------------------------------------------------------- /graphql-java-kickstart/src/main/java/graphql/kickstart/execution/DefaultGraphQLRootObjectBuilder.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.execution; 2 | 3 | public class DefaultGraphQLRootObjectBuilder extends StaticGraphQLRootObjectBuilder { 4 | 5 | public DefaultGraphQLRootObjectBuilder() { 6 | super(new Object()); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /graphql-java-kickstart/src/main/java/graphql/kickstart/execution/instrumentation/TrackingApproachException.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.execution.instrumentation; 2 | 3 | public class TrackingApproachException extends RuntimeException { 4 | 5 | TrackingApproachException(String message) { 6 | super(message); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /graphql-java-servlet/src/main/java/graphql/kickstart/servlet/InvocationInputParseException.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.servlet; 2 | 3 | public class InvocationInputParseException extends RuntimeException { 4 | 5 | public InvocationInputParseException(Throwable t) { 6 | super("Request parsing failed", t); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /graphql-java-servlet/src/main/java/graphql/kickstart/servlet/osgi/GraphQLTypesProvider.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.servlet.osgi; 2 | 3 | import graphql.schema.GraphQLType; 4 | import java.util.Collection; 5 | 6 | public interface GraphQLTypesProvider extends GraphQLProvider { 7 | 8 | Collection getTypes(); 9 | } 10 | -------------------------------------------------------------------------------- /graphql-java-kickstart/src/main/java/graphql/kickstart/execution/context/ContextSettingNotConfiguredException.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.execution.context; 2 | 3 | public class ContextSettingNotConfiguredException extends RuntimeException { 4 | 5 | ContextSettingNotConfiguredException() { 6 | super("Unconfigured context setting type"); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /graphql-java-servlet/src/main/java/graphql/kickstart/servlet/osgi/GraphQLFieldProvider.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.servlet.osgi; 2 | 3 | import graphql.schema.GraphQLFieldDefinition; 4 | import java.util.Collection; 5 | 6 | public interface GraphQLFieldProvider extends GraphQLProvider { 7 | 8 | Collection getFields(); 9 | } 10 | -------------------------------------------------------------------------------- /graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/apollo/SubscriptionCommand.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.execution.subscriptions.apollo; 2 | 3 | import graphql.kickstart.execution.subscriptions.SubscriptionSession; 4 | 5 | interface SubscriptionCommand { 6 | 7 | void apply(SubscriptionSession session, OperationMessage message); 8 | } 9 | -------------------------------------------------------------------------------- /graphql-java-kickstart/src/main/java/graphql/kickstart/execution/config/GraphQLServletObjectMapperConfigurer.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.execution.config; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | 5 | /** @author Andrew Potter */ 6 | public interface GraphQLServletObjectMapperConfigurer { 7 | 8 | void configure(ObjectMapper mapper); 9 | } 10 | -------------------------------------------------------------------------------- /graphql-java-kickstart/src/main/java/graphql/kickstart/execution/context/DefaultGraphQLContextBuilder.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.execution.context; 2 | 3 | /** Returns an empty context. */ 4 | public class DefaultGraphQLContextBuilder implements GraphQLContextBuilder { 5 | 6 | @Override 7 | public GraphQLKickstartContext build() { 8 | return new DefaultGraphQLContext(); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.github/release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -ev 3 | 4 | FLAVOUR="${1}" 5 | 6 | removeSnapshots() { 7 | sed -i 's/-SNAPSHOT//' gradle.properties 8 | } 9 | 10 | echo "Publishing release to Maven Central" 11 | removeSnapshots 12 | 13 | if [ "${FLAVOUR}" == 'javax' ]; then 14 | .github/add-javax-suffix.sh 15 | fi 16 | 17 | ./gradlew clean build publishToSonatype closeAndReleaseSonatypeStagingRepository 18 | -------------------------------------------------------------------------------- /graphql-java-servlet/src/test/groovy/graphql/kickstart/servlet/PartIOExceptionTest.groovy: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.servlet 2 | 3 | import spock.lang.Specification 4 | 5 | class PartIOExceptionTest extends Specification { 6 | 7 | def "constructs"() { 8 | when: 9 | def e = new PartIOException("some message", new IOException()) 10 | then: 11 | e instanceof RuntimeException 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /graphql-java-servlet/src/main/java/graphql/kickstart/servlet/QueryResponseWriter.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.servlet; 2 | 3 | import java.io.IOException; 4 | import jakarta.servlet.http.HttpServletRequest; 5 | import jakarta.servlet.http.HttpServletResponse; 6 | 7 | public interface QueryResponseWriter { 8 | 9 | void write(HttpServletRequest request, HttpServletResponse response) throws IOException; 10 | } 11 | -------------------------------------------------------------------------------- /graphql-java-kickstart/src/main/java/graphql/kickstart/execution/GraphQLInvokerProxy.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.execution; 2 | 3 | import graphql.ExecutionInput; 4 | import graphql.ExecutionResult; 5 | import graphql.GraphQL; 6 | import java.util.concurrent.CompletableFuture; 7 | 8 | public interface GraphQLInvokerProxy { 9 | 10 | CompletableFuture executeAsync(GraphQL graphQL, ExecutionInput executionInput); 11 | } 12 | -------------------------------------------------------------------------------- /graphql-java-kickstart/src/main/java/graphql/kickstart/execution/ObjectMapDeserializationException.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.execution; 2 | 3 | public class ObjectMapDeserializationException extends RuntimeException { 4 | 5 | ObjectMapDeserializationException(String message) { 6 | super(message); 7 | } 8 | 9 | ObjectMapDeserializationException(String message, Throwable cause) { 10 | super(message, cause); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /graphql-java-kickstart/src/main/java/graphql/kickstart/execution/config/ExecutionStrategyProvider.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.execution.config; 2 | 3 | import graphql.execution.ExecutionStrategy; 4 | 5 | public interface ExecutionStrategyProvider { 6 | 7 | ExecutionStrategy getQueryExecutionStrategy(); 8 | 9 | ExecutionStrategy getMutationExecutionStrategy(); 10 | 11 | ExecutionStrategy getSubscriptionExecutionStrategy(); 12 | } 13 | -------------------------------------------------------------------------------- /graphql-java-servlet/src/main/java/graphql/kickstart/servlet/StaticDataPublisher.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.servlet; 2 | 3 | import graphql.execution.reactive.SingleSubscriberPublisher; 4 | import org.reactivestreams.Publisher; 5 | 6 | class StaticDataPublisher extends SingleSubscriberPublisher implements Publisher { 7 | 8 | StaticDataPublisher(T data) { 9 | super(); 10 | offer(data); 11 | noMoreData(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /graphql-java-servlet/src/main/java/graphql/kickstart/servlet/osgi/GraphQLDirectiveProvider.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.servlet.osgi; 2 | 3 | import graphql.schema.GraphQLDirective; 4 | import java.util.Collection; 5 | 6 | 7 | public interface GraphQLDirectiveProvider extends GraphQLProvider { 8 | 9 | /** @return A collection of directive definitions that will be added to the schema. */ 10 | Collection getDirectives(); 11 | 12 | } 13 | -------------------------------------------------------------------------------- /graphql-java-servlet/src/main/java/graphql/kickstart/servlet/osgi/GraphQLMutationProvider.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.servlet.osgi; 2 | 3 | import graphql.schema.GraphQLFieldDefinition; 4 | import java.util.Collection; 5 | 6 | public interface GraphQLMutationProvider extends GraphQLFieldProvider { 7 | 8 | Collection getMutations(); 9 | 10 | default Collection getFields() { 11 | return getMutations(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /graphql-java-kickstart/src/main/java/graphql/kickstart/execution/StringUtils.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.execution; 2 | 3 | import lombok.AccessLevel; 4 | import lombok.NoArgsConstructor; 5 | 6 | @NoArgsConstructor(access = AccessLevel.PRIVATE) 7 | class StringUtils { 8 | 9 | static boolean isNotEmpty(CharSequence cs) { 10 | return !isEmpty(cs); 11 | } 12 | 13 | static boolean isEmpty(final CharSequence cs) { 14 | return cs == null || cs.length() == 0; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /graphql-java-kickstart/src/main/java/graphql/kickstart/execution/error/GraphQLErrorHandler.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.execution.error; 2 | 3 | import graphql.GraphQLError; 4 | import java.util.List; 5 | 6 | /** @author Andrew Potter */ 7 | public interface GraphQLErrorHandler { 8 | 9 | default boolean errorsPresent(List errors) { 10 | return errors != null && !errors.isEmpty(); 11 | } 12 | 13 | List processErrors(List errors); 14 | } 15 | -------------------------------------------------------------------------------- /graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/GraphQLSubscriptionInvocationInputFactory.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.execution.subscriptions; 2 | 3 | import graphql.kickstart.execution.GraphQLRequest; 4 | import graphql.kickstart.execution.input.GraphQLSingleInvocationInput; 5 | 6 | public interface GraphQLSubscriptionInvocationInputFactory { 7 | 8 | GraphQLSingleInvocationInput create(GraphQLRequest graphQLRequest, SubscriptionSession session); 9 | } 10 | -------------------------------------------------------------------------------- /graphql-java-servlet/src/main/java/graphql/kickstart/servlet/osgi/GraphQLSubscriptionProvider.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.servlet.osgi; 2 | 3 | import graphql.schema.GraphQLFieldDefinition; 4 | import java.util.Collection; 5 | 6 | public interface GraphQLSubscriptionProvider extends GraphQLFieldProvider { 7 | 8 | Collection getSubscriptions(); 9 | 10 | default Collection getFields() { 11 | return getSubscriptions(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /graphql-java-servlet/src/main/java/graphql/kickstart/servlet/QueryResponseWriterFactory.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.servlet; 2 | 3 | import graphql.kickstart.execution.GraphQLQueryResult; 4 | import graphql.kickstart.execution.input.GraphQLInvocationInput; 5 | 6 | public interface QueryResponseWriterFactory { 7 | 8 | QueryResponseWriter createWriter( 9 | GraphQLInvocationInput invocationInput, 10 | GraphQLQueryResult queryResult, 11 | GraphQLConfiguration configuration); 12 | } 13 | -------------------------------------------------------------------------------- /graphql-java-servlet/src/main/java/graphql/kickstart/servlet/core/GraphQLServletRootObjectBuilder.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.servlet.core; 2 | 3 | import graphql.kickstart.execution.GraphQLRootObjectBuilder; 4 | import jakarta.servlet.http.HttpServletRequest; 5 | import jakarta.websocket.server.HandshakeRequest; 6 | 7 | public interface GraphQLServletRootObjectBuilder extends GraphQLRootObjectBuilder { 8 | 9 | Object build(HttpServletRequest req); 10 | 11 | Object build(HandshakeRequest req); 12 | } 13 | -------------------------------------------------------------------------------- /graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/SubscriptionException.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.execution.subscriptions; 2 | 3 | public class SubscriptionException extends Exception { 4 | 5 | private final transient Object payload; 6 | 7 | public SubscriptionException() { 8 | this(null); 9 | } 10 | 11 | public SubscriptionException(Object payload) { 12 | this.payload = payload; 13 | } 14 | 15 | public Object getPayload() { 16 | return payload; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /graphql-java-servlet/src/main/java/graphql/kickstart/servlet/subscriptions/WebSocketSubscriptionProtocolFactory.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.servlet.subscriptions; 2 | 3 | import graphql.kickstart.execution.subscriptions.SubscriptionSession; 4 | import java.util.function.Consumer; 5 | import jakarta.websocket.Session; 6 | 7 | public interface WebSocketSubscriptionProtocolFactory { 8 | 9 | Consumer createConsumer(SubscriptionSession session); 10 | 11 | SubscriptionSession createSession(Session session); 12 | } 13 | -------------------------------------------------------------------------------- /graphql-java-servlet/src/main/java/graphql/kickstart/servlet/AsyncTimeoutListener.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.servlet; 2 | 3 | import java.io.IOException; 4 | import jakarta.servlet.AsyncEvent; 5 | import jakarta.servlet.AsyncListener; 6 | 7 | interface AsyncTimeoutListener extends AsyncListener { 8 | 9 | default void onComplete(AsyncEvent event) throws IOException {} 10 | 11 | default void onError(AsyncEvent event) throws IOException {} 12 | 13 | default void onStartAsync(AsyncEvent event) throws IOException {} 14 | } 15 | -------------------------------------------------------------------------------- /graphql-java-servlet/src/main/java/graphql/kickstart/servlet/HttpRequestInvoker.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.servlet; 2 | 3 | import graphql.kickstart.execution.input.GraphQLInvocationInput; 4 | import jakarta.servlet.http.HttpServletRequest; 5 | import jakarta.servlet.http.HttpServletResponse; 6 | 7 | public interface HttpRequestInvoker { 8 | 9 | void execute( 10 | GraphQLInvocationInput invocationInput, 11 | HttpServletRequest request, 12 | HttpServletResponse response, 13 | ListenerHandler listenerHandler); 14 | } 15 | -------------------------------------------------------------------------------- /graphql-java-servlet/src/main/java/graphql/kickstart/servlet/osgi/GraphQLQueryProvider.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.servlet.osgi; 2 | 3 | import graphql.schema.GraphQLFieldDefinition; 4 | import java.util.Collection; 5 | 6 | /** This interface is used by OSGi bundles to plugin new field into the root query type */ 7 | public interface GraphQLQueryProvider extends GraphQLProvider { 8 | 9 | /** @return a collection of field definitions that will be added to the root query type. */ 10 | Collection getQueries(); 11 | } 12 | -------------------------------------------------------------------------------- /graphql-java-kickstart/src/main/java/graphql/kickstart/execution/StaticGraphQLRootObjectBuilder.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.execution; 2 | 3 | public class StaticGraphQLRootObjectBuilder implements GraphQLRootObjectBuilder { 4 | 5 | private final Object rootObject; 6 | 7 | public StaticGraphQLRootObjectBuilder(Object rootObject) { 8 | this.rootObject = rootObject; 9 | } 10 | 11 | @Override 12 | public Object build() { 13 | return rootObject; 14 | } 15 | 16 | protected Object getRootObject() { 17 | return rootObject; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /graphql-java-kickstart/src/main/java/graphql/kickstart/execution/GraphQLSingleQueryResult.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.execution; 2 | 3 | import lombok.Getter; 4 | import lombok.RequiredArgsConstructor; 5 | 6 | @RequiredArgsConstructor 7 | class GraphQLSingleQueryResult implements GraphQLQueryResult { 8 | 9 | @Getter private final DecoratedExecutionResult result; 10 | 11 | @Override 12 | public boolean isBatched() { 13 | return false; 14 | } 15 | 16 | @Override 17 | public boolean isAsynchronous() { 18 | return result.isAsynchronous(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /graphql-java-servlet/src/main/java/graphql/kickstart/servlet/context/GraphQLWebSocketContext.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.servlet.context; 2 | 3 | import graphql.kickstart.execution.context.GraphQLKickstartContext; 4 | import jakarta.websocket.Session; 5 | import jakarta.websocket.server.HandshakeRequest; 6 | 7 | /** @deprecated Use {@link graphql.kickstart.execution.context.GraphQLKickstartContext} instead */ 8 | public interface GraphQLWebSocketContext extends GraphQLKickstartContext { 9 | 10 | Session getSession(); 11 | 12 | HandshakeRequest getHandshakeRequest(); 13 | } 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2016 Yurii Rashkovskii and Contributors 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | -------------------------------------------------------------------------------- /graphql-java-kickstart/src/main/java/graphql/kickstart/execution/config/GraphQLSchemaProvider.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.execution.config; 2 | 3 | import graphql.schema.GraphQLObjectType; 4 | import graphql.schema.GraphQLSchema; 5 | 6 | public interface GraphQLSchemaProvider { 7 | 8 | static GraphQLSchema copyReadOnly(GraphQLSchema schema) { 9 | return GraphQLSchema.newSchema(schema).mutation((GraphQLObjectType) null).build(); 10 | } 11 | 12 | /** @return a schema for handling mbean calls. */ 13 | GraphQLSchema getSchema(); 14 | 15 | GraphQLSchema getReadOnlySchema(); 16 | } 17 | -------------------------------------------------------------------------------- /graphql-java-servlet/src/main/java/graphql/kickstart/servlet/ConfiguredGraphQLHttpServlet.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.servlet; 2 | 3 | import java.util.Objects; 4 | 5 | class ConfiguredGraphQLHttpServlet extends GraphQLHttpServlet { 6 | 7 | private final GraphQLConfiguration configuration; 8 | 9 | ConfiguredGraphQLHttpServlet(GraphQLConfiguration configuration) { 10 | this.configuration = Objects.requireNonNull(configuration, "configuration is required"); 11 | } 12 | 13 | @Override 14 | protected GraphQLConfiguration getConfiguration() { 15 | return configuration; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /graphql-java-kickstart/src/main/java/graphql/kickstart/execution/instrumentation/NoOpInstrumentationProvider.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.execution.instrumentation; 2 | 3 | import graphql.execution.instrumentation.Instrumentation; 4 | import graphql.execution.instrumentation.SimplePerformantInstrumentation; 5 | import graphql.kickstart.execution.config.InstrumentationProvider; 6 | 7 | public class NoOpInstrumentationProvider implements InstrumentationProvider { 8 | 9 | @Override 10 | public Instrumentation getInstrumentation() { 11 | return SimplePerformantInstrumentation.INSTANCE; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /graphql-java-kickstart/src/main/java/graphql/kickstart/execution/GraphQLBatchedQueryResult.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.execution; 2 | 3 | import graphql.ExecutionResult; 4 | import java.util.List; 5 | import lombok.Getter; 6 | import lombok.RequiredArgsConstructor; 7 | 8 | @RequiredArgsConstructor 9 | class GraphQLBatchedQueryResult implements GraphQLQueryResult { 10 | 11 | @Getter private final List results; 12 | 13 | @Override 14 | public boolean isBatched() { 15 | return true; 16 | } 17 | 18 | @Override 19 | public boolean isAsynchronous() { 20 | return false; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/osgi/apache-karaf-feature/src/main/feature/feature.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | scr 7 | war 8 | http 9 | 10 | 11 | -------------------------------------------------------------------------------- /graphql-java-kickstart/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'biz.aQute.bnd.builder' 2 | 3 | jar { 4 | bndfile = 'bnd.bnd' 5 | } 6 | 7 | apply plugin: 'java-library-distribution' 8 | 9 | dependencies { 10 | // GraphQL 11 | api "com.graphql-java:graphql-java:$LIB_GRAPHQL_JAVA_VER" 12 | implementation "org.slf4j:slf4j-api:$LIB_SLF4J_VER" 13 | 14 | // JSON 15 | api "com.fasterxml.jackson.core:jackson-core:$LIB_JACKSON_VER" 16 | api "com.fasterxml.jackson.core:jackson-annotations:$LIB_JACKSON_VER" 17 | api "com.fasterxml.jackson.core:jackson-databind:2.17.2" 18 | api "com.fasterxml.jackson.datatype:jackson-datatype-jdk8:$LIB_JACKSON_VER" 19 | } 20 | -------------------------------------------------------------------------------- /graphql-java-kickstart/src/main/java/graphql/kickstart/execution/GraphQLErrorQueryResult.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.execution; 2 | 3 | import lombok.Getter; 4 | import lombok.RequiredArgsConstructor; 5 | 6 | @Getter 7 | @RequiredArgsConstructor 8 | class GraphQLErrorQueryResult implements GraphQLQueryResult { 9 | 10 | private final int statusCode; 11 | private final String message; 12 | 13 | @Override 14 | public boolean isBatched() { 15 | return false; 16 | } 17 | 18 | @Override 19 | public boolean isAsynchronous() { 20 | return false; 21 | } 22 | 23 | @Override 24 | public boolean isError() { 25 | return true; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /graphql-java-servlet/src/main/java/graphql/kickstart/servlet/ErrorQueryResponseWriter.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.servlet; 2 | 3 | import java.io.IOException; 4 | import jakarta.servlet.http.HttpServletRequest; 5 | import jakarta.servlet.http.HttpServletResponse; 6 | import lombok.RequiredArgsConstructor; 7 | 8 | @RequiredArgsConstructor 9 | class ErrorQueryResponseWriter implements QueryResponseWriter { 10 | 11 | private final int statusCode; 12 | private final String message; 13 | 14 | @Override 15 | public void write(HttpServletRequest request, HttpServletResponse response) throws IOException { 16 | response.sendError(statusCode, message); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /graphql-java-servlet/src/main/java/graphql/kickstart/servlet/HttpRequestHandler.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.servlet; 2 | 3 | import java.io.IOException; 4 | import jakarta.servlet.http.HttpServletRequest; 5 | import jakarta.servlet.http.HttpServletResponse; 6 | 7 | public interface HttpRequestHandler { 8 | 9 | String APPLICATION_JSON_UTF8 = "application/json;charset=UTF-8"; 10 | String APPLICATION_EVENT_STREAM_UTF8 = "text/event-stream;charset=UTF-8"; 11 | 12 | int STATUS_OK = 200; 13 | int STATUS_BAD_REQUEST = 400; 14 | int STATUS_INTERNAL_SERVER_ERROR = 500; 15 | 16 | void handle(HttpServletRequest request, HttpServletResponse response) throws IOException; 17 | } 18 | -------------------------------------------------------------------------------- /graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/SubscriptionProtocolFactory.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.execution.subscriptions; 2 | 3 | import java.util.function.Consumer; 4 | 5 | /** @author Andrew Potter */ 6 | public abstract class SubscriptionProtocolFactory { 7 | 8 | private final String protocol; 9 | 10 | protected SubscriptionProtocolFactory(String protocol) { 11 | this.protocol = protocol; 12 | } 13 | 14 | public String getProtocol() { 15 | return protocol; 16 | } 17 | 18 | public abstract Consumer createConsumer(SubscriptionSession session); 19 | 20 | public void shutdown() { 21 | // do nothing 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /graphql-java-servlet/src/main/java/graphql/kickstart/servlet/AsyncTaskExecutor.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.servlet; 2 | 3 | import java.util.concurrent.Executor; 4 | import lombok.NonNull; 5 | import lombok.RequiredArgsConstructor; 6 | 7 | @RequiredArgsConstructor 8 | class AsyncTaskExecutor implements Executor { 9 | 10 | private final Executor executor; 11 | private final AsyncTaskDecorator taskDecorator; 12 | 13 | @Override 14 | public void execute(@NonNull Runnable command) { 15 | if (taskDecorator != null) { 16 | Runnable decorated = taskDecorator.decorate(command); 17 | executor.execute(decorated); 18 | } else { 19 | executor.execute(command); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /graphql-java-servlet/src/main/java/graphql/kickstart/servlet/GraphQLHttpServlet.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.servlet; 2 | 3 | import graphql.schema.GraphQLSchema; 4 | 5 | /** @author Michiel Oliemans */ 6 | public abstract class GraphQLHttpServlet extends AbstractGraphQLHttpServlet { 7 | 8 | public static GraphQLHttpServlet with(GraphQLSchema schema) { 9 | return new ConfiguredGraphQLHttpServlet(GraphQLConfiguration.with(schema).build()); 10 | } 11 | 12 | public static GraphQLHttpServlet with(GraphQLConfiguration configuration) { 13 | return new ConfiguredGraphQLHttpServlet(configuration); 14 | } 15 | 16 | @Override 17 | protected abstract GraphQLConfiguration getConfiguration(); 18 | } 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /graphql-java-servlet/src/main/java/graphql/kickstart/servlet/OsgiGraphQLHttpServletConfiguration.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.servlet; 2 | 3 | import org.osgi.service.metatype.annotations.AttributeDefinition; 4 | import org.osgi.service.metatype.annotations.ObjectClassDefinition; 5 | 6 | @ObjectClassDefinition( 7 | name = "GraphQL HTTP Servlet", 8 | description = "GraphQL HTTP Servlet Configuration") 9 | @interface OsgiGraphQLHttpServletConfiguration { 10 | 11 | @AttributeDefinition(name = "alias", description = "Servlet alias") 12 | String alias() default "/graphql"; 13 | 14 | @AttributeDefinition(name = "jmx.objectname", description = "JMX object name") 15 | String jmx_objectname() default "graphql.servlet:type=graphql"; 16 | } 17 | -------------------------------------------------------------------------------- /graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/apollo/SubscriptionStopCommand.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.execution.subscriptions.apollo; 2 | 3 | import graphql.kickstart.execution.subscriptions.SubscriptionSession; 4 | import java.util.Collection; 5 | import lombok.RequiredArgsConstructor; 6 | 7 | @RequiredArgsConstructor 8 | class SubscriptionStopCommand implements SubscriptionCommand { 9 | 10 | private final Collection connectionListeners; 11 | 12 | @Override 13 | public void apply(SubscriptionSession session, OperationMessage message) { 14 | connectionListeners.forEach(it -> it.onStop(session, message)); 15 | session.unsubscribe(message.getId()); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /graphql-java-servlet/src/main/java/graphql/kickstart/servlet/input/NoOpBatchInputPreProcessor.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.servlet.input; 2 | 3 | import graphql.kickstart.execution.input.GraphQLBatchedInvocationInput; 4 | import jakarta.servlet.http.HttpServletRequest; 5 | import jakarta.servlet.http.HttpServletResponse; 6 | 7 | /** A default BatchInputPreProcessor that returns the input. */ 8 | public class NoOpBatchInputPreProcessor implements BatchInputPreProcessor { 9 | 10 | @Override 11 | public BatchInputPreProcessResult preProcessBatch( 12 | GraphQLBatchedInvocationInput batchedInvocationInput, 13 | HttpServletRequest request, 14 | HttpServletResponse response) { 15 | return new BatchInputPreProcessResult(batchedInvocationInput); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /graphql-java-servlet/src/main/java/graphql/kickstart/servlet/core/DefaultGraphQLRootObjectBuilder.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.servlet.core; 2 | 3 | import graphql.kickstart.execution.StaticGraphQLRootObjectBuilder; 4 | import jakarta.servlet.http.HttpServletRequest; 5 | import jakarta.websocket.server.HandshakeRequest; 6 | 7 | public class DefaultGraphQLRootObjectBuilder extends StaticGraphQLRootObjectBuilder 8 | implements GraphQLServletRootObjectBuilder { 9 | 10 | public DefaultGraphQLRootObjectBuilder() { 11 | super(new Object()); 12 | } 13 | 14 | @Override 15 | public Object build(HttpServletRequest req) { 16 | return getRootObject(); 17 | } 18 | 19 | @Override 20 | public Object build(HandshakeRequest req) { 21 | return getRootObject(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /graphql-java-servlet/src/main/java/graphql/kickstart/servlet/context/GraphQLServletContext.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.servlet.context; 2 | 3 | import graphql.kickstart.execution.context.GraphQLKickstartContext; 4 | import java.util.List; 5 | import java.util.Map; 6 | import jakarta.servlet.http.HttpServletRequest; 7 | import jakarta.servlet.http.HttpServletResponse; 8 | import jakarta.servlet.http.Part; 9 | 10 | /** @deprecated Use {@link graphql.kickstart.execution.context.GraphQLKickstartContext} instead */ 11 | public interface GraphQLServletContext extends GraphQLKickstartContext { 12 | 13 | List getFileParts(); 14 | 15 | Map> getParts(); 16 | 17 | HttpServletRequest getHttpServletRequest(); 18 | 19 | HttpServletResponse getHttpServletResponse(); 20 | } 21 | -------------------------------------------------------------------------------- /graphql-java-kickstart/src/main/java/graphql/kickstart/execution/error/DefaultGraphQLServletObjectMapperConfigurer.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.execution.error; 2 | 3 | import com.fasterxml.jackson.annotation.JsonInclude; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import com.fasterxml.jackson.databind.SerializationFeature; 6 | import graphql.kickstart.execution.config.GraphQLServletObjectMapperConfigurer; 7 | 8 | /** @author Andrew Potter */ 9 | public class DefaultGraphQLServletObjectMapperConfigurer 10 | implements GraphQLServletObjectMapperConfigurer { 11 | 12 | @Override 13 | public void configure(ObjectMapper mapper) { 14 | mapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS); 15 | mapper.setDefaultPropertyInclusion(JsonInclude.Include.ALWAYS); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /graphql-java-servlet/src/main/java/graphql/kickstart/servlet/context/GraphQLServletContextBuilder.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.servlet.context; 2 | 3 | import graphql.kickstart.execution.context.GraphQLKickstartContext; 4 | import graphql.kickstart.execution.context.GraphQLContextBuilder; 5 | import jakarta.servlet.http.HttpServletRequest; 6 | import jakarta.servlet.http.HttpServletResponse; 7 | import jakarta.websocket.Session; 8 | import jakarta.websocket.server.HandshakeRequest; 9 | 10 | public interface GraphQLServletContextBuilder extends GraphQLContextBuilder { 11 | 12 | GraphQLKickstartContext build( 13 | HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse); 14 | 15 | GraphQLKickstartContext build(Session session, HandshakeRequest handshakeRequest); 16 | } 17 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Additional context** 32 | Add any other context about the problem here. 33 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | version=16.0.0 2 | group=com.graphql-java-kickstart 3 | PROJECT_NAME=graphql-java-servlet 4 | PROJECT_DESC=GraphQL Java Kickstart 5 | PROJECT_GIT_REPO_URL=https://github.com/graphql-java-kickstart/graphql-java-servlet 6 | PROJECT_LICENSE=MIT 7 | PROJECT_LICENSE_URL=https://github.com/graphql-java-kickstart/spring-java-servlet/blob/master/LICENSE.md 8 | PROJECT_DEV_ID=oliemansm 9 | PROJECT_DEV_NAME=Michiel Oliemans 10 | LIB_GRAPHQL_JAVA_VER=22.3 11 | LIB_JACKSON_VER=2.17.2 12 | LIB_SLF4J_VER=2.0.16 13 | LIB_LOMBOK_VER=1.18.34 14 | # These constants are necessary to the automatic release of javax flavour 15 | LIB_JAVAX_SERVLET=4.0.1 16 | LIB_JAVAX_WEBSOCKET=1.1 17 | LIB_SPRINGFRAMEWORK_5=5.3.25 18 | SOURCE_COMPATIBILITY=11 19 | TARGET_COMPATIBILITY=11 20 | SOURCE_COMPATIBILITY_TEST=17 21 | TARGET_COMPATIBILITY_TEST=17 22 | -------------------------------------------------------------------------------- /graphql-java-kickstart/src/main/java/graphql/kickstart/execution/FutureErrorExecutionResult.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.execution; 2 | 3 | import graphql.kickstart.execution.input.GraphQLInvocationInput; 4 | import java.util.concurrent.CompletableFuture; 5 | import lombok.RequiredArgsConstructor; 6 | 7 | @RequiredArgsConstructor 8 | class FutureErrorExecutionResult implements FutureExecutionResult { 9 | 10 | private final GraphQLErrorQueryResult errorQueryResult; 11 | 12 | @Override 13 | public CompletableFuture thenApplyQueryResult() { 14 | return CompletableFuture.completedFuture(errorQueryResult); 15 | } 16 | 17 | @Override 18 | public GraphQLInvocationInput getInvocationInput() { 19 | return null; 20 | } 21 | 22 | @Override 23 | public void cancel() { 24 | // nothing to do 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /graphql-java-kickstart/src/main/java/graphql/kickstart/execution/FutureSingleExecutionResult.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.execution; 2 | 3 | import graphql.ExecutionResult; 4 | import graphql.kickstart.execution.input.GraphQLInvocationInput; 5 | import java.util.concurrent.CompletableFuture; 6 | import lombok.Getter; 7 | import lombok.RequiredArgsConstructor; 8 | 9 | @RequiredArgsConstructor 10 | class FutureSingleExecutionResult implements FutureExecutionResult { 11 | 12 | @Getter 13 | private final GraphQLInvocationInput invocationInput; 14 | private final CompletableFuture single; 15 | 16 | @Override 17 | public CompletableFuture thenApplyQueryResult() { 18 | return single.thenApply(GraphQLQueryResult::create); 19 | } 20 | 21 | @Override 22 | public void cancel() { 23 | single.cancel(true); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /graphql-java-servlet/src/test/groovy/graphql/kickstart/servlet/TestGraphQLErrorException.groovy: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.servlet 2 | 3 | import graphql.ErrorType 4 | import graphql.GraphQLError 5 | import graphql.language.SourceLocation 6 | 7 | /** 8 | * @author Andrew Potter 9 | */ 10 | class TestGraphQLErrorException extends RuntimeException implements GraphQLError { 11 | 12 | TestGraphQLErrorException(String message) { 13 | super(message) 14 | } 15 | 16 | @Override 17 | Map getExtensions() { 18 | Map customAttributes = new LinkedHashMap<>() 19 | customAttributes.put("foo", "bar") 20 | return customAttributes 21 | } 22 | 23 | @Override 24 | List getLocations() { 25 | return null 26 | } 27 | 28 | @Override 29 | ErrorType getErrorType() { 30 | return ErrorType.ValidationError 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /graphql-java-kickstart/src/main/java/graphql/kickstart/execution/input/GraphQLBatchedInvocationInput.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.execution.input; 2 | 3 | import static java.util.stream.Collectors.toList; 4 | 5 | import graphql.ExecutionInput; 6 | import graphql.kickstart.execution.context.ContextSetting; 7 | import java.util.List; 8 | 9 | /** Interface representing a batched input. */ 10 | public interface GraphQLBatchedInvocationInput extends GraphQLInvocationInput { 11 | 12 | /** @return each individual input in the batch, configured with a context. */ 13 | List getInvocationInputs(); 14 | 15 | default List getExecutionInputs() { 16 | return getInvocationInputs().stream() 17 | .map(GraphQLSingleInvocationInput::getExecutionInput) 18 | .collect(toList()); 19 | } 20 | 21 | ContextSetting getContextSetting(); 22 | 23 | } 24 | -------------------------------------------------------------------------------- /graphql-java-kickstart/src/main/java/graphql/kickstart/execution/FutureBatchedExecutionResult.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.execution; 2 | 3 | import graphql.ExecutionResult; 4 | import graphql.kickstart.execution.input.GraphQLInvocationInput; 5 | import java.util.List; 6 | import java.util.concurrent.CompletableFuture; 7 | import lombok.Getter; 8 | import lombok.RequiredArgsConstructor; 9 | 10 | @RequiredArgsConstructor 11 | class FutureBatchedExecutionResult implements FutureExecutionResult { 12 | 13 | @Getter 14 | private final GraphQLInvocationInput invocationInput; 15 | private final CompletableFuture> batched; 16 | 17 | @Override 18 | public CompletableFuture thenApplyQueryResult() { 19 | return batched.thenApply(GraphQLQueryResult::create); 20 | } 21 | 22 | @Override 23 | public void cancel() { 24 | batched.cancel(true); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /graphql-java-servlet/src/main/java/graphql/kickstart/servlet/core/internal/GraphQLThreadFactory.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.servlet.core.internal; 2 | 3 | import graphql.kickstart.servlet.AbstractGraphQLHttpServlet; 4 | import java.util.concurrent.ThreadFactory; 5 | import java.util.concurrent.atomic.AtomicInteger; 6 | 7 | /** 8 | * {@link ThreadFactory} implementation for {@link AbstractGraphQLHttpServlet} async operations 9 | * 10 | * @author John Nutting 11 | */ 12 | public class GraphQLThreadFactory implements ThreadFactory { 13 | 14 | static final String NAME_PREFIX = "GraphQLServlet-"; 15 | final AtomicInteger threadNumber = new AtomicInteger(1); 16 | 17 | @Override 18 | public Thread newThread(final Runnable r) { 19 | Thread t = new Thread(r, NAME_PREFIX + threadNumber.getAndIncrement()); 20 | t.setDaemon(false); 21 | t.setPriority(Thread.NORM_PRIORITY); 22 | return t; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /graphql-java-servlet/src/main/java/graphql/kickstart/servlet/input/BatchInputPreProcessor.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.servlet.input; 2 | 3 | import graphql.kickstart.execution.input.GraphQLBatchedInvocationInput; 4 | import jakarta.servlet.http.HttpServletRequest; 5 | import jakarta.servlet.http.HttpServletResponse; 6 | 7 | public interface BatchInputPreProcessor { 8 | 9 | /** 10 | * An injectable object that allows clients to manipulate a batch before executing, or abort 11 | * altogether. 12 | * 13 | * @param batchedInvocationInput the input to process 14 | * @param request the servlet request 15 | * @param response the servlet response 16 | * @return wrapped batch to possibly process. 17 | */ 18 | BatchInputPreProcessResult preProcessBatch( 19 | GraphQLBatchedInvocationInput batchedInvocationInput, 20 | HttpServletRequest request, 21 | HttpServletResponse response); 22 | } 23 | -------------------------------------------------------------------------------- /graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/AtomicSubscriptionSubscription.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.execution.subscriptions; 2 | 3 | import java.util.concurrent.atomic.AtomicReference; 4 | import org.reactivestreams.Subscription; 5 | 6 | public class AtomicSubscriptionSubscription { 7 | 8 | private final AtomicReference reference = new AtomicReference<>(null); 9 | 10 | public void set(Subscription subscription) { 11 | if (reference.get() != null) { 12 | throw new IllegalStateException("Cannot overwrite subscription!"); 13 | } 14 | 15 | reference.set(subscription); 16 | } 17 | 18 | public Subscription get() { 19 | Subscription subscription = reference.get(); 20 | if (subscription == null) { 21 | throw new IllegalStateException("Subscription has not been initialized yet!"); 22 | } 23 | 24 | return subscription; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /graphql-java-kickstart/src/main/java/graphql/kickstart/execution/config/DefaultGraphQLSchemaProvider.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.execution.config; 2 | 3 | import graphql.schema.GraphQLSchema; 4 | 5 | /** @author Andrew Potter */ 6 | public class DefaultGraphQLSchemaProvider implements GraphQLSchemaProvider { 7 | 8 | private final GraphQLSchema schema; 9 | private final GraphQLSchema readOnlySchema; 10 | 11 | public DefaultGraphQLSchemaProvider(GraphQLSchema schema) { 12 | this(schema, GraphQLSchemaProvider.copyReadOnly(schema)); 13 | } 14 | 15 | public DefaultGraphQLSchemaProvider(GraphQLSchema schema, GraphQLSchema readOnlySchema) { 16 | this.schema = schema; 17 | this.readOnlySchema = readOnlySchema; 18 | } 19 | 20 | @Override 21 | public GraphQLSchema getSchema() { 22 | return schema; 23 | } 24 | 25 | @Override 26 | public GraphQLSchema getReadOnlySchema() { 27 | return readOnlySchema; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /graphql-java-servlet/src/main/java/graphql/kickstart/servlet/AbstractGraphQLInvocationInputParser.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.servlet; 2 | 3 | import graphql.kickstart.execution.GraphQLObjectMapper; 4 | import graphql.kickstart.execution.context.ContextSetting; 5 | import graphql.kickstart.servlet.input.GraphQLInvocationInputFactory; 6 | import lombok.RequiredArgsConstructor; 7 | 8 | @RequiredArgsConstructor 9 | abstract class AbstractGraphQLInvocationInputParser implements GraphQLInvocationInputParser { 10 | 11 | final GraphQLInvocationInputFactory invocationInputFactory; 12 | final GraphQLObjectMapper graphQLObjectMapper; 13 | final ContextSetting contextSetting; 14 | 15 | boolean isSingleQuery(String query) { 16 | return query != null && !query.trim().isEmpty() && !query.trim().startsWith("["); 17 | } 18 | 19 | boolean isBatchedQuery(String query) { 20 | return query != null && query.trim().startsWith("["); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /graphql-java-kickstart/src/main/java/graphql/kickstart/execution/error/GenericGraphQLError.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.execution.error; 2 | 3 | import static java.util.Collections.emptyList; 4 | 5 | import com.fasterxml.jackson.annotation.JsonIgnore; 6 | import graphql.ErrorType; 7 | import graphql.GraphQLError; 8 | import graphql.language.SourceLocation; 9 | import java.util.List; 10 | 11 | /** @author Andrew Potter */ 12 | public class GenericGraphQLError implements GraphQLError { 13 | 14 | private final String message; 15 | 16 | public GenericGraphQLError(String message) { 17 | this.message = message; 18 | } 19 | 20 | @Override 21 | public String getMessage() { 22 | return message; 23 | } 24 | 25 | @Override 26 | @JsonIgnore 27 | public List getLocations() { 28 | return emptyList(); 29 | } 30 | 31 | @Override 32 | @JsonIgnore 33 | public ErrorType getErrorType() { 34 | return null; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /graphql-java-kickstart/src/main/java/graphql/kickstart/execution/BatchedDataLoaderGraphQLBuilder.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.execution; 2 | 3 | import graphql.GraphQL; 4 | import graphql.kickstart.execution.config.GraphQLBuilder; 5 | import graphql.kickstart.execution.input.GraphQLBatchedInvocationInput; 6 | import graphql.kickstart.execution.input.GraphQLSingleInvocationInput; 7 | 8 | public class BatchedDataLoaderGraphQLBuilder { 9 | 10 | GraphQL newGraphQL(GraphQLBatchedInvocationInput invocationInput, GraphQLBuilder graphQLBuilder) { 11 | return invocationInput.getInvocationInputs().stream() 12 | .findFirst() 13 | .map(GraphQLSingleInvocationInput::getSchema) 14 | .map(schema -> graphQLBuilder.build(schema, graphQLBuilder.getInstrumentationSupplier())) 15 | .orElseThrow( 16 | () -> 17 | new IllegalArgumentException( 18 | "Batched invocation input must contain at least one query")); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/apollo/SubscriptionConnectionTerminateCommand.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.execution.subscriptions.apollo; 2 | 3 | import static graphql.kickstart.execution.subscriptions.apollo.OperationMessage.Type.GQL_CONNECTION_TERMINATE; 4 | 5 | import graphql.kickstart.execution.subscriptions.SubscriptionSession; 6 | import java.util.Collection; 7 | import lombok.RequiredArgsConstructor; 8 | import lombok.extern.slf4j.Slf4j; 9 | 10 | @Slf4j 11 | @RequiredArgsConstructor 12 | class SubscriptionConnectionTerminateCommand implements SubscriptionCommand { 13 | 14 | private final Collection connectionListeners; 15 | 16 | @Override 17 | public void apply(SubscriptionSession session, OperationMessage message) { 18 | connectionListeners.forEach(it -> it.onTerminate(session, message)); 19 | session.close("client requested " + GQL_CONNECTION_TERMINATE.getValue()); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/apollo/ApolloSubscriptionConnectionListener.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.execution.subscriptions.apollo; 2 | 3 | import graphql.kickstart.execution.subscriptions.SubscriptionConnectionListener; 4 | import graphql.kickstart.execution.subscriptions.SubscriptionSession; 5 | 6 | public interface ApolloSubscriptionConnectionListener extends SubscriptionConnectionListener { 7 | 8 | default void onConnect(SubscriptionSession session, OperationMessage message) { 9 | // do nothing 10 | } 11 | 12 | default void onStart(SubscriptionSession session, OperationMessage message) { 13 | // do nothing 14 | } 15 | 16 | default void onStop(SubscriptionSession session, OperationMessage message) { 17 | // do nothing 18 | } 19 | 20 | default void onTerminate(SubscriptionSession session, OperationMessage message) { 21 | // do nothing 22 | } 23 | 24 | default void shutdown() { 25 | // do nothing 26 | } 27 | } -------------------------------------------------------------------------------- /graphql-java-kickstart/src/main/java/graphql/kickstart/execution/VariablesDeserializer.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.execution; 2 | 3 | import com.fasterxml.jackson.core.JsonParser; 4 | import com.fasterxml.jackson.core.ObjectCodec; 5 | import com.fasterxml.jackson.databind.DeserializationContext; 6 | import com.fasterxml.jackson.databind.JsonDeserializer; 7 | import java.io.IOException; 8 | import java.util.Map; 9 | 10 | /** @author Andrew Potter */ 11 | public class VariablesDeserializer extends JsonDeserializer> { 12 | 13 | public static Map deserializeVariablesObject( 14 | Object variables, ObjectCodec codec) { 15 | return ObjectMapDeserializeHelper.deserializeObjectMapObject(variables, codec, "variables"); 16 | } 17 | 18 | @Override 19 | public Map deserialize(JsonParser p, DeserializationContext ctxt) 20 | throws IOException { 21 | return deserializeVariablesObject(p.readValueAs(Object.class), p.getCodec()); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /graphql-java-servlet/src/main/java/graphql/kickstart/servlet/SubscriptionAsyncListener.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.servlet; 2 | 3 | import java.util.concurrent.atomic.AtomicReference; 4 | import jakarta.servlet.AsyncEvent; 5 | import jakarta.servlet.AsyncListener; 6 | import lombok.RequiredArgsConstructor; 7 | import org.reactivestreams.Subscription; 8 | 9 | @RequiredArgsConstructor 10 | class SubscriptionAsyncListener implements AsyncListener { 11 | 12 | private final AtomicReference subscriptionRef; 13 | 14 | @Override 15 | public void onComplete(AsyncEvent event) { 16 | subscriptionRef.get().cancel(); 17 | } 18 | 19 | @Override 20 | public void onTimeout(AsyncEvent event) { 21 | subscriptionRef.get().cancel(); 22 | } 23 | 24 | @Override 25 | public void onError(AsyncEvent event) { 26 | subscriptionRef.get().cancel(); 27 | } 28 | 29 | @Override 30 | public void onStartAsync(AsyncEvent event) { 31 | // default empty implementation 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /.github/add-javax-suffix.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | addSuffix() { 4 | local result 5 | result=$(grep include settings.gradle | awk '{print $2}' | tr -d "'" | tr -d ':') 6 | readarray -t <<<"$result" 7 | modules=("${MAPFILE[@]}") 8 | 9 | updateLocalDependencies 10 | } 11 | 12 | updateLocalDependencies() { 13 | for module in "${modules[@]}"; do 14 | cp -rf "$module" "$module"-javax 15 | rm -rf "$module" 16 | 17 | for dependency in "${modules[@]}"; do 18 | sed -i -E "s/project\(('|\"):${dependency}('|\")\)/project\(':${dependency}-javax'\)/" "$module"-"javax"/build.gradle 19 | done 20 | done 21 | 22 | updateGradleSettings 23 | } 24 | 25 | updateGradleSettings() { 26 | for module in "${modules[@]}"; do 27 | echo "Replace ${module} with ${module}-javax in settings.gradle" 28 | sed -i -E "s/('|\"):${module}('|\")/':${module}-javax'/" settings.gradle 29 | done 30 | 31 | cat settings.gradle 32 | } 33 | 34 | echo "Add suffix -javax to modules" 35 | addSuffix 36 | 37 | ls -lh -------------------------------------------------------------------------------- /graphql-java-kickstart/src/main/java/graphql/kickstart/execution/ExtensionsDeserializer.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.execution; 2 | 3 | import com.fasterxml.jackson.core.JsonParser; 4 | import com.fasterxml.jackson.core.ObjectCodec; 5 | import com.fasterxml.jackson.databind.DeserializationContext; 6 | import com.fasterxml.jackson.databind.JsonDeserializer; 7 | import java.io.IOException; 8 | import java.util.Map; 9 | 10 | public class ExtensionsDeserializer extends JsonDeserializer> { 11 | 12 | public static Map deserializeExtensionsObject( 13 | Object extensions, ObjectCodec codec) { 14 | return ObjectMapDeserializeHelper.deserializeObjectMapObject(extensions, codec, "extensions"); 15 | } 16 | 17 | @Override 18 | public Map deserialize(JsonParser p, DeserializationContext ctxt) 19 | throws IOException { 20 | return ExtensionsDeserializer.deserializeExtensionsObject( 21 | p.readValueAs(Object.class), p.getCodec()); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/osgi/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | graphql-java-servlet-osgi-examples 6 | 7 | com.graphql-java-kickstart 8 | 4.0.0 9 | 10 | providers 11 | apache-karaf-feature 12 | apache-karaf-package 13 | 14 | pom 15 | 16 | 17 | 11.0.0-SNAPSHOT 18 | 16.1 19 | 4.2.10 20 | 11 21 | 11 22 | 23 | 24 | 10.1.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /graphql-java-servlet/src/main/java/graphql/kickstart/servlet/config/DefaultGraphQLSchemaServletProvider.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.servlet.config; 2 | 3 | import graphql.kickstart.execution.config.DefaultGraphQLSchemaProvider; 4 | import graphql.schema.GraphQLSchema; 5 | import jakarta.servlet.http.HttpServletRequest; 6 | import jakarta.websocket.server.HandshakeRequest; 7 | 8 | /** @author Andrew Potter */ 9 | public class DefaultGraphQLSchemaServletProvider extends DefaultGraphQLSchemaProvider 10 | implements GraphQLSchemaServletProvider { 11 | 12 | public DefaultGraphQLSchemaServletProvider(GraphQLSchema schema) { 13 | super(schema); 14 | } 15 | 16 | @Override 17 | public GraphQLSchema getSchema(HttpServletRequest request) { 18 | return getSchema(); 19 | } 20 | 21 | @Override 22 | public GraphQLSchema getSchema(HandshakeRequest request) { 23 | return getSchema(); 24 | } 25 | 26 | @Override 27 | public GraphQLSchema getReadOnlySchema(HttpServletRequest request) { 28 | return getReadOnlySchema(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /graphql-java-servlet/src/main/java/graphql/kickstart/servlet/subscriptions/WebSocketSubscriptionSession.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.servlet.subscriptions; 2 | 3 | import graphql.kickstart.execution.subscriptions.DefaultSubscriptionSession; 4 | import graphql.kickstart.execution.subscriptions.GraphQLSubscriptionMapper; 5 | import java.util.Map; 6 | import jakarta.websocket.Session; 7 | 8 | public class WebSocketSubscriptionSession extends DefaultSubscriptionSession { 9 | 10 | private final Session session; 11 | 12 | public WebSocketSubscriptionSession(GraphQLSubscriptionMapper mapper, Session session) { 13 | super(mapper); 14 | this.session = session; 15 | } 16 | 17 | @Override 18 | public boolean isOpen() { 19 | return session.isOpen(); 20 | } 21 | 22 | @Override 23 | public Map getUserProperties() { 24 | return session.getUserProperties(); 25 | } 26 | 27 | @Override 28 | public String getId() { 29 | return session.getId(); 30 | } 31 | 32 | @Override 33 | public Session unwrap() { 34 | return session; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /graphql-java-kickstart/src/main/java/graphql/kickstart/execution/context/GraphQLKickstartContext.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.execution.context; 2 | 3 | import java.util.Map; 4 | import lombok.NonNull; 5 | import org.dataloader.DataLoaderRegistry; 6 | 7 | /** Represents the context required by the servlet to execute a GraphQL request. */ 8 | public interface GraphQLKickstartContext { 9 | 10 | static GraphQLKickstartContext of(Map map) { 11 | return new DefaultGraphQLContext(map); 12 | } 13 | 14 | static GraphQLKickstartContext of(DataLoaderRegistry dataLoaderRegistry) { 15 | return new DefaultGraphQLContext(dataLoaderRegistry); 16 | } 17 | 18 | static GraphQLKickstartContext of( 19 | DataLoaderRegistry dataLoaderRegistry, Map map) { 20 | return new DefaultGraphQLContext(dataLoaderRegistry, map); 21 | } 22 | 23 | /** @return the Dataloader registry to use for the execution. Must not return null */ 24 | @NonNull 25 | DataLoaderRegistry getDataLoaderRegistry(); 26 | 27 | Map getMapOfContext(); 28 | } 29 | -------------------------------------------------------------------------------- /graphql-java-kickstart/src/main/java/graphql/kickstart/execution/FutureExecutionResult.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.execution; 2 | 3 | import graphql.ExecutionResult; 4 | import graphql.kickstart.execution.input.GraphQLInvocationInput; 5 | import java.util.List; 6 | import java.util.concurrent.CompletableFuture; 7 | 8 | public interface FutureExecutionResult { 9 | 10 | static FutureExecutionResult single( 11 | GraphQLInvocationInput invocationInput, CompletableFuture single) { 12 | return new FutureSingleExecutionResult(invocationInput, single); 13 | } 14 | 15 | static FutureExecutionResult batched( 16 | GraphQLInvocationInput invocationInput, CompletableFuture> batched) { 17 | return new FutureBatchedExecutionResult(invocationInput, batched); 18 | } 19 | 20 | static FutureExecutionResult error(GraphQLErrorQueryResult result) { 21 | return new FutureErrorExecutionResult(result); 22 | } 23 | 24 | CompletableFuture thenApplyQueryResult(); 25 | 26 | GraphQLInvocationInput getInvocationInput(); 27 | 28 | void cancel(); 29 | } 30 | -------------------------------------------------------------------------------- /graphql-java-kickstart/src/main/java/graphql/kickstart/execution/instrumentation/RequestLevelTrackingApproach.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.execution.instrumentation; 2 | 3 | import graphql.execution.ExecutionId; 4 | import graphql.execution.instrumentation.InstrumentationState; 5 | import java.util.List; 6 | import org.dataloader.DataLoaderRegistry; 7 | 8 | /** Dispatching approach that expects to know about all executions before execution starts. */ 9 | public class RequestLevelTrackingApproach extends AbstractTrackingApproach { 10 | 11 | public RequestLevelTrackingApproach( 12 | List executionIds, DataLoaderRegistry dataLoaderRegistry) { 13 | super(dataLoaderRegistry); 14 | RequestStack requestStack = getStack(); 15 | executionIds.forEach(requestStack::addExecution); 16 | } 17 | 18 | @Override 19 | public InstrumentationState createState(ExecutionId executionId) { 20 | if (!getStack().contains(executionId)) { 21 | throw new TrackingApproachException( 22 | String.format("Request tracking not set up with execution id %s", executionId)); 23 | } 24 | return null; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/apollo/ApolloSubscriptionSession.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.execution.subscriptions.apollo; 2 | 3 | import graphql.kickstart.execution.subscriptions.DefaultSubscriptionSession; 4 | import graphql.kickstart.execution.subscriptions.GraphQLSubscriptionMapper; 5 | import graphql.kickstart.execution.subscriptions.apollo.OperationMessage.Type; 6 | import lombok.extern.slf4j.Slf4j; 7 | 8 | @Slf4j 9 | public class ApolloSubscriptionSession extends DefaultSubscriptionSession { 10 | 11 | public ApolloSubscriptionSession(GraphQLSubscriptionMapper mapper) { 12 | super(mapper); 13 | } 14 | 15 | @Override 16 | public void sendDataMessage(String id, Object payload) { 17 | sendMessage(new OperationMessage(Type.GQL_DATA, id, payload)); 18 | } 19 | 20 | @Override 21 | public void sendErrorMessage(String id, Object payload) { 22 | sendMessage(new OperationMessage(Type.GQL_ERROR, id, payload)); 23 | } 24 | 25 | @Override 26 | public void sendCompleteMessage(String id) { 27 | sendMessage(new OperationMessage(Type.GQL_COMPLETE, id, null)); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /graphql-java-kickstart/src/main/java/graphql/kickstart/execution/DecoratedExecutionResult.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.execution; 2 | 3 | import graphql.ExecutionResult; 4 | import graphql.GraphQLError; 5 | import java.util.List; 6 | import java.util.Map; 7 | import lombok.RequiredArgsConstructor; 8 | import org.reactivestreams.Publisher; 9 | 10 | @RequiredArgsConstructor 11 | class DecoratedExecutionResult implements ExecutionResult { 12 | 13 | private final ExecutionResult result; 14 | 15 | boolean isAsynchronous() { 16 | return result.getData() instanceof Publisher; 17 | } 18 | 19 | @Override 20 | public List getErrors() { 21 | return result.getErrors(); 22 | } 23 | 24 | @Override 25 | public T getData() { 26 | return result.getData(); 27 | } 28 | 29 | @Override 30 | public boolean isDataPresent() { 31 | return result.isDataPresent(); 32 | } 33 | 34 | @Override 35 | public Map getExtensions() { 36 | return result.getExtensions(); 37 | } 38 | 39 | @Override 40 | public Map toSpecification() { 41 | return result.toSpecification(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /graphql-java-servlet/src/test/groovy/graphql/kickstart/servlet/TestBatchInputPreProcessor.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.servlet; 2 | 3 | import graphql.kickstart.execution.input.GraphQLBatchedInvocationInput; 4 | import graphql.kickstart.servlet.input.BatchInputPreProcessResult; 5 | import graphql.kickstart.servlet.input.BatchInputPreProcessor; 6 | import jakarta.servlet.http.HttpServletRequest; 7 | import jakarta.servlet.http.HttpServletResponse; 8 | 9 | public class TestBatchInputPreProcessor implements BatchInputPreProcessor { 10 | 11 | public static String BATCH_ERROR_MESSAGE = "Batch limit exceeded"; 12 | 13 | @Override 14 | public BatchInputPreProcessResult preProcessBatch( 15 | GraphQLBatchedInvocationInput batchedInvocationInput, 16 | HttpServletRequest request, 17 | HttpServletResponse response) { 18 | BatchInputPreProcessResult preProcessResult; 19 | if (batchedInvocationInput.getExecutionInputs().size() > 2) { 20 | preProcessResult = new BatchInputPreProcessResult(400, BATCH_ERROR_MESSAGE); 21 | } else { 22 | preProcessResult = new BatchInputPreProcessResult(batchedInvocationInput); 23 | } 24 | return preProcessResult; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /graphql-java-servlet/src/main/java/graphql/kickstart/servlet/config/GraphQLSchemaServletProvider.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.servlet.config; 2 | 3 | import graphql.kickstart.execution.config.GraphQLSchemaProvider; 4 | import graphql.schema.GraphQLSchema; 5 | import jakarta.servlet.http.HttpServletRequest; 6 | import jakarta.websocket.server.HandshakeRequest; 7 | 8 | public interface GraphQLSchemaServletProvider extends GraphQLSchemaProvider { 9 | 10 | /** 11 | * @param request the http request 12 | * @return a schema based on the request (auth, etc). 13 | */ 14 | GraphQLSchema getSchema(HttpServletRequest request); 15 | 16 | /** 17 | * @param request the http request used to create a websocket 18 | * @return a schema based on the request (auth, etc). 19 | */ 20 | GraphQLSchema getSchema(HandshakeRequest request); 21 | 22 | /** 23 | * @param request the http request 24 | * @return a read-only schema based on the request (auth, etc). Should return the same schema 25 | * (query/subscription-only version) as {@link #getSchema(HttpServletRequest)} for a given 26 | * request. 27 | */ 28 | GraphQLSchema getReadOnlySchema(HttpServletRequest request); 29 | } 30 | -------------------------------------------------------------------------------- /graphql-java-kickstart/src/main/java/graphql/kickstart/execution/GraphQLQueryResult.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.execution; 2 | 3 | import static java.util.Collections.emptyList; 4 | 5 | import graphql.ExecutionResult; 6 | import java.util.List; 7 | 8 | public interface GraphQLQueryResult { 9 | 10 | static GraphQLSingleQueryResult create(ExecutionResult result) { 11 | return new GraphQLSingleQueryResult(new DecoratedExecutionResult(result)); 12 | } 13 | 14 | static GraphQLBatchedQueryResult create(List results) { 15 | return new GraphQLBatchedQueryResult(results); 16 | } 17 | 18 | static GraphQLErrorQueryResult createError(int statusCode, String message) { 19 | return new GraphQLErrorQueryResult(statusCode, message); 20 | } 21 | 22 | boolean isBatched(); 23 | 24 | boolean isAsynchronous(); 25 | 26 | default DecoratedExecutionResult getResult() { 27 | return null; 28 | } 29 | 30 | default List getResults() { 31 | return emptyList(); 32 | } 33 | 34 | default boolean isError() { 35 | return false; 36 | } 37 | 38 | default int getStatusCode() { 39 | return 200; 40 | } 41 | 42 | default String getMessage() { 43 | return null; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /graphql-java-kickstart/src/main/java/graphql/kickstart/execution/instrumentation/FieldLevelTrackingApproach.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.execution.instrumentation; 2 | 3 | import graphql.Internal; 4 | import graphql.execution.ExecutionId; 5 | import graphql.execution.instrumentation.InstrumentationState; 6 | import org.dataloader.DataLoaderRegistry; 7 | 8 | /** 9 | * This approach uses field level tracking to achieve its aims of making the data loader more 10 | * efficient. Can handle new/concurrent executions using the same sets of dataloaders, attempting to 11 | * batch load calls together. 12 | */ 13 | @Internal 14 | public class FieldLevelTrackingApproach extends AbstractTrackingApproach { 15 | 16 | public FieldLevelTrackingApproach(DataLoaderRegistry dataLoaderRegistry) { 17 | super(dataLoaderRegistry); 18 | } 19 | 20 | public InstrumentationState createState(ExecutionId executionId) { 21 | synchronized (getStack()) { 22 | if (getStack().contains(executionId)) { 23 | throw new TrackingApproachException( 24 | String.format("Execution id %s already in active execution", executionId)); 25 | } 26 | getStack().addExecution(executionId); 27 | return null; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /graphql-java-servlet/src/main/java/graphql/kickstart/servlet/SingleQueryResponseWriter.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.servlet; 2 | 3 | import graphql.ExecutionResult; 4 | import graphql.kickstart.execution.GraphQLObjectMapper; 5 | import java.io.IOException; 6 | import java.nio.charset.StandardCharsets; 7 | import jakarta.servlet.http.HttpServletRequest; 8 | import jakarta.servlet.http.HttpServletResponse; 9 | import lombok.RequiredArgsConstructor; 10 | 11 | @RequiredArgsConstructor 12 | class SingleQueryResponseWriter implements QueryResponseWriter { 13 | 14 | private final ExecutionResult result; 15 | private final GraphQLObjectMapper graphQLObjectMapper; 16 | 17 | @Override 18 | public void write(HttpServletRequest request, HttpServletResponse response) throws IOException { 19 | response.setContentType(HttpRequestHandler.APPLICATION_JSON_UTF8); 20 | response.setStatus(HttpRequestHandler.STATUS_OK); 21 | response.setCharacterEncoding(StandardCharsets.UTF_8.name()); 22 | 23 | byte[] contentBytes = graphQLObjectMapper.serializeResultAsBytes(result); 24 | response.setContentLength(contentBytes.length); 25 | response.getOutputStream().write(contentBytes); 26 | response.getOutputStream().flush(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /graphql-java-kickstart/src/main/java/graphql/kickstart/execution/ObjectMapDeserializeHelper.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.execution; 2 | 3 | import com.fasterxml.jackson.core.ObjectCodec; 4 | import com.fasterxml.jackson.core.type.TypeReference; 5 | import java.io.IOException; 6 | import java.util.Map; 7 | 8 | public class ObjectMapDeserializeHelper { 9 | 10 | public static Map deserializeObjectMapObject( 11 | Object object, ObjectCodec codec, String fieldName) { 12 | if (object instanceof Map) { 13 | @SuppressWarnings("unchecked") 14 | Map genericObjectMap = (Map) object; 15 | return genericObjectMap; 16 | } else if (object instanceof String) { 17 | try { 18 | return codec.readValue( 19 | codec.getFactory().createParser((String) object), 20 | new TypeReference>() {}); 21 | } catch (IOException e) { 22 | throw new ObjectMapDeserializationException( 23 | String.format("Cannot deserialize field '%s'", fieldName), e); 24 | } 25 | } else { 26 | throw new ObjectMapDeserializationException( 27 | String.format("Field '%s' should be either an object or a string", fieldName)); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/apollo/SubscriptionConnectionInitCommand.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.execution.subscriptions.apollo; 2 | 3 | import graphql.kickstart.execution.subscriptions.SubscriptionSession; 4 | import graphql.kickstart.execution.subscriptions.apollo.OperationMessage.Type; 5 | import java.util.Collection; 6 | import lombok.RequiredArgsConstructor; 7 | import lombok.extern.slf4j.Slf4j; 8 | 9 | @Slf4j 10 | @RequiredArgsConstructor 11 | class SubscriptionConnectionInitCommand implements SubscriptionCommand { 12 | 13 | private final Collection connectionListeners; 14 | 15 | @Override 16 | public void apply(SubscriptionSession session, OperationMessage message) { 17 | log.debug("Apollo subscription connection init: {}", session); 18 | try { 19 | connectionListeners.forEach(it -> it.onConnect(session, message)); 20 | session.sendMessage(new OperationMessage(Type.GQL_CONNECTION_ACK, message.getId(), null)); 21 | } catch (Exception t) { 22 | log.error("Cannot initialize subscription command '{}'", message, t); 23 | session.sendMessage( 24 | new OperationMessage(Type.GQL_CONNECTION_ERROR, message.getId(), t.getMessage())); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /graphql-java-servlet/src/test/groovy/graphql/kickstart/servlet/SingleAsynchronousQueryResponseWriterTest.groovy: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.servlet 2 | 3 | import graphql.ExecutionResult 4 | import graphql.kickstart.execution.GraphQLObjectMapper 5 | import org.springframework.mock.web.MockAsyncContext 6 | import spock.lang.Specification 7 | 8 | import jakarta.servlet.http.HttpServletRequest 9 | import jakarta.servlet.http.HttpServletResponse 10 | 11 | class SingleAsynchronousQueryResponseWriterTest extends Specification { 12 | 13 | def "result data is no publisher should"() { 14 | given: 15 | def result = Mock(ExecutionResult) 16 | def objectMapper = Mock(GraphQLObjectMapper) 17 | def writer = new SingleAsynchronousQueryResponseWriter(result, objectMapper, 100) 18 | def request = Mock(HttpServletRequest) 19 | def responseWriter = new PrintWriter(new StringWriter()) 20 | def response = Mock(HttpServletResponse) 21 | response.getWriter() >> responseWriter 22 | def asyncContext = new MockAsyncContext(request, response) 23 | request.getAsyncContext() >> asyncContext 24 | request.isAsyncStarted() >> true 25 | objectMapper.serializeResultAsJson(result) >> "{ }" 26 | 27 | when: 28 | writer.write(request, response) 29 | 30 | then: 31 | noExceptionThrown() 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /graphql-java-servlet/src/main/java/graphql/kickstart/servlet/cache/CachingQueryResponseWriterFactory.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.servlet.cache; 2 | 3 | import graphql.kickstart.execution.GraphQLQueryResult; 4 | import graphql.kickstart.execution.input.GraphQLInvocationInput; 5 | import graphql.kickstart.servlet.GraphQLConfiguration; 6 | import graphql.kickstart.servlet.QueryResponseWriter; 7 | import graphql.kickstart.servlet.QueryResponseWriterFactory; 8 | import graphql.kickstart.servlet.QueryResponseWriterFactoryImpl; 9 | 10 | public class CachingQueryResponseWriterFactory implements QueryResponseWriterFactory { 11 | 12 | private final QueryResponseWriterFactory queryResponseWriterFactory = 13 | new QueryResponseWriterFactoryImpl(); 14 | 15 | @Override 16 | public QueryResponseWriter createWriter( 17 | GraphQLInvocationInput invocationInput, 18 | GraphQLQueryResult queryResult, 19 | GraphQLConfiguration configuration) { 20 | QueryResponseWriter writer = 21 | queryResponseWriterFactory.createWriter(invocationInput, queryResult, configuration); 22 | if (configuration.getResponseCacheManager() != null) { 23 | return new CachingQueryResponseWriter( 24 | writer, configuration.getResponseCacheManager(), invocationInput, queryResult.isError()); 25 | } 26 | return writer; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.github/replaceJakartaWithJavax.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Set jdk11 as source and target 4 | sed -i 's/SOURCE_COMPATIBILITY=.*/SOURCE_COMPATIBILITY=11/' gradle.properties 5 | sed -i 's/TARGET_COMPATIBILITY=.*/TARGET_COMPATIBILITY=11/' gradle.properties 6 | 7 | # Replace jakarta imports and dependencies with javax 8 | grep -rl 'import jakarta' ./graphql-java-servlet | xargs sed -i 's/import jakarta/import javax/g' 9 | sed -i 's/.*jakarta.websocket:jakarta.websocket-client-api.*//' graphql-java-servlet/build.gradle 10 | sed -i \ 11 | 's/jakarta.servlet:jakarta.servlet-api.*/javax.servlet:javax.servlet-api:$LIB_JAVAX_SERVLET"/' \ 12 | graphql-java-servlet/build.gradle 13 | sed -i \ 14 | 's/jakarta.websocket.*/javax.websocket:javax.websocket-api:$LIB_JAVAX_WEBSOCKET"/' \ 15 | graphql-java-servlet/build.gradle 16 | 17 | # Final check if there are something else to replace 18 | grep -rl 'jakarta' ./graphql-java-servlet | xargs sed -i 's/jakarta/javax/g' 19 | 20 | # Set the version 5 for spring framework 21 | sed -i \ 22 | 's/org.springframework:spring-test.*/org.springframework:spring-test:$LIB_SPRINGFRAMEWORK_5"/' \ 23 | graphql-java-servlet/build.gradle 24 | sed -i \ 25 | 's/org.springframework:spring-web.*/org.springframework:spring-web:$LIB_SPRINGFRAMEWORK_5"/' \ 26 | graphql-java-servlet/build.gradle 27 | 28 | echo "Replaced jakarta occurrences with javax" -------------------------------------------------------------------------------- /graphql-java-servlet/src/main/java/graphql/kickstart/servlet/apollo/ApolloWebSocketSubscriptionSession.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.servlet.apollo; 2 | 3 | import graphql.kickstart.execution.subscriptions.GraphQLSubscriptionMapper; 4 | import graphql.kickstart.execution.subscriptions.apollo.ApolloSubscriptionSession; 5 | import graphql.kickstart.servlet.subscriptions.WebSocketSubscriptionSession; 6 | import java.util.Map; 7 | import jakarta.websocket.Session; 8 | 9 | public class ApolloWebSocketSubscriptionSession extends ApolloSubscriptionSession { 10 | 11 | private final WebSocketSubscriptionSession webSocketSubscriptionSession; 12 | 13 | public ApolloWebSocketSubscriptionSession(GraphQLSubscriptionMapper mapper, Session session) { 14 | super(mapper); 15 | webSocketSubscriptionSession = new WebSocketSubscriptionSession(mapper, session); 16 | } 17 | 18 | @Override 19 | public boolean isOpen() { 20 | return webSocketSubscriptionSession.isOpen(); 21 | } 22 | 23 | @Override 24 | public Map getUserProperties() { 25 | return webSocketSubscriptionSession.getUserProperties(); 26 | } 27 | 28 | @Override 29 | public String getId() { 30 | return webSocketSubscriptionSession.getId(); 31 | } 32 | 33 | @Override 34 | public Session unwrap() { 35 | return webSocketSubscriptionSession.unwrap(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /graphql-java-servlet/src/main/java/graphql/kickstart/servlet/QueryResponseWriterFactoryImpl.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.servlet; 2 | 3 | import graphql.kickstart.execution.GraphQLQueryResult; 4 | import graphql.kickstart.execution.input.GraphQLInvocationInput; 5 | import java.util.Objects; 6 | 7 | public class QueryResponseWriterFactoryImpl implements QueryResponseWriterFactory { 8 | 9 | @Override 10 | public QueryResponseWriter createWriter( 11 | GraphQLInvocationInput invocationInput, 12 | GraphQLQueryResult queryResult, 13 | GraphQLConfiguration configuration) { 14 | Objects.requireNonNull(queryResult, "GraphQL query result cannot be null"); 15 | 16 | if (queryResult.isBatched()) { 17 | return new BatchedQueryResponseWriter( 18 | queryResult.getResults(), configuration.getObjectMapper()); 19 | } 20 | if (queryResult.isAsynchronous()) { 21 | return new SingleAsynchronousQueryResponseWriter( 22 | queryResult.getResult(), 23 | configuration.getObjectMapper(), 24 | configuration.getSubscriptionTimeout()); 25 | } 26 | if (queryResult.isError()) { 27 | return new ErrorQueryResponseWriter(queryResult.getStatusCode(), queryResult.getMessage()); 28 | } 29 | return new SingleQueryResponseWriter(queryResult.getResult(), configuration.getObjectMapper()); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /graphql-java-servlet/src/main/java/graphql/kickstart/servlet/context/DefaultGraphQLServletContextBuilder.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.servlet.context; 2 | 3 | import graphql.kickstart.execution.context.DefaultGraphQLContextBuilder; 4 | import graphql.kickstart.execution.context.GraphQLKickstartContext; 5 | import java.util.HashMap; 6 | import java.util.Map; 7 | import jakarta.servlet.http.HttpServletRequest; 8 | import jakarta.servlet.http.HttpServletResponse; 9 | import jakarta.websocket.Session; 10 | import jakarta.websocket.server.HandshakeRequest; 11 | 12 | /** Returns an empty context. */ 13 | public class DefaultGraphQLServletContextBuilder extends DefaultGraphQLContextBuilder 14 | implements GraphQLServletContextBuilder { 15 | 16 | @Override 17 | public GraphQLKickstartContext build(HttpServletRequest request, HttpServletResponse response) { 18 | Map map = new HashMap<>(); 19 | map.put(HttpServletRequest.class, request); 20 | map.put(HttpServletResponse.class, response); 21 | return GraphQLKickstartContext.of(map); 22 | } 23 | 24 | @Override 25 | public GraphQLKickstartContext build(Session session, HandshakeRequest handshakeRequest) { 26 | Map map = new HashMap<>(); 27 | map.put(Session.class, session); 28 | map.put(HandshakeRequest.class, handshakeRequest); 29 | return GraphQLKickstartContext.of(map); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/apollo/ApolloSubscriptionConsumer.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.execution.subscriptions.apollo; 2 | 3 | import com.fasterxml.jackson.core.JsonProcessingException; 4 | import graphql.kickstart.execution.GraphQLObjectMapper; 5 | import graphql.kickstart.execution.subscriptions.SubscriptionSession; 6 | import graphql.kickstart.execution.subscriptions.apollo.OperationMessage.Type; 7 | import java.util.function.Consumer; 8 | import lombok.RequiredArgsConstructor; 9 | import lombok.extern.slf4j.Slf4j; 10 | 11 | @Slf4j 12 | @RequiredArgsConstructor 13 | public class ApolloSubscriptionConsumer implements Consumer { 14 | 15 | private final SubscriptionSession session; 16 | private final GraphQLObjectMapper objectMapper; 17 | private final ApolloCommandProvider commandProvider; 18 | 19 | @Override 20 | public void accept(String request) { 21 | try { 22 | OperationMessage message = 23 | objectMapper.getJacksonMapper().readValue(request, OperationMessage.class); 24 | SubscriptionCommand command = commandProvider.getByType(message.getType()); 25 | command.apply(session, message); 26 | } catch (JsonProcessingException e) { 27 | log.error("Cannot read subscription command '{}'", request, e); 28 | session.sendMessage(new OperationMessage(Type.GQL_CONNECTION_ERROR, null, e.getMessage())); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /graphql-java-servlet/src/main/java/graphql/kickstart/servlet/cache/GraphQLResponseCacheManager.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.servlet.cache; 2 | 3 | import graphql.kickstart.execution.input.GraphQLInvocationInput; 4 | import java.util.Optional; 5 | import jakarta.servlet.http.HttpServletRequest; 6 | 7 | public interface GraphQLResponseCacheManager { 8 | 9 | /** 10 | * Retrieve the cache by input data. If this query was not cached before, will return empty {@link 11 | * Optional}. 12 | * 13 | * @param request the http request 14 | * @param invocationInput input data 15 | * @return cached response if something available in cache or {@literal null} if nothing cached 16 | */ 17 | CachedResponse get(HttpServletRequest request, GraphQLInvocationInput invocationInput); 18 | 19 | /** 20 | * Decide to cache or not this response. It depends on the implementation. 21 | * 22 | * @param request the http request 23 | * @param invocationInput input data 24 | */ 25 | boolean isCacheable(HttpServletRequest request, GraphQLInvocationInput invocationInput); 26 | 27 | /** 28 | * Cache this response. It depends on the implementation. 29 | * 30 | * @param request the http request 31 | * @param invocationInput input data 32 | * @param cachedResponse response to cache 33 | */ 34 | void put( 35 | HttpServletRequest request, 36 | GraphQLInvocationInput invocationInput, 37 | CachedResponse cachedResponse); 38 | } 39 | -------------------------------------------------------------------------------- /graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/apollo/KeepAliveSubscriptionConnectionListener.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.execution.subscriptions.apollo; 2 | 3 | import graphql.kickstart.execution.subscriptions.SubscriptionSession; 4 | import java.time.Duration; 5 | 6 | public class KeepAliveSubscriptionConnectionListener 7 | implements ApolloSubscriptionConnectionListener { 8 | 9 | protected final ApolloSubscriptionKeepAliveRunner keepAliveRunner; 10 | 11 | public KeepAliveSubscriptionConnectionListener() { 12 | this(Duration.ofSeconds(15)); 13 | } 14 | 15 | public KeepAliveSubscriptionConnectionListener(Duration keepAliveInterval) { 16 | keepAliveRunner = new ApolloSubscriptionKeepAliveRunner(keepAliveInterval); 17 | } 18 | 19 | @Override 20 | public void onConnect(SubscriptionSession session, OperationMessage message) { 21 | keepAliveRunner.keepAlive(session); 22 | } 23 | 24 | @Override 25 | public void onStart(SubscriptionSession session, OperationMessage message) { 26 | // do nothing 27 | } 28 | 29 | @Override 30 | public void onStop(SubscriptionSession session, OperationMessage message) { 31 | // do nothing 32 | } 33 | 34 | @Override 35 | public void onTerminate(SubscriptionSession session, OperationMessage message) { 36 | keepAliveRunner.abort(session); 37 | } 38 | 39 | @Override 40 | public void shutdown() { 41 | keepAliveRunner.shutdown(); 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /.github/tag-release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -ev 3 | 4 | getVersion() { 5 | ./gradlew properties -q | grep -E "^version" | awk '{print $2}' | tr -d '[:space:]' 6 | } 7 | 8 | removeSnapshots() { 9 | sed -i 's/-SNAPSHOT//' gradle.properties 10 | } 11 | 12 | commitRelease() { 13 | local APP_VERSION 14 | APP_VERSION=$(getVersion) 15 | git commit -a -m "Update version for release" 16 | git tag -a "v${APP_VERSION}" -m "Tag release version" 17 | } 18 | 19 | bumpVersion() { 20 | echo "Bump version number" 21 | local APP_VERSION 22 | APP_VERSION=$(getVersion | xargs) 23 | local SEMANTIC_REGEX='^([0-9]+)\.([0-9]+)(\.([0-9]+))?$' 24 | if [[ ${APP_VERSION} =~ ${SEMANTIC_REGEX} ]]; then 25 | if [[ ${BASH_REMATCH[4]} ]]; then 26 | nextVersion=$((BASH_REMATCH[4] + 1)) 27 | nextVersion="${BASH_REMATCH[1]}.${BASH_REMATCH[2]}.${nextVersion}-SNAPSHOT" 28 | else 29 | nextVersion=$((BASH_REMATCH[2] + 1)) 30 | nextVersion="${BASH_REMATCH[1]}.${nextVersion}-SNAPSHOT" 31 | fi 32 | 33 | echo "Next version: ${nextVersion}" 34 | sed -i -E "s/^version(\s)?=.*/version=${nextVersion}/" gradle.properties 35 | git commit -a -m "Bumped version for next release" 36 | else 37 | echo "No semantic version and therefore cannot publish to maven repository: '${APP_VERSION}'" 38 | fi 39 | } 40 | 41 | git config --global user.email "actions@github.com" 42 | git config --global user.name "GitHub Actions" 43 | 44 | echo "Deploying release to Maven Central" 45 | removeSnapshots 46 | commitRelease 47 | bumpVersion 48 | git push --follow-tags -------------------------------------------------------------------------------- /graphql-java-kickstart/src/main/java/graphql/kickstart/execution/error/RenderableNonNullableFieldWasNullError.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.execution.error; 2 | 3 | import com.fasterxml.jackson.annotation.JsonInclude; 4 | import graphql.ErrorType; 5 | import graphql.GraphQLError; 6 | import graphql.execution.NonNullableFieldWasNullError; 7 | import graphql.language.SourceLocation; 8 | import java.util.List; 9 | import java.util.Map; 10 | 11 | class RenderableNonNullableFieldWasNullError implements GraphQLError { 12 | 13 | private final NonNullableFieldWasNullError delegate; 14 | 15 | public RenderableNonNullableFieldWasNullError( 16 | NonNullableFieldWasNullError nonNullableFieldWasNullError) { 17 | this.delegate = nonNullableFieldWasNullError; 18 | } 19 | 20 | @Override 21 | public String getMessage() { 22 | return delegate.getMessage(); 23 | } 24 | 25 | @Override 26 | @JsonInclude(JsonInclude.Include.NON_NULL) 27 | public List getLocations() { 28 | return delegate.getLocations(); 29 | } 30 | 31 | @Override 32 | public ErrorType getErrorType() { 33 | return delegate.getErrorType(); 34 | } 35 | 36 | @Override 37 | public List getPath() { 38 | return delegate.getPath(); 39 | } 40 | 41 | @Override 42 | public Map toSpecification() { 43 | return delegate.toSpecification(); 44 | } 45 | 46 | @Override 47 | @JsonInclude(JsonInclude.Include.NON_NULL) 48 | public Map getExtensions() { 49 | return delegate.getExtensions(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /graphql-java-kickstart/src/main/java/graphql/kickstart/execution/OperationNameExtractor.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.execution; 2 | 3 | import static graphql.kickstart.execution.StringUtils.isNotEmpty; 4 | 5 | import graphql.language.Document; 6 | import graphql.language.OperationDefinition; 7 | import graphql.parser.InvalidSyntaxException; 8 | import graphql.parser.Parser; 9 | import java.util.List; 10 | import lombok.AccessLevel; 11 | import lombok.NoArgsConstructor; 12 | 13 | @NoArgsConstructor(access = AccessLevel.PRIVATE) 14 | class OperationNameExtractor { 15 | 16 | static String extractOperationName( 17 | String gqlQuery, String requestedOperationName, String defaultIfNotFound) { 18 | if (isNotEmpty(requestedOperationName)) { 19 | return requestedOperationName; 20 | } 21 | if (isNotEmpty(gqlQuery)) { 22 | return parseForOperationName(gqlQuery, defaultIfNotFound); 23 | } 24 | return defaultIfNotFound; 25 | } 26 | 27 | private static String parseForOperationName(String gqlQuery, String defaultIfNotFound) { 28 | try { 29 | Document document = new Parser().parseDocument(gqlQuery); 30 | List operations = 31 | document.getDefinitionsOfType(OperationDefinition.class); 32 | if (operations.size() == 1) { 33 | String name = operations.get(0).getName(); 34 | if (isNotEmpty(name)) { 35 | return name; 36 | } 37 | } 38 | } catch (InvalidSyntaxException ignored) { 39 | // ignored 40 | } 41 | return defaultIfNotFound; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /graphql-java-kickstart/src/main/java/graphql/kickstart/execution/context/DefaultGraphQLContext.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.execution.context; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | import java.util.Objects; 6 | import org.dataloader.DataLoaderRegistry; 7 | 8 | /** 9 | * An object for the DefaultGraphQLContextBuilder to return. Can be extended to include more 10 | * context. 11 | */ 12 | public class DefaultGraphQLContext implements GraphQLKickstartContext { 13 | 14 | private final DataLoaderRegistry dataLoaderRegistry; 15 | private final Map map; 16 | 17 | public DefaultGraphQLContext(DataLoaderRegistry dataLoaderRegistry, Map map) { 18 | this.dataLoaderRegistry = 19 | Objects.requireNonNull(dataLoaderRegistry, "dataLoaderRegistry is required"); 20 | this.map = Objects.requireNonNull(map, "map is required"); 21 | } 22 | 23 | public DefaultGraphQLContext(Map map) { 24 | this(new DataLoaderRegistry(), map); 25 | } 26 | 27 | public DefaultGraphQLContext(DataLoaderRegistry dataLoaderRegistry) { 28 | this(dataLoaderRegistry, new HashMap<>()); 29 | } 30 | 31 | public DefaultGraphQLContext() { 32 | this(new DataLoaderRegistry()); 33 | } 34 | 35 | public void put(Object key, Object value) { 36 | map.put(key, value); 37 | } 38 | 39 | @Override 40 | public DataLoaderRegistry getDataLoaderRegistry() { 41 | return dataLoaderRegistry; 42 | } 43 | 44 | @Override 45 | public Map getMapOfContext() { 46 | return map; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /graphql-java-kickstart/src/main/java/graphql/kickstart/execution/instrumentation/DataLoaderDispatcherInstrumentationState.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.execution.instrumentation; 2 | 3 | import graphql.execution.ExecutionId; 4 | import graphql.execution.instrumentation.InstrumentationState; 5 | import org.dataloader.DataLoaderRegistry; 6 | 7 | /** A base class that keeps track of whether aggressive batching can be used */ 8 | public class DataLoaderDispatcherInstrumentationState implements InstrumentationState { 9 | 10 | private final TrackingApproach approach; 11 | private final DataLoaderRegistry dataLoaderRegistry; 12 | private final boolean hasNoDataLoaders; 13 | private boolean aggressivelyBatching = true; 14 | 15 | public DataLoaderDispatcherInstrumentationState( 16 | DataLoaderRegistry dataLoaderRegistry, TrackingApproach approach, ExecutionId executionId) { 17 | 18 | this.dataLoaderRegistry = dataLoaderRegistry; 19 | this.approach = approach; 20 | approach.createState(executionId); 21 | hasNoDataLoaders = dataLoaderRegistry.getKeys().isEmpty(); 22 | } 23 | 24 | boolean isAggressivelyBatching() { 25 | return aggressivelyBatching; 26 | } 27 | 28 | void setAggressivelyBatching(boolean aggressivelyBatching) { 29 | this.aggressivelyBatching = aggressivelyBatching; 30 | } 31 | 32 | TrackingApproach getApproach() { 33 | return approach; 34 | } 35 | 36 | DataLoaderRegistry getDataLoaderRegistry() { 37 | return dataLoaderRegistry; 38 | } 39 | 40 | boolean hasNoDataLoaders() { 41 | return hasNoDataLoaders; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /graphql-java-kickstart/src/main/java/graphql/kickstart/execution/config/DefaultExecutionStrategyProvider.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.execution.config; 2 | 3 | import graphql.execution.ExecutionStrategy; 4 | 5 | /** @author Andrew Potter */ 6 | public class DefaultExecutionStrategyProvider implements ExecutionStrategyProvider { 7 | 8 | private final ExecutionStrategy queryExecutionStrategy; 9 | private final ExecutionStrategy mutationExecutionStrategy; 10 | private final ExecutionStrategy subscriptionExecutionStrategy; 11 | 12 | public DefaultExecutionStrategyProvider() { 13 | this(null); 14 | } 15 | 16 | public DefaultExecutionStrategyProvider(ExecutionStrategy executionStrategy) { 17 | this(executionStrategy, executionStrategy, null); 18 | } 19 | 20 | public DefaultExecutionStrategyProvider( 21 | ExecutionStrategy queryExecutionStrategy, 22 | ExecutionStrategy mutationExecutionStrategy, 23 | ExecutionStrategy subscriptionExecutionStrategy) { 24 | this.queryExecutionStrategy = queryExecutionStrategy; 25 | this.mutationExecutionStrategy = mutationExecutionStrategy; 26 | this.subscriptionExecutionStrategy = subscriptionExecutionStrategy; 27 | } 28 | 29 | @Override 30 | public ExecutionStrategy getQueryExecutionStrategy() { 31 | return queryExecutionStrategy; 32 | } 33 | 34 | @Override 35 | public ExecutionStrategy getMutationExecutionStrategy() { 36 | return mutationExecutionStrategy; 37 | } 38 | 39 | @Override 40 | public ExecutionStrategy getSubscriptionExecutionStrategy() { 41 | return subscriptionExecutionStrategy; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /graphql-java-servlet/src/test/groovy/graphql/kickstart/servlet/GraphQLServletListenerSpec.groovy: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.servlet 2 | 3 | import graphql.kickstart.servlet.core.GraphQLServletListener 4 | import spock.lang.Specification 5 | 6 | class GraphQLServletListenerSpec extends Specification { 7 | 8 | def listener = Mock(GraphQLServletListener) 9 | def requestCallback = Mock(GraphQLServletListener.RequestCallback) 10 | def tester = new RequestTester(listener) 11 | 12 | def "query over HTTP GET calls onRequest listener"() { 13 | given: "a valid graphql query request" 14 | tester.addParameter('query', 'query { echo(arg:"test") }') 15 | 16 | and: "a listener that always returns request callback" 17 | listener.onRequest(tester.request, tester.response) >> requestCallback 18 | 19 | when: "we execute a GET request" 20 | tester.doGet() 21 | 22 | then: 23 | tester.assertThatResponseIsOk() 24 | tester.assertThatContentTypeIsJson() 25 | 1 * listener.onRequest(tester.request, tester.response) 26 | } 27 | 28 | def "query over HTTP GET calls onSuccess callback"() { 29 | given: "a valid graphql query request" 30 | tester.addParameter('query', 'query { echo(arg:"test") }') 31 | 32 | and: "a listener that always returns request callback" 33 | listener.onRequest(tester.request, tester.response) >> requestCallback 34 | 35 | when: "we execute a GET request" 36 | tester.doGet() 37 | 38 | then: 39 | tester.assertThatResponseIsOk() 40 | tester.assertThatContentTypeIsJson() 41 | 1 * requestCallback.onSuccess(tester.request, tester.response) 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /graphql-java-kickstart/src/main/java/graphql/kickstart/execution/config/ConfiguringObjectMapperProvider.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.execution.config; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import graphql.kickstart.execution.error.DefaultGraphQLServletObjectMapperConfigurer; 5 | 6 | public class ConfiguringObjectMapperProvider implements ObjectMapperProvider { 7 | 8 | private final ObjectMapper objectMapperTemplate; 9 | 10 | private final GraphQLServletObjectMapperConfigurer objectMapperConfigurer; 11 | 12 | public ConfiguringObjectMapperProvider( 13 | ObjectMapper objectMapperTemplate, 14 | GraphQLServletObjectMapperConfigurer objectMapperConfigurer) { 15 | this.objectMapperTemplate = 16 | objectMapperTemplate == null ? new ObjectMapper() : objectMapperTemplate; 17 | this.objectMapperConfigurer = 18 | objectMapperConfigurer == null 19 | ? new DefaultGraphQLServletObjectMapperConfigurer() 20 | : objectMapperConfigurer; 21 | } 22 | 23 | public ConfiguringObjectMapperProvider(ObjectMapper objectMapperTemplate) { 24 | this(objectMapperTemplate, null); 25 | } 26 | 27 | public ConfiguringObjectMapperProvider( 28 | GraphQLServletObjectMapperConfigurer objectMapperConfigurer) { 29 | this(null, objectMapperConfigurer); 30 | } 31 | 32 | public ConfiguringObjectMapperProvider() { 33 | this(null, null); 34 | } 35 | 36 | @Override 37 | public ObjectMapper provide() { 38 | ObjectMapper mapper = this.objectMapperTemplate.copy(); 39 | this.objectMapperConfigurer.configure(mapper); 40 | return mapper; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /examples/osgi/providers/src/main/java/graphql/servlet/examples/osgi/ExampleGraphQLProvider.java: -------------------------------------------------------------------------------- 1 | package graphql.servlet.examples.osgi; 2 | 3 | import graphql.schema.GraphQLFieldDefinition; 4 | import graphql.schema.GraphQLType; 5 | import graphql.kickstart.servlet.osgi.GraphQLMutationProvider; 6 | import graphql.kickstart.servlet.osgi.GraphQLQueryProvider; 7 | import graphql.kickstart.servlet.osgi.GraphQLTypesProvider; 8 | import org.osgi.service.component.annotations.Component; 9 | 10 | import java.util.ArrayList; 11 | import java.util.Collection; 12 | import java.util.List; 13 | 14 | import static graphql.Scalars.GraphQLString; 15 | import static graphql.schema.GraphQLFieldDefinition.*; 16 | 17 | @Component( 18 | name = "ExampleGraphQLProvider", 19 | immediate = true 20 | ) 21 | public class ExampleGraphQLProvider implements GraphQLQueryProvider, GraphQLMutationProvider, 22 | GraphQLTypesProvider { 23 | 24 | public Collection getQueries() { 25 | List fieldDefinitions = new ArrayList(); 26 | fieldDefinitions.add(newFieldDefinition() 27 | .type(GraphQLString) 28 | .name("hello") 29 | .description( 30 | "Basic example of a GraphQL Java Servlet provider using the Apache Karaf OSGi Runtime") 31 | .staticValue("world") 32 | .build()); 33 | return fieldDefinitions; 34 | } 35 | 36 | public Collection getMutations() { 37 | return new ArrayList(); 38 | } 39 | 40 | public Collection getTypes() { 41 | return new ArrayList(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /graphql-java-kickstart/src/main/java/graphql/kickstart/execution/input/PerRequestBatchedInvocationInput.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.execution.input; 2 | 3 | import static java.util.stream.Collectors.toList; 4 | 5 | import graphql.kickstart.execution.GraphQLRequest; 6 | import graphql.kickstart.execution.context.ContextSetting; 7 | import graphql.kickstart.execution.context.GraphQLKickstartContext; 8 | import graphql.schema.GraphQLSchema; 9 | import java.util.List; 10 | import java.util.function.Supplier; 11 | import lombok.Getter; 12 | 13 | /** A collection of GraphQLSingleInvocationInputs that share a context object. */ 14 | @Getter 15 | public class PerRequestBatchedInvocationInput implements GraphQLBatchedInvocationInput { 16 | 17 | private final List invocationInputs; 18 | private final ContextSetting contextSetting; 19 | 20 | public PerRequestBatchedInvocationInput( 21 | List requests, 22 | GraphQLSchema schema, 23 | Supplier contextSupplier, 24 | Object root, 25 | ContextSetting contextSetting) { 26 | GraphQLKickstartContext context = contextSupplier.get(); 27 | invocationInputs = 28 | requests.stream() 29 | .map(request -> new GraphQLSingleInvocationInput(request, schema, context, root)) 30 | .collect(toList()); 31 | this.contextSetting = contextSetting; 32 | } 33 | 34 | @Override 35 | public List getQueries() { 36 | return invocationInputs.stream() 37 | .map(GraphQLSingleInvocationInput::getQueries) 38 | .flatMap(List::stream) 39 | .collect(toList()); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/SessionSubscriptions.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.execution.subscriptions; 2 | 3 | import java.util.Map; 4 | import java.util.concurrent.ConcurrentHashMap; 5 | import org.reactivestreams.Subscription; 6 | 7 | /** @author Andrew Potter */ 8 | public class SessionSubscriptions { 9 | 10 | private final Object lock = new Object(); 11 | 12 | private boolean closed = false; 13 | private Map subscriptions = new ConcurrentHashMap<>(); 14 | 15 | public void add(Subscription subscription) { 16 | add(getImplicitId(subscription), subscription); 17 | } 18 | 19 | public void add(String id, Subscription subscription) { 20 | synchronized (lock) { 21 | if (closed) { 22 | throw new IllegalStateException("Websocket was already closed!"); 23 | } 24 | subscriptions.put(id, subscription); 25 | } 26 | } 27 | 28 | public void cancel(Subscription subscription) { 29 | cancel(getImplicitId(subscription)); 30 | } 31 | 32 | public void cancel(String id) { 33 | Subscription subscription = subscriptions.remove(id); 34 | if (subscription != null) { 35 | subscription.cancel(); 36 | } 37 | } 38 | 39 | public void close() { 40 | synchronized (lock) { 41 | closed = true; 42 | subscriptions.forEach((k, v) -> v.cancel()); 43 | subscriptions.clear(); 44 | } 45 | } 46 | 47 | private String getImplicitId(Subscription subscription) { 48 | return String.valueOf(subscription.hashCode()); 49 | } 50 | 51 | public int getSubscriptionCount() { 52 | return subscriptions.size(); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /graphql-java-servlet/src/main/java/graphql/kickstart/servlet/subscriptions/WebSocketSendSubscriber.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.servlet.subscriptions; 2 | 3 | import java.io.IOException; 4 | import java.util.concurrent.atomic.AtomicReference; 5 | import jakarta.websocket.Session; 6 | import lombok.RequiredArgsConstructor; 7 | import lombok.extern.slf4j.Slf4j; 8 | import org.reactivestreams.Subscriber; 9 | import org.reactivestreams.Subscription; 10 | 11 | @Slf4j 12 | @RequiredArgsConstructor 13 | public class WebSocketSendSubscriber implements Subscriber { 14 | 15 | private final Session session; 16 | private AtomicReference subscriptionRef = new AtomicReference<>(); 17 | 18 | @Override 19 | public void onSubscribe(Subscription subscription) { 20 | subscriptionRef.set(subscription); 21 | subscriptionRef.get().request(1); 22 | } 23 | 24 | @Override 25 | public void onNext(String message) { 26 | subscriptionRef.get().request(1); 27 | if (session.isOpen()) { 28 | try { 29 | session.getBasicRemote().sendText(message); 30 | } catch (IOException e) { 31 | log.error("Cannot send message {}", message, e); 32 | } 33 | } 34 | } 35 | 36 | @Override 37 | public void onError(Throwable t) { 38 | log.error("WebSocket error", t); 39 | } 40 | 41 | @Override 42 | public void onComplete() { 43 | subscriptionRef.get().request(1); 44 | if (session.isOpen()) { 45 | try { 46 | log.debug("Closing session"); 47 | session.close(); 48 | } catch (IOException e) { 49 | log.error("Cannot close session", e); 50 | } 51 | } 52 | subscriptionRef.get().cancel(); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /graphql-java-kickstart/src/main/java/graphql/kickstart/execution/input/PerQueryBatchedInvocationInput.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.execution.input; 2 | 3 | import static java.util.stream.Collectors.toList; 4 | 5 | import graphql.kickstart.execution.GraphQLRequest; 6 | import graphql.kickstart.execution.context.ContextSetting; 7 | import graphql.kickstart.execution.context.GraphQLKickstartContext; 8 | import graphql.schema.GraphQLSchema; 9 | import java.util.List; 10 | import java.util.function.Supplier; 11 | import java.util.stream.Collectors; 12 | import lombok.Getter; 13 | 14 | /** A Collection of GraphQLSingleInvocationInput that each have a unique context object. */ 15 | @Getter 16 | public class PerQueryBatchedInvocationInput implements GraphQLBatchedInvocationInput { 17 | 18 | private final List invocationInputs; 19 | private final ContextSetting contextSetting; 20 | 21 | public PerQueryBatchedInvocationInput( 22 | List requests, 23 | GraphQLSchema schema, 24 | Supplier contextSupplier, 25 | Object root, 26 | ContextSetting contextSetting) { 27 | invocationInputs = 28 | requests.stream() 29 | .map( 30 | request -> 31 | new GraphQLSingleInvocationInput(request, schema, contextSupplier.get(), root)) 32 | .collect(Collectors.toList()); 33 | this.contextSetting = contextSetting; 34 | } 35 | 36 | @Override 37 | public List getQueries() { 38 | return invocationInputs.stream() 39 | .map(GraphQLSingleInvocationInput::getQueries) 40 | .flatMap(List::stream) 41 | .collect(toList()); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/apollo/ApolloCommandProvider.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.execution.subscriptions.apollo; 2 | 3 | import graphql.kickstart.execution.GraphQLInvoker; 4 | import graphql.kickstart.execution.subscriptions.GraphQLSubscriptionInvocationInputFactory; 5 | import graphql.kickstart.execution.subscriptions.GraphQLSubscriptionMapper; 6 | import graphql.kickstart.execution.subscriptions.apollo.OperationMessage.Type; 7 | import java.util.Collection; 8 | import java.util.EnumMap; 9 | 10 | public class ApolloCommandProvider { 11 | 12 | private final EnumMap commands = new EnumMap<>(Type.class); 13 | 14 | public ApolloCommandProvider( 15 | GraphQLSubscriptionMapper mapper, 16 | GraphQLSubscriptionInvocationInputFactory invocationInputFactory, 17 | GraphQLInvoker graphQLInvoker, 18 | Collection connectionListeners) { 19 | commands.put( 20 | Type.GQL_CONNECTION_INIT, new SubscriptionConnectionInitCommand(connectionListeners)); 21 | commands.put( 22 | Type.GQL_START, 23 | new SubscriptionStartCommand( 24 | mapper, invocationInputFactory, graphQLInvoker, connectionListeners)); 25 | commands.put(Type.GQL_STOP, new SubscriptionStopCommand(connectionListeners)); 26 | commands.put( 27 | Type.GQL_CONNECTION_TERMINATE, 28 | new SubscriptionConnectionTerminateCommand(connectionListeners)); 29 | } 30 | 31 | public SubscriptionCommand getByType(Type type) { 32 | if (commands.containsKey(type)) { 33 | return commands.get(type); 34 | } 35 | throw new IllegalStateException("No command found for type " + type); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /graphql-java-servlet/src/main/java/graphql/kickstart/servlet/subscriptions/FallbackSubscriptionProtocolFactory.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.servlet.subscriptions; 2 | 3 | import graphql.kickstart.execution.GraphQLInvoker; 4 | import graphql.kickstart.execution.subscriptions.GraphQLSubscriptionInvocationInputFactory; 5 | import graphql.kickstart.execution.subscriptions.GraphQLSubscriptionMapper; 6 | import graphql.kickstart.execution.subscriptions.SubscriptionProtocolFactory; 7 | import graphql.kickstart.execution.subscriptions.SubscriptionSession; 8 | import java.util.function.Consumer; 9 | import jakarta.websocket.Session; 10 | 11 | /** @author Andrew Potter */ 12 | public class FallbackSubscriptionProtocolFactory extends SubscriptionProtocolFactory 13 | implements WebSocketSubscriptionProtocolFactory { 14 | 15 | private final GraphQLSubscriptionMapper mapper; 16 | private final GraphQLSubscriptionInvocationInputFactory invocationInputFactory; 17 | private final GraphQLInvoker graphQLInvoker; 18 | 19 | public FallbackSubscriptionProtocolFactory( 20 | GraphQLSubscriptionMapper mapper, 21 | GraphQLSubscriptionInvocationInputFactory invocationInputFactory, 22 | GraphQLInvoker graphQLInvoker) { 23 | super(""); 24 | this.mapper = mapper; 25 | this.invocationInputFactory = invocationInputFactory; 26 | this.graphQLInvoker = graphQLInvoker; 27 | } 28 | 29 | @Override 30 | public Consumer createConsumer(SubscriptionSession session) { 31 | return new FallbackSubscriptionConsumer( 32 | session, mapper, invocationInputFactory, graphQLInvoker); 33 | } 34 | 35 | @Override 36 | public SubscriptionSession createSession(Session session) { 37 | return new WebSocketSubscriptionSession(mapper, session); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /graphql-java-servlet/src/test/groovy/graphql/kickstart/servlet/TestMultipartPart.groovy: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.servlet 2 | 3 | import jakarta.servlet.http.Part 4 | 5 | /** 6 | * @author Andrew Potter 7 | */ 8 | class TestMultipartContentBuilder { 9 | 10 | static Part createPart(String name, String part) { 11 | return new MockPart(name, part) 12 | } 13 | 14 | static class MockPart implements Part { 15 | final String name 16 | final String content 17 | 18 | MockPart(String name, String content) { 19 | this.name = name 20 | this.content = content 21 | } 22 | 23 | @Override 24 | InputStream getInputStream() throws IOException { 25 | return new ByteArrayInputStream(content.getBytes()) 26 | } 27 | 28 | @Override 29 | String getContentType() { 30 | return null 31 | } 32 | 33 | @Override 34 | String getName() { 35 | return name 36 | } 37 | 38 | @Override 39 | String getSubmittedFileName() { 40 | return name 41 | } 42 | 43 | @Override 44 | long getSize() { 45 | return content.getBytes().length 46 | } 47 | 48 | @Override 49 | void write(String fileName) throws IOException { 50 | throw new IllegalArgumentException("Not supported") 51 | } 52 | 53 | @Override 54 | void delete() throws IOException { 55 | throw new IllegalArgumentException("Not supported") 56 | } 57 | 58 | @Override 59 | String getHeader(String name) { 60 | return null 61 | } 62 | 63 | @Override 64 | Collection getHeaders(String name) { 65 | return Collections.emptyList() 66 | } 67 | 68 | @Override 69 | Collection getHeaderNames() { 70 | return Collections.emptyList() 71 | } 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/SubscriptionSession.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.execution.subscriptions; 2 | 3 | import graphql.ExecutionResult; 4 | import java.util.Map; 5 | import org.reactivestreams.Publisher; 6 | import org.reactivestreams.Subscription; 7 | 8 | public interface SubscriptionSession { 9 | 10 | void subscribe(String id, Publisher data); 11 | 12 | void add(String id, Subscription subscription); 13 | 14 | void unsubscribe(String id); 15 | 16 | void send(String message); 17 | 18 | void sendMessage(Object payload); 19 | 20 | void sendDataMessage(String id, Object payload); 21 | 22 | void sendErrorMessage(String id, Object payload); 23 | 24 | void sendCompleteMessage(String id); 25 | 26 | void close(String reason); 27 | 28 | /** 29 | * While the session is open, this method returns a Map that the developer may use to store 30 | * application specific information relating to this session instance. The developer may retrieve 31 | * information from this Map at any time between the opening of the session and during the 32 | * onClose() method. But outside that time, any information stored using this Map may no longer be 33 | * kept by the container. Web socket applications running on distributed implementations of the 34 | * web container should make any application specific objects stored here java.io.Serializable, or 35 | * the object may not be recreated after a failover. 36 | * 37 | * @return an editable Map of application data. 38 | */ 39 | Map getUserProperties(); 40 | 41 | boolean isOpen(); 42 | 43 | String getId(); 44 | 45 | SessionSubscriptions getSubscriptions(); 46 | 47 | Object unwrap(); 48 | 49 | Publisher getPublisher(); 50 | } 51 | -------------------------------------------------------------------------------- /graphql-java-servlet/src/main/java/graphql/kickstart/servlet/input/BatchInputPreProcessResult.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.servlet.input; 2 | 3 | import graphql.kickstart.execution.input.GraphQLBatchedInvocationInput; 4 | 5 | /** 6 | * Wraps the result of pre processing a batch. Allows customization of the response code and message 7 | * if the batch isn't to be executed. 8 | */ 9 | public class BatchInputPreProcessResult { 10 | 11 | private final GraphQLBatchedInvocationInput batchedInvocationInput; 12 | 13 | private final int statusCode; 14 | 15 | private final boolean executable; 16 | 17 | private final String messsage; 18 | 19 | public BatchInputPreProcessResult(GraphQLBatchedInvocationInput graphQLBatchedInvocationInput) { 20 | this.batchedInvocationInput = graphQLBatchedInvocationInput; 21 | this.executable = true; 22 | this.statusCode = 200; 23 | this.messsage = null; 24 | } 25 | 26 | public BatchInputPreProcessResult(int statusCode, String messsage) { 27 | this.batchedInvocationInput = null; 28 | this.executable = false; 29 | this.statusCode = statusCode; 30 | this.messsage = messsage; 31 | } 32 | 33 | /** @return If the servlet should try executing this batched input */ 34 | public boolean isExecutable() { 35 | return executable; 36 | } 37 | 38 | /** @return the batched input the servlet will try to execute. */ 39 | public GraphQLBatchedInvocationInput getBatchedInvocationInput() { 40 | return batchedInvocationInput; 41 | } 42 | 43 | /** @return status message the servlet will use if isExecutable is false. */ 44 | public String getStatusMessage() { 45 | return messsage; 46 | } 47 | 48 | /** @return status code the servlet will use if if isExecutable is false. */ 49 | public int getStatusCode() { 50 | return statusCode; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /graphql-java-servlet/src/test/groovy/graphql/kickstart/servlet/SingleQueryResponseWriterTest.groovy: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.servlet 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper 4 | import graphql.ExecutionResultImpl 5 | import graphql.kickstart.execution.GraphQLObjectMapper 6 | import spock.lang.Specification 7 | import spock.lang.Unroll 8 | 9 | import jakarta.servlet.ServletOutputStream 10 | import jakarta.servlet.http.HttpServletRequest 11 | import jakarta.servlet.http.HttpServletResponse 12 | import java.nio.charset.StandardCharsets 13 | 14 | class SingleQueryResponseWriterTest extends Specification { 15 | 16 | @Unroll 17 | def "should write utf8 results into the response with content #result"() { 18 | given: 19 | def graphQLObjectMapperMock = GraphQLObjectMapper.newBuilder().withObjectMapperProvider({ new ObjectMapper() }).build() 20 | graphQLObjectMapperMock.getJacksonMapper() >> new ObjectMapper() 21 | 22 | def requestMock = Mock(HttpServletRequest) 23 | def responseMock = Mock(HttpServletResponse) 24 | responseMock.getOutputStream() >> Mock(ServletOutputStream) 25 | 26 | 1 * responseMock.setContentLength(expectedContentLenght) 27 | 1 * responseMock.setCharacterEncoding(StandardCharsets.UTF_8.name()) 28 | 1 * responseMock.getOutputStream().write(expectedResponseContent.getBytes(StandardCharsets.UTF_8)) 29 | 30 | expect: 31 | def writer = new SingleQueryResponseWriter(new ExecutionResultImpl(result, []), graphQLObjectMapperMock) 32 | writer.write(requestMock, responseMock) 33 | 34 | where: 35 | result || expectedContentLenght | expectedResponseContent 36 | [testValue: "abcde"] || 30 | """{"data":{"testValue":"abcde"}}""" 37 | [testValue: "äöüüöß"] || 37 | """{"data":{"testValue":"äöüüöß"}}""" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /graphql-java-servlet/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | mavenCentral() 4 | } 5 | } 6 | 7 | apply plugin: 'groovy' 8 | apply plugin: 'java-library-distribution' 9 | apply plugin: 'biz.aQute.bnd.builder' 10 | 11 | jar { 12 | bndfile = 'bnd.bnd' 13 | } 14 | 15 | dependencies { 16 | api(project(':graphql-java-kickstart')) 17 | 18 | // Servlet 19 | compileOnly "jakarta.servlet:jakarta.servlet-api:6.1.0" 20 | compileOnly "jakarta.websocket:jakarta.websocket-api:2.2.0" 21 | compileOnly "jakarta.websocket:jakarta.websocket-client-api:2.2.0" 22 | implementation "org.slf4j:slf4j-api:$LIB_SLF4J_VER" 23 | 24 | // OSGi 25 | compileOnly 'org.osgi:org.osgi.core:6.0.0' 26 | compileOnly 'org.osgi:org.osgi.service.cm:1.6.1' 27 | compileOnly 'org.osgi:org.osgi.service.component:1.5.1' 28 | compileOnly 'org.osgi:org.osgi.service.component.annotations:1.5.1' 29 | compileOnly 'org.osgi:org.osgi.service.metatype.annotations:1.4.1' 30 | compileOnly 'org.osgi:org.osgi.annotation:6.0.0' 31 | 32 | testImplementation 'io.github.graphql-java:graphql-java-annotations:9.1' 33 | 34 | // Unit testing 35 | testImplementation "org.apache.groovy:groovy-all:4.0.23" 36 | testImplementation "org.spockframework:spock-core:2.3-groovy-4.0" 37 | testRuntimeOnly "net.bytebuddy:byte-buddy:1.15.2" 38 | testRuntimeOnly "org.objenesis:objenesis:3.4" 39 | testImplementation "org.slf4j:slf4j-simple:$LIB_SLF4J_VER" 40 | testImplementation "org.springframework:spring-test:6.1.13" 41 | testRuntimeOnly "org.springframework:spring-web:6.1.13" 42 | testImplementation 'com.google.guava:guava:33.3.1-jre' 43 | testImplementation "jakarta.servlet:jakarta.servlet-api:6.1.0" 44 | testImplementation "jakarta.websocket:jakarta.websocket-api:2.2.0" 45 | testImplementation "jakarta.websocket:jakarta.websocket-client-api:2.2.0" 46 | } 47 | -------------------------------------------------------------------------------- /graphql-java-servlet/src/main/java/graphql/kickstart/servlet/GraphQLInvocationInputParser.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.servlet; 2 | 3 | import graphql.kickstart.execution.GraphQLObjectMapper; 4 | import graphql.kickstart.execution.context.ContextSetting; 5 | import graphql.kickstart.execution.input.GraphQLInvocationInput; 6 | import graphql.kickstart.servlet.input.GraphQLInvocationInputFactory; 7 | import java.io.IOException; 8 | import jakarta.servlet.ServletException; 9 | import jakarta.servlet.http.HttpServletRequest; 10 | import jakarta.servlet.http.HttpServletResponse; 11 | 12 | interface GraphQLInvocationInputParser { 13 | 14 | static GraphQLInvocationInputParser create( 15 | HttpServletRequest request, 16 | GraphQLInvocationInputFactory invocationInputFactory, 17 | GraphQLObjectMapper graphQLObjectMapper, 18 | ContextSetting contextSetting) 19 | throws IOException { 20 | if ("GET".equalsIgnoreCase(request.getMethod())) { 21 | return new GraphQLGetInvocationInputParser( 22 | invocationInputFactory, graphQLObjectMapper, contextSetting); 23 | } 24 | 25 | try { 26 | boolean notMultipartRequest = 27 | request.getContentType() == null 28 | || !request.getContentType().startsWith("multipart/form-data") 29 | || request.getParts().isEmpty(); 30 | if (notMultipartRequest) { 31 | return new GraphQLPostInvocationInputParser( 32 | invocationInputFactory, graphQLObjectMapper, contextSetting); 33 | } 34 | return new GraphQLMultipartInvocationInputParser( 35 | invocationInputFactory, graphQLObjectMapper, contextSetting); 36 | } catch (ServletException e) { 37 | throw new IOException("Cannot get parts of request", e); 38 | } 39 | } 40 | 41 | GraphQLInvocationInput getGraphQLInvocationInput( 42 | HttpServletRequest request, HttpServletResponse response) throws IOException; 43 | } 44 | -------------------------------------------------------------------------------- /examples/osgi/providers/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | graphql-java-servlet-osgi-examples-providers 6 | 7 | 8 | 9 | maven-bundle-plugin 10 | 11 | 12 | true 13 | org.apache.felix 14 | 3.3.0 15 | 16 | 17 | 18 | 19 | 20 | 21 | graphql-java-servlet 22 | com.graphql-java-kickstart 23 | provided 24 | ${graphql.java.servlet.version} 25 | 26 | 27 | graphql-java 28 | com.graphql-java 29 | provided 30 | ${graphql.java.version} 31 | 32 | 33 | osgi.enterprise 34 | org.osgi 35 | provided 36 | 6.0.0 37 | 38 | 39 | org.apache.felix.scr.ds-annotations 40 | org.apache.felix 41 | provided 42 | 1.2.4 43 | 44 | 45 | 4.0.0 46 | 47 | bundle 48 | 49 | 50 | graphql-java-servlet-osgi-examples 51 | com.graphql-java-kickstart 52 | 10.1.0 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /graphql-java-servlet/src/main/java/graphql/kickstart/servlet/apollo/ApolloScalars.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.servlet.apollo; 2 | 3 | import graphql.schema.Coercing; 4 | import graphql.schema.CoercingParseLiteralException; 5 | import graphql.schema.CoercingParseValueException; 6 | import graphql.schema.CoercingSerializeException; 7 | import graphql.schema.GraphQLScalarType; 8 | import jakarta.servlet.http.Part; 9 | import lombok.AccessLevel; 10 | import lombok.NoArgsConstructor; 11 | 12 | @NoArgsConstructor(access = AccessLevel.PRIVATE) 13 | public class ApolloScalars { 14 | 15 | public static final GraphQLScalarType Upload = 16 | GraphQLScalarType.newScalar() 17 | .name("Upload") 18 | .description("A file part in a multipart request") 19 | .coercing( 20 | new Coercing() { 21 | @Override 22 | public Void serialize(Object dataFetcherResult) { 23 | throw new CoercingSerializeException("Upload is an input-only type"); 24 | } 25 | 26 | @Override 27 | public Part parseValue(Object input) { 28 | if (input instanceof Part) { 29 | return (Part) input; 30 | } else if (null == input) { 31 | return null; 32 | } else { 33 | throw new CoercingParseValueException( 34 | "Expected type " 35 | + Part.class.getName() 36 | + " but was " 37 | + input.getClass().getName()); 38 | } 39 | } 40 | 41 | @Override 42 | public Part parseLiteral(Object input) { 43 | throw new CoercingParseLiteralException( 44 | "Must use variables to specify Upload values"); 45 | } 46 | }) 47 | .build(); 48 | } 49 | -------------------------------------------------------------------------------- /graphql-java-kickstart/src/main/java/graphql/kickstart/execution/instrumentation/TrackingApproach.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.execution.instrumentation; 2 | 3 | import graphql.execution.ExecutionId; 4 | import graphql.execution.instrumentation.ExecutionStrategyInstrumentationContext; 5 | import graphql.execution.instrumentation.InstrumentationContext; 6 | import graphql.execution.instrumentation.InstrumentationState; 7 | import graphql.execution.instrumentation.parameters.InstrumentationExecutionStrategyParameters; 8 | import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters; 9 | 10 | public interface TrackingApproach extends InstrumentationState { 11 | 12 | /** 13 | * Handles creating any state for DataLoaderDispatcherInstrumentation 14 | * 15 | * @param executionId the execution to create state for. 16 | * @return individual state, if any for the execution. 17 | */ 18 | InstrumentationState createState(ExecutionId executionId); 19 | 20 | /** Dispatch dataloaders and clean up state. */ 21 | void dispatch(); 22 | 23 | /** 24 | * Handles approach specific logic for DataLoaderDispatcherInstrumentation. 25 | * 26 | * @param parameters parameters supplied to DataLoaderDispatcherInstrumentation 27 | * @return the instrumented context 28 | */ 29 | ExecutionStrategyInstrumentationContext beginExecutionStrategy( 30 | InstrumentationExecutionStrategyParameters parameters); 31 | 32 | /** 33 | * Handles approach specific logic for DataLoaderDispatcherInstrumentation. 34 | * 35 | * @param parameters parameters supplied to DataLoaderDispatcherInstrumentation 36 | * @return the instrumented context 37 | */ 38 | InstrumentationContext beginFieldFetch(InstrumentationFieldFetchParameters parameters); 39 | 40 | /** 41 | * Removes tracking state for an execution. 42 | * 43 | * @param executionId the execution to remove state for 44 | */ 45 | void removeTracking(ExecutionId executionId); 46 | } 47 | -------------------------------------------------------------------------------- /graphql-java-kickstart/src/main/java/graphql/kickstart/execution/input/GraphQLSingleInvocationInput.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.execution.input; 2 | 3 | import static java.util.Collections.singletonList; 4 | 5 | import graphql.ExecutionInput; 6 | import graphql.execution.ExecutionId; 7 | import graphql.kickstart.execution.GraphQLRequest; 8 | import graphql.kickstart.execution.context.GraphQLKickstartContext; 9 | import graphql.schema.GraphQLSchema; 10 | import java.util.List; 11 | 12 | /** Represents a single GraphQL execution. */ 13 | public class GraphQLSingleInvocationInput implements GraphQLInvocationInput { 14 | 15 | private final GraphQLSchema schema; 16 | 17 | private final ExecutionInput executionInput; 18 | 19 | public GraphQLSingleInvocationInput( 20 | GraphQLRequest request, GraphQLSchema schema, GraphQLKickstartContext context, Object root) { 21 | this.schema = schema; 22 | this.executionInput = createExecutionInput(request, context, root); 23 | } 24 | 25 | /** @return the schema to use to execute this query. */ 26 | public GraphQLSchema getSchema() { 27 | return schema; 28 | } 29 | 30 | private ExecutionInput createExecutionInput( 31 | GraphQLRequest graphQLRequest, GraphQLKickstartContext context, Object root) { 32 | return ExecutionInput.newExecutionInput() 33 | .query(graphQLRequest.getQuery()) 34 | .operationName(graphQLRequest.getOperationName()) 35 | .context(context) 36 | .graphQLContext(context.getMapOfContext()) 37 | .root(root) 38 | .variables(graphQLRequest.getVariables()) 39 | .extensions(graphQLRequest.getExtensions()) 40 | .dataLoaderRegistry(context.getDataLoaderRegistry()) 41 | .executionId(ExecutionId.generate()) 42 | .build(); 43 | } 44 | 45 | public ExecutionInput getExecutionInput() { 46 | return executionInput; 47 | } 48 | 49 | @Override 50 | public List getQueries() { 51 | return singletonList(executionInput.getQuery()); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /graphql-java-kickstart/src/main/java/graphql/kickstart/execution/context/ContextSetting.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.execution.context; 2 | 3 | import graphql.kickstart.execution.GraphQLRequest; 4 | import graphql.kickstart.execution.input.GraphQLBatchedInvocationInput; 5 | import graphql.kickstart.execution.input.PerQueryBatchedInvocationInput; 6 | import graphql.kickstart.execution.input.PerRequestBatchedInvocationInput; 7 | import graphql.schema.GraphQLSchema; 8 | import java.util.List; 9 | import java.util.function.Supplier; 10 | 11 | /** 12 | * An enum representing possible context settings. These are modeled after Apollo's link settings. 13 | */ 14 | public enum ContextSetting { 15 | 16 | /** 17 | * A context object, and therefor dataloader registry and subject, should be shared between all 18 | * GraphQL executions in a http request. 19 | */ 20 | PER_REQUEST, 21 | /** Each GraphQL execution should always have its own context. */ 22 | PER_QUERY; 23 | 24 | /** 25 | * Creates a set of inputs with the correct context based on the setting. 26 | * 27 | * @param requests the GraphQL requests to execute. 28 | * @param schema the GraphQL schema to execute the requests against. 29 | * @param contextSupplier method that returns the context to use for each execution or for the 30 | * request as a whole. 31 | * @param root the root object to use for each execution. 32 | * @return a configured batch input. 33 | */ 34 | public GraphQLBatchedInvocationInput getBatch( 35 | List requests, 36 | GraphQLSchema schema, 37 | Supplier contextSupplier, 38 | Object root) { 39 | switch (this) { 40 | case PER_QUERY: 41 | return new PerQueryBatchedInvocationInput(requests, schema, contextSupplier, root, this); 42 | case PER_REQUEST: 43 | return new PerRequestBatchedInvocationInput(requests, schema, contextSupplier, root, this); 44 | default: 45 | throw new ContextSettingNotConfiguredException(); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/GraphQLSubscriptionMapper.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.execution.subscriptions; 2 | 3 | import com.fasterxml.jackson.core.JsonProcessingException; 4 | import graphql.ExecutionResult; 5 | import graphql.GraphQLException; 6 | import graphql.kickstart.execution.GraphQLObjectMapper; 7 | import graphql.kickstart.execution.GraphQLRequest; 8 | import java.util.Map; 9 | import java.util.Objects; 10 | import lombok.RequiredArgsConstructor; 11 | 12 | @RequiredArgsConstructor 13 | public class GraphQLSubscriptionMapper { 14 | 15 | private final GraphQLObjectMapper graphQLObjectMapper; 16 | 17 | public GraphQLRequest readGraphQLRequest(String payload) { 18 | Objects.requireNonNull(payload, "Payload is required"); 19 | try { 20 | return graphQLObjectMapper.getJacksonMapper().readValue(payload, GraphQLRequest.class); 21 | } catch (JsonProcessingException e) { 22 | throw new GraphQLException("Cannot read GraphQL request from payload '" + payload + "'", e); 23 | } 24 | } 25 | 26 | public GraphQLRequest convertGraphQLRequest(Object payload) { 27 | Objects.requireNonNull(payload, "Payload is required"); 28 | return graphQLObjectMapper.getJacksonMapper().convertValue(payload, GraphQLRequest.class); 29 | } 30 | 31 | public ExecutionResult sanitizeErrors(ExecutionResult executionResult) { 32 | return graphQLObjectMapper.sanitizeErrors(executionResult); 33 | } 34 | 35 | public boolean hasNoErrors(ExecutionResult executionResult) { 36 | return !graphQLObjectMapper.areErrorsPresent(executionResult); 37 | } 38 | 39 | public Map convertSanitizedExecutionResult(ExecutionResult executionResult) { 40 | return graphQLObjectMapper.convertSanitizedExecutionResult(executionResult, false); 41 | } 42 | 43 | public String serialize(Object payload) { 44 | try { 45 | return graphQLObjectMapper.getJacksonMapper().writeValueAsString(payload); 46 | } catch (JsonProcessingException e) { 47 | return e.getMessage(); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/apollo/OperationMessage.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.execution.subscriptions.apollo; 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator; 4 | import com.fasterxml.jackson.annotation.JsonInclude; 5 | import com.fasterxml.jackson.annotation.JsonValue; 6 | import java.util.HashMap; 7 | import java.util.Map; 8 | import lombok.AllArgsConstructor; 9 | import lombok.NoArgsConstructor; 10 | 11 | @NoArgsConstructor 12 | @AllArgsConstructor 13 | @JsonInclude(JsonInclude.Include.NON_NULL) 14 | public class OperationMessage { 15 | 16 | private Type type; 17 | private String id; 18 | private Object payload; 19 | 20 | public static OperationMessage newKeepAliveMessage() { 21 | return new OperationMessage(Type.GQL_CONNECTION_KEEP_ALIVE, null, null); 22 | } 23 | 24 | public Type getType() { 25 | return type; 26 | } 27 | 28 | public String getId() { 29 | return id; 30 | } 31 | 32 | public Object getPayload() { 33 | return payload; 34 | } 35 | 36 | public enum Type { 37 | 38 | // Server Messages 39 | GQL_CONNECTION_ACK("connection_ack"), 40 | GQL_CONNECTION_ERROR("connection_error"), 41 | GQL_CONNECTION_KEEP_ALIVE("ka"), 42 | GQL_DATA("data"), 43 | GQL_ERROR("error"), 44 | GQL_COMPLETE("complete"), 45 | 46 | // Client Messages 47 | GQL_CONNECTION_INIT("connection_init"), 48 | GQL_CONNECTION_TERMINATE("connection_terminate"), 49 | GQL_START("start"), 50 | GQL_STOP("stop"); 51 | 52 | private static final Map reverseLookup = new HashMap<>(); 53 | 54 | static { 55 | for (Type type : Type.values()) { 56 | reverseLookup.put(type.getValue(), type); 57 | } 58 | } 59 | 60 | private final String value; 61 | 62 | Type(String value) { 63 | this.value = value; 64 | } 65 | 66 | @JsonCreator 67 | public static Type findType(String value) { 68 | return reverseLookup.get(value); 69 | } 70 | 71 | @JsonValue 72 | public String getValue() { 73 | return value; 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /graphql-java-servlet/src/main/java/graphql/kickstart/servlet/cache/CacheReader.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.servlet.cache; 2 | 3 | import graphql.kickstart.execution.input.GraphQLInvocationInput; 4 | import graphql.kickstart.servlet.HttpRequestHandler; 5 | import java.io.IOException; 6 | import java.nio.charset.StandardCharsets; 7 | import jakarta.servlet.http.HttpServletRequest; 8 | import jakarta.servlet.http.HttpServletResponse; 9 | import lombok.extern.slf4j.Slf4j; 10 | 11 | @Slf4j 12 | public class CacheReader { 13 | 14 | /** 15 | * Response from cache if possible, if nothing in cache will not produce any response 16 | * 17 | * @return {@literal true} if response was fulfilled from cache, {@literal false} is cache not 18 | * found or an error occurred while reading value from cache 19 | * @throws IOException if can not read value from the cache 20 | */ 21 | public boolean responseFromCache( 22 | GraphQLInvocationInput invocationInput, 23 | HttpServletRequest request, 24 | HttpServletResponse response, 25 | GraphQLResponseCacheManager cacheManager) 26 | throws IOException { 27 | try { 28 | CachedResponse cachedResponse = cacheManager.get(request, invocationInput); 29 | if (cachedResponse != null) { 30 | write(response, cachedResponse); 31 | return true; 32 | } 33 | } catch (Exception t) { 34 | log.warn("Ignore read from cache, unexpected error happened", t); 35 | } 36 | 37 | return false; 38 | } 39 | 40 | private void write(HttpServletResponse response, CachedResponse cachedResponse) 41 | throws IOException { 42 | if (cachedResponse.isError()) { 43 | response.sendError(cachedResponse.getErrorStatusCode(), cachedResponse.getErrorMessage()); 44 | } else { 45 | response.setContentType(HttpRequestHandler.APPLICATION_JSON_UTF8); 46 | response.setStatus(HttpRequestHandler.STATUS_OK); 47 | response.setCharacterEncoding(StandardCharsets.UTF_8.name()); 48 | response.setContentLength(cachedResponse.getContentBytes().length); 49 | response.getOutputStream().write(cachedResponse.getContentBytes()); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /graphql-java-servlet/src/main/java/graphql/kickstart/servlet/cache/CachingHttpRequestInvoker.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.servlet.cache; 2 | 3 | import static graphql.kickstart.servlet.HttpRequestHandler.STATUS_BAD_REQUEST; 4 | 5 | import graphql.kickstart.execution.input.GraphQLInvocationInput; 6 | import graphql.kickstart.servlet.GraphQLConfiguration; 7 | import graphql.kickstart.servlet.HttpRequestInvoker; 8 | import graphql.kickstart.servlet.HttpRequestInvokerImpl; 9 | import graphql.kickstart.servlet.ListenerHandler; 10 | import java.io.IOException; 11 | import jakarta.servlet.http.HttpServletRequest; 12 | import jakarta.servlet.http.HttpServletResponse; 13 | import lombok.AccessLevel; 14 | import lombok.RequiredArgsConstructor; 15 | import lombok.extern.slf4j.Slf4j; 16 | 17 | @Slf4j 18 | @RequiredArgsConstructor(access = AccessLevel.PROTECTED) 19 | public class CachingHttpRequestInvoker implements HttpRequestInvoker { 20 | 21 | private final GraphQLConfiguration configuration; 22 | private final HttpRequestInvoker requestInvoker; 23 | private final CacheReader cacheReader; 24 | 25 | public CachingHttpRequestInvoker(GraphQLConfiguration configuration) { 26 | this( 27 | configuration, 28 | new HttpRequestInvokerImpl( 29 | configuration, 30 | configuration.getGraphQLInvoker(), 31 | new CachingQueryResponseWriterFactory()), 32 | new CacheReader()); 33 | } 34 | 35 | /** Try to return value from cache if cache exists, otherwise process the query normally */ 36 | @Override 37 | public void execute( 38 | GraphQLInvocationInput invocationInput, 39 | HttpServletRequest request, 40 | HttpServletResponse response, 41 | ListenerHandler listenerHandler) { 42 | try { 43 | if (!cacheReader.responseFromCache( 44 | invocationInput, request, response, configuration.getResponseCacheManager())) { 45 | requestInvoker.execute(invocationInput, request, response, listenerHandler); 46 | } 47 | } catch (IOException e) { 48 | response.setStatus(STATUS_BAD_REQUEST); 49 | log.warn("Unexpected error happened during response from cache", e); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /graphql-java-servlet/src/main/java/graphql/kickstart/servlet/ExecutionResultSubscriber.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.servlet; 2 | 3 | import graphql.ExecutionResult; 4 | import graphql.kickstart.execution.GraphQLObjectMapper; 5 | import java.io.IOException; 6 | import java.io.Writer; 7 | import java.util.concurrent.CountDownLatch; 8 | import java.util.concurrent.atomic.AtomicReference; 9 | import jakarta.servlet.AsyncContext; 10 | import org.reactivestreams.Subscriber; 11 | import org.reactivestreams.Subscription; 12 | 13 | class ExecutionResultSubscriber implements Subscriber { 14 | 15 | private final AtomicReference subscriptionRef; 16 | private final AsyncContext asyncContext; 17 | private final GraphQLObjectMapper graphQLObjectMapper; 18 | private final CountDownLatch completedLatch = new CountDownLatch(1); 19 | 20 | ExecutionResultSubscriber( 21 | AtomicReference subscriptionRef, 22 | AsyncContext asyncContext, 23 | GraphQLObjectMapper graphQLObjectMapper) { 24 | this.subscriptionRef = subscriptionRef; 25 | this.asyncContext = asyncContext; 26 | this.graphQLObjectMapper = graphQLObjectMapper; 27 | } 28 | 29 | @Override 30 | public void onSubscribe(Subscription subscription) { 31 | subscriptionRef.set(subscription); 32 | subscriptionRef.get().request(1); 33 | } 34 | 35 | @Override 36 | public void onNext(ExecutionResult executionResult) { 37 | try { 38 | Writer writer = asyncContext.getResponse().getWriter(); 39 | writer.write("data: "); 40 | writer.write(graphQLObjectMapper.serializeResultAsJson(executionResult)); 41 | writer.write("\n\n"); 42 | writer.flush(); 43 | subscriptionRef.get().request(1); 44 | } catch (IOException ignored) { 45 | // ignore 46 | } 47 | } 48 | 49 | @Override 50 | public void onError(Throwable t) { 51 | asyncContext.complete(); 52 | completedLatch.countDown(); 53 | } 54 | 55 | @Override 56 | public void onComplete() { 57 | asyncContext.complete(); 58 | completedLatch.countDown(); 59 | } 60 | 61 | void await() throws InterruptedException { 62 | completedLatch.await(); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /graphql-java-servlet/src/main/java/graphql/kickstart/servlet/cache/CachedResponse.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.servlet.cache; 2 | 3 | import java.io.Serializable; 4 | import java.util.Objects; 5 | 6 | public class CachedResponse implements Serializable { 7 | 8 | private static final long serialVersionUID = 5894555791705575139L; 9 | 10 | private final byte[] contentBytes; 11 | 12 | private final boolean error; 13 | private final Integer errorStatusCode; 14 | private final String errorMessage; 15 | 16 | private CachedResponse( 17 | byte[] contentBytes, boolean error, Integer errorStatusCode, String errorMessage) { 18 | this.contentBytes = contentBytes; 19 | this.error = error; 20 | this.errorStatusCode = errorStatusCode; 21 | this.errorMessage = errorMessage; 22 | } 23 | 24 | /** 25 | * Constructor for success response 26 | * 27 | * @param contentBytes bytes array of graphql json response 28 | */ 29 | public static CachedResponse ofContent(byte[] contentBytes) { 30 | Objects.requireNonNull(contentBytes, "contentBytes can not be null"); 31 | 32 | return new CachedResponse(contentBytes, false, null, null); 33 | } 34 | 35 | /** 36 | * Constructor for error response 37 | * 38 | * @param errorStatusCode the status code for the error response 39 | * @param errorMessage the error message for the error response 40 | */ 41 | public static CachedResponse ofError(int errorStatusCode, String errorMessage) { 42 | return new CachedResponse(null, true, errorStatusCode, errorMessage); 43 | } 44 | 45 | /** @return {@literal true} when this request was failed */ 46 | public boolean isError() { 47 | return error; 48 | } 49 | 50 | /** 51 | * @return the response body for success requests, {@literal null} when {@link #isError()} is 52 | * {@literal true} 53 | */ 54 | public byte[] getContentBytes() { 55 | return contentBytes; 56 | } 57 | 58 | /** @return the response error code */ 59 | public Integer getErrorStatusCode() { 60 | return errorStatusCode; 61 | } 62 | 63 | /** @return the response error message */ 64 | public String getErrorMessage() { 65 | return errorMessage; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | 3 | We're really glad you're reading this, because we need more volunteer developers 4 | to help with this project! 5 | 6 | We can use all the help we can get on all our [GraphQL Java Kickstart](https://github.com/graphql-java-kickstart) 7 | projects. This work ranges from adding new features, fixing bugs, and answering questions to writing documentation. 8 | 9 | ## Answering questions and writing documentation 10 | 11 | A lot of the questions asked on Spectrum or Github are caused by a lack of documentation. 12 | We should strive from now on to answer questions by adding content to 13 | our [documentation](https://github.com/graphql-java-kickstart/documentation) and referring 14 | them to the newly created content. 15 | 16 | Continuous integration will make sure that the changes are automatically deployed to 17 | https://www.graphql-java-kickstart.com. 18 | 19 | ## Submitting changes 20 | 21 | Please send a Pull Request with a clear list of what you've done using the 22 | [Github flow](https://guides.github.com/introduction/flow/). We can always use more 23 | test coverage, so we'd love to see that in the pull requests too. And make sure to 24 | follow our coding conventions (below) and make sure all your commits are atomic 25 | (one feature per commit). 26 | 27 | ## Coding conventions 28 | 29 | We use Google Style guides for our projects. See the 30 | [Java Style Guide](https://google.github.io/styleguide/javaguide.html) for a detailed 31 | description. You can download the 32 | [IntelliJ Java Google Style](https://github.com/google/styleguide/blob/gh-pages/intellij-java-google-style.xml) 33 | to import in these settings in IntelliJ. 34 | 35 | These conventions are checked during the build phase. If the build fails because 36 | the code is not using the correct style you can fix this easily by running a gradle task 37 | ```bash 38 | ./gradlew googleJavaFormat 39 | ``` 40 | 41 | ### SonarLint 42 | 43 | It would also be very helpful to install the SonarLint plugin in your IDE and fix any 44 | relevant SonarLint issues before pushing a PR. We're aware that the current state 45 | of the code raises a lot of SonarLint issues out of the box, but any help in reducing 46 | that is appreciated. More importantly we don't increase that technical debt. 47 | -------------------------------------------------------------------------------- /graphql-java-servlet/src/main/java/graphql/kickstart/servlet/GraphQLPostInvocationInputParser.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.servlet; 2 | 3 | import static java.util.stream.Collectors.joining; 4 | 5 | import graphql.GraphQLException; 6 | import graphql.kickstart.execution.GraphQLObjectMapper; 7 | import graphql.kickstart.execution.GraphQLRequest; 8 | import graphql.kickstart.execution.context.ContextSetting; 9 | import graphql.kickstart.execution.input.GraphQLInvocationInput; 10 | import graphql.kickstart.servlet.input.GraphQLInvocationInputFactory; 11 | import java.io.IOException; 12 | import java.util.List; 13 | import jakarta.servlet.http.HttpServletRequest; 14 | import jakarta.servlet.http.HttpServletResponse; 15 | 16 | class GraphQLPostInvocationInputParser extends AbstractGraphQLInvocationInputParser { 17 | 18 | private static final String APPLICATION_GRAPHQL = "application/graphql"; 19 | 20 | GraphQLPostInvocationInputParser( 21 | GraphQLInvocationInputFactory invocationInputFactory, 22 | GraphQLObjectMapper graphQLObjectMapper, 23 | ContextSetting contextSetting) { 24 | super(invocationInputFactory, graphQLObjectMapper, contextSetting); 25 | } 26 | 27 | public GraphQLInvocationInput getGraphQLInvocationInput( 28 | HttpServletRequest request, HttpServletResponse response) throws IOException { 29 | String contentType = request.getContentType(); 30 | if (contentType != null && APPLICATION_GRAPHQL.equals(contentType.split(";")[0].trim())) { 31 | String query = request.getReader().lines().collect(joining(" ")); 32 | GraphQLRequest graphqlRequest = GraphQLRequest.createQueryOnlyRequest(query); 33 | return invocationInputFactory.create(graphqlRequest, request, response); 34 | } 35 | 36 | String body = request.getReader().lines().collect(joining(" ")); 37 | if (isSingleQuery(body)) { 38 | GraphQLRequest graphqlRequest = graphQLObjectMapper.readGraphQLRequest(body); 39 | return invocationInputFactory.create(graphqlRequest, request, response); 40 | } 41 | 42 | if (isBatchedQuery(body)) { 43 | List requests = graphQLObjectMapper.readBatchedGraphQLRequest(body); 44 | return invocationInputFactory.create(contextSetting, requests, request, response); 45 | } 46 | 47 | throw new GraphQLException("No valid query found in request"); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /graphql-java-servlet/src/main/java/graphql/kickstart/servlet/cache/CachingQueryResponseWriter.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.servlet.cache; 2 | 3 | import graphql.kickstart.execution.input.GraphQLInvocationInput; 4 | import graphql.kickstart.servlet.QueryResponseWriter; 5 | import java.io.IOException; 6 | import jakarta.servlet.http.HttpServletRequest; 7 | import jakarta.servlet.http.HttpServletResponse; 8 | import lombok.extern.slf4j.Slf4j; 9 | 10 | @Slf4j 11 | public class CachingQueryResponseWriter implements QueryResponseWriter { 12 | 13 | private final QueryResponseWriter delegate; 14 | private final GraphQLResponseCacheManager responseCache; 15 | private final GraphQLInvocationInput invocationInput; 16 | private final boolean error; 17 | 18 | public CachingQueryResponseWriter( 19 | QueryResponseWriter delegate, 20 | GraphQLResponseCacheManager responseCache, 21 | GraphQLInvocationInput invocationInput, 22 | boolean error) { 23 | this.delegate = delegate; 24 | this.responseCache = responseCache; 25 | this.invocationInput = invocationInput; 26 | this.error = error; 27 | } 28 | 29 | @Override 30 | public void write(HttpServletRequest request, HttpServletResponse response) throws IOException { 31 | if (responseCache.isCacheable(request, invocationInput)) { 32 | BufferedHttpServletResponse cachingResponseWrapper = 33 | new BufferedHttpServletResponse(response); 34 | 35 | delegate.write(request, cachingResponseWrapper); 36 | 37 | try { 38 | if (error) { 39 | int errorStatusCode = cachingResponseWrapper.getStatus(); 40 | String errorMessage = cachingResponseWrapper.getErrorMessage(); 41 | 42 | responseCache.put( 43 | request, invocationInput, CachedResponse.ofError(errorStatusCode, errorMessage)); 44 | } else { 45 | byte[] contentBytes = cachingResponseWrapper.getContentAsByteArray(); 46 | 47 | responseCache.put(request, invocationInput, CachedResponse.ofContent(contentBytes)); 48 | } 49 | } catch (Exception t) { 50 | log.warn("Ignore read from cache, unexpected error happened", t); 51 | } 52 | 53 | cachingResponseWrapper.flushBuffer(); 54 | cachingResponseWrapper.close(); 55 | } else { 56 | delegate.write(request, response); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/SessionSubscriber.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.execution.subscriptions; 2 | 3 | import static java.util.Collections.singletonList; 4 | 5 | import graphql.ExecutionResult; 6 | import graphql.GraphqlErrorBuilder; 7 | import graphql.execution.NonNullableFieldWasNullException; 8 | import graphql.kickstart.execution.error.GenericGraphQLError; 9 | import java.util.HashMap; 10 | import java.util.Map; 11 | import lombok.RequiredArgsConstructor; 12 | import lombok.extern.slf4j.Slf4j; 13 | import org.reactivestreams.Subscriber; 14 | import org.reactivestreams.Subscription; 15 | 16 | @Slf4j 17 | @RequiredArgsConstructor 18 | class SessionSubscriber implements Subscriber { 19 | 20 | private final SubscriptionSession session; 21 | private final String id; 22 | private AtomicSubscriptionSubscription subscriptionReference = 23 | new AtomicSubscriptionSubscription(); 24 | 25 | @Override 26 | public void onSubscribe(Subscription subscription) { 27 | log.debug("Subscribe to execution result: {}", subscription); 28 | subscriptionReference.set(subscription); 29 | subscriptionReference.get().request(1); 30 | 31 | session.add(id, subscriptionReference.get()); 32 | } 33 | 34 | @Override 35 | public void onNext(ExecutionResult executionResult) { 36 | Map result = new HashMap<>(); 37 | result.put("data", executionResult.getData()); 38 | 39 | session.sendDataMessage(id, result); 40 | subscriptionReference.get().request(1); 41 | } 42 | 43 | @Override 44 | public void onError(Throwable throwable) { 45 | log.error("Subscription error", throwable); 46 | Map payload = new HashMap<>(); 47 | if (throwable.getCause() instanceof NonNullableFieldWasNullException) { 48 | NonNullableFieldWasNullException e = (NonNullableFieldWasNullException) throwable.getCause(); 49 | payload.put( 50 | "errors", 51 | singletonList( 52 | GraphqlErrorBuilder.newError().message(e.getMessage()).path(e.getPath()).build())); 53 | } else { 54 | payload.put("errors", singletonList(new GenericGraphQLError(throwable.getMessage()))); 55 | } 56 | 57 | session.unsubscribe(id); 58 | session.sendErrorMessage(id, payload); 59 | } 60 | 61 | @Override 62 | public void onComplete() { 63 | session.unsubscribe(id); 64 | session.sendCompleteMessage(id); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /graphql-java-servlet/src/main/java/graphql/kickstart/servlet/core/GraphQLServletListener.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.servlet.core; 2 | 3 | import jakarta.servlet.http.HttpServletRequest; 4 | import jakarta.servlet.http.HttpServletResponse; 5 | 6 | /** @author Andrew Potter */ 7 | public interface GraphQLServletListener { 8 | 9 | /** 10 | * Called this method when the request started processing. 11 | * @param request http request 12 | * @param response http response 13 | * @return request callback or {@literal null} 14 | */ 15 | default RequestCallback onRequest(HttpServletRequest request, HttpServletResponse response) { 16 | return null; 17 | } 18 | 19 | /** 20 | * The callback which used to add additional listeners for GraphQL request execution. 21 | */ 22 | interface RequestCallback { 23 | 24 | /** 25 | * Called when failed to parse InvocationInput and the response was not written. 26 | * @param request http request 27 | * @param response http response 28 | */ 29 | default void onParseError( 30 | HttpServletRequest request, HttpServletResponse response, Throwable throwable) {} 31 | 32 | /** 33 | * Called right before the response will be written and flushed. Can be used for applying some 34 | * changes to the response object, like adding response headers. 35 | * @param request http request 36 | * @param response http response 37 | */ 38 | default void beforeFlush(HttpServletRequest request, HttpServletResponse response) {} 39 | 40 | /** 41 | * Called when GraphQL invoked successfully and the response was written already. 42 | * @param request http request 43 | * @param response http response 44 | */ 45 | default void onSuccess(HttpServletRequest request, HttpServletResponse response) {} 46 | 47 | /** 48 | * Called when GraphQL was failed and the response was written already. 49 | * @param request http request 50 | * @param response http response 51 | */ 52 | default void onError( 53 | HttpServletRequest request, HttpServletResponse response, Throwable throwable) {} 54 | 55 | /** 56 | * Called finally once on both success and failed GraphQL invocation. The response is also 57 | * already written. 58 | * @param request http request 59 | * @param response http response 60 | */ 61 | default void onFinally(HttpServletRequest request, HttpServletResponse response) {} 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /graphql-java-servlet/src/main/java/graphql/kickstart/servlet/subscriptions/FallbackSubscriptionConsumer.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.servlet.subscriptions; 2 | 3 | import graphql.ExecutionResult; 4 | import graphql.kickstart.execution.GraphQLInvoker; 5 | import graphql.kickstart.execution.GraphQLRequest; 6 | import graphql.kickstart.execution.input.GraphQLSingleInvocationInput; 7 | import graphql.kickstart.execution.subscriptions.GraphQLSubscriptionInvocationInputFactory; 8 | import graphql.kickstart.execution.subscriptions.GraphQLSubscriptionMapper; 9 | import graphql.kickstart.execution.subscriptions.SubscriptionSession; 10 | import java.util.Objects; 11 | import java.util.UUID; 12 | import java.util.concurrent.CompletableFuture; 13 | import java.util.function.Consumer; 14 | import lombok.RequiredArgsConstructor; 15 | 16 | /** @author Andrew Potter */ 17 | @RequiredArgsConstructor 18 | public class FallbackSubscriptionConsumer implements Consumer { 19 | 20 | private final SubscriptionSession session; 21 | private final GraphQLSubscriptionMapper mapper; 22 | private final GraphQLSubscriptionInvocationInputFactory invocationInputFactory; 23 | private final GraphQLInvoker graphQLInvoker; 24 | 25 | @Override 26 | public void accept(String text) { 27 | CompletableFuture executionResult = executeAsync(text, session); 28 | executionResult.thenAccept( 29 | result -> handleSubscriptionStart(session, UUID.randomUUID().toString(), result)); 30 | } 31 | 32 | private CompletableFuture executeAsync( 33 | String payload, SubscriptionSession session) { 34 | Objects.requireNonNull(payload, "Payload is required"); 35 | GraphQLRequest graphQLRequest = mapper.readGraphQLRequest(payload); 36 | 37 | GraphQLSingleInvocationInput invocationInput = 38 | invocationInputFactory.create(graphQLRequest, session); 39 | return graphQLInvoker.executeAsync(invocationInput); 40 | } 41 | 42 | private void handleSubscriptionStart( 43 | SubscriptionSession session, String id, ExecutionResult executionResult) { 44 | ExecutionResult sanitizedExecutionResult = mapper.sanitizeErrors(executionResult); 45 | if (mapper.hasNoErrors(sanitizedExecutionResult)) { 46 | session.subscribe(id, sanitizedExecutionResult.getData()); 47 | } else { 48 | Object payload = mapper.convertSanitizedExecutionResult(sanitizedExecutionResult); 49 | session.sendDataMessage(id, payload); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /graphql-java-kickstart/src/main/java/graphql/kickstart/execution/error/DefaultGraphQLErrorHandler.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.execution.error; 2 | 3 | import graphql.ExceptionWhileDataFetching; 4 | import graphql.GraphQLError; 5 | import graphql.execution.NonNullableFieldWasNullError; 6 | import java.util.List; 7 | import java.util.stream.Collectors; 8 | import lombok.extern.slf4j.Slf4j; 9 | 10 | /** @author Andrew Potter */ 11 | @Slf4j 12 | public class DefaultGraphQLErrorHandler implements GraphQLErrorHandler { 13 | 14 | @Override 15 | public List processErrors(List errors) { 16 | final List clientErrors = filterGraphQLErrors(errors); 17 | if (clientErrors.size() < errors.size()) { 18 | 19 | // Some errors were filtered out to hide implementation - put a generic error in place. 20 | clientErrors.add(new GenericGraphQLError("Internal Server Error(s) while executing query")); 21 | 22 | errors.stream().filter(error -> !isClientError(error)).forEach(this::logError); 23 | } 24 | 25 | return clientErrors; 26 | } 27 | 28 | protected void logError(GraphQLError error) { 29 | if (error instanceof Throwable) { 30 | log.error("Error executing query!", (Throwable) error); 31 | } else if (error instanceof ExceptionWhileDataFetching) { 32 | log.error( 33 | "Error executing query {}", 34 | error.getMessage(), 35 | ((ExceptionWhileDataFetching) error).getException()); 36 | } else { 37 | log.error( 38 | "Error executing query ({}): {}", error.getClass().getSimpleName(), error.getMessage()); 39 | } 40 | } 41 | 42 | protected List filterGraphQLErrors(List errors) { 43 | return errors.stream() 44 | .filter(this::isClientError) 45 | .map(this::replaceNonNullableFieldWasNullError) 46 | .collect(Collectors.toList()); 47 | } 48 | 49 | protected boolean isClientError(GraphQLError error) { 50 | if (error instanceof ExceptionWhileDataFetching) { 51 | return ((ExceptionWhileDataFetching) error).getException() instanceof GraphQLError; 52 | } 53 | return true; 54 | } 55 | 56 | private GraphQLError replaceNonNullableFieldWasNullError(GraphQLError error) { 57 | if (error instanceof NonNullableFieldWasNullError) { 58 | return new RenderableNonNullableFieldWasNullError((NonNullableFieldWasNullError) error); 59 | } else { 60 | return error; 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /graphql-java-servlet/src/test/groovy/graphql/kickstart/servlet/BatchedQueryResponseWriterTest.groovy: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.servlet 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper 4 | import graphql.ExecutionResultImpl 5 | import graphql.kickstart.execution.GraphQLObjectMapper 6 | import spock.lang.Specification 7 | import spock.lang.Unroll 8 | 9 | import jakarta.servlet.ServletOutputStream 10 | import jakarta.servlet.http.HttpServletRequest 11 | import jakarta.servlet.http.HttpServletResponse 12 | import java.nio.charset.StandardCharsets 13 | 14 | class BatchedQueryResponseWriterTest extends Specification { 15 | 16 | @Unroll 17 | def "should write utf8 results into the response with content #result"() { 18 | given: 19 | def byteArrayOutputStream = new ByteArrayOutputStream() 20 | def graphQLObjectMapperMock = GraphQLObjectMapper.newBuilder().withObjectMapperProvider({ new ObjectMapper() }).build() 21 | graphQLObjectMapperMock.getJacksonMapper() >> new ObjectMapper() 22 | 23 | def requestMock = Mock(HttpServletRequest) 24 | def responseMock = Mock(HttpServletResponse) 25 | def servletOutputStreamMock = Mock(ServletOutputStream) 26 | 27 | responseMock.getOutputStream() >> servletOutputStreamMock 28 | 29 | 1 * responseMock.setContentLength(expectedContentLengh) 30 | 1 * responseMock.setCharacterEncoding(StandardCharsets.UTF_8.name()) 31 | (1.._) * servletOutputStreamMock.write(_) >> { value -> 32 | byteArrayOutputStream.write((byte[]) (value[0])) 33 | } 34 | 35 | def executionResultList = new ArrayList() 36 | for (LinkedHashMap value : result) { 37 | executionResultList.add(new ExecutionResultImpl(value, [])) 38 | } 39 | 40 | def writer = new BatchedQueryResponseWriter(executionResultList, graphQLObjectMapperMock) 41 | 42 | when: 43 | writer.write(requestMock, responseMock) 44 | 45 | then: 46 | byteArrayOutputStream.toString(StandardCharsets.UTF_8.name()) == expectedResponseContent 47 | 48 | where: 49 | result || expectedContentLengh | expectedResponseContent 50 | [[testValue: "abcde"]] || 32 | """[{"data":{"testValue":"abcde"}}]""" 51 | [[testValue: "äöüüöß"]] || 39 | """[{"data":{"testValue":"äöüüöß"}}]""" 52 | [] || 2 | """[]""" 53 | [[k1: "äöüüöß"], [k2: "a"]] || 52 | """[{"data":{"k1":"äöüüöß"}},{"data":{"k2":"a"}}]""" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /graphql-java-servlet/src/main/java/graphql/kickstart/servlet/SingleAsynchronousQueryResponseWriter.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.servlet; 2 | 3 | import graphql.ExecutionResult; 4 | import graphql.kickstart.execution.GraphQLObjectMapper; 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | import java.util.Objects; 8 | import java.util.concurrent.atomic.AtomicReference; 9 | import jakarta.servlet.AsyncContext; 10 | import jakarta.servlet.http.HttpServletRequest; 11 | import jakarta.servlet.http.HttpServletResponse; 12 | import lombok.Getter; 13 | import lombok.RequiredArgsConstructor; 14 | import org.reactivestreams.Publisher; 15 | import org.reactivestreams.Subscription; 16 | 17 | @RequiredArgsConstructor 18 | class SingleAsynchronousQueryResponseWriter implements QueryResponseWriter { 19 | 20 | @Getter private final ExecutionResult result; 21 | private final GraphQLObjectMapper graphQLObjectMapper; 22 | private final long subscriptionTimeout; 23 | 24 | @Override 25 | public void write(HttpServletRequest request, HttpServletResponse response) { 26 | Objects.requireNonNull(request, "Http servlet request cannot be null"); 27 | response.setContentType(HttpRequestHandler.APPLICATION_EVENT_STREAM_UTF8); 28 | response.setStatus(HttpRequestHandler.STATUS_OK); 29 | 30 | boolean isInAsyncThread = request.isAsyncStarted(); 31 | AsyncContext asyncContext = 32 | isInAsyncThread ? request.getAsyncContext() : request.startAsync(request, response); 33 | asyncContext.setTimeout(subscriptionTimeout); 34 | AtomicReference subscriptionRef = new AtomicReference<>(); 35 | asyncContext.addListener(new SubscriptionAsyncListener(subscriptionRef)); 36 | ExecutionResultSubscriber subscriber = 37 | new ExecutionResultSubscriber(subscriptionRef, asyncContext, graphQLObjectMapper); 38 | List> publishers = new ArrayList<>(); 39 | if (result.getData() instanceof Publisher) { 40 | publishers.add(result.getData()); 41 | } else { 42 | publishers.add(new StaticDataPublisher<>(result)); 43 | } 44 | publishers.forEach(it -> it.subscribe(subscriber)); 45 | 46 | if (isInAsyncThread) { 47 | // We need to delay the completion of async context until after the subscription has 48 | // terminated, otherwise the AsyncContext is prematurely closed. 49 | try { 50 | subscriber.await(); 51 | } catch (InterruptedException e) { 52 | Thread.currentThread().interrupt(); 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/apollo/ApolloSubscriptionKeepAliveRunner.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.execution.subscriptions.apollo; 2 | 3 | import graphql.kickstart.execution.subscriptions.SubscriptionSession; 4 | import java.time.Duration; 5 | import java.util.Map; 6 | import java.util.concurrent.ConcurrentHashMap; 7 | import java.util.concurrent.Executors; 8 | import java.util.concurrent.Future; 9 | import java.util.concurrent.ScheduledExecutorService; 10 | import java.util.concurrent.ScheduledFuture; 11 | import java.util.concurrent.TimeUnit; 12 | import lombok.extern.slf4j.Slf4j; 13 | 14 | @Slf4j 15 | class ApolloSubscriptionKeepAliveRunner { 16 | 17 | private static final int EXECUTOR_POOL_SIZE = 10; 18 | 19 | private final ScheduledExecutorService executor; 20 | private final OperationMessage keepAliveMessage; 21 | private final Map> futures; 22 | private final long keepAliveIntervalSeconds; 23 | 24 | ApolloSubscriptionKeepAliveRunner(Duration keepAliveInterval) { 25 | this.keepAliveMessage = OperationMessage.newKeepAliveMessage(); 26 | this.executor = Executors.newScheduledThreadPool(EXECUTOR_POOL_SIZE); 27 | this.futures = new ConcurrentHashMap<>(); 28 | this.keepAliveIntervalSeconds = keepAliveInterval.getSeconds(); 29 | } 30 | 31 | void keepAlive(SubscriptionSession session) { 32 | futures.computeIfAbsent(session, this::startKeepAlive); 33 | } 34 | 35 | private ScheduledFuture startKeepAlive(SubscriptionSession session) { 36 | return executor.scheduleAtFixedRate( 37 | () -> { 38 | try { 39 | if (session.isOpen()) { 40 | session.sendMessage(keepAliveMessage); 41 | } else { 42 | log.debug("Session {} appears to be closed. Aborting keep alive", session.getId()); 43 | abort(session); 44 | } 45 | } catch (Exception t) { 46 | log.error( 47 | "Cannot send keep alive message to session {}. Aborting keep alive", 48 | session.getId(), 49 | t); 50 | abort(session); 51 | } 52 | }, 53 | 0, 54 | keepAliveIntervalSeconds, 55 | TimeUnit.SECONDS); 56 | } 57 | 58 | void abort(SubscriptionSession session) { 59 | Future future = futures.remove(session); 60 | if (future != null) { 61 | future.cancel(true); 62 | } 63 | } 64 | 65 | void shutdown() { 66 | this.executor.shutdown(); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /graphql-java-servlet/src/main/java/graphql/kickstart/servlet/BatchedQueryResponseWriter.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.servlet; 2 | 3 | import graphql.ExecutionResult; 4 | import graphql.kickstart.execution.GraphQLObjectMapper; 5 | import java.io.IOException; 6 | import java.nio.charset.StandardCharsets; 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | import jakarta.servlet.http.HttpServletRequest; 10 | import jakarta.servlet.http.HttpServletResponse; 11 | import lombok.RequiredArgsConstructor; 12 | import lombok.extern.slf4j.Slf4j; 13 | 14 | @Slf4j 15 | @RequiredArgsConstructor 16 | class BatchedQueryResponseWriter implements QueryResponseWriter { 17 | private final List results; 18 | private final GraphQLObjectMapper graphQLObjectMapper; 19 | 20 | @Override 21 | public void write(HttpServletRequest request, HttpServletResponse response) throws IOException { 22 | response.setCharacterEncoding(StandardCharsets.UTF_8.name()); 23 | response.setContentType(HttpRequestHandler.APPLICATION_JSON_UTF8); 24 | response.setStatus(HttpRequestHandler.STATUS_OK); 25 | 26 | // Use direct serialization to byte arrays and avoid any string concatenation to save multiple 27 | // GiB of memory allocation during large response processing. 28 | List serializedResults = new ArrayList<>(2 * results.size() + 1); 29 | 30 | if (!results.isEmpty()) { 31 | serializedResults.add("[".getBytes(StandardCharsets.UTF_8)); 32 | } else { 33 | serializedResults.add("[]".getBytes(StandardCharsets.UTF_8)); 34 | } 35 | long totalLength = serializedResults.get(0).length; 36 | 37 | // '[', ',' and ']' are all 1 byte in UTF-8. 38 | for (int i = 0; i < results.size(); i++) { 39 | byte[] currentResult = graphQLObjectMapper.serializeResultAsBytes(results.get(i)); 40 | serializedResults.add(currentResult); 41 | 42 | if (i != results.size() - 1) { 43 | serializedResults.add(",".getBytes(StandardCharsets.UTF_8)); 44 | } else { 45 | serializedResults.add("]".getBytes(StandardCharsets.UTF_8)); 46 | } 47 | totalLength += currentResult.length + 1; // result.length + ',' or ']' 48 | } 49 | 50 | if (totalLength > Integer.MAX_VALUE) { 51 | throw new IllegalStateException( 52 | "Response size exceed 2GiB. Query will fail. Seen size: " + totalLength); 53 | } 54 | response.setContentLength((int) totalLength); 55 | 56 | for (byte[] result : serializedResults) { 57 | response.getOutputStream().write(result); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /graphql-java-kickstart/src/main/java/graphql/kickstart/execution/GraphQLRequest.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.execution; 2 | 3 | import static graphql.kickstart.execution.OperationNameExtractor.extractOperationName; 4 | 5 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 6 | import com.fasterxml.jackson.databind.annotation.JsonDeserialize; 7 | import graphql.introspection.IntrospectionQuery; 8 | import java.util.HashMap; 9 | import java.util.Map; 10 | 11 | /** @author Andrew Potter */ 12 | @JsonIgnoreProperties(ignoreUnknown = true) 13 | public class GraphQLRequest { 14 | 15 | private String query; 16 | 17 | @JsonDeserialize(using = VariablesDeserializer.class) 18 | private Map variables = new HashMap<>(); 19 | 20 | @JsonDeserialize(using = ExtensionsDeserializer.class) 21 | private Map extensions = new HashMap<>(); 22 | 23 | private String operationName; 24 | 25 | public GraphQLRequest() {} 26 | 27 | public GraphQLRequest( 28 | String query, 29 | Map variables, 30 | Map extensions, 31 | String operationName) { 32 | this.query = query; 33 | this.operationName = operationName; 34 | if (extensions != null) { 35 | this.extensions = extensions; 36 | } 37 | if (variables != null) { 38 | this.variables = variables; 39 | } 40 | } 41 | 42 | public static GraphQLRequest createIntrospectionRequest() { 43 | return new GraphQLRequest( 44 | IntrospectionQuery.INTROSPECTION_QUERY, 45 | new HashMap<>(), 46 | new HashMap<>(), 47 | "IntrospectionQuery"); 48 | } 49 | 50 | public static GraphQLRequest createQueryOnlyRequest(String query) { 51 | return new GraphQLRequest(query, new HashMap<>(), new HashMap<>(), null); 52 | } 53 | 54 | public String getQuery() { 55 | return query; 56 | } 57 | 58 | public void setQuery(String query) { 59 | this.query = query; 60 | } 61 | 62 | public Map getVariables() { 63 | return variables; 64 | } 65 | 66 | public void setVariables(Map variables) { 67 | if (variables != null) { 68 | this.variables = variables; 69 | } 70 | } 71 | 72 | public Map getExtensions() { 73 | return extensions; 74 | } 75 | 76 | public void setExtensions(Map extensions) { 77 | if (extensions != null) { 78 | this.extensions = extensions; 79 | } 80 | } 81 | 82 | public String getOperationName() { 83 | return extractOperationName(query, operationName, null); 84 | } 85 | 86 | public void setOperationName(String operationName) { 87 | this.operationName = operationName; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /graphql-java-servlet/src/test/groovy/graphql/kickstart/servlet/RequestTester.groovy: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.servlet 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper 4 | import graphql.execution.reactive.SingleSubscriberPublisher 5 | import graphql.kickstart.servlet.core.GraphQLServletListener 6 | import org.springframework.mock.web.MockHttpServletRequest 7 | import org.springframework.mock.web.MockHttpServletResponse 8 | import spock.lang.Shared 9 | 10 | import java.util.concurrent.CountDownLatch 11 | import java.util.concurrent.atomic.AtomicReference 12 | 13 | class RequestTester { 14 | 15 | public static final int STATUS_OK = 200 16 | public static final int STATUS_BAD_REQUEST = 400 17 | public static final int STATUS_ERROR = 500 18 | public static final String CONTENT_TYPE_JSON_UTF8 = 'application/json;charset=UTF-8' 19 | public static final String CONTENT_TYPE_SERVER_SENT_EVENTS = 'text/event-stream;charset=UTF-8' 20 | 21 | @Shared 22 | ObjectMapper mapper = new ObjectMapper() 23 | 24 | AbstractGraphQLHttpServlet servlet 25 | MockHttpServletRequest request 26 | MockHttpServletResponse response 27 | CountDownLatch subscriptionLatch 28 | 29 | RequestTester(GraphQLServletListener... listeners) { 30 | subscriptionLatch = new CountDownLatch(1) 31 | servlet = TestUtils.createDefaultServlet( 32 | { env -> env.arguments.arg }, 33 | { env -> env.arguments.arg }, 34 | { env -> 35 | AtomicReference> publisherRef = new AtomicReference<>() 36 | publisherRef.set(new SingleSubscriberPublisher({ 37 | SingleSubscriberPublisher publisher = publisherRef.get() 38 | publisher.offer("First\n\n" + env.arguments.arg) 39 | publisher.offer("Second\n\n" + env.arguments.arg) 40 | publisher.noMoreData() 41 | subscriptionLatch.countDown() 42 | })) 43 | return publisherRef.get() 44 | }, 45 | listeners) 46 | 47 | request = new MockHttpServletRequest() 48 | request.asyncSupported = true 49 | request.method = "GET" 50 | response = new MockHttpServletResponse() 51 | } 52 | 53 | Map getResponseContent() { 54 | mapper.readValue(response.getContentAsByteArray(), Map) 55 | } 56 | 57 | def addParameter(String name, String value) { 58 | request.addParameter(name, value) 59 | } 60 | 61 | def doGet() { 62 | servlet.doGet(request, response) 63 | } 64 | 65 | def assertThatResponseIsOk() { 66 | return response.getStatus() == STATUS_OK 67 | } 68 | 69 | def assertThatContentTypeIsJson() { 70 | return response.getContentType() == CONTENT_TYPE_JSON_UTF8 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /graphql-java-servlet/src/main/java/graphql/kickstart/servlet/context/DefaultGraphQLWebSocketContext.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.servlet.context; 2 | 3 | import graphql.kickstart.execution.context.DefaultGraphQLContext; 4 | import jakarta.websocket.Session; 5 | import jakarta.websocket.server.HandshakeRequest; 6 | import org.dataloader.DataLoaderRegistry; 7 | 8 | /** @deprecated Use {@link graphql.kickstart.execution.context.GraphQLKickstartContext} instead */ 9 | @Deprecated 10 | public class DefaultGraphQLWebSocketContext extends DefaultGraphQLContext 11 | implements GraphQLWebSocketContext { 12 | 13 | private DefaultGraphQLWebSocketContext( 14 | DataLoaderRegistry dataLoaderRegistry, Session session, HandshakeRequest handshakeRequest) { 15 | super(dataLoaderRegistry); 16 | put(Session.class, session); 17 | put(HandshakeRequest.class, handshakeRequest); 18 | } 19 | 20 | public static Builder createWebSocketContext(DataLoaderRegistry registry) { 21 | return new Builder(registry); 22 | } 23 | 24 | public static Builder createWebSocketContext() { 25 | return new Builder(new DataLoaderRegistry()); 26 | } 27 | 28 | /** 29 | * @deprecated Use {@code dataFetchingEnvironment.getGraphQlContext().get(Session.class)} instead. 30 | * Since 13.0.0 31 | */ 32 | @Override 33 | @Deprecated 34 | public Session getSession() { 35 | return (Session) getMapOfContext().get(Session.class); 36 | } 37 | 38 | /** 39 | * @deprecated Use {@code dataFetchingEnvironment.getGraphQlContext().get(HandshakeRequest.class)} 40 | * instead. Since 13.0.0 41 | */ 42 | @Override 43 | @Deprecated 44 | public HandshakeRequest getHandshakeRequest() { 45 | return (HandshakeRequest) getMapOfContext().get(HandshakeRequest.class); 46 | } 47 | 48 | public static class Builder { 49 | 50 | private Session session; 51 | private HandshakeRequest handshakeRequest; 52 | private DataLoaderRegistry dataLoaderRegistry; 53 | 54 | private Builder(DataLoaderRegistry dataLoaderRegistry) { 55 | this.dataLoaderRegistry = dataLoaderRegistry; 56 | } 57 | 58 | public DefaultGraphQLWebSocketContext build() { 59 | return new DefaultGraphQLWebSocketContext(dataLoaderRegistry, session, handshakeRequest); 60 | } 61 | 62 | public Builder with(Session session) { 63 | this.session = session; 64 | return this; 65 | } 66 | 67 | public Builder with(HandshakeRequest handshakeRequest) { 68 | this.handshakeRequest = handshakeRequest; 69 | return this; 70 | } 71 | 72 | public Builder with(DataLoaderRegistry dataLoaderRegistry) { 73 | this.dataLoaderRegistry = dataLoaderRegistry; 74 | return this; 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /graphql-java-servlet/src/test/groovy/graphql/kickstart/servlet/cache/CacheReaderTest.groovy: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.servlet.cache 2 | 3 | import graphql.kickstart.execution.input.GraphQLInvocationInput 4 | import spock.lang.Specification 5 | 6 | import jakarta.servlet.ServletOutputStream 7 | import jakarta.servlet.http.HttpServletRequest 8 | import jakarta.servlet.http.HttpServletResponse 9 | 10 | class CacheReaderTest extends Specification { 11 | 12 | def cacheManager 13 | def invocationInput 14 | def request 15 | def response 16 | def cacheReader 17 | def cachedResponse 18 | 19 | def setup() { 20 | cacheManager = Mock(GraphQLResponseCacheManager) 21 | invocationInput = Mock(GraphQLInvocationInput) 22 | request = Mock(HttpServletRequest) 23 | response = Mock(HttpServletResponse) 24 | cacheReader = new CacheReader() 25 | cachedResponse = Mock(CachedResponse) 26 | } 27 | 28 | def "should return false if no cached response"() { 29 | given: 30 | cacheManager.get(request, invocationInput) >> null 31 | 32 | when: 33 | def result = cacheReader.responseFromCache(invocationInput, request, response, cacheManager) 34 | 35 | then: 36 | !result 37 | } 38 | 39 | def "should send error response if cached response is error"() { 40 | given: 41 | cachedResponse.isError() >> true 42 | cachedResponse.getErrorStatusCode() >> 10 43 | cachedResponse.getErrorMessage() >> "some error" 44 | cacheManager.get(request, invocationInput) >> cachedResponse 45 | 46 | when: 47 | def result = cacheReader.responseFromCache(invocationInput, request, response, cacheManager) 48 | 49 | then: 50 | result 51 | 1 * response.sendError(10, "some error") 52 | } 53 | 54 | def "should send success response if cached response is ok"() { 55 | given: 56 | def outputStream = Mock(ServletOutputStream) 57 | cachedResponse.isError() >> false 58 | cachedResponse.getContentBytes() >> [00, 01, 02] 59 | response.getOutputStream() >> outputStream 60 | cacheManager.get(request, invocationInput) >> cachedResponse 61 | 62 | when: 63 | def result = cacheReader.responseFromCache(invocationInput, request, response, cacheManager) 64 | 65 | then: 66 | result 67 | 1 * response.setContentType("application/json;charset=UTF-8") 68 | 1 * response.setStatus(200) 69 | 1 * response.setCharacterEncoding("UTF-8") 70 | 1 * response.setContentLength(3) 71 | 1 * outputStream.write([00, 01, 02]) 72 | } 73 | 74 | def "should return false if exception is thrown"() { 75 | given: 76 | cacheManager.get(request, invocationInput) >> { throw new RuntimeException() } 77 | 78 | when: 79 | def result = cacheReader.responseFromCache(invocationInput, request, response, cacheManager) 80 | 81 | then: 82 | !result 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /graphql-java-servlet/src/main/java/graphql/kickstart/servlet/ListenerHandler.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.servlet; 2 | 3 | import static java.util.Collections.emptyList; 4 | 5 | import graphql.kickstart.servlet.core.GraphQLServletListener; 6 | import graphql.kickstart.servlet.core.GraphQLServletListener.RequestCallback; 7 | import java.util.List; 8 | import java.util.Objects; 9 | import java.util.function.Consumer; 10 | import java.util.function.Function; 11 | import java.util.stream.Collectors; 12 | import jakarta.servlet.http.HttpServletRequest; 13 | import jakarta.servlet.http.HttpServletResponse; 14 | import lombok.RequiredArgsConstructor; 15 | import lombok.extern.slf4j.Slf4j; 16 | 17 | @Slf4j 18 | @RequiredArgsConstructor 19 | public class ListenerHandler { 20 | 21 | private final List callbacks; 22 | private final HttpServletRequest request; 23 | private final HttpServletResponse response; 24 | 25 | static ListenerHandler start( 26 | HttpServletRequest request, 27 | HttpServletResponse response, 28 | List listeners) { 29 | if (listeners != null) { 30 | return new ListenerHandler( 31 | runListeners(listeners, it -> it.onRequest(request, response)), request, response); 32 | } 33 | return new ListenerHandler(emptyList(), request, response); 34 | } 35 | 36 | private static List runListeners( 37 | List listeners, Function action) { 38 | return listeners.stream() 39 | .map( 40 | listener -> { 41 | try { 42 | return action.apply(listener); 43 | } catch (Exception t) { 44 | log.error("Error running listener: {}", listener, t); 45 | return null; 46 | } 47 | }) 48 | .filter(Objects::nonNull) 49 | .collect(Collectors.toList()); 50 | } 51 | 52 | void runCallbacks(Consumer action) { 53 | callbacks.forEach( 54 | callback -> { 55 | try { 56 | action.accept(callback); 57 | } catch (Exception t) { 58 | log.error("Error running callback: {}", callback, t); 59 | } 60 | }); 61 | } 62 | 63 | void onParseError(Throwable throwable) { 64 | runCallbacks(it -> it.onParseError(request, response, throwable)); 65 | } 66 | 67 | void beforeFlush() { 68 | runCallbacks(it -> it.beforeFlush(request, response)); 69 | } 70 | 71 | void onSuccess() { 72 | runCallbacks(it -> it.onSuccess(request, response)); 73 | } 74 | 75 | void onError(Throwable throwable) { 76 | runCallbacks(it -> it.onError(request, response, throwable)); 77 | } 78 | 79 | void onFinally() { 80 | runCallbacks(it -> it.onFinally(request, response)); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/apollo/SubscriptionStartCommand.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.execution.subscriptions.apollo; 2 | 3 | import static graphql.kickstart.execution.subscriptions.apollo.OperationMessage.Type.GQL_ERROR; 4 | 5 | import graphql.ExecutionResult; 6 | import graphql.kickstart.execution.GraphQLInvoker; 7 | import graphql.kickstart.execution.GraphQLRequest; 8 | import graphql.kickstart.execution.input.GraphQLSingleInvocationInput; 9 | import graphql.kickstart.execution.subscriptions.GraphQLSubscriptionInvocationInputFactory; 10 | import graphql.kickstart.execution.subscriptions.GraphQLSubscriptionMapper; 11 | import graphql.kickstart.execution.subscriptions.SubscriptionSession; 12 | import java.util.Collection; 13 | import java.util.Objects; 14 | import java.util.concurrent.CompletableFuture; 15 | import lombok.RequiredArgsConstructor; 16 | import lombok.extern.slf4j.Slf4j; 17 | 18 | @Slf4j 19 | @RequiredArgsConstructor 20 | class SubscriptionStartCommand implements SubscriptionCommand { 21 | 22 | private final GraphQLSubscriptionMapper mapper; 23 | private final GraphQLSubscriptionInvocationInputFactory invocationInputFactory; 24 | private final GraphQLInvoker graphQLInvoker; 25 | private final Collection connectionListeners; 26 | 27 | @Override 28 | public void apply(SubscriptionSession session, OperationMessage message) { 29 | log.debug("Apollo subscription start: {} --> {}", session, message.getPayload()); 30 | connectionListeners.forEach(it -> it.onStart(session, message)); 31 | CompletableFuture executionResult = 32 | executeAsync(message.getPayload(), session); 33 | executionResult.thenAccept(result -> handleSubscriptionStart(session, message.getId(), result)); 34 | } 35 | 36 | private CompletableFuture executeAsync( 37 | Object payload, SubscriptionSession session) { 38 | Objects.requireNonNull(payload, "Payload is required"); 39 | GraphQLRequest graphQLRequest = mapper.convertGraphQLRequest(payload); 40 | 41 | GraphQLSingleInvocationInput invocationInput = 42 | invocationInputFactory.create(graphQLRequest, session); 43 | return graphQLInvoker.executeAsync(invocationInput); 44 | } 45 | 46 | private void handleSubscriptionStart( 47 | SubscriptionSession session, String id, ExecutionResult executionResult) { 48 | ExecutionResult sanitizedExecutionResult = mapper.sanitizeErrors(executionResult); 49 | if (mapper.hasNoErrors(sanitizedExecutionResult)) { 50 | session.subscribe(id, sanitizedExecutionResult.getData()); 51 | } else { 52 | Object payload = mapper.convertSanitizedExecutionResult(sanitizedExecutionResult); 53 | session.sendMessage(new OperationMessage(GQL_ERROR, id, payload)); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /graphql-java-servlet/src/main/java/graphql/kickstart/servlet/apollo/ApolloWebSocketSubscriptionProtocolFactory.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.servlet.apollo; 2 | 3 | import graphql.kickstart.execution.GraphQLInvoker; 4 | import graphql.kickstart.execution.GraphQLObjectMapper; 5 | import graphql.kickstart.execution.subscriptions.GraphQLSubscriptionInvocationInputFactory; 6 | import graphql.kickstart.execution.subscriptions.GraphQLSubscriptionMapper; 7 | import graphql.kickstart.execution.subscriptions.SubscriptionSession; 8 | import graphql.kickstart.execution.subscriptions.apollo.ApolloSubscriptionConnectionListener; 9 | import graphql.kickstart.execution.subscriptions.apollo.ApolloSubscriptionProtocolFactory; 10 | import graphql.kickstart.servlet.subscriptions.WebSocketSubscriptionProtocolFactory; 11 | import java.time.Duration; 12 | import java.util.Collection; 13 | import jakarta.websocket.Session; 14 | 15 | public class ApolloWebSocketSubscriptionProtocolFactory extends ApolloSubscriptionProtocolFactory 16 | implements WebSocketSubscriptionProtocolFactory { 17 | 18 | public ApolloWebSocketSubscriptionProtocolFactory( 19 | GraphQLObjectMapper objectMapper, 20 | GraphQLSubscriptionInvocationInputFactory invocationInputFactory, 21 | GraphQLInvoker graphQLInvoker) { 22 | super(objectMapper, invocationInputFactory, graphQLInvoker); 23 | } 24 | 25 | public ApolloWebSocketSubscriptionProtocolFactory( 26 | GraphQLObjectMapper objectMapper, 27 | GraphQLSubscriptionInvocationInputFactory invocationInputFactory, 28 | GraphQLInvoker graphQLInvoker, 29 | Duration keepAliveInterval) { 30 | super(objectMapper, invocationInputFactory, graphQLInvoker, keepAliveInterval); 31 | } 32 | 33 | public ApolloWebSocketSubscriptionProtocolFactory( 34 | GraphQLObjectMapper objectMapper, 35 | GraphQLSubscriptionInvocationInputFactory invocationInputFactory, 36 | GraphQLInvoker graphQLInvoker, 37 | Collection connectionListeners) { 38 | super(objectMapper, invocationInputFactory, graphQLInvoker, connectionListeners); 39 | } 40 | 41 | public ApolloWebSocketSubscriptionProtocolFactory( 42 | GraphQLObjectMapper objectMapper, 43 | GraphQLSubscriptionInvocationInputFactory invocationInputFactory, 44 | GraphQLInvoker graphQLInvoker, 45 | Collection connectionListeners, 46 | Duration keepAliveInterval) { 47 | super( 48 | objectMapper, 49 | invocationInputFactory, 50 | graphQLInvoker, 51 | connectionListeners, 52 | keepAliveInterval); 53 | } 54 | 55 | @Override 56 | public SubscriptionSession createSession(Session session) { 57 | return new ApolloWebSocketSubscriptionSession( 58 | new GraphQLSubscriptionMapper(getObjectMapper()), session); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /graphql-java-servlet/src/main/java/graphql/kickstart/servlet/HttpRequestHandlerImpl.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.servlet; 2 | 3 | import graphql.GraphQLException; 4 | import graphql.kickstart.execution.input.GraphQLInvocationInput; 5 | import java.io.IOException; 6 | import java.nio.charset.StandardCharsets; 7 | import jakarta.servlet.http.HttpServletRequest; 8 | import jakarta.servlet.http.HttpServletResponse; 9 | import lombok.extern.slf4j.Slf4j; 10 | 11 | @Slf4j 12 | class HttpRequestHandlerImpl implements HttpRequestHandler { 13 | 14 | private final GraphQLConfiguration configuration; 15 | private final HttpRequestInvoker requestInvoker; 16 | 17 | HttpRequestHandlerImpl(GraphQLConfiguration configuration) { 18 | this( 19 | configuration, 20 | new HttpRequestInvokerImpl( 21 | configuration, 22 | configuration.getGraphQLInvoker(), 23 | new QueryResponseWriterFactoryImpl())); 24 | } 25 | 26 | HttpRequestHandlerImpl( 27 | GraphQLConfiguration configuration, HttpRequestInvoker requestInvoker) { 28 | this.configuration = configuration; 29 | this.requestInvoker = requestInvoker; 30 | } 31 | 32 | @Override 33 | public void handle(HttpServletRequest request, HttpServletResponse response) throws IOException { 34 | if (request.getCharacterEncoding() == null) { 35 | request.setCharacterEncoding(StandardCharsets.UTF_8.name()); 36 | } 37 | 38 | ListenerHandler listenerHandler = 39 | ListenerHandler.start(request, response, configuration.getListeners()); 40 | 41 | try { 42 | GraphQLInvocationInput invocationInput = parseInvocationInput(request, response); 43 | requestInvoker.execute(invocationInput, request, response, listenerHandler); 44 | } catch (InvocationInputParseException e) { 45 | response.setStatus(STATUS_BAD_REQUEST); 46 | log.info("Bad request: cannot parse http request", e); 47 | listenerHandler.onParseError(e); 48 | throw e; 49 | } catch (GraphQLException e) { 50 | response.setStatus(STATUS_BAD_REQUEST); 51 | log.info("Bad request: cannot handle http request", e); 52 | throw e; 53 | } catch (Exception t) { 54 | response.setStatus(STATUS_INTERNAL_SERVER_ERROR); 55 | log.error("Cannot handle http request", t); 56 | throw t; 57 | } 58 | } 59 | 60 | private GraphQLInvocationInput parseInvocationInput( 61 | HttpServletRequest request, 62 | HttpServletResponse response) { 63 | try { 64 | GraphQLInvocationInputParser invocationInputParser = 65 | GraphQLInvocationInputParser.create( 66 | request, 67 | configuration.getInvocationInputFactory(), 68 | configuration.getObjectMapper(), 69 | configuration.getContextSetting()); 70 | return invocationInputParser.getGraphQLInvocationInput(request, response); 71 | } catch (Exception e) { 72 | throw new InvocationInputParseException(e); 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/DefaultSubscriptionSession.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.execution.subscriptions; 2 | 3 | import graphql.ExecutionResult; 4 | import graphql.execution.reactive.SingleSubscriberPublisher; 5 | import java.util.HashMap; 6 | import java.util.Map; 7 | import java.util.Objects; 8 | import lombok.Getter; 9 | import lombok.RequiredArgsConstructor; 10 | import lombok.extern.slf4j.Slf4j; 11 | import org.reactivestreams.Publisher; 12 | import org.reactivestreams.Subscription; 13 | 14 | @Slf4j 15 | @RequiredArgsConstructor 16 | public class DefaultSubscriptionSession implements SubscriptionSession { 17 | 18 | @Getter private final GraphQLSubscriptionMapper mapper; 19 | private SingleSubscriberPublisher publisher = new SingleSubscriberPublisher<>(); 20 | private SessionSubscriptions subscriptions = new SessionSubscriptions(); 21 | 22 | @Override 23 | public void send(String message) { 24 | Objects.requireNonNull(message, "message is required"); 25 | publisher.offer(message); 26 | } 27 | 28 | @Override 29 | public void sendMessage(Object payload) { 30 | Objects.requireNonNull(payload, "payload is required"); 31 | send(mapper.serialize(payload)); 32 | } 33 | 34 | @Override 35 | public void subscribe(String id, Publisher dataPublisher) { 36 | dataPublisher.subscribe(new SessionSubscriber(this, id)); 37 | } 38 | 39 | @Override 40 | public void add(String id, Subscription subscription) { 41 | subscriptions.add(id, subscription); 42 | } 43 | 44 | @Override 45 | public void unsubscribe(String id) { 46 | subscriptions.cancel(id); 47 | } 48 | 49 | @Override 50 | public void sendDataMessage(String id, Object payload) { 51 | send(mapper.serialize(payload)); 52 | } 53 | 54 | @Override 55 | public void sendErrorMessage(String id, Object payload) { 56 | send(mapper.serialize(payload)); 57 | } 58 | 59 | @Override 60 | public void sendCompleteMessage(String id) { 61 | // default empty implementation 62 | } 63 | 64 | @Override 65 | public void close(String reason) { 66 | log.debug("Closing subscription session {}", getId()); 67 | subscriptions.close(); 68 | publisher.noMoreData(); 69 | } 70 | 71 | @Override 72 | public Map getUserProperties() { 73 | return new HashMap<>(); 74 | } 75 | 76 | @Override 77 | public boolean isOpen() { 78 | return true; 79 | } 80 | 81 | @Override 82 | public String getId() { 83 | return null; 84 | } 85 | 86 | @Override 87 | public SessionSubscriptions getSubscriptions() { 88 | return subscriptions; 89 | } 90 | 91 | @Override 92 | public Object unwrap() { 93 | throw new UnsupportedOperationException(); 94 | } 95 | 96 | @Override 97 | public Publisher getPublisher() { 98 | return publisher; 99 | } 100 | 101 | @Override 102 | public String toString() { 103 | return getId(); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /.github/workflows/pull-request.yml: -------------------------------------------------------------------------------- 1 | name: "Pull request" 2 | on: 3 | pull_request: 4 | types: [ opened, synchronize, reopened ] 5 | 6 | jobs: 7 | validation: 8 | name: Gradle Wrapper Validation 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | - uses: gradle/wrapper-validation-action@v3 13 | 14 | test: 15 | name: Test run 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | os: [ ubuntu-latest, macos-latest, windows-latest ] 20 | java: [ 17, 19 ] 21 | needs: validation 22 | runs-on: ${{ matrix.os }} 23 | steps: 24 | - name: Checkout 25 | uses: actions/checkout@v4 26 | - name: Setup Java 27 | uses: actions/setup-java@v4 28 | with: 29 | distribution: 'zulu' 30 | java-version: ${{ matrix.java }} 31 | - name: Cache Gradle 32 | uses: actions/cache@v4 33 | env: 34 | java-version: ${{ matrix.java }} 35 | with: 36 | path: | 37 | ~/.gradle/caches 38 | ~/.gradle/wrapper 39 | key: ${{ runner.os }}-${{ env.java-version }}-gradle-${{ hashFiles('**/*.gradle*') }} 40 | restore-keys: ${{ runner.os }}-${{ env.java-version }}-gradle- 41 | - name: Make gradlew executable (non-Windows only) 42 | if: matrix.os != 'windows-latest' 43 | run: chmod +x ./gradlew 44 | - name: Gradle Check (non-Windows) 45 | if: matrix.os != 'windows-latest' 46 | run: ./gradlew --info check 47 | - name: Gradle Check (Windows) 48 | if: matrix.os == 'windows-latest' 49 | shell: cmd 50 | run: gradlew --info check 51 | 52 | build: 53 | name: Sonar analysis 54 | needs: validation 55 | runs-on: ubuntu-latest 56 | env: 57 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} 58 | steps: 59 | - uses: actions/checkout@v4 60 | if: env.SONAR_TOKEN != null 61 | with: 62 | fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis 63 | - name: Set up JDK 17 64 | if: env.SONAR_TOKEN != null 65 | uses: actions/setup-java@v4 66 | with: 67 | distribution: 'zulu' 68 | java-version: 17 69 | - name: Cache SonarCloud packages 70 | if: env.SONAR_TOKEN != null 71 | uses: actions/cache@v4 72 | with: 73 | path: ~/.sonar/cache 74 | key: ${{ runner.os }}-sonar 75 | restore-keys: ${{ runner.os }}-sonar 76 | - name: Cache Gradle packages 77 | if: env.SONAR_TOKEN != null 78 | uses: actions/cache@v4 79 | with: 80 | path: ~/.gradle/caches 81 | key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }} 82 | restore-keys: ${{ runner.os }}-gradle 83 | - name: Build and analyze 84 | if: env.SONAR_TOKEN != null 85 | env: 86 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any 87 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} 88 | run: ./gradlew build jacocoTestReport sonarqube --info 89 | -------------------------------------------------------------------------------- /graphql-java-servlet/src/main/java/graphql/kickstart/servlet/core/internal/VariableMapper.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.servlet.core.internal; 2 | 3 | import java.util.List; 4 | import java.util.Map; 5 | import java.util.regex.Pattern; 6 | import jakarta.servlet.http.Part; 7 | 8 | public class VariableMapper { 9 | 10 | private static final Pattern PERIOD = Pattern.compile("\\."); 11 | 12 | private static final Mapper> MAP_MAPPER = 13 | new Mapper>() { 14 | @Override 15 | public Object set(Map location, String target, Part value) { 16 | return location.put(target, value); 17 | } 18 | 19 | @Override 20 | public Object recurse(Map location, String target) { 21 | return location.get(target); 22 | } 23 | }; 24 | private static final Mapper> LIST_MAPPER = 25 | new Mapper>() { 26 | @Override 27 | public Object set(List location, String target, Part value) { 28 | return location.set(Integer.parseInt(target), value); 29 | } 30 | 31 | @Override 32 | public Object recurse(List location, String target) { 33 | return location.get(Integer.parseInt(target)); 34 | } 35 | }; 36 | 37 | @SuppressWarnings({"unchecked", "rawtypes"}) 38 | public static void mapVariable(String objectPath, Map variables, Part part) { 39 | String[] segments = PERIOD.split(objectPath); 40 | 41 | if (segments.length < 2) { 42 | throw new VariableMapException("object-path in map must have at least two segments"); 43 | } else if (!"variables".equals(segments[0])) { 44 | throw new VariableMapException("can only map into variables"); 45 | } 46 | 47 | Object currentLocation = variables; 48 | for (int i = 1; i < segments.length; i++) { 49 | String segmentName = segments[i]; 50 | Mapper mapper = determineMapper(currentLocation, objectPath, segmentName); 51 | 52 | if (i == segments.length - 1) { 53 | if (null != mapper.set(currentLocation, segmentName, part)) { 54 | throw new VariableMapException("expected null value when mapping " + objectPath); 55 | } 56 | } else { 57 | currentLocation = mapper.recurse(currentLocation, segmentName); 58 | if (null == currentLocation) { 59 | throw new VariableMapException( 60 | "found null intermediate value when trying to map " + objectPath); 61 | } 62 | } 63 | } 64 | } 65 | 66 | private static Mapper determineMapper( 67 | Object currentLocation, String objectPath, String segmentName) { 68 | if (currentLocation instanceof Map) { 69 | return MAP_MAPPER; 70 | } else if (currentLocation instanceof List) { 71 | return LIST_MAPPER; 72 | } 73 | 74 | throw new VariableMapException( 75 | "expected a map or list at " + segmentName + " when trying to map " + objectPath); 76 | } 77 | 78 | interface Mapper { 79 | 80 | Object set(T location, String target, Part value); 81 | 82 | Object recurse(T location, String target); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /graphql-java-servlet/src/main/java/graphql/kickstart/servlet/AbstractGraphQLHttpServlet.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.servlet; 2 | 3 | import static graphql.kickstart.execution.GraphQLRequest.createQueryOnlyRequest; 4 | 5 | import graphql.ExecutionResult; 6 | import graphql.kickstart.execution.GraphQLRequest; 7 | import graphql.kickstart.execution.input.GraphQLSingleInvocationInput; 8 | import graphql.kickstart.servlet.core.GraphQLMBean; 9 | import graphql.kickstart.servlet.core.GraphQLServletListener; 10 | import graphql.schema.GraphQLFieldDefinition; 11 | import jakarta.servlet.Servlet; 12 | import jakarta.servlet.http.HttpServlet; 13 | import jakarta.servlet.http.HttpServletRequest; 14 | import jakarta.servlet.http.HttpServletResponse; 15 | import lombok.extern.slf4j.Slf4j; 16 | 17 | /** @author Andrew Potter */ 18 | @Slf4j 19 | public abstract class AbstractGraphQLHttpServlet extends HttpServlet 20 | implements Servlet, GraphQLMBean { 21 | 22 | protected abstract GraphQLConfiguration getConfiguration(); 23 | 24 | public void addListener(GraphQLServletListener servletListener) { 25 | getConfiguration().add(servletListener); 26 | } 27 | 28 | public void removeListener(GraphQLServletListener servletListener) { 29 | getConfiguration().remove(servletListener); 30 | } 31 | 32 | @Override 33 | public String[] getQueries() { 34 | return getConfiguration() 35 | .getInvocationInputFactory() 36 | .getSchemaProvider() 37 | .getSchema() 38 | .getQueryType() 39 | .getFieldDefinitions() 40 | .stream() 41 | .map(GraphQLFieldDefinition::getName) 42 | .toArray(String[]::new); 43 | } 44 | 45 | @Override 46 | public String[] getMutations() { 47 | return getConfiguration() 48 | .getInvocationInputFactory() 49 | .getSchemaProvider() 50 | .getSchema() 51 | .getMutationType() 52 | .getFieldDefinitions() 53 | .stream() 54 | .map(GraphQLFieldDefinition::getName) 55 | .toArray(String[]::new); 56 | } 57 | 58 | @Override 59 | public String executeQuery(String query) { 60 | try { 61 | GraphQLRequest graphQLRequest = createQueryOnlyRequest(query); 62 | GraphQLSingleInvocationInput invocationInput = 63 | getConfiguration().getInvocationInputFactory().create(graphQLRequest); 64 | ExecutionResult result = 65 | getConfiguration().getGraphQLInvoker().query(invocationInput).getResult(); 66 | return getConfiguration().getObjectMapper().serializeResultAsJson(result); 67 | } catch (Exception e) { 68 | return e.getMessage(); 69 | } 70 | } 71 | 72 | @Override 73 | protected void doGet(HttpServletRequest req, HttpServletResponse resp) { 74 | doRequest(req, resp); 75 | } 76 | 77 | @Override 78 | protected void doPost(HttpServletRequest req, HttpServletResponse resp) { 79 | doRequest(req, resp); 80 | } 81 | 82 | private void doRequest(HttpServletRequest request, HttpServletResponse response) { 83 | try { 84 | getConfiguration().getHttpRequestHandler().handle(request, response); 85 | } catch (Exception t) { 86 | log.error("Error executing GraphQL request!", t); 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /graphql-java-servlet/src/main/java/graphql/kickstart/servlet/GraphQLGetInvocationInputParser.java: -------------------------------------------------------------------------------- 1 | package graphql.kickstart.servlet; 2 | 3 | import graphql.GraphQLException; 4 | import graphql.kickstart.execution.GraphQLObjectMapper; 5 | import graphql.kickstart.execution.GraphQLRequest; 6 | import graphql.kickstart.execution.context.ContextSetting; 7 | import graphql.kickstart.execution.input.GraphQLInvocationInput; 8 | import graphql.kickstart.servlet.input.GraphQLInvocationInputFactory; 9 | import java.io.IOException; 10 | import java.util.HashMap; 11 | import java.util.List; 12 | import java.util.Map; 13 | import java.util.Optional; 14 | import jakarta.servlet.http.HttpServletRequest; 15 | import jakarta.servlet.http.HttpServletResponse; 16 | import lombok.extern.slf4j.Slf4j; 17 | 18 | @Slf4j 19 | class GraphQLGetInvocationInputParser extends AbstractGraphQLInvocationInputParser { 20 | 21 | GraphQLGetInvocationInputParser( 22 | GraphQLInvocationInputFactory invocationInputFactory, 23 | GraphQLObjectMapper graphQLObjectMapper, 24 | ContextSetting contextSetting) { 25 | super(invocationInputFactory, graphQLObjectMapper, contextSetting); 26 | } 27 | 28 | public GraphQLInvocationInput getGraphQLInvocationInput( 29 | HttpServletRequest request, HttpServletResponse response) throws IOException { 30 | if (isIntrospectionQuery(request)) { 31 | GraphQLRequest graphqlRequest = GraphQLRequest.createIntrospectionRequest(); 32 | return invocationInputFactory.create(graphqlRequest, request, response); 33 | } 34 | 35 | String query = request.getParameter("query"); 36 | if (query == null) { 37 | throw new GraphQLException("Query parameter not found in GET request"); 38 | } 39 | 40 | if (isSingleQuery(query)) { 41 | Map variables = getVariables(request); 42 | Map extensions = getExtensions(request); 43 | String operationName = request.getParameter("operationName"); 44 | GraphQLRequest graphqlRequest = 45 | new GraphQLRequest(query, variables, extensions, operationName); 46 | return invocationInputFactory.createReadOnly(graphqlRequest, request, response); 47 | } 48 | 49 | List graphqlRequests = graphQLObjectMapper.readBatchedGraphQLRequest(query); 50 | return invocationInputFactory.createReadOnly( 51 | contextSetting, graphqlRequests, request, response); 52 | } 53 | 54 | private boolean isIntrospectionQuery(HttpServletRequest request) { 55 | String path = 56 | Optional.ofNullable(request.getPathInfo()).orElseGet(request::getServletPath).toLowerCase(); 57 | return path.contentEquals("/schema.json"); 58 | } 59 | 60 | private Map getVariables(HttpServletRequest request) { 61 | return Optional.ofNullable(request.getParameter("variables")) 62 | .map(graphQLObjectMapper::deserializeVariables) 63 | .map(HashMap::new) 64 | .orElseGet(HashMap::new); 65 | } 66 | 67 | private Map getExtensions(HttpServletRequest request) { 68 | return Optional.ofNullable(request.getParameter("extensions")) 69 | .map(graphQLObjectMapper::deserializeExtensions) 70 | .map(HashMap::new) 71 | .orElseGet(HashMap::new); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega 95 | --------------------------------------------------------------------------------