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}