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    * Formats the given date according to the RFC 1123 pattern.
129    *
130    * @param date The date to format.
131    * @return An RFC 1123 formatted date string.
132    * @see #PATTERN_RFC1123
133    */
134   public static String formatDate(final Date date) {
135      return formatDate(date, PATTERN_RFC1123);
136   }
137
138   /**
139    * Formats the given date according to the specified pattern.
140    *
141    * <p>
142    * The pattern must conform to that used by the {@link SimpleDateFormat simple date format} class.
143    *
144    * @param date The date to format.
145    * @param pattern The pattern to use for formatting the date.
146    * @return A formatted date string.
147    * @throws IllegalArgumentException If the given date pattern is invalid.
148    * @see SimpleDateFormat
149    */
150   public static String formatDate(final Date date, final String pattern) {
151      final SimpleDateFormat formatter = DateFormatHolder.formatFor(pattern);
152      return formatter.format(date);
153   }
154
155   /**
156    * Clears thread-local variable containing {@link java.text.DateFormat} cache.
157    */
158   public static void clearThreadLocal() {
159      DateFormatHolder.clearThreadLocal();
160   }
161
162   /**
163    * A factory for {@link SimpleDateFormat}s.
164    *
165    * <p>
166    * The instances are stored in a thread-local way because SimpleDateFormat is not thread-safe as noted in
167    * {@link SimpleDateFormat its javadoc}.
168    */
169   static final class DateFormatHolder {
170      private static final ThreadLocal<SoftReference<Map<String,SimpleDateFormat>>> THREADLOCAL_FORMATS =
171            new ThreadLocal<SoftReference<Map<String,SimpleDateFormat>>>() {
172         @Override
173         protected SoftReference<Map<String,SimpleDateFormat>> initialValue() {
174            return new SoftReference<Map<String,SimpleDateFormat>>(new HashMap<String,SimpleDateFormat>());
175         }
176      };
177
178      /**
179       * Creates a {@link SimpleDateFormat} for the requested format string.
180       *
181       * @param pattern
182       *    A non-<code>null</code> format String according to {@link SimpleDateFormat}.
183       *    The format is not checked against <code>null</code> since all paths go through {@link DateUtils}.
184       * @return
185       *    The requested format.
186       *    This simple date-format should not be used to {@link SimpleDateFormat#applyPattern(String) apply} to a
187       *    different pattern.
188       */
189      public static SimpleDateFormat formatFor(final String pattern) {
190         final SoftReference<Map<String,SimpleDateFormat>> ref = THREADLOCAL_FORMATS.get();
191         Map<String,SimpleDateFormat> formats = ref.get();
192         if (formats == null) {
193            formats = new HashMap<>();
194            THREADLOCAL_FORMATS.set(new SoftReference<>(formats));
195         }
196         SimpleDateFormat format = formats.get(pattern);
197         if (format == null) {
198            format = new SimpleDateFormat(pattern, Locale.US);
199            format.setTimeZone(TimeZone.getTimeZone("GMT"));
200            formats.put(pattern, format);
201         }
202         return format;
203      }
204
205      public static void clearThreadLocal() {
206         THREADLOCAL_FORMATS.remove();
207      }
208   }
209
210   /**
211    * Pads out an ISO8601 string so that it can be parsed using {@link DatatypeConverter#parseDateTime(String)}.
212    *
213    * <ul>
214    *    <li><js>"2001-07-04T15:30:45-05:00"</js> --&gt; <js>"2001-07-04T15:30:45-05:00"</js>
215    *    <li><js>"2001-07-04T15:30:45Z"</js> --&gt; <js>"2001-07-04T15:30:45Z"</js>
216    *    <li><js>"2001-07-04T15:30:45.1Z"</js> --&gt; <js>"2001-07-04T15:30:45.1Z"</js>
217    *    <li><js>"2001-07-04T15:30Z"</js> --&gt; <js>"2001-07-04T15:30:00Z"</js>
218    *    <li><js>"2001-07-04T15:30"</js> --&gt; <js>"2001-07-04T15:30:00"</js>
219    *    <li><js>"2001-07-04"</js> --&gt; <li><js>"2001-07-04T00:00:00"</js>
220    *    <li><js>"2001-07"</js> --&gt; <js>"2001-07-01T00:00:00"</js>
221    *    <li><js>"2001"</js> --&gt; <js>"2001-01-01T00:00:00"</js>
222    * </ul>
223    *
224    * @param in The string to pad.
225    * @return The padded string.
226    */
227   public static final String toValidISO8601DT(String in) {
228
229      // "2001-07-04T15:30:45Z"
230      final int
231         S1 = 1, // Looking for -
232         S2 = 2, // Found -, looking for -
233         S3 = 3, // Found -, looking for T
234         S4 = 4, // Found T, looking for :
235         S5 = 5, // Found :, looking for :
236         S6 = 6; // Found :
237
238      int state = 1;
239      boolean needsT = false;
240      for (int i = 0; i < in.length(); i++) {
241         char c = in.charAt(i);
242         if (state == S1) {
243            if (c == '-')
244               state = S2;
245         } else if (state == S2) {
246            if (c == '-')
247               state = S3;
248         } else if (state == S3) {
249            if (c == 'T')
250               state = S4;
251            if (c == ' ') {
252               state = S4;
253               needsT = true;
254            }
255         } else if (state == S4) {
256            if (c == ':')
257               state = S5;
258         } else if (state == S5) {
259            if (c == ':')
260               state = S6;
261         }
262      }
263
264      if (needsT)
265         in = in.replace(' ', 'T');
266      switch(state) {
267         case S1: return in + "-01-01T00:00:00";
268         case S2: return in + "-01T00:00:00";
269         case S3: return in + "T00:00:00";
270         case S4: return in + ":00:00";
271         case S5: return in + ":00";
272         default: return in;
273      }
274   }
275}