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