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}