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