2222import java .nio .charset .Charset ;
2323import java .text .DecimalFormat ;
2424import java .text .DecimalFormatSymbols ;
25- import java .text .ParseException ;
26- import java .text .SimpleDateFormat ;
25+ import java .time .Instant ;
26+ import java .time .ZoneId ;
27+ import java .time .ZonedDateTime ;
28+ import java .time .format .DateTimeFormatter ;
29+ import java .time .format .DateTimeParseException ;
2730import java .util .ArrayList ;
2831import java .util .Collection ;
2932import java .util .Collections ;
30- import java .util .Date ;
3133import java .util .EnumSet ;
3234import java .util .Iterator ;
3335import java .util .LinkedHashMap ;
3638import java .util .Locale ;
3739import java .util .Map ;
3840import java .util .Set ;
39- import java .util .TimeZone ;
4041import java .util .regex .Matcher ;
4142import java .util .regex .Pattern ;
4243import java .util .stream .Collectors ;
4748import org .springframework .util .MultiValueMap ;
4849import org .springframework .util .StringUtils ;
4950
51+
5052/**
5153 * Represents HTTP request and response headers, mapping string header names to a list of string values.
5254 *
@@ -372,16 +374,6 @@ public class HttpHeaders implements MultiValueMap<String, String>, Serializable
372374 */
373375 public static final String WWW_AUTHENTICATE = "WWW-Authenticate" ;
374376
375- /**
376- * Date formats as specified in the HTTP RFC
377- * @see <a href="https://tools.ietf.org/html/rfc7231#section-7.1.1.1">Section 7.1.1.1 of RFC 7231</a>
378- */
379- private static final String [] DATE_FORMATS = new String [] {
380- "EEE, dd MMM yyyy HH:mm:ss zzz" ,
381- "EEE, dd-MMM-yy HH:mm:ss zzz" ,
382- "EEE MMM dd HH:mm:ss yyyy"
383- };
384-
385377 /**
386378 * Pattern matching ETag multiple field values in headers such as "If-Match", "If-None-Match"
387379 * @see <a href="https://tools.ietf.org/html/rfc7232#section-2.3">Section 2.3 of RFC 7232</a>
@@ -390,7 +382,17 @@ public class HttpHeaders implements MultiValueMap<String, String>, Serializable
390382
391383 private static final DecimalFormatSymbols DECIMAL_FORMAT_SYMBOLS = new DecimalFormatSymbols (Locale .ENGLISH );
392384
393- private static TimeZone GMT = TimeZone .getTimeZone ("GMT" );
385+ private static final ZoneId GMT = ZoneId .of ("GMT" );
386+
387+ /**
388+ * Date formats with time zone as specified in the HTTP RFC
389+ * @see <a href="https://tools.ietf.org/html/rfc7231#section-7.1.1.1">Section 7.1.1.1 of RFC 7231</a>
390+ */
391+ private static final DateTimeFormatter [] DATE_FORMATTERS = new DateTimeFormatter [] {
392+ DateTimeFormatter .RFC_1123_DATE_TIME ,
393+ DateTimeFormatter .ofPattern ("EEEE, dd-MMM-yy HH:mm:ss zz" , Locale .US ),
394+ DateTimeFormatter .ofPattern ("EEE MMM dd HH:mm:ss yyyy" ,Locale .US ).withZone (GMT )
395+ };
394396
395397
396398 private final Map <String , List <String >> headers ;
@@ -924,6 +926,7 @@ public void setExpires(long expires) {
924926 * as specified by the {@code Expires} header.
925927 * <p>The date is returned as the number of milliseconds since
926928 * January 1, 1970 GMT. Returns -1 when the date is unknown.
929+ * @see #getFirstZonedDateTime(String)
927930 */
928931 public long getExpires () {
929932 return getFirstDate (EXPIRES , false );
@@ -1010,6 +1013,7 @@ public void setIfModifiedSince(long ifModifiedSince) {
10101013 * Return the value of the {@code If-Modified-Since} header.
10111014 * <p>The date is returned as the number of milliseconds since
10121015 * January 1, 1970 GMT. Returns -1 when the date is unknown.
1016+ * @see #getFirstZonedDateTime(String)
10131017 */
10141018 public long getIfModifiedSince () {
10151019 return getFirstDate (IF_MODIFIED_SINCE , false );
@@ -1051,6 +1055,7 @@ public void setIfUnmodifiedSince(long ifUnmodifiedSince) {
10511055 * <p>The date is returned as the number of milliseconds since
10521056 * January 1, 1970 GMT. Returns -1 when the date is unknown.
10531057 * @since 4.3
1058+ * @see #getFirstZonedDateTime(String)
10541059 */
10551060 public long getIfUnmodifiedSince () {
10561061 return getFirstDate (IF_UNMODIFIED_SINCE , false );
@@ -1071,6 +1076,7 @@ public void setLastModified(long lastModified) {
10711076 * {@code Last-Modified} header.
10721077 * <p>The date is returned as the number of milliseconds since
10731078 * January 1, 1970 GMT. Returns -1 when the date is unknown.
1079+ * @see #getFirstZonedDateTime(String)
10741080 */
10751081 public long getLastModified () {
10761082 return getFirstDate (LAST_MODIFIED , false );
@@ -1178,14 +1184,25 @@ public List<String> getVary() {
11781184
11791185 /**
11801186 * Set the given date under the given header name after formatting it as a string
1181- * using the pattern {@code "EEE, dd MMM yyyy HH:mm:ss zzz"} . The equivalent of
1187+ * using the RFC-1123 date-time formatter . The equivalent of
11821188 * {@link #set(String, String)} but for date headers.
11831189 * @since 3.2.4
1190+ * @see #setZonedDateTime(String, ZonedDateTime)
11841191 */
11851192 public void setDate (String headerName , long date ) {
1186- SimpleDateFormat dateFormat = new SimpleDateFormat (DATE_FORMATS [0 ], Locale .US );
1187- dateFormat .setTimeZone (GMT );
1188- set (headerName , dateFormat .format (new Date (date )));
1193+ Instant instant = Instant .ofEpochMilli (date );
1194+ ZonedDateTime zonedDateTime = ZonedDateTime .ofInstant (instant , GMT );
1195+ set (headerName , DATE_FORMATTERS [0 ].format (zonedDateTime ));
1196+ }
1197+
1198+ /**
1199+ * Set the given date under the given header name after formatting it as a string
1200+ * using the RFC-1123 date-time formatter. The equivalent of
1201+ * {@link #set(String, String)} but for date headers.
1202+ * @since 5.0
1203+ */
1204+ public void setZonedDateTime (String headerName , ZonedDateTime date ) {
1205+ set (headerName , DATE_FORMATTERS [0 ].format (date ));
11891206 }
11901207
11911208 /**
@@ -1195,6 +1212,7 @@ public void setDate(String headerName, long date) {
11951212 * @param headerName the header name
11961213 * @return the parsed date header, or -1 if none
11971214 * @since 3.2.4
1215+ * @see #getFirstZonedDateTime(String)
11981216 */
11991217 public long getFirstDate (String headerName ) {
12001218 return getFirstDate (headerName , true );
@@ -1210,32 +1228,69 @@ public long getFirstDate(String headerName) {
12101228 * {@link IllegalArgumentException} ({@code true}) or rather return -1
12111229 * in that case ({@code false})
12121230 * @return the parsed date header, or -1 if none (or invalid)
1213- */
1231+ * @see #getFirstZonedDateTime(String, boolean)
1232+ */
12141233 private long getFirstDate (String headerName , boolean rejectInvalid ) {
1234+ ZonedDateTime zonedDateTime = getFirstZonedDateTime (headerName , rejectInvalid );
1235+ return (zonedDateTime != null ? zonedDateTime .toInstant ().toEpochMilli () : -1 );
1236+ }
1237+
1238+ /**
1239+ * Parse the first header value for the given header name as a date,
1240+ * return {@code null} if there is no value, or raise {@link IllegalArgumentException}
1241+ * if the value cannot be parsed as a date.
1242+ * @param headerName the header name
1243+ * @return the parsed date header, or {@code null} if none
1244+ * @since 5.0
1245+ */
1246+ @ Nullable
1247+ public ZonedDateTime getFirstZonedDateTime (String headerName ) {
1248+ return getFirstZonedDateTime (headerName , true );
1249+ }
1250+
1251+ /**
1252+ * Parse the first header value for the given header name as a date,
1253+ * return {@code null} if there is no value or also in case of an invalid value
1254+ * (if {@code rejectInvalid=false}), or raise {@link IllegalArgumentException}
1255+ * if the value cannot be parsed as a date.
1256+ * @param headerName the header name
1257+ * @param rejectInvalid whether to reject invalid values with an
1258+ * {@link IllegalArgumentException} ({@code true}) or rather return {@code null}
1259+ * in that case ({@code false})
1260+ * @return the parsed date header, or {@code null} if none (or invalid)
1261+ */
1262+ @ Nullable
1263+ private ZonedDateTime getFirstZonedDateTime (String headerName , boolean rejectInvalid ) {
12151264 String headerValue = getFirst (headerName );
12161265 if (headerValue == null ) {
12171266 // No header value sent at all
1218- return - 1 ;
1267+ return null ;
12191268 }
12201269 if (headerValue .length () >= 3 ) {
12211270 // Short "0" or "-1" like values are never valid HTTP date headers...
1222- // Let's only bother with SimpleDateFormat parsing for long enough values.
1223- for (String dateFormat : DATE_FORMATS ) {
1224- SimpleDateFormat simpleDateFormat = new SimpleDateFormat (dateFormat , Locale .US );
1225- simpleDateFormat .setTimeZone (GMT );
1271+ // Let's only bother with DateTimeFormatter parsing for long enough values.
1272+
1273+ // See https://stackoverflow.com/questions/12626699/if-modified-since-http-header-passed-by-ie9-includes-length
1274+ int parametersIndex = headerValue .indexOf (";" );
1275+ if (parametersIndex != -1 ) {
1276+ headerValue = headerValue .substring (0 , parametersIndex );
1277+ }
1278+
1279+ for (DateTimeFormatter dateFormatter : DATE_FORMATTERS ) {
12261280 try {
1227- return simpleDateFormat .parse (headerValue ). getTime ( );
1281+ return ZonedDateTime .parse (headerValue , dateFormatter );
12281282 }
1229- catch (ParseException ex ) {
1283+ catch (DateTimeParseException ex ) {
12301284 // ignore
12311285 }
12321286 }
1287+
12331288 }
12341289 if (rejectInvalid ) {
12351290 throw new IllegalArgumentException ("Cannot parse date value \" " + headerValue +
12361291 "\" for \" " + headerName + "\" header" );
12371292 }
1238- return - 1 ;
1293+ return null ;
12391294 }
12401295
12411296 /**
0 commit comments