diff --git a/spring-graphql/build.gradle b/spring-graphql/build.gradle index 1f8c7f8d..03f31220 100644 --- a/spring-graphql/build.gradle +++ b/spring-graphql/build.gradle @@ -2,6 +2,7 @@ plugins { id 'io.spring.dependency-management' version '1.0.10.RELEASE' id 'java-library' + id "org.springframework.graphql.conventions" } description = "GraphQL Support for Spring Applications" diff --git a/spring-graphql/src/main/java/org/springframework/graphql/GraphQlService.java b/spring-graphql/src/main/java/org/springframework/graphql/GraphQlService.java index db2b4acc..fb4f5737 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/GraphQlService.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/GraphQlService.java @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.graphql; import graphql.ExecutionInput; @@ -20,8 +21,11 @@ import graphql.ExecutionResult; import reactor.core.publisher.Mono; /** - * Strategy to perform GraphQL request execution with input for and output from - * the invocation of {@link graphql.GraphQL}. + * Strategy to perform GraphQL request execution with input for and output from the + * invocation of {@link graphql.GraphQL}. + * + * @author Rossen Stoyanchev + * @since 1.0.0 */ public interface GraphQlService { diff --git a/spring-graphql/src/main/java/org/springframework/graphql/RequestInput.java b/spring-graphql/src/main/java/org/springframework/graphql/RequestInput.java index 4a3b229f..922fb8de 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/RequestInput.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/RequestInput.java @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.graphql; import java.util.ArrayList; @@ -30,9 +31,11 @@ import org.springframework.util.CollectionUtils; /** * Common representation for GraphQL request input. This can be converted to - * {@link ExecutionInput} via {@link #toExecutionInput()} and the - * {@code ExecutionInput} further customized via - * {@link #configureExecutionInput(BiFunction)}. + * {@link ExecutionInput} via {@link #toExecutionInput()} and the {@code ExecutionInput} + * further customized via {@link #configureExecutionInput(BiFunction)}. + * + * @author Rossen Stoyanchev + * @since 1.0.0 */ public class RequestInput { @@ -45,12 +48,11 @@ public class RequestInput { private final List> executionInputConfigurers = new ArrayList<>(); - public RequestInput(String query, @Nullable String operationName, @Nullable Map vars) { Assert.notNull(query, "'query' is required"); this.query = query; this.operationName = operationName; - this.variables = (vars != null ? vars : Collections.emptyMap()); + this.variables = ((vars != null) ? vars : Collections.emptyMap()); } public RequestInput(Map body) { @@ -62,18 +64,19 @@ public class RequestInput { return (T) body.get(key); } - /** - * Return the query name extracted from the request body. This is guaranteed - * to be a non-empty string. + * Return the query name extracted from the request body. This is guaranteed to be a + * non-empty string. + * @return the query name */ public String getQuery() { return this.query; } /** - * Return the operation name extracted from the request body or - * {@code null} if not provided. + * Return the operation name extracted from the request body or {@code null} if not + * provided. + * @return the operation name or {@code null} */ @Nullable public String getOperationName() { @@ -81,20 +84,21 @@ public class RequestInput { } /** - * Return the variables that can be referenced via $syntax extracted - * from the request body or a {@code null} if not provided. + * Return the variables that can be referenced via $syntax extracted from the request + * body or a {@code null} if not provided. + * @return the request variables or {@code null} */ public Map getVariables() { return this.variables; } /** - * Provide a consumer to configure the {@link ExecutionInput} used for input - * to {@link graphql.GraphQL#executeAsync(ExecutionInput)}. - * The builder is initially populated with the values from - * {@link #getQuery()}, {@link #getOperationName()}, and {@link #getVariables()}. - * @param configurer a {@code BiFunction} with the current - * {@code ExecutionInput} and a builder to modify it. + * Provide a consumer to configure the {@link ExecutionInput} used for input to + * {@link graphql.GraphQL#executeAsync(ExecutionInput)}. The builder is initially + * populated with the values from {@link #getQuery()}, {@link #getOperationName()}, + * and {@link #getVariables()}. + * @param configurer a {@code BiFunction} with the current {@code ExecutionInput} and + * a builder to modify it. */ public void configureExecutionInput(BiFunction configurer) { this.executionInputConfigurers.add(configurer); @@ -105,17 +109,15 @@ public class RequestInput { * populated from {@link #getQuery()}, {@link #getOperationName()}, and * {@link #getVariables()}, and is then further customized through * {@link #configureExecutionInput(BiFunction)}. + * @return the execution input */ public ExecutionInput toExecutionInput() { - ExecutionInput executionInput = ExecutionInput.newExecutionInput() - .query(this.query) - .operationName(this.operationName) - .variables(this.variables) - .build(); + ExecutionInput executionInput = ExecutionInput.newExecutionInput().query(this.query) + .operationName(this.operationName).variables(this.variables).build(); for (BiFunction configurer : this.executionInputConfigurers) { ExecutionInput current = executionInput; - executionInput = executionInput.transform(builder -> configurer.apply(current, builder)); + executionInput = executionInput.transform((builder) -> configurer.apply(current, builder)); } return executionInput; @@ -123,6 +125,7 @@ public class RequestInput { /** * Return a Map representation of the request input. + * @return map representation of the input */ public Map toMap() { Map map = new LinkedHashMap<>(3); @@ -136,12 +139,11 @@ public class RequestInput { return map; } - @Override public String toString() { - return "Query='" + getQuery() + "'" + - (getOperationName() != null ? ", Operation='" + getOperationName() + "'" : "") + - (!CollectionUtils.isEmpty(getVariables()) ? ", Variables=" + getVariables() : ""); + return "Query='" + getQuery() + "'" + + ((getOperationName() != null) ? ", Operation='" + getOperationName() + "'" : "") + + (!CollectionUtils.isEmpty(getVariables()) ? ", Variables=" + getVariables() : ""); } } diff --git a/spring-graphql/src/main/java/org/springframework/graphql/execution/CompositeThreadLocalAccessor.java b/spring-graphql/src/main/java/org/springframework/graphql/execution/CompositeThreadLocalAccessor.java index e053a5dc..7ae4d3ba 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/execution/CompositeThreadLocalAccessor.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/execution/CompositeThreadLocalAccessor.java @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.graphql.execution; import java.util.List; @@ -21,29 +22,30 @@ import java.util.Map; /** * Default implementation of a composite accessor that is returned from * {@link ThreadLocalAccessor#composite(List)}. + * + * @author Rossen Stoyanchev */ class CompositeThreadLocalAccessor implements ThreadLocalAccessor { private final List accessors; - - public CompositeThreadLocalAccessor(List accessors) { + CompositeThreadLocalAccessor(List accessors) { this.accessors = accessors; } - @Override public void extractValues(Map container) { - this.accessors.forEach(accessor -> accessor.extractValues(container)); + this.accessors.forEach((accessor) -> accessor.extractValues(container)); } @Override public void restoreValues(Map values) { - this.accessors.forEach(accessor -> accessor.restoreValues(values)); + this.accessors.forEach((accessor) -> accessor.restoreValues(values)); } @Override public void resetValues(Map values) { - this.accessors.forEach(accessor -> accessor.resetValues(values)); + this.accessors.forEach((accessor) -> accessor.resetValues(values)); } + } diff --git a/spring-graphql/src/main/java/org/springframework/graphql/execution/ContextDataFetcherDecorator.java b/spring-graphql/src/main/java/org/springframework/graphql/execution/ContextDataFetcherDecorator.java index e3cea765..90906458 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/execution/ContextDataFetcherDecorator.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/execution/ContextDataFetcherDecorator.java @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.graphql.execution; import graphql.ExecutionInput; @@ -41,21 +42,21 @@ import org.springframework.util.Assert; *
  • Re-establish Reactor Context passed via {@link ExecutionInput}. *
  • Re-establish ThreadLocal context passed via {@link ExecutionInput}. * + * + * @author Rossen Stoyanchev */ -class ContextDataFetcherDecorator implements DataFetcher { +final class ContextDataFetcherDecorator implements DataFetcher { private final DataFetcher delegate; private final boolean subscription; - private ContextDataFetcherDecorator(DataFetcher delegate, boolean subscription) { Assert.notNull(delegate, "'delegate' DataFetcher is required"); this.delegate = delegate; this.subscription = subscription; } - @Override public Object get(DataFetchingEnvironment environment) throws Exception { ContextView contextView = ContextManager.getReactorContext(environment); @@ -88,16 +89,15 @@ class ContextDataFetcherDecorator implements DataFetcher { return value; } - /** - * {@link GraphQLTypeVisitor} that wraps non-GraphQL data fetchers and - * adapts them if they return {@link Flux} or {@link Mono}. + * {@link GraphQLTypeVisitor} that wraps non-GraphQL data fetchers and adapts them if + * they return {@link Flux} or {@link Mono}. */ static GraphQLTypeVisitor TYPE_VISITOR = new GraphQLTypeVisitorStub() { @Override - public TraversalControl visitGraphQLFieldDefinition( - GraphQLFieldDefinition fieldDefinition, TraverserContext context) { + public TraversalControl visitGraphQLFieldDefinition(GraphQLFieldDefinition fieldDefinition, + TraverserContext context) { GraphQLCodeRegistry.Builder codeRegistry = context.getVarFromParents(GraphQLCodeRegistry.Builder.class); GraphQLFieldsContainer parent = (GraphQLFieldsContainer) context.getParentNode(); diff --git a/spring-graphql/src/main/java/org/springframework/graphql/execution/ContextManager.java b/spring-graphql/src/main/java/org/springframework/graphql/execution/ContextManager.java index 9beff012..5f55506a 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/execution/ContextManager.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/execution/ContextManager.java @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.graphql.execution; import java.util.LinkedHashMap; @@ -27,32 +28,34 @@ import reactor.util.context.ContextView; import org.springframework.lang.Nullable; /** - * Package private utility class for propagating a Reactor {@link ContextView} - * through the {@link ExecutionInput} and the {@link DataFetchingEnvironment} - * of a request. + * Package private utility class for propagating a Reactor {@link ContextView} through the + * {@link ExecutionInput} and the {@link DataFetchingEnvironment} of a request. + * + * @author Rossen Stoyanchev + * @since 1.0.0 */ public abstract class ContextManager { - private static final String CONTEXT_VIEW_KEY = - ContextManager.class.getName() + ".CONTEXT_VIEW"; + private static final String CONTEXT_VIEW_KEY = ContextManager.class.getName() + ".CONTEXT_VIEW"; - private static final String THREAD_LOCAL_VALUES_KEY = - ContextManager.class.getName() + ".THREAD_VALUES_ACCESSOR"; - - private static final String THREAD_LOCAL_ACCESSOR_KEY = - ContextManager.class.getName() + ".THREAD_LOCAL_ACCESSOR"; + private static final String THREAD_LOCAL_VALUES_KEY = ContextManager.class.getName() + ".THREAD_VALUES_ACCESSOR"; + private static final String THREAD_LOCAL_ACCESSOR_KEY = ContextManager.class.getName() + ".THREAD_LOCAL_ACCESSOR"; /** - * Save the given Reactor ContextView in the an {@link ExecutionInput} for + * Save the given Reactor {@link ContextView} in the an {@link ExecutionInput} for * later access through the {@link DataFetchingEnvironment}. + * @param contextView the reactor context view + * @param input the GraphQL query input */ static void setReactorContext(ContextView contextView, ExecutionInput input) { ((GraphQLContext) input.getContext()).put(CONTEXT_VIEW_KEY, contextView); } /** - * Return the Reactor ContextView saved in the given DataFetchingEnvironment. + * Return the Reactor {@link ContextView} saved in the given DataFetchingEnvironment. + * @param environment the DataFetchingEnvironment + * @return the reactor {@link ContextView} */ static ContextView getReactorContext(DataFetchingEnvironment environment) { GraphQLContext graphQlContext = environment.getContext(); @@ -60,9 +63,10 @@ public abstract class ContextManager { } /** - * Use the given accessor to extract ThreadLocal value, and return a Reactor - * context that contains both the extracted values and the accessor. + * Use the given accessor to extract ThreadLocal value, and return a Reactor context + * that contains both the extracted values and the accessor. * @param accessor the accessor to use + * @return the reactor {@link ContextView} */ public static ContextView extractThreadLocalValues(ThreadLocalAccessor accessor) { Map valuesMap = new LinkedHashMap<>(); @@ -72,6 +76,7 @@ public abstract class ContextManager { /** * Look up saved ThreadLocal values and use them to re-establish ThreadLocal context. + * @param contextView the reactor {@link ContextView} */ static void restoreThreadLocalValues(ContextView contextView) { ThreadLocalAccessor accessor = getThreadLocalAccessor(contextView); @@ -82,6 +87,7 @@ public abstract class ContextManager { /** * Look up saved ThreadLocal values and remove associated ThreadLocal context. + * @param contextView the reactor {@link ContextView} */ static void resetThreadLocalValues(ContextView contextView) { ThreadLocalAccessor accessor = getThreadLocalAccessor(contextView); diff --git a/spring-graphql/src/main/java/org/springframework/graphql/execution/DataFetcherExceptionResolver.java b/spring-graphql/src/main/java/org/springframework/graphql/execution/DataFetcherExceptionResolver.java index 19d7966c..3b49dac7 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/execution/DataFetcherExceptionResolver.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/execution/DataFetcherExceptionResolver.java @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.graphql.execution; import java.util.List; @@ -22,29 +23,32 @@ import graphql.schema.DataFetchingEnvironment; import reactor.core.publisher.Mono; /** - * Contract to resolve exceptions raised by {@link graphql.schema.DataFetcher}'s - * to {@code GraphQLError}'s to add to the GraphQL response. Implementations are - * typically declared as beans in Spring configuration and invoked in order until - * one emits a List. + * Contract to resolve exceptions raised by {@link graphql.schema.DataFetcher}'s to + * {@code GraphQLError}'s to add to the GraphQL response. Implementations are typically + * declared as beans in Spring configuration and invoked in order until one emits a List. * - *

    Use the {@link SingleErrorExceptionResolver} convenience adapter when you - * need to resolve exceptions to a single {@code GraphQLError} only. + *

    + * Use the {@link SingleErrorExceptionResolver} convenience adapter when you need to + * resolve exceptions to a single {@code GraphQLError} only. + * + * @author Rossen Stoyanchev + * @since 1.0.0 */ public interface DataFetcherExceptionResolver { /** * Resolve the given exception and return the error(s) to add to the response. - *

    Implementations can use - * {@link graphql.GraphqlErrorBuilder#newError(DataFetchingEnvironment)} to - * create an error with the coordinates of the target field, and use - * {@link ErrorType} to specify a category for the error. + *

    + * Implementations can use + * {@link graphql.GraphqlErrorBuilder#newError(DataFetchingEnvironment)} to create an + * error with the coordinates of the target field, and use {@link ErrorType} to + * specify a category for the error. * @param exception the exception to resolve * @param environment the environment for the invoked {@code DataFetcher} - * @return a {@code Mono} with errors to add to the GraphQL response; - * if the {@code Mono} completes with an empty List, the exception is - * resolved without any errors added to the response; - * if the {@code Mono} completes empty, without emitting a List, the - * exception remains unresolved and gives other resolvers a chance. + * @return a {@code Mono} with errors to add to the GraphQL response; if the + * {@code Mono} completes with an empty List, the exception is resolved without any + * errors added to the response; if the {@code Mono} completes empty, without emitting + * a List, the exception remains unresolved and gives other resolvers a chance. */ Mono> resolveException(Throwable exception, DataFetchingEnvironment environment); diff --git a/spring-graphql/src/main/java/org/springframework/graphql/execution/DefaultGraphQlSourceBuilder.java b/spring-graphql/src/main/java/org/springframework/graphql/execution/DefaultGraphQlSourceBuilder.java index 54497f6b..d1478072 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/execution/DefaultGraphQlSourceBuilder.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/execution/DefaultGraphQlSourceBuilder.java @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.graphql.execution; import java.io.IOException; @@ -38,8 +39,9 @@ import org.springframework.util.Assert; /** * Default implementation of {@link GraphQlSource.Builder} that initializes a - * {@link GraphQL} instance and wraps it with a {@link GraphQlSource} that - * returns it. + * {@link GraphQL} instance and wraps it with a {@link GraphQlSource} that returns it. + * + * @author Rossen Stoyanchev */ class DefaultGraphQlSourceBuilder implements GraphQlSource.Builder { @@ -54,14 +56,13 @@ class DefaultGraphQlSourceBuilder implements GraphQlSource.Builder { private final List instrumentations = new ArrayList<>(); - private Consumer graphQlConfigurers = builder -> {}; - + private Consumer graphQlConfigurers = (builder) -> { + }; DefaultGraphQlSourceBuilder() { this.typeVisitors.add(ContextDataFetcherDecorator.TYPE_VISITOR); } - @Override public GraphQlSource.Builder schemaResource(Resource resource) { this.schemaResource = resource; @@ -128,12 +129,10 @@ class DefaultGraphQlSourceBuilder implements GraphQlSource.Builder { } } catch (IOException ex) { - throw new IllegalArgumentException( - "Failed to load resourceLocation " + this.schemaResource.toString()); + throw new IllegalArgumentException("Failed to load resourceLocation " + this.schemaResource.toString()); } } - /** * GraphQlSource that returns the built GraphQL instance and its schema. */ @@ -157,6 +156,7 @@ class DefaultGraphQlSourceBuilder implements GraphQlSource.Builder { public GraphQLSchema schema() { return this.schema; } + } } diff --git a/spring-graphql/src/main/java/org/springframework/graphql/execution/ErrorType.java b/spring-graphql/src/main/java/org/springframework/graphql/execution/ErrorType.java index 3887b0bf..05828b51 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/execution/ErrorType.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/execution/ErrorType.java @@ -13,46 +13,49 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.graphql.execution; import graphql.ErrorClassification; /** * Common categories to use to classify for exceptions raised by - * {@link graphql.schema.DataFetcher}'s that can enable a client to make - * automated decisions. + * {@link graphql.schema.DataFetcher}'s that can enable a client to make automated + * decisions. * + * @author Rossen Stoyanchev + * @since 1.0.0 * @see graphql.GraphqlErrorBuilder#errorType(ErrorClassification) */ public enum ErrorType implements ErrorClassification { /** - * {@link graphql.schema.DataFetcher} cannot or will not fetch the data value - * due to something that is perceived to be a client error. + * {@link graphql.schema.DataFetcher} cannot or will not fetch the data value due to + * something that is perceived to be a client error. */ BAD_REQUEST, /** - * {@link graphql.schema.DataFetcher} did not fetch the data value due to a - * lack of valid authentication credentials. + * {@link graphql.schema.DataFetcher} did not fetch the data value due to a lack of + * valid authentication credentials. */ UNAUTHORIZED, /** - * {@link graphql.schema.DataFetcher} refuses to authorize the fetching of - * the data value. + * {@link graphql.schema.DataFetcher} refuses to authorize the fetching of the data + * value. */ FORBIDDEN, /** - * {@link graphql.schema.DataFetcher} did not find a data value or is not - * willing to disclose that one exists. + * {@link graphql.schema.DataFetcher} did not find a data value or is not willing to + * disclose that one exists. */ NOT_FOUND, /** - * {@link graphql.schema.DataFetcher} encountered an unexpected condition - * that prevented it from fetching the data value. + * {@link graphql.schema.DataFetcher} encountered an unexpected condition that + * prevented it from fetching the data value. */ INTERNAL_ERROR; diff --git a/spring-graphql/src/main/java/org/springframework/graphql/execution/ExceptionResolversExceptionHandler.java b/spring-graphql/src/main/java/org/springframework/graphql/execution/ExceptionResolversExceptionHandler.java index d7c485d2..08d22f22 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/execution/ExceptionResolversExceptionHandler.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/execution/ExceptionResolversExceptionHandler.java @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.graphql.execution; import java.util.ArrayList; @@ -37,48 +38,42 @@ import org.springframework.web.client.ExtractingResponseErrorHandler; /** * {@link DataFetcherExceptionHandler} that invokes {@link DataFetcherExceptionResolver}'s * in a sequence until one returns a non-null list of {@link GraphQLError}'s. + * + * @author Rossen Stoyanchev */ class ExceptionResolversExceptionHandler implements DataFetcherExceptionHandler { private static Log logger = LogFactory.getLog(ExtractingResponseErrorHandler.class); - private final List resolvers; - /** - * Create an instance + * Create an instance. * @param resolvers the resolvers to use */ - public ExceptionResolversExceptionHandler(List resolvers) { + ExceptionResolversExceptionHandler(List resolvers) { Assert.notNull(resolvers, "'resolvers' is required"); this.resolvers = new ArrayList<>(resolvers); } - @Override public DataFetcherExceptionHandlerResult onException(DataFetcherExceptionHandlerParameters parameters) { Throwable exception = parameters.getException(); - exception = (exception instanceof CompletionException ? exception.getCause() : exception); + exception = ((exception instanceof CompletionException) ? exception.getCause() : exception); return invokeChain(exception, parameters.getDataFetchingEnvironment()); } @SuppressWarnings("ConstantConditions") - public DataFetcherExceptionHandlerResult invokeChain(Throwable ex, DataFetchingEnvironment env) { + DataFetcherExceptionHandlerResult invokeChain(Throwable ex, DataFetchingEnvironment env) { // For now we have to block: // https://github.com/graphql-java/graphql-java/issues/2356 try { - return Flux.fromIterable(this.resolvers) - .flatMap(resolver -> resolver.resolveException(ex, env)) - .next() - .map(errors -> DataFetcherExceptionHandlerResult.newResult().errors(errors).build()) - .switchIfEmpty(Mono.fromCallable(() -> applyDefaultHandling(ex, env))) - .contextWrite(context -> { + return Flux.fromIterable(this.resolvers).flatMap((resolver) -> resolver.resolveException(ex, env)).next() + .map((errors) -> DataFetcherExceptionHandlerResult.newResult().errors(errors).build()) + .switchIfEmpty(Mono.fromCallable(() -> applyDefaultHandling(ex, env))).contextWrite((context) -> { ContextView contextView = ContextManager.getReactorContext(env); return (contextView.isEmpty() ? context : context.putAll(contextView)); - }) - .toFuture() - .get(); + }).toFuture().get(); } catch (Exception ex2) { if (logger.isWarnEnabled()) { @@ -89,10 +84,8 @@ class ExceptionResolversExceptionHandler implements DataFetcherExceptionHandler } private DataFetcherExceptionHandlerResult applyDefaultHandling(Throwable ex, DataFetchingEnvironment env) { - GraphQLError error = GraphqlErrorBuilder.newError(env) - .message(ex.getMessage()) - .errorType(ErrorType.INTERNAL_ERROR) - .build(); + GraphQLError error = GraphqlErrorBuilder.newError(env).message(ex.getMessage()) + .errorType(ErrorType.INTERNAL_ERROR).build(); return DataFetcherExceptionHandlerResult.newResult(error).build(); } diff --git a/spring-graphql/src/main/java/org/springframework/graphql/execution/ExecutionGraphQlService.java b/spring-graphql/src/main/java/org/springframework/graphql/execution/ExecutionGraphQlService.java index b3cefd08..8d958a5c 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/execution/ExecutionGraphQlService.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/execution/ExecutionGraphQlService.java @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.graphql.execution; import graphql.ExecutionInput; @@ -25,21 +26,22 @@ import org.springframework.graphql.GraphQlService; /** * Implementation of {@link GraphQlService} that performs GraphQL request execution * through {@link GraphQL#executeAsync(ExecutionInput)}. + * + * @author Rossen Stoyanchev + * @since 1.0.0 */ public class ExecutionGraphQlService implements GraphQlService { private final GraphQlSource graphQlSource; - public ExecutionGraphQlService(GraphQlSource graphQlSource) { this.graphQlSource = graphQlSource; } - @Override public Mono execute(ExecutionInput input) { GraphQL graphQl = this.graphQlSource.graphQl(); - return Mono.deferContextual(contextView -> { + return Mono.deferContextual((contextView) -> { ContextManager.setReactorContext(contextView, input); return Mono.fromFuture(graphQl.executeAsync(input)); }); diff --git a/spring-graphql/src/main/java/org/springframework/graphql/execution/GraphQlSource.java b/spring-graphql/src/main/java/org/springframework/graphql/execution/GraphQlSource.java index aeee7005..16491478 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/execution/GraphQlSource.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/execution/GraphQlSource.java @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.graphql.execution; import java.io.File; @@ -31,34 +32,38 @@ import org.springframework.core.io.Resource; /** * Strategy to resolve the {@link GraphQL} instance to use. * - *

    This contract also includes a {@link GraphQlSource} builder encapsulating - * the initialization of the {@link GraphQL} instance and associated + *

    + * This contract also includes a {@link GraphQlSource} builder encapsulating the + * initialization of the {@link GraphQL} instance and associated * {@link graphql.schema.GraphQLSchema}. + * + * @author Rossen Stoyanchev + * @since 1.0.0 */ public interface GraphQlSource { - /** - * Return the {@link GraphQL} to use. This can be a cached instance or a - * different one from time to time (e.g. based on a reloaded schema). + * Return the {@link GraphQL} to use. This can be a cached instance or a different one + * from time to time (e.g. based on a reloaded schema). + * @return the GraphQL instance to use */ GraphQL graphQl(); /** * Return the {@link GraphQLSchema} used by the current {@link GraphQL}. + * @return the current GraphQL schema */ GraphQLSchema schema(); - /** - * Return a builder for a {@link GraphQlSource} given input for the - * initialization of {@link GraphQL} and {@link graphql.schema.GraphQLSchema}. + * Return a builder for a {@link GraphQlSource} given input for the initialization of + * {@link GraphQL} and {@link graphql.schema.GraphQLSchema}. + * @return a builder for a GraphQlSource */ static Builder builder() { return new DefaultGraphQlSourceBuilder(); } - /** * Builder for a {@link GraphQlSource}. */ @@ -66,47 +71,62 @@ public interface GraphQlSource { /** * Provide the resource for the GraphQL {@literal ".schema"} file to parse. + * @param resource the resource for the GraphQL schema + * @return the current builder * @see graphql.schema.idl.SchemaParser#parse(File) */ Builder schemaResource(Resource resource); /** * Set a {@link RuntimeWiring} to contribute data fetchers and more. - * @see graphql.schema.idl.SchemaGenerator#makeExecutableSchema(TypeDefinitionRegistry, RuntimeWiring) + * @param runtimeWiring the runtime wiring for contribution + * @return the current builder + * @see graphql.schema.idl.SchemaGenerator#makeExecutableSchema(TypeDefinitionRegistry, + * RuntimeWiring) */ Builder runtimeWiring(RuntimeWiring runtimeWiring); /** - * Add {@link DataFetcherExceptionResolver}'s to use for resolving - * exceptions from {@link graphql.schema.DataFetcher}'s. + * Add {@link DataFetcherExceptionResolver}'s to use for resolving exceptions from + * {@link graphql.schema.DataFetcher}'s. * @param resolvers the resolvers to add + * @return the current builder */ Builder exceptionResolvers(List resolvers); /** * Add {@link GraphQLTypeVisitor}'s to transform the underlying * {@link graphql.schema.GraphQLSchema} with. - * @see graphql.schema.SchemaTransformer#transformSchema(GraphQLSchema, GraphQLTypeVisitor) + * @param typeVisitors the type visitors + * @return the current builder + * @see graphql.schema.SchemaTransformer#transformSchema(GraphQLSchema, + * GraphQLTypeVisitor) */ Builder typeVisitors(List typeVisitors); /** - * Provide {@link Instrumentation} components to instrument the execution - * of GraphQL queries. + * Provide {@link Instrumentation} components to instrument the execution of + * GraphQL queries. + * @param instrumentations the instrumentation components + * @return the current builder * @see graphql.GraphQL.Builder#instrumentation(Instrumentation) */ Builder instrumentation(List instrumentations); /** - * Configure consumers to be given access to the {@link GraphQL.Builder} - * used to build {@link GraphQL}. + * Configure consumers to be given access to the {@link GraphQL.Builder} used to + * build {@link GraphQL}. + * @param configurer the configurer + * @return the current builder */ Builder configureGraphQl(Consumer configurer); /** * Build the {@link GraphQlSource}. + * @return the built GraphQlSource */ GraphQlSource build(); + } } diff --git a/spring-graphql/src/main/java/org/springframework/graphql/execution/SyncDataFetcherExceptionResolver.java b/spring-graphql/src/main/java/org/springframework/graphql/execution/SyncDataFetcherExceptionResolver.java index 4f30fbe5..e0cca317 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/execution/SyncDataFetcherExceptionResolver.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/execution/SyncDataFetcherExceptionResolver.java @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.graphql.execution; import java.util.List; @@ -24,6 +25,9 @@ import reactor.util.context.ContextView; /** * {@link DataFetcherExceptionResolver} that resolves exceptions synchronously. + * + * @author Rossen Stoyanchev + * @since 1.0.0 */ public interface SyncDataFetcherExceptionResolver extends DataFetcherExceptionResolver { @@ -43,6 +47,7 @@ public interface SyncDataFetcherExceptionResolver extends DataFetcherExceptionRe * Implement this method to resolve exceptions. * @param exception the exception to resolve * @param environment the environment for the invoked {@code DataFetcher} + * @return the list of resolved GraphQL errors */ List doResolveException(Throwable exception, DataFetchingEnvironment environment); diff --git a/spring-graphql/src/main/java/org/springframework/graphql/execution/ThreadLocalAccessor.java b/spring-graphql/src/main/java/org/springframework/graphql/execution/ThreadLocalAccessor.java index 59297e94..75c9f7d0 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/execution/ThreadLocalAccessor.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/execution/ThreadLocalAccessor.java @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.graphql.execution; import java.util.List; @@ -21,39 +22,41 @@ import java.util.Map; import org.springframework.beans.factory.ObjectProvider; /** - * Interface to be implemented by a framework or an application in order to - * assist with extracting ThreadLocal values at the web layer, which can then be - * re-established for DataFetcher's that are potentially executing on a - * different thread. + * Interface to be implemented by a framework or an application in order to assist with + * extracting ThreadLocal values at the web layer, which can then be re-established for + * DataFetcher's that are potentially executing on a different thread. * - *

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

    + * Implementations may be declared as beans in Spring configuration and ordered as defined + * in {@link ObjectProvider#orderedStream()}. + * + * @author Rossen Stoyanchev + * @since 1.0.0 */ public interface ThreadLocalAccessor { /** - * Extract ThreadLocal values and add them to the given Map which is then - * passed to {@link #restoreValues(Map)} and {@link #resetValues(Map)} - * before and after the execution of a {@link graphql.schema.DataFetcher}. + * Extract ThreadLocal values and add them to the given Map which is then passed to + * {@link #restoreValues(Map)} and {@link #resetValues(Map)} before and after the + * execution of a {@link graphql.schema.DataFetcher}. * @param container container for ThreadLocal values */ void extractValues(Map container); /** - * Re-establish ThreadLocal context by looking up values, previously - * extracted via {@link #extractValues(Map)}. + * Re-establish ThreadLocal context by looking up values, previously extracted via + * {@link #extractValues(Map)}. * @param values the saved ThreadLocal values */ void restoreValues(Map values); /** - * Reset ThreadLocal context for the given values, previously extracted - * via {@link #extractValues(Map)}. + * Reset ThreadLocal context for the given values, previously extracted via + * {@link #extractValues(Map)}. * @param values the saved ThreadLocal values */ void resetValues(Map values); - /** * Create a composite accessor that delegates to all of the given accessors. * @param accessors the accessors to aggregate diff --git a/spring-graphql/src/main/java/org/springframework/graphql/execution/package-info.java b/spring-graphql/src/main/java/org/springframework/graphql/execution/package-info.java index 70567d52..f4eb7f0d 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/execution/package-info.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/execution/package-info.java @@ -1,6 +1,22 @@ +/* + * 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. + */ + /** - * Support for GraphQL request execution, including abstractions to configure - * and invoke {@link graphql.GraphQL}. + * Support for GraphQL request execution, including abstractions to configure and invoke + * {@link graphql.GraphQL}. */ @NonNullApi @NonNullFields diff --git a/spring-graphql/src/main/java/org/springframework/graphql/package-info.java b/spring-graphql/src/main/java/org/springframework/graphql/package-info.java index cc73a75d..6ea9c77e 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/package-info.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/package-info.java @@ -1,8 +1,23 @@ +/* + * 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. + */ + /** * Top level abstractions for processing GraphQL requests including * {@link org.springframework.graphql.GraphQlService} for executing a request and - * {@link org.springframework.graphql.RequestInput} to represent the input for - * a request. + * {@link org.springframework.graphql.RequestInput} to represent the input for a request. */ @NonNullApi @NonNullFields diff --git a/spring-graphql/src/main/java/org/springframework/graphql/web/DefaultWebGraphQlHandlerBuilder.java b/spring-graphql/src/main/java/org/springframework/graphql/web/DefaultWebGraphQlHandlerBuilder.java index 8fb6c8a6..26ca35be 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/web/DefaultWebGraphQlHandlerBuilder.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/web/DefaultWebGraphQlHandlerBuilder.java @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.graphql.web; import java.util.ArrayList; @@ -33,6 +34,8 @@ import org.springframework.util.CollectionUtils; /** * Default implementation of {@link WebGraphQlHandler.Builder}. + * + * @author Rossen Stoyanchev */ class DefaultWebGraphQlHandlerBuilder implements WebGraphQlHandler.Builder { @@ -44,13 +47,11 @@ class DefaultWebGraphQlHandlerBuilder implements WebGraphQlHandler.Builder { @Nullable private List accessors; - DefaultWebGraphQlHandlerBuilder(GraphQlService service) { Assert.notNull(service, "GraphQlService is required"); this.service = service; } - @Override public WebGraphQlHandler.Builder interceptor(WebInterceptor... interceptors) { return interceptors(Arrays.asList(interceptors)); @@ -59,7 +60,7 @@ class DefaultWebGraphQlHandlerBuilder implements WebGraphQlHandler.Builder { @Override public WebGraphQlHandler.Builder interceptors(List interceptors) { if (!CollectionUtils.isEmpty(interceptors)) { - this.interceptors = (this.interceptors != null ? this.interceptors : new ArrayList<>()); + this.interceptors = (this.interceptors != null) ? this.interceptors : new ArrayList<>(); this.interceptors.addAll(interceptors); } return this; @@ -73,7 +74,7 @@ class DefaultWebGraphQlHandlerBuilder implements WebGraphQlHandler.Builder { @Override public WebGraphQlHandler.Builder threadLocalAccessors(List accessors) { if (!CollectionUtils.isEmpty(accessors)) { - this.accessors = (this.accessors != null ? this.accessors : new ArrayList<>()); + this.accessors = (this.accessors != null) ? this.accessors : new ArrayList<>(); this.accessors.addAll(accessors); } return this; @@ -81,27 +82,25 @@ class DefaultWebGraphQlHandlerBuilder implements WebGraphQlHandler.Builder { @Override public WebGraphQlHandler build() { - List interceptorsToUse = - (this.interceptors != null ? this.interceptors : Collections.emptyList()); + List interceptorsToUse = (this.interceptors != null) ? this.interceptors + : Collections.emptyList(); - WebGraphQlHandler targetHandler = webInput -> { + WebGraphQlHandler targetHandler = (webInput) -> { ExecutionInput executionInput = webInput.toExecutionInput(); - return this.service.execute(executionInput).map(result -> new WebOutput(webInput, result)); + return this.service.execute(executionInput).map((result) -> new WebOutput(webInput, result)); }; - WebGraphQlHandler interceptionChain = interceptorsToUse.stream() - .reduce(WebInterceptor::andThen) - .map(interceptor -> (WebGraphQlHandler) input -> interceptor.intercept(input, targetHandler)) + WebGraphQlHandler interceptionChain = interceptorsToUse.stream().reduce(WebInterceptor::andThen) + .map((interceptor) -> (WebGraphQlHandler) (input) -> interceptor.intercept(input, targetHandler)) .orElse(targetHandler); - return (CollectionUtils.isEmpty(this.accessors) ? interceptionChain : - new ThreadLocalExtractingHandler(interceptionChain, ThreadLocalAccessor.composite(this.accessors))); + return (CollectionUtils.isEmpty(this.accessors) ? interceptionChain + : new ThreadLocalExtractingHandler(interceptionChain, ThreadLocalAccessor.composite(this.accessors))); } - /** - * {@link WebGraphQlHandler} that extracts ThreadLocal values and saves them - * in the Reactor context for subsequent use for DataFetcher's. + * {@link WebGraphQlHandler} that extracts ThreadLocal values and saves them in the + * Reactor context for subsequent use for DataFetcher's. */ private static class ThreadLocalExtractingHandler implements WebGraphQlHandler { @@ -116,12 +115,12 @@ class DefaultWebGraphQlHandlerBuilder implements WebGraphQlHandler.Builder { @Override public Mono handle(WebInput input) { - return this.delegate.handle(input) - .contextWrite(context -> { - ContextView view = ContextManager.extractThreadLocalValues(this.accessor); - return (!view.isEmpty() ? context.putAll(view) : context); - }); + return this.delegate.handle(input).contextWrite((context) -> { + ContextView view = ContextManager.extractThreadLocalValues(this.accessor); + return (!view.isEmpty() ? context.putAll(view) : context); + }); } + } } diff --git a/spring-graphql/src/main/java/org/springframework/graphql/web/WebGraphQlHandler.java b/spring-graphql/src/main/java/org/springframework/graphql/web/WebGraphQlHandler.java index 300875e0..557ff56f 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/web/WebGraphQlHandler.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/web/WebGraphQlHandler.java @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.graphql.web; import java.util.List; @@ -23,30 +24,31 @@ import org.springframework.graphql.GraphQlService; import org.springframework.graphql.execution.ThreadLocalAccessor; /** - * Contract to handle a GraphQL over HTTP or WebSocket request that forms the - * basis of a {@link WebInterceptor} delegation chain. + * Contract to handle a GraphQL over HTTP or WebSocket request that forms the basis of a + * {@link WebInterceptor} delegation chain. + * + * @author Rossen Stoyanchev + * @since 1.0.0 */ public interface WebGraphQlHandler { /** * Perform request execution for the given input and return the result. - * * @param input the GraphQL request input container * @return the execution result */ Mono handle(WebInput input); - /** * Provides access to a builder to create a {@link WebGraphQlHandler} instance. - * @param graphQlService the {@link GraphQlService} to use for actual - * execution of the request. + * @param graphQlService the {@link GraphQlService} to use for actual execution of the + * request. + * @return a builder for a WebGraphQlHandler */ static Builder builder(GraphQlService graphQlService) { return new DefaultWebGraphQlHandlerBuilder(graphQlService); } - /** * Builder for {@link WebGraphQlHandler} that represents a {@link WebInterceptor} * chain followed by a {@link GraphQlService}. @@ -56,31 +58,40 @@ public interface WebGraphQlHandler { /** * Configure interceptors to be invoked before the target {@code GraphQlService}. * @param interceptors the interceptors to add + * @return this builder */ Builder interceptor(WebInterceptor... interceptors); /** * Alternative to {@link #interceptor(WebInterceptor...)} with a List. + * @param interceptors the list of interceptors to add + * @return this builder */ Builder interceptors(List interceptors); /** - * Configure accessors for ThreadLocal variables to use to extract - * ThreadLocal values at the Web framework level, have those propagated - * and re-established at the DataFetcher level. + * Configure accessors for ThreadLocal variables to use to extract ThreadLocal + * values at the Web framework level, have those propagated and re-established at + * the DataFetcher level. * @param accessors the accessors to add + * @return this builder */ Builder threadLocalAccessor(ThreadLocalAccessor... accessors); /** - * Alternative to {@link #threadLocalAccessor(ThreadLocalAccessor...)} with a List. + * Alternative to {@link #threadLocalAccessor(ThreadLocalAccessor...)} with a + * List. + * @param accessors the list of accessors to add + * @return this builder */ Builder threadLocalAccessors(List accessors); /** * Build the {@link WebGraphQlHandler} instance. + * @return the built WebGraphQlHandler */ WebGraphQlHandler build(); + } } diff --git a/spring-graphql/src/main/java/org/springframework/graphql/web/WebInput.java b/spring-graphql/src/main/java/org/springframework/graphql/web/WebInput.java index 3eb16d44..e49927db 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/web/WebInput.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/web/WebInput.java @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.graphql.web; import java.net.URI; @@ -30,8 +31,11 @@ import org.springframework.web.util.UriComponentsBuilder; /** * Container for the input of a GraphQL query over HTTP. The input includes the - * {@link UriComponents URL} and the headers of the request, as well as the - * query name, operation name, and variables from the request body. + * {@link UriComponents URL} and the headers of the request, as well as the query name, + * operation name, and variables from the request body. + * + * @author Rossen Stoyanchev + * @since 1.0.0 */ public class WebInput extends RequestInput { @@ -41,15 +45,14 @@ public class WebInput extends RequestInput { private final String id; - /** * Create an instance. * @param uri the url for the HTTP request, or WebSocket handshake * @param headers the HTTP request headers * @param body the content of the request deserialized from JSON - * @param id an identifier for the GraphQL request, e.g. a subscription id - * for correlating request and response messages, or it could be an id - * associated with the underlying request/connection id, if available + * @param id an identifier for the GraphQL request, e.g. a subscription id for + * correlating request and response messages, or it could be an id associated with the + * underlying request/connection id, if available */ public WebInput(URI uri, HttpHeaders headers, Map body, @Nullable String id) { super(validateQuery(body)); @@ -57,7 +60,7 @@ public class WebInput extends RequestInput { Assert.notNull(headers, "HttpHeaders is required'"); this.uri = UriComponentsBuilder.fromUri(uri).build(true); this.headers = headers; - this.id = (id != null ? id : ObjectUtils.identityToString(this)); + this.id = (id != null) ? id : ObjectUtils.identityToString(this); } private static Map validateQuery(Map body) { @@ -68,10 +71,10 @@ public class WebInput extends RequestInput { return body; } - /** - * Return the URI of the HTTP request including - * {@link UriComponents#getQueryParams() URL query parameters}. + * Return the URI of the HTTP request including {@link UriComponents#getQueryParams() + * URL query parameters}. + * @return the HTTP request URI */ public UriComponents getUri() { return this.uri; @@ -79,6 +82,7 @@ public class WebInput extends RequestInput { /** * Return the headers of the request. + * @return the HTTP request headers */ public HttpHeaders getHeaders() { return this.headers; @@ -86,13 +90,14 @@ public class WebInput extends RequestInput { /** * Return the identifier for the request, which may be a subscription id for - * correlating request and response messages, or the underlying request or - * connection id, when available, or otherwise it's an + * correlating request and response messages, or the underlying request or connection + * id, when available, or otherwise it's an * {@link ObjectUtils#identityToString(Object) identity} hash based this * {@code WebInput} instance. + * @return the HTTP request identifier */ public String getId() { return this.id; } -} \ No newline at end of file +} diff --git a/spring-graphql/src/main/java/org/springframework/graphql/web/WebInterceptor.java b/spring-graphql/src/main/java/org/springframework/graphql/web/WebInterceptor.java index 203d1114..6a91321d 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/web/WebInterceptor.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/web/WebInterceptor.java @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.graphql.web; import graphql.ExecutionInput; @@ -23,36 +24,41 @@ import org.springframework.beans.factory.ObjectProvider; import org.springframework.util.Assert; /** - * 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 request execution. + * 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 request execution. * - *

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

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

    Supported for Spring MVC and WebFlux. + *

    + * Supported for Spring MVC and WebFlux. + * + * @author Rossen Stoyanchev + * @since 1.0.0 */ public interface WebInterceptor { /** - * Intercept a request and delegate for further handling and request execution - * via {@link WebGraphQlHandler#handle(WebInput)}. - * - * @param webInput container with HTTP request information and options to - * customize the {@link ExecutionInput}. + * Intercept a request and delegate for further handling and request execution via + * {@link WebGraphQlHandler#handle(WebInput)}. + * @param webInput container with HTTP request information and options to customize + * the {@link ExecutionInput}. * @param next the handler to delegate to for request execution * @return a {@link Mono} with the result */ Mono intercept(WebInput webInput, WebGraphQlHandler next); /** - * Return a composed {@link WebInterceptor} that invokes the current - * interceptor first one and then the one one passed in. + * Return a composed {@link WebInterceptor} that invokes the current interceptor first + * one and then the one one passed in. + * @param interceptor the interceptor to compose the current one with + * @return the composed WebInterceptor */ default WebInterceptor andThen(WebInterceptor interceptor) { Assert.notNull(interceptor, "WebInterceptor must not be null"); - return (currentInput, next) -> intercept(currentInput, nextInput -> interceptor.intercept(nextInput, next)); + return (currentInput, next) -> intercept(currentInput, (nextInput) -> interceptor.intercept(nextInput, next)); } -} \ No newline at end of file +} diff --git a/spring-graphql/src/main/java/org/springframework/graphql/web/WebOutput.java b/spring-graphql/src/main/java/org/springframework/graphql/web/WebOutput.java index c428681a..829155a7 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/web/WebOutput.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/web/WebOutput.java @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.graphql.web; import java.util.Collections; @@ -28,11 +29,13 @@ import org.springframework.http.HttpHeaders; import org.springframework.lang.Nullable; import org.springframework.util.Assert; - /** - * Decorate an {@link ExecutionResult}, provide a way to - * {@link #transform(Consumer) transform} it, and collect input for custom - * HTTP response headers for GraphQL over HTTP requests. + * Decorate an {@link ExecutionResult}, provide a way to {@link #transform(Consumer) + * transform} it, and collect input for custom HTTP response headers for GraphQL over HTTP + * requests. + * + * @author Rossen Stoyanchev + * @since 1.0.0 */ public class WebOutput implements ExecutionResult { @@ -43,9 +46,10 @@ public class WebOutput implements ExecutionResult { @Nullable private final HttpHeaders responseHeaders; - /** * Create an instance that wraps the given {@link ExecutionResult}. + * @param input the container for the GraphQL input + * @param executionResult the result of performing a graphql query */ public WebOutput(WebInput input, ExecutionResult executionResult) { this(input, executionResult, null); @@ -59,9 +63,9 @@ public class WebOutput implements ExecutionResult { this.responseHeaders = responseHeaders; } - /** * Return the associated {@link WebInput} used for the execution. + * @return the associated WebInput */ public WebInput getWebInput() { return this.input; @@ -93,20 +97,22 @@ public class WebOutput implements ExecutionResult { } /** - * Return a read-only view of any custom headers to be added to the HTTP - * response, or {@code null} until {@link #transform(Consumer)} is used to - * add such headers. + * Return a read-only view of any custom headers to be added to the HTTP response, or + * {@code null} until {@link #transform(Consumer)} is used to add such headers. + * @return the read-only HTTP response headers * @see #transform(Consumer) * @see Builder#responseHeader(String, String...) */ @Nullable public HttpHeaders getResponseHeaders() { - return (this.responseHeaders != null ? HttpHeaders.readOnlyHttpHeaders(this.responseHeaders) : null); + return (this.responseHeaders != null) ? HttpHeaders.readOnlyHttpHeaders(this.responseHeaders) : null; } /** - * Transform this {@code WebOutput} instance through a {@link Builder} and - * return a new instance with the modified values. + * Transform this {@code WebOutput} instance through a {@link Builder} and return a + * new instance with the modified values. + * @param consumer teh callback that will transform the WebOutput + * @return the transformed WebOutput */ public WebOutput transform(Consumer consumer) { Builder builder = new Builder(this); @@ -114,11 +120,10 @@ public class WebOutput implements ExecutionResult { return builder.build(); } - /** * Builder to transform a {@link WebOutput}. */ - public static class Builder { + public static final class Builder { private final WebInput input; @@ -133,7 +138,6 @@ public class WebOutput implements ExecutionResult { @Nullable private HttpHeaders headers; - private Builder(WebOutput output) { this.input = output.getWebInput(); this.data = output.getData(); @@ -142,10 +146,10 @@ public class WebOutput implements ExecutionResult { this.headers = output.responseHeaders; } - /** - * Set the {@link ExecutionResult#getData() data} of the GraphQL - * execution result. + * Set the {@link ExecutionResult#getData() data} of the GraphQL execution result. + * @param data the execution result data + * @return the current builder */ public Builder data(@Nullable Object data) { this.data = data; @@ -153,17 +157,21 @@ public class WebOutput implements ExecutionResult { } /** - * Set the {@link ExecutionResult#getErrors() errors} of the GraphQL - * execution result. + * Set the {@link ExecutionResult#getErrors() errors} of the GraphQL execution + * result. + * @param errors the execution result errors + * @return the current builder */ public Builder errors(@Nullable List errors) { - this.errors = (errors != null ? errors : Collections.emptyList()); + this.errors = (errors != null) ? errors : Collections.emptyList(); return this; } /** - * Set the {@link ExecutionResult#getExtensions() extensions} of the - * GraphQL execution result. + * Set the {@link ExecutionResult#getExtensions() extensions} of the GraphQL + * execution result. + * @param extensions the execution result extensions + * @return the current builder */ public Builder extensions(@Nullable Map extensions) { this.extensions = extensions; @@ -173,9 +181,13 @@ public class WebOutput implements ExecutionResult { /** * Add a custom header to be set on the HTTP response. * - *

    Note: This can be used for GraphQL over HTTP - * requests but has no impact for queries over a WebSocket session where - * the initial handshake request completes before queries begin. + *

    + * Note: This can be used for GraphQL over HTTP requests but has + * no impact for queries over a WebSocket session where the initial handshake + * request completes before queries begin. + * @param name the HTTP header name + * @param values the HTTP header values + * @return the current builder */ public Builder responseHeader(String name, String... values) { initHeaders(); @@ -188,9 +200,12 @@ public class WebOutput implements ExecutionResult { /** * Consume and update the headers to be set on the HTTP response. * - *

    Note: This can be used for GraphQL over HTTP - * requests but has no impact for queries over a WebSocket session where - * the initial handshake request completes before queries begin. + *

    + * Note: This can be used for GraphQL over HTTP requests but has + * no impact for queries over a WebSocket session where the initial handshake + * request completes before queries begin. + * @param consumer callback that updates the HTTP headers + * @return the current builder */ public Builder responseHeaders(Consumer consumer) { initHeaders(); @@ -199,13 +214,14 @@ public class WebOutput implements ExecutionResult { } private void initHeaders() { - this.headers = (this.headers != null ? this.headers : new HttpHeaders()); + this.headers = (this.headers != null) ? this.headers : new HttpHeaders(); } public WebOutput build() { ExecutionResult result = new ExecutionResultImpl(this.data, this.errors, this.extensions); return new WebOutput(this.input, result, this.headers); } + } } diff --git a/spring-graphql/src/main/java/org/springframework/graphql/web/package-info.java b/spring-graphql/src/main/java/org/springframework/graphql/web/package-info.java index 3a599cdf..6f0d761f 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/web/package-info.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/web/package-info.java @@ -1,10 +1,26 @@ +/* + * 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. + */ + /** - * Support for executing GraphQL requests over the Web, including handlers for - * HTTP and WebSocket. Handlers are provided for use in ether + * Support for executing GraphQL requests over the Web, including handlers for HTTP and + * WebSocket. Handlers are provided for use in ether * {@link org.springframework.graphql.web.webmvc Spring WebMvc} or * {@link org.springframework.graphql.web.webflux Spring WebFlux} with a common - * {@link org.springframework.graphql.web.WebInterceptor interception} model - * that allows applications to customize request input and output. + * {@link org.springframework.graphql.web.WebInterceptor interception} model that allows + * applications to customize request input and output. */ @NonNullApi @NonNullFields diff --git a/spring-graphql/src/main/java/org/springframework/graphql/web/webflux/GraphQlHttpHandler.java b/spring-graphql/src/main/java/org/springframework/graphql/web/webflux/GraphQlHttpHandler.java index cccd8f59..e9afac7c 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/web/webflux/GraphQlHttpHandler.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/web/webflux/GraphQlHttpHandler.java @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.graphql.web.webflux; import java.util.Map; @@ -30,18 +31,19 @@ import org.springframework.web.reactive.function.server.ServerResponse; /** * WebFlux.fn Handler for GraphQL over HTTP requests. + * + * @author Rossen Stoyanchev + * @since 1.0.0 */ public class GraphQlHttpHandler { private static final Log logger = LogFactory.getLog(GraphQlHttpHandler.class); - private static final ParameterizedTypeReference> MAP_PARAMETERIZED_TYPE_REF = - new ParameterizedTypeReference>() {}; - + private static final ParameterizedTypeReference> MAP_PARAMETERIZED_TYPE_REF = new ParameterizedTypeReference>() { + }; private final WebGraphQlHandler graphQlHandler; - /** * Create a new instance. * @param graphQlHandler common handler for GraphQL over HTTP requests @@ -51,31 +53,30 @@ public class GraphQlHttpHandler { this.graphQlHandler = graphQlHandler; } - /** * Handle GraphQL requests over HTTP. + * @param request the incoming HTTP request + * @return the HTTP response */ public Mono handleRequest(ServerRequest request) { - return request.bodyToMono(MAP_PARAMETERIZED_TYPE_REF) - .flatMap(body -> { - String id = request.exchange().getRequest().getId(); - WebInput input = new WebInput(request.uri(), request.headers().asHttpHeaders(), body, id); - if (logger.isDebugEnabled()) { - logger.debug("Executing: " + input); - } - return this.graphQlHandler.handle(input); - }) - .flatMap(output -> { - Map spec = output.toSpecification(); - if (logger.isDebugEnabled()) { - logger.debug("Execution complete"); - } - ServerResponse.BodyBuilder builder = ServerResponse.ok(); - if (output.getResponseHeaders() != null) { - builder.headers(headers -> headers.putAll(output.getResponseHeaders())); - } - return builder.bodyValue(spec); - }); + return request.bodyToMono(MAP_PARAMETERIZED_TYPE_REF).flatMap((body) -> { + String id = request.exchange().getRequest().getId(); + WebInput input = new WebInput(request.uri(), request.headers().asHttpHeaders(), body, id); + if (logger.isDebugEnabled()) { + logger.debug("Executing: " + input); + } + return this.graphQlHandler.handle(input); + }).flatMap((output) -> { + Map spec = output.toSpecification(); + if (logger.isDebugEnabled()) { + logger.debug("Execution complete"); + } + ServerResponse.BodyBuilder builder = ServerResponse.ok(); + if (output.getResponseHeaders() != null) { + builder.headers((headers) -> headers.putAll(output.getResponseHeaders())); + } + return builder.bodyValue(spec); + }); } } diff --git a/spring-graphql/src/main/java/org/springframework/graphql/web/webflux/GraphQlWebSocketHandler.java b/spring-graphql/src/main/java/org/springframework/graphql/web/webflux/GraphQlWebSocketHandler.java index 67acfc1d..90adb3bf 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/web/webflux/GraphQlWebSocketHandler.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/web/webflux/GraphQlWebSocketHandler.java @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.graphql.web.webflux; import java.time.Duration; @@ -58,19 +59,22 @@ import org.springframework.web.reactive.socket.WebSocketSession; /** * WebSocketHandler for GraphQL based on - * GraphQL Over WebSocket Protocol - * and for use in a Spring WebFlux application. + * GraphQL Over + * WebSocket Protocol and for use in a Spring WebFlux application. + * + * @author Rossen Stoyanchev + * @since 1.0.0 */ public class GraphQlWebSocketHandler implements WebSocketHandler { private static final Log logger = LogFactory.getLog(GraphQlWebSocketHandler.class); - private static final List SUB_PROTOCOL_LIST = - Arrays.asList("graphql-transport-ws", "subscriptions-transport-ws"); - - static final ResolvableType MAP_RESOLVABLE_TYPE = - ResolvableType.forType(new ParameterizedTypeReference>() {}); + private static final List SUB_PROTOCOL_LIST = Arrays.asList("graphql-transport-ws", + "subscriptions-transport-ws"); + static final ResolvableType MAP_RESOLVABLE_TYPE = ResolvableType + .forType(new ParameterizedTypeReference>() { + }); private final WebGraphQlHandler graphQlHandler; @@ -80,16 +84,15 @@ public class GraphQlWebSocketHandler implements WebSocketHandler { private final Duration initTimeoutDuration; - /** * Create a new instance. * @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. + * @param connectionInitTimeout the time within which the {@code CONNECTION_INIT} type + * message must be received. */ - public GraphQlWebSocketHandler( - WebGraphQlHandler graphQlHandler, ServerCodecConfigurer configurer, Duration connectionInitTimeout) { + public GraphQlWebSocketHandler(WebGraphQlHandler graphQlHandler, ServerCodecConfigurer configurer, + Duration connectionInitTimeout) { Assert.notNull(graphQlHandler, "WebGraphQlHandler is required"); this.graphQlHandler = graphQlHandler; @@ -100,34 +103,30 @@ public class GraphQlWebSocketHandler implements WebSocketHandler { private static Decoder initDecoder(ServerCodecConfigurer configurer) { return configurer.getReaders().stream() - .filter(reader -> reader.canRead(MAP_RESOLVABLE_TYPE, MediaType.APPLICATION_JSON)) - .map(reader -> ((DecoderHttpMessageReader) reader).getDecoder()) - .findFirst() + .filter((reader) -> reader.canRead(MAP_RESOLVABLE_TYPE, MediaType.APPLICATION_JSON)) + .map((reader) -> ((DecoderHttpMessageReader) reader).getDecoder()).findFirst() .orElseThrow(() -> new IllegalArgumentException("No JSON Decoder")); } private static Encoder initEncoder(ServerCodecConfigurer configurer) { return configurer.getWriters().stream() - .filter(writer -> writer.canWrite(MAP_RESOLVABLE_TYPE, MediaType.APPLICATION_JSON)) - .map(writer -> ((EncoderHttpMessageWriter) writer).getEncoder()) - .findFirst() + .filter((writer) -> writer.canWrite(MAP_RESOLVABLE_TYPE, MediaType.APPLICATION_JSON)) + .map((writer) -> ((EncoderHttpMessageWriter) writer).getEncoder()).findFirst() .orElseThrow(() -> new IllegalArgumentException("No JSON Encoder")); } - @Override public List getSubProtocols() { return SUB_PROTOCOL_LIST; } - @Override public Mono handle(WebSocketSession session) { HandshakeInfo handshakeInfo = session.getHandshakeInfo(); if ("subscriptions-transport-ws".equalsIgnoreCase(handshakeInfo.getSubProtocol())) { if (logger.isDebugEnabled()) { - logger.debug("apollographql/subscriptions-transport-ws is not supported, nor maintained. " + - "Please, use https://github.com/enisdenjo/graphql-ws."); + logger.debug("apollographql/subscriptions-transport-ws is not supported, nor maintained. " + + "Please, use https://github.com/enisdenjo/graphql-ws."); } return session.close(GraphQlStatus.INVALID_MESSAGE_STATUS); } @@ -136,60 +135,54 @@ public class GraphQlWebSocketHandler implements WebSocketHandler { AtomicBoolean connectionInitProcessed = new AtomicBoolean(); Map subscriptions = new ConcurrentHashMap<>(); - Mono.delay(this.initTimeoutDuration) - .then(Mono.defer(() -> - connectionInitProcessed.compareAndSet(false, true) ? - session.close(GraphQlStatus.INIT_TIMEOUT_STATUS) : - Mono.empty())) - .subscribe(); + Mono.delay(this.initTimeoutDuration).then(Mono.defer(() -> connectionInitProcessed.compareAndSet(false, true) + ? session.close(GraphQlStatus.INIT_TIMEOUT_STATUS) : Mono.empty())).subscribe(); - return session.send(session.receive() - .flatMap(message -> { - Map map = decode(message); - String id = (String) map.get("id"); - MessageType messageType = MessageType.resolve((String) map.get("type")); - if (messageType == null) { - return GraphQlStatus.close(session, GraphQlStatus.INVALID_MESSAGE_STATUS); + return session.send(session.receive().flatMap((message) -> { + Map map = decode(message); + String id = (String) map.get("id"); + MessageType messageType = MessageType.resolve((String) map.get("type")); + if (messageType == null) { + return GraphQlStatus.close(session, GraphQlStatus.INVALID_MESSAGE_STATUS); + } + switch (messageType) { + case SUBSCRIBE: + if (!connectionInitProcessed.get()) { + return GraphQlStatus.close(session, GraphQlStatus.UNAUTHORIZED_STATUS); + } + if (id == null) { + return GraphQlStatus.close(session, GraphQlStatus.INVALID_MESSAGE_STATUS); + } + WebInput input = new WebInput(handshakeInfo.getUri(), handshakeInfo.getHeaders(), getPayload(map), id); + if (logger.isDebugEnabled()) { + logger.debug("Executing: " + input); + } + return this.graphQlHandler.handle(input) + .flatMapMany((output) -> handleWebOutput(session, id, subscriptions, output)) + .doOnTerminate(() -> subscriptions.remove(id)); + case COMPLETE: + if (id != null) { + Subscription subscription = subscriptions.remove(id); + if (subscription != null) { + subscription.cancel(); } - switch (messageType) { - case SUBSCRIBE: - if (!connectionInitProcessed.get()) { - return GraphQlStatus.close(session, GraphQlStatus.UNAUTHORIZED_STATUS); - } - if (id == null) { - return GraphQlStatus.close(session, GraphQlStatus.INVALID_MESSAGE_STATUS); - } - WebInput input = new WebInput( - handshakeInfo.getUri(), handshakeInfo.getHeaders(), getPayload(map), id); - if (logger.isDebugEnabled()) { - logger.debug("Executing: " + input); - } - return this.graphQlHandler.handle(input) - .flatMapMany(output -> handleWebOutput(session, id, subscriptions, output)) - .doOnTerminate(() -> subscriptions.remove(id)); - case COMPLETE: - if (id != null) { - Subscription subscription = subscriptions.remove(id); - if (subscription != null) { - subscription.cancel(); - } - } - return Flux.empty(); - case CONNECTION_INIT: - if (!connectionInitProcessed.compareAndSet(false, true)) { - return GraphQlStatus.close(session, GraphQlStatus.TOO_MANY_INIT_REQUESTS_STATUS); - } - return Flux.just(encode(session, null, MessageType.CONNECTION_ACK, null)); - default: - return GraphQlStatus.close(session, GraphQlStatus.INVALID_MESSAGE_STATUS); - } - })); + } + return Flux.empty(); + case CONNECTION_INIT: + if (!connectionInitProcessed.compareAndSet(false, true)) { + return GraphQlStatus.close(session, GraphQlStatus.TOO_MANY_INIT_REQUESTS_STATUS); + } + return Flux.just(encode(session, null, MessageType.CONNECTION_ACK, null)); + default: + return GraphQlStatus.close(session, GraphQlStatus.INVALID_MESSAGE_STATUS); + } + })); } - @SuppressWarnings({"unchecked", "ConstantConditions"}) + @SuppressWarnings({ "unchecked", "ConstantConditions" }) private Map decode(WebSocketMessage message) { DataBuffer buffer = DataBufferUtils.retain(message.getPayload()); - return (Map) decoder.decode(buffer, MAP_RESOLVABLE_TYPE, null, null); + return (Map) this.decoder.decode(buffer, MAP_RESOLVABLE_TYPE, null, null); } @SuppressWarnings("unchecked") @@ -200,58 +193,50 @@ public class GraphQlWebSocketHandler implements WebSocketHandler { } @SuppressWarnings("unchecked") - private Flux handleWebOutput( - WebSocketSession session, String id, Map subscriptions, WebOutput output) { + private Flux handleWebOutput(WebSocketSession session, String id, + Map subscriptions, WebOutput output) { if (logger.isDebugEnabled()) { - logger.debug("Execution result ready" + - (!CollectionUtils.isEmpty(output.getErrors()) ? - " with errors: " + output.getErrors() : "") + "."); + logger.debug("Execution result ready" + + (!CollectionUtils.isEmpty(output.getErrors()) ? " with errors: " + output.getErrors() : "") + + "."); } Flux outputFlux; if (output.getData() instanceof Publisher) { // Subscription - outputFlux = Flux.from((Publisher) output.getData()) - .doOnSubscribe(subscription -> { - Subscription previous = subscriptions.putIfAbsent(id, subscription); - if (previous != null) { - throw new SubscriptionExistsException(); - } - }); + outputFlux = Flux.from((Publisher) output.getData()).doOnSubscribe((subscription) -> { + Subscription previous = subscriptions.putIfAbsent(id, subscription); + if (previous != null) { + throw new SubscriptionExistsException(); + } + }); } else { // Single response operation (query or mutation) - outputFlux = (CollectionUtils.isEmpty(output.getErrors()) ? - Flux.just(output) : - Flux.error(new IllegalStateException("Execution failed: " + output.getErrors()))); + outputFlux = (CollectionUtils.isEmpty(output.getErrors()) ? Flux.just(output) + : Flux.error(new IllegalStateException("Execution failed: " + output.getErrors()))); } - return outputFlux - .map(result -> { - Map dataMap = result.toSpecification(); - return encode(session, id, MessageType.NEXT, dataMap); - }) - .concatWith(Mono.fromCallable(() -> encode(session, id, MessageType.COMPLETE, null))) - .onErrorResume(ex -> { - if (ex instanceof SubscriptionExistsException) { - CloseStatus status = new CloseStatus(4409, "Subscriber for " + id + " already exists"); - return GraphQlStatus.close(session, status); - } - ErrorType errorType = ErrorType.DataFetchingException; - String message = ex.getMessage(); - Map errorMap = GraphqlErrorBuilder.newError() - .errorType(errorType) - .message(message) - .build() - .toSpecification(); - return Mono.just(encode(session, id, MessageType.ERROR, errorMap)); - }); + return outputFlux.map((result) -> { + Map dataMap = result.toSpecification(); + return encode(session, id, MessageType.NEXT, dataMap); + }).concatWith(Mono.fromCallable(() -> encode(session, id, MessageType.COMPLETE, null))).onErrorResume((ex) -> { + if (ex instanceof SubscriptionExistsException) { + CloseStatus status = new CloseStatus(4409, "Subscriber for " + id + " already exists"); + return GraphQlStatus.close(session, status); + } + ErrorType errorType = ErrorType.DataFetchingException; + String message = ex.getMessage(); + Map errorMap = GraphqlErrorBuilder.newError().errorType(errorType).message(message).build() + .toSpecification(); + return Mono.just(encode(session, id, MessageType.ERROR, errorMap)); + }); } @SuppressWarnings("unchecked") - private WebSocketMessage encode( - WebSocketSession session, @Nullable String id, MessageType messageType, @Nullable Object payload) { + private WebSocketMessage encode(WebSocketSession session, @Nullable String id, MessageType messageType, + @Nullable Object payload) { Map payloadMap = new HashMap<>(3); if (id != null) { @@ -262,23 +247,16 @@ public class GraphQlWebSocketHandler implements WebSocketHandler { payloadMap.put("payload", payload); } - DataBuffer buffer = ((Encoder) encoder).encodeValue( - (T) payloadMap, session.bufferFactory(), MAP_RESOLVABLE_TYPE, - MimeTypeUtils.APPLICATION_JSON, null); + DataBuffer buffer = ((Encoder) this.encoder).encodeValue((T) payloadMap, session.bufferFactory(), + MAP_RESOLVABLE_TYPE, MimeTypeUtils.APPLICATION_JSON, null); return new WebSocketMessage(WebSocketMessage.Type.TEXT, buffer); } - private enum MessageType { - CONNECTION_INIT("connection_init"), - CONNECTION_ACK("connection_ack"), - SUBSCRIBE("subscribe"), - NEXT("next"), - ERROR("error"), - COMPLETE("complete"); - + CONNECTION_INIT("connection_init"), CONNECTION_ACK("connection_ack"), SUBSCRIBE("subscribe"), NEXT( + "next"), ERROR("error"), COMPLETE("complete"); private static final Map messageTypes = new HashMap<>(6); @@ -288,7 +266,6 @@ public class GraphQlWebSocketHandler implements WebSocketHandler { } } - private final String type; MessageType(String type) { @@ -301,10 +278,10 @@ public class GraphQlWebSocketHandler implements WebSocketHandler { @Nullable public static MessageType resolve(@Nullable String type) { - return (type != null ? messageTypes.get(type) : null); + return (type != null) ? messageTypes.get(type) : null; } - } + } private static class GraphQlStatus { @@ -314,16 +291,17 @@ public class GraphQlWebSocketHandler implements WebSocketHandler { static final CloseStatus INIT_TIMEOUT_STATUS = new CloseStatus(4408, "Connection initialisation timeout"); - static final CloseStatus TOO_MANY_INIT_REQUESTS_STATUS = new CloseStatus(4429, "Too many initialisation requests"); - + static final CloseStatus TOO_MANY_INIT_REQUESTS_STATUS = new CloseStatus(4429, + "Too many initialisation requests"); static Flux close(WebSocketSession session, CloseStatus status) { return session.close(status).thenMany(Mono.empty()); } + } - private static class SubscriptionExistsException extends RuntimeException { + } } diff --git a/spring-graphql/src/main/java/org/springframework/graphql/web/webflux/package-info.java b/spring-graphql/src/main/java/org/springframework/graphql/web/webflux/package-info.java index bb2f8e9e..7f531738 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/web/webflux/package-info.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/web/webflux/package-info.java @@ -1,3 +1,19 @@ +/* + * 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. + */ + /** * HTTP and WebSocket handlers for use in a Spring WebFlux application. */ diff --git a/spring-graphql/src/main/java/org/springframework/graphql/web/webmvc/GraphQlHttpHandler.java b/spring-graphql/src/main/java/org/springframework/graphql/web/webmvc/GraphQlHttpHandler.java index 69f344d7..0e782e39 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/web/webmvc/GraphQlHttpHandler.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/web/webmvc/GraphQlHttpHandler.java @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.graphql.web.webmvc; import java.io.IOException; @@ -36,18 +37,20 @@ import org.springframework.web.servlet.function.ServerResponse; /** * GraphQL handler to expose as a WebMvc.fn endpoint via * {@link org.springframework.web.servlet.function.RouterFunctions}. + * + * @author Rossen Stoyanchev + * @author Brian Clozel + * @since 1.0.0 */ public class GraphQlHttpHandler { - private final static Log logger = LogFactory.getLog(GraphQlHttpHandler.class); - - private static final ParameterizedTypeReference> MAP_PARAMETERIZED_TYPE_REF = - new ParameterizedTypeReference>() {}; + private static final Log logger = LogFactory.getLog(GraphQlHttpHandler.class); + private static final ParameterizedTypeReference> MAP_PARAMETERIZED_TYPE_REF = new ParameterizedTypeReference>() { + }; private final WebGraphQlHandler graphQlHandler; - /** * Create a new instance. * @param graphQlHandler common handler for GraphQL over HTTP requests @@ -57,29 +60,28 @@ public class GraphQlHttpHandler { this.graphQlHandler = graphQlHandler; } - /** - * {@inheritDoc} - * - * @throws ServletException may be raised when reading the request body, - * e.g. {@link HttpMediaTypeNotSupportedException}. + * Handle GraphQL requests over HTTP. + * @param request the incoming HTTP request + * @return the HTTP response + * @throws ServletException may be raised when reading the request body, e.g. + * {@link HttpMediaTypeNotSupportedException}. */ public ServerResponse handleRequest(ServerRequest request) throws ServletException { WebInput input = new WebInput(request.uri(), request.headers().asHttpHeaders(), readBody(request), null); if (logger.isDebugEnabled()) { logger.debug("Executing: " + input); } - Mono responseMono = this.graphQlHandler.handle(input) - .map(output -> { - if (logger.isDebugEnabled()) { - logger.debug("Execution complete"); - } - ServerResponse.BodyBuilder builder = ServerResponse.ok(); - if (output.getResponseHeaders() != null) { - builder.headers(headers -> headers.putAll(output.getResponseHeaders())); - } - return builder.body(output.toSpecification()); - }); + Mono responseMono = this.graphQlHandler.handle(input).map((output) -> { + if (logger.isDebugEnabled()) { + logger.debug("Execution complete"); + } + ServerResponse.BodyBuilder builder = ServerResponse.ok(); + if (output.getResponseHeaders() != null) { + builder.headers((headers) -> headers.putAll(output.getResponseHeaders())); + } + return builder.body(output.toSpecification()); + }); return ServerResponse.async(responseMono); } @@ -92,4 +94,4 @@ public class GraphQlHttpHandler { } } -} \ No newline at end of file +} diff --git a/spring-graphql/src/main/java/org/springframework/graphql/web/webmvc/GraphQlWebSocketHandler.java b/spring-graphql/src/main/java/org/springframework/graphql/web/webmvc/GraphQlWebSocketHandler.java index ba704b99..c5b51d39 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/web/webmvc/GraphQlWebSocketHandler.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/web/webmvc/GraphQlWebSocketHandler.java @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.graphql.web.webmvc; import java.io.ByteArrayInputStream; @@ -60,16 +61,19 @@ import org.springframework.web.socket.handler.TextWebSocketHandler; /** * WebSocketHandler for GraphQL based on - * GraphQL Over WebSocket Protocol - * and for use on a Servlet container with {@code spring-websocket}. + * GraphQL Over + * WebSocket Protocol and for use on a Servlet container with + * {@code spring-websocket}. + * + * @author Rossen Stoyanchev + * @since 1.0.0 */ -public class GraphQlWebSocketHandler extends TextWebSocketHandler implements SubProtocolCapable { +public class GraphQlWebSocketHandler extends TextWebSocketHandler implements SubProtocolCapable { private static final Log logger = LogFactory.getLog(GraphQlWebSocketHandler.class); - private static final List SUB_PROTOCOL_LIST = - Arrays.asList("graphql-transport-ws", "subscriptions-transport-ws"); - + private static final List SUB_PROTOCOL_LIST = Arrays.asList("graphql-transport-ws", + "subscriptions-transport-ws"); private final WebGraphQlHandler graphQlHandler; @@ -79,16 +83,15 @@ public class GraphQlWebSocketHandler extends TextWebSocketHandler implements Sub private final Map sessionInfoMap = new ConcurrentHashMap<>(); - /** * Create a new instance. * @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. + * @param converter for JSON encoding and decoding + * @param connectionInitTimeout the time within which the {@code CONNECTION_INIT} type + * message must be received. */ - public GraphQlWebSocketHandler( - WebGraphQlHandler graphQlHandler, HttpMessageConverter converter, Duration connectionInitTimeout) { + public GraphQlWebSocketHandler(WebGraphQlHandler graphQlHandler, HttpMessageConverter converter, + Duration connectionInitTimeout) { Assert.notNull(graphQlHandler, "WebGraphQlHandler is required"); Assert.notNull(converter, "HttpMessageConverter for JSON is required"); @@ -97,19 +100,17 @@ public class GraphQlWebSocketHandler extends TextWebSocketHandler implements Sub this.converter = converter; } - @Override public List getSubProtocols() { return SUB_PROTOCOL_LIST; } - @Override public void afterConnectionEstablished(WebSocketSession session) { if ("subscriptions-transport-ws".equalsIgnoreCase(session.getAcceptedProtocol())) { if (logger.isDebugEnabled()) { - logger.debug("apollographql/subscriptions-transport-ws is not supported, nor maintained. " + - "Please, use https://github.com/enisdenjo/graphql-ws."); + logger.debug("apollographql/subscriptions-transport-ws is not supported, nor maintained. " + + "Please, use https://github.com/enisdenjo/graphql-ws."); } GraphQlStatus.closeSession(session, GraphQlStatus.INVALID_MESSAGE_STATUS); return; @@ -118,13 +119,11 @@ public class GraphQlWebSocketHandler extends TextWebSocketHandler implements Sub SessionState sessionState = new SessionState(session.getId()); this.sessionInfoMap.put(session.getId(), sessionState); - Mono.delay(this.initTimeoutDuration) - .then(Mono.fromRunnable(() -> { - if (sessionState.isConnectionInitNotProcessed()) { - GraphQlStatus.closeSession(session, GraphQlStatus.INIT_TIMEOUT_STATUS); - } - })) - .subscribe(); + Mono.delay(this.initTimeoutDuration).then(Mono.fromRunnable(() -> { + if (sessionState.isConnectionInitNotProcessed()) { + GraphQlStatus.closeSession(session, GraphQlStatus.INIT_TIMEOUT_STATUS); + } + })).subscribe(); } @Override @@ -139,45 +138,45 @@ public class GraphQlWebSocketHandler extends TextWebSocketHandler implements Sub } SessionState sessionState = getSessionInfo(session); switch (messageType) { - case SUBSCRIBE: - if (sessionState.isConnectionInitNotProcessed()) { - GraphQlStatus.closeSession(session, GraphQlStatus.UNAUTHORIZED_STATUS); - return; - } - if (id == null) { - GraphQlStatus.closeSession(session, GraphQlStatus.INVALID_MESSAGE_STATUS); - return; - } - URI uri = session.getUri(); - Assert.notNull(uri, "Expected handshake url"); - HttpHeaders headers = session.getHandshakeHeaders(); - WebInput input = new WebInput(uri, headers, getPayload(map), id); - if (logger.isDebugEnabled()) { - logger.debug("Executing: " + 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)); + case SUBSCRIBE: + if (sessionState.isConnectionInitNotProcessed()) { + GraphQlStatus.closeSession(session, GraphQlStatus.UNAUTHORIZED_STATUS); return; - case COMPLETE: - if (id != null) { - Subscription subscription = sessionState.getSubscriptions().remove(id); - if (subscription != null) { - subscription.cancel(); - } - } - return; - case CONNECTION_INIT: - if (sessionState.setConnectionInitProcessed()) { - GraphQlStatus.closeSession(session, GraphQlStatus.TOO_MANY_INIT_REQUESTS_STATUS); - return; - } - TextMessage outputMessage = encode(null, MessageType.CONNECTION_ACK, null); - session.sendMessage(outputMessage); - return; - default: + } + if (id == null) { GraphQlStatus.closeSession(session, GraphQlStatus.INVALID_MESSAGE_STATUS); + return; + } + URI uri = session.getUri(); + Assert.notNull(uri, "Expected handshake url"); + HttpHeaders headers = session.getHandshakeHeaders(); + WebInput input = new WebInput(uri, headers, getPayload(map), id); + if (logger.isDebugEnabled()) { + logger.debug("Executing: " + 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)); + return; + case COMPLETE: + if (id != null) { + Subscription subscription = sessionState.getSubscriptions().remove(id); + if (subscription != null) { + subscription.cancel(); + } + } + return; + case CONNECTION_INIT: + if (sessionState.setConnectionInitProcessed()) { + GraphQlStatus.closeSession(session, GraphQlStatus.TOO_MANY_INIT_REQUESTS_STATUS); + return; + } + TextMessage outputMessage = encode(null, MessageType.CONNECTION_ACK, null); + session.sendMessage(outputMessage); + return; + default: + GraphQlStatus.closeSession(session, GraphQlStatus.INVALID_MESSAGE_STATUS); } } @@ -203,49 +202,41 @@ public class GraphQlWebSocketHandler extends TextWebSocketHandler implements Sub @SuppressWarnings("unchecked") private Flux handleWebOutput(WebSocketSession session, String id, WebOutput output) { if (logger.isDebugEnabled()) { - logger.debug("Execution result ready" + - (!CollectionUtils.isEmpty(output.getErrors()) ? - " with errors: " + output.getErrors() : "") + "."); + logger.debug("Execution result ready" + + (!CollectionUtils.isEmpty(output.getErrors()) ? " with errors: " + output.getErrors() : "") + + "."); } Flux outputFlux; if (output.getData() instanceof Publisher) { // Subscription - outputFlux = Flux.from((Publisher) output.getData()) - .doOnSubscribe(subscription -> { - Subscription prev = getSessionInfo(session).getSubscriptions().putIfAbsent(id, subscription); - if (prev != null) { - throw new SubscriptionExistsException(); - } - }); + outputFlux = Flux.from((Publisher) output.getData()).doOnSubscribe((subscription) -> { + Subscription prev = getSessionInfo(session).getSubscriptions().putIfAbsent(id, subscription); + if (prev != null) { + throw new SubscriptionExistsException(); + } + }); } else { // Single response operation (query or mutation) - outputFlux = (CollectionUtils.isEmpty(output.getErrors()) ? - Flux.just(output) : - Flux.error(new IllegalStateException("Execution failed: " + output.getErrors()))); + outputFlux = (CollectionUtils.isEmpty(output.getErrors()) ? Flux.just(output) + : Flux.error(new IllegalStateException("Execution failed: " + output.getErrors()))); } - return outputFlux - .map(result -> { - Map dataMap = result.toSpecification(); - return encode(id, MessageType.NEXT, dataMap); - }) - .concatWith(Mono.fromCallable(() -> encode(id, MessageType.COMPLETE, null))) - .onErrorResume(ex -> { - if (ex instanceof SubscriptionExistsException) { - CloseStatus status = new CloseStatus(4409, "Subscriber for " + id + " already exists"); - GraphQlStatus.closeSession(session, status); - return Flux.empty(); - } - ErrorType errorType = ErrorType.DataFetchingException; - String message = ex.getMessage(); - Map errorMap = GraphqlErrorBuilder.newError() - .errorType(errorType) - .message(message) - .build() - .toSpecification(); - return Mono.just(encode(id, MessageType.ERROR, errorMap)); - }); + return outputFlux.map((result) -> { + Map dataMap = result.toSpecification(); + return encode(id, MessageType.NEXT, dataMap); + }).concatWith(Mono.fromCallable(() -> encode(id, MessageType.COMPLETE, null))).onErrorResume((ex) -> { + if (ex instanceof SubscriptionExistsException) { + CloseStatus status = new CloseStatus(4409, "Subscriber for " + id + " already exists"); + GraphQlStatus.closeSession(session, status); + return Flux.empty(); + } + ErrorType errorType = ErrorType.DataFetchingException; + String message = ex.getMessage(); + Map errorMap = GraphqlErrorBuilder.newError().errorType(errorType).message(message).build() + .toSpecification(); + return Mono.just(encode(id, MessageType.ERROR, errorMap)); + }); } @SuppressWarnings("unchecked") @@ -289,16 +280,10 @@ public class GraphQlWebSocketHandler extends TextWebSocketHandler implements Sub return false; } - private enum MessageType { - CONNECTION_INIT("connection_init"), - CONNECTION_ACK("connection_ack"), - SUBSCRIBE("subscribe"), - NEXT("next"), - ERROR("error"), - COMPLETE("complete"); - + CONNECTION_INIT("connection_init"), CONNECTION_ACK("connection_ack"), SUBSCRIBE("subscribe"), NEXT( + "next"), ERROR("error"), COMPLETE("complete"); private static final Map messageTypes = new HashMap<>(6); @@ -308,7 +293,6 @@ public class GraphQlWebSocketHandler extends TextWebSocketHandler implements Sub } } - private final String type; MessageType(String type) { @@ -321,10 +305,10 @@ public class GraphQlWebSocketHandler extends TextWebSocketHandler implements Sub @Nullable public static MessageType resolve(@Nullable String type) { - return (type != null ? messageTypes.get(type) : null); + return (type != null) ? messageTypes.get(type) : null; } - } + } private static class GraphQlStatus { @@ -332,12 +316,13 @@ public class GraphQlWebSocketHandler extends TextWebSocketHandler implements Sub private static final CloseStatus UNAUTHORIZED_STATUS = new CloseStatus(4401, "Unauthorized"); - private static final CloseStatus INIT_TIMEOUT_STATUS = new CloseStatus(4408, "Connection initialisation timeout"); + private static final CloseStatus INIT_TIMEOUT_STATUS = new CloseStatus(4408, + "Connection initialisation timeout"); - private static final CloseStatus TOO_MANY_INIT_REQUESTS_STATUS = new CloseStatus(4429, "Too many initialisation requests"); + private static final CloseStatus TOO_MANY_INIT_REQUESTS_STATUS = new CloseStatus(4429, + "Too many initialisation requests"); - - public static void closeSession(WebSocketSession session, CloseStatus status) { + static void closeSession(WebSocketSession session, CloseStatus status) { try { session.close(status); } @@ -347,8 +332,8 @@ public class GraphQlWebSocketHandler extends TextWebSocketHandler implements Sub } } } - } + } private static class HttpInputMessageAdapter extends ByteArrayInputStream implements HttpInputMessage { @@ -365,8 +350,8 @@ public class GraphQlWebSocketHandler extends TextWebSocketHandler implements Sub public HttpHeaders getHeaders() { return HttpHeaders.EMPTY; } - } + } private static class HttpOutputMessageAdapter extends ByteArrayOutputStream implements HttpOutputMessage { @@ -381,8 +366,8 @@ public class GraphQlWebSocketHandler extends TextWebSocketHandler implements Sub public HttpHeaders getHeaders() { return noOpHeaders; } - } + } private static class SessionState { @@ -392,25 +377,25 @@ public class GraphQlWebSocketHandler extends TextWebSocketHandler implements Sub private final Scheduler scheduler; - public SessionState(String sessionId) { + SessionState(String sessionId) { this.scheduler = Schedulers.newSingle("GraphQL-WsSession-" + sessionId); } - public boolean isConnectionInitNotProcessed() { + boolean isConnectionInitNotProcessed() { return !this.connectionInitProcessed; } - public synchronized boolean setConnectionInitProcessed() { + synchronized boolean setConnectionInitProcessed() { boolean previousValue = this.connectionInitProcessed; this.connectionInitProcessed = true; return previousValue; } - public Map getSubscriptions() { + Map getSubscriptions() { return this.subscriptions; } - public void dispose() { + void dispose() { for (Map.Entry entry : this.subscriptions.entrySet()) { try { entry.getValue().cancel(); @@ -423,11 +408,11 @@ public class GraphQlWebSocketHandler extends TextWebSocketHandler implements Sub this.scheduler.dispose(); } - public Scheduler getScheduler() { + Scheduler getScheduler() { return this.scheduler; } - } + } private static class SendMessageSubscriber extends BaseSubscriber { @@ -437,7 +422,7 @@ public class GraphQlWebSocketHandler extends TextWebSocketHandler implements Sub private final SessionState sessionState; - public SendMessageSubscriber(String subscriptionId, WebSocketSession session, SessionState sessionState) { + SendMessageSubscriber(String subscriptionId, WebSocketSession session, SessionState sessionState) { this.subscriptionId = subscriptionId; this.session = session; this.sessionState = sessionState; @@ -468,10 +453,11 @@ public class GraphQlWebSocketHandler extends TextWebSocketHandler implements Sub public void hookOnComplete() { this.sessionState.getSubscriptions().remove(this.subscriptionId); } + } - private static class SubscriptionExistsException extends RuntimeException { + } } diff --git a/spring-graphql/src/main/java/org/springframework/graphql/web/webmvc/package-info.java b/spring-graphql/src/main/java/org/springframework/graphql/web/webmvc/package-info.java index e07bdb2e..8402884d 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/web/webmvc/package-info.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/web/webmvc/package-info.java @@ -1,3 +1,19 @@ +/* + * 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. + */ + /** * HTTP and WebSocket handlers for use in a Spring WebMvc application. */ diff --git a/spring-graphql/src/test/java/org/springframework/graphql/GraphQlTestUtils.java b/spring-graphql/src/test/java/org/springframework/graphql/GraphQlTestUtils.java index d56becaf..21daa91d 100644 --- a/spring-graphql/src/test/java/org/springframework/graphql/GraphQlTestUtils.java +++ b/spring-graphql/src/test/java/org/springframework/graphql/GraphQlTestUtils.java @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.graphql; import java.nio.charset.StandardCharsets; @@ -31,30 +32,23 @@ import org.springframework.graphql.execution.GraphQlSource; */ public abstract class GraphQlTestUtils { - public static GraphQL initGraphQl( - String schemaContent, String typeName, String fieldName, DataFetcher fetcher) { + public static GraphQL initGraphQl(String schemaContent, String typeName, String fieldName, DataFetcher fetcher) { - return initGraphQlSource(schemaContent, typeName, fieldName, fetcher) - .build() - .graphQl(); + return initGraphQlSource(schemaContent, typeName, fieldName, fetcher).build().graphQl(); } - public static GraphQL initGraphQl( - String schemaContent, String typeName, String fieldName, DataFetcher fetcher, + public static GraphQL initGraphQl(String schemaContent, String typeName, String fieldName, DataFetcher fetcher, DataFetcherExceptionResolver... resolvers) { return initGraphQlSource(schemaContent, typeName, fieldName, fetcher) - .exceptionResolvers(Arrays.asList(resolvers)) - .build() - .graphQl(); + .exceptionResolvers(Arrays.asList(resolvers)).build().graphQl(); } - public static GraphQlSource.Builder initGraphQlSource( - String schemaContent, String typeName, String fieldName, DataFetcher fetcher) { + public static GraphQlSource.Builder initGraphQlSource(String schemaContent, String typeName, String fieldName, + DataFetcher fetcher) { RuntimeWiring wiring = RuntimeWiring.newRuntimeWiring() - .type(typeName, builder -> builder.dataFetcher(fieldName, fetcher)) - .build(); + .type(typeName, (builder) -> builder.dataFetcher(fieldName, fetcher)).build(); return GraphQlSource.builder() .schemaResource(new ByteArrayResource(schemaContent.getBytes(StandardCharsets.UTF_8))) diff --git a/spring-graphql/src/test/java/org/springframework/graphql/TestGraphQlSource.java b/spring-graphql/src/test/java/org/springframework/graphql/TestGraphQlSource.java index 30c5ee8b..f7abe9c4 100644 --- a/spring-graphql/src/test/java/org/springframework/graphql/TestGraphQlSource.java +++ b/spring-graphql/src/test/java/org/springframework/graphql/TestGraphQlSource.java @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.graphql; import graphql.GraphQL; @@ -27,12 +28,10 @@ public class TestGraphQlSource implements GraphQlSource { private final GraphQL graphQl; - public TestGraphQlSource(GraphQL graphQl) { this.graphQl = graphQl; } - @Override public GraphQL graphQl() { return this.graphQl; @@ -42,4 +41,5 @@ public class TestGraphQlSource implements GraphQlSource { public GraphQLSchema schema() { throw new UnsupportedOperationException(); } + } diff --git a/spring-graphql/src/test/java/org/springframework/graphql/TestThreadLocalAccessor.java b/spring-graphql/src/test/java/org/springframework/graphql/TestThreadLocalAccessor.java index 14b4a70a..66b780d7 100644 --- a/spring-graphql/src/test/java/org/springframework/graphql/TestThreadLocalAccessor.java +++ b/spring-graphql/src/test/java/org/springframework/graphql/TestThreadLocalAccessor.java @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.graphql; import java.util.Map; @@ -21,7 +22,7 @@ import org.springframework.graphql.execution.ThreadLocalAccessor; import org.springframework.lang.Nullable; import org.springframework.util.Assert; -import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.assertj.core.api.Assertions.assertThat; /** * {@link ThreadLocalAccessor} that operates on the ThreadLocal it is given. @@ -33,12 +34,10 @@ public class TestThreadLocalAccessor implements ThreadLocalAccessor { @Nullable private Long threadId; - public TestThreadLocalAccessor(ThreadLocal threadLocal) { this.threadLocal = threadLocal; } - @Override public void extractValues(Map container) { saveThreadId(); @@ -66,9 +65,7 @@ public class TestThreadLocalAccessor implements ThreadLocalAccessor { } private void checkThreadId() { - assertThat(this.threadId) - .as("No threadId to check. Was extractValues not called?") - .isNotNull(); + assertThat(this.threadId).as("No threadId to check. Was extractValues not called?").isNotNull(); assertThat(Thread.currentThread().getId() != this.threadId) .as("ThreadLocal value extracted and restored on the same thread. Propagation not tested effectively.") .isTrue(); diff --git a/spring-graphql/src/test/java/org/springframework/graphql/execution/ContextDataFetcherDecoratorTests.java b/spring-graphql/src/test/java/org/springframework/graphql/execution/ContextDataFetcherDecoratorTests.java index ef53abf2..70055cfd 100644 --- a/spring-graphql/src/test/java/org/springframework/graphql/execution/ContextDataFetcherDecoratorTests.java +++ b/spring-graphql/src/test/java/org/springframework/graphql/execution/ContextDataFetcherDecoratorTests.java @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.graphql.execution; import java.time.Duration; @@ -41,12 +42,11 @@ public class ContextDataFetcherDecoratorTests { @Test void monoDataFetcher() throws Exception { - GraphQL graphQl = GraphQlTestUtils.initGraphQl("type Query { greeting: String }", - "Query", "greeting", env -> - Mono.deferContextual(context -> { - Object name = context.get("name"); - return Mono.delay(Duration.ofMillis(50)).map(aLong -> "Hello " + name); - })); + GraphQL graphQl = GraphQlTestUtils.initGraphQl("type Query { greeting: String }", "Query", "greeting", + (env) -> Mono.deferContextual((context) -> { + Object name = context.get("name"); + return Mono.delay(Duration.ofMillis(50)).map((aLong) -> "Hello " + name); + })); ExecutionInput input = ExecutionInput.newExecutionInput().query("{ greeting }").build(); ContextManager.setReactorContext(Context.of("name", "007"), input); @@ -58,13 +58,11 @@ public class ContextDataFetcherDecoratorTests { @Test void fluxDataFetcher() throws Exception { - GraphQL graphQl = GraphQlTestUtils.initGraphQl("type Query { greetings: [String] }", - "Query", "greetings", env -> - Mono.delay(Duration.ofMillis(50)).flatMapMany(aLong -> - Flux.deferContextual(context -> { - String name = context.get("name"); - return Flux.just("Hi", "Bonjour", "Hola").map(s -> s + " " + name); - }))); + GraphQL graphQl = GraphQlTestUtils.initGraphQl("type Query { greetings: [String] }", "Query", "greetings", + (env) -> Mono.delay(Duration.ofMillis(50)).flatMapMany((aLong) -> Flux.deferContextual((context) -> { + String name = context.get("name"); + return Flux.just("Hi", "Bonjour", "Hola").map((s) -> s + " " + name); + }))); ExecutionInput input = ExecutionInput.newExecutionInput().query("{ greetings }").build(); ContextManager.setReactorContext(Context.of("name", "007"), input); @@ -77,24 +75,19 @@ public class ContextDataFetcherDecoratorTests { @Test void fluxDataFetcherSubscription() throws Exception { GraphQL graphQl = GraphQlTestUtils.initGraphQl( - "type Query { greeting: String } type Subscription { greetings: String }", - "Subscription", "greetings", env -> - Mono.delay(Duration.ofMillis(50)).flatMapMany(aLong -> - Flux.deferContextual(context -> { - String name = context.get("name"); - return Flux.just("Hi", "Bonjour", "Hola").map(s -> s + " " + name); - }))); + "type Query { greeting: String } type Subscription { greetings: String }", "Subscription", "greetings", + (env) -> Mono.delay(Duration.ofMillis(50)).flatMapMany((aLong) -> Flux.deferContextual((context) -> { + String name = context.get("name"); + return Flux.just("Hi", "Bonjour", "Hola").map((s) -> s + " " + name); + }))); ExecutionInput input = ExecutionInput.newExecutionInput().query("subscription { greetings }").build(); ContextManager.setReactorContext(Context.of("name", "007"), input); Publisher publisher = graphQl.executeAsync(input).get().getData(); - List actual = Flux.from(publisher) - .cast(ExecutionResult.class) - .map(result -> ((Map) result.getData()).get("greetings")) - .cast(String.class) - .collectList() + List actual = Flux.from(publisher).cast(ExecutionResult.class) + .map((result) -> ((Map) result.getData()).get("greetings")).cast(String.class).collectList() .block(); assertThat(actual).containsExactly("Hi 007", "Bonjour 007", "Hola 007"); @@ -106,16 +99,15 @@ public class ContextDataFetcherDecoratorTests { nameThreadLocal.set("007"); TestThreadLocalAccessor accessor = new TestThreadLocalAccessor<>(nameThreadLocal); try { - GraphQL graphQl = GraphQlTestUtils.initGraphQl("type Query { greeting: String }", - "Query", "greeting", env -> "Hello " + nameThreadLocal.get()); + GraphQL graphQl = GraphQlTestUtils.initGraphQl("type Query { greeting: String }", "Query", "greeting", + (env) -> "Hello " + nameThreadLocal.get()); ExecutionInput input = ExecutionInput.newExecutionInput().query("{ greeting }").build(); ContextView view = ContextManager.extractThreadLocalValues(accessor); ContextManager.setReactorContext(view, input); ExecutionResult result = Mono.delay(Duration.ofMillis(10)) - .flatMap(aLong -> Mono.fromFuture(graphQl.executeAsync(input))) - .block(); + .flatMap((aLong) -> Mono.fromFuture(graphQl.executeAsync(input))).block(); Map data = result.getData(); assertThat(data).hasSize(1).containsEntry("greeting", "Hello 007"); diff --git a/spring-graphql/src/test/java/org/springframework/graphql/execution/ExceptionResolversExceptionHandlerTests.java b/spring-graphql/src/test/java/org/springframework/graphql/execution/ExceptionResolversExceptionHandlerTests.java index 61ed6691..6fc9c504 100644 --- a/spring-graphql/src/test/java/org/springframework/graphql/execution/ExceptionResolversExceptionHandlerTests.java +++ b/spring-graphql/src/test/java/org/springframework/graphql/execution/ExceptionResolversExceptionHandlerTests.java @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.graphql.execution; import java.time.Duration; @@ -42,15 +43,11 @@ public class ExceptionResolversExceptionHandlerTests { @Test void resolveException() throws Exception { - GraphQL graphQl = GraphQlTestUtils.initGraphQl("type Query { greeting: String }", - "Query", "greeting", env -> { + GraphQL graphQl = GraphQlTestUtils.initGraphQl("type Query { greeting: String }", "Query", "greeting", + (env) -> { throw new IllegalArgumentException("Invalid greeting"); - }, - (ex, env) -> Mono.just(Collections.singletonList( - GraphqlErrorBuilder.newError(env) - .message("Resolved error: " + ex.getMessage()) - .errorType(ErrorType.BAD_REQUEST) - .build()))); + }, (ex, env) -> Mono.just(Collections.singletonList(GraphqlErrorBuilder.newError(env) + .message("Resolved error: " + ex.getMessage()).errorType(ErrorType.BAD_REQUEST).build()))); ExecutionInput input = ExecutionInput.newExecutionInput().query("{ greeting }").build(); ExecutionResult result = graphQl.executeAsync(input).get(); @@ -66,15 +63,13 @@ public class ExceptionResolversExceptionHandlerTests { @Test void resolveExceptionWithReactorContext() throws Exception { - GraphQL graphQl = GraphQlTestUtils.initGraphQl("type Query { greeting: String }", - "Query", "greeting", env -> { + GraphQL graphQl = GraphQlTestUtils.initGraphQl("type Query { greeting: String }", "Query", "greeting", + (env) -> { throw new IllegalArgumentException("Invalid greeting"); }, - (ex, env) -> Mono.deferContextual(view -> Mono.just(Collections.singletonList( - GraphqlErrorBuilder.newError(env) - .message("Resolved error: " + ex.getMessage() + ", name=" + view.get("name")) - .errorType(ErrorType.BAD_REQUEST) - .build())))); + (ex, env) -> Mono.deferContextual((view) -> Mono.just(Collections.singletonList(GraphqlErrorBuilder + .newError(env).message("Resolved error: " + ex.getMessage() + ", name=" + view.get("name")) + .errorType(ErrorType.BAD_REQUEST).build())))); ExecutionInput input = ExecutionInput.newExecutionInput().query("{ greeting }").build(); ContextManager.setReactorContext(Context.of("name", "007"), input); @@ -91,23 +86,21 @@ public class ExceptionResolversExceptionHandlerTests { nameThreadLocal.set("007"); TestThreadLocalAccessor accessor = new TestThreadLocalAccessor<>(nameThreadLocal); try { - GraphQL graphQl = GraphQlTestUtils.initGraphQl("type Query { greeting: String }", - "Query", "greeting", env -> { + GraphQL graphQl = GraphQlTestUtils.initGraphQl("type Query { greeting: String }", "Query", "greeting", + (env) -> { throw new IllegalArgumentException("Invalid greeting"); }, - (SyncDataFetcherExceptionResolver) (ex, env) -> Collections.singletonList( - GraphqlErrorBuilder.newError(env) + (SyncDataFetcherExceptionResolver) (ex, + env) -> Collections.singletonList(GraphqlErrorBuilder.newError(env) .message("Resolved error: " + ex.getMessage() + ", name=" + nameThreadLocal.get()) - .errorType(ErrorType.BAD_REQUEST) - .build())); + .errorType(ErrorType.BAD_REQUEST).build())); ExecutionInput input = ExecutionInput.newExecutionInput().query("{ greeting }").build(); ContextView view = ContextManager.extractThreadLocalValues(accessor); ContextManager.setReactorContext(view, input); ExecutionResult result = Mono.delay(Duration.ofMillis(10)) - .flatMap(aLong -> Mono.fromFuture(graphQl.executeAsync(input))) - .block(); + .flatMap((aLong) -> Mono.fromFuture(graphQl.executeAsync(input))).block(); List errors = result.getErrors(); assertThat(errors.get(0).getMessage()).isEqualTo("Resolved error: Invalid greeting, name=007"); @@ -119,11 +112,10 @@ public class ExceptionResolversExceptionHandlerTests { @Test void unresolvedException() throws Exception { - GraphQL graphQl = GraphQlTestUtils.initGraphQl("type Query { greeting: String }", - "Query", "greeting", env -> { + GraphQL graphQl = GraphQlTestUtils.initGraphQl("type Query { greeting: String }", "Query", "greeting", + (env) -> { throw new IllegalArgumentException("Invalid greeting"); - }, - (exception, environment) -> Mono.empty()); + }, (exception, environment) -> Mono.empty()); ExecutionInput input = ExecutionInput.newExecutionInput().query("{ greeting }").build(); ExecutionResult result = graphQl.executeAsync(input).get(); @@ -140,11 +132,10 @@ public class ExceptionResolversExceptionHandlerTests { @Test void suppressedException() throws Exception { - GraphQL graphQl = GraphQlTestUtils.initGraphQl("type Query { greeting: String }", - "Query", "greeting", env -> { + GraphQL graphQl = GraphQlTestUtils.initGraphQl("type Query { greeting: String }", "Query", "greeting", + (env) -> { throw new IllegalArgumentException("Invalid greeting"); - }, - (ex, env) -> Mono.just(Collections.emptyList())); + }, (ex, env) -> Mono.just(Collections.emptyList())); ExecutionInput input = ExecutionInput.newExecutionInput().query("{ greeting }").build(); ExecutionResult result = graphQl.executeAsync(input).get(); diff --git a/spring-graphql/src/test/java/org/springframework/graphql/web/Book.java b/spring-graphql/src/test/java/org/springframework/graphql/web/Book.java index f0cda89d..1dadfe62 100644 --- a/spring-graphql/src/test/java/org/springframework/graphql/web/Book.java +++ b/spring-graphql/src/test/java/org/springframework/graphql/web/Book.java @@ -1,3 +1,19 @@ +/* + * 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; public class Book { @@ -8,7 +24,6 @@ public class Book { String author; - public Book() { } @@ -18,7 +33,6 @@ public class Book { this.author = author; } - public Long getId() { return this.id; } @@ -42,4 +56,5 @@ public class Book { public void setAuthor(String author) { this.author = author; } + } diff --git a/spring-graphql/src/test/java/org/springframework/graphql/web/BookTestUtils.java b/spring-graphql/src/test/java/org/springframework/graphql/web/BookTestUtils.java index 0d8d585a..57c6f0c2 100644 --- a/spring-graphql/src/test/java/org/springframework/graphql/web/BookTestUtils.java +++ b/spring-graphql/src/test/java/org/springframework/graphql/web/BookTestUtils.java @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.graphql.web; import java.util.Arrays; @@ -20,41 +21,24 @@ import java.util.HashMap; import java.util.Map; import graphql.schema.idl.RuntimeWiring; +import graphql.schema.idl.TypeRuntimeWiring; import reactor.core.publisher.Flux; import org.springframework.core.io.ClassPathResource; import org.springframework.graphql.execution.ExecutionGraphQlService; import org.springframework.graphql.execution.GraphQlSource; -import static graphql.schema.idl.TypeRuntimeWiring.newTypeWiring; - public abstract class BookTestUtils { public static final String SUBSCRIPTION_ID = "1"; - public static final String BOOK_QUERY = "{" + - "\"id\":\"" + BookTestUtils.SUBSCRIPTION_ID + "\"," + - "\"type\":\"subscribe\"," + - "\"payload\":{\"query\": \"" + - " query TestQuery {" + - " bookById(id: \\\"1\\\"){ " + - " id" + - " name" + - " author" + - " }}\"}" + - "}"; + public static final String BOOK_QUERY = "{" + "\"id\":\"" + BookTestUtils.SUBSCRIPTION_ID + "\"," + + "\"type\":\"subscribe\"," + "\"payload\":{\"query\": \"" + " query TestQuery {" + + " bookById(id: \\\"1\\\"){ " + " id" + " name" + " author" + " }}\"}" + "}"; - public static final String BOOK_SUBSCRIPTION = "{" + - "\"id\":\"" + SUBSCRIPTION_ID + "\"," + - "\"type\":\"subscribe\"," + - "\"payload\":{\"query\": \"" + - " subscription TestSubscription {" + - " bookSearch(author: \\\"George\\\") {" + - " id" + - " name" + - " author" + - " }}\"}" + - "}"; + public static final String BOOK_SUBSCRIPTION = "{" + "\"id\":\"" + SUBSCRIPTION_ID + "\"," + + "\"type\":\"subscribe\"," + "\"payload\":{\"query\": \"" + " subscription TestSubscription {" + + " bookSearch(author: \\\"George\\\") {" + " id" + " name" + " author" + " }}\"}" + "}"; private static final Map booksMap = new HashMap<>(4); static { @@ -65,27 +49,23 @@ public abstract class BookTestUtils { booksMap.put(5L, new Book(5L, "Animal Farm", "George Orwell")); } - public static WebGraphQlHandler initWebGraphQlHandler(WebInterceptor... interceptors) { return WebGraphQlHandler.builder(new ExecutionGraphQlService(graphQlSource())) - .interceptors(Arrays.asList(interceptors)) - .build(); + .interceptors(Arrays.asList(interceptors)).build(); } private static GraphQlSource graphQlSource() { RuntimeWiring.Builder builder = RuntimeWiring.newRuntimeWiring(); - builder.type(newTypeWiring("Query").dataFetcher("bookById", env -> { + builder.type(TypeRuntimeWiring.newTypeWiring("Query").dataFetcher("bookById", (env) -> { Long id = Long.parseLong(env.getArgument("id")); return booksMap.get(id); })); - builder.type(newTypeWiring("Subscription").dataFetcher("bookSearch", env -> { + builder.type(TypeRuntimeWiring.newTypeWiring("Subscription").dataFetcher("bookSearch", (env) -> { String author = env.getArgument("author"); - return Flux.fromIterable(booksMap.values()).filter(book -> book.getAuthor().contains(author)); + return Flux.fromIterable(booksMap.values()).filter((book) -> book.getAuthor().contains(author)); })); - return GraphQlSource.builder() - .schemaResource(new ClassPathResource("books/schema.graphqls")) - .runtimeWiring(builder.build()) - .build(); + return GraphQlSource.builder().schemaResource(new ClassPathResource("books/schema.graphqls")) + .runtimeWiring(builder.build()).build(); } } diff --git a/spring-graphql/src/test/java/org/springframework/graphql/web/ConsumeOneAndNeverCompleteInterceptor.java b/spring-graphql/src/test/java/org/springframework/graphql/web/ConsumeOneAndNeverCompleteInterceptor.java index 2ab3a6f1..6e6e326a 100644 --- a/spring-graphql/src/test/java/org/springframework/graphql/web/ConsumeOneAndNeverCompleteInterceptor.java +++ b/spring-graphql/src/test/java/org/springframework/graphql/web/ConsumeOneAndNeverCompleteInterceptor.java @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.graphql.web; import org.reactivestreams.Publisher; @@ -25,11 +26,11 @@ public class ConsumeOneAndNeverCompleteInterceptor implements WebInterceptor { @Override public Mono intercept(WebInput webInput, WebGraphQlHandler next) { - return next.handle(webInput).map(output -> - output.transform(builder -> { - Publisher publisher = output.getData(); - assertThat(publisher).isNotNull(); - builder.data(Flux.from(publisher).take(1).concatWith(Flux.never())); - })); + return next.handle(webInput).map((output) -> output.transform((builder) -> { + Publisher publisher = output.getData(); + assertThat(publisher).isNotNull(); + builder.data(Flux.from(publisher).take(1).concatWith(Flux.never())); + })); } + } diff --git a/spring-graphql/src/test/java/org/springframework/graphql/web/WebGraphQlHandlerTests.java b/spring-graphql/src/test/java/org/springframework/graphql/web/WebGraphQlHandlerTests.java index 399de551..d68349c8 100644 --- a/spring-graphql/src/test/java/org/springframework/graphql/web/WebGraphQlHandlerTests.java +++ b/spring-graphql/src/test/java/org/springframework/graphql/web/WebGraphQlHandlerTests.java @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.graphql.web; import java.net.URI; @@ -43,25 +44,21 @@ import static org.assertj.core.api.Assertions.assertThat; */ public class WebGraphQlHandlerTests { - private static final WebInput webInput = new WebInput( - URI.create("http://abc.org"), new HttpHeaders(), Collections.singletonMap("query", "{ greeting }"), "1"); - + private static final WebInput webInput = new WebInput(URI.create("http://abc.org"), new HttpHeaders(), + Collections.singletonMap("query", "{ greeting }"), "1"); @Test void reactorContextPropagation() { - GraphQL graphQl = GraphQlTestUtils.initGraphQl("type Query { greeting: String }", - "Query", "greeting", env -> - Mono.deferContextual(context -> { - Object name = context.get("name"); - return Mono.delay(Duration.ofMillis(50)).map(aLong -> "Hello " + name); - })); + GraphQL graphQl = GraphQlTestUtils.initGraphQl("type Query { greeting: String }", "Query", "greeting", + (env) -> Mono.deferContextual((context) -> { + Object name = context.get("name"); + return Mono.delay(Duration.ofMillis(50)).map((aLong) -> "Hello " + name); + })); GraphQlService service = new ExecutionGraphQlService(new TestGraphQlSource(graphQl)); WebGraphQlHandler handler = WebGraphQlHandler.builder(service).build(); - WebOutput webOutput = handler.handle(webInput) - .contextWrite(context -> context.put("name", "007")) - .block(); + WebOutput webOutput = handler.handle(webInput).contextWrite((context) -> context.put("name", "007")).block(); Map data = webOutput.getData(); assertThat(data).hasSize(1).containsEntry("greeting", "Hello 007"); @@ -69,22 +66,18 @@ public class WebGraphQlHandlerTests { @Test void reactorContextPropagationToExceptionResolver() { - GraphQL graphQl = GraphQlTestUtils.initGraphQl("type Query { greeting: String }", - "Query", "greeting", env -> { + GraphQL graphQl = GraphQlTestUtils.initGraphQl("type Query { greeting: String }", "Query", "greeting", + (env) -> { throw new IllegalArgumentException("Invalid greeting"); }, - (ex, env) -> Mono.deferContextual(view -> Mono.just(Collections.singletonList( - GraphqlErrorBuilder.newError(env) - .message("Resolved error: " + ex.getMessage() + ", name=" + view.get("name")) - .errorType(ErrorType.BAD_REQUEST) - .build())))); + (ex, env) -> Mono.deferContextual((view) -> Mono.just(Collections.singletonList(GraphqlErrorBuilder + .newError(env).message("Resolved error: " + ex.getMessage() + ", name=" + view.get("name")) + .errorType(ErrorType.BAD_REQUEST).build())))); GraphQlService service = new ExecutionGraphQlService(new TestGraphQlSource(graphQl)); WebGraphQlHandler handler = WebGraphQlHandler.builder(service).build(); - WebOutput webOutput = handler.handle(webInput) - .contextWrite(context -> context.put("name", "007")) - .block(); + WebOutput webOutput = handler.handle(webInput).contextWrite((context) -> context.put("name", "007")).block(); Map data = webOutput.getData(); assertThat(data).hasSize(1).containsEntry("greeting", null); @@ -100,15 +93,15 @@ public class WebGraphQlHandlerTests { nameThreadLocal.set("007"); TestThreadLocalAccessor threadLocalAccessor = new TestThreadLocalAccessor<>(nameThreadLocal); try { - GraphQL graphQl = GraphQlTestUtils.initGraphQl("type Query { greeting: String }", - "Query", "greeting", env -> "Hello " + nameThreadLocal.get()); + GraphQL graphQl = GraphQlTestUtils.initGraphQl("type Query { greeting: String }", "Query", "greeting", + (env) -> "Hello " + nameThreadLocal.get()); GraphQlService service = new ExecutionGraphQlService(new TestGraphQlSource(graphQl)); WebGraphQlHandler handler = WebGraphQlHandler.builder(service) - .interceptor((input, next) -> Mono.delay(Duration.ofMillis(10)).flatMap(aLong -> next.handle(input))) - .threadLocalAccessor(threadLocalAccessor) - .build(); + .interceptor( + (input, next) -> Mono.delay(Duration.ofMillis(10)).flatMap((aLong) -> next.handle(input))) + .threadLocalAccessor(threadLocalAccessor).build(); Map data = handler.handle(webInput).block().getData(); @@ -125,22 +118,21 @@ public class WebGraphQlHandlerTests { nameThreadLocal.set("007"); TestThreadLocalAccessor threadLocalAccessor = new TestThreadLocalAccessor<>(nameThreadLocal); try { - GraphQL graphQl = GraphQlTestUtils.initGraphQl("type Query { greeting: String }", - "Query", "greeting", env -> { + GraphQL graphQl = GraphQlTestUtils.initGraphQl("type Query { greeting: String }", "Query", "greeting", + (env) -> { throw new IllegalArgumentException("Invalid greeting"); }, - (SyncDataFetcherExceptionResolver) (ex, env) -> Collections.singletonList( - GraphqlErrorBuilder.newError(env) + (SyncDataFetcherExceptionResolver) (ex, + env) -> Collections.singletonList(GraphqlErrorBuilder.newError(env) .message("Resolved error: " + ex.getMessage() + ", name=" + nameThreadLocal.get()) - .errorType(ErrorType.BAD_REQUEST) - .build())); + .errorType(ErrorType.BAD_REQUEST).build())); GraphQlService service = new ExecutionGraphQlService(new TestGraphQlSource(graphQl)); WebGraphQlHandler handler = WebGraphQlHandler.builder(service) - .interceptor((input, next) -> Mono.delay(Duration.ofMillis(10)).flatMap(aLong -> next.handle(input))) - .threadLocalAccessor(threadLocalAccessor) - .build(); + .interceptor( + (input, next) -> Mono.delay(Duration.ofMillis(10)).flatMap((aLong) -> next.handle(input))) + .threadLocalAccessor(threadLocalAccessor).build(); WebOutput webOutput = handler.handle(webInput).block(); diff --git a/spring-graphql/src/test/java/org/springframework/graphql/web/WebInterceptorTests.java b/spring-graphql/src/test/java/org/springframework/graphql/web/WebInterceptorTests.java index ca7218e8..77690922 100644 --- a/spring-graphql/src/test/java/org/springframework/graphql/web/WebInterceptorTests.java +++ b/spring-graphql/src/test/java/org/springframework/graphql/web/WebInterceptorTests.java @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.graphql.web; import java.net.URI; @@ -34,21 +35,17 @@ import static org.assertj.core.api.Assertions.assertThat; * Unit tests for a {@link WebInterceptor} chain. */ public class WebInterceptorTests { - - private static final WebInput webInput = new WebInput( - URI.create("http://abc.org"), new HttpHeaders(), Collections.singletonMap("query", "{ notUsed }"), "1"); + private static final WebInput webInput = new WebInput(URI.create("http://abc.org"), new HttpHeaders(), + Collections.singletonMap("query", "{ notUsed }"), "1"); @Test void interceptorOrder() { StringBuilder output = new StringBuilder(); - WebGraphQlHandler handler = WebGraphQlHandler.builder(input -> emptyExecutionResult()) - .interceptors(Arrays.asList( - new OrderInterceptor(1, output), - new OrderInterceptor(2, output), - new OrderInterceptor(3, output) - )) + WebGraphQlHandler handler = WebGraphQlHandler.builder((input) -> emptyExecutionResult()) + .interceptors(Arrays.asList(new OrderInterceptor(1, output), new OrderInterceptor(2, output), + new OrderInterceptor(3, output))) .build(); handler.handle(webInput).block(); @@ -57,10 +54,9 @@ public class WebInterceptorTests { @Test void responseHeader() { - WebGraphQlHandler handler = WebGraphQlHandler.builder(input -> emptyExecutionResult()) - .interceptor((input, next) -> - next.handle(input).map(output -> - output.transform(builder -> builder.responseHeader("testHeader", "testValue")))) + WebGraphQlHandler handler = WebGraphQlHandler.builder((input) -> emptyExecutionResult()) + .interceptor((input, next) -> next.handle(input).map( + (output) -> output.transform((builder) -> builder.responseHeader("testHeader", "testValue")))) .build(); HttpHeaders headers = handler.handle(webInput).block().getResponseHeaders(); @@ -72,16 +68,13 @@ public class WebInterceptorTests { void executionInputCustomization() { AtomicReference actualName = new AtomicReference<>(); - WebGraphQlHandler handler = WebGraphQlHandler - .builder(input -> { - actualName.set(input.getOperationName()); - return emptyExecutionResult(); - }) - .interceptor((webInput, next) -> { - webInput.configureExecutionInput((input, builder) -> builder.operationName("testOp").build()); - return next.handle(webInput); - }) - .build(); + WebGraphQlHandler handler = WebGraphQlHandler.builder((input) -> { + actualName.set(input.getOperationName()); + return emptyExecutionResult(); + }).interceptor((webInput, next) -> { + webInput.configureExecutionInput((input, builder) -> builder.operationName("testOp").build()); + return next.handle(webInput); + }).build(); handler.handle(webInput).block(); @@ -92,14 +85,13 @@ public class WebInterceptorTests { return Mono.just(ExecutionResultImpl.newExecutionResult().build()); } - private static class OrderInterceptor implements WebInterceptor { private final StringBuilder output; private final int order; - public OrderInterceptor(int order, StringBuilder output) { + OrderInterceptor(int order, StringBuilder output) { this.output = output; this.order = order; } @@ -107,13 +99,12 @@ public class WebInterceptorTests { @Override public Mono intercept(WebInput input, WebGraphQlHandler next) { this.output.append(":pre").append(this.order); - return next.handle(input) - .map(output -> { - this.output.append(":post").append(this.order); - return output; - }) - .subscribeOn(Schedulers.boundedElastic()); + return next.handle(input).map((output) -> { + this.output.append(":post").append(this.order); + return output; + }).subscribeOn(Schedulers.boundedElastic()); } + } } diff --git a/spring-graphql/src/test/java/org/springframework/graphql/web/webflux/GraphQlWebSocketHandlerTests.java b/spring-graphql/src/test/java/org/springframework/graphql/web/webflux/GraphQlWebSocketHandlerTests.java index 3229b82d..412baea3 100644 --- a/spring-graphql/src/test/java/org/springframework/graphql/web/webflux/GraphQlWebSocketHandlerTests.java +++ b/spring-graphql/src/test/java/org/springframework/graphql/web/webflux/GraphQlWebSocketHandlerTests.java @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.graphql.web.webflux; import java.nio.charset.StandardCharsets; @@ -23,6 +24,7 @@ import java.util.List; import java.util.Map; import java.util.function.BiConsumer; +import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.Test; import reactor.core.publisher.Flux; import reactor.core.publisher.Sinks; @@ -41,7 +43,6 @@ import org.springframework.web.reactive.socket.WebSocketMessage; import static org.assertj.core.api.Assertions.as; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.InstanceOfAssertFactories.map; /** * Unit tests for {@link GraphQlWebSocketHandler}. @@ -50,81 +51,64 @@ public class GraphQlWebSocketHandlerTests { private static final Jackson2JsonDecoder decoder = new Jackson2JsonDecoder(); - @Test void query() { - TestWebSocketSession session = handle(Flux.just( - toWebSocketMessage("{\"type\":\"connection_init\"}"), + TestWebSocketSession session = handle(Flux.just(toWebSocketMessage("{\"type\":\"connection_init\"}"), toWebSocketMessage(BookTestUtils.BOOK_QUERY))); StepVerifier.create(session.getOutput()) - .consumeNextWith(message -> assertMessageType(message, "connection_ack")) - .consumeNextWith(message -> - assertThat(decode(message)) - .hasSize(3) - .containsEntry("id", BookTestUtils.SUBSCRIPTION_ID) - .containsEntry("type", "next") - .extractingByKey("payload", as(map(String.class, Object.class))) - .extractingByKey("data", as(map(String.class, Object.class))) - .extractingByKey("bookById", as(map(String.class, Object.class))) - .containsEntry("name", "Nineteen Eighty-Four")) - .consumeNextWith(message -> assertMessageType(message, "complete")) - .verifyComplete(); + .consumeNextWith((message) -> assertMessageType(message, "connection_ack")) + .consumeNextWith((message) -> assertThat(decode(message)).hasSize(3) + .containsEntry("id", BookTestUtils.SUBSCRIPTION_ID).containsEntry("type", "next") + .extractingByKey("payload", as(InstanceOfAssertFactories.map(String.class, Object.class))) + .extractingByKey("data", as(InstanceOfAssertFactories.map(String.class, Object.class))) + .extractingByKey("bookById", as(InstanceOfAssertFactories.map(String.class, Object.class))) + .containsEntry("name", "Nineteen Eighty-Four")) + .consumeNextWith((message) -> assertMessageType(message, "complete")).verifyComplete(); } @Test void subscription() { - TestWebSocketSession session = handle(Flux.just( - toWebSocketMessage("{\"type\":\"connection_init\"}"), + TestWebSocketSession session = handle(Flux.just(toWebSocketMessage("{\"type\":\"connection_init\"}"), toWebSocketMessage(BookTestUtils.BOOK_SUBSCRIPTION))); - BiConsumer bookPayloadAssertion = (message, bookId) -> - assertThat(decode(message)) - .hasSize(3) - .containsEntry("id", BookTestUtils.SUBSCRIPTION_ID) - .containsEntry("type", "next") - .extractingByKey("payload", as(map(String.class, Object.class))) - .extractingByKey("data", as(map(String.class, Object.class))) - .extractingByKey("bookSearch", as(map(String.class, Object.class))) - .containsEntry("id", bookId); + BiConsumer bookPayloadAssertion = (message, bookId) -> assertThat(decode(message)) + .hasSize(3).containsEntry("id", BookTestUtils.SUBSCRIPTION_ID).containsEntry("type", "next") + .extractingByKey("payload", as(InstanceOfAssertFactories.map(String.class, Object.class))) + .extractingByKey("data", as(InstanceOfAssertFactories.map(String.class, Object.class))) + .extractingByKey("bookSearch", as(InstanceOfAssertFactories.map(String.class, Object.class))) + .containsEntry("id", bookId); StepVerifier.create(session.getOutput()) - .consumeNextWith(message -> assertMessageType(message, "connection_ack")) - .consumeNextWith(message -> bookPayloadAssertion.accept(message, "1")) - .consumeNextWith(message -> bookPayloadAssertion.accept(message, "5")) - .consumeNextWith(message -> assertMessageType(message, "complete")) - .verifyComplete(); + .consumeNextWith((message) -> assertMessageType(message, "connection_ack")) + .consumeNextWith((message) -> bookPayloadAssertion.accept(message, "1")) + .consumeNextWith((message) -> bookPayloadAssertion.accept(message, "5")) + .consumeNextWith((message) -> assertMessageType(message, "complete")).verifyComplete(); } @Test void unauthorizedWithoutMessageType() { - TestWebSocketSession session = handle(Flux.just( - toWebSocketMessage("{\"type\":\"connection_init\"}"), + TestWebSocketSession session = handle(Flux.just(toWebSocketMessage("{\"type\":\"connection_init\"}"), toWebSocketMessage("{\"id\":\"" + BookTestUtils.SUBSCRIPTION_ID + "\"}"))); StepVerifier.create(session.getOutput()) - .consumeNextWith(message -> assertMessageType(message, "connection_ack")) - .verifyComplete(); + .consumeNextWith((message) -> assertMessageType(message, "connection_ack")).verifyComplete(); - StepVerifier.create(session.closeStatus()) - .expectNext(new CloseStatus(4400, "Invalid message")) + StepVerifier.create(session.closeStatus()).expectNext(new CloseStatus(4400, "Invalid message")) .verifyComplete(); } @Test void invalidMessageWithoutId() { - Flux input = Flux.just( - toWebSocketMessage("{\"type\":\"connection_init\"}"), - toWebSocketMessage("{\"type\":\"subscribe\"}")); // No message id + Flux input = Flux.just(toWebSocketMessage("{\"type\":\"connection_init\"}"), + toWebSocketMessage("{\"type\":\"subscribe\"}")); // No message id TestWebSocketSession session = handle(input); StepVerifier.create(session.getOutput()) - .consumeNextWith(message -> assertMessageType(message, "connection_ack")) - .verifyComplete(); + .consumeNextWith((message) -> assertMessageType(message, "connection_ack")).verifyComplete(); - StepVerifier.create(session.closeStatus()) - .expectNext(new CloseStatus(4400, "Invalid message")) + StepVerifier.create(session.closeStatus()).expectNext(new CloseStatus(4400, "Invalid message")) .verifyComplete(); } @@ -133,52 +117,48 @@ public class GraphQlWebSocketHandlerTests { TestWebSocketSession session = handle(Flux.just(toWebSocketMessage(BookTestUtils.BOOK_SUBSCRIPTION))); StepVerifier.create(session.getOutput()).verifyComplete(); - StepVerifier.create(session.closeStatus()) - .expectNext(new CloseStatus(4401, "Unauthorized")) - .verifyComplete(); + StepVerifier.create(session.closeStatus()).expectNext(new CloseStatus(4401, "Unauthorized")).verifyComplete(); } @Test void tooManyConnectionInitRequests() { - TestWebSocketSession session = handle(Flux.just( - toWebSocketMessage("{\"type\":\"connection_init\"}"), + TestWebSocketSession session = handle(Flux.just(toWebSocketMessage("{\"type\":\"connection_init\"}"), toWebSocketMessage("{\"type\":\"connection_init\"}"))); StepVerifier.create(session.getOutput()) - .consumeNextWith(message -> assertMessageType(message, "connection_ack")) - .verifyComplete(); + .consumeNextWith((message) -> assertMessageType(message, "connection_ack")).verifyComplete(); - StepVerifier.create(session.closeStatus()) - .expectNext(new CloseStatus(4429, "Too many initialisation requests")) + StepVerifier.create(session.closeStatus()).expectNext(new CloseStatus(4429, "Too many initialisation requests")) .verifyComplete(); } @Test void connectionInitTimeout() { - GraphQlWebSocketHandler handler = new GraphQlWebSocketHandler( - BookTestUtils.initWebGraphQlHandler(), ServerCodecConfigurer.create(), Duration.ofMillis(50)); + GraphQlWebSocketHandler handler = new GraphQlWebSocketHandler(BookTestUtils.initWebGraphQlHandler(), + ServerCodecConfigurer.create(), Duration.ofMillis(50)); TestWebSocketSession session = new TestWebSocketSession(Flux.empty()); handler.handle(session).block(); StepVerifier.create(session.closeStatus()) - .expectNext(new CloseStatus(4408, "Connection initialisation timeout")) - .verifyComplete(); + .expectNext(new CloseStatus(4408, "Connection initialisation timeout")).verifyComplete(); } @Test void subscriptionExists() { - TestWebSocketSession session = handle(Flux.just( - toWebSocketMessage("{\"type\":\"connection_init\"}"), - toWebSocketMessage(BookTestUtils.BOOK_SUBSCRIPTION), - toWebSocketMessage(BookTestUtils.BOOK_SUBSCRIPTION)), new ConsumeOneAndNeverCompleteInterceptor()); + TestWebSocketSession session = handle( + Flux.just(toWebSocketMessage("{\"type\":\"connection_init\"}"), + toWebSocketMessage(BookTestUtils.BOOK_SUBSCRIPTION), + toWebSocketMessage(BookTestUtils.BOOK_SUBSCRIPTION)), + new ConsumeOneAndNeverCompleteInterceptor()); // Collect messages until session closed List> messages = new ArrayList<>(); - session.getOutput().subscribe(message -> messages.add(decode(message))); + session.getOutput().subscribe((message) -> messages.add(decode(message))); StepVerifier.create(session.closeStatus()) - .expectNext(new CloseStatus(4409, "Subscriber for " + BookTestUtils.SUBSCRIPTION_ID + " already exists")) + .expectNext( + new CloseStatus(4409, "Subscriber for " + BookTestUtils.SUBSCRIPTION_ID + " already exists")) .verifyComplete(); assertThat(messages.size()).isEqualTo(2); @@ -192,27 +172,24 @@ public class GraphQlWebSocketHandlerTests { input.tryEmitNext(toWebSocketMessage("{\"type\":\"connection_init\"}")); input.tryEmitNext(toWebSocketMessage(BookTestUtils.BOOK_SUBSCRIPTION)); - TestWebSocketSession session = - handle(input.asFlux(), new ConsumeOneAndNeverCompleteInterceptor()); + TestWebSocketSession session = handle(input.asFlux(), new ConsumeOneAndNeverCompleteInterceptor()); String completeMessage = "{\"id\":\"" + BookTestUtils.SUBSCRIPTION_ID + "\",\"type\":\"complete\"}"; StepVerifier.create(session.getOutput()) - .consumeNextWith(message -> assertMessageType(message, "connection_ack")) - .consumeNextWith(message -> assertMessageType(message, "next")) + .consumeNextWith((message) -> assertMessageType(message, "connection_ack")) + .consumeNextWith((message) -> assertMessageType(message, "next")) .then(() -> input.tryEmitNext(toWebSocketMessage(completeMessage))) .as("Second subscription with same id is possible only if the first was properly removed") .then(() -> input.tryEmitNext(toWebSocketMessage(BookTestUtils.BOOK_SUBSCRIPTION))) - .consumeNextWith(message -> assertMessageType(message, "next")) + .consumeNextWith((message) -> assertMessageType(message, "next")) .then(() -> input.tryEmitNext(toWebSocketMessage(completeMessage))) .verifyTimeout(Duration.ofMillis(500)); } private TestWebSocketSession handle(Flux input, WebInterceptor... interceptors) { - GraphQlWebSocketHandler handler = new GraphQlWebSocketHandler( - BookTestUtils.initWebGraphQlHandler(interceptors), - ServerCodecConfigurer.create(), - Duration.ofSeconds(60)); + GraphQlWebSocketHandler handler = new GraphQlWebSocketHandler(BookTestUtils.initWebGraphQlHandler(interceptors), + ServerCodecConfigurer.create(), Duration.ofSeconds(60)); TestWebSocketSession session = new TestWebSocketSession(input); handler.handle(session).block(); @@ -224,10 +201,9 @@ public class GraphQlWebSocketHandlerTests { return new WebSocketMessage(WebSocketMessage.Type.TEXT, buffer); } - @SuppressWarnings({"unchecked", "ConstantConditions"}) + @SuppressWarnings({ "unchecked", "ConstantConditions" }) private Map decode(WebSocketMessage message) { - return (Map) decoder.decode( - DataBufferUtils.retain(message.getPayload()), + return (Map) decoder.decode(DataBufferUtils.retain(message.getPayload()), GraphQlWebSocketHandler.MAP_RESOLVABLE_TYPE, null, Collections.emptyMap()); } diff --git a/spring-graphql/src/test/java/org/springframework/graphql/web/webflux/TestWebSocketSession.java b/spring-graphql/src/test/java/org/springframework/graphql/web/webflux/TestWebSocketSession.java index 33f027bb..329cb66f 100644 --- a/spring-graphql/src/test/java/org/springframework/graphql/web/webflux/TestWebSocketSession.java +++ b/spring-graphql/src/test/java/org/springframework/graphql/web/webflux/TestWebSocketSession.java @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.graphql.web.webflux; import java.net.URI; @@ -30,8 +31,8 @@ import org.springframework.web.reactive.socket.WebSocketMessage; import org.springframework.web.reactive.socket.adapter.AbstractWebSocketSession; /** - * {@link org.springframework.web.reactive.socket.WebSocketSession} that is given - * an input Flux of messages and exposes a Flux of published output messages. + * {@link org.springframework.web.reactive.socket.WebSocketSession} that is given an input + * Flux of messages and exposes a Flux of published output messages. */ class TestWebSocketSession extends AbstractWebSocketSession { @@ -41,19 +42,16 @@ class TestWebSocketSession extends AbstractWebSocketSession { private final Sinks.One closeStatusSink = Sinks.one(); - - public TestWebSocketSession(Flux input) { + TestWebSocketSession(Flux input) { this("1", URI.create("https://example.org/graphql"), input); } - public TestWebSocketSession(String id, URI uri, Flux input) { - super(new Object(), id, - new HandshakeInfo(uri, new HttpHeaders(), Mono.empty(), null), + TestWebSocketSession(String id, URI uri, Flux input) { + super(new Object(), id, new HandshakeInfo(uri, new HttpHeaders(), Mono.empty(), null), DefaultDataBufferFactory.sharedInstance); this.input = input; } - @Override public Flux receive() { return this.input; @@ -65,7 +63,7 @@ class TestWebSocketSession extends AbstractWebSocketSession { return Mono.empty(); } - public Flux getOutput() { + Flux getOutput() { return this.output; } @@ -84,4 +82,5 @@ class TestWebSocketSession extends AbstractWebSocketSession { public Mono closeStatus() { return this.closeStatusSink.asMono(); } + } diff --git a/spring-graphql/src/test/java/org/springframework/graphql/web/webmvc/GraphQlWebSocketHandlerTests.java b/spring-graphql/src/test/java/org/springframework/graphql/web/webmvc/GraphQlWebSocketHandlerTests.java index 65fb25f7..c7bb2725 100644 --- a/spring-graphql/src/test/java/org/springframework/graphql/web/webmvc/GraphQlWebSocketHandlerTests.java +++ b/spring-graphql/src/test/java/org/springframework/graphql/web/webmvc/GraphQlWebSocketHandlerTests.java @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.graphql.web.webmvc; import java.io.ByteArrayInputStream; @@ -25,6 +26,7 @@ import java.util.Map; import java.util.function.BiConsumer; import java.util.function.Consumer; +import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.Test; import reactor.test.StepVerifier; @@ -41,7 +43,6 @@ import org.springframework.web.socket.WebSocketMessage; import static org.assertj.core.api.Assertions.as; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.InstanceOfAssertFactories.map; /** * Unit tests for {@link GraphQlWebSocketHandler}. @@ -50,81 +51,69 @@ public class GraphQlWebSocketHandlerTests { private static final HttpMessageConverter converter = new MappingJackson2HttpMessageConverter(); - private final TestWebSocketSession session = new TestWebSocketSession(); private final GraphQlWebSocketHandler handler = initWebSocketHandler(); - @Test void query() throws Exception { - handle(this.handler, - new TextMessage("{\"type\":\"connection_init\"}"), + handle(this.handler, new TextMessage("{\"type\":\"connection_init\"}"), new TextMessage(BookTestUtils.BOOK_QUERY)); StepVerifier.create(this.session.getOutput()) - .consumeNextWith(message -> assertMessageType(message, "connection_ack")) - .consumeNextWith(message -> - assertThat(decode(message)) - .hasSize(3) - .containsEntry("id", BookTestUtils.SUBSCRIPTION_ID) - .containsEntry("type", "next") - .extractingByKey("payload", as(map(String.class, Object.class))) - .extractingByKey("data", as(map(String.class, Object.class))) - .extractingByKey("bookById", as(map(String.class, Object.class))) - .containsEntry("name", "Nineteen Eighty-Four")) - .consumeNextWith(message -> assertMessageType(message, "complete")) - .then(this.session::close) // Complete output Flux + .consumeNextWith((message) -> assertMessageType(message, "connection_ack")) + .consumeNextWith((message) -> assertThat(decode(message)).hasSize(3) + .containsEntry("id", BookTestUtils.SUBSCRIPTION_ID).containsEntry("type", "next") + .extractingByKey("payload", as(InstanceOfAssertFactories.map(String.class, Object.class))) + .extractingByKey("data", as(InstanceOfAssertFactories.map(String.class, Object.class))) + .extractingByKey("bookById", as(InstanceOfAssertFactories.map(String.class, Object.class))) + .containsEntry("name", "Nineteen Eighty-Four")) + .consumeNextWith((message) -> assertMessageType(message, "complete")).then(this.session::close) // Complete + // output + // Flux .verifyComplete(); } @Test void subscription() throws Exception { - handle(this.handler, - new TextMessage("{\"type\":\"connection_init\"}"), + handle(this.handler, new TextMessage("{\"type\":\"connection_init\"}"), new TextMessage(BookTestUtils.BOOK_SUBSCRIPTION)); - BiConsumer, String> bookPayloadAssertion = (message, bookId) -> - assertThat(decode(message)) - .hasSize(3) - .containsEntry("id", BookTestUtils.SUBSCRIPTION_ID) - .containsEntry("type", "next") - .extractingByKey("payload", as(map(String.class, Object.class))) - .extractingByKey("data", as(map(String.class, Object.class))) - .extractingByKey("bookSearch", as(map(String.class, Object.class))) - .containsEntry("id", bookId); + BiConsumer, String> bookPayloadAssertion = (message, bookId) -> assertThat(decode(message)) + .hasSize(3).containsEntry("id", BookTestUtils.SUBSCRIPTION_ID).containsEntry("type", "next") + .extractingByKey("payload", as(InstanceOfAssertFactories.map(String.class, Object.class))) + .extractingByKey("data", as(InstanceOfAssertFactories.map(String.class, Object.class))) + .extractingByKey("bookSearch", as(InstanceOfAssertFactories.map(String.class, Object.class))) + .containsEntry("id", bookId); StepVerifier.create(this.session.getOutput()) - .consumeNextWith(message -> assertMessageType(message, "connection_ack")) - .consumeNextWith(message -> bookPayloadAssertion.accept(message, "1")) - .consumeNextWith(message -> bookPayloadAssertion.accept(message, "5")) - .consumeNextWith(message -> assertMessageType(message, "complete")) - .then(this.session::close) // Complete output Flux + .consumeNextWith((message) -> assertMessageType(message, "connection_ack")) + .consumeNextWith((message) -> bookPayloadAssertion.accept(message, "1")) + .consumeNextWith((message) -> bookPayloadAssertion.accept(message, "5")) + .consumeNextWith((message) -> assertMessageType(message, "complete")).then(this.session::close) + // Complete output Flux .verifyComplete(); } @Test void unauthorizedWithoutMessageType() throws Exception { - handle(this.handler, - new TextMessage("{\"type\":\"connection_init\"}"), - new TextMessage("{\"id\":\"" + BookTestUtils.SUBSCRIPTION_ID + "\"}")); // No message type + handle(this.handler, new TextMessage("{\"type\":\"connection_init\"}"), + new TextMessage("{\"id\":\"" + BookTestUtils.SUBSCRIPTION_ID + "\"}")); + // No message type StepVerifier.create(this.session.getOutput()) - .consumeNextWith(message -> assertMessageType(message, "connection_ack")) - .verifyComplete(); + .consumeNextWith((message) -> assertMessageType(message, "connection_ack")).verifyComplete(); assertThat(this.session.getCloseStatus()).isEqualTo(new CloseStatus(4400, "Invalid message")); } @Test void invalidMessageWithoutId() throws Exception { - handle(this.handler, - new TextMessage("{\"type\":\"connection_init\"}"), - new TextMessage("{\"type\":\"subscribe\"}")); // No message id + handle(this.handler, new TextMessage("{\"type\":\"connection_init\"}"), + new TextMessage("{\"type\":\"subscribe\"}")); // No message id StepVerifier.create(this.session.getOutput()) - .consumeNextWith(message -> assertMessageType(message, "connection_ack")) - .verifyComplete(); + .consumeNextWith((message) -> assertMessageType(message, "connection_ack")).verifyComplete(); assertThat(this.session.getCloseStatus()).isEqualTo(new CloseStatus(4400, "Invalid message")); } @@ -139,43 +128,39 @@ public class GraphQlWebSocketHandlerTests { @Test void tooManyConnectionInitRequests() throws Exception { - handle(this.handler, - new TextMessage("{\"type\":\"connection_init\"}"), + handle(this.handler, new TextMessage("{\"type\":\"connection_init\"}"), new TextMessage("{\"type\":\"connection_init\"}")); StepVerifier.create(this.session.getOutput()) - .consumeNextWith(message -> assertMessageType(message, "connection_ack")) - .verifyComplete(); + .consumeNextWith((message) -> assertMessageType(message, "connection_ack")).verifyComplete(); - assertThat(this.session.getCloseStatus()) - .isEqualTo(new CloseStatus(4429, "Too many initialisation requests")); + assertThat(this.session.getCloseStatus()).isEqualTo(new CloseStatus(4429, "Too many initialisation requests")); } @Test void connectionInitTimeout() { - GraphQlWebSocketHandler handler = new GraphQlWebSocketHandler( - BookTestUtils.initWebGraphQlHandler(), converter, Duration.ofMillis(50)); + GraphQlWebSocketHandler handler = new GraphQlWebSocketHandler(BookTestUtils.initWebGraphQlHandler(), converter, + Duration.ofMillis(50)); handler.afterConnectionEstablished(this.session); StepVerifier.create(this.session.closeStatus()) - .expectNext(new CloseStatus(4408, "Connection initialisation timeout")) - .verifyComplete(); + .expectNext(new CloseStatus(4408, "Connection initialisation timeout")).verifyComplete(); } @Test void subscriptionExists() throws Exception { handle(initWebSocketHandler(new ConsumeOneAndNeverCompleteInterceptor()), - new TextMessage("{\"type\":\"connection_init\"}"), - new TextMessage(BookTestUtils.BOOK_SUBSCRIPTION), + new TextMessage("{\"type\":\"connection_init\"}"), new TextMessage(BookTestUtils.BOOK_SUBSCRIPTION), new TextMessage(BookTestUtils.BOOK_SUBSCRIPTION)); // Collect messages until session closed List> messages = new ArrayList<>(); - session.getOutput().subscribe(message -> messages.add(decode(message))); + this.session.getOutput().subscribe((message) -> messages.add(decode(message))); StepVerifier.create(this.session.closeStatus()) - .expectNext(new CloseStatus(4409, "Subscriber for " + BookTestUtils.SUBSCRIPTION_ID + " already exists")) + .expectNext( + new CloseStatus(4409, "Subscriber for " + BookTestUtils.SUBSCRIPTION_ID + " already exists")) .verifyComplete(); assertThat(messages.size()).isEqualTo(2); @@ -185,15 +170,13 @@ public class GraphQlWebSocketHandlerTests { @Test void clientCompletion() throws Exception { - GraphQlWebSocketHandler handler = - initWebSocketHandler(new ConsumeOneAndNeverCompleteInterceptor()); + GraphQlWebSocketHandler handler = initWebSocketHandler(new ConsumeOneAndNeverCompleteInterceptor()); - handle(handler, - new TextMessage("{\"type\":\"connection_init\"}"), + handle(handler, new TextMessage("{\"type\":\"connection_init\"}"), new TextMessage(BookTestUtils.BOOK_SUBSCRIPTION)); String completeMessage = "{\"id\":\"" + BookTestUtils.SUBSCRIPTION_ID + "\",\"type\":\"complete\"}"; - Consumer messageSender = body -> { + Consumer messageSender = (body) -> { try { handler.handleTextMessage(this.session, new TextMessage(body)); } @@ -203,14 +186,13 @@ public class GraphQlWebSocketHandlerTests { }; StepVerifier.create(this.session.getOutput()) - .consumeNextWith(message -> assertMessageType(message, "connection_ack")) - .consumeNextWith(message -> assertMessageType(message, "next")) + .consumeNextWith((message) -> assertMessageType(message, "connection_ack")) + .consumeNextWith((message) -> assertMessageType(message, "next")) .then(() -> messageSender.accept(completeMessage)) .as("Second subscription with same id is possible only if the first was properly removed") .then(() -> messageSender.accept(BookTestUtils.BOOK_SUBSCRIPTION)) - .consumeNextWith(message -> assertMessageType(message, "next")) - .then(() -> messageSender.accept(completeMessage)) - .verifyTimeout(Duration.ofMillis(500)); + .consumeNextWith((message) -> assertMessageType(message, "next")) + .then(() -> messageSender.accept(completeMessage)).verifyTimeout(Duration.ofMillis(500)); } private void handle(GraphQlWebSocketHandler handler, TextMessage... textMessages) throws Exception { @@ -222,8 +204,8 @@ public class GraphQlWebSocketHandlerTests { private GraphQlWebSocketHandler initWebSocketHandler(WebInterceptor... interceptors) { try { - return new GraphQlWebSocketHandler( - BookTestUtils.initWebGraphQlHandler(interceptors), converter, Duration.ofSeconds(60)); + return new GraphQlWebSocketHandler(BookTestUtils.initWebGraphQlHandler(interceptors), converter, + Duration.ofSeconds(60)); } catch (Exception ex) { throw new IllegalStateException(ex); @@ -254,7 +236,6 @@ public class GraphQlWebSocketHandlerTests { } } - private static class HttpInputMessageAdapter extends ByteArrayInputStream implements HttpInputMessage { HttpInputMessageAdapter(TextMessage message) { @@ -270,5 +251,7 @@ public class GraphQlWebSocketHandlerTests { public HttpHeaders getHeaders() { return HttpHeaders.EMPTY; } + } + } diff --git a/spring-graphql/src/test/java/org/springframework/graphql/web/webmvc/TestWebSocketSession.java b/spring-graphql/src/test/java/org/springframework/graphql/web/webmvc/TestWebSocketSession.java index a7b9f307..bfeb2fbc 100644 --- a/spring-graphql/src/test/java/org/springframework/graphql/web/webmvc/TestWebSocketSession.java +++ b/spring-graphql/src/test/java/org/springframework/graphql/web/webmvc/TestWebSocketSession.java @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.graphql.web.webmvc; import java.net.InetSocketAddress; @@ -35,8 +36,8 @@ import org.springframework.web.socket.WebSocketMessage; import org.springframework.web.socket.WebSocketSession; /** - * WebSocketSession that saves sent messages and exposes them as a Flux which - * makes assertions comparable to the same for WebFlux. + * WebSocketSession that saves sent messages and exposes them as a Flux which makes + * assertions comparable to the same for WebFlux. */ public class TestWebSocketSession implements WebSocketSession { @@ -52,10 +53,9 @@ public class TestWebSocketSession implements WebSocketSession { private boolean closed; - @Override public String getId() { - return "1"; + return "1"; } @Override