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