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  
18  package org.apache.commons.beanutils2.converters;
19  
20  import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
21  import static org.junit.jupiter.api.Assertions.assertEquals;
22  import static org.junit.jupiter.api.Assertions.assertInstanceOf;
23  import static org.junit.jupiter.api.Assertions.assertNotNull;
24  import static org.junit.jupiter.api.Assertions.assertThrows;
25  import static org.junit.jupiter.api.Assertions.assertTrue;
26  import static org.junit.jupiter.api.Assertions.fail;
27  
28  import java.text.DateFormat;
29  import java.text.SimpleDateFormat;
30  import java.time.Instant;
31  import java.time.LocalDate;
32  import java.time.LocalDateTime;
33  import java.time.OffsetDateTime;
34  import java.time.ZoneId;
35  import java.time.ZonedDateTime;
36  import java.util.Calendar;
37  import java.util.Date;
38  import java.util.GregorianCalendar;
39  import java.util.Locale;
40  import java.util.Objects;
41  
42  import org.apache.commons.beanutils2.ConversionException;
43  import org.apache.commons.beanutils2.Converter;
44  import org.junit.jupiter.api.Test;
45  
46  /**
47   * Abstract base for <Date>Converter classes.
48   *
49   * @param <T> type to test
50   */
51  public abstract class AbstractDateConverterTest<T> {
52  
53      /**
54       * Gets the expected type
55       *
56       * @return The expected type
57       */
58      protected abstract Class<T> getExpectedType();
59  
60      /**
61       * Converts a Date or Calendar objects to the time in milliseconds
62       *
63       * @param date The date or calendar object
64       * @return The time in milliseconds
65       */
66      protected long getTimeInMillis(final Object date) {
67          if (date instanceof java.sql.Date) {
68              return ((java.sql.Date) date).getTime();
69          }
70  
71          if (date instanceof java.sql.Timestamp) {
72              return ((java.sql.Timestamp) date).getTime();
73          }
74  
75          if (date instanceof LocalDate) {
76              return ((LocalDate) date).atStartOfDay(ZoneId.systemDefault()).toInstant().toEpochMilli();
77          }
78  
79          if (date instanceof LocalDateTime) {
80              return ((LocalDateTime) date).atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
81          }
82  
83          if (date instanceof ZonedDateTime) {
84              return ((ZonedDateTime) date).toInstant().toEpochMilli();
85          }
86  
87          if (date instanceof OffsetDateTime) {
88              return ((OffsetDateTime) date).toInstant().toEpochMilli();
89          }
90  
91          if (date instanceof Calendar) {
92              return ((Calendar) date).getTime().getTime();
93          }
94  
95          if (date instanceof Date) {
96              return ((Date) date).getTime();
97          }
98          throw new IllegalArgumentException(Objects.toString(date));
99      }
100 
101     /**
102      * Test Conversion Error
103      *
104      * @param converter The converter to use
105      * @param value     The value to convert
106      */
107     protected void invalidConversion(final Converter<T> converter, final Object value) {
108         final String valueType = value == null ? "null" : value.getClass().getName();
109         final String msg = "Converting '" + valueType + "' value '" + value + "'";
110         try {
111             final T result = converter.convert(getExpectedType(), value);
112             fail(msg + ", expected ConversionException, but result = '" + result + "'");
113         } catch (final ConversionException ex) {
114             // Expected Result
115         }
116     }
117 
118     /**
119      * Create the Converter with no default value.
120      *
121      * @return A new Converter
122      */
123     protected abstract DateTimeConverter<T> makeConverter();
124 
125     /**
126      * Create the Converter with a default value.
127      *
128      * @param defaultValue The default value
129      * @return A new Converter
130      */
131     protected abstract DateTimeConverter<T> makeConverter(T defaultValue);
132 
133     /**
134      * Test Conversion to String
135      *
136      * @param converter The converter to use
137      * @param expected  The expected result
138      * @param value     The value to convert
139      */
140     protected void stringConversion(final Converter<T> converter, final String expected, final Object value) {
141         final String valueType = value == null ? "null" : value.getClass().getName();
142         final String msg = "Converting '" + valueType + "' value '" + value + "' to String";
143         try {
144             final String result = converter.convert(String.class, value);
145             final Class<?> resultType = result == null ? null : result.getClass();
146             final Class<?> expectType = expected == null ? null : expected.getClass();
147             assertEquals(expectType, resultType, () -> "TYPE " + msg);
148             assertEquals(expected, result, () -> "VALUE " + msg);
149         } catch (final Exception ex) {
150             throw new IllegalStateException(msg + " threw " + ex.toString(), ex);
151         }
152     }
153 
154     /**
155      * Assumes convert() returns some non-null instance of getExpectedType().
156      */
157     @Test
158     public void testConvertDate() {
159         final String[] message = { "from Date", "from Calendar", "from SQL Date", "from SQL Time", "from SQL Timestamp", "from LocalDate", "from LocalDateTime",
160                 "from ZonedDateTime", "from OffsetDateTime" };
161 
162         final long nowMillis = System.currentTimeMillis();
163 
164         final Object[] date = { new Date(nowMillis), new java.util.GregorianCalendar(), new java.sql.Date(nowMillis), new java.sql.Time(nowMillis),
165                 new java.sql.Timestamp(nowMillis),
166                 Instant.ofEpochMilli(nowMillis).atZone(ZoneId.systemDefault()).toLocalDate().atStartOfDay(ZoneId.systemDefault()).toLocalDate(),
167                 Instant.ofEpochMilli(nowMillis).atZone(ZoneId.systemDefault()).toLocalDateTime(),
168                 ZonedDateTime.ofInstant(Instant.ofEpochMilli(nowMillis), ZoneId.systemDefault()),
169                 OffsetDateTime.ofInstant(Instant.ofEpochMilli(nowMillis), ZoneId.systemDefault()) };
170 
171         // Initialize calendar also with same ms to avoid a failing test in a new time slice
172         ((GregorianCalendar) date[1]).setTime(new Date(nowMillis));
173 
174         for (int i = 0; i < date.length; i++) {
175             final Class<T> expectedType = getExpectedType();
176             final Object val = makeConverter().convert(expectedType, date[i]);
177             assertNotNull(val, "Convert " + message[i] + " should not be null");
178             assertInstanceOf(expectedType, val, "Convert " + message[i] + " should return a " + expectedType.getName());
179 
180             long test = nowMillis;
181             if (date[i] instanceof LocalDate || val instanceof LocalDate) {
182                 test = Instant.ofEpochMilli(nowMillis).atZone(ZoneId.systemDefault()).toLocalDate().atStartOfDay(ZoneId.systemDefault()).toInstant()
183                         .toEpochMilli();
184             }
185 
186             assertEquals(test, getTimeInMillis(val), "Convert " + message[i] + " should return a " + date[0]);
187         }
188     }
189 
190     /**
191      * Assumes ConversionException in response to covert(getExpectedType(), null).
192      */
193     @Test
194     public void testConvertNull() {
195         assertThrows(ConversionException.class,
196                      () -> makeConverter().convert(getExpectedType(), null),
197                      "Expected ConversionException");
198     }
199 
200     /**
201      * Test default String to type conversion
202      *
203      * This method is overridden by test case implementations for java.sql.Date/Time/Timestamp
204      */
205     @Test
206     public void testDefaultStringToTypeConvert() {
207 
208         // Create & Configure the Converter
209         final DateTimeConverter<T> converter = makeConverter();
210         converter.setUseLocaleFormat(false);
211         assertThrows(ConversionException.class,
212                      () -> converter.convert(getExpectedType(), "2006-10-23"),
213                      "Expected Conversion exception");
214 
215     }
216 
217     /**
218      * Test Default Type conversion (i.e. don't specify target type)
219      */
220     @Test
221     public void testDefaultType() {
222         final String pattern = "yyyy-MM-dd";
223 
224         // Create & Configure the Converter
225         final DateTimeConverter<T> converter = makeConverter();
226         converter.setPattern(pattern);
227 
228         // Valid String --> Type Conversion
229         final String testString = "2006-10-29";
230         final Calendar calendar = toCalendar(testString, pattern, null);
231         final Object expected = toType(calendar);
232 
233         final Object result = converter.convert(null, testString);
234         final Class<T> expectedType = getExpectedType();
235         if (expectedType.equals(Calendar.class)) {
236             assertTrue(expectedType.isAssignableFrom(result.getClass()), "TYPE ");
237         } else {
238             assertInstanceOf(expectedType, result, "TYPE ");
239         }
240         assertEquals(expected, result, "VALUE ");
241     }
242 
243     /**
244      * Test Converter with types it can't handle
245      */
246     @Test
247     public void testInvalidType() {
248 
249         // Create & Configure the Converter
250         @SuppressWarnings("unchecked") // we are creating a mismatch to assert a failure
251         final DateTimeConverter<Character> converter = (DateTimeConverter<Character>) makeConverter();
252 
253         // Invalid Class Type
254         assertThrows(ConversionException.class,
255                      ()-> converter.convert(Character.class, new Date()),
256                      "Requested Character.class conversion, expected ConversionException");
257     }
258 
259     /**
260      * Test Date Converter with no default value
261      */
262     @Test
263     public void testLocale() {
264 
265         // Re-set the default Locale to Locale.US
266         final Locale defaultLocale = Locale.getDefault();
267         Locale.setDefault(Locale.US);
268 
269         final String pattern = "M/d/yy"; // SHORT style date format for US Locale
270 
271         // Create & Configure the Converter
272         final DateTimeConverter<T> converter = makeConverter();
273         converter.setUseLocaleFormat(true);
274 
275         // Valid String --> Type Conversion
276         final String testString = "10/28/06";
277         final Object expected = toType(testString, pattern, null);
278         validConversion(converter, expected, testString);
279 
280         // Invalid Conversions
281         invalidConversion(converter, null);
282         invalidConversion(converter, "");
283         invalidConversion(converter, "2006-10-2X");
284         invalidConversion(converter, "10.28.06");
285         invalidConversion(converter, "10-28-06");
286         invalidConversion(converter, Integer.valueOf(2));
287 
288         // Restore the default Locale
289         Locale.setDefault(defaultLocale);
290 
291     }
292 
293     /**
294      * Test Converter with multiple patterns
295      */
296     @Test
297     public void testMultiplePatterns() {
298         String testString;
299         Object expected;
300 
301         // Create & Configure the Converter
302         final String[] patterns = { "yyyy-MM-dd", "yyyy/MM/dd" };
303         final DateTimeConverter<T> converter = makeConverter();
304         converter.setPatterns(patterns);
305 
306         // First Pattern
307         testString = "2006-10-28";
308         expected = toType(testString, patterns[0], null);
309         validConversion(converter, expected, testString);
310 
311         // Second pattern
312         testString = "2006/10/18";
313         expected = toType(testString, patterns[1], null);
314         validConversion(converter, expected, testString);
315 
316         // Invalid Conversion
317         invalidConversion(converter, "17/03/2006");
318         invalidConversion(converter, "17.03.2006");
319 
320     }
321 
322     /**
323      * Test Converter with no default value
324      */
325     @Test
326     public void testPatternDefault() {
327 
328         final String pattern = "yyyy-MM-dd";
329 
330         // Create & Configure the Converter
331         final T defaultValue = toType("2000-01-01", pattern, null);
332         assertNotNull(defaultValue, "Check default date");
333         final DateTimeConverter<T> converter = makeConverter(defaultValue);
334         converter.setPattern(pattern);
335 
336         // Valid String --> Type Conversion
337         final String testString = "2006-10-29";
338         final Object expected = toType(testString, pattern, null);
339         validConversion(converter, expected, testString);
340 
341         // Invalid Values, expect default value
342         validConversion(converter, defaultValue, null);
343         validConversion(converter, defaultValue, "");
344         validConversion(converter, defaultValue, "2006-10-2X");
345         validConversion(converter, defaultValue, "2006/10/01");
346         validConversion(converter, defaultValue, "02/10/06");
347         validConversion(converter, defaultValue, Integer.valueOf(2));
348 
349     }
350 
351     /**
352      * Test Converter with no default value
353      */
354     @Test
355     public void testPatternNoDefault() {
356 
357         final String pattern = "yyyy-MM-dd";
358 
359         // Create & Configure the Converter
360         final DateTimeConverter<T> converter = makeConverter();
361         converter.setPattern(pattern);
362 
363         // Valid String --> Type Conversion
364         final String testString = "2006-10-29";
365         final Calendar calendar = toCalendar(testString, pattern, null);
366         final Object expected = toType(calendar);
367         validConversion(converter, expected, testString);
368 
369         // Valid java.util.Date --> Type Conversion
370         validConversion(converter, expected, calendar);
371 
372         // Valid Calendar --> Type Conversion
373         validConversion(converter, expected, toDate(calendar));
374 
375         // Test java.sql.Date --> Type Conversion
376         validConversion(converter, expected, toSqlDate(calendar));
377 
378         // java.sql.Timestamp --> String Conversion
379         validConversion(converter, expected, toSqlTimestamp(calendar));
380 
381         // java.sql.Time --> String Conversion
382         validConversion(converter, expected, toSqlTime(calendar));
383 
384         // Invalid Conversions
385         invalidConversion(converter, null);
386         invalidConversion(converter, "");
387         invalidConversion(converter, "2006-10-2X");
388         invalidConversion(converter, "2006/10/01");
389         invalidConversion(converter, "02/10/2006");
390         invalidConversion(converter, "02/10/06");
391         invalidConversion(converter, Integer.valueOf(2));
392 
393     }
394 
395     /**
396      * Test Converter with no default value
397      */
398     @Test
399     public void testPatternNullDefault() {
400 
401         final String pattern = "yyyy-MM-dd";
402 
403         // Create & Configure the Converter
404         final T defaultValue = null;
405         final DateTimeConverter<T> converter = makeConverter(defaultValue);
406         converter.setPattern(pattern);
407 
408         // Valid String --> Type Conversion
409         final String testString = "2006-10-29";
410         final Object expected = toType(testString, pattern, null);
411         validConversion(converter, expected, testString);
412 
413         // Invalid Values, expect default --> null
414         validConversion(converter, defaultValue, null);
415         validConversion(converter, defaultValue, "");
416         validConversion(converter, defaultValue, "2006-10-2X");
417         validConversion(converter, defaultValue, "2006/10/01");
418         validConversion(converter, defaultValue, "02/10/06");
419         validConversion(converter, defaultValue, Integer.valueOf(2));
420 
421     }
422 
423     /**
424      * Test Conversion to String
425      */
426     @Test
427     public void testStringConversion() {
428 
429         final String pattern = "yyyy-MM-dd";
430 
431         // Create & Configure the Converter
432         final DateTimeConverter<T> converter = makeConverter();
433         converter.setPattern(pattern);
434 
435         // Create Values
436         final String expected = "2006-10-29";
437         final Calendar calendar = toCalendar(expected, pattern, null);
438 
439         // Type --> String Conversion
440         stringConversion(converter, expected, toType(calendar));
441 
442         // Calendar --> String Conversion
443         stringConversion(converter, expected, calendar);
444 
445         // java.util.Date --> String Conversion
446         stringConversion(converter, expected, toDate(calendar));
447 
448         // java.sql.Date --> String Conversion
449         stringConversion(converter, expected, toSqlDate(calendar));
450 
451         // java.sql.Timestamp --> String Conversion
452         stringConversion(converter, expected, toSqlTimestamp(calendar));
453 
454         // java.sql.Time --> String Conversion
455         stringConversion(converter, expected, toSqlTime(calendar));
456 
457         // java.time.LocalDateTime --> String Conversion
458         stringConversion(converter, expected, toLocalDateTime(calendar));
459 
460         stringConversion(converter, null, null);
461         stringConversion(converter, "", "");
462 
463     }
464 
465     /**
466      * Parse a String value to a Calendar
467      *
468      * @param value   The String value to parse
469      * @param pattern The date pattern
470      * @param locale  The locale to use (or null)
471      * @return parsed Calendar value
472      */
473     Calendar toCalendar(final String value, final String pattern, final Locale locale) {
474         Calendar calendar = null;
475         try {
476             final DateFormat format = locale == null ? new SimpleDateFormat(pattern) : new SimpleDateFormat(pattern, locale);
477             format.setLenient(false);
478             format.parse(value);
479             calendar = format.getCalendar();
480         } catch (final Exception e) {
481             fail("Error creating Calendar value ='" + value + ", pattern='" + pattern + "' " + e.toString());
482         }
483         return calendar;
484     }
485 
486     /**
487      * Convert a Calendar to a java.util.Date
488      *
489      * @param calendar The calendar object to convert
490      * @return The converted java.util.Date
491      */
492     Date toDate(final Calendar calendar) {
493         return calendar.getTime();
494     }
495 
496     /**
497      * Convert a Calendar to a java.time.LocalDateTime
498      *
499      * @param calendar The calendar object to convert
500      * @return The converted java.time.LocalDate
501      */
502     LocalDateTime toLocalDateTime(final Calendar calendar) {
503         return Instant.ofEpochMilli(calendar.getTimeInMillis()).atZone(ZoneId.systemDefault()).toLocalDateTime();
504     }
505 
506     /**
507      * Convert a Calendar to a java.sql.Date
508      *
509      * @param calendar The calendar object to convert
510      * @return The converted java.sql.Date
511      */
512     java.sql.Date toSqlDate(final Calendar calendar) {
513         return new java.sql.Date(getTimeInMillis(calendar));
514     }
515 
516     /**
517      * Convert a Calendar to a java.sql.Time
518      *
519      * @param calendar The calendar object to convert
520      * @return The converted java.sql.Time
521      */
522     java.sql.Time toSqlTime(final Calendar calendar) {
523         return new java.sql.Time(getTimeInMillis(calendar));
524     }
525 
526     /**
527      * Convert a Calendar to a java.sql.Timestamp
528      *
529      * @param calendar The calendar object to convert
530      * @return The converted java.sql.Timestamp
531      */
532     java.sql.Timestamp toSqlTimestamp(final Calendar calendar) {
533         return new java.sql.Timestamp(getTimeInMillis(calendar));
534     }
535 
536     /**
537      * Convert from a Calendar to the appropriate Date type
538      *
539      * @param value The Calendar value to convert
540      * @return The converted value
541      */
542     protected abstract T toType(Calendar value);
543 
544     /**
545      * Parse a String value to the required type
546      *
547      * @param value   The String value to parse
548      * @param pattern The date pattern
549      * @param locale  The locale to use (or null)
550      * @return parsed Calendar value
551      */
552     protected T toType(final String value, final String pattern, final Locale locale) {
553         return toType(toCalendar(value, pattern, locale));
554     }
555 
556     /**
557      * Test Conversion to the required type
558      *
559      * @param converter The converter to use
560      * @param expected  The expected result
561      * @param value     The value to convert
562      */
563     protected void validConversion(final Converter<T> converter, final Object expected, final Object value) {
564         final String valueType = value == null ? "null" : value.getClass().getName();
565         final String msg = "Converting '" + valueType + "' value '" + value + "'";
566         final Object result = assertDoesNotThrow(() -> converter.convert(getExpectedType(), value));
567         final Class<?> resultType = result == null ? null : result.getClass();
568         final Class<?> expectType = expected == null ? null : expected.getClass();
569         assertEquals(expectType, resultType, () -> "TYPE " + msg);
570         assertEquals(expected, result, () -> "VALUE " + msg);
571     }
572 }