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 * Implementation of the {@link Configurable Configurable} interface. Configures this <code>FTPTimestampParser</code> according to the following logic: 135 * <p> 136 * Set 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 final Calendar now = Calendar.getInstance(); 245 return parseTimestamp(timestampStr, now); 246 } 247 248 /** 249 * 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 250 * not been defined, attempt to parse with the defaultDateFormat member. If that fails, throw a ParseException. 251 * 252 * This method allows a {@link Calendar} instance to be passed in which represents the current (system) time. 253 * 254 * @see FTPTimestampParser#parseTimestamp(String) 255 * @param timestampStr The timestamp to be parsed 256 * @param serverTime The current time for the server 257 * @return the calendar 258 * @throws ParseException if timestamp cannot be parsed 259 * @since 1.5 260 */ 261 public Calendar parseTimestamp(final String timestampStr, final Calendar serverTime) throws ParseException { 262 final Calendar working = (Calendar) serverTime.clone(); 263 working.setTimeZone(getServerTimeZone()); // is this needed? 264 265 Date parsed; 266 267 if (recentDateFormat != null) { 268 final Calendar now = (Calendar) serverTime.clone();// Copy this, because we may change it 269 now.setTimeZone(this.getServerTimeZone()); 270 if (lenientFutureDates) { 271 // add a day to "now" so that "slop" doesn't cause a date 272 // slightly in the future to roll back a full year. (Bug 35181 => NET-83) 273 now.add(Calendar.DAY_OF_MONTH, 1); 274 } 275 // The Java SimpleDateFormat class uses the epoch year 1970 if not present in the input 276 // As 1970 was not a leap year, it cannot parse "Feb 29" correctly. 277 // Java 1.5+ returns Mar 1 1970 278 // Temporarily add the current year to the short date time 279 // to cope with short-date leap year strings. 280 // Since Feb 29 is more that 6 months from the end of the year, this should be OK for 281 // all instances of short dates which are +- 6 months from current date. 282 // TODO this won't always work for systems that use short dates +0/-12months 283 // e.g. if today is Jan 1 2001 and the short date is Feb 29 284 final String year = Integer.toString(now.get(Calendar.YEAR)); 285 final String timeStampStrPlusYear = timestampStr + " " + year; 286 final SimpleDateFormat hackFormatter = new SimpleDateFormat(recentDateFormat.toPattern() + " yyyy", recentDateFormat.getDateFormatSymbols()); 287 hackFormatter.setLenient(false); 288 hackFormatter.setTimeZone(recentDateFormat.getTimeZone()); 289 final ParsePosition pp = new ParsePosition(0); 290 parsed = hackFormatter.parse(timeStampStrPlusYear, pp); 291 // Check if we parsed the full string, if so it must have been a short date originally 292 if (parsed != null && pp.getIndex() == timeStampStrPlusYear.length()) { 293 working.setTime(parsed); 294 if (working.after(now)) { // must have been last year instead 295 working.add(Calendar.YEAR, -1); 296 } 297 setPrecision(recentDateSmallestUnitIndex, working); 298 return working; 299 } 300 } 301 302 final ParsePosition pp = new ParsePosition(0); 303 parsed = defaultDateFormat.parse(timestampStr, pp); 304 // note, length checks are mandatory for us since 305 // SimpleDateFormat methods will succeed if less than 306 // full string is matched. They will also accept, 307 // despite "leniency" setting, a two-digit number as 308 // a valid year (e.g. 22:04 will parse as 22 A.D.) 309 // so could mistakenly confuse an hour with a year, 310 // if we don't insist on full length parsing. 311 if (parsed == null || pp.getIndex() != timestampStr.length()) { 312 throw new ParseException("Timestamp '" + timestampStr + "' could not be parsed using a server time of " + serverTime.getTime().toString(), 313 pp.getErrorIndex()); 314 } 315 working.setTime(parsed); 316 setPrecision(defaultDateSmallestUnitIndex, working); 317 return working; 318 } 319 320 /** 321 * @param format The defaultDateFormat to be set. 322 * @param dfs the symbols to use (may be null) 323 */ 324 private void setDefaultDateFormat(final String format, final DateFormatSymbols dfs) { 325 if (format != null) { 326 if (dfs != null) { 327 this.defaultDateFormat = new SimpleDateFormat(format, dfs); 328 } else { 329 this.defaultDateFormat = new SimpleDateFormat(format); 330 } 331 this.defaultDateFormat.setLenient(false); 332 } else { 333 this.defaultDateFormat = null; 334 } 335 this.defaultDateSmallestUnitIndex = getEntry(this.defaultDateFormat); 336 } 337 338 /** 339 * @param lenientFutureDates The lenientFutureDates to set. 340 */ 341 void setLenientFutureDates(final boolean lenientFutureDates) { 342 this.lenientFutureDates = lenientFutureDates; 343 } 344 345 /** 346 * @param format The recentDateFormat to set. 347 * @param dfs the symbols to use (may be null) 348 */ 349 private void setRecentDateFormat(final String format, final DateFormatSymbols dfs) { 350 if (format != null) { 351 if (dfs != null) { 352 this.recentDateFormat = new SimpleDateFormat(format, dfs); 353 } else { 354 this.recentDateFormat = new SimpleDateFormat(format); 355 } 356 this.recentDateFormat.setLenient(false); 357 } else { 358 this.recentDateFormat = null; 359 } 360 this.recentDateSmallestUnitIndex = getEntry(this.recentDateFormat); 361 } 362 363 /** 364 * sets a TimeZone represented by the supplied ID string into all the parsers used by this server. 365 * 366 * @param serverTimeZoneId Time Id java.util.TimeZone id used by the ftp server. If null the client's local time zone is assumed. 367 */ 368 private void setServerTimeZone(final String serverTimeZoneId) { 369 TimeZone serverTimeZone = TimeZone.getDefault(); 370 if (serverTimeZoneId != null) { 371 serverTimeZone = TimeZone.getTimeZone(serverTimeZoneId); 372 } 373 this.defaultDateFormat.setTimeZone(serverTimeZone); 374 if (this.recentDateFormat != null) { 375 this.recentDateFormat.setTimeZone(serverTimeZone); 376 } 377 } 378}