From e8a8334009c441e2684b57a558a647163cd1fc4e Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Fri, 18 Sep 2020 11:37:40 +0100 Subject: [PATCH] Updates to Interceptor contract --- .../graphql/GraphQLInterceptor.java | 28 ----- .../springframework/graphql/RequestInput.java | 76 ------------- .../graphql/WebFluxGraphQLHandler.java | 28 ++--- .../org/springframework/graphql/WebInput.java | 91 +++++++++++++++ .../graphql/WebInterceptor.java | 50 +++++++++ .../graphql/WebMvcGraphQLHandler.java | 60 ++++------ .../springframework/graphql/WebOutput.java | 105 ++++++++++++++++++ 7 files changed, 285 insertions(+), 153 deletions(-) delete mode 100644 spring-graphql-web/src/main/java/org/springframework/graphql/GraphQLInterceptor.java delete mode 100644 spring-graphql-web/src/main/java/org/springframework/graphql/RequestInput.java create mode 100644 spring-graphql-web/src/main/java/org/springframework/graphql/WebInput.java create mode 100644 spring-graphql-web/src/main/java/org/springframework/graphql/WebInterceptor.java create mode 100644 spring-graphql-web/src/main/java/org/springframework/graphql/WebOutput.java diff --git a/spring-graphql-web/src/main/java/org/springframework/graphql/GraphQLInterceptor.java b/spring-graphql-web/src/main/java/org/springframework/graphql/GraphQLInterceptor.java deleted file mode 100644 index 485683b5..00000000 --- a/spring-graphql-web/src/main/java/org/springframework/graphql/GraphQLInterceptor.java +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright 2020-2020 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 graphql.ExecutionInput; -import graphql.ExecutionResult; - -import org.springframework.http.HttpHeaders; - -public interface GraphQLInterceptor { - - ExecutionInput preHandle(ExecutionInput input, HttpHeaders headers); - - ExecutionResult postHandle(ExecutionResult result); -} \ No newline at end of file diff --git a/spring-graphql-web/src/main/java/org/springframework/graphql/RequestInput.java b/spring-graphql-web/src/main/java/org/springframework/graphql/RequestInput.java deleted file mode 100644 index 5233bfd0..00000000 --- a/spring-graphql-web/src/main/java/org/springframework/graphql/RequestInput.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright 2020-2020 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 org.springframework.lang.Nullable; -import org.springframework.util.StringUtils; -import org.springframework.web.server.ServerWebInputException; - -/** - * @author Andreas Marek - * @author Brian Clozel - */ -class RequestInput { - - private String query; - - private String operationName; - - private Map variables = Collections.emptyMap(); - - public RequestInput(String query, String operationName, Map variables) { - this.query = query; - this.operationName = operationName; - this.variables = variables; - } - - public RequestInput() { - } - - @Nullable - public String getQuery() { - return this.query; - } - - public void setQuery(String query) { - this.query = query; - } - - public String getOperationName() { - return this.operationName; - } - - public void setOperationName(String operationName) { - this.operationName = operationName; - } - - public Map getVariables() { - return this.variables; - } - - public void setVariables(Map variables) { - this.variables = variables; - } - - public void validate() { - if (!StringUtils.hasText(getQuery())) { - throw new ServerWebInputException("Missing query"); - } - } -} diff --git a/spring-graphql-web/src/main/java/org/springframework/graphql/WebFluxGraphQLHandler.java b/spring-graphql-web/src/main/java/org/springframework/graphql/WebFluxGraphQLHandler.java index ddd45a80..601cfdd4 100644 --- a/spring-graphql-web/src/main/java/org/springframework/graphql/WebFluxGraphQLHandler.java +++ b/spring-graphql-web/src/main/java/org/springframework/graphql/WebFluxGraphQLHandler.java @@ -20,7 +20,6 @@ import graphql.ExecutionResult; import graphql.GraphQL; import reactor.core.publisher.Mono; -import org.springframework.http.HttpHeaders; import org.springframework.web.reactive.function.server.HandlerFunction; import org.springframework.web.reactive.function.server.ServerRequest; import org.springframework.web.reactive.function.server.ServerResponse; @@ -38,26 +37,29 @@ public class WebFluxGraphQLHandler implements HandlerFunction { } public Mono handle(ServerRequest request) { - return request.bodyToMono(RequestInput.class) - .flatMap(requestInput -> { - requestInput.validate(); + return request.bodyToMono(WebInput.MAP_PARAMETERIZED_TYPE_REF) + .flatMap(body -> { + WebInput webInput = new WebInput( + request.uri(), request.headers().asHttpHeaders(), body); + ExecutionInput executionInput = ExecutionInput.newExecutionInput() - .query(requestInput.getQuery()) - .operationName(requestInput.getOperationName()) - .variables(requestInput.getVariables()) + .query(webInput.getQuery()) + .operationName(webInput.getOperationName()) + .variables(webInput.getVariables()) .build(); + // Invoke GraphQLInterceptor's preHandle here - return customizeExecutionInput(executionInput, request.headers().asHttpHeaders()); + return extendInput(executionInput, webInput); }) - .flatMap(input -> { - // Invoke GraphQLInterceptor's postHandle here - return execute(input); + .flatMap(executionInput -> { + // Invoke handleResult here + return execute(executionInput); }) .flatMap(result -> ServerResponse.ok().bodyValue(result.toSpecification())); } - protected Mono customizeExecutionInput(ExecutionInput input, HttpHeaders headers) { - return Mono.just(input); + protected Mono extendInput(ExecutionInput executionInput, WebInput webInput) { + return Mono.just(executionInput); } protected Mono execute(ExecutionInput input) { diff --git a/spring-graphql-web/src/main/java/org/springframework/graphql/WebInput.java b/spring-graphql-web/src/main/java/org/springframework/graphql/WebInput.java new file mode 100644 index 00000000..7d896bc4 --- /dev/null +++ b/spring-graphql-web/src/main/java/org/springframework/graphql/WebInput.java @@ -0,0 +1,91 @@ +/* + * Copyright 2020-2020 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.net.URI; +import java.util.Collections; +import java.util.Map; + +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpHeaders; +import org.springframework.lang.Nullable; +import org.springframework.util.StringUtils; +import org.springframework.web.server.ServerWebInputException; +import org.springframework.web.util.UriComponents; +import org.springframework.web.util.UriComponentsBuilder; + +/** + * Represents the input to a GraphQL HTTP endpoint including URI, headers, and + * the query, operationName, and variables from the request body. + */ +public class WebInput { + + static final ParameterizedTypeReference> MAP_PARAMETERIZED_TYPE_REF = + new ParameterizedTypeReference>() {}; + + + private final UriComponents uri; + + private final HttpHeaders headers; + + private final String query; + + @Nullable + private final String operationName; + + private final Map variables; + + + @SuppressWarnings("unchecked") + WebInput(URI uri, HttpHeaders headers, Map body) { + this.uri = UriComponentsBuilder.fromUri(uri).build(true); + this.headers = headers; + this.query = getAndValidateQuery(body); + this.operationName = (String) body.get("operationName"); + this.variables = (Map) body.getOrDefault("variables", Collections.emptyMap()); + } + + private static String getAndValidateQuery(Map body) { + String query = (String) body.get("query"); + if (!StringUtils.hasText(query)) { + throw new ServerWebInputException("Query is required"); + } + return query; + } + + + public UriComponents uri() { + return this.uri; + } + + public HttpHeaders getHeaders() { + return this.headers; + } + + public String getQuery() { + return this.query; + } + + @Nullable + public String getOperationName() { + return this.operationName; + } + + public Map getVariables() { + return this.variables; + } + +} \ No newline at end of file diff --git a/spring-graphql-web/src/main/java/org/springframework/graphql/WebInterceptor.java b/spring-graphql-web/src/main/java/org/springframework/graphql/WebInterceptor.java new file mode 100644 index 00000000..5580381d --- /dev/null +++ b/spring-graphql-web/src/main/java/org/springframework/graphql/WebInterceptor.java @@ -0,0 +1,50 @@ +/* + * Copyright 2020-2020 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.function.Consumer; + +import graphql.ExecutionInput; +import reactor.core.publisher.Mono; + +/** + * Allows interception of GraphQL over HTTP requests with possible customization + * of the input and the result of query execution. + */ +public interface WebInterceptor { + + /** + * Intercept a GraphQL over HTTP request before the query is executed. + * + * @param executionInput the input to use, initialized from the {@code WebInput} + * @param webInput the input from the HTTP request + * @return the same instance or a new one via {@link ExecutionInput#transform(Consumer)} + */ + default Mono preHandle(ExecutionInput executionInput, WebInput webInput) { + return Mono.just(executionInput); + } + + /** + * Intercept a GraphQL over HTTP request after the query is executed. + * + * @param webOutput the execution result + * @return the same instance or a new one via {@link WebOutput#transform(Consumer)} + */ + default Mono postHandle(WebOutput webOutput) { + return Mono.just(webOutput); + } + +} \ No newline at end of file diff --git a/spring-graphql-web/src/main/java/org/springframework/graphql/WebMvcGraphQLHandler.java b/spring-graphql-web/src/main/java/org/springframework/graphql/WebMvcGraphQLHandler.java index 0cd1215f..f9655db4 100644 --- a/spring-graphql-web/src/main/java/org/springframework/graphql/WebMvcGraphQLHandler.java +++ b/spring-graphql-web/src/main/java/org/springframework/graphql/WebMvcGraphQLHandler.java @@ -17,18 +17,15 @@ package org.springframework.graphql; import java.io.IOException; import java.util.Map; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; import javax.servlet.ServletException; import graphql.ExecutionInput; import graphql.ExecutionResult; import graphql.GraphQL; +import reactor.core.publisher.Mono; -import org.springframework.http.HttpHeaders; import org.springframework.web.HttpMediaTypeNotSupportedException; -import org.springframework.web.server.ServerErrorException; import org.springframework.web.server.ServerWebInputException; import org.springframework.web.servlet.function.HandlerFunction; import org.springframework.web.servlet.function.ServerRequest; @@ -53,47 +50,38 @@ public class WebMvcGraphQLHandler implements HandlerFunction { * e.g. {@link HttpMediaTypeNotSupportedException}. */ public ServerResponse handle(ServerRequest request) throws ServletException { - RequestInput requestInput; + WebInput webInput = createWebInput(request); + + ExecutionInput executionInput = ExecutionInput.newExecutionInput() + .query(webInput.getQuery()) + .operationName(webInput.getOperationName()) + .variables(webInput.getVariables()) + .build(); + + Mono> body = extendInput(executionInput, webInput) + .flatMap(this::execute) + .map(ExecutionResult::toSpecification); + + return ServerResponse.ok().body(body); + } + + private static WebInput createWebInput(ServerRequest request) throws ServletException { + Map body; try { - requestInput = request.body(RequestInput.class); - requestInput.validate(); + body = request.body(WebInput.MAP_PARAMETERIZED_TYPE_REF); } catch (IOException ex) { throw new ServerWebInputException("I/O error while reading request body", null, ex); } - - ExecutionInput executionInput = ExecutionInput.newExecutionInput() - .query(requestInput.getQuery()) - .operationName(requestInput.getOperationName()) - .variables(requestInput.getVariables()) - .build(); - - // Invoke GraphQLInterceptor's preHandle here - - CompletableFuture> future = - customizeExecutionInput(executionInput, request.headers().asHttpHeaders()) - .thenCompose(this::execute) - .thenApply(ExecutionResult::toSpecification); - - // Invoke GraphQLInterceptor's postHandle here - - return ServerResponse.ok().body(future.isDone() ? getResult(future) : future); + return new WebInput(request.uri(), request.headers().asHttpHeaders(), body); } - protected CompletableFuture customizeExecutionInput(ExecutionInput input, HttpHeaders headers) { - return CompletableFuture.completedFuture(input); + protected Mono extendInput(ExecutionInput executionInput, WebInput webInput) { + return Mono.just(executionInput); } - protected CompletableFuture execute(ExecutionInput input) { - return graphQL.executeAsync(input); + protected Mono execute(ExecutionInput input) { + return Mono.fromFuture(this.graphQL.executeAsync(input)); } - private Map getResult(CompletableFuture> future) { - try { - return future.get(); - } - catch (InterruptedException | ExecutionException ex) { - throw new ServerErrorException("Failed to get result", ex); - } - } } \ No newline at end of file diff --git a/spring-graphql-web/src/main/java/org/springframework/graphql/WebOutput.java b/spring-graphql-web/src/main/java/org/springframework/graphql/WebOutput.java new file mode 100644 index 00000000..d59220e5 --- /dev/null +++ b/spring-graphql-web/src/main/java/org/springframework/graphql/WebOutput.java @@ -0,0 +1,105 @@ +/* + * Copyright 2020-2020 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.List; +import java.util.Map; +import java.util.function.Consumer; + +import graphql.ExecutionResult; +import graphql.ExecutionResultImpl; +import graphql.GraphQLError; + +import org.springframework.lang.Nullable; + + +public class WebOutput implements ExecutionResult { + + private final ExecutionResult executionResult; + + + WebOutput(ExecutionResult executionResult) { + this.executionResult = executionResult; + } + + + @Nullable + @Override + public T getData() { + return this.executionResult.getData(); + } + + @Override + public boolean isDataPresent() { + return this.executionResult.isDataPresent(); + } + + public List getErrors() { + return this.executionResult.getErrors(); + } + + public Map getExtensions() { + return this.executionResult.getExtensions(); + } + + @Override + public Map toSpecification() { + return this.executionResult.toSpecification(); + } + + public WebOutput transform(Consumer consumer) { + Builder builder = new Builder(this); + consumer.accept(builder); + return builder.build(); + } + + + public static class Builder { + + @Nullable + private Object data; + + private List errors; + + private Map extensions; + + public Builder(WebOutput output) { + this.data = output.getData(); + this.errors = output.getErrors(); + this.extensions = output.getExtensions(); + } + + public Builder data(Object data) { + this.data = data; + return this; + } + + public Builder errors(List errors) { + this.errors = errors; + return this; + } + + public Builder extensions(Map extensions) { + this.extensions = extensions; + return this; + } + + public WebOutput build() { + return new WebOutput(new ExecutionResultImpl(this.data, this.errors, this.extensions)); + } + } + +}