View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      https://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.net.ftp.parser;
18  
19  import static org.junit.jupiter.api.Assertions.assertEquals;
20  import static org.junit.jupiter.api.Assertions.assertThrows;
21  import static org.junit.jupiter.api.Assertions.fail;
22  
23  import java.text.Format;
24  import java.text.ParseException;
25  import java.text.SimpleDateFormat;
26  import java.util.Calendar;
27  import java.util.Date;
28  import java.util.GregorianCalendar;
29  import java.util.Locale;
30  import java.util.TimeZone;
31  
32  import org.apache.commons.net.ftp.FTPClientConfig;
33  import org.junit.jupiter.api.Disabled;
34  import org.junit.jupiter.api.Test;
35  
36  /**
37   * Test the FTPTimestampParser class.
38   */
39  public class FTPTimestampParserImplTest {
40  
41      private static final int TWO_HOURS_OF_MILLISECONDS = 2 * 60 * 60 * 1000;
42  
43      /*
44       * Check how short date is interpreted at a given time. Check both with and without lenient future dates
45       */
46      private void checkShortParse(final String msg, final Calendar serverTime, final Calendar input) throws ParseException {
47          checkShortParse(msg, serverTime, input, false);
48          checkShortParse(msg, serverTime, input, true);
49      }
50  
51      /**
52       * Check how short date is interpreted at a given time Check only using specified lenient future dates setting
53       *
54       * @param msg        identifying message
55       * @param servertime the time at the server
56       * @param input      the time to be converted to a short date, parsed and tested against the full time
57       * @param lenient    whether to use lenient mode or not.
58       */
59      private void checkShortParse(final String msg, final Calendar servertime, final Calendar input, final boolean lenient) throws ParseException {
60          checkShortParse(msg, servertime, input, input, lenient);
61      }
62  
63      /*
64       * Check how short date is interpreted at a given time. Check both with and without lenient future dates
65       */
66      private void checkShortParse(final String msg, final Calendar serverTime, final Calendar input, final Calendar expected) throws ParseException {
67          checkShortParse(msg, serverTime, input, expected, false);
68          checkShortParse(msg, serverTime, input, expected, true);
69      }
70  
71      /**
72       * Check how short date is interpreted at a given time Check only using specified lenient future dates setting
73       *
74       * @param msg        identifying message
75       * @param servertime the time at the server
76       * @param input      the time to be converted to a short date and parsed
77       * @param expected   the expected result from parsing
78       * @param lenient    whether to use lenient mode or not.
79       */
80      private void checkShortParse(final String msg, final Calendar servertime, final Calendar input, final Calendar expected, final boolean lenient)
81              throws ParseException {
82          final FTPTimestampParserImpl parser = new FTPTimestampParserImpl();
83          parser.setLenientFutureDates(lenient);
84          final SimpleDateFormat shortFormat = parser.getRecentDateFormat(); // It's expecting this format
85  
86          final String shortDate = shortFormat.format(input.getTime());
87          final Calendar output = parser.parseTimestamp(shortDate, servertime);
88          final int outyear = output.get(Calendar.YEAR);
89          final int outdom = output.get(Calendar.DAY_OF_MONTH);
90          final int outmon = output.get(Calendar.MONTH);
91          final int inyear = expected.get(Calendar.YEAR);
92          final int indom = expected.get(Calendar.DAY_OF_MONTH);
93          final int inmon = expected.get(Calendar.MONTH);
94          if (indom != outdom || inmon != outmon || inyear != outyear) {
95              final Format longFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm");
96              fail("Test: '" + msg + "' Server=" + longFormat.format(servertime.getTime()) + ". Failed to parse " + shortDate
97                      + (lenient ? " (lenient)" : " (non-lenient)") + " using " + shortFormat.toPattern() + ". Actual " + longFormat.format(output.getTime())
98                      + ". Expected " + longFormat.format(expected.getTime()));
99          }
100     }
101 
102     // Test leap year if current year is a leap year
103     @Test
104     public void testFeb29IfLeapYear() throws Exception {
105         final GregorianCalendar now = new GregorianCalendar();
106         final int thisYear = now.get(Calendar.YEAR);
107         final GregorianCalendar target = new GregorianCalendar(thisYear, Calendar.FEBRUARY, 29);
108         if (now.isLeapYear(thisYear) && now.after(target) && now.before(new GregorianCalendar(thisYear, Calendar.AUGUST, 29))) {
109             checkShortParse("Feb 29th", now, target);
110         } else {
111             System.out.println("Skipping Feb 29 test (not leap year or before Feb 29)");
112         }
113     }
114 
115     // Test Feb 29 for a known leap year
116     @Test
117     public void testFeb29LeapYear() throws Exception {
118         final int year = 2000; // Use same year for current and short date
119         final GregorianCalendar now = new GregorianCalendar(year, Calendar.APRIL, 1, 12, 0);
120         checkShortParse("Feb 29th 2000", now, new GregorianCalendar(year, Calendar.FEBRUARY, 29));
121     }
122 
123     @Test
124     public void testFeb29LeapYear2() throws Exception {
125         final int year = 2000; // Use same year for current and short date
126         final GregorianCalendar now = new GregorianCalendar(year, Calendar.MARCH, 1, 12, 0);
127         checkShortParse("Feb 29th 2000", now, new GregorianCalendar(year, Calendar.FEBRUARY, 29));
128     }
129 
130     // same date feb 29
131     @Test
132     public void testFeb29LeapYear3() throws Exception {
133         final int year = 2000; // Use same year for current and short date
134         final GregorianCalendar now = new GregorianCalendar(year, Calendar.FEBRUARY, 29, 12, 0);
135         checkShortParse("Feb 29th 2000", now, new GregorianCalendar(year, Calendar.FEBRUARY, 29));
136     }
137 
138     // future dated Feb 29
139     @Test
140     public void testFeb29LeapYear4() throws Exception {
141         final int year = 2000; // Use same year for current and short date
142         final GregorianCalendar now = new GregorianCalendar(year, Calendar.FEBRUARY, 28, 12, 0);
143         // Must allow lenient future date here
144         checkShortParse("Feb 29th 2000", now, new GregorianCalendar(year, Calendar.FEBRUARY, 29), true);
145     }
146 
147     // Test Feb 29 for a known non-leap year - should fail
148     @Test
149     public void testFeb29NonLeapYear() {
150         final GregorianCalendar server = new GregorianCalendar(1999, Calendar.APRIL, 1, 12, 0);
151         // Note: we use a known leap year for the target date to avoid rounding up
152         final GregorianCalendar input = new GregorianCalendar(2000, Calendar.FEBRUARY, 29);
153         final GregorianCalendar expected = new GregorianCalendar(1999, Calendar.FEBRUARY, 29);
154         assertThrows(ParseException.class, () -> checkShortParse("Feb 29th 1999", server, input, expected, true));
155         assertThrows(ParseException.class, () -> checkShortParse("Feb 29th 1999", server, input, expected, false));
156     }
157 
158     @Test
159     public void testNET444() throws Exception {
160         final FTPTimestampParserImpl parser = new FTPTimestampParserImpl();
161         parser.setLenientFutureDates(true);
162         final SimpleDateFormat sdf = new SimpleDateFormat(parser.getRecentDateFormatString());
163         final GregorianCalendar now = new GregorianCalendar(2012, Calendar.FEBRUARY, 28, 12, 0);
164 
165         final GregorianCalendar nowplus1 = new GregorianCalendar(2012, Calendar.FEBRUARY, 28, 13, 0);
166         // Create a suitable short date
167         final String future1 = sdf.format(nowplus1.getTime());
168         final Calendar parsed1 = parser.parseTimestamp(future1, now);
169         assertEquals(nowplus1.get(Calendar.YEAR), parsed1.get(Calendar.YEAR));
170 
171         final GregorianCalendar nowplus25 = new GregorianCalendar(2012, Calendar.FEBRUARY, 29, 13, 0);
172         // Create a suitable short date
173         final String future25 = sdf.format(nowplus25.getTime());
174         final Calendar parsed25 = parser.parseTimestamp(future25, now);
175         assertEquals(nowplus25.get(Calendar.YEAR) - 1, parsed25.get(Calendar.YEAR));
176     }
177 
178 //    Lenient mode allows for dates up to 1 day in the future
179 
180     // This test currently fails, because we assume that short dates are +-6months when parsing Feb 29
181     @Test
182     @Disabled
183     public void testNET446() throws Exception {
184         final GregorianCalendar server = new GregorianCalendar(2001, Calendar.JANUARY, 1, 12, 0);
185         // Note: we use a known leap year for the target date to avoid rounding up
186         final GregorianCalendar input = new GregorianCalendar(2000, Calendar.FEBRUARY, 29);
187         final GregorianCalendar expected = new GregorianCalendar(2000, Calendar.FEBRUARY, 29);
188         checkShortParse("Feb 29th 2000", server, input, expected);
189     }
190 
191     /**
192      * This test is still broken:
193      * <pre>
194 [ERROR] Tests run: 20, Failures: 0, Errors: 1, Skipped: 1, Time elapsed: 0.153 s <<< FAILURE! -- in org.apache.commons.net.ftp.parser.FTPTimestampParserImplTest
195 [ERROR] org.apache.commons.net.ftp.parser.FTPTimestampParserImplTest.testNet710 -- Time elapsed: 0.009 s <<< ERROR!
196 java.text.ParseException: Timestamp 'Mar 13 02:33' could not be parsed using a server time of Wed Mar 16 10:00:27 EDT 2022
197     at org.apache.commons.net.ftp.parser.FTPTimestampParserImpl.parseTimestamp(FTPTimestampParserImpl.java:312)
198     at org.apache.commons.net.ftp.parser.FTPTimestampParserImplTest.testNet710(FTPTimestampParserImplTest.java:196)
199     at java.base/java.lang.reflect.Method.invoke(Method.java:568)
200     at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
201     at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
202      * </pre>
203      */
204     @Test
205     @Disabled
206     public void testNet710() throws ParseException {
207         final Calendar serverTime = Calendar.getInstance(TimeZone.getTimeZone("EDT"), Locale.US);
208         serverTime.set(2022, Calendar.MARCH, 16, 14, 0);
209         new FTPTimestampParserImpl().parseTimestamp("Mar 13 02:33", serverTime);
210     }
211 
212     @Test
213     public void testParseDec31Lenient() throws Exception {
214         final GregorianCalendar now = new GregorianCalendar(2007, Calendar.DECEMBER, 30, 12, 0);
215         checkShortParse("2007-12-30", now, now); // should always work
216         final GregorianCalendar target = (GregorianCalendar) now.clone();
217         target.add(Calendar.DAY_OF_YEAR, +1); // tomorrow
218         checkShortParse("2007-12-31", now, target, true);
219     }
220 
221     @Test
222     public void testParseJan01() throws Exception {
223         final GregorianCalendar now = new GregorianCalendar(2007, Calendar.JANUARY, 1, 12, 0);
224         checkShortParse("2007-01-01", now, now); // should always work
225         final GregorianCalendar target = new GregorianCalendar(2006, Calendar.DECEMBER, 31, 12, 0);
226         checkShortParse("2006-12-31", now, target, true);
227         checkShortParse("2006-12-31", now, target, false);
228     }
229 
230     @Test
231     public void testParseJan01Lenient() throws Exception {
232         final GregorianCalendar now = new GregorianCalendar(2007, Calendar.DECEMBER, 31, 12, 0);
233         checkShortParse("2007-12-31", now, now); // should always work
234         final GregorianCalendar target = (GregorianCalendar) now.clone();
235         target.add(Calendar.DAY_OF_YEAR, +1); // tomorrow
236         checkShortParse("2008-1-1", now, target, true);
237     }
238 
239     @Test
240     public void testParser() throws ParseException {
241         // This test requires an English Locale
242         final Locale locale = Locale.getDefault();
243         try {
244             Locale.setDefault(Locale.ENGLISH);
245             final FTPTimestampParserImpl parser = new FTPTimestampParserImpl();
246             parser.parseTimestamp("feb 22 2002");
247             assertThrows(ParseException.class, () -> parser.parseTimestamp("f\u00e9v 22 2002"));
248 
249             final FTPClientConfig config = new FTPClientConfig();
250             config.setDefaultDateFormatStr("d MMM yyyy");
251             config.setRecentDateFormatStr("d MMM HH:mm");
252             config.setServerLanguageCode("fr");
253             parser.configure(config);
254             assertThrows(ParseException.class, () -> parser.parseTimestamp("d\u00e9c 22 2002"), "incorrect.field.order");
255             try {
256                 parser.parseTimestamp("22 d\u00e9c 2002");
257             } catch (final ParseException e) {
258                 fail("failed.to.parse.french");
259             }
260             assertThrows(ParseException.class, () -> parser.parseTimestamp("22 dec 2002"), "incorrect.language");
261             assertThrows(ParseException.class, () -> parser.parseTimestamp("29 f\u00e9v 2002"), "nonexistent.date");
262             assertThrows(ParseException.class, () -> parser.parseTimestamp("22 ao\u00fb 30:02"), "bad.hour");
263             assertThrows(ParseException.class, () -> parser.parseTimestamp("22 ao\u00fb 20:74"), "bad.minute");
264             try {
265                 parser.parseTimestamp("28 ao\u00fb 20:02");
266             } catch (final ParseException e) {
267                 fail("failed.to.parse.french.recent");
268             }
269         } finally {
270             Locale.setDefault(locale);
271         }
272     }
273 
274     @Test
275     public void testParseShortFutureDates1() throws Exception {
276         final GregorianCalendar now = new GregorianCalendar(2001, Calendar.MAY, 30, 12, 0);
277         checkShortParse("2001-5-30", now, now); // should always work
278         final GregorianCalendar target = (GregorianCalendar) now.clone();
279         target.add(Calendar.DAY_OF_MONTH, 1);
280         checkShortParse("2001-5-30 +1 day", now, target, true);
281         final AssertionError error = assertThrows(AssertionError.class, () -> checkShortParse("2001-5-30 +1 day", now, target, false));
282         if (error.getMessage().startsWith("Expected AssertionFailedError")) { // don't swallow our failure
283             throw error;
284         }
285         target.add(Calendar.WEEK_OF_YEAR, 1);
286 //        checkShortParse("2001-5-30 +1 week",now,target);
287 //        target.add(Calendar.WEEK_OF_YEAR, 12);
288 //        checkShortParse("2001-5-30 +13 weeks",now,target);
289 //        target.add(Calendar.WEEK_OF_YEAR, 13);
290 //        checkShortParse("2001-5-30 +26 weeks",now,target);
291     }
292 
293     @Test
294     public void testParseShortFutureDates2() throws Exception {
295         final GregorianCalendar now = new GregorianCalendar(2004, Calendar.AUGUST, 1, 12, 0);
296         checkShortParse("2004-8-1", now, now); // should always work
297         final GregorianCalendar target = (GregorianCalendar) now.clone();
298         target.add(Calendar.DAY_OF_MONTH, 1);
299         checkShortParse("2004-8-1 +1 day", now, target, true);
300         final AssertionError error = assertThrows(AssertionError.class, () -> checkShortParse("2004-8-1 +1 day", now, target, false));
301         if (error.getMessage().startsWith("Expected AssertionFailedError")) { // don't swallow our failure
302             throw error;
303         }
304 //        target.add(Calendar.WEEK_OF_YEAR, 1);
305 //        checkShortParse("2004-8-1 +1 week",now,target);
306 //        target.add(Calendar.WEEK_OF_YEAR, 12);
307 //        checkShortParse("2004-8-1 +13 weeks",now,target);
308 //        target.add(Calendar.WEEK_OF_YEAR, 13);
309 //        checkShortParse("2004-8-1 +26 weeks",now,target);
310     }
311 
312     @Test
313     public void testParseShortPastDates1() throws Exception {
314         final GregorianCalendar now = new GregorianCalendar(2001, Calendar.MAY, 30, 12, 0);
315         checkShortParse("2001-5-30", now, now); // should always work
316         final GregorianCalendar target = (GregorianCalendar) now.clone();
317         target.add(Calendar.WEEK_OF_YEAR, -1);
318         checkShortParse("2001-5-30 -1 week", now, target);
319         target.add(Calendar.WEEK_OF_YEAR, -12);
320         checkShortParse("2001-5-30 -13 weeks", now, target);
321         target.add(Calendar.WEEK_OF_YEAR, -13);
322         checkShortParse("2001-5-30 -26 weeks", now, target);
323     }
324 
325     @Test
326     public void testParseShortPastDates2() throws Exception {
327         final GregorianCalendar now = new GregorianCalendar(2004, Calendar.AUGUST, 1, 12, 0);
328         checkShortParse("2004-8-1", now, now); // should always work
329         final GregorianCalendar target = (GregorianCalendar) now.clone();
330         target.add(Calendar.WEEK_OF_YEAR, -1);
331         checkShortParse("2004-8-1 -1 week", now, target);
332         target.add(Calendar.WEEK_OF_YEAR, -12);
333         checkShortParse("2004-8-1 -13 weeks", now, target);
334         target.add(Calendar.WEEK_OF_YEAR, -13);
335         checkShortParse("2004-8-1 -26 weeks", now, target);
336     }
337 
338     @Test
339     public void testParseTimestamp() {
340         final Calendar cal = Calendar.getInstance();
341         cal.add(Calendar.HOUR_OF_DAY, 1);
342         cal.set(Calendar.SECOND, 0);
343         cal.set(Calendar.MILLISECOND, 0);
344         final Date anHourFromNow = cal.getTime();
345         final FTPTimestampParserImpl parser = new FTPTimestampParserImpl();
346         final SimpleDateFormat sdf = new SimpleDateFormat(parser.getRecentDateFormatString());
347         final String fmtTime = sdf.format(anHourFromNow);
348         try {
349             final Calendar parsed = parser.parseTimestamp(fmtTime);
350             // since the timestamp is ahead of now (by one hour),
351             // this must mean the file's date refers to a year ago.
352             assertEquals(1, cal.get(Calendar.YEAR) - parsed.get(Calendar.YEAR), "test.roll.back.year");
353         } catch (final ParseException e) {
354             fail("Unable to parse");
355         }
356     }
357 
358     @Test
359     public void testParseTimestampAcrossTimeZones() {
360         final Calendar cal = Calendar.getInstance();
361         cal.set(Calendar.SECOND, 0);
362         cal.set(Calendar.MILLISECOND, 0);
363 
364         cal.add(Calendar.HOUR_OF_DAY, 1);
365         final Date anHourFromNow = cal.getTime();
366 
367         cal.add(Calendar.HOUR_OF_DAY, 2);
368         final Date threeHoursFromNow = cal.getTime();
369         cal.add(Calendar.HOUR_OF_DAY, -2);
370 
371         final FTPTimestampParserImpl parser = new FTPTimestampParserImpl();
372 
373         // assume we are FTPing a server in Chicago, two hours ahead of
374         // L. A.
375         final FTPClientConfig config = new FTPClientConfig(FTPClientConfig.SYST_UNIX);
376         config.setDefaultDateFormatStr(FTPTimestampParser.DEFAULT_SDF);
377         config.setRecentDateFormatStr(FTPTimestampParser.DEFAULT_RECENT_SDF);
378         // 2 hours difference
379         config.setServerTimeZoneId("America/Chicago");
380         config.setLenientFutureDates(false); // NET-407
381         parser.configure(config);
382 
383         final SimpleDateFormat sdf = (SimpleDateFormat) parser.getRecentDateFormat().clone();
384 
385         // assume we're in the US Pacific Time Zone
386         final TimeZone tzla = TimeZone.getTimeZone("America/Los_Angeles");
387         sdf.setTimeZone(tzla);
388 
389         // get formatted versions of time in L.A.
390         final String fmtTimePlusOneHour = sdf.format(anHourFromNow);
391         final String fmtTimePlusThreeHours = sdf.format(threeHoursFromNow);
392 
393         try {
394             final Calendar parsed = parser.parseTimestamp(fmtTimePlusOneHour);
395             // the only difference should be the two hours
396             // difference, no rolling back a year should occur.
397             assertEquals(TWO_HOURS_OF_MILLISECONDS, cal.getTime().getTime() - parsed.getTime().getTime(), "no.rollback.because.of.time.zones");
398         } catch (final ParseException e) {
399             fail("Unable to parse " + fmtTimePlusOneHour);
400         }
401 
402         // but if the file's timestamp is THREE hours ahead of now, that should
403         // cause a rollover even taking the time zone difference into account.
404         // Since that time is still later than ours, it is parsed as occurring
405         // on this date last year.
406         try {
407             final Calendar parsed = parser.parseTimestamp(fmtTimePlusThreeHours);
408             // rollback should occur here.
409             assertEquals(1, cal.get(Calendar.YEAR) - parsed.get(Calendar.YEAR), "rollback.even.with.time.zones");
410         } catch (final ParseException e) {
411             fail("Unable to parse" + fmtTimePlusThreeHours);
412         }
413     }
414 
415     @Test
416     public void testParseTimestampWithSlop() {
417         final Calendar cal = Calendar.getInstance();
418         cal.set(Calendar.SECOND, 0);
419         cal.set(Calendar.MILLISECOND, 0);
420 
421         final Calendar caltemp = (Calendar) cal.clone();
422         caltemp.add(Calendar.HOUR_OF_DAY, 1);
423         final Date anHourFromNow = caltemp.getTime();
424         caltemp.add(Calendar.DAY_OF_MONTH, 1);
425         final Date anHourFromNowTomorrow = caltemp.getTime();
426 
427         final FTPTimestampParserImpl parser = new FTPTimestampParserImpl();
428 
429         // set the "slop" factor on
430         parser.setLenientFutureDates(true);
431 
432         final SimpleDateFormat sdf = new SimpleDateFormat(parser.getRecentDateFormatString());
433         try {
434             String fmtTime = sdf.format(anHourFromNow);
435             Calendar parsed = parser.parseTimestamp(fmtTime);
436             // the timestamp is ahead of now (by one hour), but
437             // that's within range of the "slop" factor.
438             // so the date is still considered this year.
439             assertEquals(0, cal.get(Calendar.YEAR) - parsed.get(Calendar.YEAR), "test.slop.no.roll.back.year");
440 
441             // add a day to get beyond the range of the slop factor.
442             // this must mean the file's date refers to a year ago.
443             fmtTime = sdf.format(anHourFromNowTomorrow);
444             parsed = parser.parseTimestamp(fmtTime);
445             assertEquals(1, cal.get(Calendar.YEAR) - parsed.get(Calendar.YEAR), "test.slop.roll.back.year");
446 
447         } catch (final ParseException e) {
448             fail("Unable to parse");
449         }
450     }
451 
452 }