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