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 */ 017package org.apache.commons.validator; 018 019import java.io.BufferedReader; 020import java.io.IOException; 021import java.io.InputStream; 022import java.io.InputStreamReader; 023import java.io.Serializable; 024import java.lang.reflect.InvocationTargetException; 025import java.lang.reflect.Method; 026import java.lang.reflect.Modifier; 027import java.util.ArrayList; 028import java.util.Collections; 029import java.util.List; 030import java.util.Map; 031import java.util.StringTokenizer; 032 033import org.apache.commons.logging.Log; 034import org.apache.commons.logging.LogFactory; 035import 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: 1227719 $ $Date: 2012-01-05 12:45:51 -0500 (Thu, 05 Jan 2012) $ 045 */ 046public class ValidatorAction implements Serializable { 047 048 private static final long serialVersionUID = 1339713700053204597L; 049 050 /** 051 * Logger. 052 */ 053 private transient Log log = LogFactory.getLog(ValidatorAction.class); 054 055 /** 056 * The name of the validation. 057 */ 058 private String name = null; 059 060 /** 061 * The full class name of the class containing 062 * the validation method associated with this action. 063 */ 064 private String classname = null; 065 066 /** 067 * The Class object loaded from the classname. 068 */ 069 private Class validationClass = null; 070 071 /** 072 * The full method name of the validation to be performed. The method 073 * must be thread safe. 074 */ 075 private String method = null; 076 077 /** 078 * The Method object loaded from the method name. 079 */ 080 private Method validationMethod = null; 081 082 /** 083 * <p> 084 * The method signature of the validation method. This should be a comma 085 * delimited list of the full class names of each parameter in the correct 086 * order that the method takes. 087 * </p> 088 * <p> 089 * Note: <code>java.lang.Object</code> is reserved for the 090 * JavaBean that is being validated. The <code>ValidatorAction</code> 091 * and <code>Field</code> that are associated with a field's 092 * validation will automatically be populated if they are 093 * specified in the method signature. 094 * </p> 095 */ 096 private String methodParams = 097 Validator.BEAN_PARAM 098 + "," 099 + 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}