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}