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