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