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