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.  *      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. 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 analyzed 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} 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}, 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}, 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.         final 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.         lenientFutureDates = config.isLenientFutureDates();
  157.     }

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

  166.     /**
  167.      * Gets the defaultDateFormat pattern string.
  168.      *
  169.      * @return the defaultDateFormat pattern string.
  170.      */
  171.     public String getDefaultDateFormatString() {
  172.         return defaultDateFormat.toPattern();
  173.     }

  174.     /**
  175.      * Gets the recentDateFormat.
  176.      *
  177.      * @return the recentDateFormat.
  178.      */
  179.     public SimpleDateFormat getRecentDateFormat() {
  180.         return recentDateFormat;
  181.     }

  182.     /**
  183.      * Gets the recentDateFormat.
  184.      *
  185.      * @return the recentDateFormat.
  186.      */
  187.     public String getRecentDateFormatString() {
  188.         return recentDateFormat.toPattern();
  189.     }

  190.     /**
  191.      * Gets the serverTimeZone used by this parser.
  192.      *
  193.      * @return the serverTimeZone used by this parser.
  194.      */
  195.     public TimeZone getServerTimeZone() {
  196.         return defaultDateFormat.getTimeZone();
  197.     }

  198.     /**
  199.      * Gets an array of 12 strings representing the short month names used by this parse.
  200.      *
  201.      * @return an array of 12 strings representing the short month names used by this parse.
  202.      */
  203.     public String[] getShortMonths() {
  204.         return defaultDateFormat.getDateFormatSymbols().getShortMonths();
  205.     }

  206.     /**
  207.      * @return the lenientFutureDates.
  208.      */
  209.     boolean isLenientFutureDates() {
  210.         return lenientFutureDates;
  211.     }

  212.     /**
  213.      * Implements the one {@link FTPTimestampParser#parseTimestamp(String) method} in the {@link FTPTimestampParser FTPTimestampParser} interface according to
  214.      * this algorithm:
  215.      *
  216.      * 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
  217.      * not been defined, attempt to parse with the defaultDateFormat member. If that fails, throw a ParseException.
  218.      *
  219.      * This method assumes that the server time is the same as the local time.
  220.      *
  221.      * @see FTPTimestampParserImpl#parseTimestamp(String, Calendar)
  222.      * @param timestampStr The timestamp to be parsed
  223.      * @return a Calendar with the parsed timestamp
  224.      */
  225.     @Override
  226.     public Calendar parseTimestamp(final String timestampStr) throws ParseException {
  227.         return parseTimestamp(timestampStr, Calendar.getInstance());
  228.     }

  229.     /**
  230.      * 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
  231.      * not been defined, attempt to parse with the defaultDateFormat member. If that fails, throw a ParseException.
  232.      *
  233.      * This method allows a {@link Calendar} instance to be passed in which represents the current (system) time.
  234.      *
  235.      * @see FTPTimestampParser#parseTimestamp(String)
  236.      * @param timestampStr The timestamp to be parsed
  237.      * @param serverTime   The current time for the server
  238.      * @return the calendar
  239.      * @throws ParseException if timestamp cannot be parsed
  240.      * @since 1.5
  241.      */
  242.     public Calendar parseTimestamp(final String timestampStr, final Calendar serverTime) throws ParseException {
  243.         final Calendar working = (Calendar) serverTime.clone();
  244.         working.setTimeZone(getServerTimeZone()); // is this needed?

  245.         Date parsed;

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

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

  297.     /**
  298.      * @param format The defaultDateFormat to be set.
  299.      * @param dfs    the symbols to use (may be null)
  300.      */
  301.     private void setDefaultDateFormat(final String format, final DateFormatSymbols dfs) {
  302.         if (format != null) {
  303.             if (dfs != null) {
  304.                 defaultDateFormat = new SimpleDateFormat(format, dfs);
  305.             } else {
  306.                 defaultDateFormat = new SimpleDateFormat(format);
  307.             }
  308.             defaultDateFormat.setLenient(false);
  309.         } else {
  310.             defaultDateFormat = null;
  311.         }
  312.         defaultDateSmallestUnitIndex = getEntry(defaultDateFormat);
  313.     }

  314.     /**
  315.      * @param lenientFutureDates The lenientFutureDates to set.
  316.      */
  317.     void setLenientFutureDates(final boolean lenientFutureDates) {
  318.         this.lenientFutureDates = lenientFutureDates;
  319.     }

  320.     /**
  321.      * @param format The recentDateFormat to set.
  322.      * @param dfs    the symbols to use (may be null)
  323.      */
  324.     private void setRecentDateFormat(final String format, final DateFormatSymbols dfs) {
  325.         if (format != null) {
  326.             if (dfs != null) {
  327.                 recentDateFormat = new SimpleDateFormat(format, dfs);
  328.             } else {
  329.                 recentDateFormat = new SimpleDateFormat(format);
  330.             }
  331.             recentDateFormat.setLenient(false);
  332.         } else {
  333.             recentDateFormat = null;
  334.         }
  335.         recentDateSmallestUnitIndex = getEntry(recentDateFormat);
  336.     }

  337.     /**
  338.      * sets a TimeZone represented by the supplied ID string into all the parsers used by this server.
  339.      *
  340.      * @param serverTimeZoneId Time Id java.util.TimeZone id used by the ftp server. If null the client's local time zone is assumed.
  341.      */
  342.     private void setServerTimeZone(final String serverTimeZoneId) {
  343.         TimeZone serverTimeZone = TimeZone.getDefault();
  344.         if (serverTimeZoneId != null) {
  345.             serverTimeZone = TimeZone.getTimeZone(serverTimeZoneId);
  346.         }
  347.         defaultDateFormat.setTimeZone(serverTimeZone);
  348.         if (recentDateFormat != null) {
  349.             recentDateFormat.setTimeZone(serverTimeZone);
  350.         }
  351.     }
  352. }