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