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}