Add RestClient transport and GraphQlClient builder

See gh-771
This commit is contained in:
rstoyanchev
2024-01-25 10:02:04 +00:00
parent c48b2979c8
commit d460cf0b9b
5 changed files with 707 additions and 12 deletions

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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();
}
}

View File

@@ -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");
}
}

View File

@@ -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;
}
}
}