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