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