    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}