diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerResult.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerResult.java index 191a686efc9..b004a3faab5 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerResult.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerResult.java @@ -23,12 +23,12 @@ import reactor.core.publisher.Mono; import org.springframework.core.MethodParameter; import org.springframework.core.ResolvableType; -import org.springframework.ui.ConcurrentModel; import org.springframework.ui.Model; import org.springframework.util.Assert; +import org.springframework.web.reactive.result.method.BindingContext; /** - * Represent the result of the invocation of a handler. + * Represent the result of the invocation of a handler or a handler method. * * @author Rossen Stoyanchev * @since 5.0 @@ -37,12 +37,11 @@ public class HandlerResult { private final Object handler; - @SuppressWarnings("OptionalUsedAsFieldOrParameterType") - private final Optional returnValue; + private final Object returnValue; private final ResolvableType returnType; - private final Model model; + private final BindingContext bindingContext; private Function> exceptionHandler; @@ -62,15 +61,17 @@ public class HandlerResult { * @param handler the handler that handled the request * @param returnValue the return value from the handler possibly {@code null} * @param returnType the return value type - * @param model the model used for request handling + * @param context the binding context used for request handling */ - public HandlerResult(Object handler, Object returnValue, MethodParameter returnType, Model model) { + public HandlerResult(Object handler, Object returnValue, MethodParameter returnType, + BindingContext context) { + Assert.notNull(handler, "'handler' is required"); Assert.notNull(returnType, "'returnType' is required"); this.handler = handler; - this.returnValue = Optional.ofNullable(returnValue); + this.returnValue = returnValue; this.returnType = ResolvableType.forMethodParameter(returnType); - this.model = (model != null ? model : new ConcurrentModel()); + this.bindingContext = (context != null ? context : new BindingContext()); } @@ -85,7 +86,7 @@ public class HandlerResult { * Return the value returned from the handler wrapped as {@link Optional}. */ public Optional getReturnValue() { - return this.returnValue; + return Optional.ofNullable(this.returnValue); } /** @@ -104,11 +105,18 @@ public class HandlerResult { } /** - * Return the model used during request handling with attributes that may be - * used to render HTML templates with. + * Return the BindingContext used for request handling. + */ + public BindingContext getBindingContext() { + return this.bindingContext; + } + + /** + * Return the model used for request handling. This is a shortcut for + * {@code getBindingContext().getModel()}. */ public Model getModel() { - return this.model; + return this.bindingContext.getModel(); } /** diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/InvocableHandlerMethod.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/InvocableHandlerMethod.java index bb091d4a7d3..d82f9faef8e 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/InvocableHandlerMethod.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/InvocableHandlerMethod.java @@ -32,8 +32,6 @@ import org.springframework.core.DefaultParameterNameDiscoverer; import org.springframework.core.GenericTypeResolver; import org.springframework.core.MethodParameter; import org.springframework.core.ParameterNameDiscoverer; -import org.springframework.ui.Model; -import org.springframework.ui.ModelMap; import org.springframework.util.ClassUtils; import org.springframework.util.ObjectUtils; import org.springframework.util.ReflectionUtils; @@ -102,9 +100,8 @@ public class InvocableHandlerMethod extends HandlerMethod { return resolveArguments(exchange, bindingContext, providedArgs).then(args -> { try { Object value = doInvoke(args); - Model model = bindingContext.getModel(); - HandlerResult handlerResult = new HandlerResult(this, value, getReturnType(), model); - return Mono.just(handlerResult); + HandlerResult result = new HandlerResult(this, value, getReturnType(), bindingContext); + return Mono.just(result); } catch (InvocationTargetException ex) { return Mono.error(ex.getTargetException()); diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandlerTests.java index fa83247de20..6924780ab58 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandlerTests.java @@ -37,11 +37,10 @@ import org.springframework.http.codec.HttpMessageWriter; import org.springframework.http.codec.ResourceHttpMessageWriter; import org.springframework.http.codec.json.Jackson2JsonEncoder; import org.springframework.http.codec.xml.Jaxb2XmlEncoder; +import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest; import org.springframework.mock.http.server.reactive.test.MockServerHttpResponse; -import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.stereotype.Controller; -import org.springframework.ui.ExtendedModelMap; import org.springframework.util.ObjectUtils; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseStatus; @@ -121,13 +120,13 @@ public class ResponseBodyResultHandlerTests { public void writeResponseStatus() throws NoSuchMethodException { Object controller = new TestRestController(); HandlerMethod hm = handlerMethod(controller, "handleToString"); - HandlerResult handlerResult = new HandlerResult(hm, null, hm.getReturnType(), new ExtendedModelMap()); + HandlerResult handlerResult = new HandlerResult(hm, null, hm.getReturnType()); StepVerifier.create(this.resultHandler.handleResult(this.exchange, handlerResult)).expectComplete().verify(); assertEquals(HttpStatus.NO_CONTENT, this.response.getStatusCode()); hm = handlerMethod(controller, "handleToMonoVoid"); - handlerResult = new HandlerResult(hm, null, hm.getReturnType(), new ExtendedModelMap()); + handlerResult = new HandlerResult(hm, null, hm.getReturnType()); StepVerifier.create(this.resultHandler.handleResult(this.exchange, handlerResult)).expectComplete().verify(); assertEquals(HttpStatus.CREATED, this.response.getStatusCode()); @@ -135,7 +134,7 @@ public class ResponseBodyResultHandlerTests { private void testSupports(Object controller, String method, boolean result) throws NoSuchMethodException { HandlerMethod hm = handlerMethod(controller, method); - HandlerResult handlerResult = new HandlerResult(hm, null, hm.getReturnType(), new ExtendedModelMap()); + HandlerResult handlerResult = new HandlerResult(hm, null, hm.getReturnType()); assertEquals(result, this.resultHandler.supports(handlerResult)); } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandlerTests.java index 1588e37bde1..176e8e8d8d1 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandlerTests.java @@ -25,6 +25,7 @@ import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.TreeMap; import org.junit.Before; import org.junit.Test; @@ -45,7 +46,7 @@ import org.springframework.http.MediaType; import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest; import org.springframework.mock.http.server.reactive.test.MockServerHttpResponse; -import org.springframework.ui.ExtendedModelMap; +import org.springframework.ui.ConcurrentModel; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.ResponseStatus; @@ -53,6 +54,7 @@ import org.springframework.web.reactive.HandlerResult; import org.springframework.web.reactive.accept.HeaderContentTypeResolver; import org.springframework.web.reactive.accept.RequestedContentTypeResolver; import org.springframework.web.reactive.result.ResolvableMethod; +import org.springframework.web.reactive.result.method.BindingContext; import org.springframework.web.server.NotAcceptableStatusException; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.adapter.DefaultServerWebExchange; @@ -61,7 +63,6 @@ import org.springframework.web.server.session.WebSessionManager; import static java.nio.charset.StandardCharsets.UTF_8; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; import static org.mockito.Mockito.mock; import static org.springframework.core.ResolvableType.forClass; import static org.springframework.core.ResolvableType.forClassWithGenerics; @@ -80,7 +81,7 @@ public class ViewResolutionResultHandlerTests { private ServerWebExchange exchange; - private Model model = new ExtendedModelMap(); + private final BindingContext bindingContext = new BindingContext(); @Before @@ -114,7 +115,7 @@ public class ViewResolutionResultHandlerTests { private void testSupports(ResolvableMethod resolvableMethod, boolean result) { ViewResolutionResultHandler resultHandler = resultHandler(mock(ViewResolver.class)); MethodParameter returnType = resolvableMethod.resolveReturnType(); - HandlerResult handlerResult = new HandlerResult(new Object(), null, returnType, this.model); + HandlerResult handlerResult = new HandlerResult(new Object(), null, returnType, this.bindingContext); assertEquals(result, resultHandler.supports(handlerResult)); } @@ -156,7 +157,7 @@ public class ViewResolutionResultHandlerTests { assertEquals(HttpStatus.PARTIAL_CONTENT, this.exchange.getResponse().getStatusCode()); returnType = forClass(Model.class); - returnValue = new ExtendedModelMap().addAttribute("name", "Joe"); + returnValue = new ConcurrentModel().addAttribute("name", "Joe"); testHandle("/account", returnType, returnValue, "account: {id=123, name=Joe}", resolver); returnType = forClass(Map.class); @@ -190,8 +191,8 @@ public class ViewResolutionResultHandlerTests { } private void testDefaultViewName(Object returnValue, ResolvableType type) throws URISyntaxException { - Model model = new ExtendedModelMap().addAttribute("id", "123"); - HandlerResult result = new HandlerResult(new Object(), returnValue, returnType(type), model); + this.bindingContext.getModel().addAttribute("id", "123"); + HandlerResult result = new HandlerResult(new Object(), returnValue, returnType(type), this.bindingContext); ViewResolutionResultHandler handler = resultHandler(new TestViewResolver("account")); this.request.setUri("/account"); @@ -210,8 +211,8 @@ public class ViewResolutionResultHandlerTests { @Test public void unresolvedViewName() throws Exception { String returnValue = "account"; - ResolvableType type = forClass(String.class); - HandlerResult result = new HandlerResult(new Object(), returnValue, returnType(type), this.model); + MethodParameter returnType = returnType(forClass(String.class)); + HandlerResult result = new HandlerResult(new Object(), returnValue, returnType, this.bindingContext); this.request.setUri("/path"); Mono mono = resultHandler().handleResult(this.exchange, result); @@ -225,8 +226,8 @@ public class ViewResolutionResultHandlerTests { @Test public void contentNegotiation() throws Exception { TestBean value = new TestBean("Joe"); - ResolvableType type = forClass(TestBean.class); - HandlerResult handlerResult = new HandlerResult(new Object(), value, returnType(type), this.model); + MethodParameter returnType = returnType(forClass(TestBean.class)); + HandlerResult handlerResult = new HandlerResult(new Object(), value, returnType, this.bindingContext); this.request.setHeader("Accept", "application/json"); this.request.setUri("/account"); @@ -244,8 +245,8 @@ public class ViewResolutionResultHandlerTests { @Test public void contentNegotiationWith406() throws Exception { TestBean value = new TestBean("Joe"); - ResolvableType type = forClass(TestBean.class); - HandlerResult handlerResult = new HandlerResult(new Object(), value, returnType(type), this.model); + MethodParameter returnType = returnType(forClass(TestBean.class)); + HandlerResult handlerResult = new HandlerResult(new Object(), value, returnType, this.bindingContext); this.request.setHeader("Accept", "application/json"); this.request.setUri("/account"); @@ -260,13 +261,13 @@ public class ViewResolutionResultHandlerTests { @Test public void modelWithAsyncAttributes() throws Exception { - Model model = new ExtendedModelMap(); - model.addAttribute("bean1", Mono.just(new TestBean("Bean1"))); - model.addAttribute("bean2", Single.just(new TestBean("Bean2"))); - model.addAttribute("empty", Mono.empty()); + this.bindingContext.getModel() + .addAttribute("bean1", Mono.just(new TestBean("Bean1"))) + .addAttribute("bean2", Single.just(new TestBean("Bean2"))) + .addAttribute("empty", Mono.empty()); ResolvableType type = forClass(void.class); - HandlerResult result = new HandlerResult(new Object(), null, returnType(type), model); + HandlerResult result = new HandlerResult(new Object(), null, returnType(type), this.bindingContext); ViewResolutionResultHandler handler = resultHandler(new TestViewResolver("account")); this.request.setUri("/account"); @@ -304,9 +305,11 @@ public class ViewResolutionResultHandlerTests { private void testHandle(String path, ResolvableMethod resolvableMethod, Object returnValue, String responseBody, ViewResolver... resolvers) throws URISyntaxException { - Model model = new ExtendedModelMap().addAttribute("id", "123"); + Model model = this.bindingContext.getModel(); + model.asMap().clear(); + model.addAttribute("id", "123"); MethodParameter returnType = resolvableMethod.resolveReturnType(); - HandlerResult result = new HandlerResult(new Object(), returnValue, returnType, model); + HandlerResult result = new HandlerResult(new Object(), returnValue, returnType, this.bindingContext); this.request.setUri(path); resultHandler(resolvers).handleResult(this.exchange, result).block(Duration.ofSeconds(5)); assertResponseBody(responseBody); @@ -375,12 +378,12 @@ public class ViewResolutionResultHandlerTests { @Override public Mono render(Map model, MediaType mediaType, ServerWebExchange exchange) { - String value = this.name + ": " + model.toString(); - assertNotNull(value); ServerHttpResponse response = exchange.getResponse(); if (mediaType != null) { response.getHeaders().setContentType(mediaType); } + model = new TreeMap<>(model); + String value = this.name + ": " + model.toString(); ByteBuffer byteBuffer = ByteBuffer.wrap(value.getBytes(UTF_8)); DataBuffer dataBuffer = new DefaultDataBufferFactory().wrap(byteBuffer); return response.writeWith(Flux.just(dataBuffer));