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 0ad603a851..e9b6a8a924 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 @@ -31,6 +31,7 @@ import javax.activation.FileTypeMap; import javax.activation.MimetypesFileTypeMap; import javax.servlet.ServletContext; import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; import org.springframework.beans.factory.BeanFactoryUtils; import org.springframework.core.OrderComparator; @@ -114,6 +115,8 @@ public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport private boolean favorParameter = false; private String parameterName = "format"; + + private boolean useNotAcceptableStatusCode = false; private boolean ignoreAcceptHeader = false; @@ -125,7 +128,6 @@ public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport private List viewResolvers; - public void setOrder(int order) { this.order = order; } @@ -174,6 +176,20 @@ public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport this.ignoreAcceptHeader = ignoreAcceptHeader; } + /** + * Indicates 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; + } + /** * 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 @@ -337,7 +353,6 @@ public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport Collections.sort(requestedMediaTypes); } - SortedMap views = new TreeMap(); List candidateViews = new ArrayList(); for (ViewResolver viewResolver : this.viewResolvers) { View view = viewResolver.resolveViewName(viewName, locale); @@ -349,6 +364,7 @@ public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport candidateViews.addAll(this.defaultViews); } + SortedMap views = new TreeMap(); for (View candidateView : candidateViews) { String contentType = candidateView.getContentType(); if (StringUtils.hasText(contentType)) { @@ -373,11 +389,10 @@ public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport return view; } else { - return null; + return useNotAcceptableStatusCode ? new NotAcceptableView() : null; } } - /** * Inner class to avoid hard-coded JAF dependency. */ @@ -421,4 +436,15 @@ public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport } } + private static class NotAcceptableView implements View { + + public String getContentType() { + return null; + } + + public void render(Map model, HttpServletRequest request, HttpServletResponse response) + throws Exception { + response.setStatus(HttpServletResponse.SC_NOT_ACCEPTABLE); + } + } } 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 49d4a1869d..69614e2526 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 @@ -30,6 +30,7 @@ import org.junit.Test; import org.springframework.http.MediaType; import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import org.springframework.web.servlet.View; @@ -305,4 +306,58 @@ public class ContentNegotiatingViewResolverTests { verify(viewResolverMock, viewMock); } + @Test + public void resolveViewNoMatch() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/test"); + request.addHeader("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9"); + RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request)); + + ViewResolver viewResolverMock = createMock(ViewResolver.class); + viewResolver.setViewResolvers(Collections.singletonList(viewResolverMock)); + + View viewMock = createMock("application_xml", View.class); + + String viewName = "view"; + Locale locale = Locale.ENGLISH; + + expect(viewResolverMock.resolveViewName(viewName, locale)).andReturn(viewMock); + expect(viewMock.getContentType()).andReturn("application/pdf"); + + replay(viewResolverMock, viewMock); + + View result = viewResolver.resolveViewName(viewName, locale); + assertNull("Invalid view", result); + + verify(viewResolverMock, viewMock); + } + + @Test + public void resolveViewNoMatchUseUnacceptableStatus() throws Exception { + viewResolver.setUseNotAcceptableStatusCode(true); + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/test"); + request.addHeader("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9"); + RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request)); + + ViewResolver viewResolverMock = createMock(ViewResolver.class); + viewResolver.setViewResolvers(Collections.singletonList(viewResolverMock)); + + View viewMock = createMock("application_xml", View.class); + + String viewName = "view"; + Locale locale = Locale.ENGLISH; + + expect(viewResolverMock.resolveViewName(viewName, locale)).andReturn(viewMock); + expect(viewMock.getContentType()).andReturn("application/pdf"); + + replay(viewResolverMock, viewMock); + + View result = viewResolver.resolveViewName(viewName, locale); + assertNotNull("Invalid view", result); + MockHttpServletResponse response = new MockHttpServletResponse(); + result.render(null, request, response); + assertEquals("Invalid status code set", 406, response.getStatus()); + + verify(viewResolverMock, viewMock); + } + }