Skip to content

Commit 64d939b

Browse files
committed
Add ContentNegotiationManagerFactoryBean
The new FactoryBean facilitates the creation of a ContentNegotiationManager in XML configuration. Issue: SPR-8420
1 parent 028e15f commit 64d939b

File tree

6 files changed

+342
-39
lines changed

6 files changed

+342
-39
lines changed
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
/*
2+
* Copyright 2002-2012 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.web.accept;
17+
18+
import java.util.ArrayList;
19+
import java.util.HashMap;
20+
import java.util.List;
21+
import java.util.Locale;
22+
import java.util.Map;
23+
import java.util.Properties;
24+
25+
import javax.servlet.ServletContext;
26+
27+
import org.springframework.beans.factory.FactoryBean;
28+
import org.springframework.beans.factory.InitializingBean;
29+
import org.springframework.http.MediaType;
30+
import org.springframework.util.CollectionUtils;
31+
32+
/**
33+
* A factory providing convenient access to a {@code ContentNegotiationManager}
34+
* configured with one or more {@link ContentNegotiationStrategy} instances.
35+
*
36+
* <p>By default strategies for checking the extension of the request path and
37+
* the {@code Accept} header are registered. The path extension check will perform
38+
* lookups through the {@link ServletContext} and the Java Activation Framework
39+
* (if present) unless {@linkplain #setMediaTypes(Map) media types} are configured.
40+
*
41+
* @author Rossen Stoyanchev
42+
* @since 3.2
43+
*/
44+
public class ContentNegotiationManagerFactoryBean implements FactoryBean<ContentNegotiationManager>, InitializingBean {
45+
46+
private boolean favorPathExtension = true;
47+
48+
private boolean favorParameter = false;
49+
50+
private boolean ignoreAcceptHeader = false;
51+
52+
private Map<String, MediaType> mediaTypes = new HashMap<String, MediaType>();
53+
54+
private Boolean useJaf;
55+
56+
private String parameterName;
57+
58+
private MediaType defaultContentType;
59+
60+
private ContentNegotiationManager contentNegotiationManager;
61+
62+
/**
63+
* Indicate whether the extension of the request path should be used to determine
64+
* the requested media type with the <em>highest priority</em>.
65+
* <p>By default this value is set to {@code true} in which case a request
66+
* for {@code /hotels.pdf} will be interpreted as a request for
67+
* {@code "application/pdf"} regardless of the {@code Accept} header.
68+
*/
69+
public void setFavorPathExtension(boolean favorPathExtension) {
70+
this.favorPathExtension = favorPathExtension;
71+
}
72+
73+
/**
74+
* Add mappings from file extensions to media types.
75+
* <p>If this property is not set, the Java Action Framework, if available, may
76+
* still be used in conjunction with {@link #setFavorPathExtension(boolean)}.
77+
*/
78+
public void setMediaTypes(Properties mediaTypes) {
79+
if (!CollectionUtils.isEmpty(mediaTypes)) {
80+
for (Map.Entry<Object, Object> entry : mediaTypes.entrySet()) {
81+
String extension = ((String) entry.getKey()).toLowerCase(Locale.ENGLISH);
82+
this.mediaTypes.put(extension, MediaType.valueOf((String) entry.getValue()));
83+
}
84+
}
85+
}
86+
87+
/**
88+
* Indicate whether to use the Java Activation Framework as a fallback option
89+
* to map from file extensions to media types. This is used only when
90+
* {@link #setFavorPathExtension(boolean)} is set to {@code true}.
91+
* <p>The default value is {@code true}.
92+
* @see #parameterName
93+
* @see #setMediaTypes(Map)
94+
*/
95+
public void setUseJaf(boolean useJaf) {
96+
this.useJaf = useJaf;
97+
}
98+
99+
/**
100+
* Indicate whether a request parameter should be used to determine the
101+
* requested media type with the <em>2nd highest priority</em>, i.e.
102+
* after path extensions but before the {@code Accept} header.
103+
* <p>The default value is {@code false}. If set to to {@code true}, a request
104+
* for {@code /hotels?format=pdf} will be interpreted as a request for
105+
* {@code "application/pdf"} regardless of the {@code Accept} header.
106+
* <p>To use this option effectively you must also configure the MediaType
107+
* type mappings via {@link #setMediaTypes(Map)}.
108+
* @see #setParameterName(String)
109+
*/
110+
public void setFavorParameter(boolean favorParameter) {
111+
this.favorParameter = favorParameter;
112+
}
113+
114+
/**
115+
* Set the parameter name that can be used to determine the requested media type
116+
* if the {@link #setFavorParameter} property is {@code true}.
117+
* <p>The default parameter name is {@code "format"}.
118+
*/
119+
public void setParameterName(String parameterName) {
120+
this.parameterName = parameterName;
121+
}
122+
123+
/**
124+
* Indicate whether the HTTP {@code Accept} header should be ignored altogether.
125+
* If set the {@code Accept} header is checked at the
126+
* <em>3rd highest priority</em>, i.e. after the request path extension and
127+
* possibly a request parameter if configured.
128+
* <p>By default this value is set to {@code false}.
129+
*/
130+
public void setIgnoreAcceptHeader(boolean ignoreAcceptHeader) {
131+
this.ignoreAcceptHeader = ignoreAcceptHeader;
132+
}
133+
134+
/**
135+
* Set the default content type.
136+
* <p>This content type will be used when neither the request path extension,
137+
* nor a request parameter, nor the {@code Accept} header could help determine
138+
* the requested content type.
139+
*/
140+
public void setDefaultContentType(MediaType defaultContentType) {
141+
this.defaultContentType = defaultContentType;
142+
}
143+
144+
public void afterPropertiesSet() throws Exception {
145+
List<ContentNegotiationStrategy> strategies = new ArrayList<ContentNegotiationStrategy>();
146+
147+
if (this.favorPathExtension) {
148+
PathExtensionContentNegotiationStrategy strategy = new PathExtensionContentNegotiationStrategy(this.mediaTypes);
149+
if (this.useJaf != null) {
150+
strategy.setUseJaf(this.useJaf);
151+
}
152+
strategies.add(strategy);
153+
}
154+
155+
if (this.favorParameter) {
156+
ParameterContentNegotiationStrategy strategy = new ParameterContentNegotiationStrategy(this.mediaTypes);
157+
strategy.setParameterName(this.parameterName);
158+
strategies.add(strategy);
159+
}
160+
161+
if (!this.ignoreAcceptHeader) {
162+
strategies.add(new HeaderContentNegotiationStrategy());
163+
}
164+
165+
if (this.defaultContentType != null) {
166+
strategies.add(new FixedContentNegotiationStrategy(this.defaultContentType));
167+
}
168+
169+
ContentNegotiationStrategy[] array = strategies.toArray(new ContentNegotiationStrategy[strategies.size()]);
170+
this.contentNegotiationManager = new ContentNegotiationManager(array);
171+
}
172+
173+
public Class<?> getObjectType() {
174+
return ContentNegotiationManager.class;
175+
}
176+
177+
public boolean isSingleton() {
178+
return true;
179+
}
180+
181+
public ContentNegotiationManager getObject() throws Exception {
182+
return this.contentNegotiationManager;
183+
}
184+
185+
}
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
/*
2+
* Copyright 2002-2012 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.web.accept;
17+
18+
import static org.junit.Assert.assertEquals;
19+
20+
import java.util.Arrays;
21+
import java.util.Collections;
22+
import java.util.Properties;
23+
24+
import org.junit.Before;
25+
import org.junit.Test;
26+
import org.springframework.http.MediaType;
27+
import org.springframework.mock.web.MockHttpServletRequest;
28+
import org.springframework.web.context.request.NativeWebRequest;
29+
import org.springframework.web.context.request.ServletWebRequest;
30+
31+
/**
32+
* Test fixture for {@link ContentNegotiationManagerFactoryBean} tests.
33+
* @author Rossen Stoyanchev
34+
*/
35+
public class ContentNegotiationManagerFactoryBeanTests {
36+
37+
private ContentNegotiationManagerFactoryBean factoryBean;
38+
39+
private NativeWebRequest webRequest;
40+
41+
private MockHttpServletRequest servletRequest;
42+
43+
@Before
44+
public void setup() {
45+
this.factoryBean = new ContentNegotiationManagerFactoryBean();
46+
this.servletRequest = new MockHttpServletRequest();
47+
this.webRequest = new ServletWebRequest(this.servletRequest);
48+
}
49+
50+
@Test
51+
public void defaultSettings() throws Exception {
52+
this.factoryBean.afterPropertiesSet();
53+
ContentNegotiationManager manager = this.factoryBean.getObject();
54+
55+
this.servletRequest.setRequestURI("/flower.gif");
56+
57+
assertEquals("Should be able to resolve file extensions by default",
58+
Arrays.asList(MediaType.IMAGE_GIF), manager.resolveMediaTypes(this.webRequest));
59+
60+
this.servletRequest.setRequestURI("/flower?format=gif");
61+
this.servletRequest.addParameter("format", "gif");
62+
63+
assertEquals("Should not resolve request parameters by default",
64+
Collections.emptyList(), manager.resolveMediaTypes(this.webRequest));
65+
66+
this.servletRequest.setRequestURI("/flower");
67+
this.servletRequest.addHeader("Accept", MediaType.IMAGE_GIF_VALUE);
68+
69+
assertEquals("Should resolve Accept header by default",
70+
Arrays.asList(MediaType.IMAGE_GIF), manager.resolveMediaTypes(this.webRequest));
71+
}
72+
73+
@Test
74+
public void addMediaTypes() throws Exception {
75+
Properties mediaTypes = new Properties();
76+
mediaTypes.put("json", MediaType.APPLICATION_JSON_VALUE);
77+
this.factoryBean.setMediaTypes(mediaTypes);
78+
79+
this.factoryBean.afterPropertiesSet();
80+
ContentNegotiationManager manager = this.factoryBean.getObject();
81+
82+
this.servletRequest.setRequestURI("/flower.json");
83+
assertEquals(Arrays.asList(MediaType.APPLICATION_JSON), manager.resolveMediaTypes(this.webRequest));
84+
}
85+
86+
@Test
87+
public void favorParameter() throws Exception {
88+
this.factoryBean.setFavorParameter(true);
89+
this.factoryBean.setParameterName("f");
90+
91+
Properties mediaTypes = new Properties();
92+
mediaTypes.put("json", MediaType.APPLICATION_JSON_VALUE);
93+
this.factoryBean.setMediaTypes(mediaTypes);
94+
95+
this.factoryBean.afterPropertiesSet();
96+
ContentNegotiationManager manager = this.factoryBean.getObject();
97+
98+
this.servletRequest.setRequestURI("/flower");
99+
this.servletRequest.addParameter("f", "json");
100+
101+
assertEquals(Arrays.asList(MediaType.APPLICATION_JSON), manager.resolveMediaTypes(this.webRequest));
102+
}
103+
104+
@Test
105+
public void ignoreAcceptHeader() throws Exception {
106+
this.factoryBean.setIgnoreAcceptHeader(true);
107+
this.factoryBean.afterPropertiesSet();
108+
ContentNegotiationManager manager = this.factoryBean.getObject();
109+
110+
this.servletRequest.setRequestURI("/flower");
111+
this.servletRequest.addHeader("Accept", MediaType.IMAGE_GIF_VALUE);
112+
113+
assertEquals(Collections.emptyList(), manager.resolveMediaTypes(this.webRequest));
114+
}
115+
116+
@Test
117+
public void setDefaultContentType() throws Exception {
118+
this.factoryBean.setDefaultContentType(MediaType.APPLICATION_JSON);
119+
this.factoryBean.afterPropertiesSet();
120+
ContentNegotiationManager manager = this.factoryBean.getObject();
121+
122+
assertEquals(Arrays.asList(MediaType.APPLICATION_JSON), manager.resolveMediaTypes(this.webRequest));
123+
}
124+
125+
}

spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ContentNegotiationConfigurer.java

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,10 @@
3535
/**
3636
* Helps with configuring a {@link ContentNegotiationManager}.
3737
*
38-
* <p>By default the extension of the request path extension is checked first and
39-
* the {@code Accept} is checked second. The path extension check will perform a
40-
* look up in the media types configured via {@link #setMediaTypes(Map)} and
41-
* will also fall back to {@link ServletContext} and the Java Activation Framework
42-
* (if present).
38+
* <p>By default strategies for checking the extension of the request path and
39+
* the {@code Accept} header are registered. The path extension check will perform
40+
* lookups through the {@link ServletContext} and the Java Activation Framework
41+
* (if present) unless {@linkplain #setMediaTypes(Map) media types} are configured.
4342
*
4443
* @author Rossen Stoyanchev
4544
* @since 3.2
@@ -72,6 +71,16 @@ public ContentNegotiationConfigurer setFavorPathExtension(boolean favorPathExten
7271
return this;
7372
}
7473

74+
/**
75+
* Add mappings from file extensions to media types.
76+
* <p>If this property is not set, the Java Action Framework, if available, may
77+
* still be used in conjunction with {@link #setFavorPathExtension(boolean)}.
78+
*/
79+
public ContentNegotiationConfigurer addMediaType(String extension, MediaType mediaType) {
80+
this.mediaTypes.put(extension, mediaType);
81+
return this;
82+
}
83+
7584
/**
7685
* Add mappings from file extensions to media types.
7786
* <p>If this property is not set, the Java Action Framework, if available, may

spring-webmvc/src/test/java/org/springframework/web/servlet/config/MvcNamespaceTests.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -452,7 +452,7 @@ public void testViewControllersDefaultConfig() {
452452

453453
@Test
454454
public void testCustomContentNegotiationManager() throws Exception {
455-
loadBeanDefinitions("mvc-config-content-negotiation-manager.xml", 14);
455+
loadBeanDefinitions("mvc-config-content-negotiation-manager.xml", 12);
456456

457457
RequestMappingHandlerMapping mapping = appContext.getBean(RequestMappingHandlerMapping.class);
458458
ContentNegotiationManager manager = mapping.getContentNegotiationManager();

spring-webmvc/src/test/resources/org/springframework/web/servlet/config/mvc-config-content-negotiation-manager.xml

Lines changed: 6 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,23 +7,12 @@
77

88
<mvc:annotation-driven content-negotiation-manager="contentNegotiationManager" />
99

10-
<bean id="contentNegotiationManager" class="org.springframework.web.accept.ContentNegotiationManager">
11-
<constructor-arg>
12-
<list>
13-
<ref bean="pathExtensionStrategy" />
14-
<ref bean="headerStrategy" />
15-
</list>
16-
</constructor-arg>
10+
<bean id="contentNegotiationManager" class="org.springframework.web.accept.ContentNegotiationManagerFactoryBean">
11+
<property name="mediaTypes">
12+
<value>
13+
xml=application/rss+xml
14+
</value>
15+
</property>
1716
</bean>
1817

19-
<bean id="pathExtensionStrategy" class="org.springframework.web.accept.PathExtensionContentNegotiationStrategy">
20-
<constructor-arg>
21-
<map>
22-
<entry key="xml" value="application/rss+xml" />
23-
</map>
24-
</constructor-arg>
25-
</bean>
26-
27-
<bean id="headerStrategy" class="org.springframework.web.accept.HeaderContentNegotiationStrategy" />
28-
2918
</beans>

0 commit comments

Comments
 (0)