diff --git a/spring-web/src/main/java/org/springframework/http/HttpHeaders.java b/spring-web/src/main/java/org/springframework/http/HttpHeaders.java index 090822ee271..7cefa4d2384 100644 --- a/spring-web/src/main/java/org/springframework/http/HttpHeaders.java +++ b/spring-web/src/main/java/org/springframework/http/HttpHeaders.java @@ -34,6 +34,8 @@ import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.TimeZone; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import org.springframework.util.Assert; import org.springframework.util.LinkedCaseInsensitiveMap; @@ -55,6 +57,7 @@ import org.springframework.util.StringUtils; * * @author Arjen Poutsma * @author Sebastien Deleuze + * @author Brian Clozel * @since 3.0 */ public class HttpHeaders implements MultiValueMap, Serializable { @@ -372,6 +375,12 @@ public class HttpHeaders implements MultiValueMap, Serializable "EEE MMM dd HH:mm:ss yyyy" }; + /** + * Pattern matching ETag multiple field values in headers such as "If-Match", "If-None-Match" + * @see Section 2.3 of RFC 7232 + */ + private static final Pattern ETAG_HEADER_VALUE_PATTERN = Pattern.compile("\\*|\\s*((W\\/)?(\"[^\"]*\"))\\s*,?"); + private static TimeZone GMT = TimeZone.getTimeZone("GMT"); @@ -459,7 +468,7 @@ public class HttpHeaders implements MultiValueMap, Serializable * Returns the value of the {@code Access-Control-Allow-Headers} response header. */ public List getAccessControlAllowHeaders() { - return getFirstValueAsList(ACCESS_CONTROL_ALLOW_HEADERS); + return getValuesAsList(ACCESS_CONTROL_ALLOW_HEADERS); } /** @@ -476,7 +485,7 @@ public class HttpHeaders implements MultiValueMap, Serializable List result = new ArrayList(); String value = getFirst(ACCESS_CONTROL_ALLOW_METHODS); if (value != null) { - String[] tokens = value.split(",\\s*"); + String[] tokens = StringUtils.tokenizeToStringArray(value, ",", true, true); for (String token : tokens) { HttpMethod resolved = HttpMethod.resolve(token); if (resolved != null) { @@ -498,7 +507,23 @@ public class HttpHeaders implements MultiValueMap, Serializable * Return the value of the {@code Access-Control-Allow-Origin} response header. */ public String getAccessControlAllowOrigin() { - return getFirst(ACCESS_CONTROL_ALLOW_ORIGIN); + return getFieldValues(ACCESS_CONTROL_ALLOW_ORIGIN); + } + + protected String getFieldValues(String headerName) { + List headerValues = this.headers.get(headerName); + if (headerValues != null) { + StringBuilder builder = new StringBuilder(); + for (Iterator iterator = headerValues.iterator(); iterator.hasNext(); ) { + String ifNoneMatch = iterator.next(); + builder.append(ifNoneMatch); + if (iterator.hasNext()) { + builder.append(", "); + } + } + return builder.toString(); + } + return null; } /** @@ -512,7 +537,7 @@ public class HttpHeaders implements MultiValueMap, Serializable * Returns the value of the {@code Access-Control-Expose-Headers} response header. */ public List getAccessControlExposeHeaders() { - return getFirstValueAsList(ACCESS_CONTROL_EXPOSE_HEADERS); + return getValuesAsList(ACCESS_CONTROL_EXPOSE_HEADERS); } /** @@ -542,7 +567,7 @@ public class HttpHeaders implements MultiValueMap, Serializable * Returns the value of the {@code Access-Control-Request-Headers} request header. */ public List getAccessControlRequestHeaders() { - return getFirstValueAsList(ACCESS_CONTROL_REQUEST_HEADERS); + return getValuesAsList(ACCESS_CONTROL_REQUEST_HEADERS); } /** @@ -643,7 +668,7 @@ public class HttpHeaders implements MultiValueMap, Serializable * Return the value of the {@code Cache-Control} header. */ public String getCacheControl() { - return getFirst(CACHE_CONTROL); + return getFieldValues(CACHE_CONTROL); } /** @@ -664,7 +689,7 @@ public class HttpHeaders implements MultiValueMap, Serializable * Return the value of the {@code Connection} header. */ public List getConnection() { - return getFirstValueAsList(CONNECTION); + return getValuesAsList(CONNECTION); } /** @@ -782,6 +807,64 @@ public class HttpHeaders implements MultiValueMap, Serializable return getFirstDate(EXPIRES, false); } + /** + * Set the (new) value of the {@code If-Match} header. + */ + public void setIfMatch(String ifMatch) { + set(IF_MATCH, ifMatch); + } + + /** + * Set the (new) value of the {@code If-Match} header. + */ + public void setIfMatch(List ifMatchList) { + set(IF_MATCH, toCommaDelimitedString(ifMatchList)); + } + + protected String toCommaDelimitedString(List list) { + StringBuilder builder = new StringBuilder(); + for (Iterator iterator = list.iterator(); iterator.hasNext(); ) { + String ifNoneMatch = iterator.next(); + builder.append(ifNoneMatch); + if (iterator.hasNext()) { + builder.append(", "); + } + } + return builder.toString(); + } + + /** + * Return the value of the {@code If-Match} header. + */ + public List getIfMatch() { + return getETagValuesAsList(IF_MATCH); + } + + protected List getETagValuesAsList(String headerName) { + List values = get(headerName); + if (values != null) { + List result = new ArrayList(); + for (String value : values) { + if (value != null) { + Matcher matcher = ETAG_HEADER_VALUE_PATTERN.matcher(value); + while (matcher.find()) { + if ("*".equals(matcher.group())) { + result.add(matcher.group()); + } + else { + result.add(matcher.group(1)); + } + } + if(result.size() == 0) { + throw new IllegalArgumentException("Could not parse '" + headerName + "' value=" + value); + } + } + } + return result; + } + return Collections.emptyList(); + } + /** * Set the (new) value of the {@code If-Modified-Since} header. *

The date should be specified as the number of milliseconds since @@ -814,35 +897,51 @@ public class HttpHeaders implements MultiValueMap, Serializable set(IF_NONE_MATCH, toCommaDelimitedString(ifNoneMatchList)); } - protected String toCommaDelimitedString(List list) { - StringBuilder builder = new StringBuilder(); - for (Iterator iterator = list.iterator(); iterator.hasNext();) { - String ifNoneMatch = iterator.next(); - builder.append(ifNoneMatch); - if (iterator.hasNext()) { - builder.append(", "); - } - } - return builder.toString(); - } - /** * Return the value of the {@code If-None-Match} header. */ public List getIfNoneMatch() { - return getFirstValueAsList(IF_NONE_MATCH); + return getETagValuesAsList(IF_NONE_MATCH); } - protected List getFirstValueAsList(String header) { - List result = new ArrayList(); - String value = getFirst(header); - if (value != null) { - String[] tokens = value.split(",\\s*"); - for (String token : tokens) { - result.add(token); + /** + * Return all values of a given header name, + * even if this header is set multiple times. + * @since 4.3.0 + */ + public List getValuesAsList(String headerName) { + List values = get(headerName); + if (values != null) { + List result = new ArrayList(); + for (String value : values) { + if (value != null) { + String[] tokens = StringUtils.tokenizeToStringArray(value, ","); + for (String token : tokens) { + result.add(token); + } + } } + return result; } - return result; + return Collections.emptyList(); + } + + /** + * Set the (new) value of the {@code If-Unmodified-Since} header. + *

The date should be specified as the number of milliseconds since + * January 1, 1970 GMT. + */ + public void setIfUnmodifiedSince(long ifUnmodifiedSince) { + setDate(IF_UNMODIFIED_SINCE, ifUnmodifiedSince); + } + + /** + * Return the value of the {@code If-Unmodified-Since} header. + *

The date is returned as the number of milliseconds since + * January 1, 1970 GMT. Returns -1 when the date is unknown. + */ + public long getIfUnmodifiedSince() { + return getFirstDate(IF_UNMODIFIED_SINCE, false); } /** @@ -957,7 +1056,7 @@ public class HttpHeaders implements MultiValueMap, Serializable * Return the request header names subject to content negotiation. */ public List getVary() { - return getFirstValueAsList(VARY); + return getValuesAsList(VARY); } /** diff --git a/spring-web/src/test/java/org/springframework/http/HttpHeadersTests.java b/spring-web/src/test/java/org/springframework/http/HttpHeadersTests.java index 9fa07041230..019cbbdcfaf 100644 --- a/spring-web/src/test/java/org/springframework/http/HttpHeadersTests.java +++ b/spring-web/src/test/java/org/springframework/http/HttpHeadersTests.java @@ -32,6 +32,7 @@ import java.util.TimeZone; import org.hamcrest.Matchers; import org.junit.Test; +import static org.hamcrest.Matchers.is; import static org.junit.Assert.*; /** @@ -39,12 +40,20 @@ import static org.junit.Assert.*; * * @author Arjen Poutsma * @author Sebastien Deleuze + * @author Brian Clozel */ public class HttpHeadersTests { private final HttpHeaders headers = new HttpHeaders(); + @Test + public void getFirst() { + headers.add(HttpHeaders.CACHE_CONTROL, "max-age=1000, public"); + headers.add(HttpHeaders.CACHE_CONTROL, "s-maxage=1000"); + assertThat(headers.getFirst(HttpHeaders.CACHE_CONTROL), is("max-age=1000, public")); + } + @Test public void accept() { MediaType mediaType1 = new MediaType("text", "html"); @@ -132,6 +141,29 @@ public class HttpHeadersTests { assertEquals("Invalid ETag header", "\"v2.6\"", headers.getFirst("ETag")); } + @Test + public void ifMatch() { + String ifMatch = "\"v2.6\""; + headers.setIfMatch(ifMatch); + assertEquals("Invalid If-Match header", ifMatch, headers.getIfMatch().get(0)); + assertEquals("Invalid If-Match header", "\"v2.6\"", headers.getFirst("If-Match")); + } + + @Test(expected = IllegalArgumentException.class) + public void ifMatchIllegalHeader() { + headers.setIfMatch("Illegal"); + headers.getIfMatch(); + } + + @Test + public void ifMatchMultipleHeaders() { + headers.add(HttpHeaders.IF_MATCH, "\"v2,0\""); + headers.add(HttpHeaders.IF_MATCH, "W/\"v2,1\", \"v2,2\""); + assertEquals("Invalid If-Match header", "\"v2,0\"", headers.get(HttpHeaders.IF_MATCH).get(0)); + assertEquals("Invalid If-Match header", "W/\"v2,1\", \"v2,2\"", headers.get(HttpHeaders.IF_MATCH).get(1)); + assertThat(headers.getIfMatch(), Matchers.contains("\"v2,0\"", "W/\"v2,1\"", "\"v2,2\"")); + } + @Test public void ifNoneMatch() { String ifNoneMatch = "\"v2.6\""; @@ -140,16 +172,24 @@ public class HttpHeadersTests { assertEquals("Invalid If-None-Match header", "\"v2.6\"", headers.getFirst("If-None-Match")); } + @Test + public void ifNoneMatchWildCard() { + String ifNoneMatch = "*"; + headers.setIfNoneMatch(ifNoneMatch); + assertEquals("Invalid If-None-Match header", ifNoneMatch, headers.getIfNoneMatch().get(0)); + assertEquals("Invalid If-None-Match header", "*", headers.getFirst("If-None-Match")); + } + @Test public void ifNoneMatchList() { String ifNoneMatch1 = "\"v2.6\""; - String ifNoneMatch2 = "\"v2.7\""; + String ifNoneMatch2 = "\"v2.7\", \"v2.8\""; List ifNoneMatchList = new ArrayList(2); ifNoneMatchList.add(ifNoneMatch1); ifNoneMatchList.add(ifNoneMatch2); headers.setIfNoneMatch(ifNoneMatchList); - assertEquals("Invalid If-None-Match header", ifNoneMatchList, headers.getIfNoneMatch()); - assertEquals("Invalid If-None-Match header", "\"v2.6\", \"v2.7\"", headers.getFirst("If-None-Match")); + assertThat(headers.getIfNoneMatch(), Matchers.contains("\"v2.6\"", "\"v2.7\"", "\"v2.8\"")); + assertEquals("Invalid If-None-Match header", "\"v2.6\", \"v2.7\", \"v2.8\"", headers.getFirst("If-None-Match")); } @Test @@ -255,6 +295,13 @@ public class HttpHeadersTests { assertEquals("Invalid Cache-Control header", "no-cache", headers.getFirst("cache-control")); } + @Test + public void cacheControlAllValues() { + headers.add(HttpHeaders.CACHE_CONTROL, "max-age=1000, public"); + headers.add(HttpHeaders.CACHE_CONTROL, "s-maxage=1000"); + assertThat(headers.getCacheControl(), is("max-age=1000, public, s-maxage=1000")); + } + @Test public void contentDisposition() { headers.setContentDispositionFormData("name", null); @@ -290,6 +337,16 @@ public class HttpHeadersTests { assertEquals(allowedHeaders, Arrays.asList("header1", "header2")); } + @Test + public void accessControlAllowHeadersMultipleValues() { + List allowedHeaders = headers.getAccessControlAllowHeaders(); + assertThat(allowedHeaders, Matchers.emptyCollectionOf(String.class)); + headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS, "header1, header2"); + headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS, "header3"); + allowedHeaders = headers.getAccessControlAllowHeaders(); + assertEquals(Arrays.asList("header1", "header2", "header3"), allowedHeaders); + } + @Test public void accessControlAllowMethods() { List allowedMethods = headers.getAccessControlAllowMethods(); diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/WebSocketHttpHeaders.java b/spring-websocket/src/main/java/org/springframework/web/socket/WebSocketHttpHeaders.java index 8efb5fab95f..4cda2747a65 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/WebSocketHttpHeaders.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/WebSocketHttpHeaders.java @@ -172,7 +172,7 @@ public class WebSocketHttpHeaders extends HttpHeaders { return Collections.emptyList(); } else if (values.size() == 1) { - return getFirstValueAsList(SEC_WEBSOCKET_PROTOCOL); + return getValuesAsList(SEC_WEBSOCKET_PROTOCOL); } else { return values;