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