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