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