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

@ -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<Object> NO_VALUE_MONO = Mono.just(NO_VALUE);
private final List<ViewResolver> viewResolvers = new ArrayList<>(4);
private final List<View> defaultViews = new ArrayList<>(4);
@ -172,89 +176,81 @@ public class ViewResolutionResultHandler extends AbstractHandlerResultHandler
}
@Override
@SuppressWarnings("unchecked")
public Mono<Void> handleResult(ServerWebExchange exchange, HandlerResult result) {
Mono<Object> valueMono;
Mono<Object> returnValueMono;
ResolvableType elementType;
ResolvableType returnType = result.getReturnType();
ResolvableType parameterType = result.getReturnType();
Optional<Object> 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<Object> viewMono;
if (isViewNameOrReference(elementType, result)) {
Mono<Object> 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<String, ?> 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<List<View>> viewsMono;
Model model = result.getModel();
Locale locale = Locale.getDefault(); // TODO
private Mono<Object> 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<String, ?>) 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.
* <p>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<String, ?>) value);
}
else {
MethodParameter returnType = result.getReturnTypeSource();
String name = getNameForReturnValue(value, returnType);
result.getModel().addAttribute(name, value);
}
return value;
private Mono<List<View>> 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:
* <ol>
* <li>The method {@code ModelAttribute} annotation value
* <li>The declared return type if it is more specific than {@code Object}
* <li>The actual return value type
* </ol>
* @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<? extends Void> resolveAndRender(String viewName, Locale locale,
Map<String, ?> model, ServerWebExchange exchange) {
private Mono<Void> resolveAsyncAttributes(Map<String, Object> 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<String> names = new ArrayList<>();
List<Mono<Object>> valueMonos = new ArrayList<>();
List<MediaType> producibleTypes = getProducibleMediaTypes(views);
MediaType bestMediaType = selectMediaType(exchange, () -> producibleTypes);
for (Map.Entry<String, ?> 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<MediaType> getProducibleMediaTypes(List<View> views) {
List<MediaType> result = new ArrayList<>();
views.forEach(view -> result.addAll(view.getSupportedMediaTypes()));
return result;
private Mono<? extends Void> render(List<View> views, Map<String, Object> model,
ServerWebExchange exchange) {
List<MediaType> 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<MediaType> getMediaTypes(List<View> views) {
return views.stream()
.flatMap(view -> view.getSupportedMediaTypes().stream())
.collect(Collectors.toList());
}
}

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; }