Browse Source

Controller API for view rendering

Issue: SPR-15211
pull/1382/head
Rossen Stoyanchev 8 years ago
parent
commit
e4c62cc029
  1. 7
      spring-web/src/main/java/org/springframework/web/bind/annotation/RequestMapping.java
  2. 80
      spring-webflux/src/main/java/org/springframework/web/reactive/result/view/DefaultRendering.java
  3. 131
      spring-webflux/src/main/java/org/springframework/web/reactive/result/view/DefaultRenderingBuilder.java
  4. 163
      spring-webflux/src/main/java/org/springframework/web/reactive/result/view/Rendering.java
  5. 11
      spring-webflux/src/main/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandler.java
  6. 140
      spring-webflux/src/test/java/org/springframework/web/reactive/result/view/DefaultRenderingBuilderTests.java
  7. 25
      spring-webflux/src/test/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandlerTests.java
  8. 6
      src/docs/asciidoc/web/web-mvc.adoc

7
spring-web/src/main/java/org/springframework/web/bind/annotation/RequestMapping.java

@ -157,9 +157,10 @@ import org.springframework.core.annotation.AliasFor; @@ -157,9 +157,10 @@ import org.springframework.core.annotation.AliasFor;
*
* <p>The following return types are supported for handler methods:
* <ul>
* <li>{@code ModelAndView} object (from Servlet MVC),
* with the model implicitly enriched with command objects and the results
* of {@link ModelAttribute @ModelAttribute} annotated reference data accessor methods.
* <li>{@link org.springframework.web.servlet.ModelAndView} object (Spring MVC only),
* providing a view, model attributes, and optionally a response status.
* <li>{@link org.springframework.web.reactive.result.view.Rendering} object (Spring WebFlux only),
* providing a view, model attributes, and optionally a response status.
* <li>{@link org.springframework.ui.Model Model} object, with the view name implicitly
* determined through a {@link org.springframework.web.servlet.RequestToViewNameTranslator}
* and the model implicitly enriched with command objects and the results

80
spring-webflux/src/main/java/org/springframework/web/reactive/result/view/DefaultRendering.java

@ -0,0 +1,80 @@ @@ -0,0 +1,80 @@
/*
* 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.Collections;
import java.util.Map;
import java.util.Optional;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.ui.Model;
/**
* Default implementation of {@link Rendering}.
*
* @author Rossen Stoyanchev
* @since 5.0
*/
class DefaultRendering implements Rendering {
private static final HttpHeaders EMPTY_HEADERS = HttpHeaders.readOnlyHttpHeaders(new HttpHeaders());
private final Object view;
private final Map<String, Object> model;
private final HttpStatus status;
private final HttpHeaders headers;
DefaultRendering(Object view, Model model, HttpStatus status, HttpHeaders headers) {
this.view = view;
this.model = (model != null ? model.asMap() : Collections.emptyMap());
this.status = status;
this.headers = headers != null ? headers : EMPTY_HEADERS;
}
@Override
public Optional<Object> view() {
return Optional.ofNullable(this.view);
}
@Override
public Map<String, Object> modelAttributes() {
return this.model;
}
@Override
public Optional<HttpStatus> status() {
return Optional.ofNullable(this.status);
}
@Override
public HttpHeaders headers() {
return this.headers;
}
@Override
public String toString() {
return "Rendering[view=" + this.view + ", modelAttributes=" + this.model +
", status=" + this.status + ", headers=" + this.headers + "]";
}
}

131
spring-webflux/src/main/java/org/springframework/web/reactive/result/view/DefaultRenderingBuilder.java

@ -0,0 +1,131 @@ @@ -0,0 +1,131 @@
/*
* 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.Map;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.ui.ExtendedModelMap;
import org.springframework.ui.Model;
import org.springframework.util.Assert;
/**
* Default implementation of {@link Rendering.RedirectBuilder}.
*
* @author Rossen Stoyanchev
* @since 5.0
*/
class DefaultRenderingBuilder implements Rendering.RedirectBuilder {
private final Object view;
private Model model;
private HttpStatus status;
private HttpHeaders headers;
DefaultRenderingBuilder(Object view) {
this.view = view;
}
@Override
public DefaultRenderingBuilder modelAttribute(String name, Object value) {
initModel();
this.model.addAttribute(name, value);
return this;
}
private void initModel() {
if (this.model == null) {
this.model = new ExtendedModelMap();
}
}
@Override
public DefaultRenderingBuilder modelAttribute(Object value) {
initModel();
this.model.addAttribute(value);
return this;
}
@Override
public DefaultRenderingBuilder modelAttributes(Object... values) {
initModel();
this.model.addAllAttributes(Arrays.asList(values));
return this;
}
@Override
public DefaultRenderingBuilder model(Map<String, ?> 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);
}
}

163
spring-webflux/src/main/java/org/springframework/web/reactive/result/view/Rendering.java

@ -0,0 +1,163 @@ @@ -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.
*
* <p>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.
*
* <p>{@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<Object> view();
/**
* Return attributes to add to the model.
*/
Map<String, Object> modelAttributes();
/**
* Return the HTTP status to set the response to.
*/
Optional<HttpStatus> 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<B extends Builder<B>> {
/**
* 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<String, ?> 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<RedirectBuilder> {
/**
* Whether to the provided redirect URL should be prepended with the
* application context path (if any).
* <p>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.
* <p>By default this is set to {@code false}.
*
* @see RedirectView#setPropagateQuery(boolean)
*/
RedirectBuilder propagateQuery(boolean propagate);
}
}

11
spring-webflux/src/main/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandler.java

@ -159,7 +159,7 @@ public class ViewResolutionResultHandler extends HandlerResultHandlerSupport @@ -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 @@ -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);

140
spring-webflux/src/test/java/org/springframework/web/reactive/result/view/DefaultRenderingBuilderTests.java

@ -0,0 +1,140 @@ @@ -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<String, Object> map = new LinkedHashMap<>(2);
map.put("foo", foo);
map.put("bar", bar);
assertEquals(map, rendering.modelAttributes());
}
@Test
public void model() throws Exception {
Map<String, Object> 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 {}
}

25
spring-webflux/src/test/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandlerTests.java

@ -39,6 +39,7 @@ import org.springframework.core.Ordered; @@ -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 { @@ -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 { @@ -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 { @@ -429,6 +438,14 @@ public class ViewResolutionResultHandlerTests {
Long longAttribute() {
return null;
}
Rendering rendering() {
return null;
}
Mono<Rendering> monoRendering() {
return null;
}
}
}

6
src/docs/asciidoc/web/web-mvc.adoc

@ -1408,8 +1408,10 @@ of `java.util.Optional` in those cases is equivalent to having `required=false`. @@ -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.

Loading…
Cancel
Save