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    *      https://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} interface also implements the {@link org.apache.commons.net.ftp.Configurable
33   * Configurable} interface to allow the parsing to be configured from the outside.
34   *
35   * @see ConfigurableFTPFileEntryParserImpl
36   * @since 1.4
37   */
38  public class FTPTimestampParserImpl implements FTPTimestampParser, Configurable {
39  
40      /*
41       * 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
42       * the parse string. The date formats are analyzed to find the least significant unit (e.g. Minutes or Milliseconds) and the appropriate index to the array
43       * 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
44       * the array until the previous entry. e.g. for MINUTE it would clear MILLISECOND and SECOND
45       */
46      private static final int[] CALENDAR_UNITS = { Calendar.MILLISECOND, Calendar.SECOND, Calendar.MINUTE, Calendar.HOUR_OF_DAY, Calendar.DAY_OF_MONTH,
47              Calendar.MONTH, Calendar.YEAR };
48  
49      /*
50       * Return the index to the array representing the least significant unit found in the date format. Default is 0 (to avoid dropping precision)
51       */
52      private static int getEntry(final SimpleDateFormat dateFormat) {
53          if (dateFormat == null) {
54              return 0;
55          }
56          final String FORMAT_CHARS = "SsmHdM";
57          final String pattern = dateFormat.toPattern();
58          for (final char ch : FORMAT_CHARS.toCharArray()) {
59              if (pattern.indexOf(ch) != -1) { // found the character
60                  switch (ch) {
61                  case 'S':
62                      return indexOf(Calendar.MILLISECOND);
63                  case 's':
64                      return indexOf(Calendar.SECOND);
65                  case 'm':
66                      return indexOf(Calendar.MINUTE);
67                  case 'H':
68                      return indexOf(Calendar.HOUR_OF_DAY);
69                  case 'd':
70                      return indexOf(Calendar.DAY_OF_MONTH);
71                  case 'M':
72                      return indexOf(Calendar.MONTH);
73                  }
74              }
75          }
76          return 0;
77      }
78  
79      /*
80       * Find the entry in the CALENDAR_UNITS array.
81       */
82      private static int indexOf(final int calendarUnit) {
83          int i;
84          for (i = 0; i < CALENDAR_UNITS.length; i++) {
85              if (calendarUnit == CALENDAR_UNITS[i]) {
86                  return i;
87              }
88          }
89          return 0;
90      }
91  
92      /*
93       * Sets the Calendar precision (used by FTPFile#toFormattedDate) by clearing the immediately preceding unit (if any). Unfortunately the clear(int) method
94       * results in setting all other units.
95       */
96      private static void setPrecision(final int index, final Calendar working) {
97          if (index <= 0) { // e.g. MILLISECONDS
98              return;
99          }
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      * Implements the {@link Configurable Configurable} interface. Configures this {@code FTPTimestampParser} according to the following logic:
135      * <p>
136      * Sets 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}, 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}, 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         final 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         lenientFutureDates = config.isLenientFutureDates();
177     }
178 
179     /**
180      * Gets the defaultDateFormat.
181      *
182      * @return the defaultDateFormat.
183      */
184     public SimpleDateFormat getDefaultDateFormat() {
185         return defaultDateFormat;
186     }
187 
188     /**
189      * Gets the defaultDateFormat pattern string.
190      *
191      * @return the defaultDateFormat pattern string.
192      */
193     public String getDefaultDateFormatString() {
194         return defaultDateFormat.toPattern();
195     }
196 
197     /**
198      * Gets the recentDateFormat.
199      *
200      * @return the recentDateFormat.
201      */
202     public SimpleDateFormat getRecentDateFormat() {
203         return recentDateFormat;
204     }
205 
206     /**
207      * Gets the recentDateFormat.
208      *
209      * @return the recentDateFormat.
210      */
211     public String getRecentDateFormatString() {
212         return recentDateFormat.toPattern();
213     }
214 
215     /**
216      * Gets the serverTimeZone used by this parser.
217      *
218      * @return the serverTimeZone used by this parser.
219      */
220     public TimeZone getServerTimeZone() {
221         return defaultDateFormat.getTimeZone();
222     }
223 
224     /**
225      * Gets an array of 12 strings representing the short month names used by this parse.
226      *
227      * @return an array of 12 strings representing the short month names used by this parse.
228      */
229     public String[] getShortMonths() {
230         return defaultDateFormat.getDateFormatSymbols().getShortMonths();
231     }
232 
233     /**
234      * @return the lenientFutureDates.
235      */
236     boolean isLenientFutureDates() {
237         return lenientFutureDates;
238     }
239 
240     /**
241      * Implements the one {@link FTPTimestampParser#parseTimestamp(String) method} in the {@link FTPTimestampParser FTPTimestampParser} interface according to
242      * this algorithm:
243      *
244      * 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
245      * not been defined, attempt to parse with the defaultDateFormat member. If that fails, throw a ParseException.
246      *
247      * This method assumes that the server time is the same as the local time.
248      *
249      * @see FTPTimestampParserImpl#parseTimestamp(String, Calendar)
250      * @param timestampStr The timestamp to be parsed
251      * @return a Calendar with the parsed timestamp
252      */
253     @Override
254     public Calendar parseTimestamp(final String timestampStr) throws ParseException {
255         return parseTimestamp(timestampStr, Calendar.getInstance());
256     }
257 
258     /**
259      * 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
260      * not been defined, attempt to parse with the defaultDateFormat member. If that fails, throw a ParseException.
261      *
262      * This method allows a {@link Calendar} instance to be passed in which represents the current (system) time.
263      *
264      * @see FTPTimestampParser#parseTimestamp(String)
265      * @param timestampStr The timestamp to be parsed
266      * @param serverTime   The current time for the server
267      * @return the calendar
268      * @throws ParseException if timestamp cannot be parsed
269      * @since 1.5
270      */
271     public Calendar parseTimestamp(final String timestampStr, final Calendar serverTime) throws ParseException {
272         final Calendar working = (Calendar) serverTime.clone();
273         working.setTimeZone(getServerTimeZone()); // is this needed?
274 
275         Date parsed;
276 
277         if (recentDateFormat != null) {
278             final Calendar now = (Calendar) serverTime.clone(); // Copy this, because we may change it
279             now.setTimeZone(getServerTimeZone());
280             if (lenientFutureDates) {
281                 // add a day to "now" so that "slop" doesn't cause a date
282                 // slightly in the future to roll back a full year. (Bug 35181 => NET-83)
283                 now.add(Calendar.DAY_OF_MONTH, 1);
284             }
285             // The Java SimpleDateFormat class uses the epoch year 1970 if not present in the input
286             // As 1970 was not a leap year, it cannot parse "Feb 29" correctly.
287             // Java 1.5+ returns Mar 1 1970
288             // Temporarily add the current year to the short date time
289             // to cope with short-date leap year strings.
290             // Since Feb 29 is more that 6 months from the end of the year, this should be OK for
291             // all instances of short dates which are +- 6 months from current date.
292             // TODO this won't always work for systems that use short dates +0/-12months
293             // e.g. if today is Jan 1 2001 and the short date is Feb 29
294             final String year = Integer.toString(now.get(Calendar.YEAR));
295             final String timeStampStrPlusYear = timestampStr + " " + year;
296             final SimpleDateFormat hackFormatter = new SimpleDateFormat(recentDateFormat.toPattern() + " yyyy", recentDateFormat.getDateFormatSymbols());
297             hackFormatter.setLenient(false);
298             hackFormatter.setTimeZone(recentDateFormat.getTimeZone());
299             final ParsePosition pp = new ParsePosition(0);
300             parsed = hackFormatter.parse(timeStampStrPlusYear, pp);
301             // Check if we parsed the full string, if so it must have been a short date originally
302             if (parsed != null && pp.getIndex() == timeStampStrPlusYear.length()) {
303                 working.setTime(parsed);
304                 if (working.after(now)) { // must have been last year instead
305                     working.add(Calendar.YEAR, -1);
306                 }
307                 setPrecision(recentDateSmallestUnitIndex, working);
308                 return working;
309             }
310         }
311 
312         final ParsePosition pp = new ParsePosition(0);
313         parsed = defaultDateFormat.parse(timestampStr, pp);
314         // note, length checks are mandatory for us since
315         // SimpleDateFormat methods will succeed if less than
316         // full string is matched. They will also accept,
317         // despite "leniency" setting, a two-digit number as
318         // a valid year (e.g. 22:04 will parse as 22 A.D.)
319         // so could mistakenly confuse an hour with a year,
320         // if we don't insist on full length parsing.
321         if (parsed == null || pp.getIndex() != timestampStr.length()) {
322             throw new ParseException("Timestamp '" + timestampStr + "' could not be parsed using a server time of " + serverTime.getTime().toString(),
323                     pp.getErrorIndex());
324         }
325         working.setTime(parsed);
326         setPrecision(defaultDateSmallestUnitIndex, working);
327         return working;
328     }
329 
330     /**
331      * @param format The defaultDateFormat to be set.
332      * @param dfs    the symbols to use (may be null)
333      */
334     private void setDefaultDateFormat(final String format, final DateFormatSymbols dfs) {
335         if (format != null) {
336             if (dfs != null) {
337                 defaultDateFormat = new SimpleDateFormat(format, dfs);
338             } else {
339                 defaultDateFormat = new SimpleDateFormat(format);
340             }
341             defaultDateFormat.setLenient(false);
342         } else {
343             defaultDateFormat = null;
344         }
345         defaultDateSmallestUnitIndex = getEntry(defaultDateFormat);
346     }
347 
348     /**
349      * @param lenientFutureDates The lenientFutureDates to set.
350      */
351     void setLenientFutureDates(final boolean lenientFutureDates) {
352         this.lenientFutureDates = lenientFutureDates;
353     }
354 
355     /**
356      * @param format The recentDateFormat to set.
357      * @param dfs    the symbols to use (may be null)
358      */
359     private void setRecentDateFormat(final String format, final DateFormatSymbols dfs) {
360         if (format != null) {
361             if (dfs != null) {
362                 recentDateFormat = new SimpleDateFormat(format, dfs);
363             } else {
364                 recentDateFormat = new SimpleDateFormat(format);
365             }
366             recentDateFormat.setLenient(false);
367         } else {
368             recentDateFormat = null;
369         }
370         recentDateSmallestUnitIndex = getEntry(recentDateFormat);
371     }
372 
373     /**
374      * sets a TimeZone represented by the supplied ID string into all the parsers used by this server.
375      *
376      * @param serverTimeZoneId Time Id java.util.TimeZone id used by the ftp server. If null the client's local time zone is assumed.
377      */
378     private void setServerTimeZone(final String serverTimeZoneId) {
379         TimeZone serverTimeZone = TimeZone.getDefault();
380         if (serverTimeZoneId != null) {
381             serverTimeZone = TimeZone.getTimeZone(serverTimeZoneId);
382         }
383         defaultDateFormat.setTimeZone(serverTimeZone);
384         if (recentDateFormat != null) {
385             recentDateFormat.setTimeZone(serverTimeZone);
386         }
387     }
388 }