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
new file mode 100644
index 0000000000..abb8bdb7d7
--- /dev/null
+++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/view/ContentNegotiatingViewResolver.java
@@ -0,0 +1,310 @@
+/*
+ * Copyright 2002-2009 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
+ *
+ * http://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.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.SortedMap;
+import java.util.TreeMap;
+import javax.activation.FileTypeMap;
+import javax.activation.MimetypesFileTypeMap;
+import javax.servlet.ServletContext;
+import javax.servlet.http.HttpServletRequest;
+
+import org.springframework.beans.factory.BeanFactoryUtils;
+import org.springframework.core.OrderComparator;
+import org.springframework.core.Ordered;
+import org.springframework.core.io.ClassPathResource;
+import org.springframework.core.io.Resource;
+import org.springframework.util.Assert;
+import org.springframework.util.ClassUtils;
+import org.springframework.util.MediaType;
+import org.springframework.util.StringUtils;
+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.support.WebApplicationObjectSupport;
+import org.springframework.web.servlet.View;
+import org.springframework.web.servlet.ViewResolver;
+import org.springframework.web.util.UrlPathHelper;
+import org.springframework.web.util.WebUtils;
+
+/**
+ * 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}s. By default, these other view resolvers are picked up automatically from the application context,
+ * though they can also be set explicitely by using the {@link #setViewResolvers(List) viewResolvers} property.
+ * Note that in order for this view resolver to work properly, the {@link #setOrder(int) 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. This media type is determined by using the following criteria:
+ *
+ * - If the requested path has a file extension and if the {@link #setFavorPathExtension(boolean)} property is
+ *
true
, the {@link #setMediaTypes(Map) mediaTypes} property is inspected for a matching media type.
+ * - If there is no match and if the Java Activation Framework (JAF) is present on the class path,
+ * {@link FileTypeMap#getContentType(String)} is used.
+ * - If the previous steps did not result in a media type, the request {@code Accept} header is used.
+ *
+ * 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.
+ *
+ * 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
+ * @see ViewResolver
+ * @see InternalResourceViewResolver
+ * @see BeanNameViewResolver
+ * @since 3.0
+ */
+public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport implements ViewResolver, Ordered {
+
+ private static final boolean jafPresent =
+ ClassUtils.isPresent("javax.activation.FileTypeMap", ContentNegotiatingViewResolver.class.getClassLoader());
+
+ private static final String ACCEPT_HEADER = "Accept";
+
+ private UrlPathHelper urlPathHelper = new UrlPathHelper();
+
+ private boolean favorPathExtension = true;
+
+ private int order = Ordered.HIGHEST_PRECEDENCE;
+
+ private Map mediaTypes = new HashMap();
+
+ private List viewResolvers;
+
+ public void setOrder(int order) {
+ this.order = order;
+ }
+
+ public int getOrder() {
+ return 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.
+ *
+ * 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;
+ }
+
+ /**
+ * 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.
+ */
+ public void setMediaTypes(Map mediaTypes) {
+ Assert.notNull(mediaTypes, "'mediaTypes' must not be null");
+ for (Map.Entry entry : mediaTypes.entrySet()) {
+ String extension = entry.getKey().toLowerCase(Locale.ENGLISH);
+ MediaType mediaType = MediaType.parseMediaType(entry.getValue());
+ this.mediaTypes.put(extension, mediaType);
+ }
+ }
+
+ /**
+ * 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;
+ }
+
+ @Override
+ protected void initServletContext(ServletContext servletContext) {
+ if (viewResolvers == null) {
+ Map matchingBeans = BeanFactoryUtils
+ .beansOfTypeIncludingAncestors(getApplicationContext(), ViewResolver.class, true, false);
+ this.viewResolvers = new ArrayList(matchingBeans.size());
+ for (ViewResolver viewResolver : matchingBeans.values()) {
+ if (this != viewResolver) {
+ this.viewResolvers.add(viewResolver);
+ }
+ }
+ }
+ if (this.viewResolvers.isEmpty()) {
+ logger.warn("Did not find any ViewResolvers to delegate to; please configure them using the " +
+ "'viewResolvers' property on the ContentNegotiatingViewResolver");
+ }
+ Collections.sort(this.viewResolvers, new OrderComparator());
+ }
+
+ /**
+ * 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.
+ *
+ *
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) {
+ String requestUri = urlPathHelper.getRequestUri(request);
+ String filename = WebUtils.extractFullFilenameFromUrlPath(requestUri);
+ MediaType mediaType = getMediaTypeFromFilename(filename);
+ if (mediaType != null) {
+ if (logger.isDebugEnabled()) {
+ logger.debug("Requested media type is '" + mediaType + "' (based on filename '" + filename + "')");
+ }
+ List mediaTypes = new ArrayList();
+ mediaTypes.add(mediaType);
+ return mediaTypes;
+ }
+ }
+ String acceptHeader = request.getHeader(ACCEPT_HEADER);
+ if (StringUtils.hasText(acceptHeader)) {
+ List mediaTypes = MediaType.parseMediaTypes(acceptHeader);
+ if (logger.isDebugEnabled()) {
+ logger.debug("Requested media types are " + mediaTypes + " (based on Accept header)");
+ }
+ return mediaTypes;
+ }
+ else {
+ return Collections.emptyList();
+ }
+ }
+
+ /**
+ * 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
+ */
+ protected MediaType getMediaTypeFromFilename(String filename) {
+ String extension = StringUtils.getFilenameExtension(filename);
+ if (!StringUtils.hasText(extension)) {
+ return null;
+ }
+ extension = extension.toLowerCase(Locale.ENGLISH);
+ MediaType mediaType = mediaTypes.get(extension);
+ if (mediaType == null && jafPresent) {
+ mediaType = ActivationMediaTypeFactory.getMediaType(filename);
+ if (mediaType != null) {
+ mediaTypes.put(extension, mediaType);
+ }
+ }
+ return mediaType;
+ }
+
+ 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);
+
+ SortedMap views = new TreeMap();
+ for (ViewResolver viewResolver : viewResolvers) {
+ View view = viewResolver.resolveViewName(viewName, locale);
+ if (view != null) {
+ MediaType viewMediaType = MediaType.parseMediaType(view.getContentType());
+ for (MediaType requestedMediaType : requestedMediaTypes) {
+ if (requestedMediaType.includes(viewMediaType)) {
+ if (!views.containsKey(requestedMediaType)) {
+ views.put(requestedMediaType, view);
+ break;
+ }
+ }
+ }
+ }
+ }
+ if (!views.isEmpty()) {
+ MediaType mediaType = views.firstKey();
+ View view = views.get(mediaType);
+ if (logger.isDebugEnabled()) {
+ logger.debug("Returning [" + view + "] based on requested media type '" + mediaType + "'");
+ }
+ return view;
+ }
+ else {
+ return null;
+ }
+ }
+
+ /**
+ * Inner class to avoid hard-coded JAF dependency.
+ */
+ private static class ActivationMediaTypeFactory {
+
+ private static final FileTypeMap fileTypeMap;
+
+ static {
+ fileTypeMap = loadFileTypeMapFromContextSupportModule();
+ }
+
+ private static FileTypeMap loadFileTypeMapFromContextSupportModule() {
+ // see if we can find the extended mime.types from the context-support module
+ Resource mappingLocation = new ClassPathResource("org/springframework/mail/javamail/mime.types");
+ if (mappingLocation.exists()) {
+ InputStream inputStream = null;
+ try {
+ inputStream = mappingLocation.getInputStream();
+ return new MimetypesFileTypeMap(inputStream);
+ }
+ catch (IOException ex) {
+ // ignore
+ }
+ finally {
+ if (inputStream != null) {
+ try {
+ inputStream.close();
+ }
+ catch (IOException ex) {
+ // ignore
+ }
+ }
+ }
+ }
+ return FileTypeMap.getDefaultFileTypeMap();
+ }
+
+ public static MediaType getMediaType(String fileName) {
+ String mediaType = fileTypeMap.getContentType(fileName);
+ return StringUtils.hasText(mediaType) ? MediaType.parseMediaType(mediaType) : null;
+ }
+ }
+
+}
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
new file mode 100644
index 0000000000..2d381812ab
--- /dev/null
+++ b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/view/ContentNegotiatingViewResolverTests.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright 2002-2009 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
+ *
+ * http://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.Collections;
+import java.util.List;
+import java.util.Locale;
+
+import static org.easymock.EasyMock.*;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertSame;
+import org.junit.Before;
+import org.junit.Test;
+
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.util.MediaType;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+import org.springframework.web.servlet.View;
+import org.springframework.web.servlet.ViewResolver;
+
+/** @author Arjen Poutsma */
+public class ContentNegotiatingViewResolverTests {
+
+ private ContentNegotiatingViewResolver viewResolver;
+
+ @Before
+ public void createViewResolver() {
+ viewResolver = new ContentNegotiatingViewResolver();
+ }
+
+ @Test
+ public void getMediaTypeFromFilename() {
+ assertEquals("Invalid content type", new MediaType("text", "html"),
+ viewResolver.getMediaTypeFromFilename("test.html"));
+ viewResolver.setMediaTypes(Collections.singletonMap("HTML", "application/xhtml+xml"));
+ assertEquals("Invalid content type", new MediaType("application", "xhtml+xml"),
+ viewResolver.getMediaTypeFromFilename("test.html"));
+ }
+
+ @Test
+ public void getMediaTypeFilename() {
+ MockHttpServletRequest request = new MockHttpServletRequest("GET", "/test.html?foo=bar");
+ List result = viewResolver.getMediaTypes(request);
+ assertEquals("Invalid content type", Collections.singletonList(new MediaType("text", "html")), result);
+ viewResolver.setMediaTypes(Collections.singletonMap("html", "application/xhtml+xml"));
+ result = viewResolver.getMediaTypes(request);
+ assertEquals("Invalid content type", Collections.singletonList(new MediaType("application", "xhtml+xml")),
+ result);
+ }
+
+ @Test
+ public void getMediaTypeAcceptHeader() {
+ MockHttpServletRequest request = new MockHttpServletRequest("GET", "/test");
+ request.addHeader("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8");
+ List result = viewResolver.getMediaTypes(request);
+ assertEquals("Invalid amount of media types", 4, result.size());
+ assertEquals("Invalid content type", new MediaType("text", "html"), result.get(0));
+ assertEquals("Invalid content type", new MediaType("application", "xhtml+xml"), result.get(1));
+ assertEquals("Invalid content type", new MediaType("application", "xml", Collections.singletonMap("q", "0.9")),
+ result.get(2));
+ assertEquals("Invalid content type", new MediaType("*", "*", Collections.singletonMap("q", "0.8")),
+ result.get(3));
+ }
+
+ @Test
+ public void resolveViewNameAcceptHeader() 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 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", viewMock2, result);
+
+ verify(viewResolverMock1, viewResolverMock2, viewMock1, viewMock2);
+ }
+
+ @Test
+ public void resolveViewNameFilename() throws Exception {
+ MockHttpServletRequest request = new MockHttpServletRequest("GET", "/test.html");
+ RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request));
+
+ 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", viewMock2, result);
+
+ verify(viewResolverMock1, viewResolverMock2, viewMock1, viewMock2);
+ }
+
+}