Async model attributes resolved before rendering
Issue: SPR-14542
This commit is contained in:
parent
d163240ed4
commit
6abd4d5ff5
|
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
|
|
|||
Loading…
Reference in New Issue