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