From d8838152b3f0ee34175234a2acf21e5dc2d566f3 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Wed, 17 Jul 2019 11:19:55 +0200 Subject: [PATCH] Copy ClientResponseEntity::toEntity* methods to ResponseSpec This commit copies the toEntity and toEntityList methods from ClientResponse to ResponseSpec, so that it is possible to retrieve a ResponseEntity when using retrieve(). Closes gh-22368 --- .../client/DefaultClientResponse.java | 40 +----- .../function/client/DefaultWebClient.java | 27 ++++ .../reactive/function/client/WebClient.java | 43 ++++++ .../function/client/WebClientUtils.java | 72 ++++++++++ .../client/WebClientIntegrationTests.java | 130 +++++++++++++++++- 5 files changed, 274 insertions(+), 38 deletions(-) create mode 100644 spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClientUtils.java 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 762d7e1a22d..c15406bd2bf 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 @@ -157,31 +157,22 @@ class DefaultClientResponse implements ClientResponse { @Override public Mono> toEntity(Class bodyType) { - return toEntityInternal(bodyToMono(bodyType)); + return WebClientUtils.toEntity(this, bodyToMono(bodyType)); } @Override public Mono> toEntity(ParameterizedTypeReference bodyTypeReference) { - return toEntityInternal(bodyToMono(bodyTypeReference)); - } - - private Mono> toEntityInternal(Mono bodyMono) { - HttpHeaders headers = headers().asHttpHeaders(); - int status = rawStatusCode(); - return bodyMono - .map(body -> createEntity(body, headers, status)) - .switchIfEmpty(Mono.defer( - () -> Mono.just(createEntity(headers, status)))); + return WebClientUtils.toEntity(this, bodyToMono(bodyTypeReference)); } @Override public Mono>> toEntityList(Class elementClass) { - return toEntityListInternal(bodyToFlux(elementClass)); + return WebClientUtils.toEntityList(this, bodyToFlux(elementClass)); } @Override public Mono>> toEntityList(ParameterizedTypeReference elementTypeRef) { - return toEntityListInternal(bodyToFlux(elementTypeRef)); + return WebClientUtils.toEntityList(this, bodyToFlux(elementTypeRef)); } @Override @@ -224,29 +215,6 @@ class DefaultClientResponse implements ClientResponse { return this.requestSupplier.get(); } - private Mono>> toEntityListInternal(Flux bodyFlux) { - HttpHeaders headers = headers().asHttpHeaders(); - int status = rawStatusCode(); - return bodyFlux - .collectList() - .map(body -> createEntity(body, headers, status)); - } - - private ResponseEntity createEntity(HttpHeaders headers, int status) { - HttpStatus resolvedStatus = HttpStatus.resolve(status); - return resolvedStatus != null - ? new ResponseEntity<>(headers, resolvedStatus) - : ResponseEntity.status(status).headers(headers).build(); - } - - private ResponseEntity createEntity(T body, HttpHeaders headers, int status) { - HttpStatus resolvedStatus = HttpStatus.resolve(status); - return resolvedStatus != null - ? new ResponseEntity<>(body, headers, resolvedStatus) - : ResponseEntity.status(status).headers(headers).body(body); - } - - private class DefaultHeaders implements Headers { private HttpHeaders delegate() { 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 f73c09ebc0a..f301a9691ea 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 @@ -39,6 +39,7 @@ import org.springframework.http.HttpMethod; import org.springframework.http.HttpRequest; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; import org.springframework.http.client.reactive.ClientHttpRequest; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -528,6 +529,32 @@ class DefaultWebClient implements WebClient { return result.checkpoint(description); } + @Override + public Mono> toEntity(Class bodyClass) { + return this.responseMono.flatMap(response -> + WebClientUtils.toEntity(response, handleBodyMono(response, response.bodyToMono(bodyClass)))); + } + + @Override + public Mono> toEntity(ParameterizedTypeReference bodyTypeReference) { + return this.responseMono.flatMap(response -> + WebClientUtils.toEntity(response, + handleBodyMono(response, response.bodyToMono(bodyTypeReference)))); + } + + @Override + public Mono>> toEntityList(Class elementClass) { + return this.responseMono.flatMap(response -> + WebClientUtils.toEntityList(response, + handleBodyFlux(response, response.bodyToFlux(elementClass)))); + } + + @Override + public Mono>> toEntityList(ParameterizedTypeReference elementTypeRef) { + return this.responseMono.flatMap(response -> + WebClientUtils.toEntityList(response, + handleBodyFlux(response, response.bodyToFlux(elementTypeRef)))); + } private static class StatusHandler { 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 e6e1e1f0327..f39fee19e23 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 @@ -35,6 +35,7 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; import org.springframework.http.client.reactive.ClientHttpConnector; import org.springframework.http.client.reactive.ClientHttpRequest; import org.springframework.util.MultiValueMap; @@ -734,6 +735,48 @@ public interface WebClient { */ Flux bodyToFlux(ParameterizedTypeReference elementTypeRef); + /** + * Return the response as a delayed {@code ResponseEntity}. 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 bodyClass the expected response body type + * @param response body type + * @return {@code Mono} with the {@code ResponseEntity} + */ + Mono> toEntity(Class bodyClass); + + /** + * Return the response as a delayed {@code ResponseEntity}. 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 bodyTypeReference a type reference describing the expected response body type + * @param response body type + * @return {@code Mono} with the {@code ResponseEntity} + */ + Mono> toEntity(ParameterizedTypeReference bodyTypeReference); + + /** + * Return the response as a delayed list of {@code ResponseEntity}s. 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 elementClass the expected response body list element class + * @param the type of elements in the list + * @return {@code Mono} with the list of {@code ResponseEntity}s + */ + Mono>> toEntityList(Class elementClass); + + /** + * Return the response as a delayed list of {@code ResponseEntity}s. 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 elementTypeRef the expected response body list element reference type + * @param the type of elements in the list + * @return {@code Mono} with the list of {@code ResponseEntity}s + */ + Mono>> toEntityList(ParameterizedTypeReference elementTypeRef); + } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClientUtils.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClientUtils.java new file mode 100644 index 00000000000..e68c2200800 --- /dev/null +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClientUtils.java @@ -0,0 +1,72 @@ +/* + * Copyright 2002-2019 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.web.reactive.function.client; + +import java.util.List; + +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.lang.Nullable; + +/** + * Internal methods shared between {@link DefaultWebClient} and {@link DefaultClientResponse}. + * + * @author Arjen Poutsma + * @since 5.2 + */ +abstract class WebClientUtils { + + /** + * Create a delayed {@link ResponseEntity} from the given response and body. + */ + public static Mono> toEntity(ClientResponse response, Mono bodyMono) { + return Mono.defer(() -> { + HttpHeaders headers = response.headers().asHttpHeaders(); + int status = response.rawStatusCode(); + return bodyMono + .map(body -> createEntity(body, headers, status)) + .switchIfEmpty(Mono.defer( + () -> Mono.just(createEntity(null, headers, status)))); + }); + } + + /** + * Create a delayed {@link ResponseEntity} list from the given response and body. + */ + public static Mono>> toEntityList(ClientResponse response, Publisher body) { + return Mono.defer(() -> { + HttpHeaders headers = response.headers().asHttpHeaders(); + int status = response.rawStatusCode(); + return Flux.from(body) + .collectList() + .map(list -> createEntity(list, headers, status)); + }); + } + + private static ResponseEntity createEntity(@Nullable T body, HttpHeaders headers, int status) { + HttpStatus resolvedStatus = HttpStatus.resolve(status); + return resolvedStatus != null + ? new ResponseEntity<>(body, headers, resolvedStatus) + : ResponseEntity.status(status).headers(headers).body(body); + } + +} 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 e9755b743c2..bfa362341ed 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 @@ -219,7 +219,7 @@ public class WebClientIntegrationTests { } @Test - public void shouldReceiveJsonAsResponseEntityString() { + public void exchangeShouldReceiveJsonAsResponseEntityString() { String content = "{\"bar\":\"barbar\",\"foo\":\"foofoo\"}"; prepareResponse(response -> response .setHeader("Content-Type", "application/json").setBody(content)); @@ -246,7 +246,82 @@ public class WebClientIntegrationTests { } @Test - public void shouldReceiveJsonAsResponseEntityList() { + public void retrieveShouldReceiveJsonAsResponseEntityString() { + String content = "{\"bar\":\"barbar\",\"foo\":\"foofoo\"}"; + prepareResponse(response -> response + .setHeader("Content-Type", "application/json").setBody(content)); + + Mono> result = this.webClient.get() + .uri("/json").accept(MediaType.APPLICATION_JSON) + .retrieve() + .toEntity(String.class); + + StepVerifier.create(result) + .consumeNextWith(entity -> { + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(entity.getHeaders().getContentType()).isEqualTo(MediaType.APPLICATION_JSON); + assertThat(entity.getHeaders().getContentLength()).isEqualTo(31); + assertThat(entity.getBody()).isEqualTo(content); + }) + .expectComplete().verify(Duration.ofSeconds(3)); + + expectRequestCount(1); + expectRequest(request -> { + assertThat(request.getPath()).isEqualTo("/json"); + assertThat(request.getHeader(HttpHeaders.ACCEPT)).isEqualTo("application/json"); + }); + } + + @Test + public void retrieveEntityWithServerError() { + prepareResponse(response -> response.setResponseCode(500) + .setHeader("Content-Type", "text/plain").setBody("Internal Server error")); + + Mono> result = this.webClient.get() + .uri("/").accept(MediaType.APPLICATION_JSON) + .retrieve() + .toEntity(String.class); + + StepVerifier.create(result) + .expectError(WebClientResponseException.class) + .verify(Duration.ofSeconds(3)); + + expectRequestCount(1); + expectRequest(request -> { + assertThat(request.getPath()).isEqualTo("/"); + assertThat(request.getHeader(HttpHeaders.ACCEPT)).isEqualTo("application/json"); + }); + } + + @Test + public void retrieveEntityWithServerErrorStatusHandler() { + String content = "Internal Server error"; + prepareResponse(response -> response.setResponseCode(500) + .setHeader("Content-Type", "text/plain").setBody(content)); + + Mono> result = this.webClient.get() + .uri("/").accept(MediaType.APPLICATION_JSON) + .retrieve() + .onStatus(HttpStatus::is5xxServerError, response -> Mono.empty())// use normal response + .toEntity(String.class); + + StepVerifier.create(result) + .consumeNextWith(entity -> { + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR); + assertThat(entity.getBody()).isEqualTo(content); + }) + .expectComplete() + .verify(Duration.ofSeconds(3)); + + expectRequestCount(1); + expectRequest(request -> { + assertThat(request.getPath()).isEqualTo("/"); + assertThat(request.getHeader(HttpHeaders.ACCEPT)).isEqualTo("application/json"); + }); + } + + @Test + public void exchangeShouldReceiveJsonAsResponseEntityList() { String content = "[{\"bar\":\"bar1\",\"foo\":\"foo1\"}, {\"bar\":\"bar2\",\"foo\":\"foo2\"}]"; prepareResponse(response -> response .setHeader("Content-Type", "application/json").setBody(content)); @@ -274,6 +349,57 @@ public class WebClientIntegrationTests { }); } + @Test + public void retrieveShouldReceiveJsonAsResponseEntityList() { + String content = "[{\"bar\":\"bar1\",\"foo\":\"foo1\"}, {\"bar\":\"bar2\",\"foo\":\"foo2\"}]"; + prepareResponse(response -> response + .setHeader("Content-Type", "application/json").setBody(content)); + + Mono>> result = this.webClient.get() + .uri("/json").accept(MediaType.APPLICATION_JSON) + .retrieve() + .toEntityList(Pojo.class); + + StepVerifier.create(result) + .consumeNextWith(entity -> { + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(entity.getHeaders().getContentType()).isEqualTo(MediaType.APPLICATION_JSON); + assertThat(entity.getHeaders().getContentLength()).isEqualTo(58); + Pojo pojo1 = new Pojo("foo1", "bar1"); + Pojo pojo2 = new Pojo("foo2", "bar2"); + assertThat(entity.getBody()).isEqualTo(Arrays.asList(pojo1, pojo2)); + }) + .expectComplete().verify(Duration.ofSeconds(3)); + + expectRequestCount(1); + expectRequest(request -> { + assertThat(request.getPath()).isEqualTo("/json"); + assertThat(request.getHeader(HttpHeaders.ACCEPT)).isEqualTo("application/json"); + }); + } + + @Test + public void retrieveEntityListWithServerError() { + prepareResponse(response -> response.setResponseCode(500) + .setHeader("Content-Type", "text/plain").setBody("Internal Server error")); + + Mono>> result = this.webClient.get() + .uri("/").accept(MediaType.APPLICATION_JSON) + .retrieve() + .toEntityList(String.class); + + StepVerifier.create(result) + .expectError(WebClientResponseException.class) + .verify(Duration.ofSeconds(3)); + + expectRequestCount(1); + expectRequest(request -> { + assertThat(request.getPath()).isEqualTo("/"); + assertThat(request.getHeader(HttpHeaders.ACCEPT)).isEqualTo("application/json"); + }); + } + + @Test public void shouldReceiveJsonAsFluxString() { String content = "{\"bar\":\"barbar\",\"foo\":\"foofoo\"}";