Skip to content

Commit 028e15f

Browse files
committed
Add options to configure content negotiation
The MVC Java config and the MVC namespace now support options to configure content negotiation. By default both support checking path extensions first and the "Accept" header second. For path extensions .json, .xml, .atom, and .rss are recognized out of the box if the Jackson, JAXB2, or Rome libraries are available. The ServletContext and the Java Activation Framework may be used as fallback options for path extension lookups. Issue: SPR-8420
1 parent 92759ed commit 028e15f

File tree

17 files changed

+638
-50
lines changed

17 files changed

+638
-50
lines changed

spring-web/src/main/java/org/springframework/web/accept/ContentNegotiationManager.java

+18-3
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,19 @@
3030

3131
/**
3232
* This class is used to determine the requested {@linkplain MediaType media types}
33-
* in a request by delegating to a list of {@link ContentNegotiationStrategy} instances.
33+
* of a request by delegating to a list of ContentNegotiationStrategy instances.
34+
* The strategies must be provided at instantiation or alternatively if using
35+
* the default constructor, an instance of {@link HeaderContentNegotiationStrategy}
36+
* will be configured by default.
3437
*
35-
* <p>It may also be used to determine the extensions associated with a MediaType by
36-
* delegating to a list of {@link MediaTypeFileExtensionResolver} instances.
38+
* <p>This class may also be used to look up file extensions associated with a
39+
* MediaType. This is done by consulting the list of configured
40+
* {@link MediaTypeFileExtensionResolver} instances. Note that some
41+
* ContentNegotiationStrategy implementations also implement
42+
* MediaTypeFileExtensionResolver and the class constructor accepting the former
43+
* will also detect if they implement the latter. If you need to register additional
44+
* resolvers, you can use the method
45+
* {@link #addFileExtensionResolvers(MediaTypeFileExtensionResolver...)}.
3746
*
3847
* @author Rossen Stoyanchev
3948
* @since 3.2
@@ -50,6 +59,7 @@ public class ContentNegotiationManager implements ContentNegotiationStrategy, Me
5059
* Create an instance with the given ContentNegotiationStrategy instances.
5160
* <p>Each instance is checked to see if it is also an implementation of
5261
* MediaTypeFileExtensionResolver, and if so it is registered as such.
62+
* @param strategies one more more ContentNegotiationStrategy instances
5363
*/
5464
public ContentNegotiationManager(ContentNegotiationStrategy... strategies) {
5565
Assert.notEmpty(strategies, "At least one ContentNegotiationStrategy is expected");
@@ -70,6 +80,11 @@ public ContentNegotiationManager() {
7080

7181
/**
7282
* Add MediaTypeFileExtensionResolver instances.
83+
* <p>Note that some {@link ContentNegotiationStrategy} implementations also
84+
* implement {@link MediaTypeFileExtensionResolver} and the class constructor
85+
* accepting the former will also detect implementations of the latter. Therefore
86+
* you only need to use this method to register additional instances.
87+
* @param one more resolvers
7388
*/
7489
public void addFileExtensionResolvers(MediaTypeFileExtensionResolver... resolvers) {
7590
this.fileExtensionResolvers.addAll(Arrays.asList(resolvers));

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

+74-25
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@
1616

1717
package org.springframework.web.servlet.config;
1818

19+
import java.util.Arrays;
20+
import java.util.HashMap;
1921
import java.util.List;
22+
import java.util.Map;
2023

2124
import org.springframework.beans.factory.config.BeanDefinition;
2225
import org.springframework.beans.factory.config.BeanDefinitionHolder;
@@ -29,6 +32,7 @@
2932
import org.springframework.beans.factory.xml.ParserContext;
3033
import org.springframework.format.support.DefaultFormattingConversionService;
3134
import org.springframework.format.support.FormattingConversionServiceFactoryBean;
35+
import org.springframework.http.MediaType;
3236
import org.springframework.http.converter.ByteArrayHttpMessageConverter;
3337
import org.springframework.http.converter.HttpMessageConverter;
3438
import org.springframework.http.converter.ResourceHttpMessageConverter;
@@ -44,6 +48,9 @@
4448
import org.springframework.util.xml.DomUtils;
4549
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
4650
import org.springframework.web.HttpRequestHandler;
51+
import org.springframework.web.accept.ContentNegotiationManager;
52+
import org.springframework.web.accept.HeaderContentNegotiationStrategy;
53+
import org.springframework.web.accept.PathExtensionContentNegotiationStrategy;
4754
import org.springframework.web.bind.annotation.ExceptionHandler;
4855
import org.springframework.web.bind.annotation.ResponseStatus;
4956
import org.springframework.web.bind.support.ConfigurableWebBindingInitializer;
@@ -102,9 +109,10 @@
102109
* </ul>
103110
*
104111
* <p>Both the {@link RequestMappingHandlerAdapter} and the
105-
* {@link ExceptionHandlerExceptionResolver} are configured with default
106-
* instances of the following kind, unless custom instances are provided:
112+
* {@link ExceptionHandlerExceptionResolver} are configured with instances of
113+
* the following by default:
107114
* <ul>
115+
* <li>A {@link ContentNegotiationManager}
108116
* <li>A {@link DefaultFormattingConversionService}
109117
* <li>A {@link LocalValidatorFactoryBean} if a JSR-303 implementation is
110118
* available on the classpath
@@ -143,11 +151,14 @@ public BeanDefinition parse(Element element, ParserContext parserContext) {
143151
CompositeComponentDefinition compDefinition = new CompositeComponentDefinition(element.getTagName(), source);
144152
parserContext.pushContainingComponent(compDefinition);
145153

146-
RootBeanDefinition methodMappingDef = new RootBeanDefinition(RequestMappingHandlerMapping.class);
147-
methodMappingDef.setSource(source);
148-
methodMappingDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
149-
methodMappingDef.getPropertyValues().add("order", 0);
150-
String methodMappingName = parserContext.getReaderContext().registerWithGeneratedName(methodMappingDef);
154+
RuntimeBeanReference contentNegotiationManager = getContentNegotiationManager(element, source, parserContext);
155+
156+
RootBeanDefinition handlerMappingDef = new RootBeanDefinition(RequestMappingHandlerMapping.class);
157+
handlerMappingDef.setSource(source);
158+
handlerMappingDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
159+
handlerMappingDef.getPropertyValues().add("order", 0);
160+
handlerMappingDef.getPropertyValues().add("contentNegotiationManager", contentNegotiationManager);
161+
String methodMappingName = parserContext.getReaderContext().registerWithGeneratedName(handlerMappingDef);
151162

152163
RuntimeBeanReference conversionService = getConversionService(element, source, parserContext);
153164
RuntimeBeanReference validator = getValidator(element, source, parserContext);
@@ -164,22 +175,23 @@ public BeanDefinition parse(Element element, ParserContext parserContext) {
164175
ManagedList<?> argumentResolvers = getArgumentResolvers(element, source, parserContext);
165176
ManagedList<?> returnValueHandlers = getReturnValueHandlers(element, source, parserContext);
166177

167-
RootBeanDefinition methodAdapterDef = new RootBeanDefinition(RequestMappingHandlerAdapter.class);
168-
methodAdapterDef.setSource(source);
169-
methodAdapterDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
170-
methodAdapterDef.getPropertyValues().add("webBindingInitializer", bindingDef);
171-
methodAdapterDef.getPropertyValues().add("messageConverters", messageConverters);
178+
RootBeanDefinition handlerAdapterDef = new RootBeanDefinition(RequestMappingHandlerAdapter.class);
179+
handlerAdapterDef.setSource(source);
180+
handlerAdapterDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
181+
handlerAdapterDef.getPropertyValues().add("contentNegotiationManager", contentNegotiationManager);
182+
handlerAdapterDef.getPropertyValues().add("webBindingInitializer", bindingDef);
183+
handlerAdapterDef.getPropertyValues().add("messageConverters", messageConverters);
172184
if (element.hasAttribute("ignoreDefaultModelOnRedirect")) {
173185
Boolean ignoreDefaultModel = Boolean.valueOf(element.getAttribute("ignoreDefaultModelOnRedirect"));
174-
methodAdapterDef.getPropertyValues().add("ignoreDefaultModelOnRedirect", ignoreDefaultModel);
186+
handlerAdapterDef.getPropertyValues().add("ignoreDefaultModelOnRedirect", ignoreDefaultModel);
175187
}
176188
if (argumentResolvers != null) {
177-
methodAdapterDef.getPropertyValues().add("customArgumentResolvers", argumentResolvers);
189+
handlerAdapterDef.getPropertyValues().add("customArgumentResolvers", argumentResolvers);
178190
}
179191
if (returnValueHandlers != null) {
180-
methodAdapterDef.getPropertyValues().add("customReturnValueHandlers", returnValueHandlers);
192+
handlerAdapterDef.getPropertyValues().add("customReturnValueHandlers", returnValueHandlers);
181193
}
182-
String methodAdapterName = parserContext.getReaderContext().registerWithGeneratedName(methodAdapterDef);
194+
String handlerAdapterName = parserContext.getReaderContext().registerWithGeneratedName(handlerAdapterDef);
183195

184196
RootBeanDefinition csInterceptorDef = new RootBeanDefinition(ConversionServiceExposingInterceptor.class);
185197
csInterceptorDef.setSource(source);
@@ -191,13 +203,14 @@ public BeanDefinition parse(Element element, ParserContext parserContext) {
191203
mappedCsInterceptorDef.getConstructorArgumentValues().addIndexedArgumentValue(1, csInterceptorDef);
192204
String mappedInterceptorName = parserContext.getReaderContext().registerWithGeneratedName(mappedCsInterceptorDef);
193205

194-
RootBeanDefinition methodExceptionResolver = new RootBeanDefinition(ExceptionHandlerExceptionResolver.class);
195-
methodExceptionResolver.setSource(source);
196-
methodExceptionResolver.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
197-
methodExceptionResolver.getPropertyValues().add("messageConverters", messageConverters);
198-
methodExceptionResolver.getPropertyValues().add("order", 0);
206+
RootBeanDefinition exceptionHandlerExceptionResolver = new RootBeanDefinition(ExceptionHandlerExceptionResolver.class);
207+
exceptionHandlerExceptionResolver.setSource(source);
208+
exceptionHandlerExceptionResolver.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
209+
exceptionHandlerExceptionResolver.getPropertyValues().add("contentNegotiationManager", contentNegotiationManager);
210+
exceptionHandlerExceptionResolver.getPropertyValues().add("messageConverters", messageConverters);
211+
exceptionHandlerExceptionResolver.getPropertyValues().add("order", 0);
199212
String methodExceptionResolverName =
200-
parserContext.getReaderContext().registerWithGeneratedName(methodExceptionResolver);
213+
parserContext.getReaderContext().registerWithGeneratedName(exceptionHandlerExceptionResolver);
201214

202215
RootBeanDefinition responseStatusExceptionResolver = new RootBeanDefinition(ResponseStatusExceptionResolver.class);
203216
responseStatusExceptionResolver.setSource(source);
@@ -213,9 +226,9 @@ public BeanDefinition parse(Element element, ParserContext parserContext) {
213226
String defaultExceptionResolverName =
214227
parserContext.getReaderContext().registerWithGeneratedName(defaultExceptionResolver);
215228

216-
parserContext.registerComponent(new BeanComponentDefinition(methodMappingDef, methodMappingName));
217-
parserContext.registerComponent(new BeanComponentDefinition(methodAdapterDef, methodAdapterName));
218-
parserContext.registerComponent(new BeanComponentDefinition(methodExceptionResolver, methodExceptionResolverName));
229+
parserContext.registerComponent(new BeanComponentDefinition(handlerMappingDef, methodMappingName));
230+
parserContext.registerComponent(new BeanComponentDefinition(handlerAdapterDef, handlerAdapterName));
231+
parserContext.registerComponent(new BeanComponentDefinition(exceptionHandlerExceptionResolver, methodExceptionResolverName));
219232
parserContext.registerComponent(new BeanComponentDefinition(responseStatusExceptionResolver, responseStatusExceptionResolverName));
220233
parserContext.registerComponent(new BeanComponentDefinition(defaultExceptionResolver, defaultExceptionResolverName));
221234
parserContext.registerComponent(new BeanComponentDefinition(mappedCsInterceptorDef, mappedInterceptorName));
@@ -261,6 +274,42 @@ else if (jsr303Present) {
261274
}
262275
}
263276

277+
private RuntimeBeanReference getContentNegotiationManager(Element element, Object source, ParserContext parserContext) {
278+
RuntimeBeanReference contentNegotiationManagerRef;
279+
if (element.hasAttribute("content-negotiation-manager")) {
280+
contentNegotiationManagerRef = new RuntimeBeanReference(element.getAttribute("content-negotiation-manager"));
281+
}
282+
else {
283+
RootBeanDefinition managerDef = new RootBeanDefinition(ContentNegotiationManager.class);
284+
managerDef.setSource(source);
285+
managerDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
286+
PathExtensionContentNegotiationStrategy strategy1 = new PathExtensionContentNegotiationStrategy(getDefaultMediaTypes());
287+
HeaderContentNegotiationStrategy strategy2 = new HeaderContentNegotiationStrategy();
288+
managerDef.getConstructorArgumentValues().addIndexedArgumentValue(0, Arrays.asList(strategy1,strategy2));
289+
290+
String beanName = "mvcContentNegotiationManager";
291+
parserContext.getReaderContext().getRegistry().registerBeanDefinition(beanName , managerDef);
292+
parserContext.registerComponent(new BeanComponentDefinition(managerDef, beanName));
293+
contentNegotiationManagerRef = new RuntimeBeanReference(beanName);
294+
}
295+
return contentNegotiationManagerRef;
296+
}
297+
298+
private Map<String, MediaType> getDefaultMediaTypes() {
299+
Map<String, MediaType> map = new HashMap<String, MediaType>();
300+
if (romePresent) {
301+
map.put("atom", MediaType.APPLICATION_ATOM_XML);
302+
map.put("rss", MediaType.valueOf("application/rss+xml"));
303+
}
304+
if (jackson2Present || jacksonPresent) {
305+
map.put("json", MediaType.APPLICATION_JSON);
306+
}
307+
if (jaxb2Present) {
308+
map.put("xml", MediaType.APPLICATION_XML);
309+
}
310+
return map;
311+
}
312+
264313
private RuntimeBeanReference getMessageCodesResolver(Element element, Object source, ParserContext parserContext) {
265314
if (element.hasAttribute("message-codes-resolver")) {
266315
return new RuntimeBeanReference(element.getAttribute("message-codes-resolver"));

0 commit comments

Comments
 (0)