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    package org.apache.commons.net.ftp.parser;
018    
019    import java.text.Format;
020    import java.text.ParseException;
021    import java.text.SimpleDateFormat;
022    import java.util.Calendar;
023    import java.util.Date;
024    import java.util.GregorianCalendar;
025    import java.util.TimeZone;
026    
027    import org.apache.commons.net.ftp.FTPClientConfig;
028    
029    import junit.framework.TestCase;
030    import junit.framework.TestSuite;
031    
032    /**
033     * Test the FTPTimestampParser class.
034     * 
035     * @author scohen
036     *
037     */
038    public class FTPTimestampParserImplTest extends TestCase {
039        
040        private static final int TWO_HOURS_OF_MILLISECONDS = 2 * 60 * 60 * 1000;
041    
042        public void testParseTimestamp() {
043            Calendar cal = Calendar.getInstance();
044            cal.add(Calendar.HOUR_OF_DAY, 1);
045            cal.set(Calendar.SECOND,0);
046            cal.set(Calendar.MILLISECOND,0);
047            Date anHourFromNow = cal.getTime();
048            FTPTimestampParserImpl parser = new FTPTimestampParserImpl();
049            SimpleDateFormat sdf = 
050                new SimpleDateFormat(parser.getRecentDateFormatString());
051            String fmtTime = sdf.format(anHourFromNow);
052            try {
053                Calendar parsed = parser.parseTimestamp(fmtTime);
054                // since the timestamp is ahead of now (by one hour),
055                // this must mean the file's date refers to a year ago.
056                assertEquals("test.roll.back.year", 1, cal.get(Calendar.YEAR) - parsed.get(Calendar.YEAR));
057            } catch (ParseException e) {
058                fail("Unable to parse");
059            }
060        }
061            
062        public void testParseTimestampWithSlop() {
063            Calendar cal = Calendar.getInstance();
064            cal.add(Calendar.HOUR_OF_DAY, 1);
065            cal.set(Calendar.SECOND,0);
066            cal.set(Calendar.MILLISECOND,0);
067            Date anHourFromNow = cal.getTime();
068            cal.add(Calendar.DATE, 1);
069            Date anHourFromNowTomorrow = cal.getTime();
070            cal.add(Calendar.DATE, -1);
071    
072            FTPTimestampParserImpl parser = new FTPTimestampParserImpl();
073            
074            // set the "slop" factor on
075            parser.setLenientFutureDates(true);
076            
077            SimpleDateFormat sdf = 
078                new SimpleDateFormat(parser.getRecentDateFormatString());
079            try {
080                String fmtTime = sdf.format(anHourFromNow);
081                Calendar parsed = parser.parseTimestamp(fmtTime);
082                // the timestamp is ahead of now (by one hour), but
083                // that's within range of the "slop" factor.
084                // so the date is still considered this year.
085                assertEquals("test.slop.no.roll.back.year", 0, cal.get(Calendar.YEAR) - parsed.get(Calendar.YEAR));
086    
087                // add a day to get beyond the range of the slop factor. 
088                // this must mean the file's date refers to a year ago.
089                fmtTime = sdf.format(anHourFromNowTomorrow);
090                parsed = parser.parseTimestamp(fmtTime);
091                assertEquals("test.slop.roll.back.year", 1, cal.get(Calendar.YEAR) - parsed.get(Calendar.YEAR));
092                
093            } catch (ParseException e) {
094                fail("Unable to parse");
095            }
096        }
097    
098        public void testParseTimestampAcrossTimeZones() {
099            
100            
101            Calendar cal = Calendar.getInstance();
102            cal.set(Calendar.SECOND,0);
103            cal.set(Calendar.MILLISECOND,0);
104    
105            cal.add(Calendar.HOUR_OF_DAY, 1);
106            Date anHourFromNow = cal.getTime();
107            
108            cal.add(Calendar.HOUR_OF_DAY, 2);
109            Date threeHoursFromNow = cal.getTime();
110            cal.add(Calendar.HOUR_OF_DAY, -2);
111            
112            FTPTimestampParserImpl parser = new FTPTimestampParserImpl();
113    
114            // assume we are FTPing a server in Chicago, two hours ahead of 
115            // L. A.
116            FTPClientConfig config = 
117                new FTPClientConfig(FTPClientConfig.SYST_UNIX);
118            config.setDefaultDateFormatStr(FTPTimestampParser.DEFAULT_SDF);
119            config.setRecentDateFormatStr(FTPTimestampParser.DEFAULT_RECENT_SDF);
120            // 2 hours difference
121            config.setServerTimeZoneId("America/Chicago");
122            parser.configure(config);
123            
124            SimpleDateFormat sdf = (SimpleDateFormat)
125                parser.getRecentDateFormat().clone();
126            
127            // assume we're in the US Pacific Time Zone
128            TimeZone tzla = TimeZone.getTimeZone("America/Los_Angeles");
129            sdf.setTimeZone(tzla);
130            
131            // get formatted versions of time in L.A. 
132            String fmtTimePlusOneHour = sdf.format(anHourFromNow);
133            String fmtTimePlusThreeHours = sdf.format(threeHoursFromNow);
134            
135            
136            try {
137                Calendar parsed = parser.parseTimestamp(fmtTimePlusOneHour);
138                // the only difference should be the two hours
139                // difference, no rolling back a year should occur.
140                assertEquals("no.rollback.because.of.time.zones",
141                    TWO_HOURS_OF_MILLISECONDS, 
142                    cal.getTime().getTime() - parsed.getTime().getTime());
143            } catch (ParseException e){
144                fail("Unable to parse " + fmtTimePlusOneHour);
145            }
146            
147            //but if the file's timestamp is THREE hours ahead of now, that should 
148            //cause a rollover even taking the time zone difference into account.
149            //Since that time is still later than ours, it is parsed as occurring
150            //on this date last year.
151            try {
152                Calendar parsed = parser.parseTimestamp(fmtTimePlusThreeHours);
153                // rollback should occur here.
154                assertEquals("rollback.even.with.time.zones", 
155                        1, cal.get(Calendar.YEAR) - parsed.get(Calendar.YEAR));
156            } catch (ParseException e){
157                fail("Unable to parse" + fmtTimePlusThreeHours);
158            }
159        }
160    
161    
162        public void testParser() {
163            FTPTimestampParserImpl parser = new FTPTimestampParserImpl();
164            try {
165                parser.parseTimestamp("feb 22 2002");
166            } catch (ParseException e) {
167                fail("failed.to.parse.default");
168            }
169            try {
170                parser.parseTimestamp("f\u00e9v 22 2002");
171                fail("should.have.failed.to.parse.default");
172            } catch (ParseException e) {
173                // this is the success case
174            }
175    
176            FTPClientConfig config = new FTPClientConfig();
177            config.setDefaultDateFormatStr("d MMM yyyy");
178            config.setRecentDateFormatStr("d MMM HH:mm");
179            config.setServerLanguageCode("fr");
180            parser.configure(config);
181            try {
182                parser.parseTimestamp("d\u00e9c 22 2002");
183                fail("incorrect.field.order");
184            } catch (ParseException e) {
185                // this is the success case
186            }
187            try {
188                parser.parseTimestamp("22 d\u00e9c 2002");
189            } catch (ParseException e) {
190                fail("failed.to.parse.french");
191            }
192            
193            try {
194                parser.parseTimestamp("22 dec 2002");
195                fail("incorrect.language");
196            } catch (ParseException e) {
197                // this is the success case
198            }
199            try {
200                parser.parseTimestamp("29 f\u00e9v 2002");
201                fail("nonexistent.date");
202            } catch (ParseException e) {
203                // this is the success case
204            }
205    
206            try {
207                parser.parseTimestamp("22 ao\u00fb 30:02");
208                fail("bad.hour");
209            } catch (ParseException e) {
210                // this is the success case
211            }
212            
213            try {
214                parser.parseTimestamp("22 ao\u00fb 20:74");
215                fail("bad.minute");
216            } catch (ParseException e) {
217                // this is the success case
218            }
219            try {
220                parser.parseTimestamp("28 ao\u00fb 20:02");
221            } catch (ParseException e) {
222                fail("failed.to.parse.french.recent");
223            }
224        }
225        
226        /*
227         * Check how short date is interpreted at a given time.
228         * Check both with and without lenient future dates
229         */
230        private void checkShortParse(String msg, Calendar now, Calendar input) throws ParseException {
231            checkShortParse(msg, now, input, false);
232            checkShortParse(msg, now, input, true);
233        }
234    
235        /*
236         * Check how short date is interpreted at a given time
237         * Check only using specified lenient future dates setting
238         */
239        private void checkShortParse(String msg, Calendar now, Calendar input, boolean lenient) throws ParseException {
240            FTPTimestampParserImpl parser = new FTPTimestampParserImpl();
241            parser.setLenientFutureDates(lenient);
242            Format shortFormat = parser.getRecentDateFormat(); // It's expecting this format
243            Format longFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm");
244            
245            final String shortDate = shortFormat.format(input.getTime());
246            Calendar output=parser.parseTimestamp(shortDate, now);
247            int outyear = output.get(Calendar.YEAR);
248            int outdom = output.get(Calendar.DAY_OF_MONTH);
249            int outmon = output.get(Calendar.MONTH);
250            int inyear = input.get(Calendar.YEAR);
251            int indom = input.get(Calendar.DAY_OF_MONTH);
252            int inmon = input.get(Calendar.MONTH);
253            if (indom != outdom || inmon != outmon || inyear != outyear){
254                fail("Test: '"+msg+"' Server="+longFormat.format(now.getTime())
255                        +". Failed to parse "+shortDate
256                        +". Actual "+longFormat.format(output.getTime())
257                        +". Expected "+longFormat.format(input.getTime()));
258            }
259        }
260    
261        public void testParseShortPastDates1() throws Exception {
262            GregorianCalendar now = new GregorianCalendar(2001, Calendar.MAY, 30, 12, 0);
263            checkShortParse("2001-5-30",now,now); // should always work
264            GregorianCalendar target = (GregorianCalendar) now.clone();
265            target.add(Calendar.WEEK_OF_YEAR, -1);
266            checkShortParse("2001-5-30 -1 week",now,target);
267            target.add(Calendar.WEEK_OF_YEAR, -12);
268            checkShortParse("2001-5-30 -13 weeks",now,target);
269            target.add(Calendar.WEEK_OF_YEAR, -13);
270            checkShortParse("2001-5-30 -26 weeks",now,target);
271        }
272    
273        public void testParseShortPastDates2() throws Exception {
274            GregorianCalendar now = new GregorianCalendar(2004, Calendar.AUGUST, 1, 12, 0);
275            checkShortParse("2004-8-1",now,now); // should always work
276            GregorianCalendar target = (GregorianCalendar) now.clone();
277            target.add(Calendar.WEEK_OF_YEAR, -1);
278            checkShortParse("2004-8-1 -1 week",now,target);
279            target.add(Calendar.WEEK_OF_YEAR, -12);
280            checkShortParse("2004-8-1 -13 weeks",now,target);
281            target.add(Calendar.WEEK_OF_YEAR, -13);
282            checkShortParse("2004-8-1 -26 weeks",now,target);
283        }
284    
285    //    It has not yet been decided how to handle future dates, so skip these tests for now
286        
287    //    public void testParseShortFutureDates1() throws Exception {
288    //        GregorianCalendar now = new GregorianCalendar(2001, Calendar.MAY, 30, 12, 0);
289    //        checkShortParse("2001-5-30",now,now); // should always work
290    //        GregorianCalendar target = (GregorianCalendar) now.clone();
291    //        target.add(Calendar.WEEK_OF_YEAR, 1);
292    //        checkShortParse("2001-5-30 +1 week",now,target);
293    //        target.add(Calendar.WEEK_OF_YEAR, 12);
294    //        checkShortParse("2001-5-30 +13 weeks",now,target);
295    //        target.add(Calendar.WEEK_OF_YEAR, 13);
296    //        checkShortParse("2001-5-30 +26 weeks",now,target);
297    //    }
298    
299    //    public void testParseShortFutureDates2() throws Exception {
300    //        GregorianCalendar now = new GregorianCalendar(2004, Calendar.AUGUST, 1, 12, 0);
301    //        checkShortParse("2004-8-1",now,now); // should always work
302    //        GregorianCalendar target = (GregorianCalendar) now.clone();
303    //        target.add(Calendar.WEEK_OF_YEAR, 1);
304    //        checkShortParse("2004-8-1 +1 week",now,target);
305    //        target.add(Calendar.WEEK_OF_YEAR, 12);
306    //        checkShortParse("2004-8-1 +13 weeks",now,target);
307    //        target.add(Calendar.WEEK_OF_YEAR, 13);
308    //        checkShortParse("2004-8-1 +26 weeks",now,target);
309    //    }
310    
311        // Test leap year if current year is a leap year
312        public void testFeb29IfLeapYear() throws Exception{
313            GregorianCalendar now = new GregorianCalendar();
314            final int thisYear = now.get(Calendar.YEAR);
315            if (now.isLeapYear(thisYear) && now.before(new GregorianCalendar(thisYear,Calendar.AUGUST,29))){
316                GregorianCalendar target = new GregorianCalendar(thisYear,Calendar.FEBRUARY,29);            
317                checkShortParse("Feb 29th",now,target);
318            } else {
319                System.out.println("Skipping Feb 29 test");
320            }
321        }
322    
323        // Test Feb 29 for a known leap year
324        public void testFeb29LeapYear() throws Exception{
325            int year = 2000; // Use same year for current and short date
326            GregorianCalendar now = new GregorianCalendar(year, Calendar.APRIL, 1, 12, 0);
327            checkShortParse("Feb 29th 2000",now,new GregorianCalendar(year, Calendar.FEBRUARY,29));
328        }
329    
330        // Test Feb 29 for a known non-leap year - should fail
331        public void testFeb29NonLeapYear(){
332            GregorianCalendar now = new GregorianCalendar(1999, Calendar.APRIL, 1, 12, 0);
333            // Note: we use a known leap year for the target date to avoid rounding up
334            try {
335                checkShortParse("Feb 29th 1999",now,new GregorianCalendar(2000, Calendar.FEBRUARY,29));
336                fail("Should have failed to parse Feb 29th 1999");
337            } catch (ParseException expected) {
338            }
339        }
340    
341        public void testParseDec31Lenient() throws Exception {
342            GregorianCalendar now = new GregorianCalendar(2007, Calendar.DECEMBER, 30, 12, 0);
343            checkShortParse("2007-12-30",now,now); // should always work
344            GregorianCalendar target = (GregorianCalendar) now.clone();
345            target.add(Calendar.DAY_OF_YEAR, +1); // tomorrow
346            checkShortParse("2007-12-31",now,target, true);
347        }
348    
349        public void testParseJan01Lenient() throws Exception {
350            GregorianCalendar now = new GregorianCalendar(2007, Calendar.DECEMBER, 31, 12, 0);
351            checkShortParse("2007-12-31",now,now); // should always work
352            GregorianCalendar target = (GregorianCalendar) now.clone();
353            target.add(Calendar.DAY_OF_YEAR, +1); // tomorrow
354            checkShortParse("2008-1-1",now,target, true);
355        }
356    
357        public void testParseJan01() throws Exception {
358            GregorianCalendar now = new GregorianCalendar(2007, Calendar.JANUARY, 1, 12, 0);
359            checkShortParse("2007-01-01",now,now); // should always work
360            GregorianCalendar target = new GregorianCalendar(2006, Calendar.DECEMBER, 31, 12, 0);
361            checkShortParse("2006-12-31",now,target, true);
362            checkShortParse("2006-12-31",now,target, false);
363        }
364    
365        /**
366         * Method suite.
367         *
368         * @return TestSuite
369         */
370        public static TestSuite suite()
371        {
372            return(new TestSuite(FTPTimestampParserImplTest.class));
373        }
374    
375    
376    
377    }