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