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 }