2 changed files with 454 additions and 0 deletions
@ -0,0 +1,310 @@
@@ -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. |
||||
* |
||||
* <p>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. |
||||
* <strong>Note</strong> 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}.) |
||||
* |
||||
* <p>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: |
||||
* <ol> |
||||
* <li>If the requested path has a file extension and if the {@link #setFavorPathExtension(boolean)} property is |
||||
* <code>true</code>, the {@link #setMediaTypes(Map) mediaTypes} property is inspected for a matching media type.</li> |
||||
* <li>If there is no match and if the Java Activation Framework (JAF) is present on the class path, |
||||
* {@link FileTypeMap#getContentType(String)} is used.</li> |
||||
* <li>If the previous steps did not result in a media type, the request {@code Accept} header is used.</li> |
||||
* </ol> |
||||
* 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. |
||||
* |
||||
* <p>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<String, MediaType> mediaTypes = new HashMap<String, MediaType>(); |
||||
|
||||
private List<ViewResolver> 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. |
||||
* |
||||
* <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; |
||||
} |
||||
|
||||
/** |
||||
* 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. |
||||
*/ |
||||
public void setMediaTypes(Map<String, String> mediaTypes) { |
||||
Assert.notNull(mediaTypes, "'mediaTypes' must not be null"); |
||||
for (Map.Entry<String, String> 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. |
||||
* |
||||
* <p>If this property is not set, view resolvers will be detected automatically. |
||||
*/ |
||||
public void setViewResolvers(List<ViewResolver> viewResolvers) { |
||||
this.viewResolvers = viewResolvers; |
||||
} |
||||
|
||||
@Override |
||||
protected void initServletContext(ServletContext servletContext) { |
||||
if (viewResolvers == null) { |
||||
Map<String, ViewResolver> matchingBeans = BeanFactoryUtils |
||||
.beansOfTypeIncludingAncestors(getApplicationContext(), ViewResolver.class, true, false); |
||||
this.viewResolvers = new ArrayList<ViewResolver>(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}. |
||||
* |
||||
* <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. |
||||
* |
||||
* <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) { |
||||
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<MediaType> mediaTypes = new ArrayList<MediaType>(); |
||||
mediaTypes.add(mediaType); |
||||
return mediaTypes; |
||||
} |
||||
} |
||||
String acceptHeader = request.getHeader(ACCEPT_HEADER); |
||||
if (StringUtils.hasText(acceptHeader)) { |
||||
List<MediaType> 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. |
||||
* |
||||
* <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 |
||||
*/ |
||||
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<MediaType> requestedMediaTypes = getMediaTypes(servletAttrs.getRequest()); |
||||
Collections.sort(requestedMediaTypes); |
||||
|
||||
SortedMap<MediaType, View> views = new TreeMap<MediaType, View>(); |
||||
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; |
||||
} |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,144 @@
@@ -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<MediaType> 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<MediaType> 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<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", 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<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", viewMock2, result); |
||||
|
||||
verify(viewResolverMock1, viewResolverMock2, viewMock1, viewMock2); |
||||
} |
||||
|
||||
} |
||||
Loading…
Reference in new issue