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.common.internal.StringUtils.*;
016import static org.apache.juneau.common.internal.ThrowableUtils.*;
017
018import java.lang.ref.*;
019import java.text.*;
020import java.time.format.*;
021import java.util.*;
022
023import javax.xml.bind.*;
024
025import org.apache.juneau.reflect.*;
026
027/**
028 * A utility class for parsing and formatting HTTP dates as used in cookies and other headers.
029 *
030 * <p>
031 * This class handles dates as defined by RFC 2616 section 3.3.1 as well as some other common non-standard formats.
032 *
033 * <p>
034 * This class was copied from HttpClient 4.3.
035 *
036 * <h5 class='section'>See Also:</h5><ul>
037 * </ul>
038 */
039public final class DateUtils {
040
041   /**
042    * Date format pattern used to parse HTTP date headers in RFC 1123 format.
043    */
044   public static final String PATTERN_RFC1123 = "EEE, dd MMM yyyy HH:mm:ss zzz";
045
046   /**
047    * Date format pattern used to parse HTTP date headers in RFC 1036 format.
048    */
049   public static final String PATTERN_RFC1036 = "EEE, dd-MMM-yy HH:mm:ss zzz";
050
051   /**
052    * Date format pattern used to parse HTTP date headers in ANSI C <c>asctime()</c> format.
053    */
054   public static final String PATTERN_ASCTIME = "EEE MMM d HH:mm:ss yyyy";
055   private static final TimeZone GMT = TimeZone.getTimeZone("GMT");
056   static {
057      final Calendar calendar = Calendar.getInstance();
058      calendar.setTimeZone(GMT);
059      calendar.set(2000, Calendar.JANUARY, 1, 0, 0, 0);
060      calendar.set(Calendar.MILLISECOND, 0);
061   }
062
063   /**
064    * Parses an ISO8601 string and converts it to a {@link Calendar}.
065    *
066    * @param s The string to parse.
067    * @return The parsed value, or <jk>null</jk> if the string was <jk>null</jk> or empty.
068    */
069   public static Calendar parseISO8601Calendar(String s) {
070      if (isEmpty(s))
071         return null;
072      return DatatypeConverter.parseDateTime(toValidISO8601DT(s));
073   }
074
075   /**
076    * Formats the given date according to the specified pattern.
077    *
078    * <p>
079    * The pattern must conform to that used by the {@link SimpleDateFormat simple date format} class.
080    *
081    * @param date The date to format.
082    * @param pattern The pattern to use for formatting the date.
083    * @return A formatted date string.
084    * @throws IllegalArgumentException If the given date pattern is invalid.
085    * @see SimpleDateFormat
086    */
087   public static String formatDate(final Date date, final String pattern) {
088      final SimpleDateFormat formatter = DateFormatHolder.formatFor(pattern);
089      return formatter.format(date);
090   }
091
092   /**
093    * Clears thread-local variable containing {@link java.text.DateFormat} cache.
094    */
095   public static void clearThreadLocal() {
096      DateFormatHolder.clearThreadLocal();
097   }
098
099   /**
100    * A factory for {@link SimpleDateFormat}s.
101    *
102    * <p>
103    * The instances are stored in a thread-local way because SimpleDateFormat is not thread-safe as noted in
104    * {@link SimpleDateFormat its javadoc}.
105    */
106   static final class DateFormatHolder {
107      private static final ThreadLocal<SoftReference<Map<String,SimpleDateFormat>>> THREADLOCAL_FORMATS =
108            new ThreadLocal<>() {
109         @Override
110         protected SoftReference<Map<String,SimpleDateFormat>> initialValue() {
111            Map<String,SimpleDateFormat> m = new HashMap<>();
112            return new SoftReference<>(m);
113         }
114      };
115
116      /**
117       * Creates a {@link SimpleDateFormat} for the requested format string.
118       *
119       * @param pattern
120       *    A non-<c>null</c> format String according to {@link SimpleDateFormat}.
121       *    The format is not checked against <c>null</c> since all paths go through {@link DateUtils}.
122       * @return
123       *    The requested format.
124       *    This simple date-format should not be used to {@link SimpleDateFormat#applyPattern(String) apply} to a
125       *    different pattern.
126       */
127      public static SimpleDateFormat formatFor(final String pattern) {
128         final SoftReference<Map<String,SimpleDateFormat>> ref = THREADLOCAL_FORMATS.get();
129         Map<String,SimpleDateFormat> formats = ref.get();
130         if (formats == null) {
131            formats = new HashMap<>();
132            THREADLOCAL_FORMATS.set(new SoftReference<>(formats));
133         }
134         SimpleDateFormat format = formats.get(pattern);
135         if (format == null) {
136            format = new SimpleDateFormat(pattern, Locale.US);
137            format.setTimeZone(TimeZone.getTimeZone("GMT"));
138            formats.put(pattern, format);
139         }
140         return format;
141      }
142
143      public static void clearThreadLocal() {
144         THREADLOCAL_FORMATS.remove();
145      }
146   }
147
148   /**
149    * Pads out an ISO8601 string so that it can be parsed using {@link DatatypeConverter#parseDateTime(String)}.
150    *
151    * <ul>
152    *    <li><js>"2001-07-04T15:30:45-05:00"</js> -&gt; <js>"2001-07-04T15:30:45-05:00"</js>
153    *    <li><js>"2001-07-04T15:30:45Z"</js> -&gt; <js>"2001-07-04T15:30:45Z"</js>
154    *    <li><js>"2001-07-04T15:30:45.1Z"</js> -&gt; <js>"2001-07-04T15:30:45.1Z"</js>
155    *    <li><js>"2001-07-04T15:30Z"</js> -&gt; <js>"2001-07-04T15:30:00Z"</js>
156    *    <li><js>"2001-07-04T15:30"</js> -&gt; <js>"2001-07-04T15:30:00"</js>
157    *    <li><js>"2001-07-04"</js> -&gt; <li><js>"2001-07-04T00:00:00"</js>
158    *    <li><js>"2001-07"</js> -&gt; <js>"2001-07-01T00:00:00"</js>
159    *    <li><js>"2001"</js> -&gt; <js>"2001-01-01T00:00:00"</js>
160    * </ul>
161    *
162    * @param in The string to pad.
163    * @return The padded string.
164    */
165   public static final String toValidISO8601DT(String in) {
166
167      // "2001-07-04T15:30:45Z"
168      final int
169         S1 = 1, // Looking for -
170         S2 = 2, // Found -, looking for -
171         S3 = 3, // Found -, looking for T
172         S4 = 4, // Found T, looking for :
173         S5 = 5, // Found :, looking for :
174         S6 = 6; // Found :
175
176      int state = 1;
177      boolean needsT = false;
178      for (int i = 0; i < in.length(); i++) {
179         char c = in.charAt(i);
180         if (state == S1) {
181            if (c == '-')
182               state = S2;
183         } else if (state == S2) {
184            if (c == '-')
185               state = S3;
186         } else if (state == S3) {
187            if (c == 'T')
188               state = S4;
189            if (c == ' ') {
190               state = S4;
191               needsT = true;
192            }
193         } else if (state == S4) {
194            if (c == ':')
195               state = S5;
196         } else if (state == S5) {
197            if (c == ':')
198               state = S6;
199         }
200      }
201
202      if (needsT)
203         in = in.replace(' ', 'T');
204      switch(state) {
205         case S1: return in + "-01-01T00:00:00";
206         case S2: return in + "-01T00:00:00";
207         case S3: return in + "T00:00:00";
208         case S4: return in + ":00:00";
209         case S5: return in + ":00";
210         default: return in;
211      }
212   }
213
214   /**
215    * Returns a {@link DateTimeFormatter} using either a pattern or predefined pattern name.
216    *
217    * @param pattern The pattern (e.g. <js>"yyyy-MM-dd"</js>) or pattern name (e.g. <js>"ISO_INSTANT"</js>).
218    * @return The formatter.
219    */
220   public static DateTimeFormatter getFormatter(String pattern) {
221      if (isEmpty(pattern))
222         return DateTimeFormatter.ISO_INSTANT;
223      try {
224         FieldInfo fi = ClassInfo.of(DateTimeFormatter.class).getPublicField(x -> x.isStatic() && x.hasName(pattern));
225         if (fi != null)
226            return (DateTimeFormatter)fi.inner().get(null);
227         return DateTimeFormatter.ofPattern(pattern);
228      } catch (IllegalArgumentException | IllegalAccessException e) {
229         throw asRuntimeException(e);
230      }
231   }
232}