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