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