Add RestClient transport and GraphQlClient builder
See gh-771
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
/*
|
||||
* Copyright 2002-2024 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.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.codec.ClientCodecConfigurer;
|
||||
import org.springframework.http.codec.CodecConfigurer;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.client.RestClient;
|
||||
import org.springframework.web.util.DefaultUriBuilderFactory;
|
||||
import org.springframework.web.util.UriBuilderFactory;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
|
||||
/**
|
||||
* Default {@link RestClientGraphQlClient.Builder} implementation, a simple wrapper
|
||||
* around a {@link RestClient.Builder}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 1.3
|
||||
*/
|
||||
final class DefaultRestClientGraphQlClientBuilder
|
||||
extends AbstractGraphQlClientBuilder<DefaultRestClientGraphQlClientBuilder>
|
||||
implements RestClientGraphQlClient.Builder<DefaultRestClientGraphQlClientBuilder> {
|
||||
|
||||
private final RestClient.Builder restClientBuilder;
|
||||
|
||||
@Nullable
|
||||
private CodecConfigurer codecConfigurer;
|
||||
|
||||
/**
|
||||
* Constructor to start without a RestClient instance.
|
||||
*/
|
||||
DefaultRestClientGraphQlClientBuilder() {
|
||||
this(RestClient.builder());
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor to start with a pre-configured {@code RestClient}.
|
||||
*/
|
||||
DefaultRestClientGraphQlClientBuilder(RestClient client) {
|
||||
this(client.mutate());
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor to start with a pre-configured {@code RestClient}.
|
||||
*/
|
||||
DefaultRestClientGraphQlClientBuilder(RestClient.Builder clientBuilder) {
|
||||
this.restClientBuilder = clientBuilder;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public DefaultRestClientGraphQlClientBuilder url(String url) {
|
||||
this.restClientBuilder.baseUrl(url);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DefaultRestClientGraphQlClientBuilder url(URI url) {
|
||||
UriBuilderFactory factory = new DefaultUriBuilderFactory(UriComponentsBuilder.fromUri(url));
|
||||
this.restClientBuilder.uriBuilderFactory(factory);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DefaultRestClientGraphQlClientBuilder header(String name, String... values) {
|
||||
this.restClientBuilder.defaultHeader(name, values);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DefaultRestClientGraphQlClientBuilder headers(Consumer<HttpHeaders> headersConsumer) {
|
||||
this.restClientBuilder.defaultHeaders(headersConsumer);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DefaultRestClientGraphQlClientBuilder codecConfigurer(Consumer<CodecConfigurer> codecConsumer) {
|
||||
if (this.codecConfigurer == null) {
|
||||
this.codecConfigurer = ClientCodecConfigurer.create();
|
||||
}
|
||||
codecConsumer.accept(this.codecConfigurer);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DefaultRestClientGraphQlClientBuilder messageConverters(Consumer<List<HttpMessageConverter<?>>> configurer) {
|
||||
this.restClientBuilder.messageConverters(configurer);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DefaultRestClientGraphQlClientBuilder restClient(Consumer<RestClient.Builder> configurer) {
|
||||
configurer.accept(this.restClientBuilder);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RestClientGraphQlClient build() {
|
||||
|
||||
// Pass the codecs to the parent for response decoding
|
||||
if (this.codecConfigurer != null) {
|
||||
setJsonEncoder(CodecDelegate.findJsonEncoder(this.codecConfigurer));
|
||||
setJsonDecoder(CodecDelegate.findJsonDecoder(this.codecConfigurer));
|
||||
}
|
||||
else {
|
||||
this.restClientBuilder.messageConverters(converters -> {
|
||||
setJsonEncoder(HttpMessageConverterDelegate.getJsonEncoder(converters));
|
||||
setJsonDecoder(HttpMessageConverterDelegate.getJsonDecoder(converters));
|
||||
});
|
||||
}
|
||||
|
||||
RestClient restClient = this.restClientBuilder.build();
|
||||
|
||||
GraphQlClient graphQlClient = super.buildGraphQlClient(new RestClientGraphQlTransport(restClient, null));
|
||||
return new DefaultRestClientGraphQlClient(graphQlClient, restClient, getBuilderInitializer());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Default {@link HttpGraphQlClient} implementation.
|
||||
*/
|
||||
private static class DefaultRestClientGraphQlClient
|
||||
extends AbstractDelegatingGraphQlClient implements RestClientGraphQlClient {
|
||||
|
||||
private final RestClient restClient;
|
||||
|
||||
private final Consumer<AbstractGraphQlClientBuilder<?>> builderInitializer;
|
||||
|
||||
DefaultRestClientGraphQlClient(
|
||||
GraphQlClient delegate, RestClient restClient,
|
||||
Consumer<AbstractGraphQlClientBuilder<?>> builderInitializer) {
|
||||
|
||||
super(delegate);
|
||||
|
||||
Assert.notNull(restClient, "RestClient is required");
|
||||
Assert.notNull(builderInitializer, "`builderInitializer` is required");
|
||||
|
||||
this.restClient = restClient;
|
||||
this.builderInitializer = builderInitializer;
|
||||
}
|
||||
|
||||
public DefaultRestClientGraphQlClientBuilder mutate() {
|
||||
DefaultRestClientGraphQlClientBuilder builder = new DefaultRestClientGraphQlClientBuilder(this.restClient);
|
||||
this.builderInitializer.accept(builder);
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
/*
|
||||
* Copyright 2002-2024 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.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.reactivestreams.Publisher;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.core.codec.Decoder;
|
||||
import org.springframework.core.codec.DecodingException;
|
||||
import org.springframework.core.codec.Encoder;
|
||||
import org.springframework.core.codec.EncodingException;
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
import org.springframework.core.io.buffer.DataBufferFactory;
|
||||
import org.springframework.core.io.buffer.DataBufferUtils;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpInputMessage;
|
||||
import org.springframework.http.HttpOutputMessage;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.converter.GenericHttpMessageConverter;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.MimeType;
|
||||
|
||||
|
||||
/**
|
||||
* Helper class to adapt JSON {@link HttpMessageConverter} to
|
||||
* {@link Encoder} and {@link Decoder}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 1.3
|
||||
*/
|
||||
final class HttpMessageConverterDelegate {
|
||||
|
||||
static Encoder<?> getJsonEncoder(List<HttpMessageConverter<?>> converters) {
|
||||
HttpMessageConverter<Object> converter = findJsonConverter(converters);
|
||||
return new HttpMessageConverterEncoder(converter);
|
||||
}
|
||||
|
||||
static Decoder<?> getJsonDecoder(List<HttpMessageConverter<?>> converters) {
|
||||
HttpMessageConverter<Object> converter = findJsonConverter(converters);
|
||||
return new HttpMessageConverterDecoder(converter);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static HttpMessageConverter<Object> findJsonConverter(List<HttpMessageConverter<?>> converters) {
|
||||
return (HttpMessageConverter<Object>) converters.stream()
|
||||
.filter(converter -> converter.canRead(Map.class, MediaType.APPLICATION_JSON))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new IllegalArgumentException("No JSON HttpMessageConverter"));
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static MediaType toMediaType(@Nullable MimeType mimeType) {
|
||||
if (mimeType instanceof MediaType mediaType) {
|
||||
return mediaType;
|
||||
}
|
||||
return (mimeType != null ? new MediaType(mimeType) : null);
|
||||
}
|
||||
|
||||
|
||||
private static class HttpMessageConverterEncoder implements Encoder<Object> {
|
||||
|
||||
private final HttpMessageConverter<Object> converter;
|
||||
|
||||
private final List<MimeType> mimeTypes;
|
||||
|
||||
private HttpMessageConverterEncoder(HttpMessageConverter<Object> converter) {
|
||||
this.converter = converter;
|
||||
this.mimeTypes = new ArrayList<>(this.converter.getSupportedMediaTypes());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<MimeType> getEncodableMimeTypes() {
|
||||
return this.mimeTypes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canEncode(ResolvableType elementType, @Nullable MimeType mimeType) {
|
||||
return this.converter.canWrite(elementType.resolve(Object.class), toMediaType(mimeType));
|
||||
}
|
||||
|
||||
@Override
|
||||
public DataBuffer encodeValue(
|
||||
Object value, DataBufferFactory bufferFactory, ResolvableType valueType,
|
||||
@Nullable MimeType mimeType, @Nullable Map<String, Object> hints) {
|
||||
|
||||
HttpOutputMessageAdapter messageAdapter = new HttpOutputMessageAdapter();
|
||||
try {
|
||||
if (this.converter instanceof GenericHttpMessageConverter<Object> genericConverter) {
|
||||
genericConverter.write(value, valueType.getType(), toMediaType(mimeType), messageAdapter);
|
||||
}
|
||||
else {
|
||||
this.converter.write(value, toMediaType(mimeType), messageAdapter);
|
||||
}
|
||||
return bufferFactory.wrap(messageAdapter.toByteArray());
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new EncodingException(ex.getMessage(), ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<DataBuffer> encode(
|
||||
Publisher<?> inputStream, DataBufferFactory bufferFactory, ResolvableType elementType,
|
||||
@Nullable MimeType mimeType, @Nullable Map<String, Object> hints) {
|
||||
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static class HttpMessageConverterDecoder implements Decoder<Object> {
|
||||
|
||||
private final HttpMessageConverter<Object> converter;
|
||||
|
||||
private final List<MimeType> mimeTypes;
|
||||
|
||||
private HttpMessageConverterDecoder(HttpMessageConverter<Object> converter) {
|
||||
this.converter = converter;
|
||||
this.mimeTypes = new ArrayList<>(this.converter.getSupportedMediaTypes());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<MimeType> getDecodableMimeTypes() {
|
||||
return this.mimeTypes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canDecode(ResolvableType elementType, @Nullable MimeType mimeType) {
|
||||
return this.converter.canRead(elementType.resolve(Object.class), toMediaType(mimeType));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object decode(DataBuffer buffer, ResolvableType targetType,
|
||||
@Nullable MimeType mimeType, @Nullable Map<String, Object> hints) throws DecodingException {
|
||||
|
||||
try {
|
||||
HttpInputMessageAdapter messageAdapter = new HttpInputMessageAdapter(buffer);
|
||||
if (this.converter instanceof GenericHttpMessageConverter<Object> genericConverter) {
|
||||
return genericConverter.read(targetType.getType(), null, messageAdapter);
|
||||
}
|
||||
else {
|
||||
return this.converter.read(targetType.resolve(Object.class), messageAdapter);
|
||||
}
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new DecodingException(ex.getMessage(), ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Object> decodeToMono(Publisher<DataBuffer> inputStream, ResolvableType elementType,
|
||||
@Nullable MimeType mimeType, @Nullable Map<String, Object> hints) {
|
||||
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<Object> decode(
|
||||
Publisher<DataBuffer> inputStream, ResolvableType elementType,
|
||||
@Nullable MimeType mimeType, @Nullable Map<String, Object> hints) {
|
||||
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static class HttpInputMessageAdapter extends ByteArrayInputStream implements HttpInputMessage {
|
||||
|
||||
HttpInputMessageAdapter(DataBuffer buffer) {
|
||||
super(toBytes(buffer));
|
||||
}
|
||||
|
||||
private static byte[] toBytes(DataBuffer buffer) {
|
||||
byte[] bytes = new byte[buffer.readableByteCount()];
|
||||
buffer.read(bytes);
|
||||
DataBufferUtils.release(buffer);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream getBody() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpHeaders getHeaders() {
|
||||
return HttpHeaders.EMPTY;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
private static class HttpOutputMessageAdapter extends ByteArrayOutputStream implements HttpOutputMessage {
|
||||
|
||||
private static final HttpHeaders noOpHeaders = new HttpHeaders();
|
||||
|
||||
@Override
|
||||
public OutputStream getBody() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpHeaders getHeaders() {
|
||||
return noOpHeaders;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* Copyright 2002-2024 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.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.web.client.RestClient;
|
||||
|
||||
|
||||
/**
|
||||
* GraphQL over HTTP client that uses {@link RestClient}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 1.3
|
||||
*/
|
||||
public interface RestClientGraphQlClient extends WebGraphQlClient {
|
||||
|
||||
|
||||
@Override
|
||||
Builder<?> mutate();
|
||||
|
||||
|
||||
/**
|
||||
* Create an {@link RestClientGraphQlClient} that uses the given {@link RestClient}.
|
||||
*/
|
||||
static RestClientGraphQlClient create(RestClient client) {
|
||||
return builder(client.mutate()).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a builder to initialize an {@link RestClientGraphQlClient} with.
|
||||
*/
|
||||
static Builder<?> builder() {
|
||||
return new DefaultRestClientGraphQlClientBuilder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Variant of {@link #builder()} with a pre-configured {@code RestClient}
|
||||
* to mutate and customize further through the returned builder.
|
||||
*/
|
||||
static Builder<?> builder(RestClient client) {
|
||||
return builder(client.mutate());
|
||||
}
|
||||
|
||||
/**
|
||||
* Variant of {@link #builder()} with a pre-configured {@code RestClient}
|
||||
* to mutate and customize further through the returned builder.
|
||||
*/
|
||||
static Builder<?> builder(RestClient.Builder builder) {
|
||||
return new DefaultRestClientGraphQlClientBuilder(builder);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Builder for the GraphQL over HTTP client.
|
||||
*/
|
||||
interface Builder<B extends Builder<B>> extends WebGraphQlClient.Builder<B> {
|
||||
|
||||
/**
|
||||
* Configure message converters for all JSON encoding and decoding needs.
|
||||
* @param configurer the configurer to apply
|
||||
* @return this builder
|
||||
*/
|
||||
B messageConverters(Consumer<List<HttpMessageConverter<?>>> configurer);
|
||||
|
||||
/**
|
||||
* Customize the {@code RestClient} to use.
|
||||
* <p>Note that some properties of {@code RestClient.Builder} like the base URL,
|
||||
* headers, and message converters can be customized through this builder.
|
||||
* @see #url(String)
|
||||
* @see #header(String, String...)
|
||||
* @see #messageConverters(Consumer)
|
||||
*/
|
||||
B restClient(Consumer<RestClient.Builder> builderConsumer);
|
||||
|
||||
/**
|
||||
* Build the {@code RestClientGraphQlClient} instance.
|
||||
*/
|
||||
@Override
|
||||
RestClientGraphQlClient build();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* Copyright 2002-2024 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 reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.scheduler.Scheduler;
|
||||
import reactor.core.scheduler.Schedulers;
|
||||
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.graphql.GraphQlRequest;
|
||||
import org.springframework.graphql.GraphQlResponse;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.client.RestClient;
|
||||
|
||||
|
||||
/**
|
||||
* Transport to execute GraphQL requests over HTTP via {@link RestClient}.
|
||||
*
|
||||
* <p>Supports only single-response requests over HTTP POST. For subscriptions,
|
||||
* see {@link WebSocketGraphQlTransport} and {@link RSocketGraphQlTransport}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 1.3
|
||||
*/
|
||||
final class RestClientGraphQlTransport implements GraphQlTransport {
|
||||
|
||||
private static final ParameterizedTypeReference<Map<String, Object>> MAP_TYPE = new ParameterizedTypeReference<>() {};
|
||||
|
||||
|
||||
private final RestClient restClient;
|
||||
|
||||
private final MediaType contentType;
|
||||
|
||||
private final Scheduler scheduler;
|
||||
|
||||
|
||||
RestClientGraphQlTransport(RestClient restClient, @Nullable Scheduler scheduler) {
|
||||
Assert.notNull(restClient, "RestClient is required");
|
||||
this.restClient = restClient;
|
||||
this.contentType = initContentType(restClient);
|
||||
this.scheduler = (scheduler!= null ? scheduler : Schedulers.boundedElastic());
|
||||
}
|
||||
|
||||
private static MediaType initContentType(RestClient webClient) {
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
webClient.mutate().defaultHeaders(headers::putAll);
|
||||
MediaType contentType = headers.getContentType();
|
||||
return (contentType != null ? contentType : MediaType.APPLICATION_JSON);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Mono<GraphQlResponse> execute(GraphQlRequest request) {
|
||||
return Mono
|
||||
.fromCallable(() -> this.restClient.post()
|
||||
.contentType(this.contentType)
|
||||
.accept(MediaType.APPLICATION_JSON, MediaType.APPLICATION_GRAPHQL_RESPONSE)
|
||||
.body(request.toMap())
|
||||
.retrieve()
|
||||
.body(MAP_TYPE))
|
||||
.map(responseMap -> (GraphQlResponse) new ResponseMapGraphQlResponse(responseMap))
|
||||
.subscribeOn(this.scheduler);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<GraphQlResponse> executeSubscription(GraphQlRequest request) {
|
||||
throw new UnsupportedOperationException("Subscriptions not supported");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2024 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.
|
||||
@@ -16,6 +16,8 @@
|
||||
|
||||
package org.springframework.graphql.client;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Type;
|
||||
import java.net.URI;
|
||||
import java.time.Duration;
|
||||
import java.util.Collections;
|
||||
@@ -40,14 +42,25 @@ import org.springframework.graphql.server.webflux.GraphQlHttpHandler;
|
||||
import org.springframework.graphql.server.webflux.GraphQlWebSocketHandler;
|
||||
import org.springframework.graphql.support.DocumentSource;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpInputMessage;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.client.ClientHttpRequest;
|
||||
import org.springframework.http.client.ClientHttpRequestFactory;
|
||||
import org.springframework.http.client.ClientHttpResponse;
|
||||
import org.springframework.http.codec.ClientCodecConfigurer;
|
||||
import org.springframework.http.codec.json.Jackson2JsonDecoder;
|
||||
import org.springframework.http.converter.HttpMessageNotReadableException;
|
||||
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
|
||||
import org.springframework.http.server.reactive.HttpHandler;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.mock.http.client.MockClientHttpRequest;
|
||||
import org.springframework.mock.http.client.MockClientHttpResponse;
|
||||
import org.springframework.test.web.reactive.server.HttpHandlerConnector;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.MimeType;
|
||||
import org.springframework.web.client.RestClient;
|
||||
import org.springframework.web.reactive.function.client.ClientRequest;
|
||||
import org.springframework.web.reactive.function.client.ClientResponse;
|
||||
import org.springframework.web.reactive.function.client.ExchangeFunction;
|
||||
@@ -81,7 +94,7 @@ public class WebGraphQlClientBuilderTests {
|
||||
|
||||
|
||||
public static Stream<ClientBuilderSetup> argumentSource() {
|
||||
return Stream.of(new HttpBuilderSetup(), new WebSocketBuilderSetup());
|
||||
return Stream.of(new HttpBuilderSetup(), new RestClientBuilderSetup(), new WebSocketBuilderSetup());
|
||||
}
|
||||
|
||||
|
||||
@@ -209,9 +222,15 @@ public class WebGraphQlClientBuilderTests {
|
||||
void codecConfigurerRegistersJsonPathMappingProvider(ClientBuilderSetup builderSetup) {
|
||||
|
||||
TestJackson2JsonDecoder testDecoder = new TestJackson2JsonDecoder();
|
||||
TestJackson2JsonConverter testConverter = new TestJackson2JsonConverter();
|
||||
|
||||
WebGraphQlClient.Builder<?> builder = builderSetup.initBuilder()
|
||||
.codecConfigurer(codecConfigurer -> codecConfigurer.customCodecs().register(testDecoder));
|
||||
WebGraphQlClient.Builder<?> builder = builderSetup.initBuilder();
|
||||
if (builder instanceof RestClientGraphQlClient.Builder<?> restClientBuilder) {
|
||||
restClientBuilder.messageConverters(converters -> converters.add(0, testConverter));
|
||||
}
|
||||
else {
|
||||
builder.codecConfigurer(codecConfigurer -> codecConfigurer.customCodecs().register(testDecoder));
|
||||
}
|
||||
|
||||
String document = "{me {name}}";
|
||||
MovieCharacter character = MovieCharacter.create("Luke Skywalker");
|
||||
@@ -224,11 +243,16 @@ public class WebGraphQlClientBuilderTests {
|
||||
ClientGraphQlResponse response = client.document(document).execute().block(TIMEOUT);
|
||||
|
||||
testDecoder.resetLastValue();
|
||||
testConverter.resetLastValue();
|
||||
assertThat(testDecoder.getLastValue()).isNull();
|
||||
|
||||
assertThat(response).isNotNull();
|
||||
assertThat(response.field("me").toEntity(MovieCharacter.class).getName()).isEqualTo("Luke Skywalker");
|
||||
assertThat(testDecoder.getLastValue()).isEqualTo(character);
|
||||
|
||||
Object lastValue = (builder instanceof RestClientGraphQlClient.Builder<?> ?
|
||||
testConverter.getLastValue() : testDecoder.getLastValue());
|
||||
|
||||
assertThat(lastValue).isEqualTo(character);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -291,7 +315,19 @@ public class WebGraphQlClientBuilderTests {
|
||||
}
|
||||
|
||||
|
||||
private static class HttpBuilderSetup extends AbstractBuilderSetup {
|
||||
private abstract static class AbstractHttpBuilderSetup extends AbstractBuilderSetup {
|
||||
|
||||
protected WebClient.Builder initWebClientBuilder() {
|
||||
GraphQlHttpHandler handler = new GraphQlHttpHandler(webGraphQlHandler());
|
||||
RouterFunction<ServerResponse> routerFunction = route().POST("/**", handler::handleRequest).build();
|
||||
HttpHandler httpHandler = RouterFunctions.toHttpHandler(routerFunction, HandlerStrategies.withDefaults());
|
||||
return WebClient.builder().clientConnector(new HttpHandlerConnector(httpHandler));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
private static class HttpBuilderSetup extends AbstractHttpBuilderSetup {
|
||||
|
||||
private final Map<String, Object> clientAttributes = new ConcurrentHashMap<>();
|
||||
|
||||
@@ -301,12 +337,8 @@ public class WebGraphQlClientBuilderTests {
|
||||
|
||||
@Override
|
||||
public HttpGraphQlClient.Builder<?> initBuilder() {
|
||||
GraphQlHttpHandler handler = new GraphQlHttpHandler(webGraphQlHandler());
|
||||
RouterFunction<ServerResponse> routerFunction = route().POST("/**", handler::handleRequest).build();
|
||||
HttpHandler httpHandler = RouterFunctions.toHttpHandler(routerFunction, HandlerStrategies.withDefaults());
|
||||
HttpHandlerConnector connector = new HttpHandlerConnector(httpHandler);
|
||||
return HttpGraphQlClient.builder(WebClient.builder()
|
||||
.clientConnector(connector).filter(this::updateAttributes));
|
||||
WebClient client = initWebClientBuilder().filter(this::updateAttributes).build();
|
||||
return HttpGraphQlClient.builder(client);
|
||||
}
|
||||
|
||||
private Mono<ClientResponse> updateAttributes(ClientRequest request, ExchangeFunction next) {
|
||||
@@ -318,6 +350,18 @@ public class WebGraphQlClientBuilderTests {
|
||||
}
|
||||
|
||||
|
||||
private static class RestClientBuilderSetup extends AbstractHttpBuilderSetup {
|
||||
|
||||
@Override
|
||||
public RestClientGraphQlClient.Builder<?> initBuilder() {
|
||||
WebClient webClient = initWebClientBuilder().build();
|
||||
WebClientHttpRequestFactoryAdapter requestFactory = new WebClientHttpRequestFactoryAdapter(webClient);
|
||||
return RestClientGraphQlClient.builder(RestClient.builder().requestFactory(requestFactory));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
private static class WebSocketBuilderSetup extends AbstractBuilderSetup {
|
||||
|
||||
@Override
|
||||
@@ -330,6 +374,37 @@ public class WebGraphQlClientBuilderTests {
|
||||
}
|
||||
|
||||
|
||||
private record WebClientHttpRequestFactoryAdapter(WebClient webClient) implements ClientHttpRequestFactory {
|
||||
|
||||
@Override
|
||||
public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) {
|
||||
return new MockClientHttpRequest(httpMethod, uri) {
|
||||
@Override
|
||||
protected ClientHttpResponse executeInternal() {
|
||||
return getClientHttpResponse(httpMethod, uri, getHeaders(), getBodyAsBytes());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private ClientHttpResponse getClientHttpResponse(
|
||||
HttpMethod httpMethod, URI uri, HttpHeaders requestHeaders, byte[] requestBody) {
|
||||
|
||||
ResponseEntity<byte[]> entity = this.webClient.method(httpMethod).uri(uri)
|
||||
.headers(headers -> headers.putAll(requestHeaders))
|
||||
.bodyValue(requestBody)
|
||||
.retrieve()
|
||||
.toEntity(byte[].class)
|
||||
.block();
|
||||
|
||||
byte[] body = (entity.getBody() != null ? entity.getBody() : new byte[0]);
|
||||
MockClientHttpResponse response = new MockClientHttpResponse(body, entity.getStatusCode());
|
||||
response.getHeaders().putAll(entity.getHeaders());
|
||||
return response;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
private static class TestJackson2JsonDecoder extends Jackson2JsonDecoder {
|
||||
|
||||
@Nullable
|
||||
@@ -355,4 +430,28 @@ public class WebGraphQlClientBuilderTests {
|
||||
}
|
||||
|
||||
|
||||
private static class TestJackson2JsonConverter extends MappingJackson2HttpMessageConverter {
|
||||
|
||||
@Nullable
|
||||
private Object lastValue;
|
||||
|
||||
@Nullable
|
||||
Object getLastValue() {
|
||||
return this.lastValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object read(Type type, Class<?> contextClass, HttpInputMessage inputMessage)
|
||||
throws IOException, HttpMessageNotReadableException {
|
||||
|
||||
this.lastValue = super.read(type, contextClass, inputMessage);
|
||||
return this.lastValue;
|
||||
}
|
||||
|
||||
void resetLastValue() {
|
||||
this.lastValue = null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user