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 * Factory to create a {@code ContentNegotiationManager} and configure it with
* one or more {@link ContentNegotiationStrategy} instances via simple setters. * one or more {@link ContentNegotiationStrategy} instances.
* The following table shows setters, resulting strategy instances, and if in *
* use by default: * <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> * <table>
* <tr> * <tr>
@ -73,17 +78,12 @@ import org.springframework.web.context.ServletContextAware;
* </tr> * </tr>
* </table> * </table>
* *
* <p>The order in which strategies are configured is fixed. Setters may only * <strong>Note:</strong> if you must use URL-based content type resolution,
* turn individual strategies on or off. If you need a custom order for any * the use of a query parameter is simpler and preferable to the use of a path
* reason simply instantiate {@code ContentNegotiationManager} directly. * extension since the latter can cause issues with URI variables, path
* * parameters, and URI decoding. Consider setting {@link #setFavorPathExtension}
* <p>For the path extension and parameter strategies you may explicitly add * to {@literal false} or otherwise set the strategies to use explicitly via
* {@link #setMediaTypes MediaType mappings}. This will be used to resolve path * {@link #setStrategies(List)}.
* 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.
* *
* @author Rossen Stoyanchev * @author Rossen Stoyanchev
* @since 3.2 * @since 3.2
@ -91,6 +91,10 @@ import org.springframework.web.context.ServletContextAware;
public class ContentNegotiationManagerFactoryBean public class ContentNegotiationManagerFactoryBean
implements FactoryBean<ContentNegotiationManager>, ServletContextAware, InitializingBean { implements FactoryBean<ContentNegotiationManager>, ServletContextAware, InitializingBean {
@Nullable
private List<ContentNegotiationStrategy> strategies;
private boolean favorPathExtension = true; private boolean favorPathExtension = true;
private boolean favorParameter = false; private boolean favorParameter = false;
@ -116,6 +120,18 @@ public class ContentNegotiationManagerFactoryBean
private ServletContext servletContext; 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 * Whether the path extension in the URL path should be used to determine
* the requested media type. * the requested media type.
@ -280,41 +296,46 @@ public class ContentNegotiationManagerFactoryBean
public ContentNegotiationManager build() { public ContentNegotiationManager build() {
List<ContentNegotiationStrategy> strategies = new ArrayList<>(); List<ContentNegotiationStrategy> strategies = new ArrayList<>();
if (this.favorPathExtension) { if (this.strategies != null) {
PathExtensionContentNegotiationStrategy strategy; strategies.addAll(this.strategies);
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 {
if (this.favorParameter) { if (this.favorPathExtension) {
ParameterContentNegotiationStrategy strategy = PathExtensionContentNegotiationStrategy strategy;
new ParameterContentNegotiationStrategy(this.mediaTypes); if (this.servletContext != null && !useRegisteredExtensionsOnly()) {
strategy.setParameterName(this.parameterName); strategy = new ServletPathExtensionContentNegotiationStrategy(
if (this.useRegisteredExtensionsOnly != null) { this.servletContext, this.mediaTypes);
strategy.setUseRegisteredExtensionsOnly(this.useRegisteredExtensionsOnly); }
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) { if (!this.ignoreAcceptHeader) {
strategies.add(new HeaderContentNegotiationStrategy()); strategies.add(new HeaderContentNegotiationStrategy());
} }
if (this.defaultNegotiationStrategy != null) { if (this.defaultNegotiationStrategy != null) {
strategies.add(this.defaultNegotiationStrategy); strategies.add(this.defaultNegotiationStrategy);
}
} }
this.contentNegotiationManager = new ContentNegotiationManager(strategies); this.contentNegotiationManager = new ContentNegotiationManager(strategies);

View File

@ -89,6 +89,25 @@ public class ContentNegotiationManagerFactoryBeanTests {
Collections.singletonList(MediaType.IMAGE_GIF), manager.resolveMediaTypes(this.webRequest)); 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 @Test
public void favorPath() throws Exception { public void favorPath() throws Exception {
this.factoryBean.setFavorPathExtension(true); this.factoryBean.setFavorPathExtension(true);

View File

@ -18,6 +18,7 @@ package org.springframework.web.servlet.config.annotation;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import javax.servlet.ServletContext; import javax.servlet.ServletContext;
@ -34,9 +35,14 @@ import org.springframework.web.accept.PathExtensionContentNegotiationStrategy;
/** /**
* Creates a {@code ContentNegotiationManager} and configures it with * Creates a {@code ContentNegotiationManager} and configures it with
* one or more {@link ContentNegotiationStrategy} instances. The following shows * one or more {@link ContentNegotiationStrategy} instances.
* the resulting strategy instances, the methods used to configured them, and *
* whether enabled by default: * <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> * <table>
* <tr> * <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 * <p>The order in which strategies are configured is fixed. You can only turn
* them on or off. * them on or off.
* *
* <p>For the path extension and parameter strategies you may explicitly add * <strong>Note:</strong> if you must use URL-based content type resolution,
* {@link #mediaType MediaType mappings}. Those will be used to resolve path * the use of a query parameter is simpler and preferable to the use of a path
* extensions and/or a query parameter value such as "json" to a concrete media * extension since the latter can cause issues with URI variables, path
* type such as "application/json". * parameters, and URI decoding. Consider setting {@link #favorPathExtension}
* * to {@literal false} or otherwise set the strategies to use explicitly via
* <p>The path extension strategy will also use {@link ServletContext#getMimeType} * {@link #strategies(List)}.
* and the {@link MediaTypeFactory} to resolve a path
* extension to a MediaType.
* *
* @author Rossen Stoyanchev * @author Rossen Stoyanchev
* @since 3.2 * @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 * Whether the path extension in the URL path should be used to determine
* the requested media type. * the requested media type.

View File

@ -5198,10 +5198,14 @@ And in XML use the `<mvc:interceptors>` element:
[[mvc-config-content-negotiation]] [[mvc-config-content-negotiation]]
=== Content Negotiation === Content Negotiation
You can configure how Spring MVC determines the requested media types from the request. 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 The available options are to check a query parameter, the URL path for a file extension,
"Accept" header, a specific query parameter, or to fall back on a default content the "Accept" header, use a fixed list, or a custom strategy.
type when nothing is requested. By default the path extension in the request URI
is checked first and the "Accept" header is checked second. 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 The MVC Java config and the MVC namespace register `json`, `xml`, `rss`, `atom` by
default if corresponding dependencies are on the classpath. Additional default if corresponding dependencies are on the classpath. Additional