    001    /*
    002     * Licensed to the Apache Software Foundation (ASF) under one or more
    003     * contributor license agreements.  See the NOTICE file distributed with
    004     * this work for additional information regarding copyright ownership.
    005     * The ASF licenses this file to You under the Apache License, Version 2.0
    006     * (the "License"); you may not use this file except in compliance with
    007     * the License.  You may obtain a copy of the License at
    008     *
    009     *      http://www.apache.org/licenses/LICENSE-2.0
    010     *
    011     * Unless required by applicable law or agreed to in writing, software
    012     * distributed under the License is distributed on an "AS IS" BASIS,
    013     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    014     * See the License for the specific language governing permissions and
    015     * limitations under the License.
    016     */
    017
    018    package org.apache.commons.net.ftp.parser;
    019
    020    import java.text.DateFormatSymbols;
    021    import java.text.ParseException;
    022    import java.text.ParsePosition;
    023    import java.text.SimpleDateFormat;
    024    import java.util.Calendar;
    025    import java.util.Date;
    026    import java.util.TimeZone;
    027
    028    import org.apache.commons.net.ftp.Configurable;
    029    import org.apache.commons.net.ftp.FTPClientConfig;
    030
    031    /**
    032     * Default implementation of the {@link  FTPTimestampParser  FTPTimestampParser}
    033     * interface also implements the {@link  org.apache.commons.net.ftp.Configurable  Configurable}
    034     * interface to allow the parsing to be configured from the outside.
    035     *
    036     * @see ConfigurableFTPFileEntryParserImpl
    037     * @since 1.4
    038     */
    039    public class FTPTimestampParserImpl implements
    040            FTPTimestampParser, Configurable
    041    {
    042
    043
    044        private SimpleDateFormat defaultDateFormat;
    045        private SimpleDateFormat recentDateFormat;
    046        private boolean lenientFutureDates = false;
    047
    048
    049        /**
    050         * The only constructor for this class.
    051         */
    052        public FTPTimestampParserImpl() {
    053            setDefaultDateFormat(DEFAULT_SDF);
    054            setRecentDateFormat(DEFAULT_RECENT_SDF);
    055        }
    056
    057        /**
    058         * Implements the one {@link  FTPTimestampParser#parseTimestamp(String)  method}
    059         * in the {@link  FTPTimestampParser  FTPTimestampParser} interface
    060         * according to this algorithm:
    061         *
    062         * If the recentDateFormat member has been defined, try to parse the
    063         * supplied string with that.  If that parse fails, or if the recentDateFormat
    064         * member has not been defined, attempt to parse with the defaultDateFormat
    065         * member.  If that fails, throw a ParseException.
    066         *
    067         * This method assumes that the server time is the same as the local time.
    068         *
    069         * @see FTPTimestampParserImpl#parseTimestamp(String, Calendar)
    070         *
    071         * @param timestampStr The timestamp to be parsed
    072         */
    073        public Calendar parseTimestamp(String timestampStr) throws ParseException {
    074            Calendar now = Calendar.getInstance();
    075            return parseTimestamp(timestampStr, now);
    076        }
    077
    078        /**
    079         * If the recentDateFormat member has been defined, try to parse the
    080         * supplied string with that.  If that parse fails, or if the recentDateFormat
    081         * member has not been defined, attempt to parse with the defaultDateFormat
    082         * member.  If that fails, throw a ParseException.
    083         *
    084         * This method allows a {@link Calendar} instance to be passed in which represents the
    085         * current (system) time.
    086         *
    087         * @see FTPTimestampParser#parseTimestamp(String)
    088         * @param timestampStr The timestamp to be parsed
    089         * @param serverTime The current time for the server
    090         * @since 1.5
    091         */
    092        public Calendar parseTimestamp(String timestampStr, Calendar serverTime) throws ParseException {
    093            Calendar working = (Calendar) serverTime.clone();
    094            working.setTimeZone(getServerTimeZone()); // is this needed?
    095
    096            Date parsed = null;
    097
    098            if (recentDateFormat != null) {
    099                Calendar now = (Calendar) serverTime.clone();// Copy this, because we may change it
    100                now.setTimeZone(this.getServerTimeZone());
    101                if (lenientFutureDates) {
    102                    // add a day to "now" so that "slop" doesn't cause a date
    103                    // slightly in the future to roll back a full year.  (Bug 35181 => NET-83)
    104                    now.add(Calendar.DATE, 1);
    105                }
    106                // The Java SimpleDateFormat class uses the epoch year 1970 if not present in the input
    107                // As 1970 was not a leap year, it cannot parse "Feb 29" correctly.
    108                // Java 1.5+ returns Mar 1 1970
    109                // Temporarily add the current year to the short date time
    110                // to cope with short-date leap year strings.
    111                // Since Feb 29 is more that 6 months from the end of the year, this should be OK for
    112                // all instances of short dates which are +- 6 months from current date.
    113                // TODO this won't always work for systems that use short dates +0/-12months
    114                // e.g. if today is Jan 1 2001 and the short date is Feb 29
    115                String year = Integer.toString(now.get(Calendar.YEAR));
    116                String timeStampStrPlusYear = timestampStr + " " + year;
    117                SimpleDateFormat hackFormatter = new SimpleDateFormat(recentDateFormat.toPattern() + " yyyy",
    118                        recentDateFormat.getDateFormatSymbols());
    119                hackFormatter.setLenient(false);
    120                hackFormatter.setTimeZone(recentDateFormat.getTimeZone());
    121                ParsePosition pp = new ParsePosition(0);
    122                parsed = hackFormatter.parse(timeStampStrPlusYear, pp);
    123                // Check if we parsed the full string, if so it must have been a short date originally
    124                if (parsed != null && pp.getIndex() == timeStampStrPlusYear.length()) {
    125                    working.setTime(parsed);
    126                    if (working.after(now)) { // must have been last year instead
    127                        working.add(Calendar.YEAR, -1);
    128                    }
    129                    return working;
    130                }
    131            }
    132
    133            ParsePosition pp = new ParsePosition(0);
    134            parsed = defaultDateFormat.parse(timestampStr, pp);
    135            // note, length checks are mandatory for us since
    136            // SimpleDateFormat methods will succeed if less than
    137            // full string is matched.  They will also accept,
    138            // despite "leniency" setting, a two-digit number as
    139            // a valid year (e.g. 22:04 will parse as 22 A.D.)
    140            // so could mistakenly confuse an hour with a year,
    141            // if we don't insist on full length parsing.
    142            if (parsed != null && pp.getIndex() == timestampStr.length()) {
    143                working.setTime(parsed);
    144            } else {
    145                throw new ParseException(
    146                        "Timestamp '"+timestampStr+"' could not be parsed using a server time of "
    147                            +serverTime.getTime().toString(),
    148                        pp.getErrorIndex());
    149            }
    150            return working;
    151        }
    152
    153        /**
    154         * @return Returns the defaultDateFormat.
    155         */
    156        public SimpleDateFormat getDefaultDateFormat() {
    157            return defaultDateFormat;
    158        }
    159        /**
    160         * @return Returns the defaultDateFormat pattern string.
    161         */
    162        public String getDefaultDateFormatString() {
    163            return defaultDateFormat.toPattern();
    164        }
    165        /**
    166         * @param defaultDateFormat The defaultDateFormat to be set.
    167         */
    168        private void setDefaultDateFormat(String format) {
    169            if (format != null) {
    170                this.defaultDateFormat = new SimpleDateFormat(format);
    171                this.defaultDateFormat.setLenient(false);
    172            }
    173        }
    174        /**
    175         * @return Returns the recentDateFormat.
    176         */
    177        public SimpleDateFormat getRecentDateFormat() {
    178            return recentDateFormat;
    179        }
    180        /**
    181         * @return Returns the recentDateFormat.
    182         */
    183        public String getRecentDateFormatString() {
    184            return recentDateFormat.toPattern();
    185        }
    186        /**
    187         * @param recentDateFormat The recentDateFormat to set.
    188         */
    189        private void setRecentDateFormat(String format) {
    190            if (format != null) {
    191                this.recentDateFormat = new SimpleDateFormat(format);
    192                this.recentDateFormat.setLenient(false);
    193            }
    194        }
    195
    196        /**
    197         * @return returns an array of 12 strings representing the short
    198         * month names used by this parse.
    199         */
    200        public String[] getShortMonths() {
    201            return defaultDateFormat.getDateFormatSymbols().getShortMonths();
    202        }
    203
    204
    205        /**
    206         * @return Returns the serverTimeZone used by this parser.
    207         */
    208        public TimeZone getServerTimeZone() {
    209            return this.defaultDateFormat.getTimeZone();
    210        }
    211        /**
    212         * sets a TimeZone represented by the supplied ID string into all
    213         * of the parsers used by this server.
    214         * @param serverTimeZone Time Id java.util.TimeZone id used by
    215         * the ftp server.  If null the client's local time zone is assumed.
    216         */
    217        private void setServerTimeZone(String serverTimeZoneId) {
    218            TimeZone serverTimeZone = TimeZone.getDefault();
    219            if (serverTimeZoneId != null) {
    220                serverTimeZone = TimeZone.getTimeZone(serverTimeZoneId);
    221            }
    222            this.defaultDateFormat.setTimeZone(serverTimeZone);
    223            if (this.recentDateFormat != null) {
    224                this.recentDateFormat.setTimeZone(serverTimeZone);
    225            }
    226        }
    227
    228        /**
    229         * Implementation of the {@link  Configurable  Configurable}
    230         * interface. Configures this <code>FTPTimestampParser</code> according
    231         * to the following logic:
    232         * <p>
    233         * Set up the {@link  FTPClientConfig#setDefaultDateFormatStr(java.lang.String) defaultDateFormat}
    234         * and optionally the {@link  FTPClientConfig#setRecentDateFormatStr(String) recentDateFormat}
    235         * to values supplied in the config based on month names configured as follows:
    236         * </p><p><ul>
    237         * <li>If a {@link  FTPClientConfig#setShortMonthNames(String) shortMonthString}
    238         * has been supplied in the <code>config</code>, use that to parse  parse timestamps.</li>
    239         * <li>Otherwise, if a {@link  FTPClientConfig#setServerLanguageCode(String) serverLanguageCode}
    240         * has been supplied in the <code>config</code>, use the month names represented
    241         * by that {@link  FTPClientConfig#lookupDateFormatSymbols(String) language}
    242         * to parse timestamps.</li>
    243         * <li>otherwise use default English month names</li>
    244         * </ul></p><p>
    245         * Finally if a {@link  org.apache.commons.net.ftp.FTPClientConfig#setServerTimeZoneId(String) serverTimeZoneId}
    246         * has been supplied via the config, set that into all date formats that have
    247         * been configured.
    248         * </p>
    249         */
    250        public void configure(FTPClientConfig config) {
    251            DateFormatSymbols dfs = null;
    252
    253            String languageCode = config.getServerLanguageCode();
    254            String shortmonths = config.getShortMonthNames();
    255            if (shortmonths != null) {
    256                dfs = FTPClientConfig.getDateFormatSymbols(shortmonths);
    257            } else if (languageCode != null) {
    258                dfs = FTPClientConfig.lookupDateFormatSymbols(languageCode);
    259            } else {
    260                dfs = FTPClientConfig.lookupDateFormatSymbols("en");
    261            }
    262
    263
    264            String recentFormatString = config.getRecentDateFormatStr();
    265            if (recentFormatString == null) {
    266                this.recentDateFormat = null;
    267            } else {
    268                this.recentDateFormat = new SimpleDateFormat(recentFormatString, dfs);
    269                this.recentDateFormat.setLenient(false);
    270            }
    271
    272            String defaultFormatString = config.getDefaultDateFormatStr();
    273            if (defaultFormatString == null) {
    274                throw new IllegalArgumentException("defaultFormatString cannot be null");
    275            }
    276            this.defaultDateFormat = new SimpleDateFormat(defaultFormatString, dfs);
    277            this.defaultDateFormat.setLenient(false);
    278
    279            setServerTimeZone(config.getServerTimeZoneId());
    280
    281            this.lenientFutureDates = config.isLenientFutureDates();
    282        }
    283        /**
    284         * @return Returns the lenientFutureDates.
    285         */
    286        boolean isLenientFutureDates() {
    287            return lenientFutureDates;
    288        }
    289        /**
    290         * @param lenientFutureDates The lenientFutureDates to set.
    291         */
    292        void setLenientFutureDates(boolean lenientFutureDates) {
    293            this.lenientFutureDates = lenientFutureDates;
    294        }
    295    }