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 Map<String,SimpleDateFormat> m = new HashMap<>(); 187 return new SoftReference<>(m); 188 } 189 }; 190 191 /** 192 * Creates a {@link SimpleDateFormat} for the requested format string. 193 * 194 * @param pattern 195 * A non-<code>null</code> format String according to {@link SimpleDateFormat}. 196 * The format is not checked against <code>null</code> since all paths go through {@link DateUtils}. 197 * @return 198 * The requested format. 199 * This simple date-format should not be used to {@link SimpleDateFormat#applyPattern(String) apply} to a 200 * different pattern. 201 */ 202 public static SimpleDateFormat formatFor(final String pattern) { 203 final SoftReference<Map<String,SimpleDateFormat>> ref = THREADLOCAL_FORMATS.get(); 204 Map<String,SimpleDateFormat> formats = ref.get(); 205 if (formats == null) { 206 formats = new HashMap<>(); 207 THREADLOCAL_FORMATS.set(new SoftReference<>(formats)); 208 } 209 SimpleDateFormat format = formats.get(pattern); 210 if (format == null) { 211 format = new SimpleDateFormat(pattern, Locale.US); 212 format.setTimeZone(TimeZone.getTimeZone("GMT")); 213 formats.put(pattern, format); 214 } 215 return format; 216 } 217 218 public static void clearThreadLocal() { 219 THREADLOCAL_FORMATS.remove(); 220 } 221 } 222 223 /** 224 * Pads out an ISO8601 string so that it can be parsed using {@link DatatypeConverter#parseDateTime(String)}. 225 * 226 * <ul> 227 * <li><js>"2001-07-04T15:30:45-05:00"</js> --> <js>"2001-07-04T15:30:45-05:00"</js> 228 * <li><js>"2001-07-04T15:30:45Z"</js> --> <js>"2001-07-04T15:30:45Z"</js> 229 * <li><js>"2001-07-04T15:30:45.1Z"</js> --> <js>"2001-07-04T15:30:45.1Z"</js> 230 * <li><js>"2001-07-04T15:30Z"</js> --> <js>"2001-07-04T15:30:00Z"</js> 231 * <li><js>"2001-07-04T15:30"</js> --> <js>"2001-07-04T15:30:00"</js> 232 * <li><js>"2001-07-04"</js> --> <li><js>"2001-07-04T00:00:00"</js> 233 * <li><js>"2001-07"</js> --> <js>"2001-07-01T00:00:00"</js> 234 * <li><js>"2001"</js> --> <js>"2001-01-01T00:00:00"</js> 235 * </ul> 236 * 237 * @param in The string to pad. 238 * @return The padded string. 239 */ 240 public static final String toValidISO8601DT(String in) { 241 242 // "2001-07-04T15:30:45Z" 243 final int 244 S1 = 1, // Looking for - 245 S2 = 2, // Found -, looking for - 246 S3 = 3, // Found -, looking for T 247 S4 = 4, // Found T, looking for : 248 S5 = 5, // Found :, looking for : 249 S6 = 6; // Found : 250 251 int state = 1; 252 boolean needsT = false; 253 for (int i = 0; i < in.length(); i++) { 254 char c = in.charAt(i); 255 if (state == S1) { 256 if (c == '-') 257 state = S2; 258 } else if (state == S2) { 259 if (c == '-') 260 state = S3; 261 } else if (state == S3) { 262 if (c == 'T') 263 state = S4; 264 if (c == ' ') { 265 state = S4; 266 needsT = true; 267 } 268 } else if (state == S4) { 269 if (c == ':') 270 state = S5; 271 } else if (state == S5) { 272 if (c == ':') 273 state = S6; 274 } 275 } 276 277 if (needsT) 278 in = in.replace(' ', 'T'); 279 switch(state) { 280 case S1: return in + "-01-01T00:00:00"; 281 case S2: return in + "-01T00:00:00"; 282 case S3: return in + "T00:00:00"; 283 case S4: return in + ":00:00"; 284 case S5: return in + ":00"; 285 default: return in; 286 } 287 } 288}