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