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