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