Repository: graphql-java-kickstart/graphql-java-servlet Branch: master Commit: 1b69c873421b Files: 206 Total size: 394.0 KB Directory structure: gitextract_cv18k8ke/ ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ ├── config.yml │ │ └── feature_request.md │ ├── add-javax-suffix.sh │ ├── release.sh │ ├── replaceJakartaWithJavax.sh │ ├── tag-release.sh │ └── workflows/ │ ├── pull-request.yml │ ├── release.yml │ └── snapshot.yml ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── build.gradle ├── examples/ │ └── osgi/ │ ├── apache-karaf-feature/ │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ └── feature/ │ │ └── feature.xml │ ├── apache-karaf-package/ │ │ └── pom.xml │ ├── buildAndRun.sh │ ├── pom.xml │ └── providers/ │ ├── pom.xml │ └── src/ │ └── main/ │ └── java/ │ └── graphql/ │ └── servlet/ │ └── examples/ │ └── osgi/ │ └── ExampleGraphQLProvider.java ├── gradle/ │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat ├── graphql-java-kickstart/ │ ├── bnd.bnd │ ├── build.gradle │ └── src/ │ └── main/ │ └── java/ │ └── graphql/ │ └── kickstart/ │ └── execution/ │ ├── BatchedDataLoaderGraphQLBuilder.java │ ├── DecoratedExecutionResult.java │ ├── DefaultGraphQLRootObjectBuilder.java │ ├── ExtensionsDeserializer.java │ ├── FutureBatchedExecutionResult.java │ ├── FutureErrorExecutionResult.java │ ├── FutureExecutionResult.java │ ├── FutureSingleExecutionResult.java │ ├── GraphQLBatchedQueryResult.java │ ├── GraphQLErrorQueryResult.java │ ├── GraphQLInvoker.java │ ├── GraphQLInvokerProxy.java │ ├── GraphQLObjectMapper.java │ ├── GraphQLQueryInvoker.java │ ├── GraphQLQueryResult.java │ ├── GraphQLRequest.java │ ├── GraphQLRootObjectBuilder.java │ ├── GraphQLSingleQueryResult.java │ ├── ObjectMapDeserializationException.java │ ├── ObjectMapDeserializeHelper.java │ ├── OperationNameExtractor.java │ ├── StaticGraphQLRootObjectBuilder.java │ ├── StringUtils.java │ ├── VariablesDeserializer.java │ ├── config/ │ │ ├── ConfiguringObjectMapperProvider.java │ │ ├── DefaultExecutionStrategyProvider.java │ │ ├── DefaultGraphQLSchemaProvider.java │ │ ├── ExecutionStrategyProvider.java │ │ ├── GraphQLBuilder.java │ │ ├── GraphQLBuilderConfigurer.java │ │ ├── GraphQLSchemaProvider.java │ │ ├── GraphQLServletObjectMapperConfigurer.java │ │ ├── InstrumentationProvider.java │ │ └── ObjectMapperProvider.java │ ├── context/ │ │ ├── ContextSetting.java │ │ ├── ContextSettingNotConfiguredException.java │ │ ├── DefaultGraphQLContext.java │ │ ├── DefaultGraphQLContextBuilder.java │ │ ├── GraphQLContextBuilder.java │ │ └── GraphQLKickstartContext.java │ ├── error/ │ │ ├── DefaultGraphQLErrorHandler.java │ │ ├── DefaultGraphQLServletObjectMapperConfigurer.java │ │ ├── GenericGraphQLError.java │ │ ├── GraphQLErrorHandler.java │ │ └── RenderableNonNullableFieldWasNullError.java │ ├── input/ │ │ ├── GraphQLBatchedInvocationInput.java │ │ ├── GraphQLInvocationInput.java │ │ ├── GraphQLSingleInvocationInput.java │ │ ├── PerQueryBatchedInvocationInput.java │ │ └── PerRequestBatchedInvocationInput.java │ ├── instrumentation/ │ │ ├── AbstractTrackingApproach.java │ │ ├── DataLoaderDispatcherInstrumentationState.java │ │ ├── FieldLevelTrackingApproach.java │ │ ├── NoOpInstrumentationProvider.java │ │ ├── RequestLevelTrackingApproach.java │ │ ├── RequestStack.java │ │ ├── TrackingApproach.java │ │ └── TrackingApproachException.java │ └── subscriptions/ │ ├── AtomicSubscriptionSubscription.java │ ├── DefaultSubscriptionSession.java │ ├── GraphQLSubscriptionInvocationInputFactory.java │ ├── GraphQLSubscriptionMapper.java │ ├── SessionSubscriber.java │ ├── SessionSubscriptions.java │ ├── SubscriptionConnectionListener.java │ ├── SubscriptionException.java │ ├── SubscriptionProtocolFactory.java │ ├── SubscriptionSession.java │ └── apollo/ │ ├── ApolloCommandProvider.java │ ├── ApolloSubscriptionConnectionListener.java │ ├── ApolloSubscriptionConsumer.java │ ├── ApolloSubscriptionKeepAliveRunner.java │ ├── ApolloSubscriptionProtocolFactory.java │ ├── ApolloSubscriptionSession.java │ ├── KeepAliveSubscriptionConnectionListener.java │ ├── OperationMessage.java │ ├── SubscriptionCommand.java │ ├── SubscriptionConnectionInitCommand.java │ ├── SubscriptionConnectionTerminateCommand.java │ ├── SubscriptionStartCommand.java │ └── SubscriptionStopCommand.java ├── graphql-java-servlet/ │ ├── bnd.bnd │ ├── build.gradle │ └── src/ │ ├── main/ │ │ └── java/ │ │ └── graphql/ │ │ └── kickstart/ │ │ └── servlet/ │ │ ├── AbstractGraphQLHttpServlet.java │ │ ├── AbstractGraphQLInvocationInputParser.java │ │ ├── AsyncTaskDecorator.java │ │ ├── AsyncTaskExecutor.java │ │ ├── AsyncTimeoutListener.java │ │ ├── BatchedQueryResponseWriter.java │ │ ├── ConfiguredGraphQLHttpServlet.java │ │ ├── ErrorQueryResponseWriter.java │ │ ├── ExecutionResultSubscriber.java │ │ ├── GraphQLConfiguration.java │ │ ├── GraphQLGetInvocationInputParser.java │ │ ├── GraphQLHttpServlet.java │ │ ├── GraphQLInvocationInputParser.java │ │ ├── GraphQLMultipartInvocationInputParser.java │ │ ├── GraphQLPostInvocationInputParser.java │ │ ├── GraphQLWebsocketServlet.java │ │ ├── HttpRequestHandler.java │ │ ├── HttpRequestHandlerImpl.java │ │ ├── HttpRequestInvoker.java │ │ ├── HttpRequestInvokerImpl.java │ │ ├── InvocationInputParseException.java │ │ ├── ListenerHandler.java │ │ ├── OsgiGraphQLHttpServlet.java │ │ ├── OsgiGraphQLHttpServletConfiguration.java │ │ ├── OsgiSchemaBuilder.java │ │ ├── PartIOException.java │ │ ├── QueryResponseWriter.java │ │ ├── QueryResponseWriterFactory.java │ │ ├── QueryResponseWriterFactoryImpl.java │ │ ├── SingleAsynchronousQueryResponseWriter.java │ │ ├── SingleQueryResponseWriter.java │ │ ├── StaticDataPublisher.java │ │ ├── SubscriptionAsyncListener.java │ │ ├── apollo/ │ │ │ ├── ApolloScalars.java │ │ │ ├── ApolloWebSocketSubscriptionProtocolFactory.java │ │ │ └── ApolloWebSocketSubscriptionSession.java │ │ ├── cache/ │ │ │ ├── BufferedHttpServletResponse.java │ │ │ ├── CacheReader.java │ │ │ ├── CachedResponse.java │ │ │ ├── CachingHttpRequestInvoker.java │ │ │ ├── CachingQueryResponseWriter.java │ │ │ ├── CachingQueryResponseWriterFactory.java │ │ │ └── GraphQLResponseCacheManager.java │ │ ├── config/ │ │ │ ├── DefaultGraphQLSchemaServletProvider.java │ │ │ └── GraphQLSchemaServletProvider.java │ │ ├── context/ │ │ │ ├── DefaultGraphQLServletContext.java │ │ │ ├── DefaultGraphQLServletContextBuilder.java │ │ │ ├── DefaultGraphQLWebSocketContext.java │ │ │ ├── GraphQLServletContext.java │ │ │ ├── GraphQLServletContextBuilder.java │ │ │ └── GraphQLWebSocketContext.java │ │ ├── core/ │ │ │ ├── DefaultGraphQLRootObjectBuilder.java │ │ │ ├── GraphQLMBean.java │ │ │ ├── GraphQLServletListener.java │ │ │ ├── GraphQLServletRootObjectBuilder.java │ │ │ └── internal/ │ │ │ ├── GraphQLThreadFactory.java │ │ │ ├── VariableMapException.java │ │ │ └── VariableMapper.java │ │ ├── input/ │ │ │ ├── BatchInputPreProcessResult.java │ │ │ ├── BatchInputPreProcessor.java │ │ │ ├── GraphQLInvocationInputFactory.java │ │ │ └── NoOpBatchInputPreProcessor.java │ │ ├── osgi/ │ │ │ ├── GraphQLCodeRegistryProvider.java │ │ │ ├── GraphQLDirectiveProvider.java │ │ │ ├── GraphQLFieldProvider.java │ │ │ ├── GraphQLMutationProvider.java │ │ │ ├── GraphQLProvider.java │ │ │ ├── GraphQLQueryProvider.java │ │ │ ├── GraphQLSubscriptionProvider.java │ │ │ └── GraphQLTypesProvider.java │ │ └── subscriptions/ │ │ ├── FallbackSubscriptionConsumer.java │ │ ├── FallbackSubscriptionProtocolFactory.java │ │ ├── WebSocketSendSubscriber.java │ │ ├── WebSocketSubscriptionProtocolFactory.java │ │ └── WebSocketSubscriptionSession.java │ └── test/ │ └── groovy/ │ └── graphql/ │ └── kickstart/ │ └── servlet/ │ ├── AbstractGraphQLHttpServletSpec.groovy │ ├── BatchedQueryResponseWriterTest.groovy │ ├── DataLoaderDispatchingSpec.groovy │ ├── GraphQLServletListenerSpec.groovy │ ├── OsgiGraphQLHttpServletSpec.groovy │ ├── PartIOExceptionTest.groovy │ ├── RequestTester.groovy │ ├── SingleAsynchronousQueryResponseWriterTest.groovy │ ├── SingleQueryResponseWriterTest.groovy │ ├── TestBatchInputPreProcessor.java │ ├── TestException.groovy │ ├── TestGraphQLErrorException.groovy │ ├── TestMultipartPart.groovy │ ├── TestUtils.groovy │ └── cache/ │ ├── CacheReaderTest.groovy │ └── CachingHttpRequestInvokerTest.groovy ├── lombok.config ├── renovate.json └── settings.gradle ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Create a report to help us improve title: '' labels: bug assignees: '' --- **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' 4. See error **Expected behavior** A clear and concise description of what you expected to happen. **Screenshots** If applicable, add screenshots to help explain your problem. **Desktop (please complete the following information):** - OS: [e.g. iOS] - Browser [e.g. chrome, safari] - Version [e.g. 22] **Additional context** Add any other context about the problem here. ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: false contact_links: - name: Question url: https://github.com/graphql-java-kickstart/graphql-java-servlet/discussions about: Anything you are not sure about? Ask the community in Discussions! ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature request about: Suggest an idea for this project title: '' labels: enhancement assignees: '' --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context or screenshots about the feature request here. ================================================ FILE: .github/add-javax-suffix.sh ================================================ #!/bin/bash addSuffix() { local result result=$(grep include settings.gradle | awk '{print $2}' | tr -d "'" | tr -d ':') readarray -t <<<"$result" modules=("${MAPFILE[@]}") updateLocalDependencies } updateLocalDependencies() { for module in "${modules[@]}"; do cp -rf "$module" "$module"-javax rm -rf "$module" for dependency in "${modules[@]}"; do sed -i -E "s/project\(('|\"):${dependency}('|\")\)/project\(':${dependency}-javax'\)/" "$module"-"javax"/build.gradle done done updateGradleSettings } updateGradleSettings() { for module in "${modules[@]}"; do echo "Replace ${module} with ${module}-javax in settings.gradle" sed -i -E "s/('|\"):${module}('|\")/':${module}-javax'/" settings.gradle done cat settings.gradle } echo "Add suffix -javax to modules" addSuffix ls -lh ================================================ FILE: .github/release.sh ================================================ #!/bin/bash set -ev FLAVOUR="${1}" removeSnapshots() { sed -i 's/-SNAPSHOT//' gradle.properties } echo "Publishing release to Maven Central" removeSnapshots if [ "${FLAVOUR}" == 'javax' ]; then .github/add-javax-suffix.sh fi ./gradlew clean build publishToSonatype closeAndReleaseSonatypeStagingRepository ================================================ FILE: .github/replaceJakartaWithJavax.sh ================================================ #!/bin/bash # Set jdk11 as source and target sed -i 's/SOURCE_COMPATIBILITY=.*/SOURCE_COMPATIBILITY=11/' gradle.properties sed -i 's/TARGET_COMPATIBILITY=.*/TARGET_COMPATIBILITY=11/' gradle.properties # Replace jakarta imports and dependencies with javax grep -rl 'import jakarta' ./graphql-java-servlet | xargs sed -i 's/import jakarta/import javax/g' sed -i 's/.*jakarta.websocket:jakarta.websocket-client-api.*//' graphql-java-servlet/build.gradle sed -i \ 's/jakarta.servlet:jakarta.servlet-api.*/javax.servlet:javax.servlet-api:$LIB_JAVAX_SERVLET"/' \ graphql-java-servlet/build.gradle sed -i \ 's/jakarta.websocket.*/javax.websocket:javax.websocket-api:$LIB_JAVAX_WEBSOCKET"/' \ graphql-java-servlet/build.gradle # Final check if there are something else to replace grep -rl 'jakarta' ./graphql-java-servlet | xargs sed -i 's/jakarta/javax/g' # Set the version 5 for spring framework sed -i \ 's/org.springframework:spring-test.*/org.springframework:spring-test:$LIB_SPRINGFRAMEWORK_5"/' \ graphql-java-servlet/build.gradle sed -i \ 's/org.springframework:spring-web.*/org.springframework:spring-web:$LIB_SPRINGFRAMEWORK_5"/' \ graphql-java-servlet/build.gradle echo "Replaced jakarta occurrences with javax" ================================================ FILE: .github/tag-release.sh ================================================ #!/bin/bash set -ev getVersion() { ./gradlew properties -q | grep -E "^version" | awk '{print $2}' | tr -d '[:space:]' } removeSnapshots() { sed -i 's/-SNAPSHOT//' gradle.properties } commitRelease() { local APP_VERSION APP_VERSION=$(getVersion) git commit -a -m "Update version for release" git tag -a "v${APP_VERSION}" -m "Tag release version" } bumpVersion() { echo "Bump version number" local APP_VERSION APP_VERSION=$(getVersion | xargs) local SEMANTIC_REGEX='^([0-9]+)\.([0-9]+)(\.([0-9]+))?$' if [[ ${APP_VERSION} =~ ${SEMANTIC_REGEX} ]]; then if [[ ${BASH_REMATCH[4]} ]]; then nextVersion=$((BASH_REMATCH[4] + 1)) nextVersion="${BASH_REMATCH[1]}.${BASH_REMATCH[2]}.${nextVersion}-SNAPSHOT" else nextVersion=$((BASH_REMATCH[2] + 1)) nextVersion="${BASH_REMATCH[1]}.${nextVersion}-SNAPSHOT" fi echo "Next version: ${nextVersion}" sed -i -E "s/^version(\s)?=.*/version=${nextVersion}/" gradle.properties git commit -a -m "Bumped version for next release" else echo "No semantic version and therefore cannot publish to maven repository: '${APP_VERSION}'" fi } git config --global user.email "actions@github.com" git config --global user.name "GitHub Actions" echo "Deploying release to Maven Central" removeSnapshots commitRelease bumpVersion git push --follow-tags ================================================ FILE: .github/workflows/pull-request.yml ================================================ name: "Pull request" on: pull_request: types: [ opened, synchronize, reopened ] jobs: validation: name: Gradle Wrapper Validation runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: gradle/wrapper-validation-action@v3 test: name: Test run strategy: fail-fast: false matrix: os: [ ubuntu-latest, macos-latest, windows-latest ] java: [ 17, 19 ] needs: validation runs-on: ${{ matrix.os }} steps: - name: Checkout uses: actions/checkout@v4 - name: Setup Java uses: actions/setup-java@v4 with: distribution: 'zulu' java-version: ${{ matrix.java }} - name: Cache Gradle uses: actions/cache@v4 env: java-version: ${{ matrix.java }} with: path: | ~/.gradle/caches ~/.gradle/wrapper key: ${{ runner.os }}-${{ env.java-version }}-gradle-${{ hashFiles('**/*.gradle*') }} restore-keys: ${{ runner.os }}-${{ env.java-version }}-gradle- - name: Make gradlew executable (non-Windows only) if: matrix.os != 'windows-latest' run: chmod +x ./gradlew - name: Gradle Check (non-Windows) if: matrix.os != 'windows-latest' run: ./gradlew --info check - name: Gradle Check (Windows) if: matrix.os == 'windows-latest' shell: cmd run: gradlew --info check build: name: Sonar analysis needs: validation runs-on: ubuntu-latest env: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} steps: - uses: actions/checkout@v4 if: env.SONAR_TOKEN != null with: fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis - name: Set up JDK 17 if: env.SONAR_TOKEN != null uses: actions/setup-java@v4 with: distribution: 'zulu' java-version: 17 - name: Cache SonarCloud packages if: env.SONAR_TOKEN != null uses: actions/cache@v4 with: path: ~/.sonar/cache key: ${{ runner.os }}-sonar restore-keys: ${{ runner.os }}-sonar - name: Cache Gradle packages if: env.SONAR_TOKEN != null uses: actions/cache@v4 with: path: ~/.gradle/caches key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }} restore-keys: ${{ runner.os }}-gradle - name: Build and analyze if: env.SONAR_TOKEN != null env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} run: ./gradlew build jacocoTestReport sonarqube --info ================================================ FILE: .github/workflows/release.yml ================================================ name: "Publish release" on: [ workflow_dispatch ] jobs: validation: name: Gradle Wrapper Validation runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: gradle/wrapper-validation-action@v3 test-jakarta: name: Test run jakarta needs: validation runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Setup Java uses: actions/setup-java@v4 with: distribution: 'zulu' java-version: 17 - name: Cache Gradle uses: actions/cache@v4 env: java-version: 17 with: path: | ~/.gradle/caches ~/.gradle/wrapper key: ${{ runner.os }}-${{ env.java-version }}-gradle-${{ hashFiles('**/*.gradle*') }} restore-keys: | ${{ runner.os }}-${{ env.java-version }}-gradle- - name: Make gradlew executable run: chmod +x ./gradlew - name: Gradle Check run: ./gradlew --info check build-jakarta: name: Publish release jakarta needs: test-jakarta runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Setup Java uses: actions/setup-java@v4 with: distribution: 'zulu' java-version: 17 - name: Cache Gradle uses: actions/cache@v4 env: java-version: 17 with: path: | ~/.gradle/caches ~/.gradle/wrapper key: ${{ runner.os }}-${{ env.java-version }}-gradle-${{ hashFiles('**/*.gradle*') }} restore-keys: | ${{ runner.os }}-${{ env.java-version }}-gradle- - name: Prepare environment env: GPG_KEY_CONTENTS: ${{ secrets.GPG_KEY_CONTENTS }} SIGNING_SECRET_KEY_RING_FILE: ${{ secrets.GPG_SIGNING_SECRET_KEY_RING_FILE }} run: sudo bash -c "echo '$GPG_KEY_CONTENTS' | base64 -d > '$SIGNING_SECRET_KEY_RING_FILE'" - name: Publish release env: SIGNING_KEY_ID: ${{ secrets.GPG_SIGNING_KEY_ID }} SIGNING_PASSWORD: ${{ secrets.GPG_SIGNING_PASSWORD }} SIGNING_SECRET_KEY_RING_FILE: ${{ secrets.GPG_SIGNING_SECRET_KEY_RING_FILE }} OSS_USER_TOKEN_KEY: ${{ secrets.OSS_USER_TOKEN_KEY }} OSS_USER_TOKEN_PASS: ${{ secrets.OSS_USER_TOKEN_PASS }} run: .github/release.sh test-javax: name: Test run javax needs: validation runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Setup Java uses: actions/setup-java@v4 with: distribution: 'zulu' java-version: 11 - name: Cache Gradle uses: actions/cache@v4 env: java-version: 11 with: path: | ~/.gradle/caches ~/.gradle/wrapper key: ${{ runner.os }}-${{ env.java-version }}-gradle-${{ hashFiles('**/*.gradle*') }} restore-keys: | ${{ runner.os }}-${{ env.java-version }}-gradle- - name: Make gradlew executable run: chmod +x ./gradlew - name: Replace jakarta with javax run: .github/replaceJakartaWithJavax.sh - name: Gradle Check run: ./gradlew --info check build-javax: name: Publish release javax needs: test-javax runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Setup Java uses: actions/setup-java@v4 with: distribution: 'zulu' java-version: 11 - name: Cache Gradle uses: actions/cache@v4 env: java-version: 11 with: path: | ~/.gradle/caches ~/.gradle/wrapper key: ${{ runner.os }}-${{ env.java-version }}-gradle-${{ hashFiles('**/*.gradle*') }} restore-keys: | ${{ runner.os }}-${{ env.java-version }}-gradle- - name: Prepare environment env: GPG_KEY_CONTENTS: ${{ secrets.GPG_KEY_CONTENTS }} SIGNING_SECRET_KEY_RING_FILE: ${{ secrets.GPG_SIGNING_SECRET_KEY_RING_FILE }} run: sudo bash -c "echo '$GPG_KEY_CONTENTS' | base64 -d > '$SIGNING_SECRET_KEY_RING_FILE'" - name: Replace jakarta with javax run: .github/replaceJakartaWithJavax.sh - name: Publish release env: SIGNING_KEY_ID: ${{ secrets.GPG_SIGNING_KEY_ID }} SIGNING_PASSWORD: ${{ secrets.GPG_SIGNING_PASSWORD }} SIGNING_SECRET_KEY_RING_FILE: ${{ secrets.GPG_SIGNING_SECRET_KEY_RING_FILE }} OSS_USER_TOKEN_KEY: ${{ secrets.OSS_USER_TOKEN_KEY }} OSS_USER_TOKEN_PASS: ${{ secrets.OSS_USER_TOKEN_PASS }} run: .github/release.sh javax tag: name: Tag release needs: [ build-jakarta, build-javax ] runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Setup Java uses: actions/setup-java@v4 with: distribution: 'zulu' java-version: 17 - name: Cache Gradle uses: actions/cache@v4 env: java-version: 17 with: path: | ~/.gradle/caches ~/.gradle/wrapper key: ${{ runner.os }}-${{ env.java-version }}-gradle-${{ hashFiles('**/*.gradle*') }} restore-keys: | ${{ runner.os }}-${{ env.java-version }}-gradle- - name: Tag release run: .github/tag-release.sh ================================================ FILE: .github/workflows/snapshot.yml ================================================ name: "Publish snapshot" on: push: branches: - master jobs: validation: name: Gradle Wrapper Validation runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: gradle/wrapper-validation-action@v3 test-jakarta: name: Test run jakarta needs: validation runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Setup Java uses: actions/setup-java@v4 with: distribution: 'zulu' java-version: 17 - name: Cache Gradle uses: actions/cache@v4 env: java-version: 17 with: path: | ~/.gradle/caches ~/.gradle/wrapper key: ${{ runner.os }}-${{ env.java-version }}-gradle-${{ hashFiles('**/*.gradle*') }} restore-keys: | ${{ runner.os }}-${{ env.java-version }}-gradle- - name: Make gradlew executable run: chmod +x ./gradlew - name: Gradle Check run: ./gradlew --info check build-jakarta: name: Publish snapshot jakarta needs: test-jakarta runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Setup Java uses: actions/setup-java@v4 with: distribution: 'zulu' java-version: 17 - name: Cache Gradle uses: actions/cache@v4 env: java-version: 17 with: path: | ~/.gradle/caches ~/.gradle/wrapper key: ${{ runner.os }}-${{ env.java-version }}-gradle-${{ hashFiles('**/*.gradle*') }} restore-keys: | ${{ runner.os }}-${{ env.java-version }}-gradle- - name: Make gradlew executable run: chmod +x ./gradlew - name: Gradle Publish Snapshot if: env.OSS_USER_TOKEN_KEY != null env: OSS_USER_TOKEN_KEY: ${{ secrets.OSS_USER_TOKEN_KEY }} OSS_USER_TOKEN_PASS: ${{ secrets.OSS_USER_TOKEN_PASS }} run: ./gradlew clean build publish -x test test-javax: name: Test run javax needs: validation runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Setup Java uses: actions/setup-java@v4 with: distribution: 'zulu' java-version: 11 - name: Cache Gradle uses: actions/cache@v4 env: java-version: 11 with: path: | ~/.gradle/caches ~/.gradle/wrapper key: ${{ runner.os }}-${{ env.java-version }}-gradle-${{ hashFiles('**/*.gradle*') }} restore-keys: | ${{ runner.os }}-${{ env.java-version }}-gradle- - name: Make gradlew executable run: chmod +x ./gradlew - name: Replace jakarta with javax run: .github/replaceJakartaWithJavax.sh - name: Gradle Check run: ./gradlew --info check build-javax: name: Publish snapshot javax needs: test-javax runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Setup Java uses: actions/setup-java@v4 with: distribution: 'zulu' java-version: 11 - name: Cache Gradle uses: actions/cache@v4 env: java-version: 11 with: path: | ~/.gradle/caches ~/.gradle/wrapper key: ${{ runner.os }}-${{ env.java-version }}-gradle-${{ hashFiles('**/*.gradle*') }} restore-keys: | ${{ runner.os }}-${{ env.java-version }}-gradle- - name: Make gradlew executable run: chmod +x ./gradlew - name: Replace jakarta with javax run: .github/replaceJakartaWithJavax.sh - name: Add suffix to modules run: .github/add-javax-suffix.sh - name: Gradle Publish Snapshot if: env.OSS_USER_TOKEN_KEY != null env: OSS_USER_TOKEN_KEY: ${{ secrets.OSS_USER_TOKEN_KEY }} OSS_USER_TOKEN_PASS: ${{ secrets.OSS_USER_TOKEN_PASS }} run: ./gradlew clean build publish -x test sonar: name: Sonar analysis needs: validation runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis - name: Set up JDK 17 uses: actions/setup-java@v4 with: distribution: 'zulu' java-version: 17 - name: Cache SonarCloud packages uses: actions/cache@v4 with: path: ~/.sonar/cache key: ${{ runner.os }}-sonar restore-keys: ${{ runner.os }}-sonar - name: Cache Gradle packages uses: actions/cache@v4 with: path: ~/.gradle/caches key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }} restore-keys: ${{ runner.os }}-gradle - name: Build and analyze env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} if: env.SONAR_TOKEN != null run: ./gradlew build jacocoTestReport sonarqube --info ================================================ FILE: .gitignore ================================================ .gradle/ build/ *.iml *.ipr *.iws .idea/* !.idea/codeStyles/ target/ /out/ .classpath .project .settings bin .DS_Store /**/out/ /projectFilesBackup/.idea/workspace.xml ================================================ FILE: CONTRIBUTING.md ================================================ # How to contribute We're really glad you're reading this, because we need more volunteer developers to help with this project! We can use all the help we can get on all our [GraphQL Java Kickstart](https://github.com/graphql-java-kickstart) projects. This work ranges from adding new features, fixing bugs, and answering questions to writing documentation. ## Answering questions and writing documentation A lot of the questions asked on Spectrum or Github are caused by a lack of documentation. We should strive from now on to answer questions by adding content to our [documentation](https://github.com/graphql-java-kickstart/documentation) and referring them to the newly created content. Continuous integration will make sure that the changes are automatically deployed to https://www.graphql-java-kickstart.com. ## Submitting changes Please send a Pull Request with a clear list of what you've done using the [Github flow](https://guides.github.com/introduction/flow/). We can always use more test coverage, so we'd love to see that in the pull requests too. And make sure to follow our coding conventions (below) and make sure all your commits are atomic (one feature per commit). ## Coding conventions We use Google Style guides for our projects. See the [Java Style Guide](https://google.github.io/styleguide/javaguide.html) for a detailed description. You can download the [IntelliJ Java Google Style](https://github.com/google/styleguide/blob/gh-pages/intellij-java-google-style.xml) to import in these settings in IntelliJ. These conventions are checked during the build phase. If the build fails because the code is not using the correct style you can fix this easily by running a gradle task ```bash ./gradlew googleJavaFormat ``` ### SonarLint It would also be very helpful to install the SonarLint plugin in your IDE and fix any relevant SonarLint issues before pushing a PR. We're aware that the current state of the code raises a lot of SonarLint issues out of the box, but any help in reducing that is appreciated. More importantly we don't increase that technical debt. ================================================ FILE: LICENSE ================================================ Copyright 2016 Yurii Rashkovskii and Contributors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and ================================================ FILE: README.md ================================================ # GraphQL Java Servlet [![Maven Central](https://img.shields.io/maven-central/v/com.graphql-java-kickstart/graphql-java-servlet.svg)](https://maven-badges.herokuapp.com/maven-central/com.graphql-java-kickstart/graphql-java-servlet) [![Build Status](https://github.com/graphql-java-kickstart/graphql-java-servlet/workflows/Publish%20snapshot/badge.svg)](https://github.com/graphql-java-kickstart/graphql-java-servlet/actions?query=workflow%3A%22Publish+snapshot%22) [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=graphql-java-kickstart_graphql-java-servlet&metric=alert_status)](https://sonarcloud.io/dashboard?id=graphql-java-kickstart_graphql-java-servlet) [![GitHub contributors](https://img.shields.io/github/contributors/graphql-java-kickstart/graphql-java-servlet)](https://github.com/graphql-java-kickstart/graphql-java-servlet/graphs/contributors) [![Discuss on GitHub](https://img.shields.io/badge/GitHub-discuss-orange)](https://github.com/graphql-java-kickstart/graphql-java-servlet/discussions) ## We are looking for contributors! Are you interested in improving our documentation, working on the codebase, reviewing PRs? [Reach out to us on Discussions](https://github.com/graphql-java-kickstart/graphql-java-servlet/discussions) and join the team! We hope you'll get involved! Read our [Contributors' Guide](CONTRIBUTING.md) for more details. ## Overview Implementation of GraphQL Java Servlet including support for Relay.js, Apollo and OSGi out of the box. This project wraps the Java implementation of GraphQL provided by [GraphQL Java](https://www.graphql-java.com). See [GraphQL Java documentation](https://www.graphql-java.com/documentation/latest/) for more in depth details regarding GraphQL Java itself. We try to stay up to date with GraphQL Java as much as possible maintaining the retro-compatibility with javax and Springframework 5. On each release we publish three flavours of this project: - [latest jakarta](#jakarta-and-springframework-6) - [jakarta5](#jakarta5) - [javax](#javax-and-springframework-5) All of them also supports legacy projects that can compile with older JDK versions: the minimum JDK version supported is the `11`. ## Jakarta and Springframework 6.* This is the main flavour using the latest version of `Jakarta` (currently the `6.*`) and the latest version of `Springframework` (currently the `6.*`). All the codebase can be found in the branch: `master` ```xml com.graphql-java-kickstart graphql-java-servlet ${graphql-java-servlet.version} ``` ## Jakarta5 This flavour use the `jakarta` version `5.*` and it is meant to be used for all the projects that are already migrated to jakarta, but they are waiting that `jakarta6` will become more broadly used. All the codebase can be found in the branch: `jakarta5` ```xml com.graphql-java-kickstart graphql-java-servlet-jakarta5 ${graphql-java-servlet-jakarta5.version} ``` ## Javax and Springframework 5.* This is the legacy flavour using the `javax` dependency and the version `5.*` of `Springframework` (since it is still broadly used by a lot of projects). All the codebase can be found in the branch: `master` ```xml com.graphql-java-kickstart graphql-java-servlet-javax ${graphql-java-servlet.version} ``` ## Installation and getting started See [Getting started](https://graphql-java-kickstart.github.io/servlet/getting-started/) for more detailed instructions. ================================================ FILE: build.gradle ================================================ /* * The MIT License (MIT) * * Copyright (c) 2016 oEmbedler Inc. and Contributors * * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit * persons to whom the Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING * BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ buildscript { repositories { mavenLocal() mavenCentral() maven { url "https://plugins.gradle.org/m2/" } maven { url 'https://repo.spring.io/plugins-release' } } } plugins { id "biz.aQute.bnd.builder" version "6.4.0" apply false id "org.sonarqube" version "5.1.0.4882" id "jacoco" id "io.github.gradle-nexus.publish-plugin" version '2.0.0' } sonarqube { properties { property "sonar.projectKey", "graphql-java-kickstart_graphql-java-servlet" property "sonar.organization", "graphql-java-kickstart" property "sonar.host.url", "https://sonarcloud.io" } } subprojects { apply plugin: 'idea' apply plugin: 'jacoco' apply plugin: 'org.sonarqube' apply plugin: 'java' apply plugin: 'maven-publish' apply plugin: 'signing' repositories { mavenLocal() mavenCentral() maven { url "https://oss.sonatype.org/content/repositories/snapshots" } maven { url "https://repo.spring.io/libs-milestone" } } dependencies { compileOnly "org.projectlombok:lombok:$LIB_LOMBOK_VER" annotationProcessor "org.projectlombok:lombok:$LIB_LOMBOK_VER" testCompileOnly "org.projectlombok:lombok:$LIB_LOMBOK_VER" testAnnotationProcessor "org.projectlombok:lombok:$LIB_LOMBOK_VER" } idea { module { downloadJavadoc = true downloadSources = true } } compileJava { sourceCompatibility = SOURCE_COMPATIBILITY targetCompatibility = TARGET_COMPATIBILITY } compileTestJava { sourceCompatibility = SOURCE_COMPATIBILITY_TEST targetCompatibility = TARGET_COMPATIBILITY_TEST } compileJava.dependsOn(processResources) test { useJUnitPlatform() afterSuite { desc, result -> if (!desc.parent) { if (result.testCount == 0) { throw new IllegalStateException("No tests were found. Failing the build") } } } } jacocoTestReport { reports { xml.required = true html.required = false csv.required = false } } if (!it.name.startsWith('example')) { jar { from "LICENSE.md" } java { withSourcesJar() withJavadocJar() } if (!version.toString().endsWith('-SNAPSHOT')) { ext["signing.keyId"] = System.env.SIGNING_KEY_ID ext["signing.password"] = System.env.SIGNING_PASSWORD ext["signing.secretKeyRingFile"] = System.env.SIGNING_SECRET_KEY_RING_FILE signing { sign publishing.publications } } publishing { publications { mavenJava(MavenPublication) { version version from components.java versionMapping { usage('java-api') { fromResolutionOf('runtimeClasspath') } usage('java-runtime') { fromResolutionResult() } } pom { name = PROJECT_NAME description = 'relay.js-compatible GraphQL servlet' url = 'https://github.com/graphql-java-kickstart/graphql-java-servlet' inceptionYear = '2016' scm { url = 'https://github.com/graphql-java-kickstart/graphql-java-servlet' connection = 'scm:https://github.com/graphql-java-kickstart/graphql-java-servlet.git' developerConnection = 'scm:git://github.com/graphql-java-kickstart/graphql-java-servlet.git' } licenses { license { name = 'The Apache Software License, Version 2.0' url = 'https://www.apache.org/licenses/LICENSE-2.0.txt' distribution = 'repo' } } developers { developer { id = 'oliemansm' name = 'Michiel Oliemans' } developer { id = 'yrashk' name = 'Yurii Rashkovskii' email = 'yrashk@gmail.com' } developer { id = 'apottere' name = 'Andrew Potter' email = 'apottere@gmail.com' } } } } } } } } nexusPublishing { repositories { sonatype { username = System.env.OSS_USER_TOKEN_KEY ?: project.findProperty('OSS_USER_TOKEN_KEY') ?: '' password = System.env.OSS_USER_TOKEN_PASS ?: project.findProperty('OSS_USER_TOKEN_PASS') ?: '' } } } wrapper { distributionType = Wrapper.DistributionType.ALL } ================================================ FILE: examples/osgi/apache-karaf-feature/pom.xml ================================================ graphql-java-servlet-osgi-examples-karaf-feature karaf-maven-plugin true true 80 true org.apache.karaf.tooling 4.2.10 maven-install-plugin org.apache.maven.plugins 2.5.2 maven-deploy-plugin org.apache.maven.plugins 2.8.2 jackson-core com.fasterxml.jackson.core ${jackson.version} jackson-annotations com.fasterxml.jackson.core ${jackson.version} jackson-databind com.fasterxml.jackson.core ${jackson.version} jackson-datatype-jdk8 com.fasterxml.jackson.datatype ${jackson.version} guava com.google.guava [24.1.1,) commons-fileupload commons-fileupload [1.3.3,) antlr4-runtime org.antlr 4.7.1 graphql-java-servlet com.graphql-java-kickstart ${graphql.java.servlet.version} graphql-java com.graphql-java ${graphql.java.version} graphql-java-annotations com.graphql-java 0.13.1 graphql-java-servlet-osgi-examples-providers com.graphql-java-kickstart ${project.version} slf4j-api org.slf4j 1.7.25 slf4j-log4j12 org.slf4j 1.7.25 4.0.0 feature graphql-java-servlet-osgi-examples com.graphql-java-kickstart 10.1.0 2.13.4.2 ================================================ FILE: examples/osgi/apache-karaf-feature/src/main/feature/feature.xml ================================================ scr war http ================================================ FILE: examples/osgi/apache-karaf-package/pom.xml ================================================ graphql-java-servlet-osgi-examples-apache-karaf-package maven-resources-plugin resources process-resources org.apache.maven.plugins 2.6 karaf-maven-plugin minimal wrapper graphql-java-servlet-osgi-examples-karaf-feature true org.apache.karaf.tooling ${karaf.version} maven-install-plugin org.apache.maven.plugins 2.5.2 maven-deploy-plugin org.apache.maven.plugins 2.8.2 src/main/resources false **/* src/main/filtered-resources true **/* framework org.apache.karaf.features kar ${karaf.version} standard features org.apache.karaf.features runtime xml ${karaf.version} enterprise features org.apache.karaf.features runtime xml ${karaf.version} graphql-java-servlet-osgi-examples-karaf-feature features com.graphql-java-kickstart runtime xml ${project.version} 4.0.0 karaf-assembly graphql-java-servlet-osgi-examples com.graphql-java-kickstart 10.1.0 ================================================ FILE: examples/osgi/buildAndRun.sh ================================================ #!/usr/bin/env bash mvn clean install pushd apache-karaf-package/target || exit 1 tar zxvf graphql-java-servlet-osgi-examples-apache-karaf-package-10.1.0.tar.gz cd graphql-java-servlet-osgi-examples-apache-karaf-package-10.1.0/bin || exit 1 ./karaf debug popd || exit 1 ================================================ FILE: examples/osgi/pom.xml ================================================ graphql-java-servlet-osgi-examples com.graphql-java-kickstart 4.0.0 providers apache-karaf-feature apache-karaf-package pom 11.0.0-SNAPSHOT 16.1 4.2.10 11 11 10.1.0 ================================================ FILE: examples/osgi/providers/pom.xml ================================================ graphql-java-servlet-osgi-examples-providers maven-bundle-plugin true org.apache.felix 3.3.0 graphql-java-servlet com.graphql-java-kickstart provided ${graphql.java.servlet.version} graphql-java com.graphql-java provided ${graphql.java.version} osgi.enterprise org.osgi provided 6.0.0 org.apache.felix.scr.ds-annotations org.apache.felix provided 1.2.4 4.0.0 bundle graphql-java-servlet-osgi-examples com.graphql-java-kickstart 10.1.0 ================================================ FILE: examples/osgi/providers/src/main/java/graphql/servlet/examples/osgi/ExampleGraphQLProvider.java ================================================ package graphql.servlet.examples.osgi; import graphql.schema.GraphQLFieldDefinition; import graphql.schema.GraphQLType; import graphql.kickstart.servlet.osgi.GraphQLMutationProvider; import graphql.kickstart.servlet.osgi.GraphQLQueryProvider; import graphql.kickstart.servlet.osgi.GraphQLTypesProvider; import org.osgi.service.component.annotations.Component; import java.util.ArrayList; import java.util.Collection; import java.util.List; import static graphql.Scalars.GraphQLString; import static graphql.schema.GraphQLFieldDefinition.*; @Component( name = "ExampleGraphQLProvider", immediate = true ) public class ExampleGraphQLProvider implements GraphQLQueryProvider, GraphQLMutationProvider, GraphQLTypesProvider { public Collection getQueries() { List fieldDefinitions = new ArrayList(); fieldDefinitions.add(newFieldDefinition() .type(GraphQLString) .name("hello") .description( "Basic example of a GraphQL Java Servlet provider using the Apache Karaf OSGi Runtime") .staticValue("world") .build()); return fieldDefinitions; } public Collection getMutations() { return new ArrayList(); } public Collection getTypes() { return new ArrayList(); } } ================================================ FILE: gradle/wrapper/gradle-wrapper.properties ================================================ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists ================================================ FILE: gradle.properties ================================================ version=16.0.0 group=com.graphql-java-kickstart PROJECT_NAME=graphql-java-servlet PROJECT_DESC=GraphQL Java Kickstart PROJECT_GIT_REPO_URL=https://github.com/graphql-java-kickstart/graphql-java-servlet PROJECT_LICENSE=MIT PROJECT_LICENSE_URL=https://github.com/graphql-java-kickstart/spring-java-servlet/blob/master/LICENSE.md PROJECT_DEV_ID=oliemansm PROJECT_DEV_NAME=Michiel Oliemans LIB_GRAPHQL_JAVA_VER=22.3 LIB_JACKSON_VER=2.17.2 LIB_SLF4J_VER=2.0.16 LIB_LOMBOK_VER=1.18.34 # These constants are necessary to the automatic release of javax flavour LIB_JAVAX_SERVLET=4.0.1 LIB_JAVAX_WEBSOCKET=1.1 LIB_SPRINGFRAMEWORK_5=5.3.25 SOURCE_COMPATIBILITY=11 TARGET_COMPATIBILITY=11 SOURCE_COMPATIBILITY_TEST=17 TARGET_COMPATIBILITY_TEST=17 ================================================ FILE: gradlew ================================================ #!/bin/sh # # Copyright © 2015-2021 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # SPDX-License-Identifier: Apache-2.0 # ############################################################################## # # Gradle start up script for POSIX generated by Gradle. # # Important for running: # # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is # noncompliant, but you have some other compliant shell such as ksh or # bash, then to run this script, type that shell name before the whole # command line, like: # # ksh Gradle # # Busybox and similar reduced shells will NOT work, because this script # requires all of these POSIX shell features: # * functions; # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», # «${var#prefix}», «${var%suffix}», and «$( cmd )»; # * compound commands having a testable exit status, especially «case»; # * various built-in commands including «command», «set», and «ulimit». # # Important for patching: # # (2) This script targets any POSIX shell, so it avoids extensions provided # by Bash, Ksh, etc; in particular arrays are avoided. # # The "traditional" practice of packing multiple parameters into a # space-separated string is a well documented source of bugs and security # problems, so this is (mostly) avoided, by progressively accumulating # options in "$@", and eventually passing that to Java. # # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; # see the in-line comments for details. # # There are tweaks for specific operating systems such as AIX, CygWin, # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. # ############################################################################## # Attempt to set APP_HOME # Resolve links: $0 may be a link app_path=$0 # Need this for daisy-chained symlinks. while APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path [ -h "$app_path" ] do ls=$( ls -ld "$app_path" ) link=${ls#*' -> '} case $link in #( /*) app_path=$link ;; #( *) app_path=$APP_HOME$link ;; esac done # This is normally unused # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s ' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum warn () { echo "$*" } >&2 die () { echo echo "$*" echo exit 1 } >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false case "$( uname )" in #( CYGWIN* ) cygwin=true ;; #( Darwin* ) darwin=true ;; #( MSYS* | MINGW* ) msys=true ;; #( NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD=$JAVA_HOME/jre/sh/java else JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else JAVACMD=java if ! command -v java >/dev/null 2>&1 then die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi fi # Increase the maximum file descriptors if we can. if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac case $MAX_FD in #( '' | soft) :;; #( *) # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac fi # Collect all arguments for the java command, stacking in reverse order: # * args from the command line # * the main class name # * -classpath # * -D...appname settings # * --module-path (only if needed) # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. # For Cygwin or MSYS, switch paths to Windows format before running java if "$cygwin" || "$msys" ; then APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) JAVACMD=$( cygpath --unix "$JAVACMD" ) # Now convert the arguments - kludge to limit ourselves to /bin/sh for arg do if case $arg in #( -*) false ;; # don't mess with options #( /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath [ -e "$t" ] ;; #( *) false ;; esac then arg=$( cygpath --path --ignore --mixed "$arg" ) fi # Roll the args list around exactly as many times as the number of # args, so each arg winds up back in the position where it started, but # possibly modified. # # NB: a `for` loop captures its iteration list before it begins, so # changing the positional parameters here affects neither the number of # iterations, nor the values presented in `arg`. shift # remove old arg set -- "$@" "$arg" # push replacement arg done fi # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Collect all arguments for the java command: # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, # and any embedded shellness will be escaped. # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be # treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ -classpath "$CLASSPATH" \ org.gradle.wrapper.GradleWrapperMain \ "$@" # Stop when "xargs" is not available. if ! command -v xargs >/dev/null 2>&1 then die "xargs is not available" fi # Use "xargs" to parse quoted args. # # With -n1 it outputs one arg per line, with the quotes and backslashes removed. # # In Bash we could simply go: # # readarray ARGS < <( xargs -n1 <<<"$var" ) && # set -- "${ARGS[@]}" "$@" # # but POSIX shell has neither arrays nor command substitution, so instead we # post-process each arg (as a line of input to sed) to backslash-escape any # character that might be a shell metacharacter, then use eval to reverse # that process (while maintaining the separation between arguments), and wrap # the whole thing up as a single "set" statement. # # This will of course break if any of these variables contains a newline or # an unmatched quote. # eval "set -- $( printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | xargs -n1 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | tr '\n' ' ' )" '"$@"' exec "$JAVACMD" "$@" ================================================ FILE: gradlew.bat ================================================ @rem @rem Copyright 2015 the original author or authors. @rem @rem Licensed under the Apache License, Version 2.0 (the "License"); @rem you may not use this file except in compliance with the License. @rem You may obtain a copy of the License at @rem @rem https://www.apache.org/licenses/LICENSE-2.0 @rem @rem Unless required by applicable law or agreed to in writing, software @rem distributed under the License is distributed on an "AS IS" BASIS, @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem @rem SPDX-License-Identifier: Apache-2.0 @rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @rem @rem ########################################################################## @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 if "%DIRNAME%"=="" set DIRNAME=. @rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @rem Resolve any "." and ".." in APP_HOME to make it shorter. for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute echo. 1>&2 echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 echo. 1>&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo location of your Java installation. 1>&2 goto fail :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute echo. 1>&2 echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 echo. 1>&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo location of your Java installation. 1>&2 goto fail :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar @rem Execute Gradle "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* :end @rem End local scope for the variables with windows NT shell if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! set EXIT_CODE=%ERRORLEVEL% if %EXIT_CODE% equ 0 set EXIT_CODE=1 if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal :omega ================================================ FILE: graphql-java-kickstart/bnd.bnd ================================================ Export-Package: graphql.kickstart.* Import-Package: !lombok,* ================================================ FILE: graphql-java-kickstart/build.gradle ================================================ apply plugin: 'biz.aQute.bnd.builder' jar { bndfile = 'bnd.bnd' } apply plugin: 'java-library-distribution' dependencies { // GraphQL api "com.graphql-java:graphql-java:$LIB_GRAPHQL_JAVA_VER" implementation "org.slf4j:slf4j-api:$LIB_SLF4J_VER" // JSON api "com.fasterxml.jackson.core:jackson-core:$LIB_JACKSON_VER" api "com.fasterxml.jackson.core:jackson-annotations:$LIB_JACKSON_VER" api "com.fasterxml.jackson.core:jackson-databind:2.17.2" api "com.fasterxml.jackson.datatype:jackson-datatype-jdk8:$LIB_JACKSON_VER" } ================================================ FILE: graphql-java-kickstart/src/main/java/graphql/kickstart/execution/BatchedDataLoaderGraphQLBuilder.java ================================================ package graphql.kickstart.execution; import graphql.GraphQL; import graphql.kickstart.execution.config.GraphQLBuilder; import graphql.kickstart.execution.input.GraphQLBatchedInvocationInput; import graphql.kickstart.execution.input.GraphQLSingleInvocationInput; public class BatchedDataLoaderGraphQLBuilder { GraphQL newGraphQL(GraphQLBatchedInvocationInput invocationInput, GraphQLBuilder graphQLBuilder) { return invocationInput.getInvocationInputs().stream() .findFirst() .map(GraphQLSingleInvocationInput::getSchema) .map(schema -> graphQLBuilder.build(schema, graphQLBuilder.getInstrumentationSupplier())) .orElseThrow( () -> new IllegalArgumentException( "Batched invocation input must contain at least one query")); } } ================================================ FILE: graphql-java-kickstart/src/main/java/graphql/kickstart/execution/DecoratedExecutionResult.java ================================================ package graphql.kickstart.execution; import graphql.ExecutionResult; import graphql.GraphQLError; import java.util.List; import java.util.Map; import lombok.RequiredArgsConstructor; import org.reactivestreams.Publisher; @RequiredArgsConstructor class DecoratedExecutionResult implements ExecutionResult { private final ExecutionResult result; boolean isAsynchronous() { return result.getData() instanceof Publisher; } @Override public List getErrors() { return result.getErrors(); } @Override public T getData() { return result.getData(); } @Override public boolean isDataPresent() { return result.isDataPresent(); } @Override public Map getExtensions() { return result.getExtensions(); } @Override public Map toSpecification() { return result.toSpecification(); } } ================================================ FILE: graphql-java-kickstart/src/main/java/graphql/kickstart/execution/DefaultGraphQLRootObjectBuilder.java ================================================ package graphql.kickstart.execution; public class DefaultGraphQLRootObjectBuilder extends StaticGraphQLRootObjectBuilder { public DefaultGraphQLRootObjectBuilder() { super(new Object()); } } ================================================ FILE: graphql-java-kickstart/src/main/java/graphql/kickstart/execution/ExtensionsDeserializer.java ================================================ package graphql.kickstart.execution; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.ObjectCodec; import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.JsonDeserializer; import java.io.IOException; import java.util.Map; public class ExtensionsDeserializer extends JsonDeserializer> { public static Map deserializeExtensionsObject( Object extensions, ObjectCodec codec) { return ObjectMapDeserializeHelper.deserializeObjectMapObject(extensions, codec, "extensions"); } @Override public Map deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { return ExtensionsDeserializer.deserializeExtensionsObject( p.readValueAs(Object.class), p.getCodec()); } } ================================================ FILE: graphql-java-kickstart/src/main/java/graphql/kickstart/execution/FutureBatchedExecutionResult.java ================================================ package graphql.kickstart.execution; import graphql.ExecutionResult; import graphql.kickstart.execution.input.GraphQLInvocationInput; import java.util.List; import java.util.concurrent.CompletableFuture; import lombok.Getter; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor class FutureBatchedExecutionResult implements FutureExecutionResult { @Getter private final GraphQLInvocationInput invocationInput; private final CompletableFuture> batched; @Override public CompletableFuture thenApplyQueryResult() { return batched.thenApply(GraphQLQueryResult::create); } @Override public void cancel() { batched.cancel(true); } } ================================================ FILE: graphql-java-kickstart/src/main/java/graphql/kickstart/execution/FutureErrorExecutionResult.java ================================================ package graphql.kickstart.execution; import graphql.kickstart.execution.input.GraphQLInvocationInput; import java.util.concurrent.CompletableFuture; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor class FutureErrorExecutionResult implements FutureExecutionResult { private final GraphQLErrorQueryResult errorQueryResult; @Override public CompletableFuture thenApplyQueryResult() { return CompletableFuture.completedFuture(errorQueryResult); } @Override public GraphQLInvocationInput getInvocationInput() { return null; } @Override public void cancel() { // nothing to do } } ================================================ FILE: graphql-java-kickstart/src/main/java/graphql/kickstart/execution/FutureExecutionResult.java ================================================ package graphql.kickstart.execution; import graphql.ExecutionResult; import graphql.kickstart.execution.input.GraphQLInvocationInput; import java.util.List; import java.util.concurrent.CompletableFuture; public interface FutureExecutionResult { static FutureExecutionResult single( GraphQLInvocationInput invocationInput, CompletableFuture single) { return new FutureSingleExecutionResult(invocationInput, single); } static FutureExecutionResult batched( GraphQLInvocationInput invocationInput, CompletableFuture> batched) { return new FutureBatchedExecutionResult(invocationInput, batched); } static FutureExecutionResult error(GraphQLErrorQueryResult result) { return new FutureErrorExecutionResult(result); } CompletableFuture thenApplyQueryResult(); GraphQLInvocationInput getInvocationInput(); void cancel(); } ================================================ FILE: graphql-java-kickstart/src/main/java/graphql/kickstart/execution/FutureSingleExecutionResult.java ================================================ package graphql.kickstart.execution; import graphql.ExecutionResult; import graphql.kickstart.execution.input.GraphQLInvocationInput; import java.util.concurrent.CompletableFuture; import lombok.Getter; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor class FutureSingleExecutionResult implements FutureExecutionResult { @Getter private final GraphQLInvocationInput invocationInput; private final CompletableFuture single; @Override public CompletableFuture thenApplyQueryResult() { return single.thenApply(GraphQLQueryResult::create); } @Override public void cancel() { single.cancel(true); } } ================================================ FILE: graphql-java-kickstart/src/main/java/graphql/kickstart/execution/GraphQLBatchedQueryResult.java ================================================ package graphql.kickstart.execution; import graphql.ExecutionResult; import java.util.List; import lombok.Getter; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor class GraphQLBatchedQueryResult implements GraphQLQueryResult { @Getter private final List results; @Override public boolean isBatched() { return true; } @Override public boolean isAsynchronous() { return false; } } ================================================ FILE: graphql-java-kickstart/src/main/java/graphql/kickstart/execution/GraphQLErrorQueryResult.java ================================================ package graphql.kickstart.execution; import lombok.Getter; import lombok.RequiredArgsConstructor; @Getter @RequiredArgsConstructor class GraphQLErrorQueryResult implements GraphQLQueryResult { private final int statusCode; private final String message; @Override public boolean isBatched() { return false; } @Override public boolean isAsynchronous() { return false; } @Override public boolean isError() { return true; } } ================================================ FILE: graphql-java-kickstart/src/main/java/graphql/kickstart/execution/GraphQLInvoker.java ================================================ package graphql.kickstart.execution; import static java.util.stream.Collectors.toList; import graphql.ExecutionResult; import graphql.GraphQL; import graphql.kickstart.execution.config.GraphQLBuilder; import graphql.kickstart.execution.input.GraphQLBatchedInvocationInput; import graphql.kickstart.execution.input.GraphQLInvocationInput; import graphql.kickstart.execution.input.GraphQLSingleInvocationInput; import java.util.ArrayList; import java.util.List; import java.util.concurrent.CompletableFuture; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @Slf4j @RequiredArgsConstructor public class GraphQLInvoker { private final GraphQLBuilder graphQLBuilder; private final BatchedDataLoaderGraphQLBuilder batchedDataLoaderGraphQLBuilder; private final GraphQLInvokerProxy proxy = GraphQL::executeAsync; public FutureExecutionResult execute(GraphQLInvocationInput invocationInput) { if (invocationInput instanceof GraphQLSingleInvocationInput) { return FutureExecutionResult.single( invocationInput, executeAsync((GraphQLSingleInvocationInput) invocationInput)); } return FutureExecutionResult.batched( invocationInput, executeAsync((GraphQLBatchedInvocationInput) invocationInput)); } public CompletableFuture executeAsync( GraphQLSingleInvocationInput invocationInput) { GraphQL graphQL = graphQLBuilder.build(invocationInput.getSchema()); return proxy.executeAsync(graphQL, invocationInput.getExecutionInput()); } public GraphQLQueryResult query(GraphQLInvocationInput invocationInput) { return queryAsync(invocationInput).join(); } public CompletableFuture queryAsync(GraphQLInvocationInput invocationInput) { if (invocationInput instanceof GraphQLSingleInvocationInput) { return executeAsync((GraphQLSingleInvocationInput) invocationInput) .thenApply(GraphQLQueryResult::create); } GraphQLBatchedInvocationInput batchedInvocationInput = (GraphQLBatchedInvocationInput) invocationInput; return executeAsync(batchedInvocationInput).thenApply(GraphQLQueryResult::create); } private CompletableFuture> executeAsync( GraphQLBatchedInvocationInput batchedInvocationInput) { GraphQL graphQL = batchedDataLoaderGraphQLBuilder.newGraphQL(batchedInvocationInput, graphQLBuilder); return sequence( batchedInvocationInput.getExecutionInputs().stream() .map(executionInput -> proxy.executeAsync(graphQL, executionInput)) .collect(toList())); } @SuppressWarnings({"unchecked", "rawtypes"}) private CompletableFuture> sequence(List> futures) { CompletableFuture[] futuresArray = futures.toArray(new CompletableFuture[0]); return CompletableFuture.allOf(futuresArray) .thenApply( aVoid -> { List result = new ArrayList<>(futures.size()); for (CompletableFuture future : futuresArray) { assert future.isDone(); // per the API contract of allOf() result.add((T) future.join()); } return result; }); } } ================================================ FILE: graphql-java-kickstart/src/main/java/graphql/kickstart/execution/GraphQLInvokerProxy.java ================================================ package graphql.kickstart.execution; import graphql.ExecutionInput; import graphql.ExecutionResult; import graphql.GraphQL; import java.util.concurrent.CompletableFuture; public interface GraphQLInvokerProxy { CompletableFuture executeAsync(GraphQL graphQL, ExecutionInput executionInput); } ================================================ FILE: graphql-java-kickstart/src/main/java/graphql/kickstart/execution/GraphQLObjectMapper.java ================================================ package graphql.kickstart.execution; import static java.util.stream.Collectors.toList; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.MappingIterator; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectReader; import graphql.ExecutionResult; import graphql.ExecutionResultImpl; import graphql.GraphQLError; import graphql.kickstart.execution.config.ConfiguringObjectMapperProvider; import graphql.kickstart.execution.config.GraphQLServletObjectMapperConfigurer; import graphql.kickstart.execution.config.ObjectMapperProvider; import graphql.kickstart.execution.error.DefaultGraphQLErrorHandler; import graphql.kickstart.execution.error.GraphQLErrorHandler; import java.io.IOException; import java.io.InputStream; import java.io.Writer; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.function.Supplier; import lombok.SneakyThrows; /** @author Andrew Potter */ public class GraphQLObjectMapper { private static final TypeReference>> MULTIPART_MAP_TYPE_REFERENCE = new TypeReference>>() {}; private final ObjectMapperProvider objectMapperProvider; private final Supplier graphQLErrorHandlerSupplier; private ObjectMapper mapper; protected GraphQLObjectMapper( ObjectMapperProvider objectMapperProvider, Supplier graphQLErrorHandlerSupplier) { this.objectMapperProvider = objectMapperProvider; this.graphQLErrorHandlerSupplier = graphQLErrorHandlerSupplier; } public static Builder newBuilder() { return new Builder(); } // Double-check idiom for lazy initialization of instance fields. public ObjectMapper getJacksonMapper() { ObjectMapper result = mapper; if (result == null) { // First check (no locking) synchronized (this) { result = mapper; if (result == null) { // Second check (with locking) mapper = result = objectMapperProvider.provide(); } } } return result; } /** @return an {@link ObjectReader} for deserializing {@link GraphQLRequest} */ public ObjectReader getGraphQLRequestMapper() { return getJacksonMapper().reader().forType(GraphQLRequest.class); } public GraphQLRequest readGraphQLRequest(InputStream inputStream) throws IOException { return getGraphQLRequestMapper().readValue(inputStream); } public GraphQLRequest readGraphQLRequest(String text) throws IOException { return getGraphQLRequestMapper().readValue(text); } public List readBatchedGraphQLRequest(InputStream inputStream) throws IOException { MappingIterator iterator = getGraphQLRequestMapper().readValues(inputStream); List requests = new ArrayList<>(); while (iterator.hasNext()) { requests.add(iterator.next()); } return requests; } public List readBatchedGraphQLRequest(String query) throws IOException { MappingIterator iterator = getGraphQLRequestMapper().readValues(query); List requests = new ArrayList<>(); while (iterator.hasNext()) { requests.add(iterator.next()); } return requests; } @SneakyThrows public String serializeResultAsJson(ExecutionResult executionResult) { return getJacksonMapper().writeValueAsString(createResultFromExecutionResult(executionResult)); } public void serializeResultAsJson(Writer writer, ExecutionResult executionResult) throws IOException { getJacksonMapper().writeValue(writer, createResultFromExecutionResult(executionResult)); } /** * Serializes result as bytes in UTF-8 encoding instead of string. * * @param executionResult query execution result to serialize. * @return result serialized into Json representation in UTF-8 encoding, converted into {@code * byte[]}. */ @SneakyThrows public byte[] serializeResultAsBytes(ExecutionResult executionResult) { return getJacksonMapper().writeValueAsBytes(createResultFromExecutionResult(executionResult)); } public boolean areErrorsPresent(ExecutionResult executionResult) { return graphQLErrorHandlerSupplier.get().errorsPresent(executionResult.getErrors()); } public ExecutionResult sanitizeErrors(ExecutionResult executionResult) { Object data = executionResult.getData(); Map extensions = executionResult.getExtensions(); List errors = executionResult.getErrors(); GraphQLErrorHandler errorHandler = graphQLErrorHandlerSupplier.get(); if (errorHandler.errorsPresent(errors)) { errors = errorHandler.processErrors(errors); } else { errors = null; } return new ExecutionResultImpl(data, errors, extensions); } public Map createResultFromExecutionResult(ExecutionResult executionResult) { ExecutionResult sanitizedExecutionResult = sanitizeErrors(executionResult); return convertSanitizedExecutionResult(sanitizedExecutionResult); } public Map convertSanitizedExecutionResult(ExecutionResult executionResult) { return convertSanitizedExecutionResult(executionResult, true); } public Map convertSanitizedExecutionResult( ExecutionResult executionResult, boolean includeData) { final Map result = new LinkedHashMap<>(); if (areErrorsPresent(executionResult)) { result.put( "errors", executionResult.getErrors().stream() .map(GraphQLError::toSpecification) .collect(toList())); } if (executionResult.getExtensions() != null && !executionResult.getExtensions().isEmpty()) { result.put("extensions", executionResult.getExtensions()); } if (includeData) { result.put("data", executionResult.getData()); } return result; } @SneakyThrows public Map deserializeVariables(String variables) { return VariablesDeserializer.deserializeVariablesObject( getJacksonMapper().readValue(variables, Object.class), getJacksonMapper()); } @SneakyThrows public Map deserializeExtensions(String extensions) { return ExtensionsDeserializer.deserializeExtensionsObject( getJacksonMapper().readValue(extensions, Object.class), getJacksonMapper()); } @SneakyThrows public Map> deserializeMultipartMap(InputStream inputStream) { return getJacksonMapper().readValue(inputStream, MULTIPART_MAP_TYPE_REFERENCE); } public static class Builder { private ObjectMapperProvider objectMapperProvider = new ConfiguringObjectMapperProvider(); private Supplier graphQLErrorHandler = DefaultGraphQLErrorHandler::new; public Builder withObjectMapperConfigurer( GraphQLServletObjectMapperConfigurer objectMapperConfigurer) { return withObjectMapperConfigurer(() -> objectMapperConfigurer); } public Builder withObjectMapperConfigurer( Supplier objectMapperConfigurer) { this.objectMapperProvider = new ConfiguringObjectMapperProvider(objectMapperConfigurer.get()); return this; } public Builder withObjectMapperProvider(ObjectMapperProvider objectMapperProvider) { this.objectMapperProvider = objectMapperProvider; return this; } public Builder withGraphQLErrorHandler(GraphQLErrorHandler graphQLErrorHandler) { return withGraphQLErrorHandler(() -> graphQLErrorHandler); } public Builder withGraphQLErrorHandler(Supplier graphQLErrorHandler) { this.graphQLErrorHandler = graphQLErrorHandler; return this; } public GraphQLObjectMapper build() { return new GraphQLObjectMapper(objectMapperProvider, graphQLErrorHandler); } } } ================================================ FILE: graphql-java-kickstart/src/main/java/graphql/kickstart/execution/GraphQLQueryInvoker.java ================================================ package graphql.kickstart.execution; import graphql.execution.instrumentation.ChainedInstrumentation; import graphql.execution.instrumentation.Instrumentation; import graphql.execution.instrumentation.SimplePerformantInstrumentation; import graphql.execution.preparsed.NoOpPreparsedDocumentProvider; import graphql.execution.preparsed.PreparsedDocumentProvider; import graphql.kickstart.execution.config.DefaultExecutionStrategyProvider; import graphql.kickstart.execution.config.ExecutionStrategyProvider; import graphql.kickstart.execution.config.GraphQLBuilder; import java.util.List; import java.util.function.Supplier; /** * @author Andrew Potter */ public class GraphQLQueryInvoker { private final Supplier getExecutionStrategyProvider; private final Supplier getInstrumentation; private final Supplier getPreparsedDocumentProvider; protected GraphQLQueryInvoker( Supplier getExecutionStrategyProvider, Supplier getInstrumentation, Supplier getPreparsedDocumentProvider) { this.getExecutionStrategyProvider = getExecutionStrategyProvider; this.getInstrumentation = getInstrumentation; this.getPreparsedDocumentProvider = getPreparsedDocumentProvider; } public static Builder newBuilder() { return new Builder(); } public GraphQLInvoker toGraphQLInvoker() { GraphQLBuilder graphQLBuilder = new GraphQLBuilder() .executionStrategyProvider(getExecutionStrategyProvider) .instrumentation(getInstrumentation) .preparsedDocumentProvider(getPreparsedDocumentProvider); return new GraphQLInvoker(graphQLBuilder, new BatchedDataLoaderGraphQLBuilder()); } public static class Builder { private Supplier getExecutionStrategyProvider = DefaultExecutionStrategyProvider::new; private Supplier getInstrumentation = () -> SimplePerformantInstrumentation.INSTANCE; private Supplier getPreparsedDocumentProvider = () -> NoOpPreparsedDocumentProvider.INSTANCE; public Builder withExecutionStrategyProvider(ExecutionStrategyProvider provider) { return withExecutionStrategyProvider(() -> provider); } public Builder withExecutionStrategyProvider(Supplier supplier) { this.getExecutionStrategyProvider = supplier; return this; } public Builder withInstrumentation(Instrumentation instrumentation) { return withInstrumentation(() -> instrumentation); } public Builder withInstrumentation(Supplier supplier) { this.getInstrumentation = supplier; return this; } public Builder with(List instrumentations) { if (instrumentations.isEmpty()) { return this; } if (instrumentations.size() == 1) { withInstrumentation(instrumentations.get(0)); } else { withInstrumentation(new ChainedInstrumentation(instrumentations)); } return this; } public Builder withPreparsedDocumentProvider(PreparsedDocumentProvider provider) { return withPreparsedDocumentProvider(() -> provider); } public Builder withPreparsedDocumentProvider(Supplier supplier) { this.getPreparsedDocumentProvider = supplier; return this; } public GraphQLQueryInvoker build() { return new GraphQLQueryInvoker( getExecutionStrategyProvider, getInstrumentation, getPreparsedDocumentProvider); } } } ================================================ FILE: graphql-java-kickstart/src/main/java/graphql/kickstart/execution/GraphQLQueryResult.java ================================================ package graphql.kickstart.execution; import static java.util.Collections.emptyList; import graphql.ExecutionResult; import java.util.List; public interface GraphQLQueryResult { static GraphQLSingleQueryResult create(ExecutionResult result) { return new GraphQLSingleQueryResult(new DecoratedExecutionResult(result)); } static GraphQLBatchedQueryResult create(List results) { return new GraphQLBatchedQueryResult(results); } static GraphQLErrorQueryResult createError(int statusCode, String message) { return new GraphQLErrorQueryResult(statusCode, message); } boolean isBatched(); boolean isAsynchronous(); default DecoratedExecutionResult getResult() { return null; } default List getResults() { return emptyList(); } default boolean isError() { return false; } default int getStatusCode() { return 200; } default String getMessage() { return null; } } ================================================ FILE: graphql-java-kickstart/src/main/java/graphql/kickstart/execution/GraphQLRequest.java ================================================ package graphql.kickstart.execution; import static graphql.kickstart.execution.OperationNameExtractor.extractOperationName; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import graphql.introspection.IntrospectionQuery; import java.util.HashMap; import java.util.Map; /** @author Andrew Potter */ @JsonIgnoreProperties(ignoreUnknown = true) public class GraphQLRequest { private String query; @JsonDeserialize(using = VariablesDeserializer.class) private Map variables = new HashMap<>(); @JsonDeserialize(using = ExtensionsDeserializer.class) private Map extensions = new HashMap<>(); private String operationName; public GraphQLRequest() {} public GraphQLRequest( String query, Map variables, Map extensions, String operationName) { this.query = query; this.operationName = operationName; if (extensions != null) { this.extensions = extensions; } if (variables != null) { this.variables = variables; } } public static GraphQLRequest createIntrospectionRequest() { return new GraphQLRequest( IntrospectionQuery.INTROSPECTION_QUERY, new HashMap<>(), new HashMap<>(), "IntrospectionQuery"); } public static GraphQLRequest createQueryOnlyRequest(String query) { return new GraphQLRequest(query, new HashMap<>(), new HashMap<>(), null); } public String getQuery() { return query; } public void setQuery(String query) { this.query = query; } public Map getVariables() { return variables; } public void setVariables(Map variables) { if (variables != null) { this.variables = variables; } } public Map getExtensions() { return extensions; } public void setExtensions(Map extensions) { if (extensions != null) { this.extensions = extensions; } } public String getOperationName() { return extractOperationName(query, operationName, null); } public void setOperationName(String operationName) { this.operationName = operationName; } } ================================================ FILE: graphql-java-kickstart/src/main/java/graphql/kickstart/execution/GraphQLRootObjectBuilder.java ================================================ package graphql.kickstart.execution; public interface GraphQLRootObjectBuilder { /** @return the graphql root object */ Object build(); } ================================================ FILE: graphql-java-kickstart/src/main/java/graphql/kickstart/execution/GraphQLSingleQueryResult.java ================================================ package graphql.kickstart.execution; import lombok.Getter; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor class GraphQLSingleQueryResult implements GraphQLQueryResult { @Getter private final DecoratedExecutionResult result; @Override public boolean isBatched() { return false; } @Override public boolean isAsynchronous() { return result.isAsynchronous(); } } ================================================ FILE: graphql-java-kickstart/src/main/java/graphql/kickstart/execution/ObjectMapDeserializationException.java ================================================ package graphql.kickstart.execution; public class ObjectMapDeserializationException extends RuntimeException { ObjectMapDeserializationException(String message) { super(message); } ObjectMapDeserializationException(String message, Throwable cause) { super(message, cause); } } ================================================ FILE: graphql-java-kickstart/src/main/java/graphql/kickstart/execution/ObjectMapDeserializeHelper.java ================================================ package graphql.kickstart.execution; import com.fasterxml.jackson.core.ObjectCodec; import com.fasterxml.jackson.core.type.TypeReference; import java.io.IOException; import java.util.Map; public class ObjectMapDeserializeHelper { public static Map deserializeObjectMapObject( Object object, ObjectCodec codec, String fieldName) { if (object instanceof Map) { @SuppressWarnings("unchecked") Map genericObjectMap = (Map) object; return genericObjectMap; } else if (object instanceof String) { try { return codec.readValue( codec.getFactory().createParser((String) object), new TypeReference>() {}); } catch (IOException e) { throw new ObjectMapDeserializationException( String.format("Cannot deserialize field '%s'", fieldName), e); } } else { throw new ObjectMapDeserializationException( String.format("Field '%s' should be either an object or a string", fieldName)); } } } ================================================ FILE: graphql-java-kickstart/src/main/java/graphql/kickstart/execution/OperationNameExtractor.java ================================================ package graphql.kickstart.execution; import static graphql.kickstart.execution.StringUtils.isNotEmpty; import graphql.language.Document; import graphql.language.OperationDefinition; import graphql.parser.InvalidSyntaxException; import graphql.parser.Parser; import java.util.List; import lombok.AccessLevel; import lombok.NoArgsConstructor; @NoArgsConstructor(access = AccessLevel.PRIVATE) class OperationNameExtractor { static String extractOperationName( String gqlQuery, String requestedOperationName, String defaultIfNotFound) { if (isNotEmpty(requestedOperationName)) { return requestedOperationName; } if (isNotEmpty(gqlQuery)) { return parseForOperationName(gqlQuery, defaultIfNotFound); } return defaultIfNotFound; } private static String parseForOperationName(String gqlQuery, String defaultIfNotFound) { try { Document document = new Parser().parseDocument(gqlQuery); List operations = document.getDefinitionsOfType(OperationDefinition.class); if (operations.size() == 1) { String name = operations.get(0).getName(); if (isNotEmpty(name)) { return name; } } } catch (InvalidSyntaxException ignored) { // ignored } return defaultIfNotFound; } } ================================================ FILE: graphql-java-kickstart/src/main/java/graphql/kickstart/execution/StaticGraphQLRootObjectBuilder.java ================================================ package graphql.kickstart.execution; public class StaticGraphQLRootObjectBuilder implements GraphQLRootObjectBuilder { private final Object rootObject; public StaticGraphQLRootObjectBuilder(Object rootObject) { this.rootObject = rootObject; } @Override public Object build() { return rootObject; } protected Object getRootObject() { return rootObject; } } ================================================ FILE: graphql-java-kickstart/src/main/java/graphql/kickstart/execution/StringUtils.java ================================================ package graphql.kickstart.execution; import lombok.AccessLevel; import lombok.NoArgsConstructor; @NoArgsConstructor(access = AccessLevel.PRIVATE) class StringUtils { static boolean isNotEmpty(CharSequence cs) { return !isEmpty(cs); } static boolean isEmpty(final CharSequence cs) { return cs == null || cs.length() == 0; } } ================================================ FILE: graphql-java-kickstart/src/main/java/graphql/kickstart/execution/VariablesDeserializer.java ================================================ package graphql.kickstart.execution; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.ObjectCodec; import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.JsonDeserializer; import java.io.IOException; import java.util.Map; /** @author Andrew Potter */ public class VariablesDeserializer extends JsonDeserializer> { public static Map deserializeVariablesObject( Object variables, ObjectCodec codec) { return ObjectMapDeserializeHelper.deserializeObjectMapObject(variables, codec, "variables"); } @Override public Map deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { return deserializeVariablesObject(p.readValueAs(Object.class), p.getCodec()); } } ================================================ FILE: graphql-java-kickstart/src/main/java/graphql/kickstart/execution/config/ConfiguringObjectMapperProvider.java ================================================ package graphql.kickstart.execution.config; import com.fasterxml.jackson.databind.ObjectMapper; import graphql.kickstart.execution.error.DefaultGraphQLServletObjectMapperConfigurer; public class ConfiguringObjectMapperProvider implements ObjectMapperProvider { private final ObjectMapper objectMapperTemplate; private final GraphQLServletObjectMapperConfigurer objectMapperConfigurer; public ConfiguringObjectMapperProvider( ObjectMapper objectMapperTemplate, GraphQLServletObjectMapperConfigurer objectMapperConfigurer) { this.objectMapperTemplate = objectMapperTemplate == null ? new ObjectMapper() : objectMapperTemplate; this.objectMapperConfigurer = objectMapperConfigurer == null ? new DefaultGraphQLServletObjectMapperConfigurer() : objectMapperConfigurer; } public ConfiguringObjectMapperProvider(ObjectMapper objectMapperTemplate) { this(objectMapperTemplate, null); } public ConfiguringObjectMapperProvider( GraphQLServletObjectMapperConfigurer objectMapperConfigurer) { this(null, objectMapperConfigurer); } public ConfiguringObjectMapperProvider() { this(null, null); } @Override public ObjectMapper provide() { ObjectMapper mapper = this.objectMapperTemplate.copy(); this.objectMapperConfigurer.configure(mapper); return mapper; } } ================================================ FILE: graphql-java-kickstart/src/main/java/graphql/kickstart/execution/config/DefaultExecutionStrategyProvider.java ================================================ package graphql.kickstart.execution.config; import graphql.execution.ExecutionStrategy; /** @author Andrew Potter */ public class DefaultExecutionStrategyProvider implements ExecutionStrategyProvider { private final ExecutionStrategy queryExecutionStrategy; private final ExecutionStrategy mutationExecutionStrategy; private final ExecutionStrategy subscriptionExecutionStrategy; public DefaultExecutionStrategyProvider() { this(null); } public DefaultExecutionStrategyProvider(ExecutionStrategy executionStrategy) { this(executionStrategy, executionStrategy, null); } public DefaultExecutionStrategyProvider( ExecutionStrategy queryExecutionStrategy, ExecutionStrategy mutationExecutionStrategy, ExecutionStrategy subscriptionExecutionStrategy) { this.queryExecutionStrategy = queryExecutionStrategy; this.mutationExecutionStrategy = mutationExecutionStrategy; this.subscriptionExecutionStrategy = subscriptionExecutionStrategy; } @Override public ExecutionStrategy getQueryExecutionStrategy() { return queryExecutionStrategy; } @Override public ExecutionStrategy getMutationExecutionStrategy() { return mutationExecutionStrategy; } @Override public ExecutionStrategy getSubscriptionExecutionStrategy() { return subscriptionExecutionStrategy; } } ================================================ FILE: graphql-java-kickstart/src/main/java/graphql/kickstart/execution/config/DefaultGraphQLSchemaProvider.java ================================================ package graphql.kickstart.execution.config; import graphql.schema.GraphQLSchema; /** @author Andrew Potter */ public class DefaultGraphQLSchemaProvider implements GraphQLSchemaProvider { private final GraphQLSchema schema; private final GraphQLSchema readOnlySchema; public DefaultGraphQLSchemaProvider(GraphQLSchema schema) { this(schema, GraphQLSchemaProvider.copyReadOnly(schema)); } public DefaultGraphQLSchemaProvider(GraphQLSchema schema, GraphQLSchema readOnlySchema) { this.schema = schema; this.readOnlySchema = readOnlySchema; } @Override public GraphQLSchema getSchema() { return schema; } @Override public GraphQLSchema getReadOnlySchema() { return readOnlySchema; } } ================================================ FILE: graphql-java-kickstart/src/main/java/graphql/kickstart/execution/config/ExecutionStrategyProvider.java ================================================ package graphql.kickstart.execution.config; import graphql.execution.ExecutionStrategy; public interface ExecutionStrategyProvider { ExecutionStrategy getQueryExecutionStrategy(); ExecutionStrategy getMutationExecutionStrategy(); ExecutionStrategy getSubscriptionExecutionStrategy(); } ================================================ FILE: graphql-java-kickstart/src/main/java/graphql/kickstart/execution/config/GraphQLBuilder.java ================================================ package graphql.kickstart.execution.config; import graphql.GraphQL; import graphql.execution.ExecutionStrategy; import graphql.execution.instrumentation.Instrumentation; import graphql.execution.instrumentation.SimplePerformantInstrumentation; import graphql.execution.preparsed.NoOpPreparsedDocumentProvider; import graphql.execution.preparsed.PreparsedDocumentProvider; import graphql.schema.GraphQLSchema; import java.util.function.Supplier; import lombok.Getter; public class GraphQLBuilder { private Supplier executionStrategyProviderSupplier = DefaultExecutionStrategyProvider::new; private Supplier preparsedDocumentProviderSupplier = () -> NoOpPreparsedDocumentProvider.INSTANCE; @Getter private Supplier instrumentationSupplier = () -> SimplePerformantInstrumentation.INSTANCE; private Supplier graphQLBuilderConfigurerSupplier = () -> builder -> {}; public GraphQLBuilder executionStrategyProvider(Supplier supplier) { if (supplier != null) { executionStrategyProviderSupplier = supplier; } return this; } public GraphQLBuilder preparsedDocumentProvider(Supplier supplier) { if (supplier != null) { preparsedDocumentProviderSupplier = supplier; } return this; } public GraphQLBuilder instrumentation(Supplier supplier) { if (supplier != null) { instrumentationSupplier = supplier; } return this; } public GraphQLBuilder graphQLBuilderConfigurer(Supplier supplier) { if (supplier != null) { graphQLBuilderConfigurerSupplier = supplier; } return this; } public GraphQL build(GraphQLSchemaProvider schemaProvider) { return build(schemaProvider.getSchema()); } public GraphQL build(GraphQLSchema schema) { return build(schema, instrumentationSupplier); } public GraphQL build( GraphQLSchema schema, Supplier configuredInstrumentationSupplier) { ExecutionStrategyProvider executionStrategyProvider = executionStrategyProviderSupplier.get(); ExecutionStrategy queryExecutionStrategy = executionStrategyProvider.getQueryExecutionStrategy(); ExecutionStrategy mutationExecutionStrategy = executionStrategyProvider.getMutationExecutionStrategy(); ExecutionStrategy subscriptionExecutionStrategy = executionStrategyProvider.getSubscriptionExecutionStrategy(); GraphQL.Builder builder = GraphQL.newGraphQL(schema) .preparsedDocumentProvider(preparsedDocumentProviderSupplier.get()); if (queryExecutionStrategy != null) { builder.queryExecutionStrategy(queryExecutionStrategy); } if (mutationExecutionStrategy != null) { builder.mutationExecutionStrategy(mutationExecutionStrategy); } if (subscriptionExecutionStrategy != null) { builder.subscriptionExecutionStrategy(subscriptionExecutionStrategy); } Instrumentation instrumentation = configuredInstrumentationSupplier.get(); builder.instrumentation(instrumentation); graphQLBuilderConfigurerSupplier.get().configure(builder); return builder.build(); } } ================================================ FILE: graphql-java-kickstart/src/main/java/graphql/kickstart/execution/config/GraphQLBuilderConfigurer.java ================================================ package graphql.kickstart.execution.config; import graphql.GraphQL; public interface GraphQLBuilderConfigurer { void configure(GraphQL.Builder builder); } ================================================ FILE: graphql-java-kickstart/src/main/java/graphql/kickstart/execution/config/GraphQLSchemaProvider.java ================================================ package graphql.kickstart.execution.config; import graphql.schema.GraphQLObjectType; import graphql.schema.GraphQLSchema; public interface GraphQLSchemaProvider { static GraphQLSchema copyReadOnly(GraphQLSchema schema) { return GraphQLSchema.newSchema(schema).mutation((GraphQLObjectType) null).build(); } /** @return a schema for handling mbean calls. */ GraphQLSchema getSchema(); GraphQLSchema getReadOnlySchema(); } ================================================ FILE: graphql-java-kickstart/src/main/java/graphql/kickstart/execution/config/GraphQLServletObjectMapperConfigurer.java ================================================ package graphql.kickstart.execution.config; import com.fasterxml.jackson.databind.ObjectMapper; /** @author Andrew Potter */ public interface GraphQLServletObjectMapperConfigurer { void configure(ObjectMapper mapper); } ================================================ FILE: graphql-java-kickstart/src/main/java/graphql/kickstart/execution/config/InstrumentationProvider.java ================================================ package graphql.kickstart.execution.config; import graphql.execution.instrumentation.Instrumentation; public interface InstrumentationProvider { Instrumentation getInstrumentation(); } ================================================ FILE: graphql-java-kickstart/src/main/java/graphql/kickstart/execution/config/ObjectMapperProvider.java ================================================ package graphql.kickstart.execution.config; import com.fasterxml.jackson.databind.ObjectMapper; public interface ObjectMapperProvider { ObjectMapper provide(); } ================================================ FILE: graphql-java-kickstart/src/main/java/graphql/kickstart/execution/context/ContextSetting.java ================================================ package graphql.kickstart.execution.context; import graphql.kickstart.execution.GraphQLRequest; import graphql.kickstart.execution.input.GraphQLBatchedInvocationInput; import graphql.kickstart.execution.input.PerQueryBatchedInvocationInput; import graphql.kickstart.execution.input.PerRequestBatchedInvocationInput; import graphql.schema.GraphQLSchema; import java.util.List; import java.util.function.Supplier; /** * An enum representing possible context settings. These are modeled after Apollo's link settings. */ public enum ContextSetting { /** * A context object, and therefor dataloader registry and subject, should be shared between all * GraphQL executions in a http request. */ PER_REQUEST, /** Each GraphQL execution should always have its own context. */ PER_QUERY; /** * Creates a set of inputs with the correct context based on the setting. * * @param requests the GraphQL requests to execute. * @param schema the GraphQL schema to execute the requests against. * @param contextSupplier method that returns the context to use for each execution or for the * request as a whole. * @param root the root object to use for each execution. * @return a configured batch input. */ public GraphQLBatchedInvocationInput getBatch( List requests, GraphQLSchema schema, Supplier contextSupplier, Object root) { switch (this) { case PER_QUERY: return new PerQueryBatchedInvocationInput(requests, schema, contextSupplier, root, this); case PER_REQUEST: return new PerRequestBatchedInvocationInput(requests, schema, contextSupplier, root, this); default: throw new ContextSettingNotConfiguredException(); } } } ================================================ FILE: graphql-java-kickstart/src/main/java/graphql/kickstart/execution/context/ContextSettingNotConfiguredException.java ================================================ package graphql.kickstart.execution.context; public class ContextSettingNotConfiguredException extends RuntimeException { ContextSettingNotConfiguredException() { super("Unconfigured context setting type"); } } ================================================ FILE: graphql-java-kickstart/src/main/java/graphql/kickstart/execution/context/DefaultGraphQLContext.java ================================================ package graphql.kickstart.execution.context; import java.util.HashMap; import java.util.Map; import java.util.Objects; import org.dataloader.DataLoaderRegistry; /** * An object for the DefaultGraphQLContextBuilder to return. Can be extended to include more * context. */ public class DefaultGraphQLContext implements GraphQLKickstartContext { private final DataLoaderRegistry dataLoaderRegistry; private final Map map; public DefaultGraphQLContext(DataLoaderRegistry dataLoaderRegistry, Map map) { this.dataLoaderRegistry = Objects.requireNonNull(dataLoaderRegistry, "dataLoaderRegistry is required"); this.map = Objects.requireNonNull(map, "map is required"); } public DefaultGraphQLContext(Map map) { this(new DataLoaderRegistry(), map); } public DefaultGraphQLContext(DataLoaderRegistry dataLoaderRegistry) { this(dataLoaderRegistry, new HashMap<>()); } public DefaultGraphQLContext() { this(new DataLoaderRegistry()); } public void put(Object key, Object value) { map.put(key, value); } @Override public DataLoaderRegistry getDataLoaderRegistry() { return dataLoaderRegistry; } @Override public Map getMapOfContext() { return map; } } ================================================ FILE: graphql-java-kickstart/src/main/java/graphql/kickstart/execution/context/DefaultGraphQLContextBuilder.java ================================================ package graphql.kickstart.execution.context; /** Returns an empty context. */ public class DefaultGraphQLContextBuilder implements GraphQLContextBuilder { @Override public GraphQLKickstartContext build() { return new DefaultGraphQLContext(); } } ================================================ FILE: graphql-java-kickstart/src/main/java/graphql/kickstart/execution/context/GraphQLContextBuilder.java ================================================ package graphql.kickstart.execution.context; public interface GraphQLContextBuilder { /** @return the graphql context */ GraphQLKickstartContext build(); } ================================================ FILE: graphql-java-kickstart/src/main/java/graphql/kickstart/execution/context/GraphQLKickstartContext.java ================================================ package graphql.kickstart.execution.context; import java.util.Map; import lombok.NonNull; import org.dataloader.DataLoaderRegistry; /** Represents the context required by the servlet to execute a GraphQL request. */ public interface GraphQLKickstartContext { static GraphQLKickstartContext of(Map map) { return new DefaultGraphQLContext(map); } static GraphQLKickstartContext of(DataLoaderRegistry dataLoaderRegistry) { return new DefaultGraphQLContext(dataLoaderRegistry); } static GraphQLKickstartContext of( DataLoaderRegistry dataLoaderRegistry, Map map) { return new DefaultGraphQLContext(dataLoaderRegistry, map); } /** @return the Dataloader registry to use for the execution. Must not return null */ @NonNull DataLoaderRegistry getDataLoaderRegistry(); Map getMapOfContext(); } ================================================ FILE: graphql-java-kickstart/src/main/java/graphql/kickstart/execution/error/DefaultGraphQLErrorHandler.java ================================================ package graphql.kickstart.execution.error; import graphql.ExceptionWhileDataFetching; import graphql.GraphQLError; import graphql.execution.NonNullableFieldWasNullError; import java.util.List; import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; /** @author Andrew Potter */ @Slf4j public class DefaultGraphQLErrorHandler implements GraphQLErrorHandler { @Override public List processErrors(List errors) { final List clientErrors = filterGraphQLErrors(errors); if (clientErrors.size() < errors.size()) { // Some errors were filtered out to hide implementation - put a generic error in place. clientErrors.add(new GenericGraphQLError("Internal Server Error(s) while executing query")); errors.stream().filter(error -> !isClientError(error)).forEach(this::logError); } return clientErrors; } protected void logError(GraphQLError error) { if (error instanceof Throwable) { log.error("Error executing query!", (Throwable) error); } else if (error instanceof ExceptionWhileDataFetching) { log.error( "Error executing query {}", error.getMessage(), ((ExceptionWhileDataFetching) error).getException()); } else { log.error( "Error executing query ({}): {}", error.getClass().getSimpleName(), error.getMessage()); } } protected List filterGraphQLErrors(List errors) { return errors.stream() .filter(this::isClientError) .map(this::replaceNonNullableFieldWasNullError) .collect(Collectors.toList()); } protected boolean isClientError(GraphQLError error) { if (error instanceof ExceptionWhileDataFetching) { return ((ExceptionWhileDataFetching) error).getException() instanceof GraphQLError; } return true; } private GraphQLError replaceNonNullableFieldWasNullError(GraphQLError error) { if (error instanceof NonNullableFieldWasNullError) { return new RenderableNonNullableFieldWasNullError((NonNullableFieldWasNullError) error); } else { return error; } } } ================================================ FILE: graphql-java-kickstart/src/main/java/graphql/kickstart/execution/error/DefaultGraphQLServletObjectMapperConfigurer.java ================================================ package graphql.kickstart.execution.error; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import graphql.kickstart.execution.config.GraphQLServletObjectMapperConfigurer; /** @author Andrew Potter */ public class DefaultGraphQLServletObjectMapperConfigurer implements GraphQLServletObjectMapperConfigurer { @Override public void configure(ObjectMapper mapper) { mapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS); mapper.setDefaultPropertyInclusion(JsonInclude.Include.ALWAYS); } } ================================================ FILE: graphql-java-kickstart/src/main/java/graphql/kickstart/execution/error/GenericGraphQLError.java ================================================ package graphql.kickstart.execution.error; import static java.util.Collections.emptyList; import com.fasterxml.jackson.annotation.JsonIgnore; import graphql.ErrorType; import graphql.GraphQLError; import graphql.language.SourceLocation; import java.util.List; /** @author Andrew Potter */ public class GenericGraphQLError implements GraphQLError { private final String message; public GenericGraphQLError(String message) { this.message = message; } @Override public String getMessage() { return message; } @Override @JsonIgnore public List getLocations() { return emptyList(); } @Override @JsonIgnore public ErrorType getErrorType() { return null; } } ================================================ FILE: graphql-java-kickstart/src/main/java/graphql/kickstart/execution/error/GraphQLErrorHandler.java ================================================ package graphql.kickstart.execution.error; import graphql.GraphQLError; import java.util.List; /** @author Andrew Potter */ public interface GraphQLErrorHandler { default boolean errorsPresent(List errors) { return errors != null && !errors.isEmpty(); } List processErrors(List errors); } ================================================ FILE: graphql-java-kickstart/src/main/java/graphql/kickstart/execution/error/RenderableNonNullableFieldWasNullError.java ================================================ package graphql.kickstart.execution.error; import com.fasterxml.jackson.annotation.JsonInclude; import graphql.ErrorType; import graphql.GraphQLError; import graphql.execution.NonNullableFieldWasNullError; import graphql.language.SourceLocation; import java.util.List; import java.util.Map; class RenderableNonNullableFieldWasNullError implements GraphQLError { private final NonNullableFieldWasNullError delegate; public RenderableNonNullableFieldWasNullError( NonNullableFieldWasNullError nonNullableFieldWasNullError) { this.delegate = nonNullableFieldWasNullError; } @Override public String getMessage() { return delegate.getMessage(); } @Override @JsonInclude(JsonInclude.Include.NON_NULL) public List getLocations() { return delegate.getLocations(); } @Override public ErrorType getErrorType() { return delegate.getErrorType(); } @Override public List getPath() { return delegate.getPath(); } @Override public Map toSpecification() { return delegate.toSpecification(); } @Override @JsonInclude(JsonInclude.Include.NON_NULL) public Map getExtensions() { return delegate.getExtensions(); } } ================================================ FILE: graphql-java-kickstart/src/main/java/graphql/kickstart/execution/input/GraphQLBatchedInvocationInput.java ================================================ package graphql.kickstart.execution.input; import static java.util.stream.Collectors.toList; import graphql.ExecutionInput; import graphql.kickstart.execution.context.ContextSetting; import java.util.List; /** Interface representing a batched input. */ public interface GraphQLBatchedInvocationInput extends GraphQLInvocationInput { /** @return each individual input in the batch, configured with a context. */ List getInvocationInputs(); default List getExecutionInputs() { return getInvocationInputs().stream() .map(GraphQLSingleInvocationInput::getExecutionInput) .collect(toList()); } ContextSetting getContextSetting(); } ================================================ FILE: graphql-java-kickstart/src/main/java/graphql/kickstart/execution/input/GraphQLInvocationInput.java ================================================ package graphql.kickstart.execution.input; import java.util.List; public interface GraphQLInvocationInput { List getQueries(); } ================================================ FILE: graphql-java-kickstart/src/main/java/graphql/kickstart/execution/input/GraphQLSingleInvocationInput.java ================================================ package graphql.kickstart.execution.input; import static java.util.Collections.singletonList; import graphql.ExecutionInput; import graphql.execution.ExecutionId; import graphql.kickstart.execution.GraphQLRequest; import graphql.kickstart.execution.context.GraphQLKickstartContext; import graphql.schema.GraphQLSchema; import java.util.List; /** Represents a single GraphQL execution. */ public class GraphQLSingleInvocationInput implements GraphQLInvocationInput { private final GraphQLSchema schema; private final ExecutionInput executionInput; public GraphQLSingleInvocationInput( GraphQLRequest request, GraphQLSchema schema, GraphQLKickstartContext context, Object root) { this.schema = schema; this.executionInput = createExecutionInput(request, context, root); } /** @return the schema to use to execute this query. */ public GraphQLSchema getSchema() { return schema; } private ExecutionInput createExecutionInput( GraphQLRequest graphQLRequest, GraphQLKickstartContext context, Object root) { return ExecutionInput.newExecutionInput() .query(graphQLRequest.getQuery()) .operationName(graphQLRequest.getOperationName()) .context(context) .graphQLContext(context.getMapOfContext()) .root(root) .variables(graphQLRequest.getVariables()) .extensions(graphQLRequest.getExtensions()) .dataLoaderRegistry(context.getDataLoaderRegistry()) .executionId(ExecutionId.generate()) .build(); } public ExecutionInput getExecutionInput() { return executionInput; } @Override public List getQueries() { return singletonList(executionInput.getQuery()); } } ================================================ FILE: graphql-java-kickstart/src/main/java/graphql/kickstart/execution/input/PerQueryBatchedInvocationInput.java ================================================ package graphql.kickstart.execution.input; import static java.util.stream.Collectors.toList; import graphql.kickstart.execution.GraphQLRequest; import graphql.kickstart.execution.context.ContextSetting; import graphql.kickstart.execution.context.GraphQLKickstartContext; import graphql.schema.GraphQLSchema; import java.util.List; import java.util.function.Supplier; import java.util.stream.Collectors; import lombok.Getter; /** A Collection of GraphQLSingleInvocationInput that each have a unique context object. */ @Getter public class PerQueryBatchedInvocationInput implements GraphQLBatchedInvocationInput { private final List invocationInputs; private final ContextSetting contextSetting; public PerQueryBatchedInvocationInput( List requests, GraphQLSchema schema, Supplier contextSupplier, Object root, ContextSetting contextSetting) { invocationInputs = requests.stream() .map( request -> new GraphQLSingleInvocationInput(request, schema, contextSupplier.get(), root)) .collect(Collectors.toList()); this.contextSetting = contextSetting; } @Override public List getQueries() { return invocationInputs.stream() .map(GraphQLSingleInvocationInput::getQueries) .flatMap(List::stream) .collect(toList()); } } ================================================ FILE: graphql-java-kickstart/src/main/java/graphql/kickstart/execution/input/PerRequestBatchedInvocationInput.java ================================================ package graphql.kickstart.execution.input; import static java.util.stream.Collectors.toList; import graphql.kickstart.execution.GraphQLRequest; import graphql.kickstart.execution.context.ContextSetting; import graphql.kickstart.execution.context.GraphQLKickstartContext; import graphql.schema.GraphQLSchema; import java.util.List; import java.util.function.Supplier; import lombok.Getter; /** A collection of GraphQLSingleInvocationInputs that share a context object. */ @Getter public class PerRequestBatchedInvocationInput implements GraphQLBatchedInvocationInput { private final List invocationInputs; private final ContextSetting contextSetting; public PerRequestBatchedInvocationInput( List requests, GraphQLSchema schema, Supplier contextSupplier, Object root, ContextSetting contextSetting) { GraphQLKickstartContext context = contextSupplier.get(); invocationInputs = requests.stream() .map(request -> new GraphQLSingleInvocationInput(request, schema, context, root)) .collect(toList()); this.contextSetting = contextSetting; } @Override public List getQueries() { return invocationInputs.stream() .map(GraphQLSingleInvocationInput::getQueries) .flatMap(List::stream) .collect(toList()); } } ================================================ FILE: graphql-java-kickstart/src/main/java/graphql/kickstart/execution/instrumentation/AbstractTrackingApproach.java ================================================ package graphql.kickstart.execution.instrumentation; import graphql.ExecutionResult; import graphql.execution.ExecutionId; import graphql.execution.FieldValueInfo; import graphql.execution.ResultPath; import graphql.execution.instrumentation.ExecutionStrategyInstrumentationContext; import graphql.execution.instrumentation.InstrumentationContext; import graphql.execution.instrumentation.parameters.InstrumentationExecutionStrategyParameters; import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters; import java.util.List; import java.util.concurrent.CompletableFuture; import lombok.extern.slf4j.Slf4j; import org.dataloader.DataLoaderRegistry; /** Handles logic common to tracking approaches. */ @Slf4j public abstract class AbstractTrackingApproach implements TrackingApproach { private final DataLoaderRegistry dataLoaderRegistry; private final RequestStack stack = new RequestStack(); protected AbstractTrackingApproach(DataLoaderRegistry dataLoaderRegistry) { this.dataLoaderRegistry = dataLoaderRegistry; } /** @return allows extending classes to modify the stack. */ protected RequestStack getStack() { return stack; } @Override public ExecutionStrategyInstrumentationContext beginExecutionStrategy( InstrumentationExecutionStrategyParameters parameters) { ExecutionId executionId = parameters.getExecutionContext().getExecutionId(); ResultPath path = parameters.getExecutionStrategyParameters().getPath(); int parentLevel = path.getLevel(); int curLevel = parentLevel + 1; int fieldCount = parameters.getExecutionStrategyParameters().getFields().size(); synchronized (stack) { stack.increaseExpectedFetchCount(executionId, curLevel, fieldCount); stack.increaseHappenedStrategyCalls(executionId, curLevel); } return new ExecutionStrategyInstrumentationContext() { @Override public void onDispatched() { // default empty implementation } @Override public void onCompleted(ExecutionResult result, Throwable t) { // default empty implementation } @Override public void onFieldValuesInfo(List fieldValueInfoList) { synchronized (stack) { stack.setStatus( executionId, handleOnFieldValuesInfo(fieldValueInfoList, stack, executionId, curLevel)); if (stack.allReady()) { dispatchWithoutLocking(); } } } }; } // // thread safety : called with synchronised(stack) // private boolean handleOnFieldValuesInfo( List fieldValueInfoList, RequestStack stack, ExecutionId executionId, int curLevel) { stack.increaseHappenedOnFieldValueCalls(executionId, curLevel); int expectedStrategyCalls = 0; for (FieldValueInfo fieldValueInfo : fieldValueInfoList) { if (fieldValueInfo.getCompleteValueType() == FieldValueInfo.CompleteValueType.OBJECT) { expectedStrategyCalls++; } else if (fieldValueInfo.getCompleteValueType() == FieldValueInfo.CompleteValueType.LIST) { expectedStrategyCalls += getCountForList(fieldValueInfo); } } stack.increaseExpectedStrategyCalls(executionId, curLevel + 1, expectedStrategyCalls); return dispatchIfNeeded(stack, executionId, curLevel + 1); } private int getCountForList(FieldValueInfo fieldValueInfo) { int result = 0; for (FieldValueInfo cvi : fieldValueInfo.getFieldValueInfos()) { if (cvi.getCompleteValueType() == FieldValueInfo.CompleteValueType.OBJECT) { result++; } else if (cvi.getCompleteValueType() == FieldValueInfo.CompleteValueType.LIST) { result += getCountForList(cvi); } } return result; } @Override public InstrumentationContext beginFieldFetch( InstrumentationFieldFetchParameters parameters) { ExecutionId executionId = parameters.getExecutionContext().getExecutionId(); ResultPath path = parameters.getEnvironment().getExecutionStepInfo().getPath(); int level = path.getLevel(); return new InstrumentationContext() { @Override public void onDispatched() { synchronized (stack) { stack.increaseFetchCount(executionId, level); stack.setStatus(executionId, dispatchIfNeeded(stack, executionId, level)); if (stack.allReady()) { dispatchWithoutLocking(); } } } @Override public void onCompleted(Object result, Throwable t) { // default empty implementation } }; } @Override public void removeTracking(ExecutionId executionId) { synchronized (stack) { stack.removeExecution(executionId); if (stack.allReady()) { dispatchWithoutLocking(); } } } // // thread safety : called with synchronised(stack) // private boolean dispatchIfNeeded(RequestStack stack, ExecutionId executionId, int level) { if (levelReady(stack, executionId, level)) { return stack.dispatchIfNotDispatchedBefore(executionId, level); } return false; } // // thread safety : called with synchronised(stack) // private boolean levelReady(RequestStack stack, ExecutionId executionId, int level) { if (level == 1) { // level 1 is special: there is only one strategy call and that's it return stack.allFetchesHappened(executionId, 1); } return (levelReady(stack, executionId, level - 1) && stack.allOnFieldCallsHappened(executionId, level - 1) && stack.allStrategyCallsHappened(executionId, level) && stack.allFetchesHappened(executionId, level)); } @Override public void dispatch() { synchronized (stack) { dispatchWithoutLocking(); } } private void dispatchWithoutLocking() { log.debug("Dispatching data loaders ({})", dataLoaderRegistry.getKeys()); dataLoaderRegistry.dispatchAll(); stack.allReset(); } } ================================================ FILE: graphql-java-kickstart/src/main/java/graphql/kickstart/execution/instrumentation/DataLoaderDispatcherInstrumentationState.java ================================================ package graphql.kickstart.execution.instrumentation; import graphql.execution.ExecutionId; import graphql.execution.instrumentation.InstrumentationState; import org.dataloader.DataLoaderRegistry; /** A base class that keeps track of whether aggressive batching can be used */ public class DataLoaderDispatcherInstrumentationState implements InstrumentationState { private final TrackingApproach approach; private final DataLoaderRegistry dataLoaderRegistry; private final boolean hasNoDataLoaders; private boolean aggressivelyBatching = true; public DataLoaderDispatcherInstrumentationState( DataLoaderRegistry dataLoaderRegistry, TrackingApproach approach, ExecutionId executionId) { this.dataLoaderRegistry = dataLoaderRegistry; this.approach = approach; approach.createState(executionId); hasNoDataLoaders = dataLoaderRegistry.getKeys().isEmpty(); } boolean isAggressivelyBatching() { return aggressivelyBatching; } void setAggressivelyBatching(boolean aggressivelyBatching) { this.aggressivelyBatching = aggressivelyBatching; } TrackingApproach getApproach() { return approach; } DataLoaderRegistry getDataLoaderRegistry() { return dataLoaderRegistry; } boolean hasNoDataLoaders() { return hasNoDataLoaders; } } ================================================ FILE: graphql-java-kickstart/src/main/java/graphql/kickstart/execution/instrumentation/FieldLevelTrackingApproach.java ================================================ package graphql.kickstart.execution.instrumentation; import graphql.Internal; import graphql.execution.ExecutionId; import graphql.execution.instrumentation.InstrumentationState; import org.dataloader.DataLoaderRegistry; /** * This approach uses field level tracking to achieve its aims of making the data loader more * efficient. Can handle new/concurrent executions using the same sets of dataloaders, attempting to * batch load calls together. */ @Internal public class FieldLevelTrackingApproach extends AbstractTrackingApproach { public FieldLevelTrackingApproach(DataLoaderRegistry dataLoaderRegistry) { super(dataLoaderRegistry); } public InstrumentationState createState(ExecutionId executionId) { synchronized (getStack()) { if (getStack().contains(executionId)) { throw new TrackingApproachException( String.format("Execution id %s already in active execution", executionId)); } getStack().addExecution(executionId); return null; } } } ================================================ FILE: graphql-java-kickstart/src/main/java/graphql/kickstart/execution/instrumentation/NoOpInstrumentationProvider.java ================================================ package graphql.kickstart.execution.instrumentation; import graphql.execution.instrumentation.Instrumentation; import graphql.execution.instrumentation.SimplePerformantInstrumentation; import graphql.kickstart.execution.config.InstrumentationProvider; public class NoOpInstrumentationProvider implements InstrumentationProvider { @Override public Instrumentation getInstrumentation() { return SimplePerformantInstrumentation.INSTANCE; } } ================================================ FILE: graphql-java-kickstart/src/main/java/graphql/kickstart/execution/instrumentation/RequestLevelTrackingApproach.java ================================================ package graphql.kickstart.execution.instrumentation; import graphql.execution.ExecutionId; import graphql.execution.instrumentation.InstrumentationState; import java.util.List; import org.dataloader.DataLoaderRegistry; /** Dispatching approach that expects to know about all executions before execution starts. */ public class RequestLevelTrackingApproach extends AbstractTrackingApproach { public RequestLevelTrackingApproach( List executionIds, DataLoaderRegistry dataLoaderRegistry) { super(dataLoaderRegistry); RequestStack requestStack = getStack(); executionIds.forEach(requestStack::addExecution); } @Override public InstrumentationState createState(ExecutionId executionId) { if (!getStack().contains(executionId)) { throw new TrackingApproachException( String.format("Request tracking not set up with execution id %s", executionId)); } return null; } } ================================================ FILE: graphql-java-kickstart/src/main/java/graphql/kickstart/execution/instrumentation/RequestStack.java ================================================ package graphql.kickstart.execution.instrumentation; import graphql.Assert; import graphql.execution.ExecutionId; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; /** Manages sets of call stack state for ongoing executions. */ public class RequestStack { private final Map activeRequests = new LinkedHashMap<>(); private final Map status = new LinkedHashMap<>(); /** * Sets the status indicating if a specific execution is ready for dispatching. * * @param executionId must be an active execution * @param toState if ready to dispatch */ public void setStatus(ExecutionId executionId, boolean toState) { if (!activeRequests.containsKey(executionId)) { throw new IllegalStateException( String.format( "Can not set status for execution %s, it is not managed by this request stack", executionId)); } if (toState) { status.put(executionId, true); } } /** @return if all managed executions are ready to be dispatched. */ public boolean allReady() { return status.values().stream().noneMatch(Boolean.FALSE::equals) && activeRequests.values().stream().map(CallStack::getDispatchedLevels).distinct().count() <= 1; } /** Removes all dispatch status. Should be used after a call to dispatch. */ public void allReset() { status.keySet().forEach(key -> status.put(key, false)); } /** * Returns if this RequestStack is managing an execution for the supplied id. * * @param executionId no restrictions * @return if an active execution */ public boolean contains(ExecutionId executionId) { return activeRequests.containsKey(executionId); } /** * Removes any state associated with an id. * * @param executionId no restrictions */ public void removeExecution(ExecutionId executionId) { activeRequests.remove(executionId); status.remove(executionId); } /** * Creates a call stack for an associated id. * * @param executionId can not already be managed by this RequestStack */ public void addExecution(ExecutionId executionId) { if (activeRequests.containsKey(executionId)) { throw new IllegalStateException( String.format("An execution already exists for %s, can not create one", executionId)); } CallStack callStack = new CallStack(); status.put(executionId, false); activeRequests.put(executionId, callStack); } /** * Increases the expected fetch count for an execution. * * @param executionId must be managed by this RequestStack * @param curLevel the level to increase the expected count * @param fieldCount the amount to increase the expected amount */ public void increaseExpectedFetchCount(ExecutionId executionId, int curLevel, int fieldCount) { if (!activeRequests.containsKey(executionId)) { throw new IllegalStateException( String.format( "Execution %s not managed by this RequestStack, can not increase expected fetch count", executionId)); } activeRequests.get(executionId).increaseExpectedFetchCount(curLevel, fieldCount); } /** * Increments happened strategy calls for an execution at specified level. * * @param executionId must be managed by this RequestStack * @param curLevel level to increment */ public void increaseHappenedStrategyCalls(ExecutionId executionId, int curLevel) { if (!activeRequests.containsKey(executionId)) { throw new IllegalStateException( String.format( "Execution %s not managed by this RequestStack, can not increase happened happened strategy calls", executionId)); } activeRequests.get(executionId).increaseHappenedStrategyCalls(curLevel); } /** * Increments happened on field value calls for an execution at a specified level. * * @param executionId must be managed by this RequestStack * @param curLevel level to increment */ public void increaseHappenedOnFieldValueCalls(ExecutionId executionId, int curLevel) { if (!activeRequests.containsKey(executionId)) { throw new IllegalStateException( String.format( "Execution %s not managed by this RequestStack, can not increase happened on field calls", executionId)); } activeRequests.get(executionId).increaseHappenedOnFieldValueCalls(curLevel); } /** * Increases expected strategy calls for an execution at a specified level. * * @param executionId must be managed by this RequestStack * @param curLevel level to increase * @param expectedStrategyCalls number to increment by */ public void increaseExpectedStrategyCalls( ExecutionId executionId, int curLevel, int expectedStrategyCalls) { if (!activeRequests.containsKey(executionId)) { throw new IllegalStateException( String.format( "Execution %s not managed by this RequestStack, can not increase expected strategy calls", executionId)); } activeRequests.get(executionId).increaseExpectedStrategyCalls(curLevel, expectedStrategyCalls); } /** * Get the all fetches happened value for an execution at a specific level. * * @param executionId must be managed by this RequestStack * @param level the level to get the value of * @return allFetchesHappened */ public boolean allFetchesHappened(ExecutionId executionId, int level) { if (!activeRequests.containsKey(executionId)) { throw new IllegalStateException( String.format( "Execution %s not managed by this RequestStack, can not get all fetches happened value", executionId)); } return activeRequests.get(executionId).allFetchesHappened(level); } /** * Get the all on field calls happened for an exectuion at a specific level. * * @param executionId must be managed by this RequestStack * @param level the level to get the value of * @return allOnFieldCallsHappened */ public boolean allOnFieldCallsHappened(ExecutionId executionId, int level) { if (!activeRequests.containsKey(executionId)) { throw new IllegalStateException( String.format( "Execution %s not managed by this RequestStack, can not get all on field calls happened value", executionId)); } return activeRequests.get(executionId).allOnFieldCallsHappened(level); } /** * Get the all strategy calls happened value for an exectuion at a specific level. * * @param executionId must be managed by this RequestStack * @param level the level to get the value of * @return allStrategyCallsHappened */ public boolean allStrategyCallsHappened(ExecutionId executionId, int level) { if (!activeRequests.containsKey(executionId)) { throw new IllegalStateException( String.format( "Execution %s not managed by this RequestStack, can not get all strategy calls happened value", executionId)); } return activeRequests.get(executionId).allStrategyCallsHappened(level); } /** * Get the dispatch if not dispatched before value of a specific level. * * @param executionId must be managed by this RequestStack * @param level the level to get the value of * @return dispatchIfNotDispattchedBefore */ public boolean dispatchIfNotDispatchedBefore(ExecutionId executionId, int level) { if (!activeRequests.containsKey(executionId)) { throw new IllegalStateException( String.format( "Execution %s not managed by this RequestStack, can not get dispatch if not dispatched before value", executionId)); } return activeRequests.get(executionId).dispatchIfNotDispatchedBefore(level); } /** * Increment the fetch count for an execution at a specific level. * * @param executionId must be managed by this RequestStack * @param level the level to increment */ public void increaseFetchCount(ExecutionId executionId, int level) { if (!activeRequests.containsKey(executionId)) { throw new IllegalStateException( String.format( "Execution %s not managed by this RequestStack, can not increase fetch count", executionId)); } activeRequests.get(executionId).increaseFetchCount(level); } /** * Clear and mark current level as ready for an execution. * * @param executionId must be managed by this RequestStack * @param level the level to clear and mark */ public void clearAndMarkCurrentLevelAsReady(ExecutionId executionId, int level) { if (!activeRequests.containsKey(executionId)) { throw new IllegalStateException( String.format( "Execution %s not managed by this RequestStack, can not clea and mark current level as ready", executionId)); } activeRequests.get(executionId).clearAndMarkCurrentLevelAsReady(level); } private static class CallStack { private final Map expectedFetchCountPerLevel = new LinkedHashMap<>(); private final Map fetchCountPerLevel = new LinkedHashMap<>(); private final Map expectedStrategyCallsPerLevel = new LinkedHashMap<>(); private final Map happenedStrategyCallsPerLevel = new LinkedHashMap<>(); private final Map happenedOnFieldValueCallsPerLevel = new LinkedHashMap<>(); private final List dispatchedLevels = new ArrayList<>(); private CallStack() { expectedStrategyCallsPerLevel.put(1, 1); } private void increaseExpectedFetchCount(int level, int count) { expectedFetchCountPerLevel.put( level, expectedFetchCountPerLevel.getOrDefault(level, 0) + count); } private void increaseFetchCount(int level) { fetchCountPerLevel.put(level, fetchCountPerLevel.getOrDefault(level, 0) + 1); } private void increaseExpectedStrategyCalls(int level, int count) { expectedStrategyCallsPerLevel.put( level, expectedStrategyCallsPerLevel.getOrDefault(level, 0) + count); } private void increaseHappenedStrategyCalls(int level) { happenedStrategyCallsPerLevel.put( level, happenedStrategyCallsPerLevel.getOrDefault(level, 0) + 1); } private void increaseHappenedOnFieldValueCalls(int level) { happenedOnFieldValueCallsPerLevel.put( level, happenedOnFieldValueCallsPerLevel.getOrDefault(level, 0) + 1); } private boolean allStrategyCallsHappened(int level) { return Objects.equals( happenedStrategyCallsPerLevel.get(level), expectedStrategyCallsPerLevel.get(level)); } private boolean allOnFieldCallsHappened(int level) { return Objects.equals( happenedOnFieldValueCallsPerLevel.get(level), expectedStrategyCallsPerLevel.get(level)); } private boolean allFetchesHappened(int level) { return Objects.equals(fetchCountPerLevel.get(level), expectedFetchCountPerLevel.get(level)); } private List getDispatchedLevels() { return dispatchedLevels; } @Override public String toString() { return "CallStack{" + "expectedFetchCountPerLevel=" + expectedFetchCountPerLevel + ", fetchCountPerLevel=" + fetchCountPerLevel + ", expectedStrategyCallsPerLevel=" + expectedStrategyCallsPerLevel + ", happenedStrategyCallsPerLevel=" + happenedStrategyCallsPerLevel + ", happenedOnFieldValueCallsPerLevel=" + happenedOnFieldValueCallsPerLevel + ", dispatchedLevels" + dispatchedLevels + '}'; } private boolean dispatchIfNotDispatchedBefore(int level) { if (dispatchedLevels.contains(level)) { Assert.assertShouldNeverHappen("level " + level + " already dispatched"); return false; } dispatchedLevels.add(level); return true; } private void clearAndMarkCurrentLevelAsReady(int level) { expectedFetchCountPerLevel.clear(); fetchCountPerLevel.clear(); expectedStrategyCallsPerLevel.clear(); happenedStrategyCallsPerLevel.clear(); happenedOnFieldValueCallsPerLevel.clear(); dispatchedLevels.clear(); // make sure the level is ready expectedFetchCountPerLevel.put(level, 1); expectedStrategyCallsPerLevel.put(level, 1); happenedStrategyCallsPerLevel.put(level, 1); } } } ================================================ FILE: graphql-java-kickstart/src/main/java/graphql/kickstart/execution/instrumentation/TrackingApproach.java ================================================ package graphql.kickstart.execution.instrumentation; import graphql.execution.ExecutionId; import graphql.execution.instrumentation.ExecutionStrategyInstrumentationContext; import graphql.execution.instrumentation.InstrumentationContext; import graphql.execution.instrumentation.InstrumentationState; import graphql.execution.instrumentation.parameters.InstrumentationExecutionStrategyParameters; import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters; public interface TrackingApproach extends InstrumentationState { /** * Handles creating any state for DataLoaderDispatcherInstrumentation * * @param executionId the execution to create state for. * @return individual state, if any for the execution. */ InstrumentationState createState(ExecutionId executionId); /** Dispatch dataloaders and clean up state. */ void dispatch(); /** * Handles approach specific logic for DataLoaderDispatcherInstrumentation. * * @param parameters parameters supplied to DataLoaderDispatcherInstrumentation * @return the instrumented context */ ExecutionStrategyInstrumentationContext beginExecutionStrategy( InstrumentationExecutionStrategyParameters parameters); /** * Handles approach specific logic for DataLoaderDispatcherInstrumentation. * * @param parameters parameters supplied to DataLoaderDispatcherInstrumentation * @return the instrumented context */ InstrumentationContext beginFieldFetch(InstrumentationFieldFetchParameters parameters); /** * Removes tracking state for an execution. * * @param executionId the execution to remove state for */ void removeTracking(ExecutionId executionId); } ================================================ FILE: graphql-java-kickstart/src/main/java/graphql/kickstart/execution/instrumentation/TrackingApproachException.java ================================================ package graphql.kickstart.execution.instrumentation; public class TrackingApproachException extends RuntimeException { TrackingApproachException(String message) { super(message); } } ================================================ FILE: graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/AtomicSubscriptionSubscription.java ================================================ package graphql.kickstart.execution.subscriptions; import java.util.concurrent.atomic.AtomicReference; import org.reactivestreams.Subscription; public class AtomicSubscriptionSubscription { private final AtomicReference reference = new AtomicReference<>(null); public void set(Subscription subscription) { if (reference.get() != null) { throw new IllegalStateException("Cannot overwrite subscription!"); } reference.set(subscription); } public Subscription get() { Subscription subscription = reference.get(); if (subscription == null) { throw new IllegalStateException("Subscription has not been initialized yet!"); } return subscription; } } ================================================ FILE: graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/DefaultSubscriptionSession.java ================================================ package graphql.kickstart.execution.subscriptions; import graphql.ExecutionResult; import graphql.execution.reactive.SingleSubscriberPublisher; import java.util.HashMap; import java.util.Map; import java.util.Objects; import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.reactivestreams.Publisher; import org.reactivestreams.Subscription; @Slf4j @RequiredArgsConstructor public class DefaultSubscriptionSession implements SubscriptionSession { @Getter private final GraphQLSubscriptionMapper mapper; private SingleSubscriberPublisher publisher = new SingleSubscriberPublisher<>(); private SessionSubscriptions subscriptions = new SessionSubscriptions(); @Override public void send(String message) { Objects.requireNonNull(message, "message is required"); publisher.offer(message); } @Override public void sendMessage(Object payload) { Objects.requireNonNull(payload, "payload is required"); send(mapper.serialize(payload)); } @Override public void subscribe(String id, Publisher dataPublisher) { dataPublisher.subscribe(new SessionSubscriber(this, id)); } @Override public void add(String id, Subscription subscription) { subscriptions.add(id, subscription); } @Override public void unsubscribe(String id) { subscriptions.cancel(id); } @Override public void sendDataMessage(String id, Object payload) { send(mapper.serialize(payload)); } @Override public void sendErrorMessage(String id, Object payload) { send(mapper.serialize(payload)); } @Override public void sendCompleteMessage(String id) { // default empty implementation } @Override public void close(String reason) { log.debug("Closing subscription session {}", getId()); subscriptions.close(); publisher.noMoreData(); } @Override public Map getUserProperties() { return new HashMap<>(); } @Override public boolean isOpen() { return true; } @Override public String getId() { return null; } @Override public SessionSubscriptions getSubscriptions() { return subscriptions; } @Override public Object unwrap() { throw new UnsupportedOperationException(); } @Override public Publisher getPublisher() { return publisher; } @Override public String toString() { return getId(); } } ================================================ FILE: graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/GraphQLSubscriptionInvocationInputFactory.java ================================================ package graphql.kickstart.execution.subscriptions; import graphql.kickstart.execution.GraphQLRequest; import graphql.kickstart.execution.input.GraphQLSingleInvocationInput; public interface GraphQLSubscriptionInvocationInputFactory { GraphQLSingleInvocationInput create(GraphQLRequest graphQLRequest, SubscriptionSession session); } ================================================ FILE: graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/GraphQLSubscriptionMapper.java ================================================ package graphql.kickstart.execution.subscriptions; import com.fasterxml.jackson.core.JsonProcessingException; import graphql.ExecutionResult; import graphql.GraphQLException; import graphql.kickstart.execution.GraphQLObjectMapper; import graphql.kickstart.execution.GraphQLRequest; import java.util.Map; import java.util.Objects; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor public class GraphQLSubscriptionMapper { private final GraphQLObjectMapper graphQLObjectMapper; public GraphQLRequest readGraphQLRequest(String payload) { Objects.requireNonNull(payload, "Payload is required"); try { return graphQLObjectMapper.getJacksonMapper().readValue(payload, GraphQLRequest.class); } catch (JsonProcessingException e) { throw new GraphQLException("Cannot read GraphQL request from payload '" + payload + "'", e); } } public GraphQLRequest convertGraphQLRequest(Object payload) { Objects.requireNonNull(payload, "Payload is required"); return graphQLObjectMapper.getJacksonMapper().convertValue(payload, GraphQLRequest.class); } public ExecutionResult sanitizeErrors(ExecutionResult executionResult) { return graphQLObjectMapper.sanitizeErrors(executionResult); } public boolean hasNoErrors(ExecutionResult executionResult) { return !graphQLObjectMapper.areErrorsPresent(executionResult); } public Map convertSanitizedExecutionResult(ExecutionResult executionResult) { return graphQLObjectMapper.convertSanitizedExecutionResult(executionResult, false); } public String serialize(Object payload) { try { return graphQLObjectMapper.getJacksonMapper().writeValueAsString(payload); } catch (JsonProcessingException e) { return e.getMessage(); } } } ================================================ FILE: graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/SessionSubscriber.java ================================================ package graphql.kickstart.execution.subscriptions; import static java.util.Collections.singletonList; import graphql.ExecutionResult; import graphql.GraphqlErrorBuilder; import graphql.execution.NonNullableFieldWasNullException; import graphql.kickstart.execution.error.GenericGraphQLError; import java.util.HashMap; import java.util.Map; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; @Slf4j @RequiredArgsConstructor class SessionSubscriber implements Subscriber { private final SubscriptionSession session; private final String id; private AtomicSubscriptionSubscription subscriptionReference = new AtomicSubscriptionSubscription(); @Override public void onSubscribe(Subscription subscription) { log.debug("Subscribe to execution result: {}", subscription); subscriptionReference.set(subscription); subscriptionReference.get().request(1); session.add(id, subscriptionReference.get()); } @Override public void onNext(ExecutionResult executionResult) { Map result = new HashMap<>(); result.put("data", executionResult.getData()); session.sendDataMessage(id, result); subscriptionReference.get().request(1); } @Override public void onError(Throwable throwable) { log.error("Subscription error", throwable); Map payload = new HashMap<>(); if (throwable.getCause() instanceof NonNullableFieldWasNullException) { NonNullableFieldWasNullException e = (NonNullableFieldWasNullException) throwable.getCause(); payload.put( "errors", singletonList( GraphqlErrorBuilder.newError().message(e.getMessage()).path(e.getPath()).build())); } else { payload.put("errors", singletonList(new GenericGraphQLError(throwable.getMessage()))); } session.unsubscribe(id); session.sendErrorMessage(id, payload); } @Override public void onComplete() { session.unsubscribe(id); session.sendCompleteMessage(id); } } ================================================ FILE: graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/SessionSubscriptions.java ================================================ package graphql.kickstart.execution.subscriptions; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import org.reactivestreams.Subscription; /** @author Andrew Potter */ public class SessionSubscriptions { private final Object lock = new Object(); private boolean closed = false; private Map subscriptions = new ConcurrentHashMap<>(); public void add(Subscription subscription) { add(getImplicitId(subscription), subscription); } public void add(String id, Subscription subscription) { synchronized (lock) { if (closed) { throw new IllegalStateException("Websocket was already closed!"); } subscriptions.put(id, subscription); } } public void cancel(Subscription subscription) { cancel(getImplicitId(subscription)); } public void cancel(String id) { Subscription subscription = subscriptions.remove(id); if (subscription != null) { subscription.cancel(); } } public void close() { synchronized (lock) { closed = true; subscriptions.forEach((k, v) -> v.cancel()); subscriptions.clear(); } } private String getImplicitId(Subscription subscription) { return String.valueOf(subscription.hashCode()); } public int getSubscriptionCount() { return subscriptions.size(); } } ================================================ FILE: graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/SubscriptionConnectionListener.java ================================================ package graphql.kickstart.execution.subscriptions; /** Marker interface */ public interface SubscriptionConnectionListener {} ================================================ FILE: graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/SubscriptionException.java ================================================ package graphql.kickstart.execution.subscriptions; public class SubscriptionException extends Exception { private final transient Object payload; public SubscriptionException() { this(null); } public SubscriptionException(Object payload) { this.payload = payload; } public Object getPayload() { return payload; } } ================================================ FILE: graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/SubscriptionProtocolFactory.java ================================================ package graphql.kickstart.execution.subscriptions; import java.util.function.Consumer; /** @author Andrew Potter */ public abstract class SubscriptionProtocolFactory { private final String protocol; protected SubscriptionProtocolFactory(String protocol) { this.protocol = protocol; } public String getProtocol() { return protocol; } public abstract Consumer createConsumer(SubscriptionSession session); public void shutdown() { // do nothing } } ================================================ FILE: graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/SubscriptionSession.java ================================================ package graphql.kickstart.execution.subscriptions; import graphql.ExecutionResult; import java.util.Map; import org.reactivestreams.Publisher; import org.reactivestreams.Subscription; public interface SubscriptionSession { void subscribe(String id, Publisher data); void add(String id, Subscription subscription); void unsubscribe(String id); void send(String message); void sendMessage(Object payload); void sendDataMessage(String id, Object payload); void sendErrorMessage(String id, Object payload); void sendCompleteMessage(String id); void close(String reason); /** * While the session is open, this method returns a Map that the developer may use to store * application specific information relating to this session instance. The developer may retrieve * information from this Map at any time between the opening of the session and during the * onClose() method. But outside that time, any information stored using this Map may no longer be * kept by the container. Web socket applications running on distributed implementations of the * web container should make any application specific objects stored here java.io.Serializable, or * the object may not be recreated after a failover. * * @return an editable Map of application data. */ Map getUserProperties(); boolean isOpen(); String getId(); SessionSubscriptions getSubscriptions(); Object unwrap(); Publisher getPublisher(); } ================================================ FILE: graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/apollo/ApolloCommandProvider.java ================================================ package graphql.kickstart.execution.subscriptions.apollo; import graphql.kickstart.execution.GraphQLInvoker; import graphql.kickstart.execution.subscriptions.GraphQLSubscriptionInvocationInputFactory; import graphql.kickstart.execution.subscriptions.GraphQLSubscriptionMapper; import graphql.kickstart.execution.subscriptions.apollo.OperationMessage.Type; import java.util.Collection; import java.util.EnumMap; public class ApolloCommandProvider { private final EnumMap commands = new EnumMap<>(Type.class); public ApolloCommandProvider( GraphQLSubscriptionMapper mapper, GraphQLSubscriptionInvocationInputFactory invocationInputFactory, GraphQLInvoker graphQLInvoker, Collection connectionListeners) { commands.put( Type.GQL_CONNECTION_INIT, new SubscriptionConnectionInitCommand(connectionListeners)); commands.put( Type.GQL_START, new SubscriptionStartCommand( mapper, invocationInputFactory, graphQLInvoker, connectionListeners)); commands.put(Type.GQL_STOP, new SubscriptionStopCommand(connectionListeners)); commands.put( Type.GQL_CONNECTION_TERMINATE, new SubscriptionConnectionTerminateCommand(connectionListeners)); } public SubscriptionCommand getByType(Type type) { if (commands.containsKey(type)) { return commands.get(type); } throw new IllegalStateException("No command found for type " + type); } } ================================================ FILE: graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/apollo/ApolloSubscriptionConnectionListener.java ================================================ package graphql.kickstart.execution.subscriptions.apollo; import graphql.kickstart.execution.subscriptions.SubscriptionConnectionListener; import graphql.kickstart.execution.subscriptions.SubscriptionSession; public interface ApolloSubscriptionConnectionListener extends SubscriptionConnectionListener { default void onConnect(SubscriptionSession session, OperationMessage message) { // do nothing } default void onStart(SubscriptionSession session, OperationMessage message) { // do nothing } default void onStop(SubscriptionSession session, OperationMessage message) { // do nothing } default void onTerminate(SubscriptionSession session, OperationMessage message) { // do nothing } default void shutdown() { // do nothing } } ================================================ FILE: graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/apollo/ApolloSubscriptionConsumer.java ================================================ package graphql.kickstart.execution.subscriptions.apollo; import com.fasterxml.jackson.core.JsonProcessingException; import graphql.kickstart.execution.GraphQLObjectMapper; import graphql.kickstart.execution.subscriptions.SubscriptionSession; import graphql.kickstart.execution.subscriptions.apollo.OperationMessage.Type; import java.util.function.Consumer; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @Slf4j @RequiredArgsConstructor public class ApolloSubscriptionConsumer implements Consumer { private final SubscriptionSession session; private final GraphQLObjectMapper objectMapper; private final ApolloCommandProvider commandProvider; @Override public void accept(String request) { try { OperationMessage message = objectMapper.getJacksonMapper().readValue(request, OperationMessage.class); SubscriptionCommand command = commandProvider.getByType(message.getType()); command.apply(session, message); } catch (JsonProcessingException e) { log.error("Cannot read subscription command '{}'", request, e); session.sendMessage(new OperationMessage(Type.GQL_CONNECTION_ERROR, null, e.getMessage())); } } } ================================================ FILE: graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/apollo/ApolloSubscriptionKeepAliveRunner.java ================================================ package graphql.kickstart.execution.subscriptions.apollo; import graphql.kickstart.execution.subscriptions.SubscriptionSession; import java.time.Duration; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import lombok.extern.slf4j.Slf4j; @Slf4j class ApolloSubscriptionKeepAliveRunner { private static final int EXECUTOR_POOL_SIZE = 10; private final ScheduledExecutorService executor; private final OperationMessage keepAliveMessage; private final Map> futures; private final long keepAliveIntervalSeconds; ApolloSubscriptionKeepAliveRunner(Duration keepAliveInterval) { this.keepAliveMessage = OperationMessage.newKeepAliveMessage(); this.executor = Executors.newScheduledThreadPool(EXECUTOR_POOL_SIZE); this.futures = new ConcurrentHashMap<>(); this.keepAliveIntervalSeconds = keepAliveInterval.getSeconds(); } void keepAlive(SubscriptionSession session) { futures.computeIfAbsent(session, this::startKeepAlive); } private ScheduledFuture startKeepAlive(SubscriptionSession session) { return executor.scheduleAtFixedRate( () -> { try { if (session.isOpen()) { session.sendMessage(keepAliveMessage); } else { log.debug("Session {} appears to be closed. Aborting keep alive", session.getId()); abort(session); } } catch (Exception t) { log.error( "Cannot send keep alive message to session {}. Aborting keep alive", session.getId(), t); abort(session); } }, 0, keepAliveIntervalSeconds, TimeUnit.SECONDS); } void abort(SubscriptionSession session) { Future future = futures.remove(session); if (future != null) { future.cancel(true); } } void shutdown() { this.executor.shutdown(); } } ================================================ FILE: graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/apollo/ApolloSubscriptionProtocolFactory.java ================================================ package graphql.kickstart.execution.subscriptions.apollo; import graphql.kickstart.execution.GraphQLInvoker; import graphql.kickstart.execution.GraphQLObjectMapper; import graphql.kickstart.execution.subscriptions.GraphQLSubscriptionInvocationInputFactory; import graphql.kickstart.execution.subscriptions.GraphQLSubscriptionMapper; import graphql.kickstart.execution.subscriptions.SubscriptionProtocolFactory; import graphql.kickstart.execution.subscriptions.SubscriptionSession; import java.time.Duration; import java.util.Collection; import java.util.HashSet; import java.util.Set; import java.util.function.Consumer; import lombok.Getter; /** @author Andrew Potter */ public class ApolloSubscriptionProtocolFactory extends SubscriptionProtocolFactory { public static final int KEEP_ALIVE_INTERVAL = 15; @Getter private final GraphQLObjectMapper objectMapper; private final ApolloCommandProvider commandProvider; private KeepAliveSubscriptionConnectionListener keepAlive; public ApolloSubscriptionProtocolFactory( GraphQLObjectMapper objectMapper, GraphQLSubscriptionInvocationInputFactory invocationInputFactory, GraphQLInvoker graphQLInvoker) { this( objectMapper, invocationInputFactory, graphQLInvoker, Duration.ofSeconds(KEEP_ALIVE_INTERVAL)); } public ApolloSubscriptionProtocolFactory( GraphQLObjectMapper objectMapper, GraphQLSubscriptionInvocationInputFactory invocationInputFactory, GraphQLInvoker graphQLInvoker, Duration keepAliveInterval) { this(objectMapper, invocationInputFactory, graphQLInvoker, null, keepAliveInterval); } public ApolloSubscriptionProtocolFactory( GraphQLObjectMapper objectMapper, GraphQLSubscriptionInvocationInputFactory invocationInputFactory, GraphQLInvoker graphQLInvoker, Collection connectionListeners) { this( objectMapper, invocationInputFactory, graphQLInvoker, connectionListeners, Duration.ofSeconds(KEEP_ALIVE_INTERVAL)); } public ApolloSubscriptionProtocolFactory( GraphQLObjectMapper objectMapper, GraphQLSubscriptionInvocationInputFactory invocationInputFactory, GraphQLInvoker graphQLInvoker, Collection connectionListeners, Duration keepAliveInterval) { super("graphql-ws"); this.objectMapper = objectMapper; Set listeners = new HashSet<>(); if (connectionListeners != null) { listeners.addAll(connectionListeners); } if (keepAliveInterval != null && listeners.stream() .noneMatch(KeepAliveSubscriptionConnectionListener.class::isInstance)) { keepAlive = new KeepAliveSubscriptionConnectionListener(keepAliveInterval); listeners.add(keepAlive); } commandProvider = new ApolloCommandProvider( new GraphQLSubscriptionMapper(objectMapper), invocationInputFactory, graphQLInvoker, listeners); } @Override public Consumer createConsumer(SubscriptionSession session) { return new ApolloSubscriptionConsumer(session, objectMapper, commandProvider); } @Override public void shutdown() { if (keepAlive != null) { keepAlive.shutdown(); } } } ================================================ FILE: graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/apollo/ApolloSubscriptionSession.java ================================================ package graphql.kickstart.execution.subscriptions.apollo; import graphql.kickstart.execution.subscriptions.DefaultSubscriptionSession; import graphql.kickstart.execution.subscriptions.GraphQLSubscriptionMapper; import graphql.kickstart.execution.subscriptions.apollo.OperationMessage.Type; import lombok.extern.slf4j.Slf4j; @Slf4j public class ApolloSubscriptionSession extends DefaultSubscriptionSession { public ApolloSubscriptionSession(GraphQLSubscriptionMapper mapper) { super(mapper); } @Override public void sendDataMessage(String id, Object payload) { sendMessage(new OperationMessage(Type.GQL_DATA, id, payload)); } @Override public void sendErrorMessage(String id, Object payload) { sendMessage(new OperationMessage(Type.GQL_ERROR, id, payload)); } @Override public void sendCompleteMessage(String id) { sendMessage(new OperationMessage(Type.GQL_COMPLETE, id, null)); } } ================================================ FILE: graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/apollo/KeepAliveSubscriptionConnectionListener.java ================================================ package graphql.kickstart.execution.subscriptions.apollo; import graphql.kickstart.execution.subscriptions.SubscriptionSession; import java.time.Duration; public class KeepAliveSubscriptionConnectionListener implements ApolloSubscriptionConnectionListener { protected final ApolloSubscriptionKeepAliveRunner keepAliveRunner; public KeepAliveSubscriptionConnectionListener() { this(Duration.ofSeconds(15)); } public KeepAliveSubscriptionConnectionListener(Duration keepAliveInterval) { keepAliveRunner = new ApolloSubscriptionKeepAliveRunner(keepAliveInterval); } @Override public void onConnect(SubscriptionSession session, OperationMessage message) { keepAliveRunner.keepAlive(session); } @Override public void onStart(SubscriptionSession session, OperationMessage message) { // do nothing } @Override public void onStop(SubscriptionSession session, OperationMessage message) { // do nothing } @Override public void onTerminate(SubscriptionSession session, OperationMessage message) { keepAliveRunner.abort(session); } @Override public void shutdown() { keepAliveRunner.shutdown(); } } ================================================ FILE: graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/apollo/OperationMessage.java ================================================ package graphql.kickstart.execution.subscriptions.apollo; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonValue; import java.util.HashMap; import java.util.Map; import lombok.AllArgsConstructor; import lombok.NoArgsConstructor; @NoArgsConstructor @AllArgsConstructor @JsonInclude(JsonInclude.Include.NON_NULL) public class OperationMessage { private Type type; private String id; private Object payload; public static OperationMessage newKeepAliveMessage() { return new OperationMessage(Type.GQL_CONNECTION_KEEP_ALIVE, null, null); } public Type getType() { return type; } public String getId() { return id; } public Object getPayload() { return payload; } public enum Type { // Server Messages GQL_CONNECTION_ACK("connection_ack"), GQL_CONNECTION_ERROR("connection_error"), GQL_CONNECTION_KEEP_ALIVE("ka"), GQL_DATA("data"), GQL_ERROR("error"), GQL_COMPLETE("complete"), // Client Messages GQL_CONNECTION_INIT("connection_init"), GQL_CONNECTION_TERMINATE("connection_terminate"), GQL_START("start"), GQL_STOP("stop"); private static final Map reverseLookup = new HashMap<>(); static { for (Type type : Type.values()) { reverseLookup.put(type.getValue(), type); } } private final String value; Type(String value) { this.value = value; } @JsonCreator public static Type findType(String value) { return reverseLookup.get(value); } @JsonValue public String getValue() { return value; } } } ================================================ FILE: graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/apollo/SubscriptionCommand.java ================================================ package graphql.kickstart.execution.subscriptions.apollo; import graphql.kickstart.execution.subscriptions.SubscriptionSession; interface SubscriptionCommand { void apply(SubscriptionSession session, OperationMessage message); } ================================================ FILE: graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/apollo/SubscriptionConnectionInitCommand.java ================================================ package graphql.kickstart.execution.subscriptions.apollo; import graphql.kickstart.execution.subscriptions.SubscriptionSession; import graphql.kickstart.execution.subscriptions.apollo.OperationMessage.Type; import java.util.Collection; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @Slf4j @RequiredArgsConstructor class SubscriptionConnectionInitCommand implements SubscriptionCommand { private final Collection connectionListeners; @Override public void apply(SubscriptionSession session, OperationMessage message) { log.debug("Apollo subscription connection init: {}", session); try { connectionListeners.forEach(it -> it.onConnect(session, message)); session.sendMessage(new OperationMessage(Type.GQL_CONNECTION_ACK, message.getId(), null)); } catch (Exception t) { log.error("Cannot initialize subscription command '{}'", message, t); session.sendMessage( new OperationMessage(Type.GQL_CONNECTION_ERROR, message.getId(), t.getMessage())); } } } ================================================ FILE: graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/apollo/SubscriptionConnectionTerminateCommand.java ================================================ package graphql.kickstart.execution.subscriptions.apollo; import static graphql.kickstart.execution.subscriptions.apollo.OperationMessage.Type.GQL_CONNECTION_TERMINATE; import graphql.kickstart.execution.subscriptions.SubscriptionSession; import java.util.Collection; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @Slf4j @RequiredArgsConstructor class SubscriptionConnectionTerminateCommand implements SubscriptionCommand { private final Collection connectionListeners; @Override public void apply(SubscriptionSession session, OperationMessage message) { connectionListeners.forEach(it -> it.onTerminate(session, message)); session.close("client requested " + GQL_CONNECTION_TERMINATE.getValue()); } } ================================================ FILE: graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/apollo/SubscriptionStartCommand.java ================================================ package graphql.kickstart.execution.subscriptions.apollo; import static graphql.kickstart.execution.subscriptions.apollo.OperationMessage.Type.GQL_ERROR; import graphql.ExecutionResult; import graphql.kickstart.execution.GraphQLInvoker; import graphql.kickstart.execution.GraphQLRequest; import graphql.kickstart.execution.input.GraphQLSingleInvocationInput; import graphql.kickstart.execution.subscriptions.GraphQLSubscriptionInvocationInputFactory; import graphql.kickstart.execution.subscriptions.GraphQLSubscriptionMapper; import graphql.kickstart.execution.subscriptions.SubscriptionSession; import java.util.Collection; import java.util.Objects; import java.util.concurrent.CompletableFuture; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @Slf4j @RequiredArgsConstructor class SubscriptionStartCommand implements SubscriptionCommand { private final GraphQLSubscriptionMapper mapper; private final GraphQLSubscriptionInvocationInputFactory invocationInputFactory; private final GraphQLInvoker graphQLInvoker; private final Collection connectionListeners; @Override public void apply(SubscriptionSession session, OperationMessage message) { log.debug("Apollo subscription start: {} --> {}", session, message.getPayload()); connectionListeners.forEach(it -> it.onStart(session, message)); CompletableFuture executionResult = executeAsync(message.getPayload(), session); executionResult.thenAccept(result -> handleSubscriptionStart(session, message.getId(), result)); } private CompletableFuture executeAsync( Object payload, SubscriptionSession session) { Objects.requireNonNull(payload, "Payload is required"); GraphQLRequest graphQLRequest = mapper.convertGraphQLRequest(payload); GraphQLSingleInvocationInput invocationInput = invocationInputFactory.create(graphQLRequest, session); return graphQLInvoker.executeAsync(invocationInput); } private void handleSubscriptionStart( SubscriptionSession session, String id, ExecutionResult executionResult) { ExecutionResult sanitizedExecutionResult = mapper.sanitizeErrors(executionResult); if (mapper.hasNoErrors(sanitizedExecutionResult)) { session.subscribe(id, sanitizedExecutionResult.getData()); } else { Object payload = mapper.convertSanitizedExecutionResult(sanitizedExecutionResult); session.sendMessage(new OperationMessage(GQL_ERROR, id, payload)); } } } ================================================ FILE: graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/apollo/SubscriptionStopCommand.java ================================================ package graphql.kickstart.execution.subscriptions.apollo; import graphql.kickstart.execution.subscriptions.SubscriptionSession; import java.util.Collection; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor class SubscriptionStopCommand implements SubscriptionCommand { private final Collection connectionListeners; @Override public void apply(SubscriptionSession session, OperationMessage message) { connectionListeners.forEach(it -> it.onStop(session, message)); session.unsubscribe(message.getId()); } } ================================================ FILE: graphql-java-servlet/bnd.bnd ================================================ Export-Package: graphql.kickstart.servlet.* Import-Package: !lombok,* Require-Capability: osgi.extender; filter:="(&(osgi.extender=osgi.component)(version>=1.3)(!(version>=2.0)))" ================================================ FILE: graphql-java-servlet/build.gradle ================================================ buildscript { repositories { mavenCentral() } } apply plugin: 'groovy' apply plugin: 'java-library-distribution' apply plugin: 'biz.aQute.bnd.builder' jar { bndfile = 'bnd.bnd' } dependencies { api(project(':graphql-java-kickstart')) // Servlet compileOnly "jakarta.servlet:jakarta.servlet-api:6.1.0" compileOnly "jakarta.websocket:jakarta.websocket-api:2.2.0" compileOnly "jakarta.websocket:jakarta.websocket-client-api:2.2.0" implementation "org.slf4j:slf4j-api:$LIB_SLF4J_VER" // OSGi compileOnly 'org.osgi:org.osgi.core:6.0.0' compileOnly 'org.osgi:org.osgi.service.cm:1.6.1' compileOnly 'org.osgi:org.osgi.service.component:1.5.1' compileOnly 'org.osgi:org.osgi.service.component.annotations:1.5.1' compileOnly 'org.osgi:org.osgi.service.metatype.annotations:1.4.1' compileOnly 'org.osgi:org.osgi.annotation:6.0.0' testImplementation 'io.github.graphql-java:graphql-java-annotations:9.1' // Unit testing testImplementation "org.apache.groovy:groovy-all:4.0.23" testImplementation "org.spockframework:spock-core:2.3-groovy-4.0" testRuntimeOnly "net.bytebuddy:byte-buddy:1.15.2" testRuntimeOnly "org.objenesis:objenesis:3.4" testImplementation "org.slf4j:slf4j-simple:$LIB_SLF4J_VER" testImplementation "org.springframework:spring-test:6.1.13" testRuntimeOnly "org.springframework:spring-web:6.1.13" testImplementation 'com.google.guava:guava:33.3.1-jre' testImplementation "jakarta.servlet:jakarta.servlet-api:6.1.0" testImplementation "jakarta.websocket:jakarta.websocket-api:2.2.0" testImplementation "jakarta.websocket:jakarta.websocket-client-api:2.2.0" } ================================================ FILE: graphql-java-servlet/src/main/java/graphql/kickstart/servlet/AbstractGraphQLHttpServlet.java ================================================ package graphql.kickstart.servlet; import static graphql.kickstart.execution.GraphQLRequest.createQueryOnlyRequest; import graphql.ExecutionResult; import graphql.kickstart.execution.GraphQLRequest; import graphql.kickstart.execution.input.GraphQLSingleInvocationInput; import graphql.kickstart.servlet.core.GraphQLMBean; import graphql.kickstart.servlet.core.GraphQLServletListener; import graphql.schema.GraphQLFieldDefinition; import jakarta.servlet.Servlet; import jakarta.servlet.http.HttpServlet; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.extern.slf4j.Slf4j; /** @author Andrew Potter */ @Slf4j public abstract class AbstractGraphQLHttpServlet extends HttpServlet implements Servlet, GraphQLMBean { protected abstract GraphQLConfiguration getConfiguration(); public void addListener(GraphQLServletListener servletListener) { getConfiguration().add(servletListener); } public void removeListener(GraphQLServletListener servletListener) { getConfiguration().remove(servletListener); } @Override public String[] getQueries() { return getConfiguration() .getInvocationInputFactory() .getSchemaProvider() .getSchema() .getQueryType() .getFieldDefinitions() .stream() .map(GraphQLFieldDefinition::getName) .toArray(String[]::new); } @Override public String[] getMutations() { return getConfiguration() .getInvocationInputFactory() .getSchemaProvider() .getSchema() .getMutationType() .getFieldDefinitions() .stream() .map(GraphQLFieldDefinition::getName) .toArray(String[]::new); } @Override public String executeQuery(String query) { try { GraphQLRequest graphQLRequest = createQueryOnlyRequest(query); GraphQLSingleInvocationInput invocationInput = getConfiguration().getInvocationInputFactory().create(graphQLRequest); ExecutionResult result = getConfiguration().getGraphQLInvoker().query(invocationInput).getResult(); return getConfiguration().getObjectMapper().serializeResultAsJson(result); } catch (Exception e) { return e.getMessage(); } } @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) { doRequest(req, resp); } @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) { doRequest(req, resp); } private void doRequest(HttpServletRequest request, HttpServletResponse response) { try { getConfiguration().getHttpRequestHandler().handle(request, response); } catch (Exception t) { log.error("Error executing GraphQL request!", t); } } } ================================================ FILE: graphql-java-servlet/src/main/java/graphql/kickstart/servlet/AbstractGraphQLInvocationInputParser.java ================================================ package graphql.kickstart.servlet; import graphql.kickstart.execution.GraphQLObjectMapper; import graphql.kickstart.execution.context.ContextSetting; import graphql.kickstart.servlet.input.GraphQLInvocationInputFactory; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor abstract class AbstractGraphQLInvocationInputParser implements GraphQLInvocationInputParser { final GraphQLInvocationInputFactory invocationInputFactory; final GraphQLObjectMapper graphQLObjectMapper; final ContextSetting contextSetting; boolean isSingleQuery(String query) { return query != null && !query.trim().isEmpty() && !query.trim().startsWith("["); } boolean isBatchedQuery(String query) { return query != null && query.trim().startsWith("["); } } ================================================ FILE: graphql-java-servlet/src/main/java/graphql/kickstart/servlet/AsyncTaskDecorator.java ================================================ package graphql.kickstart.servlet; public interface AsyncTaskDecorator { Runnable decorate(Runnable runnable); } ================================================ FILE: graphql-java-servlet/src/main/java/graphql/kickstart/servlet/AsyncTaskExecutor.java ================================================ package graphql.kickstart.servlet; import java.util.concurrent.Executor; import lombok.NonNull; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor class AsyncTaskExecutor implements Executor { private final Executor executor; private final AsyncTaskDecorator taskDecorator; @Override public void execute(@NonNull Runnable command) { if (taskDecorator != null) { Runnable decorated = taskDecorator.decorate(command); executor.execute(decorated); } else { executor.execute(command); } } } ================================================ FILE: graphql-java-servlet/src/main/java/graphql/kickstart/servlet/AsyncTimeoutListener.java ================================================ package graphql.kickstart.servlet; import java.io.IOException; import jakarta.servlet.AsyncEvent; import jakarta.servlet.AsyncListener; interface AsyncTimeoutListener extends AsyncListener { default void onComplete(AsyncEvent event) throws IOException {} default void onError(AsyncEvent event) throws IOException {} default void onStartAsync(AsyncEvent event) throws IOException {} } ================================================ FILE: graphql-java-servlet/src/main/java/graphql/kickstart/servlet/BatchedQueryResponseWriter.java ================================================ package graphql.kickstart.servlet; import graphql.ExecutionResult; import graphql.kickstart.execution.GraphQLObjectMapper; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @Slf4j @RequiredArgsConstructor class BatchedQueryResponseWriter implements QueryResponseWriter { private final List results; private final GraphQLObjectMapper graphQLObjectMapper; @Override public void write(HttpServletRequest request, HttpServletResponse response) throws IOException { response.setCharacterEncoding(StandardCharsets.UTF_8.name()); response.setContentType(HttpRequestHandler.APPLICATION_JSON_UTF8); response.setStatus(HttpRequestHandler.STATUS_OK); // Use direct serialization to byte arrays and avoid any string concatenation to save multiple // GiB of memory allocation during large response processing. List serializedResults = new ArrayList<>(2 * results.size() + 1); if (!results.isEmpty()) { serializedResults.add("[".getBytes(StandardCharsets.UTF_8)); } else { serializedResults.add("[]".getBytes(StandardCharsets.UTF_8)); } long totalLength = serializedResults.get(0).length; // '[', ',' and ']' are all 1 byte in UTF-8. for (int i = 0; i < results.size(); i++) { byte[] currentResult = graphQLObjectMapper.serializeResultAsBytes(results.get(i)); serializedResults.add(currentResult); if (i != results.size() - 1) { serializedResults.add(",".getBytes(StandardCharsets.UTF_8)); } else { serializedResults.add("]".getBytes(StandardCharsets.UTF_8)); } totalLength += currentResult.length + 1; // result.length + ',' or ']' } if (totalLength > Integer.MAX_VALUE) { throw new IllegalStateException( "Response size exceed 2GiB. Query will fail. Seen size: " + totalLength); } response.setContentLength((int) totalLength); for (byte[] result : serializedResults) { response.getOutputStream().write(result); } } } ================================================ FILE: graphql-java-servlet/src/main/java/graphql/kickstart/servlet/ConfiguredGraphQLHttpServlet.java ================================================ package graphql.kickstart.servlet; import java.util.Objects; class ConfiguredGraphQLHttpServlet extends GraphQLHttpServlet { private final GraphQLConfiguration configuration; ConfiguredGraphQLHttpServlet(GraphQLConfiguration configuration) { this.configuration = Objects.requireNonNull(configuration, "configuration is required"); } @Override protected GraphQLConfiguration getConfiguration() { return configuration; } } ================================================ FILE: graphql-java-servlet/src/main/java/graphql/kickstart/servlet/ErrorQueryResponseWriter.java ================================================ package graphql.kickstart.servlet; import java.io.IOException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor class ErrorQueryResponseWriter implements QueryResponseWriter { private final int statusCode; private final String message; @Override public void write(HttpServletRequest request, HttpServletResponse response) throws IOException { response.sendError(statusCode, message); } } ================================================ FILE: graphql-java-servlet/src/main/java/graphql/kickstart/servlet/ExecutionResultSubscriber.java ================================================ package graphql.kickstart.servlet; import graphql.ExecutionResult; import graphql.kickstart.execution.GraphQLObjectMapper; import java.io.IOException; import java.io.Writer; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicReference; import jakarta.servlet.AsyncContext; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; class ExecutionResultSubscriber implements Subscriber { private final AtomicReference subscriptionRef; private final AsyncContext asyncContext; private final GraphQLObjectMapper graphQLObjectMapper; private final CountDownLatch completedLatch = new CountDownLatch(1); ExecutionResultSubscriber( AtomicReference subscriptionRef, AsyncContext asyncContext, GraphQLObjectMapper graphQLObjectMapper) { this.subscriptionRef = subscriptionRef; this.asyncContext = asyncContext; this.graphQLObjectMapper = graphQLObjectMapper; } @Override public void onSubscribe(Subscription subscription) { subscriptionRef.set(subscription); subscriptionRef.get().request(1); } @Override public void onNext(ExecutionResult executionResult) { try { Writer writer = asyncContext.getResponse().getWriter(); writer.write("data: "); writer.write(graphQLObjectMapper.serializeResultAsJson(executionResult)); writer.write("\n\n"); writer.flush(); subscriptionRef.get().request(1); } catch (IOException ignored) { // ignore } } @Override public void onError(Throwable t) { asyncContext.complete(); completedLatch.countDown(); } @Override public void onComplete() { asyncContext.complete(); completedLatch.countDown(); } void await() throws InterruptedException { completedLatch.await(); } } ================================================ FILE: graphql-java-servlet/src/main/java/graphql/kickstart/servlet/GraphQLConfiguration.java ================================================ package graphql.kickstart.servlet; import graphql.kickstart.execution.GraphQLInvoker; import graphql.kickstart.execution.GraphQLObjectMapper; import graphql.kickstart.execution.GraphQLQueryInvoker; import graphql.kickstart.execution.context.ContextSetting; import graphql.kickstart.servlet.cache.CachingHttpRequestInvoker; import graphql.kickstart.servlet.cache.GraphQLResponseCacheManager; import graphql.kickstart.servlet.config.DefaultGraphQLSchemaServletProvider; import graphql.kickstart.servlet.config.GraphQLSchemaServletProvider; import graphql.kickstart.servlet.context.GraphQLServletContextBuilder; import graphql.kickstart.servlet.core.GraphQLServletListener; import graphql.kickstart.servlet.core.GraphQLServletRootObjectBuilder; import graphql.kickstart.servlet.input.BatchInputPreProcessor; import graphql.kickstart.servlet.input.GraphQLInvocationInputFactory; import graphql.kickstart.servlet.input.NoOpBatchInputPreProcessor; import graphql.schema.GraphQLSchema; import java.util.ArrayList; import java.util.List; import java.util.concurrent.Executor; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.function.Supplier; import lombok.Getter; public class GraphQLConfiguration { private final GraphQLInvocationInputFactory invocationInputFactory; private final Supplier batchInputPreProcessor; private final GraphQLInvoker graphQLInvoker; private final GraphQLObjectMapper objectMapper; private final List listeners; private final long subscriptionTimeout; @Getter private final long asyncTimeout; private final ContextSetting contextSetting; private final GraphQLResponseCacheManager responseCacheManager; @Getter private final Executor asyncExecutor; private HttpRequestHandler requestHandler; private GraphQLConfiguration( GraphQLInvocationInputFactory invocationInputFactory, GraphQLInvoker graphQLInvoker, GraphQLQueryInvoker queryInvoker, GraphQLObjectMapper objectMapper, List listeners, long subscriptionTimeout, long asyncTimeout, ContextSetting contextSetting, Supplier batchInputPreProcessor, GraphQLResponseCacheManager responseCacheManager, Executor asyncExecutor) { this.invocationInputFactory = invocationInputFactory; this.asyncExecutor = asyncExecutor; this.graphQLInvoker = graphQLInvoker != null ? graphQLInvoker : queryInvoker.toGraphQLInvoker(); this.objectMapper = objectMapper; this.listeners = listeners; this.subscriptionTimeout = subscriptionTimeout; this.asyncTimeout = asyncTimeout; this.contextSetting = contextSetting; this.batchInputPreProcessor = batchInputPreProcessor; this.responseCacheManager = responseCacheManager; } public static GraphQLConfiguration.Builder with(GraphQLSchema schema) { return with(new DefaultGraphQLSchemaServletProvider(schema)); } public static GraphQLConfiguration.Builder with(GraphQLSchemaServletProvider schemaProvider) { return new Builder(GraphQLInvocationInputFactory.newBuilder(schemaProvider)); } public static GraphQLConfiguration.Builder with( GraphQLInvocationInputFactory invocationInputFactory) { return new Builder(invocationInputFactory); } public GraphQLInvocationInputFactory getInvocationInputFactory() { return invocationInputFactory; } public GraphQLInvoker getGraphQLInvoker() { return graphQLInvoker; } public GraphQLObjectMapper getObjectMapper() { return objectMapper; } public List getListeners() { return new ArrayList<>(listeners); } public void add(GraphQLServletListener listener) { listeners.add(listener); } public boolean remove(GraphQLServletListener listener) { return listeners.remove(listener); } public long getSubscriptionTimeout() { return subscriptionTimeout; } public ContextSetting getContextSetting() { return contextSetting; } public BatchInputPreProcessor getBatchInputPreProcessor() { return batchInputPreProcessor.get(); } public GraphQLResponseCacheManager getResponseCacheManager() { return responseCacheManager; } public HttpRequestHandler getHttpRequestHandler() { if (requestHandler == null) { requestHandler = createHttpRequestHandler(); } return requestHandler; } private HttpRequestHandler createHttpRequestHandler() { if (responseCacheManager == null) { return new HttpRequestHandlerImpl(this); } else { return new HttpRequestHandlerImpl(this, new CachingHttpRequestInvoker(this)); } } public static class Builder { private GraphQLInvocationInputFactory.Builder invocationInputFactoryBuilder; private GraphQLInvocationInputFactory invocationInputFactory; private GraphQLInvoker graphQLInvoker; private GraphQLQueryInvoker queryInvoker = GraphQLQueryInvoker.newBuilder().build(); private GraphQLObjectMapper objectMapper = GraphQLObjectMapper.newBuilder().build(); private List listeners = new ArrayList<>(); private long subscriptionTimeout = 0; private long asyncTimeout = 30000; private ContextSetting contextSetting = ContextSetting.PER_QUERY; private Supplier batchInputPreProcessorSupplier = NoOpBatchInputPreProcessor::new; private GraphQLResponseCacheManager responseCacheManager; private int asyncCorePoolSize = 10; private int asyncMaxPoolSize = 200; private Executor asyncExecutor; private AsyncTaskDecorator asyncTaskDecorator; private Builder(GraphQLInvocationInputFactory.Builder invocationInputFactoryBuilder) { this.invocationInputFactoryBuilder = invocationInputFactoryBuilder; } private Builder(GraphQLInvocationInputFactory invocationInputFactory) { this.invocationInputFactory = invocationInputFactory; } public Builder with(GraphQLInvoker graphQLInvoker) { this.graphQLInvoker = graphQLInvoker; return this; } public Builder with(GraphQLQueryInvoker queryInvoker) { if (queryInvoker != null) { this.queryInvoker = queryInvoker; } return this; } public Builder with(GraphQLObjectMapper objectMapper) { if (objectMapper != null) { this.objectMapper = objectMapper; } return this; } public Builder with(List listeners) { if (listeners != null) { this.listeners = listeners; } return this; } public Builder with(GraphQLServletContextBuilder contextBuilder) { this.invocationInputFactoryBuilder.withGraphQLContextBuilder(contextBuilder); return this; } public Builder with(GraphQLServletRootObjectBuilder rootObjectBuilder) { this.invocationInputFactoryBuilder.withGraphQLRootObjectBuilder(rootObjectBuilder); return this; } public Builder with(long subscriptionTimeout) { this.subscriptionTimeout = subscriptionTimeout; return this; } public Builder asyncTimeout(long asyncTimeout) { this.asyncTimeout = asyncTimeout; return this; } public Builder with(Executor asyncExecutor) { this.asyncExecutor = asyncExecutor; return this; } public Builder asyncCorePoolSize(int asyncCorePoolSize) { this.asyncCorePoolSize = asyncCorePoolSize; return this; } public Builder asyncMaxPoolSize(int asyncMaxPoolSize) { this.asyncMaxPoolSize = asyncMaxPoolSize; return this; } public Builder with(ContextSetting contextSetting) { if (contextSetting != null) { this.contextSetting = contextSetting; } return this; } public Builder with(BatchInputPreProcessor batchInputPreProcessor) { if (batchInputPreProcessor != null) { this.batchInputPreProcessorSupplier = () -> batchInputPreProcessor; } return this; } public Builder with(Supplier batchInputPreProcessor) { if (batchInputPreProcessor != null) { this.batchInputPreProcessorSupplier = batchInputPreProcessor; } return this; } public Builder with(GraphQLResponseCacheManager responseCache) { this.responseCacheManager = responseCache; return this; } public Builder with(AsyncTaskDecorator asyncTaskDecorator) { this.asyncTaskDecorator = asyncTaskDecorator; return this; } private Executor getAsyncExecutor() { if (asyncExecutor != null) { return asyncExecutor; } return new ThreadPoolExecutor( asyncCorePoolSize, asyncMaxPoolSize, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<>(Integer.MAX_VALUE)); } private Executor getAsyncTaskExecutor() { return new AsyncTaskExecutor(getAsyncExecutor(), asyncTaskDecorator); } public GraphQLConfiguration build() { return new GraphQLConfiguration( this.invocationInputFactory != null ? this.invocationInputFactory : invocationInputFactoryBuilder.build(), graphQLInvoker, queryInvoker, objectMapper, listeners, subscriptionTimeout, asyncTimeout, contextSetting, batchInputPreProcessorSupplier, responseCacheManager, getAsyncTaskExecutor()); } } } ================================================ FILE: graphql-java-servlet/src/main/java/graphql/kickstart/servlet/GraphQLGetInvocationInputParser.java ================================================ package graphql.kickstart.servlet; import graphql.GraphQLException; import graphql.kickstart.execution.GraphQLObjectMapper; import graphql.kickstart.execution.GraphQLRequest; import graphql.kickstart.execution.context.ContextSetting; import graphql.kickstart.execution.input.GraphQLInvocationInput; import graphql.kickstart.servlet.input.GraphQLInvocationInputFactory; import java.io.IOException; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.extern.slf4j.Slf4j; @Slf4j class GraphQLGetInvocationInputParser extends AbstractGraphQLInvocationInputParser { GraphQLGetInvocationInputParser( GraphQLInvocationInputFactory invocationInputFactory, GraphQLObjectMapper graphQLObjectMapper, ContextSetting contextSetting) { super(invocationInputFactory, graphQLObjectMapper, contextSetting); } public GraphQLInvocationInput getGraphQLInvocationInput( HttpServletRequest request, HttpServletResponse response) throws IOException { if (isIntrospectionQuery(request)) { GraphQLRequest graphqlRequest = GraphQLRequest.createIntrospectionRequest(); return invocationInputFactory.create(graphqlRequest, request, response); } String query = request.getParameter("query"); if (query == null) { throw new GraphQLException("Query parameter not found in GET request"); } if (isSingleQuery(query)) { Map variables = getVariables(request); Map extensions = getExtensions(request); String operationName = request.getParameter("operationName"); GraphQLRequest graphqlRequest = new GraphQLRequest(query, variables, extensions, operationName); return invocationInputFactory.createReadOnly(graphqlRequest, request, response); } List graphqlRequests = graphQLObjectMapper.readBatchedGraphQLRequest(query); return invocationInputFactory.createReadOnly( contextSetting, graphqlRequests, request, response); } private boolean isIntrospectionQuery(HttpServletRequest request) { String path = Optional.ofNullable(request.getPathInfo()).orElseGet(request::getServletPath).toLowerCase(); return path.contentEquals("/schema.json"); } private Map getVariables(HttpServletRequest request) { return Optional.ofNullable(request.getParameter("variables")) .map(graphQLObjectMapper::deserializeVariables) .map(HashMap::new) .orElseGet(HashMap::new); } private Map getExtensions(HttpServletRequest request) { return Optional.ofNullable(request.getParameter("extensions")) .map(graphQLObjectMapper::deserializeExtensions) .map(HashMap::new) .orElseGet(HashMap::new); } } ================================================ FILE: graphql-java-servlet/src/main/java/graphql/kickstart/servlet/GraphQLHttpServlet.java ================================================ package graphql.kickstart.servlet; import graphql.schema.GraphQLSchema; /** @author Michiel Oliemans */ public abstract class GraphQLHttpServlet extends AbstractGraphQLHttpServlet { public static GraphQLHttpServlet with(GraphQLSchema schema) { return new ConfiguredGraphQLHttpServlet(GraphQLConfiguration.with(schema).build()); } public static GraphQLHttpServlet with(GraphQLConfiguration configuration) { return new ConfiguredGraphQLHttpServlet(configuration); } @Override protected abstract GraphQLConfiguration getConfiguration(); } ================================================ FILE: graphql-java-servlet/src/main/java/graphql/kickstart/servlet/GraphQLInvocationInputParser.java ================================================ package graphql.kickstart.servlet; import graphql.kickstart.execution.GraphQLObjectMapper; import graphql.kickstart.execution.context.ContextSetting; import graphql.kickstart.execution.input.GraphQLInvocationInput; import graphql.kickstart.servlet.input.GraphQLInvocationInputFactory; import java.io.IOException; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; interface GraphQLInvocationInputParser { static GraphQLInvocationInputParser create( HttpServletRequest request, GraphQLInvocationInputFactory invocationInputFactory, GraphQLObjectMapper graphQLObjectMapper, ContextSetting contextSetting) throws IOException { if ("GET".equalsIgnoreCase(request.getMethod())) { return new GraphQLGetInvocationInputParser( invocationInputFactory, graphQLObjectMapper, contextSetting); } try { boolean notMultipartRequest = request.getContentType() == null || !request.getContentType().startsWith("multipart/form-data") || request.getParts().isEmpty(); if (notMultipartRequest) { return new GraphQLPostInvocationInputParser( invocationInputFactory, graphQLObjectMapper, contextSetting); } return new GraphQLMultipartInvocationInputParser( invocationInputFactory, graphQLObjectMapper, contextSetting); } catch (ServletException e) { throw new IOException("Cannot get parts of request", e); } } GraphQLInvocationInput getGraphQLInvocationInput( HttpServletRequest request, HttpServletResponse response) throws IOException; } ================================================ FILE: graphql-java-servlet/src/main/java/graphql/kickstart/servlet/GraphQLMultipartInvocationInputParser.java ================================================ package graphql.kickstart.servlet; import static java.util.stream.Collectors.joining; import graphql.GraphQLException; import graphql.kickstart.execution.GraphQLObjectMapper; import graphql.kickstart.execution.GraphQLRequest; import graphql.kickstart.execution.context.ContextSetting; import graphql.kickstart.execution.input.GraphQLInvocationInput; import graphql.kickstart.servlet.core.internal.VariableMapper; import graphql.kickstart.servlet.input.GraphQLInvocationInputFactory; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.Part; import lombok.extern.slf4j.Slf4j; @Slf4j class GraphQLMultipartInvocationInputParser extends AbstractGraphQLInvocationInputParser { private static final String[] MULTIPART_KEYS = new String[] {"operations", "graphql", "query"}; GraphQLMultipartInvocationInputParser( GraphQLInvocationInputFactory invocationInputFactory, GraphQLObjectMapper graphQLObjectMapper, ContextSetting contextSetting) { super(invocationInputFactory, graphQLObjectMapper, contextSetting); } @Override public GraphQLInvocationInput getGraphQLInvocationInput( HttpServletRequest request, HttpServletResponse response) throws IOException { try { final Map> parts = request.getParts().stream().collect(Collectors.groupingBy(Part::getName)); for (String key : MULTIPART_KEYS) { // Check to see if there is a part under the key we seek if (!parts.containsKey(key)) { continue; } final Optional queryItem = findPart(parts); if (!queryItem.isPresent()) { log.info("Bad POST multipart request: no part named {}", Arrays.toString(MULTIPART_KEYS)); throw new GraphQLException( "Bad POST multipart request: no part named " + Arrays.toString(MULTIPART_KEYS)); } return getGraphQLInvocationInput(request, response, parts, key, queryItem.get()); } log.info("Bad POST multipart request: no part named {}", Arrays.toString(MULTIPART_KEYS)); throw new GraphQLException( "Bad POST multipart request: no part named " + Arrays.toString(MULTIPART_KEYS)); } catch (ServletException e) { throw new IOException("Cannot get parts from request", e); } } private GraphQLInvocationInput getGraphQLInvocationInput( HttpServletRequest request, HttpServletResponse response, Map> parts, String key, Part queryItem) throws IOException { InputStream inputStream = queryItem.getInputStream(); final Optional>> variablesMap = getPart(parts, "map") .map( part -> { try (InputStream is = part.getInputStream()) { return graphQLObjectMapper.deserializeMultipartMap(is); } catch (IOException e) { throw new PartIOException("Unable to read input stream from part", e); } }); String query = read(inputStream, request.getCharacterEncoding()); if ("query".equals(key) && isSingleQuery(query)) { GraphQLRequest graphqlRequest = buildRequestFromQuery(query, graphQLObjectMapper, parts, request.getCharacterEncoding()); variablesMap.ifPresent(m -> mapMultipartVariables(graphqlRequest, m, parts)); return invocationInputFactory.create(graphqlRequest, request, response); } else if (isSingleQuery(query)) { GraphQLRequest graphqlRequest = graphQLObjectMapper.readGraphQLRequest(query); variablesMap.ifPresent(m -> mapMultipartVariables(graphqlRequest, m, parts)); return invocationInputFactory.create(graphqlRequest, request, response); } else { List graphqlRequests = graphQLObjectMapper.readBatchedGraphQLRequest(query); variablesMap.ifPresent( map -> graphqlRequests.forEach(r -> mapMultipartVariables(r, map, parts))); return invocationInputFactory.create(contextSetting, graphqlRequests, request, response); } } private Optional findPart(Map> parts) { return Arrays.stream(MULTIPART_KEYS) .filter(parts::containsKey) .map(key -> getPart(parts, key)) .findFirst() .filter(Optional::isPresent) .map(Optional::get); } private Optional getPart(Map> parts, String name) { return Optional.ofNullable(parts.get(name)) .filter(list -> !list.isEmpty()) .map(list -> list.get(0)); } private void mapMultipartVariables( GraphQLRequest request, Map> variablesMap, Map> fileItems) { Map variables = request.getVariables(); variablesMap.forEach( (partName, objectPaths) -> { Part part = getPart(fileItems, partName) .orElseThrow( () -> new RuntimeException( "unable to find part name " + partName + " as referenced in the variables map")); objectPaths.forEach( objectPath -> VariableMapper.mapVariable(objectPath, variables, part)); }); } private GraphQLRequest buildRequestFromQuery( String query, GraphQLObjectMapper graphQLObjectMapper, Map> parts, String charset) throws IOException { Map variables = null; final Optional variablesItem = getPart(parts, "variables"); if (variablesItem.isPresent()) { variables = graphQLObjectMapper.deserializeVariables( read(variablesItem.get().getInputStream(), charset)); } Map extensions = null; final Optional extensionsItem = getPart(parts, "extensions"); if (extensionsItem.isPresent()) { extensions = graphQLObjectMapper.deserializeExtensions( read(extensionsItem.get().getInputStream(), charset)); } String operationName = null; final Optional operationNameItem = getPart(parts, "operationName"); if (operationNameItem.isPresent()) { operationName = read(operationNameItem.get().getInputStream(), charset).trim(); } return new GraphQLRequest(query, variables, extensions, operationName); } private String read(InputStream inputStream, String charset) throws IOException { try (InputStreamReader streamReader = new InputStreamReader(inputStream, charset); BufferedReader reader = new BufferedReader(streamReader)) { return reader.lines().collect(joining()); } } } ================================================ FILE: graphql-java-servlet/src/main/java/graphql/kickstart/servlet/GraphQLPostInvocationInputParser.java ================================================ package graphql.kickstart.servlet; import static java.util.stream.Collectors.joining; import graphql.GraphQLException; import graphql.kickstart.execution.GraphQLObjectMapper; import graphql.kickstart.execution.GraphQLRequest; import graphql.kickstart.execution.context.ContextSetting; import graphql.kickstart.execution.input.GraphQLInvocationInput; import graphql.kickstart.servlet.input.GraphQLInvocationInputFactory; import java.io.IOException; import java.util.List; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; class GraphQLPostInvocationInputParser extends AbstractGraphQLInvocationInputParser { private static final String APPLICATION_GRAPHQL = "application/graphql"; GraphQLPostInvocationInputParser( GraphQLInvocationInputFactory invocationInputFactory, GraphQLObjectMapper graphQLObjectMapper, ContextSetting contextSetting) { super(invocationInputFactory, graphQLObjectMapper, contextSetting); } public GraphQLInvocationInput getGraphQLInvocationInput( HttpServletRequest request, HttpServletResponse response) throws IOException { String contentType = request.getContentType(); if (contentType != null && APPLICATION_GRAPHQL.equals(contentType.split(";")[0].trim())) { String query = request.getReader().lines().collect(joining(" ")); GraphQLRequest graphqlRequest = GraphQLRequest.createQueryOnlyRequest(query); return invocationInputFactory.create(graphqlRequest, request, response); } String body = request.getReader().lines().collect(joining(" ")); if (isSingleQuery(body)) { GraphQLRequest graphqlRequest = graphQLObjectMapper.readGraphQLRequest(body); return invocationInputFactory.create(graphqlRequest, request, response); } if (isBatchedQuery(body)) { List requests = graphQLObjectMapper.readBatchedGraphQLRequest(body); return invocationInputFactory.create(contextSetting, requests, request, response); } throw new GraphQLException("No valid query found in request"); } } ================================================ FILE: graphql-java-servlet/src/main/java/graphql/kickstart/servlet/GraphQLWebsocketServlet.java ================================================ package graphql.kickstart.servlet; import static java.util.Arrays.asList; import static java.util.Collections.singletonList; import static java.util.stream.Collectors.toList; import graphql.kickstart.execution.GraphQLInvoker; import graphql.kickstart.execution.GraphQLObjectMapper; import graphql.kickstart.execution.subscriptions.GraphQLSubscriptionInvocationInputFactory; import graphql.kickstart.execution.subscriptions.GraphQLSubscriptionMapper; import graphql.kickstart.execution.subscriptions.SessionSubscriptions; import graphql.kickstart.execution.subscriptions.SubscriptionConnectionListener; import graphql.kickstart.execution.subscriptions.SubscriptionProtocolFactory; import graphql.kickstart.execution.subscriptions.SubscriptionSession; import graphql.kickstart.execution.subscriptions.apollo.ApolloSubscriptionConnectionListener; import graphql.kickstart.servlet.apollo.ApolloWebSocketSubscriptionProtocolFactory; import graphql.kickstart.servlet.subscriptions.FallbackSubscriptionProtocolFactory; import graphql.kickstart.servlet.subscriptions.WebSocketSendSubscriber; import graphql.kickstart.servlet.subscriptions.WebSocketSubscriptionProtocolFactory; import java.io.EOFException; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Consumer; import java.util.stream.Stream; import jakarta.websocket.CloseReason; import jakarta.websocket.Endpoint; import jakarta.websocket.EndpointConfig; import jakarta.websocket.HandshakeResponse; import jakarta.websocket.MessageHandler; import jakarta.websocket.Session; import jakarta.websocket.server.HandshakeRequest; import jakarta.websocket.server.ServerEndpointConfig; import lombok.extern.slf4j.Slf4j; /** * Must be used with {@link #modifyHandshake(ServerEndpointConfig, HandshakeRequest, * HandshakeResponse)} * * @author Andrew Potter */ @Slf4j public class GraphQLWebsocketServlet extends Endpoint { private static final String HANDSHAKE_REQUEST_KEY = HandshakeRequest.class.getName(); private static final String PROTOCOL_FACTORY_REQUEST_KEY = SubscriptionProtocolFactory.class.getName(); private static final CloseReason ERROR_CLOSE_REASON = new CloseReason(CloseReason.CloseCodes.UNEXPECTED_CONDITION, "Internal Server Error"); private static final CloseReason SHUTDOWN_CLOSE_REASON = new CloseReason(CloseReason.CloseCodes.UNEXPECTED_CONDITION, "Server Shut Down"); private final List subscriptionProtocolFactories; private final SubscriptionProtocolFactory fallbackSubscriptionProtocolFactory; private final List allSubscriptionProtocols; private final Map sessionSubscriptionCache = new ConcurrentHashMap<>(); private final AtomicBoolean isShuttingDown = new AtomicBoolean(false); private final AtomicBoolean isShutDown = new AtomicBoolean(false); private final Object cacheLock = new Object(); public GraphQLWebsocketServlet(GraphQLConfiguration configuration) { this(configuration, null); } public GraphQLWebsocketServlet( GraphQLConfiguration configuration, Collection connectionListeners) { this( configuration.getGraphQLInvoker(), configuration.getInvocationInputFactory(), configuration.getObjectMapper(), connectionListeners); } public GraphQLWebsocketServlet( GraphQLInvoker graphQLInvoker, GraphQLSubscriptionInvocationInputFactory invocationInputFactory, GraphQLObjectMapper graphQLObjectMapper) { this(graphQLInvoker, invocationInputFactory, graphQLObjectMapper, null); } public GraphQLWebsocketServlet( GraphQLInvoker graphQLInvoker, GraphQLSubscriptionInvocationInputFactory invocationInputFactory, GraphQLObjectMapper graphQLObjectMapper, Collection connectionListeners) { List listeners = new ArrayList<>(); if (connectionListeners != null) { connectionListeners.stream() .filter(ApolloSubscriptionConnectionListener.class::isInstance) .map(ApolloSubscriptionConnectionListener.class::cast) .forEach(listeners::add); } subscriptionProtocolFactories = singletonList( new ApolloWebSocketSubscriptionProtocolFactory( graphQLObjectMapper, invocationInputFactory, graphQLInvoker, listeners)); fallbackSubscriptionProtocolFactory = new FallbackSubscriptionProtocolFactory( new GraphQLSubscriptionMapper(graphQLObjectMapper), invocationInputFactory, graphQLInvoker); allSubscriptionProtocols = Stream.concat( subscriptionProtocolFactories.stream(), Stream.of(fallbackSubscriptionProtocolFactory)) .map(SubscriptionProtocolFactory::getProtocol) .collect(toList()); } public GraphQLWebsocketServlet( List subscriptionProtocolFactory, SubscriptionProtocolFactory fallbackSubscriptionProtocolFactory) { this.subscriptionProtocolFactories = subscriptionProtocolFactory; this.fallbackSubscriptionProtocolFactory = fallbackSubscriptionProtocolFactory; allSubscriptionProtocols = Stream.concat( subscriptionProtocolFactories.stream(), Stream.of(fallbackSubscriptionProtocolFactory)) .map(SubscriptionProtocolFactory::getProtocol) .collect(toList()); } @Override public void onOpen(Session session, EndpointConfig endpointConfig) { final WebSocketSubscriptionProtocolFactory subscriptionProtocolFactory = (WebSocketSubscriptionProtocolFactory) endpointConfig.getUserProperties().get(PROTOCOL_FACTORY_REQUEST_KEY); SubscriptionSession subscriptionSession = subscriptionProtocolFactory.createSession(session); synchronized (cacheLock) { if (isShuttingDown.get()) { throw new IllegalStateException("Server is shutting down!"); } sessionSubscriptionCache.put(session, subscriptionSession.getSubscriptions()); } subscriptionSession.getPublisher().subscribe(new WebSocketSendSubscriber(session)); log.debug("Session opened: {}, {}", session.getId(), endpointConfig); Consumer consumer = subscriptionProtocolFactory.createConsumer(subscriptionSession); // This *cannot* be a lambda because of the way undertow checks the class... session.addMessageHandler( new MessageHandler.Whole() { @Override public void onMessage(String text) { try { consumer.accept(text); } catch (Exception t) { log.error("Error executing websocket query for session: {}", session.getId(), t); closeUnexpectedly(session, t); } } }); } @Override public void onClose(Session session, CloseReason closeReason) { log.debug("Session closed: {}, {}", session.getId(), closeReason); SessionSubscriptions subscriptions; synchronized (cacheLock) { subscriptions = sessionSubscriptionCache.remove(session); } if (subscriptions != null) { subscriptions.close(); } } @Override public void onError(Session session, Throwable thr) { if (thr instanceof EOFException) { log.warn( "Session {} was killed abruptly without calling onClose. Cleaning up session", session.getId()); onClose(session, ERROR_CLOSE_REASON); } else { log.error("Error in websocket session: {}", session.getId(), thr); closeUnexpectedly(session, thr); } } private void closeUnexpectedly(Session session, Throwable t) { try { session.close(ERROR_CLOSE_REASON); } catch (IOException e) { log.error("Error closing websocket session for session: {}", session.getId(), t); } } public void modifyHandshake( ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) { sec.getUserProperties().put(HANDSHAKE_REQUEST_KEY, request); List protocol = request.getHeaders().get(HandshakeRequest.SEC_WEBSOCKET_PROTOCOL); if (protocol == null) { protocol = Collections.emptyList(); } SubscriptionProtocolFactory subscriptionProtocolFactory = getSubscriptionProtocolFactory(protocol); sec.getUserProperties().put(PROTOCOL_FACTORY_REQUEST_KEY, subscriptionProtocolFactory); if (request.getHeaders().get(HandshakeResponse.SEC_WEBSOCKET_ACCEPT) != null) { response.getHeaders().put(HandshakeResponse.SEC_WEBSOCKET_ACCEPT, allSubscriptionProtocols); } if (!protocol.isEmpty()) { //noinspection ArraysAsListWithZeroOrOneArgument response .getHeaders() .put( HandshakeRequest.SEC_WEBSOCKET_PROTOCOL, new ArrayList<>(asList(subscriptionProtocolFactory.getProtocol()))); } } /** Stops accepting connections and closes all existing connections */ public void beginShutDown() { synchronized (cacheLock) { isShuttingDown.set(true); Map copy = new HashMap<>(sessionSubscriptionCache); // Prevent comodification exception since #onClose() is called during session.close(), but we // can't necessarily rely on that happening so we close subscriptions here anyway. copy.forEach( (session, wsSessionSubscriptions) -> { wsSessionSubscriptions.close(); try { session.close(SHUTDOWN_CLOSE_REASON); } catch (IOException e) { log.error("Error closing websocket session!", e); } }); copy.clear(); if (!sessionSubscriptionCache.isEmpty()) { log.error("GraphQLWebsocketServlet did not shut down cleanly!"); sessionSubscriptionCache.clear(); } for (SubscriptionProtocolFactory protocolFactory : subscriptionProtocolFactories) { protocolFactory.shutdown(); } fallbackSubscriptionProtocolFactory.shutdown(); } isShutDown.set(true); } /** @return true when shutdown is complete */ public boolean isShutDown() { return isShutDown.get(); } private SubscriptionProtocolFactory getSubscriptionProtocolFactory(List accept) { for (String protocol : accept) { for (SubscriptionProtocolFactory subscriptionProtocolFactory : subscriptionProtocolFactories) { if (subscriptionProtocolFactory.getProtocol().equals(protocol)) { return subscriptionProtocolFactory; } } } return fallbackSubscriptionProtocolFactory; } public int getSessionCount() { return sessionSubscriptionCache.size(); } public int getSubscriptionCount() { return sessionSubscriptionCache.values().stream() .mapToInt(SessionSubscriptions::getSubscriptionCount) .sum(); } } ================================================ FILE: graphql-java-servlet/src/main/java/graphql/kickstart/servlet/HttpRequestHandler.java ================================================ package graphql.kickstart.servlet; import java.io.IOException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; public interface HttpRequestHandler { String APPLICATION_JSON_UTF8 = "application/json;charset=UTF-8"; String APPLICATION_EVENT_STREAM_UTF8 = "text/event-stream;charset=UTF-8"; int STATUS_OK = 200; int STATUS_BAD_REQUEST = 400; int STATUS_INTERNAL_SERVER_ERROR = 500; void handle(HttpServletRequest request, HttpServletResponse response) throws IOException; } ================================================ FILE: graphql-java-servlet/src/main/java/graphql/kickstart/servlet/HttpRequestHandlerImpl.java ================================================ package graphql.kickstart.servlet; import graphql.GraphQLException; import graphql.kickstart.execution.input.GraphQLInvocationInput; import java.io.IOException; import java.nio.charset.StandardCharsets; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.extern.slf4j.Slf4j; @Slf4j class HttpRequestHandlerImpl implements HttpRequestHandler { private final GraphQLConfiguration configuration; private final HttpRequestInvoker requestInvoker; HttpRequestHandlerImpl(GraphQLConfiguration configuration) { this( configuration, new HttpRequestInvokerImpl( configuration, configuration.getGraphQLInvoker(), new QueryResponseWriterFactoryImpl())); } HttpRequestHandlerImpl( GraphQLConfiguration configuration, HttpRequestInvoker requestInvoker) { this.configuration = configuration; this.requestInvoker = requestInvoker; } @Override public void handle(HttpServletRequest request, HttpServletResponse response) throws IOException { if (request.getCharacterEncoding() == null) { request.setCharacterEncoding(StandardCharsets.UTF_8.name()); } ListenerHandler listenerHandler = ListenerHandler.start(request, response, configuration.getListeners()); try { GraphQLInvocationInput invocationInput = parseInvocationInput(request, response); requestInvoker.execute(invocationInput, request, response, listenerHandler); } catch (InvocationInputParseException e) { response.setStatus(STATUS_BAD_REQUEST); log.info("Bad request: cannot parse http request", e); listenerHandler.onParseError(e); throw e; } catch (GraphQLException e) { response.setStatus(STATUS_BAD_REQUEST); log.info("Bad request: cannot handle http request", e); throw e; } catch (Exception t) { response.setStatus(STATUS_INTERNAL_SERVER_ERROR); log.error("Cannot handle http request", t); throw t; } } private GraphQLInvocationInput parseInvocationInput( HttpServletRequest request, HttpServletResponse response) { try { GraphQLInvocationInputParser invocationInputParser = GraphQLInvocationInputParser.create( request, configuration.getInvocationInputFactory(), configuration.getObjectMapper(), configuration.getContextSetting()); return invocationInputParser.getGraphQLInvocationInput(request, response); } catch (Exception e) { throw new InvocationInputParseException(e); } } } ================================================ FILE: graphql-java-servlet/src/main/java/graphql/kickstart/servlet/HttpRequestInvoker.java ================================================ package graphql.kickstart.servlet; import graphql.kickstart.execution.input.GraphQLInvocationInput; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; public interface HttpRequestInvoker { void execute( GraphQLInvocationInput invocationInput, HttpServletRequest request, HttpServletResponse response, ListenerHandler listenerHandler); } ================================================ FILE: graphql-java-servlet/src/main/java/graphql/kickstart/servlet/HttpRequestInvokerImpl.java ================================================ package graphql.kickstart.servlet; import static graphql.kickstart.servlet.HttpRequestHandler.STATUS_BAD_REQUEST; import static graphql.kickstart.servlet.HttpRequestHandler.STATUS_INTERNAL_SERVER_ERROR; import graphql.ExecutionResult; import graphql.ExecutionResultImpl; import graphql.GraphQLException; import graphql.kickstart.execution.FutureExecutionResult; import graphql.kickstart.execution.GraphQLInvoker; import graphql.kickstart.execution.GraphQLQueryResult; import graphql.kickstart.execution.error.GenericGraphQLError; import graphql.kickstart.execution.input.GraphQLBatchedInvocationInput; import graphql.kickstart.execution.input.GraphQLInvocationInput; import graphql.kickstart.execution.input.GraphQLSingleInvocationInput; import graphql.kickstart.servlet.input.BatchInputPreProcessResult; import graphql.kickstart.servlet.input.BatchInputPreProcessor; import java.io.IOException; import java.io.UncheckedIOException; import java.util.concurrent.CancellationException; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import jakarta.servlet.AsyncContext; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @Slf4j @RequiredArgsConstructor public class HttpRequestInvokerImpl implements HttpRequestInvoker { private final GraphQLConfiguration configuration; private final GraphQLInvoker graphQLInvoker; private final QueryResponseWriterFactory queryResponseWriterFactory; @Override public void execute( GraphQLInvocationInput invocationInput, HttpServletRequest request, HttpServletResponse response, ListenerHandler listenerHandler) { if (request.isAsyncSupported()) { invokeAndHandleAsync(invocationInput, request, response, listenerHandler); } else { handle(invocationInput, request, response, listenerHandler); } } private void invokeAndHandleAsync( GraphQLInvocationInput invocationInput, HttpServletRequest request, HttpServletResponse response, ListenerHandler listenerHandler) { AsyncContext asyncContext = request.isAsyncStarted() ? request.getAsyncContext() : request.startAsync(request, response); asyncContext.setTimeout(configuration.getAsyncTimeout()); AtomicReference futureHolder = new AtomicReference<>(); AsyncTimeoutListener timeoutListener = event -> { log.warn( "GraphQL execution canceled because timeout of " + configuration.getAsyncTimeout() + " millis was reached. The following query was being executed when this happened:\n{}", String.join("\n", invocationInput.getQueries())); FutureExecutionResult futureResult = futureHolder.get(); if (futureResult != null) { futureResult.cancel(); } else { writeErrorResponse( invocationInput, request, response, listenerHandler, new CancellationException()); } }; asyncContext.addListener(timeoutListener); configuration .getAsyncExecutor() .execute( () -> { try { FutureExecutionResult futureResult = invoke(invocationInput, request, response); futureHolder.set(futureResult); handleInternal(futureResult, request, response, listenerHandler) .thenAccept(it -> asyncContext.complete()); } catch (GraphQLException e) { response.setStatus(STATUS_BAD_REQUEST); log.info("Bad request: cannot handle http request", e); listenerHandler.onError(e); asyncContext.complete(); } catch (Exception e) { response.setStatus(STATUS_INTERNAL_SERVER_ERROR); log.error("Cannot handle http request", e); listenerHandler.onError(e); asyncContext.complete(); } }); } private void handle( GraphQLInvocationInput invocationInput, HttpServletRequest request, HttpServletResponse response, ListenerHandler listenerHandler) { try { FutureExecutionResult futureResult = invoke(invocationInput, request, response); handleInternal(futureResult, request, response, listenerHandler) .get(configuration.getAsyncTimeout(), TimeUnit.MILLISECONDS); } catch (GraphQLException e) { response.setStatus(STATUS_BAD_REQUEST); log.info("Bad request: cannot handle http request", e); listenerHandler.onError(e); } catch (Exception e) { response.setStatus(STATUS_INTERNAL_SERVER_ERROR); log.error("Cannot handle http request", e); listenerHandler.onError(e); if (e instanceof InterruptedException) { Thread.currentThread().interrupt(); } } } private CompletableFuture handleInternal( FutureExecutionResult futureResult, HttpServletRequest request, HttpServletResponse response, ListenerHandler listenerHandler) { return futureResult .thenApplyQueryResult() .thenAccept( it -> { listenerHandler.beforeFlush(); writeResultResponse(futureResult.getInvocationInput(), it, request, response); }) .thenAccept(it -> listenerHandler.onSuccess()) .exceptionally( t -> writeErrorResponse( futureResult.getInvocationInput(), request, response, listenerHandler, t)) .thenAccept(it -> listenerHandler.onFinally()); } private void writeResultResponse( GraphQLInvocationInput invocationInput, GraphQLQueryResult queryResult, HttpServletRequest request, HttpServletResponse response) { QueryResponseWriter queryResponseWriter = createWriter(invocationInput, queryResult); try { queryResponseWriter.write(request, response); } catch (IOException e) { throw new UncheckedIOException(e); } } private Void writeErrorResponse( GraphQLInvocationInput invocationInput, HttpServletRequest request, HttpServletResponse response, ListenerHandler listenerHandler, Throwable t) { Throwable cause = getCause(t); if (!response.isCommitted()) { writeResultResponse( invocationInput, GraphQLQueryResult.create(toErrorResult(cause)), request, response); listenerHandler.onError(cause); } else { log.warn( "Cannot write GraphQL response, because the HTTP response is already committed. It most likely timed out."); } return null; } private Throwable getCause(Throwable t) { return t instanceof CompletionException && t.getCause() != null ? t.getCause() : t; } private ExecutionResult toErrorResult(Throwable t) { String message = t instanceof CancellationException ? "Execution canceled because timeout of " + configuration.getAsyncTimeout() + " millis was reached" : t.getMessage(); if (message == null) { message = "Unexpected error occurred"; } return new ExecutionResultImpl(new GenericGraphQLError(message)); } protected QueryResponseWriter createWriter( GraphQLInvocationInput invocationInput, GraphQLQueryResult queryResult) { return queryResponseWriterFactory.createWriter(invocationInput, queryResult, configuration); } private FutureExecutionResult invoke( GraphQLInvocationInput invocationInput, HttpServletRequest request, HttpServletResponse response) { if (invocationInput instanceof GraphQLSingleInvocationInput) { return graphQLInvoker.execute(invocationInput); } return invokeBatched((GraphQLBatchedInvocationInput) invocationInput, request, response); } private FutureExecutionResult invokeBatched( GraphQLBatchedInvocationInput batchedInvocationInput, HttpServletRequest request, HttpServletResponse response) { BatchInputPreProcessor preprocessor = configuration.getBatchInputPreProcessor(); BatchInputPreProcessResult result = preprocessor.preProcessBatch(batchedInvocationInput, request, response); if (result.isExecutable()) { return graphQLInvoker.execute(result.getBatchedInvocationInput()); } return FutureExecutionResult.error( GraphQLQueryResult.createError(result.getStatusCode(), result.getStatusMessage())); } } ================================================ FILE: graphql-java-servlet/src/main/java/graphql/kickstart/servlet/InvocationInputParseException.java ================================================ package graphql.kickstart.servlet; public class InvocationInputParseException extends RuntimeException { public InvocationInputParseException(Throwable t) { super("Request parsing failed", t); } } ================================================ FILE: graphql-java-servlet/src/main/java/graphql/kickstart/servlet/ListenerHandler.java ================================================ package graphql.kickstart.servlet; import static java.util.Collections.emptyList; import graphql.kickstart.servlet.core.GraphQLServletListener; import graphql.kickstart.servlet.core.GraphQLServletListener.RequestCallback; import java.util.List; import java.util.Objects; import java.util.function.Consumer; import java.util.function.Function; import java.util.stream.Collectors; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @Slf4j @RequiredArgsConstructor public class ListenerHandler { private final List callbacks; private final HttpServletRequest request; private final HttpServletResponse response; static ListenerHandler start( HttpServletRequest request, HttpServletResponse response, List listeners) { if (listeners != null) { return new ListenerHandler( runListeners(listeners, it -> it.onRequest(request, response)), request, response); } return new ListenerHandler(emptyList(), request, response); } private static List runListeners( List listeners, Function action) { return listeners.stream() .map( listener -> { try { return action.apply(listener); } catch (Exception t) { log.error("Error running listener: {}", listener, t); return null; } }) .filter(Objects::nonNull) .collect(Collectors.toList()); } void runCallbacks(Consumer action) { callbacks.forEach( callback -> { try { action.accept(callback); } catch (Exception t) { log.error("Error running callback: {}", callback, t); } }); } void onParseError(Throwable throwable) { runCallbacks(it -> it.onParseError(request, response, throwable)); } void beforeFlush() { runCallbacks(it -> it.beforeFlush(request, response)); } void onSuccess() { runCallbacks(it -> it.onSuccess(request, response)); } void onError(Throwable throwable) { runCallbacks(it -> it.onError(request, response, throwable)); } void onFinally() { runCallbacks(it -> it.onFinally(request, response)); } } ================================================ FILE: graphql-java-servlet/src/main/java/graphql/kickstart/servlet/OsgiGraphQLHttpServlet.java ================================================ package graphql.kickstart.servlet; import graphql.execution.preparsed.NoOpPreparsedDocumentProvider; import graphql.execution.preparsed.PreparsedDocumentProvider; import graphql.kickstart.execution.GraphQLRootObjectBuilder; import graphql.kickstart.execution.config.DefaultExecutionStrategyProvider; import graphql.kickstart.execution.config.ExecutionStrategyProvider; import graphql.kickstart.execution.config.InstrumentationProvider; import graphql.kickstart.execution.error.DefaultGraphQLErrorHandler; import graphql.kickstart.execution.error.GraphQLErrorHandler; import graphql.kickstart.execution.instrumentation.NoOpInstrumentationProvider; import graphql.kickstart.servlet.context.DefaultGraphQLServletContextBuilder; import graphql.kickstart.servlet.context.GraphQLServletContextBuilder; import graphql.kickstart.servlet.core.DefaultGraphQLRootObjectBuilder; import graphql.kickstart.servlet.core.GraphQLServletListener; import graphql.kickstart.servlet.core.GraphQLServletRootObjectBuilder; import graphql.kickstart.servlet.osgi.GraphQLCodeRegistryProvider; import graphql.kickstart.servlet.osgi.GraphQLDirectiveProvider; import graphql.kickstart.servlet.osgi.GraphQLMutationProvider; import graphql.kickstart.servlet.osgi.GraphQLProvider; import graphql.kickstart.servlet.osgi.GraphQLQueryProvider; import graphql.kickstart.servlet.osgi.GraphQLSubscriptionProvider; import graphql.kickstart.servlet.osgi.GraphQLTypesProvider; import graphql.schema.GraphQLCodeRegistry; import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.Deactivate; import org.osgi.service.component.annotations.Reference; import org.osgi.service.component.annotations.ReferenceCardinality; import org.osgi.service.component.annotations.ReferencePolicy; import org.osgi.service.component.annotations.ReferencePolicyOption; import org.osgi.service.metatype.annotations.Designate; @Component( service = {jakarta.servlet.http.HttpServlet.class, jakarta.servlet.Servlet.class}, property = {"service.description=GraphQL HTTP Servlet"}) @Designate(ocd = OsgiGraphQLHttpServletConfiguration.class, factory = true) public class OsgiGraphQLHttpServlet extends AbstractGraphQLHttpServlet { private final OsgiSchemaBuilder schemaBuilder = new OsgiSchemaBuilder(); public OsgiGraphQLHttpServlet() { schemaBuilder.updateSchema(); } @Activate public void activate(Config config) { schemaBuilder.activate(config.schema_update_delay()); } @Deactivate public void deactivate() { schemaBuilder.deactivate(); } @Override protected GraphQLConfiguration getConfiguration() { return schemaBuilder.buildConfiguration(); } protected void updateSchema() { schemaBuilder.updateSchema(); } @Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC) public void bindProvider(GraphQLProvider provider) { if (provider instanceof GraphQLQueryProvider) { schemaBuilder.add((GraphQLQueryProvider) provider); } if (provider instanceof GraphQLMutationProvider) { schemaBuilder.add((GraphQLMutationProvider) provider); } if (provider instanceof GraphQLSubscriptionProvider) { schemaBuilder.add((GraphQLSubscriptionProvider) provider); } if (provider instanceof GraphQLTypesProvider) { schemaBuilder.add((GraphQLTypesProvider) provider); } if (provider instanceof GraphQLDirectiveProvider) { schemaBuilder.add((GraphQLDirectiveProvider) provider); } if (provider instanceof GraphQLCodeRegistryProvider) { schemaBuilder.setCodeRegistryProvider((GraphQLCodeRegistryProvider) provider); } updateSchema(); } public void unbindProvider(GraphQLProvider provider) { if (provider instanceof GraphQLQueryProvider) { schemaBuilder.remove((GraphQLQueryProvider) provider); } if (provider instanceof GraphQLMutationProvider) { schemaBuilder.remove((GraphQLMutationProvider) provider); } if (provider instanceof GraphQLSubscriptionProvider) { schemaBuilder.remove((GraphQLSubscriptionProvider) provider); } if (provider instanceof GraphQLTypesProvider) { schemaBuilder.remove((GraphQLTypesProvider) provider); } if (provider instanceof GraphQLDirectiveProvider) { schemaBuilder.remove((GraphQLDirectiveProvider) provider); } if (provider instanceof GraphQLCodeRegistryProvider) { schemaBuilder.setCodeRegistryProvider(() -> GraphQLCodeRegistry.newCodeRegistry().build()); } updateSchema(); } @Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC) public void bindQueryProvider(GraphQLQueryProvider queryProvider) { schemaBuilder.add(queryProvider); updateSchema(); } public void unbindQueryProvider(GraphQLQueryProvider queryProvider) { schemaBuilder.remove(queryProvider); updateSchema(); } @Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC) public void bindMutationProvider(GraphQLMutationProvider mutationProvider) { schemaBuilder.add(mutationProvider); updateSchema(); } public void unbindMutationProvider(GraphQLMutationProvider mutationProvider) { schemaBuilder.remove(mutationProvider); updateSchema(); } @Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC) public void bindSubscriptionProvider(GraphQLSubscriptionProvider subscriptionProvider) { schemaBuilder.add(subscriptionProvider); updateSchema(); } public void unbindSubscriptionProvider(GraphQLSubscriptionProvider subscriptionProvider) { schemaBuilder.remove(subscriptionProvider); updateSchema(); } @Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC) public void bindTypesProvider(GraphQLTypesProvider typesProvider) { schemaBuilder.add(typesProvider); updateSchema(); } public void unbindTypesProvider(GraphQLTypesProvider typesProvider) { schemaBuilder.remove(typesProvider); updateSchema(); } @Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC) public void bindDirectivesProvider(GraphQLDirectiveProvider directiveProvider) { schemaBuilder.add(directiveProvider); updateSchema(); } public void unbindDirectivesProvider(GraphQLDirectiveProvider directiveProvider) { schemaBuilder.remove(directiveProvider); updateSchema(); } @Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC) public void bindServletListener(GraphQLServletListener listener) { schemaBuilder.add(listener); } public void unbindServletListener(GraphQLServletListener listener) { schemaBuilder.remove(listener); } @Reference(cardinality = ReferenceCardinality.OPTIONAL, policy = ReferencePolicy.DYNAMIC) public void setContextBuilder(GraphQLServletContextBuilder contextBuilder) { schemaBuilder.setContextBuilder(contextBuilder); } public void unsetContextBuilder(GraphQLServletContextBuilder contextBuilder) { schemaBuilder.setContextBuilder(new DefaultGraphQLServletContextBuilder()); } @Reference(cardinality = ReferenceCardinality.OPTIONAL, policy = ReferencePolicy.DYNAMIC) public void setRootObjectBuilder(GraphQLServletRootObjectBuilder rootObjectBuilder) { schemaBuilder.setRootObjectBuilder(rootObjectBuilder); } public void unsetRootObjectBuilder(GraphQLRootObjectBuilder rootObjectBuilder) { schemaBuilder.setRootObjectBuilder(new DefaultGraphQLRootObjectBuilder()); } @Reference( cardinality = ReferenceCardinality.OPTIONAL, policy = ReferencePolicy.DYNAMIC, policyOption = ReferencePolicyOption.GREEDY) public void setExecutionStrategyProvider(ExecutionStrategyProvider provider) { schemaBuilder.setExecutionStrategyProvider(provider); } public void unsetExecutionStrategyProvider(ExecutionStrategyProvider provider) { schemaBuilder.setExecutionStrategyProvider(new DefaultExecutionStrategyProvider()); } @Reference( cardinality = ReferenceCardinality.OPTIONAL, policy = ReferencePolicy.DYNAMIC, policyOption = ReferencePolicyOption.GREEDY) public void setInstrumentationProvider(InstrumentationProvider provider) { schemaBuilder.setInstrumentationProvider(provider); } public void unsetInstrumentationProvider(InstrumentationProvider provider) { schemaBuilder.setInstrumentationProvider(new NoOpInstrumentationProvider()); } @Reference( cardinality = ReferenceCardinality.OPTIONAL, policy = ReferencePolicy.DYNAMIC, policyOption = ReferencePolicyOption.GREEDY) public void setErrorHandler(GraphQLErrorHandler errorHandler) { schemaBuilder.setErrorHandler(errorHandler); } public void unsetErrorHandler(GraphQLErrorHandler errorHandler) { schemaBuilder.setErrorHandler(new DefaultGraphQLErrorHandler()); } @Reference( cardinality = ReferenceCardinality.OPTIONAL, policy = ReferencePolicy.DYNAMIC, policyOption = ReferencePolicyOption.GREEDY) public void setPreparsedDocumentProvider(PreparsedDocumentProvider preparsedDocumentProvider) { schemaBuilder.setPreparsedDocumentProvider(preparsedDocumentProvider); } public void unsetPreparsedDocumentProvider(PreparsedDocumentProvider preparsedDocumentProvider) { schemaBuilder.setPreparsedDocumentProvider(NoOpPreparsedDocumentProvider.INSTANCE); } @Reference( cardinality = ReferenceCardinality.OPTIONAL, policy = ReferencePolicy.DYNAMIC, policyOption = ReferencePolicyOption.GREEDY) public void bindCodeRegistryProvider(GraphQLCodeRegistryProvider graphQLCodeRegistryProvider) { schemaBuilder.setCodeRegistryProvider(graphQLCodeRegistryProvider); updateSchema(); } public void unbindCodeRegistryProvider(GraphQLCodeRegistryProvider graphQLCodeRegistryProvider) { schemaBuilder.setCodeRegistryProvider(() -> GraphQLCodeRegistry.newCodeRegistry().build()); updateSchema(); } @interface Config { int schema_update_delay() default 0; } } ================================================ FILE: graphql-java-servlet/src/main/java/graphql/kickstart/servlet/OsgiGraphQLHttpServletConfiguration.java ================================================ package graphql.kickstart.servlet; import org.osgi.service.metatype.annotations.AttributeDefinition; import org.osgi.service.metatype.annotations.ObjectClassDefinition; @ObjectClassDefinition( name = "GraphQL HTTP Servlet", description = "GraphQL HTTP Servlet Configuration") @interface OsgiGraphQLHttpServletConfiguration { @AttributeDefinition(name = "alias", description = "Servlet alias") String alias() default "/graphql"; @AttributeDefinition(name = "jmx.objectname", description = "JMX object name") String jmx_objectname() default "graphql.servlet:type=graphql"; } ================================================ FILE: graphql-java-servlet/src/main/java/graphql/kickstart/servlet/OsgiSchemaBuilder.java ================================================ package graphql.kickstart.servlet; import static graphql.schema.GraphQLObjectType.newObject; import static graphql.schema.GraphQLSchema.newSchema; import static java.util.stream.Collectors.toSet; import graphql.Scalars; import graphql.execution.preparsed.NoOpPreparsedDocumentProvider; import graphql.execution.preparsed.PreparsedDocumentProvider; import graphql.kickstart.execution.GraphQLObjectMapper; import graphql.kickstart.execution.GraphQLQueryInvoker; import graphql.kickstart.execution.config.DefaultExecutionStrategyProvider; import graphql.kickstart.execution.config.ExecutionStrategyProvider; import graphql.kickstart.execution.config.InstrumentationProvider; import graphql.kickstart.execution.error.DefaultGraphQLErrorHandler; import graphql.kickstart.execution.error.GraphQLErrorHandler; import graphql.kickstart.execution.instrumentation.NoOpInstrumentationProvider; import graphql.kickstart.servlet.config.DefaultGraphQLSchemaServletProvider; import graphql.kickstart.servlet.config.GraphQLSchemaServletProvider; import graphql.kickstart.servlet.context.DefaultGraphQLServletContextBuilder; import graphql.kickstart.servlet.context.GraphQLServletContextBuilder; import graphql.kickstart.servlet.core.DefaultGraphQLRootObjectBuilder; import graphql.kickstart.servlet.core.GraphQLServletListener; import graphql.kickstart.servlet.core.GraphQLServletRootObjectBuilder; import graphql.kickstart.servlet.input.GraphQLInvocationInputFactory; import graphql.kickstart.servlet.osgi.GraphQLCodeRegistryProvider; import graphql.kickstart.servlet.osgi.GraphQLFieldProvider; import graphql.kickstart.servlet.osgi.GraphQLDirectiveProvider; import graphql.kickstart.servlet.osgi.GraphQLMutationProvider; import graphql.kickstart.servlet.osgi.GraphQLQueryProvider; import graphql.kickstart.servlet.osgi.GraphQLSubscriptionProvider; import graphql.kickstart.servlet.osgi.GraphQLTypesProvider; import graphql.schema.GraphQLCodeRegistry; import graphql.schema.GraphQLDirective; import graphql.schema.GraphQLFieldDefinition; import graphql.schema.GraphQLObjectType; import graphql.schema.GraphQLType; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Set; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import lombok.Setter; @Setter class OsgiSchemaBuilder { private final List queryProviders = new ArrayList<>(); private final List mutationProviders = new ArrayList<>(); private final List subscriptionProviders = new ArrayList<>(); private final List typesProviders = new ArrayList<>(); private final List directiveProviders = new ArrayList<>(); private final List listeners = new ArrayList<>(); private GraphQLServletContextBuilder contextBuilder = new DefaultGraphQLServletContextBuilder(); private GraphQLServletRootObjectBuilder rootObjectBuilder = new DefaultGraphQLRootObjectBuilder(); private ExecutionStrategyProvider executionStrategyProvider = new DefaultExecutionStrategyProvider(); private InstrumentationProvider instrumentationProvider = new NoOpInstrumentationProvider(); private GraphQLErrorHandler errorHandler = new DefaultGraphQLErrorHandler(); private PreparsedDocumentProvider preparsedDocumentProvider = NoOpPreparsedDocumentProvider.INSTANCE; private GraphQLCodeRegistryProvider codeRegistryProvider = () -> GraphQLCodeRegistry.newCodeRegistry().build(); private GraphQLSchemaServletProvider schemaProvider; private ScheduledExecutorService executor; private ScheduledFuture updateFuture; private int schemaUpdateDelay; void activate(int schemaUpdateDelay) { this.schemaUpdateDelay = schemaUpdateDelay; if (schemaUpdateDelay != 0) { executor = Executors.newSingleThreadScheduledExecutor(); } } void deactivate() { if (executor != null) { executor.shutdown(); } } void updateSchema() { if (schemaUpdateDelay == 0) { doUpdateSchema(); } else { if (updateFuture != null) { updateFuture.cancel(true); } updateFuture = executor.schedule(this::doUpdateSchema, schemaUpdateDelay, TimeUnit.MILLISECONDS); } } private void doUpdateSchema() { this.schemaProvider = new DefaultGraphQLSchemaServletProvider( newSchema() .query(buildQueryType()) .mutation(buildMutationType()) .subscription(buildSubscriptionType()) .additionalTypes(buildTypes()) .additionalDirectives(buildDirectives()) .codeRegistry(codeRegistryProvider.getCodeRegistry()) .build()); } private GraphQLObjectType buildQueryType() { final GraphQLObjectType.Builder queryTypeBuilder = newObject().name("Query").description("Root query type"); if (!queryProviders.isEmpty()) { for (GraphQLQueryProvider provider : queryProviders) { if (provider.getQueries() != null && !provider.getQueries().isEmpty()) { provider.getQueries().forEach(queryTypeBuilder::field); } } } else { // graphql-java enforces Query type to be there with at least some field. queryTypeBuilder.field( GraphQLFieldDefinition.newFieldDefinition() .name("_empty") .type(Scalars.GraphQLBoolean) .build()); } return queryTypeBuilder.build(); } private Set buildTypes() { return typesProviders.stream() .map(GraphQLTypesProvider::getTypes) .flatMap(Collection::stream) .collect(toSet()); } private GraphQLObjectType buildMutationType() { return buildObjectType("Mutation", new ArrayList<>(mutationProviders)); } private GraphQLObjectType buildSubscriptionType() { return buildObjectType("Subscription", new ArrayList<>(subscriptionProviders)); } private GraphQLObjectType buildObjectType(String name, List providers) { if (!providers.isEmpty()) { final GraphQLObjectType.Builder typeBuilder = newObject().name(name).description("Root " + name.toLowerCase() + " type"); for (GraphQLFieldProvider provider : providers) { provider.getFields().forEach(typeBuilder::field); } if (!typeBuilder.build().getFieldDefinitions().isEmpty()) { return typeBuilder.build(); } } return null; } private Set buildDirectives() { return directiveProviders.stream() .map(GraphQLDirectiveProvider::getDirectives) .flatMap(Collection::stream) .collect(toSet()); } void add(GraphQLQueryProvider provider) { queryProviders.add(provider); } void add(GraphQLMutationProvider provider) { mutationProviders.add(provider); } void add(GraphQLSubscriptionProvider provider) { subscriptionProviders.add(provider); } void add(GraphQLTypesProvider provider) { typesProviders.add(provider); } void add(GraphQLDirectiveProvider provider) { directiveProviders.add(provider); } void remove(GraphQLQueryProvider provider) { queryProviders.remove(provider); } void remove(GraphQLMutationProvider provider) { mutationProviders.remove(provider); } void remove(GraphQLSubscriptionProvider provider) { subscriptionProviders.remove(provider); } void remove(GraphQLTypesProvider provider) { typesProviders.remove(provider); } void remove(GraphQLDirectiveProvider provider) { directiveProviders.remove(provider); } GraphQLSchemaServletProvider getSchemaProvider() { return schemaProvider; } GraphQLConfiguration buildConfiguration() { return GraphQLConfiguration.with(buildInvocationInputFactory()) .with(buildQueryInvoker()) .with(buildObjectMapper()) .with(listeners) .build(); } private GraphQLInvocationInputFactory buildInvocationInputFactory() { return GraphQLInvocationInputFactory.newBuilder(this::getSchemaProvider) .withGraphQLContextBuilder(contextBuilder) .withGraphQLRootObjectBuilder(rootObjectBuilder) .build(); } private GraphQLQueryInvoker buildQueryInvoker() { return GraphQLQueryInvoker.newBuilder() .withPreparsedDocumentProvider(preparsedDocumentProvider) .withInstrumentation(() -> instrumentationProvider.getInstrumentation()) .withExecutionStrategyProvider(executionStrategyProvider) .build(); } private GraphQLObjectMapper buildObjectMapper() { return GraphQLObjectMapper.newBuilder().withGraphQLErrorHandler(errorHandler).build(); } void add(GraphQLServletListener listener) { listeners.add(listener); } void remove(GraphQLServletListener listener) { listeners.remove(listener); } } ================================================ FILE: graphql-java-servlet/src/main/java/graphql/kickstart/servlet/PartIOException.java ================================================ package graphql.kickstart.servlet; public class PartIOException extends RuntimeException { public PartIOException(String message, Throwable cause) { super(message, cause); } } ================================================ FILE: graphql-java-servlet/src/main/java/graphql/kickstart/servlet/QueryResponseWriter.java ================================================ package graphql.kickstart.servlet; import java.io.IOException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; public interface QueryResponseWriter { void write(HttpServletRequest request, HttpServletResponse response) throws IOException; } ================================================ FILE: graphql-java-servlet/src/main/java/graphql/kickstart/servlet/QueryResponseWriterFactory.java ================================================ package graphql.kickstart.servlet; import graphql.kickstart.execution.GraphQLQueryResult; import graphql.kickstart.execution.input.GraphQLInvocationInput; public interface QueryResponseWriterFactory { QueryResponseWriter createWriter( GraphQLInvocationInput invocationInput, GraphQLQueryResult queryResult, GraphQLConfiguration configuration); } ================================================ FILE: graphql-java-servlet/src/main/java/graphql/kickstart/servlet/QueryResponseWriterFactoryImpl.java ================================================ package graphql.kickstart.servlet; import graphql.kickstart.execution.GraphQLQueryResult; import graphql.kickstart.execution.input.GraphQLInvocationInput; import java.util.Objects; public class QueryResponseWriterFactoryImpl implements QueryResponseWriterFactory { @Override public QueryResponseWriter createWriter( GraphQLInvocationInput invocationInput, GraphQLQueryResult queryResult, GraphQLConfiguration configuration) { Objects.requireNonNull(queryResult, "GraphQL query result cannot be null"); if (queryResult.isBatched()) { return new BatchedQueryResponseWriter( queryResult.getResults(), configuration.getObjectMapper()); } if (queryResult.isAsynchronous()) { return new SingleAsynchronousQueryResponseWriter( queryResult.getResult(), configuration.getObjectMapper(), configuration.getSubscriptionTimeout()); } if (queryResult.isError()) { return new ErrorQueryResponseWriter(queryResult.getStatusCode(), queryResult.getMessage()); } return new SingleQueryResponseWriter(queryResult.getResult(), configuration.getObjectMapper()); } } ================================================ FILE: graphql-java-servlet/src/main/java/graphql/kickstart/servlet/SingleAsynchronousQueryResponseWriter.java ================================================ package graphql.kickstart.servlet; import graphql.ExecutionResult; import graphql.kickstart.execution.GraphQLObjectMapper; import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.concurrent.atomic.AtomicReference; import jakarta.servlet.AsyncContext; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.Getter; import lombok.RequiredArgsConstructor; import org.reactivestreams.Publisher; import org.reactivestreams.Subscription; @RequiredArgsConstructor class SingleAsynchronousQueryResponseWriter implements QueryResponseWriter { @Getter private final ExecutionResult result; private final GraphQLObjectMapper graphQLObjectMapper; private final long subscriptionTimeout; @Override public void write(HttpServletRequest request, HttpServletResponse response) { Objects.requireNonNull(request, "Http servlet request cannot be null"); response.setContentType(HttpRequestHandler.APPLICATION_EVENT_STREAM_UTF8); response.setStatus(HttpRequestHandler.STATUS_OK); boolean isInAsyncThread = request.isAsyncStarted(); AsyncContext asyncContext = isInAsyncThread ? request.getAsyncContext() : request.startAsync(request, response); asyncContext.setTimeout(subscriptionTimeout); AtomicReference subscriptionRef = new AtomicReference<>(); asyncContext.addListener(new SubscriptionAsyncListener(subscriptionRef)); ExecutionResultSubscriber subscriber = new ExecutionResultSubscriber(subscriptionRef, asyncContext, graphQLObjectMapper); List> publishers = new ArrayList<>(); if (result.getData() instanceof Publisher) { publishers.add(result.getData()); } else { publishers.add(new StaticDataPublisher<>(result)); } publishers.forEach(it -> it.subscribe(subscriber)); if (isInAsyncThread) { // We need to delay the completion of async context until after the subscription has // terminated, otherwise the AsyncContext is prematurely closed. try { subscriber.await(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } } } ================================================ FILE: graphql-java-servlet/src/main/java/graphql/kickstart/servlet/SingleQueryResponseWriter.java ================================================ package graphql.kickstart.servlet; import graphql.ExecutionResult; import graphql.kickstart.execution.GraphQLObjectMapper; import java.io.IOException; import java.nio.charset.StandardCharsets; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor class SingleQueryResponseWriter implements QueryResponseWriter { private final ExecutionResult result; private final GraphQLObjectMapper graphQLObjectMapper; @Override public void write(HttpServletRequest request, HttpServletResponse response) throws IOException { response.setContentType(HttpRequestHandler.APPLICATION_JSON_UTF8); response.setStatus(HttpRequestHandler.STATUS_OK); response.setCharacterEncoding(StandardCharsets.UTF_8.name()); byte[] contentBytes = graphQLObjectMapper.serializeResultAsBytes(result); response.setContentLength(contentBytes.length); response.getOutputStream().write(contentBytes); response.getOutputStream().flush(); } } ================================================ FILE: graphql-java-servlet/src/main/java/graphql/kickstart/servlet/StaticDataPublisher.java ================================================ package graphql.kickstart.servlet; import graphql.execution.reactive.SingleSubscriberPublisher; import org.reactivestreams.Publisher; class StaticDataPublisher extends SingleSubscriberPublisher implements Publisher { StaticDataPublisher(T data) { super(); offer(data); noMoreData(); } } ================================================ FILE: graphql-java-servlet/src/main/java/graphql/kickstart/servlet/SubscriptionAsyncListener.java ================================================ package graphql.kickstart.servlet; import java.util.concurrent.atomic.AtomicReference; import jakarta.servlet.AsyncEvent; import jakarta.servlet.AsyncListener; import lombok.RequiredArgsConstructor; import org.reactivestreams.Subscription; @RequiredArgsConstructor class SubscriptionAsyncListener implements AsyncListener { private final AtomicReference subscriptionRef; @Override public void onComplete(AsyncEvent event) { subscriptionRef.get().cancel(); } @Override public void onTimeout(AsyncEvent event) { subscriptionRef.get().cancel(); } @Override public void onError(AsyncEvent event) { subscriptionRef.get().cancel(); } @Override public void onStartAsync(AsyncEvent event) { // default empty implementation } } ================================================ FILE: graphql-java-servlet/src/main/java/graphql/kickstart/servlet/apollo/ApolloScalars.java ================================================ package graphql.kickstart.servlet.apollo; import graphql.schema.Coercing; import graphql.schema.CoercingParseLiteralException; import graphql.schema.CoercingParseValueException; import graphql.schema.CoercingSerializeException; import graphql.schema.GraphQLScalarType; import jakarta.servlet.http.Part; import lombok.AccessLevel; import lombok.NoArgsConstructor; @NoArgsConstructor(access = AccessLevel.PRIVATE) public class ApolloScalars { public static final GraphQLScalarType Upload = GraphQLScalarType.newScalar() .name("Upload") .description("A file part in a multipart request") .coercing( new Coercing() { @Override public Void serialize(Object dataFetcherResult) { throw new CoercingSerializeException("Upload is an input-only type"); } @Override public Part parseValue(Object input) { if (input instanceof Part) { return (Part) input; } else if (null == input) { return null; } else { throw new CoercingParseValueException( "Expected type " + Part.class.getName() + " but was " + input.getClass().getName()); } } @Override public Part parseLiteral(Object input) { throw new CoercingParseLiteralException( "Must use variables to specify Upload values"); } }) .build(); } ================================================ FILE: graphql-java-servlet/src/main/java/graphql/kickstart/servlet/apollo/ApolloWebSocketSubscriptionProtocolFactory.java ================================================ package graphql.kickstart.servlet.apollo; import graphql.kickstart.execution.GraphQLInvoker; import graphql.kickstart.execution.GraphQLObjectMapper; import graphql.kickstart.execution.subscriptions.GraphQLSubscriptionInvocationInputFactory; import graphql.kickstart.execution.subscriptions.GraphQLSubscriptionMapper; import graphql.kickstart.execution.subscriptions.SubscriptionSession; import graphql.kickstart.execution.subscriptions.apollo.ApolloSubscriptionConnectionListener; import graphql.kickstart.execution.subscriptions.apollo.ApolloSubscriptionProtocolFactory; import graphql.kickstart.servlet.subscriptions.WebSocketSubscriptionProtocolFactory; import java.time.Duration; import java.util.Collection; import jakarta.websocket.Session; public class ApolloWebSocketSubscriptionProtocolFactory extends ApolloSubscriptionProtocolFactory implements WebSocketSubscriptionProtocolFactory { public ApolloWebSocketSubscriptionProtocolFactory( GraphQLObjectMapper objectMapper, GraphQLSubscriptionInvocationInputFactory invocationInputFactory, GraphQLInvoker graphQLInvoker) { super(objectMapper, invocationInputFactory, graphQLInvoker); } public ApolloWebSocketSubscriptionProtocolFactory( GraphQLObjectMapper objectMapper, GraphQLSubscriptionInvocationInputFactory invocationInputFactory, GraphQLInvoker graphQLInvoker, Duration keepAliveInterval) { super(objectMapper, invocationInputFactory, graphQLInvoker, keepAliveInterval); } public ApolloWebSocketSubscriptionProtocolFactory( GraphQLObjectMapper objectMapper, GraphQLSubscriptionInvocationInputFactory invocationInputFactory, GraphQLInvoker graphQLInvoker, Collection connectionListeners) { super(objectMapper, invocationInputFactory, graphQLInvoker, connectionListeners); } public ApolloWebSocketSubscriptionProtocolFactory( GraphQLObjectMapper objectMapper, GraphQLSubscriptionInvocationInputFactory invocationInputFactory, GraphQLInvoker graphQLInvoker, Collection connectionListeners, Duration keepAliveInterval) { super( objectMapper, invocationInputFactory, graphQLInvoker, connectionListeners, keepAliveInterval); } @Override public SubscriptionSession createSession(Session session) { return new ApolloWebSocketSubscriptionSession( new GraphQLSubscriptionMapper(getObjectMapper()), session); } } ================================================ FILE: graphql-java-servlet/src/main/java/graphql/kickstart/servlet/apollo/ApolloWebSocketSubscriptionSession.java ================================================ package graphql.kickstart.servlet.apollo; import graphql.kickstart.execution.subscriptions.GraphQLSubscriptionMapper; import graphql.kickstart.execution.subscriptions.apollo.ApolloSubscriptionSession; import graphql.kickstart.servlet.subscriptions.WebSocketSubscriptionSession; import java.util.Map; import jakarta.websocket.Session; public class ApolloWebSocketSubscriptionSession extends ApolloSubscriptionSession { private final WebSocketSubscriptionSession webSocketSubscriptionSession; public ApolloWebSocketSubscriptionSession(GraphQLSubscriptionMapper mapper, Session session) { super(mapper); webSocketSubscriptionSession = new WebSocketSubscriptionSession(mapper, session); } @Override public boolean isOpen() { return webSocketSubscriptionSession.isOpen(); } @Override public Map getUserProperties() { return webSocketSubscriptionSession.getUserProperties(); } @Override public String getId() { return webSocketSubscriptionSession.getId(); } @Override public Session unwrap() { return webSocketSubscriptionSession.unwrap(); } } ================================================ FILE: graphql-java-servlet/src/main/java/graphql/kickstart/servlet/cache/BufferedHttpServletResponse.java ================================================ package graphql.kickstart.servlet.cache; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.PrintWriter; import jakarta.servlet.ServletOutputStream; import jakarta.servlet.WriteListener; import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponseWrapper; import lombok.extern.slf4j.Slf4j; @Slf4j public class BufferedHttpServletResponse extends HttpServletResponseWrapper { private BufferedOutputStream copier; private ServletOutputStream outputStream; private PrintWriter writer; private String errorMessage; public BufferedHttpServletResponse(HttpServletResponse response) { super(response); } @Override public void sendError(int sc, String msg) throws IOException { errorMessage = msg; super.sendError(sc, msg); } @Override public void sendError(int sc) throws IOException { sendError(sc, null); } @Override public ServletOutputStream getOutputStream() throws IOException { if (writer != null) { throw new IllegalStateException("getWriter() has already been called on this response."); } if (outputStream == null) { outputStream = getResponse().getOutputStream(); copier = new BufferedOutputStream(outputStream); } return copier; } @Override public PrintWriter getWriter() throws IOException { if (outputStream != null) { throw new IllegalStateException( "getOutputStream() has already been called on this response."); } if (writer == null) { copier = new BufferedOutputStream(getResponse().getOutputStream()); writer = new PrintWriter( new OutputStreamWriter(copier, getResponse().getCharacterEncoding()), true); } return writer; } @Override public void flushBuffer() throws IOException { if (writer != null) { writer.flush(); } else if (copier != null) { copier.flush(); } } @Override public boolean isCommitted() { return false; } public void close() throws IOException { if (writer != null) { writer.close(); } else if (copier != null) { copier.close(); } } public String getErrorMessage() { return errorMessage; } public byte[] getContentAsByteArray() { if (copier != null) { return copier.toByteArray(); } else { return new byte[0]; } } private static final class BufferedOutputStream extends ServletOutputStream { private final OutputStream delegate; private final ByteArrayOutputStream buf = new ByteArrayOutputStream(); public BufferedOutputStream(OutputStream delegate) { this.delegate = delegate; } public void write(int b) throws IOException { buf.write(b); delegate.write(b); } @Override public void flush() throws IOException { buf.flush(); delegate.flush(); } @Override public void close() throws IOException { buf.close(); delegate.close(); } @Override public boolean isReady() { return true; } @Override public void setWriteListener(WriteListener writeListener) { // write listener not supported } public byte[] toByteArray() { return buf.toByteArray(); } } } ================================================ FILE: graphql-java-servlet/src/main/java/graphql/kickstart/servlet/cache/CacheReader.java ================================================ package graphql.kickstart.servlet.cache; import graphql.kickstart.execution.input.GraphQLInvocationInput; import graphql.kickstart.servlet.HttpRequestHandler; import java.io.IOException; import java.nio.charset.StandardCharsets; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.extern.slf4j.Slf4j; @Slf4j public class CacheReader { /** * Response from cache if possible, if nothing in cache will not produce any response * * @return {@literal true} if response was fulfilled from cache, {@literal false} is cache not * found or an error occurred while reading value from cache * @throws IOException if can not read value from the cache */ public boolean responseFromCache( GraphQLInvocationInput invocationInput, HttpServletRequest request, HttpServletResponse response, GraphQLResponseCacheManager cacheManager) throws IOException { try { CachedResponse cachedResponse = cacheManager.get(request, invocationInput); if (cachedResponse != null) { write(response, cachedResponse); return true; } } catch (Exception t) { log.warn("Ignore read from cache, unexpected error happened", t); } return false; } private void write(HttpServletResponse response, CachedResponse cachedResponse) throws IOException { if (cachedResponse.isError()) { response.sendError(cachedResponse.getErrorStatusCode(), cachedResponse.getErrorMessage()); } else { response.setContentType(HttpRequestHandler.APPLICATION_JSON_UTF8); response.setStatus(HttpRequestHandler.STATUS_OK); response.setCharacterEncoding(StandardCharsets.UTF_8.name()); response.setContentLength(cachedResponse.getContentBytes().length); response.getOutputStream().write(cachedResponse.getContentBytes()); } } } ================================================ FILE: graphql-java-servlet/src/main/java/graphql/kickstart/servlet/cache/CachedResponse.java ================================================ package graphql.kickstart.servlet.cache; import java.io.Serializable; import java.util.Objects; public class CachedResponse implements Serializable { private static final long serialVersionUID = 5894555791705575139L; private final byte[] contentBytes; private final boolean error; private final Integer errorStatusCode; private final String errorMessage; private CachedResponse( byte[] contentBytes, boolean error, Integer errorStatusCode, String errorMessage) { this.contentBytes = contentBytes; this.error = error; this.errorStatusCode = errorStatusCode; this.errorMessage = errorMessage; } /** * Constructor for success response * * @param contentBytes bytes array of graphql json response */ public static CachedResponse ofContent(byte[] contentBytes) { Objects.requireNonNull(contentBytes, "contentBytes can not be null"); return new CachedResponse(contentBytes, false, null, null); } /** * Constructor for error response * * @param errorStatusCode the status code for the error response * @param errorMessage the error message for the error response */ public static CachedResponse ofError(int errorStatusCode, String errorMessage) { return new CachedResponse(null, true, errorStatusCode, errorMessage); } /** @return {@literal true} when this request was failed */ public boolean isError() { return error; } /** * @return the response body for success requests, {@literal null} when {@link #isError()} is * {@literal true} */ public byte[] getContentBytes() { return contentBytes; } /** @return the response error code */ public Integer getErrorStatusCode() { return errorStatusCode; } /** @return the response error message */ public String getErrorMessage() { return errorMessage; } } ================================================ FILE: graphql-java-servlet/src/main/java/graphql/kickstart/servlet/cache/CachingHttpRequestInvoker.java ================================================ package graphql.kickstart.servlet.cache; import static graphql.kickstart.servlet.HttpRequestHandler.STATUS_BAD_REQUEST; import graphql.kickstart.execution.input.GraphQLInvocationInput; import graphql.kickstart.servlet.GraphQLConfiguration; import graphql.kickstart.servlet.HttpRequestInvoker; import graphql.kickstart.servlet.HttpRequestInvokerImpl; import graphql.kickstart.servlet.ListenerHandler; import java.io.IOException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.AccessLevel; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @Slf4j @RequiredArgsConstructor(access = AccessLevel.PROTECTED) public class CachingHttpRequestInvoker implements HttpRequestInvoker { private final GraphQLConfiguration configuration; private final HttpRequestInvoker requestInvoker; private final CacheReader cacheReader; public CachingHttpRequestInvoker(GraphQLConfiguration configuration) { this( configuration, new HttpRequestInvokerImpl( configuration, configuration.getGraphQLInvoker(), new CachingQueryResponseWriterFactory()), new CacheReader()); } /** Try to return value from cache if cache exists, otherwise process the query normally */ @Override public void execute( GraphQLInvocationInput invocationInput, HttpServletRequest request, HttpServletResponse response, ListenerHandler listenerHandler) { try { if (!cacheReader.responseFromCache( invocationInput, request, response, configuration.getResponseCacheManager())) { requestInvoker.execute(invocationInput, request, response, listenerHandler); } } catch (IOException e) { response.setStatus(STATUS_BAD_REQUEST); log.warn("Unexpected error happened during response from cache", e); } } } ================================================ FILE: graphql-java-servlet/src/main/java/graphql/kickstart/servlet/cache/CachingQueryResponseWriter.java ================================================ package graphql.kickstart.servlet.cache; import graphql.kickstart.execution.input.GraphQLInvocationInput; import graphql.kickstart.servlet.QueryResponseWriter; import java.io.IOException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.extern.slf4j.Slf4j; @Slf4j public class CachingQueryResponseWriter implements QueryResponseWriter { private final QueryResponseWriter delegate; private final GraphQLResponseCacheManager responseCache; private final GraphQLInvocationInput invocationInput; private final boolean error; public CachingQueryResponseWriter( QueryResponseWriter delegate, GraphQLResponseCacheManager responseCache, GraphQLInvocationInput invocationInput, boolean error) { this.delegate = delegate; this.responseCache = responseCache; this.invocationInput = invocationInput; this.error = error; } @Override public void write(HttpServletRequest request, HttpServletResponse response) throws IOException { if (responseCache.isCacheable(request, invocationInput)) { BufferedHttpServletResponse cachingResponseWrapper = new BufferedHttpServletResponse(response); delegate.write(request, cachingResponseWrapper); try { if (error) { int errorStatusCode = cachingResponseWrapper.getStatus(); String errorMessage = cachingResponseWrapper.getErrorMessage(); responseCache.put( request, invocationInput, CachedResponse.ofError(errorStatusCode, errorMessage)); } else { byte[] contentBytes = cachingResponseWrapper.getContentAsByteArray(); responseCache.put(request, invocationInput, CachedResponse.ofContent(contentBytes)); } } catch (Exception t) { log.warn("Ignore read from cache, unexpected error happened", t); } cachingResponseWrapper.flushBuffer(); cachingResponseWrapper.close(); } else { delegate.write(request, response); } } } ================================================ FILE: graphql-java-servlet/src/main/java/graphql/kickstart/servlet/cache/CachingQueryResponseWriterFactory.java ================================================ package graphql.kickstart.servlet.cache; import graphql.kickstart.execution.GraphQLQueryResult; import graphql.kickstart.execution.input.GraphQLInvocationInput; import graphql.kickstart.servlet.GraphQLConfiguration; import graphql.kickstart.servlet.QueryResponseWriter; import graphql.kickstart.servlet.QueryResponseWriterFactory; import graphql.kickstart.servlet.QueryResponseWriterFactoryImpl; public class CachingQueryResponseWriterFactory implements QueryResponseWriterFactory { private final QueryResponseWriterFactory queryResponseWriterFactory = new QueryResponseWriterFactoryImpl(); @Override public QueryResponseWriter createWriter( GraphQLInvocationInput invocationInput, GraphQLQueryResult queryResult, GraphQLConfiguration configuration) { QueryResponseWriter writer = queryResponseWriterFactory.createWriter(invocationInput, queryResult, configuration); if (configuration.getResponseCacheManager() != null) { return new CachingQueryResponseWriter( writer, configuration.getResponseCacheManager(), invocationInput, queryResult.isError()); } return writer; } } ================================================ FILE: graphql-java-servlet/src/main/java/graphql/kickstart/servlet/cache/GraphQLResponseCacheManager.java ================================================ package graphql.kickstart.servlet.cache; import graphql.kickstart.execution.input.GraphQLInvocationInput; import java.util.Optional; import jakarta.servlet.http.HttpServletRequest; public interface GraphQLResponseCacheManager { /** * Retrieve the cache by input data. If this query was not cached before, will return empty {@link * Optional}. * * @param request the http request * @param invocationInput input data * @return cached response if something available in cache or {@literal null} if nothing cached */ CachedResponse get(HttpServletRequest request, GraphQLInvocationInput invocationInput); /** * Decide to cache or not this response. It depends on the implementation. * * @param request the http request * @param invocationInput input data */ boolean isCacheable(HttpServletRequest request, GraphQLInvocationInput invocationInput); /** * Cache this response. It depends on the implementation. * * @param request the http request * @param invocationInput input data * @param cachedResponse response to cache */ void put( HttpServletRequest request, GraphQLInvocationInput invocationInput, CachedResponse cachedResponse); } ================================================ FILE: graphql-java-servlet/src/main/java/graphql/kickstart/servlet/config/DefaultGraphQLSchemaServletProvider.java ================================================ package graphql.kickstart.servlet.config; import graphql.kickstart.execution.config.DefaultGraphQLSchemaProvider; import graphql.schema.GraphQLSchema; import jakarta.servlet.http.HttpServletRequest; import jakarta.websocket.server.HandshakeRequest; /** @author Andrew Potter */ public class DefaultGraphQLSchemaServletProvider extends DefaultGraphQLSchemaProvider implements GraphQLSchemaServletProvider { public DefaultGraphQLSchemaServletProvider(GraphQLSchema schema) { super(schema); } @Override public GraphQLSchema getSchema(HttpServletRequest request) { return getSchema(); } @Override public GraphQLSchema getSchema(HandshakeRequest request) { return getSchema(); } @Override public GraphQLSchema getReadOnlySchema(HttpServletRequest request) { return getReadOnlySchema(); } } ================================================ FILE: graphql-java-servlet/src/main/java/graphql/kickstart/servlet/config/GraphQLSchemaServletProvider.java ================================================ package graphql.kickstart.servlet.config; import graphql.kickstart.execution.config.GraphQLSchemaProvider; import graphql.schema.GraphQLSchema; import jakarta.servlet.http.HttpServletRequest; import jakarta.websocket.server.HandshakeRequest; public interface GraphQLSchemaServletProvider extends GraphQLSchemaProvider { /** * @param request the http request * @return a schema based on the request (auth, etc). */ GraphQLSchema getSchema(HttpServletRequest request); /** * @param request the http request used to create a websocket * @return a schema based on the request (auth, etc). */ GraphQLSchema getSchema(HandshakeRequest request); /** * @param request the http request * @return a read-only schema based on the request (auth, etc). Should return the same schema * (query/subscription-only version) as {@link #getSchema(HttpServletRequest)} for a given * request. */ GraphQLSchema getReadOnlySchema(HttpServletRequest request); } ================================================ FILE: graphql-java-servlet/src/main/java/graphql/kickstart/servlet/context/DefaultGraphQLServletContext.java ================================================ package graphql.kickstart.servlet.context; import graphql.kickstart.execution.context.DefaultGraphQLContext; import java.util.List; import java.util.Map; import java.util.stream.Collectors; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.Part; import lombok.SneakyThrows; import org.dataloader.DataLoaderRegistry; /** @deprecated Use {@link graphql.kickstart.execution.context.GraphQLKickstartContext} instead */ public class DefaultGraphQLServletContext extends DefaultGraphQLContext implements GraphQLServletContext { protected DefaultGraphQLServletContext( DataLoaderRegistry dataLoaderRegistry, HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) { super(dataLoaderRegistry); put(HttpServletRequest.class, httpServletRequest); put(HttpServletResponse.class, httpServletResponse); } public static Builder createServletContext(DataLoaderRegistry registry) { return new Builder(registry); } public static Builder createServletContext() { return new Builder(new DataLoaderRegistry()); } /** * @deprecated Use {@code * dataFetchingEnvironment.getGraphQlContext().get(HttpServletRequest.class)} instead. Since * 13.0.0 */ @Override @Deprecated public HttpServletRequest getHttpServletRequest() { return (HttpServletRequest) getMapOfContext().get(HttpServletRequest.class); } /** * @deprecated Use {@code * dataFetchingEnvironment.getGraphQlContext().get(HttpServletResponse.class)} instead. Since * 13.0.0 */ @Override @Deprecated public HttpServletResponse getHttpServletResponse() { return (HttpServletResponse) getMapOfContext().get(HttpServletResponse.class); } /** * @deprecated Use {@code * dataFetchingEnvironment.getGraphQlContext().get(HttpServletRequest.class)} instead to get * the request and retrieve the file parts yourself. Since 13.0.0 */ @Override @Deprecated @SneakyThrows public List getFileParts() { return getHttpServletRequest().getParts().stream() .filter(part -> part.getContentType() != null) .collect(Collectors.toList()); } /** * @deprecated Use {@code * dataFetchingEnvironment.getGraphQlContext().get(HttpServletRequest.class)} instead to get * the request and retrieve the parts yourself. Since 13.0.0 */ @Override @Deprecated @SneakyThrows public Map> getParts() { return getHttpServletRequest().getParts().stream() .collect(Collectors.groupingBy(Part::getName)); } public static class Builder { private HttpServletRequest httpServletRequest; private HttpServletResponse httpServletResponse; private DataLoaderRegistry dataLoaderRegistry; private Builder(DataLoaderRegistry dataLoaderRegistry) { this.dataLoaderRegistry = dataLoaderRegistry; } public DefaultGraphQLServletContext build() { return new DefaultGraphQLServletContext( dataLoaderRegistry, httpServletRequest, httpServletResponse); } public Builder with(HttpServletRequest httpServletRequest) { this.httpServletRequest = httpServletRequest; return this; } public Builder with(DataLoaderRegistry dataLoaderRegistry) { this.dataLoaderRegistry = dataLoaderRegistry; return this; } public Builder with(HttpServletResponse httpServletResponse) { this.httpServletResponse = httpServletResponse; return this; } } } ================================================ FILE: graphql-java-servlet/src/main/java/graphql/kickstart/servlet/context/DefaultGraphQLServletContextBuilder.java ================================================ package graphql.kickstart.servlet.context; import graphql.kickstart.execution.context.DefaultGraphQLContextBuilder; import graphql.kickstart.execution.context.GraphQLKickstartContext; import java.util.HashMap; import java.util.Map; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jakarta.websocket.Session; import jakarta.websocket.server.HandshakeRequest; /** Returns an empty context. */ public class DefaultGraphQLServletContextBuilder extends DefaultGraphQLContextBuilder implements GraphQLServletContextBuilder { @Override public GraphQLKickstartContext build(HttpServletRequest request, HttpServletResponse response) { Map map = new HashMap<>(); map.put(HttpServletRequest.class, request); map.put(HttpServletResponse.class, response); return GraphQLKickstartContext.of(map); } @Override public GraphQLKickstartContext build(Session session, HandshakeRequest handshakeRequest) { Map map = new HashMap<>(); map.put(Session.class, session); map.put(HandshakeRequest.class, handshakeRequest); return GraphQLKickstartContext.of(map); } } ================================================ FILE: graphql-java-servlet/src/main/java/graphql/kickstart/servlet/context/DefaultGraphQLWebSocketContext.java ================================================ package graphql.kickstart.servlet.context; import graphql.kickstart.execution.context.DefaultGraphQLContext; import jakarta.websocket.Session; import jakarta.websocket.server.HandshakeRequest; import org.dataloader.DataLoaderRegistry; /** @deprecated Use {@link graphql.kickstart.execution.context.GraphQLKickstartContext} instead */ @Deprecated public class DefaultGraphQLWebSocketContext extends DefaultGraphQLContext implements GraphQLWebSocketContext { private DefaultGraphQLWebSocketContext( DataLoaderRegistry dataLoaderRegistry, Session session, HandshakeRequest handshakeRequest) { super(dataLoaderRegistry); put(Session.class, session); put(HandshakeRequest.class, handshakeRequest); } public static Builder createWebSocketContext(DataLoaderRegistry registry) { return new Builder(registry); } public static Builder createWebSocketContext() { return new Builder(new DataLoaderRegistry()); } /** * @deprecated Use {@code dataFetchingEnvironment.getGraphQlContext().get(Session.class)} instead. * Since 13.0.0 */ @Override @Deprecated public Session getSession() { return (Session) getMapOfContext().get(Session.class); } /** * @deprecated Use {@code dataFetchingEnvironment.getGraphQlContext().get(HandshakeRequest.class)} * instead. Since 13.0.0 */ @Override @Deprecated public HandshakeRequest getHandshakeRequest() { return (HandshakeRequest) getMapOfContext().get(HandshakeRequest.class); } public static class Builder { private Session session; private HandshakeRequest handshakeRequest; private DataLoaderRegistry dataLoaderRegistry; private Builder(DataLoaderRegistry dataLoaderRegistry) { this.dataLoaderRegistry = dataLoaderRegistry; } public DefaultGraphQLWebSocketContext build() { return new DefaultGraphQLWebSocketContext(dataLoaderRegistry, session, handshakeRequest); } public Builder with(Session session) { this.session = session; return this; } public Builder with(HandshakeRequest handshakeRequest) { this.handshakeRequest = handshakeRequest; return this; } public Builder with(DataLoaderRegistry dataLoaderRegistry) { this.dataLoaderRegistry = dataLoaderRegistry; return this; } } } ================================================ FILE: graphql-java-servlet/src/main/java/graphql/kickstart/servlet/context/GraphQLServletContext.java ================================================ package graphql.kickstart.servlet.context; import graphql.kickstart.execution.context.GraphQLKickstartContext; import java.util.List; import java.util.Map; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.Part; /** @deprecated Use {@link graphql.kickstart.execution.context.GraphQLKickstartContext} instead */ public interface GraphQLServletContext extends GraphQLKickstartContext { List getFileParts(); Map> getParts(); HttpServletRequest getHttpServletRequest(); HttpServletResponse getHttpServletResponse(); } ================================================ FILE: graphql-java-servlet/src/main/java/graphql/kickstart/servlet/context/GraphQLServletContextBuilder.java ================================================ package graphql.kickstart.servlet.context; import graphql.kickstart.execution.context.GraphQLKickstartContext; import graphql.kickstart.execution.context.GraphQLContextBuilder; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jakarta.websocket.Session; import jakarta.websocket.server.HandshakeRequest; public interface GraphQLServletContextBuilder extends GraphQLContextBuilder { GraphQLKickstartContext build( HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse); GraphQLKickstartContext build(Session session, HandshakeRequest handshakeRequest); } ================================================ FILE: graphql-java-servlet/src/main/java/graphql/kickstart/servlet/context/GraphQLWebSocketContext.java ================================================ package graphql.kickstart.servlet.context; import graphql.kickstart.execution.context.GraphQLKickstartContext; import jakarta.websocket.Session; import jakarta.websocket.server.HandshakeRequest; /** @deprecated Use {@link graphql.kickstart.execution.context.GraphQLKickstartContext} instead */ public interface GraphQLWebSocketContext extends GraphQLKickstartContext { Session getSession(); HandshakeRequest getHandshakeRequest(); } ================================================ FILE: graphql-java-servlet/src/main/java/graphql/kickstart/servlet/core/DefaultGraphQLRootObjectBuilder.java ================================================ package graphql.kickstart.servlet.core; import graphql.kickstart.execution.StaticGraphQLRootObjectBuilder; import jakarta.servlet.http.HttpServletRequest; import jakarta.websocket.server.HandshakeRequest; public class DefaultGraphQLRootObjectBuilder extends StaticGraphQLRootObjectBuilder implements GraphQLServletRootObjectBuilder { public DefaultGraphQLRootObjectBuilder() { super(new Object()); } @Override public Object build(HttpServletRequest req) { return getRootObject(); } @Override public Object build(HandshakeRequest req) { return getRootObject(); } } ================================================ FILE: graphql-java-servlet/src/main/java/graphql/kickstart/servlet/core/GraphQLMBean.java ================================================ package graphql.kickstart.servlet.core; public interface GraphQLMBean { String[] getQueries(); String[] getMutations(); String executeQuery(String query); } ================================================ FILE: graphql-java-servlet/src/main/java/graphql/kickstart/servlet/core/GraphQLServletListener.java ================================================ package graphql.kickstart.servlet.core; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; /** @author Andrew Potter */ public interface GraphQLServletListener { /** * Called this method when the request started processing. * @param request http request * @param response http response * @return request callback or {@literal null} */ default RequestCallback onRequest(HttpServletRequest request, HttpServletResponse response) { return null; } /** * The callback which used to add additional listeners for GraphQL request execution. */ interface RequestCallback { /** * Called when failed to parse InvocationInput and the response was not written. * @param request http request * @param response http response */ default void onParseError( HttpServletRequest request, HttpServletResponse response, Throwable throwable) {} /** * Called right before the response will be written and flushed. Can be used for applying some * changes to the response object, like adding response headers. * @param request http request * @param response http response */ default void beforeFlush(HttpServletRequest request, HttpServletResponse response) {} /** * Called when GraphQL invoked successfully and the response was written already. * @param request http request * @param response http response */ default void onSuccess(HttpServletRequest request, HttpServletResponse response) {} /** * Called when GraphQL was failed and the response was written already. * @param request http request * @param response http response */ default void onError( HttpServletRequest request, HttpServletResponse response, Throwable throwable) {} /** * Called finally once on both success and failed GraphQL invocation. The response is also * already written. * @param request http request * @param response http response */ default void onFinally(HttpServletRequest request, HttpServletResponse response) {} } } ================================================ FILE: graphql-java-servlet/src/main/java/graphql/kickstart/servlet/core/GraphQLServletRootObjectBuilder.java ================================================ package graphql.kickstart.servlet.core; import graphql.kickstart.execution.GraphQLRootObjectBuilder; import jakarta.servlet.http.HttpServletRequest; import jakarta.websocket.server.HandshakeRequest; public interface GraphQLServletRootObjectBuilder extends GraphQLRootObjectBuilder { Object build(HttpServletRequest req); Object build(HandshakeRequest req); } ================================================ FILE: graphql-java-servlet/src/main/java/graphql/kickstart/servlet/core/internal/GraphQLThreadFactory.java ================================================ package graphql.kickstart.servlet.core.internal; import graphql.kickstart.servlet.AbstractGraphQLHttpServlet; import java.util.concurrent.ThreadFactory; import java.util.concurrent.atomic.AtomicInteger; /** * {@link ThreadFactory} implementation for {@link AbstractGraphQLHttpServlet} async operations * * @author John Nutting */ public class GraphQLThreadFactory implements ThreadFactory { static final String NAME_PREFIX = "GraphQLServlet-"; final AtomicInteger threadNumber = new AtomicInteger(1); @Override public Thread newThread(final Runnable r) { Thread t = new Thread(r, NAME_PREFIX + threadNumber.getAndIncrement()); t.setDaemon(false); t.setPriority(Thread.NORM_PRIORITY); return t; } } ================================================ FILE: graphql-java-servlet/src/main/java/graphql/kickstart/servlet/core/internal/VariableMapException.java ================================================ package graphql.kickstart.servlet.core.internal; public class VariableMapException extends RuntimeException { VariableMapException(String message) { super(message); } } ================================================ FILE: graphql-java-servlet/src/main/java/graphql/kickstart/servlet/core/internal/VariableMapper.java ================================================ package graphql.kickstart.servlet.core.internal; import java.util.List; import java.util.Map; import java.util.regex.Pattern; import jakarta.servlet.http.Part; public class VariableMapper { private static final Pattern PERIOD = Pattern.compile("\\."); private static final Mapper> MAP_MAPPER = new Mapper>() { @Override public Object set(Map location, String target, Part value) { return location.put(target, value); } @Override public Object recurse(Map location, String target) { return location.get(target); } }; private static final Mapper> LIST_MAPPER = new Mapper>() { @Override public Object set(List location, String target, Part value) { return location.set(Integer.parseInt(target), value); } @Override public Object recurse(List location, String target) { return location.get(Integer.parseInt(target)); } }; @SuppressWarnings({"unchecked", "rawtypes"}) public static void mapVariable(String objectPath, Map variables, Part part) { String[] segments = PERIOD.split(objectPath); if (segments.length < 2) { throw new VariableMapException("object-path in map must have at least two segments"); } else if (!"variables".equals(segments[0])) { throw new VariableMapException("can only map into variables"); } Object currentLocation = variables; for (int i = 1; i < segments.length; i++) { String segmentName = segments[i]; Mapper mapper = determineMapper(currentLocation, objectPath, segmentName); if (i == segments.length - 1) { if (null != mapper.set(currentLocation, segmentName, part)) { throw new VariableMapException("expected null value when mapping " + objectPath); } } else { currentLocation = mapper.recurse(currentLocation, segmentName); if (null == currentLocation) { throw new VariableMapException( "found null intermediate value when trying to map " + objectPath); } } } } private static Mapper determineMapper( Object currentLocation, String objectPath, String segmentName) { if (currentLocation instanceof Map) { return MAP_MAPPER; } else if (currentLocation instanceof List) { return LIST_MAPPER; } throw new VariableMapException( "expected a map or list at " + segmentName + " when trying to map " + objectPath); } interface Mapper { Object set(T location, String target, Part value); Object recurse(T location, String target); } } ================================================ FILE: graphql-java-servlet/src/main/java/graphql/kickstart/servlet/input/BatchInputPreProcessResult.java ================================================ package graphql.kickstart.servlet.input; import graphql.kickstart.execution.input.GraphQLBatchedInvocationInput; /** * Wraps the result of pre processing a batch. Allows customization of the response code and message * if the batch isn't to be executed. */ public class BatchInputPreProcessResult { private final GraphQLBatchedInvocationInput batchedInvocationInput; private final int statusCode; private final boolean executable; private final String messsage; public BatchInputPreProcessResult(GraphQLBatchedInvocationInput graphQLBatchedInvocationInput) { this.batchedInvocationInput = graphQLBatchedInvocationInput; this.executable = true; this.statusCode = 200; this.messsage = null; } public BatchInputPreProcessResult(int statusCode, String messsage) { this.batchedInvocationInput = null; this.executable = false; this.statusCode = statusCode; this.messsage = messsage; } /** @return If the servlet should try executing this batched input */ public boolean isExecutable() { return executable; } /** @return the batched input the servlet will try to execute. */ public GraphQLBatchedInvocationInput getBatchedInvocationInput() { return batchedInvocationInput; } /** @return status message the servlet will use if isExecutable is false. */ public String getStatusMessage() { return messsage; } /** @return status code the servlet will use if if isExecutable is false. */ public int getStatusCode() { return statusCode; } } ================================================ FILE: graphql-java-servlet/src/main/java/graphql/kickstart/servlet/input/BatchInputPreProcessor.java ================================================ package graphql.kickstart.servlet.input; import graphql.kickstart.execution.input.GraphQLBatchedInvocationInput; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; public interface BatchInputPreProcessor { /** * An injectable object that allows clients to manipulate a batch before executing, or abort * altogether. * * @param batchedInvocationInput the input to process * @param request the servlet request * @param response the servlet response * @return wrapped batch to possibly process. */ BatchInputPreProcessResult preProcessBatch( GraphQLBatchedInvocationInput batchedInvocationInput, HttpServletRequest request, HttpServletResponse response); } ================================================ FILE: graphql-java-servlet/src/main/java/graphql/kickstart/servlet/input/GraphQLInvocationInputFactory.java ================================================ package graphql.kickstart.servlet.input; import graphql.kickstart.execution.GraphQLRequest; import graphql.kickstart.execution.config.GraphQLSchemaProvider; import graphql.kickstart.execution.context.ContextSetting; import graphql.kickstart.execution.input.GraphQLBatchedInvocationInput; import graphql.kickstart.execution.input.GraphQLSingleInvocationInput; import graphql.kickstart.execution.subscriptions.GraphQLSubscriptionInvocationInputFactory; import graphql.kickstart.execution.subscriptions.SubscriptionSession; import graphql.kickstart.servlet.config.DefaultGraphQLSchemaServletProvider; import graphql.kickstart.servlet.config.GraphQLSchemaServletProvider; import graphql.kickstart.servlet.context.DefaultGraphQLServletContextBuilder; import graphql.kickstart.servlet.context.GraphQLServletContextBuilder; import graphql.kickstart.servlet.core.DefaultGraphQLRootObjectBuilder; import graphql.kickstart.servlet.core.GraphQLServletRootObjectBuilder; import graphql.schema.GraphQLSchema; import java.util.List; import java.util.function.Supplier; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jakarta.websocket.Session; import jakarta.websocket.server.HandshakeRequest; /** @author Andrew Potter */ public class GraphQLInvocationInputFactory implements GraphQLSubscriptionInvocationInputFactory { private final Supplier schemaProviderSupplier; private final Supplier contextBuilderSupplier; private final Supplier rootObjectBuilderSupplier; protected GraphQLInvocationInputFactory( Supplier schemaProviderSupplier, Supplier contextBuilderSupplier, Supplier rootObjectBuilderSupplier) { this.schemaProviderSupplier = schemaProviderSupplier; this.contextBuilderSupplier = contextBuilderSupplier; this.rootObjectBuilderSupplier = rootObjectBuilderSupplier; } public static Builder newBuilder(GraphQLSchema schema) { return new Builder(new DefaultGraphQLSchemaServletProvider(schema)); } public static Builder newBuilder(GraphQLSchemaServletProvider schemaProvider) { return new Builder(schemaProvider); } public static Builder newBuilder(Supplier schemaProviderSupplier) { return new Builder(schemaProviderSupplier); } public GraphQLSchemaProvider getSchemaProvider() { return schemaProviderSupplier.get(); } public GraphQLSingleInvocationInput create( GraphQLRequest graphQLRequest, HttpServletRequest request, HttpServletResponse response) { return create(graphQLRequest, request, response, false); } public GraphQLBatchedInvocationInput create( ContextSetting contextSetting, List graphQLRequests, HttpServletRequest request, HttpServletResponse response) { return create(contextSetting, graphQLRequests, request, response, false); } public GraphQLSingleInvocationInput createReadOnly( GraphQLRequest graphQLRequest, HttpServletRequest request, HttpServletResponse response) { return create(graphQLRequest, request, response, true); } public GraphQLBatchedInvocationInput createReadOnly( ContextSetting contextSetting, List graphQLRequests, HttpServletRequest request, HttpServletResponse response) { return create(contextSetting, graphQLRequests, request, response, true); } public GraphQLSingleInvocationInput create(GraphQLRequest graphQLRequest) { return new GraphQLSingleInvocationInput( graphQLRequest, schemaProviderSupplier.get().getSchema(), contextBuilderSupplier.get().build(), rootObjectBuilderSupplier.get().build()); } private GraphQLSingleInvocationInput create( GraphQLRequest graphQLRequest, HttpServletRequest request, HttpServletResponse response, boolean readOnly) { return new GraphQLSingleInvocationInput( graphQLRequest, readOnly ? schemaProviderSupplier.get().getReadOnlySchema(request) : schemaProviderSupplier.get().getSchema(request), contextBuilderSupplier.get().build(request, response), rootObjectBuilderSupplier.get().build(request)); } private GraphQLBatchedInvocationInput create( ContextSetting contextSetting, List graphQLRequests, HttpServletRequest request, HttpServletResponse response, boolean readOnly) { return contextSetting.getBatch( graphQLRequests, readOnly ? schemaProviderSupplier.get().getReadOnlySchema(request) : schemaProviderSupplier.get().getSchema(request), () -> contextBuilderSupplier.get().build(request, response), rootObjectBuilderSupplier.get().build(request)); } @Override public GraphQLSingleInvocationInput create( GraphQLRequest graphQLRequest, SubscriptionSession session) { HandshakeRequest request = (HandshakeRequest) session.getUserProperties().get(HandshakeRequest.class.getName()); return new GraphQLSingleInvocationInput( graphQLRequest, schemaProviderSupplier.get().getSchema(request), contextBuilderSupplier.get().build((Session) session.unwrap(), request), rootObjectBuilderSupplier.get().build(request)); } public GraphQLBatchedInvocationInput create( ContextSetting contextSetting, List graphQLRequest, Session session) { HandshakeRequest request = (HandshakeRequest) session.getUserProperties().get(HandshakeRequest.class.getName()); return contextSetting.getBatch( graphQLRequest, schemaProviderSupplier.get().getSchema(request), () -> contextBuilderSupplier.get().build(session, request), rootObjectBuilderSupplier.get().build(request)); } public static class Builder { private final Supplier schemaProviderSupplier; private Supplier contextBuilderSupplier = DefaultGraphQLServletContextBuilder::new; private Supplier rootObjectBuilderSupplier = DefaultGraphQLRootObjectBuilder::new; public Builder(GraphQLSchemaServletProvider schemaProvider) { this(() -> schemaProvider); } public Builder(Supplier schemaProviderSupplier) { this.schemaProviderSupplier = schemaProviderSupplier; } public Builder withGraphQLContextBuilder(GraphQLServletContextBuilder contextBuilder) { return withGraphQLContextBuilder(() -> contextBuilder); } public Builder withGraphQLContextBuilder( Supplier contextBuilderSupplier) { this.contextBuilderSupplier = contextBuilderSupplier; return this; } public Builder withGraphQLRootObjectBuilder(GraphQLServletRootObjectBuilder rootObjectBuilder) { return withGraphQLRootObjectBuilder(() -> rootObjectBuilder); } public Builder withGraphQLRootObjectBuilder( Supplier rootObjectBuilderSupplier) { this.rootObjectBuilderSupplier = rootObjectBuilderSupplier; return this; } public GraphQLInvocationInputFactory build() { return new GraphQLInvocationInputFactory( schemaProviderSupplier, contextBuilderSupplier, rootObjectBuilderSupplier); } } } ================================================ FILE: graphql-java-servlet/src/main/java/graphql/kickstart/servlet/input/NoOpBatchInputPreProcessor.java ================================================ package graphql.kickstart.servlet.input; import graphql.kickstart.execution.input.GraphQLBatchedInvocationInput; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; /** A default BatchInputPreProcessor that returns the input. */ public class NoOpBatchInputPreProcessor implements BatchInputPreProcessor { @Override public BatchInputPreProcessResult preProcessBatch( GraphQLBatchedInvocationInput batchedInvocationInput, HttpServletRequest request, HttpServletResponse response) { return new BatchInputPreProcessResult(batchedInvocationInput); } } ================================================ FILE: graphql-java-servlet/src/main/java/graphql/kickstart/servlet/osgi/GraphQLCodeRegistryProvider.java ================================================ package graphql.kickstart.servlet.osgi; import graphql.schema.GraphQLCodeRegistry; public interface GraphQLCodeRegistryProvider extends GraphQLProvider { GraphQLCodeRegistry getCodeRegistry(); } ================================================ FILE: graphql-java-servlet/src/main/java/graphql/kickstart/servlet/osgi/GraphQLDirectiveProvider.java ================================================ package graphql.kickstart.servlet.osgi; import graphql.schema.GraphQLDirective; import java.util.Collection; public interface GraphQLDirectiveProvider extends GraphQLProvider { /** @return A collection of directive definitions that will be added to the schema. */ Collection getDirectives(); } ================================================ FILE: graphql-java-servlet/src/main/java/graphql/kickstart/servlet/osgi/GraphQLFieldProvider.java ================================================ package graphql.kickstart.servlet.osgi; import graphql.schema.GraphQLFieldDefinition; import java.util.Collection; public interface GraphQLFieldProvider extends GraphQLProvider { Collection getFields(); } ================================================ FILE: graphql-java-servlet/src/main/java/graphql/kickstart/servlet/osgi/GraphQLMutationProvider.java ================================================ package graphql.kickstart.servlet.osgi; import graphql.schema.GraphQLFieldDefinition; import java.util.Collection; public interface GraphQLMutationProvider extends GraphQLFieldProvider { Collection getMutations(); default Collection getFields() { return getMutations(); } } ================================================ FILE: graphql-java-servlet/src/main/java/graphql/kickstart/servlet/osgi/GraphQLProvider.java ================================================ package graphql.kickstart.servlet.osgi; public interface GraphQLProvider {} ================================================ FILE: graphql-java-servlet/src/main/java/graphql/kickstart/servlet/osgi/GraphQLQueryProvider.java ================================================ package graphql.kickstart.servlet.osgi; import graphql.schema.GraphQLFieldDefinition; import java.util.Collection; /** This interface is used by OSGi bundles to plugin new field into the root query type */ public interface GraphQLQueryProvider extends GraphQLProvider { /** @return a collection of field definitions that will be added to the root query type. */ Collection getQueries(); } ================================================ FILE: graphql-java-servlet/src/main/java/graphql/kickstart/servlet/osgi/GraphQLSubscriptionProvider.java ================================================ package graphql.kickstart.servlet.osgi; import graphql.schema.GraphQLFieldDefinition; import java.util.Collection; public interface GraphQLSubscriptionProvider extends GraphQLFieldProvider { Collection getSubscriptions(); default Collection getFields() { return getSubscriptions(); } } ================================================ FILE: graphql-java-servlet/src/main/java/graphql/kickstart/servlet/osgi/GraphQLTypesProvider.java ================================================ package graphql.kickstart.servlet.osgi; import graphql.schema.GraphQLType; import java.util.Collection; public interface GraphQLTypesProvider extends GraphQLProvider { Collection getTypes(); } ================================================ FILE: graphql-java-servlet/src/main/java/graphql/kickstart/servlet/subscriptions/FallbackSubscriptionConsumer.java ================================================ package graphql.kickstart.servlet.subscriptions; import graphql.ExecutionResult; import graphql.kickstart.execution.GraphQLInvoker; import graphql.kickstart.execution.GraphQLRequest; import graphql.kickstart.execution.input.GraphQLSingleInvocationInput; import graphql.kickstart.execution.subscriptions.GraphQLSubscriptionInvocationInputFactory; import graphql.kickstart.execution.subscriptions.GraphQLSubscriptionMapper; import graphql.kickstart.execution.subscriptions.SubscriptionSession; import java.util.Objects; import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.function.Consumer; import lombok.RequiredArgsConstructor; /** @author Andrew Potter */ @RequiredArgsConstructor public class FallbackSubscriptionConsumer implements Consumer { private final SubscriptionSession session; private final GraphQLSubscriptionMapper mapper; private final GraphQLSubscriptionInvocationInputFactory invocationInputFactory; private final GraphQLInvoker graphQLInvoker; @Override public void accept(String text) { CompletableFuture executionResult = executeAsync(text, session); executionResult.thenAccept( result -> handleSubscriptionStart(session, UUID.randomUUID().toString(), result)); } private CompletableFuture executeAsync( String payload, SubscriptionSession session) { Objects.requireNonNull(payload, "Payload is required"); GraphQLRequest graphQLRequest = mapper.readGraphQLRequest(payload); GraphQLSingleInvocationInput invocationInput = invocationInputFactory.create(graphQLRequest, session); return graphQLInvoker.executeAsync(invocationInput); } private void handleSubscriptionStart( SubscriptionSession session, String id, ExecutionResult executionResult) { ExecutionResult sanitizedExecutionResult = mapper.sanitizeErrors(executionResult); if (mapper.hasNoErrors(sanitizedExecutionResult)) { session.subscribe(id, sanitizedExecutionResult.getData()); } else { Object payload = mapper.convertSanitizedExecutionResult(sanitizedExecutionResult); session.sendDataMessage(id, payload); } } } ================================================ FILE: graphql-java-servlet/src/main/java/graphql/kickstart/servlet/subscriptions/FallbackSubscriptionProtocolFactory.java ================================================ package graphql.kickstart.servlet.subscriptions; import graphql.kickstart.execution.GraphQLInvoker; import graphql.kickstart.execution.subscriptions.GraphQLSubscriptionInvocationInputFactory; import graphql.kickstart.execution.subscriptions.GraphQLSubscriptionMapper; import graphql.kickstart.execution.subscriptions.SubscriptionProtocolFactory; import graphql.kickstart.execution.subscriptions.SubscriptionSession; import java.util.function.Consumer; import jakarta.websocket.Session; /** @author Andrew Potter */ public class FallbackSubscriptionProtocolFactory extends SubscriptionProtocolFactory implements WebSocketSubscriptionProtocolFactory { private final GraphQLSubscriptionMapper mapper; private final GraphQLSubscriptionInvocationInputFactory invocationInputFactory; private final GraphQLInvoker graphQLInvoker; public FallbackSubscriptionProtocolFactory( GraphQLSubscriptionMapper mapper, GraphQLSubscriptionInvocationInputFactory invocationInputFactory, GraphQLInvoker graphQLInvoker) { super(""); this.mapper = mapper; this.invocationInputFactory = invocationInputFactory; this.graphQLInvoker = graphQLInvoker; } @Override public Consumer createConsumer(SubscriptionSession session) { return new FallbackSubscriptionConsumer( session, mapper, invocationInputFactory, graphQLInvoker); } @Override public SubscriptionSession createSession(Session session) { return new WebSocketSubscriptionSession(mapper, session); } } ================================================ FILE: graphql-java-servlet/src/main/java/graphql/kickstart/servlet/subscriptions/WebSocketSendSubscriber.java ================================================ package graphql.kickstart.servlet.subscriptions; import java.io.IOException; import java.util.concurrent.atomic.AtomicReference; import jakarta.websocket.Session; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; @Slf4j @RequiredArgsConstructor public class WebSocketSendSubscriber implements Subscriber { private final Session session; private AtomicReference subscriptionRef = new AtomicReference<>(); @Override public void onSubscribe(Subscription subscription) { subscriptionRef.set(subscription); subscriptionRef.get().request(1); } @Override public void onNext(String message) { subscriptionRef.get().request(1); if (session.isOpen()) { try { session.getBasicRemote().sendText(message); } catch (IOException e) { log.error("Cannot send message {}", message, e); } } } @Override public void onError(Throwable t) { log.error("WebSocket error", t); } @Override public void onComplete() { subscriptionRef.get().request(1); if (session.isOpen()) { try { log.debug("Closing session"); session.close(); } catch (IOException e) { log.error("Cannot close session", e); } } subscriptionRef.get().cancel(); } } ================================================ FILE: graphql-java-servlet/src/main/java/graphql/kickstart/servlet/subscriptions/WebSocketSubscriptionProtocolFactory.java ================================================ package graphql.kickstart.servlet.subscriptions; import graphql.kickstart.execution.subscriptions.SubscriptionSession; import java.util.function.Consumer; import jakarta.websocket.Session; public interface WebSocketSubscriptionProtocolFactory { Consumer createConsumer(SubscriptionSession session); SubscriptionSession createSession(Session session); } ================================================ FILE: graphql-java-servlet/src/main/java/graphql/kickstart/servlet/subscriptions/WebSocketSubscriptionSession.java ================================================ package graphql.kickstart.servlet.subscriptions; import graphql.kickstart.execution.subscriptions.DefaultSubscriptionSession; import graphql.kickstart.execution.subscriptions.GraphQLSubscriptionMapper; import java.util.Map; import jakarta.websocket.Session; public class WebSocketSubscriptionSession extends DefaultSubscriptionSession { private final Session session; public WebSocketSubscriptionSession(GraphQLSubscriptionMapper mapper, Session session) { super(mapper); this.session = session; } @Override public boolean isOpen() { return session.isOpen(); } @Override public Map getUserProperties() { return session.getUserProperties(); } @Override public String getId() { return session.getId(); } @Override public Session unwrap() { return session; } } ================================================ FILE: graphql-java-servlet/src/test/groovy/graphql/kickstart/servlet/AbstractGraphQLHttpServletSpec.groovy ================================================ package graphql.kickstart.servlet import com.fasterxml.jackson.databind.ObjectMapper import graphql.Scalars import graphql.execution.ExecutionStepInfo import graphql.execution.MergedField import graphql.execution.reactive.SingleSubscriberPublisher import graphql.kickstart.servlet.input.GraphQLInvocationInputFactory import graphql.language.Field import graphql.schema.GraphQLNonNull import org.springframework.mock.web.MockHttpServletRequest import org.springframework.mock.web.MockHttpServletResponse import spock.lang.Shared import spock.lang.Specification import java.nio.charset.StandardCharsets import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicReference /** * @author Andrew Potter */ class AbstractGraphQLHttpServletSpec extends Specification { public static final int STATUS_OK = 200 public static final int STATUS_BAD_REQUEST = 400 public static final int STATUS_ERROR = 500 public static final String CONTENT_TYPE_JSON_UTF8 = 'application/json;charset=UTF-8' public static final String CONTENT_TYPE_SERVER_SENT_EVENTS = 'text/event-stream;charset=UTF-8' @Shared ObjectMapper mapper = new ObjectMapper() AbstractGraphQLHttpServlet servlet MockHttpServletRequest request MockHttpServletResponse response CountDownLatch subscriptionLatch def setup() { subscriptionLatch = new CountDownLatch(1) servlet = TestUtils.createDefaultServlet({ env -> env.arguments.arg }, { env -> env.arguments.arg }, { env -> AtomicReference> publisherRef = new AtomicReference<>() publisherRef.set(new SingleSubscriberPublisher({ SingleSubscriberPublisher publisher = publisherRef.get() publisher.offer("First\n\n" + env.arguments.arg) publisher.offer("Second\n\n" + env.arguments.arg) publisher.noMoreData() subscriptionLatch.countDown() })) return publisherRef.get() }) request = new MockHttpServletRequest() request.setAsyncSupported(true) request.asyncSupported = true request.setMethod("GET") response = new MockHttpServletResponse() } Map getResponseContent() { mapper.readValue(response.getContentAsByteArray(), Map) } List> getSubscriptionResponseContent() { String[] data = response.getContentAsString().split("\n\n") return data.collect { dataLine -> if (dataLine.startsWith("data: ")) { return mapper.readValue(dataLine.substring(5), Map) } else { throw new IllegalStateException("Could not read event stream") } } } List> getBatchedResponseContent() { mapper.readValue(response.getContentAsByteArray(), List) } def "HTTP GET without info returns bad request"() { when: servlet.doGet(request, response) then: response.getStatus() == STATUS_BAD_REQUEST } def "HTTP GET to /schema.json returns introspection query"() { setup: request.setPathInfo('/schema.json') when: servlet.doGet(request, response) then: response.getStatus() == STATUS_OK response.getContentType() == CONTENT_TYPE_JSON_UTF8 getResponseContent().data.__schema != null } def "query over HTTP GET returns data"() { setup: request.addParameter('query', 'query { echo(arg:"test") }') when: servlet.doGet(request, response) then: response.getStatus() == STATUS_OK response.getContentType() == CONTENT_TYPE_JSON_UTF8 response.getContentLength() == mapper.writeValueAsString(["data": ["echo": "test"]]).getBytes(StandardCharsets.UTF_8).length getResponseContent().data.echo == "test" } def "query over HTTP GET returns data with correct contentLength"() { setup: request.addParameter('query', 'query { echo(arg:"special char á") }') when: servlet.doGet(request, response) then: response.getStatus() == STATUS_OK response.getContentType() == CONTENT_TYPE_JSON_UTF8 response.getContentLength() == mapper.writeValueAsString(["data": ["echo": "special char á"]]).getBytes(StandardCharsets.UTF_8).length getResponseContent().data.echo == "special char á" } def "disabling async support on request over HTTP GET does not start async request"() { setup: servlet = TestUtils.createDefaultServlet({ env -> env.arguments.arg }, { env -> env.arguments.arg }, { env -> AtomicReference> publisherRef = new AtomicReference<>() publisherRef.set(new SingleSubscriberPublisher<>({ subscription -> publisherRef.get().offer((String) env.arguments.arg) publisherRef.get().noMoreData() })) return publisherRef.get() }) request.addParameter('query', 'query { echo(arg:"test") }') request.setAsyncSupported(false) when: servlet.doGet(request, response) then: request.asyncContext == null } def "query over HTTP GET with variables returns data"() { setup: request.addParameter('query', 'query Echo($arg: String) { echo(arg:$arg) }') request.addParameter('variables', '{"arg": "test"}') when: servlet.doGet(request, response) then: response.getStatus() == STATUS_OK response.getContentType() == CONTENT_TYPE_JSON_UTF8 getResponseContent().data.echo == "test" } def "query over HTTP GET with variables as string returns data"() { setup: request.addParameter('query', 'query Echo($arg: String) { echo(arg:$arg) }') request.addParameter('variables', '"{\\"arg\\": \\"test\\"}"') when: servlet.doGet(request, response) then: response.getStatus() == STATUS_OK response.getContentType() == CONTENT_TYPE_JSON_UTF8 getResponseContent().data.echo == "test" } def "query over HTTP GET with operationName returns data"() { when: response = new MockHttpServletResponse() request.addParameter('query', 'query one{ echoOne: echo(arg:"test-one") } query two{ echoTwo: echo(arg:"test-two") }') request.addParameter('operationName', 'two') servlet.doGet(request, response) then: response.getStatus() == STATUS_OK response.getContentType() == CONTENT_TYPE_JSON_UTF8 getResponseContent().data.echoOne == null getResponseContent().data.echoTwo == "test-two" } def "query over HTTP GET with empty non-null operationName returns data"() { when: response = new MockHttpServletResponse() request.addParameter('query', 'query echo{ echo: echo(arg:"test") }') request.addParameter('operationName', '') servlet.doGet(request, response) then: response.getStatus() == STATUS_OK response.getContentType() == CONTENT_TYPE_JSON_UTF8 getResponseContent().data.echo == "test" } def "query over HTTP GET with unknown property 'test' returns data"() { setup: request.addParameter('query', 'query { echo(arg:"test") }') request.addParameter('test', 'test') when: servlet.doGet(request, response) then: response.getStatus() == STATUS_OK response.getContentType() == CONTENT_TYPE_JSON_UTF8 getResponseContent().data.echo == "test" } def "batched query over HTTP GET returns data"() { setup: request.addParameter('query', '[{ "query": "query { echo(arg:\\"test\\") }" }, { "query": "query { echo(arg:\\"test\\") }" }]') when: servlet.doGet(request, response) then: response.getStatus() == STATUS_OK response.getContentType() == CONTENT_TYPE_JSON_UTF8 getBatchedResponseContent()[0].data.echo == "test" getBatchedResponseContent()[1].data.echo == "test" } def "batched query over HTTP GET returns data with correct contentLength"() { setup: request.addParameter('query', '[{ "query": "query { echo(arg:\\"special char á\\") }" }, { "query": "query { echo(arg:\\"test\\") }" }]') when: servlet.doGet(request, response) then: response.getStatus() == STATUS_OK response.getContentType() == CONTENT_TYPE_JSON_UTF8 response.getContentLength() == mapper.writeValueAsString([["data": ["echo": "special char á"]], ["data": ["echo": "test"]]]).getBytes(StandardCharsets.UTF_8).length getBatchedResponseContent()[0].data.echo == "special char á" getBatchedResponseContent()[1].data.echo == "test" } def "batched query over HTTP GET with variables returns data"() { setup: request.addParameter('query', '[{ "query": "query { echo(arg:\\"test\\") }", "variables": { "arg": "test" } }, { "query": "query { echo(arg:\\"test\\") }", "variables": { "arg": "test" } }]') when: servlet.doGet(request, response) then: response.getStatus() == STATUS_OK response.getContentType() == CONTENT_TYPE_JSON_UTF8 getBatchedResponseContent()[0].data.echo == "test" getBatchedResponseContent()[1].data.echo == "test" } def "batched query over HTTP GET with variables as string returns data"() { setup: request.addParameter('query', '[{ "query": "query { echo(arg:\\"test\\") }", "variables": "{ \\"arg\\": \\"test\\" }" }, { "query": "query { echo(arg:\\"test\\") }", "variables": "{ \\"arg\\": \\"test\\" }" }]') when: servlet.doGet(request, response) then: response.getStatus() == STATUS_OK response.getContentType() == CONTENT_TYPE_JSON_UTF8 getBatchedResponseContent()[0].data.echo == "test" getBatchedResponseContent()[1].data.echo == "test" } def "batched query over HTTP GET with operationName returns data"() { when: response = new MockHttpServletResponse() request.addParameter('query', '[{ "query": "query one{ echoOne: echo(arg:\\"test-one\\") } query two{ echoTwo: echo(arg:\\"test-two\\") }", "operationName": "one" }, { "query": "query one{ echoOne: echo(arg:\\"test-one\\") } query two{ echoTwo: echo(arg:\\"test-two\\") }", "operationName": "two" }]') servlet.doGet(request, response) then: response.getStatus() == STATUS_OK response.getContentType() == CONTENT_TYPE_JSON_UTF8 getBatchedResponseContent()[0].data.echoOne == "test-one" getBatchedResponseContent()[0].data.echoTwo == null getBatchedResponseContent()[1].data.echoOne == null getBatchedResponseContent()[1].data.echoTwo == "test-two" } def "batched query over HTTP GET with empty non-null operationName returns data"() { when: response = new MockHttpServletResponse() request.addParameter('query', '[{ "query": "query echo{ echo: echo(arg:\\"test\\") }", "operationName": "" }, { "query": "query echo{ echo: echo(arg:\\"test\\") }", "operationName": "" }]') servlet.doGet(request, response) then: response.getStatus() == STATUS_OK response.getContentType() == CONTENT_TYPE_JSON_UTF8 getBatchedResponseContent()[0].data.echo == "test" getBatchedResponseContent()[1].data.echo == "test" } def "batched query over HTTP GET with unknown property 'test' returns data"() { setup: request.addParameter('query', '[{ "query": "query { echo(arg:\\"test\\") }", "test": "test" }, { "query": "query { echo(arg:\\"test\\") }", "test": "test" }]') when: servlet.doGet(request, response) then: response.getStatus() == STATUS_OK response.getContentType() == CONTENT_TYPE_JSON_UTF8 getBatchedResponseContent()[0].data.echo == "test" getBatchedResponseContent()[1].data.echo == "test" } def "Batch Execution Handler allows limiting batches and sending error messages."() { setup: servlet = TestUtils.createBatchCustomizedServlet({ env -> env.arguments.arg }, { env -> env.arguments.arg }, { env -> AtomicReference> publisherRef = new AtomicReference<>() publisherRef.set(new SingleSubscriberPublisher({ SingleSubscriberPublisher publisher = publisherRef.get() publisher.offer("First\n\n" + env.arguments.arg) publisher.offer("Second\n\n" + env.arguments.arg) publisher.noMoreData() subscriptionLatch.countDown() })) return publisherRef.get() }) request.addParameter('query', '[{ "query": "query { echo(arg:\\"test\\") }" }, { "query": "query { echo(arg:\\"test\\") }" }, { "query": "query { echo(arg:\\"test\\") }" }]') when: servlet.doGet(request, response) then: response.getStatus() == STATUS_BAD_REQUEST response.getErrorMessage() == TestBatchInputPreProcessor.BATCH_ERROR_MESSAGE } def "Default Execution Result Handler does not limit number of queries"() { setup: request.addParameter('query', '[{ "query": "query { echo(arg:\\"test\\") }" }, { "query": "query { echo(arg:\\"test\\") }" }, { "query": "query { echo(arg:\\"test\\") }" }]') when: servlet.doGet(request, response) then: response.getStatus() == STATUS_OK response.getContentType() == CONTENT_TYPE_JSON_UTF8 getBatchedResponseContent().size() == 3 } def "mutation over HTTP GET returns errors"() { setup: request.addParameter('query', 'mutation { echo(arg:"test") }') when: servlet.doGet(request, response) then: response.getStatus() == STATUS_OK response.getContentType() == CONTENT_TYPE_JSON_UTF8 getResponseContent().errors.size() == 1 } def "batched mutation over HTTP GET returns errors"() { setup: request.addParameter('query', '[{ "query": "mutation { echo(arg:\\"test\\") }" }, { "query": "mutation {echo(arg:\\"test\\") }" }]') when: servlet.doGet(request, response) then: response.getStatus() == STATUS_OK response.getContentType() == CONTENT_TYPE_JSON_UTF8 getBatchedResponseContent()[0].errors.size() == 1 getBatchedResponseContent()[1].errors.size() == 1 } def "subscription query over HTTP GET with variables as string returns data"() { setup: request.addParameter('query', 'subscription Subscription($arg: String!) { echo(arg: $arg) }') request.addParameter('operationName', 'Subscription') request.addParameter('variables', '{"arg": "test"}') request.setAsyncSupported(true) when: servlet.doGet(request, response) then: response.getStatus() == STATUS_OK response.getContentType() == CONTENT_TYPE_SERVER_SENT_EVENTS when: subscriptionLatch.await(1, TimeUnit.SECONDS) then: getSubscriptionResponseContent()[0].data.echo == "First\n\ntest" getSubscriptionResponseContent()[1].data.echo == "Second\n\ntest" } def "query over HTTP POST without part or body returns bad request"() { when: request.setMethod("POST") servlet.doPost(request, response) then: response.getStatus() == STATUS_BAD_REQUEST } def "query over HTTP POST body returns data"() { setup: request.setContent(mapper.writeValueAsBytes([ query: 'query { echo(arg:"test") }' ])) request.setMethod("POST") when: servlet.doPost(request, response) then: response.getStatus() == STATUS_OK response.getContentType() == CONTENT_TYPE_JSON_UTF8 getResponseContent().data.echo == "test" } def "query over HTTP POST multiline body returns data"() { setup: request.setContent(""" query { object { a b } }""".bytes) request.setMethod("POST") request.contentType = "application/graphql" when: servlet.doPost(request, response) then: response.getStatus() == STATUS_OK response.getContentType() == CONTENT_TYPE_JSON_UTF8 getResponseContent().data.object.b == null } def "disabling async support on request over HTTP POST does not start async request"() { setup: servlet = TestUtils.createDefaultServlet({ env -> env.arguments.arg }, { env -> env.arguments.arg }, { env -> AtomicReference> publisherRef = new AtomicReference<>() publisherRef.set(new SingleSubscriberPublisher<>({ subscription -> publisherRef.get().offer((String) env.arguments.arg) publisherRef.get().noMoreData() })) return publisherRef.get() }) request.setContent(mapper.writeValueAsBytes([ query: 'query { echo(arg:"test") }' ])) request.setAsyncSupported(false) when: servlet.doPost(request, response) then: request.asyncContext == null } def "query over HTTP POST body with graphql contentType returns data"() { setup: request.addHeader("Content-Type", "application/graphql") request.setContent('query { echo(arg:"test") }'.getBytes("UTF-8")) request.setMethod("POST") when: servlet.doPost(request, response) then: response.getStatus() == STATUS_OK response.getContentType() == CONTENT_TYPE_JSON_UTF8 getResponseContent().data.echo == "test" } def "query over HTTP POST body with variables returns data"() { setup: request.setContent(mapper.writeValueAsBytes([ query : 'query Echo($arg: String) { echo(arg:$arg) }', variables: '{"arg": "test"}' ])) request.setMethod("POST") when: servlet.doPost(request, response) then: response.getStatus() == STATUS_OK response.getContentType() == CONTENT_TYPE_JSON_UTF8 getResponseContent().data.echo == "test" } def "query over HTTP POST body with operationName returns data"() { setup: request.setContent(mapper.writeValueAsBytes([ query : 'query one{ echoOne: echo(arg:"test-one") } query two{ echoTwo: echo(arg:"test-two") }', operationName: 'two' ])) request.setMethod("POST") when: servlet.doPost(request, response) then: response.getStatus() == STATUS_OK response.getContentType() == CONTENT_TYPE_JSON_UTF8 getResponseContent().data.echoOne == null getResponseContent().data.echoTwo == "test-two" } def "query over HTTP POST body with empty non-null operationName returns data"() { setup: request.setContent(mapper.writeValueAsBytes([ query : 'query echo{ echo: echo(arg:"test") }', operationName: '' ])) request.setMethod("POST") when: servlet.doPost(request, response) then: response.getStatus() == STATUS_OK response.getContentType() == CONTENT_TYPE_JSON_UTF8 getResponseContent().data.echo == "test" } def "query over HTTP POST body with unknown property 'test' returns data"() { setup: request.setContent(mapper.writeValueAsBytes([ query: 'query { echo(arg:"test") }', test : 'test' ])) request.setMethod("POST") when: servlet.doPost(request, response) then: response.getStatus() == STATUS_OK response.getContentType() == CONTENT_TYPE_JSON_UTF8 getResponseContent().data.echo == "test" } def "query over HTTP POST multipart named 'graphql' returns data"() { setup: request.setContentType("multipart/form-data, boundary=---test") request.setMethod("POST") request.addPart(TestMultipartContentBuilder.createPart('graphql', mapper.writeValueAsString([query: 'query { echo(arg:"test") }']))) when: servlet.doPost(request, response) then: response.getStatus() == STATUS_OK response.getContentType() == CONTENT_TYPE_JSON_UTF8 getResponseContent().data.echo == "test" } def "query over HTTP POST multipart named 'query' returns data"() { setup: request.setContentType("multipart/form-data, boundary=test") request.setMethod("POST") request.addPart(TestMultipartContentBuilder.createPart('query', 'query { echo(arg:"test") }')) when: servlet.doPost(request, response) then: response.getStatus() == STATUS_OK response.getContentType() == CONTENT_TYPE_JSON_UTF8 getResponseContent().data.echo == "test" } def "query over HTTP POST multipart named 'query' with operationName returns data"() { setup: request.setContentType("multipart/form-data, boundary=test") request.setMethod("POST") request.addPart(TestMultipartContentBuilder.createPart('query', 'query one{ echoOne: echo(arg:"test-one") } query two{ echoTwo: echo(arg:"test-two") }')) request.addPart(TestMultipartContentBuilder.createPart('operationName', 'two')) when: servlet.doPost(request, response) then: response.getStatus() == STATUS_OK response.getContentType() == CONTENT_TYPE_JSON_UTF8 getResponseContent().data.echoOne == null getResponseContent().data.echoTwo == "test-two" } def "query over HTTP POST multipart named 'query' with empty non-null operationName returns data"() { setup: request.setContentType("multipart/form-data, boundary=test") request.setMethod("POST") request.addPart(TestMultipartContentBuilder.createPart('query', 'query echo{ echo: echo(arg:"test") }')) request.addPart(TestMultipartContentBuilder.createPart('operationName', '')) when: servlet.doPost(request, response) then: response.getStatus() == STATUS_OK response.getContentType() == CONTENT_TYPE_JSON_UTF8 getResponseContent().data.echo == "test" } def "query over HTTP POST multipart named 'query' with variables returns data"() { setup: request.setContentType("multipart/form-data, boundary=test") request.setMethod("POST") request.addPart(TestMultipartContentBuilder.createPart('query', 'query Echo($arg: String) { echo(arg:$arg) }')) request.addPart(TestMultipartContentBuilder.createPart('variables', '{"arg": "test"}')) when: servlet.doPost(request, response) then: response.getStatus() == STATUS_OK response.getContentType() == CONTENT_TYPE_JSON_UTF8 getResponseContent().data.echo == "test" } def "query over HTTP POST multipart named 'query' with unknown property 'test' returns data"() { setup: request.setContentType("multipart/form-data, boundary=test") request.setMethod("POST") request.addPart(TestMultipartContentBuilder.createPart('query', 'query { echo(arg:"test") }')) request.addPart(TestMultipartContentBuilder.createPart('test', 'test')) when: servlet.doPost(request, response) then: response.getStatus() == STATUS_OK response.getContentType() == CONTENT_TYPE_JSON_UTF8 getResponseContent().data.echo == "test" } def "query over HTTP POST multipart named 'operations' returns data"() { setup: request.setContentType("multipart/form-data, boundary=test") request.setMethod("POST") request.addPart(TestMultipartContentBuilder.createPart('operations', '{"query": "query { echo(arg:\\"test\\") }"}')) when: servlet.doPost(request, response) then: response.getStatus() == STATUS_OK response.getContentType() == CONTENT_TYPE_JSON_UTF8 getResponseContent().data.echo == "test" } def "query over HTTP POST multipart named 'operations' with operationName returns data"() { setup: request.setContentType("multipart/form-data, boundary=test") request.setMethod("POST") request.addPart(TestMultipartContentBuilder.createPart('operations', '{"query": "query one{ echoOne: echo(arg:\\"test-one\\") } query two{ echoTwo: echo(arg:\\"test-two\\") }", "operationName": "two"}')) when: servlet.doPost(request, response) then: response.getStatus() == STATUS_OK response.getContentType() == CONTENT_TYPE_JSON_UTF8 getResponseContent().data.echoOne == null getResponseContent().data.echoTwo == "test-two" } def "query over HTTP POST multipart named 'operations' with empty non-null operationName returns data"() { setup: request.setContentType("multipart/form-data, boundary=test") request.setMethod("POST") request.addPart(TestMultipartContentBuilder.createPart('operations', '{"query": "query echo{ echo: echo(arg:\\"test\\") }", "operationName": "" }')) when: servlet.doPost(request, response) then: response.getStatus() == STATUS_OK response.getContentType() == CONTENT_TYPE_JSON_UTF8 getResponseContent().data.echo == "test" } def "query over HTTP POST multipart named 'operations' with variables returns data"() { setup: request.setContentType("multipart/form-data, boundary=test") request.setMethod("POST") request.addPart(TestMultipartContentBuilder.createPart('operations', '{"query": "query Echo($arg: String) { echo(arg:$arg) }", "variables": {"arg": "test"} }')) when: servlet.doPost(request, response) then: response.getStatus() == STATUS_OK response.getContentType() == CONTENT_TYPE_JSON_UTF8 getResponseContent().data.echo == "test" } def "query over HTTP POST multipart named 'operations' with unknown property 'test' returns data"() { setup: request.setContentType("multipart/form-data, boundary=test") request.setMethod("POST") request.addPart(TestMultipartContentBuilder.createPart('operations', '{\"query\": \"query { echo(arg:\\"test\\") }\"}')) request.addPart(TestMultipartContentBuilder.createPart('test', 'test')) when: servlet.doPost(request, response) then: response.getStatus() == STATUS_OK response.getContentType() == CONTENT_TYPE_JSON_UTF8 getResponseContent().data.echo == "test" } def "query over HTTP POST multipart named 'operations' will interpolate variables from map"() { setup: request.setContentType("multipart/form-data, boundary=test") request.setMethod("POST") request.addPart(TestMultipartContentBuilder.createPart('operations', '{"query": "mutation test($file: Upload!) { echoFile(file: $file) }", "variables": { "file": null }}')) request.addPart(TestMultipartContentBuilder.createPart('map', '{"0": ["variables.file"]}')) request.addPart(TestMultipartContentBuilder.createPart('0', 'test')) when: servlet.doPost(request, response) then: response.getStatus() == STATUS_OK response.getContentType() == CONTENT_TYPE_JSON_UTF8 getResponseContent().data.echoFile == "test" } def "query over HTTP POST multipart named 'operations' will interpolate variable list from map"() { setup: request.setContentType("multipart/form-data, boundary=test") request.setMethod("POST") request.addPart(TestMultipartContentBuilder.createPart('operations', '{"query": "mutation test($files: [Upload!]!) { echoFiles(files: $files) }", "variables": { "files": [null, null] }}')) request.addPart(TestMultipartContentBuilder.createPart('map', '{"0": ["variables.files.0"], "1": ["variables.files.1"]}')) request.addPart(TestMultipartContentBuilder.createPart('0', 'test')) request.addPart(TestMultipartContentBuilder.createPart('1', 'test again')) when: servlet.doPost(request, response) then: response.getStatus() == STATUS_OK response.getContentType() == CONTENT_TYPE_JSON_UTF8 getResponseContent().data.echoFiles == ["test", "test again"] } def "errors while accessing file from the request"() { setup: request = Spy(MockHttpServletRequest) request.setMethod("POST") request.setContentType("multipart/form-data, boundary=test") // See https://github.com/apache/tomcat/blob/main/java/org/apache/catalina/connector/Request.java#L2775...L2791 request.getParts() >> { throw new IllegalStateException() } when: servlet.doPost(request, response) then: response.getStatus() == STATUS_BAD_REQUEST response.getContentLength() == 0 } def "batched query over HTTP POST body returns data"() { setup: request.setContent('[{ "query": "query { echo(arg:\\"test\\") }" }, { "query": "query { echo(arg:\\"test\\") }" }]'.bytes) request.setMethod("POST") when: servlet.doPost(request, response) then: response.getStatus() == STATUS_OK response.getContentType() == CONTENT_TYPE_JSON_UTF8 response.getContentLength() == mapper.writeValueAsString([["data": ["echo": "test"]], ["data": ["echo": "test"]]]).length() getBatchedResponseContent()[0].data.echo == "test" getBatchedResponseContent()[1].data.echo == "test" } def "batched query over HTTP POST body with variables returns data"() { setup: request.setContent('[{ "query": "query { echo(arg:\\"test\\") }", "variables": { "arg": "test" } }, { "query": "query { echo(arg:\\"test\\") }", "variables": { "arg": "test" } }]'.bytes) request.setMethod("POST") when: servlet.doPost(request, response) then: response.getStatus() == STATUS_OK response.getContentType() == CONTENT_TYPE_JSON_UTF8 getBatchedResponseContent()[0].data.echo == "test" getBatchedResponseContent()[1].data.echo == "test" } def "batched query over HTTP POST body with operationName returns data"() { setup: request.setContent('[{ "query": "query one{ echoOne: echo(arg:\\"test-one\\") } query two{ echoTwo: echo(arg:\\"test-two\\") }", "operationName": "one" }, { "query": "query one{ echoOne: echo(arg:\\"test-one\\") } query two{ echoTwo: echo(arg:\\"test-two\\") }", "operationName": "two" }]'.bytes) request.setMethod("POST") when: servlet.doPost(request, response) then: response.getStatus() == STATUS_OK response.getContentType() == CONTENT_TYPE_JSON_UTF8 getBatchedResponseContent()[0].data.echoOne == "test-one" getBatchedResponseContent()[0].data.echoTwo == null getBatchedResponseContent()[1].data.echoOne == null getBatchedResponseContent()[1].data.echoTwo == "test-two" } def "batched query over HTTP POST body with empty non-null operationName returns data"() { setup: request.setContent('[{ "query": "query echo{ echo: echo(arg:\\"test\\") }", "operationName": "" }, { "query": "query echo{ echo: echo(arg:\\"test\\") }", "operationName": "" }]'.bytes) request.setMethod("POST") when: servlet.doPost(request, response) then: response.getStatus() == STATUS_OK response.getContentType() == CONTENT_TYPE_JSON_UTF8 getBatchedResponseContent()[0].data.echo == "test" getBatchedResponseContent()[1].data.echo == "test" } def "batched query over HTTP POST body with unknown property 'test' returns data"() { setup: request.setContent('[{ "query": "query { echo(arg:\\"test\\") }", "test": "test" }, { "query": "query { echo(arg:\\"test\\") }", "test": "test" }]'.bytes) request.setMethod("POST") when: servlet.doPost(request, response) then: response.getStatus() == STATUS_OK response.getContentType() == CONTENT_TYPE_JSON_UTF8 getBatchedResponseContent()[0].data.echo == "test" getBatchedResponseContent()[1].data.echo == "test" } def "batched query over HTTP POST multipart named 'graphql' returns data"() { setup: request.setContentType("multipart/form-data, boundary=test") request.setMethod("POST") request.addPart(TestMultipartContentBuilder.createPart('graphql', '[{ "query": "query { echo(arg:\\"test\\") }" }, { "query": "query { echo(arg:\\"test\\") }" }]')) when: servlet.doPost(request, response) then: response.getStatus() == STATUS_OK response.getContentType() == CONTENT_TYPE_JSON_UTF8 getBatchedResponseContent()[0].data.echo == "test" getBatchedResponseContent()[1].data.echo == "test" } def "batched query over HTTP POST multipart named 'graphql' with unknown property 'test' returns data"() { setup: request.setContentType("multipart/form-data, boundary=test") request.setMethod("POST") request.addPart(TestMultipartContentBuilder.createPart('graphql', '[{ "query": "query { echo(arg:\\"test\\") }", "test": "test" }, { "query": "query { echo(arg:\\"test\\") }", "test": "test" }]')) when: servlet.doPost(request, response) then: response.getStatus() == STATUS_OK response.getContentType() == CONTENT_TYPE_JSON_UTF8 getBatchedResponseContent()[0].data.echo == "test" getBatchedResponseContent()[1].data.echo == "test" } def "batched query over HTTP POST multipart named 'query' returns data"() { setup: request.setContentType("multipart/form-data, boundary=test") request.setMethod("POST") request.addPart(TestMultipartContentBuilder.createPart('query', '[{ "query": "query { echo(arg:\\"test\\") }" }, { "query": "query { echo(arg:\\"test\\") }" }]')) when: servlet.doPost(request, response) then: response.getStatus() == STATUS_OK response.getContentType() == CONTENT_TYPE_JSON_UTF8 getBatchedResponseContent()[0].data.echo == "test" getBatchedResponseContent()[1].data.echo == "test" } def "batched query over HTTP POST multipart named 'query' with operationName returns data"() { setup: request.setContentType("multipart/form-data, boundary=test") request.setMethod("POST") request.addPart(TestMultipartContentBuilder.createPart('query', '[{ "query": "query one{ echoOne: echo(arg:\\"test-one\\") } query two{ echoTwo: echo(arg:\\"test-two\\") }", "operationName": "one" }, { "query": "query one{ echoOne: echo(arg:\\"test-one\\") } query two{ echoTwo: echo(arg:\\"test-two\\") }", "operationName": "two" }]')) when: servlet.doPost(request, response) then: response.getStatus() == STATUS_OK response.getContentType() == CONTENT_TYPE_JSON_UTF8 getBatchedResponseContent()[0].data.echoOne == "test-one" getBatchedResponseContent()[0].data.echoTwo == null getBatchedResponseContent()[1].data.echoOne == null getBatchedResponseContent()[1].data.echoTwo == "test-two" } def "batched query over HTTP POST multipart named 'query' with empty non-null operationName returns data"() { setup: request.setContentType("multipart/form-data, boundary=test") request.setMethod("POST") request.addPart(TestMultipartContentBuilder.createPart('query', '[{ "query": "query echo{ echo: echo(arg:\\"test\\") }", "operationName": "" }, { "query": "query echo{ echo: echo(arg:\\"test\\") }", "operationName": "" }]')) when: servlet.doPost(request, response) then: response.getStatus() == STATUS_OK response.getContentType() == CONTENT_TYPE_JSON_UTF8 getBatchedResponseContent()[0].data.echo == "test" getBatchedResponseContent()[1].data.echo == "test" } def "batched query over HTTP POST multipart named 'query' with variables returns data"() { setup: request.setContentType("multipart/form-data, boundary=test") request.setMethod("POST") request.addPart(TestMultipartContentBuilder.createPart('query', '[{ "query": "query echo($arg: String) { echo(arg:$arg) }", "variables": { "arg": "test" } }, { "query": "query echo($arg: String) { echo(arg:$arg) }", "variables": { "arg": "test" } }]')) when: servlet.doPost(request, response) then: response.getStatus() == STATUS_OK response.getContentType() == CONTENT_TYPE_JSON_UTF8 getBatchedResponseContent()[0].data.echo == "test" getBatchedResponseContent()[1].data.echo == "test" } def "batched query over HTTP POST multipart named 'query' with unknown property 'test' returns data"() { setup: request.setContentType("multipart/form-data, boundary=test") request.setMethod("POST") request.addPart(TestMultipartContentBuilder.createPart('query', '[{ "query": "query { echo(arg:\\"test\\") }", "test": "test" }, { "query": "query { echo(arg:\\"test\\") }", "test": "test" }]')) when: servlet.doPost(request, response) then: response.getStatus() == STATUS_OK response.getContentType() == CONTENT_TYPE_JSON_UTF8 getBatchedResponseContent()[0].data.echo == "test" getBatchedResponseContent()[1].data.echo == "test" } def "batched query over HTTP POST multipart named 'operations' returns data"() { setup: request.setContentType("multipart/form-data, boundary=test") request.setMethod("POST") request.addPart(TestMultipartContentBuilder.createPart('operations', '[{ "query": "query { echo(arg:\\"test\\") }" }, { "query": "query { echo(arg:\\"test\\") }" }]')) when: servlet.doPost(request, response) then: response.getStatus() == STATUS_OK response.getContentType() == CONTENT_TYPE_JSON_UTF8 getBatchedResponseContent()[0].data.echo == "test" getBatchedResponseContent()[1].data.echo == "test" } def "batched query over HTTP POST multipart named 'operations' with unknown property 'test' returns data"() { setup: request.setContentType("multipart/form-data, boundary=test") request.setMethod("POST") request.addPart(TestMultipartContentBuilder.createPart('operations', '[{ "query": "query { echo(arg:\\"test\\") }", "test": "test" }, { "query": "query { echo(arg:\\"test\\") }", "test": "test" }]')) when: servlet.doPost(request, response) then: response.getStatus() == STATUS_OK response.getContentType() == CONTENT_TYPE_JSON_UTF8 getBatchedResponseContent()[0].data.echo == "test" getBatchedResponseContent()[1].data.echo == "test" } def "batched query over HTTP POST multipart named 'operations' with operationName returns data"() { setup: request.setContentType("multipart/form-data, boundary=test") request.setMethod("POST") request.addPart(TestMultipartContentBuilder.createPart('operations', '[{ "query": "query one{ echoOne: echo(arg:\\"test-one\\") } query two{ echoTwo: echo(arg:\\"test-two\\") }", "operationName": "one" }, { "query": "query one{ echoOne: echo(arg:\\"test-one\\") } query two{ echoTwo: echo(arg:\\"test-two\\") }", "operationName": "two" }]')) when: servlet.doPost(request, response) then: response.getStatus() == STATUS_OK response.getContentType() == CONTENT_TYPE_JSON_UTF8 getBatchedResponseContent()[0].data.echoOne == "test-one" getBatchedResponseContent()[0].data.echoTwo == null getBatchedResponseContent()[1].data.echoOne == null getBatchedResponseContent()[1].data.echoTwo == "test-two" } def "batched query over HTTP POST multipart named 'operations' with empty non-null operationName returns data"() { setup: request.setContentType("multipart/form-data, boundary=test") request.setMethod("POST") request.addPart(TestMultipartContentBuilder.createPart('operations', '[{ "query": "query echo{ echo: echo(arg:\\"test\\") }", "operationName": "" }, { "query": "query echo{ echo: echo(arg:\\"test\\") }", "operationName": "" }]')) when: servlet.doPost(request, response) then: response.getStatus() == STATUS_OK response.getContentType() == CONTENT_TYPE_JSON_UTF8 getBatchedResponseContent()[0].data.echo == "test" getBatchedResponseContent()[1].data.echo == "test" } def "batched query over HTTP POST multipart named 'operations' with variables returns data"() { setup: request.setContentType("multipart/form-data, boundary=test") request.setMethod("POST") request.addPart(TestMultipartContentBuilder.createPart('operations', '[{ "query": "query echo($arg: String) { echo(arg:$arg) }", "variables": { "arg": "test" } }, { "query": "query echo($arg: String) { echo(arg:$arg) }", "variables": { "arg": "test" } }]')) when: servlet.doPost(request, response) then: response.getStatus() == STATUS_OK response.getContentType() == CONTENT_TYPE_JSON_UTF8 getBatchedResponseContent()[0].data.echo == "test" getBatchedResponseContent()[1].data.echo == "test" } def "mutation over HTTP POST body returns data"() { setup: request.setContent(mapper.writeValueAsBytes([ query: 'mutation { echo(arg:"test") }' ])) request.setMethod("POST") when: servlet.doPost(request, response) then: response.getStatus() == STATUS_OK response.getContentType() == CONTENT_TYPE_JSON_UTF8 getResponseContent().data.echo == "test" } def "batched mutation over HTTP POST body returns data"() { setup: request.setContent('[{ "query": "mutation { echo(arg:\\"test\\") }" }, { "query": "mutation { echo(arg:\\"test\\") }" }]'.bytes) request.setMethod("POST") when: servlet.doPost(request, response) then: response.getStatus() == STATUS_OK response.getContentType() == CONTENT_TYPE_JSON_UTF8 getBatchedResponseContent()[0].data.echo == "test" getBatchedResponseContent()[1].data.echo == "test" } def "batched mutation over HTTP POST body with unknown property 'test' returns data"() { setup: request.setContent('[{ "query": "mutation { echo(arg:\\"test\\") }", "test": "test" }, { "query": "mutation { echo(arg:\\"test\\") }", "test": "test" }]'.bytes) request.setMethod("POST") when: servlet.doPost(request, response) then: response.getStatus() == STATUS_OK response.getContentType() == CONTENT_TYPE_JSON_UTF8 getBatchedResponseContent()[0].data.echo == "test" getBatchedResponseContent()[1].data.echo == "test" } def "subscription query over HTTP POST with variables as string returns data"() { setup: request.setContent('{"query": "subscription Subscription($arg: String!) { echo(arg: $arg) }", "operationName": "Subscription", "variables": {"arg": "test"}}'.bytes) request.setAsyncSupported(true) request.setMethod("POST") when: servlet.doPost(request, response) then: response.getStatus() == STATUS_OK response.getContentType() == CONTENT_TYPE_SERVER_SENT_EVENTS when: subscriptionLatch.await(1, TimeUnit.SECONDS) then: getSubscriptionResponseContent()[0].data.echo == "First\n\ntest" getSubscriptionResponseContent()[1].data.echo == "Second\n\ntest" } def "errors before graphql schema execution return internal server error"() { setup: GraphQLConfiguration configuration = GraphQLConfiguration.with(GraphQLInvocationInputFactory.newBuilder { throw new TestException() }.build()).build() servlet = GraphQLHttpServlet.with(configuration) servlet.init() request.setPathInfo('/schema.json') when: servlet.doGet(request, response) then: response.getStatus() == STATUS_BAD_REQUEST } def "errors while data fetching are masked in the response"() { setup: servlet = TestUtils.createDefaultServlet({ throw new TestException() }) request.addParameter('query', 'query { echo(arg:"test") }') when: servlet.doGet(request, response) then: response.getStatus() == STATUS_OK response.getContentType() == CONTENT_TYPE_JSON_UTF8 def errors = getResponseContent().errors errors.size() == 1 errors.first().message.startsWith("Internal Server Error(s)") } def "errors that also implement GraphQLError thrown while data fetching are passed to caller"() { setup: servlet = TestUtils.createDefaultServlet({ throw new TestGraphQLErrorException("This is a test message") }) request.addParameter('query', 'query { echo(arg:"test") }') when: servlet.doGet(request, response) then: response.getStatus() == STATUS_OK response.getContentType() == CONTENT_TYPE_JSON_UTF8 def errors = getResponseContent().errors errors.size() == 1 errors.first().extensions.foo == "bar" errors.first().message.startsWith("Exception while fetching data (/echo) : This is a test message") } def "batched errors while data fetching are masked in the response"() { setup: servlet = TestUtils.createDefaultServlet({ throw new TestException() }) request.addParameter('query', '[{ "query": "query { echo(arg:\\"test\\") }" }, { "query": "query { echo(arg:\\"test\\") }" }]') when: servlet.doGet(request, response) then: response.getStatus() == STATUS_OK response.getContentType() == CONTENT_TYPE_JSON_UTF8 def errors = getBatchedResponseContent().errors errors[0].size() == 1 errors[0].first().message.startsWith("Internal Server Error(s)") errors[1].size() == 1 errors[1].first().message.startsWith("Internal Server Error(s)") } def "data field is present and null if no data can be returned"() { setup: request.addParameter('query', 'query { not-a-field(arg:"test") }') when: servlet.doGet(request, response) then: response.getStatus() == STATUS_OK response.getContentType() == CONTENT_TYPE_JSON_UTF8 def resp = getResponseContent() resp.containsKey("data") resp.data == null resp.errors != null } def "batched data field is present and null if no data can be returned"() { setup: request.addParameter('query', '[{ "query": "query { not-a-field(arg:\\"test\\") }" }, { "query": "query { not-a-field(arg:\\"test\\") }" }]') when: servlet.doGet(request, response) then: response.getStatus() == STATUS_OK response.getContentType() == CONTENT_TYPE_JSON_UTF8 def resp = getBatchedResponseContent() resp[0].containsKey("data") resp[0].data == null resp[0].errors != null resp[1].containsKey("data") resp[1].data == null resp[1].errors != null } def "typeInfo is serialized correctly"() { setup: MergedField field = MergedField.newMergedField().addField(new Field("test")).build() ExecutionStepInfo stepInfo = ExecutionStepInfo.newExecutionStepInfo().field(field).type(new GraphQLNonNull(Scalars.GraphQLString)).build() expect: servlet.getConfiguration().getObjectMapper().getJacksonMapper().writeValueAsString(stepInfo) != "{}" } } ================================================ FILE: graphql-java-servlet/src/test/groovy/graphql/kickstart/servlet/BatchedQueryResponseWriterTest.groovy ================================================ package graphql.kickstart.servlet import com.fasterxml.jackson.databind.ObjectMapper import graphql.ExecutionResultImpl import graphql.kickstart.execution.GraphQLObjectMapper import spock.lang.Specification import spock.lang.Unroll import jakarta.servlet.ServletOutputStream import jakarta.servlet.http.HttpServletRequest import jakarta.servlet.http.HttpServletResponse import java.nio.charset.StandardCharsets class BatchedQueryResponseWriterTest extends Specification { @Unroll def "should write utf8 results into the response with content #result"() { given: def byteArrayOutputStream = new ByteArrayOutputStream() def graphQLObjectMapperMock = GraphQLObjectMapper.newBuilder().withObjectMapperProvider({ new ObjectMapper() }).build() graphQLObjectMapperMock.getJacksonMapper() >> new ObjectMapper() def requestMock = Mock(HttpServletRequest) def responseMock = Mock(HttpServletResponse) def servletOutputStreamMock = Mock(ServletOutputStream) responseMock.getOutputStream() >> servletOutputStreamMock 1 * responseMock.setContentLength(expectedContentLengh) 1 * responseMock.setCharacterEncoding(StandardCharsets.UTF_8.name()) (1.._) * servletOutputStreamMock.write(_) >> { value -> byteArrayOutputStream.write((byte[]) (value[0])) } def executionResultList = new ArrayList() for (LinkedHashMap value : result) { executionResultList.add(new ExecutionResultImpl(value, [])) } def writer = new BatchedQueryResponseWriter(executionResultList, graphQLObjectMapperMock) when: writer.write(requestMock, responseMock) then: byteArrayOutputStream.toString(StandardCharsets.UTF_8.name()) == expectedResponseContent where: result || expectedContentLengh | expectedResponseContent [[testValue: "abcde"]] || 32 | """[{"data":{"testValue":"abcde"}}]""" [[testValue: "äöüüöß"]] || 39 | """[{"data":{"testValue":"äöüüöß"}}]""" [] || 2 | """[]""" [[k1: "äöüüöß"], [k2: "a"]] || 52 | """[{"data":{"k1":"äöüüöß"}},{"data":{"k2":"a"}}]""" } } ================================================ FILE: graphql-java-servlet/src/test/groovy/graphql/kickstart/servlet/DataLoaderDispatchingSpec.groovy ================================================ package graphql.kickstart.servlet import com.fasterxml.jackson.databind.ObjectMapper import graphql.execution.instrumentation.ChainedInstrumentation import graphql.execution.instrumentation.Instrumentation import graphql.execution.instrumentation.SimplePerformantInstrumentation import graphql.kickstart.execution.context.ContextSetting import graphql.kickstart.execution.context.DefaultGraphQLContext import graphql.kickstart.execution.context.GraphQLKickstartContext import graphql.kickstart.servlet.context.GraphQLServletContextBuilder import graphql.schema.DataFetcher import graphql.schema.DataFetchingEnvironment import org.dataloader.BatchLoader import org.dataloader.DataLoaderFactory import org.dataloader.DataLoaderRegistry import org.springframework.mock.web.MockHttpServletRequest import org.springframework.mock.web.MockHttpServletResponse import spock.lang.Shared import spock.lang.Specification import jakarta.servlet.http.HttpServletRequest import jakarta.servlet.http.HttpServletResponse import jakarta.websocket.Session import jakarta.websocket.server.HandshakeRequest import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletionStage import java.util.concurrent.atomic.AtomicInteger class DataLoaderDispatchingSpec extends Specification { public static final int STATUS_OK = 200 public static final String CONTENT_TYPE_JSON_UTF8 = 'application/json;charset=UTF-8' @Shared ObjectMapper mapper = new ObjectMapper() AbstractGraphQLHttpServlet servlet MockHttpServletRequest request MockHttpServletResponse response AtomicInteger fetchCounterA = new AtomicInteger() AtomicInteger loadCounterA = new AtomicInteger() AtomicInteger fetchCounterB = new AtomicInteger() AtomicInteger loadCounterB = new AtomicInteger() AtomicInteger fetchCounterC = new AtomicInteger() AtomicInteger loadCounterC = new AtomicInteger() BatchLoader batchLoaderWithCounter(AtomicInteger fetchCounter) { return new BatchLoader() { @Override CompletionStage> load(List keys) { fetchCounter.incrementAndGet() CompletableFuture.completedFuture(keys) } } } def registry() { DataLoaderRegistry registry = new DataLoaderRegistry() registry.register("A", DataLoaderFactory.newDataLoader(batchLoaderWithCounter(fetchCounterA))) registry.register("B", DataLoaderFactory.newDataLoader(batchLoaderWithCounter(fetchCounterB))) registry.register("C", DataLoaderFactory.newDataLoader(batchLoaderWithCounter(fetchCounterC))) registry } def setup() { request = new MockHttpServletRequest() request.setAsyncSupported(true) request.asyncSupported = true response = new MockHttpServletResponse() } def queryDataFetcher(String dataLoaderName, AtomicInteger loadCounter) { return new DataFetcher() { @Override Object get(DataFetchingEnvironment environment) { String id = environment.arguments.arg loadCounter.incrementAndGet() environment.getDataLoader(dataLoaderName).load(id) } } } def contextBuilder() { return new GraphQLServletContextBuilder() { @Override GraphQLKickstartContext build(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) { new DefaultGraphQLContext(registry()) } @Override GraphQLKickstartContext build(Session session, HandshakeRequest handshakeRequest) { new DefaultGraphQLContext(registry()) } @Override GraphQLKickstartContext build() { new DefaultGraphQLContext(registry()) } } } def configureServlet(ContextSetting contextSetting) { servlet = TestUtils.createDataLoadingServlet(queryDataFetcher("A", loadCounterA), queryDataFetcher("B", loadCounterB), queryDataFetcher("C", loadCounterC) , contextSetting, contextBuilder()) } def resetCounters() { fetchCounterA.set(0) fetchCounterB.set(0) loadCounterA.set(0) loadCounterB.set(0) } List> getBatchedResponseContent() { mapper.readValue(response.getContentAsByteArray(), List) } def "batched query with per query context does not batch loads together"() { setup: configureServlet(ContextSetting.PER_QUERY) request.addParameter('query', '[{ "query": "query { query(arg:\\"test\\") { echo(arg:\\"test\\") { echo(arg:\\"test\\") } }}" }, { "query": "query{query(arg:\\"test\\") { echo (arg:\\"test\\") { echo(arg:\\"test\\")} }}" },' + ' { "query": "query{queryTwo(arg:\\"test\\") { echo (arg:\\"test\\")}}" }, { "query": "query{queryTwo(arg:\\"test\\") { echo (arg:\\"test\\")}}" }]') resetCounters() request.setMethod("GET") when: servlet.doGet(request, response) then: response.getStatus() == STATUS_OK response.getContentType() == CONTENT_TYPE_JSON_UTF8 getBatchedResponseContent()[0].data.query.echo.echo == "test" getBatchedResponseContent()[1].data.query.echo.echo == "test" getBatchedResponseContent()[2].data.queryTwo.echo == "test" getBatchedResponseContent()[3].data.queryTwo.echo == "test" fetchCounterA.get() == 2 loadCounterA.get() == 2 fetchCounterB.get() == 2 loadCounterB.get() == 2 fetchCounterC.get() == 2 loadCounterC.get() == 2 } def "batched query with per request context batches all queries within the request"() { setup: servlet = configureServlet(ContextSetting.PER_REQUEST) request.addParameter('query', '[{ "query": "query { query(arg:\\"test\\") { echo(arg:\\"test\\") { echo(arg:\\"test\\") } }}" }, { "query": "query{query(arg:\\"test\\") { echo (arg:\\"test\\") { echo(arg:\\"test\\")} }}" },' + ' { "query": "query{queryTwo(arg:\\"test\\") { echo (arg:\\"test\\")}}" }, { "query": "query{queryTwo(arg:\\"test\\") { echo (arg:\\"test\\")}}" }]') resetCounters() request.setMethod("GET") when: servlet.doGet(request, response) then: response.getStatus() == STATUS_OK response.getContentType() == CONTENT_TYPE_JSON_UTF8 getBatchedResponseContent()[0].data.query.echo.echo == "test" getBatchedResponseContent()[1].data.query.echo.echo == "test" getBatchedResponseContent()[2].data.queryTwo.echo == "test" getBatchedResponseContent()[3].data.queryTwo.echo == "test" fetchCounterA.get() == 1 loadCounterA.get() == 2 fetchCounterB.get() == 1 loadCounterB.get() == 2 fetchCounterC.get() == 1 loadCounterC.get() == 2 } } ================================================ FILE: graphql-java-servlet/src/test/groovy/graphql/kickstart/servlet/GraphQLServletListenerSpec.groovy ================================================ package graphql.kickstart.servlet import graphql.kickstart.servlet.core.GraphQLServletListener import spock.lang.Specification class GraphQLServletListenerSpec extends Specification { def listener = Mock(GraphQLServletListener) def requestCallback = Mock(GraphQLServletListener.RequestCallback) def tester = new RequestTester(listener) def "query over HTTP GET calls onRequest listener"() { given: "a valid graphql query request" tester.addParameter('query', 'query { echo(arg:"test") }') and: "a listener that always returns request callback" listener.onRequest(tester.request, tester.response) >> requestCallback when: "we execute a GET request" tester.doGet() then: tester.assertThatResponseIsOk() tester.assertThatContentTypeIsJson() 1 * listener.onRequest(tester.request, tester.response) } def "query over HTTP GET calls onSuccess callback"() { given: "a valid graphql query request" tester.addParameter('query', 'query { echo(arg:"test") }') and: "a listener that always returns request callback" listener.onRequest(tester.request, tester.response) >> requestCallback when: "we execute a GET request" tester.doGet() then: tester.assertThatResponseIsOk() tester.assertThatContentTypeIsJson() 1 * requestCallback.onSuccess(tester.request, tester.response) } } ================================================ FILE: graphql-java-servlet/src/test/groovy/graphql/kickstart/servlet/OsgiGraphQLHttpServletSpec.groovy ================================================ package graphql.kickstart.servlet import graphql.AssertException import graphql.annotations.annotationTypes.GraphQLField import graphql.annotations.annotationTypes.GraphQLName import graphql.annotations.processor.GraphQLAnnotations import graphql.execution.instrumentation.InstrumentationState import graphql.execution.instrumentation.SimplePerformantInstrumentation import graphql.execution.instrumentation.parameters.InstrumentationCreateStateParameters import graphql.introspection.Introspection import graphql.kickstart.execution.GraphQLRequest import graphql.kickstart.execution.config.ExecutionStrategyProvider import graphql.kickstart.execution.config.InstrumentationProvider import graphql.kickstart.execution.context.DefaultGraphQLContext import graphql.kickstart.execution.context.GraphQLKickstartContext import graphql.kickstart.servlet.context.GraphQLServletContextBuilder import graphql.kickstart.servlet.core.GraphQLServletListener import graphql.kickstart.servlet.core.GraphQLServletRootObjectBuilder import graphql.kickstart.servlet.osgi.* import graphql.schema.* import org.dataloader.DataLoaderRegistry import spock.lang.Specification import static graphql.Scalars.GraphQLInt import static graphql.schema.GraphQLFieldDefinition.newFieldDefinition class OsgiGraphQLHttpServletSpec extends Specification { static class TestQueryProvider implements GraphQLQueryProvider { @Override Collection getQueries() { List fieldDefinitions = new ArrayList<>() fieldDefinitions.add(newFieldDefinition() .name("query") .type(new GraphQLAnnotations().object(Query.class)) .staticValue(new Query()) .build()) return fieldDefinitions } @GraphQLName("query") static class Query { @GraphQLField public String field } } def "query provider adds query objects"() { setup: OsgiGraphQLHttpServlet servlet = new OsgiGraphQLHttpServlet() TestQueryProvider queryProvider = new TestQueryProvider() servlet.bindQueryProvider(queryProvider) GraphQLFieldDefinition query when: query = servlet.getConfiguration().getInvocationInputFactory().getSchemaProvider().getSchema().getQueryType().getFieldDefinition("query") then: query.getType().name == "query" when: query = servlet.getConfiguration().getInvocationInputFactory().getSchemaProvider().getReadOnlySchema().getQueryType().getFieldDefinition("query") then: query.getType().name == "query" when: servlet.unbindQueryProvider(queryProvider) then: servlet.getConfiguration().getInvocationInputFactory().getSchemaProvider().getSchema().getQueryType().getFieldDefinitions().get(0).name == "_empty" servlet.getConfiguration().getInvocationInputFactory().getSchemaProvider().getReadOnlySchema().getQueryType().getFieldDefinitions().get(0).name == "_empty" } static class TestMutationProvider implements GraphQLMutationProvider { @Override Collection getMutations() { return Collections.singletonList(newFieldDefinition().name("int").type(GraphQLInt).staticValue(1).build()) } } def "mutation provider adds mutation objects"() { setup: OsgiGraphQLHttpServlet servlet = new OsgiGraphQLHttpServlet() TestMutationProvider mutationProvider = new TestMutationProvider() when: servlet.bindMutationProvider(mutationProvider) then: servlet.getConfiguration().getInvocationInputFactory().getSchemaProvider().getSchema().getMutationType().getFieldDefinition("int").getType() == GraphQLInt servlet.getConfiguration().getInvocationInputFactory().getSchemaProvider().getReadOnlySchema().getMutationType() == null when: servlet.unbindMutationProvider(mutationProvider) then: servlet.getConfiguration().getInvocationInputFactory().getSchemaProvider().getSchema().getMutationType() == null when: servlet.bindProvider(mutationProvider) then: servlet.getConfiguration().getInvocationInputFactory().getSchemaProvider().getSchema().getMutationType().getFieldDefinition("int").getType() == GraphQLInt servlet.getConfiguration().getInvocationInputFactory().getSchemaProvider().getReadOnlySchema().getMutationType() == null when: servlet.unbindProvider(mutationProvider) then: servlet.getConfiguration().getInvocationInputFactory().getSchemaProvider().getSchema().getMutationType() == null } static class TestSubscriptionProvider implements GraphQLSubscriptionProvider { @Override Collection getSubscriptions() { return Collections.singletonList(newFieldDefinition().name("subscription").type(new GraphQLAnnotations().object(Subscription.class)).build()) } @GraphQLName("subscription") static class Subscription { @GraphQLField public String field } } def "subscription provider adds subscription objects"() { setup: OsgiGraphQLHttpServlet servlet = new OsgiGraphQLHttpServlet() TestSubscriptionProvider subscriptionProvider = new TestSubscriptionProvider() servlet.bindSubscriptionProvider(subscriptionProvider) GraphQLFieldDefinition subscription when: subscription = servlet.getConfiguration().getInvocationInputFactory().getSchemaProvider().getSchema().getSubscriptionType().getFieldDefinition("subscription") then: subscription.getType().getName() == "subscription" when: subscription = servlet.getConfiguration().getInvocationInputFactory().getSchemaProvider().getReadOnlySchema().getSubscriptionType().getFieldDefinition("subscription") then: subscription.getType().getName() == "subscription" when: servlet.unbindSubscriptionProvider(subscriptionProvider) then: servlet.getConfiguration().getInvocationInputFactory().getSchemaProvider().getSchema().getSubscriptionType() == null when: servlet.bindProvider(subscriptionProvider) then: def subscription2 = servlet.getConfiguration().getInvocationInputFactory().getSchemaProvider().getReadOnlySchema().getSubscriptionType().getFieldDefinition("subscription") subscription2.getType().getName() == "subscription" when: servlet.unbindProvider(subscriptionProvider) then: servlet.getConfiguration().getInvocationInputFactory().getSchemaProvider().getSchema().getSubscriptionType() == null } static class TestCodeRegistryProvider implements GraphQLCodeRegistryProvider { @Override GraphQLCodeRegistry getCodeRegistry() { return GraphQLCodeRegistry.newCodeRegistry().typeResolver("Type", { env -> null }).build() } } def "code registry provider adds type resolver"() { setup: OsgiGraphQLHttpServlet servlet = new OsgiGraphQLHttpServlet() TestCodeRegistryProvider codeRegistryProvider = new TestCodeRegistryProvider() when: servlet.bindCodeRegistryProvider(codeRegistryProvider) servlet.getConfiguration().getInvocationInputFactory().getSchemaProvider().getSchema().getCodeRegistry().getTypeResolver(GraphQLInterfaceType.newInterface().name("Type").build()) then: notThrown AssertException when: servlet.unbindCodeRegistryProvider(codeRegistryProvider) servlet.getConfiguration().getInvocationInputFactory().getSchemaProvider().getSchema().getCodeRegistry().getTypeResolver(GraphQLInterfaceType.newInterface().name("Type").build()) then: thrown AssertException when: servlet.bindProvider(codeRegistryProvider) servlet.getConfiguration().getInvocationInputFactory().getSchemaProvider().getSchema().getCodeRegistry().getTypeResolver(GraphQLInterfaceType.newInterface().name("Type").build()) then: notThrown AssertException when: servlet.unbindProvider(codeRegistryProvider) servlet.getConfiguration().getInvocationInputFactory().getSchemaProvider().getSchema().getCodeRegistry().getTypeResolver(GraphQLInterfaceType.newInterface().name("Type").build()) then: thrown AssertException } def "schema update delay throws no exception"() { setup: OsgiGraphQLHttpServlet servlet = new OsgiGraphQLHttpServlet() def config = Mock(OsgiGraphQLHttpServlet.Config) when: config.schema_update_delay() >> 1 servlet.activate(config) servlet.updateSchema() servlet.updateSchema() servlet.deactivate() then: noExceptionThrown() } def "bind query provider adds query objects"() { setup: def servlet = new OsgiGraphQLHttpServlet() def queryProvider = new TestQueryProvider() def query when: servlet.bindProvider(queryProvider) query = servlet.getConfiguration().getInvocationInputFactory().getSchemaProvider().getSchema().getQueryType().getFieldDefinition("query") then: query.getType().name == "query" when: query = servlet.getConfiguration().getInvocationInputFactory().getSchemaProvider().getReadOnlySchema().getQueryType().getFieldDefinition("query") then: query.getType().name == "query" when: servlet.unbindProvider(queryProvider) then: null != servlet.getConfiguration().getInvocationInputFactory().getSchemaProvider().getSchema().getQueryType().getFieldDefinition("_empty") } def "type provider adds types"() { setup: def servlet = new OsgiGraphQLHttpServlet() def typesProvider = Mock(GraphQLTypesProvider) def coercing = Mock(Coercing) typesProvider.types >> [GraphQLScalarType.newScalar().name("Upload").coercing(coercing).build()] when: servlet.bindTypesProvider(typesProvider) then: def type = servlet.configuration.invocationInputFactory.schemaProvider.schema.getType("Upload") type != null type.name == "Upload" type instanceof GraphQLScalarType def scalarType = (GraphQLScalarType) type scalarType.coercing == coercing when: servlet.unbindTypesProvider(typesProvider) then: null == servlet.configuration.invocationInputFactory.schemaProvider.schema.getType("Upload") when: servlet.bindProvider(typesProvider) then: servlet.configuration.invocationInputFactory.schemaProvider.schema.getType("Upload").name == "Upload" when: servlet.unbindProvider(typesProvider) then: null == servlet.configuration.invocationInputFactory.schemaProvider.schema.getType("Upload") } static class TestDirectiveProvider implements GraphQLDirectiveProvider { @Override Set getDirectives() { return new HashSet<>(Arrays.asList(GraphQLDirective.newDirective().name("myDirective").validLocation(Introspection.DirectiveLocation.FIELD).build())); } } def "directive provider adds directives"() { setup: OsgiGraphQLHttpServlet servlet = new OsgiGraphQLHttpServlet() TestDirectiveProvider directiveProvider = new TestDirectiveProvider() when: servlet.bindDirectivesProvider(directiveProvider) then: def directive = servlet.configuration.invocationInputFactory.schemaProvider.schema.getDirective("myDirective") directive != null directive.name == "myDirective" when: servlet.unbindDirectivesProvider(directiveProvider) then: null == servlet.configuration.invocationInputFactory.schemaProvider.schema.getDirective("myDirective") when: servlet.bindProvider(directiveProvider) then: servlet.configuration.invocationInputFactory.schemaProvider.schema.getDirective("myDirective").name == "myDirective" when: servlet.unbindProvider(directiveProvider) then: null == servlet.configuration.invocationInputFactory.schemaProvider.schema.getType("myDirective") } def "servlet listener is bound and unbound"() { setup: def servlet = new OsgiGraphQLHttpServlet() def listener = Mock(GraphQLServletListener) when: servlet.bindServletListener(listener) then: servlet.configuration.listeners.contains(listener) when: servlet.unbindServletListener(listener) then: !servlet.configuration.listeners.contains(listener) } def "context builder is bound and unbound"() { setup: def servlet = new OsgiGraphQLHttpServlet() def context = Mock(GraphQLKickstartContext) context.getDataLoaderRegistry() >> new DataLoaderRegistry() context.getMapOfContext() >> new HashMap() def contextBuilder = Mock(GraphQLServletContextBuilder) contextBuilder.build() >> context def request = GraphQLRequest.createIntrospectionRequest() when: servlet.setContextBuilder(contextBuilder) then: def invocationInput = servlet.configuration.invocationInputFactory.create(request) invocationInput.executionInput.context == context when: servlet.unsetContextBuilder(contextBuilder) then: servlet.configuration.invocationInputFactory.create(request).executionInput.context instanceof DefaultGraphQLContext } def "root object builder is bound and unbound"() { setup: def servlet = new OsgiGraphQLHttpServlet() def rootObject = Mock(Object) def rootObjectBuilder = Mock(GraphQLServletRootObjectBuilder) rootObjectBuilder.build() >> rootObject def request = GraphQLRequest.createIntrospectionRequest() when: servlet.setRootObjectBuilder(rootObjectBuilder) then: def invocationInput = servlet.configuration.invocationInputFactory.create(request) invocationInput.executionInput.root == rootObject when: servlet.unsetRootObjectBuilder(rootObjectBuilder) then: servlet.configuration.invocationInputFactory.create(request).executionInput.root != rootObject } def "execution strategy is bound and unbound"() { setup: def servlet = new OsgiGraphQLHttpServlet() def executionStrategy = Mock(ExecutionStrategyProvider) def request = GraphQLRequest.createIntrospectionRequest() when: servlet.setExecutionStrategyProvider(executionStrategy) def invocationInput = servlet.configuration.invocationInputFactory.create(request) servlet.configuration.graphQLInvoker.query(invocationInput) then: 1 * executionStrategy.getQueryExecutionStrategy() when: servlet.unsetExecutionStrategyProvider(executionStrategy) def invocationInput2 = servlet.configuration.invocationInputFactory.create(request) servlet.configuration.graphQLInvoker.query(invocationInput2) then: 0 * executionStrategy.getQueryExecutionStrategy() } def "instrumentation provider is bound and unbound"() { setup: def servlet = new OsgiGraphQLHttpServlet() def instrumentation = new SimplePerformantInstrumentation() def instrumentationProvider = Mock(InstrumentationProvider) instrumentationProvider.getInstrumentation() >> instrumentation def request = GraphQLRequest.createIntrospectionRequest() instrumentation.createState(_ as InstrumentationCreateStateParameters) >> Mock(InstrumentationState) when: servlet.setInstrumentationProvider(instrumentationProvider) def invocationInput = servlet.configuration.invocationInputFactory.create(request) servlet.configuration.graphQLInvoker.query(invocationInput) then: noExceptionThrown() when: servlet.unsetInstrumentationProvider(instrumentationProvider) then: noExceptionThrown() } } ================================================ FILE: graphql-java-servlet/src/test/groovy/graphql/kickstart/servlet/PartIOExceptionTest.groovy ================================================ package graphql.kickstart.servlet import spock.lang.Specification class PartIOExceptionTest extends Specification { def "constructs"() { when: def e = new PartIOException("some message", new IOException()) then: e instanceof RuntimeException } } ================================================ FILE: graphql-java-servlet/src/test/groovy/graphql/kickstart/servlet/RequestTester.groovy ================================================ package graphql.kickstart.servlet import com.fasterxml.jackson.databind.ObjectMapper import graphql.execution.reactive.SingleSubscriberPublisher import graphql.kickstart.servlet.core.GraphQLServletListener import org.springframework.mock.web.MockHttpServletRequest import org.springframework.mock.web.MockHttpServletResponse import spock.lang.Shared import java.util.concurrent.CountDownLatch import java.util.concurrent.atomic.AtomicReference class RequestTester { public static final int STATUS_OK = 200 public static final int STATUS_BAD_REQUEST = 400 public static final int STATUS_ERROR = 500 public static final String CONTENT_TYPE_JSON_UTF8 = 'application/json;charset=UTF-8' public static final String CONTENT_TYPE_SERVER_SENT_EVENTS = 'text/event-stream;charset=UTF-8' @Shared ObjectMapper mapper = new ObjectMapper() AbstractGraphQLHttpServlet servlet MockHttpServletRequest request MockHttpServletResponse response CountDownLatch subscriptionLatch RequestTester(GraphQLServletListener... listeners) { subscriptionLatch = new CountDownLatch(1) servlet = TestUtils.createDefaultServlet( { env -> env.arguments.arg }, { env -> env.arguments.arg }, { env -> AtomicReference> publisherRef = new AtomicReference<>() publisherRef.set(new SingleSubscriberPublisher({ SingleSubscriberPublisher publisher = publisherRef.get() publisher.offer("First\n\n" + env.arguments.arg) publisher.offer("Second\n\n" + env.arguments.arg) publisher.noMoreData() subscriptionLatch.countDown() })) return publisherRef.get() }, listeners) request = new MockHttpServletRequest() request.asyncSupported = true request.method = "GET" response = new MockHttpServletResponse() } Map getResponseContent() { mapper.readValue(response.getContentAsByteArray(), Map) } def addParameter(String name, String value) { request.addParameter(name, value) } def doGet() { servlet.doGet(request, response) } def assertThatResponseIsOk() { return response.getStatus() == STATUS_OK } def assertThatContentTypeIsJson() { return response.getContentType() == CONTENT_TYPE_JSON_UTF8 } } ================================================ FILE: graphql-java-servlet/src/test/groovy/graphql/kickstart/servlet/SingleAsynchronousQueryResponseWriterTest.groovy ================================================ package graphql.kickstart.servlet import graphql.ExecutionResult import graphql.kickstart.execution.GraphQLObjectMapper import org.springframework.mock.web.MockAsyncContext import spock.lang.Specification import jakarta.servlet.http.HttpServletRequest import jakarta.servlet.http.HttpServletResponse class SingleAsynchronousQueryResponseWriterTest extends Specification { def "result data is no publisher should"() { given: def result = Mock(ExecutionResult) def objectMapper = Mock(GraphQLObjectMapper) def writer = new SingleAsynchronousQueryResponseWriter(result, objectMapper, 100) def request = Mock(HttpServletRequest) def responseWriter = new PrintWriter(new StringWriter()) def response = Mock(HttpServletResponse) response.getWriter() >> responseWriter def asyncContext = new MockAsyncContext(request, response) request.getAsyncContext() >> asyncContext request.isAsyncStarted() >> true objectMapper.serializeResultAsJson(result) >> "{ }" when: writer.write(request, response) then: noExceptionThrown() } } ================================================ FILE: graphql-java-servlet/src/test/groovy/graphql/kickstart/servlet/SingleQueryResponseWriterTest.groovy ================================================ package graphql.kickstart.servlet import com.fasterxml.jackson.databind.ObjectMapper import graphql.ExecutionResultImpl import graphql.kickstart.execution.GraphQLObjectMapper import spock.lang.Specification import spock.lang.Unroll import jakarta.servlet.ServletOutputStream import jakarta.servlet.http.HttpServletRequest import jakarta.servlet.http.HttpServletResponse import java.nio.charset.StandardCharsets class SingleQueryResponseWriterTest extends Specification { @Unroll def "should write utf8 results into the response with content #result"() { given: def graphQLObjectMapperMock = GraphQLObjectMapper.newBuilder().withObjectMapperProvider({ new ObjectMapper() }).build() graphQLObjectMapperMock.getJacksonMapper() >> new ObjectMapper() def requestMock = Mock(HttpServletRequest) def responseMock = Mock(HttpServletResponse) responseMock.getOutputStream() >> Mock(ServletOutputStream) 1 * responseMock.setContentLength(expectedContentLenght) 1 * responseMock.setCharacterEncoding(StandardCharsets.UTF_8.name()) 1 * responseMock.getOutputStream().write(expectedResponseContent.getBytes(StandardCharsets.UTF_8)) expect: def writer = new SingleQueryResponseWriter(new ExecutionResultImpl(result, []), graphQLObjectMapperMock) writer.write(requestMock, responseMock) where: result || expectedContentLenght | expectedResponseContent [testValue: "abcde"] || 30 | """{"data":{"testValue":"abcde"}}""" [testValue: "äöüüöß"] || 37 | """{"data":{"testValue":"äöüüöß"}}""" } } ================================================ FILE: graphql-java-servlet/src/test/groovy/graphql/kickstart/servlet/TestBatchInputPreProcessor.java ================================================ package graphql.kickstart.servlet; import graphql.kickstart.execution.input.GraphQLBatchedInvocationInput; import graphql.kickstart.servlet.input.BatchInputPreProcessResult; import graphql.kickstart.servlet.input.BatchInputPreProcessor; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; public class TestBatchInputPreProcessor implements BatchInputPreProcessor { public static String BATCH_ERROR_MESSAGE = "Batch limit exceeded"; @Override public BatchInputPreProcessResult preProcessBatch( GraphQLBatchedInvocationInput batchedInvocationInput, HttpServletRequest request, HttpServletResponse response) { BatchInputPreProcessResult preProcessResult; if (batchedInvocationInput.getExecutionInputs().size() > 2) { preProcessResult = new BatchInputPreProcessResult(400, BATCH_ERROR_MESSAGE); } else { preProcessResult = new BatchInputPreProcessResult(batchedInvocationInput); } return preProcessResult; } } ================================================ FILE: graphql-java-servlet/src/test/groovy/graphql/kickstart/servlet/TestException.groovy ================================================ package graphql.kickstart.servlet /** * @author Andrew Potter */ class TestException extends RuntimeException { } ================================================ FILE: graphql-java-servlet/src/test/groovy/graphql/kickstart/servlet/TestGraphQLErrorException.groovy ================================================ package graphql.kickstart.servlet import graphql.ErrorType import graphql.GraphQLError import graphql.language.SourceLocation /** * @author Andrew Potter */ class TestGraphQLErrorException extends RuntimeException implements GraphQLError { TestGraphQLErrorException(String message) { super(message) } @Override Map getExtensions() { Map customAttributes = new LinkedHashMap<>() customAttributes.put("foo", "bar") return customAttributes } @Override List getLocations() { return null } @Override ErrorType getErrorType() { return ErrorType.ValidationError } } ================================================ FILE: graphql-java-servlet/src/test/groovy/graphql/kickstart/servlet/TestMultipartPart.groovy ================================================ package graphql.kickstart.servlet import jakarta.servlet.http.Part /** * @author Andrew Potter */ class TestMultipartContentBuilder { static Part createPart(String name, String part) { return new MockPart(name, part) } static class MockPart implements Part { final String name final String content MockPart(String name, String content) { this.name = name this.content = content } @Override InputStream getInputStream() throws IOException { return new ByteArrayInputStream(content.getBytes()) } @Override String getContentType() { return null } @Override String getName() { return name } @Override String getSubmittedFileName() { return name } @Override long getSize() { return content.getBytes().length } @Override void write(String fileName) throws IOException { throw new IllegalArgumentException("Not supported") } @Override void delete() throws IOException { throw new IllegalArgumentException("Not supported") } @Override String getHeader(String name) { return null } @Override Collection getHeaders(String name) { return Collections.emptyList() } @Override Collection getHeaderNames() { return Collections.emptyList() } } } ================================================ FILE: graphql-java-servlet/src/test/groovy/graphql/kickstart/servlet/TestUtils.groovy ================================================ package graphql.kickstart.servlet import com.google.common.io.ByteStreams import graphql.Scalars import graphql.execution.reactive.SingleSubscriberPublisher import graphql.kickstart.execution.context.ContextSetting import graphql.kickstart.servlet.apollo.ApolloScalars import graphql.kickstart.servlet.context.GraphQLServletContextBuilder import graphql.kickstart.servlet.core.GraphQLServletListener import graphql.kickstart.servlet.input.BatchInputPreProcessor import graphql.schema.* import graphql.schema.idl.RuntimeWiring import graphql.schema.idl.SchemaGenerator import graphql.schema.idl.SchemaParser import graphql.schema.idl.TypeRuntimeWiring import graphql.schema.idl.errors.SchemaProblem import lombok.NonNull import java.util.concurrent.Executor import java.util.concurrent.atomic.AtomicReference class TestUtils { static def createDefaultServlet( DataFetcher queryDataFetcher = { env -> env.arguments.arg }, DataFetcher mutationDataFetcher = { env -> env.arguments.arg }, DataFetcher subscriptionDataFetcher = { env -> AtomicReference> publisherRef = new AtomicReference<>() publisherRef.set(new SingleSubscriberPublisher<>({ subscription -> publisherRef.get().offer(env.arguments.arg) publisherRef.get().noMoreData() })) return publisherRef.get() }, GraphQLServletListener... listeners) { createServlet(queryDataFetcher, mutationDataFetcher, subscriptionDataFetcher, null, listeners) } static def createBatchCustomizedServlet( DataFetcher queryDataFetcher = { env -> env.arguments.arg }, DataFetcher mutationDataFetcher = { env -> env.arguments.arg }, DataFetcher subscriptionDataFetcher = { env -> AtomicReference> publisherRef = new AtomicReference<>() publisherRef.set(new SingleSubscriberPublisher<>({ subscription -> publisherRef.get().offer(env.arguments.arg) publisherRef.get().noMoreData() })) return publisherRef.get() }) { createServlet(queryDataFetcher, mutationDataFetcher, subscriptionDataFetcher, createBatchExecutionHandler()) } static def createDataLoadingServlet( DataFetcher queryDataFetcher = { env -> env.arguments.arg }, DataFetcher fieldDataFetcher = { env -> env.arguments.arg }, DataFetcher otherDataFetcher, ContextSetting contextSetting, GraphQLServletContextBuilder contextBuilder) { GraphQLSchema schema = createGraphQlSchemaWithTwoLevels(queryDataFetcher, fieldDataFetcher, otherDataFetcher) GraphQLHttpServlet servlet = GraphQLHttpServlet.with(GraphQLConfiguration .with(schema) .with(contextSetting) .with(contextBuilder) .with(executor()) .build()) servlet.init() return servlet } private static def createServlet( DataFetcher queryDataFetcher = { env -> env.arguments.arg }, DataFetcher mutationDataFetcher = { env -> env.arguments.arg }, DataFetcher subscriptionDataFetcher = { env -> AtomicReference> publisherRef = new AtomicReference<>() publisherRef.set(new SingleSubscriberPublisher<>({ subscription -> publisherRef.get().offer(env.arguments.arg) publisherRef.get().noMoreData() })) return publisherRef.get() }, BatchInputPreProcessor batchHandler, GraphQLServletListener... listeners) { GraphQLHttpServlet servlet = GraphQLHttpServlet.with( graphQLConfiguration( createGraphQlSchema(queryDataFetcher, mutationDataFetcher, subscriptionDataFetcher), batchHandler, listeners ) ) servlet.init() return servlet } static def graphQLConfiguration(GraphQLSchema schema, BatchInputPreProcessor batchInputPreProcessor, GraphQLServletListener... listeners) { def configBuilder = GraphQLConfiguration.with(schema) if (batchInputPreProcessor != null) { configBuilder.with(batchInputPreProcessor) } if (listeners != null) { configBuilder.with(Arrays.asList(listeners)) } configBuilder.with(executor()); configBuilder.build() } private static Executor executor() { new Executor() { @Override void execute(@NonNull Runnable command) { command.run() } } } static def createBatchExecutionHandler() { new TestBatchInputPreProcessor() } static def createGraphQlSchema( DataFetcher queryDataFetcher = { env -> env.arguments.arg }, DataFetcher mutationDataFetcher = { env -> env.arguments.arg }, DataFetcher subscriptionDataFetcher = { env -> AtomicReference> publisherRef = new AtomicReference<>() publisherRef.set(new SingleSubscriberPublisher<>({ subscription -> publisherRef.get().offer(env.arguments.arg) publisherRef.get().noMoreData() })) return publisherRef.get() }) { GraphQLObjectType query = GraphQLObjectType.newObject() .name("Query") .field { GraphQLFieldDefinition.Builder field -> field.name("echo") field.type(Scalars.GraphQLString) field.argument { argument -> argument.name("arg") argument.type(Scalars.GraphQLString) } field.dataFetcher(queryDataFetcher) } .field { GraphQLFieldDefinition.Builder field -> field.name("object") field.type( GraphQLObjectType.newObject() .name("NestedObject") .field { nested -> nested.name("a") nested.type(Scalars.GraphQLString) nested.argument { argument -> argument.name("arg") argument.type(Scalars.GraphQLString) } nested.dataFetcher(queryDataFetcher) } .field { nested -> nested.name("b") nested.type(Scalars.GraphQLString) nested.argument { argument -> argument.name("arg") argument.type(Scalars.GraphQLString) } nested.dataFetcher(queryDataFetcher) } ) field.dataFetcher(new StaticDataFetcher([:])) } .field { GraphQLFieldDefinition.Builder field -> field.name("returnsNullIncorrectly") field.type(new GraphQLNonNull(Scalars.GraphQLString)) field.dataFetcher({ env -> null }) } .build() GraphQLObjectType mutation = GraphQLObjectType.newObject() .name("Mutation") .field { field -> field.name("echo") field.type(Scalars.GraphQLString) field.argument { argument -> argument.name("arg") argument.type(Scalars.GraphQLString) } field.dataFetcher(mutationDataFetcher) } .field { field -> field.name("echoFile") field.type(Scalars.GraphQLString) field.argument { argument -> argument.name("file") argument.type(ApolloScalars.Upload) } field.dataFetcher({ env -> new String(ByteStreams.toByteArray(env.arguments.file.getInputStream())) }) } .field { field -> field.name("echoFiles") field.type(GraphQLList.list(Scalars.GraphQLString)) field.argument { argument -> argument.name("files") argument.type(GraphQLList.list(GraphQLNonNull.nonNull(ApolloScalars.Upload))) } field.dataFetcher({ env -> env.arguments.files.collect { new String(ByteStreams.toByteArray(it.getInputStream())) } }) } .build() GraphQLObjectType subscription = GraphQLObjectType.newObject() .name("Subscription") .field { field -> field.name("echo") field.type(Scalars.GraphQLString) field.argument { argument -> argument.name("arg") argument.type(Scalars.GraphQLString) } field.dataFetcher(subscriptionDataFetcher) } .build() return GraphQLSchema.newSchema() .query(query) .mutation(mutation) .subscription(subscription) .additionalType(ApolloScalars.Upload) .build() } static def createGraphQlSchemaWithTwoLevels(DataFetcher queryDataFetcher, DataFetcher fieldDataFetcher, DataFetcher otherQueryFetcher) { String sdl = """schema { query: Query } type Query{ query(arg : String): QueryEcho queryTwo(arg: String): OtherQueryEcho } type QueryEcho { echo(arg: String): FieldEcho } type OtherQueryEcho { echo(arg: String): String } type FieldEcho { echo(arg:String): String } """ def wiring = RuntimeWiring.newRuntimeWiring() .type(TypeRuntimeWiring.newTypeWiring("Query").dataFetcher("query", { env -> env.arguments.arg }) .dataFetcher("queryTwo", { env -> env.arguments.arg })) .type(TypeRuntimeWiring.newTypeWiring("QueryEcho").dataFetcher("echo", queryDataFetcher)) .type(TypeRuntimeWiring.newTypeWiring("FieldEcho").dataFetcher("echo", fieldDataFetcher)) .type(TypeRuntimeWiring.newTypeWiring("OtherQueryEcho").dataFetcher("echo", otherQueryFetcher)) .build() try { def registry = new SchemaParser().parse(new StringReader(sdl)) def options = SchemaGenerator.Options.defaultOptions() return new SchemaGenerator().makeExecutableSchema(options, registry, wiring) } catch (SchemaProblem e) { assert false: "The schema could not be compiled : ${e}" return null } } } ================================================ FILE: graphql-java-servlet/src/test/groovy/graphql/kickstart/servlet/cache/CacheReaderTest.groovy ================================================ package graphql.kickstart.servlet.cache import graphql.kickstart.execution.input.GraphQLInvocationInput import spock.lang.Specification import jakarta.servlet.ServletOutputStream import jakarta.servlet.http.HttpServletRequest import jakarta.servlet.http.HttpServletResponse class CacheReaderTest extends Specification { def cacheManager def invocationInput def request def response def cacheReader def cachedResponse def setup() { cacheManager = Mock(GraphQLResponseCacheManager) invocationInput = Mock(GraphQLInvocationInput) request = Mock(HttpServletRequest) response = Mock(HttpServletResponse) cacheReader = new CacheReader() cachedResponse = Mock(CachedResponse) } def "should return false if no cached response"() { given: cacheManager.get(request, invocationInput) >> null when: def result = cacheReader.responseFromCache(invocationInput, request, response, cacheManager) then: !result } def "should send error response if cached response is error"() { given: cachedResponse.isError() >> true cachedResponse.getErrorStatusCode() >> 10 cachedResponse.getErrorMessage() >> "some error" cacheManager.get(request, invocationInput) >> cachedResponse when: def result = cacheReader.responseFromCache(invocationInput, request, response, cacheManager) then: result 1 * response.sendError(10, "some error") } def "should send success response if cached response is ok"() { given: def outputStream = Mock(ServletOutputStream) cachedResponse.isError() >> false cachedResponse.getContentBytes() >> [00, 01, 02] response.getOutputStream() >> outputStream cacheManager.get(request, invocationInput) >> cachedResponse when: def result = cacheReader.responseFromCache(invocationInput, request, response, cacheManager) then: result 1 * response.setContentType("application/json;charset=UTF-8") 1 * response.setStatus(200) 1 * response.setCharacterEncoding("UTF-8") 1 * response.setContentLength(3) 1 * outputStream.write([00, 01, 02]) } def "should return false if exception is thrown"() { given: cacheManager.get(request, invocationInput) >> { throw new RuntimeException() } when: def result = cacheReader.responseFromCache(invocationInput, request, response, cacheManager) then: !result } } ================================================ FILE: graphql-java-servlet/src/test/groovy/graphql/kickstart/servlet/cache/CachingHttpRequestInvokerTest.groovy ================================================ package graphql.kickstart.servlet.cache import graphql.ExecutionResult import graphql.kickstart.execution.FutureExecutionResult import graphql.kickstart.execution.GraphQLInvoker import graphql.kickstart.execution.GraphQLObjectMapper import graphql.kickstart.execution.GraphQLQueryResult import graphql.kickstart.execution.input.GraphQLSingleInvocationInput import graphql.kickstart.servlet.GraphQLConfiguration import graphql.kickstart.servlet.HttpRequestInvoker import graphql.kickstart.servlet.ListenerHandler import spock.lang.Specification import jakarta.servlet.ServletOutputStream import jakarta.servlet.http.HttpServletRequest import jakarta.servlet.http.HttpServletResponse import java.util.concurrent.CompletableFuture class CachingHttpRequestInvokerTest extends Specification { def cacheReaderMock def cachingInvoker def invocationInputMock def requestMock def responseMock def responseCacheManagerMock def httpRequestInvokerMock def graphqlInvoker def configuration def graphqlObjectMapper def outputStreamMock def listenerHandlerMock def setup() { cacheReaderMock = Mock(CacheReader) invocationInputMock = Mock(GraphQLSingleInvocationInput) requestMock = Mock(HttpServletRequest) responseMock = Mock(HttpServletResponse) responseCacheManagerMock = Mock(GraphQLResponseCacheManager) configuration = Mock(GraphQLConfiguration) httpRequestInvokerMock = Mock(HttpRequestInvoker) graphqlInvoker = Mock(GraphQLInvoker) graphqlObjectMapper = Mock(GraphQLObjectMapper) outputStreamMock = Mock(ServletOutputStream) graphqlInvoker.execute(invocationInputMock) >> FutureExecutionResult.single(invocationInputMock, CompletableFuture.completedFuture(Mock(GraphQLQueryResult))) cachingInvoker = new CachingHttpRequestInvoker(configuration, httpRequestInvokerMock, cacheReaderMock) listenerHandlerMock = Mock(ListenerHandler) configuration.getResponseCacheManager() >> responseCacheManagerMock configuration.getGraphQLInvoker() >> graphqlInvoker configuration.getObjectMapper() >> graphqlObjectMapper graphqlObjectMapper.serializeResultAsBytes(_ as ExecutionResult) >> new byte[0] graphqlInvoker.queryAsync(invocationInputMock) >> CompletableFuture.completedFuture(Mock(GraphQLQueryResult)) responseMock.getOutputStream() >> outputStreamMock } def "should execute regular invoker if cache not exists"() { given: cacheReaderMock.responseFromCache(invocationInputMock, requestMock, responseMock, responseCacheManagerMock) >> false when: cachingInvoker.execute(invocationInputMock, requestMock, responseMock, listenerHandlerMock) then: 1 * httpRequestInvokerMock.execute(invocationInputMock, requestMock, responseMock, listenerHandlerMock) } def "should not execute regular invoker if cache exists"() { given: cacheReaderMock.responseFromCache(invocationInputMock, requestMock, responseMock, responseCacheManagerMock) >> true when: cachingInvoker.execute(invocationInputMock, requestMock, responseMock, listenerHandlerMock) then: 0 * httpRequestInvokerMock.execute(invocationInputMock, requestMock, responseMock, listenerHandlerMock) } def "should return bad request response when ioexception"() { given: cacheReaderMock.responseFromCache(invocationInputMock, requestMock, responseMock, responseCacheManagerMock) >> { throw new IOException() } when: cachingInvoker.execute(invocationInputMock, requestMock, responseMock, listenerHandlerMock) then: 1 * responseMock.setStatus(400) } def "should initialize completely when using single param constructor"() { given: def invoker = new CachingHttpRequestInvoker(configuration) when: invoker.execute(invocationInputMock, requestMock, responseMock, listenerHandlerMock) then: noExceptionThrown() } } ================================================ FILE: lombok.config ================================================ lombok.addLombokGeneratedAnnotation = true ================================================ FILE: renovate.json ================================================ { "extends": [ "github>graphql-java-kickstart/renovate-config" ] } ================================================ FILE: settings.gradle ================================================ rootProject.name = 'graphql-java-servlet' include ':graphql-java-kickstart' include ':graphql-java-servlet'