/* * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ 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 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.ResolvableType; import org.springframework.core.annotation.AnnotationAwareOrderComparator; import org.springframework.core.convert.ConversionService; import org.springframework.http.MediaType; import org.springframework.ui.Model; import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.reactive.HandlerResult; import org.springframework.web.reactive.HandlerResultHandler; import org.springframework.web.reactive.accept.HeaderContentTypeResolver; import org.springframework.web.reactive.accept.RequestedContentTypeResolver; import org.springframework.web.reactive.result.ContentNegotiatingResultHandlerSupport; import org.springframework.web.server.NotAcceptableStatusException; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.util.HttpRequestPathHelper; /** * {@code HandlerResultHandler} that encapsulates the view resolution algorithm * supporting the following return types: * <ul> * <li>String-based view name * <li>Reference to a {@link View} * <li>{@link Model} * <li>{@link Map} * <li>Return types annotated with {@code @ModelAttribute} * <li>{@link BeanUtils#isSimpleProperty Non-simple} return types are * treated as a model attribute * </ul> * * <p>A String-based view name is resolved through the configured * {@link ViewResolver} instances into a {@link View} to use for rendering. * If a view is left unspecified (e.g. by returning {@code null} or a * model-related return value), a default view name is selected. * * <p>By default this resolver is ordered at {@link Ordered#LOWEST_PRECEDENCE} * and generally needs to be late in the order since it interprets any String * return value as a view name while others may interpret the same otherwise * based on annotations (e.g. for {@code @ResponseBody}). * * @author Rossen Stoyanchev */ public class ViewResolutionResultHandler extends ContentNegotiatingResultHandlerSupport implements HandlerResultHandler, Ordered { private final List<ViewResolver> viewResolvers = new ArrayList<>(4); private final List<View> defaultViews = new ArrayList<>(4); private final HttpRequestPathHelper pathHelper = new HttpRequestPathHelper(); /** * Constructor with {@code ViewResolver}s and a {@code ConversionService} only * and creating a {@link HeaderContentTypeResolver}, i.e. using Accept header * to determine the requested content type. * @param resolvers the resolver to use * @param conversionService for converting other reactive types (e.g. rx.Single) to Mono */ public ViewResolutionResultHandler(List<ViewResolver> resolvers, ConversionService conversionService) { this(resolvers, conversionService, new HeaderContentTypeResolver()); } /** * Constructor with {@code ViewResolver}s tand a {@code ConversionService}. * @param resolvers the resolver to use * @param conversionService for converting other reactive types (e.g. rx.Single) to Mono * @param contentTypeResolver for resolving the requested content type */ public ViewResolutionResultHandler(List<ViewResolver> resolvers, ConversionService conversionService, RequestedContentTypeResolver contentTypeResolver) { super(conversionService, contentTypeResolver); this.viewResolvers.addAll(resolvers); AnnotationAwareOrderComparator.sort(this.viewResolvers); } /** * Return a read-only list of view resolvers. */ public List<ViewResolver> getViewResolvers() { return Collections.unmodifiableList(this.viewResolvers); } /** * Set the default views to consider always when resolving view names and * trying to satisfy the best matching content type. */ public void setDefaultViews(List<View> defaultViews) { this.defaultViews.clear(); if (defaultViews != null) { this.defaultViews.addAll(defaultViews); } } /** * Return the configured default {@code View}'s. */ public List<View> getDefaultViews() { return this.defaultViews; } @Override public boolean supports(HandlerResult result) { Class<?> clazz = result.getReturnType().getRawClass(); if (hasModelAttributeAnnotation(result)) { return true; } if (isSupportedType(clazz)) { return true; } if (getConversionService().canConvert(clazz, Mono.class)) { clazz = result.getReturnType().getGeneric(0).getRawClass(); return isSupportedType(clazz); } return false; } private boolean hasModelAttributeAnnotation(HandlerResult result) { MethodParameter returnType = result.getReturnTypeSource(); return returnType.hasMethodAnnotation(ModelAttribute.class); } private boolean isSupportedType(Class<?> clazz) { return (CharSequence.class.isAssignableFrom(clazz) || View.class.isAssignableFrom(clazz) || Model.class.isAssignableFrom(clazz) || Map.class.isAssignableFrom(clazz) || !BeanUtils.isSimpleProperty(clazz)); } @Override public Mono<Void> handleResult(ServerWebExchange exchange, HandlerResult result) { Mono<Object> valueMono; ResolvableType elementType; ResolvableType returnType = result.getReturnType(); if (getConversionService().canConvert(returnType.getRawClass(), Mono.class)) { Optional<Object> optionalValue = result.getReturnValue(); if (optionalValue.isPresent()) { Mono<?> converted = getConversionService().convert(optionalValue.get(), Mono.class); valueMono = converted.map(o -> o); } else { valueMono = Mono.empty(); } elementType = returnType.getGeneric(0); } else { valueMono = Mono.justOrEmpty(result.getReturnValue()); elementType = returnType; } 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)); } return viewMono.then(view -> { if (view instanceof View) { return ((View) view).render(result, null, exchange); } else if (view instanceof CharSequence) { String viewName = view.toString(); Locale locale = Locale.getDefault(); // TODO return resolveAndRender(viewName, locale, result, exchange); } else { // Should not happen return Mono.error(new IllegalStateException("Unexpected view type")); } }); } 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) { 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. */ @SuppressWarnings("UnusedParameters") protected String getDefaultViewName(HandlerResult result, ServerWebExchange exchange) { String path = this.pathHelper.getLookupPathForRequest(exchange); if (path.startsWith("/")) { path = path.substring(1); } if (path.endsWith("/")) { path = path.substring(0, path.length() - 1); } return StringUtils.stripFilenameExtension(path); } private Object updateModel(Object value, HandlerResult result) { if (value instanceof Model) { result.getModel().addAllAttributes(((Model) value).asMap()); } else if (value instanceof Map) { //noinspection unchecked result.getModel().addAllAttributes((Map<String, ?>) value); } else { MethodParameter returnType = result.getReturnTypeSource(); String name = getNameForReturnValue(value, returnType); result.getModel().addAttribute(name, value); } return value; } /** * 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 */ private static String getNameForReturnValue(Object returnValue, 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); } } private Mono<? extends Void> resolveAndRender(String viewName, Locale locale, HandlerResult result, 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); if (bestMediaType != null) { for (View view : views) { for (MediaType supported : view.getSupportedMediaTypes()) { if (supported.isCompatibleWith(bestMediaType)) { return view.render(result, bestMediaType, exchange); } } } } 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; } }