Add abstractions for content negotiation

Introduced ContentNeogtiationStrategy for resolving the requested
media types from an incoming request. The available implementations
are based on path extension, request parameter, 'Accept' header,
and a fixed default content type. The logic for these implementations
is based on equivalent options, previously available only in the
ContentNegotiatingViewResolver.

Also in this commit is ContentNegotiationManager, the central class to
use when configuring content negotiation options. It accepts one or
more ContentNeogtiationStrategy instances and delegates to them.

The ContentNeogiationManager can now be used to configure the
following classes:

- RequestMappingHandlerMappingm
- RequestMappingHandlerAdapter
- ExceptionHandlerExceptionResolver
- ContentNegotiatingViewResolver

Issue: SPR-8410, SPR-8417, SPR-8418,SPR-8416, SPR-8419,SPR-7722
This commit is contained in:
Rossen Stoyanchev
2012-06-08 08:56:57 -04:00
parent 35055fd866
commit f05e2bc56f
29 changed files with 1469 additions and 489 deletions

View File

@@ -21,6 +21,7 @@ import javax.servlet.http.HttpServletRequest;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.http.MediaType;
import org.springframework.web.HttpMediaTypeException;
import org.springframework.web.bind.annotation.RequestMapping;
/**
@@ -68,15 +69,12 @@ abstract class AbstractMediaTypeExpression implements Comparable<AbstractMediaTy
boolean match = matchMediaType(request);
return !isNegated ? match : !match;
}
catch (IllegalArgumentException ex) {
if (logger.isDebugEnabled()) {
logger.debug("Could not parse media type header: " + ex.getMessage());
}
catch (HttpMediaTypeException ex) {
return false;
}
}
protected abstract boolean matchMediaType(HttpServletRequest request);
protected abstract boolean matchMediaType(HttpServletRequest request) throws HttpMediaTypeException;
public int compareTo(AbstractMediaTypeExpression other) {
return MediaType.SPECIFICITY_COMPARATOR.compare(this.getMediaType(), other.getMediaType());

View File

@@ -28,17 +28,18 @@ import javax.servlet.http.HttpServletRequest;
import org.springframework.http.MediaType;
import org.springframework.util.StringUtils;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.mvc.condition.HeadersRequestCondition.HeaderExpression;
/**
* A logical disjunction (' || ') request condition to match a request's
* 'Content-Type' header to a list of media type expressions. Two kinds of
* media type expressions are supported, which are described in
* {@link RequestMapping#consumes()} and {@link RequestMapping#headers()}
* where the header name is 'Content-Type'. Regardless of which syntax is
* A logical disjunction (' || ') request condition to match a request's
* 'Content-Type' header to a list of media type expressions. Two kinds of
* media type expressions are supported, which are described in
* {@link RequestMapping#consumes()} and {@link RequestMapping#headers()}
* where the header name is 'Content-Type'. Regardless of which syntax is
* used, the semantics are the same.
*
*
* @author Arjen Poutsma
* @author Rossen Stoyanchev
* @since 3.1
@@ -49,8 +50,8 @@ public final class ConsumesRequestCondition extends AbstractRequestCondition<Con
/**
* Creates a new instance from 0 or more "consumes" expressions.
* @param consumes expressions with the syntax described in
* {@link RequestMapping#consumes()}; if 0 expressions are provided,
* @param consumes expressions with the syntax described in
* {@link RequestMapping#consumes()}; if 0 expressions are provided,
* the condition will match to every request.
*/
public ConsumesRequestCondition(String... consumes) {
@@ -58,9 +59,9 @@ public final class ConsumesRequestCondition extends AbstractRequestCondition<Con
}
/**
* Creates a new instance with "consumes" and "header" expressions.
* Creates a new instance with "consumes" and "header" expressions.
* "Header" expressions where the header name is not 'Content-Type' or have
* no header value defined are ignored. If 0 expressions are provided in
* no header value defined are ignored. If 0 expressions are provided in
* total, the condition will match to every request
* @param consumes as described in {@link RequestMapping#consumes()}
* @param headers as described in {@link RequestMapping#headers()}
@@ -116,7 +117,7 @@ public final class ConsumesRequestCondition extends AbstractRequestCondition<Con
}
return result;
}
/**
* Whether the condition has any media type expressions.
*/
@@ -135,8 +136,8 @@ public final class ConsumesRequestCondition extends AbstractRequestCondition<Con
}
/**
* Returns the "other" instance if it has any expressions; returns "this"
* instance otherwise. Practically that means a method-level "consumes"
* Returns the "other" instance if it has any expressions; returns "this"
* instance otherwise. Practically that means a method-level "consumes"
* overrides a type-level "consumes" condition.
*/
public ConsumesRequestCondition combine(ConsumesRequestCondition other) {
@@ -144,15 +145,15 @@ public final class ConsumesRequestCondition extends AbstractRequestCondition<Con
}
/**
* Checks if any of the contained media type expressions match the given
* request 'Content-Type' header and returns an instance that is guaranteed
* Checks if any of the contained media type expressions match the given
* request 'Content-Type' header and returns an instance that is guaranteed
* to contain matching expressions only. The match is performed via
* {@link MediaType#includes(MediaType)}.
*
*
* @param request the current request
*
* @return the same instance if the condition contains no expressions;
* or a new condition with matching expressions only;
*
* @return the same instance if the condition contains no expressions;
* or a new condition with matching expressions only;
* or {@code null} if no expressions match.
*/
public ConsumesRequestCondition getMatchingCondition(HttpServletRequest request) {
@@ -175,10 +176,10 @@ public final class ConsumesRequestCondition extends AbstractRequestCondition<Con
* <li>0 if the two conditions have the same number of expressions
* <li>Less than 0 if "this" has more or more specific media type expressions
* <li>Greater than 0 if "other" has more or more specific media type expressions
* </ul>
*
* <p>It is assumed that both instances have been obtained via
* {@link #getMatchingCondition(HttpServletRequest)} and each instance contains
* </ul>
*
* <p>It is assumed that both instances have been obtained via
* {@link #getMatchingCondition(HttpServletRequest)} and each instance contains
* the matching consumable media type expression only or is otherwise empty.
*/
public int compareTo(ConsumesRequestCondition other, HttpServletRequest request) {
@@ -197,7 +198,7 @@ public final class ConsumesRequestCondition extends AbstractRequestCondition<Con
}
/**
* Parses and matches a single media type expression to a request's 'Content-Type' header.
* Parses and matches a single media type expression to a request's 'Content-Type' header.
*/
static class ConsumeMediaTypeExpression extends AbstractMediaTypeExpression {
@@ -210,11 +211,17 @@ public final class ConsumesRequestCondition extends AbstractRequestCondition<Con
}
@Override
protected boolean matchMediaType(HttpServletRequest request) {
MediaType contentType = StringUtils.hasLength(request.getContentType()) ?
MediaType.parseMediaType(request.getContentType()) :
MediaType.APPLICATION_OCTET_STREAM ;
return getMediaType().includes(contentType);
protected boolean matchMediaType(HttpServletRequest request) throws HttpMediaTypeNotSupportedException {
try {
MediaType contentType = StringUtils.hasLength(request.getContentType()) ?
MediaType.parseMediaType(request.getContentType()) :
MediaType.APPLICATION_OCTET_STREAM;
return getMediaType().includes(contentType);
}
catch (IllegalArgumentException ex) {
throw new HttpMediaTypeNotSupportedException(
"Can't parse Content-Type [" + request.getContentType() + "]: " + ex.getMessage());
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2011 the original author or authors.
* Copyright 2002-2012 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.
@@ -17,7 +17,6 @@
package org.springframework.web.servlet.mvc.condition;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
@@ -28,17 +27,19 @@ import java.util.Set;
import javax.servlet.http.HttpServletRequest;
import org.springframework.http.MediaType;
import org.springframework.util.StringUtils;
import org.springframework.web.HttpMediaTypeNotAcceptableException;
import org.springframework.web.accept.ContentNegotiationManager;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.servlet.mvc.condition.HeadersRequestCondition.HeaderExpression;
/**
* A logical disjunction (' || ') request condition to match a request's 'Accept' header
* to a list of media type expressions. Two kinds of media type expressions are
* to a list of media type expressions. Two kinds of media type expressions are
* supported, which are described in {@link RequestMapping#produces()} and
* {@link RequestMapping#headers()} where the header name is 'Accept'.
* {@link RequestMapping#headers()} where the header name is 'Accept'.
* Regardless of which syntax is used, the semantics are the same.
*
*
* @author Arjen Poutsma
* @author Rossen Stoyanchev
* @since 3.1
@@ -47,35 +48,45 @@ public final class ProducesRequestCondition extends AbstractRequestCondition<Pro
private final List<ProduceMediaTypeExpression> expressions;
private final ContentNegotiationManager contentNegotiationManager;
/**
* Creates a new instance from 0 or more "produces" expressions.
* @param produces expressions with the syntax described in {@link RequestMapping#produces()}
* if 0 expressions are provided, the condition matches to every request
* Creates a new instance from "produces" expressions. If 0 expressions
* are provided in total, this condition will match to any request.
* @param produces expressions with syntax defined by {@link RequestMapping#produces()}
*/
public ProducesRequestCondition(String... produces) {
this(parseExpressions(produces, null));
this(produces, (String[]) null);
}
/**
* Creates a new instance with "produces" and "header" expressions. "Header" expressions
* where the header name is not 'Accept' or have no header value defined are ignored.
* If 0 expressions are provided in total, the condition matches to every request
* @param produces expressions with the syntax described in {@link RequestMapping#produces()}
* @param headers expressions with the syntax described in {@link RequestMapping#headers()}
* Creates a new instance with "produces" and "header" expressions. "Header"
* expressions where the header name is not 'Accept' or have no header value
* defined are ignored. If 0 expressions are provided in total, this condition
* will match to any request.
* @param produces expressions with syntax defined by {@link RequestMapping#produces()}
* @param headers expressions with syntax defined by {@link RequestMapping#headers()}
*/
public ProducesRequestCondition(String[] produces, String[] headers) {
this(parseExpressions(produces, headers));
this(produces, headers, null);
}
/**
* Private constructor accepting parsed media type expressions.
* Same as {@link #ProducesRequestCondition(String[], String[])} but also
* accepting a {@link ContentNegotiationManager}.
* @param produces expressions with syntax defined by {@link RequestMapping#produces()}
* @param headers expressions with syntax defined by {@link RequestMapping#headers()}
* @param contentNegotiationManager used to determine requested media types
*/
private ProducesRequestCondition(Collection<ProduceMediaTypeExpression> expressions) {
this.expressions = new ArrayList<ProduceMediaTypeExpression>(expressions);
public ProducesRequestCondition(String[] produces, String[] headers,
ContentNegotiationManager manager) {
this.expressions = new ArrayList<ProduceMediaTypeExpression>(parseExpressions(produces, headers));
Collections.sort(this.expressions);
this.contentNegotiationManager = (manager != null) ? manager : new ContentNegotiationManager();
}
private static Set<ProduceMediaTypeExpression> parseExpressions(String[] produces, String[] headers) {
private Set<ProduceMediaTypeExpression> parseExpressions(String[] produces, String[] headers) {
Set<ProduceMediaTypeExpression> result = new LinkedHashSet<ProduceMediaTypeExpression>();
if (headers != null) {
for (String header : headers) {
@@ -95,6 +106,17 @@ public final class ProducesRequestCondition extends AbstractRequestCondition<Pro
return result;
}
/**
* Private constructor with already parsed media type expressions.
*/
private ProducesRequestCondition(Collection<ProduceMediaTypeExpression> expressions,
ContentNegotiationManager manager) {
this.expressions = new ArrayList<ProduceMediaTypeExpression>(expressions);
Collections.sort(this.expressions);
this.contentNegotiationManager = (manager != null) ? manager : new ContentNegotiationManager();
}
/**
* Return the contained "produces" expressions.
*/
@@ -133,8 +155,8 @@ public final class ProducesRequestCondition extends AbstractRequestCondition<Pro
}
/**
* Returns the "other" instance if it has any expressions; returns "this"
* instance otherwise. Practically that means a method-level "produces"
* Returns the "other" instance if it has any expressions; returns "this"
* instance otherwise. Practically that means a method-level "produces"
* overrides a type-level "produces" condition.
*/
public ProducesRequestCondition combine(ProducesRequestCondition other) {
@@ -142,15 +164,15 @@ public final class ProducesRequestCondition extends AbstractRequestCondition<Pro
}
/**
* Checks if any of the contained media type expressions match the given
* request 'Content-Type' header and returns an instance that is guaranteed
* Checks if any of the contained media type expressions match the given
* request 'Content-Type' header and returns an instance that is guaranteed
* to contain matching expressions only. The match is performed via
* {@link MediaType#isCompatibleWith(MediaType)}.
*
*
* @param request the current request
*
* @return the same instance if there are no expressions;
* or a new condition with matching expressions;
*
* @return the same instance if there are no expressions;
* or a new condition with matching expressions;
* or {@code null} if no expressions match.
*/
public ProducesRequestCondition getMatchingCondition(HttpServletRequest request) {
@@ -164,58 +186,57 @@ public final class ProducesRequestCondition extends AbstractRequestCondition<Pro
iterator.remove();
}
}
return (result.isEmpty()) ? null : new ProducesRequestCondition(result);
return (result.isEmpty()) ? null : new ProducesRequestCondition(result, this.contentNegotiationManager);
}
/**
* Compares this and another "produces" condition as follows:
*
*
* <ol>
* <li>Sort 'Accept' header media types by quality value via
* {@link MediaType#sortByQualityValue(List)} and iterate the list.
* <li>Get the first index of matching media types in each "produces"
* condition first matching with {@link MediaType#equals(Object)} and
* condition first matching with {@link MediaType#equals(Object)} and
* then with {@link MediaType#includes(MediaType)}.
* <li>If a lower index is found, the condition at that index wins.
* <li>If both indexes are equal, the media types at the index are
* <li>If both indexes are equal, the media types at the index are
* compared further with {@link MediaType#SPECIFICITY_COMPARATOR}.
* </ol>
*
* <p>It is assumed that both instances have been obtained via
* {@link #getMatchingCondition(HttpServletRequest)} and each instance
* contains the matching producible media type expression only or
*
* <p>It is assumed that both instances have been obtained via
* {@link #getMatchingCondition(HttpServletRequest)} and each instance
* contains the matching producible media type expression only or
* is otherwise empty.
*/
public int compareTo(ProducesRequestCondition other, HttpServletRequest request) {
List<MediaType> acceptedMediaTypes = getAcceptedMediaTypes(request);
MediaType.sortByQualityValue(acceptedMediaTypes);
try {
List<MediaType> acceptedMediaTypes = getAcceptedMediaTypes(request);
for (MediaType acceptedMediaType : acceptedMediaTypes) {
int thisIndex = this.indexOfEqualMediaType(acceptedMediaType);
int otherIndex = other.indexOfEqualMediaType(acceptedMediaType);
int result = compareMatchingMediaTypes(this, thisIndex, other, otherIndex);
if (result != 0) {
return result;
}
thisIndex = this.indexOfIncludedMediaType(acceptedMediaType);
otherIndex = other.indexOfIncludedMediaType(acceptedMediaType);
result = compareMatchingMediaTypes(this, thisIndex, other, otherIndex);
if (result != 0) {
return result;
for (MediaType acceptedMediaType : acceptedMediaTypes) {
int thisIndex = this.indexOfEqualMediaType(acceptedMediaType);
int otherIndex = other.indexOfEqualMediaType(acceptedMediaType);
int result = compareMatchingMediaTypes(this, thisIndex, other, otherIndex);
if (result != 0) {
return result;
}
thisIndex = this.indexOfIncludedMediaType(acceptedMediaType);
otherIndex = other.indexOfIncludedMediaType(acceptedMediaType);
result = compareMatchingMediaTypes(this, thisIndex, other, otherIndex);
if (result != 0) {
return result;
}
}
return 0;
}
catch (HttpMediaTypeNotAcceptableException e) {
// should never happen
throw new IllegalStateException("Cannot compare without having any requested media types");
}
return 0;
}
private static List<MediaType> getAcceptedMediaTypes(HttpServletRequest request) {
String acceptHeader = request.getHeader("Accept");
if (StringUtils.hasLength(acceptHeader)) {
return MediaType.parseMediaTypes(acceptHeader);
}
else {
return Collections.singletonList(MediaType.ALL);
}
private List<MediaType> getAcceptedMediaTypes(HttpServletRequest request) throws HttpMediaTypeNotAcceptableException {
List<MediaType> mediaTypes = this.contentNegotiationManager.resolveMediaTypes(new ServletWebRequest(request));
return mediaTypes.isEmpty() ? Collections.singletonList(MediaType.ALL) : mediaTypes;
}
private int indexOfEqualMediaType(MediaType mediaType) {
@@ -238,8 +259,9 @@ public final class ProducesRequestCondition extends AbstractRequestCondition<Pro
return -1;
}
private static int compareMatchingMediaTypes(ProducesRequestCondition condition1, int index1,
ProducesRequestCondition condition2, int index2) {
private int compareMatchingMediaTypes(ProducesRequestCondition condition1,
int index1, ProducesRequestCondition condition2, int index2) {
int result = 0;
if (index1 != index2) {
result = index2 - index1;
@@ -254,22 +276,22 @@ public final class ProducesRequestCondition extends AbstractRequestCondition<Pro
}
/**
* Return the contained "produces" expressions or if that's empty, a list
* with a {@code MediaType_ALL} expression.
*/
* Return the contained "produces" expressions or if that's empty, a list
* with a {@code MediaType_ALL} expression.
*/
private List<ProduceMediaTypeExpression> getExpressionsToCompare() {
return this.expressions.isEmpty() ? DEFAULT_EXPRESSION_LIST : this.expressions;
return this.expressions.isEmpty() ? MEDIA_TYPE_ALL_LIST : this.expressions;
}
private static final List<ProduceMediaTypeExpression> DEFAULT_EXPRESSION_LIST =
Arrays.asList(new ProduceMediaTypeExpression("*/*"));
private final List<ProduceMediaTypeExpression> MEDIA_TYPE_ALL_LIST =
Collections.singletonList(new ProduceMediaTypeExpression("*/*"));
/**
* Parses and matches a single media type expression to a request's 'Accept' header.
* Parses and matches a single media type expression to a request's 'Accept' header.
*/
static class ProduceMediaTypeExpression extends AbstractMediaTypeExpression {
class ProduceMediaTypeExpression extends AbstractMediaTypeExpression {
ProduceMediaTypeExpression(MediaType mediaType, boolean negated) {
super(mediaType, negated);
}
@@ -279,7 +301,7 @@ public final class ProducesRequestCondition extends AbstractRequestCondition<Pro
}
@Override
protected boolean matchMediaType(HttpServletRequest request) {
protected boolean matchMediaType(HttpServletRequest request) throws HttpMediaTypeNotAcceptableException {
List<MediaType> acceptedMediaTypes = getAcceptedMediaTypes(request);
for (MediaType acceptedMediaType : acceptedMediaTypes) {
if (getMediaType().isCompatibleWith(acceptedMediaType)) {

View File

@@ -27,7 +27,6 @@ import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
@@ -35,7 +34,9 @@ import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.util.CollectionUtils;
import org.springframework.web.HttpMediaTypeNotAcceptableException;
import org.springframework.web.accept.ContentNegotiationManager;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
import org.springframework.web.servlet.HandlerMapping;
@@ -52,8 +53,17 @@ public abstract class AbstractMessageConverterMethodProcessor extends AbstractMe
private static final MediaType MEDIA_TYPE_APPLICATION = new MediaType("application");
private final ContentNegotiationManager contentNegotiationManager;
protected AbstractMessageConverterMethodProcessor(List<HttpMessageConverter<?>> messageConverters) {
this(messageConverters, null);
}
protected AbstractMessageConverterMethodProcessor(List<HttpMessageConverter<?>> messageConverters,
ContentNegotiationManager manager) {
super(messageConverters);
this.contentNegotiationManager = (manager != null) ? manager : new ContentNegotiationManager();
}
/**
@@ -100,14 +110,15 @@ public abstract class AbstractMessageConverterMethodProcessor extends AbstractMe
Class<?> returnValueClass = returnValue.getClass();
List<MediaType> acceptableMediaTypes = getAcceptableMediaTypes(inputMessage);
List<MediaType> producibleMediaTypes = getProducibleMediaTypes(inputMessage.getServletRequest(), returnValueClass);
HttpServletRequest servletRequest = inputMessage.getServletRequest();
List<MediaType> requestedMediaTypes = getAcceptableMediaTypes(servletRequest);
List<MediaType> producibleMediaTypes = getProducibleMediaTypes(servletRequest, returnValueClass);
Set<MediaType> compatibleMediaTypes = new LinkedHashSet<MediaType>();
for (MediaType a : acceptableMediaTypes) {
for (MediaType r : requestedMediaTypes) {
for (MediaType p : producibleMediaTypes) {
if (a.isCompatibleWith(p)) {
compatibleMediaTypes.add(getMostSpecificMediaType(a, p));
if (r.isCompatibleWith(p)) {
compatibleMediaTypes.add(getMostSpecificMediaType(r, p));
}
}
}
@@ -175,17 +186,9 @@ public abstract class AbstractMessageConverterMethodProcessor extends AbstractMe
}
}
private List<MediaType> getAcceptableMediaTypes(HttpInputMessage inputMessage) {
try {
List<MediaType> result = inputMessage.getHeaders().getAccept();
return result.isEmpty() ? Collections.singletonList(MediaType.ALL) : result;
}
catch (IllegalArgumentException ex) {
if (logger.isDebugEnabled()) {
logger.debug("Could not parse Accept header: " + ex.getMessage());
}
return Collections.emptyList();
}
private List<MediaType> getAcceptableMediaTypes(HttpServletRequest request) throws HttpMediaTypeNotAcceptableException {
List<MediaType> mediaTypes = this.contentNegotiationManager.resolveMediaTypes(new ServletWebRequest(request));
return mediaTypes.isEmpty() ? Collections.singletonList(MediaType.ALL) : mediaTypes;
}
/**

View File

@@ -32,6 +32,7 @@ import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.http.converter.xml.SourceHttpMessageConverter;
import org.springframework.http.converter.xml.XmlAwareFormHttpMessageConverter;
import org.springframework.web.accept.ContentNegotiationManager;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.method.HandlerMethod;
@@ -69,6 +70,8 @@ public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExce
private List<HttpMessageConverter<?>> messageConverters;
private ContentNegotiationManager contentNegotiationManager = new ContentNegotiationManager();
private final Map<Class<?>, ExceptionHandlerMethodResolver> exceptionHandlerMethodResolvers =
new ConcurrentHashMap<Class<?>, ExceptionHandlerMethodResolver>();
@@ -182,6 +185,14 @@ public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExce
return messageConverters;
}
/**
* Set the {@link ContentNegotiationManager} to use to determine requested media types.
* If not set, the default constructor is used.
*/
public void setContentNegotiationManager(ContentNegotiationManager contentNegotiationManager) {
this.contentNegotiationManager = contentNegotiationManager;
}
public void afterPropertiesSet() {
if (this.argumentResolvers == null) {
List<HandlerMethodArgumentResolver> resolvers = getDefaultArgumentResolvers();
@@ -223,11 +234,11 @@ public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExce
handlers.add(new ModelAndViewMethodReturnValueHandler());
handlers.add(new ModelMethodProcessor());
handlers.add(new ViewMethodReturnValueHandler());
handlers.add(new HttpEntityMethodProcessor(getMessageConverters()));
handlers.add(new HttpEntityMethodProcessor(getMessageConverters(), this.contentNegotiationManager));
// Annotation-based return value types
handlers.add(new ModelAttributeMethodProcessor(false));
handlers.add(new RequestResponseBodyMethodProcessor(getMessageConverters()));
handlers.add(new RequestResponseBodyMethodProcessor(getMessageConverters(), this.contentNegotiationManager));
// Multi-purpose return value types
handlers.add(new ViewNameMethodReturnValueHandler());

View File

@@ -33,6 +33,7 @@ import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.util.Assert;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.accept.ContentNegotiationManager;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.ModelAndViewContainer;
@@ -56,6 +57,12 @@ public class HttpEntityMethodProcessor extends AbstractMessageConverterMethodPro
super(messageConverters);
}
public HttpEntityMethodProcessor(List<HttpMessageConverter<?>> messageConverters,
ContentNegotiationManager contentNegotiationManager) {
super(messageConverters, contentNegotiationManager);
}
public boolean supportsParameter(MethodParameter parameter) {
Class<?> parameterType = parameter.getParameterType();
return HttpEntity.class.equals(parameterType);
@@ -123,7 +130,7 @@ public class HttpEntityMethodProcessor extends AbstractMessageConverterMethodPro
if (!entityHeaders.isEmpty()) {
outputMessage.getHeaders().putAll(entityHeaders);
}
Object body = responseEntity.getBody();
if (body != null) {
writeWithMessageConverters(body, returnType, inputMessage, outputMessage);

View File

@@ -47,6 +47,7 @@ import org.springframework.ui.ModelMap;
import org.springframework.util.ClassUtils;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ReflectionUtils.MethodFilter;
import org.springframework.web.accept.ContentNegotiationManager;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
@@ -147,6 +148,8 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter i
private Long asyncRequestTimeout;
private ContentNegotiationManager contentNegotiationManager = new ContentNegotiationManager();
/**
* Default constructor.
*/
@@ -410,6 +413,14 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter i
this.asyncRequestTimeout = asyncRequestTimeout;
}
/**
* Set the {@link ContentNegotiationManager} to use to determine requested media types.
* If not set, the default constructor is used.
*/
public void setContentNegotiationManager(ContentNegotiationManager contentNegotiationManager) {
this.contentNegotiationManager = contentNegotiationManager;
}
/**
* {@inheritDoc}
* <p>A {@link ConfigurableBeanFactory} is expected for resolving
@@ -525,12 +536,12 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter i
handlers.add(new ModelAndViewMethodReturnValueHandler());
handlers.add(new ModelMethodProcessor());
handlers.add(new ViewMethodReturnValueHandler());
handlers.add(new HttpEntityMethodProcessor(getMessageConverters()));
handlers.add(new HttpEntityMethodProcessor(getMessageConverters(), this.contentNegotiationManager));
handlers.add(new AsyncMethodReturnValueHandler());
// Annotation-based return value types
handlers.add(new ModelAttributeMethodProcessor(false));
handlers.add(new RequestResponseBodyMethodProcessor(getMessageConverters()));
handlers.add(new RequestResponseBodyMethodProcessor(getMessageConverters(), this.contentNegotiationManager));
// Multi-purpose return value types
handlers.add(new ViewNameMethodReturnValueHandler());

View File

@@ -20,6 +20,8 @@ import java.lang.reflect.Method;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.stereotype.Controller;
import org.springframework.web.accept.ContentNegotiationManager;
import org.springframework.web.accept.HeaderContentNegotiationStrategy;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.mvc.condition.AbstractRequestCondition;
import org.springframework.web.servlet.mvc.condition.CompositeRequestCondition;
@@ -48,6 +50,8 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi
private boolean useTrailingSlashMatch = true;
private ContentNegotiationManager contentNegotiationManager = new ContentNegotiationManager();
/**
* Whether to use suffix pattern match (".*") when matching patterns to
* requests. If enabled a method mapped to "/users" also matches to "/users.*".
@@ -66,6 +70,14 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi
this.useTrailingSlashMatch = useTrailingSlashMatch;
}
/**
* Set the {@link ContentNegotiationManager} to use to determine requested media types.
* If not set, the default constructor is used.
*/
public void setContentNegotiationManager(ContentNegotiationManager contentNegotiationManager) {
this.contentNegotiationManager = contentNegotiationManager;
}
/**
* Whether to use suffix pattern matching.
*/
@@ -79,6 +91,13 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi
return this.useTrailingSlashMatch;
}
/**
* Return the configured {@link ContentNegotiationManager}.
*/
public ContentNegotiationManager getContentNegotiationManager() {
return contentNegotiationManager;
}
/**
* {@inheritDoc}
* Expects a handler to have a type-level @{@link Controller} annotation.
@@ -160,7 +179,7 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi
new ParamsRequestCondition(annotation.params()),
new HeadersRequestCondition(annotation.headers()),
new ConsumesRequestCondition(annotation.consumes(), annotation.headers()),
new ProducesRequestCondition(annotation.produces(), annotation.headers()),
new ProducesRequestCondition(annotation.produces(), annotation.headers(), getContentNegotiationManager()),
customCondition);
}

View File

@@ -29,6 +29,7 @@ import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.validation.BindingResult;
import org.springframework.web.HttpMediaTypeNotAcceptableException;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.accept.ContentNegotiationManager;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.RequestBody;
@@ -60,6 +61,12 @@ public class RequestResponseBodyMethodProcessor extends AbstractMessageConverter
super(messageConverters);
}
public RequestResponseBodyMethodProcessor(List<HttpMessageConverter<?>> messageConverters,
ContentNegotiationManager contentNegotiationManager) {
super(messageConverters, contentNegotiationManager);
}
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(RequestBody.class);
}

View File

@@ -16,48 +16,47 @@
package org.springframework.web.servlet.view;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
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.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.beans.factory.InitializingBean;
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.http.MediaType;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.HttpMediaTypeNotAcceptableException;
import org.springframework.web.accept.ContentNegotiationManager;
import org.springframework.web.accept.FixedContentNegotiationStrategy;
import org.springframework.web.accept.PathExtensionContentNegotiationStrategy;
import org.springframework.web.accept.HeaderContentNegotiationStrategy;
import org.springframework.web.accept.ParameterContentNegotiationStrategy;
import org.springframework.web.accept.ContentNegotiationStrategy;
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.request.ServletWebRequest;
import org.springframework.web.context.support.WebApplicationObjectSupport;
import org.springframework.web.servlet.HandlerMapping;
import org.springframework.web.servlet.SmartView;
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.
@@ -69,23 +68,8 @@ import org.springframework.web.util.WebUtils;
* 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} property is
* {@code true}, the {@link #setMediaTypes(Map) mediaTypes} property is inspected for a matching media type.</li>
* <li>If the request contains a parameter defining the extension and if the {@link #setFavorParameter}
* property is <code>true</code>, the {@link #setMediaTypes(Map) mediaTypes} property is inspected for a matching
* media type. The default name of the parameter is <code>format</code> and it can be configured using the
* {@link #setParameterName(String) parameterName} property.</li>
* <li>If there is no match in the {@link #setMediaTypes(Map) mediaTypes} property and if the Java Activation
* Framework (JAF) is both {@linkplain #setUseJaf enabled} and present on the classpath,
* {@link FileTypeMap#getContentType(String)} is used instead.</li>
* <li>If the previous steps did not result in a media type, and
* {@link #setIgnoreAcceptHeader ignoreAcceptHeader} is {@code false}, the request {@code Accept} header is
* used.</li>
* </ol>
*
* <p>Once the requested media type has been determined, this resolver queries each delegate view resolver for a
* request. The requested media type is determined through the configured {@link ContentNegotiationManager}.
* 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.
*
@@ -107,44 +91,33 @@ import org.springframework.web.util.WebUtils;
* @see InternalResourceViewResolver
* @see BeanNameViewResolver
*/
public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport implements ViewResolver, Ordered {
public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport implements ViewResolver, Ordered, InitializingBean {
private static final Log logger = LogFactory.getLog(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();
static {
urlPathHelper.setUrlDecode(false);
}
private int order = Ordered.HIGHEST_PRECEDENCE;
private ContentNegotiationManager contentNegotiationManager;
private boolean favorPathExtension = true;
private boolean favorParameter = false;
private String parameterName = "format";
private boolean ignoreAcceptHeader = false;
private Map<String, String> mediaTypes = new HashMap<String, String>();
private Boolean useJaf;
private String parameterName;
private MediaType defaultContentType;
private boolean useNotAcceptableStatusCode = false;
private boolean ignoreAcceptHeader = false;
private boolean useJaf = jafPresent;
private ConcurrentMap<String, MediaType> mediaTypes = new ConcurrentHashMap<String, MediaType>();
private List<View> defaultViews;
private MediaType defaultContentType;
private List<ViewResolver> viewResolvers;
public ContentNegotiatingViewResolver() {
super();
}
public void setOrder(int order) {
this.order = order;
}
@@ -153,23 +126,45 @@ public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport
return this.order;
}
/**
* Set the {@link ContentNegotiationManager} to use to determine requested media types.
* If not set, the default constructor is used.
*/
public void setContentNegotiationManager(ContentNegotiationManager contentNegotiationManager) {
this.contentNegotiationManager = contentNegotiationManager;
}
/**
* Indicate 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}.
*
* @deprecated use {@link #setContentNegotiationManager(ContentNegotiationManager)}
*/
public void setFavorPathExtension(boolean favorPathExtension) {
this.favorPathExtension = favorPathExtension;
}
/**
* Indicate whether to use the Java Activation Framework to map from file extensions to media types.
* <p>Default is {@code true}, i.e. the Java Activation Framework is used (if available).
*
* @deprecated use {@link #setContentNegotiationManager(ContentNegotiationManager)}
*/
public void setUseJaf(boolean useJaf) {
this.useJaf = useJaf;
}
/**
* Indicate 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}.
*
* @deprecated use {@link #setContentNegotiationManager(ContentNegotiationManager)}
*/
public void setFavorParameter(boolean favorParameter) {
this.favorParameter = favorParameter;
@@ -178,6 +173,8 @@ public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport
/**
* Set the parameter name that can be used to determine the requested media type if the {@link
* #setFavorParameter} property is {@code true}. The default parameter name is {@code format}.
*
* @deprecated use {@link #setContentNegotiationManager(ContentNegotiationManager)}
*/
public void setParameterName(String parameterName) {
this.parameterName = parameterName;
@@ -188,11 +185,35 @@ public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport
* <p>If set to {@code true}, this view resolver will only refer to the file extension and/or
* parameter, as indicated by the {@link #setFavorPathExtension favorPathExtension} and
* {@link #setFavorParameter favorParameter} properties.
*
* @deprecated use {@link #setContentNegotiationManager(ContentNegotiationManager)}
*/
public void setIgnoreAcceptHeader(boolean ignoreAcceptHeader) {
this.ignoreAcceptHeader = ignoreAcceptHeader;
}
/**
* Set 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.
*
* @deprecated use {@link #setContentNegotiationManager(ContentNegotiationManager)}
*/
public void setMediaTypes(Map<String, String> mediaTypes) {
this.mediaTypes = mediaTypes;
}
/**
* Set 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.
*
* @deprecated use {@link #setContentNegotiationManager(ContentNegotiationManager)}
*/
public void setDefaultContentType(MediaType defaultContentType) {
this.defaultContentType = defaultContentType;
}
/**
* Indicate whether a {@link HttpServletResponse#SC_NOT_ACCEPTABLE 406 Not Acceptable}
* status code should be returned if no suitable view can be found.
@@ -206,20 +227,6 @@ public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport
this.useNotAcceptableStatusCode = useNotAcceptableStatusCode;
}
/**
* Set 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);
}
}
/**
* Set the default views to use when a more specific view can not be obtained
* from the {@link ViewResolver} chain.
@@ -228,23 +235,6 @@ public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport
this.defaultViews = defaultViews;
}
/**
* Set 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) {
this.defaultContentType = defaultContentType;
}
/**
* Indicate whether to use the Java Activation Framework to map from file extensions to media types.
* <p>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.
* <p>If this property is not set, view resolvers will be detected automatically.
@@ -283,6 +273,32 @@ public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport
OrderComparator.sort(this.viewResolvers);
}
public void afterPropertiesSet() throws Exception {
if (this.contentNegotiationManager == null) {
List<ContentNegotiationStrategy> strategies = new ArrayList<ContentNegotiationStrategy>();
if (this.favorPathExtension) {
PathExtensionContentNegotiationStrategy strategy = new PathExtensionContentNegotiationStrategy(this.mediaTypes);
if (this.useJaf != null) {
strategy.setUseJaf(this.useJaf);
}
strategies.add(strategy);
}
if (this.favorParameter) {
ParameterContentNegotiationStrategy strategy = new ParameterContentNegotiationStrategy(this.mediaTypes);
strategy.setParameterName(this.parameterName);
strategies.add(strategy);
}
if (!this.ignoreAcceptHeader) {
strategies.add(new HeaderContentNegotiationStrategy());
}
if (this.defaultContentType != null) {
strategies.add(new FixedContentNegotiationStrategy(this.defaultContentType));
}
ContentNegotiationStrategy[] array = strategies.toArray(new ContentNegotiationStrategy[strategies.size()]);
this.contentNegotiationManager = new ContentNegotiationManager(array);
}
}
public View resolveViewName(String viewName, Locale locale) throws Exception {
RequestAttributes attrs = RequestContextHolder.getRequestAttributes();
Assert.isInstanceOf(ServletRequestAttributes.class, attrs);
@@ -317,69 +333,28 @@ public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport
* @return the list of media types requested, if any
*/
protected List<MediaType> getMediaTypes(HttpServletRequest request) {
if (this.favorPathExtension) {
String requestUri = urlPathHelper.getLookupPathForRequest(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 + "')");
try {
ServletWebRequest webRequest = new ServletWebRequest(request);
List<MediaType> acceptableMediaTypes = this.contentNegotiationManager.resolveMediaTypes(webRequest);
List<MediaType> producibleMediaTypes = getProducibleMediaTypes(request);
Set<MediaType> compatibleMediaTypes = new LinkedHashSet<MediaType>();
for (MediaType acceptable : acceptableMediaTypes) {
for (MediaType producible : producibleMediaTypes) {
if (acceptable.isCompatibleWith(producible)) {
compatibleMediaTypes.add(getMostSpecificMediaType(acceptable, producible));
}
return Collections.singletonList(mediaType);
}
}
}
if (!this.ignoreAcceptHeader) {
String acceptHeader = request.getHeader(ACCEPT_HEADER);
if (StringUtils.hasText(acceptHeader)) {
try {
List<MediaType> acceptableMediaTypes = MediaType.parseMediaTypes(acceptHeader);
List<MediaType> producibleMediaTypes = getProducibleMediaTypes(request);
Set<MediaType> compatibleMediaTypes = new LinkedHashSet<MediaType>();
for (MediaType acceptable : acceptableMediaTypes) {
for (MediaType producible : producibleMediaTypes) {
if (acceptable.isCompatibleWith(producible)) {
compatibleMediaTypes.add(getMostSpecificMediaType(acceptable, producible));
}
}
}
List<MediaType> selectedMediaTypes = new ArrayList<MediaType>(compatibleMediaTypes);
MediaType.sortBySpecificityAndQuality(selectedMediaTypes);
if (logger.isDebugEnabled()) {
logger.debug("Requested media types are " + selectedMediaTypes + " based on Accept header types " +
"and producible media types " + producibleMediaTypes + ")");
}
return selectedMediaTypes;
}
catch (IllegalArgumentException ex) {
if (logger.isDebugEnabled()) {
logger.debug("Could not parse accept header [" + acceptHeader + "]: " + ex.getMessage());
}
return null;
}
}
}
if (this.defaultContentType != null) {
List<MediaType> selectedMediaTypes = new ArrayList<MediaType>(compatibleMediaTypes);
MediaType.sortBySpecificityAndQuality(selectedMediaTypes);
if (logger.isDebugEnabled()) {
logger.debug("Requested media types is " + this.defaultContentType +
" (based on defaultContentType property)");
logger.debug("Requested media types are " + selectedMediaTypes + " based on Accept header types " +
"and producible media types " + producibleMediaTypes + ")");
}
return Collections.singletonList(this.defaultContentType);
return selectedMediaTypes;
}
else {
return Collections.emptyList();
catch (HttpMediaTypeNotAcceptableException ex) {
return null;
}
}
@@ -404,52 +379,6 @@ public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport
return MediaType.SPECIFICITY_COMPARATOR.compare(acceptType, produceType) < 0 ? acceptType : produceType;
}
/**
* 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 classpath, it will call {@link FileTypeMap#getContentType(String)}
* <p>This method can be overridden 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 = this.mediaTypes.get(extension);
if (mediaType == null) {
String mimeType = getServletContext().getMimeType(filename);
if (StringUtils.hasText(mimeType)) {
mediaType = MediaType.parseMediaType(mimeType);
}
if (this.useJaf && (mediaType == null || MediaType.APPLICATION_OCTET_STREAM.equals(mediaType))) {
MediaType jafMediaType = ActivationMediaTypeFactory.getMediaType(filename);
if (jafMediaType != null && !MediaType.APPLICATION_OCTET_STREAM.equals(jafMediaType)) {
mediaType = jafMediaType;
}
}
if (mediaType != null) {
this.mediaTypes.putIfAbsent(extension, mediaType);
}
}
return mediaType;
}
/**
* 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>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));
}
private List<View> getCandidateViews(String viewName, Locale locale, List<MediaType> requestedMediaTypes)
throws Exception {
@@ -460,7 +389,7 @@ public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport
candidateViews.add(view);
}
for (MediaType requestedMediaType : requestedMediaTypes) {
List<String> extensions = getExtensionsForMediaType(requestedMediaType);
List<String> extensions = this.contentNegotiationManager.resolveExtensions(requestedMediaType);
for (String extension : extensions) {
String viewNameWithExtension = viewName + "." + extension;
view = viewResolver.resolveViewName(viewNameWithExtension, locale);
@@ -468,7 +397,6 @@ public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport
candidateViews.add(view);
}
}
}
}
if (!CollectionUtils.isEmpty(this.defaultViews)) {
@@ -477,16 +405,6 @@ public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport
return candidateViews;
}
private List<String> getExtensionsForMediaType(MediaType requestedMediaType) {
List<String> result = new ArrayList<String>();
for (Entry<String, MediaType> entry : this.mediaTypes.entrySet()) {
if (requestedMediaType.includes(entry.getValue())) {
result.add(entry.getKey());
}
}
return result;
}
private View getBestView(List<View> candidateViews, List<MediaType> requestedMediaTypes) {
for (View candidateView : candidateViews) {
if (candidateView instanceof SmartView) {
@@ -528,54 +446,4 @@ public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport
}
};
/**
* 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()) {
if (logger.isTraceEnabled()) {
logger.trace("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.trace("Loading default Java Activation Framework FileTypeMap");
}
return FileTypeMap.getDefaultFileTypeMap();
}
public static MediaType getMediaType(String filename) {
String mediaType = fileTypeMap.getContentType(filename);
return (StringUtils.hasText(mediaType) ? MediaType.parseMediaType(mediaType) : null);
}
}
}