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
27 import org.apache.commons.validator.GenericValidator;
28
29 /**
30 * <p>Abstract class for Date/Time/Calendar validation.</p>
31 *
32 * <p>This is a <em>base</em> class for building Date / Time
33 * Validators using format parsing.</p>
34 *
35 * @since 1.3.0
36 */
37 public abstract class AbstractCalendarValidator extends AbstractFormatValidator {
38
39 private static final long serialVersionUID = -1410008585975827379L;
40
41 /**
42 * The date style to use for Locale validation.
43 */
44 private final int dateStyle;
45
46 /**
47 * The time style to use for Locale validation.
48 */
49 private final int timeStyle;
50
51 /**
52 * Constructs an instance with the specified <em>strict</em>,
53 * <em>time</em> and <em>date</em> style parameters.
54 *
55 * @param strict {@code true} if strict
56 * {@code Format} parsing should be used.
57 * @param dateStyle the date style to use for Locale validation.
58 * @param timeStyle the time style to use for Locale validation.
59 */
60 public AbstractCalendarValidator(final boolean strict, final int dateStyle, final int timeStyle) {
61 super(strict);
62 this.dateStyle = dateStyle;
63 this.timeStyle = timeStyle;
64 }
65
66 /**
67 * <p>Compares the field from two calendars indicating whether the field for the
68 * first calendar is equal to, less than or greater than the field from the
69 * second calendar.
70 *
71 * @param value The Calendar value.
72 * @param compare The {@link Calendar} to check the value against.
73 * @param field The field to compare for the calendars.
74 * @return Zero if the first calendar's field is equal to the seconds, -1
75 * if it is less than the seconds or +1 if it is greater than the seconds.
76 */
77 private int calculateCompareResult(final Calendar value, final Calendar compare, final int field) {
78 return Integer.compare(value.get(field), compare.get(field));
79 }
80
81 /**
82 * <p>Calculate the quarter for the specified Calendar.</p>
83 *
84 * @param calendar The Calendar value.
85 * @param monthOfFirstQuarter The month that the first quarter starts.
86 * @return The calculated quarter.
87 */
88 private int calculateQuarter(final Calendar calendar, final int monthOfFirstQuarter) {
89 // Add Year
90 int year = calendar.get(Calendar.YEAR);
91
92 final int month = calendar.get(Calendar.MONTH) + 1;
93 final int relativeMonth = month >= monthOfFirstQuarter
94 ? month - monthOfFirstQuarter
95 : month + 12 - monthOfFirstQuarter; // CHECKSTYLE IGNORE MagicNumber
96 final int quarter = relativeMonth / 3 + 1; // CHECKSTYLE IGNORE MagicNumber
97 // adjust the year if the quarter doesn't start in January
98 if (month < monthOfFirstQuarter) {
99 --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 }