Browse Source

Consistent support for multiple Accept headers

Issue: SPR-14506
pull/1112/merge
Juergen Hoeller 10 years ago
parent
commit
e59a5993f3
  1. 24
      spring-web/src/main/java/org/springframework/http/HttpHeaders.java
  2. 35
      spring-web/src/main/java/org/springframework/http/MediaType.java
  3. 15
      spring-web/src/main/java/org/springframework/web/accept/HeaderContentNegotiationStrategy.java
  4. 11
      spring-web/src/test/java/org/springframework/http/HttpHeadersTests.java
  5. 2
      spring-web/src/test/java/org/springframework/http/MediaTypeTests.java
  6. 32
      spring-web/src/test/java/org/springframework/web/accept/HeaderContentNegotiationStrategyTests.java

24
spring-web/src/main/java/org/springframework/http/HttpHeaders.java

@ -429,19 +429,7 @@ public class HttpHeaders implements MultiValueMap<String, String>, Serializable
* <p>Returns an empty list when the acceptable media types are unspecified. * <p>Returns an empty list when the acceptable media types are unspecified.
*/ */
public List<MediaType> getAccept() { public List<MediaType> getAccept() {
String value = getFirst(ACCEPT); return MediaType.parseMediaTypes(get(ACCEPT));
List<MediaType> result = (value != null ? MediaType.parseMediaTypes(value) : Collections.<MediaType>emptyList());
// Some containers parse 'Accept' into multiple values
if (result.size() == 1) {
List<String> acceptHeader = get(ACCEPT);
if (acceptHeader.size() > 1) {
value = StringUtils.collectionToCommaDelimitedString(acceptHeader);
result = MediaType.parseMediaTypes(value);
}
}
return result;
} }
/** /**
@ -452,7 +440,7 @@ public class HttpHeaders implements MultiValueMap<String, String>, Serializable
} }
/** /**
* Returns the value of the {@code Access-Control-Allow-Credentials} response header. * Return the value of the {@code Access-Control-Allow-Credentials} response header.
*/ */
public boolean getAccessControlAllowCredentials() { public boolean getAccessControlAllowCredentials() {
return Boolean.parseBoolean(getFirst(ACCESS_CONTROL_ALLOW_CREDENTIALS)); return Boolean.parseBoolean(getFirst(ACCESS_CONTROL_ALLOW_CREDENTIALS));
@ -466,7 +454,7 @@ public class HttpHeaders implements MultiValueMap<String, String>, Serializable
} }
/** /**
* Returns the value of the {@code Access-Control-Allow-Headers} response header. * Return the value of the {@code Access-Control-Allow-Headers} response header.
*/ */
public List<String> getAccessControlAllowHeaders() { public List<String> getAccessControlAllowHeaders() {
return getValuesAsList(ACCESS_CONTROL_ALLOW_HEADERS); return getValuesAsList(ACCESS_CONTROL_ALLOW_HEADERS);
@ -519,7 +507,7 @@ public class HttpHeaders implements MultiValueMap<String, String>, Serializable
} }
/** /**
* Returns the value of the {@code Access-Control-Expose-Headers} response header. * Return the value of the {@code Access-Control-Expose-Headers} response header.
*/ */
public List<String> getAccessControlExposeHeaders() { public List<String> getAccessControlExposeHeaders() {
return getValuesAsList(ACCESS_CONTROL_EXPOSE_HEADERS); return getValuesAsList(ACCESS_CONTROL_EXPOSE_HEADERS);
@ -533,7 +521,7 @@ public class HttpHeaders implements MultiValueMap<String, String>, Serializable
} }
/** /**
* Returns the value of the {@code Access-Control-Max-Age} response header. * Return the value of the {@code Access-Control-Max-Age} response header.
* <p>Returns -1 when the max age is unknown. * <p>Returns -1 when the max age is unknown.
*/ */
public long getAccessControlMaxAge() { public long getAccessControlMaxAge() {
@ -549,7 +537,7 @@ public class HttpHeaders implements MultiValueMap<String, String>, Serializable
} }
/** /**
* Returns the value of the {@code Access-Control-Request-Headers} request header. * Return the value of the {@code Access-Control-Request-Headers} request header.
*/ */
public List<String> getAccessControlRequestHeaders() { public List<String> getAccessControlRequestHeaders() {
return getValuesAsList(ACCESS_CONTROL_REQUEST_HEADERS); return getValuesAsList(ACCESS_CONTROL_REQUEST_HEADERS);

35
spring-web/src/main/java/org/springframework/http/MediaType.java

@ -28,6 +28,7 @@ import java.util.Map;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.InvalidMimeTypeException; import org.springframework.util.InvalidMimeTypeException;
import org.springframework.util.MimeType; import org.springframework.util.MimeType;
import org.springframework.util.MimeTypeUtils; import org.springframework.util.MimeTypeUtils;
@ -397,6 +398,8 @@ public class MediaType extends MimeType implements Serializable {
* Parse the given String value into a {@code MediaType} object, * Parse the given String value into a {@code MediaType} object,
* with this method name following the 'valueOf' naming convention * with this method name following the 'valueOf' naming convention
* (as supported by {@link org.springframework.core.convert.ConversionService}. * (as supported by {@link org.springframework.core.convert.ConversionService}.
* @param value the string to parse
* @throws InvalidMediaTypeException if the media type value cannot be parsed
* @see #parseMediaType(String) * @see #parseMediaType(String)
*/ */
public static MediaType valueOf(String value) { public static MediaType valueOf(String value) {
@ -407,7 +410,7 @@ public class MediaType extends MimeType implements Serializable {
* Parse the given String into a single {@code MediaType}. * Parse the given String into a single {@code MediaType}.
* @param mediaType the string to parse * @param mediaType the string to parse
* @return the media type * @return the media type
* @throws InvalidMediaTypeException if the string cannot be parsed * @throws InvalidMediaTypeException if the media type value cannot be parsed
*/ */
public static MediaType parseMediaType(String mediaType) { public static MediaType parseMediaType(String mediaType) {
MimeType type; MimeType type;
@ -425,13 +428,12 @@ public class MediaType extends MimeType implements Serializable {
} }
} }
/** /**
* Parse the given, comma-separated string into a list of {@code MediaType} objects. * Parse the given comma-separated string into a list of {@code MediaType} objects.
* <p>This method can be used to parse an Accept or Content-Type header. * <p>This method can be used to parse an Accept or Content-Type header.
* @param mediaTypes the string to parse * @param mediaTypes the string to parse
* @return the list of media types * @return the list of media types
* @throws IllegalArgumentException if the string cannot be parsed * @throws InvalidMediaTypeException if the media type value cannot be parsed
*/ */
public static List<MediaType> parseMediaTypes(String mediaTypes) { public static List<MediaType> parseMediaTypes(String mediaTypes) {
if (!StringUtils.hasLength(mediaTypes)) { if (!StringUtils.hasLength(mediaTypes)) {
@ -445,6 +447,31 @@ public class MediaType extends MimeType implements Serializable {
return result; return result;
} }
/**
* Parse the given list of (potentially) comma-separated strings into a
* list of {@code MediaType} objects.
* <p>This method can be used to parse an Accept or Content-Type header.
* @param mediaTypes the string to parse
* @return the list of media types
* @throws InvalidMediaTypeException if the media type value cannot be parsed
* @since 4.3.2
*/
public static List<MediaType> parseMediaTypes(List<String> mediaTypes) {
if (CollectionUtils.isEmpty(mediaTypes)) {
return Collections.<MediaType>emptyList();
}
else if (mediaTypes.size() == 1) {
return parseMediaTypes(mediaTypes.get(0));
}
else {
List<MediaType> result = new ArrayList<>(8);
for (String mediaType : mediaTypes) {
result.addAll(parseMediaTypes(mediaType));
}
return result;
}
}
/** /**
* Re-create the given mime types as media types. * Re-create the given mime types as media types.
* @since 5.0 * @since 5.0

15
spring-web/src/main/java/org/springframework/web/accept/HeaderContentNegotiationStrategy.java

@ -16,13 +16,13 @@
package org.springframework.web.accept; package org.springframework.web.accept;
import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.InvalidMediaTypeException; import org.springframework.http.InvalidMediaTypeException;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.util.StringUtils;
import org.springframework.web.HttpMediaTypeNotAcceptableException; import org.springframework.web.HttpMediaTypeNotAcceptableException;
import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.context.request.NativeWebRequest;
@ -30,6 +30,7 @@ import org.springframework.web.context.request.NativeWebRequest;
* A {@code ContentNegotiationStrategy} that checks the 'Accept' request header. * A {@code ContentNegotiationStrategy} that checks the 'Accept' request header.
* *
* @author Rossen Stoyanchev * @author Rossen Stoyanchev
* @author Juergen Hoeller
* @since 3.2 * @since 3.2
*/ */
public class HeaderContentNegotiationStrategy implements ContentNegotiationStrategy { public class HeaderContentNegotiationStrategy implements ContentNegotiationStrategy {
@ -42,18 +43,20 @@ public class HeaderContentNegotiationStrategy implements ContentNegotiationStrat
public List<MediaType> resolveMediaTypes(NativeWebRequest request) public List<MediaType> resolveMediaTypes(NativeWebRequest request)
throws HttpMediaTypeNotAcceptableException { throws HttpMediaTypeNotAcceptableException {
String header = request.getHeader(HttpHeaders.ACCEPT); String[] headerValueArray = request.getHeaderValues(HttpHeaders.ACCEPT);
if (!StringUtils.hasText(header)) { if (headerValueArray == null) {
return Collections.emptyList(); return Collections.<MediaType>emptyList();
} }
List<String> headerValues = Arrays.asList(headerValueArray);
try { try {
List<MediaType> mediaTypes = MediaType.parseMediaTypes(header); List<MediaType> mediaTypes = MediaType.parseMediaTypes(headerValues);
MediaType.sortBySpecificityAndQuality(mediaTypes); MediaType.sortBySpecificityAndQuality(mediaTypes);
return mediaTypes; return mediaTypes;
} }
catch (InvalidMediaTypeException ex) { catch (InvalidMediaTypeException ex) {
throw new HttpMediaTypeNotAcceptableException( throw new HttpMediaTypeNotAcceptableException(
"Could not parse 'Accept' header [" + header + "]: " + ex.getMessage()); "Could not parse 'Accept' header " + headerValues + ": " + ex.getMessage());
} }
} }

11
spring-web/src/test/java/org/springframework/http/HttpHeadersTests.java

@ -68,13 +68,22 @@ public class HttpHeadersTests {
} }
@Test // SPR-9655 @Test // SPR-9655
public void acceptIPlanet() { public void acceptWithMultipleHeaderValues() {
headers.add("Accept", "text/html"); headers.add("Accept", "text/html");
headers.add("Accept", "text/plain"); headers.add("Accept", "text/plain");
List<MediaType> expected = Arrays.asList(new MediaType("text", "html"), new MediaType("text", "plain")); List<MediaType> expected = Arrays.asList(new MediaType("text", "html"), new MediaType("text", "plain"));
assertEquals("Invalid Accept header", expected, headers.getAccept()); assertEquals("Invalid Accept header", expected, headers.getAccept());
} }
@Test // SPR-14506
public void acceptWithMultipleCommaSeparatedHeaderValues() {
headers.add("Accept", "text/html,text/pdf");
headers.add("Accept", "text/plain,text/csv");
List<MediaType> expected = Arrays.asList(new MediaType("text", "html"), new MediaType("text", "pdf"),
new MediaType("text", "plain"), new MediaType("text", "csv"));
assertEquals("Invalid Accept header", expected, headers.getAccept());
}
@Test @Test
public void acceptCharsets() { public void acceptCharsets() {
Charset charset1 = StandardCharsets.UTF_8; Charset charset1 = StandardCharsets.UTF_8;

2
spring-web/src/test/java/org/springframework/http/MediaTypeTests.java

@ -138,7 +138,7 @@ public class MediaTypeTests {
assertNotNull("No media types returned", mediaTypes); assertNotNull("No media types returned", mediaTypes);
assertEquals("Invalid amount of media types", 4, mediaTypes.size()); assertEquals("Invalid amount of media types", 4, mediaTypes.size());
mediaTypes = MediaType.parseMediaTypes(null); mediaTypes = MediaType.parseMediaTypes("");
assertNotNull("No media types returned", mediaTypes); assertNotNull("No media types returned", mediaTypes);
assertEquals("Invalid amount of media types", 0, mediaTypes.size()); assertEquals("Invalid amount of media types", 0, mediaTypes.size());
} }

32
spring-web/src/test/java/org/springframework/web/accept/HeaderContentNegotiationStrategyTests.java

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2012 the original author or authors. * Copyright 2002-2016 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -13,11 +13,11 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.web.accept; package org.springframework.web.accept;
import java.util.List; import java.util.List;
import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
@ -32,21 +32,16 @@ import static org.junit.Assert.*;
* Test fixture for HeaderContentNegotiationStrategy tests. * Test fixture for HeaderContentNegotiationStrategy tests.
* *
* @author Rossen Stoyanchev * @author Rossen Stoyanchev
* @author Juergen Hoeller
*/ */
public class HeaderContentNegotiationStrategyTests { public class HeaderContentNegotiationStrategyTests {
private HeaderContentNegotiationStrategy strategy; private final HeaderContentNegotiationStrategy strategy = new HeaderContentNegotiationStrategy();
private NativeWebRequest webRequest; private final MockHttpServletRequest servletRequest = new MockHttpServletRequest();
private MockHttpServletRequest servletRequest; private final NativeWebRequest webRequest = new ServletWebRequest(this.servletRequest);
@Before
public void setup() {
this.strategy = new HeaderContentNegotiationStrategy();
this.servletRequest = new MockHttpServletRequest();
this.webRequest = new ServletWebRequest(servletRequest );
}
@Test @Test
public void resolveMediaTypes() throws Exception { public void resolveMediaTypes() throws Exception {
@ -60,7 +55,20 @@ public class HeaderContentNegotiationStrategyTests {
assertEquals("text/plain;q=0.5", mediaTypes.get(3).toString()); assertEquals("text/plain;q=0.5", mediaTypes.get(3).toString());
} }
@Test(expected=HttpMediaTypeNotAcceptableException.class) @Test // SPR-14506
public void resolveMediaTypesFromMultipleHeaderValues() throws Exception {
this.servletRequest.addHeader("Accept", "text/plain; q=0.5, text/html");
this.servletRequest.addHeader("Accept", "text/x-dvi; q=0.8, text/x-c");
List<MediaType> mediaTypes = this.strategy.resolveMediaTypes(this.webRequest);
assertEquals(4, mediaTypes.size());
assertEquals("text/html", mediaTypes.get(0).toString());
assertEquals("text/x-c", mediaTypes.get(1).toString());
assertEquals("text/x-dvi;q=0.8", mediaTypes.get(2).toString());
assertEquals("text/plain;q=0.5", mediaTypes.get(3).toString());
}
@Test(expected = HttpMediaTypeNotAcceptableException.class)
public void resolveMediaTypesParseError() throws Exception { public void resolveMediaTypesParseError() throws Exception {
this.servletRequest.addHeader("Accept", "textplain; q=0.5"); this.servletRequest.addHeader("Accept", "textplain; q=0.5");
this.strategy.resolveMediaTypes(this.webRequest); this.strategy.resolveMediaTypes(this.webRequest);

Loading…
Cancel
Save