Move toEntity(List) from WebClient.ResponseSpec to ClientResponse

This commit moves `toEntity(Class<T>)` and `toEntityList(Class<T>)`
from WebClient.ResponseSpec to ClientResponse. The main reason for doing
so is that the newly introduced `onStatus` method (see
2f9bd6e075) does not apply to these two
methods, and the result would be confusing. Also, `ClientResponse` and
`ResponseEntity` represent the same data: status code, headers, and a
body.

Issue: SPR-15724
This commit is contained in:
Arjen Poutsma 2017-07-11 17:40:16 +02:00
parent 51e02c2911
commit c9a3b863c4
9 changed files with 79 additions and 100 deletions

View File

@ -27,6 +27,7 @@ import org.springframework.http.HttpHeaders;
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.util.MultiValueMap;
import org.springframework.web.reactive.function.BodyExtractor;
@ -82,6 +83,22 @@ public interface ClientResponse {
*/
<T> Flux<T> bodyToFlux(Class<? extends T> elementClass);
/**
* Return this response as a delayed {@code ResponseEntity}.
* @param bodyType the expected response body type
* @param <T> response body type
* @return {@code Mono} with the {@code ResponseEntity}
*/
<T> Mono<ResponseEntity<T>> toEntity(Class<T> bodyType);
/**
* Return this response as a delayed list of {@code ResponseEntity}s.
* @param elementType the expected response body list element type
* @param <T> the type of elements in the list
* @return {@code Mono} with the list of {@code ResponseEntity}s
*/
<T> Mono<ResponseEntity<List<T>>> toEntityList(Class<T> elementType);
/**
* Represents the headers of the HTTP response.

View File

@ -29,6 +29,7 @@ import org.springframework.http.HttpHeaders;
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.HttpMessageReader;
import org.springframework.http.server.reactive.ServerHttpResponse;
@ -103,6 +104,25 @@ class DefaultClientResponse implements ClientResponse {
return body(BodyExtractors.toFlux(elementClass));
}
@Override
public <T> Mono<ResponseEntity<T>> toEntity(Class<T> bodyType) {
HttpHeaders headers = headers().asHttpHeaders();
HttpStatus statusCode = statusCode();
return bodyToMono(bodyType)
.map(body -> new ResponseEntity<>(body, headers, statusCode))
.switchIfEmpty(Mono.defer(
() -> Mono.just(new ResponseEntity<>(headers, statusCode))));
}
@Override
public <T> Mono<ResponseEntity<List<T>>> toEntityList(Class<T> responseType) {
HttpHeaders headers = headers().asHttpHeaders();
HttpStatus statusCode = statusCode();
return bodyToFlux(responseType)
.collectList()
.map(body -> new ResponseEntity<>(body, headers, statusCode));
}
private class DefaultHeaders implements Headers {

View File

@ -39,7 +39,6 @@ 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.ClientHttpRequest;
import org.springframework.http.client.reactive.ClientHttpResponse;
import org.springframework.lang.Nullable;
@ -443,28 +442,5 @@ class DefaultWebClient implements WebClient {
.orElse(response.body(extractor));
}
@Override
public <T> Mono<ResponseEntity<T>> toEntity(Class<T> bodyType) {
return this.responseMono.flatMap(response -> {
HttpHeaders headers = response.headers().asHttpHeaders();
HttpStatus statusCode = response.statusCode();
return response.bodyToMono(bodyType)
.map(body -> new ResponseEntity<>(body, headers, statusCode))
.switchIfEmpty(Mono.defer(
() -> Mono.just(new ResponseEntity<>(headers, statusCode))));
}
);
}
@Override
public <T> Mono<ResponseEntity<List<T>>> toEntityList(Class<T> responseType) {
return this.responseMono.flatMap(response ->
response.bodyToFlux(responseType).collectList().map(body -> {
HttpHeaders headers = response.headers().asHttpHeaders();
return new ResponseEntity<>(body, headers, response.statusCode());
})
);
}
}
}

View File

@ -33,7 +33,6 @@ 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;
@ -573,26 +572,6 @@ public interface WebClient {
*/
<T> Flux<T> bodyToFlux(Class<T> elementType);
/**
* Returns the response as a delayed {@code ResponseEntity}. Unlike
* {@link #bodyToMono(Class)} and {@link #bodyToFlux(Class)}, this method does not check
* for a 4xx or 5xx status code before extracting the body.
* @param bodyType the expected response body type
* @param <T> response body type
* @return {@code Mono} with the {@code ResponseEntity}
*/
<T> Mono<ResponseEntity<T>> toEntity(Class<T> bodyType);
/**
* Returns the response as a delayed list of {@code ResponseEntity}s. Unlike
* {@link #bodyToMono(Class)} and {@link #bodyToFlux(Class)}, this method does not check
* for a 4xx or 5xx status code before extracting the body.
* @param elementType the expected response body list element type
* @param <T> the type of elements in the list
* @return {@code Mono} with the list of {@code ResponseEntity}s
*/
<T> Mono<ResponseEntity<List<T>>> toEntityList(Class<T> elementType);
}

View File

@ -16,6 +16,7 @@
package org.springframework.web.reactive.function.client
import org.springframework.http.ResponseEntity
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
@ -35,3 +36,19 @@ inline fun <reified T : Any> ClientResponse.bodyToMono(): Mono<T> = bodyToMono(T
* @since 5.0
*/
inline fun <reified T : Any> ClientResponse.bodyToFlux(): Flux<T> = bodyToFlux(T::class.java)
/**
* Extension for [ClientResponse.toEntity] providing a `toEntity<Foo>()` variant.
*
* @author Sebastien Deleuze
* @since 5.0
*/
inline fun <reified T : Any> ClientResponse.toEntity(): Mono<ResponseEntity<T>> = toEntity(T::class.java)
/**
* Extension for [ClientResponse.toEntityList] providing a `bodyToEntityList<Foo>()` variant.
*
* @author Sebastien Deleuze
* @since 5.0
*/
inline fun <reified T : Any> ClientResponse.toEntityList(): Mono<ResponseEntity<List<T>>> = toEntityList(T::class.java)

View File

@ -17,7 +17,6 @@
package org.springframework.web.reactive.function.client
import org.reactivestreams.Publisher
import org.springframework.http.ResponseEntity
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
@ -49,19 +48,3 @@ inline fun <reified T : Any> WebClient.ResponseSpec.bodyToMono(): Mono<T> = body
* @since 5.0
*/
inline fun <reified T : Any> WebClient.ResponseSpec.bodyToFlux(): Flux<T> = bodyToFlux(T::class.java)
/**
* Extension for [WebClient.ResponseSpec.toEntity] providing a `bodyToEntity<Foo>()` variant.
*
* @author Sebastien Deleuze
* @since 5.0
*/
inline fun <reified T : Any> WebClient.ResponseSpec.toEntity(): Mono<ResponseEntity<T>> = toEntity(T::class.java)
/**
* Extension for [WebClient.ResponseSpec.toEntityList] providing a `bodyToEntityList<Foo>()` variant.
*
* @author Sebastien Deleuze
* @since 5.0
*/
inline fun <reified T : Any> WebClient.ResponseSpec.toEntityList(): Mono<ResponseEntity<List<T>>> = toEntityList(T::class.java)

View File

@ -158,7 +158,7 @@ public class WebClientIntegrationTests {
}
@Test
public void jsonStringRetrieveEntity() throws Exception {
public void jsonStringExchangeEntity() throws Exception {
String content = "{\"bar\":\"barbar\",\"foo\":\"foofoo\"}";
this.server.enqueue(new MockResponse().setHeader("Content-Type", "application/json")
.setBody(content));
@ -166,8 +166,8 @@ public class WebClientIntegrationTests {
Mono<ResponseEntity<String>> result = this.webClient.get()
.uri("/json")
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.toEntity(String.class);
.exchange()
.flatMap(response -> response.toEntity(String.class));
StepVerifier.create(result)
.consumeNextWith(entity -> {
@ -186,15 +186,15 @@ public class WebClientIntegrationTests {
}
@Test
public void jsonStringRetrieveEntityList() throws Exception {
public void jsonStringExchangeEntityList() throws Exception {
String content = "[{\"bar\":\"bar1\",\"foo\":\"foo1\"}, {\"bar\":\"bar2\",\"foo\":\"foo2\"}]";
this.server.enqueue(new MockResponse().setHeader("Content-Type", "application/json").setBody(content));
Mono<ResponseEntity<List<Pojo>>> result = this.webClient.get()
.uri("/json")
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.toEntityList(Pojo.class);
.exchange()
.flatMap(response -> response.toEntityList(Pojo.class));
StepVerifier.create(result)
.consumeNextWith(entity -> {
@ -412,14 +412,14 @@ public class WebClientIntegrationTests {
}
@Test
public void retrieveToEntityNotFound() throws Exception {
public void exchangeToEntityNotFound() throws Exception {
this.server.enqueue(new MockResponse().setResponseCode(404)
.setHeader("Content-Type", "text/plain").setBody("Not Found"));
Mono<ResponseEntity<String>> result = this.webClient.get()
.uri("/greeting?name=Spring")
.retrieve()
.toEntity(String.class);
.exchange()
.flatMap(response -> response.toEntity(String.class));
StepVerifier.create(result)
.consumeNextWith(response -> assertEquals(HttpStatus.NOT_FOUND, response.getStatusCode()))
@ -521,21 +521,6 @@ public class WebClientIntegrationTests {
}).verifyComplete();
}
@Test
public void retrieveNoContent() throws Exception {
this.server.enqueue(new MockResponse().setHeader("Content-Length", "0"));
Mono<ResponseEntity<Void>> result = this.webClient.get()
.uri("/noContent")
.retrieve()
.toEntity(Void.class);
StepVerifier.create(result).assertNext(r -> {
assertFalse(r.hasBody());
assertTrue(r.getStatusCode().is2xxSuccessful());
}).verifyComplete();
}
@SuppressWarnings("serial")
private static class MyException extends RuntimeException {

View File

@ -20,7 +20,8 @@ import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Answers
import org.mockito.Mock
import org.mockito.Mockito.*
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.mockito.junit.MockitoJUnitRunner
/**
@ -46,5 +47,17 @@ class ClientResponseExtensionsTests {
verify(response, times(1)).bodyToFlux(Foo::class.java)
}
@Test
fun `toEntity with reified type parameters`() {
response.toEntity<Foo>()
verify(response, times(1)).toEntity(Foo::class.java)
}
@Test
fun `ResponseSpec#toEntityList with reified type parameters`() {
response.toEntityList<Foo>()
verify(response, times(1)).toEntityList(Foo::class.java)
}
class Foo
}

View File

@ -21,7 +21,8 @@ import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Answers
import org.mockito.Mock
import org.mockito.Mockito.*
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.mockito.junit.MockitoJUnitRunner
import org.reactivestreams.Publisher
@ -59,17 +60,5 @@ class WebClientExtensionsTests {
verify(responseSpec, times(1)).bodyToFlux(Foo::class.java)
}
@Test
fun `ResponseSpec#toEntity with reified type parameters`() {
responseSpec.toEntity<Foo>()
verify(responseSpec, times(1)).toEntity(Foo::class.java)
}
@Test
fun `ResponseSpec#toEntityList with reified type parameters`() {
responseSpec.toEntityList<Foo>()
verify(responseSpec, times(1)).toEntityList(Foo::class.java)
}
class Foo
}