SPR-6467 - Allow ContentNegotiatingViewResolver to be strict ant return a 406 if no view found
This commit is contained in:
parent
0cb9271b95
commit
1cd0a9750d
|
|
@ -31,6 +31,7 @@ import javax.activation.FileTypeMap;
|
||||||
import javax.activation.MimetypesFileTypeMap;
|
import javax.activation.MimetypesFileTypeMap;
|
||||||
import javax.servlet.ServletContext;
|
import javax.servlet.ServletContext;
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
import org.springframework.beans.factory.BeanFactoryUtils;
|
import org.springframework.beans.factory.BeanFactoryUtils;
|
||||||
import org.springframework.core.OrderComparator;
|
import org.springframework.core.OrderComparator;
|
||||||
|
|
@ -115,6 +116,8 @@ public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport
|
||||||
|
|
||||||
private String parameterName = "format";
|
private String parameterName = "format";
|
||||||
|
|
||||||
|
private boolean useNotAcceptableStatusCode = false;
|
||||||
|
|
||||||
private boolean ignoreAcceptHeader = false;
|
private boolean ignoreAcceptHeader = false;
|
||||||
|
|
||||||
private ConcurrentMap<String, MediaType> mediaTypes = new ConcurrentHashMap<String, MediaType>();
|
private ConcurrentMap<String, MediaType> mediaTypes = new ConcurrentHashMap<String, MediaType>();
|
||||||
|
|
@ -125,7 +128,6 @@ public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport
|
||||||
|
|
||||||
private List<ViewResolver> viewResolvers;
|
private List<ViewResolver> viewResolvers;
|
||||||
|
|
||||||
|
|
||||||
public void setOrder(int order) {
|
public void setOrder(int order) {
|
||||||
this.order = order;
|
this.order = order;
|
||||||
}
|
}
|
||||||
|
|
@ -174,6 +176,20 @@ public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport
|
||||||
this.ignoreAcceptHeader = ignoreAcceptHeader;
|
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.
|
||||||
|
*
|
||||||
|
* <p>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.
|
* Sets the mapping from file extensions to media types.
|
||||||
* <p>When this mapping is not set or when an extension is not present, this view resolver
|
* <p>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);
|
Collections.sort(requestedMediaTypes);
|
||||||
}
|
}
|
||||||
|
|
||||||
SortedMap<MediaType, View> views = new TreeMap<MediaType, View>();
|
|
||||||
List<View> candidateViews = new ArrayList<View>();
|
List<View> candidateViews = new ArrayList<View>();
|
||||||
for (ViewResolver viewResolver : this.viewResolvers) {
|
for (ViewResolver viewResolver : this.viewResolvers) {
|
||||||
View view = viewResolver.resolveViewName(viewName, locale);
|
View view = viewResolver.resolveViewName(viewName, locale);
|
||||||
|
|
@ -349,6 +364,7 @@ public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport
|
||||||
candidateViews.addAll(this.defaultViews);
|
candidateViews.addAll(this.defaultViews);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SortedMap<MediaType, View> views = new TreeMap<MediaType, View>();
|
||||||
for (View candidateView : candidateViews) {
|
for (View candidateView : candidateViews) {
|
||||||
String contentType = candidateView.getContentType();
|
String contentType = candidateView.getContentType();
|
||||||
if (StringUtils.hasText(contentType)) {
|
if (StringUtils.hasText(contentType)) {
|
||||||
|
|
@ -373,11 +389,10 @@ public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport
|
||||||
return view;
|
return view;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
return null;
|
return useNotAcceptableStatusCode ? new NotAcceptableView() : null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Inner class to avoid hard-coded JAF dependency.
|
* 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<String, ?> model, HttpServletRequest request, HttpServletResponse response)
|
||||||
|
throws Exception {
|
||||||
|
response.setStatus(HttpServletResponse.SC_NOT_ACCEPTABLE);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ import org.junit.Test;
|
||||||
|
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.mock.web.MockHttpServletRequest;
|
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.RequestContextHolder;
|
||||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||||
import org.springframework.web.servlet.View;
|
import org.springframework.web.servlet.View;
|
||||||
|
|
@ -305,4 +306,58 @@ public class ContentNegotiatingViewResolverTests {
|
||||||
verify(viewResolverMock, viewMock);
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue