View Javadoc

1   /**
2    * Copyright 2005-2013 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.StringUtils;
22  import org.apache.commons.logging.Log;
23  import org.apache.commons.logging.LogFactory;
24  import org.kuali.rice.core.api.config.property.ConfigContext;
25  import org.kuali.rice.core.api.util.ClassLoaderUtils;
26  import org.kuali.rice.krad.bo.PersistableBusinessObjectExtension;
27  import org.kuali.rice.krad.datadictionary.exception.AttributeValidationException;
28  import org.kuali.rice.krad.datadictionary.exception.CompletionException;
29  import org.kuali.rice.krad.datadictionary.parse.StringListConverter;
30  import org.kuali.rice.krad.datadictionary.parse.StringMapConverter;
31  import org.kuali.rice.krad.datadictionary.uif.UifDictionaryIndex;
32  import org.kuali.rice.krad.service.KRADServiceLocator;
33  import org.kuali.rice.krad.service.KRADServiceLocatorWeb;
34  import org.kuali.rice.krad.service.LegacyDataAdapter;
35  import org.kuali.rice.krad.service.PersistenceStructureService;
36  import org.kuali.rice.krad.uif.UifConstants.ViewType;
37  import org.kuali.rice.krad.uif.util.ComponentBeanPostProcessor;
38  import org.kuali.rice.krad.uif.util.UifBeanFactoryPostProcessor;
39  import org.kuali.rice.krad.uif.view.View;
40  import org.springframework.beans.PropertyValues;
41  import org.springframework.beans.factory.config.BeanPostProcessor;
42  import org.springframework.beans.factory.config.PropertyPlaceholderConfigurer;
43  import org.springframework.beans.factory.support.DefaultListableBeanFactory;
44  import org.springframework.beans.factory.xml.XmlBeanDefinitionReader;
45  import org.springframework.context.expression.StandardBeanExpressionResolver;
46  import org.springframework.core.convert.support.GenericConversionService;
47  import org.springframework.core.io.DefaultResourceLoader;
48  import org.springframework.core.io.Resource;
49  
50  import java.beans.PropertyDescriptor;
51  import java.io.File;
52  import java.io.IOException;
53  import java.util.ArrayList;
54  import java.util.Arrays;
55  import java.util.Collection;
56  import java.util.HashMap;
57  import java.util.List;
58  import java.util.Map;
59  import java.util.Set;
60  import java.util.TreeMap;
61  
62  /**
63   * Encapsulates a bean factory and indexes to the beans within the factory for providing
64   * framework metadata
65   *
66   * @author Kuali Rice Team (rice.collab@kuali.org)
67   */
68  public class DataDictionary {
69      private static final Log LOG = LogFactory.getLog(DataDictionary.class);
70  
71      protected static boolean validateEBOs = true;
72  
73      protected DefaultListableBeanFactory ddBeans = new DefaultListableBeanFactory();
74      protected XmlBeanDefinitionReader xmlReader = new XmlBeanDefinitionReader(ddBeans);
75  
76      protected DataDictionaryIndex ddIndex = new DataDictionaryIndex(ddBeans);
77      protected UifDictionaryIndex uifIndex = new UifDictionaryIndex(ddBeans);
78  
79      protected DataDictionaryMapper ddMapper = new DataDictionaryIndexMapper();
80  
81      protected Map<String, List<String>> moduleDictionaryFiles = new HashMap<String, List<String>>();
82      protected List<String> moduleLoadOrder = new ArrayList<String>();
83  
84      protected ArrayList<String> beanValidationFiles = new ArrayList<String>();
85  
86      /**
87       * Populates and processes the dictionary bean factory based on the configured files and
88       * performs indexing
89       *
90       * @param allowConcurrentValidation - indicates whether the indexing should occur on a different thread
91       * or the same thread
92       */
93      public void parseDataDictionaryConfigurationFiles(boolean allowConcurrentValidation) {
94          setupProcessor(ddBeans);
95  
96          loadDictionaryBeans(ddBeans, moduleDictionaryFiles, ddIndex, beanValidationFiles);
97  
98          performDictionaryPostProcessing(allowConcurrentValidation);
99      }
100 
101     /**
102      * Sets up the bean post processor and conversion service
103      *
104      * @param beans - The bean factory for the the dictionary beans
105      */
106     public static void setupProcessor(DefaultListableBeanFactory beans) {
107         try {
108             // UIF post processor that sets component ids
109             BeanPostProcessor idPostProcessor = ComponentBeanPostProcessor.class.newInstance();
110             beans.addBeanPostProcessor(idPostProcessor);
111             beans.setBeanExpressionResolver(new StandardBeanExpressionResolver());
112 
113             // special converters for shorthand map and list property syntax
114             GenericConversionService conversionService = new GenericConversionService();
115             conversionService.addConverter(new StringMapConverter());
116             conversionService.addConverter(new StringListConverter());
117 
118             beans.setConversionService(conversionService);
119         } catch (Exception e1) {
120             throw new DataDictionaryException("Cannot create component decorator post processor: " + e1.getMessage(),
121                     e1);
122         }
123     }
124 
125     /**
126      * Populates and processes the dictionary bean factory based on the configured files
127      *
128      * @param beans - The bean factory for the dictionary bean
129      * @param moduleDictionaryFiles - List of bean xml files
130      * @param index - Index of the data dictionary beans
131      * @param validationFiles - The List of bean xml files loaded into the bean file
132      */
133     public void loadDictionaryBeans(DefaultListableBeanFactory beans,
134             Map<String, List<String>> moduleDictionaryFiles, DataDictionaryIndex index,
135             ArrayList<String> validationFiles) {
136         // expand configuration locations into files
137         LOG.info("Starting DD XML File Load");
138 
139         List<String> allBeanNames = new ArrayList<String>();
140         for (String namespaceCode : moduleLoadOrder) {
141             List<String> moduleDictionaryLocations = moduleDictionaryFiles.get(namespaceCode);
142 
143             if (moduleDictionaryLocations == null) {
144                continue;
145             }
146 
147             XmlBeanDefinitionReader xmlReader = new XmlBeanDefinitionReader(beans);
148 
149             String configFileLocationsArray[] = new String[moduleDictionaryLocations.size()];
150             configFileLocationsArray = moduleDictionaryLocations.toArray(configFileLocationsArray);
151             for (int i = 0; i < configFileLocationsArray.length; i++) {
152                 validationFiles.add(configFileLocationsArray[i]);
153             }
154 
155             try {
156                 xmlReader.loadBeanDefinitions(configFileLocationsArray);
157 
158                 // get updated bean names from factory and compare to our previous list to get those that
159                 // were added by the last namespace
160                 List<String> addedBeanNames = Arrays.asList(beans.getBeanDefinitionNames());
161                 addedBeanNames = ListUtils.removeAll(addedBeanNames, allBeanNames);
162                 index.addBeanNamesToNamespace(namespaceCode, addedBeanNames);
163 
164                 allBeanNames.addAll(addedBeanNames);
165             } catch (Exception e) {
166                 throw new DataDictionaryException("Error loading bean definitions: " + e.getLocalizedMessage());
167             }
168         }
169 
170         LOG.info("Completed DD XML File Load");
171     }
172 
173     /**
174      * Invokes post processors and builds indexes for the beans contained in the dictionary
175      *
176      * @param allowConcurrentValidation - indicates whether the indexing should occur on a different thread
177      * or the same thread
178      */
179     public void performDictionaryPostProcessing(boolean allowConcurrentValidation) {
180         PropertyPlaceholderConfigurer propertyPlaceholderConfigurer = new PropertyPlaceholderConfigurer();
181         propertyPlaceholderConfigurer.setProperties(ConfigContext.getCurrentContextConfig().getProperties());
182         propertyPlaceholderConfigurer.postProcessBeanFactory(ddBeans);
183 
184         DictionaryBeanFactoryPostProcessor dictionaryBeanPostProcessor = new DictionaryBeanFactoryPostProcessor(this,
185                 ddBeans);
186         dictionaryBeanPostProcessor.postProcessBeanFactory();
187 
188         // post processes UIF beans for pulling out expressions within property values
189         UifBeanFactoryPostProcessor factoryPostProcessor = new UifBeanFactoryPostProcessor();
190         factoryPostProcessor.postProcessBeanFactory(ddBeans);
191 
192         if (allowConcurrentValidation) {
193             Thread t = new Thread(ddIndex);
194             t.start();
195 
196             Thread t2 = new Thread(uifIndex);
197             t2.start();
198         } else {
199             ddIndex.run();
200             uifIndex.run();
201         }
202     }
203 
204     public void validateDD(boolean validateEbos) {
205         DataDictionary.validateEBOs = validateEbos;
206 
207         /*  ValidationController validator = new ValidationController();
208     String files[] = new String[beanValidationFiles.size()];
209     files = beanValidationFiles.toArray(files);
210     validator.validate(files, xmlReader.getResourceLoader(), ddBeans, LOG, false);*/
211 
212         Map<String, DataObjectEntry> doBeans = ddBeans.getBeansOfType(DataObjectEntry.class);
213         for (DataObjectEntry entry : doBeans.values()) {
214             entry.completeValidation();
215         }
216 
217         Map<String, DocumentEntry> docBeans = ddBeans.getBeansOfType(DocumentEntry.class);
218         for (DocumentEntry entry : docBeans.values()) {
219             entry.completeValidation();
220         }
221     }
222 
223     public void validateDD() {
224         validateDD(true);
225     }
226 
227     /**
228      * Adds a location of files or a individual resource to the data dictionary
229      *
230      * <p>
231      * The location can either be an XML file on the classpath or a file or folder location within the
232      * file system. If a folder location is given, the folder and all sub-folders will be traversed and any
233      * XML files will be added to the dictionary
234      * </p>
235      *
236      * @param namespaceCode - namespace the beans loaded from the location should be associated with
237      * @param location - classpath resource or file system location
238      * @throws IOException
239      */
240     public void addConfigFileLocation(String namespaceCode, String location) throws IOException {
241         // add module to load order so we load in the order modules were configured
242         if (!moduleLoadOrder.contains(namespaceCode)) {
243             moduleLoadOrder.add(namespaceCode);
244         }
245 
246         indexSource(namespaceCode, location);
247     }
248 
249     /**
250      * Processes a given source for XML files to populate the dictionary with
251      *
252      * @param namespaceCode - namespace the beans loaded from the location should be associated with
253      * @param sourceName - a file system or classpath resource locator
254      * @throws IOException
255      */
256     protected void indexSource(String namespaceCode, String sourceName) throws IOException {
257         if (sourceName == null) {
258             throw new DataDictionaryException("Source Name given is null");
259         }
260 
261         if (!sourceName.endsWith(".xml")) {
262             Resource resource = getFileResource(sourceName);
263             if (resource.exists()) {
264                 try {
265                     indexSource(namespaceCode, resource.getFile());
266                 } catch (IOException e) {
267                     // ignore resources that exist and cause an error here
268                     // they may be directories resident in jar files
269                     LOG.debug("Skipped existing resource without absolute file path");
270                 }
271             } else {
272                 LOG.warn("Could not find " + sourceName);
273                 throw new DataDictionaryException("DD Resource " + sourceName + " not found");
274             }
275         } else {
276             if (LOG.isDebugEnabled()) {
277                 LOG.debug("adding sourceName " + sourceName + " ");
278             }
279 
280             Resource resource = getFileResource(sourceName);
281             if (!resource.exists()) {
282                 throw new DataDictionaryException("DD Resource " + sourceName + " not found");
283             }
284 
285             addModuleDictionaryFile(namespaceCode, sourceName);
286         }
287     }
288 
289     protected Resource getFileResource(String sourceName) {
290         DefaultResourceLoader resourceLoader = new DefaultResourceLoader(ClassLoaderUtils.getDefaultClassLoader());
291 
292         return resourceLoader.getResource(sourceName);
293     }
294 
295     protected void indexSource(String namespaceCode, File dir) {
296         for (File file : dir.listFiles()) {
297             if (file.isDirectory()) {
298                 indexSource(namespaceCode, file);
299             } else if (file.getName().endsWith(".xml")) {
300                 addModuleDictionaryFile(namespaceCode, "file:" + file.getAbsolutePath());
301             } else {
302                 if (LOG.isDebugEnabled()) {
303                     LOG.debug("Skipping non xml file " + file.getAbsolutePath() + " in DD load");
304                 }
305             }
306         }
307     }
308 
309     /**
310      * Adds a file location to the list of dictionary files for the given namespace code
311      *
312      * @param namespaceCode - namespace to add location for
313      * @param location - file or resource location to add
314      */
315     protected void addModuleDictionaryFile(String namespaceCode, String location) {
316         List<String> moduleFileLocations = new ArrayList<String>();
317         if (moduleDictionaryFiles.containsKey(namespaceCode)) {
318             moduleFileLocations = moduleDictionaryFiles.get(namespaceCode);
319         }
320         moduleFileLocations.add(location);
321 
322         moduleDictionaryFiles.put(namespaceCode, moduleFileLocations);
323     }
324 
325     /**
326      * Mapping of namespace codes to dictionary files that are associated with
327      * that namespace
328      *
329      * @return Map<String, List<String>> where map key is namespace code, and value is list of dictionary
330      *         file locations
331      */
332     public Map<String, List<String>> getModuleDictionaryFiles() {
333         return moduleDictionaryFiles;
334     }
335 
336     /**
337      * Setter for the map of module dictionary files
338      *
339      * @param moduleDictionaryFiles
340      */
341     public void setModuleDictionaryFiles(Map<String, List<String>> moduleDictionaryFiles) {
342         this.moduleDictionaryFiles = moduleDictionaryFiles;
343     }
344 
345     /**
346      * Order modules should be loaded into the dictionary
347      *
348      * <p>
349      * Modules are loaded in the order they are found in this list. If not explicity set, they will be loaded in
350      * the order their dictionary file locations are added
351      * </p>
352      *
353      * @return List<String> list of namespace codes indicating the module load order
354      */
355     public List<String> getModuleLoadOrder() {
356         return moduleLoadOrder;
357     }
358 
359     /**
360      * Setter for the list of namespace codes indicating the module load order
361      *
362      * @param moduleLoadOrder
363      */
364     public void setModuleLoadOrder(List<String> moduleLoadOrder) {
365         this.moduleLoadOrder = moduleLoadOrder;
366     }
367 
368     /**
369      * Sets the DataDictionaryMapper
370      *
371      * @param mapper the datadictionary mapper
372      */
373     public void setDataDictionaryMapper(DataDictionaryMapper mapper) {
374         this.ddMapper = mapper;
375     }
376 
377     /**
378      * @param className
379      * @return BusinessObjectEntry for the named class, or null if none exists
380      */
381     @Deprecated
382     public BusinessObjectEntry getBusinessObjectEntry(String className) {
383         return ddMapper.getBusinessObjectEntry(ddIndex, className);
384     }
385 
386     /**
387      * @param className
388      * @return BusinessObjectEntry for the named class, or null if none exists
389      */
390     public DataObjectEntry getDataObjectEntry(String className) {
391         return ddMapper.getDataObjectEntry(ddIndex, className);
392     }
393 
394     /**
395      * This method gets the business object entry for a concrete class
396      *
397      * @param className
398      * @return
399      */
400     public BusinessObjectEntry getBusinessObjectEntryForConcreteClass(String className) {
401         return ddMapper.getBusinessObjectEntryForConcreteClass(ddIndex, className);
402     }
403 
404     /**
405      * @return List of businessObject classnames
406      */
407     public List<String> getBusinessObjectClassNames() {
408         return ddMapper.getBusinessObjectClassNames(ddIndex);
409     }
410 
411     /**
412      * @return Map of (classname, BusinessObjectEntry) pairs
413      */
414     public Map<String, BusinessObjectEntry> getBusinessObjectEntries() {
415         return ddMapper.getBusinessObjectEntries(ddIndex);
416     }
417 
418     /**
419      * @param className
420      * @return DataDictionaryEntryBase for the named class, or null if none
421      *         exists
422      */
423     public DataDictionaryEntry getDictionaryObjectEntry(String className) {
424         return ddMapper.getDictionaryObjectEntry(ddIndex, className);
425     }
426 
427     /**
428      * Returns the KNS document entry for the given lookup key.  The documentTypeDDKey is interpreted
429      * successively in the following ways until a mapping is found (or none if found):
430      * <ol>
431      * <li>KEW/workflow document type</li>
432      * <li>business object class name</li>
433      * <li>maintainable class name</li>
434      * </ol>
435      * This mapping is compiled when DataDictionary files are parsed on startup (or demand).  Currently this
436      * means the mapping is static, and one-to-one (one KNS document maps directly to one and only
437      * one key).
438      *
439      * @param documentTypeDDKey the KEW/workflow document type name
440      * @return the KNS DocumentEntry if it exists
441      */
442     public DocumentEntry getDocumentEntry(String documentTypeDDKey) {
443         return ddMapper.getDocumentEntry(ddIndex, documentTypeDDKey);
444     }
445 
446     /**
447      * Note: only MaintenanceDocuments are indexed by businessObject Class
448      *
449      * This is a special case that is referenced in one location. Do we need
450      * another map for this stuff??
451      *
452      * @param businessObjectClass
453      * @return DocumentEntry associated with the given Class, or null if there
454      *         is none
455      */
456     public MaintenanceDocumentEntry getMaintenanceDocumentEntryForBusinessObjectClass(Class<?> businessObjectClass) {
457         return ddMapper.getMaintenanceDocumentEntryForBusinessObjectClass(ddIndex, businessObjectClass);
458     }
459 
460     public Map<String, DocumentEntry> getDocumentEntries() {
461         return ddMapper.getDocumentEntries(ddIndex);
462     }
463 
464     /**
465      * Returns the View entry identified by the given id
466      *
467      * @param viewId unique id for view
468      * @return View instance associated with the id
469      */
470     public View getViewById(String viewId) {
471         return ddMapper.getViewById(uifIndex, viewId);
472     }
473 
474     /**
475      * Returns the View entry identified by the given id, meant for view readonly
476      * access (not running the lifecycle but just checking configuration)
477      *
478      * @param viewId unique id for view
479      * @return View instance associated with the id
480      */
481     public View getImmutableViewById(String viewId) {
482         return ddMapper.getImmutableViewById(uifIndex, viewId);
483     }
484 
485     /**
486      * Returns View instance identified by the view type name and index
487      *
488      * @param viewTypeName - type name for the view
489      * @param indexKey - Map of index key parameters, these are the parameters the
490      * indexer used to index the view initially and needs to identify
491      * an unique view instance
492      * @return View instance that matches the given index
493      */
494     public View getViewByTypeIndex(ViewType viewTypeName, Map<String, String> indexKey) {
495         return ddMapper.getViewByTypeIndex(uifIndex, viewTypeName, indexKey);
496     }
497 
498     /**
499      * Returns the view id for the view that matches the given view type and index
500      *
501      * @param viewTypeName type name for the view
502      * @param indexKey Map of index key parameters, these are the parameters the
503      * indexer used to index the view initially and needs to identify
504      * an unique view instance
505      * @return id for the view that matches the view type and index or null if a match is not found
506      */
507     public String getViewIdByTypeIndex(ViewType viewTypeName, Map<String, String> indexKey) {
508         return ddMapper.getViewIdByTypeIndex(uifIndex, viewTypeName, indexKey);
509     }
510 
511     /**
512      * Indicates whether a <code>View</code> exists for the given view type and index information
513      *
514      * @param viewTypeName - type name for the view
515      * @param indexKey - Map of index key parameters, these are the parameters the
516      * indexer used to index the view initially and needs to identify
517      * an unique view instance
518      * @return boolean true if view exists, false if not
519      */
520     public boolean viewByTypeExist(ViewType viewTypeName, Map<String, String> indexKey) {
521         return ddMapper.viewByTypeExist(uifIndex, viewTypeName, indexKey);
522     }
523 
524     /**
525      * Gets all <code>View</code> prototypes configured for the given view type
526      * name
527      *
528      * @param viewTypeName - view type name to retrieve
529      * @return List<View> view prototypes with the given type name, or empty
530      *         list
531      */
532     public List<View> getViewsForType(ViewType viewTypeName) {
533         return ddMapper.getViewsForType(uifIndex, viewTypeName);
534     }
535 
536     /**
537      * Returns an object from the dictionary by its spring bean name
538      *
539      * @param beanName - id or name for the bean definition
540      * @return Object object instance created or the singleton being maintained
541      */
542     public Object getDictionaryObject(String beanName) {
543         return ddBeans.getBean(beanName);
544     }
545 
546     /**
547      * Indicates whether the data dictionary contains a bean with the given id
548      *
549      * @param id - id of the bean to check for
550      * @return boolean true if dictionary contains bean, false otherwise
551      */
552     public boolean containsDictionaryObject(String id) {
553         return ddBeans.containsBean(id);
554     }
555 
556     /**
557      * Retrieves the configured property values for the view bean definition associated with the given id
558      *
559      * <p>
560      * Since constructing the View object can be expensive, when metadata only is needed this method can be used
561      * to retrieve the configured property values. Note this looks at the merged bean definition
562      * </p>
563      *
564      * @param viewId - id for the view to retrieve
565      * @return PropertyValues configured on the view bean definition, or null if view is not found
566      */
567     public PropertyValues getViewPropertiesById(String viewId) {
568         return ddMapper.getViewPropertiesById(uifIndex, viewId);
569     }
570 
571     /**
572      * Retrieves the configured property values for the view bean definition associated with the given type and
573      * index
574      *
575      * <p>
576      * Since constructing the View object can be expensive, when metadata only is needed this method can be used
577      * to retrieve the configured property values. Note this looks at the merged bean definition
578      * </p>
579      *
580      * @param viewTypeName - type name for the view
581      * @param indexKey - Map of index key parameters, these are the parameters the indexer used to index
582      * the view initially and needs to identify an unique view instance
583      * @return PropertyValues configured on the view bean definition, or null if view is not found
584      */
585     public PropertyValues getViewPropertiesByType(ViewType viewTypeName, Map<String, String> indexKey) {
586         return ddMapper.getViewPropertiesByType(uifIndex, viewTypeName, indexKey);
587     }
588 
589     /**
590      * Retrieves the list of dictionary bean names that are associated with the given namespace code
591      *
592      * @param namespaceCode - namespace code to retrieve associated bean names for
593      * @return List<String> bean names associated with the namespace
594      */
595     public List<String> getBeanNamesForNamespace(String namespaceCode) {
596         List<String> namespaceBeans = new ArrayList<String>();
597 
598         Map<String, List<String>> dictionaryBeansByNamespace = ddIndex.getDictionaryBeansByNamespace();
599         if (dictionaryBeansByNamespace.containsKey(namespaceCode)) {
600             namespaceBeans = dictionaryBeansByNamespace.get(namespaceCode);
601         }
602 
603         return namespaceBeans;
604     }
605 
606     /**
607      * Retrieves the namespace code the given bean name is associated with
608      *
609      * @param beanName - name of the dictionary bean to find namespace code for
610      * @return String namespace code the bean is associated with, or null if a namespace was not found
611      */
612     public String getNamespaceForBeanDefinition(String beanName) {
613         String beanNamespace = null;
614 
615         Map<String, List<String>> dictionaryBeansByNamespace = ddIndex.getDictionaryBeansByNamespace();
616         for (Map.Entry<String, List<String>> moduleDefinitions : dictionaryBeansByNamespace.entrySet()) {
617             List<String> namespaceBeans = moduleDefinitions.getValue();
618             if (namespaceBeans.contains(beanName)) {
619                 beanNamespace = moduleDefinitions.getKey();
620                 break;
621             }
622         }
623 
624         return beanNamespace;
625     }
626 
627     /**
628      * @param targetClass
629      * @param propertyName
630      * @return true if the given propertyName names a property of the given class
631      * @throws CompletionException if there is a problem accessing the named property on the given class
632      */
633     public static boolean isPropertyOf(Class targetClass, String propertyName) {
634         if (targetClass == null) {
635             throw new IllegalArgumentException("invalid (null) targetClass");
636         }
637         if (StringUtils.isBlank(propertyName)) {
638             throw new IllegalArgumentException("invalid (blank) propertyName");
639         }
640         try {
641             PropertyDescriptor propertyDescriptor = buildReadDescriptor(targetClass, propertyName);
642 
643             return propertyDescriptor != null;
644         } catch ( Exception ex ) {
645             LOG.error( "Exception while obtaining property descriptor for " + targetClass.getName() + "." + propertyName, ex );
646             return false;
647         }
648     }
649 
650     /**
651      * @param targetClass
652      * @param propertyName
653      * @return true if the given propertyName names a Collection property of the given class
654      * @throws CompletionException if there is a problem accessing the named property on the given class
655      */
656     public static boolean isCollectionPropertyOf(Class targetClass, String propertyName) {
657         boolean isCollectionPropertyOf = false;
658 
659         PropertyDescriptor propertyDescriptor = buildReadDescriptor(targetClass, propertyName);
660         if (propertyDescriptor != null) {
661             Class clazz = propertyDescriptor.getPropertyType();
662 
663             if ((clazz != null) && Collection.class.isAssignableFrom(clazz)) {
664                 isCollectionPropertyOf = true;
665             }
666         }
667 
668         return isCollectionPropertyOf;
669     }
670 
671     public static PersistenceStructureService persistenceStructureService;
672 
673     /**
674      * @return the persistenceStructureService
675      */
676     public static PersistenceStructureService getPersistenceStructureService() {
677         if (persistenceStructureService == null) {
678             persistenceStructureService = KRADServiceLocator.getPersistenceStructureService();
679         }
680         return persistenceStructureService;
681     }
682 
683     public static LegacyDataAdapter legacyDataAdapter;
684 
685     public static LegacyDataAdapter getLegacyDataAdapter() {
686         if (legacyDataAdapter == null) {
687             legacyDataAdapter = KRADServiceLocatorWeb.getLegacyDataAdapter();
688         }
689         return legacyDataAdapter;
690     }
691 
692     /**
693      * This method determines the Class of the attributeName passed in. Null will be returned if the member is not
694      * available, or if
695      * a reflection exception is thrown.
696      *
697      * @param boClass - Class that the attributeName property exists in.
698      * @param attributeName - Name of the attribute you want a class for.
699      * @return The Class of the attributeName, if the attribute exists on the rootClass. Null otherwise.
700      */
701     public static Class getAttributeClass(Class boClass, String attributeName) {
702 
703         // fail loudly if the attributeName isnt a member of rootClass
704         if (!isPropertyOf(boClass, attributeName)) {
705             throw new AttributeValidationException(
706                     "unable to find attribute '" + attributeName + "' in rootClass '" + boClass.getName() + "'");
707         }
708 
709         //Implementing Externalizable Business Object Services...
710         //The boClass can be an interface, hence handling this separately,
711         //since the original method was throwing exception if the class could not be instantiated.
712         if (boClass.isInterface()) {
713             return getAttributeClassWhenBOIsInterface(boClass, attributeName);
714         } else {
715             return getAttributeClassWhenBOIsClass(boClass, attributeName);
716         }
717 
718     }
719 
720     /**
721      * This method gets the property type of the given attributeName when the bo class is a concrete class
722      *
723      * @param boClass
724      * @param attributeName
725      * @return
726      */
727     private static Class getAttributeClassWhenBOIsClass(Class boClass, String attributeName) {
728         Object boInstance;
729         try {
730             boInstance = boClass.newInstance();
731         } catch (Exception e) {
732             throw new RuntimeException("Unable to instantiate Data Object: " + boClass, e);
733         }
734 
735         // attempt to retrieve the class of the property
736         try {
737             return getLegacyDataAdapter().getPropertyType(boInstance, attributeName);
738         } catch (Exception e) {
739             throw new RuntimeException(
740                     "Unable to determine property type for: " + boClass.getName() + "." + attributeName, e);
741         }
742     }
743 
744     /**
745      * This method gets the property type of the given attributeName when the bo class is an interface
746      * This method will also work if the bo class is not an interface,
747      * but that case requires special handling, hence a separate method getAttributeClassWhenBOIsClass
748      *
749      * @param boClass
750      * @param attributeName
751      * @return
752      */
753     private static Class getAttributeClassWhenBOIsInterface(Class boClass, String attributeName) {
754         if (boClass == null) {
755             throw new IllegalArgumentException("invalid (null) boClass");
756         }
757         if (StringUtils.isBlank(attributeName)) {
758             throw new IllegalArgumentException("invalid (blank) attributeName");
759         }
760 
761         PropertyDescriptor propertyDescriptor = null;
762 
763         String[] intermediateProperties = attributeName.split("\\.");
764         int lastLevel = intermediateProperties.length - 1;
765         Class currentClass = boClass;
766 
767         for (int i = 0; i <= lastLevel; ++i) {
768 
769             String currentPropertyName = intermediateProperties[i];
770             propertyDescriptor = buildSimpleReadDescriptor(currentClass, currentPropertyName);
771 
772             if (propertyDescriptor != null) {
773 
774                 Class propertyType = propertyDescriptor.getPropertyType();
775                 if (propertyType.equals(PersistableBusinessObjectExtension.class)) {
776                     propertyType = getPersistenceStructureService().getBusinessObjectAttributeClass(currentClass,
777                             currentPropertyName);
778                 }
779                 if (Collection.class.isAssignableFrom(propertyType)) {
780                     // TODO: determine property type using generics type definition
781                     throw new AttributeValidationException(
782                             "Can't determine the Class of Collection elements because when the business object is an (possibly ExternalizableBusinessObject) interface.");
783                 } else {
784                     currentClass = propertyType;
785                 }
786             } else {
787                 throw new AttributeValidationException(
788                         "Can't find getter method of " + boClass.getName() + " for property " + attributeName);
789             }
790         }
791         return currentClass;
792     }
793 
794     /**
795      * This method determines the Class of the elements in the collectionName passed in.
796      *
797      * @param boClass Class that the collectionName collection exists in.
798      * @param collectionName the name of the collection you want the element class for
799      * @return
800      */
801     public static Class getCollectionElementClass(Class boClass, String collectionName) {
802         if (boClass == null) {
803             throw new IllegalArgumentException("invalid (null) boClass");
804         }
805         if (StringUtils.isBlank(collectionName)) {
806             throw new IllegalArgumentException("invalid (blank) collectionName");
807         }
808 
809         PropertyDescriptor propertyDescriptor = null;
810 
811         String[] intermediateProperties = collectionName.split("\\.");
812         Class currentClass = boClass;
813 
814         for (int i = 0; i < intermediateProperties.length; ++i) {
815 
816             String currentPropertyName = intermediateProperties[i];
817             propertyDescriptor = buildSimpleReadDescriptor(currentClass, currentPropertyName);
818 
819             if (propertyDescriptor != null) {
820 
821                 Class type = propertyDescriptor.getPropertyType();
822                 if (Collection.class.isAssignableFrom(type)) {
823                     currentClass = getLegacyDataAdapter().determineCollectionObjectType(currentClass, currentPropertyName);
824                 } else {
825                     currentClass = propertyDescriptor.getPropertyType();
826                 }
827             }
828         }
829 
830         return currentClass;
831     }
832 
833     static private Map<String, Map<String, PropertyDescriptor>> cache =
834             new TreeMap<String, Map<String, PropertyDescriptor>>();
835 
836     /**
837      * @param propertyClass
838      * @param propertyName
839      * @return PropertyDescriptor for the getter for the named property of the given class, if one exists.
840      */
841     public static PropertyDescriptor buildReadDescriptor(Class propertyClass, String propertyName) {
842         if (propertyClass == null) {
843             throw new IllegalArgumentException("invalid (null) propertyClass");
844         }
845         if (StringUtils.isBlank(propertyName)) {
846             throw new IllegalArgumentException("invalid (blank) propertyName");
847         }
848 
849         PropertyDescriptor propertyDescriptor = null;
850 
851         String[] intermediateProperties = propertyName.split("\\.");
852         int lastLevel = intermediateProperties.length - 1;
853         Class currentClass = propertyClass;
854 
855         for (int i = 0; i <= lastLevel; ++i) {
856 
857             String currentPropertyName = intermediateProperties[i];
858             propertyDescriptor = buildSimpleReadDescriptor(currentClass, currentPropertyName);
859 
860             if (i < lastLevel) {
861 
862                 if (propertyDescriptor != null) {
863 
864                     Class propertyType = propertyDescriptor.getPropertyType();
865                     if (propertyType.equals(PersistableBusinessObjectExtension.class)) {
866                         propertyType = getPersistenceStructureService().getBusinessObjectAttributeClass(currentClass,
867                                 currentPropertyName);
868                     }
869                     if (Collection.class.isAssignableFrom(propertyType)) {
870                         currentClass = getLegacyDataAdapter().determineCollectionObjectType(currentClass, currentPropertyName);
871                     } else {
872                         currentClass = propertyType;
873                     }
874 
875                 }
876 
877             }
878 
879         }
880 
881         return propertyDescriptor;
882     }
883 
884     /**
885      * @param propertyClass
886      * @param propertyName
887      * @return PropertyDescriptor for the getter for the named property of the given class, if one exists.
888      */
889     public static PropertyDescriptor buildSimpleReadDescriptor(Class propertyClass, String propertyName) {
890         if (propertyClass == null) {
891             throw new IllegalArgumentException("invalid (null) propertyClass");
892         }
893         if (StringUtils.isBlank(propertyName)) {
894             throw new IllegalArgumentException("invalid (blank) propertyName");
895         }
896 
897         PropertyDescriptor p = null;
898 
899         // check to see if we've cached this descriptor already. if yes, return true.
900         String propertyClassName = propertyClass.getName();
901         Map<String, PropertyDescriptor> m = cache.get(propertyClassName);
902         if (null != m) {
903             p = m.get(propertyName);
904             if (null != p) {
905                 return p;
906             }
907         }
908 
909         // Use PropertyUtils.getPropertyDescriptors instead of manually constructing PropertyDescriptor because of
910         // issues with introspection and generic/co-variant return types
911         // See https://issues.apache.org/jira/browse/BEANUTILS-340 for more details
912 
913         PropertyDescriptor[] descriptors = PropertyUtils.getPropertyDescriptors(propertyClass);
914         if (ArrayUtils.isNotEmpty(descriptors)) {
915             for (PropertyDescriptor descriptor : descriptors) {
916                 if (descriptor.getName().equals(propertyName)) {
917                     p = descriptor;
918                 }
919             }
920         }
921 
922         // cache the property descriptor if we found it.
923         if (p != null) {
924             if (m == null) {
925                 m = new TreeMap<String, PropertyDescriptor>();
926                 cache.put(propertyClassName, m);
927             }
928             m.put(propertyName, p);
929         }
930 
931         return p;
932     }
933 
934     public Set<InactivationBlockingMetadata> getAllInactivationBlockingMetadatas(Class blockedClass) {
935         return ddMapper.getAllInactivationBlockingMetadatas(ddIndex, blockedClass);
936     }
937 
938     /**
939      * This method gathers beans of type BeanOverride and invokes each one's performOverride() method.
940      */
941     // KULRICE-4513
942     public void performBeanOverrides() {
943         Collection<BeanOverride> beanOverrides = ddBeans.getBeansOfType(BeanOverride.class).values();
944 
945         if (beanOverrides.isEmpty()) {
946             LOG.info("DataDictionary.performOverrides(): No beans to override");
947         }
948         for (BeanOverride beanOverride : beanOverrides) {
949 
950             Object bean = ddBeans.getBean(beanOverride.getBeanName());
951             beanOverride.performOverride(bean);
952             LOG.info("DataDictionary.performOverrides(): Performing override on bean: " + bean.toString());
953         }
954     }
955 
956 }