FTPTimestampParserImpl.java

  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. package org.apache.commons.net.ftp.parser;

  18. import java.text.DateFormatSymbols;
  19. import java.text.ParseException;
  20. import java.text.ParsePosition;
  21. import java.text.SimpleDateFormat;
  22. import java.util.Calendar;
  23. import java.util.Date;
  24. import java.util.TimeZone;

  25. import org.apache.commons.net.ftp.Configurable;
  26. import org.apache.commons.net.ftp.FTPClientConfig;

  27. /**
  28.  * Default implementation of the {@link FTPTimestampParser FTPTimestampParser} interface also implements the {@link org.apache.commons.net.ftp.Configurable
  29.  * Configurable} interface to allow the parsing to be configured from the outside.
  30.  *
  31.  * @see ConfigurableFTPFileEntryParserImpl
  32.  * @since 1.4
  33.  */
  34. public class FTPTimestampParserImpl implements FTPTimestampParser, Configurable {

  35.     /*
  36.      * 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
  37.      * the parse string. The date formats are analysed to find the least significant unit (e.g. Minutes or Milliseconds) and the appropriate index to the array
  38.      * 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
  39.      * the array until the previous entry. e.g. for MINUTE it would clear MILLISECOND and SECOND
  40.      */
  41.     private static final int[] CALENDAR_UNITS = { Calendar.MILLISECOND, Calendar.SECOND, Calendar.MINUTE, Calendar.HOUR_OF_DAY, Calendar.DAY_OF_MONTH,
  42.             Calendar.MONTH, Calendar.YEAR };

  43.     /*
  44.      * Return the index to the array representing the least significant unit found in the date format. Default is 0 (to avoid dropping precision)
  45.      */
  46.     private static int getEntry(final SimpleDateFormat dateFormat) {
  47.         if (dateFormat == null) {
  48.             return 0;
  49.         }
  50.         final String FORMAT_CHARS = "SsmHdM";
  51.         final String pattern = dateFormat.toPattern();
  52.         for (final char ch : FORMAT_CHARS.toCharArray()) {
  53.             if (pattern.indexOf(ch) != -1) { // found the character
  54.                 switch (ch) {
  55.                 case 'S':
  56.                     return indexOf(Calendar.MILLISECOND);
  57.                 case 's':
  58.                     return indexOf(Calendar.SECOND);
  59.                 case 'm':
  60.                     return indexOf(Calendar.MINUTE);
  61.                 case 'H':
  62.                     return indexOf(Calendar.HOUR_OF_DAY);
  63.                 case 'd':
  64.                     return indexOf(Calendar.DAY_OF_MONTH);
  65.                 case 'M':
  66.                     return indexOf(Calendar.MONTH);
  67.                 }
  68.             }
  69.         }
  70.         return 0;
  71.     }

  72.     /*
  73.      * Find the entry in the CALENDAR_UNITS array.
  74.      */
  75.     private static int indexOf(final int calendarUnit) {
  76.         int i;
  77.         for (i = 0; i < CALENDAR_UNITS.length; i++) {
  78.             if (calendarUnit == CALENDAR_UNITS[i]) {
  79.                 return i;
  80.             }
  81.         }
  82.         return 0;
  83.     }

  84.     /*
  85.      * Sets the Calendar precision (used by FTPFile#toFormattedDate) by clearing the immediately preceding unit (if any). Unfortunately the clear(int) method
  86.      * results in setting all other units.
  87.      */
  88.     private static void setPrecision(final int index, final Calendar working) {
  89.         if (index <= 0) { // e.g. MILLISECONDS
  90.             return;
  91.         }
  92.         final int field = CALENDAR_UNITS[index - 1];
  93.         // Just in case the analysis is wrong, stop clearing if
  94.         // field value is not the default.
  95.         final int value = working.get(field);
  96.         if (value != 0) { // don't reset if it has a value
  97. //            new Throwable("Unexpected value "+value).printStackTrace(); // DEBUG
  98.         } else {
  99.             working.clear(field); // reset just the required field
  100.         }
  101.     }

  102.     /** The date format for all dates, except possibly recent dates. Assumed to include the year. */
  103.     private SimpleDateFormat defaultDateFormat;

  104.     /* The index in CALENDAR_UNITS of the smallest time unit in defaultDateFormat */
  105.     private int defaultDateSmallestUnitIndex;

  106.     /** The format used for recent dates (which don't have the year). May be null. */
  107.     private SimpleDateFormat recentDateFormat;

  108.     /* The index in CALENDAR_UNITS of the smallest time unit in recentDateFormat */
  109.     private int recentDateSmallestUnitIndex;

  110.     private boolean lenientFutureDates;

  111.     /**
  112.      * The only constructor for this class.
  113.      */
  114.     public FTPTimestampParserImpl() {
  115.         setDefaultDateFormat(DEFAULT_SDF, null);
  116.         setRecentDateFormat(DEFAULT_RECENT_SDF, null);
  117.     }

  118.     /**
  119.      * Implements the {@link Configurable Configurable} interface. Configures this <code>FTPTimestampParser</code> according to the following logic:
  120.      * <p>
  121.      * Sets up the {@link FTPClientConfig#setDefaultDateFormatStr(java.lang.String) defaultDateFormat} and optionally the
  122.      * {@link FTPClientConfig#setRecentDateFormatStr(String) recentDateFormat} to values supplied in the config based on month names configured as follows:
  123.      * </p>
  124.      * <ul>
  125.      * <li>If a {@link FTPClientConfig#setShortMonthNames(String) shortMonthString} has been supplied in the <code>config</code>, use that to parse parse
  126.      * timestamps.</li>
  127.      * <li>Otherwise, if a {@link FTPClientConfig#setServerLanguageCode(String) serverLanguageCode} has been supplied in the <code>config</code>, use the month
  128.      * names represented by that {@link FTPClientConfig#lookupDateFormatSymbols(String) language} to parse timestamps.</li>
  129.      * <li>otherwise use default English month names</li>
  130.      * </ul>
  131.      * <p>
  132.      * Finally if a {@link org.apache.commons.net.ftp.FTPClientConfig#setServerTimeZoneId(String) serverTimeZoneId} has been supplied via the config, set that
  133.      * into all date formats that have been configured.
  134.      * </p>
  135.      */
  136.     @Override
  137.     public void configure(final FTPClientConfig config) {
  138.         DateFormatSymbols dfs;

  139.         final String languageCode = config.getServerLanguageCode();
  140.         final String shortmonths = config.getShortMonthNames();
  141.         if (shortmonths != null) {
  142.             dfs = FTPClientConfig.getDateFormatSymbols(shortmonths);
  143.         } else if (languageCode != null) {
  144.             dfs = FTPClientConfig.lookupDateFormatSymbols(languageCode);
  145.         } else {
  146.             dfs = FTPClientConfig.lookupDateFormatSymbols("en");
  147.         }

  148.         final String recentFormatString = config.getRecentDateFormatStr();
  149.         setRecentDateFormat(recentFormatString, dfs);

  150.         final String defaultFormatString = config.getDefaultDateFormatStr();
  151.         if (defaultFormatString == null) {
  152.             throw new IllegalArgumentException("defaultFormatString cannot be null");
  153.         }
  154.         setDefaultDateFormat(defaultFormatString, dfs);

  155.         setServerTimeZone(config.getServerTimeZoneId());

  156.         this.lenientFutureDates = config.isLenientFutureDates();
  157.     }

  158.     /**
  159.      * @return Returns the defaultDateFormat.
  160.      */
  161.     public SimpleDateFormat getDefaultDateFormat() {
  162.         return defaultDateFormat;
  163.     }

  164.     /**
  165.      * @return Returns the defaultDateFormat pattern string.
  166.      */
  167.     public String getDefaultDateFormatString() {
  168.         return defaultDateFormat.toPattern();
  169.     }

  170.     /**
  171.      * @return Returns the recentDateFormat.
  172.      */
  173.     public SimpleDateFormat getRecentDateFormat() {
  174.         return recentDateFormat;
  175.     }

  176.     /**
  177.      * @return Returns the recentDateFormat.
  178.      */
  179.     public String getRecentDateFormatString() {
  180.         return recentDateFormat.toPattern();
  181.     }

  182.     /**
  183.      * @return Returns the serverTimeZone used by this parser.
  184.      */
  185.     public TimeZone getServerTimeZone() {
  186.         return this.defaultDateFormat.getTimeZone();
  187.     }

  188.     /**
  189.      * @return returns an array of 12 strings representing the short month names used by this parse.
  190.      */
  191.     public String[] getShortMonths() {
  192.         return defaultDateFormat.getDateFormatSymbols().getShortMonths();
  193.     }

  194.     /**
  195.      * @return Returns the lenientFutureDates.
  196.      */
  197.     boolean isLenientFutureDates() {
  198.         return lenientFutureDates;
  199.     }

  200.     /**
  201.      * Implements the one {@link FTPTimestampParser#parseTimestamp(String) method} in the {@link FTPTimestampParser FTPTimestampParser} interface according to
  202.      * this algorithm:
  203.      *
  204.      * 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
  205.      * not been defined, attempt to parse with the defaultDateFormat member. If that fails, throw a ParseException.
  206.      *
  207.      * This method assumes that the server time is the same as the local time.
  208.      *
  209.      * @see FTPTimestampParserImpl#parseTimestamp(String, Calendar)
  210.      *
  211.      * @param timestampStr The timestamp to be parsed
  212.      * @return a Calendar with the parsed timestamp
  213.      */
  214.     @Override
  215.     public Calendar parseTimestamp(final String timestampStr) throws ParseException {
  216.         return parseTimestamp(timestampStr, Calendar.getInstance());
  217.     }

  218.     /**
  219.      * 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
  220.      * not been defined, attempt to parse with the defaultDateFormat member. If that fails, throw a ParseException.
  221.      *
  222.      * This method allows a {@link Calendar} instance to be passed in which represents the current (system) time.
  223.      *
  224.      * @see FTPTimestampParser#parseTimestamp(String)
  225.      * @param timestampStr The timestamp to be parsed
  226.      * @param serverTime   The current time for the server
  227.      * @return the calendar
  228.      * @throws ParseException if timestamp cannot be parsed
  229.      * @since 1.5
  230.      */
  231.     public Calendar parseTimestamp(final String timestampStr, final Calendar serverTime) throws ParseException {
  232.         final Calendar working = (Calendar) serverTime.clone();
  233.         working.setTimeZone(getServerTimeZone()); // is this needed?

  234.         Date parsed;

  235.         if (recentDateFormat != null) {
  236.             final Calendar now = (Calendar) serverTime.clone(); // Copy this, because we may change it
  237.             now.setTimeZone(getServerTimeZone());
  238.             if (lenientFutureDates) {
  239.                 // add a day to "now" so that "slop" doesn't cause a date
  240.                 // slightly in the future to roll back a full year. (Bug 35181 => NET-83)
  241.                 now.add(Calendar.DAY_OF_MONTH, 1);
  242.             }
  243.             // The Java SimpleDateFormat class uses the epoch year 1970 if not present in the input
  244.             // As 1970 was not a leap year, it cannot parse "Feb 29" correctly.
  245.             // Java 1.5+ returns Mar 1 1970
  246.             // Temporarily add the current year to the short date time
  247.             // to cope with short-date leap year strings.
  248.             // Since Feb 29 is more that 6 months from the end of the year, this should be OK for
  249.             // all instances of short dates which are +- 6 months from current date.
  250.             // TODO this won't always work for systems that use short dates +0/-12months
  251.             // e.g. if today is Jan 1 2001 and the short date is Feb 29
  252.             final String year = Integer.toString(now.get(Calendar.YEAR));
  253.             final String timeStampStrPlusYear = timestampStr + " " + year;
  254.             final SimpleDateFormat hackFormatter = new SimpleDateFormat(recentDateFormat.toPattern() + " yyyy", recentDateFormat.getDateFormatSymbols());
  255.             hackFormatter.setLenient(false);
  256.             hackFormatter.setTimeZone(recentDateFormat.getTimeZone());
  257.             final ParsePosition pp = new ParsePosition(0);
  258.             parsed = hackFormatter.parse(timeStampStrPlusYear, pp);
  259.             // Check if we parsed the full string, if so it must have been a short date originally
  260.             if (parsed != null && pp.getIndex() == timeStampStrPlusYear.length()) {
  261.                 working.setTime(parsed);
  262.                 if (working.after(now)) { // must have been last year instead
  263.                     working.add(Calendar.YEAR, -1);
  264.                 }
  265.                 setPrecision(recentDateSmallestUnitIndex, working);
  266.                 return working;
  267.             }
  268.         }

  269.         final ParsePosition pp = new ParsePosition(0);
  270.         parsed = defaultDateFormat.parse(timestampStr, pp);
  271.         // note, length checks are mandatory for us since
  272.         // SimpleDateFormat methods will succeed if less than
  273.         // full string is matched. They will also accept,
  274.         // despite "leniency" setting, a two-digit number as
  275.         // a valid year (e.g. 22:04 will parse as 22 A.D.)
  276.         // so could mistakenly confuse an hour with a year,
  277.         // if we don't insist on full length parsing.
  278.         if (parsed == null || pp.getIndex() != timestampStr.length()) {
  279.             throw new ParseException("Timestamp '" + timestampStr + "' could not be parsed using a server time of " + serverTime.getTime().toString(),
  280.                     pp.getErrorIndex());
  281.         }
  282.         working.setTime(parsed);
  283.         setPrecision(defaultDateSmallestUnitIndex, working);
  284.         return working;
  285.     }

  286.     /**
  287.      * @param format The defaultDateFormat to be set.
  288.      * @param dfs    the symbols to use (may be null)
  289.      */
  290.     private void setDefaultDateFormat(final String format, final DateFormatSymbols dfs) {
  291.         if (format != null) {
  292.             if (dfs != null) {
  293.                 this.defaultDateFormat = new SimpleDateFormat(format, dfs);
  294.             } else {
  295.                 this.defaultDateFormat = new SimpleDateFormat(format);
  296.             }
  297.             this.defaultDateFormat.setLenient(false);
  298.         } else {
  299.             this.defaultDateFormat = null;
  300.         }
  301.         this.defaultDateSmallestUnitIndex = getEntry(this.defaultDateFormat);
  302.     }

  303.     /**
  304.      * @param lenientFutureDates The lenientFutureDates to set.
  305.      */
  306.     void setLenientFutureDates(final boolean lenientFutureDates) {
  307.         this.lenientFutureDates = lenientFutureDates;
  308.     }

  309.     /**
  310.      * @param format The recentDateFormat to set.
  311.      * @param dfs    the symbols to use (may be null)
  312.      */
  313.     private void setRecentDateFormat(final String format, final DateFormatSymbols dfs) {
  314.         if (format != null) {
  315.             if (dfs != null) {
  316.                 this.recentDateFormat = new SimpleDateFormat(format, dfs);
  317.             } else {
  318.                 this.recentDateFormat = new SimpleDateFormat(format);
  319.             }
  320.             this.recentDateFormat.setLenient(false);
  321.         } else {
  322.             this.recentDateFormat = null;
  323.         }
  324.         this.recentDateSmallestUnitIndex = getEntry(this.recentDateFormat);
  325.     }

  326.     /**
  327.      * sets a TimeZone represented by the supplied ID string into all the parsers used by this server.
  328.      *
  329.      * @param serverTimeZoneId Time Id java.util.TimeZone id used by the ftp server. If null the client's local time zone is assumed.
  330.      */
  331.     private void setServerTimeZone(final String serverTimeZoneId) {
  332.         TimeZone serverTimeZone = TimeZone.getDefault();
  333.         if (serverTimeZoneId != null) {
  334.             serverTimeZone = TimeZone.getTimeZone(serverTimeZoneId);
  335.         }
  336.         this.defaultDateFormat.setTimeZone(serverTimeZone);
  337.         if (this.recentDateFormat != null) {
  338.             this.recentDateFormat.setTimeZone(serverTimeZone);
  339.         }
  340.     }
  341. }