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.
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      *
149      * @param dateValue the instant value to parse
150      * @param dateFormatters the date/time formats to use
151      *
152      * @return the parsed instant or null if input could not be parsed
153      *
154      * @since 5.2
155      */
156     public static Instant parseDate(final String dateValue, final DateTimeFormatter... dateFormatters) {
157         Args.notNull(dateValue, "Date value");
158         String v = dateValue;
159         // trim single quotes around date if present
160         // see issue #5279
161         if (v.length() > 1 && v.startsWith("'") && v.endsWith("'")) {
162             v = v.substring (1, v.length() - 1);
163         }
164 
165         for (final DateTimeFormatter dateFormatter : dateFormatters) {
166             try {
167                 return Instant.from(dateFormatter.parse(v));
168             } catch (final DateTimeParseException ignore) {
169             }
170         }
171         return null;
172     }
173 
174     /**
175      * Parses the instant value using the standard date/time formats ({@link #PATTERN_RFC1123},
176      * {@link #PATTERN_RFC1036}, {@link #PATTERN_ASCTIME}).
177      *
178      * @param dateValue the instant value to parse
179      *
180      * @return the parsed instant or null if input could not be parsed
181      *
182      * @since 5.2
183      */
184     public static Instant parseStandardDate(final String dateValue) {
185         return parseDate(dateValue, STANDARD_PATTERNS);
186     }
187 
188     /**
189      * Parses an instant value from a header with the given name.
190      *
191      * @param headers message headers
192      * @param headerName header name
193      *
194      * @return the parsed instant or null if input could not be parsed
195      *
196      * @since 5.2
197      */
198     public static Instant parseStandardDate(final MessageHeaders headers, final String headerName) {
199         if (headers == null) {
200             return null;
201         }
202         final Header header = headers.getFirstHeader(headerName);
203         if (header == null) {
204             return null;
205         }
206         return parseStandardDate(header.getValue());
207     }
208 
209     /**
210      * Formats the given instant according to the RFC 1123 pattern.
211      *
212      * @param instant Instant to format.
213      * @return An RFC 1123 formatted instant string.
214      *
215      * @see #PATTERN_RFC1123
216      *
217      * @since 5.2
218      */
219     public static String formatStandardDate(final Instant instant) {
220         return formatDate(instant, FORMATTER_RFC1123);
221     }
222 
223     /**
224      * Formats the given date according to the specified pattern.
225      *
226      * @param instant Instant to format.
227      * @param dateTimeFormatter The pattern to use for formatting the instant.
228      * @return A formatted instant string.
229      *
230      * @throws IllegalArgumentException If the given date pattern is invalid.
231      *
232      * @since 5.2
233      */
234     public static String formatDate(final Instant instant, final DateTimeFormatter dateTimeFormatter) {
235         Args.notNull(instant, "Instant");
236         Args.notNull(dateTimeFormatter, "DateTimeFormatter");
237         return dateTimeFormatter.format(instant.atZone(GMT_ID));
238     }
239 
240     /**
241      * @deprecated This attribute is no longer supported as a part of the public API.
242      */
243     @Deprecated
244     public static final TimeZone GMT = TimeZone.getTimeZone("GMT");
245 
246     /**
247      * Parses a date value.  The formats used for parsing the date value are retrieved from
248      * the default http params.
249      *
250      * @param dateValue the date value to parse
251      *
252      * @return the parsed date or null if input could not be parsed
253      *
254      * @deprecated Use {@link #parseStandardDate(String)}
255      */
256     @Deprecated
257     public static Date parseDate(final String dateValue) {
258         return parseDate(dateValue, null, null);
259     }
260 
261     /**
262      * Parses a date value from a header with the given name.
263      *
264      * @param headers message headers
265      * @param headerName header name
266      *
267      * @return the parsed date or null if input could not be parsed
268      *
269      * @since 5.0
270      *
271      * @deprecated Use {@link #parseStandardDate(MessageHeaders, String)}
272      */
273     @Deprecated
274     public static Date parseDate(final MessageHeaders headers, final String headerName) {
275         return toDate(parseStandardDate(headers, headerName));
276     }
277 
278     /**
279      * Tests if the first message is after (newer) than second one
280      * using the given message header for comparison.
281      *
282      * @param message1 the first message
283      * @param message2 the second message
284      * @param headerName header name
285      *
286      * @return {@code true} if both messages contain a header with the given name
287      *  and the value of the header from the first message is newer that of
288      *  the second message.
289      *
290      * @since 5.0
291      *
292      * @deprecated This method is no longer supported as a part of the public API.
293      */
294     @Deprecated
295     public static boolean isAfter(
296             final MessageHeaders message1,
297             final MessageHeaders message2,
298             final String headerName) {
299         if (message1 != null && message2 != null) {
300             final Header dateHeader1 = message1.getFirstHeader(headerName);
301             if (dateHeader1 != null) {
302                 final Header dateHeader2 = message2.getFirstHeader(headerName);
303                 if (dateHeader2 != null) {
304                     final Date date1 = parseDate(dateHeader1.getValue());
305                     if (date1 != null) {
306                         final Date date2 = parseDate(dateHeader2.getValue());
307                         if (date2 != null) {
308                             return date1.after(date2);
309                         }
310                     }
311                 }
312             }
313         }
314         return false;
315     }
316 
317     /**
318      * Tests if the first message is before (older) than the second one
319      * using the given message header for comparison.
320      *
321      * @param message1 the first message
322      * @param message2 the second message
323      * @param headerName header name
324      *
325      * @return {@code true} if both messages contain a header with the given name
326      *  and the value of the header from the first message is older that of
327      *  the second message.
328      *
329      * @since 5.0
330      *
331      * @deprecated This method is no longer supported as a part of the public API.
332      */
333     @Deprecated
334     public static boolean isBefore(
335             final MessageHeaders message1,
336             final MessageHeaders message2,
337             final String headerName) {
338         if (message1 != null && message2 != null) {
339             final Header dateHeader1 = message1.getFirstHeader(headerName);
340             if (dateHeader1 != null) {
341                 final Header dateHeader2 = message2.getFirstHeader(headerName);
342                 if (dateHeader2 != null) {
343                     final Date date1 = parseDate(dateHeader1.getValue());
344                     if (date1 != null) {
345                         final Date date2 = parseDate(dateHeader2.getValue());
346                         if (date2 != null) {
347                             return date1.before(date2);
348                         }
349                     }
350                 }
351             }
352         }
353         return false;
354     }
355 
356     /**
357      * Parses the date value using the given date/time formats.
358      *
359      * @param dateValue the date value to parse
360      * @param dateFormats the date/time formats to use
361      *
362      * @return the parsed date or null if input could not be parsed
363      *
364      * @deprecated Use {@link #parseDate(String, DateTimeFormatter...)}
365      */
366     @Deprecated
367     public static Date parseDate(final String dateValue, final String[] dateFormats) {
368         return parseDate(dateValue, dateFormats, null);
369     }
370 
371     /**
372      * Parses the date value using the given date/time formats.
373      *
374      * @param dateValue the date value to parse
375      * @param dateFormats the date/time formats to use
376      * @param startDate During parsing, two digit years will be placed in the range
377      * {@code startDate} to {@code startDate + 100 years}. This value may
378      * be {@code null}. When {@code null} is given as a parameter, year
379      * {@code 2000} will be used.
380      *
381      * @return the parsed date or null if input could not be parsed
382      *
383      * @deprecated Use {@link #parseDate(String, DateTimeFormatter...)}
384      */
385     @Deprecated
386     public static Date parseDate(
387             final String dateValue,
388             final String[] dateFormats,
389             final Date startDate) {
390         final DateTimeFormatter[] dateTimeFormatters;
391         if (dateFormats != null) {
392             dateTimeFormatters = new DateTimeFormatter[dateFormats.length];
393             for (int i = 0; i < dateFormats.length; i++) {
394                 dateTimeFormatters[i] = new DateTimeFormatterBuilder()
395                         .parseLenient()
396                         .parseCaseInsensitive()
397                         .appendPattern(dateFormats[i])
398                         .toFormatter();
399             }
400         } else {
401             dateTimeFormatters = STANDARD_PATTERNS;
402         }
403         return toDate(parseDate(dateValue, dateTimeFormatters));
404     }
405 
406     /**
407      * Formats the given date according to the RFC 1123 pattern.
408      *
409      * @param date The date to format.
410      * @return An RFC 1123 formatted date string.
411      *
412      * @see #PATTERN_RFC1123
413      *
414      * @deprecated Use {@link #formatStandardDate(Instant)}
415      */
416     @Deprecated
417     public static String formatDate(final Date date) {
418         return formatStandardDate(toInstant(date));
419     }
420 
421     /**
422      * Formats the given date according to the specified pattern.
423      *
424      * @param date The date to format.
425      * @param pattern The pattern to use for formatting the date.
426      * @return A formatted date string.
427      *
428      * @throws IllegalArgumentException If the given date pattern is invalid.
429      *
430      * @deprecated Use {@link #formatDate(Instant, DateTimeFormatter)}
431      */
432     @Deprecated
433     public static String formatDate(final Date date, final String pattern) {
434         Args.notNull(date, "Date");
435         Args.notNull(pattern, "Pattern");
436         return DateTimeFormatter.ofPattern(pattern).format(toInstant(date).atZone(GMT_ID));
437     }
438 
439     /**
440      * Clears thread-local variable containing {@link java.text.DateFormat} cache.
441      *
442      * @since 4.3
443      *
444      * @deprecated Noop method. Do not use.
445      */
446     @Deprecated
447     public static void clearThreadLocal() {
448     }
449 
450     /** This class should not be instantiated. */
451     private DateUtils() {
452     }
453 
454 }