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;
026
027import org.apache.commons.validator.GenericValidator;
028
029/**
030 * <p>Abstract class for Date/Time/Calendar validation.</p>
031 *
032 * <p>This is a <em>base</em> class for building Date / Time
033 *    Validators using format parsing.</p>
034 *
035 * @since 1.3.0
036 */
037public abstract class AbstractCalendarValidator extends AbstractFormatValidator {
038
039    private static final long serialVersionUID = -1410008585975827379L;
040
041    /**
042     * The date style to use for Locale validation.
043     */
044    private final int dateStyle;
045
046    /**
047     * The time style to use for Locale validation.
048     */
049    private final int timeStyle;
050
051    /**
052     * Constructs an instance with the specified <em>strict</em>,
053     * <em>time</em> and <em>date</em> style parameters.
054     *
055     * @param strict {@code true} if strict
056     *        {@code Format} parsing should be used.
057     * @param dateStyle the date style to use for Locale validation.
058     * @param timeStyle the time style to use for Locale validation.
059     */
060    public AbstractCalendarValidator(final boolean strict, final int dateStyle, final int timeStyle) {
061        super(strict);
062        this.dateStyle = dateStyle;
063        this.timeStyle = timeStyle;
064    }
065
066    /**
067     * <p>Compares the field from two calendars indicating whether the field for the
068     *    first calendar is equal to, less than or greater than the field from the
069     *    second calendar.
070     *
071     * @param value The Calendar value.
072     * @param compare The {@link Calendar} to check the value against.
073     * @param field The field to compare for the calendars.
074     * @return Zero if the first calendar's field is equal to the seconds, -1
075     *         if it is less than the seconds or +1 if it is greater than the seconds.
076     */
077    private int calculateCompareResult(final Calendar value, final Calendar compare, final int field) {
078        return Integer.compare(value.get(field), compare.get(field));
079    }
080
081    /**
082     * <p>Calculate the quarter for the specified Calendar.</p>
083     *
084     * @param calendar The Calendar value.
085     * @param monthOfFirstQuarter The  month that the first quarter starts.
086     * @return The calculated quarter.
087     */
088    private int calculateQuarter(final Calendar calendar, final int monthOfFirstQuarter) {
089        // Add Year
090        int year = calendar.get(Calendar.YEAR);
091
092        final int month = calendar.get(Calendar.MONTH) + 1;
093        final int relativeMonth = month >= monthOfFirstQuarter
094                          ? month - monthOfFirstQuarter
095                          : month + 12 - monthOfFirstQuarter; // CHECKSTYLE IGNORE MagicNumber
096        final int quarter = relativeMonth / 3 + 1; // CHECKSTYLE IGNORE MagicNumber
097        // adjust the year if the quarter doesn't start in January
098        if (month < monthOfFirstQuarter) {
099            --year;
100        }
101        return year * 10 + quarter; // CHECKSTYLE IGNORE MagicNumber
102    }
103
104    /**
105     * <p>Compares a calendar value to another, indicating whether it is
106     *    equal, less than or more than at a specified level.</p>
107     *
108     * @param value The Calendar value.
109     * @param compare The {@link Calendar} to check the value against.
110     * @param field The field <em>level</em> to compare to - for example, specifying
111     *        {@code Calendar.MONTH} will compare the year and month
112     *        portions of the calendar.
113     * @return Zero if the first value is equal to the second, -1
114     *         if it is less than the second or +1 if it is greater than the second.
115     */
116    protected int compare(final Calendar value, final Calendar compare, final int field) {
117
118        int result;
119
120        // Compare Year
121        result = calculateCompareResult(value, compare, Calendar.YEAR);
122        if (result != 0 || field == Calendar.YEAR) {
123            return result;
124        }
125
126        // Compare Week of Year
127        if (field == Calendar.WEEK_OF_YEAR) {
128            return calculateCompareResult(value, compare, Calendar.WEEK_OF_YEAR);
129        }
130
131        // Compare Day of the Year
132        if (field == Calendar.DAY_OF_YEAR) {
133            return calculateCompareResult(value, compare, Calendar.DAY_OF_YEAR);
134        }
135
136        // Compare Month
137        result = calculateCompareResult(value, compare, Calendar.MONTH);
138        if (result != 0 || field == Calendar.MONTH) {
139            return result;
140        }
141
142        // Compare Week of Month
143        if (field == Calendar.WEEK_OF_MONTH) {
144            return calculateCompareResult(value, compare, Calendar.WEEK_OF_MONTH);
145        }
146
147        // Compare Date
148        result = calculateCompareResult(value, compare, Calendar.DATE);
149        if (result != 0 || field == Calendar.DATE ||
150                          field == Calendar.DAY_OF_WEEK ||
151                          field == Calendar.DAY_OF_WEEK_IN_MONTH) {
152            return result;
153        }
154
155        // Compare Time fields
156        return compareTime(value, compare, field);
157
158    }
159
160    /**
161     * <p>Compares a calendar's quarter value to another, indicating whether it is
162     *    equal, less than or more than the specified quarter.</p>
163     *
164     * @param value The Calendar value.
165     * @param compare The {@link Calendar} to check the value against.
166     * @param monthOfFirstQuarter The  month that the first quarter starts.
167     * @return Zero if the first quarter is equal to the second, -1
168     *         if it is less than the second or +1 if it is greater than the second.
169     */
170    protected int compareQuarters(final Calendar value, final Calendar compare, final int monthOfFirstQuarter) {
171        final int valueQuarter = calculateQuarter(value, monthOfFirstQuarter);
172        final int compareQuarter = calculateQuarter(compare, monthOfFirstQuarter);
173        return Integer.compare(valueQuarter, compareQuarter);
174    }
175
176    /**
177     * <p>Compares a calendar time value to another, indicating whether it is
178     *    equal, less than or more than at a specified level.</p>
179     *
180     * @param value The Calendar value.
181     * @param compare The {@link Calendar} to check the value against.
182     * @param field The field <em>level</em> to compare to - for example, specifying
183     *        {@code Calendar.MINUTE} will compare the hours and minutes
184     *        portions of the calendar.
185     * @return Zero if the first value is equal to the second, -1
186     *         if it is less than the second or +1 if it is greater than the second.
187     */
188    protected int compareTime(final Calendar value, final Calendar compare, final int field) {
189
190        int result;
191
192        // Compare Hour
193        result = calculateCompareResult(value, compare, Calendar.HOUR_OF_DAY);
194        if (result != 0 || field == Calendar.HOUR || field == Calendar.HOUR_OF_DAY) {
195            return result;
196        }
197
198        // Compare Minute
199        result = calculateCompareResult(value, compare, Calendar.MINUTE);
200        if (result != 0 || field == Calendar.MINUTE) {
201            return result;
202        }
203
204        // Compare Second
205        result = calculateCompareResult(value, compare, Calendar.SECOND);
206        if (result != 0 || field == Calendar.SECOND) {
207            return result;
208        }
209
210        // Compare Milliseconds
211        if (field == Calendar.MILLISECOND) {
212            return calculateCompareResult(value, compare, Calendar.MILLISECOND);
213        }
214
215        throw new IllegalArgumentException("Invalid field: " + field);
216
217    }
218
219    /**
220     * <p>Format a value with the specified {@code DateFormat}.</p>
221     *
222     * @param value The value to be formatted.
223     * @param formatter The Format to use.
224     * @return The formatted value.
225     */
226    @Override
227    protected String format(Object value, final Format formatter) {
228        if (value == null) {
229            return null;
230        }
231        if (value instanceof Calendar) {
232            value = ((Calendar) value).getTime();
233        }
234        return formatter.format(value);
235    }
236
237    /**
238     * <p>Format an object into a {@link String} using
239     * the specified Locale.</p>
240     *
241     * @param value The value validation is being performed on.
242     * @param locale The locale to use for the Format.
243     * @param timeZone The Time Zone used to format the date,
244     *  system default if null unless value is a {@link Calendar}.
245     * @return The value formatted as a {@link String}.
246     */
247    public String format(final Object value, final Locale locale, final TimeZone timeZone) {
248        return format(value, (String) null, locale, timeZone);
249    }
250
251    /**
252     * <p>Format an object using the specified pattern and/or
253     *    {@link Locale}.
254     *
255     * @param value The value validation is being performed on.
256     * @param pattern The pattern used to format the value.
257     * @param locale The locale to use for the Format.
258     * @return The value formatted as a {@link String}.
259     */
260    @Override
261    public String format(final Object value, final String pattern, final Locale locale) {
262        return format(value, pattern, locale, (TimeZone) null);
263    }
264
265    /**
266     * <p>Format an object using the specified pattern and/or
267     *    {@link Locale}.
268     *
269     * @param value The value validation is being performed on.
270     * @param pattern The pattern used to format the value.
271     * @param locale The locale to use for the Format.
272     * @param timeZone The Time Zone used to format the date,
273     *  system default if null unless value is a {@link Calendar}.
274     * @return The value formatted as a {@link String}.
275     */
276    public String format(final Object value, final String pattern, final Locale locale, final TimeZone timeZone) {
277        final DateFormat formatter = (DateFormat) getFormat(pattern, locale);
278        if (timeZone != null) {
279            formatter.setTimeZone(timeZone);
280        } else if (value instanceof Calendar) {
281            formatter.setTimeZone(((Calendar) value).getTimeZone());
282        }
283        return format(value, formatter);
284    }
285
286    /**
287     * <p>Format an object into a {@link String} using
288     * the specified pattern.</p>
289     *
290     * @param value The value validation is being performed on.
291     * @param pattern The pattern used to format the value.
292     * @param timeZone The Time Zone used to format the date,
293     *  system default if null unless value is a {@link Calendar}.
294     * @return The value formatted as a {@link String}.
295     */
296    public String format(final Object value, final String pattern, final TimeZone timeZone) {
297        return format(value, pattern, (Locale) null, timeZone);
298    }
299
300    /**
301     * <p>Format an object into a {@link String} using
302     * the default Locale.</p>
303     *
304     * @param value The value validation is being performed on.
305     * @param timeZone The Time Zone used to format the date,
306     *  system default if null unless value is a {@link Calendar}.
307     * @return The value formatted as a {@link String}.
308     */
309    public String format(final Object value, final TimeZone timeZone) {
310        return format(value, (String) null, (Locale) null, timeZone);
311    }
312
313    /**
314     * <p>Returns a {@code DateFormat} for the specified Locale.</p>
315     *
316     * @param locale The locale a {@code DateFormat} is required for,
317     *        system default if null.
318     * @return The {@code DateFormat} to created.
319     */
320    protected Format getFormat(final Locale locale) {
321        final DateFormat formatter;
322        if (dateStyle >= 0 && timeStyle >= 0) {
323            if (locale == null) {
324                formatter = DateFormat.getDateTimeInstance(dateStyle, timeStyle);
325            } else {
326                formatter = DateFormat.getDateTimeInstance(dateStyle, timeStyle, locale);
327            }
328        } else if (timeStyle >= 0) {
329            if (locale == null) {
330                formatter = DateFormat.getTimeInstance(timeStyle);
331            } else {
332                formatter = DateFormat.getTimeInstance(timeStyle, locale);
333            }
334        } else {
335            final int useDateStyle = dateStyle >= 0 ? dateStyle : DateFormat.SHORT;
336            if (locale == null) {
337                formatter = DateFormat.getDateInstance(useDateStyle);
338            } else {
339                formatter = DateFormat.getDateInstance(useDateStyle, locale);
340            }
341        }
342        formatter.setLenient(false);
343        return formatter;
344    }
345
346    /**
347     * <p>Returns a {@code DateFormat} for the specified <em>pattern</em>
348     *    and/or {@link Locale}.</p>
349     *
350     * @param pattern The pattern used to validate the value against or
351     *        {@code null} to use the default for the {@link Locale}.
352     * @param locale The locale to use for the currency format, system default if null.
353     * @return The {@code DateFormat} to created.
354     */
355    @Override
356    protected Format getFormat(final String pattern, final Locale locale) {
357        final DateFormat formatter;
358        final boolean usePattern = !GenericValidator.isBlankOrNull(pattern);
359        if (!usePattern) {
360            formatter = (DateFormat) getFormat(locale);
361        } else if (locale == null) {
362            formatter = new SimpleDateFormat(pattern);
363        } else {
364            final DateFormatSymbols symbols = new DateFormatSymbols(locale);
365            formatter = new SimpleDateFormat(pattern, symbols);
366        }
367        formatter.setLenient(false);
368        return formatter;
369    }
370
371    /**
372     * <p>Validate using the specified {@link Locale}.
373     *
374     * @param value The value validation is being performed on.
375     * @param pattern The pattern used to format the value.
376     * @param locale The locale to use for the Format, defaults to the default
377     * @return {@code true} if the value is valid.
378     */
379    @Override
380    public boolean isValid(final String value, final String pattern, final Locale locale) {
381        return parse(value, pattern, locale, (TimeZone) null) != null;
382    }
383
384    /**
385     * <p>Checks if the value is valid against a specified pattern.</p>
386     *
387     * @param value The value validation is being performed on.
388     * @param pattern The pattern used to validate the value against, or the
389     *        default for the {@link Locale} if {@code null}.
390     * @param locale The locale to use for the date format, system default if null.
391     * @param timeZone The Time Zone used to parse the date, system default if null.
392     * @return The parsed value if valid or {@code null} if invalid.
393     */
394    protected Object parse(String value, final String pattern, final Locale locale, final TimeZone timeZone) {
395        value = value == null ? null : value.trim();
396        final String value1 = value;
397        if (GenericValidator.isBlankOrNull(value1)) {
398            return null;
399        }
400        final DateFormat formatter = (DateFormat) getFormat(pattern, locale);
401        if (timeZone != null) {
402            formatter.setTimeZone(timeZone);
403        }
404        return parse(value, formatter);
405
406    }
407
408    /**
409     * <p>Process the parsed value, performing any further validation
410     *    and type conversion required.</p>
411     *
412     * @param value The parsed object created.
413     * @param formatter The Format used to parse the value with.
414     * @return The parsed value converted to the appropriate type
415     *         if valid or {@code null} if invalid.
416     */
417    @Override
418    protected abstract Object processParsedValue(Object value, Format formatter);
419}