001/*
002 * Copyright 2002-2017 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.servlet.view;
018
019import java.util.ArrayList;
020import java.util.Collection;
021import java.util.Collections;
022import java.util.LinkedHashSet;
023import java.util.List;
024import java.util.Locale;
025import java.util.Map;
026import java.util.Set;
027import javax.servlet.ServletContext;
028import javax.servlet.http.HttpServletRequest;
029import javax.servlet.http.HttpServletResponse;
030
031import org.springframework.beans.factory.BeanFactoryUtils;
032import org.springframework.beans.factory.InitializingBean;
033import org.springframework.core.Ordered;
034import org.springframework.core.annotation.AnnotationAwareOrderComparator;
035import org.springframework.http.MediaType;
036import org.springframework.util.Assert;
037import org.springframework.util.CollectionUtils;
038import org.springframework.util.StringUtils;
039import org.springframework.web.HttpMediaTypeNotAcceptableException;
040import org.springframework.web.accept.ContentNegotiationManager;
041import org.springframework.web.accept.ContentNegotiationManagerFactoryBean;
042import org.springframework.web.context.request.RequestAttributes;
043import org.springframework.web.context.request.RequestContextHolder;
044import org.springframework.web.context.request.ServletRequestAttributes;
045import org.springframework.web.context.request.ServletWebRequest;
046import org.springframework.web.context.support.WebApplicationObjectSupport;
047import org.springframework.web.servlet.HandlerMapping;
048import org.springframework.web.servlet.SmartView;
049import org.springframework.web.servlet.View;
050import org.springframework.web.servlet.ViewResolver;
051
052/**
053 * Implementation of {@link ViewResolver} that resolves a view based on the request file name
054 * or {@code Accept} header.
055 *
056 * <p>The {@code ContentNegotiatingViewResolver} does not resolve views itself, but delegates to
057 * other {@link ViewResolver}s. By default, these other view resolvers are picked up automatically
058 * from the application context, though they can also be set explicitly by using the
059 * {@link #setViewResolvers viewResolvers} property. <strong>Note</strong> that in order for this
060 * view resolver to work properly, the {@link #setOrder order} property needs to be set to a higher
061 * precedence than the others (the default is {@link Ordered#HIGHEST_PRECEDENCE}).
062 *
063 * <p>This view resolver uses the requested {@linkplain MediaType media type} to select a suitable
064 * {@link View} for a request. The requested media type is determined through the configured
065 * {@link ContentNegotiationManager}. Once the requested media type has been determined, this resolver
066 * queries each delegate view resolver for a {@link View} and determines if the requested media type
067 * is {@linkplain MediaType#includes(MediaType) compatible} with the view's
068 * {@linkplain View#getContentType() content type}). The most compatible view is returned.
069 *
070 * <p>Additionally, this view resolver exposes the {@link #setDefaultViews(List) defaultViews} property,
071 * allowing you to override the views provided by the view resolvers. Note that these default views are
072 * offered as candidates, and still need have the content type requested (via file extension, parameter,
073 * or {@code Accept} header, described above).
074 *
075 * <p>For example, if the request path is {@code /view.html}, this view resolver will look for a view
076 * that has the {@code text/html} content type (based on the {@code html} file extension). A request
077 * for {@code /view} with a {@code text/html} request {@code Accept} header has the same result.
078 *
079 * @author Arjen Poutsma
080 * @author Juergen Hoeller
081 * @author Rossen Stoyanchev
082 * @since 3.0
083 * @see ViewResolver
084 * @see InternalResourceViewResolver
085 * @see BeanNameViewResolver
086 */
087public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport
088                implements ViewResolver, Ordered, InitializingBean {
089
090        private ContentNegotiationManager contentNegotiationManager;
091
092        private final ContentNegotiationManagerFactoryBean cnmFactoryBean = new ContentNegotiationManagerFactoryBean();
093
094        private boolean useNotAcceptableStatusCode = false;
095
096        private List<View> defaultViews;
097
098        private List<ViewResolver> viewResolvers;
099
100        private int order = Ordered.HIGHEST_PRECEDENCE;
101
102
103        /**
104         * Set the {@link ContentNegotiationManager} to use to determine requested media types.
105         * <p>If not set, ContentNegotiationManager's default constructor will be used,
106         * applying a {@link org.springframework.web.accept.HeaderContentNegotiationStrategy}.
107         * @see ContentNegotiationManager#ContentNegotiationManager()
108         */
109        public void setContentNegotiationManager(ContentNegotiationManager contentNegotiationManager) {
110                this.contentNegotiationManager = contentNegotiationManager;
111        }
112
113        /**
114         * Return the {@link ContentNegotiationManager} to use to determine requested media types.
115         * @since 4.1.9
116         */
117        public ContentNegotiationManager getContentNegotiationManager() {
118                return this.contentNegotiationManager;
119        }
120
121        /**
122         * Indicate whether a {@link HttpServletResponse#SC_NOT_ACCEPTABLE 406 Not Acceptable}
123         * status code should be returned if no suitable view can be found.
124         * <p>Default is {@code false}, meaning that this view resolver returns {@code null} for
125         * {@link #resolveViewName(String, Locale)} when an acceptable view cannot be found.
126         * This will allow for view resolvers chaining. When this property is set to {@code true},
127         * {@link #resolveViewName(String, Locale)} will respond with a view that sets the
128         * response status to {@code 406 Not Acceptable} instead.
129         */
130        public void setUseNotAcceptableStatusCode(boolean useNotAcceptableStatusCode) {
131                this.useNotAcceptableStatusCode = useNotAcceptableStatusCode;
132        }
133
134        /**
135         * Whether to return HTTP Status 406 if no suitable is found.
136         */
137        public boolean isUseNotAcceptableStatusCode() {
138                return this.useNotAcceptableStatusCode;
139        }
140
141        /**
142         * Set the default views to use when a more specific view can not be obtained
143         * from the {@link ViewResolver} chain.
144         */
145        public void setDefaultViews(List<View> defaultViews) {
146                this.defaultViews = defaultViews;
147        }
148
149        public List<View> getDefaultViews() {
150                return Collections.unmodifiableList(this.defaultViews);
151        }
152
153        /**
154         * Sets the view resolvers to be wrapped by this view resolver.
155         * <p>If this property is not set, view resolvers will be detected automatically.
156         */
157        public void setViewResolvers(List<ViewResolver> viewResolvers) {
158                this.viewResolvers = viewResolvers;
159        }
160
161        public List<ViewResolver> getViewResolvers() {
162                return Collections.unmodifiableList(this.viewResolvers);
163        }
164
165        public void setOrder(int order) {
166                this.order = order;
167        }
168
169        @Override
170        public int getOrder() {
171                return this.order;
172        }
173
174
175        @Override
176        protected void initServletContext(ServletContext servletContext) {
177                Collection<ViewResolver> matchingBeans =
178                                BeanFactoryUtils.beansOfTypeIncludingAncestors(getApplicationContext(), ViewResolver.class).values();
179                if (this.viewResolvers == null) {
180                        this.viewResolvers = new ArrayList<ViewResolver>(matchingBeans.size());
181                        for (ViewResolver viewResolver : matchingBeans) {
182                                if (this != viewResolver) {
183                                        this.viewResolvers.add(viewResolver);
184                                }
185                        }
186                }
187                else {
188                        for (int i = 0; i < viewResolvers.size(); i++) {
189                                if (matchingBeans.contains(viewResolvers.get(i))) {
190                                        continue;
191                                }
192                                String name = viewResolvers.get(i).getClass().getName() + i;
193                                getApplicationContext().getAutowireCapableBeanFactory().initializeBean(viewResolvers.get(i), name);
194                        }
195
196                }
197                if (this.viewResolvers.isEmpty()) {
198                        logger.warn("Did not find any ViewResolvers to delegate to; please configure them using the " +
199                                        "'viewResolvers' property on the ContentNegotiatingViewResolver");
200                }
201                AnnotationAwareOrderComparator.sort(this.viewResolvers);
202                this.cnmFactoryBean.setServletContext(servletContext);
203        }
204
205        @Override
206        public void afterPropertiesSet() {
207                if (this.contentNegotiationManager == null) {
208                        this.cnmFactoryBean.afterPropertiesSet();
209                        this.contentNegotiationManager = this.cnmFactoryBean.getObject();
210                }
211        }
212
213
214        @Override
215        public View resolveViewName(String viewName, Locale locale) throws Exception {
216                RequestAttributes attrs = RequestContextHolder.getRequestAttributes();
217                Assert.state(attrs instanceof ServletRequestAttributes, "No current ServletRequestAttributes");
218                List<MediaType> requestedMediaTypes = getMediaTypes(((ServletRequestAttributes) attrs).getRequest());
219                if (requestedMediaTypes != null) {
220                        List<View> candidateViews = getCandidateViews(viewName, locale, requestedMediaTypes);
221                        View bestView = getBestView(candidateViews, requestedMediaTypes, attrs);
222                        if (bestView != null) {
223                                return bestView;
224                        }
225                }
226                if (this.useNotAcceptableStatusCode) {
227                        if (logger.isDebugEnabled()) {
228                                logger.debug("No acceptable view found; returning 406 (Not Acceptable) status code");
229                        }
230                        return NOT_ACCEPTABLE_VIEW;
231                }
232                else {
233                        logger.debug("No acceptable view found; returning null");
234                        return null;
235                }
236        }
237
238        /**
239         * Determines the list of {@link MediaType} for the given {@link HttpServletRequest}.
240         * @param request the current servlet request
241         * @return the list of media types requested, if any
242         */
243        protected List<MediaType> getMediaTypes(HttpServletRequest request) {
244                try {
245                        ServletWebRequest webRequest = new ServletWebRequest(request);
246
247                        List<MediaType> acceptableMediaTypes = this.contentNegotiationManager.resolveMediaTypes(webRequest);
248                        acceptableMediaTypes = (!acceptableMediaTypes.isEmpty() ? acceptableMediaTypes :
249                                        Collections.singletonList(MediaType.ALL));
250
251                        List<MediaType> producibleMediaTypes = getProducibleMediaTypes(request);
252                        Set<MediaType> compatibleMediaTypes = new LinkedHashSet<MediaType>();
253                        for (MediaType acceptable : acceptableMediaTypes) {
254                                for (MediaType producible : producibleMediaTypes) {
255                                        if (acceptable.isCompatibleWith(producible)) {
256                                                compatibleMediaTypes.add(getMostSpecificMediaType(acceptable, producible));
257                                        }
258                                }
259                        }
260                        List<MediaType> selectedMediaTypes = new ArrayList<MediaType>(compatibleMediaTypes);
261                        MediaType.sortBySpecificityAndQuality(selectedMediaTypes);
262                        if (logger.isDebugEnabled()) {
263                                logger.debug("Requested media types are " + selectedMediaTypes + " based on Accept header types " +
264                                                "and producible media types " + producibleMediaTypes + ")");
265                        }
266                        return selectedMediaTypes;
267                }
268                catch (HttpMediaTypeNotAcceptableException ex) {
269                        return null;
270                }
271        }
272
273        @SuppressWarnings("unchecked")
274        private List<MediaType> getProducibleMediaTypes(HttpServletRequest request) {
275                Set<MediaType> mediaTypes = (Set<MediaType>)
276                                request.getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);
277                if (!CollectionUtils.isEmpty(mediaTypes)) {
278                        return new ArrayList<MediaType>(mediaTypes);
279                }
280                else {
281                        return Collections.singletonList(MediaType.ALL);
282                }
283        }
284
285        /**
286         * Return the more specific of the acceptable and the producible media types
287         * with the q-value of the former.
288         */
289        private MediaType getMostSpecificMediaType(MediaType acceptType, MediaType produceType) {
290                produceType = produceType.copyQualityValue(acceptType);
291                return (MediaType.SPECIFICITY_COMPARATOR.compare(acceptType, produceType) < 0 ? acceptType : produceType);
292        }
293
294        private List<View> getCandidateViews(String viewName, Locale locale, List<MediaType> requestedMediaTypes)
295                        throws Exception {
296
297                List<View> candidateViews = new ArrayList<View>();
298                for (ViewResolver viewResolver : this.viewResolvers) {
299                        View view = viewResolver.resolveViewName(viewName, locale);
300                        if (view != null) {
301                                candidateViews.add(view);
302                        }
303                        for (MediaType requestedMediaType : requestedMediaTypes) {
304                                List<String> extensions = this.contentNegotiationManager.resolveFileExtensions(requestedMediaType);
305                                for (String extension : extensions) {
306                                        String viewNameWithExtension = viewName + '.' + extension;
307                                        view = viewResolver.resolveViewName(viewNameWithExtension, locale);
308                                        if (view != null) {
309                                                candidateViews.add(view);
310                                        }
311                                }
312                        }
313                }
314                if (!CollectionUtils.isEmpty(this.defaultViews)) {
315                        candidateViews.addAll(this.defaultViews);
316                }
317                return candidateViews;
318        }
319
320        private View getBestView(List<View> candidateViews, List<MediaType> requestedMediaTypes, RequestAttributes attrs) {
321                for (View candidateView : candidateViews) {
322                        if (candidateView instanceof SmartView) {
323                                SmartView smartView = (SmartView) candidateView;
324                                if (smartView.isRedirectView()) {
325                                        if (logger.isDebugEnabled()) {
326                                                logger.debug("Returning redirect view [" + candidateView + "]");
327                                        }
328                                        return candidateView;
329                                }
330                        }
331                }
332                for (MediaType mediaType : requestedMediaTypes) {
333                        for (View candidateView : candidateViews) {
334                                if (StringUtils.hasText(candidateView.getContentType())) {
335                                        MediaType candidateContentType = MediaType.parseMediaType(candidateView.getContentType());
336                                        if (mediaType.isCompatibleWith(candidateContentType)) {
337                                                if (logger.isDebugEnabled()) {
338                                                        logger.debug("Returning [" + candidateView + "] based on requested media type '" +
339                                                                        mediaType + "'");
340                                                }
341                                                attrs.setAttribute(View.SELECTED_CONTENT_TYPE, mediaType, RequestAttributes.SCOPE_REQUEST);
342                                                return candidateView;
343                                        }
344                                }
345                        }
346                }
347                return null;
348        }
349
350
351        private static final View NOT_ACCEPTABLE_VIEW = new View() {
352
353                @Override
354                public String getContentType() {
355                        return null;
356                }
357
358                @Override
359                public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) {
360                        response.setStatus(HttpServletResponse.SC_NOT_ACCEPTABLE);
361                }
362        };
363
364}