001    /**
002     * Copyright 2005-2013 The Kuali Foundation
003     *
004     * Licensed under the Educational Community License, Version 2.0 (the "License");
005     * you may not use this file except in compliance with the License.
006     * You may obtain a copy of the License at
007     *
008     * http://www.opensource.org/licenses/ecl2.php
009     *
010     * Unless required by applicable law or agreed to in writing, software
011     * distributed under the License is distributed on an "AS IS" BASIS,
012     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013     * See the License for the specific language governing permissions and
014     * limitations under the License.
015     */
016    package org.kuali.rice.krad.datadictionary.validator;
017    
018    import org.apache.commons.lang.StringUtils;
019    import org.apache.commons.logging.Log;
020    import org.apache.commons.logging.LogFactory;
021    import org.kuali.rice.krad.datadictionary.DataDictionary;
022    import org.kuali.rice.krad.datadictionary.DataDictionaryEntry;
023    import org.kuali.rice.krad.datadictionary.DataDictionaryException;
024    import org.kuali.rice.krad.uif.component.Component;
025    import org.kuali.rice.krad.uif.util.ExpressionUtils;
026    import org.kuali.rice.krad.uif.util.UifBeanFactoryPostProcessor;
027    import org.kuali.rice.krad.uif.view.View;
028    import org.springframework.beans.factory.support.KualiDefaultListableBeanFactory;
029    import org.springframework.beans.factory.xml.XmlBeanDefinitionReader;
030    import org.springframework.core.io.FileSystemResource;
031    import org.springframework.core.io.Resource;
032    import org.springframework.core.io.ResourceLoader;
033    
034    import java.util.ArrayList;
035    import java.util.Map;
036    
037    /**
038     * A validator for Rice Dictionaries that stores the information found during its validation.
039     *
040     * @author Kuali Rice Team (rice.collab@kuali.org)
041     */
042    public class Validator {
043        private static final Log LOG = LogFactory.getLog(Validator.class);
044    
045        private static ArrayList<ErrorReport> errorReports = new ArrayList<ErrorReport>();
046        ;
047        private ValidationTrace tracerTemp;
048        private int numberOfErrors;
049        private int numberOfWarnings;
050    
051        /**
052         * Constructor creating an empty validation report
053         */
054        public Validator() {
055            tracerTemp = new ValidationTrace();
056            numberOfErrors = 0;
057            numberOfWarnings = 0;
058        }
059    
060        public static void addErrorReport(ErrorReport report) {
061            errorReports.add(report);
062        }
063    
064        public static void resetErrorReport() {
065            errorReports = new ArrayList<ErrorReport>();
066        }
067    
068        /**
069         * Runs the validations on a collection of beans
070         *
071         * @param beans - Collection of beans being validated
072         * @param failOnWarning - Whether detecting a warning should cause the validation to fail
073         * @return Returns true if the beans past validation
074         */
075        private boolean runValidations(KualiDefaultListableBeanFactory beans, boolean failOnWarning) {
076            LOG.info("Starting Dictionary Validation");
077            resetErrorReport();
078            Map<String, View> uifBeans;
079    
080            try {
081                uifBeans = beans.getBeansOfType(View.class);
082                for (View views : uifBeans.values()) {
083                    try {
084                        ValidationTrace tracer = tracerTemp.getCopy();
085                        if (doValidationOnUIFBean(views)) {
086                            tracer.setValidationStage(ValidationTrace.START_UP);
087                            runValidationsOnComponents(views, tracer);
088                        }
089                    } catch (Exception e) {
090                        String value[] = {views.getId(), "Exception = " + e.getMessage()};
091                        tracerTemp.createError("Error Validating Bean View", value);
092                    }
093                }
094            } catch (Exception e) {
095                String value[] = {"Validation set = views", "Exception = " + e.getMessage()};
096                tracerTemp.createError("Error in Loading Spring Beans", value);
097            }
098    
099            Map<String, DataDictionaryEntry> ddBeans;
100    
101            try {
102                ddBeans = beans.getBeansOfType(DataDictionaryEntry.class);
103                for (DataDictionaryEntry entry : ddBeans.values()) {
104                    try {
105    
106                        ValidationTrace tracer = tracerTemp.getCopy();
107                        tracer.setValidationStage(ValidationTrace.BUILD);
108                        entry.completeValidation(tracer);
109    
110                    } catch (Exception e) {
111                        String value[] = {"Validation set = Data Dictionary Entries", "Exception = " + e.getMessage()};
112                        tracerTemp.createError("Error in Loading Spring Beans", value);
113                    }
114                }
115            } catch (Exception e) {
116                String value[] = {"Validation set = Data Dictionary Entries", "Exception = " + e.getMessage()};
117                tracerTemp.createError("Error in Loading Spring Beans", value);
118            }
119    
120            compileFinalReport();
121    
122            LOG.info("Completed Dictionary Validation");
123    
124            if (numberOfErrors > 0) {
125                return false;
126            }
127            if (failOnWarning) {
128                if (numberOfWarnings > 0) {
129                    return false;
130                }
131            }
132    
133            return true;
134        }
135    
136        /**
137         * Validates a UIF Component
138         *
139         * @param object - The UIF Component to be validated
140         * @param failOnWarning - Whether the validation should fail if warnings are found
141         * @return Returns true if the validation passes
142         */
143        public boolean validate(Component object, boolean failOnWarning) {
144            LOG.info("Starting Dictionary Validation");
145    
146            if (doValidationOnUIFBean(object)) {
147                ValidationTrace tracer = tracerTemp.getCopy();
148                resetErrorReport();
149    
150                tracer.setValidationStage(ValidationTrace.BUILD);
151    
152                LOG.debug("Validating Component: " + object.getId());
153                object.completeValidation(tracer.getCopy());
154    
155                runValidationsOnLifecycle(object, tracer.getCopy());
156    
157                runValidationsOnPrototype(object, tracer.getCopy());
158            }
159    
160            compileFinalReport();
161    
162            LOG.info("Completed Dictionary Validation");
163    
164            if (numberOfErrors > 0) {
165                return false;
166            }
167            if (failOnWarning) {
168                if (numberOfWarnings > 0) {
169                    return false;
170                }
171            }
172    
173            return true;
174        }
175    
176        /**
177         * Validates the beans in a collection of xml files
178         *
179         * @param failOnWarning - Whether detecting a warning should cause the validation to fail
180         * @return Returns true if the beans past validation
181         */
182        public boolean validate(String[] xmlFiles, boolean failOnWarning) {
183            KualiDefaultListableBeanFactory beans = loadBeans(xmlFiles);
184    
185            return runValidations(beans, failOnWarning);
186        }
187    
188        /**
189         * Validates a collection of beans
190         *
191         * @param xmlFiles - The collection of xml files used to load the provided beans
192         * @param loader - The source that was used to load the beans
193         * @param beans - Collection of preloaded beans
194         * @param failOnWarning - Whether detecting a warning should cause the validation to fail
195         * @return Returns true if the beans past validation
196         */
197        public boolean validate(String xmlFiles[], ResourceLoader loader, KualiDefaultListableBeanFactory beans,
198                boolean failOnWarning) {
199            tracerTemp = new ValidationTrace(xmlFiles, loader);
200            return runValidations(beans, failOnWarning);
201        }
202    
203        /**
204         * Runs the validations on a component
205         *
206         * @param component - The component being checked
207         * @param tracer - The current bean trace for the validation line
208         */
209        private void runValidationsOnComponents(Component component, ValidationTrace tracer) {
210    
211            try {
212                ExpressionUtils.populatePropertyExpressionsFromGraph(component, false);
213            } catch (Exception e) {
214                String value[] = {"view = " + component.getId()};
215                tracerTemp.createError("Error Validating Bean View while loading expressions", value);
216            }
217    
218            LOG.debug("Validating View: " + component.getId());
219    
220            try {
221                component.completeValidation(tracer.getCopy());
222            } catch (Exception e) {
223                String value[] = {component.getId()};
224                tracerTemp.createError("Error Validating Bean View", value);
225            }
226    
227            try {
228                runValidationsOnLifecycle(component, tracer.getCopy());
229            } catch (Exception e) {
230                String value[] = {component.getId(), component.getComponentsForLifecycle().size() + "",
231                        "Exception " + e.getMessage()};
232                tracerTemp.createError("Error Validating Bean Lifecycle", value);
233            }
234    
235            try {
236                runValidationsOnPrototype(component, tracer.getCopy());
237            } catch (Exception e) {
238                String value[] = {component.getId(), component.getComponentPrototypes().size() + "",
239                        "Exceptions : " + e.getLocalizedMessage()};
240                tracerTemp.createError("Error Validating Bean Prototypes", value);
241            }
242        }
243    
244        /**
245         * Runs the validations on a components lifecycle items
246         *
247         * @param component - The component whose lifecycle items are being checked
248         * @param tracer - The current bean trace for the validation line
249         */
250        private void runValidationsOnLifecycle(Component component, ValidationTrace tracer) {
251            if (component.getComponentsForLifecycle() == null) {
252                return;
253            }
254            if (!doValidationOnUIFBean(component)) {
255                return;
256            }
257            tracer.addBean(component);
258            for (int j = 0; j < component.getComponentsForLifecycle().size(); j++) {
259                Component temp = component.getComponentsForLifecycle().get(j);
260                if (temp == null) {
261                    continue;
262                }
263                if (tracer.getValidationStage() == ValidationTrace.START_UP) {
264                    ExpressionUtils.populatePropertyExpressionsFromGraph(temp, false);
265                }
266                if (temp.isRender()) {
267                    temp.completeValidation(tracer.getCopy());
268                    runValidationsOnLifecycle(temp, tracer.getCopy());
269                }
270            }
271        }
272    
273        /**
274         * Runs the validations on a components prototypes
275         *
276         * @param component - The component whose prototypes are being checked
277         * @param tracer - The current bean trace for the validation line
278         */
279        private void runValidationsOnPrototype(Component component, ValidationTrace tracer) {
280            if (component.getComponentPrototypes() == null) {
281                return;
282            }
283            if (!doValidationOnUIFBean(component)) {
284                return;
285            }
286            tracer.addBean(component);
287            for (int j = 0; j < component.getComponentPrototypes().size(); j++) {
288                Component temp = component.getComponentPrototypes().get(j);
289                if (temp == null) {
290                    continue;
291                }
292                if (tracer.getValidationStage() == ValidationTrace.START_UP) {
293                    ExpressionUtils.populatePropertyExpressionsFromGraph(temp, false);
294                }
295                if (temp.isRender()) {
296                    temp.completeValidation(tracer.getCopy());
297                    runValidationsOnPrototype(temp, tracer.getCopy());
298                }
299            }
300        }
301    
302        /**
303         * Checks if the component being checked is a default or template component by seeing if its id starts with "uif"
304         *
305         * @param component - The component being checked
306         * @return Returns true if the component is not a default or template
307         */
308        private boolean doValidationOnUIFBean(Component component) {
309            if (component.getId() == null) {
310                return true;
311            }
312            if (component.getId().length() < 3) {
313                return true;
314            }
315            String temp = component.getId().substring(0, 3).toLowerCase();
316            if (temp.contains("uif")) {
317                return false;
318            }
319            return true;
320        }
321    
322        /**
323         * Validates an expression string for correct Spring Expression language syntax
324         *
325         * @param expression - The expression being validated
326         * @return Returns true if the expression is of correct SpringEL syntax
327         */
328        public static boolean validateSpringEL(String expression) {
329            if (expression == null) {
330                return true;
331            }
332            if (expression.compareTo("") == 0) {
333                return true;
334            }
335            if (expression.length() <= 3) {
336                return false;
337            }
338    
339            if (!expression.substring(0, 1).contains("@") || !expression.substring(1, 2).contains("{") ||
340                    !expression.substring(expression.length() - 1, expression.length()).contains("}")) {
341                return false;
342            }
343    
344            expression = expression.substring(2, expression.length() - 2);
345    
346            ArrayList<String> values = getExpressionValues(expression);
347    
348            for (int i = 0; i < values.size(); i++) {
349                checkPropertyName(values.get(i));
350            }
351    
352            return true;
353        }
354    
355        /**
356         * Gets the list of properties from an expression
357         *
358         * @param expression - The expression being validated.
359         * @return A list of properties from the expression.
360         */
361        private static ArrayList<String> getExpressionValues(String expression) {
362            expression = StringUtils.replace(expression, "!=", " != ");
363            expression = StringUtils.replace(expression, "==", " == ");
364            expression = StringUtils.replace(expression, ">", " > ");
365            expression = StringUtils.replace(expression, "<", " < ");
366            expression = StringUtils.replace(expression, "<=", " <= ");
367            expression = StringUtils.replace(expression, ">=", " >= ");
368    
369            String stack = "";
370            ArrayList<String> controlNames = new ArrayList<String>();
371    
372            boolean expectingSingleQuote = false;
373            boolean ignoreNext = false;
374            for (int i = 0; i < expression.length(); i++) {
375                char c = expression.charAt(i);
376                if (!expectingSingleQuote && !ignoreNext && (c == '(' || c == ' ' || c == ')')) {
377                    ExpressionUtils.evaluateCurrentStack(stack.trim(), controlNames);
378                    //reset stack
379                    stack = "";
380                    continue;
381                } else if (!ignoreNext && c == '\'') {
382                    stack = stack + c;
383                    expectingSingleQuote = !expectingSingleQuote;
384                } else if (c == '\\') {
385                    stack = stack + c;
386                    ignoreNext = !ignoreNext;
387                } else {
388                    stack = stack + c;
389                    ignoreNext = false;
390                }
391            }
392    
393            if (StringUtils.isNotEmpty(stack)) {
394                ExpressionUtils.evaluateCurrentStack(stack.trim(), controlNames);
395            }
396    
397            return controlNames;
398        }
399    
400        /**
401         * Checks the property for a valid name.
402         *
403         * @param name - The property name.
404         * @return True if the validation passes, false if not
405         */
406        private static boolean checkPropertyName(String name) {
407            if (!Character.isLetter(name.charAt(0))) {
408                return false;
409            }
410    
411            return true;
412        }
413    
414        /**
415         * Checks if a property of a Component is being set by expressions
416         *
417         * @param object - The Component being checked
418         * @param property - The property being set
419         * @return Returns true if the property is contained in the Components property expressions
420         */
421        public static boolean checkExpressions(Component object, String property) {
422            if (object.getPropertyExpressions().containsKey(property)) {
423                return true;
424            }
425            return false;
426        }
427    
428        /**
429         * Compiles general information on the validation from the list of generated error reports
430         */
431        private void compileFinalReport() {
432            ArrayList<ErrorReport> reports = Validator.errorReports;
433            for (int i = 0; i < reports.size(); i++) {
434                if (reports.get(i).getErrorStatus() == ErrorReport.ERROR) {
435                    numberOfErrors++;
436                } else if (reports.get(i).getErrorStatus() == ErrorReport.WARNING) {
437                    numberOfWarnings++;
438                }
439            }
440        }
441    
442        /**
443         * Loads the Spring Beans from a list of xml files
444         *
445         * @param xmlFiles
446         * @return The Spring Bean Factory for the provided list of xml files
447         */
448        public KualiDefaultListableBeanFactory loadBeans(String[] xmlFiles) {
449    
450            LOG.info("Starting XML File Load");
451            KualiDefaultListableBeanFactory beans = new KualiDefaultListableBeanFactory();
452            XmlBeanDefinitionReader xmlReader = new XmlBeanDefinitionReader(beans);
453    
454            DataDictionary.setupProcessor(beans);
455    
456            ArrayList<String> coreFiles = new ArrayList<String>();
457            ArrayList<String> testFiles = new ArrayList<String>();
458    
459            for (int i = 0; i < xmlFiles.length; i++) {
460                if (xmlFiles[i].contains("classpath")) {
461                    coreFiles.add(xmlFiles[i]);
462                } else {
463                    testFiles.add(xmlFiles[i]);
464                }
465            }
466            String core[] = new String[coreFiles.size()];
467            coreFiles.toArray(core);
468    
469            String test[] = new String[testFiles.size()];
470            testFiles.toArray(test);
471    
472            try {
473                xmlReader.loadBeanDefinitions(core);
474            } catch (Exception e) {
475                LOG.error("Error loading bean definitions", e);
476                throw new DataDictionaryException("Error loading bean definitions: " + e.getLocalizedMessage());
477            }
478    
479            try {
480                xmlReader.loadBeanDefinitions(getResources(test));
481            } catch (Exception e) {
482                LOG.error("Error loading bean definitions", e);
483                throw new DataDictionaryException("Error loading bean definitions: " + e.getLocalizedMessage());
484            }
485    
486            UifBeanFactoryPostProcessor factoryPostProcessor = new UifBeanFactoryPostProcessor();
487            factoryPostProcessor.postProcessBeanFactory(beans);
488    
489            tracerTemp = new ValidationTrace(xmlFiles, xmlReader.getResourceLoader());
490    
491            LOG.info("Completed XML File Load");
492    
493            return beans;
494        }
495    
496        /**
497         * Converts the list of file paths into a list of resources
498         *
499         * @param files The list of file paths for conversion
500         * @return A list of resources created from the file paths
501         */
502        private Resource[] getResources(String files[]) {
503            Resource resources[] = new Resource[files.length];
504            for (int i = 0; i < files.length; i++) {
505                resources[0] = new FileSystemResource(files[i]);
506            }
507    
508            return resources;
509        }
510    
511        /**
512         * Retrieves the number of errors found in the validation
513         *
514         * @return The number of errors found in the validation
515         */
516        public int getNumberOfErrors() {
517            return numberOfErrors;
518        }
519    
520        /**
521         * Retrieves the number of warnings found in the validation
522         *
523         * @return The number of warnings found in the validation
524         */
525        public int getNumberOfWarnings() {
526            return numberOfWarnings;
527        }
528    
529        /**
530         * Retrieves an individual error report for errors found during the validation
531         *
532         * @param index
533         * @return The error report at the provided index
534         */
535        public ErrorReport getErrorReport(int index) {
536            return errorReports.get(index);
537        }
538    
539        /**
540         * Retrieves the number of error reports generated during the validation
541         *
542         * @return The number of ErrorReports
543         */
544        public int getErrorReportSize() {
545            return errorReports.size();
546        }
547    }