fixed ContentNegotiatingViewResolver to work with the combination of ignoreAcceptHeader=true plus defaultContentType as well (SPR-6163)
This commit is contained in:
parent
7a700edaa7
commit
0c47a01953
|
|
@ -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<String, MediaType> mediaTypes = new ConcurrentHashMap<String, MediaType>();
|
||||
|
||||
private List<View> defaultViews;
|
||||
|
||||
private MediaType defaultContentType;
|
||||
|
||||
private List<ViewResolver> 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}.
|
||||
*
|
||||
* <p>For instance, when this flag is <code>true</code> (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}.
|
||||
* <p>For instance, when this flag is <code>true</code> (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}.
|
||||
*
|
||||
* <p>For instance, when this flag is <code>true</code>, 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}.
|
||||
* <p>For instance, when this flag is <code>true</code>, 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.
|
||||
*
|
||||
* <p>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.
|
||||
* <p>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<String, String> 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<View> 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.
|
||||
* <p>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.
|
||||
*
|
||||
* <p>If this property is not set, view resolvers will be detected automatically.
|
||||
*/
|
||||
public void setViewResolvers(List<ViewResolver> viewResolvers) {
|
||||
|
|
@ -214,9 +215,9 @@ public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport
|
|||
|
||||
@Override
|
||||
protected void initServletContext(ServletContext servletContext) {
|
||||
if (viewResolvers == null) {
|
||||
Map<String, ViewResolver> matchingBeans = BeanFactoryUtils
|
||||
.beansOfTypeIncludingAncestors(getApplicationContext(), ViewResolver.class, true, false);
|
||||
if (this.viewResolvers == null) {
|
||||
Map<String, ViewResolver> matchingBeans =
|
||||
BeanFactoryUtils.beansOfTypeIncludingAncestors(getApplicationContext(), ViewResolver.class);
|
||||
this.viewResolvers = new ArrayList<ViewResolver>(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}.
|
||||
*
|
||||
* <p>The default implementation invokes {@link #getMediaTypeFromFilename(String)} if {@linkplain
|
||||
* #setFavorPathExtension(boolean) favorPathExtension} property is <code>true</code>. If the property is
|
||||
* <code>false</code>, or when a media type cannot be determined from the request path, this method will inspect the
|
||||
* {@code Accept} header of the request.
|
||||
*
|
||||
* <code>false</code>, or when a media type cannot be determined from the request path, this method will
|
||||
* inspect the {@code Accept} header of the request.
|
||||
* <p>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<MediaType> 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<MediaType> mediaTypes = new ArrayList<MediaType>();
|
||||
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<MediaType> 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.
|
||||
*
|
||||
* <p>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)}
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>The default implementation will check the {@linkplain #setMediaTypes(Map) media types} property for a defined
|
||||
* mapping.
|
||||
*
|
||||
* <p>The default implementation will check the {@linkplain #setMediaTypes(Map) media types}
|
||||
* property for a defined mapping.
|
||||
* <p>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<MediaType> requestedMediaTypes = getMediaTypes(servletAttrs.getRequest());
|
||||
Collections.sort(requestedMediaTypes);
|
||||
if (requestedMediaTypes.size() > 1) {
|
||||
// avoid sorting attempt for empty list and singleton list
|
||||
Collections.sort(requestedMediaTypes);
|
||||
}
|
||||
|
||||
SortedMap<MediaType, View> views = new TreeMap<MediaType, View>();
|
||||
List<View> candidateViews = new ArrayList<View>();
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -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<MediaType> 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<ViewResolver> viewResolverMocks = new ArrayList<ViewResolver>();
|
||||
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");
|
||||
|
|
|
|||
Loading…
Reference in New Issue