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; 017 018import java.beans.PropertyDescriptor; 019import java.io.File; 020import java.io.IOException; 021import java.util.ArrayList; 022import java.util.Arrays; 023import java.util.Collection; 024import java.util.HashMap; 025import java.util.List; 026import java.util.Map; 027import java.util.Set; 028import java.util.TreeMap; 029 030import org.apache.commons.beanutils.PropertyUtils; 031import org.apache.commons.collections.ListUtils; 032import org.apache.commons.lang3.ArrayUtils; 033import org.apache.commons.lang.ClassUtils; 034import org.apache.commons.lang.StringUtils; 035import org.kuali.rice.core.api.config.property.ConfigContext; 036import org.kuali.rice.core.api.util.ClassLoaderUtils; 037import org.kuali.rice.krad.data.provider.annotation.UifAutoCreateViewType; 038import org.kuali.rice.krad.datadictionary.exception.AttributeValidationException; 039import org.kuali.rice.krad.datadictionary.exception.CompletionException; 040import org.kuali.rice.krad.datadictionary.parse.StringListConverter; 041import org.kuali.rice.krad.datadictionary.parse.StringMapConverter; 042import org.kuali.rice.krad.datadictionary.uif.ComponentBeanPostProcessor; 043import org.kuali.rice.krad.datadictionary.uif.UifBeanFactoryPostProcessor; 044import org.kuali.rice.krad.datadictionary.uif.UifDictionaryIndex; 045import org.kuali.rice.krad.datadictionary.validator.ErrorReport; 046import org.kuali.rice.krad.datadictionary.validator.ValidationTrace; 047import org.kuali.rice.krad.datadictionary.validator.Validator; 048import org.kuali.rice.krad.lookup.LookupView; 049import org.kuali.rice.krad.service.KRADServiceLocatorWeb; 050import org.kuali.rice.krad.service.LegacyDataAdapter; 051import org.kuali.rice.krad.uif.UifConstants; 052import org.kuali.rice.krad.uif.UifConstants.ViewType; 053import org.kuali.rice.krad.uif.util.ComponentFactory; 054import org.kuali.rice.krad.uif.util.ExpressionFunctions; 055import org.kuali.rice.krad.uif.util.ObjectPropertyUtils; 056import org.kuali.rice.krad.uif.view.InquiryView; 057import org.kuali.rice.krad.uif.view.View; 058import org.kuali.rice.krad.util.KRADConstants; 059import org.slf4j.Logger; 060import org.slf4j.LoggerFactory; 061import org.springframework.beans.PropertyValue; 062import org.springframework.beans.PropertyValues; 063import org.springframework.beans.factory.config.BeanDefinition; 064import org.springframework.beans.factory.config.BeanExpressionContext; 065import org.springframework.beans.factory.config.BeanPostProcessor; 066import org.springframework.beans.factory.config.PropertyPlaceholderConfigurer; 067import org.springframework.beans.factory.config.Scope; 068import org.springframework.beans.factory.support.ChildBeanDefinition; 069import org.springframework.beans.factory.support.DefaultListableBeanFactory; 070import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; 071import org.springframework.context.expression.StandardBeanExpressionResolver; 072import org.springframework.core.convert.support.GenericConversionService; 073import org.springframework.core.io.DefaultResourceLoader; 074import org.springframework.core.io.Resource; 075import org.springframework.expression.spel.support.StandardEvaluationContext; 076import org.springframework.util.StopWatch; 077 078/** 079 * Encapsulates a bean factory and indexes to the beans within the factory for providing 080 * framework metadata 081 * 082 * @author Kuali Rice Team (rice.collab@kuali.org) 083 */ 084public class DataDictionary { 085 086 private static final Logger LOG = LoggerFactory.getLogger(DataDictionary.class); 087 088 protected static boolean validateEBOs = true; 089 090 protected DefaultListableBeanFactory ddBeans = new DefaultListableBeanFactory(); 091 protected XmlBeanDefinitionReader xmlReader = new XmlBeanDefinitionReader(ddBeans); 092 093 protected DataDictionaryIndex ddIndex = new DataDictionaryIndex(ddBeans); 094 protected UifDictionaryIndex uifIndex = new UifDictionaryIndex(ddBeans); 095 096 protected DataDictionaryMapper ddMapper = new DataDictionaryIndexMapper(); 097 098 protected Map<String, List<String>> moduleDictionaryFiles = new HashMap<String, List<String>>(); 099 protected List<String> moduleLoadOrder = new ArrayList<String>(); 100 101 protected ArrayList<String> beanValidationFiles = new ArrayList<String>(); 102 103 public static LegacyDataAdapter legacyDataAdapter; 104 105 protected transient StopWatch timer; 106 107 /** 108 * Populates and processes the dictionary bean factory based on the configured files and 109 * performs indexing 110 * 111 * @param allowConcurrentValidation - indicates whether the indexing should occur on a different thread 112 * or the same thread 113 */ 114 public void parseDataDictionaryConfigurationFiles(boolean allowConcurrentValidation) { 115 timer = new StopWatch("DD Processing"); 116 setupProcessor(ddBeans); 117 118 loadDictionaryBeans(ddBeans, moduleDictionaryFiles, ddIndex, beanValidationFiles); 119 120 performDictionaryPostProcessing(allowConcurrentValidation); 121 } 122 123 /** 124 * Sets up the bean post processor and conversion service 125 * 126 * @param beans - The bean factory for the the dictionary beans 127 */ 128 public static void setupProcessor(DefaultListableBeanFactory beans) { 129 try { 130 // UIF post processor that sets component ids 131 BeanPostProcessor idPostProcessor = ComponentBeanPostProcessor.class.newInstance(); 132 beans.addBeanPostProcessor(idPostProcessor); 133 beans.setBeanExpressionResolver(new StandardBeanExpressionResolver() { 134 @Override 135 protected void customizeEvaluationContext(StandardEvaluationContext evalContext) { 136 try { 137 evalContext.registerFunction("getService", ExpressionFunctions.class.getDeclaredMethod("getService", new Class[]{String.class})); 138 } catch(NoSuchMethodException me) { 139 LOG.error("Unable to register custom expression to data dictionary bean factory", me); 140 } 141 } 142 }); 143 144 // special converters for shorthand map and list property syntax 145 GenericConversionService conversionService = new GenericConversionService(); 146 conversionService.addConverter(new StringMapConverter()); 147 conversionService.addConverter(new StringListConverter()); 148 149 beans.setConversionService(conversionService); 150 } catch (Exception e1) { 151 throw new DataDictionaryException("Cannot create component decorator post processor: " + e1.getMessage(), 152 e1); 153 } 154 } 155 156 /** 157 * Populates and processes the dictionary bean factory based on the configured files 158 * 159 * @param beans - The bean factory for the dictionary bean 160 * @param moduleDictionaryFiles - List of bean xml files 161 * @param index - Index of the data dictionary beans 162 * @param validationFiles - The List of bean xml files loaded into the bean file 163 */ 164 public void loadDictionaryBeans(DefaultListableBeanFactory beans, 165 Map<String, List<String>> moduleDictionaryFiles, DataDictionaryIndex index, 166 ArrayList<String> validationFiles) { 167 // expand configuration locations into files 168 timer.start("XML File Loading"); 169 LOG.info("Starting DD XML File Load"); 170 171 List<String> allBeanNames = new ArrayList<String>(); 172 for (String namespaceCode : moduleLoadOrder) { 173 LOG.info( "Processing Module: " + namespaceCode); 174 List<String> moduleDictionaryLocations = moduleDictionaryFiles.get(namespaceCode); 175 if ( LOG.isDebugEnabled() ) { 176 LOG.debug("DD Locations in Module: " + moduleDictionaryLocations); 177 } 178 179 if (moduleDictionaryLocations == null) { 180 continue; 181 } 182 183 XmlBeanDefinitionReader xmlReader = new XmlBeanDefinitionReader(beans); 184 185 String configFileLocationsArray[] = new String[moduleDictionaryLocations.size()]; 186 configFileLocationsArray = moduleDictionaryLocations.toArray(configFileLocationsArray); 187 for (int i = 0; i < configFileLocationsArray.length; i++) { 188 validationFiles.add(configFileLocationsArray[i]); 189 } 190 191 try { 192 xmlReader.loadBeanDefinitions(configFileLocationsArray); 193 194 // get updated bean names from factory and compare to our previous list to get those that 195 // were added by the last namespace 196 List<String> addedBeanNames = Arrays.asList(beans.getBeanDefinitionNames()); 197 addedBeanNames = ListUtils.removeAll(addedBeanNames, allBeanNames); 198 index.addBeanNamesToNamespace(namespaceCode, addedBeanNames); 199 200 allBeanNames.addAll(addedBeanNames); 201 } catch (Exception e) { 202 throw new DataDictionaryException("Error loading bean definitions: " + e.getLocalizedMessage(),e); 203 } 204 } 205 206 LOG.info("Completed DD XML File Load"); 207 timer.stop(); 208 } 209 210 /** 211 * Invokes post processors and builds indexes for the beans contained in the dictionary 212 * 213 * @param allowConcurrentValidation - indicates whether the indexing should occur on a different thread 214 * or the same thread 215 */ 216 public void performDictionaryPostProcessing(boolean allowConcurrentValidation) { 217 LOG.info("Starting Data Dictionary Post Processing"); 218 219 timer.start("Spring Post Processing"); 220 PropertyPlaceholderConfigurer propertyPlaceholderConfigurer = new PropertyPlaceholderConfigurer(); 221 propertyPlaceholderConfigurer.setProperties(ConfigContext.getCurrentContextConfig().getProperties()); 222 propertyPlaceholderConfigurer.postProcessBeanFactory(ddBeans); 223 224 DictionaryBeanFactoryPostProcessor dictionaryBeanPostProcessor = 225 new DictionaryBeanFactoryPostProcessor(DataDictionary.this, ddBeans); 226 dictionaryBeanPostProcessor.postProcessBeanFactory(); 227 timer.stop(); 228 229 // post processes UIF beans for pulling out expressions within property values 230 timer.start("UIF Post Processing"); 231 UifBeanFactoryPostProcessor factoryPostProcessor = new UifBeanFactoryPostProcessor(); 232 factoryPostProcessor.postProcessBeanFactory(ddBeans); 233 timer.stop(); 234 235 if (ConfigContext.getCurrentContextConfig().getBooleanProperty(KRADConstants.Config.ENABLE_PREINSTANTIATE_BEANS, false)) { 236 timer.start("Instantiating DD Beans"); 237 ddBeans.preInstantiateSingletons(); 238 timer.stop(); 239 } 240 241 // Allow the DD to perform final post processing in a controlled order 242 // Unlike the Spring post processor, we will only call for these operations on the 243 // "top-level" beans and have them call post processing actions on embedded DD objects, if needed 244 timer.start("DD Post Processing"); 245 246 for (DataObjectEntry entry : ddBeans.getBeansOfType(DataObjectEntry.class).values()) { 247 entry.dataDictionaryPostProcessing(); 248 } 249 250 for (DocumentEntry entry : ddBeans.getBeansOfType(DocumentEntry.class).values()) { 251 entry.dataDictionaryPostProcessing(); 252 } 253 254 timer.stop(); 255 256 timer.start("Data Dictionary Indexing"); 257 ddIndex.run(); 258 timer.stop(); 259 260 // the UIF defaulting must be done before the UIF indexing but after the main DD data object indexing 261 if (ConfigContext.getCurrentContextConfig().getBooleanProperty(KRADConstants.Config.ENABLE_VIEW_AUTOGENERATION, false)) { 262 timer.start("UIF Defaulting"); 263 generateMissingInquiryDefinitions(); 264 generateMissingLookupDefinitions(); 265 timer.stop(); 266 } 267 268 timer.start("UIF Indexing"); 269 uifIndex.run(); 270 timer.stop(); 271 272 LOG.info("Completed Data Dictionary Post Processing"); 273 } 274 275 protected void generateMissingInquiryDefinitions() { 276 Collection<InquiryView> inquiryViewBeans = ddBeans.getBeansOfType(InquiryView.class).values(); 277 278 // Index all the inquiry views by the data object class so we can find them easily below 279 Map<Class<?>,InquiryView> defaultViewsByDataObjectClass = new HashMap<Class<?>, InquiryView>(); 280 281 for ( InquiryView view : inquiryViewBeans ) { 282 if ( view.getViewName().equals(UifConstants.DEFAULT_VIEW_NAME) ) { 283 defaultViewsByDataObjectClass.put(view.getDataObjectClassName(), view); 284 } 285 } 286 287 for (DataObjectEntry entry : ddBeans.getBeansOfType(DataObjectEntry.class).values()) { 288 // if an inquiry already exists, just ignore - we only default if none exist 289 if ( defaultViewsByDataObjectClass.containsKey(entry.getDataObjectClass())) { 290 continue; 291 } 292 293 // We only generate the inquiry if the metadata says to 294 if ( entry.getDataObjectMetadata() == null ) { 295 continue; 296 } 297 298 if ( !entry.getDataObjectMetadata().shouldAutoCreateUifViewOfType(UifAutoCreateViewType.INQUIRY)) { 299 continue; 300 } 301 302 // no inquiry exists and we want one to, create one 303 if ( LOG.isInfoEnabled() ) { 304 LOG.info( "Generating Inquiry View for : " + entry.getDataObjectClass() ); 305 } 306 307 String inquiryBeanName = entry.getDataObjectClass().getSimpleName()+"-InquiryView-default"; 308 309 InquiryView inquiryView = KRADServiceLocatorWeb.getUifDefaultingService().deriveInquiryViewFromMetadata(entry); 310 inquiryView.setId(inquiryBeanName); 311 inquiryView.setViewName(UifConstants.DEFAULT_VIEW_NAME); 312 313 ChildBeanDefinition inquiryBean = new ChildBeanDefinition("Uif-InquiryView"); 314 inquiryBean.setScope(BeanDefinition.SCOPE_SINGLETON); 315 inquiryBean.setAttribute("dataObjectClassName", inquiryView.getDataObjectClassName()); 316 inquiryBean.getPropertyValues().add("dataObjectClassName", inquiryView.getDataObjectClassName().getName()); 317 inquiryBean.setResourceDescription("Autogenerated From Metadata"); 318 ddBeans.registerBeanDefinition(inquiryBeanName, inquiryBean); 319 ddBeans.registerSingleton(inquiryBeanName, inquiryView); 320 } 321 } 322 323 protected void generateMissingLookupDefinitions() { 324 Collection<LookupView> lookupViewBeans = ddBeans.getBeansOfType(LookupView.class).values(); 325 // Index all the inquiry views by the data object class so we can find them easily below 326 Map<Class<?>,LookupView> defaultViewsByDataObjectClass = new HashMap<Class<?>, LookupView>(); 327 for ( LookupView view : lookupViewBeans ) { 328 if ( view.getViewName().equals(UifConstants.DEFAULT_VIEW_NAME) ) { 329 defaultViewsByDataObjectClass.put(view.getDataObjectClass(), view); 330 } 331 } 332 for (DataObjectEntry entry : ddBeans.getBeansOfType(DataObjectEntry.class).values()) { 333 // if an inquiry already exists, just ignore - we only default if none exist 334 if ( defaultViewsByDataObjectClass.containsKey(entry.getDataObjectClass())) { 335 continue; 336 } 337 // We only generate the inquiry if the metadata says to 338 if ( entry.getDataObjectMetadata() == null ) { 339 continue; 340 } 341 if ( !entry.getDataObjectMetadata().shouldAutoCreateUifViewOfType(UifAutoCreateViewType.LOOKUP)) { 342 continue; 343 } 344 // no inquiry exists and we want one to, create one 345 if ( LOG.isInfoEnabled() ) { 346 LOG.info( "Generating Lookup View for : " + entry.getDataObjectClass() ); 347 } 348 String lookupBeanName = entry.getDataObjectClass().getSimpleName()+"-LookupView-default"; 349 350 LookupView lookupView = KRADServiceLocatorWeb.getUifDefaultingService().deriveLookupViewFromMetadata(entry); 351 lookupView.setId(lookupBeanName); 352 lookupView.setViewName(UifConstants.DEFAULT_VIEW_NAME); 353 354 ChildBeanDefinition lookupBean = new ChildBeanDefinition(ComponentFactory.LOOKUP_VIEW); 355 lookupBean.setScope(BeanDefinition.SCOPE_SINGLETON); 356 lookupBean.setAttribute("dataObjectClassName", lookupView.getDataObjectClass()); 357 lookupBean.getPropertyValues().add("dataObjectClassName", lookupView.getDataObjectClass().getName()); 358 lookupBean.setResourceDescription("Autogenerated From Metadata"); 359 ddBeans.registerBeanDefinition(lookupBeanName, lookupBean); 360 ddBeans.registerSingleton(lookupBeanName, lookupView); 361 } 362 } 363 364 public void validateDD(boolean validateEbos) { 365 timer.start("Validation"); 366 DataDictionary.validateEBOs = validateEbos; 367 368 Validator.resetErrorReport(); 369 370 Map<String, DataObjectEntry> doBeans = ddBeans.getBeansOfType(DataObjectEntry.class); 371 for (DataObjectEntry entry : doBeans.values()) { 372 entry.completeValidation(new ValidationTrace()); 373 } 374 375 Map<String, DocumentEntry> docBeans = ddBeans.getBeansOfType(DocumentEntry.class); 376 for (DocumentEntry entry : docBeans.values()) { 377 entry.completeValidation(new ValidationTrace()); 378 } 379 380 List<ErrorReport> errorReports = Validator.getErrorReports(); 381 if (!errorReports.isEmpty()) { 382 boolean hasErrors = hasErrors(errorReports); 383 String errorReport = produceErrorReport(errorReports, hasErrors); 384 if (hasErrors) { 385 String message = "Errors during DD validation, failing validation.\n" + errorReport; 386 throw new DataDictionaryException(message); 387 } else { 388 String message = "Warnings during DD validation.\n" + errorReport; 389 LOG.warn(message); 390 } 391 } 392 393 timer.stop(); 394 } 395 396 private boolean hasErrors(List<ErrorReport> errorReports) { 397 for (ErrorReport err : errorReports) { 398 if (err.isError()) { 399 return true; 400 } 401 } 402 return false; 403 } 404 405 protected String produceErrorReport(List<ErrorReport> errorReports, boolean hasErrors) { 406 StringBuilder builder = new StringBuilder(); 407 builder.append("***********************************************************\n"); 408 if (hasErrors) { 409 builder.append("ERRORS REPORTED UPON DATA DICTIONARY VALIDATION\n"); 410 } else { 411 builder.append("WARNINGS REPORTED UPON DATA DICTIONARY VALIDATION\n"); 412 } 413 builder.append("***********************************************************\n"); 414 for (ErrorReport report : errorReports) { 415 builder.append(report.errorMessage()).append("\n"); 416 } 417 return builder.toString(); 418 } 419 420 public void validateDD() { 421 validateDD(true); 422 } 423 424 /** 425 * Adds a location of files or a individual resource to the data dictionary 426 * 427 * <p> 428 * The location can either be an XML file on the classpath or a file or folder location within the 429 * file system. If a folder location is given, the folder and all sub-folders will be traversed and any 430 * XML files will be added to the dictionary 431 * </p> 432 * 433 * @param namespaceCode - namespace the beans loaded from the location should be associated with 434 * @param location - classpath resource or file system location 435 * @throws IOException 436 */ 437 public void addConfigFileLocation(String namespaceCode, String location) throws IOException { 438 // add module to load order so we load in the order modules were configured 439 if (!moduleLoadOrder.contains(namespaceCode)) { 440 moduleLoadOrder.add(namespaceCode); 441 } 442 443 indexSource(namespaceCode, location); 444 } 445 446 /** 447 * Processes a given source for XML files to populate the dictionary with 448 * 449 * @param namespaceCode - namespace the beans loaded from the location should be associated with 450 * @param sourceName - a file system or classpath resource locator 451 * @throws IOException 452 */ 453 protected void indexSource(String namespaceCode, String sourceName) throws IOException { 454 if (sourceName == null) { 455 throw new DataDictionaryException("Source Name given is null"); 456 } 457 458 if (!sourceName.endsWith(".xml")) { 459 Resource resource = getFileResource(sourceName); 460 if (resource.exists()) { 461 try { 462 indexSource(namespaceCode, resource.getFile()); 463 } catch (IOException e) { 464 // ignore resources that exist and cause an error here 465 // they may be directories resident in jar files 466 LOG.debug("Skipped existing resource without absolute file path"); 467 } 468 } else { 469 LOG.warn("Could not find " + sourceName); 470 throw new DataDictionaryException("DD Resource " + sourceName + " not found"); 471 } 472 } else { 473 if (LOG.isDebugEnabled()) { 474 LOG.debug("adding sourceName " + sourceName + " "); 475 } 476 477 Resource resource = getFileResource(sourceName); 478 if (!resource.exists()) { 479 throw new DataDictionaryException("DD Resource " + sourceName + " not found"); 480 } 481 482 addModuleDictionaryFile(namespaceCode, sourceName); 483 } 484 } 485 486 protected Resource getFileResource(String sourceName) { 487 DefaultResourceLoader resourceLoader = new DefaultResourceLoader(ClassLoaderUtils.getDefaultClassLoader()); 488 489 return resourceLoader.getResource(sourceName); 490 } 491 492 protected void indexSource(String namespaceCode, File dir) { 493 for (File file : dir.listFiles()) { 494 if (file.isDirectory()) { 495 indexSource(namespaceCode, file); 496 } else if (file.getName().endsWith(".xml")) { 497 addModuleDictionaryFile(namespaceCode, "file:" + file.getAbsolutePath()); 498 } else { 499 if (LOG.isDebugEnabled()) { 500 LOG.debug("Skipping non xml file " + file.getAbsolutePath() + " in DD load"); 501 } 502 } 503 } 504 } 505 506 /** 507 * Adds a file location to the list of dictionary files for the given namespace code 508 * 509 * @param namespaceCode - namespace to add location for 510 * @param location - file or resource location to add 511 */ 512 protected void addModuleDictionaryFile(String namespaceCode, String location) { 513 List<String> moduleFileLocations = new ArrayList<String>(); 514 if (moduleDictionaryFiles.containsKey(namespaceCode)) { 515 moduleFileLocations = moduleDictionaryFiles.get(namespaceCode); 516 } 517 moduleFileLocations.add(location); 518 519 moduleDictionaryFiles.put(namespaceCode, moduleFileLocations); 520 } 521 522 /** 523 * Mapping of namespace codes to dictionary files that are associated with 524 * that namespace 525 * 526 * @return Map<String, List<String>> where map key is namespace code, and value is list of dictionary 527 * file locations 528 */ 529 public Map<String, List<String>> getModuleDictionaryFiles() { 530 return moduleDictionaryFiles; 531 } 532 533 /** 534 * Setter for the map of module dictionary files 535 * 536 * @param moduleDictionaryFiles 537 */ 538 public void setModuleDictionaryFiles(Map<String, List<String>> moduleDictionaryFiles) { 539 this.moduleDictionaryFiles = moduleDictionaryFiles; 540 } 541 542 /** 543 * Order modules should be loaded into the dictionary 544 * 545 * <p> 546 * Modules are loaded in the order they are found in this list. If not explicity set, they will be loaded in 547 * the order their dictionary file locations are added 548 * </p> 549 * 550 * @return List<String> list of namespace codes indicating the module load order 551 */ 552 public List<String> getModuleLoadOrder() { 553 return moduleLoadOrder; 554 } 555 556 /** 557 * Setter for the list of namespace codes indicating the module load order 558 * 559 * @param moduleLoadOrder 560 */ 561 public void setModuleLoadOrder(List<String> moduleLoadOrder) { 562 this.moduleLoadOrder = moduleLoadOrder; 563 } 564 565 /** 566 * Sets the DataDictionaryMapper 567 * 568 * @param mapper the datadictionary mapper 569 */ 570 public void setDataDictionaryMapper(DataDictionaryMapper mapper) { 571 this.ddMapper = mapper; 572 } 573 574 /** 575 * @param className 576 * @return BusinessObjectEntry for the named class, or null if none exists 577 */ 578 @Deprecated 579 public BusinessObjectEntry getBusinessObjectEntry(String className) { 580 return ddMapper.getBusinessObjectEntry(ddIndex, className); 581 } 582 583 /** 584 * @param className 585 * @return BusinessObjectEntry for the named class, or null if none exists 586 */ 587 public DataObjectEntry getDataObjectEntry(String className) { 588 return ddMapper.getDataObjectEntry(ddIndex, className); 589 } 590 591 /** 592 * This method gets the business object entry for a concrete class 593 * 594 * @param className 595 * @return business object entry 596 */ 597 public BusinessObjectEntry getBusinessObjectEntryForConcreteClass(String className) { 598 return ddMapper.getBusinessObjectEntryForConcreteClass(ddIndex, className); 599 } 600 601 /** 602 * @return List of businessObject classnames 603 */ 604 public List<String> getBusinessObjectClassNames() { 605 return ddMapper.getBusinessObjectClassNames(ddIndex); 606 } 607 608 /** 609 * @return Map of (classname, BusinessObjectEntry) pairs 610 */ 611 public Map<String, BusinessObjectEntry> getBusinessObjectEntries() { 612 return ddMapper.getBusinessObjectEntries(ddIndex); 613 } 614 615 public Map<String, DataObjectEntry> getDataObjectEntries() { 616 return ddMapper.getDataObjectEntries(ddIndex); 617 } 618 619 /** 620 * @param className 621 * @return DataDictionaryEntryBase for the named class, or null if none 622 * exists 623 */ 624 public DataDictionaryEntry getDictionaryObjectEntry(String className) { 625 return ddMapper.getDictionaryObjectEntry(ddIndex, className); 626 } 627 628 /** 629 * Returns the KNS document entry for the given lookup key. The documentTypeDDKey is interpreted 630 * successively in the following ways until a mapping is found (or none if found): 631 * <ol> 632 * <li>KEW/workflow document type</li> 633 * <li>business object class name</li> 634 * <li>maintainable class name</li> 635 * </ol> 636 * This mapping is compiled when DataDictionary files are parsed on startup (or demand). Currently this 637 * means the mapping is static, and one-to-one (one KNS document maps directly to one and only 638 * one key). 639 * 640 * @param documentTypeDDKey the KEW/workflow document type name 641 * @return the KNS DocumentEntry if it exists 642 */ 643 public DocumentEntry getDocumentEntry(String documentTypeDDKey) { 644 return ddMapper.getDocumentEntry(ddIndex, documentTypeDDKey); 645 } 646 647 /** 648 * Note: only MaintenanceDocuments are indexed by businessObject Class 649 * 650 * This is a special case that is referenced in one location. Do we need 651 * another map for this stuff?? 652 * 653 * @param businessObjectClass 654 * @return DocumentEntry associated with the given Class, or null if there 655 * is none 656 */ 657 public MaintenanceDocumentEntry getMaintenanceDocumentEntryForBusinessObjectClass(Class<?> businessObjectClass) { 658 return ddMapper.getMaintenanceDocumentEntryForBusinessObjectClass(ddIndex, businessObjectClass); 659 } 660 661 public Map<String, DocumentEntry> getDocumentEntries() { 662 return ddMapper.getDocumentEntries(ddIndex); 663 } 664 665 /** 666 * Returns the View entry identified by the given id 667 * 668 * @param viewId unique id for view 669 * @return View instance associated with the id 670 */ 671 public View getViewById(String viewId) { 672 return ddMapper.getViewById(uifIndex, viewId); 673 } 674 675 /** 676 * Returns the View entry identified by the given id, meant for view readonly 677 * access (not running the lifecycle but just checking configuration) 678 * 679 * @param viewId unique id for view 680 * @return View instance associated with the id 681 */ 682 public View getImmutableViewById(String viewId) { 683 return ddMapper.getImmutableViewById(uifIndex, viewId); 684 } 685 686 /** 687 * Returns View instance identified by the view type name and index 688 * 689 * @param viewTypeName - type name for the view 690 * @param indexKey - Map of index key parameters, these are the parameters the 691 * indexer used to index the view initially and needs to identify 692 * an unique view instance 693 * @return View instance that matches the given index 694 */ 695 public View getViewByTypeIndex(ViewType viewTypeName, Map<String, String> indexKey) { 696 return ddMapper.getViewByTypeIndex(uifIndex, viewTypeName, indexKey); 697 } 698 699 /** 700 * Returns the view id for the view that matches the given view type and index 701 * 702 * @param viewTypeName type name for the view 703 * @param indexKey Map of index key parameters, these are the parameters the 704 * indexer used to index the view initially and needs to identify 705 * an unique view instance 706 * @return id for the view that matches the view type and index or null if a match is not found 707 */ 708 public String getViewIdByTypeIndex(ViewType viewTypeName, Map<String, String> indexKey) { 709 return ddMapper.getViewIdByTypeIndex(uifIndex, viewTypeName, indexKey); 710 } 711 712 /** 713 * Indicates whether a <code>View</code> exists for the given view type and index information 714 * 715 * @param viewTypeName - type name for the view 716 * @param indexKey - Map of index key parameters, these are the parameters the 717 * indexer used to index the view initially and needs to identify 718 * an unique view instance 719 * @return boolean true if view exists, false if not 720 */ 721 public boolean viewByTypeExist(ViewType viewTypeName, Map<String, String> indexKey) { 722 return ddMapper.viewByTypeExist(uifIndex, viewTypeName, indexKey); 723 } 724 725 /** 726 * Gets all <code>View</code> prototypes configured for the given view type 727 * name 728 * 729 * @param viewTypeName - view type name to retrieve 730 * @return List<View> view prototypes with the given type name, or empty 731 * list 732 */ 733 public List<View> getViewsForType(ViewType viewTypeName) { 734 return ddMapper.getViewsForType(uifIndex, viewTypeName); 735 } 736 737 /** 738 * Returns an object from the dictionary by its spring bean name 739 * 740 * @param beanName id or name for the bean definition 741 * @return Object object instance created or the singleton being maintained 742 */ 743 public Object getDictionaryBean(final String beanName) { 744 return ddBeans.getBean(beanName); 745 } 746 747 /** 748 * Indicates whether the data dictionary contains a bean with the given id 749 * 750 * @param id id of the bean to check for 751 * @return boolean true if dictionary contains bean, false otherwise 752 */ 753 public boolean containsDictionaryBean(String id) { 754 return ddBeans.containsBean(id); 755 } 756 757 /** 758 * Returns a prototype object from the dictionary by its spring bean name 759 * 760 * @param beanName id or name for the bean definition 761 * @return Object object instance created 762 */ 763 public Object getDictionaryPrototype(final String beanName) { 764 if (!ddBeans.isPrototype(beanName)) { 765 throw new IllegalArgumentException("Bean name " + beanName 766 + " doesn't refer to a prototype bean in the data dictionary"); 767 } 768 769 return getDictionaryBean(beanName); 770 } 771 772 /** 773 * Returns a property value for the bean with the given name from the dictionary. 774 * 775 * @param beanName id or name for the bean definition 776 * @param propertyName name of the property to retrieve, must be a valid property configured on 777 * the bean definition 778 * @return Object property value for property 779 */ 780 public Object getDictionaryBeanProperty(String beanName, String propertyName) { 781 Object bean = ddBeans.getSingleton(beanName); 782 if (bean != null) { 783 return ObjectPropertyUtils.getPropertyValue(bean, propertyName); 784 } 785 786 BeanDefinition beanDefinition = ddBeans.getMergedBeanDefinition(beanName); 787 788 if (beanDefinition == null) { 789 throw new RuntimeException("Unable to get bean for bean name: " + beanName); 790 } 791 792 PropertyValues pvs = beanDefinition.getPropertyValues(); 793 if (pvs.contains(propertyName)) { 794 PropertyValue propertyValue = pvs.getPropertyValue(propertyName); 795 796 Object value; 797 if (propertyValue.isConverted()) { 798 value = propertyValue.getConvertedValue(); 799 } else if (propertyValue.getValue() instanceof String) { 800 String unconvertedValue = (String) propertyValue.getValue(); 801 Scope scope = ddBeans.getRegisteredScope(beanDefinition.getScope()); 802 BeanExpressionContext beanExpressionContext = new BeanExpressionContext(ddBeans, scope); 803 804 value = ddBeans.getBeanExpressionResolver().evaluate(unconvertedValue, beanExpressionContext); 805 } else { 806 value = propertyValue.getValue(); 807 } 808 809 return value; 810 } 811 812 return null; 813 } 814 815 /** 816 * Retrieves the configured property values for the view bean definition associated with the given id 817 * 818 * <p> 819 * Since constructing the View object can be expensive, when metadata only is needed this method can be used 820 * to retrieve the configured property values. Note this looks at the merged bean definition 821 * </p> 822 * 823 * @param viewId - id for the view to retrieve 824 * @return PropertyValues configured on the view bean definition, or null if view is not found 825 */ 826 public PropertyValues getViewPropertiesById(String viewId) { 827 return ddMapper.getViewPropertiesById(uifIndex, viewId); 828 } 829 830 /** 831 * Retrieves the configured property values for the view bean definition associated with the given type and 832 * index 833 * 834 * <p> 835 * Since constructing the View object can be expensive, when metadata only is needed this method can be used 836 * to retrieve the configured property values. Note this looks at the merged bean definition 837 * </p> 838 * 839 * @param viewTypeName - type name for the view 840 * @param indexKey - Map of index key parameters, these are the parameters the indexer used to index 841 * the view initially and needs to identify an unique view instance 842 * @return PropertyValues configured on the view bean definition, or null if view is not found 843 */ 844 public PropertyValues getViewPropertiesByType(ViewType viewTypeName, Map<String, String> indexKey) { 845 return ddMapper.getViewPropertiesByType(uifIndex, viewTypeName, indexKey); 846 } 847 848 /** 849 * Retrieves the list of dictionary bean names that are associated with the given namespace code 850 * 851 * @param namespaceCode - namespace code to retrieve associated bean names for 852 * @return List<String> bean names associated with the namespace 853 */ 854 public List<String> getBeanNamesForNamespace(String namespaceCode) { 855 List<String> namespaceBeans = new ArrayList<String>(); 856 857 Map<String, List<String>> dictionaryBeansByNamespace = ddIndex.getDictionaryBeansByNamespace(); 858 if (dictionaryBeansByNamespace.containsKey(namespaceCode)) { 859 namespaceBeans = dictionaryBeansByNamespace.get(namespaceCode); 860 } 861 862 return namespaceBeans; 863 } 864 865 /** 866 * Retrieves the namespace code the given bean name is associated with 867 * 868 * @param beanName - name of the dictionary bean to find namespace code for 869 * @return String namespace code the bean is associated with, or null if a namespace was not found 870 */ 871 public String getNamespaceForBeanDefinition(String beanName) { 872 String beanNamespace = null; 873 874 Map<String, List<String>> dictionaryBeansByNamespace = ddIndex.getDictionaryBeansByNamespace(); 875 for (Map.Entry<String, List<String>> moduleDefinitions : dictionaryBeansByNamespace.entrySet()) { 876 List<String> namespaceBeans = moduleDefinitions.getValue(); 877 if (namespaceBeans.contains(beanName)) { 878 beanNamespace = moduleDefinitions.getKey(); 879 break; 880 } 881 } 882 883 return beanNamespace; 884 } 885 886 /** 887 * @param targetClass 888 * @param propertyName 889 * @return true if the given propertyName names a property of the given class 890 * @throws CompletionException if there is a problem accessing the named property on the given class 891 */ 892 public static boolean isPropertyOf(Class targetClass, String propertyName) { 893 if (targetClass == null) { 894 throw new IllegalArgumentException("invalid (null) targetClass"); 895 } 896 if (StringUtils.isBlank(propertyName)) { 897 throw new IllegalArgumentException("invalid (blank) propertyName"); 898 } 899 try { 900 PropertyDescriptor propertyDescriptor = buildReadDescriptor(targetClass, propertyName); 901 902 return propertyDescriptor != null; 903 } catch ( Exception ex ) { 904 LOG.error( "Exception while obtaining property descriptor for " + targetClass.getName() + "." + propertyName, ex ); 905 return false; 906 } 907 } 908 909 /** 910 * @param targetClass 911 * @param propertyName 912 * @return true if the given propertyName names a Collection property of the given class 913 * @throws CompletionException if there is a problem accessing the named property on the given class 914 */ 915 public static boolean isCollectionPropertyOf(Class targetClass, String propertyName) { 916 boolean isCollectionPropertyOf = false; 917 918 PropertyDescriptor propertyDescriptor = buildReadDescriptor(targetClass, propertyName); 919 if (propertyDescriptor != null) { 920 Class clazz = propertyDescriptor.getPropertyType(); 921 922 if ((clazz != null) && Collection.class.isAssignableFrom(clazz)) { 923 isCollectionPropertyOf = true; 924 } 925 } 926 927 return isCollectionPropertyOf; 928 } 929 930 public static LegacyDataAdapter getLegacyDataAdapter() { 931 if (legacyDataAdapter == null) { 932 legacyDataAdapter = KRADServiceLocatorWeb.getLegacyDataAdapter(); 933 } 934 return legacyDataAdapter; 935 } 936 937 /** 938 * This method determines the Class of the attributeName passed in. Null will be returned if the member is not 939 * available, or if 940 * a reflection exception is thrown. 941 * 942 * @param boClass - Class that the attributeName property exists in. 943 * @param attributeName - Name of the attribute you want a class for. 944 * @return The Class of the attributeName, if the attribute exists on the rootClass. Null otherwise. 945 */ 946 public static Class getAttributeClass(Class boClass, String attributeName) { 947 948 // fail loudly if the attributeName isnt a member of rootClass 949 if (!isPropertyOf(boClass, attributeName)) { 950 throw new AttributeValidationException( 951 "unable to find attribute '" + attributeName + "' in rootClass '" + boClass.getName() + "'"); 952 } 953 954 //Implementing Externalizable Business Object Services... 955 //The boClass can be an interface, hence handling this separately, 956 //since the original method was throwing exception if the class could not be instantiated. 957 if (boClass.isInterface()) { 958 return getAttributeClassWhenBOIsInterface(boClass, attributeName); 959 } else { 960 return getAttributeClassWhenBOIsClass(boClass, attributeName); 961 } 962 963 } 964 965 /** 966 * This method gets the property type of the given attributeName when the bo class is a concrete class 967 * 968 * @param boClass 969 * @param attributeName 970 * @return property type 971 */ 972 private static Class<?> getAttributeClassWhenBOIsClass(Class<?> boClass, String attributeName) { 973 Object boInstance; 974 try { 975 976 //KULRICE-11351 should not differentiate between primitive types and their wrappers during DD validation 977 if (boClass.isPrimitive()) { 978 boClass = ClassUtils.primitiveToWrapper(boClass); 979 } 980 981 boInstance = boClass.newInstance(); 982 } catch (Exception e) { 983 throw new RuntimeException("Unable to instantiate Data Object: " + boClass, e); 984 } 985 986 // attempt to retrieve the class of the property 987 try { 988 return getLegacyDataAdapter().getPropertyType(boInstance, attributeName); 989 } catch (Exception e) { 990 throw new RuntimeException( 991 "Unable to determine property type for: " + boClass.getName() + "." + attributeName, e); 992 } 993 } 994 995 /** 996 * This method gets the property type of the given attributeName when the bo class is an interface 997 * This method will also work if the bo class is not an interface, 998 * but that case requires special handling, hence a separate method getAttributeClassWhenBOIsClass 999 * 1000 * @param boClass 1001 * @param attributeName 1002 * @return property type 1003 */ 1004 private static Class<?> getAttributeClassWhenBOIsInterface(Class<?> boClass, String attributeName) { 1005 if (boClass == null) { 1006 throw new IllegalArgumentException("invalid (null) boClass"); 1007 } 1008 if (StringUtils.isBlank(attributeName)) { 1009 throw new IllegalArgumentException("invalid (blank) attributeName"); 1010 } 1011 1012 PropertyDescriptor propertyDescriptor = null; 1013 1014 String[] intermediateProperties = attributeName.split("\\."); 1015 int lastLevel = intermediateProperties.length - 1; 1016 Class currentClass = boClass; 1017 1018 for (int i = 0; i <= lastLevel; ++i) { 1019 1020 String currentPropertyName = intermediateProperties[i]; 1021 propertyDescriptor = buildSimpleReadDescriptor(currentClass, currentPropertyName); 1022 1023 if (propertyDescriptor != null) { 1024 1025 Class propertyType = propertyDescriptor.getPropertyType(); 1026 if (getLegacyDataAdapter().isExtensionAttribute(currentClass, currentPropertyName, propertyType)) { 1027 propertyType = getLegacyDataAdapter().getExtensionAttributeClass(currentClass, currentPropertyName); 1028 } 1029 if (Collection.class.isAssignableFrom(propertyType)) { 1030 // TODO: determine property type using generics type definition 1031 throw new AttributeValidationException( 1032 "Can't determine the Class of Collection elements because when the business object is an (possibly ExternalizableBusinessObject) interface."); 1033 } else { 1034 currentClass = propertyType; 1035 } 1036 } else { 1037 throw new AttributeValidationException( 1038 "Can't find getter method of " + boClass.getName() + " for property " + attributeName); 1039 } 1040 } 1041 return currentClass; 1042 } 1043 1044 /** 1045 * This method determines the Class of the elements in the collectionName passed in. 1046 * 1047 * @param boClass Class that the collectionName collection exists in. 1048 * @param collectionName the name of the collection you want the element class for 1049 * @return collection element type 1050 */ 1051 public static Class getCollectionElementClass(Class boClass, String collectionName) { 1052 if (boClass == null) { 1053 throw new IllegalArgumentException("invalid (null) boClass"); 1054 } 1055 if (StringUtils.isBlank(collectionName)) { 1056 throw new IllegalArgumentException("invalid (blank) collectionName"); 1057 } 1058 1059 PropertyDescriptor propertyDescriptor = null; 1060 1061 String[] intermediateProperties = collectionName.split("\\."); 1062 Class currentClass = boClass; 1063 1064 for (int i = 0; i < intermediateProperties.length; ++i) { 1065 1066 String currentPropertyName = intermediateProperties[i]; 1067 propertyDescriptor = buildSimpleReadDescriptor(currentClass, currentPropertyName); 1068 1069 if (propertyDescriptor != null) { 1070 1071 Class type = propertyDescriptor.getPropertyType(); 1072 if (Collection.class.isAssignableFrom(type)) { 1073 currentClass = getLegacyDataAdapter().determineCollectionObjectType(currentClass, currentPropertyName); 1074 } else { 1075 currentClass = propertyDescriptor.getPropertyType(); 1076 } 1077 } 1078 } 1079 1080 return currentClass; 1081 } 1082 1083 static private Map<String, Map<String, PropertyDescriptor>> cache = 1084 new TreeMap<String, Map<String, PropertyDescriptor>>(); 1085 1086 /** 1087 * @param propertyClass 1088 * @param propertyName 1089 * @return PropertyDescriptor for the getter for the named property of the given class, if one exists. 1090 */ 1091 public static PropertyDescriptor buildReadDescriptor(Class propertyClass, String propertyName) { 1092 if (propertyClass == null) { 1093 throw new IllegalArgumentException("invalid (null) propertyClass"); 1094 } 1095 if (StringUtils.isBlank(propertyName)) { 1096 throw new IllegalArgumentException("invalid (blank) propertyName"); 1097 } 1098 1099 PropertyDescriptor propertyDescriptor = null; 1100 1101 String[] intermediateProperties = propertyName.split("\\."); 1102 int lastLevel = intermediateProperties.length - 1; 1103 Class currentClass = propertyClass; 1104 1105 for (int i = 0; i <= lastLevel; ++i) { 1106 1107 String currentPropertyName = intermediateProperties[i]; 1108 propertyDescriptor = buildSimpleReadDescriptor(currentClass, currentPropertyName); 1109 1110 if (i < lastLevel) { 1111 1112 if (propertyDescriptor != null) { 1113 1114 Class propertyType = propertyDescriptor.getPropertyType(); 1115 if (getLegacyDataAdapter().isExtensionAttribute(currentClass, currentPropertyName, propertyType)) { 1116 propertyType = getLegacyDataAdapter().getExtensionAttributeClass(currentClass, 1117 currentPropertyName); 1118 } 1119 if (Collection.class.isAssignableFrom(propertyType)) { 1120 currentClass = getLegacyDataAdapter().determineCollectionObjectType(currentClass, currentPropertyName); 1121 } else { 1122 currentClass = propertyType; 1123 } 1124 1125 } 1126 1127 } 1128 1129 } 1130 1131 return propertyDescriptor; 1132 } 1133 1134 /** 1135 * @param propertyClass 1136 * @param propertyName 1137 * @return PropertyDescriptor for the getter for the named property of the given class, if one exists. 1138 */ 1139 public static PropertyDescriptor buildSimpleReadDescriptor(Class propertyClass, String propertyName) { 1140 if (propertyClass == null) { 1141 throw new IllegalArgumentException("invalid (null) propertyClass"); 1142 } 1143 if (StringUtils.isBlank(propertyName)) { 1144 throw new IllegalArgumentException("invalid (blank) propertyName"); 1145 } 1146 1147 PropertyDescriptor p = null; 1148 1149 // check to see if we've cached this descriptor already. if yes, return true. 1150 String propertyClassName = propertyClass.getName(); 1151 Map<String, PropertyDescriptor> m = cache.get(propertyClassName); 1152 if (null != m) { 1153 p = m.get(propertyName); 1154 if (null != p) { 1155 return p; 1156 } 1157 } 1158 1159 // Use PropertyUtils.getPropertyDescriptors instead of manually constructing PropertyDescriptor because of 1160 // issues with introspection and generic/co-variant return types 1161 // See https://issues.apache.org/jira/browse/BEANUTILS-340 for more details 1162 1163 PropertyDescriptor[] descriptors = PropertyUtils.getPropertyDescriptors(propertyClass); 1164 if (ArrayUtils.isNotEmpty(descriptors)) { 1165 for (PropertyDescriptor descriptor : descriptors) { 1166 if (descriptor.getName().equals(propertyName)) { 1167 p = descriptor; 1168 } 1169 } 1170 } 1171 1172 // cache the property descriptor if we found it. 1173 if (p != null) { 1174 if (m == null) { 1175 m = new TreeMap<String, PropertyDescriptor>(); 1176 cache.put(propertyClassName, m); 1177 } 1178 m.put(propertyName, p); 1179 } 1180 1181 return p; 1182 } 1183 1184 public Set<InactivationBlockingMetadata> getAllInactivationBlockingMetadatas(Class blockedClass) { 1185 return ddMapper.getAllInactivationBlockingMetadatas(ddIndex, blockedClass); 1186 } 1187 1188 /** 1189 * This method gathers beans of type BeanOverride and invokes each one's performOverride() method. 1190 */ 1191 // KULRICE-4513 1192 public void performBeanOverrides() { 1193 timer.start("Processing BeanOverride beans"); 1194 Collection<BeanOverride> beanOverrides = ddBeans.getBeansOfType(BeanOverride.class).values(); 1195 1196 if (beanOverrides.isEmpty()) { 1197 LOG.info("DataDictionary.performOverrides(): No beans to override"); 1198 } 1199 for (BeanOverride beanOverride : beanOverrides) { 1200 1201 Object bean = ddBeans.getBean(beanOverride.getBeanName()); 1202 beanOverride.performOverride(bean); 1203 LOG.info("DataDictionary.performOverrides(): Performing override on bean: " + bean.toString()); 1204 } 1205 timer.stop(); 1206 // This is the last hook we have upon startup, so pretty-print the results here 1207 LOG.info( "\n" + timer.prettyPrint() ); 1208 } 1209 1210}