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 }