22
22
import java .nio .charset .Charset ;
23
23
import java .text .DecimalFormat ;
24
24
import 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 ;
27
30
import java .util .ArrayList ;
28
31
import java .util .Collection ;
29
32
import java .util .Collections ;
30
- import java .util .Date ;
31
33
import java .util .EnumSet ;
32
34
import java .util .Iterator ;
33
35
import java .util .LinkedHashMap ;
36
38
import java .util .Locale ;
37
39
import java .util .Map ;
38
40
import java .util .Set ;
39
- import java .util .TimeZone ;
40
41
import java .util .regex .Matcher ;
41
42
import java .util .regex .Pattern ;
42
43
import java .util .stream .Collectors ;
47
48
import org .springframework .util .MultiValueMap ;
48
49
import org .springframework .util .StringUtils ;
49
50
51
+
50
52
/**
51
53
* Represents HTTP request and response headers, mapping string header names to a list of string values.
52
54
*
@@ -372,16 +374,6 @@ public class HttpHeaders implements MultiValueMap<String, String>, Serializable
372
374
*/
373
375
public static final String WWW_AUTHENTICATE = "WWW-Authenticate" ;
374
376
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
-
385
377
/**
386
378
* Pattern matching ETag multiple field values in headers such as "If-Match", "If-None-Match"
387
379
* @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
390
382
391
383
private static final DecimalFormatSymbols DECIMAL_FORMAT_SYMBOLS = new DecimalFormatSymbols (Locale .ENGLISH );
392
384
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
+ };
394
396
395
397
396
398
private final Map <String , List <String >> headers ;
@@ -924,6 +926,7 @@ public void setExpires(long expires) {
924
926
* as specified by the {@code Expires} header.
925
927
* <p>The date is returned as the number of milliseconds since
926
928
* January 1, 1970 GMT. Returns -1 when the date is unknown.
929
+ * @see #getFirstZonedDateTime(String)
927
930
*/
928
931
public long getExpires () {
929
932
return getFirstDate (EXPIRES , false );
@@ -1010,6 +1013,7 @@ public void setIfModifiedSince(long ifModifiedSince) {
1010
1013
* Return the value of the {@code If-Modified-Since} header.
1011
1014
* <p>The date is returned as the number of milliseconds since
1012
1015
* January 1, 1970 GMT. Returns -1 when the date is unknown.
1016
+ * @see #getFirstZonedDateTime(String)
1013
1017
*/
1014
1018
public long getIfModifiedSince () {
1015
1019
return getFirstDate (IF_MODIFIED_SINCE , false );
@@ -1051,6 +1055,7 @@ public void setIfUnmodifiedSince(long ifUnmodifiedSince) {
1051
1055
* <p>The date is returned as the number of milliseconds since
1052
1056
* January 1, 1970 GMT. Returns -1 when the date is unknown.
1053
1057
* @since 4.3
1058
+ * @see #getFirstZonedDateTime(String)
1054
1059
*/
1055
1060
public long getIfUnmodifiedSince () {
1056
1061
return getFirstDate (IF_UNMODIFIED_SINCE , false );
@@ -1071,6 +1076,7 @@ public void setLastModified(long lastModified) {
1071
1076
* {@code Last-Modified} header.
1072
1077
* <p>The date is returned as the number of milliseconds since
1073
1078
* January 1, 1970 GMT. Returns -1 when the date is unknown.
1079
+ * @see #getFirstZonedDateTime(String)
1074
1080
*/
1075
1081
public long getLastModified () {
1076
1082
return getFirstDate (LAST_MODIFIED , false );
@@ -1178,14 +1184,25 @@ public List<String> getVary() {
1178
1184
1179
1185
/**
1180
1186
* 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
1182
1188
* {@link #set(String, String)} but for date headers.
1183
1189
* @since 3.2.4
1190
+ * @see #setZonedDateTime(String, ZonedDateTime)
1184
1191
*/
1185
1192
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 ));
1189
1206
}
1190
1207
1191
1208
/**
@@ -1195,6 +1212,7 @@ public void setDate(String headerName, long date) {
1195
1212
* @param headerName the header name
1196
1213
* @return the parsed date header, or -1 if none
1197
1214
* @since 3.2.4
1215
+ * @see #getFirstZonedDateTime(String)
1198
1216
*/
1199
1217
public long getFirstDate (String headerName ) {
1200
1218
return getFirstDate (headerName , true );
@@ -1210,32 +1228,69 @@ public long getFirstDate(String headerName) {
1210
1228
* {@link IllegalArgumentException} ({@code true}) or rather return -1
1211
1229
* in that case ({@code false})
1212
1230
* @return the parsed date header, or -1 if none (or invalid)
1213
- */
1231
+ * @see #getFirstZonedDateTime(String, boolean)
1232
+ */
1214
1233
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 ) {
1215
1264
String headerValue = getFirst (headerName );
1216
1265
if (headerValue == null ) {
1217
1266
// No header value sent at all
1218
- return - 1 ;
1267
+ return null ;
1219
1268
}
1220
1269
if (headerValue .length () >= 3 ) {
1221
1270
// 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 ) {
1226
1280
try {
1227
- return simpleDateFormat .parse (headerValue ). getTime ( );
1281
+ return ZonedDateTime .parse (headerValue , dateFormatter );
1228
1282
}
1229
- catch (ParseException ex ) {
1283
+ catch (DateTimeParseException ex ) {
1230
1284
// ignore
1231
1285
}
1232
1286
}
1287
+
1233
1288
}
1234
1289
if (rejectInvalid ) {
1235
1290
throw new IllegalArgumentException ("Cannot parse date value \" " + headerValue +
1236
1291
"\" for \" " + headerName + "\" header" );
1237
1292
}
1238
- return - 1 ;
1293
+ return null ;
1239
1294
}
1240
1295
1241
1296
/**
0 commit comments