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:
@@ -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 {
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user