001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017 018package org.apache.commons.net.ftp.parser; 019 020import java.text.DateFormatSymbols; 021import java.text.ParseException; 022import java.text.ParsePosition; 023import java.text.SimpleDateFormat; 024import java.util.Calendar; 025import java.util.Date; 026import java.util.TimeZone; 027 028import org.apache.commons.net.ftp.Configurable; 029import org.apache.commons.net.ftp.FTPClientConfig; 030 031/** 032 * Default implementation of the {@link FTPTimestampParser FTPTimestampParser} 033 * interface also implements the {@link org.apache.commons.net.ftp.Configurable Configurable} 034 * interface to allow the parsing to be configured from the outside. 035 * 036 * @see ConfigurableFTPFileEntryParserImpl 037 * @since 1.4 038 */ 039public class FTPTimestampParserImpl implements 040 FTPTimestampParser, Configurable 041{ 042 043 044 /** The date format for all dates, except possibly recent dates. Assumed to include the year. */ 045 private SimpleDateFormat defaultDateFormat; 046 /* The index in CALENDAR_UNITS of the smallest time unit in defaultDateFormat */ 047 private int defaultDateSmallestUnitIndex; 048 049 /** The format used for recent dates (which don't have the year). May be null. */ 050 private SimpleDateFormat recentDateFormat; 051 /* The index in CALENDAR_UNITS of the smallest time unit in recentDateFormat */ 052 private int recentDateSmallestUnitIndex; 053 054 private boolean lenientFutureDates = false; 055 056 /* 057 * List of units in order of increasing significance. 058 * This allows the code to clear all units in the Calendar until it 059 * reaches the least significant unit in the parse string. 060 * The date formats are analysed to find the least significant 061 * unit (e.g. Minutes or Milliseconds) and the appropriate index to 062 * the array is saved. 063 * This is done by searching the array for the unit specifier, 064 * and returning the index. When clearing the Calendar units, 065 * the code loops through the array until the previous entry. 066 * e.g. for MINUTE it would clear MILLISECOND and SECOND 067 */ 068 private static final int[] CALENDAR_UNITS = { 069 Calendar.MILLISECOND, 070 Calendar.SECOND, 071 Calendar.MINUTE, 072 Calendar.HOUR_OF_DAY, 073 Calendar.DAY_OF_MONTH, 074 Calendar.MONTH, 075 Calendar.YEAR}; 076 077 /* 078 * Return the index to the array representing the least significant 079 * unit found in the date format. 080 * Default is 0 (to avoid dropping precision) 081 */ 082 private static int getEntry(SimpleDateFormat dateFormat) { 083 if (dateFormat == null) { 084 return 0; 085 } 086 final String FORMAT_CHARS="SsmHdM"; 087 final String pattern = dateFormat.toPattern(); 088 for(char ch : FORMAT_CHARS.toCharArray()) { 089 if (pattern.indexOf(ch) != -1){ // found the character 090 switch(ch) { 091 case 'S': 092 return indexOf(Calendar.MILLISECOND); 093 case 's': 094 return indexOf(Calendar.SECOND); 095 case 'm': 096 return indexOf(Calendar.MINUTE); 097 case 'H': 098 return indexOf(Calendar.HOUR_OF_DAY); 099 case 'd': 100 return indexOf(Calendar.DAY_OF_MONTH); 101 case 'M': 102 return indexOf(Calendar.MONTH); 103 } 104 } 105 } 106 return 0; 107 } 108 109 /* 110 * Find the entry in the CALENDAR_UNITS array. 111 */ 112 private static int indexOf(int calendarUnit) { 113 int i; 114 for(i = 0; i <CALENDAR_UNITS.length; i++) { 115 if (calendarUnit == CALENDAR_UNITS[i]) { 116 return i; 117 } 118 } 119 return 0; 120 } 121 122 /* 123 * Sets the Calendar precision (used by FTPFile#toFormattedDate) by clearing 124 * the immediately preceeding unit (if any). 125 * Unfortunately the clear(int) method results in setting all other units. 126 */ 127 private static void setPrecision(int index, Calendar working) { 128 if (index <= 0) { // e.g. MILLISECONDS 129 return; 130 } 131 final int field = CALENDAR_UNITS[index-1]; 132 // Just in case the analysis is wrong, stop clearing if 133 // field value is not the default. 134 final int value = working.get(field); 135 if (value != 0) { // don't reset if it has a value 136// new Throwable("Unexpected value "+value).printStackTrace(); // DEBUG 137 } else { 138 working.clear(field); // reset just the required field 139 } 140 } 141 142 /** 143 * The only constructor for this class. 144 */ 145 public FTPTimestampParserImpl() { 146 setDefaultDateFormat(DEFAULT_SDF, null); 147 setRecentDateFormat(DEFAULT_RECENT_SDF, null); 148 } 149 150 /** 151 * Implements the one {@link FTPTimestampParser#parseTimestamp(String) method} 152 * in the {@link FTPTimestampParser FTPTimestampParser} interface 153 * according to this algorithm: 154 * 155 * If the recentDateFormat member has been defined, try to parse the 156 * supplied string with that. If that parse fails, or if the recentDateFormat 157 * member has not been defined, attempt to parse with the defaultDateFormat 158 * member. If that fails, throw a ParseException. 159 * 160 * This method assumes that the server time is the same as the local time. 161 * 162 * @see FTPTimestampParserImpl#parseTimestamp(String, Calendar) 163 * 164 * @param timestampStr The timestamp to be parsed 165 * @return a Calendar with the parsed timestamp 166 */ 167 @Override 168 public Calendar parseTimestamp(String timestampStr) throws ParseException { 169 Calendar now = Calendar.getInstance(); 170 return parseTimestamp(timestampStr, now); 171 } 172 173 /** 174 * If the recentDateFormat member has been defined, try to parse the 175 * supplied string with that. If that parse fails, or if the recentDateFormat 176 * member has not been defined, attempt to parse with the defaultDateFormat 177 * member. If that fails, throw a ParseException. 178 * 179 * This method allows a {@link Calendar} instance to be passed in which represents the 180 * current (system) time. 181 * 182 * @see FTPTimestampParser#parseTimestamp(String) 183 * @param timestampStr The timestamp to be parsed 184 * @param serverTime The current time for the server 185 * @return the calendar 186 * @throws ParseException if timestamp cannot be parsed 187 * @since 1.5 188 */ 189 public Calendar parseTimestamp(String timestampStr, Calendar serverTime) throws ParseException { 190 Calendar working = (Calendar) serverTime.clone(); 191 working.setTimeZone(getServerTimeZone()); // is this needed? 192 193 Date parsed = null; 194 195 if (recentDateFormat != null) { 196 Calendar now = (Calendar) serverTime.clone();// Copy this, because we may change it 197 now.setTimeZone(this.getServerTimeZone()); 198 if (lenientFutureDates) { 199 // add a day to "now" so that "slop" doesn't cause a date 200 // slightly in the future to roll back a full year. (Bug 35181 => NET-83) 201 now.add(Calendar.DAY_OF_MONTH, 1); 202 } 203 // The Java SimpleDateFormat class uses the epoch year 1970 if not present in the input 204 // As 1970 was not a leap year, it cannot parse "Feb 29" correctly. 205 // Java 1.5+ returns Mar 1 1970 206 // Temporarily add the current year to the short date time 207 // to cope with short-date leap year strings. 208 // Since Feb 29 is more that 6 months from the end of the year, this should be OK for 209 // all instances of short dates which are +- 6 months from current date. 210 // TODO this won't always work for systems that use short dates +0/-12months 211 // e.g. if today is Jan 1 2001 and the short date is Feb 29 212 String year = Integer.toString(now.get(Calendar.YEAR)); 213 String timeStampStrPlusYear = timestampStr + " " + year; 214 SimpleDateFormat hackFormatter = new SimpleDateFormat(recentDateFormat.toPattern() + " yyyy", 215 recentDateFormat.getDateFormatSymbols()); 216 hackFormatter.setLenient(false); 217 hackFormatter.setTimeZone(recentDateFormat.getTimeZone()); 218 ParsePosition pp = new ParsePosition(0); 219 parsed = hackFormatter.parse(timeStampStrPlusYear, pp); 220 // Check if we parsed the full string, if so it must have been a short date originally 221 if (parsed != null && pp.getIndex() == timeStampStrPlusYear.length()) { 222 working.setTime(parsed); 223 if (working.after(now)) { // must have been last year instead 224 working.add(Calendar.YEAR, -1); 225 } 226 setPrecision(recentDateSmallestUnitIndex, working); 227 return working; 228 } 229 } 230 231 ParsePosition pp = new ParsePosition(0); 232 parsed = defaultDateFormat.parse(timestampStr, pp); 233 // note, length checks are mandatory for us since 234 // SimpleDateFormat methods will succeed if less than 235 // full string is matched. They will also accept, 236 // despite "leniency" setting, a two-digit number as 237 // a valid year (e.g. 22:04 will parse as 22 A.D.) 238 // so could mistakenly confuse an hour with a year, 239 // if we don't insist on full length parsing. 240 if (parsed != null && pp.getIndex() == timestampStr.length()) { 241 working.setTime(parsed); 242 } else { 243 throw new ParseException( 244 "Timestamp '"+timestampStr+"' could not be parsed using a server time of " 245 +serverTime.getTime().toString(), 246 pp.getErrorIndex()); 247 } 248 setPrecision(defaultDateSmallestUnitIndex, working); 249 return working; 250 } 251 252 /** 253 * @return Returns the defaultDateFormat. 254 */ 255 public SimpleDateFormat getDefaultDateFormat() { 256 return defaultDateFormat; 257 } 258 /** 259 * @return Returns the defaultDateFormat pattern string. 260 */ 261 public String getDefaultDateFormatString() { 262 return defaultDateFormat.toPattern(); 263 } 264 /** 265 * @param format The defaultDateFormat to be set. 266 * @param dfs the symbols to use (may be null) 267 */ 268 private void setDefaultDateFormat(String format, DateFormatSymbols dfs) { 269 if (format != null) { 270 if (dfs != null) { 271 this.defaultDateFormat = new SimpleDateFormat(format, dfs); 272 } else { 273 this.defaultDateFormat = new SimpleDateFormat(format); 274 } 275 this.defaultDateFormat.setLenient(false); 276 } else { 277 this.defaultDateFormat = null; 278 } 279 this.defaultDateSmallestUnitIndex = getEntry(this.defaultDateFormat); 280 } 281 /** 282 * @return Returns the recentDateFormat. 283 */ 284 public SimpleDateFormat getRecentDateFormat() { 285 return recentDateFormat; 286 } 287 /** 288 * @return Returns the recentDateFormat. 289 */ 290 public String getRecentDateFormatString() { 291 return recentDateFormat.toPattern(); 292 } 293 /** 294 * @param format The recentDateFormat to set. 295 * @param dfs the symbols to use (may be null) 296 */ 297 private void setRecentDateFormat(String format, DateFormatSymbols dfs) { 298 if (format != null) { 299 if (dfs != null) { 300 this.recentDateFormat = new SimpleDateFormat(format, dfs); 301 } else { 302 this.recentDateFormat = new SimpleDateFormat(format); 303 } 304 this.recentDateFormat.setLenient(false); 305 } else { 306 this.recentDateFormat = null; 307 } 308 this.recentDateSmallestUnitIndex = getEntry(this.recentDateFormat); 309 } 310 311 /** 312 * @return returns an array of 12 strings representing the short 313 * month names used by this parse. 314 */ 315 public String[] getShortMonths() { 316 return defaultDateFormat.getDateFormatSymbols().getShortMonths(); 317 } 318 319 320 /** 321 * @return Returns the serverTimeZone used by this parser. 322 */ 323 public TimeZone getServerTimeZone() { 324 return this.defaultDateFormat.getTimeZone(); 325 } 326 /** 327 * sets a TimeZone represented by the supplied ID string into all 328 * of the parsers used by this server. 329 * @param serverTimeZone Time Id java.util.TimeZone id used by 330 * the ftp server. If null the client's local time zone is assumed. 331 */ 332 private void setServerTimeZone(String serverTimeZoneId) { 333 TimeZone serverTimeZone = TimeZone.getDefault(); 334 if (serverTimeZoneId != null) { 335 serverTimeZone = TimeZone.getTimeZone(serverTimeZoneId); 336 } 337 this.defaultDateFormat.setTimeZone(serverTimeZone); 338 if (this.recentDateFormat != null) { 339 this.recentDateFormat.setTimeZone(serverTimeZone); 340 } 341 } 342 343 /** 344 * Implementation of the {@link Configurable Configurable} 345 * interface. Configures this <code>FTPTimestampParser</code> according 346 * to the following logic: 347 * <p> 348 * Set up the {@link FTPClientConfig#setDefaultDateFormatStr(java.lang.String) defaultDateFormat} 349 * and optionally the {@link FTPClientConfig#setRecentDateFormatStr(String) recentDateFormat} 350 * to values supplied in the config based on month names configured as follows: 351 * </p> 352 * <ul> 353 * <li>If a {@link FTPClientConfig#setShortMonthNames(String) shortMonthString} 354 * has been supplied in the <code>config</code>, use that to parse parse timestamps.</li> 355 * <li>Otherwise, if a {@link FTPClientConfig#setServerLanguageCode(String) serverLanguageCode} 356 * has been supplied in the <code>config</code>, use the month names represented 357 * by that {@link FTPClientConfig#lookupDateFormatSymbols(String) language} 358 * to parse timestamps.</li> 359 * <li>otherwise use default English month names</li> 360 * </ul><p> 361 * Finally if a {@link org.apache.commons.net.ftp.FTPClientConfig#setServerTimeZoneId(String) serverTimeZoneId} 362 * has been supplied via the config, set that into all date formats that have 363 * been configured. 364 * </p> 365 */ 366 @Override 367 public void configure(FTPClientConfig config) { 368 DateFormatSymbols dfs = null; 369 370 String languageCode = config.getServerLanguageCode(); 371 String shortmonths = config.getShortMonthNames(); 372 if (shortmonths != null) { 373 dfs = FTPClientConfig.getDateFormatSymbols(shortmonths); 374 } else if (languageCode != null) { 375 dfs = FTPClientConfig.lookupDateFormatSymbols(languageCode); 376 } else { 377 dfs = FTPClientConfig.lookupDateFormatSymbols("en"); 378 } 379 380 381 String recentFormatString = config.getRecentDateFormatStr(); 382 setRecentDateFormat(recentFormatString, dfs); 383 384 String defaultFormatString = config.getDefaultDateFormatStr(); 385 if (defaultFormatString == null) { 386 throw new IllegalArgumentException("defaultFormatString cannot be null"); 387 } 388 setDefaultDateFormat(defaultFormatString, dfs); 389 390 setServerTimeZone(config.getServerTimeZoneId()); 391 392 this.lenientFutureDates = config.isLenientFutureDates(); 393 } 394 /** 395 * @return Returns the lenientFutureDates. 396 */ 397 boolean isLenientFutureDates() { 398 return lenientFutureDates; 399 } 400 /** 401 * @param lenientFutureDates The lenientFutureDates to set. 402 */ 403 void setLenientFutureDates(boolean lenientFutureDates) { 404 this.lenientFutureDates = lenientFutureDates; 405 } 406}