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