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.nio.charset.Charset; 020import java.nio.charset.StandardCharsets; 021import java.util.ArrayList; 022import java.util.Collection; 023import java.util.Collections; 024import java.util.List; 025import java.util.Map; 026import java.util.concurrent.ConcurrentHashMap; 027 028import org.apache.commons.logging.Log; 029import org.apache.commons.logging.LogFactory; 030import reactor.core.publisher.Flux; 031import reactor.core.publisher.Mono; 032 033import org.springframework.beans.BeanUtils; 034import org.springframework.beans.factory.BeanNameAware; 035import org.springframework.context.ApplicationContext; 036import org.springframework.context.ApplicationContextAware; 037import org.springframework.core.ReactiveAdapter; 038import org.springframework.core.ReactiveAdapterRegistry; 039import org.springframework.http.MediaType; 040import org.springframework.lang.Nullable; 041import org.springframework.util.Assert; 042import org.springframework.validation.BindingResult; 043import org.springframework.web.reactive.BindingContext; 044import org.springframework.web.server.ServerWebExchange; 045 046/** 047 * Base class for {@link View} implementations. 048 * 049 * @author Rossen Stoyanchev 050 * @author Sam Brannen 051 * @since 5.0 052 */ 053public abstract class AbstractView implements View, BeanNameAware, ApplicationContextAware { 054 055 /** Well-known name for the RequestDataValueProcessor in the bean factory. */ 056 public static final String REQUEST_DATA_VALUE_PROCESSOR_BEAN_NAME = "requestDataValueProcessor"; 057 058 059 /** Logger that is available to subclasses. */ 060 protected final Log logger = LogFactory.getLog(getClass()); 061 062 private final ReactiveAdapterRegistry adapterRegistry; 063 064 private final List<MediaType> mediaTypes = new ArrayList<>(4); 065 066 private Charset defaultCharset = StandardCharsets.UTF_8; 067 068 @Nullable 069 private String requestContextAttribute; 070 071 @Nullable 072 private String beanName; 073 074 @Nullable 075 private ApplicationContext applicationContext; 076 077 078 public AbstractView() { 079 this(ReactiveAdapterRegistry.getSharedInstance()); 080 } 081 082 public AbstractView(ReactiveAdapterRegistry reactiveAdapterRegistry) { 083 this.adapterRegistry = reactiveAdapterRegistry; 084 this.mediaTypes.add(ViewResolverSupport.DEFAULT_CONTENT_TYPE); 085 } 086 087 088 /** 089 * Set the supported media types for this view. 090 * <p>Default is {@code "text/html;charset=UTF-8"}. 091 */ 092 public void setSupportedMediaTypes(List<MediaType> supportedMediaTypes) { 093 Assert.notEmpty(supportedMediaTypes, "MediaType List must not be empty"); 094 this.mediaTypes.clear(); 095 this.mediaTypes.addAll(supportedMediaTypes); 096 } 097 098 /** 099 * Get the configured media types supported by this view. 100 */ 101 @Override 102 public List<MediaType> getSupportedMediaTypes() { 103 return this.mediaTypes; 104 } 105 106 /** 107 * Set the default charset for this view, used when the 108 * {@linkplain #setSupportedMediaTypes(List) content type} does not contain one. 109 * <p>Default is {@linkplain StandardCharsets#UTF_8 UTF 8}. 110 */ 111 public void setDefaultCharset(Charset defaultCharset) { 112 Assert.notNull(defaultCharset, "'defaultCharset' must not be null"); 113 this.defaultCharset = defaultCharset; 114 } 115 116 /** 117 * Get the default charset, used when the 118 * {@linkplain #setSupportedMediaTypes(List) content type} does not contain one. 119 */ 120 public Charset getDefaultCharset() { 121 return this.defaultCharset; 122 } 123 124 /** 125 * Set the name of the {@code RequestContext} attribute for this view. 126 * <p>Default is none ({@code null}). 127 */ 128 public void setRequestContextAttribute(@Nullable String requestContextAttribute) { 129 this.requestContextAttribute = requestContextAttribute; 130 } 131 132 /** 133 * Get the name of the {@code RequestContext} attribute for this view, if any. 134 */ 135 @Nullable 136 public String getRequestContextAttribute() { 137 return this.requestContextAttribute; 138 } 139 140 /** 141 * Set the view's name. Helpful for traceability. 142 * <p>Framework code must call this when constructing views. 143 */ 144 @Override 145 public void setBeanName(@Nullable String beanName) { 146 this.beanName = beanName; 147 } 148 149 /** 150 * Get the view's name. 151 * <p>Should never be {@code null} if the view was correctly configured. 152 */ 153 @Nullable 154 public String getBeanName() { 155 return this.beanName; 156 } 157 158 @Override 159 public void setApplicationContext(@Nullable ApplicationContext applicationContext) { 160 this.applicationContext = applicationContext; 161 } 162 163 @Nullable 164 public ApplicationContext getApplicationContext() { 165 return this.applicationContext; 166 } 167 168 /** 169 * Obtain the {@link ApplicationContext} for actual use. 170 * @return the {@code ApplicationContext} (never {@code null}) 171 * @throws IllegalStateException if the ApplicationContext cannot be obtained 172 * @see #getApplicationContext() 173 */ 174 protected final ApplicationContext obtainApplicationContext() { 175 ApplicationContext applicationContext = getApplicationContext(); 176 Assert.state(applicationContext != null, "No ApplicationContext"); 177 return applicationContext; 178 } 179 180 181 /** 182 * Prepare the model to render. 183 * @param model a map with attribute names as keys and corresponding model 184 * objects as values (the map can also be {@code null} in case of an empty model) 185 * @param contentType the content type selected to render with, which should 186 * match one of the {@link #getSupportedMediaTypes() supported media types} 187 * @param exchange the current exchange 188 * @return a {@code Mono} that represents when and if rendering succeeds 189 */ 190 @Override 191 public Mono<Void> render(@Nullable Map<String, ?> model, @Nullable MediaType contentType, 192 ServerWebExchange exchange) { 193 194 if (logger.isDebugEnabled()) { 195 logger.debug(exchange.getLogPrefix() + "View " + formatViewName() + 196 ", model " + (model != null ? model : Collections.emptyMap())); 197 } 198 199 if (contentType != null) { 200 exchange.getResponse().getHeaders().setContentType(contentType); 201 } 202 203 return getModelAttributes(model, exchange).flatMap(mergedModel -> { 204 // Expose RequestContext? 205 if (this.requestContextAttribute != null) { 206 mergedModel.put(this.requestContextAttribute, createRequestContext(exchange, mergedModel)); 207 } 208 return renderInternal(mergedModel, contentType, exchange); 209 }); 210 } 211 212 /** 213 * Prepare the model to use for rendering. 214 * <p>The default implementation creates a combined output Map that includes 215 * model as well as static attributes with the former taking precedence. 216 */ 217 protected Mono<Map<String, Object>> getModelAttributes( 218 @Nullable Map<String, ?> model, ServerWebExchange exchange) { 219 220 Map<String, Object> attributes; 221 if (model != null) { 222 attributes = new ConcurrentHashMap<>(model.size()); 223 for (Map.Entry<String, ?> entry : model.entrySet()) { 224 if (entry.getValue() != null) { 225 attributes.put(entry.getKey(), entry.getValue()); 226 } 227 } 228 } 229 else { 230 attributes = new ConcurrentHashMap<>(0); 231 } 232 233 //noinspection deprecation 234 return resolveAsyncAttributes(attributes) 235 .then(resolveAsyncAttributes(attributes, exchange)) 236 .doOnTerminate(() -> exchange.getAttributes().remove(BINDING_CONTEXT_ATTRIBUTE)) 237 .thenReturn(attributes); 238 } 239 240 /** 241 * Use the configured {@link ReactiveAdapterRegistry} to adapt asynchronous 242 * attributes to {@code Mono<T>} or {@code Mono<List<T>>} and then wait to 243 * resolve them into actual values. When the returned {@code Mono<Void>} 244 * completes, the asynchronous attributes in the model will have been 245 * replaced with their corresponding resolved values. 246 * @return result a {@code Mono} that completes when the model is ready 247 * @since 5.1.8 248 */ 249 protected Mono<Void> resolveAsyncAttributes(Map<String, Object> model, ServerWebExchange exchange) { 250 List<Mono<?>> asyncAttributes = null; 251 for (Map.Entry<String, ?> entry : model.entrySet()) { 252 Object value = entry.getValue(); 253 if (value == null) { 254 continue; 255 } 256 ReactiveAdapter adapter = this.adapterRegistry.getAdapter(null, value); 257 if (adapter != null) { 258 if (asyncAttributes == null) { 259 asyncAttributes = new ArrayList<>(); 260 } 261 String name = entry.getKey(); 262 if (adapter.isMultiValue()) { 263 asyncAttributes.add( 264 Flux.from(adapter.toPublisher(value)) 265 .collectList() 266 .doOnSuccess(result -> model.put(name, result))); 267 } 268 else { 269 asyncAttributes.add( 270 Mono.from(adapter.toPublisher(value)) 271 .doOnSuccess(result -> { 272 if (result != null) { 273 model.put(name, result); 274 addBindingResult(name, result, model, exchange); 275 } 276 else { 277 model.remove(name); 278 } 279 })); 280 } 281 } 282 } 283 return asyncAttributes != null ? Mono.when(asyncAttributes) : Mono.empty(); 284 } 285 286 private void addBindingResult(String name, Object value, Map<String, Object> model, ServerWebExchange exchange) { 287 BindingContext context = exchange.getAttribute(BINDING_CONTEXT_ATTRIBUTE); 288 if (context == null || value.getClass().isArray() || value instanceof Collection || 289 value instanceof Map || BeanUtils.isSimpleValueType(value.getClass())) { 290 return; 291 } 292 BindingResult result = context.createDataBinder(exchange, value, name).getBindingResult(); 293 model.put(BindingResult.MODEL_KEY_PREFIX + name, result); 294 } 295 296 /** 297 * Use the configured {@link ReactiveAdapterRegistry} to adapt asynchronous 298 * attributes to {@code Mono<T>} or {@code Mono<List<T>>} and then wait to 299 * resolve them into actual values. When the returned {@code Mono<Void>} 300 * completes, the asynchronous attributes in the model would have been 301 * replaced with their corresponding resolved values. 302 * @return result {@code Mono} that completes when the model is ready 303 * @deprecated as of 5.1.8 this method is still invoked but it is a no-op. 304 * Please use {@link #resolveAsyncAttributes(Map, ServerWebExchange)} 305 * instead. It is invoked after this one and does the actual work. 306 */ 307 @Deprecated 308 protected Mono<Void> resolveAsyncAttributes(Map<String, Object> model) { 309 return Mono.empty(); 310 } 311 312 /** 313 * Create a {@link RequestContext} to expose under the 314 * {@linkplain #setRequestContextAttribute specified attribute name}. 315 * <p>The default implementation creates a standard {@code RequestContext} 316 * instance for the given exchange and model. 317 * <p>Can be overridden in subclasses to create custom instances. 318 * @param exchange the current exchange 319 * @param model a combined output Map (never {@code null}), with dynamic values 320 * taking precedence over static attributes 321 * @return the {@code RequestContext} instance 322 * @see #setRequestContextAttribute 323 */ 324 protected RequestContext createRequestContext(ServerWebExchange exchange, Map<String, Object> model) { 325 return new RequestContext(exchange, model, obtainApplicationContext(), getRequestDataValueProcessor()); 326 } 327 328 /** 329 * Get the {@link RequestDataValueProcessor} to use. 330 * <p>The default implementation looks in the {@link #getApplicationContext() 331 * ApplicationContext} for a {@code RequestDataValueProcessor} bean with 332 * the name {@link #REQUEST_DATA_VALUE_PROCESSOR_BEAN_NAME}. 333 * @return the {@code RequestDataValueProcessor}, or {@code null} if there is 334 * none in the application context 335 */ 336 @Nullable 337 protected RequestDataValueProcessor getRequestDataValueProcessor() { 338 ApplicationContext context = getApplicationContext(); 339 if (context != null && context.containsBean(REQUEST_DATA_VALUE_PROCESSOR_BEAN_NAME)) { 340 return context.getBean(REQUEST_DATA_VALUE_PROCESSOR_BEAN_NAME, RequestDataValueProcessor.class); 341 } 342 return null; 343 } 344 345 /** 346 * Subclasses must implement this method to actually render the view. 347 * @param renderAttributes combined output Map (never {@code null}), 348 * with dynamic values taking precedence over static attributes 349 * @param contentType the content type selected to render with, which should 350 * match one of the {@linkplain #getSupportedMediaTypes() supported media types} 351 * @param exchange current exchange 352 * @return a {@code Mono} that represents when and if rendering succeeds 353 */ 354 protected abstract Mono<Void> renderInternal(Map<String, Object> renderAttributes, 355 @Nullable MediaType contentType, ServerWebExchange exchange); 356 357 358 @Override 359 public String toString() { 360 return getClass().getName() + ": " + formatViewName(); 361 } 362 363 protected String formatViewName() { 364 return (getBeanName() != null ? 365 "name '" + getBeanName() + "'" : "[" + getClass().getSimpleName() + "]"); 366 } 367 368}