Set all strategies in CNM factory bean

ContentNegotiationManagerFactoryBean now provides an option to
explicitly set the strategies to use vs customizing a fixed
list of default strategies.

Issue: SPR-11114
This commit is contained in:
Rossen Stoyanchev 2017-07-11 21:00:57 +02:00
parent befacf4a35
commit 134ceac58e
4 changed files with 119 additions and 59 deletions

View File

@ -36,9 +36,14 @@ import org.springframework.web.context.ServletContextAware;
/**
* Factory to create a {@code ContentNegotiationManager} and configure it with
* one or more {@link ContentNegotiationStrategy} instances via simple setters.
* The following table shows setters, resulting strategy instances, and if in
* use by default:
* one or more {@link ContentNegotiationStrategy} instances.
*
* <p>As of 5.0 you can set the exact strategies to use via
* {@link #setStrategies(List)}.
*
* <p>As an alternative you can also rely on the set of defaults described below
* which can be turned on or off or customized through the methods of this
* builder:
*
* <table>
* <tr>
@ -73,17 +78,12 @@ import org.springframework.web.context.ServletContextAware;
* </tr>
* </table>
*
* <p>The order in which strategies are configured is fixed. Setters may only
* turn individual strategies on or off. If you need a custom order for any
* reason simply instantiate {@code ContentNegotiationManager} directly.
*
* <p>For the path extension and parameter strategies you may explicitly add
* {@link #setMediaTypes MediaType mappings}. This will be used to resolve path
* extensions or a parameter value such as "json" to a media type such as
* "application/json".
*
* <p>The path extension strategy will also use {@link ServletContext#getMimeType}
* and {@link MediaTypeFactory} to resolve a path extension to a MediaType.
* <strong>Note:</strong> if you must use URL-based content type resolution,
* the use of a query parameter is simpler and preferable to the use of a path
* extension since the latter can cause issues with URI variables, path
* parameters, and URI decoding. Consider setting {@link #setFavorPathExtension}
* to {@literal false} or otherwise set the strategies to use explicitly via
* {@link #setStrategies(List)}.
*
* @author Rossen Stoyanchev
* @since 3.2
@ -91,6 +91,10 @@ import org.springframework.web.context.ServletContextAware;
public class ContentNegotiationManagerFactoryBean
implements FactoryBean<ContentNegotiationManager>, ServletContextAware, InitializingBean {
@Nullable
private List<ContentNegotiationStrategy> strategies;
private boolean favorPathExtension = true;
private boolean favorParameter = false;
@ -116,6 +120,18 @@ public class ContentNegotiationManagerFactoryBean
private ServletContext servletContext;
/**
* Set the exact list of strategies to use.
* <p><strong>Note:</strong> use of this method is mutually exclusive with
* use of all other setters in this class which customize a default, fixed
* set of strategies. See class level doc for more details.
* @param strategies the strategies to use
* @since 5.0
*/
public void setStrategies(@Nullable List<ContentNegotiationStrategy> strategies) {
this.strategies = (strategies != null ? new ArrayList<>(strategies) : null);
}
/**
* Whether the path extension in the URL path should be used to determine
* the requested media type.
@ -280,41 +296,46 @@ public class ContentNegotiationManagerFactoryBean
public ContentNegotiationManager build() {
List<ContentNegotiationStrategy> strategies = new ArrayList<>();
if (this.favorPathExtension) {
PathExtensionContentNegotiationStrategy strategy;
if (this.servletContext != null && !useRegisteredExtensionsOnly()) {
strategy = new ServletPathExtensionContentNegotiationStrategy(
this.servletContext, this.mediaTypes);
}
else {
strategy = new PathExtensionContentNegotiationStrategy(this.mediaTypes);
}
strategy.setIgnoreUnknownExtensions(this.ignoreUnknownPathExtensions);
if (this.useRegisteredExtensionsOnly != null) {
strategy.setUseRegisteredExtensionsOnly(this.useRegisteredExtensionsOnly);
}
strategies.add(strategy);
if (this.strategies != null) {
strategies.addAll(this.strategies);
}
if (this.favorParameter) {
ParameterContentNegotiationStrategy strategy =
new ParameterContentNegotiationStrategy(this.mediaTypes);
strategy.setParameterName(this.parameterName);
if (this.useRegisteredExtensionsOnly != null) {
strategy.setUseRegisteredExtensionsOnly(this.useRegisteredExtensionsOnly);
else {
if (this.favorPathExtension) {
PathExtensionContentNegotiationStrategy strategy;
if (this.servletContext != null && !useRegisteredExtensionsOnly()) {
strategy = new ServletPathExtensionContentNegotiationStrategy(
this.servletContext, this.mediaTypes);
}
else {
strategy = new PathExtensionContentNegotiationStrategy(this.mediaTypes);
}
strategy.setIgnoreUnknownExtensions(this.ignoreUnknownPathExtensions);
if (this.useRegisteredExtensionsOnly != null) {
strategy.setUseRegisteredExtensionsOnly(this.useRegisteredExtensionsOnly);
}
strategies.add(strategy);
}
else {
strategy.setUseRegisteredExtensionsOnly(true); // backwards compatibility
if (this.favorParameter) {
ParameterContentNegotiationStrategy strategy =
new ParameterContentNegotiationStrategy(this.mediaTypes);
strategy.setParameterName(this.parameterName);
if (this.useRegisteredExtensionsOnly != null) {
strategy.setUseRegisteredExtensionsOnly(this.useRegisteredExtensionsOnly);
}
else {
strategy.setUseRegisteredExtensionsOnly(true); // backwards compatibility
}
strategies.add(strategy);
}
strategies.add(strategy);
}
if (!this.ignoreAcceptHeader) {
strategies.add(new HeaderContentNegotiationStrategy());
}
if (!this.ignoreAcceptHeader) {
strategies.add(new HeaderContentNegotiationStrategy());
}
if (this.defaultNegotiationStrategy != null) {
strategies.add(this.defaultNegotiationStrategy);
if (this.defaultNegotiationStrategy != null) {
strategies.add(this.defaultNegotiationStrategy);
}
}
this.contentNegotiationManager = new ContentNegotiationManager(strategies);

View File

@ -89,6 +89,25 @@ public class ContentNegotiationManagerFactoryBeanTests {
Collections.singletonList(MediaType.IMAGE_GIF), manager.resolveMediaTypes(this.webRequest));
}
@Test
public void explicitStrategies() throws Exception {
Map<String, MediaType> mediaTypes = Collections.singletonMap("bar", new MediaType("application", "bar"));
ParameterContentNegotiationStrategy strategy1 = new ParameterContentNegotiationStrategy(mediaTypes);
HeaderContentNegotiationStrategy strategy2 = new HeaderContentNegotiationStrategy();
List<ContentNegotiationStrategy> strategies = Arrays.asList(strategy1, strategy2);
this.factoryBean.setStrategies(strategies);
this.factoryBean.afterPropertiesSet();
ContentNegotiationManager manager = this.factoryBean.getObject();
assertEquals(strategies, manager.getStrategies());
this.servletRequest.setRequestURI("/flower");
this.servletRequest.addParameter("format", "bar");
assertEquals(Collections.singletonList(new MediaType("application", "bar")),
manager.resolveMediaTypes(this.webRequest));
}
@Test
public void favorPath() throws Exception {
this.factoryBean.setFavorPathExtension(true);

View File

@ -18,6 +18,7 @@ package org.springframework.web.servlet.config.annotation;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.ServletContext;
@ -34,9 +35,14 @@ import org.springframework.web.accept.PathExtensionContentNegotiationStrategy;
/**
* Creates a {@code ContentNegotiationManager} and configures it with
* one or more {@link ContentNegotiationStrategy} instances. The following shows
* the resulting strategy instances, the methods used to configured them, and
* whether enabled by default:
* one or more {@link ContentNegotiationStrategy} instances.
*
* <p>As of 5.0 you can set the exact strategies to use via
* {@link #strategies(List)}.
*
* <p>As an alternative you can also rely on the set of defaults described below
* which can be turned on or off or customized through the methods of this
* builder:
*
* <table>
* <tr>
@ -74,14 +80,12 @@ import org.springframework.web.accept.PathExtensionContentNegotiationStrategy;
* <p>The order in which strategies are configured is fixed. You can only turn
* them on or off.
*
* <p>For the path extension and parameter strategies you may explicitly add
* {@link #mediaType MediaType mappings}. Those will be used to resolve path
* extensions and/or a query parameter value such as "json" to a concrete media
* type such as "application/json".
*
* <p>The path extension strategy will also use {@link ServletContext#getMimeType}
* and the {@link MediaTypeFactory} to resolve a path
* extension to a MediaType.
* <strong>Note:</strong> if you must use URL-based content type resolution,
* the use of a query parameter is simpler and preferable to the use of a path
* extension since the latter can cause issues with URI variables, path
* parameters, and URI decoding. Consider setting {@link #favorPathExtension}
* to {@literal false} or otherwise set the strategies to use explicitly via
* {@link #strategies(List)}.
*
* @author Rossen Stoyanchev
* @since 3.2
@ -103,6 +107,18 @@ public class ContentNegotiationConfigurer {
}
/**
* Set the exact list of strategies to use.
* <p><strong>Note:</strong> use of this method is mutually exclusive with
* use of all other setters in this class which customize a default, fixed
* set of strategies. See class level doc for more details.
* @param strategies the strategies to use
* @since 5.0
*/
public void strategies(@Nullable List<ContentNegotiationStrategy> strategies) {
this.factory.setStrategies(strategies);
}
/**
* Whether the path extension in the URL path should be used to determine
* the requested media type.

View File

@ -5198,10 +5198,14 @@ And in XML use the `<mvc:interceptors>` element:
[[mvc-config-content-negotiation]]
=== Content Negotiation
You can configure how Spring MVC determines the requested media types from the request.
The available options are to check the URL path for a file extension, check the
"Accept" header, a specific query parameter, or to fall back on a default content
type when nothing is requested. By default the path extension in the request URI
is checked first and the "Accept" header is checked second.
The available options are to check a query parameter, the URL path for a file extension,
the "Accept" header, use a fixed list, or a custom strategy.
By default for backwards compatibility the path extension in the request URI is checked
first and the "Accept" header is checked second. However if you must use URL-based content
type resolution, we highly recommend using the query parameter strategy over the path
extension since the latter can cause issues with URI variables, path parameters, and also
in combination with URI decoding.
The MVC Java config and the MVC namespace register `json`, `xml`, `rss`, `atom` by
default if corresponding dependencies are on the classpath. Additional