diff --git a/graphql-spring-boot-starter/src/main/java/org/springframework/graphql/boot/GraphQLServiceAutoConfiguration.java b/graphql-spring-boot-starter/src/main/java/org/springframework/graphql/boot/GraphQLServiceAutoConfiguration.java new file mode 100644 index 0000000..a205100 --- /dev/null +++ b/graphql-spring-boot-starter/src/main/java/org/springframework/graphql/boot/GraphQLServiceAutoConfiguration.java @@ -0,0 +1,41 @@ +/* + * Copyright 2002-2021 the original author or 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. + */ +package org.springframework.graphql.boot; + +import graphql.GraphQL; + +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.graphql.GraphQLService; +import org.springframework.graphql.support.ExecutionGraphQLService; +import org.springframework.graphql.support.GraphQLSource; + +@Configuration +@ConditionalOnClass(GraphQL.class) +@ConditionalOnMissingBean(GraphQLService.class) +@AutoConfigureAfter(GraphQLAutoConfiguration.class) +public class GraphQLServiceAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + public GraphQLService graphQLService(GraphQLSource graphQLSource) { + return new ExecutionGraphQLService(graphQLSource); + } + +} diff --git a/graphql-spring-boot-starter/src/main/java/org/springframework/graphql/boot/WebFluxGraphQLAutoConfiguration.java b/graphql-spring-boot-starter/src/main/java/org/springframework/graphql/boot/WebFluxGraphQLAutoConfiguration.java index f27f03a..08f3340 100644 --- a/graphql-spring-boot-starter/src/main/java/org/springframework/graphql/boot/WebFluxGraphQLAutoConfiguration.java +++ b/graphql-spring-boot-starter/src/main/java/org/springframework/graphql/boot/WebFluxGraphQLAutoConfiguration.java @@ -34,9 +34,9 @@ import org.springframework.context.annotation.Configuration; import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; +import org.springframework.graphql.GraphQLService; import org.springframework.graphql.support.GraphQLSource; -import org.springframework.graphql.web.DefaultWebGraphQLService; -import org.springframework.graphql.web.WebGraphQLService; +import org.springframework.graphql.web.WebGraphQLHandler; import org.springframework.graphql.web.WebInterceptor; import org.springframework.graphql.web.webflux.GraphQLHttpHandler; import org.springframework.graphql.web.webflux.GraphQLWebSocketHandler; @@ -64,16 +64,14 @@ public class WebFluxGraphQLAutoConfiguration { @Bean @ConditionalOnMissingBean - public WebGraphQLService webGraphQLService(GraphQLSource graphQLSource, ObjectProvider interceptors) { - DefaultWebGraphQLService handler = new DefaultWebGraphQLService(graphQLSource); - handler.setInterceptors(interceptors.orderedStream().collect(Collectors.toList())); - return handler; + public WebGraphQLHandler webGraphQLHandler(ObjectProvider interceptors, GraphQLService service) { + return WebInterceptor.createHandler(interceptors.orderedStream().collect(Collectors.toList()), service); } @Bean @ConditionalOnMissingBean - public GraphQLHttpHandler graphQLHandler(WebGraphQLService service) { - return new GraphQLHttpHandler(service); + public GraphQLHttpHandler graphQLHttpHandler(WebGraphQLHandler webGraphQLHandler) { + return new GraphQLHttpHandler(webGraphQLHandler); } @Bean @@ -99,22 +97,22 @@ static class WebSocketConfiguration { @Bean @ConditionalOnMissingBean public GraphQLWebSocketHandler graphQLWebSocketHandler( - WebGraphQLService service, GraphQLProperties properties, ServerCodecConfigurer configurer) { + WebGraphQLHandler webGraphQLHandler, GraphQLProperties properties, ServerCodecConfigurer configurer) { return new GraphQLWebSocketHandler( - service, configurer, properties.getWebsocket().getConnectionInitTimeout()); + webGraphQLHandler, configurer, properties.getWebsocket().getConnectionInitTimeout()); } @Bean public HandlerMapping graphQLWebSocketEndpoint( - GraphQLWebSocketHandler handler, GraphQLProperties properties) { + GraphQLWebSocketHandler graphQLWebSocketHandler, GraphQLProperties properties) { String path = properties.getWebsocket().getPath(); if (logger.isInfoEnabled()) { logger.info("GraphQL endpoint WebSocket " + path); } WebSocketHandlerMapping handlerMapping = new WebSocketHandlerMapping(); - handlerMapping.setUrlMap(Collections.singletonMap(path, handler)); + handlerMapping.setUrlMap(Collections.singletonMap(path, graphQLWebSocketHandler)); handlerMapping.setOrder(-2); // Ahead of HTTP endpoint ("routerFunctionMapping" bean) return handlerMapping; } diff --git a/graphql-spring-boot-starter/src/main/java/org/springframework/graphql/boot/WebMvcGraphQLAutoConfiguration.java b/graphql-spring-boot-starter/src/main/java/org/springframework/graphql/boot/WebMvcGraphQLAutoConfiguration.java index 3a0f4f3..1c58886 100644 --- a/graphql-spring-boot-starter/src/main/java/org/springframework/graphql/boot/WebMvcGraphQLAutoConfiguration.java +++ b/graphql-spring-boot-starter/src/main/java/org/springframework/graphql/boot/WebMvcGraphQLAutoConfiguration.java @@ -38,9 +38,9 @@ import org.springframework.context.annotation.Configuration; import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; +import org.springframework.graphql.GraphQLService; import org.springframework.graphql.support.GraphQLSource; -import org.springframework.graphql.web.DefaultWebGraphQLService; -import org.springframework.graphql.web.WebGraphQLService; +import org.springframework.graphql.web.WebGraphQLHandler; import org.springframework.graphql.web.WebInterceptor; import org.springframework.graphql.web.webmvc.GraphQLHttpHandler; import org.springframework.graphql.web.webmvc.GraphQLWebSocketHandler; @@ -71,16 +71,14 @@ public class WebMvcGraphQLAutoConfiguration { @Bean @ConditionalOnMissingBean - public WebGraphQLService webGraphQLService(GraphQLSource graphQLSource, ObjectProvider interceptors) { - DefaultWebGraphQLService handler = new DefaultWebGraphQLService(graphQLSource); - handler.setInterceptors(interceptors.orderedStream().collect(Collectors.toList())); - return handler; + public WebGraphQLHandler webGraphQLHandler(ObjectProvider interceptors, GraphQLService service) { + return WebInterceptor.createHandler(interceptors.orderedStream().collect(Collectors.toList()), service); } @Bean @ConditionalOnMissingBean - public GraphQLHttpHandler graphQLHandler(WebGraphQLService service) { - return new GraphQLHttpHandler(service); + public GraphQLHttpHandler graphQLHttpHandler(WebGraphQLHandler webGraphQLHandler) { + return new GraphQLHttpHandler(webGraphQLHandler); } @Bean @@ -108,7 +106,7 @@ static class WebSocketConfiguration { @Bean @ConditionalOnMissingBean public GraphQLWebSocketHandler graphQLWebSocketHandler( - WebGraphQLService service, GraphQLProperties properties, HttpMessageConverters converters) { + WebGraphQLHandler webGraphQLHandler, GraphQLProperties properties, HttpMessageConverters converters) { HttpMessageConverter converter = converters.getConverters().stream() .filter(candidate -> candidate.canRead(Map.class, MediaType.APPLICATION_JSON)) @@ -116,7 +114,7 @@ public GraphQLWebSocketHandler graphQLWebSocketHandler( .orElseThrow(() -> new IllegalStateException("No JSON converter")); return new GraphQLWebSocketHandler( - service, converter, properties.getWebsocket().getConnectionInitTimeout()); + webGraphQLHandler, converter, properties.getWebsocket().getConnectionInitTimeout()); } @Bean diff --git a/graphql-spring-boot-starter/src/main/resources/META-INF/spring.factories b/graphql-spring-boot-starter/src/main/resources/META-INF/spring.factories index 15942d6..26047ae 100644 --- a/graphql-spring-boot-starter/src/main/resources/META-INF/spring.factories +++ b/graphql-spring-boot-starter/src/main/resources/META-INF/spring.factories @@ -1,5 +1,6 @@ org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ org.springframework.graphql.boot.actuate.metrics.GraphQLMetricsAutoConfiguration,\ org.springframework.graphql.boot.GraphQLAutoConfiguration,\ +org.springframework.graphql.boot.GraphQLServiceAutoConfiguration,\ org.springframework.graphql.boot.WebFluxGraphQLAutoConfiguration,\ org.springframework.graphql.boot.WebMvcGraphQLAutoConfiguration diff --git a/graphql-spring-boot-starter/src/test/java/org/springframework/graphql/boot/WebFluxApplicationContextTests.java b/graphql-spring-boot-starter/src/test/java/org/springframework/graphql/boot/WebFluxApplicationContextTests.java index 65e5690..75e084c 100644 --- a/graphql-spring-boot-starter/src/test/java/org/springframework/graphql/boot/WebFluxApplicationContextTests.java +++ b/graphql-spring-boot-starter/src/test/java/org/springframework/graphql/boot/WebFluxApplicationContextTests.java @@ -19,7 +19,6 @@ import java.util.function.Consumer; import org.junit.jupiter.api.Test; -import reactor.core.publisher.Mono; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.http.codec.CodecsAutoConfiguration; @@ -32,7 +31,6 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.graphql.web.WebInterceptor; -import org.springframework.graphql.web.WebOutput; import org.springframework.http.MediaType; import org.springframework.test.web.reactive.server.WebTestClient; @@ -43,7 +41,8 @@ class WebFluxApplicationContextTests { private static final AutoConfigurations AUTO_CONFIGURATIONS = AutoConfigurations.of( HttpHandlerAutoConfiguration.class, WebFluxAutoConfiguration.class, CodecsAutoConfiguration.class, JacksonAutoConfiguration.class, - GraphQLAutoConfiguration.class, WebFluxGraphQLAutoConfiguration.class); + GraphQLAutoConfiguration.class, GraphQLServiceAutoConfiguration.class, + WebFluxGraphQLAutoConfiguration.class); private static final String BASE_URL = "https://spring.example.org/graphql"; @@ -139,12 +138,8 @@ static class CustomWebInterceptor { @Bean public WebInterceptor customWebInterceptor() { - return new WebInterceptor() { - @Override - public Mono postHandle(WebOutput output) { - return Mono.just(output.transform(builder -> builder.responseHeader("X-Custom-Header", "42"))); - } - }; + return (input, next) -> next.handle(input).map(output -> + output.transform(builder -> builder.responseHeader("X-Custom-Header", "42"))); } } diff --git a/graphql-spring-boot-starter/src/test/java/org/springframework/graphql/boot/WebMvcApplicationContextTests.java b/graphql-spring-boot-starter/src/test/java/org/springframework/graphql/boot/WebMvcApplicationContextTests.java index 9d34626..1d13736 100644 --- a/graphql-spring-boot-starter/src/test/java/org/springframework/graphql/boot/WebMvcApplicationContextTests.java +++ b/graphql-spring-boot-starter/src/test/java/org/springframework/graphql/boot/WebMvcApplicationContextTests.java @@ -18,7 +18,6 @@ import org.junit.jupiter.api.Test; -import reactor.core.publisher.Mono; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration; @@ -29,7 +28,6 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.graphql.web.WebInterceptor; -import org.springframework.graphql.web.WebOutput; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; @@ -39,7 +37,8 @@ import static graphql.schema.idl.TypeRuntimeWiring.newTypeWiring; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.asyncDispatch; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -48,7 +47,8 @@ class WebMvcApplicationContextTests { public static final AutoConfigurations AUTO_CONFIGURATIONS = AutoConfigurations.of( DispatcherServletAutoConfiguration.class, WebMvcAutoConfiguration.class, HttpMessageConvertersAutoConfiguration.class, JacksonAutoConfiguration.class, - GraphQLAutoConfiguration.class, WebMvcGraphQLAutoConfiguration.class); + GraphQLAutoConfiguration.class, GraphQLServiceAutoConfiguration.class, + WebMvcGraphQLAutoConfiguration.class); @Test void endpointHandlesGraphQLQuery() { @@ -135,13 +135,8 @@ static class CustomWebInterceptor { @Bean public WebInterceptor customWebInterceptor() { - return new WebInterceptor() { - @Override - public Mono postHandle(WebOutput output) { - return Mono.just(output.transform(builder -> - builder.responseHeader("X-Custom-Header", "42"))); - } - }; + return (input, next) -> next.handle(input).map(output -> + output.transform(builder -> builder.responseHeader("X-Custom-Header", "42"))); } } diff --git a/samples/webflux-websocket/src/test/java/io/spring/sample/graphql/QueryTests.java b/samples/webflux-websocket/src/test/java/io/spring/sample/graphql/QueryTests.java index a39277a..8ad291b 100644 --- a/samples/webflux-websocket/src/test/java/io/spring/sample/graphql/QueryTests.java +++ b/samples/webflux-websocket/src/test/java/io/spring/sample/graphql/QueryTests.java @@ -21,7 +21,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.graphql.web.WebGraphQLService; +import org.springframework.graphql.web.WebGraphQLHandler; import org.springframework.graphql.test.tester.GraphQLTester; /** @@ -34,9 +34,9 @@ public class QueryTests { @BeforeEach - public void setUp(@Autowired WebGraphQLService service) { + public void setUp(@Autowired WebGraphQLHandler handler) { this.graphQLTester = GraphQLTester.create(webInput -> - service.execute(webInput).contextWrite(context -> context.put("name", "James"))); + handler.handle(webInput).contextWrite(context -> context.put("name", "James"))); } diff --git a/samples/webflux-websocket/src/test/java/io/spring/sample/graphql/SubscriptionTests.java b/samples/webflux-websocket/src/test/java/io/spring/sample/graphql/SubscriptionTests.java index aa3e196..9bd8505 100644 --- a/samples/webflux-websocket/src/test/java/io/spring/sample/graphql/SubscriptionTests.java +++ b/samples/webflux-websocket/src/test/java/io/spring/sample/graphql/SubscriptionTests.java @@ -23,7 +23,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.graphql.web.WebGraphQLService; +import org.springframework.graphql.web.WebGraphQLHandler; import org.springframework.graphql.test.tester.GraphQLTester; /** @@ -36,9 +36,9 @@ public class SubscriptionTests { @BeforeEach - public void setUp(@Autowired WebGraphQLService service) { + public void setUp(@Autowired WebGraphQLHandler handler) { this.graphQLTester = GraphQLTester.create(webInput -> - service.execute(webInput).contextWrite(context -> context.put("name", "James"))); + handler.handle(webInput).contextWrite(context -> context.put("name", "James"))); } diff --git a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultGraphQLTester.java b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultGraphQLTester.java index 717d39a..2e4c615 100644 --- a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultGraphQLTester.java +++ b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultGraphQLTester.java @@ -43,7 +43,7 @@ import org.springframework.core.ParameterizedTypeReference; import org.springframework.graphql.RequestInput; -import org.springframework.graphql.web.WebGraphQLService; +import org.springframework.graphql.web.WebGraphQLHandler; import org.springframework.graphql.web.WebInput; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; @@ -83,9 +83,9 @@ class DefaultGraphQLTester implements GraphQLTester { this.requestStrategy = new WebTestClientRequestStrategy(client, this.jsonPathConfig); } - DefaultGraphQLTester(WebGraphQLService service) { + DefaultGraphQLTester(WebGraphQLHandler handler) { this.jsonPathConfig = initJsonPathConfig(); - this.requestStrategy = new DirectRequestStrategy(service, this.jsonPathConfig); + this.requestStrategy = new DirectRequestStrategy(handler, this.jsonPathConfig); } private Configuration initJsonPathConfig() { @@ -182,12 +182,12 @@ private static class DirectRequestStrategy implements RequestStrategy { private static final Duration DEFAULT_TIMEOUT = Duration.ofSeconds(5); - private final WebGraphQLService graphQLService; + private final WebGraphQLHandler graphQLHandler; private final Configuration jsonPathConfig; - public DirectRequestStrategy(WebGraphQLService service, Configuration jsonPathConfig) { - this.graphQLService = service; + public DirectRequestStrategy(WebGraphQLHandler handler, Configuration jsonPathConfig) { + this.graphQLHandler = handler; this.jsonPathConfig = jsonPathConfig; } @@ -213,7 +213,7 @@ public SubscriptionSpec executeSubscription(RequestInput input) { private ExecutionResult executeInternal(RequestInput input) { WebInput webInput = new WebInput(DEFAULT_URL, DEFAULT_HEADERS, input.toMap(), null); - ExecutionResult result = this.graphQLService.execute(webInput).block(DEFAULT_TIMEOUT); + ExecutionResult result = this.graphQLHandler.handle(webInput).block(DEFAULT_TIMEOUT); Assert.notNull(result, "Expected ExecutionResult"); return result; } diff --git a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/GraphQLTester.java b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/GraphQLTester.java index 5ae2563..21d5121 100644 --- a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/GraphQLTester.java +++ b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/GraphQLTester.java @@ -24,13 +24,14 @@ import reactor.core.publisher.Flux; import org.springframework.core.ParameterizedTypeReference; -import org.springframework.graphql.web.WebGraphQLService; +import org.springframework.graphql.web.WebGraphQLHandler; import org.springframework.lang.Nullable; import org.springframework.test.web.reactive.server.WebTestClient; /** - * Main entry point for testing GraphQL with requests performed via - * {@link WebTestClient} as an HTTP client or via any {@link WebGraphQLService}. + * Main entry point for testing GraphQL with requests performed either as an + * HTTP client via {@link WebTestClient} or directly via a + * {@link WebGraphQLHandler}. * * *

GraphQL requests to Spring MVC without an HTTP server: @@ -75,7 +76,7 @@ * } * * - *

GraphQL requests to any {@link WebGraphQLService}: + *

GraphQL requests to any {@link WebGraphQLHandler}: *

  * @SpringBootTest
  * public class MyTests {
@@ -83,8 +84,8 @@
  *  private GraphQLTester graphQLTester;
  *
  *  @BeforeEach
- *  public void setUp(@Autowired WebGraphQLService service) {
- *      this.graphQLTester = GraphQLTester.create(service);
+ *  public void setUp(@Autowired WebGraphQLHandler handler) {
+ *      this.graphQLTester = GraphQLTester.create(handler);
  *  }
  * 
*/ @@ -112,13 +113,13 @@ static GraphQLTester create(WebTestClient client) { } /** - * Create a {@code GraphQLTester} that performs GraphQL requests through the - * given {@link WebGraphQLService}. - * @param service the handler to execute requests with + * Create a {@code GraphQLTester} that performs GraphQL requests through + * the given {@link WebGraphQLHandler}. + * @param handler the handler to execute requests with * @return the created {@code GraphQLTester} instance */ - static GraphQLTester create(WebGraphQLService service) { - return new DefaultGraphQLTester(service); + static GraphQLTester create(WebGraphQLHandler handler) { + return new DefaultGraphQLTester(handler); } diff --git a/spring-graphql-test/src/test/java/org/springframework/graphql/test/tester/GraphQLTesterTests.java b/spring-graphql-test/src/test/java/org/springframework/graphql/test/tester/GraphQLTesterTests.java index 330c15f..41358be 100644 --- a/spring-graphql-test/src/test/java/org/springframework/graphql/test/tester/GraphQLTesterTests.java +++ b/spring-graphql-test/src/test/java/org/springframework/graphql/test/tester/GraphQLTesterTests.java @@ -40,7 +40,7 @@ import reactor.core.publisher.Mono; import org.springframework.core.ParameterizedTypeReference; -import org.springframework.graphql.web.WebGraphQLService; +import org.springframework.graphql.web.WebGraphQLHandler; import org.springframework.graphql.web.WebInput; import org.springframework.graphql.web.WebOutput; import org.springframework.http.HttpHeaders; @@ -58,7 +58,7 @@ * Tests for {@link GraphQLTester} parameterized to: *
    *
  • Connect to {@link MockWebServer} and return a preset HTTP response. - *
  • Use mock {@link WebGraphQLService} to return a preset {@link ExecutionResult}. + *
  • Use mock {@link WebGraphQLHandler} to return a preset {@link ExecutionResult}. *
* *

There is no actual handling via {@link graphql.GraphQL} in either scenario. @@ -71,7 +71,7 @@ public class GraphQLTesterTests { public static Stream argumentSource() { - return Stream.of(new MockWebServerSetup(), new MockWebGraphQLServiceSetup()); + return Stream.of(new MockWebServerSetup(), new MockWebGraphQLHandlerSetup()); } @@ -393,16 +393,16 @@ public void shutdown() throws Exception { } - private static class MockWebGraphQLServiceSetup implements GraphQLTesterSetup { + private static class MockWebGraphQLHandlerSetup implements GraphQLTesterSetup { - private final WebGraphQLService service = mock(WebGraphQLService.class); + private final WebGraphQLHandler handler = mock(WebGraphQLHandler.class); private final ArgumentCaptor bodyCaptor = ArgumentCaptor.forClass(WebInput.class); private final GraphQLTester graphQLTester; - public MockWebGraphQLServiceSetup() { - this.graphQLTester = GraphQLTester.create(this.service); + public MockWebGraphQLHandlerSetup() { + this.graphQLTester = GraphQLTester.create(this.handler); } @Override @@ -421,7 +421,7 @@ public void response(@Nullable String data, List errors) throws Ex } ExecutionResult result = builder.build(); WebOutput output = new WebOutput(mock(WebInput.class), result); - when(this.service.execute(this.bodyCaptor.capture())).thenReturn(Mono.just(output)); + when(this.handler.handle(this.bodyCaptor.capture())).thenReturn(Mono.just(output)); } @Override diff --git a/spring-graphql/build.gradle b/spring-graphql/build.gradle index 6f64479..0c55fca 100644 --- a/spring-graphql/build.gradle +++ b/spring-graphql/build.gradle @@ -43,6 +43,9 @@ dependencies { testImplementation 'org.springframework:spring-test' testImplementation 'javax.servlet:javax.servlet-api:4.0.1' testImplementation 'com.fasterxml.jackson.core:jackson-databind' + + testRuntime 'org.apache.logging.log4j:log4j-core:2.14.1' + testRuntime 'org.apache.logging.log4j:log4j-slf4j-impl:2.14.1' } test { diff --git a/spring-graphql/src/main/java/org/springframework/graphql/GraphQLService.java b/spring-graphql/src/main/java/org/springframework/graphql/GraphQLService.java index 5c3ef29..fae8401 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/GraphQLService.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/GraphQLService.java @@ -15,23 +15,21 @@ */ package org.springframework.graphql; +import graphql.ExecutionInput; import graphql.ExecutionResult; import reactor.core.publisher.Mono; /** - * Contract to execute a GraphQL request. - * - * @param container for the GraphQL query along additional transport - * related context such as the HTTP url or headers for web. - * @param the query execution result + * Strategy to perform GraphQL query execution with input for and output from + * the invocation of {@link graphql.GraphQL}. */ -public interface GraphQLService { +public interface GraphQLService { /** - * Perform the request and return the result. - * @param input the GraphQL query container + * Perform the query and return the result. + * @param input the input for query execution via {@link graphql.GraphQL} * @return the execution result */ - Mono execute(IN input); + Mono execute(ExecutionInput input); } diff --git a/spring-graphql/src/main/java/org/springframework/graphql/RequestInput.java b/spring-graphql/src/main/java/org/springframework/graphql/RequestInput.java index e23c81e..cbe32e8 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/RequestInput.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/RequestInput.java @@ -15,9 +15,12 @@ */ package org.springframework.graphql; +import java.util.ArrayList; import java.util.Collections; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; +import java.util.function.BiFunction; import graphql.ExecutionInput; @@ -37,6 +40,8 @@ public class RequestInput { private final Map variables; + private final List> executionInputConfigurers = new ArrayList<>(); + public RequestInput(String query, @Nullable String operationName, @Nullable Map vars) { Assert.notNull(query, "'query' is required"); @@ -80,24 +85,44 @@ public Map getVariables() { return this.variables; } + /** + * Provide a consumer to configure the {@link ExecutionInput} used for input + * to {@link graphql.GraphQL#executeAsync(ExecutionInput)}. + * The builder is initially populated with the values from + * {@link #getQuery()}, {@link #getOperationName()}, and {@link #getVariables()}. + * @param configurer a {@code BiFunction} with the current + * {@code ExecutionInput} and a builder to modify it. + */ + public void configureExecutionInput(BiFunction configurer) { + this.executionInputConfigurers.add(configurer); + } /** - * Create an {@link ExecutionInput} initialized with the {@link #getQuery()}, - * {@link #getOperationName()}, and {@link #getVariables()}. + * Create the {@link ExecutionInput} for query execution. This is initially + * populated from {@link #getQuery()}, {@link #getOperationName()}, and + * {@link #getVariables()}, and is then further customized through + * {@link #configureExecutionInput(BiFunction)}. */ public ExecutionInput toExecutionInput() { - return ExecutionInput.newExecutionInput() - .query(getQuery()) - .operationName(getOperationName()) - .variables(getVariables()) + ExecutionInput executionInput = ExecutionInput.newExecutionInput() + .query(this.query) + .operationName(this.operationName) + .variables(this.variables) .build(); + + for (BiFunction configurer : this.executionInputConfigurers) { + ExecutionInput current = executionInput; + executionInput = executionInput.transform(builder -> configurer.apply(current, builder)); + } + + return executionInput; } /** * Return a Map representation of the request input. */ public Map toMap() { - Map map = new LinkedHashMap<>(); + Map map = new LinkedHashMap<>(3); map.put("query", getQuery()); if (getOperationName() != null) { map.put("operationName", getOperationName()); diff --git a/spring-graphql/src/main/java/org/springframework/graphql/web/DefaultWebGraphQLService.java b/spring-graphql/src/main/java/org/springframework/graphql/support/ExecutionGraphQLService.java similarity index 54% rename from spring-graphql/src/main/java/org/springframework/graphql/web/DefaultWebGraphQLService.java rename to spring-graphql/src/main/java/org/springframework/graphql/support/ExecutionGraphQLService.java index c7d532b..ab4a38e 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/web/DefaultWebGraphQLService.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/support/ExecutionGraphQLService.java @@ -13,33 +13,36 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.graphql.web; - -import java.util.concurrent.CompletableFuture; +package org.springframework.graphql.support; import graphql.ExecutionInput; import graphql.ExecutionResult; import graphql.GraphQL; +import reactor.core.publisher.Mono; -import org.springframework.graphql.support.GraphQLSource; +import org.springframework.graphql.GraphQLService; /** - * Extension of {@link AbstractWebGraphQLService} that executes GraphQL queries - * through the {@link GraphQL} instance it is configured with. + * Implementation of {@link GraphQLService} that performs GraphQL query execution + * through {@link GraphQL#executeAsync(ExecutionInput)}. */ -public class DefaultWebGraphQLService extends AbstractWebGraphQLService { +public class ExecutionGraphQLService implements GraphQLService { private final GraphQLSource graphQLSource; - public DefaultWebGraphQLService(GraphQLSource graphQLSource) { + public ExecutionGraphQLService(GraphQLSource graphQLSource) { this.graphQLSource = graphQLSource; } @Override - protected CompletableFuture executeInternal(ExecutionInput input) { - return this.graphQLSource.graphQL().executeAsync(input); + public Mono execute(ExecutionInput input) { + GraphQL graphQL = this.graphQLSource.graphQL(); + return Mono.deferContextual(contextView -> { + ReactorDataFetcherAdapter.addReactorContext(input, contextView); + return Mono.fromFuture(graphQL.executeAsync(input)); + }); } } diff --git a/spring-graphql/src/main/java/org/springframework/graphql/support/ReactorDataFetcherAdapter.java b/spring-graphql/src/main/java/org/springframework/graphql/support/ReactorDataFetcherAdapter.java index b985265..5da3a79 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/support/ReactorDataFetcherAdapter.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/support/ReactorDataFetcherAdapter.java @@ -39,13 +39,12 @@ import org.springframework.util.ClassUtils; /** - * Adapter to wrap a registered {@link DataFetcher} and enable it to return + * Adapter that can wrap a registered {@link DataFetcher} and enable it to return * {@link Flux} or {@link Mono}, also adding Reactor Context passed through - * the {@link ExecutionInput} via {@link #addReactorContext(ExecutionInput, ContextView)}. - * Use {@link #TYPE_VISITOR} to transform the - * {@link graphql.schema.GraphQLSchema} and apply the adapter. + * the {@link ExecutionInput}. Also exposes a {@link #TYPE_VISITOR} to apply + * the adapter. */ -public class ReactorDataFetcherAdapter implements DataFetcher { +class ReactorDataFetcherAdapter implements DataFetcher { private static final String REACTOR_CONTEXT_KEY = ReactorDataFetcherAdapter.class.getName() + ".REACTOR_CONTEXT"; @@ -108,7 +107,7 @@ public static void addReactorContext(ExecutionInput executionInput, ContextView * {@link GraphQLTypeVisitor} that wraps non-GraphQL data fetchers and * adapts them if they return {@link Flux} or {@link Mono}. */ - public static GraphQLTypeVisitor TYPE_VISITOR = new GraphQLTypeVisitorStub() { + static GraphQLTypeVisitor TYPE_VISITOR = new GraphQLTypeVisitorStub() { @Override public TraversalControl visitGraphQLFieldDefinition( diff --git a/spring-graphql/src/main/java/org/springframework/graphql/web/AbstractWebGraphQLService.java b/spring-graphql/src/main/java/org/springframework/graphql/web/AbstractWebGraphQLService.java deleted file mode 100644 index 8f22a05..0000000 --- a/spring-graphql/src/main/java/org/springframework/graphql/web/AbstractWebGraphQLService.java +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Copyright 2002-2021 the original author or 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. - */ -package org.springframework.graphql.web; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.CompletableFuture; - -import graphql.ExecutionInput; -import graphql.ExecutionResult; -import reactor.core.publisher.Mono; - -import org.springframework.graphql.support.ReactorDataFetcherAdapter; - -/** - * Base class for {@link WebGraphQLService} implementations, providing support - * for customizations of the request through a {@link WebInterceptor} chain. - * Sub-classes must implement {@link #executeInternal(ExecutionInput)} to - * actually perform the GraphQL query. - */ -public abstract class AbstractWebGraphQLService implements WebGraphQLService { - - private final List interceptors = new ArrayList<>(); - - - /** - * Set the interceptors to invoke to handle request. - * @param interceptors the interceptors to use - */ - public void setInterceptors(List interceptors) { - this.interceptors.clear(); - this.interceptors.addAll(interceptors); - } - - /** - * Return the {@link #setInterceptors(List) configured} interceptors. - */ - public List getInterceptors() { - return this.interceptors; - } - - - @Override - public final Mono execute(WebInput input) { - return preHandle(input) - .flatMap(executionInput -> Mono.fromFuture(executeInternal(executionInput))) - .flatMap(executionResult -> postHandle(new WebOutput(input, executionResult))); - } - - private Mono preHandle(WebInput input) { - Mono resultMono = Mono.deferContextual(contextView -> { - ExecutionInput executionInput = input.toExecutionInput(); - ReactorDataFetcherAdapter.addReactorContext(executionInput, contextView); - return Mono.just(executionInput); - }); - for (WebInterceptor interceptor : this.interceptors) { - resultMono = resultMono.flatMap(executionInput -> interceptor.preHandle(executionInput, input)); - } - return resultMono; - } - - private Mono postHandle(WebOutput output) { - Mono outputMono = Mono.just(output); - for (int i = this.interceptors.size() - 1 ; i >= 0; i--) { - WebInterceptor interceptor = this.interceptors.get(i); - outputMono = outputMono.flatMap(interceptor::postHandle); - } - return outputMono; - } - - /** - * Sub-classes must implement this method to actually handle the request. - * @param input the input to invoke {@link graphql.GraphQL} with - * @return the result from handling - */ - protected abstract CompletableFuture executeInternal(ExecutionInput input); - -} diff --git a/spring-graphql/src/main/java/org/springframework/graphql/web/WebGraphQLService.java b/spring-graphql/src/main/java/org/springframework/graphql/web/WebGraphQLHandler.java similarity index 59% rename from spring-graphql/src/main/java/org/springframework/graphql/web/WebGraphQLService.java rename to spring-graphql/src/main/java/org/springframework/graphql/web/WebGraphQLHandler.java index e467b22..fea98c2 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/web/WebGraphQLService.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/web/WebGraphQLHandler.java @@ -15,12 +15,26 @@ */ package org.springframework.graphql.web; +import java.util.List; + +import reactor.core.publisher.Mono; + import org.springframework.graphql.GraphQLService; /** - * {@link GraphQLService} for executing GraphQL requests in a Web environment, - * over HTTP or WebSocket. + * Contract to handle a GraphQL over HTTP or WebSocket request that forms the + * basis of a {@link WebInterceptor} delegation chain. + * + * @see WebInterceptor#createHandler(List, GraphQLService) */ -public interface WebGraphQLService extends GraphQLService { +public interface WebGraphQLHandler { + + /** + * Perform query execution for the given request and return the result. + * + * @param input the GraphQL query container + * @return the execution result + */ + Mono handle(WebInput input); } diff --git a/spring-graphql/src/main/java/org/springframework/graphql/web/WebInterceptor.java b/spring-graphql/src/main/java/org/springframework/graphql/web/WebInterceptor.java index c6ba41f..aeb0c71 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/web/WebInterceptor.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/web/WebInterceptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2020 the original author or authors. + * Copyright 2020-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,56 +15,81 @@ */ package org.springframework.graphql.web; -import java.util.function.Consumer; +import java.util.List; import graphql.ExecutionInput; import graphql.ExecutionResult; import reactor.core.publisher.Mono; -import org.springframework.graphql.web.webmvc.GraphQLHttpHandler; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.graphql.GraphQLService; +import org.springframework.util.Assert; /** - * Web interceptor for GraphQL queries over HTTP. The interceptor allows - * customization of the {@link ExecutionInput} for the query as well as the - * {@link ExecutionResult} of the query and is supported for both Spring MVC and - * Spring WebFlux. + * Interceptor for intercepting GraphQL over HTTP or WebSocket requests. + * Provides information about the HTTP request or WebSocket handshake, allows + * customization of the {@link ExecutionInput} and of the {@link ExecutionResult} + * from query execution. * - *

A list of interceptors may be provided to {@link GraphQLHttpHandler} or - * to {@link org.springframework.graphql.web.webflux.GraphQLHttpHandler}. Interceptors are executed in that provided - * order where each interceptor sees the {@code ExecutionInput} or the - * {@code ExecutionResult} that was customized by the previous interceptor. + *

Interceptors may be declared as beans in Spring configuration and ordered + * as defined in {@link ObjectProvider#orderedStream()}. + * + *

Supported for Spring MVC and WebFlux. */ public interface WebInterceptor { /** - * Intercept a GraphQL over HTTP request before the query is executed. - * - *

{@code ExecutionInput} is initially populated with the input from the - * request body via {@link WebInput#toExecutionInput()} where the - * {@link WebInput#getQuery() query} is guaranteed to be a non-empty String. - * Interceptors are then executed in order to further customize the input - * and or perform other actions or checks. + * Intercept a request and delegate for further handling and query execution + * via {@link WebGraphQLHandler#handle(WebInput)}. * - * @param executionInput the input to use, initialized from {@code WebInput} - * @param webInput the input from the HTTP request - * @return the same instance or a new one via {@link ExecutionInput#transform(Consumer)} + * @param webInput container with HTTP request information and options to + * customize the {@link ExecutionInput}. + * @param next the handler to delegate to for query execution + * @return a {@link Mono} with the result + */ + Mono intercept(WebInput webInput, WebGraphQLHandler next); + + /** + * Return a composed {@link WebInterceptor} that invokes the current + * interceptor first one and then the one one passed in. */ - default Mono preHandle(ExecutionInput executionInput, WebInput webInput) { - return Mono.just(executionInput); + default WebInterceptor andThen(WebInterceptor interceptor) { + Assert.notNull(interceptor, "WebInterceptor must not be null"); + return (currentInput, next) -> intercept(currentInput, nextInput -> interceptor.intercept(nextInput, next)); } /** - * Intercept a GraphQL over HTTP request after the query is executed. - * - *

{@code WebOutput} initially wraps the {@link ExecutionResult} returned - * from the execution of the query. Interceptors are then executed in order - * to further customize it and/or perform other actions. - * - * @param webOutput the execution result - * @return the same instance or a new one via {@link WebOutput#transform(Consumer)} + * Return {@link WebGraphQLHandler} that invokes the current interceptor + * first and then the given {@link GraphQLService} for actual execution of + * the GraphQL query. + */ + default WebGraphQLHandler apply(GraphQLService service) { + Assert.notNull(service, "GraphQLService must not be null"); + return currentInput -> intercept(currentInput, createHandler(service)); + } + + + /** + * Factory method for a {@link WebGraphQLHandler} with a chain of + * interceptors followed by a {@link GraphQLService} at the end. + */ + static WebGraphQLHandler createHandler(List interceptors, GraphQLService service) { + return interceptors.stream() + .reduce(WebInterceptor::andThen) + .map(interceptor -> interceptor.apply(service)) + .orElse(createHandler(service)); + } + + /** + * Factory method for a {@link WebGraphQLHandler} that simple invokes the + * given {@link GraphQLService} adapting to its input and output. */ - default Mono postHandle(WebOutput webOutput) { - return Mono.just(webOutput); + static WebGraphQLHandler createHandler(GraphQLService graphQLService) { + Assert.notNull(graphQLService, "GraphQLService must not be null"); + return webInput -> { + ExecutionInput executionInput = webInput.toExecutionInput(); + return graphQLService.execute(executionInput).map(result -> new WebOutput(webInput, result)); + }; } } \ No newline at end of file diff --git a/spring-graphql/src/main/java/org/springframework/graphql/web/webflux/GraphQLHttpHandler.java b/spring-graphql/src/main/java/org/springframework/graphql/web/webflux/GraphQLHttpHandler.java index ed8dfd2..1144097 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/web/webflux/GraphQLHttpHandler.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/web/webflux/GraphQLHttpHandler.java @@ -22,7 +22,7 @@ import reactor.core.publisher.Mono; import org.springframework.core.ParameterizedTypeReference; -import org.springframework.graphql.web.WebGraphQLService; +import org.springframework.graphql.web.WebGraphQLHandler; import org.springframework.graphql.web.WebInput; import org.springframework.util.Assert; import org.springframework.web.reactive.function.server.ServerRequest; @@ -39,16 +39,16 @@ public class GraphQLHttpHandler { new ParameterizedTypeReference>() {}; - private final WebGraphQLService graphQLService; + private final WebGraphQLHandler graphQLHandler; /** * Create a new instance. - * @param service for GraphQL query execution + * @param graphQLHandler common handler for GraphQL over HTTP requests */ - public GraphQLHttpHandler(WebGraphQLService service) { - Assert.notNull(service, "WebGraphQLService is required"); - this.graphQLService = service; + public GraphQLHttpHandler(WebGraphQLHandler graphQLHandler) { + Assert.notNull(graphQLHandler, "WebGraphQLHandler is required"); + this.graphQLHandler = graphQLHandler; } @@ -63,7 +63,7 @@ public Mono handleQuery(ServerRequest request) { if (logger.isDebugEnabled()) { logger.debug("Executing: " + input); } - return this.graphQLService.execute(input); + return this.graphQLHandler.handle(input); }) .flatMap(output -> { Map spec = output.toSpecification(); diff --git a/spring-graphql/src/main/java/org/springframework/graphql/web/webflux/GraphQLWebSocketHandler.java b/spring-graphql/src/main/java/org/springframework/graphql/web/webflux/GraphQLWebSocketHandler.java index d46c65d..4fa7aa7 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/web/webflux/GraphQLWebSocketHandler.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/web/webflux/GraphQLWebSocketHandler.java @@ -39,7 +39,7 @@ import org.springframework.core.codec.Encoder; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferUtils; -import org.springframework.graphql.web.WebGraphQLService; +import org.springframework.graphql.web.WebGraphQLHandler; import org.springframework.graphql.web.WebInput; import org.springframework.graphql.web.WebOutput; import org.springframework.http.MediaType; @@ -72,7 +72,7 @@ public class GraphQLWebSocketHandler implements WebSocketHandler { ResolvableType.forType(new ParameterizedTypeReference>() {}); - private final WebGraphQLService graphQLService; + private final WebGraphQLHandler graphQLHandler; private final Decoder decoder; @@ -83,16 +83,16 @@ public class GraphQLWebSocketHandler implements WebSocketHandler { /** * Create a new instance. - * @param service for GraphQL query execution + * @param graphQLHandler common handler for GraphQL over HTTP requests * @param configurer codec configurer for JSON encoding and decoding * @param connectionInitTimeout the time within which the {@code CONNECTION_INIT} * type message must be received. */ public GraphQLWebSocketHandler( - WebGraphQLService service, ServerCodecConfigurer configurer, Duration connectionInitTimeout) { + WebGraphQLHandler graphQLHandler, ServerCodecConfigurer configurer, Duration connectionInitTimeout) { - Assert.notNull(service, "WebGraphQLService is required"); - this.graphQLService = service; + Assert.notNull(graphQLHandler, "WebGraphQLHandler is required"); + this.graphQLHandler = graphQLHandler; this.decoder = initDecoder(configurer); this.encoder = initEncoder(configurer); this.initTimeoutDuration = connectionInitTimeout; @@ -164,7 +164,7 @@ public Mono handle(WebSocketSession session) { if (logger.isDebugEnabled()) { logger.debug("Executing: " + input); } - return this.graphQLService.execute(input) + return this.graphQLHandler.handle(input) .flatMapMany(output -> handleWebOutput(session, id, subscriptions, output)) .doOnTerminate(() -> subscriptions.remove(id)); case COMPLETE: diff --git a/spring-graphql/src/main/java/org/springframework/graphql/web/webmvc/GraphQLHttpHandler.java b/spring-graphql/src/main/java/org/springframework/graphql/web/webmvc/GraphQLHttpHandler.java index 36ae034..3daa183 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/web/webmvc/GraphQLHttpHandler.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/web/webmvc/GraphQLHttpHandler.java @@ -25,7 +25,7 @@ import reactor.core.publisher.Mono; import org.springframework.core.ParameterizedTypeReference; -import org.springframework.graphql.web.WebGraphQLService; +import org.springframework.graphql.web.WebGraphQLHandler; import org.springframework.graphql.web.WebInput; import org.springframework.util.Assert; import org.springframework.web.HttpMediaTypeNotSupportedException; @@ -45,16 +45,16 @@ public class GraphQLHttpHandler { new ParameterizedTypeReference>() {}; - private final WebGraphQLService graphQLService; + private final WebGraphQLHandler graphQLHandler; /** * Create a new instance. - * @param service for GraphQL query execution + * @param graphQLHandler common handler for GraphQL over HTTP requests */ - public GraphQLHttpHandler(WebGraphQLService service) { - Assert.notNull(service, "WebGraphQLService is required"); - this.graphQLService = service; + public GraphQLHttpHandler(WebGraphQLHandler graphQLHandler) { + Assert.notNull(graphQLHandler, "WebGraphQLHandler is required"); + this.graphQLHandler = graphQLHandler; } @@ -69,7 +69,7 @@ public ServerResponse handle(ServerRequest request) throws ServletException { if (logger.isDebugEnabled()) { logger.debug("Executing: " + input); } - Mono responseMono = this.graphQLService.execute(input) + Mono responseMono = this.graphQLHandler.handle(input) .map(output -> { if (logger.isDebugEnabled()) { logger.debug("Execution complete"); diff --git a/spring-graphql/src/main/java/org/springframework/graphql/web/webmvc/GraphQLWebSocketHandler.java b/spring-graphql/src/main/java/org/springframework/graphql/web/webmvc/GraphQLWebSocketHandler.java index b3686b7..f539f5f 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/web/webmvc/GraphQLWebSocketHandler.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/web/webmvc/GraphQLWebSocketHandler.java @@ -41,7 +41,7 @@ import reactor.core.scheduler.Scheduler; import reactor.core.scheduler.Schedulers; -import org.springframework.graphql.web.WebGraphQLService; +import org.springframework.graphql.web.WebGraphQLHandler; import org.springframework.graphql.web.WebInput; import org.springframework.graphql.web.WebOutput; import org.springframework.http.HttpHeaders; @@ -71,7 +71,7 @@ public class GraphQLWebSocketHandler extends TextWebSocketHandler implements Sub Arrays.asList("graphql-transport-ws", "subscriptions-transport-ws"); - private final WebGraphQLService service; + private final WebGraphQLHandler graphQLHandler; private final Duration initTimeoutDuration; @@ -82,17 +82,17 @@ public class GraphQLWebSocketHandler extends TextWebSocketHandler implements Sub /** * Create a new instance. - * @param service for GraphQL query execution + * @param graphQLHandler common handler for GraphQL over HTTP requests * @param converter for JSON encoding and decoding * @param connectionInitTimeout the time within which the {@code CONNECTION_INIT} * type message must be received. */ public GraphQLWebSocketHandler( - WebGraphQLService service, HttpMessageConverter converter, Duration connectionInitTimeout) { + WebGraphQLHandler graphQLHandler, HttpMessageConverter converter, Duration connectionInitTimeout) { - Assert.notNull(service, "WebGraphQLService is required"); + Assert.notNull(graphQLHandler, "WebGraphQLHandler is required"); Assert.notNull(converter, "HttpMessageConverter for JSON is required"); - this.service = service; + this.graphQLHandler = graphQLHandler; this.initTimeoutDuration = connectionInitTimeout; this.converter = converter; } @@ -155,7 +155,7 @@ protected void handleTextMessage(WebSocketSession session, TextMessage message) if (logger.isDebugEnabled()) { logger.debug("Executing: " + input); } - this.service.execute(input) + this.graphQLHandler.handle(input) .flatMapMany(output -> handleWebOutput(session, input.getId(), output)) .publishOn(sessionState.getScheduler()) // Serial blocking send via single thread .subscribe(new SendMessageSubscriber(id, session, sessionState)); diff --git a/spring-graphql/src/test/java/org/springframework/graphql/support/ReactorDataFetcherAdapterTests.java b/spring-graphql/src/test/java/org/springframework/graphql/support/ReactorDataFetcherAdapterTests.java index eaf2f9d..f5b4e71 100644 --- a/spring-graphql/src/test/java/org/springframework/graphql/support/ReactorDataFetcherAdapterTests.java +++ b/spring-graphql/src/test/java/org/springframework/graphql/support/ReactorDataFetcherAdapterTests.java @@ -15,9 +15,9 @@ */ package org.springframework.graphql.support; -import java.io.IOException; import java.nio.charset.StandardCharsets; import java.time.Duration; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.function.Consumer; @@ -25,12 +25,7 @@ import graphql.ExecutionInput; import graphql.ExecutionResult; import graphql.GraphQL; -import graphql.schema.GraphQLSchema; -import graphql.schema.SchemaTransformer; import graphql.schema.idl.RuntimeWiring; -import graphql.schema.idl.SchemaGenerator; -import graphql.schema.idl.SchemaParser; -import graphql.schema.idl.TypeDefinitionRegistry; import org.junit.jupiter.api.Test; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; @@ -38,7 +33,6 @@ import reactor.util.context.Context; import org.springframework.core.io.ByteArrayResource; -import org.springframework.core.io.Resource; import static org.assertj.core.api.Assertions.assertThat; @@ -109,16 +103,16 @@ void fluxDataFetcherSubscription() throws Exception { } - private GraphQL initGraphQL(String schemaValue, Consumer consumer) throws IOException { - Resource schemaResource = new ByteArrayResource(schemaValue.getBytes(StandardCharsets.UTF_8)); - TypeDefinitionRegistry typeRegistry = new SchemaParser().parse(schemaResource.getInputStream()); - + private GraphQL initGraphQL(String schemaValue, Consumer consumer) { RuntimeWiring.Builder wiringBuilder = RuntimeWiring.newRuntimeWiring(); consumer.accept(wiringBuilder); - GraphQLSchema schema = new SchemaGenerator().makeExecutableSchema(typeRegistry, wiringBuilder.build()); - schema = SchemaTransformer.transformSchema(schema, ReactorDataFetcherAdapter.TYPE_VISITOR); - return GraphQL.newGraphQL(schema).build(); + return GraphQLSource.builder() + .schemaResource(new ByteArrayResource(schemaValue.getBytes(StandardCharsets.UTF_8))) + .runtimeWiring(wiringBuilder.build()) + .typeVisitors(Collections.singletonList(ReactorDataFetcherAdapter.TYPE_VISITOR)) + .build() + .graphQL(); } private ExecutionInput initExecutionInput(String query, Context reactorContext) { diff --git a/spring-graphql/src/test/java/org/springframework/graphql/web/ConsumeOneAndNeverCompleteInterceptor.java b/spring-graphql/src/test/java/org/springframework/graphql/web/ConsumeOneAndNeverCompleteInterceptor.java index ccd0a7f..cdc5054 100644 --- a/spring-graphql/src/test/java/org/springframework/graphql/web/ConsumeOneAndNeverCompleteInterceptor.java +++ b/spring-graphql/src/test/java/org/springframework/graphql/web/ConsumeOneAndNeverCompleteInterceptor.java @@ -19,19 +19,17 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import org.springframework.graphql.web.WebInterceptor; -import org.springframework.graphql.web.WebOutput; - import static org.assertj.core.api.Assertions.assertThat; public class ConsumeOneAndNeverCompleteInterceptor implements WebInterceptor { @Override - public Mono postHandle(WebOutput output) { - return Mono.just(output.transform(builder -> { - Publisher publisher = output.getData(); - assertThat(publisher).isNotNull(); - builder.data(Flux.from(publisher).take(1).concatWith(Flux.never())); - })); + public Mono intercept(WebInput webInput, WebGraphQLHandler next) { + return next.handle(webInput).map(output -> + output.transform(builder -> { + Publisher publisher = output.getData(); + assertThat(publisher).isNotNull(); + builder.data(Flux.from(publisher).take(1).concatWith(Flux.never())); + })); } } diff --git a/spring-graphql/src/test/java/org/springframework/graphql/web/DefaultWebGraphQLServiceTests.java b/spring-graphql/src/test/java/org/springframework/graphql/web/DefaultWebGraphQLServiceTests.java deleted file mode 100644 index d197f0e..0000000 --- a/spring-graphql/src/test/java/org/springframework/graphql/web/DefaultWebGraphQLServiceTests.java +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Copyright 2020-2021 the original author or 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. - */ -package org.springframework.graphql.web; - -import java.net.URI; -import java.time.Duration; -import java.util.Arrays; -import java.util.List; -import java.util.Map; - -import com.fasterxml.jackson.databind.ObjectMapper; -import graphql.ExecutionInput; -import graphql.schema.idl.RuntimeWiring; -import org.junit.jupiter.api.Test; -import reactor.core.publisher.Mono; - -import org.springframework.core.io.ClassPathResource; -import org.springframework.graphql.support.GraphQLSource; -import org.springframework.http.HttpHeaders; - -import static graphql.schema.idl.TypeRuntimeWiring.newTypeWiring; -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Unit tests for {@link DefaultWebGraphQLService}. - */ -public class DefaultWebGraphQLServiceTests { - - @Test - void testInterceptorInvocation() throws Exception { - - StringBuilder sb = new StringBuilder(); - List interceptors = Arrays.asList( - new TestWebInterceptor(sb, 1), new TestWebInterceptor(sb, 2), new TestWebInterceptor(sb, 3)); - - String query = "{" + - " bookById(id: \\\"book-1\\\"){ " + - " id" + - " name" + - " pageCount" + - " author" + - " }" + - "}"; - - ObjectMapper mapper = new ObjectMapper(); - Map body = mapper.reader().readValue("{\"query\": \"" + query + "\"}", Map.class); - WebInput webInput = new WebInput(URI.create("/graphql"), new HttpHeaders(), body, "1"); - - DefaultWebGraphQLService requestHandler = new DefaultWebGraphQLService(createGraphQLSource()); - requestHandler.setInterceptors(interceptors); - - WebOutput webOutput = requestHandler.execute(webInput).block(); - - assertThat(sb.toString()).isEqualTo(":pre1:pre2:pre3:post3:post2:post1"); - assertThat(webOutput.isDataPresent()).isTrue(); - assertThat(webOutput.getResponseHeaders().get("MyHeader")).containsExactly("MyValue3", "MyValue2", "MyValue1"); - } - - - private static GraphQLSource createGraphQLSource() { - RuntimeWiring runtimeWiring = RuntimeWiring.newRuntimeWiring() - .type(newTypeWiring("Query").dataFetcher("bookById", GraphQLDataFetchers.getBookByIdDataFetcher())) - .build(); - - return GraphQLSource.builder() - .schemaResource(new ClassPathResource("books/schema.graphqls")) - .runtimeWiring(runtimeWiring) - .build(); - } - - - private static class TestWebInterceptor implements WebInterceptor { - - private final StringBuilder output; - - private final int index; - - public TestWebInterceptor(StringBuilder output, int index) { - this.output = output; - this.index = index; - } - - @Override - public Mono preHandle(ExecutionInput executionInput, WebInput webInput) { - this.output.append(":pre").append(this.index); - return Mono.delay(Duration.ofMillis(50)).map(aLong -> executionInput); - } - - @Override - public Mono postHandle(WebOutput output) { - this.output.append(":post").append(this.index); - return Mono.delay(Duration.ofMillis(50)) - .map(aLong -> output.transform(builder -> - builder.responseHeader("myHeader", "MyValue" + this.index))); - } - } - -} diff --git a/spring-graphql/src/test/java/org/springframework/graphql/web/WebInterceptorTests.java b/spring-graphql/src/test/java/org/springframework/graphql/web/WebInterceptorTests.java new file mode 100644 index 0000000..bdd8e60 --- /dev/null +++ b/spring-graphql/src/test/java/org/springframework/graphql/web/WebInterceptorTests.java @@ -0,0 +1,109 @@ +/* + * Copyright 2002-2021 the original author or 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. + */ +package org.springframework.graphql.web; + +import java.net.URI; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.fasterxml.jackson.databind.ObjectMapper; +import graphql.ExecutionInput; +import graphql.ExecutionResult; +import graphql.ExecutionResultImpl; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Schedulers; + +import org.springframework.graphql.GraphQLService; +import org.springframework.http.HttpHeaders; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Unit tests for a {@link WebInterceptor} chain. + */ +public class WebInterceptorTests { + + @Test + void interceptorChain() { + StringBuilder sb = new StringBuilder(); + List interceptors = Arrays.asList( + new TestWebInterceptor(sb, 1), new TestWebInterceptor(sb, 2), new TestWebInterceptor(sb, 3)); + + TestGraphQLService service = new TestGraphQLService(); + + WebInput input = new WebInput( + URI.create("/"), new HttpHeaders(), Collections.singletonMap("query", "any"), "1"); + + WebOutput output = WebInterceptor.createHandler(interceptors, service).handle(input).block(); + + assertThat(sb.toString()).isEqualTo(":pre1:pre2:pre3:post3:post2:post1"); + assertThat(output.getResponseHeaders().get("name")).containsExactly("value3", "value2", "value1"); + assertThat(service.getSavedInput().getExtensions()).containsOnlyKeys("eKey1", "eKey2", "eKey3"); + } + + + private static class TestGraphQLService implements GraphQLService { + + private ExecutionInput savedInput; + + public ExecutionInput getSavedInput() { + return this.savedInput; + } + + @Override + public Mono execute(ExecutionInput input) { + this.savedInput = input; + return Mono.just(ExecutionResultImpl.newExecutionResult().build()); + } + } + + + private static class TestWebInterceptor implements WebInterceptor { + + private final StringBuilder output; + + private final int index; + + public TestWebInterceptor(StringBuilder output, int index) { + this.output = output; + this.index = index; + } + + @Override + public Mono intercept(WebInput webInput, WebGraphQLHandler next) { + + this.output.append(":pre").append(this.index); + + webInput.configureExecutionInput((executionInput, builder) -> { + Map extensions = new HashMap<>(executionInput.getExtensions()); + extensions.put("eKey" + this.index, "eValue" + this.index); + return builder.extensions(extensions).build(); + }); + + return next.handle(webInput) + .map(output -> { + this.output.append(":post").append(this.index); + return output.transform(builder -> builder.responseHeader("name", "value" + this.index)); + }) + .subscribeOn(Schedulers.boundedElastic()); + } + } + +} diff --git a/spring-graphql/src/test/java/org/springframework/graphql/web/webflux/GraphQLWebSocketHandlerTests.java b/spring-graphql/src/test/java/org/springframework/graphql/web/webflux/GraphQLWebSocketHandlerTests.java index 40ae6c4..d716651 100644 --- a/spring-graphql/src/test/java/org/springframework/graphql/web/webflux/GraphQLWebSocketHandlerTests.java +++ b/spring-graphql/src/test/java/org/springframework/graphql/web/webflux/GraphQLWebSocketHandlerTests.java @@ -36,10 +36,11 @@ import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferUtils; import org.springframework.core.io.buffer.DefaultDataBufferFactory; +import org.springframework.graphql.support.ExecutionGraphQLService; import org.springframework.graphql.support.GraphQLSource; import org.springframework.graphql.web.ConsumeOneAndNeverCompleteInterceptor; -import org.springframework.graphql.web.DefaultWebGraphQLService; import org.springframework.graphql.web.GraphQLDataFetchers; +import org.springframework.graphql.web.WebGraphQLHandler; import org.springframework.graphql.web.WebInterceptor; import org.springframework.http.HttpHeaders; import org.springframework.http.codec.ServerCodecConfigurer; @@ -273,12 +274,11 @@ private GraphQLWebSocketHandler initWebSocketHandler() throws Exception { private GraphQLWebSocketHandler initWebSocketHandler( @Nullable List interceptors, @Nullable Duration initTimeoutDuration) throws Exception { - DefaultWebGraphQLService requestHandler = new DefaultWebGraphQLService(initGraphQLSource()); - if (interceptors != null) { - requestHandler.setInterceptors(interceptors); - } + WebGraphQLHandler graphQLHandler = WebInterceptor.createHandler( + (interceptors != null ? interceptors : Collections.emptyList()), + new ExecutionGraphQLService(initGraphQLSource())); - return new GraphQLWebSocketHandler(requestHandler, + return new GraphQLWebSocketHandler(graphQLHandler, ServerCodecConfigurer.create(), (initTimeoutDuration != null ? initTimeoutDuration : Duration.ofSeconds(60))); } diff --git a/spring-graphql/src/test/java/org/springframework/graphql/web/webmvc/GraphQLWebSocketHandlerTests.java b/spring-graphql/src/test/java/org/springframework/graphql/web/webmvc/GraphQLWebSocketHandlerTests.java index 316dc88..07db8d1 100644 --- a/spring-graphql/src/test/java/org/springframework/graphql/web/webmvc/GraphQLWebSocketHandlerTests.java +++ b/spring-graphql/src/test/java/org/springframework/graphql/web/webmvc/GraphQLWebSocketHandlerTests.java @@ -31,10 +31,11 @@ import reactor.test.StepVerifier; import org.springframework.core.io.ClassPathResource; +import org.springframework.graphql.support.ExecutionGraphQLService; import org.springframework.graphql.support.GraphQLSource; import org.springframework.graphql.web.ConsumeOneAndNeverCompleteInterceptor; -import org.springframework.graphql.web.DefaultWebGraphQLService; import org.springframework.graphql.web.GraphQLDataFetchers; +import org.springframework.graphql.web.WebGraphQLHandler; import org.springframework.graphql.web.WebInterceptor; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpInputMessage; @@ -257,12 +258,11 @@ private GraphQLWebSocketHandler initWebSocketHandler( @Nullable List interceptors, @Nullable Duration initTimeoutDuration) { try { - DefaultWebGraphQLService requestHandler = new DefaultWebGraphQLService(initGraphQLSource()); - if (interceptors != null) { - requestHandler.setInterceptors(interceptors); - } + WebGraphQLHandler graphQLHandler = WebInterceptor.createHandler( + (interceptors != null ? interceptors : Collections.emptyList()), + new ExecutionGraphQLService(initGraphQLSource())); - return new GraphQLWebSocketHandler(requestHandler, converter, + return new GraphQLWebSocketHandler(graphQLHandler, converter, (initTimeoutDuration != null ? initTimeoutDuration : Duration.ofSeconds(60))); } catch (Exception ex) {