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