001/* 002 * Copyright 2002-2019 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * https://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.springframework.web.reactive.result.view; 018 019import java.util.ArrayList; 020import java.util.Collection; 021import java.util.Collections; 022import java.util.List; 023import java.util.Locale; 024import java.util.Map; 025import java.util.Optional; 026import java.util.stream.Collectors; 027 028import reactor.core.publisher.Flux; 029import reactor.core.publisher.Mono; 030 031import org.springframework.beans.BeanUtils; 032import org.springframework.context.i18n.LocaleContextHolder; 033import org.springframework.core.Conventions; 034import org.springframework.core.MethodParameter; 035import org.springframework.core.Ordered; 036import org.springframework.core.ReactiveAdapter; 037import org.springframework.core.ReactiveAdapterRegistry; 038import org.springframework.core.ResolvableType; 039import org.springframework.core.annotation.AnnotationAwareOrderComparator; 040import org.springframework.http.HttpStatus; 041import org.springframework.http.MediaType; 042import org.springframework.lang.Nullable; 043import org.springframework.ui.Model; 044import org.springframework.util.StringUtils; 045import org.springframework.validation.BindingResult; 046import org.springframework.web.bind.annotation.ModelAttribute; 047import org.springframework.web.bind.support.WebExchangeDataBinder; 048import org.springframework.web.reactive.BindingContext; 049import org.springframework.web.reactive.HandlerResult; 050import org.springframework.web.reactive.HandlerResultHandler; 051import org.springframework.web.reactive.accept.RequestedContentTypeResolver; 052import org.springframework.web.reactive.result.HandlerResultHandlerSupport; 053import org.springframework.web.server.NotAcceptableStatusException; 054import org.springframework.web.server.ServerWebExchange; 055 056/** 057 * {@code HandlerResultHandler} that encapsulates the view resolution algorithm 058 * supporting the following return types: 059 * <ul> 060 * <li>{@link Void} or no value -- default view name</li> 061 * <li>{@link String} -- view name unless {@code @ModelAttribute}-annotated 062 * <li>{@link View} -- View to render with 063 * <li>{@link Model} -- attributes to add to the model 064 * <li>{@link Map} -- attributes to add to the model 065 * <li>{@link Rendering} -- use case driven API for view resolution</li> 066 * <li>{@link ModelAttribute @ModelAttribute} -- attribute for the model 067 * <li>Non-simple value -- attribute for the model 068 * </ul> 069 * 070 * <p>A String-based view name is resolved through the configured 071 * {@link ViewResolver} instances into a {@link View} to use for rendering. 072 * If a view is left unspecified (e.g. by returning {@code null} or a 073 * model-related return value), a default view name is selected. 074 * 075 * <p>By default this resolver is ordered at {@link Ordered#LOWEST_PRECEDENCE} 076 * and generally needs to be late in the order since it interprets any String 077 * return value as a view name or any non-simple value type as a model attribute 078 * while other result handlers may interpret the same otherwise based on the 079 * presence of annotations, e.g. for {@code @ResponseBody}. 080 * 081 * @author Rossen Stoyanchev 082 * @since 5.0 083 */ 084public class ViewResolutionResultHandler extends HandlerResultHandlerSupport implements HandlerResultHandler, Ordered { 085 086 private static final Object NO_VALUE = new Object(); 087 088 private static final Mono<Object> NO_VALUE_MONO = Mono.just(NO_VALUE); 089 090 091 private final List<ViewResolver> viewResolvers = new ArrayList<>(4); 092 093 private final List<View> defaultViews = new ArrayList<>(4); 094 095 096 /** 097 * Basic constructor with a default {@link ReactiveAdapterRegistry}. 098 * @param viewResolvers the resolver to use 099 * @param contentTypeResolver to determine the requested content type 100 */ 101 public ViewResolutionResultHandler(List<ViewResolver> viewResolvers, 102 RequestedContentTypeResolver contentTypeResolver) { 103 104 this(viewResolvers, contentTypeResolver, ReactiveAdapterRegistry.getSharedInstance()); 105 } 106 107 /** 108 * Constructor with an {@link ReactiveAdapterRegistry} instance. 109 * @param viewResolvers the view resolver to use 110 * @param contentTypeResolver to determine the requested content type 111 * @param registry for adaptation to reactive types 112 */ 113 public ViewResolutionResultHandler(List<ViewResolver> viewResolvers, 114 RequestedContentTypeResolver contentTypeResolver, ReactiveAdapterRegistry registry) { 115 116 super(contentTypeResolver, registry); 117 this.viewResolvers.addAll(viewResolvers); 118 AnnotationAwareOrderComparator.sort(this.viewResolvers); 119 } 120 121 122 /** 123 * Return a read-only list of view resolvers. 124 */ 125 public List<ViewResolver> getViewResolvers() { 126 return Collections.unmodifiableList(this.viewResolvers); 127 } 128 129 /** 130 * Set the default views to consider always when resolving view names and 131 * trying to satisfy the best matching content type. 132 */ 133 public void setDefaultViews(@Nullable List<View> defaultViews) { 134 this.defaultViews.clear(); 135 if (defaultViews != null) { 136 this.defaultViews.addAll(defaultViews); 137 } 138 } 139 140 /** 141 * Return the configured default {@code View}'s. 142 */ 143 public List<View> getDefaultViews() { 144 return this.defaultViews; 145 } 146 147 148 @Override 149 public boolean supports(HandlerResult result) { 150 if (hasModelAnnotation(result.getReturnTypeSource())) { 151 return true; 152 } 153 154 Class<?> type = result.getReturnType().toClass(); 155 ReactiveAdapter adapter = getAdapter(result); 156 if (adapter != null) { 157 if (adapter.isNoValue()) { 158 return true; 159 } 160 type = result.getReturnType().getGeneric().toClass(); 161 } 162 163 return (CharSequence.class.isAssignableFrom(type) || 164 Rendering.class.isAssignableFrom(type) || 165 Model.class.isAssignableFrom(type) || 166 Map.class.isAssignableFrom(type) || 167 View.class.isAssignableFrom(type) || 168 !BeanUtils.isSimpleProperty(type)); 169 } 170 171 @Override 172 @SuppressWarnings("unchecked") 173 public Mono<Void> handleResult(ServerWebExchange exchange, HandlerResult result) { 174 Mono<Object> valueMono; 175 ResolvableType valueType; 176 ReactiveAdapter adapter = getAdapter(result); 177 178 if (adapter != null) { 179 if (adapter.isMultiValue()) { 180 throw new IllegalArgumentException( 181 "Multi-value reactive types not supported in view resolution: " + result.getReturnType()); 182 } 183 184 valueMono = (result.getReturnValue() != null ? 185 Mono.from(adapter.toPublisher(result.getReturnValue())) : Mono.empty()); 186 187 valueType = (adapter.isNoValue() ? ResolvableType.forClass(Void.class) : 188 result.getReturnType().getGeneric()); 189 } 190 else { 191 valueMono = Mono.justOrEmpty(result.getReturnValue()); 192 valueType = result.getReturnType(); 193 } 194 195 return valueMono 196 .switchIfEmpty(exchange.isNotModified() ? Mono.empty() : NO_VALUE_MONO) 197 .flatMap(returnValue -> { 198 199 Mono<List<View>> viewsMono; 200 Model model = result.getModel(); 201 MethodParameter parameter = result.getReturnTypeSource(); 202 Locale locale = LocaleContextHolder.getLocale(exchange.getLocaleContext()); 203 204 Class<?> clazz = valueType.toClass(); 205 if (clazz == Object.class) { 206 clazz = returnValue.getClass(); 207 } 208 209 if (returnValue == NO_VALUE || clazz == void.class || clazz == Void.class) { 210 viewsMono = resolveViews(getDefaultViewName(exchange), locale); 211 } 212 else if (CharSequence.class.isAssignableFrom(clazz) && !hasModelAnnotation(parameter)) { 213 viewsMono = resolveViews(returnValue.toString(), locale); 214 } 215 else if (Rendering.class.isAssignableFrom(clazz)) { 216 Rendering render = (Rendering) returnValue; 217 HttpStatus status = render.status(); 218 if (status != null) { 219 exchange.getResponse().setStatusCode(status); 220 } 221 exchange.getResponse().getHeaders().putAll(render.headers()); 222 model.addAllAttributes(render.modelAttributes()); 223 Object view = render.view(); 224 if (view == null) { 225 view = getDefaultViewName(exchange); 226 } 227 viewsMono = (view instanceof String ? resolveViews((String) view, locale) : 228 Mono.just(Collections.singletonList((View) view))); 229 } 230 else if (Model.class.isAssignableFrom(clazz)) { 231 model.addAllAttributes(((Model) returnValue).asMap()); 232 viewsMono = resolveViews(getDefaultViewName(exchange), locale); 233 } 234 else if (Map.class.isAssignableFrom(clazz) && !hasModelAnnotation(parameter)) { 235 model.addAllAttributes((Map<String, ?>) returnValue); 236 viewsMono = resolveViews(getDefaultViewName(exchange), locale); 237 } 238 else if (View.class.isAssignableFrom(clazz)) { 239 viewsMono = Mono.just(Collections.singletonList((View) returnValue)); 240 } 241 else { 242 String name = getNameForReturnValue(parameter); 243 model.addAttribute(name, returnValue); 244 viewsMono = resolveViews(getDefaultViewName(exchange), locale); 245 } 246 BindingContext bindingContext = result.getBindingContext(); 247 updateBindingResult(bindingContext, exchange); 248 return viewsMono.flatMap(views -> render(views, model.asMap(), bindingContext, exchange)); 249 }); 250 } 251 252 253 private boolean hasModelAnnotation(MethodParameter parameter) { 254 return parameter.hasMethodAnnotation(ModelAttribute.class); 255 } 256 257 /** 258 * Select a default view name when a controller did not specify it. 259 * Use the request path the leading and trailing slash stripped. 260 */ 261 private String getDefaultViewName(ServerWebExchange exchange) { 262 String path = exchange.getRequest().getPath().pathWithinApplication().value(); 263 if (path.startsWith("/")) { 264 path = path.substring(1); 265 } 266 if (path.endsWith("/")) { 267 path = path.substring(0, path.length() - 1); 268 } 269 return StringUtils.stripFilenameExtension(path); 270 } 271 272 private Mono<List<View>> resolveViews(String viewName, Locale locale) { 273 return Flux.fromIterable(getViewResolvers()) 274 .concatMap(resolver -> resolver.resolveViewName(viewName, locale)) 275 .collectList() 276 .map(views -> { 277 if (views.isEmpty()) { 278 throw new IllegalStateException( 279 "Could not resolve view with name '" + viewName + "'."); 280 } 281 views.addAll(getDefaultViews()); 282 return views; 283 }); 284 } 285 286 private String getNameForReturnValue(MethodParameter returnType) { 287 return Optional.ofNullable(returnType.getMethodAnnotation(ModelAttribute.class)) 288 .filter(ann -> StringUtils.hasText(ann.value())) 289 .map(ModelAttribute::value) 290 .orElseGet(() -> Conventions.getVariableNameForParameter(returnType)); 291 } 292 293 private void updateBindingResult(BindingContext context, ServerWebExchange exchange) { 294 Map<String, Object> model = context.getModel().asMap(); 295 for (Map.Entry<String, Object> entry : model.entrySet()) { 296 String name = entry.getKey(); 297 Object value = entry.getValue(); 298 if (isBindingCandidate(name, value)) { 299 if (!model.containsKey(BindingResult.MODEL_KEY_PREFIX + name)) { 300 WebExchangeDataBinder binder = context.createDataBinder(exchange, value, name); 301 model.put(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult()); 302 } 303 } 304 } 305 } 306 307 private boolean isBindingCandidate(String name, @Nullable Object value) { 308 return (!name.startsWith(BindingResult.MODEL_KEY_PREFIX) && value != null && 309 !value.getClass().isArray() && !(value instanceof Collection) && !(value instanceof Map) && 310 getAdapterRegistry().getAdapter(null, value) == null && 311 !BeanUtils.isSimpleValueType(value.getClass())); 312 } 313 314 private Mono<? extends Void> render(List<View> views, Map<String, Object> model, 315 BindingContext bindingContext, ServerWebExchange exchange) { 316 317 for (View view : views) { 318 if (view.isRedirectView()) { 319 return renderWith(view, model, null, exchange, bindingContext); 320 } 321 } 322 List<MediaType> mediaTypes = getMediaTypes(views); 323 MediaType bestMediaType = selectMediaType(exchange, () -> mediaTypes); 324 if (bestMediaType != null) { 325 for (View view : views) { 326 for (MediaType mediaType : view.getSupportedMediaTypes()) { 327 if (mediaType.isCompatibleWith(bestMediaType)) { 328 return renderWith(view, model, mediaType, exchange, bindingContext); 329 } 330 } 331 } 332 } 333 throw new NotAcceptableStatusException(mediaTypes); 334 } 335 336 private Mono<? extends Void> renderWith(View view, Map<String, Object> model, 337 @Nullable MediaType mediaType, ServerWebExchange exchange, BindingContext bindingContext) { 338 339 exchange.getAttributes().put(View.BINDING_CONTEXT_ATTRIBUTE, bindingContext); 340 return view.render(model, mediaType, exchange) 341 .doOnTerminate(() -> exchange.getAttributes().remove(View.BINDING_CONTEXT_ATTRIBUTE)); 342 } 343 344 private List<MediaType> getMediaTypes(List<View> views) { 345 return views.stream() 346 .flatMap(view -> view.getSupportedMediaTypes().stream()) 347 .collect(Collectors.toList()); 348 } 349 350}