Parameterize GraphQLRequestHandler

This makes the contract more general and applicable to any GraphQL
request, not necessary coupled to HTTP.

Closes gh-42
This commit is contained in:
Rossen Stoyanchev
2021-04-08 20:36:39 +01:00
parent b378231d39
commit 0338607a9b
14 changed files with 160 additions and 109 deletions

View File

@@ -34,9 +34,11 @@ 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.DefaultGraphQLRequestHandler;
import org.springframework.graphql.DefaultWebGraphQLRequestHandler;
import org.springframework.graphql.GraphQLRequestHandler;
import org.springframework.graphql.WebInput;
import org.springframework.graphql.WebInterceptor;
import org.springframework.graphql.WebOutput;
import org.springframework.graphql.webflux.GraphQLHttpHandler;
import org.springframework.graphql.webflux.GraphQLWebSocketHandler;
import org.springframework.http.MediaType;
@@ -63,15 +65,17 @@ public class WebFluxGraphQLAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public GraphQLRequestHandler graphQLRequestHandler(GraphQL graphQL, ObjectProvider<WebInterceptor> interceptors) {
DefaultGraphQLRequestHandler handler = new DefaultGraphQLRequestHandler(graphQL);
public GraphQLRequestHandler<WebInput, WebOutput> graphQLRequestHandler(
GraphQL graphQL, ObjectProvider<WebInterceptor> interceptors) {
DefaultWebGraphQLRequestHandler handler = new DefaultWebGraphQLRequestHandler(graphQL);
handler.setInterceptors(interceptors.orderedStream().collect(Collectors.toList()));
return handler;
}
@Bean
@ConditionalOnMissingBean
public GraphQLHttpHandler graphQLHandler(GraphQLRequestHandler requestHandler) {
public GraphQLHttpHandler graphQLHandler(GraphQLRequestHandler<WebInput, WebOutput> requestHandler) {
return new GraphQLHttpHandler(requestHandler);
}
@@ -98,7 +102,8 @@ public class WebFluxGraphQLAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public GraphQLWebSocketHandler graphQLWebSocketHandler(
GraphQLRequestHandler handler, GraphQLProperties properties, ServerCodecConfigurer configurer) {
GraphQLRequestHandler<WebInput, WebOutput> handler, GraphQLProperties properties,
ServerCodecConfigurer configurer) {
return new GraphQLWebSocketHandler(
handler, configurer, properties.getWebsocket().getConnectionInitTimeout());

View File

@@ -38,9 +38,11 @@ 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.DefaultGraphQLRequestHandler;
import org.springframework.graphql.DefaultWebGraphQLRequestHandler;
import org.springframework.graphql.GraphQLRequestHandler;
import org.springframework.graphql.WebInput;
import org.springframework.graphql.WebInterceptor;
import org.springframework.graphql.WebOutput;
import org.springframework.graphql.webmvc.GraphQLHttpHandler;
import org.springframework.graphql.webmvc.GraphQLWebSocketHandler;
import org.springframework.http.HttpHeaders;
@@ -70,15 +72,17 @@ public class WebMvcGraphQLAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public GraphQLRequestHandler graphQLRequestHandler(GraphQL graphQL, ObjectProvider<WebInterceptor> interceptors) {
DefaultGraphQLRequestHandler handler = new DefaultGraphQLRequestHandler(graphQL);
public GraphQLRequestHandler<WebInput, WebOutput> graphQLRequestHandler(
GraphQL graphQL, ObjectProvider<WebInterceptor> interceptors) {
DefaultWebGraphQLRequestHandler handler = new DefaultWebGraphQLRequestHandler(graphQL);
handler.setInterceptors(interceptors.orderedStream().collect(Collectors.toList()));
return handler;
}
@Bean
@ConditionalOnMissingBean
public GraphQLHttpHandler graphQLHandler(GraphQLRequestHandler requestHandler) {
public GraphQLHttpHandler graphQLHandler(GraphQLRequestHandler<WebInput, WebOutput> requestHandler) {
return new GraphQLHttpHandler(requestHandler);
}
@@ -107,7 +111,8 @@ public class WebMvcGraphQLAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public GraphQLWebSocketHandler graphQLWebSocketHandler(
GraphQLRequestHandler handler, GraphQLProperties properties, HttpMessageConverters converters) {
GraphQLRequestHandler<WebInput, WebOutput> handler, GraphQLProperties properties,
HttpMessageConverters converters) {
HttpMessageConverter<?> converter = converters.getConverters().stream()
.filter(candidate -> candidate.canRead(Map.class, MediaType.APPLICATION_JSON))

View File

@@ -24,10 +24,12 @@ import graphql.ExecutionResult;
import reactor.core.publisher.Mono;
/**
* Base class for {@link GraphQLRequestHandler} implementations that support a
* {@link WebInterceptor} chain.
* Base class for {@link GraphQLRequestHandler} implementations that supports
* customizations of request handling through a {@link WebInterceptor} chain.
* Sub-classes must implement {@link #handleInternal(ExecutionInput)} for the
* actual handling of the GraphQL query.
*/
public abstract class AbstractInterceptingGraphQLRequestHandler implements GraphQLRequestHandler {
public abstract class AbstractWebGraphQLRequestHandler implements GraphQLRequestHandler<WebInput, WebOutput> {
private final List<WebInterceptor> interceptors = new ArrayList<>();

View File

@@ -22,15 +22,15 @@ import graphql.ExecutionResult;
import graphql.GraphQL;
/**
* Default implementation that invokes {@link GraphQL} and supports a
* {@link WebInterceptor} chain for pre- and post-handling.
* Extension of {@link AbstractWebGraphQLRequestHandler} that simply delegates
* to {@link GraphQL}to execute the request.
*/
public class DefaultGraphQLRequestHandler extends AbstractInterceptingGraphQLRequestHandler {
public class DefaultWebGraphQLRequestHandler extends AbstractWebGraphQLRequestHandler {
private final GraphQL graphQL;
public DefaultGraphQLRequestHandler(GraphQL graphQL) {
public DefaultWebGraphQLRequestHandler(GraphQL graphQL) {
this.graphQL = graphQL;
}

View File

@@ -15,19 +15,20 @@
*/
package org.springframework.graphql;
import graphql.ExecutionResult;
import reactor.core.publisher.Mono;
/**
* Contract to handle a GraphQL request.
*/
@FunctionalInterface
public interface GraphQLRequestHandler {
public interface GraphQLRequestHandler<I extends RequestInput, O extends ExecutionResult> {
/**
* Handle the request and return the result of execution.
* @param input the GraphQL query
* @param input the GraphQL query container
* @return the execution result
*/
Mono<WebOutput> handle(WebInput input);
Mono<O> handle(I input);
}

View File

@@ -0,0 +1,104 @@
/*
* 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;
import java.util.Collections;
import java.util.Map;
import graphql.ExecutionInput;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebInputException;
/**
* Container for a GraphQL request.
*/
public class RequestInput {
protected final String query;
@Nullable
protected final String operationName;
protected final Map<String, Object> variables;
@SuppressWarnings("unchecked")
public RequestInput(Map<String, Object> body) {
Assert.notNull(body, "'body' is required'");
this.query = getAndValidateQuery(body);
this.operationName = (String) body.get("operationName");
this.variables = (body.get("variables") != null ?
(Map<String, Object>) body.get("variables") : Collections.emptyMap());
}
private static String getAndValidateQuery(Map<String, Object> body) {
String query = (String) body.get("query");
if (!StringUtils.hasText(query)) {
throw new ServerWebInputException("Query is required");
}
return query;
}
/**
* Return the query name extracted from the request body. This is guaranteed
* to be a non-empty string, or otherwise the request is rejected via
* {@link ServerWebInputException} as a 400 error.
*/
public String query() {
return this.query;
}
/**
* Return the query operation name extracted from the request body or
* {@code null} if not provided.
*/
@Nullable
public String operationName() {
return this.operationName;
}
/**
* Return the query variables that can be referenced via $syntax extracted
* from the request body or a {@code null} if not provided.
*/
public Map<String, Object> variables() {
return this.variables;
}
/**
* Create an {@link ExecutionInput} initialized with the {@link #query()},
* {@link #operationName()}, and {@link #variables()}.
*/
public ExecutionInput toExecutionInput() {
return ExecutionInput.newExecutionInput()
.query(query())
.operationName(operationName())
.variables(variables())
.build();
}
@Override
public String toString() {
return "Query='" + query() + "'" +
(operationName() != null ? ", Operation='" + operationName() + "'" : "") +
(!CollectionUtils.isEmpty(variables()) ? ", Variables=" + variables() : "");
}
}

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.
@@ -16,17 +16,10 @@
package org.springframework.graphql;
import java.net.URI;
import java.util.Collections;
import java.util.Map;
import graphql.ExecutionInput;
import org.springframework.http.HttpHeaders;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebInputException;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
@@ -35,38 +28,19 @@ import org.springframework.web.util.UriComponentsBuilder;
* {@link UriComponents URL} and the headers of the request, as well as the
* query name, operation name, and variables from the request body.
*/
public class WebInput {
public class WebInput extends RequestInput {
private final UriComponents uri;
private final HttpHeaders headers;
private final String query;
@Nullable
private final String operationName;
private final Map<String, Object> variables;
@SuppressWarnings("unchecked")
public WebInput(URI uri, HttpHeaders headers, Map<String, Object> body) {
super(body);
Assert.notNull(uri, "URI is required'");
Assert.notNull(body, "HttpHeaders is required'");
Assert.notNull(body, "'body' is required'");
Assert.notNull(headers, "HttpHeaders is required'");
this.uri = UriComponentsBuilder.fromUri(uri).build(true);
this.headers = headers;
this.query = getAndValidateQuery(body);
this.operationName = (String) body.get("operationName");
this.variables = (Map<String, Object>) (body.get("variables") != null ? body.get("variables"): Collections.emptyMap());
}
private static String getAndValidateQuery(Map<String, Object> body) {
String query = (String) body.get("query");
if (!StringUtils.hasText(query)) {
throw new ServerWebInputException("Query is required");
}
return query;
}
@@ -85,48 +59,4 @@ public class WebInput {
return this.headers;
}
/**
* Return the query name extracted from the request body. This is guaranteed
* to be a non-empty string, or otherwise the request is rejected via
* {@link ServerWebInputException} as a 400 error.
*/
public String query() {
return this.query;
}
/**
* Return the query operation name extracted from the request body or
* {@code null} if not provided.
*/
@Nullable
public String operationName() {
return this.operationName;
}
/**
* Return the query variables that can be referenced via $syntax extracted
* from the request body or a {@code null} if not provided.
*/
public Map<String, Object> variables() {
return this.variables;
}
/**
* Create an {@link ExecutionInput} initialized with the {@link #query()},
* {@link #operationName()}, and {@link #variables()}.
*/
public ExecutionInput toExecutionInput() {
return ExecutionInput.newExecutionInput()
.query(query())
.operationName(operationName())
.variables(variables())
.build();
}
@Override
public String toString() {
return "Query='" + query() + "'" +
(operationName() != null ? ", Operation='" + operationName() + "'" : "") +
(!CollectionUtils.isEmpty(variables()) ? ", Variables=" + variables() : "");
}
}

View File

@@ -24,6 +24,7 @@ import reactor.core.publisher.Mono;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.graphql.GraphQLRequestHandler;
import org.springframework.graphql.WebInput;
import org.springframework.graphql.WebOutput;
import org.springframework.util.Assert;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
@@ -39,14 +40,14 @@ public class GraphQLHttpHandler {
new ParameterizedTypeReference<Map<String, Object>>() {};
private final GraphQLRequestHandler requestHandler;
private final GraphQLRequestHandler<WebInput, WebOutput> requestHandler;
/**
* Create a new instance.
* @param requestHandler the handler to use for GraphQL query handling
*/
public GraphQLHttpHandler(GraphQLRequestHandler requestHandler) {
public GraphQLHttpHandler(GraphQLRequestHandler<WebInput, WebOutput> requestHandler) {
Assert.notNull(requestHandler, "GraphQLRequestHandler is required");
this.requestHandler = requestHandler;
}

View File

@@ -40,6 +40,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.GraphQLRequestHandler;
import org.springframework.graphql.WebInput;
import org.springframework.graphql.WebOutput;
import org.springframework.graphql.WebSocketMessageInput;
import org.springframework.http.MediaType;
@@ -72,7 +73,7 @@ public class GraphQLWebSocketHandler implements WebSocketHandler {
ResolvableType.forType(new ParameterizedTypeReference<Map<String, Object>>() {});
private final GraphQLRequestHandler requestHandler;
private final GraphQLRequestHandler<WebInput, WebOutput> requestHandler;
private final Decoder<?> decoder;
@@ -88,7 +89,7 @@ public class GraphQLWebSocketHandler implements WebSocketHandler {
* @param connectionInitTimeout the time within which the {@code CONNECTION_INIT}
* type message must be received.
*/
public GraphQLWebSocketHandler(GraphQLRequestHandler requestHandler,
public GraphQLWebSocketHandler(GraphQLRequestHandler<WebInput, WebOutput> requestHandler,
ServerCodecConfigurer configurer, Duration connectionInitTimeout) {
Assert.notNull(requestHandler, "GraphQLRequestHandler is required");

View File

@@ -27,6 +27,7 @@ import reactor.core.publisher.Mono;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.graphql.GraphQLRequestHandler;
import org.springframework.graphql.WebInput;
import org.springframework.graphql.WebOutput;
import org.springframework.util.Assert;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.server.ServerWebInputException;
@@ -45,14 +46,14 @@ public class GraphQLHttpHandler {
new ParameterizedTypeReference<Map<String, Object>>() {};
private final GraphQLRequestHandler requestHandler;
private final GraphQLRequestHandler<WebInput, WebOutput> requestHandler;
/**
* Create a new instance.
* @param requestHandler the handler to use for GraphQL query handling
*/
public GraphQLHttpHandler(GraphQLRequestHandler requestHandler) {
public GraphQLHttpHandler(GraphQLRequestHandler<WebInput, WebOutput> requestHandler) {
Assert.notNull(requestHandler, "GraphQLRequestHandler is required");
this.requestHandler = requestHandler;
}

View File

@@ -42,6 +42,7 @@ import reactor.core.scheduler.Scheduler;
import reactor.core.scheduler.Schedulers;
import org.springframework.graphql.GraphQLRequestHandler;
import org.springframework.graphql.WebInput;
import org.springframework.graphql.WebOutput;
import org.springframework.graphql.WebSocketMessageInput;
import org.springframework.http.HttpHeaders;
@@ -71,7 +72,7 @@ public class GraphQLWebSocketHandler extends TextWebSocketHandler implements Sub
Arrays.asList("graphql-transport-ws", "subscriptions-transport-ws");
private final GraphQLRequestHandler requestHandler;
private final GraphQLRequestHandler<WebInput, WebOutput> requestHandler;
private final Duration initTimeoutDuration;
@@ -88,7 +89,7 @@ public class GraphQLWebSocketHandler extends TextWebSocketHandler implements Sub
* type message must be received.
*/
public GraphQLWebSocketHandler(
GraphQLRequestHandler requestHandler, HttpMessageConverter<?> converter,
GraphQLRequestHandler<WebInput, WebOutput> requestHandler, HttpMessageConverter<?> converter,
Duration connectionInitTimeout) {
Assert.notNull(converter, "HttpMessageConverter for JSON is required");

View File

@@ -40,9 +40,9 @@ import static graphql.schema.idl.TypeRuntimeWiring.newTypeWiring;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Unit tests for {@link DefaultGraphQLRequestHandler}.
* Unit tests for {@link DefaultWebGraphQLRequestHandler}.
*/
public class DefaultGraphQLRequestHandlerTests {
public class DefaultWebGraphQLRequestHandlerTests {
@Test
void testInterceptorInvocation() throws Exception {
@@ -64,7 +64,7 @@ public class DefaultGraphQLRequestHandlerTests {
Map body = mapper.reader().readValue("{\"query\": \"" + query + "\"}", Map.class);
WebInput webInput = new WebInput(URI.create("/graphql"), new HttpHeaders(), body);
DefaultGraphQLRequestHandler requestHandler = new DefaultGraphQLRequestHandler(createGraphQL());
DefaultWebGraphQLRequestHandler requestHandler = new DefaultWebGraphQLRequestHandler(createGraphQL());
requestHandler.setInterceptors(interceptors);
WebOutput webOutput = requestHandler.handle(webInput).block();

View File

@@ -42,7 +42,7 @@ 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.ConsumeOneAndNeverCompleteInterceptor;
import org.springframework.graphql.DefaultGraphQLRequestHandler;
import org.springframework.graphql.DefaultWebGraphQLRequestHandler;
import org.springframework.graphql.GraphQLDataFetchers;
import org.springframework.graphql.WebInterceptor;
import org.springframework.http.HttpHeaders;
@@ -280,7 +280,7 @@ public class GraphQLWebSocketHandlerTests {
GraphQL graphQL = initGraphQL();
DefaultGraphQLRequestHandler requestHandler = new DefaultGraphQLRequestHandler(graphQL);
DefaultWebGraphQLRequestHandler requestHandler = new DefaultWebGraphQLRequestHandler(graphQL);
if (interceptors != null) {
requestHandler.setInterceptors(interceptors);
}

View File

@@ -37,7 +37,7 @@ import org.junit.jupiter.api.Test;
import reactor.test.StepVerifier;
import org.springframework.graphql.ConsumeOneAndNeverCompleteInterceptor;
import org.springframework.graphql.DefaultGraphQLRequestHandler;
import org.springframework.graphql.DefaultWebGraphQLRequestHandler;
import org.springframework.graphql.GraphQLDataFetchers;
import org.springframework.graphql.WebInterceptor;
import org.springframework.http.HttpHeaders;
@@ -264,7 +264,7 @@ public class GraphQLWebSocketHandlerTests {
try {
GraphQL graphQL = initGraphQL();
DefaultGraphQLRequestHandler requestHandler = new DefaultGraphQLRequestHandler(graphQL);
DefaultWebGraphQLRequestHandler requestHandler = new DefaultWebGraphQLRequestHandler(graphQL);
if (interceptors != null) {
requestHandler.setInterceptors(interceptors);
}