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}