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    }