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