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$ 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 final List<String> dependencyList = Collections.synchronizedList(new ArrayList<String>()); 153 154 /** 155 * An internal List representation of all the validation method's 156 * parameters defined in the methodParams String. 157 */ 158 private final List<String> 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 StringBuilder buffer = new StringBuilder(); 423 BufferedReader reader = new BufferedReader(new InputStreamReader(is)); // TODO encoding 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 fname = this.jsFunction.substring(1); 451 452 if (!this.jsFunction.startsWith("/")) { 453 fname = jsFunction.replace('.', '/') + ".js"; 454 } 455 456 return fname; 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 StringBuilder jsName = 471 new StringBuilder("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<String> 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 @Override 503 public String toString() { 504 StringBuilder results = new StringBuilder("ValidatorAction: "); 505 results.append(name); 506 results.append("\n"); 507 508 return results.toString(); 509 } 510 511 /** 512 * Dynamically runs the validation method for this validator and returns 513 * true if the data is valid. 514 * @param field 515 * @param params A Map of class names to parameter values. 516 * @param results 517 * @param pos The index of the list property to validate if it's indexed. 518 * @throws ValidatorException 519 */ 520 boolean executeValidationMethod( 521 Field field, 522 // TODO What is this the correct value type? 523 // both ValidatorAction and Validator are added as parameters 524 Map<String, Object> params, 525 ValidatorResults results, 526 int pos) 527 throws ValidatorException { 528 529 params.put(Validator.VALIDATOR_ACTION_PARAM, this); 530 531 try { 532 if (this.validationMethod == null) { 533 synchronized(this) { 534 ClassLoader loader = this.getClassLoader(params); 535 this.loadValidationClass(loader); 536 this.loadParameterClasses(loader); 537 this.loadValidationMethod(); 538 } 539 } 540 541 Object[] paramValues = this.getParameterValues(params); 542 543 if (field.isIndexed()) { 544 this.handleIndexedField(field, pos, paramValues); 545 } 546 547 Object result = null; 548 try { 549 result = 550 validationMethod.invoke( 551 getValidationClassInstance(), 552 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: " + e.getMessage(), 586 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 = 606 this.validationClass.getMethod(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 * Stores the output in {@link #parameterClasses}. 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.size()]; 648 649 for (int i = 0; i < this.methodParameterList.size(); i++) { 650 String paramClassName = this.methodParameterList.get(i); 651 652 try { 653 parameterClasses[i] = loader.loadClass(paramClassName); 654 655 } catch (ClassNotFoundException e) { 656 throw new ValidatorException(e.getMessage()); 657 } 658 } 659 660 this.parameterClasses = parameterClasses; 661 } 662 663 /** 664 * Converts a List of parameter class names into their values contained in 665 * the parameters Map. 666 * @param params A Map of class names to parameter values. 667 * @return An array containing the value object for each parameter. This 668 * array is in the same order as the given List and is suitable for passing 669 * to the validation method. 670 */ 671 private Object[] getParameterValues(Map<String, ? super Object> params) { 672 673 Object[] paramValue = new Object[this.methodParameterList.size()]; 674 675 for (int i = 0; i < this.methodParameterList.size(); i++) { 676 String paramClassName = this.methodParameterList.get(i); 677 paramValue[i] = params.get(paramClassName); 678 } 679 680 return paramValue; 681 } 682 683 /** 684 * Return an instance of the validation class or null if the validation 685 * method is static so does not require an instance to be executed. 686 */ 687 private Object getValidationClassInstance() throws ValidatorException { 688 if (Modifier.isStatic(this.validationMethod.getModifiers())) { 689 this.instance = null; 690 691 } else { 692 if (this.instance == null) { 693 try { 694 this.instance = this.validationClass.newInstance(); 695 } catch (InstantiationException e) { 696 String msg1 = 697 "Couldn't create instance of " 698 + this.classname 699 + ". " 700 + e.getMessage(); 701 702 throw new ValidatorException(msg1); 703 704 } catch (IllegalAccessException e) { 705 String msg1 = 706 "Couldn't create instance of " 707 + this.classname 708 + ". " 709 + e.getMessage(); 710 711 throw new ValidatorException(msg1); 712 } 713 } 714 } 715 716 return this.instance; 717 } 718 719 /** 720 * Modifies the paramValue array with indexed fields. 721 * 722 * @param field 723 * @param pos 724 * @param paramValues 725 */ 726 private void handleIndexedField(Field field, int pos, Object[] paramValues) 727 throws ValidatorException { 728 729 int beanIndex = this.methodParameterList.indexOf(Validator.BEAN_PARAM); 730 int fieldIndex = this.methodParameterList.indexOf(Validator.FIELD_PARAM); 731 732 Object indexedList[] = field.getIndexedProperty(paramValues[beanIndex]); 733 734 // Set current iteration object to the parameter array 735 paramValues[beanIndex] = indexedList[pos]; 736 737 // Set field clone with the key modified to represent 738 // the current field 739 Field indexedField = (Field) field.clone(); 740 indexedField.setKey( 741 ValidatorUtils.replace( 742 indexedField.getKey(), 743 Field.TOKEN_INDEXED, 744 "[" + pos + "]")); 745 746 paramValues[fieldIndex] = indexedField; 747 } 748 749 /** 750 * If the result object is a <code>Boolean</code>, it will return its 751 * value. If not it will return <code>false</code> if the object is 752 * <code>null</code> and <code>true</code> if it isn't. 753 */ 754 private boolean isValid(Object result) { 755 if (result instanceof Boolean) { 756 Boolean valid = (Boolean) result; 757 return valid.booleanValue(); 758 } 759 return result != null; 760 } 761 762 /** 763 * Returns the ClassLoader set in the Validator contained in the parameter 764 * Map. 765 */ 766 private ClassLoader getClassLoader(Map<String, Object> 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<String, Object> 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}