View Javadoc

1   /*
2    * $Id: ValidatorAction.java 366933 2006-01-07 22:51:01Z niallp $
3    * $Rev: 366933 $
4    * $Date: 2006-01-07 22:51:01 +0000 (Sat, 07 Jan 2006) $
5    *
6    * ====================================================================
7    * Copyright 2001-2006 The Apache Software Foundation
8    *
9    * Licensed under the Apache License, Version 2.0 (the "License");
10   * you may not use this file except in compliance with the License.
11   * You may obtain a copy of the License at
12   *
13   *     http://www.apache.org/licenses/LICENSE-2.0
14   *
15   * Unless required by applicable law or agreed to in writing, software
16   * distributed under the License is distributed on an "AS IS" BASIS,
17   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18   * See the License for the specific language governing permissions and
19   * limitations under the License.
20   */
21  
22  package org.apache.commons.validator;
23  
24  import java.io.BufferedReader;
25  import java.io.IOException;
26  import java.io.InputStream;
27  import java.io.InputStreamReader;
28  import java.io.Serializable;
29  import java.lang.reflect.InvocationTargetException;
30  import java.lang.reflect.Method;
31  import java.lang.reflect.Modifier;
32  import java.util.ArrayList;
33  import java.util.Collections;
34  import java.util.List;
35  import java.util.Map;
36  import java.util.StringTokenizer;
37  
38  import org.apache.commons.logging.Log;
39  import org.apache.commons.logging.LogFactory;
40  import org.apache.commons.validator.util.ValidatorUtils;
41  
42  /***
43   * Contains the information to dynamically create and run a validation
44   * method.  This is the class representation of a pluggable validator that can 
45   * be defined in an xml file with the <validator> element.
46   *
47   * <strong>Note</strong>: The validation method is assumed to be thread safe.
48   */
49  public class ValidatorAction implements Serializable {
50      
51      /***
52       * Logger.
53       */
54      private transient Log log = LogFactory.getLog(ValidatorAction.class);
55  
56      /***
57       * The name of the validation.
58       */
59      private String name = null;
60  
61      /***
62       * The full class name of the class containing
63       * the validation method associated with this action.
64       */
65      private String classname = null;
66      
67      /***
68       * The Class object loaded from the classname.
69       */
70      private Class validationClass = null;
71  
72      /***
73       * The full method name of the validation to be performed.  The method
74       * must be thread safe.
75       */
76      private String method = null;
77      
78      /***
79       * The Method object loaded from the method name.
80       */
81      private Method validationMethod = null;
82  
83      /***
84       * <p>
85       * The method signature of the validation method.  This should be a comma
86       * delimited list of the full class names of each parameter in the correct 
87       * order that the method takes.
88       * </p>
89       * <p>
90       * Note: <code>java.lang.Object</code> is reserved for the
91       * JavaBean that is being validated.  The <code>ValidatorAction</code>
92       * and <code>Field</code> that are associated with a field's
93       * validation will automatically be populated if they are
94       * specified in the method signature.
95       * </p>
96       */
97      private String methodParams =
98              Validator.BEAN_PARAM
99              + ","
100             + Validator.VALIDATOR_ACTION_PARAM
101             + ","
102             + Validator.FIELD_PARAM;
103             
104     /***
105      * The Class objects for each entry in methodParameterList.
106      */        
107     private Class[] parameterClasses = null;
108 
109     /***
110      * The other <code>ValidatorAction</code>s that this one depends on.  If 
111      * any errors occur in an action that this one depends on, this action will 
112      * not be processsed.
113      */
114     private String depends = null;
115 
116     /***
117      * The default error message associated with this action.
118      */
119     private String msg = null;
120 
121     /***
122      * An optional field to contain the name to be used if JavaScript is 
123      * generated.
124      */
125     private String jsFunctionName = null;
126 
127     /***
128      * An optional field to contain the class path to be used to retrieve the
129      * JavaScript function.
130      */
131     private String jsFunction = null;
132 
133     /***
134      * An optional field to containing a JavaScript representation of the
135      * java method assocated with this action.
136      */
137     private String javascript = null;
138 
139     /***
140      * If the java method matching the correct signature isn't static, the 
141      * instance is stored in the action.  This assumes the method is thread 
142      * safe.
143      */
144     private Object instance = null;
145 
146     /***
147      * An internal List representation of the other <code>ValidatorAction</code>s
148      * this one depends on (if any).  This List gets updated
149      * whenever setDepends() gets called.  This is synchronized so a call to
150      * setDepends() (which clears the List) won't interfere with a call to
151      * isDependency().
152      */
153     private List dependencyList = Collections.synchronizedList(new ArrayList());
154 
155     /***
156      * An internal List representation of all the validation method's 
157      * parameters defined in the methodParams String.
158      */
159     private List methodParameterList = new ArrayList();
160 
161     /***
162      * Gets the name of the validator action.
163      * @return Validator Action name.
164      */
165     public String getName() {
166         return name;
167     }
168 
169     /***
170      * Sets the name of the validator action.
171      * @param name Validator Action name.
172      */
173     public void setName(String name) {
174         this.name = name;
175     }
176 
177     /***
178      * Gets the class of the validator action.
179      * @return Class name of the validator Action.
180      */
181     public String getClassname() {
182         return classname;
183     }
184 
185     /***
186      * Sets the class of the validator action.
187      * @param classname Class name of the validator Action.
188      */
189     public void setClassname(String classname) {
190         this.classname = classname;
191     }
192 
193     /***
194      * Gets the name of method being called for the validator action.
195      * @return The method name.
196      */
197     public String getMethod() {
198         return method;
199     }
200 
201     /***
202      * Sets the name of method being called for the validator action.
203      * @param method The method name.
204      */
205     public void setMethod(String method) {
206         this.method = method;
207     }
208 
209     /***
210      * Gets the method parameters for the method.
211      * @return Method's parameters.
212      */
213     public String getMethodParams() {
214         return methodParams;
215     }
216 
217     /***
218      * Sets the method parameters for the method.
219      * @param methodParams A comma separated list of parameters.
220      */
221     public void setMethodParams(String methodParams) {
222         this.methodParams = methodParams;
223 
224         this.methodParameterList.clear();
225 
226         StringTokenizer st = new StringTokenizer(methodParams, ",");
227         while (st.hasMoreTokens()) {
228             String value = st.nextToken().trim();
229 
230             if (value != null && value.length() > 0) {
231                 this.methodParameterList.add(value);
232             }
233         }
234     }
235 
236     /***
237      * Gets the dependencies of the validator action as a comma separated list 
238      * of validator names.
239      * @return The validator action's dependencies.
240      */
241     public String getDepends() {
242         return this.depends;
243     }
244 
245     /***
246      * Sets the dependencies of the validator action.
247      * @param depends A comma separated list of validator names.
248      */
249     public void setDepends(String depends) {
250         this.depends = depends;
251 
252         this.dependencyList.clear();
253 
254         StringTokenizer st = new StringTokenizer(depends, ",");
255         while (st.hasMoreTokens()) {
256             String depend = st.nextToken().trim();
257 
258             if (depend != null && depend.length() > 0) {
259                 this.dependencyList.add(depend);
260             }
261         }
262     }
263 
264     /***
265      * Gets the message associated with the validator action.
266      * @return The message for the validator action.
267      */
268     public String getMsg() {
269         return msg;
270     }
271 
272     /***
273      * Sets the message associated with the validator action.
274      * @param msg The message for the validator action.
275      */
276     public void setMsg(String msg) {
277         this.msg = msg;
278     }
279 
280     /***
281      * Gets the Javascript function name.  This is optional and can
282      * be used instead of validator action name for the name of the
283      * Javascript function/object.
284      * @return The Javascript function name.
285      */
286     public String getJsFunctionName() {
287         return jsFunctionName;
288     }
289 
290     /***
291      * Sets the Javascript function name.  This is optional and can
292      * be used instead of validator action name for the name of the
293      * Javascript function/object.
294      * @param jsFunctionName The Javascript function name.
295      */
296     public void setJsFunctionName(String jsFunctionName) {
297         this.jsFunctionName = jsFunctionName;
298     }
299 
300     /***
301      * Sets the fully qualified class path of the Javascript function.
302      * <p>
303      * This is optional and can be used <strong>instead</strong> of the setJavascript().
304      * Attempting to call both <code>setJsFunction</code> and <code>setJavascript</code>
305      * will result in an <code>IllegalStateException</code> being thrown. </p>
306      * <p>
307      * If <strong>neither</strong> setJsFunction or setJavascript is set then 
308      * validator will attempt to load the default javascript definition.
309      * </p>
310      * <pre>
311      * <b>Examples</b>
312      *   If in the validator.xml :
313      * #1:
314      *      &lt;validator name="tire"
315      *            jsFunction="com.yourcompany.project.tireFuncion"&gt;
316      *     Validator will attempt to load com.yourcompany.project.validateTireFunction.js from
317      *     its class path.
318      * #2:
319      *    &lt;validator name="tire"&gt;
320      *      Validator will use the name attribute to try and load
321      *         org.apache.commons.validator.javascript.validateTire.js
322      *      which is the default javascript definition.
323      * </pre>
324      * @param jsFunction The Javascript function's fully qualified class path.
325      */
326     public void setJsFunction(String jsFunction) {
327         if (javascript != null) {
328             throw new IllegalStateException("Cannot call setJsFunction() after calling setJavascript()");
329         }
330 
331         this.jsFunction = jsFunction;
332     }
333 
334     /***
335      * Gets the Javascript equivalent of the java class and method
336      * associated with this action.
337      * @return The Javascript validation.
338      */
339     public String getJavascript() {
340         return javascript;
341     }
342 
343     /***
344      * Sets the Javascript equivalent of the java class and method
345      * associated with this action.
346      * @param javascript The Javascript validation.
347      */
348     public void setJavascript(String javascript) {
349         if (jsFunction != null) {
350             throw new IllegalStateException("Cannot call setJavascript() after calling setJsFunction()");
351         }
352 
353         this.javascript = javascript;
354     }
355 
356     /***
357      * Initialize based on set.
358      */
359     protected void init() {
360         this.loadJavascriptFunction();
361     }
362 
363     /***
364      * Load the javascript function specified by the given path.  For this
365      * implementation, the <code>jsFunction</code> property should contain a 
366      * fully qualified package and script name, separated by periods, to be 
367      * loaded from the class loader that created this instance.
368      *
369      * TODO if the path begins with a '/' the path will be intepreted as 
370      * absolute, and remain unchanged.  If this fails then it will attempt to 
371      * treat the path as a file path.  It is assumed the script ends with a 
372      * '.js'.
373      */
374     protected synchronized void loadJavascriptFunction() {
375 
376         if (this.javascriptAlreadyLoaded()) {
377             return;
378         }
379 
380         if (getLog().isTraceEnabled()) {
381             getLog().trace("  Loading function begun");
382         }
383 
384         if (this.jsFunction == null) {
385             this.jsFunction = this.generateJsFunction();
386         }
387 
388         String javascriptFileName = this.formatJavascriptFileName();
389 
390         if (getLog().isTraceEnabled()) {
391             getLog().trace("  Loading js function '" + javascriptFileName + "'");
392         }
393 
394         this.javascript = this.readJavascriptFile(javascriptFileName);
395 
396         if (getLog().isTraceEnabled()) {
397             getLog().trace("  Loading javascript function completed");
398         }
399 
400     }
401 
402     /***
403      * Read a javascript function from a file.
404      * @param javascriptFileName The file containing the javascript.
405      * @return The javascript function or null if it could not be loaded.
406      */
407     private String readJavascriptFile(String javascriptFileName) {
408         ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
409         if (classLoader == null) {
410             classLoader = this.getClass().getClassLoader();
411         }
412 
413         InputStream is = classLoader.getResourceAsStream(javascriptFileName);
414         if (is == null) {
415             is = this.getClass().getResourceAsStream(javascriptFileName);
416         }
417 
418         if (is == null) {
419             getLog().debug("  Unable to read javascript name "+javascriptFileName);
420             return null;
421         }
422 
423         StringBuffer buffer = new StringBuffer();
424         BufferedReader reader = new BufferedReader(new InputStreamReader(is));
425         try {
426             String line = null;
427             while ((line = reader.readLine()) != null) {
428                 buffer.append(line + "\n");
429             }
430 
431         } catch(IOException e) {
432             getLog().error("Error reading javascript file.", e);
433 
434         } finally {
435             try {
436                 reader.close();
437             } catch(IOException e) {
438                 getLog().error("Error closing stream to javascript file.", e);
439             }
440         }
441         
442         String function = buffer.toString();
443         return function.equals("") ? null : function;
444     }
445 
446     /***
447      * @return A filename suitable for passing to a 
448      * ClassLoader.getResourceAsStream() method.
449      */
450     private String formatJavascriptFileName() {
451         String name = this.jsFunction.substring(1);
452 
453         if (!this.jsFunction.startsWith("/")) {
454             name = jsFunction.replace('.', '/') + ".js";
455         }
456 
457         return name;
458     }
459 
460     /***
461      * @return true if the javascript for this action has already been loaded.
462      */
463     private boolean javascriptAlreadyLoaded() {
464         return (this.javascript != null);
465     }
466 
467     /***
468      * Used to generate the javascript name when it is not specified.
469      */
470     private String generateJsFunction() {
471         StringBuffer jsName =
472                 new StringBuffer("org.apache.commons.validator.javascript");
473 
474         jsName.append(".validate");
475         jsName.append(name.substring(0, 1).toUpperCase());
476         jsName.append(name.substring(1, name.length()));
477 
478         return jsName.toString();
479     }
480 
481     /***
482      * Checks whether or not the value passed in is in the depends field.
483      * @param validatorName Name of the dependency to check.
484      * @return Whether the named validator is a dependant.
485      */
486     public boolean isDependency(String validatorName) {
487         return this.dependencyList.contains(validatorName);
488     }
489 
490     /***
491      * Returns the dependent validator names as an unmodifiable
492      * <code>List</code>.
493      * @return List of the validator action's depedents.
494      */
495     public List getDependencyList() {
496         return Collections.unmodifiableList(this.dependencyList);
497     }
498 
499     /***
500      * Returns a string representation of the object.
501      * @return a string representation.
502      */
503     public String toString() {
504         StringBuffer results = new StringBuffer("ValidatorAction: ");
505         results.append(name);
506         results.append("\n");
507 
508         return results.toString();
509     }
510     
511     /***
512      * Dynamically runs the validation method for this validator and returns 
513      * true if the data is valid.
514      * @param field
515      * @param params A Map of class names to parameter values.
516      * @param results
517      * @param pos The index of the list property to validate if it's indexed.
518      * @throws ValidatorException
519      */
520     boolean executeValidationMethod(
521         Field field,
522         Map params,
523         ValidatorResults results,
524         int pos)
525         throws ValidatorException {
526 
527         params.put(Validator.VALIDATOR_ACTION_PARAM, this);
528 
529         try {
530             ClassLoader loader = this.getClassLoader(params);
531             this.loadValidationClass(loader);
532             this.loadParameterClasses(loader);
533             this.loadValidationMethod();
534 
535             Object[] paramValues = this.getParameterValues(params);
536             
537             if (field.isIndexed()) {
538                 this.handleIndexedField(field, pos, paramValues);
539             }
540 
541             Object result = null;
542             try {
543                 result =
544                     validationMethod.invoke(
545                         getValidationClassInstance(),
546                         paramValues);
547 
548             } catch (IllegalArgumentException e) {
549                 throw new ValidatorException(e.getMessage());
550             } catch (IllegalAccessException e) {
551                 throw new ValidatorException(e.getMessage());
552             } catch (InvocationTargetException e) {
553 
554                 if (e.getTargetException() instanceof Exception) {
555                     throw (Exception) e.getTargetException();
556 
557                 } else if (e.getTargetException() instanceof Error) {
558                     throw (Error) e.getTargetException();
559                 }
560             }
561 
562             boolean valid = this.isValid(result);
563             if (!valid || (valid && !onlyReturnErrors(params))) {
564                 results.add(field, this.name, valid, result);
565             }
566 
567             if (!valid) {
568                 return false;
569             }
570 
571             // TODO This catch block remains for backward compatibility.  Remove
572             // this for Validator 2.0 when exception scheme changes.
573         } catch (Exception e) {
574             if (e instanceof ValidatorException) {
575                 throw (ValidatorException) e;
576             }
577 
578             getLog().error(
579                 "Unhandled exception thrown during validation: " + e.getMessage(),
580                 e);
581 
582             results.add(field, this.name, false);
583             return false;
584         }
585 
586         return true;
587     }
588     
589     /***
590      * Load the Method object for the configured validation method name.
591      * @throws ValidatorException
592      */
593     private void loadValidationMethod() throws ValidatorException {
594         if (this.validationMethod != null) {
595             return;
596         }
597      
598         try {
599             this.validationMethod =
600                 this.validationClass.getMethod(this.method, this.parameterClasses);
601      
602         } catch (NoSuchMethodException e) {
603             throw new ValidatorException("No such validation method: " + 
604                 e.getMessage());
605         }
606     }
607     
608     /***
609      * Load the Class object for the configured validation class name.
610      * @param loader The ClassLoader used to load the Class object.
611      * @throws ValidatorException
612      */
613     private void loadValidationClass(ClassLoader loader) 
614         throws ValidatorException {
615         
616         if (this.validationClass != null) {
617             return;
618         }
619         
620         try {
621             this.validationClass = loader.loadClass(this.classname);
622         } catch (ClassNotFoundException e) {
623             throw new ValidatorException(e.getMessage());
624         }
625     }
626     
627     /***
628      * Converts a List of parameter class names into their Class objects.
629      * @return An array containing the Class object for each parameter.  This 
630      * array is in the same order as the given List and is suitable for passing 
631      * to the validation method.
632      * @throws ValidatorException if a class cannot be loaded.
633      */
634     private void loadParameterClasses(ClassLoader loader)
635         throws ValidatorException {
636 
637         if (this.parameterClasses != null) {
638             return;
639         }
640         
641         this.parameterClasses = new Class[this.methodParameterList.size()];
642 
643         for (int i = 0; i < this.methodParameterList.size(); i++) {
644             String paramClassName = (String) this.methodParameterList.get(i);
645 
646             try {
647                 this.parameterClasses[i] = loader.loadClass(paramClassName);
648                     
649             } catch (ClassNotFoundException e) {
650                 throw new ValidatorException(e.getMessage());
651             }
652         }
653     }
654     
655     /***
656      * Converts a List of parameter class names into their values contained in 
657      * the parameters Map.
658      * @param params A Map of class names to parameter values.
659      * @return An array containing the value object for each parameter.  This 
660      * array is in the same order as the given List and is suitable for passing 
661      * to the validation method.
662      */
663     private Object[] getParameterValues(Map params) {
664 
665         Object[] paramValue = new Object[this.methodParameterList.size()];
666 
667         for (int i = 0; i < this.methodParameterList.size(); i++) {
668             String paramClassName = (String) this.methodParameterList.get(i);
669             paramValue[i] = params.get(paramClassName);
670         }
671 
672         return paramValue;
673     }
674     
675     /***
676      * Return an instance of the validation class or null if the validation 
677      * method is static so does not require an instance to be executed.
678      */
679     private Object getValidationClassInstance() throws ValidatorException {
680         if (Modifier.isStatic(this.validationMethod.getModifiers())) {
681             this.instance = null;
682 
683         } else {
684             if (this.instance == null) {
685                 try {
686                     this.instance = this.validationClass.newInstance();
687                 } catch (InstantiationException e) {
688                     String msg =
689                         "Couldn't create instance of "
690                             + this.classname
691                             + ".  "
692                             + e.getMessage();
693 
694                     throw new ValidatorException(msg);
695 
696                 } catch (IllegalAccessException e) {
697                     String msg =
698                         "Couldn't create instance of "
699                             + this.classname
700                             + ".  "
701                             + e.getMessage();
702 
703                     throw new ValidatorException(msg);
704                 }
705             }
706         }
707 
708         return this.instance;
709     }
710     
711     /***
712      * Modifies the paramValue array with indexed fields.
713      *
714      * @param field
715      * @param pos
716      * @param paramValues
717      */
718     private void handleIndexedField(Field field, int pos, Object[] paramValues)
719         throws ValidatorException {
720 
721         int beanIndex = this.methodParameterList.indexOf(Validator.BEAN_PARAM);
722         int fieldIndex = this.methodParameterList.indexOf(Validator.FIELD_PARAM);
723 
724         Object indexedList[] = field.getIndexedProperty(paramValues[beanIndex]);
725 
726         // Set current iteration object to the parameter array
727         paramValues[beanIndex] = indexedList[pos];
728 
729         // Set field clone with the key modified to represent
730         // the current field
731         Field indexedField = (Field) field.clone();
732         indexedField.setKey(
733             ValidatorUtils.replace(
734                 indexedField.getKey(),
735                 Field.TOKEN_INDEXED,
736                 "[" + pos + "]"));
737 
738         paramValues[fieldIndex] = indexedField;
739     }
740     
741     /***
742      * If the result object is a <code>Boolean</code>, it will return its 
743      * value.  If not it will return <code>false</code> if the object is 
744      * <code>null</code> and <code>true</code> if it isn't.
745      */
746     private boolean isValid(Object result) {
747         if (result instanceof Boolean) {
748             Boolean valid = (Boolean) result;
749             return valid.booleanValue();
750         } else {
751             return (result != null);
752         }
753     }
754 
755     /***
756      * Returns the ClassLoader set in the Validator contained in the parameter
757      * Map.
758      */
759     private ClassLoader getClassLoader(Map params) {
760         Validator v = (Validator) params.get(Validator.VALIDATOR_PARAM);
761         return v.getClassLoader();
762     }
763     
764     /***
765      * Returns the onlyReturnErrors setting in the Validator contained in the 
766      * parameter Map.
767      */
768     private boolean onlyReturnErrors(Map params) {
769         Validator v = (Validator) params.get(Validator.VALIDATOR_PARAM);
770         return v.getOnlyReturnErrors();
771     }
772 
773     /***
774      * Accessor method for Log instance.
775      *
776      * The Log instance variable is transient and
777      * accessing it through this method ensures it
778      * is re-initialized when this instance is
779      * de-serialized.
780      *
781      * @return The Log instance.
782      */
783     private Log getLog() {
784         if (log == null) {
785             log =  LogFactory.getLog(ValidatorAction.class);
786         }
787         return log;
788     }
789 }