    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 allows a {@link Calendar} instance to be passed in which represents the
    068         * current (system) time.
    069         *
    070         * @see org.apache.commons.net.ftp.parser.FTPTimestampParser#parseTimestamp(java.lang.String)
    071         *
    072         * @param timestampStr The timestamp to be parsed
    073         */
    074        public Calendar parseTimestamp(String timestampStr) throws ParseException {
    075            Calendar now = Calendar.getInstance();
    076            return parseTimestamp(timestampStr, now);
    077        }
    078
    079        /**
    080         * Implements the one {@link  FTPTimestampParser#parseTimestamp(String)  method}
    081         * in the {@link  FTPTimestampParser  FTPTimestampParser} interface
    082         * according to this algorithm:
    083         *
    084         * If the recentDateFormat member has been defined, try to parse the
    085         * supplied string with that.  If that parse fails, or if the recentDateFormat
    086         * member has not been defined, attempt to parse with the defaultDateFormat
    087         * member.  If that fails, throw a ParseException.
    088         *
    089         * @see org.apache.commons.net.ftp.parser.FTPTimestampParser#parseTimestamp(java.lang.String)
    090         * @param timestampStr The timestamp to be parsed
    091         * @param serverTime The current time for the server
    092         * @since 1.5
    093         */
    094        public Calendar parseTimestamp(String timestampStr, Calendar serverTime) throws ParseException {
    095            Calendar now = (Calendar) serverTime.clone();// Copy this, because we may change it
    096            now.setTimeZone(this.getServerTimeZone());
    097            Calendar working = (Calendar) now.clone();
    098            working.setTimeZone(getServerTimeZone()); // is this needed?
    099            ParsePosition pp = new ParsePosition(0);
    100
    101            Date parsed = null;
    102            if (recentDateFormat != null) {
    103                if (lenientFutureDates) {
    104                    // add a day to "now" so that "slop" doesn't cause a date
    105                    // slightly in the future to roll back a full year.  (Bug 35181 => NET-83)
    106                    now.add(Calendar.DATE, 1);
    107                }
    108                parsed = recentDateFormat.parse(timestampStr, pp);
    109            }
    110            if (parsed != null && pp.getIndex() == timestampStr.length())
    111            {
    112                working.setTime(parsed);
    113                working.set(Calendar.YEAR, now.get(Calendar.YEAR));
    114
    115                if (working.after(now)) {
    116                    working.add(Calendar.YEAR, -1);
    117                }
    118            } else {
    119                // Temporarily add the current year to the short date time
    120                // to cope with short-date leap year strings.
    121                // e.g. Java's DateFormatter will assume that "Feb 29 12:00" refers to
    122                // Feb 29 1970 (an invalid date) rather than a potentially valid leap year date.
    123                // This is pretty bad hack to work around the deficiencies of the JDK date/time classes.
    124                if (recentDateFormat != null) {
    125                    pp = new ParsePosition(0);
    126                    int year = now.get(Calendar.YEAR);
    127                    String timeStampStrPlusYear = timestampStr + " " + year;
    128                    SimpleDateFormat hackFormatter = new SimpleDateFormat(recentDateFormat.toPattern() + " yyyy",
    129                            recentDateFormat.getDateFormatSymbols());
    130                    hackFormatter.setLenient(false);
    131                    hackFormatter.setTimeZone(recentDateFormat.getTimeZone());
    132                    parsed = hackFormatter.parse(timeStampStrPlusYear, pp);
    133                }
    134                if (parsed != null && pp.getIndex() == timestampStr.length() + 5) {
    135                    working.setTime(parsed);
    136                }
    137                else {
    138                    pp = new ParsePosition(0);
    139                    parsed = defaultDateFormat.parse(timestampStr, pp);
    140                    // note, length checks are mandatory for us since
    141                    // SimpleDateFormat methods will succeed if less than
    142                    // full string is matched.  They will also accept,
    143                    // despite "leniency" setting, a two-digit number as
    144                    // a valid year (e.g. 22:04 will parse as 22 A.D.)
    145                    // so could mistakenly confuse an hour with a year,
    146                    // if we don't insist on full length parsing.
    147                    if (parsed != null && pp.getIndex() == timestampStr.length()) {
    148                        working.setTime(parsed);
    149                    } else {
    150                        throw new ParseException(
    151                                "Timestamp could not be parsed with older or recent DateFormat",
    152                                pp.getErrorIndex());
    153                    }
    154                }
    155            }
    156            return working;
    157        }
    158
    159        /**
    160         * @return Returns the defaultDateFormat.
    161         */
    162        public SimpleDateFormat getDefaultDateFormat() {
    163            return defaultDateFormat;
    164        }
    165        /**
    166         * @return Returns the defaultDateFormat pattern string.
    167         */
    168        public String getDefaultDateFormatString() {
    169            return defaultDateFormat.toPattern();
    170        }
    171        /**
    172         * @param defaultDateFormat The defaultDateFormat to be set.
    173         */
    174        private void setDefaultDateFormat(String format) {
    175            if (format != null) {
    176                this.defaultDateFormat = new SimpleDateFormat(format);
    177                this.defaultDateFormat.setLenient(false);
    178            }
    179        }
    180        /**
    181         * @return Returns the recentDateFormat.
    182         */
    183        public SimpleDateFormat getRecentDateFormat() {
    184            return recentDateFormat;
    185        }
    186        /**
    187         * @return Returns the recentDateFormat.
    188         */
    189        public String getRecentDateFormatString() {
    190            return recentDateFormat.toPattern();
    191        }
    192        /**
    193         * @param recentDateFormat The recentDateFormat to set.
    194         */
    195        private void setRecentDateFormat(String format) {
    196            if (format != null) {
    197                this.recentDateFormat = new SimpleDateFormat(format);
    198                this.recentDateFormat.setLenient(false);
    199            }
    200        }
    201
    202        /**
    203         * @return returns an array of 12 strings representing the short
    204         * month names used by this parse.
    205         */
    206        public String[] getShortMonths() {
    207            return defaultDateFormat.getDateFormatSymbols().getShortMonths();
    208        }
    209
    210
    211        /**
    212         * @return Returns the serverTimeZone used by this parser.
    213         */
    214        public TimeZone getServerTimeZone() {
    215            return this.defaultDateFormat.getTimeZone();
    216        }
    217        /**
    218         * sets a TimeZone represented by the supplied ID string into all
    219         * of the parsers used by this server.
    220         * @param serverTimeZone Time Id java.util.TimeZone id used by
    221         * the ftp server.  If null the client's local time zone is assumed.
    222         */
    223        private void setServerTimeZone(String serverTimeZoneId) {
    224            TimeZone serverTimeZone = TimeZone.getDefault();
    225            if (serverTimeZoneId != null) {
    226                serverTimeZone = TimeZone.getTimeZone(serverTimeZoneId);
    227            }
    228            this.defaultDateFormat.setTimeZone(serverTimeZone);
    229            if (this.recentDateFormat != null) {
    230                this.recentDateFormat.setTimeZone(serverTimeZone);
    231            }
    232        }
    233
    234        /**
    235         * Implementation of the {@link  Configurable  Configurable}
    236         * interface. Configures this <code>FTPTimestampParser</code> according
    237         * to the following logic:
    238         * <p>
    239         * Set up the {@link  FTPClientConfig#setDefaultDateFormatStr(java.lang.String) defaultDateFormat}
    240         * and optionally the {@link  FTPClientConfig#setRecentDateFormatStr(String) recentDateFormat}
    241         * to values supplied in the config based on month names configured as follows:
    242         * </p><p><ul>
    243         * <li>If a {@link  FTPClientConfig#setShortMonthNames(String) shortMonthString}
    244         * has been supplied in the <code>config</code>, use that to parse  parse timestamps.</li>
    245         * <li>Otherwise, if a {@link  FTPClientConfig#setServerLanguageCode(String) serverLanguageCode}
    246         * has been supplied in the <code>config</code>, use the month names represented
    247         * by that {@link  FTPClientConfig#lookupDateFormatSymbols(String) language}
    248         * to parse timestamps.</li>
    249         * <li>otherwise use default English month names</li>
    250         * </ul></p><p>
    251         * Finally if a {@link  org.apache.commons.net.ftp.FTPClientConfig#setServerTimeZoneId(String) serverTimeZoneId}
    252         * has been supplied via the config, set that into all date formats that have
    253         * been configured.
    254         * </p>
    255         */
    256        public void configure(FTPClientConfig config) {
    257            DateFormatSymbols dfs = null;
    258
    259            String languageCode = config.getServerLanguageCode();
    260            String shortmonths = config.getShortMonthNames();
    261            if (shortmonths != null) {
    262                dfs = FTPClientConfig.getDateFormatSymbols(shortmonths);
    263            } else if (languageCode != null) {
    264                dfs = FTPClientConfig.lookupDateFormatSymbols(languageCode);
    265            } else {
    266                dfs = FTPClientConfig.lookupDateFormatSymbols("en");
    267            }
    268
    269
    270            String recentFormatString = config.getRecentDateFormatStr();
    271            if (recentFormatString == null) {
    272                this.recentDateFormat = null;
    273            } else {
    274                this.recentDateFormat = new SimpleDateFormat(recentFormatString, dfs);
    275                this.recentDateFormat.setLenient(false);
    276            }
    277
    278            String defaultFormatString = config.getDefaultDateFormatStr();
    279            if (defaultFormatString == null) {
    280                throw new IllegalArgumentException("defaultFormatString cannot be null");
    281            }
    282            this.defaultDateFormat = new SimpleDateFormat(defaultFormatString, dfs);
    283            this.defaultDateFormat.setLenient(false);
    284
    285            setServerTimeZone(config.getServerTimeZoneId());
    286
    287            this.lenientFutureDates = config.isLenientFutureDates();
    288        }
    289        /**
    290         * @return Returns the lenientFutureDates.
    291         */
    292        boolean isLenientFutureDates() {
    293            return lenientFutureDates;
    294        }
    295        /**
    296         * @param lenientFutureDates The lenientFutureDates to set.
    297         */
    298        void setLenientFutureDates(boolean lenientFutureDates) {
    299            this.lenientFutureDates = lenientFutureDates;
    300        }
    301    }