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 }