View Javadoc

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