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}