Add abstractions for content negotiation

Introduced ContentNeogtiationStrategy for resolving the requested
media types from an incoming request. The available implementations
are based on path extension, request parameter, 'Accept' header,
and a fixed default content type. The logic for these implementations
is based on equivalent options, previously available only in the
ContentNegotiatingViewResolver.

Also in this commit is ContentNegotiationManager, the central class to
use when configuring content negotiation options. It accepts one or
more ContentNeogtiationStrategy instances and delegates to them.

The ContentNeogiationManager can now be used to configure the
following classes:

- RequestMappingHandlerMappingm
- RequestMappingHandlerAdapter
- ExceptionHandlerExceptionResolver
- ContentNegotiatingViewResolver

Issue: SPR-8410, SPR-8417, SPR-8418,SPR-8416, SPR-8419,SPR-7722
This commit is contained in:
Rossen Stoyanchev
2012-06-08 08:56:57 -04:00
parent 35055fd866
commit f05e2bc56f
29 changed files with 1469 additions and 489 deletions

View File

@@ -44,7 +44,7 @@ public class ParamsRequestConditionTests {
new ParamsRequestCondition("foo=bar").equals(new ParamsRequestCondition("FOO=bar")));
}
@Test
@Test
public void paramPresent() {
ParamsRequestCondition condition = new ParamsRequestCondition("foo");
@@ -96,7 +96,7 @@ public class ParamsRequestConditionTests {
@Test
public void compareTo() {
MockHttpServletRequest request = new MockHttpServletRequest();
ParamsRequestCondition condition1 = new ParamsRequestCondition("foo", "bar", "baz");
ParamsRequestCondition condition2 = new ParamsRequestCondition("foo", "bar");

View File

@@ -36,7 +36,7 @@ import org.springframework.web.servlet.mvc.condition.ProducesRequestCondition.Pr
public class ProducesRequestConditionTests {
@Test
public void producesMatch() {
public void match() {
ProducesRequestCondition condition = new ProducesRequestCondition("text/plain");
MockHttpServletRequest request = new MockHttpServletRequest();
@@ -46,7 +46,7 @@ public class ProducesRequestConditionTests {
}
@Test
public void negatedProducesMatch() {
public void matchNegated() {
ProducesRequestCondition condition = new ProducesRequestCondition("!text/plain");
MockHttpServletRequest request = new MockHttpServletRequest();
@@ -56,13 +56,13 @@ public class ProducesRequestConditionTests {
}
@Test
public void getProducibleMediaTypesNegatedExpression() {
public void getProducibleMediaTypes() {
ProducesRequestCondition condition = new ProducesRequestCondition("!application/xml");
assertEquals(Collections.emptySet(), condition.getProducibleMediaTypes());
}
@Test
public void producesWildcardMatch() {
public void matchWildcard() {
ProducesRequestCondition condition = new ProducesRequestCondition("text/*");
MockHttpServletRequest request = new MockHttpServletRequest();
@@ -72,7 +72,7 @@ public class ProducesRequestConditionTests {
}
@Test
public void producesMultipleMatch() {
public void matchMultiple() {
ProducesRequestCondition condition = new ProducesRequestCondition("text/plain", "application/xml");
MockHttpServletRequest request = new MockHttpServletRequest();
@@ -82,7 +82,7 @@ public class ProducesRequestConditionTests {
}
@Test
public void producesSingleNoMatch() {
public void matchSingle() {
ProducesRequestCondition condition = new ProducesRequestCondition("text/plain");
MockHttpServletRequest request = new MockHttpServletRequest();
@@ -92,7 +92,7 @@ public class ProducesRequestConditionTests {
}
@Test
public void producesParseError() {
public void matchParseError() {
ProducesRequestCondition condition = new ProducesRequestCondition("text/plain");
MockHttpServletRequest request = new MockHttpServletRequest();
@@ -102,7 +102,7 @@ public class ProducesRequestConditionTests {
}
@Test
public void producesParseErrorWithNegation() {
public void matchParseErrorWithNegation() {
ProducesRequestCondition condition = new ProducesRequestCondition("!text/plain");
MockHttpServletRequest request = new MockHttpServletRequest();
@@ -111,6 +111,15 @@ public class ProducesRequestConditionTests {
assertNull(condition.getMatchingCondition(request));
}
@Test
public void matchByRequestParameter() {
ProducesRequestCondition condition = new ProducesRequestCondition(new String[] {"text/plain"}, new String[] {});
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo.txt");
assertNotNull(condition.getMatchingCondition(request));
}
@Test
public void compareTo() {
ProducesRequestCondition html = new ProducesRequestCondition("text/html");
@@ -286,7 +295,7 @@ public class ProducesRequestConditionTests {
}
@Test
public void parseProducesAndHeaders() {
public void instantiateWithProducesAndHeaderConditions() {
String[] produces = new String[] {"text/plain"};
String[] headers = new String[]{"foo=bar", "accept=application/xml,application/pdf"};
ProducesRequestCondition condition = new ProducesRequestCondition(produces, headers);
@@ -312,7 +321,7 @@ public class ProducesRequestConditionTests {
private void assertConditions(ProducesRequestCondition condition, String... expected) {
Collection<ProduceMediaTypeExpression> expressions = condition.getContent();
assertEquals("Invalid amount of conditions", expressions.size(), expected.length);
assertEquals("Invalid number of conditions", expressions.size(), expected.length);
for (String s : expected) {
boolean found = false;
for (ProduceMediaTypeExpression expr : expressions) {

View File

@@ -24,12 +24,10 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
@@ -42,6 +40,12 @@ import org.springframework.http.MediaType;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.mock.web.MockServletContext;
import org.springframework.web.accept.ContentNegotiationManager;
import org.springframework.web.accept.FixedContentNegotiationStrategy;
import org.springframework.web.accept.HeaderContentNegotiationStrategy;
import org.springframework.web.accept.MappingMediaTypeExtensionsResolver;
import org.springframework.web.accept.ParameterContentNegotiationStrategy;
import org.springframework.web.accept.PathExtensionContentNegotiationStrategy;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.context.support.StaticWebApplicationContext;
@@ -75,104 +79,22 @@ public class ContentNegotiatingViewResolverTests {
}
@Test
public void getMediaTypeFromFilenameMediaTypes() {
viewResolver.setMediaTypes(Collections.singletonMap("HTML", "application/xhtml+xml"));
assertEquals("Invalid content type", new MediaType("application", "xhtml+xml"),
viewResolver.getMediaTypeFromFilename("test.html"));
}
@Test
public void getMediaTypeFromFilenameJaf() {
assertEquals("Invalid content type", new MediaType("application", "vnd.ms-excel"),
viewResolver.getMediaTypeFromFilename("test.xls"));
}
@Test
public void getMediaTypeFromFilenameNoJaf() {
viewResolver.setUseJaf(false);
assertEquals("Invalid content type", MediaType.APPLICATION_OCTET_STREAM,
viewResolver.getMediaTypeFromFilename("test.xls"));
}
@Test
public void getMediaTypeFilename() {
request.setRequestURI("/test.html?foo=bar");
List<MediaType> result = viewResolver.getMediaTypes(request);
assertEquals("Invalid content type", Collections.singletonList(new MediaType("text", "html")), result);
viewResolver.setMediaTypes(Collections.singletonMap("html", "application/xhtml+xml"));
result = viewResolver.getMediaTypes(request);
assertEquals("Invalid content type", Collections.singletonList(new MediaType("application", "xhtml+xml")),
result);
}
// SPR-8678
@Test
public void getMediaTypeFilenameWithContextPath() {
request.setContextPath("/project-1.0.0.M3");
request.setRequestURI("/project-1.0.0.M3/");
assertTrue("Context path should be excluded", viewResolver.getMediaTypes(request).isEmpty());
request.setRequestURI("/project-1.0.0.M3");
assertTrue("Context path should be excluded", viewResolver.getMediaTypes(request).isEmpty());
}
// SPR-9390
@Test
public void getMediaTypeFilenameWithEncodedURI() {
request.setRequestURI("/quo%20vadis%3f.html");
List<MediaType> result = viewResolver.getMediaTypes(request);
assertEquals("Invalid content type", Collections.singletonList(new MediaType("text", "html")), result);
}
@Test
public void getMediaTypeParameter() {
viewResolver.setFavorParameter(true);
viewResolver.setMediaTypes(Collections.singletonMap("html", "application/xhtml+xml"));
request.addParameter("format", "html");
List<MediaType> result = viewResolver.getMediaTypes(request);
assertEquals("Invalid content type", Collections.singletonList(new MediaType("application", "xhtml+xml")),
result);
}
@Test
public void getMediaTypeAcceptHeader() {
request.addHeader("Accept", "text/html,application/xml;q=0.9,application/xhtml+xml,*/*;q=0.8");
List<MediaType> result = viewResolver.getMediaTypes(request);
assertEquals("Invalid amount of media types", 4, result.size());
assertEquals("Invalid content type", new MediaType("text", "html"), result.get(0));
assertEquals("Invalid content type", new MediaType("application", "xhtml+xml"), result.get(1));
assertEquals("Invalid content type", new MediaType("application", "xml", Collections.singletonMap("q", "0.9")),
result.get(2));
assertEquals("Invalid content type", new MediaType("*", "*", Collections.singletonMap("q", "0.8")),
result.get(3));
}
@Test
public void getMediaTypeAcceptHeaderWithProduces() {
public void getMediaTypeAcceptHeaderWithProduces() throws Exception {
Set<MediaType> producibleTypes = Collections.singleton(MediaType.APPLICATION_XHTML_XML);
request.setAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE, producibleTypes);
request.addHeader("Accept", "text/html,application/xml;q=0.9,application/xhtml+xml,*/*;q=0.8");
viewResolver.afterPropertiesSet();
List<MediaType> result = viewResolver.getMediaTypes(request);
assertEquals("Invalid content type", new MediaType("application", "xhtml+xml"), result.get(0));
}
@Test
public void getDefaultContentType() {
request.addHeader("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8");
viewResolver.setIgnoreAcceptHeader(true);
viewResolver.setDefaultContentType(new MediaType("application", "pdf"));
List<MediaType> result = viewResolver.getMediaTypes(request);
assertEquals("Invalid amount of media types", 1, result.size());
assertEquals("Invalid content type", new MediaType("application", "pdf"), result.get(0));
}
@Test
public void resolveViewNameWithPathExtension() throws Exception {
request.setRequestURI("/test.xls");
ViewResolver viewResolverMock = createMock(ViewResolver.class);
viewResolver.setViewResolvers(Collections.singletonList(viewResolverMock));
viewResolver.afterPropertiesSet();
View viewMock = createMock("application_xls", View.class);
@@ -195,7 +117,11 @@ public class ContentNegotiatingViewResolverTests {
public void resolveViewNameWithAcceptHeader() throws Exception {
request.addHeader("Accept", "application/vnd.ms-excel");
viewResolver.setMediaTypes(Collections.singletonMap("xls", "application/vnd.ms-excel"));
Map<String, String> mapping = Collections.singletonMap("xls", "application/vnd.ms-excel");
MappingMediaTypeExtensionsResolver extensionsResolver = new MappingMediaTypeExtensionsResolver(mapping);
ContentNegotiationManager manager = new ContentNegotiationManager(new HeaderContentNegotiationStrategy());
manager.addExtensionsResolver(extensionsResolver);
viewResolver.setContentNegotiationManager(manager);
ViewResolver viewResolverMock = createMock(ViewResolver.class);
viewResolver.setViewResolvers(Collections.singletonList(viewResolverMock));
@@ -220,6 +146,7 @@ public class ContentNegotiatingViewResolverTests {
public void resolveViewNameWithInvalidAcceptHeader() throws Exception {
request.addHeader("Accept", "application");
viewResolver.afterPropertiesSet();
View result = viewResolver.resolveViewName("test", Locale.ENGLISH);
assertNull(result);
}
@@ -228,12 +155,15 @@ public class ContentNegotiatingViewResolverTests {
public void resolveViewNameWithRequestParameter() throws Exception {
request.addParameter("format", "xls");
viewResolver.setFavorParameter(true);
viewResolver.setMediaTypes(Collections.singletonMap("xls", "application/vnd.ms-excel"));
Map<String, String> mapping = Collections.singletonMap("xls", "application/vnd.ms-excel");
ParameterContentNegotiationStrategy paramStrategy = new ParameterContentNegotiationStrategy(mapping);
viewResolver.setContentNegotiationManager(new ContentNegotiationManager(paramStrategy));
ViewResolver viewResolverMock = createMock(ViewResolver.class);
viewResolver.setViewResolvers(Collections.singletonList(viewResolverMock));
viewResolver.afterPropertiesSet();
View viewMock = createMock("application_xls", View.class);
String viewName = "view";
@@ -255,13 +185,16 @@ public class ContentNegotiatingViewResolverTests {
public void resolveViewNameWithDefaultContentType() throws Exception {
request.addHeader("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8");
viewResolver.setIgnoreAcceptHeader(true);
viewResolver.setDefaultContentType(new MediaType("application", "xml"));
MediaType mediaType = new MediaType("application", "xml");
FixedContentNegotiationStrategy fixedStrategy = new FixedContentNegotiationStrategy(mediaType);
viewResolver.setContentNegotiationManager(new ContentNegotiationManager(fixedStrategy));
ViewResolver viewResolverMock1 = createMock("viewResolver1", ViewResolver.class);
ViewResolver viewResolverMock2 = createMock("viewResolver2", ViewResolver.class);
viewResolver.setViewResolvers(Arrays.asList(viewResolverMock1, viewResolverMock2));
viewResolver.afterPropertiesSet();
View viewMock1 = createMock("application_xml", View.class);
View viewMock2 = createMock("text_html", View.class);
@@ -289,6 +222,8 @@ public class ContentNegotiatingViewResolverTests {
ViewResolver viewResolverMock2 = createMock(ViewResolver.class);
viewResolver.setViewResolvers(Arrays.asList(viewResolverMock1, viewResolverMock2));
viewResolver.afterPropertiesSet();
View viewMock1 = createMock("application_xml", View.class);
View viewMock2 = createMock("text_html", View.class);
@@ -314,6 +249,8 @@ public class ContentNegotiatingViewResolverTests {
public void resolveViewNameAcceptHeaderSortByQuality() throws Exception {
request.addHeader("Accept", "text/plain;q=0.5, application/json");
viewResolver.setContentNegotiationManager(new ContentNegotiationManager(new HeaderContentNegotiationStrategy()));
ViewResolver htmlViewResolver = createMock(ViewResolver.class);
ViewResolver jsonViewResolver = createMock(ViewResolver.class);
viewResolver.setViewResolvers(Arrays.asList(htmlViewResolver, jsonViewResolver));
@@ -330,7 +267,6 @@ public class ContentNegotiatingViewResolverTests {
expect(jsonViewMock.getContentType()).andReturn("application/json").anyTimes();
replay(htmlViewResolver, jsonViewResolver, htmlView, jsonViewMock);
viewResolver.setFavorPathExtension(false);
View result = viewResolver.resolveViewName(viewName, locale);
assertSame("Invalid view", jsonViewMock, result);
@@ -353,6 +289,8 @@ public class ContentNegotiatingViewResolverTests {
defaultViews.add(viewMock3);
viewResolver.setDefaultViews(defaultViews);
viewResolver.afterPropertiesSet();
String viewName = "view";
Locale locale = Locale.ENGLISH;
@@ -378,6 +316,8 @@ public class ContentNegotiatingViewResolverTests {
ViewResolver viewResolverMock2 = createMock("viewResolver2", ViewResolver.class);
viewResolver.setViewResolvers(Arrays.asList(viewResolverMock1, viewResolverMock2));
viewResolver.afterPropertiesSet();
View viewMock1 = createMock("application_xml", View.class);
View viewMock2 = createMock("text_html", View.class);
@@ -403,9 +343,10 @@ public class ContentNegotiatingViewResolverTests {
public void resolveViewNameFilenameDefaultView() throws Exception {
request.setRequestURI("/test.json");
Map<String, String> mediaTypes = new HashMap<String, String>();
mediaTypes.put("json", "application/json");
viewResolver.setMediaTypes(mediaTypes);
Map<String, String> mapping = Collections.singletonMap("json", "application/json");
PathExtensionContentNegotiationStrategy pathStrategy = new PathExtensionContentNegotiationStrategy(mapping);
viewResolver.setContentNegotiationManager(new ContentNegotiationManager(pathStrategy));
ViewResolver viewResolverMock1 = createMock(ViewResolver.class);
ViewResolver viewResolverMock2 = createMock(ViewResolver.class);
@@ -419,6 +360,8 @@ public class ContentNegotiatingViewResolverTests {
defaultViews.add(viewMock3);
viewResolver.setDefaultViews(defaultViews);
viewResolver.afterPropertiesSet();
String viewName = "view";
Locale locale = Locale.ENGLISH;
@@ -445,6 +388,8 @@ public class ContentNegotiatingViewResolverTests {
ViewResolver viewResolverMock = createMock(ViewResolver.class);
viewResolver.setViewResolvers(Collections.singletonList(viewResolverMock));
viewResolver.afterPropertiesSet();
View viewMock = createMock("application_xml", View.class);
String viewName = "view";
@@ -479,6 +424,8 @@ public class ContentNegotiatingViewResolverTests {
View jsonView = createMock("application_json", View.class);
viewResolver.setDefaultViews(Arrays.asList(jsonView));
viewResolver.afterPropertiesSet();
String viewName = "redirect:anotherTest";
Locale locale = Locale.ENGLISH;
@@ -500,6 +447,8 @@ public class ContentNegotiatingViewResolverTests {
ViewResolver viewResolverMock = createMock(ViewResolver.class);
viewResolver.setViewResolvers(Collections.singletonList(viewResolverMock));
viewResolver.afterPropertiesSet();
View viewMock = createMock("application_xml", View.class);
String viewName = "view";
@@ -524,6 +473,8 @@ public class ContentNegotiatingViewResolverTests {
ViewResolver viewResolverMock = createMock(ViewResolver.class);
viewResolver.setViewResolvers(Collections.singletonList(viewResolverMock));
viewResolver.afterPropertiesSet();
View viewMock = createMock("application_xml", View.class);
String viewName = "view";
@@ -553,7 +504,11 @@ public class ContentNegotiatingViewResolverTests {
nestedResolver.setApplicationContext(webAppContext);
nestedResolver.setViewClass(InternalResourceView.class);
viewResolver.setViewResolvers(new ArrayList<ViewResolver>(Arrays.asList(nestedResolver)));
viewResolver.setDefaultContentType(MediaType.TEXT_HTML);
FixedContentNegotiationStrategy fixedStrategy = new FixedContentNegotiationStrategy(MediaType.TEXT_HTML);
viewResolver.setContentNegotiationManager(new ContentNegotiationManager(fixedStrategy));
viewResolver.afterPropertiesSet();
String viewName = "view";
Locale locale = Locale.ENGLISH;