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