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