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.lang.ref.SoftReference;
31  import java.text.ParsePosition;
32  import java.text.SimpleDateFormat;
33  import java.util.Calendar;
34  import java.util.Date;
35  import java.util.HashMap;
36  import java.util.Locale;
37  import java.util.Map;
38  import java.util.TimeZone;
39  
40  import org.apache.hc.core5.http.Header;
41  import org.apache.hc.core5.http.MessageHeaders;
42  import org.apache.hc.core5.util.Args;
43  
44  /**
45   * A utility class for parsing and formatting HTTP dates as used in cookies and
46   * other headers.  This class handles dates as defined by RFC 2616 section
47   * 3.3.1 as well as some other common non-standard formats.
48   *
49   * @since 4.3
50   */
51  public final class DateUtils {
52  
53      /**
54       * Date format pattern used to parse HTTP date headers in RFC 1123 format.
55       */
56      public static final String PATTERN_RFC1123 = "EEE, dd MMM yyyy HH:mm:ss zzz";
57  
58      /**
59       * Date format pattern used to parse HTTP date headers in RFC 1036 format.
60       */
61      public static final String PATTERN_RFC1036 = "EEE, dd-MMM-yy HH:mm:ss zzz";
62  
63      /**
64       * Date format pattern used to parse HTTP date headers in ANSI C
65       * {@code asctime()} format.
66       */
67      public static final String PATTERN_ASCTIME = "EEE MMM d HH:mm:ss yyyy";
68  
69      private static final String[] DEFAULT_PATTERNS = new String[] {
70          PATTERN_RFC1123,
71          PATTERN_RFC1036,
72          PATTERN_ASCTIME
73      };
74  
75      private static final Date DEFAULT_TWO_DIGIT_YEAR_START;
76  
77      public static final TimeZone GMT = TimeZone.getTimeZone("GMT");
78  
79      static {
80          final Calendar calendar = Calendar.getInstance();
81          calendar.setTimeZone(GMT);
82          calendar.set(2000, Calendar.JANUARY, 1, 0, 0, 0);
83          calendar.set(Calendar.MILLISECOND, 0);
84          DEFAULT_TWO_DIGIT_YEAR_START = calendar.getTime();
85      }
86  
87      /**
88       * Parses a date value.  The formats used for parsing the date value are retrieved from
89       * the default http params.
90       *
91       * @param dateValue the date value to parse
92       *
93       * @return the parsed date or null if input could not be parsed
94       */
95      public static Date parseDate(final String dateValue) {
96          return parseDate(dateValue, null, null);
97      }
98  
99      /**
100      * Parses a date value from a header with the given name.
101      *
102      * @param headers message headers
103      * @param headerName header name
104      *
105      * @return the parsed date or null if input could not be parsed
106      *
107      * @since 5.0
108      */
109     public static Date parseDate(final MessageHeaders headers, final String headerName) {
110         if (headers == null) {
111             return null;
112         }
113         final Header header = headers.getFirstHeader(headerName);
114         if (header == null) {
115             return null;
116         }
117         return parseDate(header.getValue(), null, null);
118     }
119 
120     /**
121      * Tests if the first message is after (newer) than second one
122      * using the given message header for comparison.
123      *
124      * @param message1 the first message
125      * @param message2 the second message
126      * @param headerName header name
127      *
128      * @return {@code true} if both messages contain a header with the given name
129      *  and the value of the header from the first message is newer that of
130      *  the second message.
131      *
132      * @since 5.0
133      */
134     public static boolean isAfter(
135             final MessageHeaders message1,
136             final MessageHeaders message2,
137             final String headerName) {
138         if (message1 != null && message2 != null) {
139             final Header dateHeader1 = message1.getFirstHeader(headerName);
140             if (dateHeader1 != null) {
141                 final Header dateHeader2 = message2.getFirstHeader(headerName);
142                 if (dateHeader2 != null) {
143                     final Date date1 = parseDate(dateHeader1.getValue());
144                     if (date1 != null) {
145                         final Date date2 = parseDate(dateHeader2.getValue());
146                         if (date2 != null) {
147                             return date1.after(date2);
148                         }
149                     }
150                 }
151             }
152         }
153         return false;
154     }
155 
156     /**
157      * Tests if the first message is before (older) than the second one
158      * using the given message header for comparison.
159      *
160      * @param message1 the first message
161      * @param message2 the second message
162      * @param headerName header name
163      *
164      * @return {@code true} if both messages contain a header with the given name
165      *  and the value of the header from the first message is older that of
166      *  the second message.
167      *
168      * @since 5.0
169      */
170     public static boolean isBefore(
171             final MessageHeaders message1,
172             final MessageHeaders message2,
173             final String headerName) {
174         if (message1 != null && message2 != null) {
175             final Header dateHeader1 = message1.getFirstHeader(headerName);
176             if (dateHeader1 != null) {
177                 final Header dateHeader2 = message2.getFirstHeader(headerName);
178                 if (dateHeader2 != null) {
179                     final Date date1 = parseDate(dateHeader1.getValue());
180                     if (date1 != null) {
181                         final Date date2 = parseDate(dateHeader2.getValue());
182                         if (date2 != null) {
183                             return date1.before(date2);
184                         }
185                     }
186                 }
187             }
188         }
189         return false;
190     }
191 
192     /**
193      * Parses the date value using the given date formats.
194      *
195      * @param dateValue the date value to parse
196      * @param dateFormats the date formats to use
197      *
198      * @return the parsed date or null if input could not be parsed
199      */
200     public static Date parseDate(final String dateValue, final String[] dateFormats) {
201         return parseDate(dateValue, dateFormats, null);
202     }
203 
204     /**
205      * Parses the date value using the given date formats.
206      *
207      * @param dateValue the date value to parse
208      * @param dateFormats the date formats to use
209      * @param startDate During parsing, two digit years will be placed in the range
210      * {@code startDate} to {@code startDate + 100 years}. This value may
211      * be {@code null}. When {@code null} is given as a parameter, year
212      * {@code 2000} will be used.
213      *
214      * @return the parsed date or null if input could not be parsed
215      */
216     public static Date parseDate(
217             final String dateValue,
218             final String[] dateFormats,
219             final Date startDate) {
220         Args.notNull(dateValue, "Date value");
221         final String[] localDateFormats = dateFormats != null ? dateFormats : DEFAULT_PATTERNS;
222         final Date localStartDate = startDate != null ? startDate : DEFAULT_TWO_DIGIT_YEAR_START;
223         String v = dateValue;
224         // trim single quotes around date if present
225         // see issue #5279
226         if (v.length() > 1 && v.startsWith("'") && v.endsWith("'")) {
227             v = v.substring (1, v.length() - 1);
228         }
229 
230         for (final String dateFormat : localDateFormats) {
231             final SimpleDateFormat dateParser = DateFormatHolder.formatFor(dateFormat);
232             dateParser.set2DigitYearStart(localStartDate);
233             final ParsePosition pos = new ParsePosition(0);
234             final Date result = dateParser.parse(v, pos);
235             if (pos.getIndex() != 0) {
236                 return result;
237             }
238         }
239         return null;
240     }
241 
242     /**
243      * Formats the given date according to the RFC 1123 pattern.
244      *
245      * @param date The date to format.
246      * @return An RFC 1123 formatted date string.
247      *
248      * @see #PATTERN_RFC1123
249      */
250     public static String formatDate(final Date date) {
251         return formatDate(date, PATTERN_RFC1123);
252     }
253 
254     /**
255      * Formats the given date according to the specified pattern.  The pattern
256      * must conform to that used by the {@link SimpleDateFormat simple date
257      * format} class.
258      *
259      * @param date The date to format.
260      * @param pattern The pattern to use for formatting the date.
261      * @return A formatted date string.
262      *
263      * @throws IllegalArgumentException If the given date pattern is invalid.
264      *
265      * @see SimpleDateFormat
266      */
267     public static String formatDate(final Date date, final String pattern) {
268         Args.notNull(date, "Date");
269         Args.notNull(pattern, "Pattern");
270         final SimpleDateFormat formatter = DateFormatHolder.formatFor(pattern);
271         return formatter.format(date);
272     }
273 
274     /**
275      * Clears thread-local variable containing {@link java.text.DateFormat} cache.
276      *
277      * @since 4.3
278      */
279     public static void clearThreadLocal() {
280         DateFormatHolder.clearThreadLocal();
281     }
282 
283     /** This class should not be instantiated. */
284     private DateUtils() {
285     }
286 
287     /**
288      * A factory for {@link SimpleDateFormat}s. The instances are stored in a
289      * threadlocal way because SimpleDateFormat is not threadsafe as noted in
290      * {@link SimpleDateFormat its javadoc}.
291      *
292      */
293     final static class DateFormatHolder {
294 
295         private static final ThreadLocal<SoftReference<Map<String, SimpleDateFormat>>> THREADLOCAL_FORMATS = new ThreadLocal<>();
296 
297         /**
298          * creates a {@link SimpleDateFormat} for the requested format string.
299          *
300          * @param pattern
301          *            a non-{@code null} format String according to
302          *            {@link SimpleDateFormat}. The format is not checked against
303          *            {@code null} since all paths go through
304          *            {@link DateUtils}.
305          * @return the requested format. This simple dateformat should not be used
306          *         to {@link SimpleDateFormat#applyPattern(String) apply} to a
307          *         different pattern.
308          */
309         public static SimpleDateFormat formatFor(final String pattern) {
310             final SoftReference<Map<String, SimpleDateFormat>> ref = THREADLOCAL_FORMATS.get();
311             Map<String, SimpleDateFormat> formats = ref == null ? null : ref.get();
312             if (formats == null) {
313                 formats = new HashMap<>();
314                 THREADLOCAL_FORMATS.set(new SoftReference<>(formats));
315             }
316 
317             SimpleDateFormat format = formats.get(pattern);
318             if (format == null) {
319                 format = new SimpleDateFormat(pattern, Locale.US);
320                 format.setTimeZone(TimeZone.getTimeZone("GMT"));
321                 formats.put(pattern, format);
322             }
323 
324             return format;
325         }
326 
327         public static void clearThreadLocal() {
328             THREADLOCAL_FORMATS.remove();
329         }
330 
331     }
332 
333 }