Async model attributes resolved before rendering

Issue: SPR-14542
This commit is contained in:
Rossen Stoyanchev
2016-11-07 11:03:45 +02:00
parent d163240ed4
commit 6abd4d5ff5
2 changed files with 207 additions and 188 deletions

View File

@@ -18,7 +18,6 @@ package org.springframework.web.reactive.result.view;
import java.net.URISyntaxException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.Arrays;
import java.util.Collections;
@@ -40,7 +39,6 @@ import org.springframework.core.Ordered;
import org.springframework.core.ResolvableType;
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.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
@@ -61,21 +59,24 @@ import org.springframework.web.server.adapter.DefaultServerWebExchange;
import org.springframework.web.server.session.DefaultWebSessionManager;
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;
import static org.springframework.core.io.buffer.support.DataBufferTestUtils.dumpString;
import static org.springframework.http.MediaType.APPLICATION_JSON;
/**
* Unit tests for {@link ViewResolutionResultHandler}.
*
* @author Rossen Stoyanchev
*/
public class ViewResolutionResultHandlerTests {
private MockServerHttpRequest request;
private final MockServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, "/path");
private MockServerHttpResponse response = new MockServerHttpResponse();
private final MockServerHttpResponse response = new MockServerHttpResponse();
private ServerWebExchange exchange;
@@ -84,7 +85,6 @@ public class ViewResolutionResultHandlerTests {
@Before
public void setUp() throws Exception {
this.request = new MockServerHttpRequest(HttpMethod.GET, "/path");
WebSessionManager manager = new DefaultWebSessionManager();
this.exchange = new DefaultServerWebExchange(this.request, this.response, manager);
}
@@ -92,21 +92,30 @@ public class ViewResolutionResultHandlerTests {
@Test
public void supports() throws Exception {
testSupports(forClass(String.class), true);
testSupports(forClass(View.class), true);
testSupports(forClassWithGenerics(Mono.class, String.class), true);
testSupports(forClassWithGenerics(Mono.class, View.class), true);
testSupports(forClassWithGenerics(Single.class, String.class), true);
testSupports(forClassWithGenerics(Single.class, View.class), true);
testSupports(forClassWithGenerics(Mono.class, Void.class), true);
testSupports(forClass(Completable.class), true);
testSupports(forClass(Model.class), true);
testSupports(forClass(Map.class), true);
testSupports(forClass(TestBean.class), true);
testSupports(forClass(Integer.class), false);
testSupports(resolvableMethod().annotated(ModelAttribute.class), true);
}
testSupports(ResolvableType.forClass(String.class), true);
testSupports(ResolvableType.forClass(View.class), true);
testSupports(ResolvableType.forClassWithGenerics(Mono.class, String.class), true);
testSupports(ResolvableType.forClassWithGenerics(Mono.class, View.class), true);
testSupports(ResolvableType.forClassWithGenerics(Single.class, String.class), true);
testSupports(ResolvableType.forClassWithGenerics(Single.class, View.class), true);
testSupports(ResolvableType.forClassWithGenerics(Mono.class, Void.class), true);
testSupports(ResolvableType.forClass(Completable.class), true);
testSupports(ResolvableType.forClass(Model.class), true);
testSupports(ResolvableType.forClass(Map.class), true);
testSupports(ResolvableType.forClass(TestBean.class), true);
testSupports(ResolvableType.forClass(Integer.class), false);
private void testSupports(ResolvableType type, boolean result) {
testSupports(resolvableMethod().returning(type), result);
}
testSupports(ResolvableMethod.onClass(TestController.class).annotated(ModelAttribute.class), true);
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);
assertEquals(result, resultHandler.supports(handlerResult));
}
@Test
@@ -115,7 +124,7 @@ public class ViewResolutionResultHandlerTests {
TestViewResolver resolver2 = new TestViewResolver("profile");
resolver1.setOrder(2);
resolver2.setOrder(1);
List<ViewResolver> resolvers = createResultHandler(resolver1, resolver2).getViewResolvers();
List<ViewResolver> resolvers = resultHandler(resolver1, resolver2).getViewResolvers();
assertEquals(Arrays.asList(resolver2, resolver1), resolvers);
}
@@ -126,47 +135,47 @@ public class ViewResolutionResultHandlerTests {
ResolvableType returnType;
ViewResolver resolver = new TestViewResolver("account");
returnType = ResolvableType.forClass(View.class);
returnType = forClass(View.class);
returnValue = new TestView("account");
testHandle("/path", returnType, returnValue, "account: {id=123}");
assertEquals(HttpStatus.NO_CONTENT, this.exchange.getResponse().getStatusCode());
returnType = ResolvableType.forClassWithGenerics(Mono.class, View.class);
returnType = forClassWithGenerics(Mono.class, View.class);
returnValue = Mono.just(new TestView("account"));
testHandle("/path", returnType, returnValue, "account: {id=123}");
assertEquals(HttpStatus.SEE_OTHER, this.exchange.getResponse().getStatusCode());
returnType = ResolvableType.forClass(String.class);
returnType = forClass(String.class);
returnValue = "account";
testHandle("/path", returnType, returnValue, "account: {id=123}", resolver);
assertEquals(HttpStatus.CREATED, this.exchange.getResponse().getStatusCode());
returnType = ResolvableType.forClassWithGenerics(Mono.class, String.class);
returnType = forClassWithGenerics(Mono.class, String.class);
returnValue = Mono.just("account");
testHandle("/path", returnType, returnValue, "account: {id=123}", resolver);
assertEquals(HttpStatus.PARTIAL_CONTENT, this.exchange.getResponse().getStatusCode());
returnType = ResolvableType.forClass(Model.class);
returnType = forClass(Model.class);
returnValue = new ExtendedModelMap().addAttribute("name", "Joe");
testHandle("/account", returnType, returnValue, "account: {id=123, name=Joe}", resolver);
returnType = ResolvableType.forClass(Map.class);
returnType = forClass(Map.class);
returnValue = Collections.singletonMap("name", "Joe");
testHandle("/account", returnType, returnValue, "account: {id=123, name=Joe}", resolver);
returnType = ResolvableType.forClass(TestBean.class);
returnType = forClass(TestBean.class);
returnValue = new TestBean("Joe");
String responseBody = "account: {id=123, testBean=TestBean[name=Joe]}";
testHandle("/account", returnType, returnValue, responseBody, resolver);
testHandle("/account", ResolvableMethod.onClass(TestController.class).annotated(ModelAttribute.class),
testHandle("/account", resolvableMethod().annotated(ModelAttribute.class),
99L, "account: {id=123, num=99}", resolver);
}
@Test
public void handleWithMultipleResolvers() throws Exception {
Object returnValue = "profile";
ResolvableType returnType = ResolvableType.forClass(String.class);
ResolvableType returnType = forClass(String.class);
ViewResolver[] resolvers = {new TestViewResolver("account"), new TestViewResolver("profile")};
testHandle("/account", returnType, returnValue, "profile: {id=123}", resolvers);
@@ -174,51 +183,49 @@ public class ViewResolutionResultHandlerTests {
@Test
public void defaultViewName() throws Exception {
testDefaultViewName(null, ResolvableType.forClass(String.class));
testDefaultViewName(Mono.empty(), ResolvableType.forClassWithGenerics(Mono.class, String.class));
testDefaultViewName(Mono.empty(), ResolvableType.forClassWithGenerics(Mono.class, Void.class));
testDefaultViewName(Completable.complete(), ResolvableType.forClass(Completable.class));
testDefaultViewName(null, forClass(String.class));
testDefaultViewName(Mono.empty(), forClassWithGenerics(Mono.class, String.class));
testDefaultViewName(Mono.empty(), forClassWithGenerics(Mono.class, Void.class));
testDefaultViewName(Completable.complete(), forClass(Completable.class));
}
private void testDefaultViewName(Object returnValue, ResolvableType type)
throws URISyntaxException {
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);
ViewResolutionResultHandler handler = createResultHandler(new TestViewResolver("account"));
ViewResolutionResultHandler handler = resultHandler(new TestViewResolver("account"));
this.request.setUri("/account");
handler.handleResult(this.exchange, result).block(Duration.ofSeconds(5));
handler.handleResult(this.exchange, result).blockMillis(5000);
assertResponseBody("account: {id=123}");
this.request.setUri("/account/");
handler.handleResult(this.exchange, result).block(Duration.ofSeconds(5));
handler.handleResult(this.exchange, result).blockMillis(5000);
assertResponseBody("account: {id=123}");
this.request.setUri("/account.123");
handler.handleResult(this.exchange, result).block(Duration.ofSeconds(5));
handler.handleResult(this.exchange, result).blockMillis(5000);
assertResponseBody("account: {id=123}");
}
@Test
public void unresolvedViewName() throws Exception {
String returnValue = "account";
ResolvableType type = ResolvableType.forClass(String.class);
HandlerResult handlerResult = new HandlerResult(new Object(), returnValue, returnType(type), this.model);
ResolvableType type = forClass(String.class);
HandlerResult result = new HandlerResult(new Object(), returnValue, returnType(type), this.model);
this.request.setUri("/path");
Mono<Void> mono = createResultHandler().handleResult(this.exchange, handlerResult);
Mono<Void> mono = resultHandler().handleResult(this.exchange, result);
StepVerifier.create(mono)
.expectNextCount(0)
.expectErrorMatches(err -> err.getMessage().equals("Could not resolve view with name 'account'."))
.expectErrorMessage("Could not resolve view with name 'account'.")
.verify();
}
@Test
public void contentNegotiation() throws Exception {
TestBean value = new TestBean("Joe");
ResolvableType type = ResolvableType.forClass(TestBean.class);
ResolvableType type = forClass(TestBean.class);
HandlerResult handlerResult = new HandlerResult(new Object(), value, returnType(type), this.model);
this.request.setHeader("Accept", "application/json");
@@ -226,7 +233,7 @@ public class ViewResolutionResultHandlerTests {
TestView defaultView = new TestView("jsonView", APPLICATION_JSON);
createResultHandler(Collections.singletonList(defaultView), new TestViewResolver("account"))
resultHandler(Collections.singletonList(defaultView), new TestViewResolver("account"))
.handleResult(this.exchange, handlerResult)
.block(Duration.ofSeconds(5));
@@ -237,13 +244,13 @@ public class ViewResolutionResultHandlerTests {
@Test
public void contentNegotiationWith406() throws Exception {
TestBean value = new TestBean("Joe");
ResolvableType type = ResolvableType.forClass(TestBean.class);
ResolvableType type = forClass(TestBean.class);
HandlerResult handlerResult = new HandlerResult(new Object(), value, returnType(type), this.model);
this.request.setHeader("Accept", "application/json");
this.request.setUri("/account");
ViewResolutionResultHandler resultHandler = createResultHandler(new TestViewResolver("account"));
ViewResolutionResultHandler resultHandler = resultHandler(new TestViewResolver("account"));
Mono<Void> mono = resultHandler.handleResult(this.exchange, handlerResult);
StepVerifier.create(mono)
.expectNextCount(0)
@@ -251,16 +258,32 @@ public class ViewResolutionResultHandlerTests {
.verify();
}
@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());
ResolvableType type = forClass(void.class);
HandlerResult result = new HandlerResult(new Object(), null, returnType(type), model);
ViewResolutionResultHandler handler = resultHandler(new TestViewResolver("account"));
this.request.setUri("/account");
handler.handleResult(this.exchange, result).blockMillis(5000);
assertResponseBody("account: {bean1=TestBean[name=Bean1], bean2=TestBean[name=Bean2]}");
}
private MethodParameter returnType(ResolvableType type) {
return ResolvableMethod.onClass(TestController.class).returning(type).resolveReturnType();
return resolvableMethod().returning(type).resolveReturnType();
}
private ViewResolutionResultHandler createResultHandler(ViewResolver... resolvers) {
return createResultHandler(Collections.emptyList(), resolvers);
private ViewResolutionResultHandler resultHandler(ViewResolver... resolvers) {
return resultHandler(Collections.emptyList(), resolvers);
}
private ViewResolutionResultHandler createResultHandler(List<View> defaultViews, ViewResolver... resolvers) {
private ViewResolutionResultHandler resultHandler(List<View> defaultViews, ViewResolver... resolvers) {
List<ViewResolver> resolverList = Arrays.asList(resolvers);
RequestedContentTypeResolver contentTypeResolver = new HeaderContentTypeResolver();
ViewResolutionResultHandler handler = new ViewResolutionResultHandler(resolverList, contentTypeResolver);
@@ -268,22 +291,14 @@ public class ViewResolutionResultHandlerTests {
return handler;
}
private void testSupports(ResolvableType type, boolean result) {
testSupports(ResolvableMethod.onClass(TestController.class).returning(type), result);
}
private void testSupports(ResolvableMethod resolvableMethod, boolean result) {
ViewResolutionResultHandler resultHandler = createResultHandler(mock(ViewResolver.class));
MethodParameter returnType = resolvableMethod.resolveReturnType();
HandlerResult handlerResult = new HandlerResult(new Object(), null, returnType, this.model);
assertEquals(result, resultHandler.supports(handlerResult));
private ResolvableMethod resolvableMethod() {
return ResolvableMethod.onClass(TestController.class);
}
private void testHandle(String path, ResolvableType returnType, Object returnValue,
String responseBody, ViewResolver... resolvers) throws URISyntaxException {
testHandle(path, ResolvableMethod.onClass(TestController.class).returning(returnType),
returnValue, responseBody, resolvers);
testHandle(path, resolvableMethod().returning(returnType), returnValue, responseBody, resolvers);
}
private void testHandle(String path, ResolvableMethod resolvableMethod, Object returnValue,
@@ -293,14 +308,13 @@ public class ViewResolutionResultHandlerTests {
MethodParameter returnType = resolvableMethod.resolveReturnType();
HandlerResult result = new HandlerResult(new Object(), returnValue, returnType, model);
this.request.setUri(path);
createResultHandler(resolvers).handleResult(this.exchange, result).block(Duration.ofSeconds(5));
resultHandler(resolvers).handleResult(this.exchange, result).block(Duration.ofSeconds(5));
assertResponseBody(responseBody);
}
private void assertResponseBody(String responseBody) {
StepVerifier.create(this.response.getBody())
.consumeNextWith(buf -> assertEquals(responseBody,
DataBufferTestUtils.dumpString(buf, StandardCharsets.UTF_8)))
.consumeNextWith(buf -> assertEquals(responseBody, dumpString(buf, UTF_8)))
.expectComplete()
.verify();
}
@@ -360,15 +374,14 @@ public class ViewResolutionResultHandlerTests {
}
@Override
public Mono<Void> render(Map<String, ?> model, MediaType mediaType,
ServerWebExchange exchange) {
public Mono<Void> render(Map<String, ?> model, MediaType mediaType, ServerWebExchange exchange) {
String value = this.name + ": " + model.toString();
assertNotNull(value);
ServerHttpResponse response = exchange.getResponse();
if (mediaType != null) {
response.getHeaders().setContentType(mediaType);
}
ByteBuffer byteBuffer = ByteBuffer.wrap(value.getBytes(StandardCharsets.UTF_8));
ByteBuffer byteBuffer = ByteBuffer.wrap(value.getBytes(UTF_8));
DataBuffer dataBuffer = new DefaultDataBufferFactory().wrap(byteBuffer);
return response.writeWith(Flux.just(dataBuffer));
}
@@ -411,6 +424,8 @@ public class ViewResolutionResultHandlerTests {
Mono<Void> monoVoid() { return null; }
void voidMethod() {}
Single<String> singleString() { return null; }
Single<View> singleView() { return null; }