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