001 package hirondelle.web4j.model; 002 003 import java.util.*; 004 import java.util.logging.Logger; 005 import java.util.regex.*; 006 import java.math.BigDecimal; 007 008 import hirondelle.web4j.BuildImpl; 009 import hirondelle.web4j.security.SafeText; 010 import hirondelle.web4j.security.SpamDetector; 011 import hirondelle.web4j.util.Util; 012 import hirondelle.web4j.util.WebUtil; 013 import static hirondelle.web4j.util.Consts.PASSES; 014 import static hirondelle.web4j.util.Consts.FAILS; 015 import hirondelle.web4j.util.Args; 016 017 /** 018 <span class="highlight">Returns commonly needed {@link Validator} objects.</span> 019 020 <P>In general, the number of possible validations is <em>very</em> large. It is not appropriate 021 for a framework to attempt to implement <em>all</em> possible validations. Rather, a framework should 022 provide the most common validations, and allow the application programmer to extend 023 the validation mechanism as needed. 024 025 <P>Validations are important parts of your program's logic. 026 Using tools such as JUnit to test your validation code is highly recommended. 027 Since your Model Objects have no dependencies on heavyweight objects, they're almost always easy to test. 028 029 <P>If a specific validation is not provided here, other options include : 030 <ul> 031 <li>performing the validation directly in the Model Object, without using <tt>Check</tt> or 032 a {@link Validator} 033 <li>create a new {@link Validator}, and pass it to either {@link #required(Object, Validator...)} 034 or {@link #optional(Object, Validator...)}. This option is especially attractive when it will eliminate 035 code repetition. 036 <li>subclassing this class, and adding new <tt>static</tt> methods 037 </ul> 038 039 <P>The {@link #range(long, long)}, {@link #min(long)} and {@link #max(long)} methods return {@link Validator}s 040 that perform checks on a <tt>long</tt> value. <span class='highlight'>The <em>source</em> of the <tt>long</tt> value varies 041 according to the type of <tt>Object</tt> passed to the {@link Validator}</span>, and is taken as follows 042 (<tt>int</tt> is internally converted to <tt>long</tt> when necessary) : 043 <ul> 044 <li>{@link Integer#intValue()} 045 <li>{@link Long#longValue()} 046 <li>length of a trimmed {@link String} having content; the same is applied to {@link Id#toString()}, 047 {@link Code#getText()}, and {@link SafeText#getRawString()}. 048 <li>{@link Collection#size()} 049 <li>{@link Map#size()} 050 <li>{@link Date#getTime()} - underlying millisecond value 051 <li>{@link Calendar#getTimeInMillis()} - underlying millisecond value 052 <li>any other class will cause an exception to be thrown by {@link #min(long)} and {@link #max(long)} 053 </ul> 054 055 <P>The {@link #required(Object)}, {@link #required(Object, Validator...)} and {@link #optional(Object, Validator...)} 056 methods are important, and are separated out as distinct validations. <span class='highlight'>In addition, the 057 required/optional character of a field is always the <em>first</em> validation performed</span> (see examples below). 058 059 <P><span class="highlight">In general, it is highly recommended that applications 060 aggressively perform all possible validations.</span> 061 062 <P> Note that when validation is performed in a Model Object, then it will apply both to objects created from 063 user input, and to objects created from a database <tt>ResultSet</tt>. 064 065 <P><b>Example 1</b><br> 066 Example of a required field in a Model Object (that is, the field is of any type, and 067 must be non-<tt>null</tt>) : 068 <PRE> 069 if ( ! Check.required(fStartDate) ) { 070 ex.add("Start Date is Required."); 071 } 072 </PRE> 073 074 075 <P><b>Example 2</b><br> 076 Example of a required <em>text</em> field, which must have visible content 077 (as in {@link Util#textHasContent(String)}) : 078 <PRE> 079 if ( ! Check.required(fTitle) ) { 080 ex.add("Title is required, and must have content."); 081 } 082 </PRE> 083 084 <P><b>Example 3</b><br> 085 Example of a required text field, whose length must be in the range <tt>2..50</tt> : 086 <PRE> 087 if ( ! Check.required(fTitle, Check.range(2,50)) ) { 088 ex.add("Title is required, and must have between 2 and 50 characters."); 089 } 090 </PRE> 091 092 <P><b>Example 4</b><br> 093 Example of an optional <tt>String</tt> field that matches the format '<tt>1234-5678</tt>' : 094 <PRE> 095 //compile the pattern once, when the class is loaded 096 private static final Pattern fID_PATTERN = Pattern.compile("(\\d){4}-(\\d){4}"); 097 ... 098 if ( ! Check.optional(fSomeId, Check.pattern(fID_PATTERN)) ) { 099 ex.add("Id is optional, and must have the form '1234-5678'."); 100 } 101 </PRE> 102 103 <P><b>Example 5</b><br> 104 The initial <tt>!</tt> negation operator is easy to forget. Many will prefer a more explicit style, which seems 105 to be more legible : 106 <PRE> 107 import static hirondelle.web4j.util.Consts.FAILS; 108 ... 109 if ( FAILS == Check.required(fStartDate) ) { 110 ex.add("Start Date is Required."); 111 } 112 </PRE> 113 114 <P>Here is one style for implementing custom validations for your application : 115 <PRE> 116 //Checks that a person's age is in the range 0..150 117 public static Validator ageRange(){ 118 class CheckAge implements Validator { 119 public boolean isValid(Object aObject) { 120 Integer age = (Integer)aObject; 121 return 0 <= age <= 150; 122 } 123 } 124 return new CheckAge(); 125 } 126 </PRE> 127 */ 128 public class Check { 129 130 /** 131 Return <tt>true</tt> only if <tt>aObject</tt> is non-<tt>null</tt>. 132 133 <P><em><tt>String</tt> and {@link SafeText} objects are a special case</em> : instead of just 134 being non-<tt>null</tt>, they must have content according to {@link Util#textHasContent(String)}. 135 136 @param aObject possibly-null field of a Model Object. 137 */ 138 public static boolean required(Object aObject){ 139 boolean result = FAILS; 140 if ( isText(aObject) ) { 141 result = Util.textHasContent(getText(aObject)); 142 } 143 else { 144 result = (aObject != null); 145 } 146 return result; 147 } 148 149 /** 150 Return <tt>true</tt> only if <tt>aObject</tt> satisfies {@link #required(Object)}, 151 <em>and</em> it passes all given validations 152 153 @param aObject possibly-null field of a Model Object. 154 */ 155 public static boolean required(Object aObject, Validator... aValidators){ 156 boolean result = PASSES; 157 if ( ! required(aObject) ) { 158 result = FAILS; 159 } 160 else { 161 result = passesValidations(aObject, aValidators); 162 } 163 return result; 164 } 165 166 /** 167 Return <tt>true</tt> only if <tt>aObject</tt> is <tt>null</tt>, OR if <tt>aObject</tt> is non-<tt>null</tt> 168 and passes all validations. 169 170 <P><em><tt>String</tt> and {@link SafeText} objects are a special case</em> : instead of just 171 being non-<tt>null</tt>, they must have content according to {@link Util#textHasContent(String)}. 172 173 @param aObject possibly-null field of a Model Object. 174 */ 175 public static boolean optional(Object aObject, Validator... aValidators){ 176 boolean result = PASSES; 177 if ( aObject != null ){ 178 if( isText(aObject) ) { 179 result = Util.textHasContent(getText(aObject)) && passesValidations(aObject, aValidators); 180 } 181 else { 182 result = passesValidations(aObject, aValidators); 183 } 184 } 185 return result; 186 } 187 188 /** 189 Return a {@link Validator} to check that all passed booleans are <tt>true</tt>. 190 Note that the single parameter is a sequence parameter, so you may pass in many booleans, not just one. 191 <P>This is a bizarre method, but it's actually useful. It allows checks on an object's state to be treated as any other check. 192 */ 193 public static Validator isTrue(Boolean... aPredicates){ 194 class AllTrue implements Validator{ 195 AllTrue(Boolean... aPreds){ 196 fPredicates = aPreds; 197 } 198 public boolean isValid(Object aObject) { 199 //aObject is ignored here 200 boolean result = true; 201 for (Boolean predicate: fPredicates){ 202 result = result && predicate; 203 } 204 return result; 205 }; 206 private Boolean[] fPredicates; 207 } 208 return new AllTrue(aPredicates); 209 } 210 211 /** 212 Return a {@link Validator} to check that all passed booleans are <tt>false</tt>. 213 Note that the single parameter is a sequence parameter, so you may pass in many booleans, not just one. 214 <P>This is a bizarre method, but it's actually useful. It allows checks on an object's state to be treated as any other check. 215 */ 216 public static Validator isFalse(Boolean... aPredicates){ 217 class AllFalse implements Validator{ 218 AllFalse(Boolean... aPreds){ 219 fPredicates = aPreds; 220 } 221 public boolean isValid(Object aObject) { 222 //aObject is ignored here 223 boolean result = true; 224 for (Boolean predicate: fPredicates){ 225 result = result && !predicate; 226 } 227 return result; 228 }; 229 private Boolean[] fPredicates; 230 } 231 return new AllFalse(aPredicates); 232 } 233 234 /** 235 Return a {@link Validator} to check that a field's value is greater than or equal to <tt>aMinimumValue</tt>. 236 See class comment for the kind of objects which may be checked by the returned {@link Validator}. 237 */ 238 public static Validator min(final long aMinimumValue){ 239 class Minimum implements Validator { 240 Minimum(long aMinValue){ 241 fMinValue = aMinValue; 242 } 243 public boolean isValid(Object aObject){ 244 long value = getValueAsLong(aObject); 245 return value >= fMinValue; 246 } 247 private final long fMinValue; 248 } 249 return new Minimum(aMinimumValue); 250 } 251 252 /** 253 Return a {@link Validator} to check that a {@link BigDecimal} field is greater than or equal 254 to <tt>aMinimumValue</tt>. 255 */ 256 public static Validator min(final BigDecimal aMinimumValue){ 257 class Minimum implements Validator { 258 Minimum(BigDecimal aMinValue){ 259 fMinValue = aMinValue; 260 } 261 public boolean isValid(Object aObject){ 262 BigDecimal value = (BigDecimal)aObject; 263 return value.compareTo(fMinValue) >= 0; 264 } 265 private final BigDecimal fMinValue; 266 } 267 return new Minimum(aMinimumValue); 268 } 269 270 /** 271 Return a {@link Validator} to check that a {@link Decimal} amount is greater than or equal 272 to <tt>aMinimumValue</tt>. This methods allows comparisons between 273 <tt>Money</tt> objects of different currency. 274 */ 275 public static Validator min(final Decimal aMinimumValue){ 276 class Minimum implements Validator { 277 Minimum(Decimal aMinValue){ 278 fMinValue = aMinValue; 279 } 280 public boolean isValid(Object aObject){ 281 Decimal value = (Decimal)aObject; 282 return value.gteq(fMinValue); 283 } 284 private final Decimal fMinValue; 285 } 286 return new Minimum(aMinimumValue); 287 } 288 289 /** 290 Return a {@link Validator} to check that a {@link DateTime} is greater than or equal 291 to <tt>aMinimumValue</tt>. 292 */ 293 public static Validator min(final DateTime aMinimumValue){ 294 class Minimum implements Validator { 295 Minimum(DateTime aMinValue){ 296 fMinValue = aMinValue; 297 } 298 public boolean isValid(Object aObject){ 299 DateTime value = (DateTime)aObject; 300 return value.gteq(fMinValue); 301 } 302 private final DateTime fMinValue; 303 } 304 return new Minimum(aMinimumValue); 305 } 306 307 /** 308 Return a {@link Validator} to check that a field's value is less than or equal to <tt>aMaximumValue</tt>. 309 See class comment for the kind of objects which may be checked by the returned {@link Validator}. 310 */ 311 public static Validator max(final long aMaximumValue){ 312 class Maximum implements Validator { 313 Maximum(long aMaxValue){ 314 fMaxValue = aMaxValue; 315 } 316 public boolean isValid(Object aObject){ 317 long value = getValueAsLong(aObject); 318 return value <= fMaxValue; 319 } 320 private final long fMaxValue; 321 } 322 return new Maximum(aMaximumValue); 323 } 324 325 /** 326 Return a {@link Validator} to check that a {@link BigDecimal} field is less than or equal 327 to <tt>aMaximumValue</tt>. 328 */ 329 public static Validator max(final BigDecimal aMaximumValue){ 330 class Maximum implements Validator { 331 Maximum(BigDecimal aMaxValue){ 332 fMaxValue = aMaxValue; 333 } 334 public boolean isValid(Object aObject){ 335 BigDecimal value = (BigDecimal)aObject; 336 return value.compareTo(fMaxValue) <= 0; 337 } 338 private final BigDecimal fMaxValue; 339 } 340 return new Maximum(aMaximumValue); 341 } 342 343 /** 344 Return a {@link Validator} to check that a {@link Decimal} amount is less than or equal 345 to <tt>aMaximumValue</tt>. This methods allows comparisons between 346 <tt>Money</tt> objects of different currency. 347 */ 348 public static Validator max(final Decimal aMaximumValue){ 349 class Maximum implements Validator { 350 Maximum(Decimal aMaxValue){ 351 fMaxValue = aMaxValue; 352 } 353 public boolean isValid(Object aObject){ 354 Decimal value = (Decimal)aObject; 355 return value.lteq(fMaxValue); 356 } 357 private final Decimal fMaxValue; 358 } 359 return new Maximum(aMaximumValue); 360 } 361 362 /** 363 Return a {@link Validator} to check that a {@link DateTime} is less than or equal 364 to <tt>aMaximumValue</tt>. 365 */ 366 public static Validator max(final DateTime aMaximumValue){ 367 class Maximum implements Validator { 368 Maximum(DateTime aMaxValue){ 369 fMaxValue = aMaxValue; 370 } 371 public boolean isValid(Object aObject){ 372 DateTime value = (DateTime)aObject; 373 return value.lteq(fMaxValue); 374 } 375 private final DateTime fMaxValue; 376 } 377 return new Maximum(aMaximumValue); 378 } 379 380 /** 381 Return a {@link Validator} to check that a field's value is in a certain range (inclusive). 382 See class comment for the kind of objects which may be checked by the returned {@link Validator}. 383 */ 384 public static Validator range(final long aMinimumValue, final long aMaximumValue){ 385 class Range implements Validator { 386 Range(long aMin, long aMax){ 387 fMinValue = aMin; 388 fMaxValue = aMax; 389 } 390 public boolean isValid(Object aObject){ 391 long value = getValueAsLong(aObject); 392 return fMinValue <= value && value <= fMaxValue; 393 } 394 private final long fMinValue; 395 private final long fMaxValue; 396 } 397 return new Range(aMinimumValue, aMaximumValue); 398 } 399 400 /** 401 Return a {@link Validator} to check that a {@link BigDecimal} value is in a certain range (inclusive). 402 */ 403 public static Validator range(final BigDecimal aMinimumValue, final BigDecimal aMaximumValue){ 404 class Range implements Validator { 405 Range(BigDecimal aMin, BigDecimal aMax){ 406 fMinValue = aMin; 407 fMaxValue = aMax; 408 } 409 public boolean isValid(Object aObject){ 410 BigDecimal value = (BigDecimal)aObject; 411 return value.compareTo(fMinValue) >= 0 && value.compareTo(fMaxValue) <= 0; 412 } 413 private final BigDecimal fMinValue; 414 private final BigDecimal fMaxValue; 415 } 416 return new Range(aMinimumValue, aMaximumValue); 417 } 418 419 /** 420 Return a {@link Validator} to check that a {@link Decimal} amount is in a certain range (inclusive). 421 This methods allows comparisons between <tt>Money</tt> objects of different currency. 422 */ 423 public static Validator range(final Decimal aMinimumValue, final Decimal aMaximumValue){ 424 class Range implements Validator { 425 Range(Decimal aMin, Decimal aMax){ 426 fMinValue = aMin; 427 fMaxValue = aMax; 428 } 429 public boolean isValid(Object aObject){ 430 Decimal value = (Decimal)aObject; 431 return value.gteq(fMinValue) && value.lteq(fMaxValue); 432 } 433 private final Decimal fMinValue; 434 private final Decimal fMaxValue; 435 } 436 return new Range(aMinimumValue, aMaximumValue); 437 } 438 439 /** 440 Return a {@link Validator} to check that a {@link DateTime} is in a certain range (inclusive). 441 */ 442 public static Validator range(final DateTime aMinimumValue, final DateTime aMaximumValue){ 443 class Range implements Validator { 444 Range(DateTime aMin, DateTime aMax){ 445 fMinValue = aMin; 446 fMaxValue = aMax; 447 } 448 public boolean isValid(Object aObject){ 449 DateTime value = (DateTime)aObject; 450 boolean isInRange = value.gt(fMinValue) && value.lt(fMaxValue); 451 boolean isAtAnEndpoint = value.equals(fMinValue) || value.equals(fMaxValue); 452 return isInRange || isAtAnEndpoint; 453 } 454 private final DateTime fMinValue; 455 private final DateTime fMaxValue; 456 } 457 return new Range(aMinimumValue, aMaximumValue); 458 } 459 460 /** 461 Return a {@link Validator} to check that the number of decimal places of a {@link Decimal} or 462 {@link BigDecimal} is <em>less than or equal to</em> <tt>aMaxNumberOfDecimalPlaces</tt>. 463 464 @param aMaxNumberOfDecimalPlaces is greater than or equal to <tt>1</tt>. 465 */ 466 public static Validator numDecimalsMax(final int aMaxNumberOfDecimalPlaces){ 467 Args.checkForPositive(aMaxNumberOfDecimalPlaces); 468 class MaxNumPlaces implements Validator { 469 MaxNumPlaces(int aMaxNumberOfDecimals){ 470 Args.checkForPositive(aMaxNumberOfDecimals); 471 fMaxNumPlaces = aMaxNumberOfDecimals; 472 } 473 public boolean isValid(Object aObject){ 474 return Util.hasMaxDecimals(extractNumber(aObject), fMaxNumPlaces); 475 } 476 private final int fMaxNumPlaces; 477 } 478 return new MaxNumPlaces(aMaxNumberOfDecimalPlaces); 479 } 480 481 /** 482 Return a {@link Validator} to check that the number of decimal places of a {@link Decimal} 483 or {@link BigDecimal} is <em>exactly equal to</em> <tt>aNumDecimals</tt>. 484 485 @param aNumDecimals is 0 or more. 486 */ 487 public static Validator numDecimalsAlways(final int aNumDecimals){ 488 class NumPlaces implements Validator { 489 NumPlaces(int aNumberOfDecimalPlaces){ 490 fNumPlaces = aNumberOfDecimalPlaces; 491 } 492 public boolean isValid(Object aObject){ 493 return Util.hasNumDecimals(extractNumber(aObject), fNumPlaces); 494 } 495 private final int fNumPlaces; 496 } 497 return new NumPlaces(aNumDecimals); 498 } 499 500 /** 501 Return a {@link Validator} that checks a {@link String} or {@link SafeText} 502 field versus a regular expression {@link Pattern}. 503 504 <P>This method might be used to validate a zip code, phone number, and so on - any text which has a 505 well defined format. 506 507 <P>There must be a complete match versus the whole text, as in {@link Matcher#matches()}. 508 In addition, the returned {@link Validator} will not trim the text before performing the validation. 509 510 @param aRegex is a {@link Pattern}, which holds a regular expression. 511 */ 512 public static Validator pattern(final Pattern aRegex){ 513 class PatternValidator implements Validator { 514 PatternValidator(Pattern aSomeRegex){ 515 fRegex = aSomeRegex; 516 } 517 public boolean isValid(Object aObject) { 518 Matcher matcher = fRegex.matcher(getText(aObject)); 519 return matcher.matches(); 520 } 521 private final Pattern fRegex; 522 } 523 return new PatternValidator(aRegex); 524 } 525 526 /** 527 Return a {@link Validator} to verify a {@link String} or {@link SafeText} field is a syntactically 528 valid email address. 529 530 <P>See {@link WebUtil#isValidEmailAddress(String)}. The text is not trimmed by the returned 531 {@link Validator}. 532 */ 533 public static Validator email(){ 534 class EmailValidator implements Validator { 535 public boolean isValid(Object aObject) { 536 return WebUtil.isValidEmailAddress(getText(aObject)); 537 } 538 } 539 return new EmailValidator(); 540 } 541 542 /** 543 Return a {@link Validator} to check that a {@link String} or {@link SafeText} field is not spam, 544 according to {@link SpamDetector}. 545 */ 546 public static Validator forSpam(){ 547 class SpamValidator implements Validator { 548 public boolean isValid(Object aObject) { 549 SpamDetector spamDetector = BuildImpl.forSpamDetector(); 550 return !spamDetector.isSpam(getText(aObject)); 551 } 552 } 553 return new SpamValidator(); 554 } 555 556 /* 557 Note : forURL() method was attempted, but abandoned. 558 The JDK implementation of the URL constructor seems very flaky. 559 */ 560 561 /** 562 This no-argument constructor is empty. 563 564 <P>This constructor exists only because of it has <tt>protected</tt> scope. 565 Having <tt>protected</tt> scope has two desirable effects: 566 <ul> 567 <li>typical callers cannot create <tt>Check</tt> objects. This is appropriate since this class contains 568 only static methods. 569 <li>if needed, this class may be subclassed. This is useful when you need to add custom validations. 570 </ul> 571 */ 572 protected Check(){ 573 //empty - prevent construction by caller, but allow it for subclasses 574 } 575 576 // PRIVATE // 577 578 private static final Logger fLogger = Util.getLogger(Check.class); 579 580 private static boolean passesValidations(Object aObject, Validator... aValidators) { 581 boolean result = PASSES; 582 for(Validator validator: aValidators){ 583 if ( ! validator.isValid(aObject) ) { 584 result = FAILS; 585 fLogger.fine("Failed a validation."); 586 break; 587 } 588 } 589 return result; 590 } 591 592 private static long getValueAsLong(Object aObject){ 593 long result = 0; 594 if ( aObject instanceof Integer){ 595 Integer value = (Integer)aObject; 596 result = value.intValue(); 597 } 598 else if (aObject instanceof Long) { 599 Long value = (Long)aObject; 600 result = value.longValue(); 601 } 602 else if (aObject instanceof String){ 603 String text = (String)aObject; 604 if ( Util.textHasContent(text) ) { 605 result = text.trim().length(); 606 } 607 } 608 else if (aObject instanceof Id){ 609 Id id = (Id)aObject; 610 if ( Util.textHasContent(id.toString()) ) { 611 result = id.toString().trim().length(); 612 } 613 } 614 else if (aObject instanceof SafeText){ 615 SafeText text = (SafeText)aObject; 616 if ( Util.textHasContent(text.getRawString()) ) { 617 result = text.getRawString().trim().length(); 618 } 619 } 620 else if (aObject instanceof Code){ 621 Code code = (Code)aObject; 622 if ( Util.textHasContent(code.getText()) ) { 623 result = code.getText().getRawString().trim().length(); 624 } 625 } 626 else if (aObject instanceof Collection) { 627 Collection collection = (Collection)aObject; 628 result = collection.size(); 629 } 630 else if (aObject instanceof Map) { 631 Map map = (Map)aObject; 632 result = map.size(); 633 } 634 else if (aObject instanceof Date) { 635 Date date = (Date)aObject; 636 result = date.getTime(); 637 } 638 else if (aObject instanceof Calendar){ 639 Calendar calendar = (Calendar)aObject; 640 result = calendar.getTimeInMillis(); 641 } 642 else { 643 String message = "Unexpected type of Object: " + aObject.getClass().getName(); 644 fLogger.severe(message); 645 throw new AssertionError(message); 646 } 647 return result; 648 } 649 650 private static boolean isText(Object aObject){ 651 return (aObject instanceof String) || (aObject instanceof SafeText); 652 } 653 654 private static String getText(Object aObject){ 655 String result = null; 656 if ( aObject instanceof String ){ 657 String text = (String) aObject; 658 result = text.toString(); 659 } 660 else if (aObject instanceof SafeText ){ 661 SafeText text = (SafeText)aObject; 662 result = text.getRawString(); 663 } 664 return result; 665 } 666 667 /** aObject must be a BigDecimal or a Money object. */ 668 private static BigDecimal extractNumber(Object aObject){ 669 BigDecimal result = null; 670 if( aObject instanceof BigDecimal){ 671 result = (BigDecimal)aObject; 672 } 673 else if (aObject instanceof Decimal) { 674 Decimal decimal = (Decimal)aObject; 675 result = decimal.getAmount(); 676 } 677 else { 678 throw new IllegalArgumentException("Unexpected class: " + aObject.getClass()); 679 } 680 return result; 681 } 682 }