View Javadoc
1   /*
2    * ====================================================================
3    * Licensed to the Apache Software Foundation (ASF) under one
4    * or more contributor license agreements.  See the NOTICE file
5    * distributed with this work for additional information
6    * regarding copyright ownership.  The ASF licenses this file
7    * to you under the Apache License, Version 2.0 (the
8    * "License"); you may not use this file except in compliance
9    * with the License.  You may obtain a copy of the License at
10   *
11   *   http://www.apache.org/licenses/LICENSE-2.0
12   *
13   * Unless required by applicable law or agreed to in writing,
14   * software distributed under the License is distributed on an
15   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16   * KIND, either express or implied.  See the License for the
17   * specific language governing permissions and limitations
18   * under the License.
19   * ====================================================================
20   *
21   * This software consists of voluntary contributions made by many
22   * individuals on behalf of the Apache Software Foundation.  For more
23   * information on the Apache Software Foundation, please see
24   * <http://www.apache.org/>.
25   *
26   */
27  
28  package org.apache.hc.client5.http.utils;
29  
30  import java.time.Instant;
31  import java.time.LocalDateTime;
32  import java.time.ZoneId;
33  import java.time.ZoneOffset;
34  import java.time.format.DateTimeFormatter;
35  import java.time.format.DateTimeFormatterBuilder;
36  import java.time.format.DateTimeParseException;
37  import java.util.Date;
38  import java.util.Locale;
39  import java.util.TimeZone;
40  
41  import org.apache.hc.core5.http.Header;
42  import org.apache.hc.core5.http.MessageHeaders;
43  import org.apache.hc.core5.util.Args;
44  
45  /**
46   * A utility class for parsing and formatting HTTP dates as used in cookies and
47   * other headers.  This class handles dates as defined by RFC 2616 section
48   * 3.3.1 as well as some other common non-standard formats.
49   *
50   * @since 4.3
51   */
52  public final class DateUtils {
53  
54      /**
55       * Date format pattern used to parse HTTP date headers in RFC 1123 format.
56       */
57      public static final String PATTERN_RFC1123 = "EEE, dd MMM yyyy HH:mm:ss zzz";
58  
59      /**
60       * Date formatter used to parse HTTP date headers in RFC 1123 format.
61       *
62       * @since 5.2
63       */
64      public static final DateTimeFormatter FORMATTER_RFC1123 = new DateTimeFormatterBuilder()
65              .parseLenient()
66              .parseCaseInsensitive()
67              .appendPattern(PATTERN_RFC1123)
68              .toFormatter(Locale.ENGLISH);
69  
70      /**
71       * Date format pattern used to parse HTTP date headers in RFC 1036 format.
72       */
73      public static final String PATTERN_RFC1036 = "EEE, dd-MMM-yy HH:mm:ss zzz";
74  
75      /**
76       * Date formatter used to parse HTTP date headers in RFC 1036 format.
77       *
78       * @since 5.2
79       */
80      public static final DateTimeFormatter FORMATTER_RFC1036 = new DateTimeFormatterBuilder()
81              .parseLenient()
82              .parseCaseInsensitive()
83              .appendPattern(PATTERN_RFC1036)
84              .toFormatter(Locale.ENGLISH);
85  
86      /**
87       * Date format pattern used to parse HTTP date headers in ANSI C
88       * {@code asctime()} format.
89       */
90      public static final String PATTERN_ASCTIME = "EEE MMM d HH:mm:ss yyyy";
91  
92      /**
93       * Date formatter used to parse HTTP date headers in in ANSI C {@code asctime()} format.
94       *
95       * @since 5.2
96       */
97      public static final DateTimeFormatter FORMATTER_ASCTIME = new DateTimeFormatterBuilder()
98              .parseLenient()
99              .parseCaseInsensitive()
100             .appendPattern(PATTERN_ASCTIME)
101             .toFormatter(Locale.ENGLISH);
102 
103     /**
104      * Standard date formatters: {@link #FORMATTER_RFC1123}, {@link #FORMATTER_RFC1036}, {@link #FORMATTER_ASCTIME}.
105      *
106      * @since 5.2
107      */
108     public static final DateTimeFormatter[] STANDARD_PATTERNS = new DateTimeFormatter[] {
109             FORMATTER_RFC1123,
110             FORMATTER_RFC1036,
111             FORMATTER_ASCTIME
112     };
113 
114     static final ZoneId GMT_ID = ZoneId.of("GMT");
115 
116     /**
117      * @since 5.2
118      */
119     public static Date toDate(final Instant instant) {
120         return instant != null ? new Date(instant.toEpochMilli()) : null;
121     }
122 
123     /**
124      * @since 5.2
125      */
126     public static Instant toInstant(final Date date) {
127         return date != null ? Instant.ofEpochMilli(date.getTime()) : null;
128     }
129 
130     /**
131      * @since 5.2
132      */
133     public static LocalDateTime toUTC(final Instant instant) {
134         return instant != null ? instant.atZone(ZoneOffset.UTC).toLocalDateTime() : null;
135     }
136 
137     /**
138      * @since 5.2
139      */
140     public static LocalDateTime toUTC(final Date date) {
141         return toUTC(toInstant(date));
142     }
143 
144     /**
145      * Parses the date value using the given date/time formats.
146      *
147      * @param dateValue the instant value to parse
148      * @param dateFormatters the date/time formats to use
149      *
150      * @return the parsed instant or null if input could not be parsed
151      *
152      * @since 5.2
153      */
154     public static Instant parseDate(final String dateValue, final DateTimeFormatter... dateFormatters) {
155         Args.notNull(dateValue, "Date value");
156         String v = dateValue;
157         // trim single quotes around date if present
158         // see issue #5279
159         if (v.length() > 1 && v.startsWith("'") && v.endsWith("'")) {
160             v = v.substring (1, v.length() - 1);
161         }
162 
163         for (final DateTimeFormatter dateFormatter : dateFormatters) {
164             try {
165                 return Instant.from(dateFormatter.parse(v));
166             } catch (final DateTimeParseException ignore) {
167             }
168         }
169         return null;
170     }
171 
172     /**
173      * Parses the instant value using the standard date/time formats ({@link #PATTERN_RFC1123},
174      * {@link #PATTERN_RFC1036}, {@link #PATTERN_ASCTIME}).
175      *
176      * @param dateValue the instant value to parse
177      *
178      * @return the parsed instant or null if input could not be parsed
179      *
180      * @since 5.2
181      */
182     public static Instant parseStandardDate(final String dateValue) {
183         return parseDate(dateValue, STANDARD_PATTERNS);
184     }
185 
186     /**
187      * Parses an instant value from a header with the given name.
188      *
189      * @param headers message headers
190      * @param headerName header name
191      *
192      * @return the parsed instant or null if input could not be parsed
193      *
194      * @since 5.2
195      */
196     public static Instant parseStandardDate(final MessageHeaders headers, final String headerName) {
197         if (headers == null) {
198             return null;
199         }
200         final Header header = headers.getFirstHeader(headerName);
201         if (header == null) {
202             return null;
203         }
204         return parseStandardDate(header.getValue());
205     }
206 
207     /**
208      * Formats the given instant according to the RFC 1123 pattern.
209      *
210      * @param instant Instant to format.
211      * @return An RFC 1123 formatted instant string.
212      *
213      * @see #PATTERN_RFC1123
214      *
215      * @since 5.2
216      */
217     public static String formatStandardDate(final Instant instant) {
218         return formatDate(instant, FORMATTER_RFC1123);
219     }
220 
221     /**
222      * Formats the given date according to the specified pattern.
223      *
224      * @param instant Instant to format.
225      * @param dateTimeFormatter The pattern to use for formatting the instant.
226      * @return A formatted instant string.
227      *
228      * @throws IllegalArgumentException If the given date pattern is invalid.
229      *
230      * @since 5.2
231      */
232     public static String formatDate(final Instant instant, final DateTimeFormatter dateTimeFormatter) {
233         Args.notNull(instant, "Instant");
234         Args.notNull(dateTimeFormatter, "DateTimeFormatter");
235         return dateTimeFormatter.format(instant.atZone(GMT_ID));
236     }
237 
238     /**
239      * @deprecated This attribute is no longer supported as a part of the public API.
240      */
241     @Deprecated
242     public static final TimeZone GMT = TimeZone.getTimeZone("GMT");
243 
244     /**
245      * Parses a date value.  The formats used for parsing the date value are retrieved from
246      * the default http params.
247      *
248      * @param dateValue the date value to parse
249      *
250      * @return the parsed date or null if input could not be parsed
251      *
252      * @deprecated Use {@link #parseStandardDate(String)}
253      */
254     @Deprecated
255     public static Date parseDate(final String dateValue) {
256         return parseDate(dateValue, null, null);
257     }
258 
259     /**
260      * Parses a date value from a header with the given name.
261      *
262      * @param headers message headers
263      * @param headerName header name
264      *
265      * @return the parsed date or null if input could not be parsed
266      *
267      * @since 5.0
268      *
269      * @deprecated Use {@link #parseStandardDate(MessageHeaders, String)}
270      */
271     @Deprecated
272     public static Date parseDate(final MessageHeaders headers, final String headerName) {
273         return toDate(parseStandardDate(headers, headerName));
274     }
275 
276     /**
277      * Tests if the first message is after (newer) than second one
278      * using the given message header for comparison.
279      *
280      * @param message1 the first message
281      * @param message2 the second message
282      * @param headerName header name
283      *
284      * @return {@code true} if both messages contain a header with the given name
285      *  and the value of the header from the first message is newer that of
286      *  the second message.
287      *
288      * @since 5.0
289      *
290      * @deprecated This method is no longer supported as a part of the public API.
291      */
292     @Deprecated
293     public static boolean isAfter(
294             final MessageHeaders message1,
295             final MessageHeaders message2,
296             final String headerName) {
297         if (message1 != null && message2 != null) {
298             final Header dateHeader1 = message1.getFirstHeader(headerName);
299             if (dateHeader1 != null) {
300                 final Header dateHeader2 = message2.getFirstHeader(headerName);
301                 if (dateHeader2 != null) {
302                     final Date date1 = parseDate(dateHeader1.getValue());
303                     if (date1 != null) {
304                         final Date date2 = parseDate(dateHeader2.getValue());
305                         if (date2 != null) {
306                             return date1.after(date2);
307                         }
308                     }
309                 }
310             }
311         }
312         return false;
313     }
314 
315     /**
316      * Tests if the first message is before (older) than the second one
317      * using the given message header for comparison.
318      *
319      * @param message1 the first message
320      * @param message2 the second message
321      * @param headerName header name
322      *
323      * @return {@code true} if both messages contain a header with the given name
324      *  and the value of the header from the first message is older that of
325      *  the second message.
326      *
327      * @since 5.0
328      *
329      * @deprecated This method is no longer supported as a part of the public API.
330      */
331     @Deprecated
332     public static boolean isBefore(
333             final MessageHeaders message1,
334             final MessageHeaders message2,
335             final String headerName) {
336         if (message1 != null && message2 != null) {
337             final Header dateHeader1 = message1.getFirstHeader(headerName);
338             if (dateHeader1 != null) {
339                 final Header dateHeader2 = message2.getFirstHeader(headerName);
340                 if (dateHeader2 != null) {
341                     final Date date1 = parseDate(dateHeader1.getValue());
342                     if (date1 != null) {
343                         final Date date2 = parseDate(dateHeader2.getValue());
344                         if (date2 != null) {
345                             return date1.before(date2);
346                         }
347                     }
348                 }
349             }
350         }
351         return false;
352     }
353 
354     /**
355      * Parses the date value using the given date/time formats.
356      *
357      * @param dateValue the date value to parse
358      * @param dateFormats the date/time formats to use
359      *
360      * @return the parsed date or null if input could not be parsed
361      *
362      * @deprecated Use {@link #parseDate(String, DateTimeFormatter...)}
363      */
364     @Deprecated
365     public static Date parseDate(final String dateValue, final String[] dateFormats) {
366         return parseDate(dateValue, dateFormats, null);
367     }
368 
369     /**
370      * Parses the date value using the given date/time formats.
371      *
372      * @param dateValue the date value to parse
373      * @param dateFormats the date/time formats to use
374      * @param startDate During parsing, two digit years will be placed in the range
375      * {@code startDate} to {@code startDate + 100 years}. This value may
376      * be {@code null}. When {@code null} is given as a parameter, year
377      * {@code 2000} will be used.
378      *
379      * @return the parsed date or null if input could not be parsed
380      *
381      * @deprecated Use {@link #parseDate(String, DateTimeFormatter...)}
382      */
383     @Deprecated
384     public static Date parseDate(
385             final String dateValue,
386             final String[] dateFormats,
387             final Date startDate) {
388         final DateTimeFormatter[] dateTimeFormatters;
389         if (dateFormats != null) {
390             dateTimeFormatters = new DateTimeFormatter[dateFormats.length];
391             for (int i = 0; i < dateFormats.length; i++) {
392                 dateTimeFormatters[i] = new DateTimeFormatterBuilder()
393                         .parseLenient()
394                         .parseCaseInsensitive()
395                         .appendPattern(dateFormats[i])
396                         .toFormatter();
397             }
398         } else {
399             dateTimeFormatters = STANDARD_PATTERNS;
400         }
401         return toDate(parseDate(dateValue, dateTimeFormatters));
402     }
403 
404     /**
405      * Formats the given date according to the RFC 1123 pattern.
406      *
407      * @param date The date to format.
408      * @return An RFC 1123 formatted date string.
409      *
410      * @see #PATTERN_RFC1123
411      *
412      * @deprecated Use {@link #formatStandardDate(Instant)}
413      */
414     @Deprecated
415     public static String formatDate(final Date date) {
416         return formatStandardDate(toInstant(date));
417     }
418 
419     /**
420      * Formats the given date according to the specified pattern.
421      *
422      * @param date The date to format.
423      * @param pattern The pattern to use for formatting the date.
424      * @return A formatted date string.
425      *
426      * @throws IllegalArgumentException If the given date pattern is invalid.
427      *
428      * @deprecated Use {@link #formatDate(Instant, DateTimeFormatter)}
429      */
430     @Deprecated
431     public static String formatDate(final Date date, final String pattern) {
432         Args.notNull(date, "Date");
433         Args.notNull(pattern, "Pattern");
434         return DateTimeFormatter.ofPattern(pattern).format(toInstant(date).atZone(GMT_ID));
435     }
436 
437     /**
438      * Clears thread-local variable containing {@link java.text.DateFormat} cache.
439      *
440      * @since 4.3
441      *
442      * @deprecated Noop method. Do not use.
443      */
444     @Deprecated
445     public static void clearThreadLocal() {
446     }
447 
448     /** This class should not be instantiated. */
449     private DateUtils() {
450     }
451 
452 }