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