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 java.lang.ref.*; 016import java.text.*; 017import java.util.*; 018 019import javax.xml.bind.*; 020 021/** 022 * A utility class for parsing and formatting HTTP dates as used in cookies and other headers. 023 * 024 * <p> 025 * This class handles dates as defined by RFC 2616 section 3.3.1 as well as some other common non-standard formats. 026 * 027 * <p> 028 * This class was copied from HttpClient 4.3. 029 */ 030public final class DateUtils { 031 032 /** 033 * Date format pattern used to parse HTTP date headers in RFC 1123 format. 034 */ 035 public static final String PATTERN_RFC1123 = "EEE, dd MMM yyyy HH:mm:ss zzz"; 036 037 /** 038 * Date format pattern used to parse HTTP date headers in RFC 1036 format. 039 */ 040 public static final String PATTERN_RFC1036 = "EEE, dd-MMM-yy HH:mm:ss zzz"; 041 042 /** 043 * Date format pattern used to parse HTTP date headers in ANSI C <code>asctime()</code> format. 044 */ 045 public static final String PATTERN_ASCTIME = "EEE MMM d HH:mm:ss yyyy"; 046 private static final String[] DEFAULT_PATTERNS = new String[] { PATTERN_RFC1123, PATTERN_RFC1036, PATTERN_ASCTIME }; 047 private static final Date DEFAULT_TWO_DIGIT_YEAR_START; 048 private static final TimeZone GMT = TimeZone.getTimeZone("GMT"); 049 static { 050 final Calendar calendar = Calendar.getInstance(); 051 calendar.setTimeZone(GMT); 052 calendar.set(2000, Calendar.JANUARY, 1, 0, 0, 0); 053 calendar.set(Calendar.MILLISECOND, 0); 054 DEFAULT_TWO_DIGIT_YEAR_START = calendar.getTime(); 055 } 056 057 /** 058 * Parses a date value. The formats used for parsing the date value are retrieved from the default http params. 059 * 060 * @param dateValue the date value to parse 061 * @return the parsed date or null if input could not be parsed 062 */ 063 public static Date parseDate(final String dateValue) { 064 return parseDate(dateValue, null, null); 065 } 066 067 /** 068 * Parses the date value using the given date formats. 069 * 070 * @param dateValue the date value to parse 071 * @param dateFormats the date formats to use 072 * @return the parsed date or null if input could not be parsed 073 */ 074 public static Date parseDate(final String dateValue, final String[] dateFormats) { 075 return parseDate(dateValue, dateFormats, null); 076 } 077 078 /** 079 * Parses the date value using the given date formats. 080 * 081 * @param dateValue the date value to parse 082 * @param dateFormats the date formats to use 083 * @param startDate 084 * During parsing, two digit years will be placed in the range <code>startDate</code> to 085 * <code>startDate + 100 years</code>. This value may be <code>null</code>. When 086 * <code>null</code> is given as a parameter, year <code>2000</code> will be used. 087 * @return the parsed date or null if input could not be parsed 088 */ 089 public static Date parseDate(final String dateValue, final String[] dateFormats, final Date startDate) { 090 final String[] localDateFormats = dateFormats != null ? dateFormats : DEFAULT_PATTERNS; 091 final Date localStartDate = startDate != null ? startDate : DEFAULT_TWO_DIGIT_YEAR_START; 092 String v = dateValue; 093 // trim single quotes around date if present 094 // see issue #5279 095 if (v.length() > 1 && v.startsWith("'") && v.endsWith("'")) { 096 v = v.substring(1, v.length() - 1); 097 } 098 for (final String dateFormat : localDateFormats) { 099 final SimpleDateFormat dateParser = DateFormatHolder.formatFor(dateFormat); 100 dateParser.set2DigitYearStart(localStartDate); 101 final ParsePosition pos = new ParsePosition(0); 102 final Date result = dateParser.parse(v, pos); 103 if (pos.getIndex() != 0) { 104 return result; 105 } 106 } 107 return null; 108 } 109 110 /** 111 * Parses an ISO8601 string and converts it to a {@link Calendar}. 112 * 113 * @param s The string to parse. 114 * @return The parsed value, or <jk>null</jk> if the string was <jk>null</jk> or empty. 115 */ 116 public static Calendar parseISO8601Calendar(String s) { 117 if (StringUtils.isEmpty(s)) 118 return null; 119 return DatatypeConverter.parseDateTime(toValidISO8601DT(s)); 120 } 121 122 /** 123 * Formats the given date according to the RFC 1123 pattern. 124 * 125 * @param date The date to format. 126 * @return An RFC 1123 formatted date string. 127 * @see #PATTERN_RFC1123 128 */ 129 public static String formatDate(final Date date) { 130 return formatDate(date, PATTERN_RFC1123); 131 } 132 133 /** 134 * Formats the given date according to the specified pattern. 135 * 136 * <p> 137 * The pattern must conform to that used by the {@link SimpleDateFormat simple date format} class. 138 * 139 * @param date The date to format. 140 * @param pattern The pattern to use for formatting the date. 141 * @return A formatted date string. 142 * @throws IllegalArgumentException If the given date pattern is invalid. 143 * @see SimpleDateFormat 144 */ 145 public static String formatDate(final Date date, final String pattern) { 146 final SimpleDateFormat formatter = DateFormatHolder.formatFor(pattern); 147 return formatter.format(date); 148 } 149 150 /** 151 * Clears thread-local variable containing {@link java.text.DateFormat} cache. 152 */ 153 public static void clearThreadLocal() { 154 DateFormatHolder.clearThreadLocal(); 155 } 156 157 /** 158 * A factory for {@link SimpleDateFormat}s. 159 * 160 * <p> 161 * The instances are stored in a thread-local way because SimpleDateFormat is not thread-safe as noted in 162 * {@link SimpleDateFormat its javadoc}. 163 */ 164 static final class DateFormatHolder { 165 private static final ThreadLocal<SoftReference<Map<String,SimpleDateFormat>>> THREADLOCAL_FORMATS = 166 new ThreadLocal<SoftReference<Map<String,SimpleDateFormat>>>() { 167 @Override 168 protected SoftReference<Map<String,SimpleDateFormat>> initialValue() { 169 return new SoftReference<Map<String,SimpleDateFormat>>(new HashMap<String,SimpleDateFormat>()); 170 } 171 }; 172 173 /** 174 * Creates a {@link SimpleDateFormat} for the requested format string. 175 * 176 * @param pattern 177 * A non-<code>null</code> format String according to {@link SimpleDateFormat}. 178 * The format is not checked against <code>null</code> since all paths go through {@link DateUtils}. 179 * @return 180 * The requested format. 181 * This simple date-format should not be used to {@link SimpleDateFormat#applyPattern(String) apply} to a 182 * different pattern. 183 */ 184 public static SimpleDateFormat formatFor(final String pattern) { 185 final SoftReference<Map<String,SimpleDateFormat>> ref = THREADLOCAL_FORMATS.get(); 186 Map<String,SimpleDateFormat> formats = ref.get(); 187 if (formats == null) { 188 formats = new HashMap<>(); 189 THREADLOCAL_FORMATS.set(new SoftReference<>(formats)); 190 } 191 SimpleDateFormat format = formats.get(pattern); 192 if (format == null) { 193 format = new SimpleDateFormat(pattern, Locale.US); 194 format.setTimeZone(TimeZone.getTimeZone("GMT")); 195 formats.put(pattern, format); 196 } 197 return format; 198 } 199 200 public static void clearThreadLocal() { 201 THREADLOCAL_FORMATS.remove(); 202 } 203 } 204 205 /** 206 * Pads out an ISO8601 string so that it can be parsed using {@link DatatypeConverter#parseDateTime(String)}. 207 * 208 * <ul> 209 * <li><js>"2001-07-04T15:30:45-05:00"</js> --> <js>"2001-07-04T15:30:45-05:00"</js> 210 * <li><js>"2001-07-04T15:30:45Z"</js> --> <js>"2001-07-04T15:30:45Z"</js> 211 * <li><js>"2001-07-04T15:30:45.1Z"</js> --> <js>"2001-07-04T15:30:45.1Z"</js> 212 * <li><js>"2001-07-04T15:30Z"</js> --> <js>"2001-07-04T15:30:00Z"</js> 213 * <li><js>"2001-07-04T15:30"</js> --> <js>"2001-07-04T15:30:00"</js> 214 * <li><js>"2001-07-04"</js> --> <li><js>"2001-07-04T00:00:00"</js> 215 * <li><js>"2001-07"</js> --> <js>"2001-07-01T00:00:00"</js> 216 * <li><js>"2001"</js> --> <js>"2001-01-01T00:00:00"</js> 217 * </ul> 218 * 219 * @param in The string to pad. 220 * @return The padded string. 221 */ 222 public static final String toValidISO8601DT(String in) { 223 224 // "2001-07-04T15:30:45Z" 225 final int 226 S1 = 1, // Looking for - 227 S2 = 2, // Found -, looking for - 228 S3 = 3, // Found -, looking for T 229 S4 = 4, // Found T, looking for : 230 S5 = 5, // Found :, looking for : 231 S6 = 6; // Found : 232 233 int state = 1; 234 boolean needsT = false; 235 for (int i = 0; i < in.length(); i++) { 236 char c = in.charAt(i); 237 if (state == S1) { 238 if (c == '-') 239 state = S2; 240 } else if (state == S2) { 241 if (c == '-') 242 state = S3; 243 } else if (state == S3) { 244 if (c == 'T') 245 state = S4; 246 if (c == ' ') { 247 state = S4; 248 needsT = true; 249 } 250 } else if (state == S4) { 251 if (c == ':') 252 state = S5; 253 } else if (state == S5) { 254 if (c == ':') 255 state = S6; 256 } 257 } 258 259 if (needsT) 260 in = in.replace(' ', 'T'); 261 switch(state) { 262 case S1: return in + "-01-01T00:00:00"; 263 case S2: return in + "-01T00:00:00"; 264 case S3: return in + "T00:00:00"; 265 case S4: return in + ":00:00"; 266 case S5: return in + ":00"; 267 default: return in; 268 } 269 } 270}