map) {
+ initModel();
+ this.model.addAllAttributes(map);
+ return this;
+ }
+
+ @Override
+ public DefaultRenderingBuilder status(HttpStatus status) {
+ this.status = status;
+ return this;
+ }
+
+ @Override
+ public DefaultRenderingBuilder header(String headerName, String... headerValues) {
+ initHeaders();
+ this.headers.put(headerName, Arrays.asList(headerValues));
+ return this;
+ }
+
+ @Override
+ public DefaultRenderingBuilder headers(HttpHeaders headers) {
+ initHeaders();
+ this.headers.putAll(headers);
+ return this;
+ }
+
+ private void initHeaders() {
+ if (this.headers == null) {
+ this.headers = new HttpHeaders();
+ }
+ }
+
+ @Override
+ public Rendering.RedirectBuilder contextRelative(boolean contextRelative) {
+ getRedirectView().setContextRelative(contextRelative);
+ return this;
+ }
+
+ @Override
+ public Rendering.RedirectBuilder propagateQuery(boolean propagate) {
+ getRedirectView().setPropagateQuery(propagate);
+ return this;
+ }
+
+ private RedirectView getRedirectView() {
+ Assert.isInstanceOf(RedirectView.class, this.view);
+ return (RedirectView) this.view;
+ }
+
+ @Override
+ public Rendering build() {
+ return new DefaultRendering(this.view, this.model, this.status, this.headers);
+ }
+
+}
diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/Rendering.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/Rendering.java
new file mode 100644
index 0000000000..de41b81e7e
--- /dev/null
+++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/Rendering.java
@@ -0,0 +1,163 @@
+/*
+ * Copyright 2002-2017 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.result.view;
+
+import java.util.Collection;
+import java.util.Map;
+import java.util.Optional;
+
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpStatus;
+import org.springframework.ui.Model;
+
+
+/**
+ * Public API for HTML rendering. Supported as a return value in Spring WebFlux
+ * controllers. Comparable to the use of {@code ModelAndView} as a return value
+ * in Spring MVC controllers.
+ *
+ * Controllers typically return a {@link String} view name and rely on the
+ * "implicit" model which can also be injected into the controller method.
+ * Or controllers may return model attribute(s) and rely on a default view name
+ * being selected based on the request path.
+ *
+ *
{@link Rendering} can be used to combine a view name with model attributes,
+ * set the HTTP status or headers, and for other more advanced options around
+ * redirect scenarios.
+ *
+ * @author Rossen Stoyanchev
+ * @since 5.0
+ */
+public interface Rendering {
+
+ /**
+ * Return the selected {@link String} view name or {@link View} object.
+ */
+ Optional view();
+
+ /**
+ * Return attributes to add to the model.
+ */
+ Map modelAttributes();
+
+ /**
+ * Return the HTTP status to set the response to.
+ */
+ Optional status();
+
+ /**
+ * Return headers to add to the response.
+ */
+ HttpHeaders headers();
+
+
+ /**
+ * Create a new builder for response rendering based on the given view name.
+ * @param name the view name to be resolved to a {@link View}
+ * @return the builder
+ */
+ static Builder> view(String name) {
+ return new DefaultRenderingBuilder(name);
+ }
+
+ /**
+ * Create a new builder for a redirect through a {@link RedirectView}.
+ * @param url the redirect URL
+ * @return the builder
+ */
+ static RedirectBuilder redirectTo(String url) {
+ return new DefaultRenderingBuilder(new RedirectView(url));
+ }
+
+
+ /**
+ * Defines a builder for {@link Rendering}.
+ */
+ interface Builder> {
+
+ /**
+ * Add the given model attribute with the supplied name.
+ * @see Model#addAttribute(String, Object)
+ */
+ B modelAttribute(String name, Object value);
+
+ /**
+ * Add an attribute to the model using a
+ * {@link org.springframework.core.Conventions#getVariableName generated name}.
+ * @see Model#addAttribute(Object)
+ */
+ B modelAttribute(Object value);
+
+ /**
+ * Add all given attributes to the model using
+ * {@link org.springframework.core.Conventions#getVariableName generated names}.
+ * @see Model#addAllAttributes(Collection)
+ */
+ B modelAttributes(Object... values);
+
+ /**
+ * Add the given attributes to the model.
+ * @see Model#addAllAttributes(Map)
+ */
+ B model(Map map);
+
+ /**
+ * Specify the status to use for the response.
+ */
+ B status(HttpStatus status);
+
+ /**
+ * Specify a header to add to the response.
+ */
+ B header(String headerName, String... headerValues);
+
+ /**
+ * Specify headers to add to the response.
+ */
+ B headers(HttpHeaders headers);
+
+ /**
+ * Builder the {@link Rendering} instance.
+ */
+ Rendering build();
+ }
+
+
+ /**
+ * Extends {@link Builder} with extra options for redirect scenarios.
+ */
+ interface RedirectBuilder extends Builder {
+
+ /**
+ * Whether to the provided redirect URL should be prepended with the
+ * application context path (if any).
+ * By default this is set to {@code true}.
+ *
+ * @see RedirectView#setContextRelative(boolean)
+ */
+ RedirectBuilder contextRelative(boolean contextRelative);
+
+ /**
+ * Whether to append the query string of the current URL to the target
+ * redirect URL or not.
+ *
By default this is set to {@code false}.
+ *
+ * @see RedirectView#setPropagateQuery(boolean)
+ */
+ RedirectBuilder propagateQuery(boolean propagate);
+ }
+
+}
diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandler.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandler.java
index 445116d557..1f59bd55cf 100644
--- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandler.java
+++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandler.java
@@ -159,7 +159,7 @@ public class ViewResolutionResultHandler extends HandlerResultHandlerSupport
}
return (CharSequence.class.isAssignableFrom(type) || View.class.isAssignableFrom(type) ||
Model.class.isAssignableFrom(type) || Map.class.isAssignableFrom(type) ||
- !BeanUtils.isSimpleProperty(type));
+ Rendering.class.isAssignableFrom(type) || !BeanUtils.isSimpleProperty(type));
}
private boolean hasModelAnnotation(MethodParameter parameter) {
@@ -224,6 +224,15 @@ public class ViewResolutionResultHandler extends HandlerResultHandlerSupport
else if (CharSequence.class.isAssignableFrom(clazz) && !hasModelAnnotation(parameter)) {
viewsMono = resolveViews(returnValue.toString(), locale);
}
+ else if (Rendering.class.isAssignableFrom(clazz)) {
+ Rendering render = (Rendering) returnValue;
+ render.status().ifPresent(exchange.getResponse()::setStatusCode);
+ exchange.getResponse().getHeaders().putAll(render.headers());
+ model.addAllAttributes(render.modelAttributes());
+ Object view = render.view().orElse(getDefaultViewName(exchange));
+ viewsMono = (view instanceof String ? resolveViews((String) view, locale) :
+ Mono.just(Collections.singletonList((View) view)));
+ }
else {
String name = getNameForReturnValue(clazz, parameter);
model.addAttribute(name, returnValue);
diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/DefaultRenderingBuilderTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/DefaultRenderingBuilderTests.java
new file mode 100644
index 0000000000..3767b541eb
--- /dev/null
+++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/DefaultRenderingBuilderTests.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright 2002-2017 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.result.view;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import org.junit.Test;
+
+import org.springframework.http.HttpHeaders;
+import org.springframework.ui.ExtendedModelMap;
+import org.springframework.ui.Model;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+
+/**
+ * Unit tests for {@link DefaultRenderingBuilder}.
+ * @author Rossen Stoyanchev
+ */
+public class DefaultRenderingBuilderTests {
+
+
+ @Test
+ public void defaultValues() {
+ Rendering rendering = Rendering.view("abc").build();
+
+ assertEquals("abc", rendering.view().orElse(null));
+ assertEquals(Collections.emptyMap(), rendering.modelAttributes());
+ assertNull(rendering.status().orElse(null));
+ assertEquals(0, rendering.headers().size());
+ }
+
+ @Test
+ public void defaultValuesForRedirect() throws Exception {
+ Rendering rendering = Rendering.redirectTo("abc").build();
+
+ Object view = rendering.view().orElse(null);
+ assertEquals(RedirectView.class, view.getClass());
+ assertEquals("abc", ((RedirectView) view).getUrl());
+ assertTrue(((RedirectView) view).isContextRelative());
+ assertFalse(((RedirectView) view).isPropagateQuery());
+ }
+
+
+ @Test
+ public void viewName() {
+ Rendering rendering = Rendering.view("foo").build();
+ assertEquals("foo", rendering.view().orElse(null));
+ }
+
+ @Test
+ public void modelAttribute() throws Exception {
+ Foo foo = new Foo();
+ Rendering rendering = Rendering.view("foo").modelAttribute(foo).build();
+
+ assertEquals(Collections.singletonMap("foo", foo), rendering.modelAttributes());
+ }
+
+ @Test
+ public void modelAttributes() throws Exception {
+ Foo foo = new Foo();
+ Bar bar = new Bar();
+ Rendering rendering = Rendering.view("foo").modelAttributes(foo, bar).build();
+
+ Map map = new LinkedHashMap<>(2);
+ map.put("foo", foo);
+ map.put("bar", bar);
+ assertEquals(map, rendering.modelAttributes());
+ }
+
+ @Test
+ public void model() throws Exception {
+ Map map = new LinkedHashMap<>();
+ map.put("foo", new Foo());
+ map.put("bar", new Bar());
+ Rendering rendering = Rendering.view("foo").model(map).build();
+
+ assertEquals(map, rendering.modelAttributes());
+ }
+
+ @Test
+ public void header() throws Exception {
+ Rendering rendering = Rendering.view("foo").header("foo", "bar").build();
+
+ assertEquals(1, rendering.headers().size());
+ assertEquals(Collections.singletonList("bar"), rendering.headers().get("foo"));
+ }
+
+ @Test
+ public void httpHeaders() throws Exception {
+ HttpHeaders headers = new HttpHeaders();
+ headers.add("foo", "bar");
+ Rendering rendering = Rendering.view("foo").headers(headers).build();
+
+ assertEquals(headers, rendering.headers());
+ }
+
+ @Test
+ public void redirectWithAbsoluteUrl() throws Exception {
+ Rendering rendering = Rendering.redirectTo("foo").contextRelative(false).build();
+
+ Object view = rendering.view().orElse(null);
+ assertEquals(RedirectView.class, view.getClass());
+ assertFalse(((RedirectView) view).isContextRelative());
+ }
+
+ @Test
+ public void redirectWithPropagateQuery() throws Exception {
+ Rendering rendering = Rendering.redirectTo("foo").propagateQuery(true).build();
+
+ Object view = rendering.view().orElse(null);
+ assertEquals(RedirectView.class, view.getClass());
+ assertTrue(((RedirectView) view).isPropagateQuery());
+ }
+
+
+ private static class Foo {}
+
+ private static class Bar {}
+
+}
diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandlerTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandlerTests.java
index 293d4f9233..934096866f 100644
--- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandlerTests.java
+++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandlerTests.java
@@ -39,6 +39,7 @@ import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.core.io.buffer.support.DataBufferTestUtils;
+import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.mock.http.server.reactive.test.MockServerWebExchange;
@@ -86,6 +87,8 @@ public class ViewResolutionResultHandlerTests {
testSupports(on(TestController.class).resolveReturnType(Model.class));
testSupports(on(TestController.class).resolveReturnType(Map.class));
testSupports(on(TestController.class).resolveReturnType(TestBean.class));
+ testSupports(on(TestController.class).resolveReturnType(Rendering.class));
+ testSupports(on(TestController.class).resolveReturnType(Mono.class, Rendering.class));
testSupports(on(TestController.class).annotPresent(ModelAttribute.class).resolveReturnType());
}
@@ -148,16 +151,22 @@ public class ViewResolutionResultHandlerTests {
returnType = on(TestController.class).resolveReturnType(TestBean.class);
returnValue = new TestBean("Joe");
- String responseBody = "account: {" +
- "id=123, " +
+ String responseBody = "account: {id=123, " +
"org.springframework.validation.BindingResult.testBean=" +
"org.springframework.validation.BeanPropertyBindingResult: 0 errors, " +
- "testBean=TestBean[name=Joe]" +
- "}";
+ "testBean=TestBean[name=Joe]}";
testHandle("/account", returnType, returnValue, responseBody, resolver);
returnType = on(TestController.class).annotPresent(ModelAttribute.class).resolveReturnType();
testHandle("/account", returnType, 99L, "account: {id=123, num=99}", resolver);
+
+ returnType = on(TestController.class).resolveReturnType(Rendering.class);
+ HttpStatus status = HttpStatus.UNPROCESSABLE_ENTITY;
+ returnValue = Rendering.view("account").modelAttribute("a", "a1").status(status).header("h", "h1").build();
+ String expected = "account: {a=a1, id=123}";
+ ServerWebExchange exchange = testHandle("/path", returnType, returnValue, expected, resolver);
+ assertEquals(status, exchange.getResponse().getStatusCode());
+ assertEquals("h1", exchange.getResponse().getHeaders().getFirst("h"));
}
@Test
@@ -429,6 +438,14 @@ public class ViewResolutionResultHandlerTests {
Long longAttribute() {
return null;
}
+
+ Rendering rendering() {
+ return null;
+ }
+
+ Mono monoRendering() {
+ return null;
+ }
}
}
diff --git a/src/docs/asciidoc/web/web-mvc.adoc b/src/docs/asciidoc/web/web-mvc.adoc
index 616ad6cc9f..daabe4862d 100644
--- a/src/docs/asciidoc/web/web-mvc.adoc
+++ b/src/docs/asciidoc/web/web-mvc.adoc
@@ -1408,8 +1408,10 @@ of `java.util.Optional` in those cases is equivalent to having `required=false`.
==== Supported method return types
The following are the supported return types:
-* `ModelAndView` object, with the model implicitly enriched with command objects and
- the results of `@ModelAttribute` annotated reference data accessor methods.
+* `ModelAndView` object (Spring MVC), providing a view, model attributes, and
+ optionally a response status.
+* `Rendering` object (Spring WebFlux), providing a view, model attributes, and
+ optionally a response status.
* `Model` object, with the view name implicitly determined through a
`RequestToViewNameTranslator` and the model implicitly enriched with command objects
and the results of `@ModelAttribute` annotated reference data accessor methods.