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 018 package org.apache.commons.net.ftp.parser; 019 020 import java.text.DateFormatSymbols; 021 import java.text.ParseException; 022 import java.text.ParsePosition; 023 import java.text.SimpleDateFormat; 024 import java.util.Calendar; 025 import java.util.Date; 026 import java.util.TimeZone; 027 028 import org.apache.commons.net.ftp.Configurable; 029 import 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 */ 039 public class FTPTimestampParserImpl implements 040 FTPTimestampParser, Configurable 041 { 042 043 044 private SimpleDateFormat defaultDateFormat; 045 private SimpleDateFormat recentDateFormat; 046 private boolean lenientFutureDates = false; 047 048 049 /** 050 * The only constructor for this class. 051 */ 052 public FTPTimestampParserImpl() { 053 setDefaultDateFormat(DEFAULT_SDF); 054 setRecentDateFormat(DEFAULT_RECENT_SDF); 055 } 056 057 /** 058 * Implements the one {@link FTPTimestampParser#parseTimestamp(String) method} 059 * in the {@link FTPTimestampParser FTPTimestampParser} interface 060 * according to this algorithm: 061 * 062 * If the recentDateFormat member has been defined, try to parse the 063 * supplied string with that. If that parse fails, or if the recentDateFormat 064 * member has not been defined, attempt to parse with the defaultDateFormat 065 * member. If that fails, throw a ParseException. 066 * 067 * This method assumes that the server time is the same as the local time. 068 * 069 * @see FTPTimestampParserImpl#parseTimestamp(String, Calendar) 070 * 071 * @param timestampStr The timestamp to be parsed 072 */ 073 public Calendar parseTimestamp(String timestampStr) throws ParseException { 074 Calendar now = Calendar.getInstance(); 075 return parseTimestamp(timestampStr, now); 076 } 077 078 /** 079 * If the recentDateFormat member has been defined, try to parse the 080 * supplied string with that. If that parse fails, or if the recentDateFormat 081 * member has not been defined, attempt to parse with the defaultDateFormat 082 * member. If that fails, throw a ParseException. 083 * 084 * This method allows a {@link Calendar} instance to be passed in which represents the 085 * current (system) time. 086 * 087 * @see FTPTimestampParser#parseTimestamp(String) 088 * @param timestampStr The timestamp to be parsed 089 * @param serverTime The current time for the server 090 * @since 1.5 091 */ 092 public Calendar parseTimestamp(String timestampStr, Calendar serverTime) throws ParseException { 093 Calendar working = (Calendar) serverTime.clone(); 094 working.setTimeZone(getServerTimeZone()); // is this needed? 095 096 Date parsed = null; 097 098 if (recentDateFormat != null) { 099 Calendar now = (Calendar) serverTime.clone();// Copy this, because we may change it 100 now.setTimeZone(this.getServerTimeZone()); 101 if (lenientFutureDates) { 102 // add a day to "now" so that "slop" doesn't cause a date 103 // slightly in the future to roll back a full year. (Bug 35181 => NET-83) 104 now.add(Calendar.DATE, 1); 105 } 106 // The Java SimpleDateFormat class uses the epoch year 1970 if not present in the input 107 // As 1970 was not a leap year, it cannot parse "Feb 29" correctly. 108 // Java 1.5+ returns Mar 1 1970 109 // Temporarily add the current year to the short date time 110 // to cope with short-date leap year strings. 111 // Since Feb 29 is more that 6 months from the end of the year, this should be OK for 112 // all instances of short dates which are +- 6 months from current date. 113 // TODO this won't always work for systems that use short dates +0/-12months 114 // e.g. if today is Jan 1 2001 and the short date is Feb 29 115 String year = Integer.toString(now.get(Calendar.YEAR)); 116 String timeStampStrPlusYear = timestampStr + " " + year; 117 SimpleDateFormat hackFormatter = new SimpleDateFormat(recentDateFormat.toPattern() + " yyyy", 118 recentDateFormat.getDateFormatSymbols()); 119 hackFormatter.setLenient(false); 120 hackFormatter.setTimeZone(recentDateFormat.getTimeZone()); 121 ParsePosition pp = new ParsePosition(0); 122 parsed = hackFormatter.parse(timeStampStrPlusYear, pp); 123 // Check if we parsed the full string, if so it must have been a short date originally 124 if (parsed != null && pp.getIndex() == timeStampStrPlusYear.length()) { 125 working.setTime(parsed); 126 if (working.after(now)) { // must have been last year instead 127 working.add(Calendar.YEAR, -1); 128 } 129 return working; 130 } 131 } 132 133 ParsePosition pp = new ParsePosition(0); 134 parsed = defaultDateFormat.parse(timestampStr, pp); 135 // note, length checks are mandatory for us since 136 // SimpleDateFormat methods will succeed if less than 137 // full string is matched. They will also accept, 138 // despite "leniency" setting, a two-digit number as 139 // a valid year (e.g. 22:04 will parse as 22 A.D.) 140 // so could mistakenly confuse an hour with a year, 141 // if we don't insist on full length parsing. 142 if (parsed != null && pp.getIndex() == timestampStr.length()) { 143 working.setTime(parsed); 144 } else { 145 throw new ParseException( 146 "Timestamp '"+timestampStr+"' could not be parsed using a server time of " 147 +serverTime.getTime().toString(), 148 pp.getErrorIndex()); 149 } 150 return working; 151 } 152 153 /** 154 * @return Returns the defaultDateFormat. 155 */ 156 public SimpleDateFormat getDefaultDateFormat() { 157 return defaultDateFormat; 158 } 159 /** 160 * @return Returns the defaultDateFormat pattern string. 161 */ 162 public String getDefaultDateFormatString() { 163 return defaultDateFormat.toPattern(); 164 } 165 /** 166 * @param defaultDateFormat The defaultDateFormat to be set. 167 */ 168 private void setDefaultDateFormat(String format) { 169 if (format != null) { 170 this.defaultDateFormat = new SimpleDateFormat(format); 171 this.defaultDateFormat.setLenient(false); 172 } 173 } 174 /** 175 * @return Returns the recentDateFormat. 176 */ 177 public SimpleDateFormat getRecentDateFormat() { 178 return recentDateFormat; 179 } 180 /** 181 * @return Returns the recentDateFormat. 182 */ 183 public String getRecentDateFormatString() { 184 return recentDateFormat.toPattern(); 185 } 186 /** 187 * @param recentDateFormat The recentDateFormat to set. 188 */ 189 private void setRecentDateFormat(String format) { 190 if (format != null) { 191 this.recentDateFormat = new SimpleDateFormat(format); 192 this.recentDateFormat.setLenient(false); 193 } 194 } 195 196 /** 197 * @return returns an array of 12 strings representing the short 198 * month names used by this parse. 199 */ 200 public String[] getShortMonths() { 201 return defaultDateFormat.getDateFormatSymbols().getShortMonths(); 202 } 203 204 205 /** 206 * @return Returns the serverTimeZone used by this parser. 207 */ 208 public TimeZone getServerTimeZone() { 209 return this.defaultDateFormat.getTimeZone(); 210 } 211 /** 212 * sets a TimeZone represented by the supplied ID string into all 213 * of the parsers used by this server. 214 * @param serverTimeZone Time Id java.util.TimeZone id used by 215 * the ftp server. If null the client's local time zone is assumed. 216 */ 217 private void setServerTimeZone(String serverTimeZoneId) { 218 TimeZone serverTimeZone = TimeZone.getDefault(); 219 if (serverTimeZoneId != null) { 220 serverTimeZone = TimeZone.getTimeZone(serverTimeZoneId); 221 } 222 this.defaultDateFormat.setTimeZone(serverTimeZone); 223 if (this.recentDateFormat != null) { 224 this.recentDateFormat.setTimeZone(serverTimeZone); 225 } 226 } 227 228 /** 229 * Implementation of the {@link Configurable Configurable} 230 * interface. Configures this <code>FTPTimestampParser</code> according 231 * to the following logic: 232 * <p> 233 * Set up the {@link FTPClientConfig#setDefaultDateFormatStr(java.lang.String) defaultDateFormat} 234 * and optionally the {@link FTPClientConfig#setRecentDateFormatStr(String) recentDateFormat} 235 * to values supplied in the config based on month names configured as follows: 236 * </p><p><ul> 237 * <li>If a {@link FTPClientConfig#setShortMonthNames(String) shortMonthString} 238 * has been supplied in the <code>config</code>, use that to parse parse timestamps.</li> 239 * <li>Otherwise, if a {@link FTPClientConfig#setServerLanguageCode(String) serverLanguageCode} 240 * has been supplied in the <code>config</code>, use the month names represented 241 * by that {@link FTPClientConfig#lookupDateFormatSymbols(String) language} 242 * to parse timestamps.</li> 243 * <li>otherwise use default English month names</li> 244 * </ul></p><p> 245 * Finally if a {@link org.apache.commons.net.ftp.FTPClientConfig#setServerTimeZoneId(String) serverTimeZoneId} 246 * has been supplied via the config, set that into all date formats that have 247 * been configured. 248 * </p> 249 */ 250 public void configure(FTPClientConfig config) { 251 DateFormatSymbols dfs = null; 252 253 String languageCode = config.getServerLanguageCode(); 254 String shortmonths = config.getShortMonthNames(); 255 if (shortmonths != null) { 256 dfs = FTPClientConfig.getDateFormatSymbols(shortmonths); 257 } else if (languageCode != null) { 258 dfs = FTPClientConfig.lookupDateFormatSymbols(languageCode); 259 } else { 260 dfs = FTPClientConfig.lookupDateFormatSymbols("en"); 261 } 262 263 264 String recentFormatString = config.getRecentDateFormatStr(); 265 if (recentFormatString == null) { 266 this.recentDateFormat = null; 267 } else { 268 this.recentDateFormat = new SimpleDateFormat(recentFormatString, dfs); 269 this.recentDateFormat.setLenient(false); 270 } 271 272 String defaultFormatString = config.getDefaultDateFormatStr(); 273 if (defaultFormatString == null) { 274 throw new IllegalArgumentException("defaultFormatString cannot be null"); 275 } 276 this.defaultDateFormat = new SimpleDateFormat(defaultFormatString, dfs); 277 this.defaultDateFormat.setLenient(false); 278 279 setServerTimeZone(config.getServerTimeZoneId()); 280 281 this.lenientFutureDates = config.isLenientFutureDates(); 282 } 283 /** 284 * @return Returns the lenientFutureDates. 285 */ 286 boolean isLenientFutureDates() { 287 return lenientFutureDates; 288 } 289 /** 290 * @param lenientFutureDates The lenientFutureDates to set. 291 */ 292 void setLenientFutureDates(boolean lenientFutureDates) { 293 this.lenientFutureDates = lenientFutureDates; 294 } 295 }