diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolverTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolverTests.java index 791e1c38d59d038d03fc2b6aea6e34777668abbb..8071712096b2aac347ceb16367f5ec9949538acb 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolverTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolverTests.java @@ -15,6 +15,7 @@ */ package org.springframework.web.reactive.result.method.annotation; +import java.net.URISyntaxException; import java.util.Map; import java.util.function.Function; @@ -29,10 +30,9 @@ import org.springframework.core.MethodParameter; import org.springframework.core.ReactiveAdapterRegistry; import org.springframework.core.ResolvableType; import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest; import org.springframework.mock.http.server.reactive.test.MockServerHttpResponse; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; import org.springframework.validation.BindingResult; import org.springframework.validation.annotation.Validated; import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; @@ -41,18 +41,19 @@ import org.springframework.web.bind.support.ConfigurableWebBindingInitializer; import org.springframework.web.bind.support.WebExchangeBindException; import org.springframework.web.reactive.BindingContext; import org.springframework.web.reactive.result.ResolvableMethod; -import org.springframework.web.reactive.result.method.HandlerMethodArgumentResolver; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.adapter.DefaultServerWebExchange; import org.springframework.web.server.session.MockWebSessionManager; +import org.springframework.web.server.session.WebSessionManager; import static junit.framework.TestCase.assertNotNull; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; -import static org.springframework.core.ResolvableType.*; -import static org.springframework.util.Assert.*; +import static org.springframework.core.ResolvableType.forClass; +import static org.springframework.core.ResolvableType.forClassWithGenerics; +import static org.springframework.util.Assert.isTrue; /** @@ -61,28 +62,18 @@ import static org.springframework.util.Assert.*; */ public class ModelAttributeMethodArgumentResolverTests { - private ServerWebExchange exchange; - - private final MockServerHttpRequest request = new MockServerHttpRequest(HttpMethod.POST, "/path"); - - private final MultiValueMap formData = new LinkedMultiValueMap<>(); - - private BindingContext bindingContext; + private BindingContext bindContext; private ResolvableMethod testMethod = ResolvableMethod.onClass(this.getClass()).name("handle"); @Before public void setUp() throws Exception { - MockServerHttpResponse response = new MockServerHttpResponse(); - this.exchange = new DefaultServerWebExchange(this.request, response, new MockWebSessionManager()); - this.exchange = this.exchange.mutate().formData(Mono.just(this.formData)).build(); - LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean(); validator.afterPropertiesSet(); ConfigurableWebBindingInitializer initializer = new ConfigurableWebBindingInitializer(); initializer.setValidator(validator); - this.bindingContext = new BindingContext(initializer); + this.bindContext = new BindingContext(initializer); } @@ -156,49 +147,49 @@ public class ModelAttributeMethodArgumentResolverTests { public void bindExisting() throws Exception { Foo foo = new Foo(); foo.setName("Jim"); - this.bindingContext.getModel().addAttribute(foo); + this.bindContext.getModel().addAttribute(foo); testBindFoo(forClass(Foo.class), value -> { assertEquals(Foo.class, value.getClass()); return (Foo) value; }); - assertSame(foo, this.bindingContext.getModel().asMap().get("foo")); + assertSame(foo, this.bindContext.getModel().asMap().get("foo")); } @Test public void bindExistingMono() throws Exception { Foo foo = new Foo(); foo.setName("Jim"); - this.bindingContext.getModel().addAttribute("foo", Mono.just(foo)); + this.bindContext.getModel().addAttribute("foo", Mono.just(foo)); testBindFoo(forClass(Foo.class), value -> { assertEquals(Foo.class, value.getClass()); return (Foo) value; }); - assertSame(foo, this.bindingContext.getModel().asMap().get("foo")); + assertSame(foo, this.bindContext.getModel().asMap().get("foo")); } @Test public void bindExistingSingle() throws Exception { Foo foo = new Foo(); foo.setName("Jim"); - this.bindingContext.getModel().addAttribute("foo", Single.just(foo)); + this.bindContext.getModel().addAttribute("foo", Single.just(foo)); testBindFoo(forClass(Foo.class), value -> { assertEquals(Foo.class, value.getClass()); return (Foo) value; }); - assertSame(foo, this.bindingContext.getModel().asMap().get("foo")); + assertSame(foo, this.bindContext.getModel().asMap().get("foo")); } @Test public void bindExistingMonoToMono() throws Exception { Foo foo = new Foo(); foo.setName("Jim"); - this.bindingContext.getModel().addAttribute("foo", Mono.just(foo)); + this.bindContext.getModel().addAttribute("foo", Mono.just(foo)); testBindFoo(forClassWithGenerics(Mono.class, Foo.class), mono -> { assertTrue(mono.getClass().getName(), mono instanceof Mono); @@ -238,14 +229,11 @@ public class ModelAttributeMethodArgumentResolverTests { } - private void testBindFoo(ResolvableType type, Function valueExtractor) { - - this.formData.add("name", "Robert"); - this.formData.add("age", "25"); + private void testBindFoo(ResolvableType type, Function valueExtractor) throws Exception { Object value = createResolver() - .resolveArgument(parameter(type), this.bindingContext, this.exchange) - .blockMillis(5000); + .resolveArgument(parameter(type), this.bindContext, exchange("name=Robert&age=25")) + .blockMillis(0); Foo foo = valueExtractor.apply(value); assertEquals("Robert", foo.getName()); @@ -253,19 +241,18 @@ public class ModelAttributeMethodArgumentResolverTests { String key = "foo"; String bindingResultKey = BindingResult.MODEL_KEY_PREFIX + key; - Map map = bindingContext.getModel().asMap(); + Map map = bindContext.getModel().asMap(); assertEquals(map.toString(), 2, map.size()); assertSame(foo, map.get(key)); assertNotNull(map.get(bindingResultKey)); assertTrue(map.get(bindingResultKey) instanceof BindingResult); } - private void testValidationError(ResolvableType type, Function, Mono> valueMonoExtractor) { - - this.formData.add("age", "invalid"); + private void testValidationError(ResolvableType type, Function, Mono> valueMonoExtractor) + throws URISyntaxException { - HandlerMethodArgumentResolver resolver = createResolver(); - Mono mono = resolver.resolveArgument(parameter(type), this.bindingContext, this.exchange); + ServerWebExchange exchange = exchange("age=invalid"); + Mono mono = createResolver().resolveArgument(parameter(type), this.bindContext, exchange); mono = valueMonoExtractor.apply(mono); @@ -294,6 +281,15 @@ public class ModelAttributeMethodArgumentResolverTests { parameter -> !parameter.hasParameterAnnotations()); } + private ServerWebExchange exchange(String formData) throws URISyntaxException { + MockServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, "/"); + request.getHeaders().setContentType(MediaType.APPLICATION_FORM_URLENCODED); + request.setBody(formData); + MockServerHttpResponse response = new MockServerHttpResponse(); + WebSessionManager manager = new MockWebSessionManager(); + return new DefaultServerWebExchange(request, response, manager); + } + @SuppressWarnings("unused") void handle( diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/DefaultServerHttpRequestBuilder.java b/spring-web/src/main/java/org/springframework/http/server/reactive/DefaultServerHttpRequestBuilder.java index 8cafcbf4217607e0d9882de109682035b4847eea..8fdfe7f29d2867348061085a2c758009256d611a 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/DefaultServerHttpRequestBuilder.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/DefaultServerHttpRequestBuilder.java @@ -17,14 +17,9 @@ package org.springframework.http.server.reactive; import java.net.URI; -import reactor.core.publisher.Flux; - -import org.springframework.core.io.buffer.DataBuffer; -import org.springframework.http.HttpCookie; -import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.util.Assert; -import org.springframework.util.MultiValueMap; +import org.springframework.web.util.UriComponentsBuilder; /** * Package private default implementation of {@link ServerHttpRequest.Builder}. @@ -36,21 +31,12 @@ class DefaultServerHttpRequestBuilder implements ServerHttpRequest.Builder { private final ServerHttpRequest delegate; - private HttpMethod httpMethod; - private URI uri; + private String path; private String contextPath; - private MultiValueMap queryParams; - - private HttpHeaders headers; - - private MultiValueMap cookies; - - private Flux body; - public DefaultServerHttpRequestBuilder(ServerHttpRequest delegate) { Assert.notNull(delegate, "ServerHttpRequest delegate is required."); @@ -65,8 +51,8 @@ class DefaultServerHttpRequestBuilder implements ServerHttpRequest.Builder { } @Override - public ServerHttpRequest.Builder uri(URI uri) { - this.uri = uri; + public ServerHttpRequest.Builder path(String path) { + this.path = path; return this; } @@ -76,34 +62,14 @@ class DefaultServerHttpRequestBuilder implements ServerHttpRequest.Builder { return this; } - @Override - public ServerHttpRequest.Builder queryParams(MultiValueMap queryParams) { - this.queryParams = queryParams; - return this; - } - - @Override - public ServerHttpRequest.Builder headers(HttpHeaders headers) { - this.headers = headers; - return this; - } - - @Override - public ServerHttpRequest.Builder cookies(MultiValueMap cookies) { - this.cookies = cookies; - return this; - } - - @Override - public ServerHttpRequest.Builder body(Flux body) { - this.body = body; - return this; - } - @Override public ServerHttpRequest build() { - return new MutativeDecorator(this.delegate, this.httpMethod, this.uri, this.contextPath, - this.queryParams, this.headers, this.cookies, this.body); + URI uri = null; + if (this.path != null) { + uri = this.delegate.getURI(); + uri = UriComponentsBuilder.fromUri(uri).replacePath(this.path).build(true).toUri(); + } + return new MutativeDecorator(this.delegate, this.httpMethod, uri, this.contextPath); } @@ -119,27 +85,14 @@ class DefaultServerHttpRequestBuilder implements ServerHttpRequest.Builder { private final String contextPath; - private final MultiValueMap queryParams; - - private final HttpHeaders headers; - private final MultiValueMap cookies; - - private final Flux body; - - - public MutativeDecorator(ServerHttpRequest delegate, HttpMethod httpMethod, URI uri, - String contextPath, MultiValueMap queryParams, HttpHeaders headers, - MultiValueMap cookies, Flux body) { + public MutativeDecorator(ServerHttpRequest delegate, HttpMethod httpMethod, + URI uri, String contextPath) { super(delegate); this.httpMethod = httpMethod; this.uri = uri; this.contextPath = contextPath; - this.queryParams = queryParams; - this.headers = headers; - this.cookies = cookies; - this.body = body; } @Override @@ -157,25 +110,6 @@ class DefaultServerHttpRequestBuilder implements ServerHttpRequest.Builder { return (this.contextPath != null ? this.contextPath : super.getContextPath()); } - @Override - public MultiValueMap getQueryParams() { - return (this.queryParams != null ? this.queryParams : super.getQueryParams()); - } - - @Override - public HttpHeaders getHeaders() { - return (this.headers != null ? this.headers : super.getHeaders()); - } - - @Override - public MultiValueMap getCookies() { - return (this.cookies != null ? this.cookies : super.getCookies()); - } - - @Override - public Flux getBody() { - return (this.body != null ? this.body : super.getBody()); - } } } diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpRequest.java b/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpRequest.java index 92578a2be621b79717824135929d1152dd07340c..8b760a10ee431454d87d61031497453422e59996 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpRequest.java @@ -61,9 +61,9 @@ public interface ServerHttpRequest extends HttpRequest, ReactiveHttpInputMessage /** - * Return a builder to mutate properties of this request. The resulting - * new request is an immutable {@link ServerHttpRequestDecorator decorator} - * around the current exchange instance returning mutated values. + * Return a builder to mutate properties of this request by wrapping it + * with {@link ServerHttpRequestDecorator} and returning either mutated + * values or delegating back to this instance. */ default ServerHttpRequest.Builder mutate() { return new DefaultServerHttpRequestBuilder(this); @@ -71,51 +71,29 @@ public interface ServerHttpRequest extends HttpRequest, ReactiveHttpInputMessage /** - * Builder for mutating properties of a {@link ServerHttpRequest}. + * Builder for mutating an existing {@link ServerHttpRequest}. */ interface Builder { /** - * Set the HTTP method. + * Set the HTTP method to return. */ Builder method(HttpMethod httpMethod); /** - * Set the request URI. + * Set the request URI to return. */ - Builder uri(URI uri); - + Builder path(String path); /** - * Set the contextPath for the request. + * Set the contextPath to return. */ Builder contextPath(String contextPath); /** - * Set the query params to return. - */ - Builder queryParams(MultiValueMap queryParams); - - /** - * Set the headers to use. - */ - Builder headers(HttpHeaders headers); - - /** - * Set the cookies to use. - */ - Builder cookies(MultiValueMap cookies); - - /** - * Set the body to return. - */ - Builder body(Flux body); - - /** - * Build an immutable wrapper that returning the mutated properties. + * Build a {@link ServerHttpRequest} decorator with the mutated properties. */ ServerHttpRequest build(); - } } diff --git a/spring-web/src/main/java/org/springframework/web/server/DefaultServerWebExchangeBuilder.java b/spring-web/src/main/java/org/springframework/web/server/DefaultServerWebExchangeBuilder.java index 903fffecd701f44402580f0050f2deb78f43631f..e08e04226d79a317df8c496661a95a4b4fc33776 100644 --- a/spring-web/src/main/java/org/springframework/web/server/DefaultServerWebExchangeBuilder.java +++ b/spring-web/src/main/java/org/springframework/web/server/DefaultServerWebExchangeBuilder.java @@ -16,13 +16,13 @@ package org.springframework.web.server; import java.security.Principal; +import java.util.function.Consumer; import reactor.core.publisher.Mono; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.util.Assert; -import org.springframework.util.MultiValueMap; /** * Package private implementation of {@link ServerWebExchange.Builder}. @@ -38,19 +38,22 @@ class DefaultServerWebExchangeBuilder implements ServerWebExchange.Builder { private ServerHttpResponse response; - private Mono user; + private Mono principalMono; - private Mono session; - private Mono> formData; - - - public DefaultServerWebExchangeBuilder(ServerWebExchange delegate) { + DefaultServerWebExchangeBuilder(ServerWebExchange delegate) { Assert.notNull(delegate, "'delegate' is required."); this.delegate = delegate; } + @Override + public ServerWebExchange.Builder request(Consumer consumer) { + ServerHttpRequest.Builder builder = this.delegate.getRequest().mutate(); + consumer.accept(builder); + return request(builder.build()); + } + @Override public ServerWebExchange.Builder request(ServerHttpRequest request) { this.request = request; @@ -64,27 +67,14 @@ class DefaultServerWebExchangeBuilder implements ServerWebExchange.Builder { } @Override - public ServerWebExchange.Builder principal(Mono user) { - this.user = user; - return this; - } - - @Override - public ServerWebExchange.Builder session(Mono session) { - this.session = session; - return this; - } - - @Override - public ServerWebExchange.Builder formData(Mono> formData) { - this.formData = formData; + public ServerWebExchange.Builder principal(Mono principalMono) { + this.principalMono = principalMono; return this; } @Override public ServerWebExchange build() { - return new MutativeDecorator(this.delegate, this.request, this.response, - this.user, this.session, this.formData); + return new MutativeDecorator(this.delegate, this.request, this.response, this.principalMono); } @@ -98,23 +88,16 @@ class DefaultServerWebExchangeBuilder implements ServerWebExchange.Builder { private final ServerHttpResponse response; - private final Mono userMono; - - private final Mono session; + private final Mono principalMono; - private final Mono> formData; - - public MutativeDecorator(ServerWebExchange delegate, - ServerHttpRequest request, ServerHttpResponse response, Mono user, - Mono session, Mono> formData) { + public MutativeDecorator(ServerWebExchange delegate, ServerHttpRequest request, + ServerHttpResponse response, Mono principalMono) { super(delegate); this.request = request; this.response = response; - this.userMono = user; - this.session = session; - this.formData = formData; + this.principalMono = principalMono; } @@ -128,20 +111,11 @@ class DefaultServerWebExchangeBuilder implements ServerWebExchange.Builder { return (this.response != null ? this.response : getDelegate().getResponse()); } - @Override - public Mono getSession() { - return (this.session != null ? this.session : getDelegate().getSession()); - } - @SuppressWarnings("unchecked") @Override public Mono getPrincipal() { - return (this.userMono != null ? (Mono) this.userMono : getDelegate().getPrincipal()); - } - - @Override - public Mono> getFormData() { - return (this.formData != null ? this.formData : getDelegate().getFormData()); + return (this.principalMono != null ? + (Mono) this.principalMono : getDelegate().getPrincipal()); } } diff --git a/spring-web/src/main/java/org/springframework/web/server/ServerWebExchange.java b/spring-web/src/main/java/org/springframework/web/server/ServerWebExchange.java index 2e1e9bde36de691268e4c492273536e64b36c5ca..ebd06b5a71eb072efc4ed8da49379ad4bd3be071 100644 --- a/spring-web/src/main/java/org/springframework/web/server/ServerWebExchange.java +++ b/spring-web/src/main/java/org/springframework/web/server/ServerWebExchange.java @@ -20,6 +20,7 @@ import java.security.Principal; import java.time.Instant; import java.util.Map; import java.util.Optional; +import java.util.function.Consumer; import reactor.core.publisher.Mono; @@ -134,9 +135,9 @@ public interface ServerWebExchange { /** - * Return a builder to mutate properties of this exchange. The resulting - * new exchange is an immutable {@link ServerWebExchangeDecorator decorator} - * around the current exchange instance returning mutated values. + * Return a builder to mutate properties of this exchange by wrapping it + * with {@link ServerWebExchangeDecorator} and returning either mutated + * values or delegating back to this instance. */ default Builder mutate() { return new DefaultServerWebExchangeBuilder(this); @@ -144,40 +145,52 @@ public interface ServerWebExchange { /** - * Builder for mutating the properties of a {@link ServerWebExchange}. + * Builder for mutating an existing {@link ServerWebExchange}. + * Removes the need */ interface Builder { /** - * Set the request to use. + * Configure a consumer to modify the current request using a builder. + *

Effectively this: + *

+		 * exchange.mutate().request(builder-> builder.method(HttpMethod.PUT));
+		 *
+		 * // vs...
+		 *
+		 * ServerHttpRequest request = exchange.getRequest().mutate()
+		 *     .method(HttpMethod.PUT)
+		 *     .build();
+		 *
+		 * exchange.mutate().request(request);
+		 * 
+ * @see ServerHttpRequest#mutate() */ - Builder request(ServerHttpRequest request); - - /** - * Set the response to use. - */ - Builder response(ServerHttpResponse response); + Builder request(Consumer requestBuilderConsumer); /** - * Set the principal to use. + * Set the request to use especially when there is a need to override + * {@link ServerHttpRequest} methods. To simply mutate request properties + * see {@link #request(Consumer)} instead. + * @see org.springframework.http.server.reactive.ServerHttpRequestDecorator */ - Builder principal(Mono user); + Builder request(ServerHttpRequest request); /** - * Set the session to use. + * Set the response to use. + * @see org.springframework.http.server.reactive.ServerHttpResponseDecorator */ - Builder session(Mono session); + Builder response(ServerHttpResponse response); /** - * Set the form data. + * Set the {@code Mono} to return for this exchange. */ - Builder formData(Mono> formData); + Builder principal(Mono principalMono); /** - * Build an immutable wrapper that returning the mutated properties. + * Build a {@link ServerWebExchange} decorator with the mutated properties. */ ServerWebExchange build(); - } } diff --git a/spring-web/src/test/java/org/springframework/http/server/reactive/HttpHandlerAdapterSupportTests.java b/spring-web/src/test/java/org/springframework/http/server/reactive/HttpHandlerAdapterSupportTests.java index 28127b55208be7d7c426ba020b4c77e3e3dc66c1..a332669459593929423935eadf7b471625818574 100644 --- a/spring-web/src/test/java/org/springframework/http/server/reactive/HttpHandlerAdapterSupportTests.java +++ b/spring-web/src/test/java/org/springframework/http/server/reactive/HttpHandlerAdapterSupportTests.java @@ -18,7 +18,6 @@ package org.springframework.http.server.reactive; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; -import java.util.LinkedHashMap; import java.util.Map; import org.junit.Test;