View Javadoc

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