From 6abd4d5ff533fd8f41003029010665e7e8522021 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Mon, 7 Nov 2016 11:03:45 +0200 Subject: [PATCH] Async model attributes resolved before rendering Issue: SPR-14542 --- .../view/ViewResolutionResultHandler.java | 242 +++++++++--------- .../ViewResolutionResultHandlerTests.java | 153 ++++++----- 2 files changed, 207 insertions(+), 188 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandler.java index 570232d6470..0024d5ec262 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandler.java @@ -16,20 +16,18 @@ package org.springframework.web.reactive.result.view; -import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Optional; +import java.util.stream.Collectors; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import org.springframework.beans.BeanUtils; -import org.springframework.core.Conventions; -import org.springframework.core.GenericTypeResolver; import org.springframework.core.MethodParameter; import org.springframework.core.Ordered; import org.springframework.core.ReactiveAdapter; @@ -38,6 +36,7 @@ import org.springframework.core.ResolvableType; import org.springframework.core.annotation.AnnotationAwareOrderComparator; import org.springframework.http.MediaType; import org.springframework.ui.Model; +import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.reactive.HandlerResult; @@ -77,6 +76,11 @@ import org.springframework.web.util.HttpRequestPathHelper; public class ViewResolutionResultHandler extends AbstractHandlerResultHandler implements HandlerResultHandler, Ordered { + private static final Object NO_VALUE = new Object(); + + private static final Mono NO_VALUE_MONO = Mono.just(NO_VALUE); + + private final List viewResolvers = new ArrayList<>(4); private final List defaultViews = new ArrayList<>(4); @@ -172,89 +176,81 @@ public class ViewResolutionResultHandler extends AbstractHandlerResultHandler } @Override + @SuppressWarnings("unchecked") public Mono handleResult(ServerWebExchange exchange, HandlerResult result) { - Mono valueMono; + Mono returnValueMono; ResolvableType elementType; - ResolvableType returnType = result.getReturnType(); + ResolvableType parameterType = result.getReturnType(); Optional optional = result.getReturnValue(); - ReactiveAdapter adapter = getAdapterRegistry().getAdapterFrom(returnType.getRawClass(), optional); + ReactiveAdapter adapter = getAdapterRegistry().getAdapterFrom(parameterType.getRawClass(), optional); + if (adapter != null) { if (optional.isPresent()) { Mono converted = adapter.toMono(optional); - valueMono = converted.map(o -> o); + returnValueMono = converted.map(o -> o); } else { - valueMono = Mono.empty(); + returnValueMono = Mono.empty(); } elementType = adapter.getDescriptor().isNoValue() ? - ResolvableType.forClass(Void.class) : returnType.getGeneric(0); + ResolvableType.forClass(Void.class) : parameterType.getGeneric(0); } else { - valueMono = Mono.justOrEmpty(result.getReturnValue()); - elementType = returnType; + returnValueMono = Mono.justOrEmpty(result.getReturnValue()); + elementType = parameterType; } - Mono viewMono; - if (isViewNameOrReference(elementType, result)) { - Mono viewName = getDefaultViewNameMono(exchange, result); - viewMono = valueMono.otherwiseIfEmpty(viewName); - } - else { - viewMono = valueMono.map(value -> updateModel(value, result)) - .defaultIfEmpty(result.getModel()) - .then(model -> getDefaultViewNameMono(exchange, result)); - } - Map model = result.getModel().asMap(); - return viewMono.then(view -> { - updateResponseStatus(result.getReturnTypeSource(), exchange); - if (view instanceof View) { - return ((View) view).render(model, null, exchange); - } - else if (view instanceof CharSequence) { - String viewName = view.toString(); - Locale locale = Locale.getDefault(); // TODO - return resolveAndRender(viewName, locale, model, exchange); + return returnValueMono + .otherwiseIfEmpty(exchange.isNotModified() ? Mono.empty() : NO_VALUE_MONO) + .then(returnValue -> { - } - else { - // Should not happen - return Mono.error(new IllegalStateException("Unexpected view type")); - } - }); - } + updateResponseStatus(result.getReturnTypeSource(), exchange); - private boolean isViewNameOrReference(ResolvableType elementType, HandlerResult result) { - Class clazz = elementType.getRawClass(); - return (View.class.isAssignableFrom(clazz) || - (CharSequence.class.isAssignableFrom(clazz) && !hasModelAttributeAnnotation(result))); - } + Mono> viewsMono; + Model model = result.getModel(); + Locale locale = Locale.getDefault(); // TODO - private Mono getDefaultViewNameMono(ServerWebExchange exchange, HandlerResult result) { - if (exchange.isNotModified()) { - return Mono.empty(); - } - String defaultViewName = getDefaultViewName(result, exchange); - if (defaultViewName != null) { - return Mono.just(defaultViewName); - } - else { - return Mono.error(new IllegalStateException( - "Handler [" + result.getHandler() + "] " + - "neither returned a view name nor a View object")); - } + Class clazz = elementType.getRawClass(); + if (clazz == null) { + clazz = returnValue.getClass(); + } + + if (returnValue == NO_VALUE || Void.class.equals(clazz) || void.class.equals(clazz)) { + viewsMono = resolveViews(getDefaultViewName(result, exchange), locale); + } + else if (Model.class.isAssignableFrom(clazz)) { + model.addAllAttributes(((Model) returnValue).asMap()); + viewsMono = resolveViews(getDefaultViewName(result, exchange), locale); + } + else if (Map.class.isAssignableFrom(clazz)) { + model.addAllAttributes((Map) returnValue); + viewsMono = resolveViews(getDefaultViewName(result, exchange), locale); + } + else if (View.class.isAssignableFrom(clazz)) { + viewsMono = Mono.just(Collections.singletonList((View) returnValue)); + } + else if (CharSequence.class.isAssignableFrom(clazz) && !hasModelAttributeAnnotation(result)) { + viewsMono = resolveViews(returnValue.toString(), locale); + } + else { + String name = getNameForReturnValue(clazz, result.getReturnTypeSource()); + model.addAttribute(name, returnValue); + viewsMono = resolveViews(getDefaultViewName(result, exchange), locale); + } + + return resolveAsyncAttributes(model.asMap()) + .then(viewsMono) + .then(views -> render(views, model.asMap(), exchange)); + }); } /** - * Translate the given request into a default view name. This is useful when - * the application leaves the view name unspecified. - *

The default implementation strips the leading and trailing slash from - * the as well as any extension and uses that as the view name. - * @return the default view name to use; if {@code null} is returned - * processing will result in an IllegalStateException. + * Select a default view name when a controller leaves the view unspecified. + * The default implementation strips the leading and trailing slash from the + * as well as any extension and uses that as the view name. */ - @SuppressWarnings("UnusedParameters") protected String getDefaultViewName(HandlerResult result, ServerWebExchange exchange) { String path = this.pathHelper.getLookupPathForRequest(exchange); if (path.startsWith("/")) { @@ -266,79 +262,87 @@ public class ViewResolutionResultHandler extends AbstractHandlerResultHandler return StringUtils.stripFilenameExtension(path); } - @SuppressWarnings("unchecked") - private Object updateModel(Object value, HandlerResult result) { - if (value instanceof Model) { - result.getModel().addAllAttributes(((Model) value).asMap()); - } - else if (value instanceof Map) { - result.getModel().addAllAttributes((Map) value); - } - else { - MethodParameter returnType = result.getReturnTypeSource(); - String name = getNameForReturnValue(value, returnType); - result.getModel().addAttribute(name, value); - } - return value; + private Mono> resolveViews(String viewName, Locale locale) { + return Flux.fromIterable(getViewResolvers()) + .concatMap(resolver -> resolver.resolveViewName(viewName, locale)) + .collectList() + .map(views -> { + if (views.isEmpty()) { + throw new IllegalStateException( + "Could not resolve view with name '" + viewName + "'."); + } + views.addAll(getDefaultViews()); + return views; + }); } /** - * Derive the model attribute name for the given return value using one of: - *

    - *
  1. The method {@code ModelAttribute} annotation value - *
  2. The declared return type if it is more specific than {@code Object} - *
  3. The actual return value type - *
- * @param returnValue the value returned from a method invocation - * @param returnType the return type of the method - * @return the model name, never {@code null} nor empty + * Return the name of a model attribute return value based on the method + * {@code @ModelAttribute} annotation, if present, or derived from the type + * of the return value otherwise. */ - private static String getNameForReturnValue(Object returnValue, MethodParameter returnType) { + private String getNameForReturnValue(Class returnValueType, MethodParameter returnType) { ModelAttribute annotation = returnType.getMethodAnnotation(ModelAttribute.class); if (annotation != null && StringUtils.hasText(annotation.value())) { return annotation.value(); } - else { - Method method = returnType.getMethod(); - Class containingClass = returnType.getContainingClass(); - Class resolvedType = GenericTypeResolver.resolveReturnType(method, containingClass); - return Conventions.getVariableNameForReturnType(method, resolvedType, returnValue); - } + // TODO: Conventions does not deal with async wrappers + return ClassUtils.getShortNameAsProperty(returnValueType); } - private Mono resolveAndRender(String viewName, Locale locale, - Map model, ServerWebExchange exchange) { + private Mono resolveAsyncAttributes(Map model) { - return Flux.fromIterable(getViewResolvers()) - .concatMap(resolver -> resolver.resolveViewName(viewName, locale)) - .switchIfEmpty(Mono.error( - new IllegalStateException( - "Could not resolve view with name '" + viewName + "'."))) - .collectList() - .then(views -> { - views.addAll(getDefaultViews()); + List names = new ArrayList<>(); + List> valueMonos = new ArrayList<>(); - List producibleTypes = getProducibleMediaTypes(views); - MediaType bestMediaType = selectMediaType(exchange, () -> producibleTypes); + for (Map.Entry entry : model.entrySet()) { + ReactiveAdapter adapter = getAdapterRegistry().getAdapterFrom(null, entry.getValue()); + if (adapter != null) { + names.add(entry.getKey()); + valueMonos.add(adapter.toMono(entry.getValue()).defaultIfEmpty(NO_VALUE)); + } + } - if (bestMediaType != null) { - for (View view : views) { - for (MediaType supported : view.getSupportedMediaTypes()) { - if (supported.isCompatibleWith(bestMediaType)) { - return view.render(model, bestMediaType, exchange); - } - } + if (names.isEmpty()) { + return Mono.empty(); + } + + return Mono.when(valueMonos, + values -> { + for (int i=0; i < values.length; i++) { + if (values[i] != NO_VALUE) { + model.put(names.get(i), values[i]); + } + else { + model.remove(names.get(i)); } } - - return Mono.error(new NotAcceptableStatusException(producibleTypes)); - }); + return NO_VALUE; + }) + .then(); } - private List getProducibleMediaTypes(List views) { - List result = new ArrayList<>(); - views.forEach(view -> result.addAll(view.getSupportedMediaTypes())); - return result; + private Mono render(List views, Map model, + ServerWebExchange exchange) { + + List mediaTypes = getMediaTypes(views); + MediaType bestMediaType = selectMediaType(exchange, () -> mediaTypes); + if (bestMediaType != null) { + for (View view : views) { + for (MediaType mediaType : view.getSupportedMediaTypes()) { + if (mediaType.isCompatibleWith(bestMediaType)) { + return view.render(model, mediaType, exchange); + } + } + } + } + throw new NotAcceptableStatusException(mediaTypes); + } + + private List getMediaTypes(List views) { + return views.stream() + .flatMap(view -> view.getSupportedMediaTypes().stream()) + .collect(Collectors.toList()); } } 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 39bb5e5dade..1588e37bde1 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 @@ -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 resolvers = createResultHandler(resolver1, resolver2).getViewResolvers(); + List 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 mono = createResultHandler().handleResult(this.exchange, handlerResult); + Mono 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 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 defaultViews, ViewResolver... resolvers) { + private ViewResolutionResultHandler resultHandler(List defaultViews, ViewResolver... resolvers) { List 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 render(Map model, MediaType mediaType, - ServerWebExchange exchange) { + 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); } - 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 monoVoid() { return null; } + void voidMethod() {} + Single singleString() { return null; } Single singleView() { return null; }