From 04c2a2990dda185cca75ded2b218dff3054dafa8 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Thu, 8 Mar 2018 15:47:48 +0100 Subject: [PATCH] Provide simple way to create ClientResponse This commit introduces ClientResponse.Builder, an easier way to create a ClientResponse from an existing response, or from scratch. Issue: SPR-16553 --- .../function/client/ClientResponse.java | 149 +++++++++++++- .../client/DefaultClientResponse.java | 6 +- .../client/DefaultClientResponseBuilder.java | 189 ++++++++++++++++++ .../client/support/ClientResponseWrapper.java | 176 ++++++++++++++++ .../function/client/support/package-info.java | 26 +++ .../server/support/ServerRequestWrapper.java | 13 +- .../function/server/support/package-info.java | 2 +- .../DefaultClientResponseBuilderTests.java | 104 ++++++++++ .../support/ClientResponseWrapperTests.java | 163 +++++++++++++++ .../support/ServerRequestWrapperTests.java | 60 +++++- 10 files changed, 879 insertions(+), 9 deletions(-) create mode 100644 spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientResponseBuilder.java create mode 100644 spring-webflux/src/main/java/org/springframework/web/reactive/function/client/support/ClientResponseWrapper.java create mode 100644 spring-webflux/src/main/java/org/springframework/web/reactive/function/client/support/package-info.java create mode 100644 spring-webflux/src/test/java/org/springframework/web/reactive/function/client/DefaultClientResponseBuilderTests.java create mode 100644 spring-webflux/src/test/java/org/springframework/web/reactive/function/client/support/ClientResponseWrapperTests.java 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 2e24eb080c1..e15862e9548 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2018 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,20 +16,26 @@ package org.springframework.web.reactive.function.client; +import java.util.Collections; import java.util.List; import java.util.Optional; import java.util.OptionalLong; +import java.util.function.Consumer; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import org.springframework.core.ParameterizedTypeReference; +import org.springframework.core.io.buffer.DataBuffer; 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.codec.HttpMessageWriter; +import org.springframework.util.Assert; import org.springframework.util.MultiValueMap; import org.springframework.web.reactive.function.BodyExtractor; @@ -68,6 +74,11 @@ public interface ClientResponse { */ MultiValueMap cookies(); + /** + * Return the strategies used to convert the body of this response. + */ + ExchangeStrategies strategies(); + /** * Extract the body with the given {@code BodyExtractor}. * @param extractor the {@code BodyExtractor} that reads from the response @@ -141,6 +152,66 @@ public interface ClientResponse { Mono>> toEntityList(ParameterizedTypeReference typeReference); + // Static builder methods + + /** + * Create a builder with the status, headers, and cookies of the given response. + * @param other the response to copy the status, headers, and cookies from + * @return the created builder + */ + static Builder from(ClientResponse other) { + Assert.notNull(other, "'other' must not be null"); + return new DefaultClientResponseBuilder(other); + } + + /** + * Create a response builder with the given status code and using default strategies for reading + * the body. + * @param statusCode the status code + * @return the created builder + */ + static Builder create(HttpStatus statusCode) { + return create(statusCode, ExchangeStrategies.withDefaults()); + } + + /** + * Create a response builder with the given status code and strategies for reading the body. + * @param statusCode the status code + * @param strategies the strategies + * @return the created builder + */ + static Builder create(HttpStatus statusCode, ExchangeStrategies strategies) { + Assert.notNull(statusCode, "'statusCode' must not be null"); + Assert.notNull(strategies, "'strategies' must not be null"); + return new DefaultClientResponseBuilder(strategies) + .statusCode(statusCode); + } + + /** + * Create a response builder with the given status code and message body readers. + * @param statusCode the status code + * @param messageReaders the message readers + * @return the created builder + */ + static Builder create(HttpStatus statusCode, List> messageReaders) { + Assert.notNull(statusCode, "'statusCode' must not be null"); + Assert.notNull(messageReaders, "'messageReaders' must not be null"); + + return create(statusCode, new ExchangeStrategies() { + @Override + public List> messageReaders() { + return messageReaders; + } + + @Override + public List> messageWriters() { + // not used in the response + return Collections.emptyList(); + } + }); + + } + /** * Represents the headers of the HTTP response. * @see ClientResponse#headers() @@ -172,4 +243,80 @@ public interface ClientResponse { HttpHeaders asHttpHeaders(); } + /** + * Defines a builder for a response. + */ + interface Builder { + + /** + * Set the status code of the response. + * @param statusCode the new status code. + * @return this builder + */ + Builder statusCode(HttpStatus statusCode); + + /** + * Add the given header value(s) under the given name. + * @param headerName the header name + * @param headerValues the header value(s) + * @return this builder + * @see HttpHeaders#add(String, String) + */ + Builder header(String headerName, String... headerValues); + + /** + * Manipulate this response's headers with the given consumer. The + * headers provided to the consumer are "live", so that the consumer can be used to + * {@linkplain HttpHeaders#set(String, String) overwrite} existing header values, + * {@linkplain HttpHeaders#remove(Object) remove} values, or use any of the other + * {@link HttpHeaders} methods. + * @param headersConsumer a function that consumes the {@code HttpHeaders} + * @return this builder + */ + Builder headers(Consumer headersConsumer); + + /** + * Add a cookie with the given name and value(s). + * @param name the cookie name + * @param values the cookie value(s) + * @return this builder + */ + Builder cookie(String name, String... values); + + /** + * Manipulate this response's cookies with the given consumer. The + * map provided to the consumer is "live", so that the consumer can be used to + * {@linkplain MultiValueMap#set(Object, Object) overwrite} existing header values, + * {@linkplain MultiValueMap#remove(Object) remove} values, or use any of the other + * {@link MultiValueMap} methods. + * @param cookiesConsumer a function that consumes the cookies map + * @return this builder + */ + Builder cookies(Consumer> cookiesConsumer); + + /** + * Sets 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. + * @return this builder + */ + Builder body(Flux body); + + /** + * Sets 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. + * @param body the new body. + * @return this builder + */ + Builder body(String body); + + /** + * Builds the response. + * @return the response + */ + ClientResponse build(); + } } 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 ca6b2cc5a2b..3c4bbd3806e 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2018 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. @@ -61,6 +61,10 @@ class DefaultClientResponse implements ClientResponse { this.headers = new DefaultHeaders(); } + @Override + public ExchangeStrategies strategies() { + return this.strategies; + } @Override public HttpStatus statusCode() { 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 new file mode 100644 index 00000000000..c8293f92e5f --- /dev/null +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientResponseBuilder.java @@ -0,0 +1,189 @@ +/* + * Copyright 2002-2018 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 + * + * http://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.nio.charset.StandardCharsets; +import java.util.function.Consumer; + +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; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseCookie; +import org.springframework.http.client.reactive.ClientHttpResponse; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +/** + * Default implementation of {@link ClientResponse.Builder}. + * + * @author Arjen Poutsma + * @since 5.0.5 + */ +class DefaultClientResponseBuilder implements ClientResponse.Builder { + + private final HttpHeaders headers = new HttpHeaders(); + + private final MultiValueMap cookies = new LinkedMultiValueMap<>(); + + private HttpStatus statusCode = HttpStatus.OK; + + private Flux body = Flux.empty(); + + private ExchangeStrategies strategies; + + + public DefaultClientResponseBuilder(ExchangeStrategies strategies) { + Assert.notNull(strategies, "'strategies' must not be null"); + this.strategies = strategies; + } + + public DefaultClientResponseBuilder(ClientResponse other) { + this(other.strategies()); + statusCode(other.statusCode()); + headers(headers -> headers.addAll(other.headers().asHttpHeaders())); + cookies(cookies -> cookies.addAll(other.cookies())); + } + + @Override + public DefaultClientResponseBuilder statusCode(HttpStatus statusCode) { + Assert.notNull(statusCode, "'statusCode' must not be null"); + this.statusCode = statusCode; + return this; + } + + @Override + public ClientResponse.Builder header(String headerName, String... headerValues) { + for (String headerValue : headerValues) { + this.headers.add(headerName, headerValue); + } + return this; + } + + @Override + public ClientResponse.Builder headers(Consumer headersConsumer) { + Assert.notNull(headersConsumer, "'headersConsumer' must not be null"); + headersConsumer.accept(this.headers); + return this; + } + + @Override + public DefaultClientResponseBuilder cookie(String name, String... values) { + for (String value : values) { + this.cookies.add(name, ResponseCookie.from(name, value).build()); + } + return this; + } + + @Override + public ClientResponse.Builder cookies( + Consumer> cookiesConsumer) { + Assert.notNull(cookiesConsumer, "'cookiesConsumer' must not be null"); + cookiesConsumer.accept(this.cookies); + return this; + } + + @Override + public ClientResponse.Builder body(Flux body) { + Assert.notNull(body, "'body' must not be null"); + releaseBody(); + this.body = body; + return this; + } + + @Override + 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 this; + } + + private void releaseBody() { + this.body.subscribe(DataBufferUtils.releaseConsumer()); + } + + @Override + public ClientResponse build() { + ClientHttpResponse clientHttpResponse = new BuiltClientHttpResponse(this.statusCode, + this.headers, this.cookies, this.body); + return new DefaultClientResponse(clientHttpResponse, this.strategies); + } + + private static class BuiltClientHttpResponse implements ClientHttpResponse { + + private final HttpStatus statusCode; + + private final HttpHeaders headers; + + private final MultiValueMap cookies; + + private final Flux body; + + public BuiltClientHttpResponse(HttpStatus statusCode, HttpHeaders headers, + MultiValueMap cookies, + Flux body) { + + this.statusCode = statusCode; + this.headers = HttpHeaders.readOnlyHttpHeaders(headers); + this.cookies = unmodifiableCopy(cookies); + this.body = body; + } + + private static @Nullable MultiValueMap unmodifiableCopy(@Nullable MultiValueMap original) { + if (original != null) { + return CollectionUtils.unmodifiableMultiValueMap(new LinkedMultiValueMap<>(original)); + } + else { + return null; + } + } + + @Override + public HttpStatus getStatusCode() { + return this.statusCode; + } + + @Override + public HttpHeaders getHeaders() { + return this.headers; + } + + @Override + public MultiValueMap getCookies() { + return this.cookies; + } + + @Override + public Flux getBody() { + return this.body; + } + } + +} diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/support/ClientResponseWrapper.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/support/ClientResponseWrapper.java new file mode 100644 index 00000000000..8540cd893eb --- /dev/null +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/support/ClientResponseWrapper.java @@ -0,0 +1,176 @@ +/* + * Copyright 2002-2018 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 + * + * http://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.support; + +import java.util.List; +import java.util.Optional; +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; +import org.springframework.http.ResponseCookie; +import org.springframework.http.ResponseEntity; +import org.springframework.http.client.reactive.ClientHttpResponse; +import org.springframework.util.Assert; +import org.springframework.util.MultiValueMap; +import org.springframework.web.reactive.function.BodyExtractor; +import org.springframework.web.reactive.function.client.ClientResponse; +import org.springframework.web.reactive.function.client.ExchangeStrategies; + +/** + * Implementation of the {@link ClientResponse} interface that can be subclassed + * to adapt the request in a + * {@link org.springframework.web.reactive.function.client.ExchangeFilterFunction exchange filter function}. + * All methods default to calling through to the wrapped request. + * + * @author Arjen Poutsma + * @since 5.0.5 + */ +public class ClientResponseWrapper implements ClientResponse { + + private final ClientResponse delegate; + + + /** + * Create a new {@code ClientResponseWrapper} that wraps the given response. + * @param delegate the response to wrap + */ + public ClientResponseWrapper(ClientResponse delegate) { + Assert.notNull(delegate, "'delegate' must not be null"); + this.delegate = delegate; + } + + + /** + * Return the wrapped request. + */ + public ClientResponse response() { + return this.delegate; + } + + @Override + public ExchangeStrategies strategies() { + return this.delegate.strategies(); + } + + @Override + public HttpStatus statusCode() { + return this.delegate.statusCode(); + } + + @Override + public Headers headers() { + return this.delegate.headers(); + } + + @Override + public MultiValueMap cookies() { + return this.delegate.cookies(); + } + + @Override + public T body(BodyExtractor extractor) { + return this.delegate.body(extractor); + } + + @Override + public Mono bodyToMono(Class elementClass) { + return this.delegate.bodyToMono(elementClass); + } + + @Override + public Mono bodyToMono(ParameterizedTypeReference typeReference) { + return this.delegate.bodyToMono(typeReference); + } + + @Override + public Flux bodyToFlux(Class elementClass) { + return this.delegate.bodyToFlux(elementClass); + } + + @Override + public Flux bodyToFlux(ParameterizedTypeReference typeReference) { + return this.delegate.bodyToFlux(typeReference); + } + + @Override + public Mono> toEntity(Class bodyType) { + return this.delegate.toEntity(bodyType); + } + + @Override + public Mono> toEntity(ParameterizedTypeReference typeReference) { + return this.delegate.toEntity(typeReference); + } + + @Override + public Mono>> toEntityList(Class elementType) { + return this.delegate.toEntityList(elementType); + } + + @Override + public Mono>> toEntityList(ParameterizedTypeReference typeReference) { + return this.delegate.toEntityList(typeReference); + } + + /** + * Implementation of the {@code Headers} interface that can be subclassed + * to adapt the headers in a + * {@link org.springframework.web.reactive.function.client.ExchangeFilterFunction exchange filter function}. + * All methods default to calling through to the wrapped request. + */ + public static class HeadersWrapper implements ClientResponse.Headers { + + private final Headers headers; + + + /** + * Create a new {@code HeadersWrapper} that wraps the given request. + * @param headers the headers to wrap + */ + public HeadersWrapper(Headers headers) { + this.headers = headers; + } + + + @Override + public OptionalLong contentLength() { + return this.headers.contentLength(); + } + + @Override + public Optional contentType() { + return this.headers.contentType(); + } + + @Override + public List header(String headerName) { + return this.headers.header(headerName); + } + + @Override + public HttpHeaders asHttpHeaders() { + return this.headers.asHttpHeaders(); + } + } + +} diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/support/package-info.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/support/package-info.java new file mode 100644 index 00000000000..4e85280e65f --- /dev/null +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/support/package-info.java @@ -0,0 +1,26 @@ +/* + * Copyright 2002-2018 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 + * + * http://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. + */ + +/** + * Classes supporting the {@code org.springframework.web.reactive.function.client} package. + * Contains a {@code ClientResponse} wrapper to adapt a request. + */ +@NonNullApi +@NonNullFields +package org.springframework.web.reactive.function.client.support; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/support/ServerRequestWrapper.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/support/ServerRequestWrapper.java index c4b100819fb..86176860705 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/support/ServerRequestWrapper.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/support/ServerRequestWrapper.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2018 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. @@ -40,14 +40,14 @@ import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.util.Assert; import org.springframework.util.MultiValueMap; import org.springframework.web.reactive.function.BodyExtractor; -import org.springframework.web.reactive.function.server.HandlerFunction; import org.springframework.web.reactive.function.server.ServerRequest; import org.springframework.web.server.WebSession; import org.springframework.web.util.UriBuilder; /** * Implementation of the {@link ServerRequest} interface that can be subclassed - * to adapt the request to a {@link HandlerFunction handler function}. + * to adapt the request in a + * {@link org.springframework.web.reactive.function.server.HandlerFilterFunction handler filter function}. * All methods default to calling through to the wrapped request. * * @author Arjen Poutsma @@ -59,7 +59,7 @@ public class ServerRequestWrapper implements ServerRequest { /** - * Create a new {@code RequestWrapper} that wraps the given request. + * Create a new {@code ServerRequestWrapper} that wraps the given request. * @param delegate the request to wrap */ public ServerRequestWrapper(ServerRequest delegate) { @@ -187,13 +187,15 @@ public class ServerRequestWrapper implements ServerRequest { /** * Implementation of the {@code Headers} interface that can be subclassed - * to adapt the headers to a {@link HandlerFunction handler function}. + * to adapt the headers in a + * {@link org.springframework.web.reactive.function.server.HandlerFilterFunction handler filter function}. * All methods default to calling through to the wrapped headers. */ public static class HeadersWrapper implements ServerRequest.Headers { private final Headers headers; + /** * Create a new {@code HeadersWrapper} that wraps the given request. * @param headers the headers to wrap @@ -203,6 +205,7 @@ public class ServerRequestWrapper implements ServerRequest { this.headers = headers; } + @Override public List accept() { return this.headers.accept(); diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/support/package-info.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/support/package-info.java index e3780124050..2f4f6d65044 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/support/package-info.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/support/package-info.java @@ -1,5 +1,5 @@ /** - * Classes supporting the {@code org.springframework.web.reactive.function} package. + * Classes supporting the {@code org.springframework.web.reactive.function.server} package. * Contains a {@code HandlerAdapter} that supports {@code HandlerFunction}s, * a {@code HandlerResultHandler} that supports {@code ServerResponse}s, and * a {@code ServerRequest} wrapper to adapt a request. 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 new file mode 100644 index 00000000000..c2ecfd46aba --- /dev/null +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/DefaultClientResponseBuilderTests.java @@ -0,0 +1,104 @@ +/* + * Copyright 2002-2018 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 + * + * http://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.nio.charset.StandardCharsets; + +import org.junit.Before; +import org.junit.Test; +import reactor.core.publisher.Flux; +import reactor.test.StepVerifier; + +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferFactory; +import org.springframework.core.io.buffer.DefaultDataBufferFactory; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseCookie; + +import static org.junit.Assert.*; + +/** + * @author Arjen Poutsma + */ +public class DefaultClientResponseBuilderTests { + + private DataBufferFactory dataBufferFactory; + + @Before + public void createBufferFactory() { + this.dataBufferFactory = new DefaultDataBufferFactory(); + } + + @Test + public void normal() { + Flux body = Flux.just("baz") + .map(s -> s.getBytes(StandardCharsets.UTF_8)) + .map(dataBufferFactory::wrap); + + ClientResponse response = ClientResponse.create(HttpStatus.BAD_GATEWAY, ExchangeStrategies.withDefaults()) + .header("foo", "bar") + .cookie("baz", "qux") + .body(body) + .build(); + + assertEquals(HttpStatus.BAD_GATEWAY, response.statusCode()); + HttpHeaders responseHeaders = response.headers().asHttpHeaders(); + assertEquals("bar", responseHeaders.getFirst("foo")); + assertNotNull("qux", response.cookies().getFirst("baz")); + assertEquals("qux", response.cookies().getFirst("baz").getValue()); + + StepVerifier.create(response.bodyToFlux(String.class)) + .expectNext("baz") + .verifyComplete(); + } + + @Test + public void from() throws Exception { + Flux otherBody = Flux.just("foo", "bar") + .map(s -> s.getBytes(StandardCharsets.UTF_8)) + .map(dataBufferFactory::wrap); + + ClientResponse other = ClientResponse.create(HttpStatus.BAD_REQUEST, ExchangeStrategies.withDefaults()) + .header("foo", "bar") + .cookie("baz", "qux") + .body(otherBody) + .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")) + .cookies(cookies -> cookies.set("baz", ResponseCookie.from("baz", "quux").build())) + .body(body) + .build(); + + assertEquals(HttpStatus.BAD_REQUEST, result.statusCode()); + assertEquals(1, result.headers().asHttpHeaders().size()); + assertEquals("baar", result.headers().asHttpHeaders().getFirst("foo")); + assertEquals(1, result.cookies().size()); + assertEquals("quux", result.cookies().getFirst("baz").getValue()); + + StepVerifier.create(result.bodyToFlux(String.class)) + .expectNext("baz") + .verifyComplete(); + } + + +} \ No newline at end of file diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/support/ClientResponseWrapperTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/support/ClientResponseWrapperTests.java new file mode 100644 index 00000000000..6937bb761e5 --- /dev/null +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/support/ClientResponseWrapperTests.java @@ -0,0 +1,163 @@ +/* + * Copyright 2002-2018 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 + * + * http://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.support; + +import java.util.List; + +import org.junit.Before; +import org.junit.Test; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpStatus; +import org.springframework.http.ReactiveHttpInputMessage; +import org.springframework.http.ResponseCookie; +import org.springframework.http.ResponseEntity; +import org.springframework.util.MultiValueMap; +import org.springframework.web.reactive.function.BodyExtractor; +import org.springframework.web.reactive.function.BodyExtractors; +import org.springframework.web.reactive.function.client.ClientResponse; + +import static java.util.Collections.singletonList; +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +/** + * @author Arjen Poutsma + */ +public class ClientResponseWrapperTests { + + private ClientResponse mockResponse; + + private ClientResponseWrapper wrapper; + + @Before + public void createWrapper() { + this.mockResponse = mock(ClientResponse.class); + this.wrapper = new ClientResponseWrapper(mockResponse); + } + + @Test + public void response() throws Exception { + assertSame(mockResponse, wrapper.response()); + } + + @Test + public void statusCode() throws Exception { + HttpStatus status = HttpStatus.BAD_REQUEST; + when(mockResponse.statusCode()).thenReturn(status); + + assertSame(status, wrapper.statusCode()); + } + + @Test + public void headers() throws Exception { + ClientResponse.Headers headers = mock(ClientResponse.Headers.class); + when(mockResponse.headers()).thenReturn(headers); + + assertSame(headers, wrapper.headers()); + } + + @Test + public void cookies() throws Exception { + MultiValueMap cookies = mock(MultiValueMap.class); + when(mockResponse.cookies()).thenReturn(cookies); + + assertSame(cookies, wrapper.cookies()); + } + + @Test + public void bodyExtractor() throws Exception { + Mono result = Mono.just("foo"); + BodyExtractor, ReactiveHttpInputMessage> extractor = BodyExtractors.toMono(String.class); + when(mockResponse.body(extractor)).thenReturn(result); + + assertSame(result, wrapper.body(extractor)); + } + + @Test + public void bodyToMonoClass() throws Exception { + Mono result = Mono.just("foo"); + when(mockResponse.bodyToMono(String.class)).thenReturn(result); + + assertSame(result, wrapper.bodyToMono(String.class)); + } + + @Test + public void bodyToMonoParameterizedTypeReference() throws Exception { + Mono result = Mono.just("foo"); + ParameterizedTypeReference reference = new ParameterizedTypeReference() {}; + when(mockResponse.bodyToMono(reference)).thenReturn(result); + + assertSame(result, wrapper.bodyToMono(reference)); + } + + @Test + public void bodyToFluxClass() throws Exception { + Flux result = Flux.just("foo"); + when(mockResponse.bodyToFlux(String.class)).thenReturn(result); + + assertSame(result, wrapper.bodyToFlux(String.class)); + } + + @Test + public void bodyToFluxParameterizedTypeReference() throws Exception { + Flux result = Flux.just("foo"); + ParameterizedTypeReference reference = new ParameterizedTypeReference() {}; + when(mockResponse.bodyToFlux(reference)).thenReturn(result); + + assertSame(result, wrapper.bodyToFlux(reference)); + } + + @Test + public void toEntityClass() throws Exception { + Mono> result = Mono.just(new ResponseEntity<>("foo", HttpStatus.OK)); + when(mockResponse.toEntity(String.class)).thenReturn(result); + + assertSame(result, wrapper.toEntity(String.class)); + } + + @Test + public void toEntityParameterizedTypeReference() throws Exception { + Mono> result = Mono.just(new ResponseEntity<>("foo", HttpStatus.OK)); + ParameterizedTypeReference reference = new ParameterizedTypeReference() {}; + when(mockResponse.toEntity(reference)).thenReturn(result); + + assertSame(result, wrapper.toEntity(reference)); + } + + @Test + public void toEntityListClass() throws Exception { + Mono>> result = Mono.just(new ResponseEntity<>(singletonList("foo"), HttpStatus.OK)); + when(mockResponse.toEntityList(String.class)).thenReturn(result); + + assertSame(result, wrapper.toEntityList(String.class)); + } + + @Test + public void toEntityListParameterizedTypeReference() throws Exception { + Mono>> result = Mono.just(new ResponseEntity<>(singletonList("foo"), HttpStatus.OK)); + ParameterizedTypeReference reference = new ParameterizedTypeReference() {}; + when(mockResponse.toEntityList(reference)).thenReturn(result); + + assertSame(result, wrapper.toEntityList(reference)); + } + + + +} \ No newline at end of file diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/support/ServerRequestWrapperTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/support/ServerRequestWrapperTests.java index 7fbe2714161..876fc885447 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/support/ServerRequestWrapperTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/support/ServerRequestWrapperTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2018 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. @@ -23,10 +23,17 @@ import java.util.Optional; import org.junit.Before; import org.junit.Test; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpCookie; import org.springframework.http.HttpMethod; +import org.springframework.http.ReactiveHttpInputMessage; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; +import org.springframework.web.reactive.function.BodyExtractor; +import org.springframework.web.reactive.function.BodyExtractors; import org.springframework.web.reactive.function.server.ServerRequest; import static org.junit.Assert.*; @@ -128,4 +135,55 @@ public class ServerRequestWrapperTests { assertSame(pathVariables, wrapper.pathVariables()); } + @Test + public void cookies() throws Exception { + MultiValueMap cookies = mock(MultiValueMap.class); + when(mockRequest.cookies()).thenReturn(cookies); + + assertSame(cookies, wrapper.cookies()); + } + + @Test + public void bodyExtractor() throws Exception { + Mono result = Mono.just("foo"); + BodyExtractor, ReactiveHttpInputMessage> extractor = BodyExtractors.toMono(String.class); + when(mockRequest.body(extractor)).thenReturn(result); + + assertSame(result, wrapper.body(extractor)); + } + + @Test + public void bodyToMonoClass() throws Exception { + Mono result = Mono.just("foo"); + when(mockRequest.bodyToMono(String.class)).thenReturn(result); + + assertSame(result, wrapper.bodyToMono(String.class)); + } + + @Test + public void bodyToMonoParameterizedTypeReference() throws Exception { + Mono result = Mono.just("foo"); + ParameterizedTypeReference reference = new ParameterizedTypeReference() {}; + when(mockRequest.bodyToMono(reference)).thenReturn(result); + + assertSame(result, wrapper.bodyToMono(reference)); + } + + @Test + public void bodyToFluxClass() throws Exception { + Flux result = Flux.just("foo"); + when(mockRequest.bodyToFlux(String.class)).thenReturn(result); + + assertSame(result, wrapper.bodyToFlux(String.class)); + } + + @Test + public void bodyToFluxParameterizedTypeReference() throws Exception { + Flux result = Flux.just("foo"); + ParameterizedTypeReference reference = new ParameterizedTypeReference() {}; + when(mockRequest.bodyToFlux(reference)).thenReturn(result); + + assertSame(result, wrapper.bodyToFlux(reference)); + } + } \ No newline at end of file