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: {@link 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 * <validator name="tire"
679 * jsFunction="com.yourcompany.project.tireFuncion">
680 * Validator will attempt to load com.yourcompany.project.validateTireFunction.js from
681 * its class path.
682 * #2:
683 * <validator name="tire">
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 }