View Javadoc

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