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
new file mode 100644
index 00000000..527ea0de
--- /dev/null
+++ b/spring-graphql/src/main/java/org/springframework/graphql/client/AbstractDelegatingGraphQlClient.java
@@ -0,0 +1,52 @@
+/*
+ * 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 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.
+ *
+ *
Subclasses must implement {@link GraphQlClient#mutate()} to allow mutation
+ * of both {@code GraphQlClient} and {@code GraphQlTransport} configuration.
+ *
+ * @author Rossen Stoyanchev
+ * @since 1.0.0
+ */
+public abstract class AbstractDelegatingGraphQlClient implements GraphQlClient {
+
+ private final GraphQlClient graphQlClient;
+
+
+ protected AbstractDelegatingGraphQlClient(GraphQlClient graphQlClient) {
+ Assert.notNull(graphQlClient, "GraphQlClient is required");
+ this.graphQlClient = graphQlClient;
+ }
+
+
+ public GraphQlClient.RequestSpec document(String document) {
+ return this.graphQlClient.document(document);
+ }
+
+ public GraphQlClient.RequestSpec documentName(String name) {
+ return this.graphQlClient.documentName(name);
+ }
+
+}
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 c04303e5..984c08eb 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
@@ -18,6 +18,7 @@ package org.springframework.graphql.client;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
+import java.util.function.Consumer;
import com.jayway.jsonpath.Configuration;
import com.jayway.jsonpath.DocumentContext;
@@ -35,7 +36,11 @@ import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
- * Default implementation of {@link GraphQlClient}.
+ * 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.
*
* @author Rossen Stoyanchev
* @since 1.0.0
@@ -48,9 +53,12 @@ final class DefaultGraphQlClient implements GraphQlClient {
private final DocumentSource documentSource;
+ private final Consumer> builderInitializer;
+
DefaultGraphQlClient(
- GraphQlTransport transport, Configuration jsonPathConfig, DocumentSource documentSource) {
+ GraphQlTransport transport, Configuration jsonPathConfig, DocumentSource documentSource,
+ Consumer> builderInitializer) {
Assert.notNull(transport, "GraphQlTransport is required");
Assert.notNull(jsonPathConfig, "Configuration is required");
@@ -59,15 +67,10 @@ final class DefaultGraphQlClient implements GraphQlClient {
this.transport = transport;
this.jsonPathConfig = jsonPathConfig;
this.documentSource = documentSource;
+ this.builderInitializer = builderInitializer;
}
- @SuppressWarnings("unchecked")
- @Override
- public T getTransport(Class requiredType) {
- return (requiredType.isInstance(this.transport) ? (T) this.transport : null);
- }
-
@Override
public RequestSpec document(String document) {
return new DefaultRequestSpec(Mono.just(document), this.transport, this.jsonPathConfig);
@@ -79,6 +82,13 @@ final class DefaultGraphQlClient implements GraphQlClient {
return new DefaultRequestSpec(document, this.transport, this.jsonPathConfig);
}
+ @Override
+ public Builder> mutate() {
+ DefaultGraphQlClientBuilder> builder = new DefaultGraphQlClientBuilder<>(this.transport);
+ this.builderInitializer.accept(builder);
+ return builder;
+ }
+
private static final class DefaultRequestSpec implements RequestSpec {
@@ -112,6 +122,12 @@ final class DefaultGraphQlClient implements GraphQlClient {
return this;
}
+ @Override
+ public RequestSpec variables(Map variables) {
+ this.variables.putAll(variables);
+ return this;
+ }
+
@Override
public Mono execute() {
return getRequestMono()
@@ -136,11 +152,14 @@ final class DefaultGraphQlClient implements GraphQlClient {
private static class DefaultResponseSpec implements ResponseSpec {
+ private final ExecutionResult result;
+
private final DocumentContext jsonPathDocument;
private final List errors;
private DefaultResponseSpec(ExecutionResult result, Configuration jsonPathConfig) {
+ this.result = result;
this.jsonPathDocument = JsonPath.parse(result.toSpecification(), jsonPathConfig);
this.errors = result.getErrors();
}
@@ -180,6 +199,11 @@ final class DefaultGraphQlClient implements GraphQlClient {
return this.errors;
}
+ @Override
+ public ExecutionResult andReturn() {
+ return this.result;
+ }
+
}
}
diff --git a/spring-graphql/src/main/java/org/springframework/graphql/client/DefaultGraphQlClientBuilder.java b/spring-graphql/src/main/java/org/springframework/graphql/client/DefaultGraphQlClientBuilder.java
index 67ac8e61..36142938 100644
--- a/spring-graphql/src/main/java/org/springframework/graphql/client/DefaultGraphQlClientBuilder.java
+++ b/spring-graphql/src/main/java/org/springframework/graphql/client/DefaultGraphQlClientBuilder.java
@@ -13,25 +13,34 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
package org.springframework.graphql.client;
+import java.util.function.Consumer;
+
import com.jayway.jsonpath.Configuration;
import com.jayway.jsonpath.spi.json.JacksonJsonProvider;
import com.jayway.jsonpath.spi.mapper.JacksonMappingProvider;
+import org.springframework.graphql.support.CachingDocumentSource;
import org.springframework.graphql.support.DocumentSource;
import org.springframework.graphql.support.ResourceDocumentSource;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
+
/**
- * Default implementation of {@link GraphQlClient.Builder}.
+ * Default {@link GraphQlClient.Builder} implementation that builds a
+ * {@link GraphQlClient} for use with any transport.
+ *
+ * Intended for use as a base class for builders that do assist with building
+ * the underlying transport. Such extension
*
* @author Rossen Stoyanchev
* @since 1.0.0
*/
-class DefaultGraphQlClientBuilder implements GraphQlClient.Builder {
+public class DefaultGraphQlClientBuilder> implements GraphQlClient.Builder {
private static final boolean jackson2Present;
@@ -42,53 +51,70 @@ class DefaultGraphQlClientBuilder implements GraphQlClient.Builder {
}
- private final GraphQlTransport transport;
-
@Nullable
- private Configuration jsonPathConfig;
+ 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()}.
+ */
+ DefaultGraphQlClientBuilder() {
+ }
- @Override
- public GraphQlClient.Builder jsonPathConfig(@Nullable Configuration config) {
- this.jsonPathConfig = config;
- return this;
+ protected void transport(GraphQlTransport transport) {
+ this.transport = transport;
}
@Override
- public GraphQlClient.Builder documentSource(@Nullable DocumentSource contentLoader) {
+ public B documentSource(@Nullable DocumentSource contentLoader) {
this.documentSource = contentLoader;
- return this;
+ return self();
+ }
+
+ @SuppressWarnings("unchecked")
+ private T self() {
+ return (T) this;
}
@Override
public GraphQlClient build() {
- return new DefaultGraphQlClient(this.transport, initJsonPathConfig(), initRequestNameResolver());
+ Assert.notNull(this.transport, "No GraphQlTransport. Has a subclass not initialized it?");
+ return new DefaultGraphQlClient(this.transport, initJsonPathConfig(), initDocumentSource(), getBuilderInitializer());
}
private Configuration initJsonPathConfig() {
- if (this.jsonPathConfig != null) {
- return this.jsonPathConfig;
- }
- else if (jackson2Present) {
- return Jackson2Configuration.create();
- }
- else {
- return Configuration.builder().build();
- }
+ // Allow configuring JSONPath with codecs from transport subclasses
+ return (jackson2Present ? Jackson2Configuration.create() : Configuration.builder().build());
}
- private DocumentSource initRequestNameResolver() {
+ private DocumentSource initDocumentSource() {
return (this.documentSource == null ?
- new ResourceDocumentSource() : this.documentSource);
+ new CachingDocumentSource(new ResourceDocumentSource()) : this.documentSource);
+ }
+
+ /**
+ * Exposes a {@code Consumer} to subclasses to initialize new builder instances
+ * from the configuration of "this" builder.
+ */
+ protected Consumer> getBuilderInitializer() {
+ return builder -> {
+ if (this.documentSource != null) {
+ builder.documentSource(documentSource);
+ }
+ };
+
}
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
new file mode 100644
index 00000000..13c634ad
--- /dev/null
+++ b/spring-graphql/src/main/java/org/springframework/graphql/client/DefaultHttpGraphQlClient.java
@@ -0,0 +1,206 @@
+/*
+ * 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.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.web.reactive.function.client.WebClient;
+
+/**
+ * Default {@link HttpGraphQlClient} implementation.
+ *
+ * @author Rossen Stoyanchev
+ * @since 1.0.0
+ */
+final class DefaultHttpGraphQlClient extends AbstractDelegatingGraphQlClient implements HttpGraphQlClient {
+
+ private final Supplier mutateBuilder;
+
+
+ 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();
+ }
+
+ @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);
+ };
+ }
+
+ }
+
+
+ /**
+ * Default {@link HttpGraphQlClient.Builder} implementation.
+ */
+ static final class Builder extends BaseBuilder implements HttpGraphQlClient.Builder {
+
+ @Nullable
+ private WebClient webClient;
+
+ @Nullable
+ private Consumer webClientConfigurers;
+
+
+ /**
+ * Constructor to start without a WebClient instance.
+ */
+ Builder() {
+ }
+
+ /**
+ * Constructor to start with a pre-configured {@code WebClient}.
+ */
+ Builder(WebClient webClient) {
+ this.webClient = webClient;
+ }
+
+
+ @Override
+ public Builder webClient(Consumer configurer) {
+ this.webClientConfigurers = (this.webClientConfigurers != null ? this.webClientConfigurers.andThen(configurer) : configurer);
+ 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;
+ };
+ }
+
+ }
+
+}
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
new file mode 100644
index 00000000..6fa7dff0
--- /dev/null
+++ b/spring-graphql/src/main/java/org/springframework/graphql/client/DefaultWebSocketGraphQlClient.java
@@ -0,0 +1,135 @@
+/*
+ * 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.util.Map;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+
+import reactor.core.publisher.Mono;
+
+import org.springframework.http.codec.ClientCodecConfigurer;
+import org.springframework.lang.Nullable;
+import org.springframework.util.Assert;
+import org.springframework.web.reactive.socket.client.WebSocketClient;
+
+
+/**
+ * Default {@link WebSocketGraphQlClient} implementation.
+ *
+ * @author Rossen Stoyanchev
+ * @since 1.0.0
+ */
+final class DefaultWebSocketGraphQlClient extends AbstractDelegatingGraphQlClient implements WebSocketGraphQlClient {
+
+ private final WebSocketGraphQlTransport transport;
+
+ private final Supplier mutateBuilderFactory;
+
+
+ DefaultWebSocketGraphQlClient(
+ GraphQlClient delegate, WebSocketGraphQlTransport transport, Supplier mutateBuilderFactory) {
+
+ super(delegate);
+ this.transport = transport;
+ this.mutateBuilderFactory = mutateBuilderFactory;
+ }
+
+
+ @Override
+ public Mono start() {
+ return this.transport.start();
+ }
+
+ @Override
+ public Mono stop() {
+ return this.transport.stop();
+ }
+
+ @Override
+ public Builder mutate() {
+ return this.mutateBuilderFactory.get();
+ }
+
+
+ /**
+ * Default {@link WebSocketGraphQlClient.Builder} implementation.
+ */
+ static final class Builder extends DefaultHttpGraphQlClient.BaseBuilder
+ implements WebSocketGraphQlClient.Builder {
+
+ private final WebSocketClient webSocketClient;
+
+ @Nullable
+ private Object initPayload;
+
+ private Consumer