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 *      https://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 */
017package org.apache.commons.validator.routines;
018
019import java.text.DateFormat;
020import java.text.DateFormatSymbols;
021import java.text.Format;
022import java.text.SimpleDateFormat;
023import java.util.Calendar;
024import java.util.Locale;
025import java.util.TimeZone;
026import java.util.concurrent.TimeUnit;
027
028import org.apache.commons.validator.GenericValidator;
029
030/**
031 * Abstract class for Date/Time/Calendar validation.
032 *
033 * <p>This is a <em>base</em> class for building Date / Time
034 *    Validators using format parsing.</p>
035 *
036 * @since 1.3.0
037 */
038public abstract class AbstractCalendarValidator extends AbstractFormatValidator {
039
040    private static final long serialVersionUID = -1410008585975827379L;
041
042    /** Number of milliseconds in a week, beyond which two instants cannot share a week. */
043    private static final long MILLIS_PER_WEEK = TimeUnit.DAYS.toMillis(7);
044
045    /**
046     * The date style to use for Locale validation.
047     */
048    private final int dateStyle;
049
050    /**
051     * The time style to use for Locale validation.
052     */
053    private final int timeStyle;
054
055    /**
056     * Constructs an instance with the specified <em>strict</em>,
057     * <em>time</em> and <em>date</em> style parameters.
058     *
059     * @param strict {@code true} if strict
060     *        {@code Format} parsing should be used.
061     * @param dateStyle The date style to use for Locale validation.
062     * @param timeStyle The time style to use for Locale validation.
063     */
064    public AbstractCalendarValidator(final boolean strict, final int dateStyle, final int timeStyle) {
065        super(strict);
066        this.dateStyle = dateStyle;
067        this.timeStyle = timeStyle;
068    }
069
070    /**
071     * Compares the field from two calendars indicating whether the field for the
072     *    first calendar is equal to, less than or greater than the field from the
073     *    second calendar.
074     *
075     * @param value The Calendar value.
076     * @param compare The {@link Calendar} to check the value against.
077     * @param field The field to compare for the calendars.
078     * @return Zero if the first calendar's field is equal to the seconds, -1
079     *         if it is less than the seconds or +1 if it is greater than the seconds.
080     */
081    private int calculateCompareResult(final Calendar value, final Calendar compare, final int field) {
082        return Integer.compare(value.get(field), compare.get(field));
083    }
084
085    /**
086     * Calculate the quarter for the specified Calendar.
087     *
088     * @param calendar The Calendar value.
089     * @param monthOfFirstQuarter The  month that the first quarter starts.
090     * @return The calculated quarter.
091     */
092    private int calculateQuarter(final Calendar calendar, final int monthOfFirstQuarter) {
093        // Add Year
094        int year = calendar.get(Calendar.YEAR);
095
096        final int month = calendar.get(Calendar.MONTH) + 1;
097        final int relativeMonth = month >= monthOfFirstQuarter
098                          ? month - monthOfFirstQuarter
099                          : 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}