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 527ea0de7..940354291 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 @@ 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 36142938a..94e3a97e5 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 @@ /** - * 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 @@ private T self() { return (T) this; } - @Override - public GraphQlClient build() { - Assert.notNull(this.transport, "No GraphQlTransport. Has a subclass not initialized it?"); - return new DefaultGraphQlClient(this.transport, initJsonPathConfig(), initDocumentSource(), getBuilderInitializer()); + /** + * Subclasses call this from {@link #build()} to provide the transport and get + * the default {@code GraphQlClient to delegate to for request execution. + */ + protected GraphQlClient buildGraphQlClient(GraphQlTransport transport) { + Assert.notNull(transport, "GraphQlTransport is required"); + return new DefaultGraphQlClient(transport, initJsonPathConfig(), initDocumentSource(), getBuilderInitializer()); } private Configuration initJsonPathConfig() { @@ -105,8 +98,8 @@ private DocumentSource initDocumentSource() { } /** - * Exposes a {@code Consumer} to subclasses to initialize new builder instances - * from the configuration of "this" builder. + * Subclasses call this from {@link #build()} to obtain a {@code Consumer} to + * initialize new builder instances with, based on "this" builder. */ protected Consumer> getBuilderInitializer() { return builder -> { @@ -114,7 +107,6 @@ protected Consumer> getBuilderInitializer() { builder.documentSource(documentSource); } }; - } diff --git a/spring-graphql/src/main/java/org/springframework/graphql/client/DefaultGraphQlClient.java b/spring-graphql/src/main/java/org/springframework/graphql/client/DefaultGraphQlClient.java index 984c08eb3..efb53847b 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/client/DefaultGraphQlClient.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/client/DefaultGraphQlClient.java @@ -36,11 +36,7 @@ import org.springframework.util.StringUtils; /** - * Default {@link GraphQlClient} implementation with the logic to initialize - * requests and handle responses, and delegates to a {@link GraphQlTransport} - * for actual request execution. - * - *

This 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 @@ public RequestSpec documentName(String name) { } @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 13c634ad0..1bf52dcb8 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) { - super(graphQlClient); - this.mutateBuilder = mutateBuilder; - } - - - 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(); - } + DefaultHttpGraphQlClient(GraphQlClient graphQlClient, WebClient webClient, + Consumer> builderInitializer) { - @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; - } + super(graphQlClient); - protected HttpHeaders getHeaders() { - return this.headers; - } + Assert.notNull(webClient, "WebClient is required"); + Assert.notNull(builderInitializer, "`builderInitializer` is required"); - @Nullable - protected Consumer getCodecConfigurerConsumer() { - return this.codecConfigurerConsumer; - } + this.webClient = webClient; + this.builderInitializer = builderInitializer; + } - @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); - }; - } + public Builder mutate() { + 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 client) { + this(client.mutate()); } /** * Constructor to start with a pre-configured {@code WebClient}. */ - Builder(WebClient webClient) { - this.webClient = webClient; + Builder(WebClient.Builder clientBuilder) { + this.webClientBuilder = clientBuilder; } @Override - public Builder webClient(Consumer configurer) { - this.webClientConfigurers = (this.webClientConfigurers != null ? this.webClientConfigurers.andThen(configurer) : configurer); + public Builder url(String url) { + this.webClientBuilder.baseUrl(url); 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)); + public Builder url(URI url) { + UriBuilderFactory factory = new DefaultUriBuilderFactory(UriComponentsBuilder.fromUri(url)); + this.webClientBuilder.uriBuilderFactory(factory); + return this; } - 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())); + @Override + public Builder header(String name, String... values) { + this.webClientBuilder.defaultHeader(name, values); + return this; + } - if (getCodecConfigurerConsumer() != null) { - builder.codecs(getCodecConfigurerConsumer()); - } + @Override + public Builder headers(Consumer headersConsumer) { + this.webClientBuilder.defaultHeaders(headersConsumer); + return this; + } - if (this.webClientConfigurers != null) { - this.webClientConfigurers.accept(builder); - } + @Override + public Builder codecConfigurer(Consumer codecsConsumer) { + this.webClientBuilder.codecs(codecsConsumer::accept); + return this; + } - return builder.build(); + @Override + public Builder webClient(Consumer configurer) { + configurer.accept(this.webClientBuilder); + return this; } - private Supplier initMutateBuilderFactory(WebClient webClient) { - Consumer> parentInitializer = getWebBuilderInitializer(); - return () -> { - Builder builder = new Builder(webClient); - parentInitializer.accept(builder); - return builder; - }; + @Override + public HttpGraphQlClient build() { + 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 6fa7dff08..968197331 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 @@ public Mono stop() { @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 final WebSocketClient webSocketClient; + private URI url; - @Nullable - private Object initPayload; + private final HttpHeaders headers = new HttpHeaders(); - private Consumer> connectionAckHandler = ackPayload -> {}; + private final WebSocketClient webSocketClient; + private final CodecConfigurer codecConfigurer; - 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 WebSocketGraphQlClient build() { - Assert.notNull(getUrl(), "GraphQL endpoint URI is required"); - - WebSocketGraphQlTransport transport = new WebSocketGraphQlTransport( - getUrl(), getHeaders(), this.webSocketClient, initClientCodecConfigurer(), - this.initPayload, this.connectionAckHandler); - - transport(transport); - GraphQlClient graphQlClient = super.build(); + public Builder header(String name, String... values) { + this.headers.put(name, Arrays.asList(values)); + return this; + } - return new DefaultWebSocketGraphQlClient(graphQlClient, transport, mutateBuilderFactory()); + @Override + public Builder headers(Consumer headersConsumer) { + headersConsumer.accept(this.headers); + return this; } - private ClientCodecConfigurer initClientCodecConfigurer() { - ClientCodecConfigurer configurer = ClientCodecConfigurer.create(); - if (getCodecConfigurerConsumer() != null) { - getCodecConfigurerConsumer().accept(configurer); - } - return configurer; + @Override + public Builder codecConfigurer(Consumer codecConsumer) { + codecConsumer.accept(this.codecConfigurer); + return this; } - 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; - }; + @Override + public WebSocketGraphQlClient build() { + + WebSocketGraphQlTransport transport = new WebSocketGraphQlTransport( + this.url, this.headers, this.webSocketClient, this.codecConfigurer, null, payload -> {}); + + 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 1fa668ddd..0df68f6dc 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.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 @@ interface Builder> { * {@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 1651dc40b..ee24f6ae9 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 @@ /** - * 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 @@ 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 b376bc695..89d9fdc68 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 @@ static Builder builder() { /** * 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 9f929446e..3d0ccfd8e 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.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 000000000..534fdd6c6 --- /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 cd8cf79a3..8e95640df 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 @@ private static Encoder initEncoder(CodecConfigurer configurer) { } + 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 d2e55d834..c229aae3c 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 4f6699cfc..651c29d62 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 @@ private static Mono initGraphQlSession( } + 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 @@ private static class GraphQlSessionHandler implements WebSocketHandler { } + 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 f44e1a2dd..d9fd92bd7 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