1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one or more
3 * contributor license agreements. See the NOTICE file distributed with
4 * this work for additional information regarding copyright ownership.
5 * The ASF licenses this file to You under the Apache License, Version 2.0
6 * (the "License"); you may not use this file except in compliance with
7 * the License. You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18 package org.apache.commons.net.ftp.parser;
19
20 import java.text.DateFormatSymbols;
21 import java.text.ParseException;
22 import java.text.ParsePosition;
23 import java.text.SimpleDateFormat;
24 import java.util.Calendar;
25 import java.util.Date;
26 import java.util.TimeZone;
27
28 import org.apache.commons.net.ftp.Configurable;
29 import org.apache.commons.net.ftp.FTPClientConfig;
30
31 /**
32 * Default implementation of the {@link FTPTimestampParser FTPTimestampParser}
33 * interface also implements the {@link org.apache.commons.net.ftp.Configurable Configurable}
34 * interface to allow the parsing to be configured from the outside.
35 *
36 * @see ConfigurableFTPFileEntryParserImpl
37 * @since 1.4
38 */
39 public class FTPTimestampParserImpl implements
40 FTPTimestampParser, Configurable
41 {
42
43
44 private SimpleDateFormat defaultDateFormat;
45 private SimpleDateFormat recentDateFormat;
46 private boolean lenientFutureDates = false;
47
48
49 /**
50 * The only constructor for this class.
51 */
52 public FTPTimestampParserImpl() {
53 setDefaultDateFormat(DEFAULT_SDF);
54 setRecentDateFormat(DEFAULT_RECENT_SDF);
55 }
56
57 /**
58 * Implements the one {@link FTPTimestampParser#parseTimestamp(String) method}
59 * in the {@link FTPTimestampParser FTPTimestampParser} interface
60 * according to this algorithm:
61 *
62 * If the recentDateFormat member has been defined, try to parse the
63 * supplied string with that. If that parse fails, or if the recentDateFormat
64 * member has not been defined, attempt to parse with the defaultDateFormat
65 * member. If that fails, throw a ParseException.
66 *
67 * This method assumes that the server time is the same as the local time.
68 *
69 * @see FTPTimestampParserImpl#parseTimestamp(String, Calendar)
70 *
71 * @param timestampStr The timestamp to be parsed
72 */
73 @Override
74 public Calendar parseTimestamp(String timestampStr) throws ParseException {
75 Calendar now = Calendar.getInstance();
76 return parseTimestamp(timestampStr, now);
77 }
78
79 /**
80 * If the recentDateFormat member has been defined, try to parse the
81 * supplied string with that. If that parse fails, or if the recentDateFormat
82 * member has not been defined, attempt to parse with the defaultDateFormat
83 * member. If that fails, throw a ParseException.
84 *
85 * This method allows a {@link Calendar} instance to be passed in which represents the
86 * current (system) time.
87 *
88 * @see FTPTimestampParser#parseTimestamp(String)
89 * @param timestampStr The timestamp to be parsed
90 * @param serverTime The current time for the server
91 * @since 1.5
92 */
93 public Calendar parseTimestamp(String timestampStr, Calendar serverTime) throws ParseException {
94 Calendar working = (Calendar) serverTime.clone();
95 working.setTimeZone(getServerTimeZone()); // is this needed?
96
97 Date parsed = null;
98
99 if (recentDateFormat != null) {
100 Calendar now = (Calendar) serverTime.clone();// Copy this, because we may change it
101 now.setTimeZone(this.getServerTimeZone());
102 if (lenientFutureDates) {
103 // add a day to "now" so that "slop" doesn't cause a date
104 // slightly in the future to roll back a full year. (Bug 35181 => NET-83)
105 now.add(Calendar.DATE, 1);
106 }
107 // The Java SimpleDateFormat class uses the epoch year 1970 if not present in the input
108 // As 1970 was not a leap year, it cannot parse "Feb 29" correctly.
109 // Java 1.5+ returns Mar 1 1970
110 // Temporarily add the current year to the short date time
111 // to cope with short-date leap year strings.
112 // Since Feb 29 is more that 6 months from the end of the year, this should be OK for
113 // all instances of short dates which are +- 6 months from current date.
114 // TODO this won't always work for systems that use short dates +0/-12months
115 // e.g. if today is Jan 1 2001 and the short date is Feb 29
116 String year = Integer.toString(now.get(Calendar.YEAR));
117 String timeStampStrPlusYear = timestampStr + " " + year;
118 SimpleDateFormat hackFormatter = new SimpleDateFormat(recentDateFormat.toPattern() + " yyyy",
119 recentDateFormat.getDateFormatSymbols());
120 hackFormatter.setLenient(false);
121 hackFormatter.setTimeZone(recentDateFormat.getTimeZone());
122 ParsePosition pp = new ParsePosition(0);
123 parsed = hackFormatter.parse(timeStampStrPlusYear, pp);
124 // Check if we parsed the full string, if so it must have been a short date originally
125 if (parsed != null && pp.getIndex() == timeStampStrPlusYear.length()) {
126 working.setTime(parsed);
127 if (working.after(now)) { // must have been last year instead
128 working.add(Calendar.YEAR, -1);
129 }
130 return working;
131 }
132 }
133
134 ParsePosition pp = new ParsePosition(0);
135 parsed = defaultDateFormat.parse(timestampStr, pp);
136 // note, length checks are mandatory for us since
137 // SimpleDateFormat methods will succeed if less than
138 // full string is matched. They will also accept,
139 // despite "leniency" setting, a two-digit number as
140 // a valid year (e.g. 22:04 will parse as 22 A.D.)
141 // so could mistakenly confuse an hour with a year,
142 // if we don't insist on full length parsing.
143 if (parsed != null && pp.getIndex() == timestampStr.length()) {
144 working.setTime(parsed);
145 } else {
146 throw new ParseException(
147 "Timestamp '"+timestampStr+"' could not be parsed using a server time of "
148 +serverTime.getTime().toString(),
149 pp.getErrorIndex());
150 }
151 return working;
152 }
153
154 /**
155 * @return Returns the defaultDateFormat.
156 */
157 public SimpleDateFormat getDefaultDateFormat() {
158 return defaultDateFormat;
159 }
160 /**
161 * @return Returns the defaultDateFormat pattern string.
162 */
163 public String getDefaultDateFormatString() {
164 return defaultDateFormat.toPattern();
165 }
166 /**
167 * @param defaultDateFormat The defaultDateFormat to be set.
168 */
169 private void setDefaultDateFormat(String format) {
170 if (format != null) {
171 this.defaultDateFormat = new SimpleDateFormat(format);
172 this.defaultDateFormat.setLenient(false);
173 }
174 }
175 /**
176 * @return Returns the recentDateFormat.
177 */
178 public SimpleDateFormat getRecentDateFormat() {
179 return recentDateFormat;
180 }
181 /**
182 * @return Returns the recentDateFormat.
183 */
184 public String getRecentDateFormatString() {
185 return recentDateFormat.toPattern();
186 }
187 /**
188 * @param recentDateFormat The recentDateFormat to set.
189 */
190 private void setRecentDateFormat(String format) {
191 if (format != null) {
192 this.recentDateFormat = new SimpleDateFormat(format);
193 this.recentDateFormat.setLenient(false);
194 }
195 }
196
197 /**
198 * @return returns an array of 12 strings representing the short
199 * month names used by this parse.
200 */
201 public String[] getShortMonths() {
202 return defaultDateFormat.getDateFormatSymbols().getShortMonths();
203 }
204
205
206 /**
207 * @return Returns the serverTimeZone used by this parser.
208 */
209 public TimeZone getServerTimeZone() {
210 return this.defaultDateFormat.getTimeZone();
211 }
212 /**
213 * sets a TimeZone represented by the supplied ID string into all
214 * of the parsers used by this server.
215 * @param serverTimeZone Time Id java.util.TimeZone id used by
216 * the ftp server. If null the client's local time zone is assumed.
217 */
218 private void setServerTimeZone(String serverTimeZoneId) {
219 TimeZone serverTimeZone = TimeZone.getDefault();
220 if (serverTimeZoneId != null) {
221 serverTimeZone = TimeZone.getTimeZone(serverTimeZoneId);
222 }
223 this.defaultDateFormat.setTimeZone(serverTimeZone);
224 if (this.recentDateFormat != null) {
225 this.recentDateFormat.setTimeZone(serverTimeZone);
226 }
227 }
228
229 /**
230 * Implementation of the {@link Configurable Configurable}
231 * interface. Configures this <code>FTPTimestampParser</code> according
232 * to the following logic:
233 * <p>
234 * Set up the {@link FTPClientConfig#setDefaultDateFormatStr(java.lang.String) defaultDateFormat}
235 * and optionally the {@link FTPClientConfig#setRecentDateFormatStr(String) recentDateFormat}
236 * to values supplied in the config based on month names configured as follows:
237 * </p><p><ul>
238 * <li>If a {@link FTPClientConfig#setShortMonthNames(String) shortMonthString}
239 * has been supplied in the <code>config</code>, use that to parse parse timestamps.</li>
240 * <li>Otherwise, if a {@link FTPClientConfig#setServerLanguageCode(String) serverLanguageCode}
241 * has been supplied in the <code>config</code>, use the month names represented
242 * by that {@link FTPClientConfig#lookupDateFormatSymbols(String) language}
243 * to parse timestamps.</li>
244 * <li>otherwise use default English month names</li>
245 * </ul></p><p>
246 * Finally if a {@link org.apache.commons.net.ftp.FTPClientConfig#setServerTimeZoneId(String) serverTimeZoneId}
247 * has been supplied via the config, set that into all date formats that have
248 * been configured.
249 * </p>
250 */
251 @Override
252 public void configure(FTPClientConfig config) {
253 DateFormatSymbols dfs = null;
254
255 String languageCode = config.getServerLanguageCode();
256 String shortmonths = config.getShortMonthNames();
257 if (shortmonths != null) {
258 dfs = FTPClientConfig.getDateFormatSymbols(shortmonths);
259 } else if (languageCode != null) {
260 dfs = FTPClientConfig.lookupDateFormatSymbols(languageCode);
261 } else {
262 dfs = FTPClientConfig.lookupDateFormatSymbols("en");
263 }
264
265
266 String recentFormatString = config.getRecentDateFormatStr();
267 if (recentFormatString == null) {
268 this.recentDateFormat = null;
269 } else {
270 this.recentDateFormat = new SimpleDateFormat(recentFormatString, dfs);
271 this.recentDateFormat.setLenient(false);
272 }
273
274 String defaultFormatString = config.getDefaultDateFormatStr();
275 if (defaultFormatString == null) {
276 throw new IllegalArgumentException("defaultFormatString cannot be null");
277 }
278 this.defaultDateFormat = new SimpleDateFormat(defaultFormatString, dfs);
279 this.defaultDateFormat.setLenient(false);
280
281 setServerTimeZone(config.getServerTimeZoneId());
282
283 this.lenientFutureDates = config.isLenientFutureDates();
284 }
285 /**
286 * @return Returns the lenientFutureDates.
287 */
288 boolean isLenientFutureDates() {
289 return lenientFutureDates;
290 }
291 /**
292 * @param lenientFutureDates The lenientFutureDates to set.
293 */
294 void setLenientFutureDates(boolean lenientFutureDates) {
295 this.lenientFutureDates = lenientFutureDates;
296 }
297 }