org.frameworkset.web.servlet.view.ContentNegotiatingViewResolver Maven / Gradle / Ivy
Show all versions of bboss-mvc Show documentation
/*
* Copyright 2008-2010 biaoping.yin
*
* 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.frameworkset.web.servlet.view;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import javax.activation.FileTypeMap;
import javax.activation.MimetypesFileTypeMap;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.frameworkset.http.MediaType;
import org.frameworkset.util.Assert;
import org.frameworkset.util.ClassUtils;
import org.frameworkset.util.CollectionUtils;
import org.frameworkset.util.io.ClassPathResource;
import org.frameworkset.util.io.Resource;
import org.frameworkset.web.servlet.context.RequestAttributes;
import org.frameworkset.web.servlet.context.RequestContextHolder;
import org.frameworkset.web.servlet.context.ServletRequestAttributes;
import org.frameworkset.web.servlet.support.WebApplicationObjectSupport;
import org.frameworkset.web.util.UrlPathHelper;
import org.frameworkset.web.util.WebUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.frameworkset.util.StringUtil;
/**
* 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 explicitly 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
* {@code true}, the {@link #setMediaTypes(Map) mediaTypes} property is inspected for a matching media type.
* - If the request contains a parameter defining the extension and if the {@link #setFavorParameter(boolean)}
* property is
true
, the {@link #setMediaTypes(Map) mediaTypes} property is inspected for a matching
* media type. The default name of the parameter is format
and it can be configured using the
* {@link #setParameterName(String) parameterName} property.
* - If there is no match in the {@link #setMediaTypes(Map) mediaTypes} property and if the Java Activation
* Framework (JAF) is both {@linkplain #setUseJaf(boolean) enabled} and present on the class path,
* {@link FileTypeMap#getContentType(String)} is used instead.
* - If the previous steps did not result in a media type, and
* {@link #setIgnoreAcceptHeader(boolean) ignoreAcceptHeader} is {@code false}, 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.
*
* Additionally, this view resolver exposes the {@link #setDefaultViews(List) defaultViews} property, allowing you to
* override the views provided by the view resolvers. Note that these default views are offered as candicates, and
* still need have the content type requested (via file extension, parameter, or {@code Accept} header, described above).
* You can also set the {@linkplain #setDefaultContentType(MediaType) default content type} directly, which will be
* returned when the other mechanisms ({@code Accept} header, file extension or parameter) do not result in a match.
*
*
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
* @author Juergen Hoeller
* @since 3.0
* @see ViewResolver
* @see InternalResourceViewResolver
* @see BeanNameViewResolver
*/
public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport implements ViewResolver {
private final static Logger logger = LoggerFactory.getLogger(ContentNegotiatingViewResolver.class);
private static final String ACCEPT_HEADER = "Accept";
private static final boolean jafPresent =
ClassUtils.isPresent("javax.activation.FileTypeMap", ContentNegotiatingViewResolver.class.getClassLoader());
private static final UrlPathHelper urlPathHelper = new UrlPathHelper();
private boolean favorPathExtension = true;
private boolean favorParameter = false;
private String parameterName = "format";
private boolean useNotAcceptableStatusCode = false;
private boolean ignoreAcceptHeader = false;
private boolean useJaf = true;
private ConcurrentMap mediaTypes = new ConcurrentHashMap();
private List defaultViews;
private MediaType defaultContentType;
private List viewResolvers;
/**
* 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}.
* 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;
}
/**
* 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}.
*
For instance, when this flag is true
, 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;
}
/**
* Sets the parameter name that can be used to determine the requested media type if the {@link
* #setFavorParameter(boolean)} property is {@code true}. The default parameter name is {@code format}.
*/
public void setParameterName(String parameterName) {
this.parameterName = parameterName;
}
/**
* 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.
*/
public void setIgnoreAcceptHeader(boolean 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.
*
*
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
* 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 default views to use when a more specific view can not be obtained
* from the {@link ViewResolver} chain.
*/
public void setDefaultViews(List defaultViews) {
this.defaultViews = defaultViews;
}
/**
* Sets the default content type.
* 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) {
this.defaultContentType = defaultContentType;
}
/**
* Indicates whether to use the Java Activation Framework to map from file extensions to media types.
*
Default is {@code true}, i.e. the Java Activation Framework is used (if available).
*/
public void setUseJaf(boolean useJaf) {
this.useJaf = useJaf;
}
/**
* 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 (this.viewResolvers == null) {
// Map matchingBeans =
// BeanFactoryUtils.beansOfTypeIncludingAncestors(getApplicationContext(), ViewResolver.class);
// 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");
}
// OrderComparator.sort(this.viewResolvers);
}
/**
* 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 (this.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 + "')");
}
return Collections.singletonList(mediaType);
}
}
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 '" +
this.parameterName + "'='" + parameterValue + "')");
}
return Collections.singletonList(mediaType);
}
}
}
if (!this.ignoreAcceptHeader) {
String acceptHeader = request.getHeader(ACCEPT_HEADER);
if (StringUtil.hasText(acceptHeader)) {
List mediaTypes = MediaType.parseMediaTypes(acceptHeader);
MediaType.sortByQualityValue(mediaTypes);
if (logger.isDebugEnabled()) {
logger.debug("Requested media types are " + mediaTypes + " (based on Accept header)");
}
return mediaTypes;
}
}
if (this.defaultContentType != null) {
if (logger.isDebugEnabled()) {
logger.debug("Requested media types is " + this.defaultContentType +
" (based on defaultContentType property)");
}
return Collections.singletonList(this.defaultContentType);
}
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 classpath, 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 = StringUtil.getFilenameExtension(filename);
if (!StringUtil.hasText(extension)) {
return null;
}
extension = extension.toLowerCase(Locale.ENGLISH);
MediaType mediaType = this.mediaTypes.get(extension);
if (mediaType == null && this.useJaf && jafPresent) {
mediaType = ActivationMediaTypeFactory.getMediaType(filename);
if (mediaType != null) {
this.mediaTypes.putIfAbsent(extension, mediaType);
}
}
return mediaType;
}
/**
* Determines the {@link MediaType} for the given parameter value.
*
The default implementation will check the {@linkplain #setMediaTypes(Map) media types}
* property for a defined mapping.
*
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) {
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);
List requestedMediaTypes = getMediaTypes(((ServletRequestAttributes) attrs).getRequest());
List candidateViews = getCandidateViews(viewName, locale, requestedMediaTypes);
View bestView = getBestView(candidateViews, requestedMediaTypes);
if (bestView != null) {
return bestView;
}
else {
if (this.useNotAcceptableStatusCode) {
if (logger.isDebugEnabled()) {
logger.debug("No acceptable view found; returning 406 (Not Acceptable) status code");
}
return NOT_ACCEPTABLE_VIEW;
}
else {
if (logger.isDebugEnabled()) {
logger.debug("No acceptable view found; returning null");
}
return null;
}
}
}
private List getCandidateViews(String viewName, Locale locale, List requestedMediaTypes)
throws Exception {
List candidateViews = new ArrayList();
for (ViewResolver viewResolver : this.viewResolvers) {
View view = viewResolver.resolveViewName(viewName, locale);
if (view != null) {
candidateViews.add(view);
}
for (MediaType requestedMediaType : requestedMediaTypes) {
List extensions = getExtensionsForMediaType(requestedMediaType);
for (String extension : extensions) {
String viewNameWithExtension = viewName + "." + extension;
view = viewResolver.resolveViewName(viewNameWithExtension, locale);
if (view != null) {
candidateViews.add(view);
}
}
}
}
if (!CollectionUtils.isEmpty(this.defaultViews)) {
candidateViews.addAll(this.defaultViews);
}
return candidateViews;
}
private List getExtensionsForMediaType(MediaType requestedMediaType) {
List result = new ArrayList();
for (Entry entry : mediaTypes.entrySet()) {
if (requestedMediaType.includes(entry.getValue())) {
result.add(entry.getKey());
}
}
return result;
}
private View getBestView(List candidateViews, List requestedMediaTypes) {
MediaType bestRequestedMediaType = null;
View bestView = null;
for (MediaType requestedMediaType : requestedMediaTypes) {
for (View candidateView : candidateViews) {
if (StringUtil.hasText(candidateView.getContentType())) {
MediaType candidateContentType = MediaType.parseMediaType(candidateView.getContentType());
if (requestedMediaType.includes(candidateContentType)) {
bestRequestedMediaType = requestedMediaType;
bestView = candidateView;
break;
}
}
}
if (bestView != null) {
if (logger.isDebugEnabled()) {
logger.debug(
"Returning [" + bestView + "] based on requested media type '" + bestRequestedMediaType +
"'");
}
break;
}
}
return bestView;
}
/**
* 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/frameworkset/web/servlet/mime.types");
if (mappingLocation.exists()) {
// if (logger.isTraceEnabled())
{
logger.info("Loading Java Activation Framework FileTypeMap from " + mappingLocation);
}
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
}
}
}
}
// if (logger.isTraceEnabled())
{
logger.info("Loading default Java Activation Framework FileTypeMap");
}
return FileTypeMap.getDefaultFileTypeMap();
}
public static MediaType getMediaType(String fileName) {
String mediaType = fileTypeMap.getContentType(fileName);
return StringUtil.hasText(mediaType) ? MediaType.parseMediaType(mediaType) : null;
}
}
private static final View NOT_ACCEPTABLE_VIEW = new View() {
public String getContentType() {
return null;
}
public void render(Map model, HttpServletRequest request, HttpServletResponse response)
throws Exception {
response.setStatus(HttpServletResponse.SC_NOT_ACCEPTABLE);
}
};
}