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
This commit is contained in:
Rossen Stoyanchev
2012-07-20 21:12:13 -04:00
parent 92759ed1f8
commit 028e15faa3
17 changed files with 638 additions and 50 deletions

View File

@@ -19,6 +19,7 @@ package org.springframework.web.servlet.config;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Locale;
@@ -38,6 +39,7 @@ import org.springframework.core.io.ClassPathResource;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.format.annotation.DateTimeFormat.ISO;
import org.springframework.format.support.FormattingConversionServiceFactoryBean;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
@@ -49,8 +51,11 @@ import org.springframework.validation.Errors;
import org.springframework.validation.Validator;
import org.springframework.validation.annotation.Validated;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
import org.springframework.web.accept.ContentNegotiationManager;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.context.support.GenericWebApplicationContext;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.method.support.InvocableHandlerMethod;
@@ -98,13 +103,18 @@ public class MvcNamespaceTests {
@Test
public void testDefaultConfig() throws Exception {
loadBeanDefinitions("mvc-config.xml", 11);
loadBeanDefinitions("mvc-config.xml", 12);
RequestMappingHandlerMapping mapping = appContext.getBean(RequestMappingHandlerMapping.class);
assertNotNull(mapping);
assertEquals(0, mapping.getOrder());
mapping.setDefaultHandler(handlerMethod);
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo.json");
NativeWebRequest webRequest = new ServletWebRequest(request);
ContentNegotiationManager manager = mapping.getContentNegotiationManager();
assertEquals(Arrays.asList(MediaType.APPLICATION_JSON), manager.resolveMediaTypes(webRequest));
RequestMappingHandlerAdapter adapter = appContext.getBean(RequestMappingHandlerAdapter.class);
assertNotNull(adapter);
assertEquals(false, new DirectFieldAccessor(adapter).getPropertyValue("ignoreDefaultModelOnRedirect"));
@@ -118,7 +128,7 @@ public class MvcNamespaceTests {
assertNotNull(appContext.getBean(Validator.class));
// default web binding initializer behavior test
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/");
request = new MockHttpServletRequest("GET", "/");
request.addParameter("date", "2009-10-31");
MockHttpServletResponse response = new MockHttpServletResponse();
@@ -135,7 +145,7 @@ public class MvcNamespaceTests {
@Test(expected=TypeMismatchException.class)
public void testCustomConversionService() throws Exception {
loadBeanDefinitions("mvc-config-custom-conversion-service.xml", 11);
loadBeanDefinitions("mvc-config-custom-conversion-service.xml", 12);
RequestMappingHandlerMapping mapping = appContext.getBean(RequestMappingHandlerMapping.class);
assertNotNull(mapping);
@@ -161,7 +171,7 @@ public class MvcNamespaceTests {
@Test
public void testCustomValidator() throws Exception {
loadBeanDefinitions("mvc-config-custom-validator.xml", 11);
loadBeanDefinitions("mvc-config-custom-validator.xml", 12);
RequestMappingHandlerAdapter adapter = appContext.getBean(RequestMappingHandlerAdapter.class);
assertNotNull(adapter);
@@ -179,7 +189,7 @@ public class MvcNamespaceTests {
@Test
public void testInterceptors() throws Exception {
loadBeanDefinitions("mvc-config-interceptors.xml", 16);
loadBeanDefinitions("mvc-config-interceptors.xml", 17);
RequestMappingHandlerMapping mapping = appContext.getBean(RequestMappingHandlerMapping.class);
assertNotNull(mapping);
@@ -308,7 +318,7 @@ public class MvcNamespaceTests {
@Test
public void testBeanDecoration() throws Exception {
loadBeanDefinitions("mvc-config-bean-decoration.xml", 13);
loadBeanDefinitions("mvc-config-bean-decoration.xml", 14);
RequestMappingHandlerMapping mapping = appContext.getBean(RequestMappingHandlerMapping.class);
assertNotNull(mapping);
@@ -329,7 +339,7 @@ public class MvcNamespaceTests {
@Test
public void testViewControllers() throws Exception {
loadBeanDefinitions("mvc-config-view-controllers.xml", 14);
loadBeanDefinitions("mvc-config-view-controllers.xml", 15);
RequestMappingHandlerMapping mapping = appContext.getBean(RequestMappingHandlerMapping.class);
assertNotNull(mapping);
@@ -389,7 +399,7 @@ public class MvcNamespaceTests {
/** WebSphere gives trailing servlet path slashes by default!! */
@Test
public void testViewControllersOnWebSphere() throws Exception {
loadBeanDefinitions("mvc-config-view-controllers.xml", 14);
loadBeanDefinitions("mvc-config-view-controllers.xml", 15);
SimpleUrlHandlerMapping mapping2 = appContext.getBean(SimpleUrlHandlerMapping.class);
SimpleControllerHandlerAdapter adapter = appContext.getBean(SimpleControllerHandlerAdapter.class);
@@ -440,6 +450,19 @@ public class MvcNamespaceTests {
assertEquals(2, beanNameMapping.getOrder());
}
@Test
public void testCustomContentNegotiationManager() throws Exception {
loadBeanDefinitions("mvc-config-content-negotiation-manager.xml", 14);
RequestMappingHandlerMapping mapping = appContext.getBean(RequestMappingHandlerMapping.class);
ContentNegotiationManager manager = mapping.getContentNegotiationManager();
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo.xml");
NativeWebRequest webRequest = new ServletWebRequest(request);
assertEquals(Arrays.asList(MediaType.valueOf("application/rss+xml")), manager.resolveMediaTypes(webRequest));
}
private void loadBeanDefinitions(String fileName, int expectedBeanCount) {
XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(appContext);
ClassPathResource resource = new ClassPathResource(fileName, AnnotationDrivenBeanDefinitionParserTests.class);
@@ -448,6 +471,7 @@ public class MvcNamespaceTests {
appContext.refresh();
}
@Controller
public static class TestController {

View File

@@ -0,0 +1,112 @@
/*
* 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.servlet.config.annotation;
import static org.junit.Assert.assertEquals;
import java.util.Arrays;
import java.util.Collections;
import org.junit.Before;
import org.junit.Test;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.web.accept.ContentNegotiationManager;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.ServletWebRequest;
/**
* Test fixture for {@link ContentNegotiationConfigurer} tests.
* @author Rossen Stoyanchev
*/
public class ContentNegotiationConfigurerTests {
private ContentNegotiationConfigurer configurer;
private NativeWebRequest webRequest;
private MockHttpServletRequest servletRequest;
@Before
public void setup() {
this.configurer = new ContentNegotiationConfigurer();
this.servletRequest = new MockHttpServletRequest();
this.webRequest = new ServletWebRequest(this.servletRequest);
}
@Test
public void defaultSettings() throws Exception {
ContentNegotiationManager manager = this.configurer.getContentNegotiationManager();
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 {
this.configurer.addMediaTypes(Collections.singletonMap("json", MediaType.APPLICATION_JSON));
ContentNegotiationManager manager = this.configurer.getContentNegotiationManager();
this.servletRequest.setRequestURI("/flower.json");
assertEquals(Arrays.asList(MediaType.APPLICATION_JSON), manager.resolveMediaTypes(this.webRequest));
}
@Test
public void favorParameter() throws Exception {
this.configurer.setFavorParameter(true);
this.configurer.setParameterName("f");
this.configurer.addMediaTypes(Collections.singletonMap("json", MediaType.APPLICATION_JSON));
ContentNegotiationManager manager = this.configurer.getContentNegotiationManager();
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.configurer.setIgnoreAcceptHeader(true);
ContentNegotiationManager manager = this.configurer.getContentNegotiationManager();
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.configurer.setDefaultContentType(MediaType.APPLICATION_JSON);
ContentNegotiationManager manager = this.configurer.getContentNegotiationManager();
assertEquals(Arrays.asList(MediaType.APPLICATION_JSON), manager.resolveMediaTypes(this.webRequest));
}
}

View File

@@ -67,11 +67,13 @@ public class DelegatingWebMvcConfigurationTests {
@Test
public void requestMappingHandlerAdapter() throws Exception {
Capture<List<HttpMessageConverter<?>>> converters = new Capture<List<HttpMessageConverter<?>>>();
Capture<ContentNegotiationConfigurer> contentNegotiationConfigurer = new Capture<ContentNegotiationConfigurer>();
Capture<FormattingConversionService> conversionService = new Capture<FormattingConversionService>();
Capture<List<HandlerMethodArgumentResolver>> resolvers = new Capture<List<HandlerMethodArgumentResolver>>();
Capture<List<HandlerMethodReturnValueHandler>> handlers = new Capture<List<HandlerMethodReturnValueHandler>>();
configurer.configureMessageConverters(capture(converters));
configurer.configureContentNegotiation(capture(contentNegotiationConfigurer));
expect(configurer.getValidator()).andReturn(null);
expect(configurer.getMessageCodesResolver()).andReturn(null);
configurer.addFormatters(capture(conversionService));
@@ -135,8 +137,10 @@ public class DelegatingWebMvcConfigurationTests {
public void handlerExceptionResolver() throws Exception {
Capture<List<HttpMessageConverter<?>>> converters = new Capture<List<HttpMessageConverter<?>>>();
Capture<List<HandlerExceptionResolver>> exceptionResolvers = new Capture<List<HandlerExceptionResolver>>();
Capture<ContentNegotiationConfigurer> contentNegotiationConfigurer = new Capture<ContentNegotiationConfigurer>();
configurer.configureMessageConverters(capture(converters));
configurer.configureContentNegotiation(capture(contentNegotiationConfigurer));
configurer.configureHandlerExceptionResolvers(capture(exceptionResolvers));
replay(configurer);

View File

@@ -21,6 +21,7 @@ import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
@@ -34,6 +35,7 @@ import org.springframework.core.convert.converter.Converter;
import org.springframework.core.io.FileSystemResourceLoader;
import org.springframework.format.FormatterRegistry;
import org.springframework.format.support.FormattingConversionService;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.mock.web.MockHttpServletRequest;
@@ -45,8 +47,11 @@ import org.springframework.validation.Errors;
import org.springframework.validation.MessageCodesResolver;
import org.springframework.validation.Validator;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
import org.springframework.web.accept.ContentNegotiationManager;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.support.ConfigurableWebBindingInitializer;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.context.support.StaticWebApplicationContext;
import org.springframework.web.method.annotation.ModelAttributeMethodProcessor;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
@@ -182,6 +187,24 @@ public class WebMvcConfigurationSupportTests {
String actual = webConfig.mvcConversionService().convert(new TestBean(), String.class);
assertEquals("converted", actual);
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo.json");
NativeWebRequest webRequest = new ServletWebRequest(request);
ContentNegotiationManager manager = webConfig.requestMappingHandlerMapping().getContentNegotiationManager();
assertEquals(Arrays.asList(MediaType.APPLICATION_JSON), manager.resolveMediaTypes(webRequest));
request.setRequestURI("/foo.xml");
assertEquals(Arrays.asList(MediaType.APPLICATION_XML), manager.resolveMediaTypes(webRequest));
request.setRequestURI("/foo.rss");
assertEquals(Arrays.asList(MediaType.valueOf("application/rss+xml")), manager.resolveMediaTypes(webRequest));
request.setRequestURI("/foo.atom");
assertEquals(Arrays.asList(MediaType.APPLICATION_ATOM_XML), manager.resolveMediaTypes(webRequest));
request.setRequestURI("/foo");
request.setParameter("f", "json");
assertEquals(Arrays.asList(MediaType.APPLICATION_JSON), manager.resolveMediaTypes(webRequest));
RequestMappingHandlerAdapter adapter = webConfig.requestMappingHandlerAdapter();
assertEquals(1, adapter.getMessageConverters().size());
@@ -242,7 +265,6 @@ public class WebMvcConfigurationSupportTests {
@Controller
private static class TestController {
@SuppressWarnings("unused")
@RequestMapping("/")
public void handle() {
}
@@ -287,6 +309,11 @@ public class WebMvcConfigurationSupportTests {
};
}
@Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
configurer.setFavorParameter(true).setParameterName("f");
}
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(new ModelAttributeMethodProcessor(true));