From 64d939bb16099fcb647850a5f3e04990f5562099 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Sat, 21 Jul 2012 06:16:42 -0400 Subject: [PATCH] Add ContentNegotiationManagerFactoryBean The new FactoryBean facilitates the creation of a ContentNegotiationManager in XML configuration. Issue: SPR-8420 --- .../ContentNegotiationManagerFactoryBean.java | 185 ++++++++++++++++++ ...entNegotiationManagerFactoryBeanTests.java | 125 ++++++++++++ .../ContentNegotiationConfigurer.java | 19 +- .../web/servlet/config/MvcNamespaceTests.java | 2 +- ...mvc-config-content-negotiation-manager.xml | 23 +-- src/reference/docbook/mvc.xml | 27 ++- 6 files changed, 342 insertions(+), 39 deletions(-) create mode 100644 spring-web/src/main/java/org/springframework/web/accept/ContentNegotiationManagerFactoryBean.java create mode 100644 spring-web/src/test/java/org/springframework/web/accept/ContentNegotiationManagerFactoryBeanTests.java 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 new file mode 100644 index 0000000000..fbe1b71925 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/accept/ContentNegotiationManagerFactoryBean.java @@ -0,0 +1,185 @@ +/* + * 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.web.accept; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Properties; + +import javax.servlet.ServletContext; + +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.http.MediaType; +import org.springframework.util.CollectionUtils; + +/** + * A factory providing convenient access to a {@code ContentNegotiationManager} + * configured with one or more {@link ContentNegotiationStrategy} instances. + * + *

By default strategies for checking the extension of the request path and + * the {@code Accept} header are registered. The path extension check will perform + * lookups through the {@link ServletContext} and the Java Activation Framework + * (if present) unless {@linkplain #setMediaTypes(Map) media types} are configured. + * + * @author Rossen Stoyanchev + * @since 3.2 + */ +public class ContentNegotiationManagerFactoryBean implements FactoryBean, InitializingBean { + + private boolean favorPathExtension = true; + + private boolean favorParameter = false; + + private boolean ignoreAcceptHeader = false; + + private Map mediaTypes = new HashMap(); + + private Boolean useJaf; + + private String parameterName; + + private MediaType defaultContentType; + + private ContentNegotiationManager contentNegotiationManager; + + /** + * Indicate whether the extension of the request path should be used to determine + * the requested media type with the highest priority. + *

By default this value is set to {@code true} in which case a request + * for {@code /hotels.pdf} will be interpreted as a request for + * {@code "application/pdf"} regardless of the {@code Accept} header. + */ + public void setFavorPathExtension(boolean favorPathExtension) { + this.favorPathExtension = favorPathExtension; + } + + /** + * Add mappings from file extensions to media types. + *

If this property is not set, the Java Action Framework, if available, may + * still be used in conjunction with {@link #setFavorPathExtension(boolean)}. + */ + public void setMediaTypes(Properties mediaTypes) { + if (!CollectionUtils.isEmpty(mediaTypes)) { + for (Map.Entry entry : mediaTypes.entrySet()) { + String extension = ((String) entry.getKey()).toLowerCase(Locale.ENGLISH); + this.mediaTypes.put(extension, MediaType.valueOf((String) entry.getValue())); + } + } + } + + /** + * Indicate whether to use the Java Activation Framework as a fallback option + * to map from file extensions to media types. This is used only when + * {@link #setFavorPathExtension(boolean)} is set to {@code true}. + *

The default value is {@code true}. + * @see #parameterName + * @see #setMediaTypes(Map) + */ + public void setUseJaf(boolean useJaf) { + this.useJaf = useJaf; + } + + /** + * Indicate whether a request parameter should be used to determine the + * requested media type with the 2nd highest priority, i.e. + * after path extensions but before the {@code Accept} header. + *

The default value is {@code false}. If set to to {@code true}, a request + * for {@code /hotels?format=pdf} will be interpreted as a request for + * {@code "application/pdf"} regardless of the {@code Accept} header. + *

To use this option effectively you must also configure the MediaType + * type mappings via {@link #setMediaTypes(Map)}. + * @see #setParameterName(String) + */ + public void setFavorParameter(boolean favorParameter) { + this.favorParameter = favorParameter; + } + + /** + * 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"}. + */ + public void setParameterName(String parameterName) { + this.parameterName = parameterName; + } + + /** + * Indicate whether the HTTP {@code Accept} header should be ignored altogether. + * If set the {@code Accept} header is checked at the + * 3rd highest priority, i.e. after the request path extension and + * possibly a request parameter if configured. + *

By default this value is set to {@code false}. + */ + public void setIgnoreAcceptHeader(boolean ignoreAcceptHeader) { + this.ignoreAcceptHeader = ignoreAcceptHeader; + } + + /** + * Set the default content type. + *

This content type will be used when neither the request path extension, + * nor a request parameter, nor the {@code Accept} header could help determine + * the requested content type. + */ + public void setDefaultContentType(MediaType defaultContentType) { + this.defaultContentType = defaultContentType; + } + + public void afterPropertiesSet() throws Exception { + List strategies = new ArrayList(); + + 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 Class getObjectType() { + return ContentNegotiationManager.class; + } + + public boolean isSingleton() { + return true; + } + + public ContentNegotiationManager getObject() throws Exception { + return this.contentNegotiationManager; + } + +} 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 new file mode 100644 index 0000000000..cbdb2c8ae7 --- /dev/null +++ b/spring-web/src/test/java/org/springframework/web/accept/ContentNegotiationManagerFactoryBeanTests.java @@ -0,0 +1,125 @@ +/* + * 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.web.accept; + +import static org.junit.Assert.assertEquals; + +import java.util.Arrays; +import java.util.Collections; +import java.util.Properties; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.context.request.ServletWebRequest; + +/** + * Test fixture for {@link ContentNegotiationManagerFactoryBean} tests. + * @author Rossen Stoyanchev + */ +public class ContentNegotiationManagerFactoryBeanTests { + + private ContentNegotiationManagerFactoryBean factoryBean; + + private NativeWebRequest webRequest; + + private MockHttpServletRequest servletRequest; + + @Before + public void setup() { + this.factoryBean = new ContentNegotiationManagerFactoryBean(); + this.servletRequest = new MockHttpServletRequest(); + this.webRequest = new ServletWebRequest(this.servletRequest); + } + + @Test + public void defaultSettings() throws Exception { + this.factoryBean.afterPropertiesSet(); + ContentNegotiationManager manager = this.factoryBean.getObject(); + + this.servletRequest.setRequestURI("/flower.gif"); + + assertEquals("Should be able to resolve file extensions by default", + Arrays.asList(MediaType.IMAGE_GIF), manager.resolveMediaTypes(this.webRequest)); + + this.servletRequest.setRequestURI("/flower?format=gif"); + this.servletRequest.addParameter("format", "gif"); + + assertEquals("Should not resolve request parameters by default", + Collections.emptyList(), manager.resolveMediaTypes(this.webRequest)); + + this.servletRequest.setRequestURI("/flower"); + this.servletRequest.addHeader("Accept", MediaType.IMAGE_GIF_VALUE); + + assertEquals("Should resolve Accept header by default", + Arrays.asList(MediaType.IMAGE_GIF), manager.resolveMediaTypes(this.webRequest)); + } + + @Test + public void addMediaTypes() throws Exception { + Properties mediaTypes = new Properties(); + mediaTypes.put("json", MediaType.APPLICATION_JSON_VALUE); + this.factoryBean.setMediaTypes(mediaTypes); + + this.factoryBean.afterPropertiesSet(); + ContentNegotiationManager manager = this.factoryBean.getObject(); + + this.servletRequest.setRequestURI("/flower.json"); + assertEquals(Arrays.asList(MediaType.APPLICATION_JSON), manager.resolveMediaTypes(this.webRequest)); + } + + @Test + public void favorParameter() throws Exception { + this.factoryBean.setFavorParameter(true); + this.factoryBean.setParameterName("f"); + + Properties mediaTypes = new Properties(); + mediaTypes.put("json", MediaType.APPLICATION_JSON_VALUE); + this.factoryBean.setMediaTypes(mediaTypes); + + this.factoryBean.afterPropertiesSet(); + ContentNegotiationManager manager = this.factoryBean.getObject(); + + this.servletRequest.setRequestURI("/flower"); + this.servletRequest.addParameter("f", "json"); + + assertEquals(Arrays.asList(MediaType.APPLICATION_JSON), manager.resolveMediaTypes(this.webRequest)); + } + + @Test + public void ignoreAcceptHeader() throws Exception { + this.factoryBean.setIgnoreAcceptHeader(true); + this.factoryBean.afterPropertiesSet(); + ContentNegotiationManager manager = this.factoryBean.getObject(); + + this.servletRequest.setRequestURI("/flower"); + this.servletRequest.addHeader("Accept", MediaType.IMAGE_GIF_VALUE); + + assertEquals(Collections.emptyList(), manager.resolveMediaTypes(this.webRequest)); + } + + @Test + public void setDefaultContentType() throws Exception { + this.factoryBean.setDefaultContentType(MediaType.APPLICATION_JSON); + this.factoryBean.afterPropertiesSet(); + ContentNegotiationManager manager = this.factoryBean.getObject(); + + assertEquals(Arrays.asList(MediaType.APPLICATION_JSON), manager.resolveMediaTypes(this.webRequest)); + } + +} 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 8468a365b3..2fb32c6bde 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 @@ -35,11 +35,10 @@ import org.springframework.web.accept.PathExtensionContentNegotiationStrategy; /** * Helps with configuring a {@link ContentNegotiationManager}. * - *

By default the extension of the request path extension is checked first and - * the {@code Accept} is checked second. The path extension check will perform a - * look up in the media types configured via {@link #setMediaTypes(Map)} and - * will also fall back to {@link ServletContext} and the Java Activation Framework - * (if present). + *

By default strategies for checking the extension of the request path and + * the {@code Accept} header are registered. The path extension check will perform + * lookups through the {@link ServletContext} and the Java Activation Framework + * (if present) unless {@linkplain #setMediaTypes(Map) media types} are configured. * * @author Rossen Stoyanchev * @since 3.2 @@ -72,6 +71,16 @@ public class ContentNegotiationConfigurer { return this; } + /** + * Add mappings from file extensions to media types. + *

If this property is not set, the Java Action Framework, if available, may + * still be used in conjunction with {@link #setFavorPathExtension(boolean)}. + */ + public ContentNegotiationConfigurer addMediaType(String extension, MediaType mediaType) { + this.mediaTypes.put(extension, mediaType); + return this; + } + /** * Add mappings from file extensions to media types. *

If this property is not set, the Java Action Framework, if available, may diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/MvcNamespaceTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/MvcNamespaceTests.java index f5fa9469fe..bedc9ac477 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/MvcNamespaceTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/MvcNamespaceTests.java @@ -452,7 +452,7 @@ public class MvcNamespaceTests { @Test public void testCustomContentNegotiationManager() throws Exception { - loadBeanDefinitions("mvc-config-content-negotiation-manager.xml", 14); + loadBeanDefinitions("mvc-config-content-negotiation-manager.xml", 12); RequestMappingHandlerMapping mapping = appContext.getBean(RequestMappingHandlerMapping.class); ContentNegotiationManager manager = mapping.getContentNegotiationManager(); diff --git a/spring-webmvc/src/test/resources/org/springframework/web/servlet/config/mvc-config-content-negotiation-manager.xml b/spring-webmvc/src/test/resources/org/springframework/web/servlet/config/mvc-config-content-negotiation-manager.xml index 4fb4b48757..612999a52f 100644 --- a/spring-webmvc/src/test/resources/org/springframework/web/servlet/config/mvc-config-content-negotiation-manager.xml +++ b/spring-webmvc/src/test/resources/org/springframework/web/servlet/config/mvc-config-content-negotiation-manager.xml @@ -7,23 +7,12 @@ - - - - - - - + + + + xml=application/rss+xml + + - - - - - - - - - - diff --git a/src/reference/docbook/mvc.xml b/src/reference/docbook/mvc.xml index d3d91a63d6..09cd747815 100644 --- a/src/reference/docbook/mvc.xml +++ b/src/reference/docbook/mvc.xml @@ -4503,25 +4503,20 @@ public class WebConfig extends WebMvcConfigurerAdapter { } } - In XML you'll need to use the content-negotiation-manager property: + In XML you'll need to use the content-negotiation-manager + property of annotation-driven: <mvc:annotation-driven content-negotiation-manager="contentNegotiationManager" /> -<bean id="contentNegotiationManager" class="org.springframework.web.accept.ContentNegotiationManager"> - <constructor-arg> - <list> - <ref bean="pathExtensionStrategy" /> - <bean id="headerStrategy" class="org.springframework.web.accept.HeaderContentNegotiationStrategy"/> - </list> - </constructor-arg> -</bean> - -<bean id="pathExtensionStrategy" class="org.springframework.web.accept.PathExtensionContentNegotiationStrategy"> - <constructor-arg> - <map> - <entry key="xml" value="application/rss+xml" /> - </map> - </constructor-arg> +<bean id="contentNegotiationManager" class="org.springframework.web.accept.ContentNegotiationManagerFactoryBean"> + <property name="favorPathExtension" value="false" /> + <property name="favorParameter" value="true" /> + <property name="mediaTypes" > + <value> + json=application/json + xml=application/xml + </value> + </property> </bean>