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: 493905 $ $Date: 2007-01-08 03:11:38 +0100 (Mo, 08. Jan 2007) $ 045 */ 046 public class ValidatorAction implements Serializable { 047 048 /** 049 * Logger. 050 */ 051 private transient Log log = LogFactory.getLog(ValidatorAction.class); 052 053 /** 054 * The name of the validation. 055 */ 056 private String name = null; 057 058 /** 059 * The full class name of the class containing 060 * the validation method associated with this action. 061 */ 062 private String classname = null; 063 064 /** 065 * The Class object loaded from the classname. 066 */ 067 private Class validationClass = null; 068 069 /** 070 * The full method name of the validation to be performed. The method 071 * must be thread safe. 072 */ 073 private String method = null; 074 075 /** 076 * The Method object loaded from the method name. 077 */ 078 private Method validationMethod = null; 079 080 /** 081 * <p> 082 * The method signature of the validation method. This should be a comma 083 * delimited list of the full class names of each parameter in the correct 084 * order that the method takes. 085 * </p> 086 * <p> 087 * Note: <code>java.lang.Object</code> is reserved for the 088 * JavaBean that is being validated. The <code>ValidatorAction</code> 089 * and <code>Field</code> that are associated with a field's 090 * validation will automatically be populated if they are 091 * specified in the method signature. 092 * </p> 093 */ 094 private String methodParams = 095 Validator.BEAN_PARAM 096 + "," 097 + Validator.VALIDATOR_ACTION_PARAM 098 + "," 099 + 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 // TODO This catch block remains for backward compatibility. Remove 573 // this for Validator 2.0 when exception scheme changes. 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 // Set current iteration object to the parameter array 730 paramValues[beanIndex] = indexedList[pos]; 731 732 // Set field clone with the key modified to represent 733 // the current field 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 }