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