diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a8838804..b2880247 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -66,3 +66,32 @@ jobs: BINTRAY_USER: ${{ secrets.BINTRAY_USER }} BINTRAY_PASS: ${{ secrets.BINTRAY_PASSWORD }} run: ./gradlew artifactoryPublish -Dsnapshot=true -Dbuild.number=${{ env.GITHUB_RUN_NUMBER }} + sonar: + name: Sonar analysis + needs: validation + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis + - name: Set up JDK 11 + uses: actions/setup-java@v1 + with: + java-version: 11 + - name: Cache SonarCloud packages + uses: actions/cache@v1 + with: + path: ~/.sonar/cache + key: ${{ runner.os }}-sonar + restore-keys: ${{ runner.os }}-sonar + - name: Cache Gradle packages + uses: actions/cache@v1 + 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 }} + run: ./gradlew build jacocoTestReport sonarqube --info diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index f5613cd8..787e598e 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -4,6 +4,7 @@ on: branches-ignore: - master pull_request: + types: [opened, synchronize, reopened] jobs: validation: @@ -49,3 +50,39 @@ jobs: 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@v2 + if: env.SONAR_TOKEN != null + with: + fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis + - name: Set up JDK 11 + if: env.SONAR_TOKEN != null + uses: actions/setup-java@v1 + with: + java-version: 11 + - name: Cache SonarCloud packages + if: env.SONAR_TOKEN != null + uses: actions/cache@v1 + 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@v1 + 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 diff --git a/build.gradle b/build.gradle index 32a5556e..274d0a3d 100644 --- a/build.gradle +++ b/build.gradle @@ -25,10 +25,22 @@ plugins { id 'net.researchgate.release' version "$LIB_RELEASE_PLUGIN_VER" id "org.springframework.boot" version "$LIB_SPRING_BOOT_VER" apply false id "com.jfrog.artifactory" version "4.15.1" apply false + id "org.sonarqube" version "3.0" + id "jacoco" +} + +sonarqube { + properties { + property "sonar.projectKey", "graphql-java-kickstart_graphql-spring-boot" + 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: 'java-library' apply plugin: 'maven-publish' @@ -76,6 +88,14 @@ subprojects { } } + jacocoTestReport { + reports { + xml.enabled = true + html.enabled = false + csv.enabled = false + } + } + idea { module { downloadJavadoc = true diff --git a/example/src/main/resources/application.yml b/example/src/main/resources/application.yml index 1f66d7ec..1bb92046 100644 --- a/example/src/main/resources/application.yml +++ b/example/src/main/resources/application.yml @@ -25,8 +25,14 @@ altair: graphiql: enabled: true cdn: - enabled: true + enabled: false version: 0.17.5 + headers: + Test: TestHeader + props: + variables: + headerEditorEnabled: true + headers: '{ "Authorization": "SomeValue" }' voyager: enabled: true cdn: diff --git a/gradle.properties b/gradle.properties index 6cdb03fa..274dd8c4 100644 --- a/gradle.properties +++ b/gradle.properties @@ -40,9 +40,9 @@ TARGET_COMPATIBILITY = 1.8 ### Dependencies LIB_GRAPHQL_JAVA_VER = 15.0 -LIB_SPRING_BOOT_VER = 2.3.4.RELEASE -LIB_GRAPHQL_SERVLET_VER = 10.0.0 -LIB_GRAPHQL_JAVA_TOOLS_VER = 6.2.0 +LIB_SPRING_BOOT_VER = 2.3.6.RELEASE +LIB_GRAPHQL_SERVLET_VER = 10.1.0-SNAPSHOT +LIB_GRAPHQL_JAVA_TOOLS_VER = 6.3.0 LIB_GRAPHQL_ANNOTATIONS_VER = 8.2 LIB_REFLECTIONS_VER = 0.9.11 LIB_APACHE_COMMONS_TEXT=1.8 diff --git a/graphiql-spring-boot-autoconfigure/src/main/java/graphql/kickstart/graphiql/boot/GraphiQLController.java b/graphiql-spring-boot-autoconfigure/src/main/java/graphql/kickstart/graphiql/boot/GraphiQLController.java index e8a45a10..b3b1a15c 100644 --- a/graphiql-spring-boot-autoconfigure/src/main/java/graphql/kickstart/graphiql/boot/GraphiQLController.java +++ b/graphiql-spring-boot-autoconfigure/src/main/java/graphql/kickstart/graphiql/boot/GraphiQLController.java @@ -2,25 +2,23 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.Charset; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.text.StrSubstitutor; +import org.apache.commons.text.StringSubstitutor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.env.Environment; import org.springframework.core.io.ClassPathResource; -import org.springframework.http.MediaType; import org.springframework.security.web.csrf.CsrfToken; import org.springframework.util.StreamUtils; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestParam; -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.Charset; -import java.util.HashMap; -import java.util.Map; -import java.util.Properties; - /** * @author Andrew Potter */ @@ -61,17 +59,10 @@ private void loadProps() throws IOException { private void loadHeaders() { PropertyGroupReader propertyReader = new PropertyGroupReader(environment, "graphiql.headers."); headerProperties = propertyReader.load(); - addIfAbsent(headerProperties, "Accept"); - addIfAbsent(headerProperties, "Content-Type"); } - private void addIfAbsent(Properties headerProperties, String header) { - if (!headerProperties.containsKey(header)) { - headerProperties.setProperty(header, MediaType.APPLICATION_JSON_VALUE); - } - } - - public byte[] graphiql(String contextPath, @PathVariable Map params, Object csrf) { + public byte[] graphiql(String contextPath, @PathVariable Map params, + Object csrf) { if (csrf != null) { CsrfToken csrfToken = (CsrfToken) csrf; headerProperties.setProperty(csrfToken.getHeaderName(), csrfToken.getToken()); @@ -83,7 +74,7 @@ public byte[] graphiql(String contextPath, @PathVariable Map par contextPath + graphiQLProperties.getSTATIC().getBasePath() ); - String populatedTemplate = StrSubstitutor.replace(template, replacements); + String populatedTemplate = StringSubstitutor.replace(template, replacements); return populatedTemplate.getBytes(Charset.defaultCharset()); } @@ -97,7 +88,8 @@ private Map getReplacements( replacements.put("subscriptionsEndpoint", subscriptionsEndpoint); replacements.put("staticBasePath", staticBasePath); replacements.put("pageTitle", graphiQLProperties.getPageTitle()); - replacements.put("pageFavicon", getResourceUrl(staticBasePath, "favicon.ico", FAVICON_GRAPHQL_ORG)); + replacements + .put("pageFavicon", getResourceUrl(staticBasePath, "favicon.ico", FAVICON_GRAPHQL_ORG)); replacements.put("es6PromiseJsUrl", getResourceUrl(staticBasePath, "es6-promise.auto.min.js", joinCdnjsPath("es6-promise", "4.1.1", "es6-promise.auto.min.js"))); replacements.put("fetchJsUrl", getResourceUrl(staticBasePath, "fetch.min.js", @@ -123,9 +115,11 @@ private Map getReplacements( log.error("Cannot serialize headers", e); } replacements - .put("subscriptionClientTimeout", String.valueOf(graphiQLProperties.getSubscriptions().getTimeout() * 1000)); + .put("subscriptionClientTimeout", + String.valueOf(graphiQLProperties.getSubscriptions().getTimeout() * 1000)); replacements - .put("subscriptionClientReconnect", String.valueOf(graphiQLProperties.getSubscriptions().isReconnect())); + .put("subscriptionClientReconnect", + String.valueOf(graphiQLProperties.getSubscriptions().isReconnect())); replacements.put("editorThemeCss", getEditorThemeCssURL()); return replacements; } @@ -161,7 +155,8 @@ private String joinJsDelivrPath(String library, String cdnVersion, String cdnFil return CDN_JSDELIVR_NET_NPM + library + "@" + cdnVersion + "/" + cdnFileName; } - private String constructGraphQlEndpoint(String contextPath, @RequestParam Map params) { + private String constructGraphQlEndpoint(String contextPath, + @RequestParam Map params) { String endpoint = graphiQLProperties.getEndpoint().getGraphql(); for (Map.Entry param : params.entrySet()) { endpoint = endpoint.replaceAll("\\{" + param.getKey() + "}", param.getValue()); diff --git a/graphiql-spring-boot-autoconfigure/src/main/resources/graphiql.html b/graphiql-spring-boot-autoconfigure/src/main/resources/graphiql.html index c45fa978..3ac2e992 100644 --- a/graphiql-spring-boot-autoconfigure/src/main/resources/graphiql.html +++ b/graphiql-spring-boot-autoconfigure/src/main/resources/graphiql.html @@ -101,12 +101,22 @@ var headers = ${headers} + function addRequiredHeadersIfAbsent() { + if (!headers['Accept']) { + headers['Accept'] = 'application/json' + } + if (!headers['Content-Type']) { + headers['Content-Type'] = 'application/json' + } + } + function onEditHeaders(newHeaders) { try { headers = JSON.parse(newHeaders) } catch(e) { headers = {} } + addRequiredHeadersIfAbsent() } // Defines a GraphQL fetcher using the fetch API. You're not required to @@ -162,8 +172,13 @@ props.onEditVariables = onEditVariables props.onEditOperationName = onEditOperationName props.onEditHeaders = onEditHeaders + props.headers = props.headers || '{}' + if (headers) { + var newHeaders = Object.assign({}, JSON.parse(props.headers), headers) + props.headers = JSON.stringify(newHeaders, undefined, 2) + } + onEditHeaders(props.headers) - console.debug(props) // Render into the body. ReactDOM.render( React.createElement(GraphiQL, props), diff --git a/graphql-kickstart-spring-support/src/main/java/graphql/kickstart/spring/AbstractGraphQLController.java b/graphql-kickstart-spring-support/src/main/java/graphql/kickstart/spring/AbstractGraphQLController.java index 20821da4..991a6b9c 100644 --- a/graphql-kickstart-spring-support/src/main/java/graphql/kickstart/spring/AbstractGraphQLController.java +++ b/graphql-kickstart-spring-support/src/main/java/graphql/kickstart/spring/AbstractGraphQLController.java @@ -75,8 +75,7 @@ public Object graphqlPOST( throw new ResponseStatusException(HttpStatus.UNPROCESSABLE_ENTITY, "Could not process GraphQL request"); } - @GetMapping(value = "${graphql.url:graphql}", - produces = MediaType.APPLICATION_JSON_VALUE) + @GetMapping(value = "${graphql.url:graphql}", produces = MediaType.APPLICATION_JSON_VALUE) public Object graphqlGET( @Nullable @RequestParam("query") String query, @Nullable @RequestParam(value = "operationName", required = false) String operationName, diff --git a/graphql-spring-boot-autoconfigure/src/main/java/graphql/kickstart/spring/web/boot/GraphQLWebAutoConfiguration.java b/graphql-spring-boot-autoconfigure/src/main/java/graphql/kickstart/spring/web/boot/GraphQLWebAutoConfiguration.java index 233a7bc5..14fdb181 100644 --- a/graphql-spring-boot-autoconfigure/src/main/java/graphql/kickstart/spring/web/boot/GraphQLWebAutoConfiguration.java +++ b/graphql-spring-boot-autoconfigure/src/main/java/graphql/kickstart/spring/web/boot/GraphQLWebAutoConfiguration.java @@ -281,7 +281,6 @@ public GraphQLConfiguration graphQLServletConfiguration(GraphQLInvocationInputFa .with(queryInvoker) .with(graphQLObjectMapper) .with(listeners) - .with(graphQLServletProperties.isAsyncModeEnabled()) .with(graphQLServletProperties.getSubscriptionTimeout()) .with(batchInputPreProcessor) .with(graphQLServletProperties.getContextSetting()) diff --git a/graphql-spring-boot-test-autoconfigure/src/main/java/com/graphql/spring/boot/test/GraphQLTest.java b/graphql-spring-boot-test-autoconfigure/src/main/java/com/graphql/spring/boot/test/GraphQLTest.java index 605d020b..4a17dfee 100644 --- a/graphql-spring-boot-test-autoconfigure/src/main/java/com/graphql/spring/boot/test/GraphQLTest.java +++ b/graphql-spring-boot-test-autoconfigure/src/main/java/com/graphql/spring/boot/test/GraphQLTest.java @@ -13,8 +13,9 @@ import graphql.kickstart.execution.config.GraphQLServletObjectMapperConfigurer; import graphql.kickstart.execution.context.GraphQLContextBuilder; import graphql.kickstart.execution.error.GraphQLErrorHandler; +import graphql.kickstart.servlet.AbstractGraphQLHttpServlet; +import graphql.kickstart.servlet.GraphQLHttpServlet; import graphql.kickstart.servlet.GraphQLWebsocketServlet; -import graphql.kickstart.servlet.SimpleGraphQLHttpServlet; import graphql.kickstart.servlet.core.GraphQLServletListener; import graphql.kickstart.servlet.input.GraphQLInvocationInputFactory; import graphql.kickstart.spring.web.boot.GraphQLInstrumentationAutoConfiguration; @@ -47,8 +48,9 @@ import org.springframework.web.socket.server.standard.ServerEndpointExporter; /** - * Annotation that can be specified on a test class in combination with {@code @RunWith(SpringRunner.class)} - * for running tests that focus only on GraphQL components. + * Annotation that can be specified on a test class in combination with + * {@code @RunWith(SpringRunner.class)} for running tests that focus only on GraphQL + * components. *

* Provides the following features over the regular Spring TestContext Framework: *

    @@ -83,69 +85,70 @@ @ImportAutoConfiguration public @interface GraphQLTest { - String[] value() default {}; + String[] value() default {}; - @AliasFor( - annotation = ActiveProfiles.class - ) - String[] profiles() default {"test"}; + @AliasFor( + annotation = ActiveProfiles.class + ) + String[] profiles() default {"test"}; - @AliasFor( - annotation = SpringBootTest.class - ) - SpringBootTest.WebEnvironment webEnvironment() default SpringBootTest.WebEnvironment.RANDOM_PORT; + @AliasFor( + annotation = SpringBootTest.class + ) + SpringBootTest.WebEnvironment webEnvironment() default SpringBootTest.WebEnvironment.RANDOM_PORT; - @AliasFor( - annotation = ImportAutoConfiguration.class - ) - Class[] classes() default { - GraphQLInstrumentationAutoConfiguration.class, - ServletWebServerFactoryAutoConfiguration.class, - GraphQLJavaToolsAutoConfiguration.class, - GraphQLWebAutoConfiguration.class, - GraphQLTestAutoConfiguration.class, - PropertySourcesPlaceholderConfigurer.class, - WebSocketServletAutoConfiguration.class, - MetricsAutoConfiguration.class, - SimpleMetricsExportAutoConfiguration.class, - JacksonAutoConfiguration.class - }; + @AliasFor( + annotation = ImportAutoConfiguration.class + ) + Class[] classes() default { + GraphQLInstrumentationAutoConfiguration.class, + ServletWebServerFactoryAutoConfiguration.class, + GraphQLJavaToolsAutoConfiguration.class, + GraphQLWebAutoConfiguration.class, + GraphQLTestAutoConfiguration.class, + PropertySourcesPlaceholderConfigurer.class, + WebSocketServletAutoConfiguration.class, + MetricsAutoConfiguration.class, + SimpleMetricsExportAutoConfiguration.class, + JacksonAutoConfiguration.class + }; - @AliasFor( - annotation = ComponentScan.class - ) - boolean useDefaultFilters() default false; + @AliasFor( + annotation = ComponentScan.class + ) + boolean useDefaultFilters() default false; - @AliasFor( - annotation = ComponentScan.class - ) - ComponentScan.Filter[] includeFilters() default { - @ComponentScan.Filter(type = ASSIGNABLE_TYPE, classes = { - SchemaParser.class, - GraphQLResolver.class, - SchemaParserDictionary.class, - GraphQLScalarType.class, - SchemaParserOptions.class, - GraphQLSchema.class, - GraphQLSchemaProvider.class, - GraphQLServletListener.class, - Instrumentation.class, - GraphQLErrorHandler.class, - ExecutionStrategy.class, - GraphQLContextBuilder.class, - GraphQLRootObjectBuilder.class, - GraphQLServletObjectMapperConfigurer.class, - PreparsedDocumentProvider.class, - CorsFilter.class, - ExecutionStrategyProvider.class, - GraphQLInvocationInputFactory.class, - GraphQLQueryInvoker.class, - GraphQLObjectMapper.class, - SimpleGraphQLHttpServlet.class, - GraphQLWebsocketServlet.class, - ServerEndpointExporter.class, - MultipartConfigElement.class - }) - }; + @AliasFor( + annotation = ComponentScan.class + ) + ComponentScan.Filter[] includeFilters() default { + @ComponentScan.Filter(type = ASSIGNABLE_TYPE, classes = { + SchemaParser.class, + GraphQLResolver.class, + SchemaParserDictionary.class, + GraphQLScalarType.class, + SchemaParserOptions.class, + GraphQLSchema.class, + GraphQLSchemaProvider.class, + GraphQLServletListener.class, + Instrumentation.class, + GraphQLErrorHandler.class, + ExecutionStrategy.class, + GraphQLContextBuilder.class, + GraphQLRootObjectBuilder.class, + GraphQLServletObjectMapperConfigurer.class, + PreparsedDocumentProvider.class, + CorsFilter.class, + ExecutionStrategyProvider.class, + GraphQLInvocationInputFactory.class, + GraphQLQueryInvoker.class, + GraphQLObjectMapper.class, + GraphQLHttpServlet.class, + AbstractGraphQLHttpServlet.class, + GraphQLWebsocketServlet.class, + ServerEndpointExporter.class, + MultipartConfigElement.class + }) + }; }