All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.springframework.web.servlet.view.ContentNegotiatingViewResolver Maven / Gradle / Ivy

There is a newer version: 6.1.6
Show newest version
/*
 * Copyright 2002-2021 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.web.servlet.view;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;

import jakarta.servlet.ServletContext;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
import org.springframework.http.MediaType;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.MimeTypeUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.HttpMediaTypeNotAcceptableException;
import org.springframework.web.accept.ContentNegotiationManager;
import org.springframework.web.accept.ContentNegotiationManagerFactoryBean;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.context.support.WebApplicationObjectSupport;
import org.springframework.web.servlet.HandlerMapping;
import org.springframework.web.servlet.SmartView;
import org.springframework.web.servlet.View;
import org.springframework.web.servlet.ViewResolver;

/**
 * Implementation of {@link ViewResolver} that resolves a view based on the request file name
 * or {@code Accept} header.
 *
 * 

The {@code ContentNegotiatingViewResolver} does not resolve views itself, but delegates to * other {@link ViewResolver ViewResolvers}. By default, these other view resolvers are picked up automatically * from the application context, though they can also be set explicitly by using the * {@link #setViewResolvers viewResolvers} property. Note that in order for this * view resolver to work properly, the {@link #setOrder order} property needs to be set to a higher * precedence than the others (the default is {@link Ordered#HIGHEST_PRECEDENCE}). * *

This view resolver uses the requested {@linkplain MediaType media type} to select a suitable * {@link View} for a request. The requested media type is determined through the configured * {@link ContentNegotiationManager}. Once the requested media type has been determined, this resolver * queries each delegate view resolver for a {@link View} and determines if the requested media type * is {@linkplain MediaType#includes(MediaType) compatible} with the view's * {@linkplain View#getContentType() content type}). The most compatible view is returned. * *

Additionally, this view resolver exposes the {@link #setDefaultViews(List) defaultViews} property, * allowing you to override the views provided by the view resolvers. Note that these default views are * offered as candidates, and still need have the content type requested (via file extension, parameter, * or {@code Accept} header, described above). * *

For example, if the request path is {@code /view.html}, this view resolver will look for a view * that has the {@code text/html} content type (based on the {@code html} file extension). A request * for {@code /view} with a {@code text/html} request {@code Accept} header has the same result. * * @author Arjen Poutsma * @author Juergen Hoeller * @author Rossen Stoyanchev * @since 3.0 * @see ViewResolver * @see InternalResourceViewResolver * @see BeanNameViewResolver */ public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport implements ViewResolver, Ordered, InitializingBean { @Nullable private ContentNegotiationManager contentNegotiationManager; private final ContentNegotiationManagerFactoryBean cnmFactoryBean = new ContentNegotiationManagerFactoryBean(); private boolean useNotAcceptableStatusCode = false; @Nullable private List defaultViews; @Nullable private List viewResolvers; private int order = Ordered.HIGHEST_PRECEDENCE; /** * Set the {@link ContentNegotiationManager} to use to determine requested media types. *

If not set, ContentNegotiationManager's default constructor will be used, * applying a {@link org.springframework.web.accept.HeaderContentNegotiationStrategy}. * @see ContentNegotiationManager#ContentNegotiationManager() */ public void setContentNegotiationManager(@Nullable ContentNegotiationManager contentNegotiationManager) { this.contentNegotiationManager = contentNegotiationManager; } /** * Return the {@link ContentNegotiationManager} to use to determine requested media types. * @since 4.1.9 */ @Nullable public ContentNegotiationManager getContentNegotiationManager() { return this.contentNegotiationManager; } /** * Indicate whether a {@link HttpServletResponse#SC_NOT_ACCEPTABLE 406 Not Acceptable} * status code should be returned if no suitable view can be found. *

Default is {@code false}, meaning that this view resolver returns {@code null} for * {@link #resolveViewName(String, Locale)} when an acceptable view cannot be found. * This will allow for view resolvers chaining. When this property is set to {@code true}, * {@link #resolveViewName(String, Locale)} will respond with a view that sets the * response status to {@code 406 Not Acceptable} instead. */ public void setUseNotAcceptableStatusCode(boolean useNotAcceptableStatusCode) { this.useNotAcceptableStatusCode = useNotAcceptableStatusCode; } /** * Whether to return HTTP Status 406 if no suitable is found. */ public boolean isUseNotAcceptableStatusCode() { return this.useNotAcceptableStatusCode; } /** * Set the default views to use when a more specific view can not be obtained * from the {@link ViewResolver} chain. */ public void setDefaultViews(List defaultViews) { this.defaultViews = defaultViews; } public List getDefaultViews() { return (this.defaultViews != null ? Collections.unmodifiableList(this.defaultViews) : Collections.emptyList()); } /** * Sets the view resolvers to be wrapped by this view resolver. *

If this property is not set, view resolvers will be detected automatically. */ public void setViewResolvers(List viewResolvers) { this.viewResolvers = viewResolvers; } public List getViewResolvers() { return (this.viewResolvers != null ? Collections.unmodifiableList(this.viewResolvers) : Collections.emptyList()); } public void setOrder(int order) { this.order = order; } @Override public int getOrder() { return this.order; } @Override protected void initServletContext(ServletContext servletContext) { Collection matchingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors(obtainApplicationContext(), ViewResolver.class).values(); if (this.viewResolvers == null) { this.viewResolvers = new ArrayList<>(matchingBeans.size()); for (ViewResolver viewResolver : matchingBeans) { if (this != viewResolver) { this.viewResolvers.add(viewResolver); } } } else { for (int i = 0; i < this.viewResolvers.size(); i++) { ViewResolver vr = this.viewResolvers.get(i); if (matchingBeans.contains(vr)) { continue; } String name = vr.getClass().getName() + i; obtainApplicationContext().getAutowireCapableBeanFactory().initializeBean(vr, name); } } AnnotationAwareOrderComparator.sort(this.viewResolvers); this.cnmFactoryBean.setServletContext(servletContext); } @Override public void afterPropertiesSet() { if (this.contentNegotiationManager == null) { this.contentNegotiationManager = this.cnmFactoryBean.build(); } if (this.viewResolvers == null || this.viewResolvers.isEmpty()) { logger.warn("No ViewResolvers configured"); } } @Override @Nullable public View resolveViewName(String viewName, Locale locale) throws Exception { RequestAttributes attrs = RequestContextHolder.getRequestAttributes(); Assert.state(attrs instanceof ServletRequestAttributes, "No current ServletRequestAttributes"); List requestedMediaTypes = getMediaTypes(((ServletRequestAttributes) attrs).getRequest()); if (requestedMediaTypes != null) { List candidateViews = getCandidateViews(viewName, locale, requestedMediaTypes); View bestView = getBestView(candidateViews, requestedMediaTypes, attrs); if (bestView != null) { return bestView; } } String mediaTypeInfo = logger.isDebugEnabled() && requestedMediaTypes != null ? " given " + requestedMediaTypes.toString() : ""; if (this.useNotAcceptableStatusCode) { if (logger.isDebugEnabled()) { logger.debug("Using 406 NOT_ACCEPTABLE" + mediaTypeInfo); } return NOT_ACCEPTABLE_VIEW; } else { logger.debug("View remains unresolved" + mediaTypeInfo); return null; } } /** * Determines the list of {@link MediaType} for the given {@link HttpServletRequest}. * @param request the current servlet request * @return the list of media types requested, if any */ @Nullable protected List getMediaTypes(HttpServletRequest request) { Assert.state(this.contentNegotiationManager != null, "No ContentNegotiationManager set"); try { ServletWebRequest webRequest = new ServletWebRequest(request); List acceptableMediaTypes = this.contentNegotiationManager.resolveMediaTypes(webRequest); List producibleMediaTypes = getProducibleMediaTypes(request); Set compatibleMediaTypes = new LinkedHashSet<>(); for (MediaType acceptable : acceptableMediaTypes) { for (MediaType producible : producibleMediaTypes) { if (acceptable.isCompatibleWith(producible)) { compatibleMediaTypes.add(getMostSpecificMediaType(acceptable, producible)); } } } List selectedMediaTypes = new ArrayList<>(compatibleMediaTypes); MimeTypeUtils.sortBySpecificity(selectedMediaTypes); return selectedMediaTypes; } catch (HttpMediaTypeNotAcceptableException ex) { if (logger.isDebugEnabled()) { logger.debug(ex.getMessage()); } return null; } } @SuppressWarnings("unchecked") private List getProducibleMediaTypes(HttpServletRequest request) { Set mediaTypes = (Set) request.getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE); if (!CollectionUtils.isEmpty(mediaTypes)) { return new ArrayList<>(mediaTypes); } else { return Collections.singletonList(MediaType.ALL); } } /** * Return the more specific of the acceptable and the producible media types * with the q-value of the former. */ private MediaType getMostSpecificMediaType(MediaType acceptType, MediaType produceType) { produceType = produceType.copyQualityValue(acceptType); if (acceptType.isLessSpecific(produceType)) { return produceType; } else { return acceptType; } } private List getCandidateViews(String viewName, Locale locale, List requestedMediaTypes) throws Exception { List candidateViews = new ArrayList<>(); if (this.viewResolvers != null) { Assert.state(this.contentNegotiationManager != null, "No ContentNegotiationManager set"); for (ViewResolver viewResolver : this.viewResolvers) { View view = viewResolver.resolveViewName(viewName, locale); if (view != null) { candidateViews.add(view); } for (MediaType requestedMediaType : requestedMediaTypes) { List extensions = this.contentNegotiationManager.resolveFileExtensions(requestedMediaType); for (String extension : extensions) { String viewNameWithExtension = viewName + '.' + extension; view = viewResolver.resolveViewName(viewNameWithExtension, locale); if (view != null) { candidateViews.add(view); } } } } } if (!CollectionUtils.isEmpty(this.defaultViews)) { candidateViews.addAll(this.defaultViews); } return candidateViews; } @Nullable private View getBestView(List candidateViews, List requestedMediaTypes, RequestAttributes attrs) { for (View candidateView : candidateViews) { if (candidateView instanceof SmartView smartView) { if (smartView.isRedirectView()) { return candidateView; } } } for (MediaType mediaType : requestedMediaTypes) { for (View candidateView : candidateViews) { if (StringUtils.hasText(candidateView.getContentType())) { MediaType candidateContentType = MediaType.parseMediaType(candidateView.getContentType()); if (mediaType.isCompatibleWith(candidateContentType)) { mediaType = mediaType.removeQualityValue(); if (logger.isDebugEnabled()) { logger.debug("Selected '" + mediaType + "' given " + requestedMediaTypes); } attrs.setAttribute(View.SELECTED_CONTENT_TYPE, mediaType, RequestAttributes.SCOPE_REQUEST); return candidateView; } } } } return null; } private static final View NOT_ACCEPTABLE_VIEW = new View() { @Override @Nullable public String getContentType() { return null; } @Override public void render(@Nullable Map model, HttpServletRequest request, HttpServletResponse response) { response.setStatus(HttpServletResponse.SC_NOT_ACCEPTABLE); } }; }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy