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 <validator> 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}