diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/view/ContentNegotiatingViewResolver.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/view/ContentNegotiatingViewResolver.java index 1043a3cb24c..1ffbb93e744 100644 --- a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/view/ContentNegotiatingViewResolver.java +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/view/ContentNegotiatingViewResolver.java @@ -91,66 +91,66 @@ import org.springframework.web.util.WebUtils; * text/html} request {@code Accept} header has the same result. * * @author Arjen Poutsma - * @author Rostislav Hristov + * @author Juergen Hoeller + * @since 3.0 * @see ViewResolver * @see InternalResourceViewResolver * @see BeanNameViewResolver - * @since 3.0 */ public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport implements ViewResolver, Ordered { + private static final String ACCEPT_HEADER = "Accept"; + private static final boolean jafPresent = ClassUtils.isPresent("javax.activation.FileTypeMap", ContentNegotiatingViewResolver.class.getClassLoader()); - private static final String ACCEPT_HEADER = "Accept"; + private static final UrlPathHelper urlPathHelper = new UrlPathHelper(); - private UrlPathHelper urlPathHelper = new UrlPathHelper(); + + private int order = Ordered.HIGHEST_PRECEDENCE; private boolean favorPathExtension = true; private boolean favorParameter = false; - private boolean ignoreAcceptHeader = false; - private String parameterName = "format"; - private int order = Ordered.HIGHEST_PRECEDENCE; + private boolean ignoreAcceptHeader = false; private ConcurrentMap mediaTypes = new ConcurrentHashMap(); private List defaultViews; + private MediaType defaultContentType; + private List viewResolvers; - private MediaType defaultContentType; public void setOrder(int order) { this.order = order; } public int getOrder() { - return order; + return this.order; } /** - * Indicates whether the extension of the request path should be used to determine the requested media type, in favor - * of looking at the {@code Accept} header. The default value is {@code true}. - * - *

For instance, when this flag is true (the default), a request for {@code /hotels.pdf} will result in - * an {@code AbstractPdfView} being resolved, while the {@code Accept} header can be the browser-defined {@code - * text/html,application/xhtml+xml}. + * Indicates whether the extension of the request path should be used to determine the requested media type, + * in favor of looking at the {@code Accept} header. The default value is {@code true}. + *

For instance, when this flag is true (the default), a request for {@code /hotels.pdf} + * will result in an {@code AbstractPdfView} being resolved, while the {@code Accept} header can be the + * browser-defined {@code text/html,application/xhtml+xml}. */ public void setFavorPathExtension(boolean favorPathExtension) { this.favorPathExtension = favorPathExtension; } /** - * Indicates whether a request parameter should be used to determine the requested media type, in favor of looking at - * the {@code Accept} header. The default value is {@code false}. - * - *

For instance, when this flag is true, a request for {@code /hotels?format=pdf} will result in an - * {@code AbstractPdfView} being resolved, while the {@code Accept} header can be the browser-defined {@code - * text/html,application/xhtml+xml}. + * Indicates whether a request parameter should be used to determine the requested media type, + * in favor of looking at the {@code Accept} header. The default value is {@code false}. + *

For instance, when this flag is true, a request for {@code /hotels?format=pdf} will result + * in an {@code AbstractPdfView} being resolved, while the {@code Accept} header can be the browser-defined + * {@code text/html,application/xhtml+xml}. */ public void setFavorParameter(boolean favorParameter) { this.favorParameter = favorParameter; @@ -166,10 +166,9 @@ public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport /** * Indicates whether the HTTP {@code Accept} header should be ignored. Default is {@code false}. - * - * If set to {@code true}, this view resolver will only refer to the file extension and/or paramter, as indicated by - * the {@link #setFavorPathExtension(boolean) favorPathExtension} and {@link #setFavorParameter(boolean) - * favorParameter} properties. + * If set to {@code true}, this view resolver will only refer to the file extension and/or paramter, + * as indicated by the {@link #setFavorPathExtension(boolean) favorPathExtension} and + * {@link #setFavorParameter(boolean) favorParameter} properties. */ public void setIgnoreAcceptHeader(boolean ignoreAcceptHeader) { this.ignoreAcceptHeader = ignoreAcceptHeader; @@ -177,9 +176,8 @@ public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport /** * Sets the mapping from file extensions to media types. - * - *

When this mapping is not set or when an extension is not present, this view resolver will fall back to using a - * {@link FileTypeMap} when the Java Action Framework is available. + *

When this mapping is not set or when an extension is not present, this view resolver + * will fall back to using a {@link FileTypeMap} when the Java Action Framework is available. */ public void setMediaTypes(Map mediaTypes) { Assert.notNull(mediaTypes, "'mediaTypes' must not be null"); @@ -190,13 +188,17 @@ public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport } } - /** Sets the default views to use when a more specific view can not be obtained from the {@link ViewResolver} chain. */ + /** + * Sets 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; } /** - * Sets the default content type. This content type will be used when file extension, parameter, nor {@code Accept} + * Sets the default content type. + *

This content type will be used when file extension, parameter, nor {@code Accept} * header define a content-type, either through being disabled or empty. */ public void setDefaultContentType(MediaType defaultContentType) { @@ -205,7 +207,6 @@ public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport /** * 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) { @@ -214,9 +215,9 @@ public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport @Override protected void initServletContext(ServletContext servletContext) { - if (viewResolvers == null) { - Map matchingBeans = BeanFactoryUtils - .beansOfTypeIncludingAncestors(getApplicationContext(), ViewResolver.class, true, false); + if (this.viewResolvers == null) { + Map matchingBeans = + BeanFactoryUtils.beansOfTypeIncludingAncestors(getApplicationContext(), ViewResolver.class); this.viewResolvers = new ArrayList(matchingBeans.size()); for (ViewResolver viewResolver : matchingBeans.values()) { if (this != viewResolver) { @@ -233,19 +234,16 @@ public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport /** * Determines the list of {@link MediaType} for the given {@link HttpServletRequest}. - * *

The default implementation invokes {@link #getMediaTypeFromFilename(String)} if {@linkplain * #setFavorPathExtension(boolean) favorPathExtension} property is true. If the property is - * false, or when a media type cannot be determined from the request path, this method will inspect the - * {@code Accept} header of the request. - * + * false, or when a media type cannot be determined from the request path, this method will + * inspect the {@code Accept} header of the request. *

This method can be overriden to provide a different algorithm. - * * @param request the current servlet request * @return the list of media types requested, if any */ protected List getMediaTypes(HttpServletRequest request) { - if (favorPathExtension) { + if (this.favorPathExtension) { String requestUri = urlPathHelper.getRequestUri(request); String filename = WebUtils.extractFullFilenameFromUrlPath(requestUri); MediaType mediaType = getMediaTypeFromFilename(filename); @@ -258,15 +256,14 @@ public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport return mediaTypes; } } - if (favorParameter) { - if (request.getParameter(parameterName) != null) { - String parameterValue = request.getParameter(parameterName); + if (this.favorParameter) { + if (request.getParameter(this.parameterName) != null) { + String parameterValue = request.getParameter(this.parameterName); MediaType mediaType = getMediaTypeFromParameter(parameterValue); if (mediaType != null) { if (logger.isDebugEnabled()) { - logger.debug( - "Requested media type is '" + mediaType + "' (based on parameter '" + parameterName + - "'='" + parameterValue + "')"); + logger.debug("Requested media type is '" + mediaType + "' (based on parameter '" + + this.parameterName + "'='" + parameterValue + "')"); } List mediaTypes = new ArrayList(); mediaTypes.add(mediaType); @@ -274,7 +271,7 @@ public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport } } } - if (!ignoreAcceptHeader) { + if (!this.ignoreAcceptHeader) { String acceptHeader = request.getHeader(ACCEPT_HEADER); if (StringUtils.hasText(acceptHeader)) { List mediaTypes = MediaType.parseMediaTypes(acceptHeader); @@ -284,8 +281,8 @@ public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport return mediaTypes; } } - if (defaultContentType != null) { - return Collections.singletonList(defaultContentType); + if (this.defaultContentType != null) { + return Collections.singletonList(this.defaultContentType); } else { return Collections.emptyList(); @@ -294,13 +291,10 @@ public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport /** * Determines the {@link MediaType} for the given filename. - * *

The default implementation will check the {@linkplain #setMediaTypes(Map) media types} property first for a * defined mapping. If not present, and if the Java Activation Framework can be found on the class path, it will call * {@link FileTypeMap#getContentType(String)} - * *

This method can be overriden to provide a different algorithm. - * * @param filename the current request file name (i.e. {@code hotels.html}) * @return the media type, if any */ @@ -310,11 +304,11 @@ public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport return null; } extension = extension.toLowerCase(Locale.ENGLISH); - MediaType mediaType = mediaTypes.get(extension); + MediaType mediaType = this.mediaTypes.get(extension); if (mediaType == null && jafPresent) { mediaType = ActivationMediaTypeFactory.getMediaType(filename); if (mediaType != null) { - mediaTypes.putIfAbsent(extension, mediaType); + this.mediaTypes.putIfAbsent(extension, mediaType); } } return mediaType; @@ -322,38 +316,39 @@ public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport /** * Determines the {@link MediaType} for the given parameter value. - * - *

The default implementation will check the {@linkplain #setMediaTypes(Map) media types} property for a defined - * mapping. - * + *

The default implementation will check the {@linkplain #setMediaTypes(Map) media types} + * property for a defined mapping. *

This method can be overriden to provide a different algorithm. - * * @param parameterValue the parameter value (i.e. {@code pdf}). * @return the media type, if any */ protected MediaType getMediaTypeFromParameter(String parameterValue) { - parameterValue = parameterValue.toLowerCase(Locale.ENGLISH); - return mediaTypes.get(parameterValue); + return this.mediaTypes.get(parameterValue.toLowerCase(Locale.ENGLISH)); } public View resolveViewName(String viewName, Locale locale) throws Exception { RequestAttributes attrs = RequestContextHolder.getRequestAttributes(); Assert.isInstanceOf(ServletRequestAttributes.class, attrs); ServletRequestAttributes servletAttrs = (ServletRequestAttributes) attrs; + List requestedMediaTypes = getMediaTypes(servletAttrs.getRequest()); - Collections.sort(requestedMediaTypes); + if (requestedMediaTypes.size() > 1) { + // avoid sorting attempt for empty list and singleton list + Collections.sort(requestedMediaTypes); + } SortedMap views = new TreeMap(); List candidateViews = new ArrayList(); - for (ViewResolver viewResolver : viewResolvers) { + for (ViewResolver viewResolver : this.viewResolvers) { View view = viewResolver.resolveViewName(viewName, locale); if (view != null) { candidateViews.add(view); } } - if (!CollectionUtils.isEmpty(defaultViews)) { - candidateViews.addAll(defaultViews); + if (!CollectionUtils.isEmpty(this.defaultViews)) { + candidateViews.addAll(this.defaultViews); } + for (View candidateView : candidateViews) { MediaType viewMediaType = MediaType.parseMediaType(candidateView.getContentType()); for (MediaType requestedMediaType : requestedMediaTypes) { @@ -365,6 +360,7 @@ public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport } } } + if (!views.isEmpty()) { MediaType mediaType = views.firstKey(); View view = views.get(mediaType); @@ -378,7 +374,10 @@ public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport } } - /** Inner class to avoid hard-coded JAF dependency. */ + + /** + * Inner class to avoid hard-coded JAF dependency. + */ private static class ActivationMediaTypeFactory { private static final FileTypeMap fileTypeMap; diff --git a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/view/ContentNegotiatingViewResolverTests.java b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/view/ContentNegotiatingViewResolverTests.java index d94b8a0ab0f..8c1ce84dd00 100644 --- a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/view/ContentNegotiatingViewResolverTests.java +++ b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/view/ContentNegotiatingViewResolverTests.java @@ -93,16 +93,51 @@ public class ContentNegotiatingViewResolverTests { } @Test - public void getDefaultMediaType() { + public void getDefaultContentType() { MockHttpServletRequest request = new MockHttpServletRequest("GET", "/test"); request.addHeader("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"); - viewResolver.setDefaultContentType(new MediaType("application", "pdf")); viewResolver.setIgnoreAcceptHeader(true); + viewResolver.setDefaultContentType(new MediaType("application", "pdf")); List result = viewResolver.getMediaTypes(request); assertEquals("Invalid amount of media types", 1, result.size()); assertEquals("Invalid content type", new MediaType("application", "pdf"), result.get(0)); } + @Test + public void resolveViewNameWithDefaultContentType() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/test"); + request.addHeader("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"); + RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request)); + + viewResolver.setIgnoreAcceptHeader(true); + viewResolver.setDefaultContentType(new MediaType("application", "xml")); + + ViewResolver viewResolverMock1 = createMock(ViewResolver.class); + ViewResolver viewResolverMock2 = createMock(ViewResolver.class); + List viewResolverMocks = new ArrayList(); + viewResolverMocks.add(viewResolverMock1); + viewResolverMocks.add(viewResolverMock2); + viewResolver.setViewResolvers(viewResolverMocks); + + View viewMock1 = createMock("application_xml", View.class); + View viewMock2 = createMock("text_html", View.class); + + String viewName = "view"; + Locale locale = Locale.ENGLISH; + + expect(viewResolverMock1.resolveViewName(viewName, locale)).andReturn(viewMock1); + expect(viewResolverMock2.resolveViewName(viewName, locale)).andReturn(viewMock2); + expect(viewMock1.getContentType()).andReturn("application/xml"); + expect(viewMock2.getContentType()).andReturn("text/html;charset=ISO-8859-1"); + + replay(viewResolverMock1, viewResolverMock2, viewMock1, viewMock2); + + View result = viewResolver.resolveViewName(viewName, locale); + assertSame("Invalid view", viewMock1, result); + + verify(viewResolverMock1, viewResolverMock2, viewMock1, viewMock2); + } + @Test public void resolveViewNameAcceptHeader() throws Exception { MockHttpServletRequest request = new MockHttpServletRequest("GET", "/test");