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