/* * Copyright 2002-2017 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.util.ArrayList; import java.util.Collection; 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.MethodParameter; import org.springframework.core.Ordered; import org.springframework.core.ReactiveAdapter; import org.springframework.core.ReactiveAdapterRegistry; 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.Assert; import org.springframework.util.StringUtils; import org.springframework.validation.BindingResult; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.support.WebExchangeDataBinder; import org.springframework.web.reactive.BindingContext; import org.springframework.web.reactive.HandlerResult; import org.springframework.web.reactive.HandlerResultHandler; import org.springframework.web.reactive.accept.RequestedContentTypeResolver; import org.springframework.web.reactive.result.HandlerResultHandlerSupport; import org.springframework.web.server.NotAcceptableStatusException; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.support.HttpRequestPathHelper; /** * {@code HandlerResultHandler} that encapsulates the view resolution algorithm * supporting the following return types: * <ul> * <li>{@link Void} or no value -- default view name</li> * <li>{@link String} -- view name unless {@code @ModelAttribute}-annotated * <li>{@link View} -- View to render with * <li>{@link Model} -- attributes to add to the model * <li>{@link Map} -- attributes to add to the model * <li>{@link ModelAttribute @ModelAttribute} -- attribute for the model * <li>Non-simple value -- attribute for the model * </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 or any non-simple value type as a model attribute * while other result handlers may interpret the same otherwise based on the * presence of annotations, e.g. for {@code @ResponseBody}. * * @author Rossen Stoyanchev * @since 5.0 */ public class ViewResolutionResultHandler extends HandlerResultHandlerSupport 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); private final HttpRequestPathHelper pathHelper = new HttpRequestPathHelper(); /** * Basic constructor with a default {@link ReactiveAdapterRegistry}. * @param viewResolvers the resolver to use * @param contentTypeResolver to determine the requested content type */ public ViewResolutionResultHandler(List<ViewResolver> viewResolvers, RequestedContentTypeResolver contentTypeResolver) { this(viewResolvers, contentTypeResolver, new ReactiveAdapterRegistry()); } /** * Constructor with an {@link ReactiveAdapterRegistry} instance. * @param viewResolvers the view resolver to use * @param contentTypeResolver to determine the requested content type * @param registry for adaptation to reactive types */ public ViewResolutionResultHandler(List<ViewResolver> viewResolvers, RequestedContentTypeResolver contentTypeResolver, ReactiveAdapterRegistry registry) { super(contentTypeResolver, registry); this.viewResolvers.addAll(viewResolvers); 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) { if (hasModelAnnotation(result.getReturnTypeSource())) { return true; } Class<?> type = result.getReturnType().getRawClass(); ReactiveAdapter adapter = getAdapter(result); if (adapter != null) { if (adapter.isNoValue()) { return true; } type = result.getReturnType().getGeneric(0).resolve(Object.class); } return (CharSequence.class.isAssignableFrom(type) || Rendering.class.isAssignableFrom(type) || Model.class.isAssignableFrom(type) || Map.class.isAssignableFrom(type) || void.class.equals(type) || View.class.isAssignableFrom(type) || !BeanUtils.isSimpleProperty(type)); } private boolean hasModelAnnotation(MethodParameter parameter) { return parameter.hasMethodAnnotation(ModelAttribute.class); } @Override @SuppressWarnings("unchecked") public Mono<Void> handleResult(ServerWebExchange exchange, HandlerResult result) { Mono<Object> valueMono; ResolvableType valueType; ReactiveAdapter adapter = getAdapter(result); if (adapter != null) { Assert.isTrue(!adapter.isMultiValue(), "Multi-value " + "reactive types not supported in view resolution: " + result.getReturnType()); valueMono = result.getReturnValue() .map(value -> Mono.from(adapter.toPublisher(value))) .orElse(Mono.empty()); valueType = adapter.isNoValue() ? ResolvableType.forClass(Void.class) : result.getReturnType().getGeneric(0); } else { valueMono = Mono.justOrEmpty(result.getReturnValue()); valueType = result.getReturnType(); } return valueMono .switchIfEmpty(exchange.isNotModified() ? Mono.empty() : NO_VALUE_MONO) .flatMap(returnValue -> { Mono<List<View>> viewsMono; Model model = result.getModel(); MethodParameter parameter = result.getReturnTypeSource(); List<Locale> locales = exchange.getRequest().getHeaders().getAcceptLanguageAsLocales(); Locale locale = locales.isEmpty() ? Locale.getDefault() : locales.get(0); Class<?> clazz = valueType.getRawClass(); if (clazz == null) { clazz = returnValue.getClass(); } if (returnValue == NO_VALUE || Void.class.equals(clazz) || void.class.equals(clazz)) { viewsMono = resolveViews(getDefaultViewName(exchange), locale); } else if (CharSequence.class.isAssignableFrom(clazz) && !hasModelAnnotation(parameter)) { viewsMono = resolveViews(returnValue.toString(), locale); } else if (Rendering.class.isAssignableFrom(clazz)) { Rendering render = (Rendering) returnValue; render.status().ifPresent(exchange.getResponse()::setStatusCode); exchange.getResponse().getHeaders().putAll(render.headers()); model.addAllAttributes(render.modelAttributes()); Object view = render.view().orElse(getDefaultViewName(exchange)); viewsMono = (view instanceof String ? resolveViews((String) view, locale) : Mono.just(Collections.singletonList((View) view))); } else if (Model.class.isAssignableFrom(clazz)) { model.addAllAttributes(((Model) returnValue).asMap()); viewsMono = resolveViews(getDefaultViewName(exchange), locale); } else if (Map.class.isAssignableFrom(clazz) && !hasModelAnnotation(parameter)) { model.addAllAttributes((Map<String, ?>) returnValue); viewsMono = resolveViews(getDefaultViewName(exchange), locale); } else if (View.class.isAssignableFrom(clazz)) { viewsMono = Mono.just(Collections.singletonList((View) returnValue)); } else { String name = getNameForReturnValue(parameter); model.addAttribute(name, returnValue); viewsMono = resolveViews(getDefaultViewName(exchange), locale); } updateBindingContext(result.getBindingContext(), exchange); return viewsMono.flatMap(views -> render(views, model.asMap(), exchange)); }); } /** * Select a default view name when a controller did not specify it. * Use the request path the leading and trailing slash stripped. */ private String getDefaultViewName(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 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; }); } private String getNameForReturnValue(MethodParameter returnType) { return Optional.ofNullable(returnType.getMethodAnnotation(ModelAttribute.class)) .filter(ann -> StringUtils.hasText(ann.value())) .map(ModelAttribute::value) .orElse(Conventions.getVariableNameForParameter(returnType)); } private void updateBindingContext(BindingContext context, ServerWebExchange exchange) { Map<String, Object> model = context.getModel().asMap(); model.keySet().stream() .filter(name -> isBindingCandidate(name, model.get(name))) .filter(name -> !model.containsKey(BindingResult.MODEL_KEY_PREFIX + name)) .forEach(name -> { WebExchangeDataBinder binder = context.createDataBinder(exchange, model.get(name), name); model.put(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult()); }); } private boolean isBindingCandidate(String name, Object value) { return !name.startsWith(BindingResult.MODEL_KEY_PREFIX) && value != null && !value.getClass().isArray() && !(value instanceof Collection) && !(value instanceof Map) && !BeanUtils.isSimpleValueType(value.getClass()); } 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()); } }