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