001// ***************************************************************************************************************************
002// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements.  See the NOTICE file *
003// * distributed with this work for additional information regarding copyright ownership.  The ASF licenses this file        *
004// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance            *
005// * with the License.  You may obtain a copy of the License at                                                              *
006// *                                                                                                                         *
007// *  http://www.apache.org/licenses/LICENSE-2.0                                                                             *
008// *                                                                                                                         *
009// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an  *
010// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the License for the        *
011// * specific language governing permissions and limitations under the License.                                              *
012// ***************************************************************************************************************************
013package org.apache.juneau.internal;
014
015import java.lang.ref.*;
016import java.text.*;
017import java.util.*;
018
019import javax.xml.bind.*;
020
021/**
022 * A utility class for parsing and formatting HTTP dates as used in cookies and other headers.
023 * 
024 * <p>
025 * This class handles dates as defined by RFC 2616 section 3.3.1 as well as some other common non-standard formats.
026 * 
027 * <p>
028 * This class was copied from HttpClient 4.3.
029 */
030public final class DateUtils {
031
032   /**
033    * Date format pattern used to parse HTTP date headers in RFC 1123 format.
034    */
035   public static final String PATTERN_RFC1123 = "EEE, dd MMM yyyy HH:mm:ss zzz";
036
037   /**
038    * Date format pattern used to parse HTTP date headers in RFC 1036 format.
039    */
040   public static final String PATTERN_RFC1036 = "EEE, dd-MMM-yy HH:mm:ss zzz";
041
042   /**
043    * Date format pattern used to parse HTTP date headers in ANSI C <code>asctime()</code> format.
044    */
045   public static final String PATTERN_ASCTIME = "EEE MMM d HH:mm:ss yyyy";
046   private static final String[] DEFAULT_PATTERNS = new String[] { PATTERN_RFC1123, PATTERN_RFC1036, PATTERN_ASCTIME };
047   private static final Date DEFAULT_TWO_DIGIT_YEAR_START;
048   private static final TimeZone GMT = TimeZone.getTimeZone("GMT");
049   static {
050      final Calendar calendar = Calendar.getInstance();
051      calendar.setTimeZone(GMT);
052      calendar.set(2000, Calendar.JANUARY, 1, 0, 0, 0);
053      calendar.set(Calendar.MILLISECOND, 0);
054      DEFAULT_TWO_DIGIT_YEAR_START = calendar.getTime();
055   }
056
057   /**
058    * Parses a date value. The formats used for parsing the date value are retrieved from the default http params.
059    * 
060    * @param dateValue the date value to parse
061    * @return the parsed date or null if input could not be parsed
062    */
063   public static Date parseDate(final String dateValue) {
064      return parseDate(dateValue, null, null);
065   }
066
067   /**
068    * Parses the date value using the given date formats.
069    * 
070    * @param dateValue the date value to parse
071    * @param dateFormats the date formats to use
072    * @return the parsed date or null if input could not be parsed
073    */
074   public static Date parseDate(final String dateValue, final String[] dateFormats) {
075      return parseDate(dateValue, dateFormats, null);
076   }
077
078   /**
079    * Parses the date value using the given date formats.
080    * 
081    * @param dateValue the date value to parse
082    * @param dateFormats the date formats to use
083    * @param startDate
084    *    During parsing, two digit years will be placed in the range <code>startDate</code> to
085    *    <code>startDate + 100 years</code>. This value may be <code>null</code>. When
086    *    <code>null</code> is given as a parameter, year <code>2000</code> will be used.
087    * @return the parsed date or null if input could not be parsed
088    */
089   public static Date parseDate(final String dateValue, final String[] dateFormats, final Date startDate) {
090      final String[] localDateFormats = dateFormats != null ? dateFormats : DEFAULT_PATTERNS;
091      final Date localStartDate = startDate != null ? startDate : DEFAULT_TWO_DIGIT_YEAR_START;
092      String v = dateValue;
093      // trim single quotes around date if present
094      // see issue #5279
095      if (v.length() > 1 && v.startsWith("'") && v.endsWith("'")) {
096         v = v.substring(1, v.length() - 1);
097      }
098      for (final String dateFormat : localDateFormats) {
099         final SimpleDateFormat dateParser = DateFormatHolder.formatFor(dateFormat);
100         dateParser.set2DigitYearStart(localStartDate);
101         final ParsePosition pos = new ParsePosition(0);
102         final Date result = dateParser.parse(v, pos);
103         if (pos.getIndex() != 0) {
104            return result;
105         }
106      }
107      return null;
108   }
109
110   /**
111    * Parses an ISO8601 string and converts it to a {@link Calendar}.
112    * 
113    * @param s The string to parse.
114    * @return The parsed value, or <jk>null</jk> if the string was <jk>null</jk> or empty.
115    */
116   public static Calendar parseISO8601Calendar(String s) {
117      if (StringUtils.isEmpty(s))
118         return null;
119      return DatatypeConverter.parseDateTime(toValidISO8601DT(s));
120   }
121
122   /**
123    * Formats the given date according to the RFC 1123 pattern.
124    * 
125    * @param date The date to format.
126    * @return An RFC 1123 formatted date string.
127    * @see #PATTERN_RFC1123
128    */
129   public static String formatDate(final Date date) {
130      return formatDate(date, PATTERN_RFC1123);
131   }
132
133   /**
134    * Formats the given date according to the specified pattern.
135    * 
136    * <p>
137    * The pattern must conform to that used by the {@link SimpleDateFormat simple date format} class.
138    * 
139    * @param date The date to format.
140    * @param pattern The pattern to use for formatting the date.
141    * @return A formatted date string.
142    * @throws IllegalArgumentException If the given date pattern is invalid.
143    * @see SimpleDateFormat
144    */
145   public static String formatDate(final Date date, final String pattern) {
146      final SimpleDateFormat formatter = DateFormatHolder.formatFor(pattern);
147      return formatter.format(date);
148   }
149
150   /**
151    * Clears thread-local variable containing {@link java.text.DateFormat} cache.
152    */
153   public static void clearThreadLocal() {
154      DateFormatHolder.clearThreadLocal();
155   }
156
157   /**
158    * A factory for {@link SimpleDateFormat}s.
159    * 
160    * <p>
161    * The instances are stored in a thread-local way because SimpleDateFormat is not thread-safe as noted in
162    * {@link SimpleDateFormat its javadoc}.
163    */
164   static final class DateFormatHolder {
165      private static final ThreadLocal<SoftReference<Map<String,SimpleDateFormat>>> THREADLOCAL_FORMATS =
166            new ThreadLocal<SoftReference<Map<String,SimpleDateFormat>>>() {
167         @Override
168         protected SoftReference<Map<String,SimpleDateFormat>> initialValue() {
169            return new SoftReference<Map<String,SimpleDateFormat>>(new HashMap<String,SimpleDateFormat>());
170         }
171      };
172
173      /**
174       * Creates a {@link SimpleDateFormat} for the requested format string.
175       * 
176       * @param pattern
177       *    A non-<code>null</code> format String according to {@link SimpleDateFormat}.
178       *    The format is not checked against <code>null</code> since all paths go through {@link DateUtils}.
179       * @return
180       *    The requested format.
181       *    This simple date-format should not be used to {@link SimpleDateFormat#applyPattern(String) apply} to a
182       *    different pattern.
183       */
184      public static SimpleDateFormat formatFor(final String pattern) {
185         final SoftReference<Map<String,SimpleDateFormat>> ref = THREADLOCAL_FORMATS.get();
186         Map<String,SimpleDateFormat> formats = ref.get();
187         if (formats == null) {
188            formats = new HashMap<>();
189            THREADLOCAL_FORMATS.set(new SoftReference<>(formats));
190         }
191         SimpleDateFormat format = formats.get(pattern);
192         if (format == null) {
193            format = new SimpleDateFormat(pattern, Locale.US);
194            format.setTimeZone(TimeZone.getTimeZone("GMT"));
195            formats.put(pattern, format);
196         }
197         return format;
198      }
199
200      public static void clearThreadLocal() {
201         THREADLOCAL_FORMATS.remove();
202      }
203   }
204
205   /**
206    * Pads out an ISO8601 string so that it can be parsed using {@link DatatypeConverter#parseDateTime(String)}.
207    * 
208    * <ul>
209    *    <li><js>"2001-07-04T15:30:45-05:00"</js> --&gt; <js>"2001-07-04T15:30:45-05:00"</js>
210    *    <li><js>"2001-07-04T15:30:45Z"</js> --&gt; <js>"2001-07-04T15:30:45Z"</js>
211    *    <li><js>"2001-07-04T15:30:45.1Z"</js> --&gt; <js>"2001-07-04T15:30:45.1Z"</js>
212    *    <li><js>"2001-07-04T15:30Z"</js> --&gt; <js>"2001-07-04T15:30:00Z"</js>
213    *    <li><js>"2001-07-04T15:30"</js> --&gt; <js>"2001-07-04T15:30:00"</js>
214    *    <li><js>"2001-07-04"</js> --&gt; <li><js>"2001-07-04T00:00:00"</js>
215    *    <li><js>"2001-07"</js> --&gt; <js>"2001-07-01T00:00:00"</js>
216    *    <li><js>"2001"</js> --&gt; <js>"2001-01-01T00:00:00"</js>
217    * </ul>
218    * 
219    * @param in The string to pad.
220    * @return The padded string.
221    */
222   public static final String toValidISO8601DT(String in) {
223
224      // "2001-07-04T15:30:45Z"
225      final int
226         S1 = 1, // Looking for -
227         S2 = 2, // Found -, looking for -
228         S3 = 3, // Found -, looking for T
229         S4 = 4, // Found T, looking for :
230         S5 = 5, // Found :, looking for :
231         S6 = 6; // Found :
232
233      int state = 1;
234      boolean needsT = false;
235      for (int i = 0; i < in.length(); i++) {
236         char c = in.charAt(i);
237         if (state == S1) {
238            if (c == '-')
239               state = S2;
240         } else if (state == S2) {
241            if (c == '-')
242               state = S3;
243         } else if (state == S3) {
244            if (c == 'T')
245               state = S4;
246            if (c == ' ') {
247               state = S4;
248               needsT = true;
249            }
250         } else if (state == S4) {
251            if (c == ':')
252               state = S5;
253         } else if (state == S5) {
254            if (c == ':')
255               state = S6;
256         }
257      }
258
259      if (needsT)
260         in = in.replace(' ', 'T');
261      switch(state) {
262         case S1: return in + "-01-01T00:00:00";
263         case S2: return in + "-01T00:00:00";
264         case S3: return in + "T00:00:00";
265         case S4: return in + ":00:00";
266         case S5: return in + ":00";
267         default: return in;
268      }
269   }
270}