diff --git a/spring-web/src/main/java/org/springframework/web/accept/ContentNegotiationManagerFactoryBean.java b/spring-web/src/main/java/org/springframework/web/accept/ContentNegotiationManagerFactoryBean.java
index 6e4faa3dc3..5fa34b9386 100644
--- a/spring-web/src/main/java/org/springframework/web/accept/ContentNegotiationManagerFactoryBean.java
+++ b/spring-web/src/main/java/org/springframework/web/accept/ContentNegotiationManagerFactoryBean.java
@@ -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.
+ *
+ *
As of 5.0 you can set the exact strategies to use via
+ * {@link #setStrategies(List)}.
+ *
+ *
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:
*
*
*
@@ -73,17 +78,12 @@ import org.springframework.web.context.ServletContextAware;
*
*
*
- * 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.
- *
- *
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".
- *
- *
The path extension strategy will also use {@link ServletContext#getMimeType}
- * and {@link MediaTypeFactory} to resolve a path extension to a MediaType.
+ * Note: 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, ServletContextAware, InitializingBean {
+ @Nullable
+ private List 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.
+ * Note: 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 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 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);
diff --git a/spring-web/src/test/java/org/springframework/web/accept/ContentNegotiationManagerFactoryBeanTests.java b/spring-web/src/test/java/org/springframework/web/accept/ContentNegotiationManagerFactoryBeanTests.java
index e4bdd55323..d78c691b1b 100644
--- a/spring-web/src/test/java/org/springframework/web/accept/ContentNegotiationManagerFactoryBeanTests.java
+++ b/spring-web/src/test/java/org/springframework/web/accept/ContentNegotiationManagerFactoryBeanTests.java
@@ -89,6 +89,25 @@ public class ContentNegotiationManagerFactoryBeanTests {
Collections.singletonList(MediaType.IMAGE_GIF), manager.resolveMediaTypes(this.webRequest));
}
+ @Test
+ public void explicitStrategies() throws Exception {
+ Map mediaTypes = Collections.singletonMap("bar", new MediaType("application", "bar"));
+ ParameterContentNegotiationStrategy strategy1 = new ParameterContentNegotiationStrategy(mediaTypes);
+ HeaderContentNegotiationStrategy strategy2 = new HeaderContentNegotiationStrategy();
+ List 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);
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ContentNegotiationConfigurer.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ContentNegotiationConfigurer.java
index b5ff2cb608..e97e168632 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ContentNegotiationConfigurer.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ContentNegotiationConfigurer.java
@@ -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.
+ *
+ * As of 5.0 you can set the exact strategies to use via
+ * {@link #strategies(List)}.
+ *
+ *
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:
*
*
*
@@ -74,14 +80,12 @@ import org.springframework.web.accept.PathExtensionContentNegotiationStrategy;
* The order in which strategies are configured is fixed. You can only turn
* them on or off.
*
- *
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".
- *
- *
The path extension strategy will also use {@link ServletContext#getMimeType}
- * and the {@link MediaTypeFactory} to resolve a path
- * extension to a MediaType.
+ * Note: 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.
+ *
Note: 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 strategies) {
+ this.factory.setStrategies(strategies);
+ }
+
/**
* Whether the path extension in the URL path should be used to determine
* the requested media type.
diff --git a/src/docs/asciidoc/web/web-mvc.adoc b/src/docs/asciidoc/web/web-mvc.adoc
index c5d3338b58..5906d1446d 100644
--- a/src/docs/asciidoc/web/web-mvc.adoc
+++ b/src/docs/asciidoc/web/web-mvc.adoc
@@ -5198,10 +5198,14 @@ And in XML use the `` 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