From 805b7159b0264f99a45095c55a481581c86c0fd4 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Wed, 12 Jul 2017 14:21:00 +0200 Subject: [PATCH] Add ParameterizedTypeReference variants to bodyTo[Mono|Flux], toEntity[List] This commit introduces overloaded variants of `bodytoMono`, `bodyToFlux`, `toEntity`, and `toEntityList` that take a `ParameterizedTypeReference`. It also adds similar methods to `WebClient.ResponseSpec`. Issue: SPR-15725 --- .../function/client/ClientResponse.java | 33 ++++ .../client/DefaultClientResponse.java | 34 +++- .../function/client/DefaultWebClient.java | 15 ++ .../reactive/function/client/WebClient.java | 33 +++- .../client/DefaultClientResponseTests.java | 145 ++++++++++++++++++ .../client/WebClientIntegrationTests.java | 47 ++++++ 6 files changed, 301 insertions(+), 6 deletions(-) diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ClientResponse.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ClientResponse.java index 6a6b3f7640..268fba5121 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ClientResponse.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ClientResponse.java @@ -23,6 +23,7 @@ import java.util.OptionalLong; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; @@ -75,6 +76,14 @@ public interface ClientResponse { */ Mono bodyToMono(Class elementClass); + /** + * Extract the body to a {@code Mono}. + * @param typeReference a type reference describing the expected response body type + * @param the element type + * @return a mono containing the body of the given type {@code T} + */ + Mono bodyToMono(ParameterizedTypeReference typeReference); + /** * Extract the body to a {@code Flux}. * @param elementClass the class of element in the {@code Flux} @@ -83,6 +92,14 @@ public interface ClientResponse { */ Flux bodyToFlux(Class elementClass); + /** + * Extract the body to a {@code Flux}. + * @param typeReference a type reference describing the expected response body type + * @param the element type + * @return a flux containing the body of the given type {@code T} + */ + Flux bodyToFlux(ParameterizedTypeReference typeReference); + /** * Return this response as a delayed {@code ResponseEntity}. * @param bodyType the expected response body type @@ -91,6 +108,14 @@ public interface ClientResponse { */ Mono> toEntity(Class bodyType); + /** + * Return this response as a delayed {@code ResponseEntity}. + * @param typeReference a type reference describing the expected response body type + * @param response body type + * @return {@code Mono} with the {@code ResponseEntity} + */ + Mono> toEntity(ParameterizedTypeReference typeReference); + /** * Return this response as a delayed list of {@code ResponseEntity}s. * @param elementType the expected response body list element type @@ -99,6 +124,14 @@ public interface ClientResponse { */ Mono>> toEntityList(Class elementType); + /** + * Return this response as a delayed list of {@code ResponseEntity}s. + * @param typeReference a type reference describing the expected response body type + * @param the type of elements in the list + * @return {@code Mono} with the list of {@code ResponseEntity}s + */ + Mono>> toEntityList(ParameterizedTypeReference typeReference); + /** * Represents the headers of the HTTP response. diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientResponse.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientResponse.java index 3d8fb9b95f..664e058ce0 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientResponse.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientResponse.java @@ -25,6 +25,7 @@ import java.util.OptionalLong; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; @@ -99,16 +100,35 @@ class DefaultClientResponse implements ClientResponse { return body(BodyExtractors.toMono(elementClass)); } + @Override + public Mono bodyToMono(ParameterizedTypeReference typeReference) { + return body(BodyExtractors.toMono(typeReference)); + } + @Override public Flux bodyToFlux(Class elementClass) { return body(BodyExtractors.toFlux(elementClass)); } + @Override + public Flux bodyToFlux(ParameterizedTypeReference typeReference) { + return body(BodyExtractors.toFlux(typeReference)); + } + @Override public Mono> toEntity(Class bodyType) { + return toEntityInternal(bodyToMono(bodyType)); + } + + @Override + public Mono> toEntity(ParameterizedTypeReference typeReference) { + return toEntityInternal(bodyToMono(typeReference)); + } + + private Mono> toEntityInternal(Mono bodyMono) { HttpHeaders headers = headers().asHttpHeaders(); HttpStatus statusCode = statusCode(); - return bodyToMono(bodyType) + return bodyMono .map(body -> new ResponseEntity<>(body, headers, statusCode)) .switchIfEmpty(Mono.defer( () -> Mono.just(new ResponseEntity<>(headers, statusCode)))); @@ -116,9 +136,19 @@ class DefaultClientResponse implements ClientResponse { @Override public Mono>> toEntityList(Class responseType) { + return toEntityListInternal(bodyToFlux(responseType)); + } + + @Override + public Mono>> toEntityList( + ParameterizedTypeReference typeReference) { + return toEntityListInternal(bodyToFlux(typeReference)); + } + + private Mono>> toEntityListInternal(Flux bodyFlux) { HttpHeaders headers = headers().asHttpHeaders(); HttpStatus statusCode = statusCode(); - return bodyToFlux(responseType) + return bodyFlux .collectList() .map(body -> new ResponseEntity<>(body, headers, statusCode)); } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClient.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClient.java index 1dc3551b38..568b46bbc5 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClient.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClient.java @@ -35,6 +35,7 @@ import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; @@ -422,6 +423,13 @@ class DefaultWebClient implements WebClient { Mono::error)); } + @Override + public Mono bodyToMono(ParameterizedTypeReference typeReference) { + return this.responseMono.flatMap( + response -> bodyToPublisher(response, BodyExtractors.toMono(typeReference), + Mono::error)); + } + @Override public Flux bodyToFlux(Class elementType) { return this.responseMono.flatMapMany( @@ -429,6 +437,13 @@ class DefaultWebClient implements WebClient { Flux::error)); } + @Override + public Flux bodyToFlux(ParameterizedTypeReference typeReference) { + return this.responseMono.flatMapMany( + response -> bodyToPublisher(response, BodyExtractors.toFlux(typeReference), + Flux::error)); + } + private > T bodyToPublisher(ClientResponse response, BodyExtractor extractor, Function errorFunction) { diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java index c7b4943c0a..55818bd948 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java @@ -29,6 +29,7 @@ import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; @@ -559,8 +560,9 @@ public interface WebClient { Function exceptionFunction); /** - * Extract the body to a {@code Mono}. If the response has status code 4xx or 5xx, the - * {@code Mono} will contain a {@link WebClientException}. + * Extract the body to a {@code Mono}. By default, if the response has status code 4xx or + * 5xx, the {@code Mono} will contain a {@link WebClientException}. This can be overridden + * with {@link #onStatus(Predicate, Function)}. * @param bodyType the expected response body type * @param response body type * @return a mono containing the body, or a {@link WebClientException} if the status code is @@ -569,8 +571,20 @@ public interface WebClient { Mono bodyToMono(Class bodyType); /** - * Extract the body to a {@code Flux}. If the response has status code 4xx or 5xx, the - * {@code Flux} will contain a {@link WebClientException}. + * Extract the body to a {@code Mono}. By default, if the response has status code 4xx or + * 5xx, the {@code Mono} will contain a {@link WebClientException}. This can be overridden + * with {@link #onStatus(Predicate, Function)}. + * @param typeReference a type reference describing the expected response body type + * @param response body type + * @return a mono containing the body, or a {@link WebClientException} if the status code is + * 4xx or 5xx + */ + Mono bodyToMono(ParameterizedTypeReference typeReference); + + /** + * Extract the body to a {@code Flux}. By default, if the response has status code 4xx or + * 5xx, the {@code Flux} will contain a {@link WebClientException}. This can be overridden + * with {@link #onStatus(Predicate, Function)}. * @param elementType the type of element in the response * @param the type of elements in the response * @return a flux containing the body, or a {@link WebClientException} if the status code is @@ -578,6 +592,17 @@ public interface WebClient { */ Flux bodyToFlux(Class elementType); + /** + * Extract the body to a {@code Flux}. By default, if the response has status code 4xx or + * 5xx, the {@code Flux} will contain a {@link WebClientException}. This can be overridden + * with {@link #onStatus(Predicate, Function)}. + * @param typeReference a type reference describing the expected response body type + * @param the type of elements in the response + * @return a flux containing the body, or a {@link WebClientException} if the status code is + * 4xx or 5xx + */ + Flux bodyToFlux(ParameterizedTypeReference typeReference); + } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/DefaultClientResponseTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/DefaultClientResponseTests.java index c2bdc28b67..56ad811cc7 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/DefaultClientResponseTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/DefaultClientResponseTests.java @@ -29,6 +29,7 @@ import org.junit.Test; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import org.springframework.core.ParameterizedTypeReference; import org.springframework.core.codec.StringDecoder; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DefaultDataBuffer; @@ -38,6 +39,7 @@ import org.springframework.http.HttpRange; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseCookie; +import org.springframework.http.ResponseEntity; import org.springframework.http.client.reactive.ClientHttpResponse; import org.springframework.http.codec.DecoderHttpMessageReader; import org.springframework.http.codec.HttpMessageReader; @@ -149,6 +151,29 @@ public class DefaultClientResponseTests { assertEquals("foo", resultMono.block()); } + @Test + public void bodyToMonoTypeReference() throws Exception { + DefaultDataBufferFactory factory = new DefaultDataBufferFactory(); + DefaultDataBuffer dataBuffer = + factory.wrap(ByteBuffer.wrap("foo".getBytes(StandardCharsets.UTF_8))); + Flux body = Flux.just(dataBuffer); + + HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.setContentType(MediaType.TEXT_PLAIN); + when(mockResponse.getHeaders()).thenReturn(httpHeaders); + when(mockResponse.getStatusCode()).thenReturn(HttpStatus.OK); + when(mockResponse.getBody()).thenReturn(body); + + List> messageReaders = Collections + .singletonList(new DecoderHttpMessageReader<>(StringDecoder.allMimeTypes(true))); + when(mockExchangeStrategies.messageReaders()).thenReturn(messageReaders); + + Mono resultMono = + defaultClientResponse.bodyToMono(new ParameterizedTypeReference() { + }); + assertEquals("foo", resultMono.block()); + } + @Test public void bodyToFlux() throws Exception { DefaultDataBufferFactory factory = new DefaultDataBufferFactory(); @@ -171,4 +196,124 @@ public class DefaultClientResponseTests { assertEquals(Collections.singletonList("foo"), result.block()); } + @Test + public void bodyToFluxTypeReference() throws Exception { + DefaultDataBufferFactory factory = new DefaultDataBufferFactory(); + DefaultDataBuffer dataBuffer = + factory.wrap(ByteBuffer.wrap("foo".getBytes(StandardCharsets.UTF_8))); + Flux body = Flux.just(dataBuffer); + + HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.setContentType(MediaType.TEXT_PLAIN); + when(mockResponse.getHeaders()).thenReturn(httpHeaders); + when(mockResponse.getStatusCode()).thenReturn(HttpStatus.OK); + when(mockResponse.getBody()).thenReturn(body); + + List> messageReaders = Collections + .singletonList(new DecoderHttpMessageReader<>(StringDecoder.allMimeTypes(true))); + when(mockExchangeStrategies.messageReaders()).thenReturn(messageReaders); + + Flux resultFlux = + defaultClientResponse.bodyToFlux(new ParameterizedTypeReference() {}); + Mono> result = resultFlux.collectList(); + assertEquals(Collections.singletonList("foo"), result.block()); + } + + @Test + public void toEntity() throws Exception { + DefaultDataBufferFactory factory = new DefaultDataBufferFactory(); + DefaultDataBuffer dataBuffer = + factory.wrap(ByteBuffer.wrap("foo".getBytes(StandardCharsets.UTF_8))); + Flux body = Flux.just(dataBuffer); + + HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.setContentType(MediaType.TEXT_PLAIN); + when(mockResponse.getHeaders()).thenReturn(httpHeaders); + when(mockResponse.getStatusCode()).thenReturn(HttpStatus.OK); + when(mockResponse.getBody()).thenReturn(body); + + List> messageReaders = Collections + .singletonList(new DecoderHttpMessageReader<>(StringDecoder.allMimeTypes(true))); + when(mockExchangeStrategies.messageReaders()).thenReturn(messageReaders); + + ResponseEntity result = defaultClientResponse.toEntity(String.class).block(); + assertEquals("foo", result.getBody()); + assertEquals(HttpStatus.OK, result.getStatusCode()); + assertEquals(MediaType.TEXT_PLAIN, result.getHeaders().getContentType()); + } + + @Test + public void toEntityTypeReference() throws Exception { + DefaultDataBufferFactory factory = new DefaultDataBufferFactory(); + DefaultDataBuffer dataBuffer = + factory.wrap(ByteBuffer.wrap("foo".getBytes(StandardCharsets.UTF_8))); + Flux body = Flux.just(dataBuffer); + + HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.setContentType(MediaType.TEXT_PLAIN); + when(mockResponse.getHeaders()).thenReturn(httpHeaders); + when(mockResponse.getStatusCode()).thenReturn(HttpStatus.OK); + when(mockResponse.getBody()).thenReturn(body); + + List> messageReaders = Collections + .singletonList(new DecoderHttpMessageReader<>(StringDecoder.allMimeTypes(true))); + when(mockExchangeStrategies.messageReaders()).thenReturn(messageReaders); + + ResponseEntity result = defaultClientResponse.toEntity( + new ParameterizedTypeReference() {}).block(); + assertEquals("foo", result.getBody()); + assertEquals(HttpStatus.OK, result.getStatusCode()); + assertEquals(MediaType.TEXT_PLAIN, result.getHeaders().getContentType()); + } + + @Test + public void toEntityList() throws Exception { + DefaultDataBufferFactory factory = new DefaultDataBufferFactory(); + DefaultDataBuffer dataBuffer = + factory.wrap(ByteBuffer.wrap("foo".getBytes(StandardCharsets.UTF_8))); + Flux body = Flux.just(dataBuffer); + + HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.setContentType(MediaType.TEXT_PLAIN); + when(mockResponse.getHeaders()).thenReturn(httpHeaders); + when(mockResponse.getStatusCode()).thenReturn(HttpStatus.OK); + when(mockResponse.getBody()).thenReturn(body); + + List> messageReaders = Collections + .singletonList(new DecoderHttpMessageReader<>(StringDecoder.allMimeTypes(true))); + when(mockExchangeStrategies.messageReaders()).thenReturn(messageReaders); + + ResponseEntity> result = defaultClientResponse.toEntityList(String.class).block(); + assertEquals(Collections.singletonList("foo"), result.getBody()); + assertEquals(HttpStatus.OK, result.getStatusCode()); + assertEquals(MediaType.TEXT_PLAIN, result.getHeaders().getContentType()); + } + + @Test + public void toEntityListTypeReference() throws Exception { + DefaultDataBufferFactory factory = new DefaultDataBufferFactory(); + DefaultDataBuffer dataBuffer = + factory.wrap(ByteBuffer.wrap("foo".getBytes(StandardCharsets.UTF_8))); + Flux body = Flux.just(dataBuffer); + + HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.setContentType(MediaType.TEXT_PLAIN); + when(mockResponse.getHeaders()).thenReturn(httpHeaders); + when(mockResponse.getStatusCode()).thenReturn(HttpStatus.OK); + when(mockResponse.getBody()).thenReturn(body); + + List> messageReaders = Collections + .singletonList(new DecoderHttpMessageReader<>(StringDecoder.allMimeTypes(true))); + when(mockExchangeStrategies.messageReaders()).thenReturn(messageReaders); + + ResponseEntity> result = defaultClientResponse.toEntityList( + new ParameterizedTypeReference() {}).block(); + assertEquals(Collections.singletonList("foo"), result.getBody()); + assertEquals(HttpStatus.OK, result.getStatusCode()); + assertEquals(MediaType.TEXT_PLAIN, result.getHeaders().getContentType()); + } + + + + } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientIntegrationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientIntegrationTests.java index ebe485e978..dd26d193de 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientIntegrationTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientIntegrationTests.java @@ -32,6 +32,7 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; +import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; @@ -157,6 +158,29 @@ public class WebClientIntegrationTests { Assert.assertEquals("application/json", recordedRequest.getHeader(HttpHeaders.ACCEPT)); } + @Test + public void jsonStringRetrieveMonoTypeReference() throws Exception { + String content = "{\"bar\":\"barbar\",\"foo\":\"foofoo\"}"; + this.server.enqueue(new MockResponse().setHeader("Content-Type", "application/json") + .setBody(content)); + + Mono result = this.webClient.get() + .uri("/json") + .accept(MediaType.APPLICATION_JSON) + .retrieve() + .bodyToMono(new ParameterizedTypeReference() {}); + + StepVerifier.create(result) + .expectNext(content) + .expectComplete() + .verify(Duration.ofSeconds(3)); + + RecordedRequest recordedRequest = server.takeRequest(); + Assert.assertEquals(1, server.getRequestCount()); + Assert.assertEquals("/json", recordedRequest.getPath()); + Assert.assertEquals("application/json", recordedRequest.getHeader(HttpHeaders.ACCEPT)); + } + @Test public void jsonStringExchangeEntity() throws Exception { String content = "{\"bar\":\"barbar\",\"foo\":\"foofoo\"}"; @@ -237,6 +261,29 @@ public class WebClientIntegrationTests { Assert.assertEquals("application/json", recordedRequest.getHeader(HttpHeaders.ACCEPT)); } + @Test + public void jsonStringRetrieveFluxTypeReference() throws Exception { + String content = "{\"bar\":\"barbar\",\"foo\":\"foofoo\"}"; + this.server.enqueue(new MockResponse().setHeader("Content-Type", "application/json") + .setBody(content)); + + Flux result = this.webClient.get() + .uri("/json") + .accept(MediaType.APPLICATION_JSON) + .retrieve() + .bodyToFlux(new ParameterizedTypeReference() {}); + + StepVerifier.create(result) + .expectNext(content) + .expectComplete() + .verify(Duration.ofSeconds(3)); + + RecordedRequest recordedRequest = server.takeRequest(); + Assert.assertEquals(1, server.getRequestCount()); + Assert.assertEquals("/json", recordedRequest.getPath()); + Assert.assertEquals("application/json", recordedRequest.getHeader(HttpHeaders.ACCEPT)); + } + @Test public void jsonPojoMono() throws Exception { this.server.enqueue(new MockResponse().setHeader("Content-Type", "application/json")