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 801de88faf5..6116fad2aef 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 @@ -21,6 +21,7 @@ import java.util.List; import java.util.Optional; import java.util.OptionalLong; import java.util.function.Consumer; +import java.util.function.Function; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -215,16 +216,32 @@ public interface ClientResponse { */ String logPrefix(); + /** + * Return a builder to mutate the this response, for example to change + * the status, headers, cookies, and replace or transform the body. + * @return a builder to mutate the request with + * @since 5.3 + */ + default Builder mutate() { + return new DefaultClientResponseBuilder(this, true); + } + // Static builder methods /** * Create a builder with the status, headers, and cookies of the given response. + *

Note: Note that the body in the returned builder is + * {@link Flux#empty()} by default. To carry over the one from the original + * response, use {@code otherResponse.bodyToFlux(DataBuffer.class)} or + * simply use the instance based {@link #mutate()} method. * @param other the response to copy the status, headers, and cookies from * @return the created builder + * @deprecated as of 5.3 in favor of the instance based {@link #mutate()}. */ + @Deprecated static Builder from(ClientResponse other) { - return new DefaultClientResponseBuilder(other); + return new DefaultClientResponseBuilder(other, false); } /** @@ -371,19 +388,26 @@ public interface ClientResponse { Builder cookies(Consumer> cookiesConsumer); /** - * Set the body of the response. Calling this methods will - * {@linkplain org.springframework.core.io.buffer.DataBufferUtils#release(DataBuffer) release} - * the existing body of the builder. - * @param body the new body. + * Transform the response body, if set in the builder. + * @param transformer the transformation function to use + * @return this builder + * @since 5.3 + */ + Builder body(Function, Flux> transformer); + + /** + * Set the body of the response. + *

Note: This methods will drain the existing body, + * if set in the builder. + * @param body the new body to use * @return this builder */ Builder body(Flux body); /** * Set the body of the response to the UTF-8 encoded bytes of the given string. - * Calling this methods will - * {@linkplain org.springframework.core.io.buffer.DataBufferUtils#release(DataBuffer) release} - * the existing body of the builder. + *

Note: This methods will drain the existing body, + * if set in the builder. * @param body the new body. * @return this builder */ diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientResponseBuilder.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientResponseBuilder.java index 3c43175d04f..45ad32585dc 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientResponseBuilder.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientResponseBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2020 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. @@ -19,11 +19,11 @@ package org.springframework.web.reactive.function.client; import java.net.URI; import java.nio.charset.StandardCharsets; import java.util.function.Consumer; +import java.util.function.Function; import reactor.core.publisher.Flux; import org.springframework.core.io.buffer.DataBuffer; -import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.core.io.buffer.DataBufferUtils; import org.springframework.core.io.buffer.DefaultDataBufferFactory; import org.springframework.http.HttpHeaders; @@ -69,9 +69,9 @@ final class DefaultClientResponseBuilder implements ClientResponse.Builder { private int statusCode = 200; - private final HttpHeaders headers = new HttpHeaders(); + private final HttpHeaders headers; - private final MultiValueMap cookies = new LinkedMultiValueMap<>(); + private final MultiValueMap cookies; private Flux body = Flux.empty(); @@ -81,21 +81,26 @@ final class DefaultClientResponseBuilder implements ClientResponse.Builder { public DefaultClientResponseBuilder(ExchangeStrategies strategies) { Assert.notNull(strategies, "ExchangeStrategies must not be null"); this.strategies = strategies; + this.headers = new HttpHeaders(); + this.cookies = new LinkedMultiValueMap<>(); this.request = EMPTY_REQUEST; } - public DefaultClientResponseBuilder(ClientResponse other) { + public DefaultClientResponseBuilder(ClientResponse other, boolean mutate) { Assert.notNull(other, "ClientResponse must not be null"); this.strategies = other.strategies(); this.statusCode = other.rawStatusCode(); - headers(headers -> headers.addAll(other.headers().asHttpHeaders())); - cookies(cookies -> cookies.addAll(other.cookies())); - if (other instanceof DefaultClientResponse) { - this.request = ((DefaultClientResponse) other).request(); + if (mutate) { + this.headers = HttpHeaders.writableHttpHeaders(other.headers().asHttpHeaders()); + this.body = other.bodyToFlux(DataBuffer.class); } else { - this.request = EMPTY_REQUEST; + this.headers = new HttpHeaders(); + headers(headers -> headers.addAll(other.headers().asHttpHeaders())); } + this.cookies = new LinkedMultiValueMap<>(other.cookies()); + this.request = (other instanceof DefaultClientResponse ? + ((DefaultClientResponse) other).request() : EMPTY_REQUEST); } @@ -139,6 +144,12 @@ final class DefaultClientResponseBuilder implements ClientResponse.Builder { return this; } + @Override + public ClientResponse.Builder body(Function, Flux> transformer) { + this.body = transformer.apply(this.body); + return this; + } + @Override public ClientResponse.Builder body(Flux body) { Assert.notNull(body, "Body must not be null"); @@ -151,11 +162,10 @@ final class DefaultClientResponseBuilder implements ClientResponse.Builder { public ClientResponse.Builder body(String body) { Assert.notNull(body, "Body must not be null"); releaseBody(); - DataBufferFactory dataBufferFactory = new DefaultDataBufferFactory(); this.body = Flux.just(body). map(s -> { byte[] bytes = body.getBytes(StandardCharsets.UTF_8); - return dataBufferFactory.wrap(bytes); + return new DefaultDataBufferFactory().wrap(bytes); }); return this; } @@ -173,12 +183,12 @@ final class DefaultClientResponseBuilder implements ClientResponse.Builder { @Override public ClientResponse build() { + ClientHttpResponse httpResponse = new BuiltClientHttpResponse(this.statusCode, this.headers, this.cookies, this.body); - // When building ClientResponse manually, the ClientRequest.logPrefix() has to be passed, - // e.g. via ClientResponse.Builder, but this (builder) is not used currently. - return new DefaultClientResponse(httpResponse, this.strategies, "", "", () -> this.request); + return new DefaultClientResponse( + httpResponse, this.strategies, "", "", () -> this.request); } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFilterFunctions.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFilterFunctions.java index baeb77a9c8c..b7d6c3d7200 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFilterFunctions.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFilterFunctions.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2020 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. @@ -22,16 +22,13 @@ import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; -import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferUtils; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.lang.Nullable; import org.springframework.util.Assert; -import org.springframework.web.reactive.function.BodyExtractors; /** * Static factory methods providing access to built-in implementations of @@ -64,11 +61,10 @@ public abstract class ExchangeFilterFunctions { */ public static ExchangeFilterFunction limitResponseSize(long maxByteCount) { return (request, next) -> - next.exchange(request).map(response -> { - Flux body = response.body(BodyExtractors.toDataBuffers()); - body = DataBufferUtils.takeUntilByteCount(body, maxByteCount); - return ClientResponse.from(response).body(body).build(); - }); + next.exchange(request).map(response -> + response.mutate() + .body(body -> DataBufferUtils.takeUntilByteCount(body, maxByteCount)) + .build()); } /** diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultServerRequest.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultServerRequest.java index 8ea642a90d4..ea94cf843c5 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultServerRequest.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultServerRequest.java @@ -260,7 +260,7 @@ class DefaultServerRequest implements ServerRequest { private class DefaultHeaders implements Headers { - + private final HttpHeaders httpHeaders = HttpHeaders.readOnlyHttpHeaders(request().getHeaders()); diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/DefaultClientResponseBuilderTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/DefaultClientResponseBuilderTests.java index 8e6c01ef63b..70cebb42f4f 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/DefaultClientResponseBuilderTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/DefaultClientResponseBuilderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2020 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. @@ -64,42 +64,38 @@ public class DefaultClientResponseBuilderTests { } @Test - public void from() { - Flux otherBody = Flux.just("foo", "bar") - .map(s -> s.getBytes(StandardCharsets.UTF_8)) - .map(dataBufferFactory::wrap); + public void mutate() { - ClientResponse other = ClientResponse.create(HttpStatus.BAD_REQUEST, ExchangeStrategies.withDefaults()) + ClientResponse originalResponse = ClientResponse + .create(HttpStatus.BAD_REQUEST, ExchangeStrategies.withDefaults()) .header("foo", "bar") + .header("bar", "baz") .cookie("baz", "qux") - .body(otherBody) + .body(Flux.just("foobar".getBytes(StandardCharsets.UTF_8)).map(dataBufferFactory::wrap)) .build(); - Flux body = Flux.just("baz") - .map(s -> s.getBytes(StandardCharsets.UTF_8)) - .map(dataBufferFactory::wrap); - - ClientResponse result = ClientResponse.from(other) - .headers(httpHeaders -> httpHeaders.set("foo", "baar")) + ClientResponse result = originalResponse.mutate() + .statusCode(HttpStatus.OK) + .headers(headers -> headers.set("foo", "baar")) .cookies(cookies -> cookies.set("baz", ResponseCookie.from("baz", "quux").build())) - .body(body) .build(); - assertThat(result.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST); - assertThat(result.headers().asHttpHeaders().size()).isEqualTo(1); + assertThat(result.statusCode()).isEqualTo(HttpStatus.OK); + assertThat(result.headers().asHttpHeaders().size()).isEqualTo(2); assertThat(result.headers().asHttpHeaders().getFirst("foo")).isEqualTo("baar"); + assertThat(result.headers().asHttpHeaders().getFirst("bar")).isEqualTo("baz"); assertThat(result.cookies().size()).isEqualTo(1); assertThat(result.cookies().getFirst("baz").getValue()).isEqualTo("quux"); StepVerifier.create(result.bodyToFlux(String.class)) - .expectNext("baz") + .expectNext("foobar") .verifyComplete(); } @Test - public void fromCustomStatus() { + public void mutateWithCustomStatus() { ClientResponse other = ClientResponse.create(499, ExchangeStrategies.withDefaults()).build(); - ClientResponse result = ClientResponse.from(other).build(); + ClientResponse result = other.mutate().build(); assertThat(result.rawStatusCode()).isEqualTo(499); assertThatIllegalArgumentException().isThrownBy(result::statusCode);