View Javadoc

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