Fix content negotiation issue with sort by q-value
Before this fix the q-value of media types in the Accept header were
ignored when using the new RequestMappingHandlerAdapter in combination
with @ResponseBody and HttpMessageConverters.
Issue: SPR-9160
Backport-Issue: SPR-9168
Backport-Commit: 982cb2f258
This commit is contained in:
@@ -21,7 +21,6 @@ import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
@@ -117,7 +116,7 @@ public abstract class AbstractMessageConverterMethodProcessor extends AbstractMe
|
||||
}
|
||||
|
||||
List<MediaType> mediaTypes = new ArrayList<MediaType>(compatibleMediaTypes);
|
||||
MediaType.sortBySpecificity(mediaTypes);
|
||||
MediaType.sortBySpecificityAndQuality(mediaTypes);
|
||||
|
||||
MediaType selectedMediaType = null;
|
||||
for (MediaType mediaType : mediaTypes) {
|
||||
@@ -131,6 +130,8 @@ public abstract class AbstractMessageConverterMethodProcessor extends AbstractMe
|
||||
}
|
||||
}
|
||||
|
||||
selectedMediaType = selectedMediaType.removeQualityValue();
|
||||
|
||||
if (selectedMediaType != null) {
|
||||
for (HttpMessageConverter<?> messageConverter : messageConverters) {
|
||||
if (messageConverter.canWrite(returnValueClass, selectedMediaType)) {
|
||||
@@ -188,14 +189,12 @@ public abstract class AbstractMessageConverterMethodProcessor extends AbstractMe
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the more specific media type using the q-value of the first media type for both.
|
||||
* Return the more specific of the acceptable and the producible media types
|
||||
* with the q-value of the former.
|
||||
*/
|
||||
private MediaType getMostSpecificMediaType(MediaType type1, MediaType type2) {
|
||||
double quality = type1.getQualityValue();
|
||||
Map<String, String> params = Collections.singletonMap("q", String.valueOf(quality));
|
||||
MediaType t1 = new MediaType(type1, params);
|
||||
MediaType t2 = new MediaType(type2, params);
|
||||
return MediaType.SPECIFICITY_COMPARATOR.compare(t1, t2) <= 0 ? type1 : type2;
|
||||
private MediaType getMostSpecificMediaType(MediaType acceptType, MediaType produceType) {
|
||||
produceType = produceType.copyQualityValue(acceptType);
|
||||
return MediaType.SPECIFICITY_COMPARATOR.compare(acceptType, produceType) < 0 ? acceptType : produceType;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2011 the original author or authors.
|
||||
* 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.
|
||||
@@ -101,6 +101,7 @@ import org.springframework.web.util.WebUtils;
|
||||
*
|
||||
* @author Arjen Poutsma
|
||||
* @author Juergen Hoeller
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 3.0
|
||||
* @see ViewResolver
|
||||
* @see InternalResourceViewResolver
|
||||
@@ -117,6 +118,9 @@ public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport
|
||||
|
||||
private static final UrlPathHelper urlPathHelper = new UrlPathHelper();
|
||||
|
||||
static {
|
||||
urlPathHelper.setUrlDecode(false);
|
||||
}
|
||||
|
||||
private int order = Ordered.HIGHEST_PRECEDENCE;
|
||||
|
||||
@@ -270,7 +274,7 @@ public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport
|
||||
String name = viewResolvers.get(i).getClass().getName() + i;
|
||||
getApplicationContext().getAutowireCapableBeanFactory().initializeBean(viewResolvers.get(i), name);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
if (this.viewResolvers.isEmpty()) {
|
||||
logger.warn("Did not find any ViewResolvers to delegate to; please configure them using the " +
|
||||
@@ -351,13 +355,13 @@ public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport
|
||||
}
|
||||
}
|
||||
}
|
||||
List<MediaType> mediaTypes = new ArrayList<MediaType>(compatibleMediaTypes);
|
||||
MediaType.sortByQualityValue(mediaTypes);
|
||||
List<MediaType> selectedMediaTypes = new ArrayList<MediaType>(compatibleMediaTypes);
|
||||
MediaType.sortBySpecificityAndQuality(selectedMediaTypes);
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Requested media types are " + mediaTypes + " based on Accept header types " +
|
||||
logger.debug("Requested media types are " + selectedMediaTypes + " based on Accept header types " +
|
||||
"and producible media types " + producibleMediaTypes + ")");
|
||||
}
|
||||
return mediaTypes;
|
||||
return selectedMediaTypes;
|
||||
}
|
||||
catch (IllegalArgumentException ex) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
@@ -392,14 +396,12 @@ public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the more specific media type using the q-value of the first media type for both.
|
||||
* Return the more specific of the acceptable and the producible media types
|
||||
* with the q-value of the former.
|
||||
*/
|
||||
private MediaType getMostSpecificMediaType(MediaType type1, MediaType type2) {
|
||||
double quality = type1.getQualityValue();
|
||||
Map<String, String> params = Collections.singletonMap("q", String.valueOf(quality));
|
||||
MediaType t1 = new MediaType(type1, params);
|
||||
MediaType t2 = new MediaType(type2, params);
|
||||
return MediaType.SPECIFICITY_COMPARATOR.compare(t1, t2) <= 0 ? type1 : type2;
|
||||
private MediaType getMostSpecificMediaType(MediaType acceptType, MediaType produceType) {
|
||||
produceType = produceType.copyQualityValue(acceptType);
|
||||
return MediaType.SPECIFICITY_COMPARATOR.compare(acceptType, produceType) < 0 ? acceptType : produceType;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -48,25 +48,25 @@ import org.springframework.http.MediaType;
|
||||
import org.springframework.http.converter.ByteArrayHttpMessageConverter;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.http.converter.StringHttpMessageConverter;
|
||||
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
|
||||
import org.springframework.web.HttpMediaTypeNotAcceptableException;
|
||||
import org.springframework.web.HttpMediaTypeNotSupportedException;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
import org.springframework.web.bind.WebDataBinder;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
import org.springframework.web.bind.support.WebDataBinderFactory;
|
||||
import org.springframework.web.context.request.NativeWebRequest;
|
||||
import org.springframework.web.context.request.ServletWebRequest;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
import org.springframework.web.method.support.ModelAndViewContainer;
|
||||
import org.springframework.web.servlet.HandlerMapping;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor;
|
||||
|
||||
/**
|
||||
* Test fixture with {@link RequestResponseBodyMethodProcessor} and mock {@link HttpMessageConverter}.
|
||||
*
|
||||
*
|
||||
* @author Arjen Poutsma
|
||||
* @author Rossen Stoyanchev
|
||||
*/
|
||||
@@ -100,7 +100,7 @@ public class RequestResponseBodyMethodProcessorTests {
|
||||
|
||||
processor = new RequestResponseBodyMethodProcessor(Collections.<HttpMessageConverter<?>>singletonList(messageConverter));
|
||||
reset(messageConverter);
|
||||
|
||||
|
||||
Method handle = getClass().getMethod("handle1", String.class, Integer.TYPE);
|
||||
paramRequestBodyString = new MethodParameter(handle, 0);
|
||||
paramInt = new MethodParameter(handle, 1);
|
||||
@@ -110,7 +110,7 @@ public class RequestResponseBodyMethodProcessorTests {
|
||||
paramValidBean = new MethodParameter(getClass().getMethod("handle4", SimpleBean.class), 0);
|
||||
|
||||
mavContainer = new ModelAndViewContainer();
|
||||
|
||||
|
||||
servletRequest = new MockHttpServletRequest();
|
||||
servletResponse = new MockHttpServletResponse();
|
||||
webRequest = new ServletWebRequest(servletRequest, servletResponse);
|
||||
@@ -175,10 +175,10 @@ public class RequestResponseBodyMethodProcessorTests {
|
||||
|
||||
processor = new RequestResponseBodyMethodProcessor(Collections.<HttpMessageConverter<?>>singletonList(beanConverter));
|
||||
processor.resolveArgument(paramValidBean, mavContainer, webRequest, new ValidatingBinderFactory());
|
||||
|
||||
|
||||
verify(beanConverter);
|
||||
}
|
||||
|
||||
|
||||
@Test(expected = HttpMediaTypeNotSupportedException.class)
|
||||
public void resolveArgumentNotReadable() throws Exception {
|
||||
MediaType contentType = MediaType.TEXT_PLAIN;
|
||||
@@ -248,7 +248,7 @@ public class RequestResponseBodyMethodProcessorTests {
|
||||
|
||||
fail("Expected exception");
|
||||
}
|
||||
|
||||
|
||||
@Test(expected = HttpMediaTypeNotAcceptableException.class)
|
||||
public void handleReturnValueNotAcceptableProduces() throws Exception {
|
||||
MediaType accepted = MediaType.TEXT_PLAIN;
|
||||
@@ -264,19 +264,35 @@ public class RequestResponseBodyMethodProcessorTests {
|
||||
fail("Expected exception");
|
||||
}
|
||||
|
||||
// SPR-9160
|
||||
|
||||
@Test
|
||||
public void handleStringReturnValue() throws Exception {
|
||||
public void handleReturnValueSortByQuality() throws Exception {
|
||||
this.servletRequest.addHeader("Accept", "text/plain; q=0.5, application/json");
|
||||
|
||||
List<HttpMessageConverter<?>> converters = new ArrayList<HttpMessageConverter<?>>();
|
||||
converters.add(new MappingJackson2HttpMessageConverter());
|
||||
converters.add(new StringHttpMessageConverter());
|
||||
RequestResponseBodyMethodProcessor handler = new RequestResponseBodyMethodProcessor(converters);
|
||||
|
||||
handler.writeWithMessageConverters("Foo", returnTypeStringProduces, webRequest);
|
||||
|
||||
assertEquals("application/json;charset=UTF-8", servletResponse.getHeader("Content-Type"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void handleReturnValueString() throws Exception {
|
||||
List<HttpMessageConverter<?>>converters = new ArrayList<HttpMessageConverter<?>>();
|
||||
converters.add(new ByteArrayHttpMessageConverter());
|
||||
converters.add(new StringHttpMessageConverter());
|
||||
|
||||
|
||||
processor = new RequestResponseBodyMethodProcessor(converters);
|
||||
processor.handleReturnValue("Foo", returnTypeString, mavContainer, webRequest);
|
||||
|
||||
|
||||
assertEquals("text/plain;charset=ISO-8859-1", servletResponse.getHeader("Content-Type"));
|
||||
assertEquals("Foo", servletResponse.getContentAsString());
|
||||
}
|
||||
|
||||
|
||||
@ResponseBody
|
||||
public String handle1(@RequestBody String s, int i) {
|
||||
return s;
|
||||
@@ -293,7 +309,7 @@ public class RequestResponseBodyMethodProcessorTests {
|
||||
|
||||
public void handle4(@Valid @RequestBody SimpleBean b) {
|
||||
}
|
||||
|
||||
|
||||
private final class ValidatingBinderFactory implements WebDataBinderFactory {
|
||||
public WebDataBinder createBinder(NativeWebRequest webRequest, Object target, String objectName) throws Exception {
|
||||
LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean();
|
||||
@@ -318,5 +334,5 @@ public class RequestResponseBodyMethodProcessorTests {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2011 the original author or authors.
|
||||
* 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.
|
||||
@@ -16,6 +16,16 @@
|
||||
|
||||
package org.springframework.web.servlet.view;
|
||||
|
||||
import static org.easymock.EasyMock.createMock;
|
||||
import static org.easymock.EasyMock.expect;
|
||||
import static org.easymock.EasyMock.replay;
|
||||
import static org.easymock.EasyMock.verify;
|
||||
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;
|
||||
@@ -28,7 +38,6 @@ import java.util.Set;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
@@ -40,9 +49,6 @@ import org.springframework.web.servlet.HandlerMapping;
|
||||
import org.springframework.web.servlet.View;
|
||||
import org.springframework.web.servlet.ViewResolver;
|
||||
|
||||
import static org.easymock.EasyMock.*;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
/**
|
||||
* @author Arjen Poutsma
|
||||
*/
|
||||
@@ -100,7 +106,7 @@ public class ContentNegotiatingViewResolverTests {
|
||||
}
|
||||
|
||||
// SPR-8678
|
||||
|
||||
|
||||
@Test
|
||||
public void getMediaTypeFilenameWithContextPath() {
|
||||
request.setContextPath("/project-1.0.0.M3");
|
||||
@@ -110,6 +116,15 @@ public class ContentNegotiatingViewResolverTests {
|
||||
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);
|
||||
@@ -139,7 +154,6 @@ public class ContentNegotiatingViewResolverTests {
|
||||
request.setAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE, producibleTypes);
|
||||
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", 1, result.size());
|
||||
assertEquals("Invalid content type", new MediaType("application", "xhtml+xml"), result.get(0));
|
||||
}
|
||||
|
||||
@@ -294,6 +308,35 @@ public class ContentNegotiatingViewResolverTests {
|
||||
verify(viewResolverMock1, viewResolverMock2, viewMock1, viewMock2);
|
||||
}
|
||||
|
||||
// SPR-9160
|
||||
|
||||
@Test
|
||||
public void resolveViewNameAcceptHeaderSortByQuality() throws Exception {
|
||||
request.addHeader("Accept", "text/plain;q=0.5, application/json");
|
||||
|
||||
ViewResolver htmlViewResolver = createMock(ViewResolver.class);
|
||||
ViewResolver jsonViewResolver = createMock(ViewResolver.class);
|
||||
viewResolver.setViewResolvers(Arrays.asList(htmlViewResolver, jsonViewResolver));
|
||||
|
||||
View htmlView = createMock("text_html", View.class);
|
||||
View jsonViewMock = createMock("application_json", View.class);
|
||||
|
||||
String viewName = "view";
|
||||
Locale locale = Locale.ENGLISH;
|
||||
|
||||
expect(htmlViewResolver.resolveViewName(viewName, locale)).andReturn(htmlView);
|
||||
expect(jsonViewResolver.resolveViewName(viewName, locale)).andReturn(jsonViewMock);
|
||||
expect(htmlView.getContentType()).andReturn("text/html").anyTimes();
|
||||
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);
|
||||
|
||||
verify(htmlViewResolver, jsonViewResolver, htmlView, jsonViewMock);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveViewNameAcceptHeaderDefaultView() throws Exception {
|
||||
request.addHeader("Accept", "application/json");
|
||||
@@ -426,7 +469,7 @@ public class ContentNegotiatingViewResolverTests {
|
||||
StaticWebApplicationContext webAppContext = new StaticWebApplicationContext();
|
||||
webAppContext.setServletContext(new MockServletContext());
|
||||
webAppContext.refresh();
|
||||
|
||||
|
||||
UrlBasedViewResolver urlViewResolver = new InternalResourceViewResolver();
|
||||
urlViewResolver.setApplicationContext(webAppContext);
|
||||
ViewResolver xmlViewResolver = createMock(ViewResolver.class);
|
||||
@@ -435,7 +478,7 @@ public class ContentNegotiatingViewResolverTests {
|
||||
View xmlView = createMock("application_xml", View.class);
|
||||
View jsonView = createMock("application_json", View.class);
|
||||
viewResolver.setDefaultViews(Arrays.asList(jsonView));
|
||||
|
||||
|
||||
String viewName = "redirect:anotherTest";
|
||||
Locale locale = Locale.ENGLISH;
|
||||
|
||||
@@ -449,7 +492,7 @@ public class ContentNegotiatingViewResolverTests {
|
||||
|
||||
verify(xmlViewResolver, xmlView, jsonView);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void resolveViewNoMatch() throws Exception {
|
||||
request.addHeader("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9");
|
||||
@@ -505,7 +548,7 @@ public class ContentNegotiatingViewResolverTests {
|
||||
StaticWebApplicationContext webAppContext = new StaticWebApplicationContext();
|
||||
webAppContext.setServletContext(new MockServletContext());
|
||||
webAppContext.refresh();
|
||||
|
||||
|
||||
InternalResourceViewResolver nestedResolver = new InternalResourceViewResolver();
|
||||
nestedResolver.setApplicationContext(webAppContext);
|
||||
nestedResolver.setViewClass(InternalResourceView.class);
|
||||
|
||||
Reference in New Issue
Block a user