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> --> <js>"2001-07-04T15:30:45-05:00"</js> 231 * <li><js>"2001-07-04T15:30:45Z"</js> --> <js>"2001-07-04T15:30:45Z"</js> 232 * <li><js>"2001-07-04T15:30:45.1Z"</js> --> <js>"2001-07-04T15:30:45.1Z"</js> 233 * <li><js>"2001-07-04T15:30Z"</js> --> <js>"2001-07-04T15:30:00Z"</js> 234 * <li><js>"2001-07-04T15:30"</js> --> <js>"2001-07-04T15:30:00"</js> 235 * <li><js>"2001-07-04"</js> --> <li><js>"2001-07-04T00:00:00"</js> 236 * <li><js>"2001-07"</js> --> <js>"2001-07-01T00:00:00"</js> 237 * <li><js>"2001"</js> --> <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}