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 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());
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)
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.getIndex());
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}