001    /**
002     * Copyright 2005-2014 The Kuali Foundation
003     *
004     * Licensed under the Educational Community License, Version 2.0 (the "License");
005     * you may not use this file except in compliance with the License.
006     * You may obtain a copy of the License at
007     *
008     * http://www.opensource.org/licenses/ecl2.php
009     *
010     * Unless required by applicable law or agreed to in writing, software
011     * distributed under the License is distributed on an "AS IS" BASIS,
012     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013     * See the License for the specific language governing permissions and
014     * limitations under the License.
015     */
016    package org.kuali.rice.krad.datadictionary;
017    
018    import java.beans.PropertyDescriptor;
019    import java.io.File;
020    import java.io.IOException;
021    import java.util.ArrayList;
022    import java.util.Arrays;
023    import java.util.Collection;
024    import java.util.HashMap;
025    import java.util.List;
026    import java.util.Map;
027    import java.util.Set;
028    import java.util.TreeMap;
029    
030    import org.apache.commons.beanutils.PropertyUtils;
031    import org.apache.commons.collections.ListUtils;
032    import org.apache.commons.lang.ArrayUtils;
033    import org.apache.commons.lang.StringUtils;
034    import org.apache.commons.logging.Log;
035    import org.apache.commons.logging.LogFactory;
036    import org.kuali.rice.core.api.config.property.ConfigContext;
037    import org.kuali.rice.core.api.util.ClassLoaderUtils;
038    import org.kuali.rice.krad.bo.PersistableBusinessObjectExtension;
039    import org.kuali.rice.krad.datadictionary.exception.AttributeValidationException;
040    import org.kuali.rice.krad.datadictionary.exception.CompletionException;
041    import org.kuali.rice.krad.datadictionary.parse.StringListConverter;
042    import org.kuali.rice.krad.datadictionary.parse.StringMapConverter;
043    import org.kuali.rice.krad.datadictionary.uif.UifDictionaryIndex;
044    import org.kuali.rice.krad.service.KRADServiceLocator;
045    import org.kuali.rice.krad.service.PersistenceStructureService;
046    import org.kuali.rice.krad.uif.UifConstants.ViewType;
047    import org.kuali.rice.krad.uif.util.ComponentBeanPostProcessor;
048    import org.kuali.rice.krad.uif.util.UifBeanFactoryPostProcessor;
049    import org.kuali.rice.krad.uif.view.View;
050    import org.kuali.rice.krad.util.ObjectUtils;
051    import org.springframework.beans.PropertyValues;
052    import org.springframework.beans.factory.config.BeanPostProcessor;
053    import org.springframework.beans.factory.config.PropertyPlaceholderConfigurer;
054    import org.springframework.beans.factory.xml.XmlBeanDefinitionReader;
055    import org.springframework.context.expression.StandardBeanExpressionResolver;
056    import org.springframework.core.convert.support.GenericConversionService;
057    import org.springframework.core.io.DefaultResourceLoader;
058    import org.springframework.core.io.Resource;
059    
060    /**
061     * Encapsulates a bean factory and indexes to the beans within the factory for providing
062     * framework metadata
063     *
064     * @author Kuali Rice Team (rice.collab@kuali.org)
065     */
066    public class DataDictionary {
067        private static final Log LOG = LogFactory.getLog(DataDictionary.class);
068    
069        protected static boolean validateEBOs = true;
070    
071        protected DefaultListableBeanFactory ddBeans = new DefaultListableBeanFactory();
072        protected XmlBeanDefinitionReader xmlReader = new XmlBeanDefinitionReader(ddBeans);
073    
074        protected DataDictionaryIndex ddIndex = new DataDictionaryIndex(ddBeans);
075        protected UifDictionaryIndex uifIndex = new UifDictionaryIndex(ddBeans);
076    
077        protected DataDictionaryMapper ddMapper = new DataDictionaryIndexMapper();
078    
079        protected Map<String, List<String>> moduleDictionaryFiles = new HashMap<String, List<String>>();
080        protected List<String> moduleLoadOrder = new ArrayList<String>();
081    
082        protected ArrayList<String> beanValidationFiles = new ArrayList<String>();
083    
084        /**
085         * Populates and processes the dictionary bean factory based on the configured files and
086         * performs indexing
087         *
088         * @param allowConcurrentValidation - indicates whether the indexing should occur on a different thread
089         * or the same thread
090         */
091        public void parseDataDictionaryConfigurationFiles(boolean allowConcurrentValidation) {
092            setupProcessor(ddBeans);
093    
094            loadDictionaryBeans(ddBeans, moduleDictionaryFiles, ddIndex, beanValidationFiles);
095    
096            performDictionaryPostProcessing(allowConcurrentValidation);
097        }
098    
099        /**
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    }