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    }