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 }