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}