View Javadoc

    1   /*
    2    * Licensed to the Apache Software Foundation (ASF) under one or more
    3    * contributor license agreements.  See the NOTICE file distributed with
    4    * this work for additional information regarding copyright ownership.
    5    * The ASF licenses this file to You under the Apache License, Version 2.0
    6    * (the "License"); you may not use this file except in compliance with
    7    * the License.  You may obtain a copy of the License at
    8    *
    9    *      http://www.apache.org/licenses/LICENSE-2.0
    10   *
    11   * Unless required by applicable law or agreed to in writing, software
    12   * distributed under the License is distributed on an "AS IS" BASIS,
    13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    14   * See the License for the specific language governing permissions and
    15   * limitations under the License.
    16   */
    17  package org.apache.commons.validator;
    18
    19  import java.io.BufferedReader;
    20  import java.io.IOException;
    21  import java.io.InputStream;
    22  import java.io.InputStreamReader;
    23  import java.io.Serializable;
    24  import java.lang.reflect.InvocationTargetException;
    25  import java.lang.reflect.Method;
    26  import java.lang.reflect.Modifier;
    27  import java.util.ArrayList;
    28  import java.util.Collections;
    29  import java.util.List;
    30  import java.util.Map;
    31  import java.util.StringTokenizer;
    32
    33  import org.apache.commons.logging.Log;
    34  import org.apache.commons.logging.LogFactory;
    35  import org.apache.commons.validator.util.ValidatorUtils;
    36
    37  /***
    38   * Contains the information to dynamically create and run a validation
    39   * method.  This is the class representation of a pluggable validator that can
    40   * be defined in an xml file with the &lt;validator&gt; element.
    41   *
    42   * <strong>Note</strong>: The validation method is assumed to be thread safe.
    43   *
    44   * @version $Revision: 478334 $ $Date: 2006-11-22 21:31:54 +0000 (Wed, 22 Nov 2006) $
    45   */
    46  public class ValidatorAction implements Serializable {
    47
    48      /***
    49       * Logger.
    50       */
    51      private transient Log log = LogFactory.getLog(ValidatorAction.class);
    52
    53      /***
    54       * The name of the validation.
    55       */
    56      private String name = null;
    57
    58      /***
    59       * The full class name of the class containing
    60       * the validation method associated with this action.
    61       */
    62      private String classname = null;
    63
    64      /***
    65       * The Class object loaded from the classname.
    66       */
    67      private Class validationClass = null;
    68
    69      /***
    70       * The full method name of the validation to be performed.  The method
    71       * must be thread safe.
    72       */
    73      private String method = null;
    74
    75      /***
    76       * The Method object loaded from the method name.
    77       */
    78      private Method validationMethod = null;
    79
    80      /***
    81       * <p>
    82       * The method signature of the validation method.  This should be a comma
    83       * delimited list of the full class names of each parameter in the correct
    84       * order that the method takes.
    85       * </p>
    86       * <p>
    87       * Note: <code>java.lang.Object</code> is reserved for the
    88       * JavaBean that is being validated.  The <code>ValidatorAction</code>
    89       * and <code>Field</code> that are associated with a field's
    90       * validation will automatically be populated if they are
    91       * specified in the method signature.
    92       * </p>
    93       */
    94      private String methodParams =
    95              Validator.BEAN_PARAM
    96              + ","
    97              + Validator.VALIDATOR_ACTION_PARAM
    98              + ","
    99              + Validator.FIELD_PARAM;
    100
    101     /***
    102      * The Class objects for each entry in methodParameterList.
    103      */
    104     private Class[] parameterClasses = null;
    105
    106     /***
    107      * The other <code>ValidatorAction</code>s that this one depends on.  If
    108      * any errors occur in an action that this one depends on, this action will
    109      * not be processsed.
    110      */
    111     private String depends = null;
    112
    113     /***
    114      * The default error message associated with this action.
    115      */
    116     private String msg = null;
    117
    118     /***
    119      * An optional field to contain the name to be used if JavaScript is
    120      * generated.
    121      */
    122     private String jsFunctionName = null;
    123
    124     /***
    125      * An optional field to contain the class path to be used to retrieve the
    126      * JavaScript function.
    127      */
    128     private String jsFunction = null;
    129
    130     /***
    131      * An optional field to containing a JavaScript representation of the
    132      * java method assocated with this action.
    133      */
    134     private String javascript = null;
    135
    136     /***
    137      * If the java method matching the correct signature isn't static, the
    138      * instance is stored in the action.  This assumes the method is thread
    139      * safe.
    140      */
    141     private Object instance = null;
    142
    143     /***
    144      * An internal List representation of the other <code>ValidatorAction</code>s
    145      * this one depends on (if any).  This List gets updated
    146      * whenever setDepends() gets called.  This is synchronized so a call to
    147      * setDepends() (which clears the List) won't interfere with a call to
    148      * isDependency().
    149      */
    150     private List dependencyList = Collections.synchronizedList(new ArrayList());
    151
    152     /***
    153      * An internal List representation of all the validation method's
    154      * parameters defined in the methodParams String.
    155      */
    156     private List methodParameterList = new ArrayList();
    157
    158     /***
    159      * Gets the name of the validator action.
    160      * @return Validator Action name.
    161      */
    162     public String getName() {
    163         return name;
    164     }
    165
    166     /***
    167      * Sets the name of the validator action.
    168      * @param name Validator Action name.
    169      */
    170     public void setName(String name) {
    171         this.name = name;
    172     }
    173
    174     /***
    175      * Gets the class of the validator action.
    176      * @return Class name of the validator Action.
    177      */
    178     public String getClassname() {
    179         return classname;
    180     }
    181
    182     /***
    183      * Sets the class of the validator action.
    184      * @param classname Class name of the validator Action.
    185      */
    186     public void setClassname(String classname) {
    187         this.classname = classname;
    188     }
    189
    190     /***
    191      * Gets the name of method being called for the validator action.
    192      * @return The method name.
    193      */
    194     public String getMethod() {
    195         return method;
    196     }
    197
    198     /***
    199      * Sets the name of method being called for the validator action.
    200      * @param method The method name.
    201      */
    202     public void setMethod(String method) {
    203         this.method = method;
    204     }
    205
    206     /***
    207      * Gets the method parameters for the method.
    208      * @return Method's parameters.
    209      */
    210     public String getMethodParams() {
    211         return methodParams;
    212     }
    213
    214     /***
    215      * Sets the method parameters for the method.
    216      * @param methodParams A comma separated list of parameters.
    217      */
    218     public void setMethodParams(String methodParams) {
    219         this.methodParams = methodParams;
    220
    221         this.methodParameterList.clear();
    222
    223         StringTokenizer st = new StringTokenizer(methodParams, ",");
    224         while (st.hasMoreTokens()) {
    225             String value = st.nextToken().trim();
    226
    227             if (value != null && value.length() > 0) {
    228                 this.methodParameterList.add(value);
    229             }
    230         }
    231     }
    232
    233     /***
    234      * Gets the dependencies of the validator action as a comma separated list
    235      * of validator names.
    236      * @return The validator action's dependencies.
    237      */
    238     public String getDepends() {
    239         return this.depends;
    240     }
    241
    242     /***
    243      * Sets the dependencies of the validator action.
    244      * @param depends A comma separated list of validator names.
    245      */
    246     public void setDepends(String depends) {
    247         this.depends = depends;
    248
    249         this.dependencyList.clear();
    250
    251         StringTokenizer st = new StringTokenizer(depends, ",");
    252         while (st.hasMoreTokens()) {
    253             String depend = st.nextToken().trim();
    254
    255             if (depend != null && depend.length() > 0) {
    256                 this.dependencyList.add(depend);
    257             }
    258         }
    259     }
    260
    261     /***
    262      * Gets the message associated with the validator action.
    263      * @return The message for the validator action.
    264      */
    265     public String getMsg() {
    266         return msg;
    267     }
    268
    269     /***
    270      * Sets the message associated with the validator action.
    271      * @param msg The message for the validator action.
    272      */
    273     public void setMsg(String msg) {
    274         this.msg = msg;
    275     }
    276
    277     /***
    278      * Gets the Javascript function name.  This is optional and can
    279      * be used instead of validator action name for the name of the
    280      * Javascript function/object.
    281      * @return The Javascript function name.
    282      */
    283     public String getJsFunctionName() {
    284         return jsFunctionName;
    285     }
    286
    287     /***
    288      * Sets the Javascript function name.  This is optional and can
    289      * be used instead of validator action name for the name of the
    290      * Javascript function/object.
    291      * @param jsFunctionName The Javascript function name.
    292      */
    293     public void setJsFunctionName(String jsFunctionName) {
    294         this.jsFunctionName = jsFunctionName;
    295     }
    296
    297     /***
    298      * Sets the fully qualified class path of the Javascript function.
    299      * <p>
    300      * This is optional and can be used <strong>instead</strong> of the setJavascript().
    301      * Attempting to call both <code>setJsFunction</code> and <code>setJavascript</code>
    302      * will result in an <code>IllegalStateException</code> being thrown. </p>
    303      * <p>
    304      * If <strong>neither</strong> setJsFunction or setJavascript is set then
    305      * validator will attempt to load the default javascript definition.
    306      * </p>
    307      * <pre>
    308      * <b>Examples</b>
    309      *   If in the validator.xml :
    310      * #1:
    311      *      &lt;validator name="tire"
    312      *            jsFunction="com.yourcompany.project.tireFuncion"&gt;
    313      *     Validator will attempt to load com.yourcompany.project.validateTireFunction.js from
    314      *     its class path.
    315      * #2:
    316      *    &lt;validator name="tire"&gt;
    317      *      Validator will use the name attribute to try and load
    318      *         org.apache.commons.validator.javascript.validateTire.js
    319      *      which is the default javascript definition.
    320      * </pre>
    321      * @param jsFunction The Javascript function's fully qualified class path.
    322      */
    323     public void setJsFunction(String jsFunction) {
    324         if (javascript != null) {
    325             throw new IllegalStateException("Cannot call setJsFunction() after calling setJavascript()");
    326         }
    327
    328         this.jsFunction = jsFunction;
    329     }
    330
    331     /***
    332      * Gets the Javascript equivalent of the java class and method
    333      * associated with this action.
    334      * @return The Javascript validation.
    335      */
    336     public String getJavascript() {
    337         return javascript;
    338     }
    339
    340     /***
    341      * Sets the Javascript equivalent of the java class and method
    342      * associated with this action.
    343      * @param javascript The Javascript validation.
    344      */
    345     public void setJavascript(String javascript) {
    346         if (jsFunction != null) {
    347             throw new IllegalStateException("Cannot call setJavascript() after calling setJsFunction()");
    348         }
    349
    350         this.javascript = javascript;
    351     }
    352
    353     /***
    354      * Initialize based on set.
    355      */
    356     protected void init() {
    357         this.loadJavascriptFunction();
    358     }
    359
    360     /***
    361      * Load the javascript function specified by the given path.  For this
    362      * implementation, the <code>jsFunction</code> property should contain a
    363      * fully qualified package and script name, separated by periods, to be
    364      * loaded from the class loader that created this instance.
    365      *
    366      * TODO if the path begins with a '/' the path will be intepreted as
    367      * absolute, and remain unchanged.  If this fails then it will attempt to
    368      * treat the path as a file path.  It is assumed the script ends with a
    369      * '.js'.
    370      */
    371     protected synchronized void loadJavascriptFunction() {
    372
    373         if (this.javascriptAlreadyLoaded()) {
    374             return;
    375         }
    376
    377         if (getLog().isTraceEnabled()) {
    378             getLog().trace("  Loading function begun");
    379         }
    380
    381         if (this.jsFunction == null) {
    382             this.jsFunction = this.generateJsFunction();
    383         }
    384
    385         String javascriptFileName = this.formatJavascriptFileName();
    386
    387         if (getLog().isTraceEnabled()) {
    388             getLog().trace("  Loading js function '" + javascriptFileName + "'");
    389         }
    390
    391         this.javascript = this.readJavascriptFile(javascriptFileName);
    392
    393         if (getLog().isTraceEnabled()) {
    394             getLog().trace("  Loading javascript function completed");
    395         }
    396
    397     }
    398
    399     /***
    400      * Read a javascript function from a file.
    401      * @param javascriptFileName The file containing the javascript.
    402      * @return The javascript function or null if it could not be loaded.
    403      */
    404     private String readJavascriptFile(String javascriptFileName) {
    405         ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
    406         if (classLoader == null) {
    407             classLoader = this.getClass().getClassLoader();
    408         }
    409
    410         InputStream is = classLoader.getResourceAsStream(javascriptFileName);
    411         if (is == null) {
    412             is = this.getClass().getResourceAsStream(javascriptFileName);
    413         }
    414
    415         if (is == null) {
    416             getLog().debug("  Unable to read javascript name "+javascriptFileName);
    417             return null;
    418         }
    419
    420         StringBuffer buffer = new StringBuffer();
    421         BufferedReader reader = new BufferedReader(new InputStreamReader(is));
    422         try {
    423             String line = null;
    424             while ((line = reader.readLine()) != null) {
    425                 buffer.append(line + "\n");
    426             }
    427
    428         } catch(IOException e) {
    429             getLog().error("Error reading javascript file.", e);
    430
    431         } finally {
    432             try {
    433                 reader.close();
    434             } catch(IOException e) {
    435                 getLog().error("Error closing stream to javascript file.", e);
    436             }
    437         }
    438
    439         String function = buffer.toString();
    440         return function.equals("") ? null : function;
    441     }
    442
    443     /***
    444      * @return A filename suitable for passing to a
    445      * ClassLoader.getResourceAsStream() method.
    446      */
    447     private String formatJavascriptFileName() {
    448         String name = this.jsFunction.substring(1);
    449
    450         if (!this.jsFunction.startsWith("/")) {
    451             name = jsFunction.replace('.', '/') + ".js";
    452         }
    453
    454         return name;
    455     }
    456
    457     /***
    458      * @return true if the javascript for this action has already been loaded.
    459      */
    460     private boolean javascriptAlreadyLoaded() {
    461         return (this.javascript != null);
    462     }
    463
    464     /***
    465      * Used to generate the javascript name when it is not specified.
    466      */
    467     private String generateJsFunction() {
    468         StringBuffer jsName =
    469                 new StringBuffer("org.apache.commons.validator.javascript");
    470
    471         jsName.append(".validate");
    472         jsName.append(name.substring(0, 1).toUpperCase());
    473         jsName.append(name.substring(1, name.length()));
    474
    475         return jsName.toString();
    476     }
    477
    478     /***
    479      * Checks whether or not the value passed in is in the depends field.
    480      * @param validatorName Name of the dependency to check.
    481      * @return Whether the named validator is a dependant.
    482      */
    483     public boolean isDependency(String validatorName) {
    484         return this.dependencyList.contains(validatorName);
    485     }
    486
    487     /***
    488      * Returns the dependent validator names as an unmodifiable
    489      * <code>List</code>.
    490      * @return List of the validator action's depedents.
    491      */
    492     public List getDependencyList() {
    493         return Collections.unmodifiableList(this.dependencyList);
    494     }
    495
    496     /***
    497      * Returns a string representation of the object.
    498      * @return a string representation.
    499      */
    500     public String toString() {
    501         StringBuffer results = new StringBuffer("ValidatorAction: ");
    502         results.append(name);
    503         results.append("\n");
    504
    505         return results.toString();
    506     }
    507
    508     /***
    509      * Dynamically runs the validation method for this validator and returns
    510      * true if the data is valid.
    511      * @param field
    512      * @param params A Map of class names to parameter values.
    513      * @param results
    514      * @param pos The index of the list property to validate if it's indexed.
    515      * @throws ValidatorException
    516      */
    517     boolean executeValidationMethod(
    518         Field field,
    519         Map params,
    520         ValidatorResults results,
    521         int pos)
    522         throws ValidatorException {
    523
    524         params.put(Validator.VALIDATOR_ACTION_PARAM, this);
    525
    526         try {
    527             if (this.validationMethod == null) {
    528                 synchronized(this) {
    529                     ClassLoader loader = this.getClassLoader(params);
    530                     this.loadValidationClass(loader);
    531                     this.loadParameterClasses(loader);
    532                     this.loadValidationMethod();
    533                 }
    534             }
    535
    536             Object[] paramValues = this.getParameterValues(params);
    537
    538             if (field.isIndexed()) {
    539                 this.handleIndexedField(field, pos, paramValues);
    540             }
    541
    542             Object result = null;
    543             try {
    544                 result =
    545                     validationMethod.invoke(
    546                         getValidationClassInstance(),
    547                         paramValues);
    548
    549             } catch (IllegalArgumentException e) {
    550                 throw new ValidatorException(e.getMessage());
    551             } catch (IllegalAccessException e) {
    552                 throw new ValidatorException(e.getMessage());
    553             } catch (InvocationTargetException e) {
    554
    555                 if (e.getTargetException() instanceof Exception) {
    556                     throw (Exception) e.getTargetException();
    557
    558                 } else if (e.getTargetException() instanceof Error) {
    559                     throw (Error) e.getTargetException();
    560                 }
    561             }
    562
    563             boolean valid = this.isValid(result);
    564             if (!valid || (valid && !onlyReturnErrors(params))) {
    565                 results.add(field, this.name, valid, result);
    566             }
    567
    568             if (!valid) {
    569                 return false;
    570             }
    571
    572             // TODO This catch block remains for backward compatibility.  Remove
    573             // this for Validator 2.0 when exception scheme changes.
    574         } catch (Exception e) {
    575             if (e instanceof ValidatorException) {
    576                 throw (ValidatorException) e;
    577             }
    578
    579             getLog().error(
    580                 "Unhandled exception thrown during validation: " + e.getMessage(),
    581                 e);
    582
    583             results.add(field, this.name, false);
    584             return false;
    585         }
    586
    587         return true;
    588     }
    589
    590     /***
    591      * Load the Method object for the configured validation method name.
    592      * @throws ValidatorException
    593      */
    594     private void loadValidationMethod() throws ValidatorException {
    595         if (this.validationMethod != null) {
    596             return;
    597         }
    598
    599         try {
    600             this.validationMethod =
    601                 this.validationClass.getMethod(this.method, this.parameterClasses);
    602
    603         } catch (NoSuchMethodException e) {
    604             throw new ValidatorException("No such validation method: " +
    605                 e.getMessage());
    606         }
    607     }
    608
    609     /***
    610      * Load the Class object for the configured validation class name.
    611      * @param loader The ClassLoader used to load the Class object.
    612      * @throws ValidatorException
    613      */
    614     private void loadValidationClass(ClassLoader loader)
    615         throws ValidatorException {
    616
    617         if (this.validationClass != null) {
    618             return;
    619         }
    620
    621         try {
    622             this.validationClass = loader.loadClass(this.classname);
    623         } catch (ClassNotFoundException e) {
    624             throw new ValidatorException(e.toString());
    625         }
    626     }
    627
    628     /***
    629      * Converts a List of parameter class names into their Class objects.
    630      * @return An array containing the Class object for each parameter.  This
    631      * array is in the same order as the given List and is suitable for passing
    632      * to the validation method.
    633      * @throws ValidatorException if a class cannot be loaded.
    634      */
    635     private void loadParameterClasses(ClassLoader loader)
    636         throws ValidatorException {
    637
    638         if (this.parameterClasses != null) {
    639             return;
    640         }
    641
    642         Class[] parameterClasses = new Class[this.methodParameterList.size()];
    643
    644         for (int i = 0; i < this.methodParameterList.size(); i++) {
    645             String paramClassName = (String) this.methodParameterList.get(i);
    646
    647             try {
    648                 parameterClasses[i] = loader.loadClass(paramClassName);
    649
    650             } catch (ClassNotFoundException e) {
    651                 throw new ValidatorException(e.getMessage());
    652             }
    653         }
    654
    655         this.parameterClasses = parameterClasses;
    656     }
    657
    658     /***
    659      * Converts a List of parameter class names into their values contained in
    660      * the parameters Map.
    661      * @param params A Map of class names to parameter values.
    662      * @return An array containing the value object for each parameter.  This
    663      * array is in the same order as the given List and is suitable for passing
    664      * to the validation method.
    665      */
    666     private Object[] getParameterValues(Map params) {
    667
    668         Object[] paramValue = new Object[this.methodParameterList.size()];
    669
    670         for (int i = 0; i < this.methodParameterList.size(); i++) {
    671             String paramClassName = (String) this.methodParameterList.get(i);
    672             paramValue[i] = params.get(paramClassName);
    673         }
    674
    675         return paramValue;
    676     }
    677
    678     /***
    679      * Return an instance of the validation class or null if the validation
    680      * method is static so does not require an instance to be executed.
    681      */
    682     private Object getValidationClassInstance() throws ValidatorException {
    683         if (Modifier.isStatic(this.validationMethod.getModifiers())) {
    684             this.instance = null;
    685
    686         } else {
    687             if (this.instance == null) {
    688                 try {
    689                     this.instance = this.validationClass.newInstance();
    690                 } catch (InstantiationException e) {
    691                     String msg =
    692                         "Couldn't create instance of "
    693                             + this.classname
    694                             + ".  "
    695                             + e.getMessage();
    696
    697                     throw new ValidatorException(msg);
    698
    699                 } catch (IllegalAccessException e) {
    700                     String msg =
    701                         "Couldn't create instance of "
    702                             + this.classname
    703                             + ".  "
    704                             + e.getMessage();
    705
    706                     throw new ValidatorException(msg);
    707                 }
    708             }
    709         }
    710
    711         return this.instance;
    712     }
    713
    714     /***
    715      * Modifies the paramValue array with indexed fields.
    716      *
    717      * @param field
    718      * @param pos
    719      * @param paramValues
    720      */
    721     private void handleIndexedField(Field field, int pos, Object[] paramValues)
    722         throws ValidatorException {
    723
    724         int beanIndex = this.methodParameterList.indexOf(Validator.BEAN_PARAM);
    725         int fieldIndex = this.methodParameterList.indexOf(Validator.FIELD_PARAM);
    726
    727         Object indexedList[] = field.getIndexedProperty(paramValues[beanIndex]);
    728
    729         // Set current iteration object to the parameter array
    730         paramValues[beanIndex] = indexedList[pos];
    731
    732         // Set field clone with the key modified to represent
    733         // the current field
    734         Field indexedField = (Field) field.clone();
    735         indexedField.setKey(
    736             ValidatorUtils.replace(
    737                 indexedField.getKey(),
    738                 Field.TOKEN_INDEXED,
    739                 "[" + pos + "]"));
    740
    741         paramValues[fieldIndex] = indexedField;
    742     }
    743
    744     /***
    745      * If the result object is a <code>Boolean</code>, it will return its
    746      * value.  If not it will return <code>false</code> if the object is
    747      * <code>null</code> and <code>true</code> if it isn't.
    748      */
    749     private boolean isValid(Object result) {
    750         if (result instanceof Boolean) {
    751             Boolean valid = (Boolean) result;
    752             return valid.booleanValue();
    753         } else {
    754             return (result != null);
    755         }
    756     }
    757
    758     /***
    759      * Returns the ClassLoader set in the Validator contained in the parameter
    760      * Map.
    761      */
    762     private ClassLoader getClassLoader(Map params) {
    763         Validator v = (Validator) params.get(Validator.VALIDATOR_PARAM);
    764         return v.getClassLoader();
    765     }
    766
    767     /***
    768      * Returns the onlyReturnErrors setting in the Validator contained in the
    769      * parameter Map.
    770      */
    771     private boolean onlyReturnErrors(Map params) {
    772         Validator v = (Validator) params.get(Validator.VALIDATOR_PARAM);
    773         return v.getOnlyReturnErrors();
    774     }
    775
    776     /***
    777      * Accessor method for Log instance.
    778      *
    779      * The Log instance variable is transient and
    780      * accessing it through this method ensures it
    781      * is re-initialized when this instance is
    782      * de-serialized.
    783      *
    784      * @return The Log instance.
    785      */
    786     private Log getLog() {
    787         if (log == null) {
    788             log =  LogFactory.getLog(ValidatorAction.class);
    789         }
    790         return log;
    791     }
    792 }