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