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