From d7f5e44ad6096e424a79c979d681fb5c7bbb6d02 Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Thu, 3 Mar 2022 10:15:29 +0000 Subject: [PATCH] Simplify GraphQlClient hierarchy and implementations Add WebGraphQlClient as a common representation for a Web GraphQlClient extension and its builders that has to be started through the HTTP or WebSocket implementations. Remove the common base builder for HTTP and WebSocket, with HTTP simply delegating to the underlying WebClient builder For WebSocket, builder state is exposed from the transport to simplify the mutation logic. DefaultGraphQlClientBuilder is now abstract, leaving subclasses to implement the build method. See gh-10 --- .../AbstractDelegatingGraphQlClient.java | 11 +- ...java => AbstractGraphQlClientBuilder.java} | 54 ++--- .../graphql/client/DefaultGraphQlClient.java | 37 +++- .../client/DefaultHttpGraphQlClient.java | 208 ++++++------------ .../client/DefaultWebSocketGraphQlClient.java | 111 ++++++---- .../graphql/client/GraphQlClient.java | 22 +- .../graphql/client/GraphQlTransport.java | 10 +- .../graphql/client/HttpGraphQlClient.java | 67 ++---- .../graphql/client/HttpGraphQlTransport.java | 1 + .../graphql/client/WebGraphQlClient.java | 85 +++++++ .../client/WebSocketCodecDelegate.java | 16 +- .../client/WebSocketGraphQlClient.java | 45 ++-- .../client/WebSocketGraphQlTransport.java | 38 +++- .../graphql/client/package-info.java | 3 +- 14 files changed, 375 insertions(+), 333 deletions(-) rename spring-graphql/src/main/java/org/springframework/graphql/client/{DefaultGraphQlClientBuilder.java => AbstractGraphQlClientBuilder.java} (65%) create mode 100644 spring-graphql/src/main/java/org/springframework/graphql/client/WebGraphQlClient.java diff --git a/spring-graphql/src/main/java/org/springframework/graphql/client/AbstractDelegatingGraphQlClient.java b/spring-graphql/src/main/java/org/springframework/graphql/client/AbstractDelegatingGraphQlClient.java index 527ea0de..94035429 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/client/AbstractDelegatingGraphQlClient.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/client/AbstractDelegatingGraphQlClient.java @@ -20,15 +20,16 @@ package org.springframework.graphql.client; import org.springframework.util.Assert; /** - * Base class for extensions of {@link GraphQlClient} that mainly assist with - * building the underlying transport, but otherwise delegate to the default - * {@link GraphQlClient} implementation for actual request execution. + * Base class for {@link GraphQlClient} extensions that assist with building an + * underlying transport, but otherwise delegate to the default + * {@link GraphQlClient} implementation to execute requests. * - *

Subclasses must implement {@link GraphQlClient#mutate()} to allow mutation - * of both {@code GraphQlClient} and {@code GraphQlTransport} configuration. + *

Subclasses must implement {@link GraphQlClient#mutate()} to return a + * builder for the specific {@code GraphQlClient} extension. * * @author Rossen Stoyanchev * @since 1.0.0 + * @see AbstractGraphQlClientBuilder */ public abstract class AbstractDelegatingGraphQlClient implements GraphQlClient { diff --git a/spring-graphql/src/main/java/org/springframework/graphql/client/DefaultGraphQlClientBuilder.java b/spring-graphql/src/main/java/org/springframework/graphql/client/AbstractGraphQlClientBuilder.java similarity index 65% rename from spring-graphql/src/main/java/org/springframework/graphql/client/DefaultGraphQlClientBuilder.java rename to spring-graphql/src/main/java/org/springframework/graphql/client/AbstractGraphQlClientBuilder.java index 36142938..94e3a97e 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/client/DefaultGraphQlClientBuilder.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/client/AbstractGraphQlClientBuilder.java @@ -31,54 +31,44 @@ import org.springframework.util.ClassUtils; /** - * Default {@link GraphQlClient.Builder} implementation that builds a - * {@link GraphQlClient} for use with any transport. + * Abstract, base class for transport specific {@link GraphQlClient.Builder} + * implementations. * - *

Intended for use as a base class for builders that do assist with building - * the underlying transport. Such extension + *

Subclasses must implement {@link #build()} and call + * {@link #buildGraphQlClient(GraphQlTransport)} to obtain a default, transport + * agnostic {@code GraphQlClient}. A transport specific extension can then wrap + * this default tester by extending {@link AbstractDelegatingGraphQlClient}. * * @author Rossen Stoyanchev * @since 1.0.0 + * @see AbstractDelegatingGraphQlClient */ -public class DefaultGraphQlClientBuilder> implements GraphQlClient.Builder { +public abstract class AbstractGraphQlClientBuilder> implements GraphQlClient.Builder { private static final boolean jackson2Present; static { - ClassLoader classLoader = DefaultGraphQlClientBuilder.class.getClassLoader(); + ClassLoader classLoader = AbstractGraphQlClientBuilder.class.getClassLoader(); jackson2Present = ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", classLoader) && ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", classLoader); } - @Nullable - private GraphQlTransport transport; - @Nullable private DocumentSource documentSource; - /** - * Constructor with a given transport instance. - */ - DefaultGraphQlClientBuilder(GraphQlTransport transport) { - Assert.notNull(transport, "GraphQlTransport is required"); - this.transport = transport; - } /** - * Constructor for subclass builders that will call - * {@link #transport(GraphQlTransport)} to set the transport instance - * before {@link #build()}. + * Default constructor for use from subclasses. + *

Subclasses must set the transport to use before {@link #build()} or + * during, by overriding {@link #build()}. */ - DefaultGraphQlClientBuilder() { + protected AbstractGraphQlClientBuilder() { } - protected void transport(GraphQlTransport transport) { - this.transport = transport; - } @Override - public B documentSource(@Nullable DocumentSource contentLoader) { + public B documentSource(DocumentSource contentLoader) { this.documentSource = contentLoader; return self(); } @@ -88,10 +78,13 @@ public class DefaultGraphQlClientBuilder> getBuilderInitializer() { return builder -> { @@ -114,7 +107,6 @@ public class DefaultGraphQlClientBuilderThis class is final but works with any transport. + * Default, final {@link GraphQlClient} implementation for use with any transport. * * @author Rossen Stoyanchev * @since 1.0.0 @@ -53,16 +49,17 @@ final class DefaultGraphQlClient implements GraphQlClient { private final DocumentSource documentSource; - private final Consumer> builderInitializer; + private final Consumer> builderInitializer; DefaultGraphQlClient( GraphQlTransport transport, Configuration jsonPathConfig, DocumentSource documentSource, - Consumer> builderInitializer) { + Consumer> builderInitializer) { Assert.notNull(transport, "GraphQlTransport is required"); - Assert.notNull(jsonPathConfig, "Configuration is required"); + Assert.notNull(jsonPathConfig, "JSONPath Configuration is required"); Assert.notNull(documentSource, "DocumentSource is required"); + Assert.notNull(documentSource, "`builderInitializer` is required"); this.transport = transport; this.jsonPathConfig = jsonPathConfig; @@ -83,13 +80,33 @@ final class DefaultGraphQlClient implements GraphQlClient { } @Override - public Builder mutate() { - DefaultGraphQlClientBuilder builder = new DefaultGraphQlClientBuilder<>(this.transport); + public Builder mutate() { + Builder builder = new Builder(this.transport); this.builderInitializer.accept(builder); return builder; } + /** + * Default {@link GraphQlClient.Builder} with a given transport. + */ + static final class Builder extends AbstractGraphQlClientBuilder { + + private final GraphQlTransport transport; + + Builder(GraphQlTransport transport) { + Assert.notNull(transport, "GraphQlTransport is required"); + this.transport = transport; + } + + @Override + public GraphQlClient build() { + return super.buildGraphQlClient(this.transport); + } + + } + + private static final class DefaultRequestSpec implements RequestSpec { private final Mono documentMono; diff --git a/spring-graphql/src/main/java/org/springframework/graphql/client/DefaultHttpGraphQlClient.java b/spring-graphql/src/main/java/org/springframework/graphql/client/DefaultHttpGraphQlClient.java index 13c634ad..1bf52dcb 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/client/DefaultHttpGraphQlClient.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/client/DefaultHttpGraphQlClient.java @@ -17,188 +17,122 @@ package org.springframework.graphql.client; import java.net.URI; -import java.util.Arrays; import java.util.function.Consumer; -import java.util.function.Supplier; import org.springframework.http.HttpHeaders; -import org.springframework.http.codec.ClientCodecConfigurer; -import org.springframework.lang.Nullable; +import org.springframework.http.codec.CodecConfigurer; +import org.springframework.util.Assert; import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.util.DefaultUriBuilderFactory; +import org.springframework.web.util.UriBuilderFactory; +import org.springframework.web.util.UriComponentsBuilder; /** - * Default {@link HttpGraphQlClient} implementation. + * Default {@link HttpGraphQlClient} implementation that builds the underlying + * {@code HttpGraphQlTransport} to use. * * @author Rossen Stoyanchev * @since 1.0.0 */ final class DefaultHttpGraphQlClient extends AbstractDelegatingGraphQlClient implements HttpGraphQlClient { - private final Supplier mutateBuilder; + private final WebClient webClient; + + private final Consumer> builderInitializer; - DefaultHttpGraphQlClient(GraphQlClient graphQlClient, Supplier mutateBuilder) { + DefaultHttpGraphQlClient(GraphQlClient graphQlClient, WebClient webClient, + Consumer> builderInitializer) { + super(graphQlClient); - this.mutateBuilder = mutateBuilder; + + Assert.notNull(webClient, "WebClient is required"); + Assert.notNull(builderInitializer, "`builderInitializer` is required"); + + this.webClient = webClient; + this.builderInitializer = builderInitializer; } public Builder mutate() { - return this.mutateBuilder.get(); - } - - - static class BaseBuilder> extends DefaultGraphQlClientBuilder - implements HttpGraphQlClient.BaseBuilder { - - @Nullable - private URI url; - - private final HttpHeaders headers = new HttpHeaders(); - - @Nullable - private Consumer codecConfigurerConsumer; - - - @Override - public B url(@Nullable String url) { - this.url = (url != null ? URI.create(url) : null); - return self(); - } - - @Override - public B url(@Nullable URI url) { - this.url = url; - return self(); - } - - @Override - public B header(String name, String... values) { - Arrays.stream(values).forEach(value -> this.headers.add(name, value)); - return self(); - } - - @Override - public B headers(Consumer headersConsumer) { - headersConsumer.accept(this.headers); - return self(); - } - - @Override - public B codecConfigurer(Consumer codecConsumer) { - this.codecConfigurerConsumer = codecConsumer; - return self(); - } - - @Nullable - protected URI getUrl() { - return this.url; - } - - protected HttpHeaders getHeaders() { - return this.headers; - } - - @Nullable - protected Consumer getCodecConfigurerConsumer() { - return this.codecConfigurerConsumer; - } - - @SuppressWarnings("unchecked") - private T self() { - return (T) this; - } - - /** - * Exposes a {@code Consumer} to subclasses to initialize new builder instances - * from the configuration of "this" builder. - */ - protected Consumer> getWebBuilderInitializer() { - Consumer> parentInitializer = getBuilderInitializer(); - HttpHeaders headersCopy = new HttpHeaders(); - headersCopy.putAll(getHeaders()); - return builder -> { - builder.url(getUrl()).headers(headers -> headers.putAll(headersCopy)); - if (getCodecConfigurerConsumer() != null) { - builder.codecConfigurer(getCodecConfigurerConsumer()); - } - parentInitializer.accept(builder); - }; - } - + Builder builder = new Builder(this.webClient); + this.builderInitializer.accept(builder); + return builder; } /** - * Default {@link HttpGraphQlClient.Builder} implementation. + * Default {@link HttpGraphQlClient.Builder} implementation, simply wrapping + * and delegating to {@link WebClient.Builder}. */ - static final class Builder extends BaseBuilder implements HttpGraphQlClient.Builder { - - @Nullable - private WebClient webClient; - - @Nullable - private Consumer webClientConfigurers; + static final class Builder extends AbstractGraphQlClientBuilder implements HttpGraphQlClient.Builder { + private final WebClient.Builder webClientBuilder; /** * Constructor to start without a WebClient instance. */ Builder() { + this(WebClient.builder()); } /** * Constructor to start with a pre-configured {@code WebClient}. */ - Builder(WebClient webClient) { - this.webClient = webClient; + Builder(WebClient client) { + this(client.mutate()); + } + + /** + * Constructor to start with a pre-configured {@code WebClient}. + */ + Builder(WebClient.Builder clientBuilder) { + this.webClientBuilder = clientBuilder; } + @Override + public Builder url(String url) { + this.webClientBuilder.baseUrl(url); + return this; + } + + @Override + public Builder url(URI url) { + UriBuilderFactory factory = new DefaultUriBuilderFactory(UriComponentsBuilder.fromUri(url)); + this.webClientBuilder.uriBuilderFactory(factory); + return this; + } + + @Override + public Builder header(String name, String... values) { + this.webClientBuilder.defaultHeader(name, values); + return this; + } + + @Override + public Builder headers(Consumer headersConsumer) { + this.webClientBuilder.defaultHeaders(headersConsumer); + return this; + } + + @Override + public Builder codecConfigurer(Consumer codecsConsumer) { + this.webClientBuilder.codecs(codecsConsumer::accept); + return this; + } + @Override public Builder webClient(Consumer configurer) { - this.webClientConfigurers = (this.webClientConfigurers != null ? this.webClientConfigurers.andThen(configurer) : configurer); + configurer.accept(this.webClientBuilder); return this; } @Override public HttpGraphQlClient build() { - - WebClient webClient = initWebClient(); - HttpGraphQlTransport transport = new HttpGraphQlTransport(webClient); - transport(transport); - - GraphQlClient graphQlClient = super.build(); - return new DefaultHttpGraphQlClient(graphQlClient, initMutateBuilderFactory(webClient)); - } - - private WebClient initWebClient() { - WebClient.Builder builder = (this.webClient != null ? this.webClient.mutate() : WebClient.builder()); - - if (getUrl() != null) { - builder.baseUrl(getUrl().toASCIIString()); - } - - builder.defaultHeaders(headers -> headers.putAll(getHeaders())); - - if (getCodecConfigurerConsumer() != null) { - builder.codecs(getCodecConfigurerConsumer()); - } - - if (this.webClientConfigurers != null) { - this.webClientConfigurers.accept(builder); - } - - return builder.build(); - } - - private Supplier initMutateBuilderFactory(WebClient webClient) { - Consumer> parentInitializer = getWebBuilderInitializer(); - return () -> { - Builder builder = new Builder(webClient); - parentInitializer.accept(builder); - return builder; - }; + WebClient webClient = this.webClientBuilder.build(); + GraphQlClient graphQlClient = super.buildGraphQlClient(new HttpGraphQlTransport(webClient)); + return new DefaultHttpGraphQlClient(graphQlClient, webClient, getBuilderInitializer()); } } diff --git a/spring-graphql/src/main/java/org/springframework/graphql/client/DefaultWebSocketGraphQlClient.java b/spring-graphql/src/main/java/org/springframework/graphql/client/DefaultWebSocketGraphQlClient.java index 6fa7dff0..96819733 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/client/DefaultWebSocketGraphQlClient.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/client/DefaultWebSocketGraphQlClient.java @@ -16,20 +16,23 @@ package org.springframework.graphql.client; -import java.util.Map; +import java.net.URI; +import java.util.Arrays; import java.util.function.Consumer; -import java.util.function.Supplier; import reactor.core.publisher.Mono; +import org.springframework.http.HttpHeaders; import org.springframework.http.codec.ClientCodecConfigurer; -import org.springframework.lang.Nullable; +import org.springframework.http.codec.CodecConfigurer; import org.springframework.util.Assert; import org.springframework.web.reactive.socket.client.WebSocketClient; +import org.springframework.web.util.DefaultUriBuilderFactory; /** - * Default {@link WebSocketGraphQlClient} implementation. + * Default {@link WebSocketGraphQlClient} implementation that builds the underlying + * {@code WebSocketGraphQlTransport} to use. * * @author Rossen Stoyanchev * @since 1.0.0 @@ -38,15 +41,19 @@ final class DefaultWebSocketGraphQlClient extends AbstractDelegatingGraphQlClien private final WebSocketGraphQlTransport transport; - private final Supplier mutateBuilderFactory; + private final Consumer> builderInitializer; - DefaultWebSocketGraphQlClient( - GraphQlClient delegate, WebSocketGraphQlTransport transport, Supplier mutateBuilderFactory) { + DefaultWebSocketGraphQlClient(GraphQlClient delegate, WebSocketGraphQlTransport transport, + Consumer> builderInitializer) { super(delegate); + + Assert.notNull(transport, "WebSocketGraphQlTransport is required"); + Assert.notNull(builderInitializer, "`builderInitializer` is required"); + this.transport = transport; - this.mutateBuilderFactory = mutateBuilderFactory; + this.builderInitializer = builderInitializer; } @@ -62,72 +69,84 @@ final class DefaultWebSocketGraphQlClient extends AbstractDelegatingGraphQlClien @Override public Builder mutate() { - return this.mutateBuilderFactory.get(); + Builder builder = new Builder(this.transport); + this.builderInitializer.accept(builder); + return builder; } /** * Default {@link WebSocketGraphQlClient.Builder} implementation. */ - static final class Builder extends DefaultHttpGraphQlClient.BaseBuilder + static final class Builder extends AbstractGraphQlClientBuilder implements WebSocketGraphQlClient.Builder { + private URI url; + + private final HttpHeaders headers = new HttpHeaders(); + private final WebSocketClient webSocketClient; - @Nullable - private Object initPayload; + private final CodecConfigurer codecConfigurer; - private Consumer> connectionAckHandler = ackPayload -> {}; - - - Builder(WebSocketClient client) { + /** + * Constructor to start via {@link WebSocketGraphQlClient#builder(URI, WebSocketClient)}. + */ + Builder(URI url, WebSocketClient client) { + this.url = url; this.webSocketClient = client; + this.codecConfigurer = ClientCodecConfigurer.create(); } + /** + * Constructor to mutate. + * @param transport the underlying transport with the current state + */ + Builder(WebSocketGraphQlTransport transport) { + this.url = transport.getUrl(); + this.headers.putAll(transport.getHeaders()); + this.webSocketClient = transport.getWebSocketClient(); + this.codecConfigurer = transport.getCodecConfigurer(); + } @Override - public Builder connectionInitPayload(@Nullable Object connectionInitPayload) { - this.initPayload = connectionInitPayload; + public Builder url(String url) { + url(new DefaultUriBuilderFactory().uriString(url).build()); return this; } @Override - public Builder connectionAckHandler(Consumer> ackHandler) { - this.connectionAckHandler = ackHandler; + public Builder url(URI url) { + this.url = url; + return this; + } + + @Override + public Builder header(String name, String... values) { + this.headers.put(name, Arrays.asList(values)); + return this; + } + + @Override + public Builder headers(Consumer headersConsumer) { + headersConsumer.accept(this.headers); + return this; + } + + @Override + public Builder codecConfigurer(Consumer codecConsumer) { + codecConsumer.accept(this.codecConfigurer); return this; } @Override public WebSocketGraphQlClient build() { - Assert.notNull(getUrl(), "GraphQL endpoint URI is required"); WebSocketGraphQlTransport transport = new WebSocketGraphQlTransport( - getUrl(), getHeaders(), this.webSocketClient, initClientCodecConfigurer(), - this.initPayload, this.connectionAckHandler); + this.url, this.headers, this.webSocketClient, this.codecConfigurer, null, payload -> {}); - transport(transport); - GraphQlClient graphQlClient = super.build(); - - return new DefaultWebSocketGraphQlClient(graphQlClient, transport, mutateBuilderFactory()); - } - - private ClientCodecConfigurer initClientCodecConfigurer() { - ClientCodecConfigurer configurer = ClientCodecConfigurer.create(); - if (getCodecConfigurerConsumer() != null) { - getCodecConfigurerConsumer().accept(configurer); - } - return configurer; - } - - private Supplier mutateBuilderFactory() { - Consumer> parentBuilderInitializer = getWebBuilderInitializer(); - return () -> { - Builder builder = new Builder(this.webSocketClient); - builder.connectionInitPayload(this.initPayload); - builder.connectionAckHandler(this.connectionAckHandler); - parentBuilderInitializer.accept(builder); - return builder; - }; + GraphQlClient graphQlClient = super.buildGraphQlClient(transport); + return new DefaultWebSocketGraphQlClient(graphQlClient, transport, getBuilderInitializer()); } } diff --git a/spring-graphql/src/main/java/org/springframework/graphql/client/GraphQlClient.java b/spring-graphql/src/main/java/org/springframework/graphql/client/GraphQlClient.java index 1fa668dd..0df68f6d 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/client/GraphQlClient.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/client/GraphQlClient.java @@ -29,16 +29,18 @@ import org.springframework.graphql.support.ResourceDocumentSource; import org.springframework.lang.Nullable; /** - * Defines workflow to execute GraphQL requests, independent of the transport. + * Define a workflow to execute GraphQL requests that is independent of the + * underlying transport. * - *

In most cases, you'll want to use a transport specific extension: + *

For most cases, use a transport specific extension: *

* - *

Alternatively, use {@link #builder(GraphQlTransport)} to create an instance - * with any other transport. Or create a transport specific extension. + *

Alternatively, create an instance with any other transport via + * {@link #builder(GraphQlTransport)}. Or create a transport specific extension + * similar to HTTP and WebSocket. * * @author Rossen Stoyanchev * @since 1.0.0 @@ -70,15 +72,15 @@ public interface GraphQlClient { /** - * Create a builder with the given {@code GraphQlTransport}. - *

For GraphQL over HTTP and WebSocket, consider using the extensions - * {@link HttpGraphQlClient} and {@link WebSocketGraphQlClient}. - * This allows plugging in any other transport implementation. + * Create a builder with the given custom {@code GraphQlTransport}. + *

For most cases, use a transport specific extension such as + * {@link HttpGraphQlClient} or {@link WebSocketGraphQlClient}. This method + * is for use with a custom {@code GraphQlTransport}. * @param transport the transport to execute requests with * @return the builder for further initialization */ static Builder builder(GraphQlTransport transport) { - return new DefaultGraphQlClientBuilder<>(transport); + return new DefaultGraphQlClient.Builder(transport); } @@ -92,7 +94,7 @@ public interface GraphQlClient { * {@link #documentName(String)} for resolving a document by name. *

By default, {@link ResourceDocumentSource} is used. */ - B documentSource(@Nullable DocumentSource contentLoader); + B documentSource(DocumentSource contentLoader); /** * Build the {@code GraphQlClient} instance. diff --git a/spring-graphql/src/main/java/org/springframework/graphql/client/GraphQlTransport.java b/spring-graphql/src/main/java/org/springframework/graphql/client/GraphQlTransport.java index 1651dc40..ee24f6ae 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/client/GraphQlTransport.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/client/GraphQlTransport.java @@ -24,7 +24,7 @@ import org.springframework.graphql.GraphQlRequest; /** - * Contract for GraphQL request execution over some transport. + * Contract for executing GraphQL requests over some transport. * * @author Rossen Stoyanchev * @since 1.0.0 @@ -32,20 +32,18 @@ import org.springframework.graphql.GraphQlRequest; public interface GraphQlTransport { /** - * Execute a request that returns a single response such as a "query" or a - * "mutation" operation. + * Execute a request with a single response such as a "query" or "mutation". * @param request the request to execute * @return a {@code Mono} with the {@code ExecutionResult} for the response. * The {@code Mono} may end wth an error due to transport or other issues * such as failures to encode the request or decode the response. - * */ Mono execute(GraphQlRequest request); /** - * Execute a "subscription" request that returns a stream of responses. + * Execute a "subscription" request with a stream of responses. * @param request the request to execute - * @return a {@code Flux} with an {@code ExecutionResult} for each response. + * @return a {@code Flux} of {@code ExecutionResult} responses. * The {@code Flux} may terminate as follows: *

    *
  • Completes if the subscription completes before the connection is closed. diff --git a/spring-graphql/src/main/java/org/springframework/graphql/client/HttpGraphQlClient.java b/spring-graphql/src/main/java/org/springframework/graphql/client/HttpGraphQlClient.java index b376bc69..89d9fdc6 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/client/HttpGraphQlClient.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/client/HttpGraphQlClient.java @@ -16,22 +16,18 @@ package org.springframework.graphql.client; -import java.net.URI; import java.util.function.Consumer; -import org.springframework.http.HttpHeaders; -import org.springframework.http.codec.ClientCodecConfigurer; -import org.springframework.lang.Nullable; import org.springframework.web.reactive.function.client.WebClient; /** - * {@code GraphQlClient} for GraphQL over HTTP via {@link WebClient}. + * GraphQL over HTTP client that uses {@link WebClient}. * * @author Rossen Stoyanchev * @since 1.0.0 */ -public interface HttpGraphQlClient extends GraphQlClient { +public interface HttpGraphQlClient extends WebGraphQlClient { @Override @@ -42,7 +38,7 @@ public interface HttpGraphQlClient extends GraphQlClient { * Create an {@link HttpGraphQlClient} that uses the given {@link WebClient}. */ static HttpGraphQlClient create(WebClient webClient) { - return builder(webClient).build(); + return builder(webClient.mutate()).build(); } /** @@ -54,65 +50,30 @@ public interface HttpGraphQlClient extends GraphQlClient { /** * Variant of {@link #builder()} with a pre-configured {@code WebClient} - * which may be mutated and further customized through the returned builder. + * to mutate and customize further through the returned builder. */ - static Builder builder(WebClient webClient) { - return new DefaultHttpGraphQlClient.Builder(webClient); + static Builder builder(WebClient.Builder webClientBuilder) { + return new DefaultHttpGraphQlClient.Builder(webClientBuilder); } /** - * Base builder for GraphQL clients over a Web transport. + * Builder for the GraphQL over HTTP client. */ - interface BaseBuilder> extends GraphQlClient.Builder { - - /** - * Set the GraphQL endpoint URL. - * @param url the url to make requests to - */ - B url(@Nullable String url); - - /** - * Set the GraphQL endpoint URL. - * @param url the url to make requests to - */ - B url(@Nullable URI url); - - /** - * Add the given header to HTTP requests to the endpoint URL. - * @param name the header name - * @param values the header values - */ - B header(String name, String... values); - - /** - * Variant of {@link #header(String, String...)} that provides access - * to the underlying headers to inspect or modify directly. - * @param headersConsumer a function that consumes the {@code HttpHeaders} - */ - B headers(Consumer headersConsumer); - - /** - * Provide a {@code Consumer} to customize the {@code ClientCodecConfigurer} - * for JSON encoding and decoding of GraphQL payloads. - */ - B codecConfigurer(Consumer codecsConsumer); - - } - - - /** - * Builder for a GraphQL over HTTP client. - */ - interface Builder> extends BaseBuilder { + interface Builder> extends WebGraphQlClient.Builder { /** * Customize the {@code WebClient} to use. + *

    Note that some properties of {@code WebClient.Builder} like the + * base URL, headers, and codecs can be customized through this builder. + * @see #url(String) + * @see #header(String, String...) + * @see #codecConfigurer(Consumer) */ B webClient(Consumer webClient); /** - * Build the {@code HttpGraphQlClient}. + * Build the {@code HttpGraphQlClient} instance. */ @Override HttpGraphQlClient build(); diff --git a/spring-graphql/src/main/java/org/springframework/graphql/client/HttpGraphQlTransport.java b/spring-graphql/src/main/java/org/springframework/graphql/client/HttpGraphQlTransport.java index 9f929446..3d0ccfd8 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/client/HttpGraphQlTransport.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/client/HttpGraphQlTransport.java @@ -29,6 +29,7 @@ import org.springframework.http.MediaType; import org.springframework.util.Assert; import org.springframework.web.reactive.function.client.WebClient; + /** * Transport to execute GraphQL requests over HTTP via {@link WebClient}. * Supports only single-response requests over HTTP POST. For subscription diff --git a/spring-graphql/src/main/java/org/springframework/graphql/client/WebGraphQlClient.java b/spring-graphql/src/main/java/org/springframework/graphql/client/WebGraphQlClient.java new file mode 100644 index 00000000..534fdd6c --- /dev/null +++ b/spring-graphql/src/main/java/org/springframework/graphql/client/WebGraphQlClient.java @@ -0,0 +1,85 @@ +/* + * Copyright 2002-2022 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.client; + +import java.net.URI; +import java.util.function.Consumer; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.codec.CodecConfigurer; + + +/** + * Base contract for the HTTP and WebSocket {@code GraphQlClient} extensions. + * Defines a builder with common configuration for both transports. + * + * @author Rossen Stoyanchev + * @since 1.0.0 + */ +public interface WebGraphQlClient extends GraphQlClient { + + + @Override + Builder mutate(); + + + /** + * Base builder for GraphQL clients over a Web transport. + */ + interface Builder> extends GraphQlClient.Builder { + + /** + * Set the GraphQL endpoint URL as a String. + * @param url the url to send HTTP requests to or connect over WebSocket + */ + B url(String url); + + /** + * Set the GraphQL endpoint URL. + * @param url the url to send HTTP requests to or connect over WebSocket + */ + B url(URI url); + + /** + * Add the given header to HTTP requests or to the WebSocket handshake request. + * @param name the header name + * @param values the header values + */ + B header(String name, String... values); + + /** + * Variant of {@link #header(String, String...)} that provides access + * to the underlying headers to inspect or modify directly. + * @param headersConsumer a function that consumes the {@code HttpHeaders} + */ + B headers(Consumer headersConsumer); + + /** + * Configure the underlying {@code CodecConfigurer} to use for all JSON + * encoding and decoding needs. + */ + B codecConfigurer(Consumer codecsConsumer); + + /** + * Build a {@code WebGraphQlClient} instance. + */ + @Override + WebGraphQlClient build(); + + } + +} diff --git a/spring-graphql/src/main/java/org/springframework/graphql/client/WebSocketCodecDelegate.java b/spring-graphql/src/main/java/org/springframework/graphql/client/WebSocketCodecDelegate.java index cd8cf79a..8e95640d 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/client/WebSocketCodecDelegate.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/client/WebSocketCodecDelegate.java @@ -43,6 +43,8 @@ final class WebSocketCodecDelegate { private static final ResolvableType MESSAGE_TYPE = ResolvableType.forClass(GraphQlWebSocketMessage.class); + private final CodecConfigurer configurer; + private final Decoder decoder; private final Encoder encoder; @@ -52,10 +54,11 @@ final class WebSocketCodecDelegate { this(ClientCodecConfigurer.create()); } - WebSocketCodecDelegate(CodecConfigurer codecConfigurer) { - Assert.notNull(codecConfigurer, "CodecConfigurer is required"); - this.decoder = initDecoder(codecConfigurer); - this.encoder = initEncoder(codecConfigurer); + WebSocketCodecDelegate(CodecConfigurer configurer) { + Assert.notNull(configurer, "CodecConfigurer is required"); + this.configurer = configurer; + this.decoder = initDecoder(configurer); + this.encoder = initEncoder(configurer); } private static Decoder initDecoder(CodecConfigurer configurer) { @@ -75,6 +78,11 @@ final class WebSocketCodecDelegate { } + public CodecConfigurer getCodecConfigurer() { + return this.configurer; + } + + @SuppressWarnings("unchecked") public WebSocketMessage encode(WebSocketSession session, GraphQlWebSocketMessage message) { diff --git a/spring-graphql/src/main/java/org/springframework/graphql/client/WebSocketGraphQlClient.java b/spring-graphql/src/main/java/org/springframework/graphql/client/WebSocketGraphQlClient.java index d2e55d83..c229aae3 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/client/WebSocketGraphQlClient.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/client/WebSocketGraphQlClient.java @@ -17,33 +17,32 @@ package org.springframework.graphql.client; import java.net.URI; -import java.util.Map; -import java.util.function.Consumer; import reactor.core.publisher.Mono; -import org.springframework.lang.Nullable; import org.springframework.web.reactive.socket.client.WebSocketClient; /** - * {@code GraphQlClient} for GraphQL over Web via {@link WebSocketClient}. + * GraphQL over WebSocket client that uses {@link WebSocketClient}. * * @author Rossen Stoyanchev * @since 1.0.0 */ -public interface WebSocketGraphQlClient extends GraphQlClient { +public interface WebSocketGraphQlClient extends WebGraphQlClient { /** - * Start the transport by connecting the WebSocket, sending the - * "connection_init" and waiting for the "connection_ack" message. + * Start the GraphQL session by connecting the WebSocket, sending the + * "connection_init" and receiving the "connection_ack" message. + *

    Note: Only one session is started at a time. + * Additional attempts to start have no impact while a session is active. * @return {@code Mono} that completes when the WebSocket is connected and - * ready to begin sending GraphQL requests + * the GraphQL session is ready to send requests */ Mono start(); /** - * Stop the transport by closing the WebSocket with + * Stop the GraphQL session by closing the WebSocket with * {@link org.springframework.web.reactive.socket.CloseStatus#NORMAL} and * terminating in-progress requests with an error signal. *

    New requests are rejected from the time of this call. If necessary, @@ -57,38 +56,28 @@ public interface WebSocketGraphQlClient extends GraphQlClient { /** - * Create a {@link WebSocketGraphQlClient} that uses the given - * {@code WebSocketClient} to connect to the given URL. + * Create a {@link WebSocketGraphQlClient}. * @param url the GraphQL endpoint URL - * @param webSocketClient the transport client to use + * @param webSocketClient the underlying transport client to use */ static WebSocketGraphQlClient create(URI url, WebSocketClient webSocketClient) { - return builder(webSocketClient).url(url).build(); + return builder(url, webSocketClient).build(); } /** - * Return a builder to initialize a {@link WebSocketGraphQlClient} with. - * @param webSocketClient the transport client to use + * Return a builder for a {@link WebSocketGraphQlClient}. + * @param url the GraphQL endpoint URL + * @param webSocketClient the underlying transport client to use */ - static Builder builder(WebSocketClient webSocketClient) { - return new DefaultWebSocketGraphQlClient.Builder(webSocketClient); + static Builder builder(URI url, WebSocketClient webSocketClient) { + return new DefaultWebSocketGraphQlClient.Builder(url, webSocketClient); } /** * Builder for a GraphQL over WebSocket client. */ - interface Builder> extends HttpGraphQlClient.BaseBuilder { - - /** - * The payload to send with the "connection_init" message. - */ - B connectionInitPayload(@Nullable Object connectionInitPayload); - - /** - * Handler for the payload received with the "connection_ack" message. - */ - B connectionAckHandler(Consumer> ackHandler); + interface Builder> extends WebGraphQlClient.Builder { /** * Build the {@code WebSocketGraphQlClient}. diff --git a/spring-graphql/src/main/java/org/springframework/graphql/client/WebSocketGraphQlTransport.java b/spring-graphql/src/main/java/org/springframework/graphql/client/WebSocketGraphQlTransport.java index 4f6699cf..651c29d6 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/client/WebSocketGraphQlTransport.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/client/WebSocketGraphQlTransport.java @@ -57,6 +57,11 @@ final class WebSocketGraphQlTransport implements GraphQlTransport { private static final Log logger = LogFactory.getLog(WebSocketGraphQlTransport.class); + private final URI url; + + private final HttpHeaders headers = new HttpHeaders(); + + private final WebSocketClient webSocketClient; private final GraphQlSessionHandler graphQlSessionHandler; @@ -64,13 +69,20 @@ final class WebSocketGraphQlTransport implements GraphQlTransport { WebSocketGraphQlTransport( - URI uri, HttpHeaders headers, WebSocketClient client, CodecConfigurer codecConfigurer, + URI url, @Nullable HttpHeaders headers, WebSocketClient client, CodecConfigurer codecConfigurer, @Nullable Object connectionInitPayload, Consumer> connectionAckHandler) { + Assert.notNull(url, "URI is required"); + Assert.notNull(url, "URI is required"); + + this.url = url; + this.headers.putAll(headers != null ? headers : HttpHeaders.EMPTY); + this.webSocketClient = client; + this.graphQlSessionHandler = new GraphQlSessionHandler( codecConfigurer, connectionInitPayload, connectionAckHandler); - this.graphQlSessionMono = initGraphQlSession(uri, headers, client, this.graphQlSessionHandler) + this.graphQlSessionMono = initGraphQlSession(this.url, this.headers, client, this.graphQlSessionHandler) .cacheInvalidateWhen(GraphQlSession::notifyWhenClosed); } @@ -90,6 +102,23 @@ final class WebSocketGraphQlTransport implements GraphQlTransport { } + public URI getUrl() { + return this.url; + } + + public HttpHeaders getHeaders() { + return this.headers; + } + + public WebSocketClient getWebSocketClient() { + return this.webSocketClient; + } + + public CodecConfigurer getCodecConfigurer() { + return this.graphQlSessionHandler.getCodecConfigurer(); + } + + /** * Start the transport by connecting the WebSocket, sending the * "connection_init" and waiting for the "connection_ack" message. @@ -158,6 +187,11 @@ final class WebSocketGraphQlTransport implements GraphQlTransport { } + public CodecConfigurer getCodecConfigurer() { + return this.codecDelegate.getCodecConfigurer(); + } + + @Override public List getSubProtocols() { return Collections.singletonList("graphql-transport-ws"); diff --git a/spring-graphql/src/main/java/org/springframework/graphql/client/package-info.java b/spring-graphql/src/main/java/org/springframework/graphql/client/package-info.java index f44e1a2d..d9fd92bd 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/client/package-info.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/client/package-info.java @@ -15,7 +15,8 @@ */ /** - * GraphQL client. + * This package contains a {@link org.springframework.graphql.client.GraphQlClient} + * along with HTTP and WebSocket extensions. */ @NonNullApi @NonNullFields