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