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 -> {
 | 
			
		||||
		return returnValueMono
 | 
			
		||||
				.otherwiseIfEmpty(exchange.isNotModified() ? Mono.empty() : NO_VALUE_MONO)
 | 
			
		||||
				.then(returnValue -> {
 | 
			
		||||
 | 
			
		||||
					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);
 | 
			
		||||
 | 
			
		||||
					Mono<List<View>> viewsMono;
 | 
			
		||||
					Model model = result.getModel();
 | 
			
		||||
					Locale locale = Locale.getDefault(); // TODO
 | 
			
		||||
 | 
			
		||||
					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 {
 | 
			
		||||
				// Should not happen
 | 
			
		||||
				return Mono.error(new IllegalStateException("Unexpected view type"));
 | 
			
		||||
						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));
 | 
			
		||||
				});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private boolean isViewNameOrReference(ResolvableType elementType, HandlerResult result) {
 | 
			
		||||
		Class<?> clazz = elementType.getRawClass();
 | 
			
		||||
		return (View.class.isAssignableFrom(clazz) ||
 | 
			
		||||
				(CharSequence.class.isAssignableFrom(clazz) && !hasModelAttributeAnnotation(result)));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	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"));
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * 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());
 | 
			
		||||
	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 + "'.");
 | 
			
		||||
					}
 | 
			
		||||
		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;
 | 
			
		||||
					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();
 | 
			
		||||
		}
 | 
			
		||||
		// TODO: Conventions does not deal with async wrappers
 | 
			
		||||
		return ClassUtils.getShortNameAsProperty(returnValueType);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private Mono<Void> resolveAsyncAttributes(Map<String, Object> model) {
 | 
			
		||||
 | 
			
		||||
		List<String> names = new ArrayList<>();
 | 
			
		||||
		List<Mono<Object>> valueMonos = new ArrayList<>();
 | 
			
		||||
 | 
			
		||||
		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 (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 {
 | 
			
		||||
			Method method = returnType.getMethod();
 | 
			
		||||
			Class<?> containingClass = returnType.getContainingClass();
 | 
			
		||||
			Class<?> resolvedType = GenericTypeResolver.resolveReturnType(method, containingClass);
 | 
			
		||||
			return Conventions.getVariableNameForReturnType(method, resolvedType, returnValue);
 | 
			
		||||
							model.remove(names.get(i));
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
					return NO_VALUE;
 | 
			
		||||
				})
 | 
			
		||||
				.then();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private Mono<? extends Void> resolveAndRender(String viewName, Locale locale,
 | 
			
		||||
			Map<String, ?> model, ServerWebExchange exchange) {
 | 
			
		||||
 | 
			
		||||
		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<MediaType> producibleTypes = getProducibleMediaTypes(views);
 | 
			
		||||
					MediaType bestMediaType = selectMediaType(exchange, () -> producibleTypes);
 | 
			
		||||
	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 supported : view.getSupportedMediaTypes()) {
 | 
			
		||||
								if (supported.isCompatibleWith(bestMediaType)) {
 | 
			
		||||
									return view.render(model, bestMediaType, exchange);
 | 
			
		||||
				for (MediaType mediaType : view.getSupportedMediaTypes()) {
 | 
			
		||||
					if (mediaType.isCompatibleWith(bestMediaType)) {
 | 
			
		||||
						return view.render(model, mediaType, exchange);
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		throw new NotAcceptableStatusException(mediaTypes);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
					return Mono.error(new NotAcceptableStatusException(producibleTypes));
 | 
			
		||||
				});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private List<MediaType> getProducibleMediaTypes(List<View> views) {
 | 
			
		||||
		List<MediaType> result = new ArrayList<>();
 | 
			
		||||
		views.forEach(view -> result.addAll(view.getSupportedMediaTypes()));
 | 
			
		||||
		return result;
 | 
			
		||||
	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