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