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