WebInterceptor contract revision

WebInterceptor now uses delegation, forming a chain of interceptors
followed by a GraphQLService at the end to invoke graphql.GraphQL.

Closes gh-49
This commit is contained in:
Rossen Stoyanchev
2021-04-30 21:26:26 +01:00
parent e5a92a310d
commit f93c46eee3
29 changed files with 402 additions and 407 deletions

View File

@@ -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);
}
}

View File

@@ -34,9 +34,9 @@ import org.springframework.context.annotation.Bean;
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<WebInterceptor> interceptors) {
DefaultWebGraphQLService handler = new DefaultWebGraphQLService(graphQLSource);
handler.setInterceptors(interceptors.orderedStream().collect(Collectors.toList()));
return handler;
public WebGraphQLHandler webGraphQLHandler(ObjectProvider<WebInterceptor> 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 @@ public class WebFluxGraphQLAutoConfiguration {
@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;
}

View File

@@ -38,9 +38,9 @@ import org.springframework.context.annotation.Bean;
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<WebInterceptor> interceptors) {
DefaultWebGraphQLService handler = new DefaultWebGraphQLService(graphQLSource);
handler.setInterceptors(interceptors.orderedStream().collect(Collectors.toList()));
return handler;
public WebGraphQLHandler webGraphQLHandler(ObjectProvider<WebInterceptor> 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 @@ public class WebMvcGraphQLAutoConfiguration {
@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 class WebMvcGraphQLAutoConfiguration {
.orElseThrow(() -> new IllegalStateException("No JSON converter"));
return new GraphQLWebSocketHandler(
service, converter, properties.getWebsocket().getConnectionInitTimeout());
webGraphQLHandler, converter, properties.getWebsocket().getConnectionInitTimeout());
}
@Bean

View File

@@ -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

View File

@@ -19,7 +19,6 @@ import java.util.Collections;
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.ApplicationContext;
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 @@ class WebFluxApplicationContextTests {
@Bean
public WebInterceptor customWebInterceptor() {
return new WebInterceptor() {
@Override
public Mono<WebOutput> 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")));
}
}

View File

@@ -18,7 +18,6 @@ package org.springframework.graphql.boot;
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.boot.test.context.runner.WebApplicationContextRunner;
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 org.springframework.test.web.servlet.setup.MockMvcBuilders;
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 @@ class WebMvcApplicationContextTests {
@Bean
public WebInterceptor customWebInterceptor() {
return new WebInterceptor() {
@Override
public Mono<WebOutput> 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")));
}
}

View File

@@ -21,7 +21,7 @@ import org.junit.jupiter.api.Test;
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")));
}

View File

@@ -23,7 +23,7 @@ import reactor.test.StepVerifier;
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")));
}

View File

@@ -43,7 +43,7 @@ import reactor.core.publisher.Flux;
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 @@ class DefaultGraphQLTester implements GraphQLTester {
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 @@ class DefaultGraphQLTester implements GraphQLTester {
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;
}

View File

@@ -24,13 +24,14 @@ import graphql.GraphQLError;
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}.
*
*
* <p>GraphQL requests to Spring MVC without an HTTP server:
@@ -75,7 +76,7 @@ import org.springframework.test.web.reactive.server.WebTestClient;
* }
* </pre>
*
* <p>GraphQL requests to any {@link WebGraphQLService}:
* <p>GraphQL requests to any {@link WebGraphQLHandler}:
* <pre class="code">
* &#064;SpringBootTest
* public class MyTests {
@@ -83,8 +84,8 @@ import org.springframework.test.web.reactive.server.WebTestClient;
* private GraphQLTester graphQLTester;
*
* &#064;BeforeEach
* public void setUp(&#064;Autowired WebGraphQLService service) {
* this.graphQLTester = GraphQLTester.create(service);
* public void setUp(&#064;Autowired WebGraphQLHandler handler) {
* this.graphQLTester = GraphQLTester.create(handler);
* }
* </pre>
*/
@@ -112,13 +113,13 @@ public interface GraphQLTester {
}
/**
* 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);
}

View File

@@ -40,7 +40,7 @@ import org.mockito.ArgumentCaptor;
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 @@ import static org.mockito.Mockito.when;
* Tests for {@link GraphQLTester} parameterized to:
* <ul>
* <li>Connect to {@link MockWebServer} and return a preset HTTP response.
* <li>Use mock {@link WebGraphQLService} to return a preset {@link ExecutionResult}.
* <li>Use mock {@link WebGraphQLHandler} to return a preset {@link ExecutionResult}.
* </ul>
*
* <p>There is no actual handling via {@link graphql.GraphQL} in either scenario.
@@ -71,7 +71,7 @@ public class GraphQLTesterTests {
public static Stream<GraphQLTesterSetup> argumentSource() {
return Stream.of(new MockWebServerSetup(), new MockWebGraphQLServiceSetup());
return Stream.of(new MockWebServerSetup(), new MockWebGraphQLHandlerSetup());
}
@@ -393,16 +393,16 @@ public class GraphQLTesterTests {
}
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<WebInput> 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 class GraphQLTesterTests {
}
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

View File

@@ -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 {

View File

@@ -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 <IN> container for the GraphQL query along additional transport
* related context such as the HTTP url or headers for web.
* @param <OUT> 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<IN extends RequestInput, OUT extends ExecutionResult> {
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<OUT> execute(IN input);
Mono<ExecutionResult> execute(ExecutionInput input);
}

View File

@@ -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<String, Object> variables;
private final List<BiFunction<ExecutionInput, ExecutionInput.Builder, ExecutionInput>> executionInputConfigurers = new ArrayList<>();
public RequestInput(String query, @Nullable String operationName, @Nullable Map<String, Object> vars) {
Assert.notNull(query, "'query' is required");
@@ -80,24 +85,44 @@ public class RequestInput {
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<ExecutionInput, ExecutionInput.Builder, ExecutionInput> 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<ExecutionInput, ExecutionInput.Builder, ExecutionInput> 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<String, Object> toMap() {
Map<String, Object> map = new LinkedHashMap<>();
Map<String, Object> map = new LinkedHashMap<>(3);
map.put("query", getQuery());
if (getOperationName() != null) {
map.put("operationName", getOperationName());

View File

@@ -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<ExecutionResult> executeInternal(ExecutionInput input) {
return this.graphQLSource.graphQL().executeAsync(input);
public Mono<ExecutionResult> execute(ExecutionInput input) {
GraphQL graphQL = this.graphQLSource.graphQL();
return Mono.deferContextual(contextView -> {
ReactorDataFetcherAdapter.addReactorContext(input, contextView);
return Mono.fromFuture(graphQL.executeAsync(input));
});
}
}

View File

@@ -39,13 +39,12 @@ import org.springframework.util.Assert;
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<Object> {
class ReactorDataFetcherAdapter implements DataFetcher<Object> {
private static final String REACTOR_CONTEXT_KEY =
ReactorDataFetcherAdapter.class.getName() + ".REACTOR_CONTEXT";
@@ -108,7 +107,7 @@ public class ReactorDataFetcherAdapter implements DataFetcher<Object> {
* {@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(

View File

@@ -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<WebInterceptor> interceptors = new ArrayList<>();
/**
* Set the interceptors to invoke to handle request.
* @param interceptors the interceptors to use
*/
public void setInterceptors(List<WebInterceptor> interceptors) {
this.interceptors.clear();
this.interceptors.addAll(interceptors);
}
/**
* Return the {@link #setInterceptors(List) configured} interceptors.
*/
public List<WebInterceptor> getInterceptors() {
return this.interceptors;
}
@Override
public final Mono<WebOutput> execute(WebInput input) {
return preHandle(input)
.flatMap(executionInput -> Mono.fromFuture(executeInternal(executionInput)))
.flatMap(executionResult -> postHandle(new WebOutput(input, executionResult)));
}
private Mono<ExecutionInput> preHandle(WebInput input) {
Mono<ExecutionInput> 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<WebOutput> postHandle(WebOutput output) {
Mono<WebOutput> 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<ExecutionResult> executeInternal(ExecutionInput input);
}

View File

@@ -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<WebInput, WebOutput> {
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<WebOutput> handle(WebInput input);
}

View File

@@ -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.
*
* <p>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.
* <p>Interceptors may be declared as beans in Spring configuration and ordered
* as defined in {@link ObjectProvider#orderedStream()}.
*
* <p>Supported for Spring MVC and WebFlux.
*/
public interface WebInterceptor {
/**
* Intercept a GraphQL over HTTP request before the query is executed.
* Intercept a request and delegate for further handling and query execution
* via {@link WebGraphQLHandler#handle(WebInput)}.
*
* <p>{@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.
*
* @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
*/
default Mono<ExecutionInput> preHandle(ExecutionInput executionInput, WebInput webInput) {
return Mono.just(executionInput);
Mono<WebOutput> 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 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.
*
* <p>{@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 Mono<WebOutput> postHandle(WebOutput webOutput) {
return Mono.just(webOutput);
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<WebInterceptor> 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.
*/
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));
};
}
}

View File

@@ -22,7 +22,7 @@ import org.apache.commons.logging.LogFactory;
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<Map<String, Object>>() {};
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 class GraphQLHttpHandler {
if (logger.isDebugEnabled()) {
logger.debug("Executing: " + input);
}
return this.graphQLService.execute(input);
return this.graphQLHandler.handle(input);
})
.flatMap(output -> {
Map<String, Object> spec = output.toSpecification();

View File

@@ -39,7 +39,7 @@ import org.springframework.core.codec.Decoder;
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<Map<String, Object>>() {});
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 class GraphQLWebSocketHandler implements WebSocketHandler {
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:

View File

@@ -25,7 +25,7 @@ import org.apache.commons.logging.LogFactory;
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<Map<String, Object>>() {};
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 class GraphQLHttpHandler {
if (logger.isDebugEnabled()) {
logger.debug("Executing: " + input);
}
Mono<ServerResponse> responseMono = this.graphQLService.execute(input)
Mono<ServerResponse> responseMono = this.graphQLHandler.handle(input)
.map(output -> {
if (logger.isDebugEnabled()) {
logger.debug("Execution complete");

View File

@@ -41,7 +41,7 @@ import reactor.core.publisher.Mono;
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 @@ public class GraphQLWebSocketHandler extends TextWebSocketHandler implements Sub
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));

View File

@@ -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 java.util.function.Consumer;
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.core.publisher.Mono;
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 @@ public class ReactorDataFetcherAdapterTests {
}
private GraphQL initGraphQL(String schemaValue, Consumer<RuntimeWiring.Builder> 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<RuntimeWiring.Builder> 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) {

View File

@@ -19,19 +19,17 @@ import org.reactivestreams.Publisher;
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<WebOutput> 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<WebOutput> 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()));
}));
}
}

View File

@@ -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<WebInterceptor> 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<ExecutionInput> preHandle(ExecutionInput executionInput, WebInput webInput) {
this.output.append(":pre").append(this.index);
return Mono.delay(Duration.ofMillis(50)).map(aLong -> executionInput);
}
@Override
public Mono<WebOutput> 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)));
}
}
}

View File

@@ -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<WebInterceptor> 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<ExecutionResult> 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<WebOutput> intercept(WebInput webInput, WebGraphQLHandler next) {
this.output.append(":pre").append(this.index);
webInput.configureExecutionInput((executionInput, builder) -> {
Map<String, Object> 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());
}
}
}

View File

@@ -36,10 +36,11 @@ import org.springframework.core.io.ClassPathResource;
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 @@ public class GraphQLWebSocketHandlerTests {
private GraphQLWebSocketHandler initWebSocketHandler(
@Nullable List<WebInterceptor> 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)));
}

View File

@@ -31,10 +31,11 @@ import org.junit.jupiter.api.Test;
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 @@ public class GraphQLWebSocketHandlerTests {
@Nullable List<WebInterceptor> 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) {