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.validator.routines;
18  
19  import java.text.DateFormat;
20  import java.text.DateFormatSymbols;
21  import java.text.Format;
22  import java.text.SimpleDateFormat;
23  import java.util.Calendar;
24  import java.util.Locale;
25  import java.util.TimeZone;
26  import java.util.concurrent.TimeUnit;
27  
28  import org.apache.commons.validator.GenericValidator;
29  
30  /**
31   * Abstract class for Date/Time/Calendar validation.
32   *
33   * <p>This is a <em>base</em> class for building Date / Time
34   *    Validators using format parsing.</p>
35   *
36   * @since 1.3.0
37   */
38  public abstract class AbstractCalendarValidator extends AbstractFormatValidator {
39  
40      private static final long serialVersionUID = -1410008585975827379L;
41  
42      /** Number of milliseconds in a week, beyond which two instants cannot share a week. */
43      private static final long MILLIS_PER_WEEK = TimeUnit.DAYS.toMillis(7);
44  
45      /**
46       * The date style to use for Locale validation.
47       */
48      private final int dateStyle;
49  
50      /**
51       * The time style to use for Locale validation.
52       */
53      private final int timeStyle;
54  
55      /**
56       * Constructs an instance with the specified <em>strict</em>,
57       * <em>time</em> and <em>date</em> style parameters.
58       *
59       * @param strict {@code true} if strict
60       *        {@code Format} parsing should be used.
61       * @param dateStyle The date style to use for Locale validation.
62       * @param timeStyle The time style to use for Locale validation.
63       */
64      public AbstractCalendarValidator(final boolean strict, final int dateStyle, final int timeStyle) {
65          super(strict);
66          this.dateStyle = dateStyle;
67          this.timeStyle = timeStyle;
68      }
69  
70      /**
71       * Compares the field from two calendars indicating whether the field for the
72       *    first calendar is equal to, less than or greater than the field from the
73       *    second calendar.
74       *
75       * @param value The Calendar value.
76       * @param compare The {@link Calendar} to check the value against.
77       * @param field The field to compare for the calendars.
78       * @return Zero if the first calendar's field is equal to the seconds, -1
79       *         if it is less than the seconds or +1 if it is greater than the seconds.
80       */
81      private int calculateCompareResult(final Calendar value, final Calendar compare, final int field) {
82          return Integer.compare(value.get(field), compare.get(field));
83      }
84  
85      /**
86       * Calculate the quarter for the specified Calendar.
87       *
88       * @param calendar The Calendar value.
89       * @param monthOfFirstQuarter The  month that the first quarter starts.
90       * @return The calculated quarter.
91       */
92      private int calculateQuarter(final Calendar calendar, final int monthOfFirstQuarter) {
93          // Add Year
94          int year = calendar.get(Calendar.YEAR);
95  
96          final int month = calendar.get(Calendar.MONTH) + 1;
97          final int relativeMonth = month >= monthOfFirstQuarter
98                            ? month - monthOfFirstQuarter
99                            : month + 12 - monthOfFirstQuarter; // CHECKSTYLE IGNORE MagicNumber
100         final int quarter = relativeMonth / 3 + 1; // CHECKSTYLE IGNORE MagicNumber
101         // adjust the year if the quarter doesn't start in January
102         if (month < monthOfFirstQuarter) {
103             --year;
104         }
105         return year * 10 + quarter; // CHECKSTYLE IGNORE MagicNumber
106     }
107 
108     /**
109      * Compares a calendar value to another, indicating whether it is
110      *    equal, less than or more than at a specified level.
111      *
112      * @param value The Calendar value.
113      * @param compare The {@link Calendar} to check the value against.
114      * @param field The field <em>level</em> to compare to - for example, specifying
115      *        {@code Calendar.MONTH} will compare the year and month
116      *        portions of the calendar.
117      * @return Zero if the first value is equal to the second, -1
118      *         if it is less than the second or +1 if it is greater than the second.
119      */
120     protected int compare(final Calendar value, final Calendar compare, final int field) {
121 
122         int result;
123 
124         // Week of Year and Week of Month numbers repeat across the boundaries they reset on, and a
125         // week can belong to a different calendar year or month than its number suggests (for
126         // example 31 December may fall in week 1 of the following year), so the week is compared by
127         // day distance and week number rather than by comparing the calendar year first.
128         if (field == Calendar.WEEK_OF_YEAR || field == Calendar.WEEK_OF_MONTH) {
129             return compareWeek(value, compare, field);
130         }
131 
132         // Compare Year
133         result = calculateCompareResult(value, compare, Calendar.YEAR);
134         if (result != 0 || field == Calendar.YEAR) {
135             return result;
136         }
137 
138         // Compare Day of the Year
139         if (field == Calendar.DAY_OF_YEAR) {
140             return calculateCompareResult(value, compare, Calendar.DAY_OF_YEAR);
141         }
142 
143         // Compare Month
144         result = calculateCompareResult(value, compare, Calendar.MONTH);
145         if (result != 0 || field == Calendar.MONTH) {
146             return result;
147         }
148 
149         // Compare Date
150         result = calculateCompareResult(value, compare, Calendar.DATE);
151         if (result != 0 || field == Calendar.DATE ||
152                           field == Calendar.DAY_OF_WEEK ||
153                           field == Calendar.DAY_OF_WEEK_IN_MONTH) {
154             return result;
155         }
156 
157         // Compare Time fields
158         return compareTime(value, compare, field);
159 
160     }
161 
162     /**
163      * Compares a calendar's quarter value to another, indicating whether it is
164      *    equal, less than or more than the specified quarter.
165      *
166      * @param value The Calendar value.
167      * @param compare The {@link Calendar} to check the value against.
168      * @param monthOfFirstQuarter The  month that the first quarter starts.
169      * @return Zero if the first quarter is equal to the second, -1
170      *         if it is less than the second or +1 if it is greater than the second.
171      */
172     protected int compareQuarters(final Calendar value, final Calendar compare, final int monthOfFirstQuarter) {
173         final int valueQuarter = calculateQuarter(value, monthOfFirstQuarter);
174         final int compareQuarter = calculateQuarter(compare, monthOfFirstQuarter);
175         return Integer.compare(valueQuarter, compareQuarter);
176     }
177 
178     /**
179      * Compares a calendar time value to another, indicating whether it is
180      *    equal, less than or more than at a specified level.
181      *
182      * @param value The Calendar value.
183      * @param compare The {@link Calendar} to check the value against.
184      * @param field The field <em>level</em> to compare to - for example, specifying
185      *        {@code Calendar.MINUTE} will compare the hours and minutes
186      *        portions of the calendar.
187      * @return Zero if the first value is equal to the second, -1
188      *         if it is less than the second or +1 if it is greater than the second.
189      */
190     protected int compareTime(final Calendar value, final Calendar compare, final int field) {
191 
192         int result;
193 
194         // Compare Hour
195         result = calculateCompareResult(value, compare, Calendar.HOUR_OF_DAY);
196         if (result != 0 || field == Calendar.HOUR || field == Calendar.HOUR_OF_DAY) {
197             return result;
198         }
199 
200         // Compare Minute
201         result = calculateCompareResult(value, compare, Calendar.MINUTE);
202         if (result != 0 || field == Calendar.MINUTE) {
203             return result;
204         }
205 
206         // Compare Second
207         result = calculateCompareResult(value, compare, Calendar.SECOND);
208         if (result != 0 || field == Calendar.SECOND) {
209             return result;
210         }
211 
212         // Compare Milliseconds
213         if (field == Calendar.MILLISECOND) {
214             return calculateCompareResult(value, compare, Calendar.MILLISECOND);
215         }
216 
217         throw new IllegalArgumentException("Invalid field: " + field);
218 
219     }
220 
221     /**
222      * Compares the week two calendars fall in, ordering by the actual week rather than by the
223      * {@code WEEK_OF_YEAR} or {@code WEEK_OF_MONTH} number alone. Those numbers repeat across the
224      * boundaries they reset on (for example 31 December may be week 1 of the following year, and
225      * the first week of a month can hold days carried over from the previous month), so the gap
226      * between the two instants is checked first: dates a week or more apart are always in different
227      * weeks, and nearer dates share a week only when the week number also matches.
228      *
229      * @param value The Calendar value.
230      * @param compare The {@link Calendar} to check the value against.
231      * @param field {@code Calendar.WEEK_OF_YEAR} or {@code Calendar.WEEK_OF_MONTH}.
232      * @return Zero if both calendars are in the same week, -1 or +1 otherwise.
233      */
234     private int compareWeek(final Calendar value, final Calendar compare, final int field) {
235         final long millis = value.getTimeInMillis() - compare.getTimeInMillis();
236         if (Math.abs(millis) >= MILLIS_PER_WEEK || calculateCompareResult(value, compare, field) != 0) {
237             return Long.signum(millis);
238         }
239         return 0;
240     }
241 
242     /**
243      * Format a value with the specified {@code DateFormat}.
244      *
245      * @param value The value to be formatted.
246      * @param formatter The Format to use.
247      * @return The formatted value.
248      */
249     @Override
250     protected String format(Object value, final Format formatter) {
251         if (value == null) {
252             return null;
253         }
254         if (value instanceof Calendar) {
255             value = ((Calendar) value).getTime();
256         }
257         return formatter.format(value);
258     }
259 
260     /**
261      * Format an object into a {@link String} using
262      * the specified Locale.
263      *
264      * @param value The value validation is being performed on.
265      * @param locale The locale to use for the Format.
266      * @param timeZone The Time Zone used to format the date,
267      *  system default if null unless value is a {@link Calendar}.
268      * @return The value formatted as a {@link String}.
269      */
270     public String format(final Object value, final Locale locale, final TimeZone timeZone) {
271         return format(value, (String) null, locale, timeZone);
272     }
273 
274     /**
275      * Format an object using the specified pattern and/or
276      *    {@link Locale}.
277      *
278      * @param value The value validation is being performed on.
279      * @param pattern The pattern used to format the value.
280      * @param locale The locale to use for the Format.
281      * @return The value formatted as a {@link String}.
282      */
283     @Override
284     public String format(final Object value, final String pattern, final Locale locale) {
285         return format(value, pattern, locale, (TimeZone) null);
286     }
287 
288     /**
289      * Format an object using the specified pattern and/or
290      *    {@link Locale}.
291      *
292      * @param value The value validation is being performed on.
293      * @param pattern The pattern used to format the value.
294      * @param locale The locale to use for the Format.
295      * @param timeZone The Time Zone used to format the date,
296      *  system default if null unless value is a {@link Calendar}.
297      * @return The value formatted as a {@link String}.
298      */
299     public String format(final Object value, final String pattern, final Locale locale, final TimeZone timeZone) {
300         final DateFormat formatter = (DateFormat) getFormat(pattern, locale);
301         if (timeZone != null) {
302             formatter.setTimeZone(timeZone);
303         } else if (value instanceof Calendar) {
304             formatter.setTimeZone(((Calendar) value).getTimeZone());
305         }
306         return format(value, formatter);
307     }
308 
309     /**
310      * Format an object into a {@link String} using
311      * the specified pattern.
312      *
313      * @param value The value validation is being performed on.
314      * @param pattern The pattern used to format the value.
315      * @param timeZone The Time Zone used to format the date,
316      *  system default if null unless value is a {@link Calendar}.
317      * @return The value formatted as a {@link String}.
318      */
319     public String format(final Object value, final String pattern, final TimeZone timeZone) {
320         return format(value, pattern, (Locale) null, timeZone);
321     }
322 
323     /**
324      * Format an object into a {@link String} using
325      * the default Locale.
326      *
327      * @param value The value validation is being performed on.
328      * @param timeZone The Time Zone used to format the date,
329      *  system default if null unless value is a {@link Calendar}.
330      * @return The value formatted as a {@link String}.
331      */
332     public String format(final Object value, final TimeZone timeZone) {
333         return format(value, (String) null, (Locale) null, timeZone);
334     }
335 
336     /**
337      * Returns a {@code DateFormat} for the specified Locale.
338      *
339      * @param locale The locale a {@code DateFormat} is required for,
340      *        system default if null.
341      * @return The {@code DateFormat} to created.
342      */
343     protected Format getFormat(final Locale locale) {
344         final DateFormat formatter;
345         if (dateStyle >= 0 && timeStyle >= 0) {
346             if (locale == null) {
347                 formatter = DateFormat.getDateTimeInstance(dateStyle, timeStyle);
348             } else {
349                 formatter = DateFormat.getDateTimeInstance(dateStyle, timeStyle, locale);
350             }
351         } else if (timeStyle >= 0) {
352             if (locale == null) {
353                 formatter = DateFormat.getTimeInstance(timeStyle);
354             } else {
355                 formatter = DateFormat.getTimeInstance(timeStyle, locale);
356             }
357         } else {
358             final int useDateStyle = dateStyle >= 0 ? dateStyle : DateFormat.SHORT;
359             if (locale == null) {
360                 formatter = DateFormat.getDateInstance(useDateStyle);
361             } else {
362                 formatter = DateFormat.getDateInstance(useDateStyle, locale);
363             }
364         }
365         formatter.setLenient(false);
366         return formatter;
367     }
368 
369     /**
370      * Returns a {@code DateFormat} for the specified <em>pattern</em>
371      *    and/or {@link Locale}.
372      *
373      * @param pattern The pattern used to validate the value against or
374      *        {@code null} to use the default for the {@link Locale}.
375      * @param locale The locale to use for the currency format, system default if null.
376      * @return The {@code DateFormat} to created.
377      */
378     @Override
379     protected Format getFormat(final String pattern, final Locale locale) {
380         final DateFormat formatter;
381         final boolean usePattern = !GenericValidator.isBlankOrNull(pattern);
382         if (!usePattern) {
383             formatter = (DateFormat) getFormat(locale);
384         } else if (locale == null) {
385             formatter = new SimpleDateFormat(pattern);
386         } else {
387             final DateFormatSymbols symbols = new DateFormatSymbols(locale);
388             formatter = new SimpleDateFormat(pattern, symbols);
389         }
390         formatter.setLenient(false);
391         return formatter;
392     }
393 
394     /**
395      * Validate using the specified {@link Locale}.
396      *
397      * @param value The value validation is being performed on.
398      * @param pattern The pattern used to format the value.
399      * @param locale The locale to use for the Format, defaults to the default
400      * @return {@code true} if the value is valid.
401      */
402     @Override
403     public boolean isValid(final String value, final String pattern, final Locale locale) {
404         return parse(value, pattern, locale, (TimeZone) null) != null;
405     }
406 
407     /**
408      * Checks if the value is valid against a specified pattern.
409      *
410      * @param value The value validation is being performed on.
411      * @param pattern The pattern used to validate the value against, or the
412      *        default for the {@link Locale} if {@code null}.
413      * @param locale The locale to use for the date format, system default if null.
414      * @param timeZone The Time Zone used to parse the date, system default if null.
415      * @return The parsed value if valid or {@code null} if invalid.
416      */
417     protected Object parse(String value, final String pattern, final Locale locale, final TimeZone timeZone) {
418         value = value == null ? null : value.trim();
419         final String value1 = value;
420         if (GenericValidator.isBlankOrNull(value1)) {
421             return null;
422         }
423         final DateFormat formatter = (DateFormat) getFormat(pattern, locale);
424         if (timeZone != null) {
425             formatter.setTimeZone(timeZone);
426         }
427         return parse(value, formatter);
428 
429     }
430 
431     /**
432      * rocess the parsed value, performing any further validation
433      *    and type conversion required.
434      *
435      * @param value The parsed object created.
436      * @param formatter The Format used to parse the value with.
437      * @return The parsed value converted to the appropriate type
438      *         if valid or {@code null} if invalid.
439      */
440     @Override
441     protected abstract Object processParsedValue(Object value, Format formatter);
442 }