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