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