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