View Javadoc

1   /*
2    * Copyright 2005-2007 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  
17  package org.kuali.rice.kns.datadictionary;
18  
19  import java.beans.IntrospectionException;
20  import java.beans.PropertyDescriptor;
21  import java.io.File;
22  import java.io.IOException;
23  import java.util.ArrayList;
24  import java.util.Collection;
25  import java.util.Collections;
26  import java.util.HashMap;
27  import java.util.HashSet;
28  import java.util.List;
29  import java.util.Map;
30  import java.util.Set;
31  import java.util.TreeMap;
32  
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.util.ClassLoaderUtils;
37  import org.kuali.rice.kns.bo.BusinessObject;
38  import org.kuali.rice.kns.bo.PersistableBusinessObjectExtension;
39  import org.kuali.rice.kns.datadictionary.exception.AttributeValidationException;
40  import org.kuali.rice.kns.datadictionary.exception.CompletionException;
41  import org.kuali.rice.kns.service.KNSServiceLocator;
42  import org.kuali.rice.kns.service.ModuleService;
43  import org.kuali.rice.kns.service.PersistenceStructureService;
44  import org.kuali.rice.kns.util.ObjectUtils;
45  import org.springframework.beans.factory.support.DefaultListableBeanFactory;
46  import org.springframework.beans.factory.xml.XmlBeanDefinitionReader;
47  import org.springframework.core.io.DefaultResourceLoader;
48  import org.springframework.core.io.Resource;
49  
50  /**
51   * Collection of named BusinessObjectEntry objects, each of which contains
52   * information relating to the display, validation, and general maintenance of a
53   * BusinessObject.
54   * 
55   * 
56   */
57  public class DataDictionary {
58  
59      private DefaultListableBeanFactory ddBeans = new DefaultListableBeanFactory();
60      private XmlBeanDefinitionReader xmlReader = new XmlBeanDefinitionReader(ddBeans);
61  
62  	// logger
63  	private static final Log LOG = LogFactory.getLog(DataDictionary.class);
64  
65  	/**
66  	 * The encapsulation of DataDictionary indices
67  	 */
68  	private DataDictionaryIndex ddIndex = new DataDictionaryIndex(ddBeans);
69  
70  	/**
71  	 * The DataDictionaryMapper
72  	 * The default mapper simply consults the initialized indices
73  	 * on workflow document type
74  	 */
75  	private DataDictionaryMapper ddMapper = new DataDictionaryIndexMapper();
76  
77  	private List<String> configFileLocations = new ArrayList<String>();
78  
79      public DataDictionary() { }
80      
81  	public List<String> getConfigFileLocations() {
82          return this.configFileLocations;
83      }
84  
85      public void setConfigFileLocations(List<String> configFileLocations) {
86          this.configFileLocations = configFileLocations;
87      }
88      
89      public void addConfigFileLocation( String location ) throws IOException {
90          indexSource( location );
91      }
92  
93      /**
94       * Sets the DataDictionaryMapper
95       * @param mapper the datadictionary mapper
96       */
97      public void setDataDictionaryMapper(DataDictionaryMapper mapper) {
98      	this.ddMapper = mapper;
99      }
100     
101     private void indexSource(String sourceName) throws IOException {        
102         if (sourceName == null) {
103             throw new DataDictionaryException("Source Name given is null");
104         }
105 
106         if (!sourceName.endsWith(".xml") ) {
107             Resource resource = getFileResource(sourceName);
108             if (resource.exists()) {
109                 indexSource(resource.getFile());
110             } else {
111                 LOG.warn("Could not find " + sourceName);
112                 throw new DataDictionaryException("DD Resource " + sourceName + " not found");
113             }
114         } else {
115             if ( LOG.isDebugEnabled() ) {
116                 LOG.debug("adding sourceName " + sourceName + " ");
117             }
118             Resource resource = getFileResource(sourceName);
119             if (! resource.exists()) {
120                 throw new DataDictionaryException("DD Resource " + sourceName + " not found");  
121             }
122             String indexName = sourceName.substring(sourceName.lastIndexOf("/") + 1, sourceName.indexOf(".xml"));
123             configFileLocations.add( sourceName );
124         }
125     }    
126 
127     private Resource getFileResource(String sourceName) {
128         DefaultResourceLoader resourceLoader = new DefaultResourceLoader(ClassLoaderUtils.getDefaultClassLoader());
129         return resourceLoader.getResource(sourceName);
130     }
131 
132     private void indexSource(File dir) {
133         for (File file : dir.listFiles()) {
134             if (file.isDirectory()) {
135                 indexSource(file);
136             } else if (file.getName().endsWith(".xml") ) {
137                 configFileLocations.add( "file:" + file.getAbsolutePath());
138             } else {
139                 if ( LOG.isDebugEnabled() ) {
140                     LOG.debug("Skipping non xml file " + file.getAbsolutePath() + " in DD load");
141                 }
142             }
143         }
144     }
145     
146     public void parseDataDictionaryConfigurationFiles( boolean allowConcurrentValidation ) {
147         // expand configuration locations into files
148 
149         LOG.info( "Starting DD XML File Load" );
150         String[] configFileLocationsArray = new String[configFileLocations.size()];
151         configFileLocationsArray = configFileLocations.toArray( configFileLocationsArray );
152         configFileLocations.clear(); // empty the list out so other items can be added
153         try {
154             xmlReader.loadBeanDefinitions( configFileLocationsArray );
155         } catch (Exception e) {
156             LOG.error("Error loading bean definitions", e);
157             throw new DataDictionaryException("Error loading bean definitions: " + e.getLocalizedMessage());
158         }
159         LOG.info( "Completed DD XML File Load" );
160         if ( allowConcurrentValidation ) {
161             Thread t = new Thread(ddIndex);
162             t.start();
163         } else {
164             ddIndex.run();
165         }
166     }
167 	    
168     static boolean validateEBOs = true;
169     
170     public void validateDD( boolean validateEbos ) {
171     	DataDictionary.validateEBOs = validateEbos;
172         Map<String,BusinessObjectEntry> boBeans = ddBeans.getBeansOfType(BusinessObjectEntry.class);
173         for ( BusinessObjectEntry entry : boBeans.values() ) {
174             entry.completeValidation();
175         }
176         Map<String,DocumentEntry> docBeans = ddBeans.getBeansOfType(DocumentEntry.class);
177         for ( DocumentEntry entry : docBeans.values() ) {
178             entry.completeValidation();
179         }
180     }
181     
182     public void validateDD() {
183     	DataDictionary.validateEBOs = true;
184         Map<String,BusinessObjectEntry> boBeans = ddBeans.getBeansOfType(BusinessObjectEntry.class);
185         for ( BusinessObjectEntry entry : boBeans.values() ) {
186             entry.completeValidation();
187         }
188         Map<String,DocumentEntry> docBeans = ddBeans.getBeansOfType(DocumentEntry.class);
189         for ( DocumentEntry entry : docBeans.values() ) {
190             entry.completeValidation();
191         }
192     }
193 
194 	/**
195 	 * @param className
196 	 * @return BusinessObjectEntry for the named class, or null if none exists
197 	 */
198 	public BusinessObjectEntry getBusinessObjectEntry(String className ) {
199 		return ddMapper.getBusinessObjectEntry(ddIndex, className);
200 	}
201 
202 	/**
203 	 * This method gets the business object entry for a concrete class
204 	 * 
205 	 * @param className
206 	 * @return
207 	 */
208 	public BusinessObjectEntry getBusinessObjectEntryForConcreteClass(String className){
209 		return ddMapper.getBusinessObjectEntryForConcreteClass(ddIndex, className);
210 	}
211 	
212 	/**
213 	 * @return List of businessObject classnames
214 	 */
215 	public List<String> getBusinessObjectClassNames() {
216 		return ddMapper.getBusinessObjectClassNames(ddIndex);
217 	}
218 
219 	/**
220 	 * @return Map of (classname, BusinessObjectEntry) pairs
221 	 */
222 	public Map<String, BusinessObjectEntry> getBusinessObjectEntries() {
223 		return ddMapper.getBusinessObjectEntries(ddIndex);
224 	}
225 
226 	/**
227 	 * @param className
228 	 * @return DataDictionaryEntryBase for the named class, or null if none
229 	 *         exists
230 	 */
231 	public DataDictionaryEntry getDictionaryObjectEntry(String className) {
232 		return ddMapper.getDictionaryObjectEntry(ddIndex, className);
233 	}
234 
235 	/**
236 	 * Returns the KNS document entry for the given lookup key.  The documentTypeDDKey is interpreted
237 	 * successively in the following ways until a mapping is found (or none if found):
238 	 * <ol>
239 	 * <li>KEW/workflow document type</li>
240 	 * <li>business object class name</li>
241 	 * <li>maintainable class name</li>
242 	 * </ol>
243 	 * This mapping is compiled when DataDictionary files are parsed on startup (or demand).  Currently this
244 	 * means the mapping is static, and one-to-one (one KNS document maps directly to one and only
245 	 * one key).
246 	 * 
247 	 * @param documentTypeDDKey the KEW/workflow document type name
248 	 * @return the KNS DocumentEntry if it exists
249 	 */
250 	public DocumentEntry getDocumentEntry(String documentTypeDDKey ) {
251 		return ddMapper.getDocumentEntry(ddIndex, documentTypeDDKey);
252 	}
253 
254 	/**
255 	 * Note: only MaintenanceDocuments are indexed by businessObject Class
256 	 * 
257 	 * This is a special case that is referenced in one location. Do we need
258 	 * another map for this stuff??
259 	 * 
260 	 * @param businessObjectClass
261 	 * @return DocumentEntry associated with the given Class, or null if there
262 	 *         is none
263 	 */
264 	public MaintenanceDocumentEntry getMaintenanceDocumentEntryForBusinessObjectClass(Class businessObjectClass) {
265 		return ddMapper.getMaintenanceDocumentEntryForBusinessObjectClass(ddIndex, businessObjectClass);
266 	}
267 
268 	public Map<String, DocumentEntry> getDocumentEntries() {
269 		return ddMapper.getDocumentEntries(ddIndex);
270 	}
271 
272     /**
273      * @param clazz
274      * @param propertyName
275      * @return true if the given propertyName names a property of the given class
276      * @throws CompletionException if there is a problem accessing the named property on the given class
277      */
278     public static boolean isPropertyOf(Class targetClass, String propertyName) {
279         if (targetClass == null) {
280             throw new IllegalArgumentException("invalid (null) targetClass");
281         }
282         if (StringUtils.isBlank(propertyName)) {
283             throw new IllegalArgumentException("invalid (blank) propertyName");
284         }
285 
286         PropertyDescriptor propertyDescriptor = buildReadDescriptor(targetClass, propertyName);
287 
288         boolean isPropertyOf = (propertyDescriptor != null);
289         return isPropertyOf;
290     }
291 
292     /**
293      * @param clazz
294      * @param propertyName
295      * @return true if the given propertyName names a Collection property of the given class
296      * @throws CompletionException if there is a problem accessing the named property on the given class
297      */
298     public static boolean isCollectionPropertyOf(Class targetClass, String propertyName) {
299         boolean isCollectionPropertyOf = false;
300 
301         PropertyDescriptor propertyDescriptor = buildReadDescriptor(targetClass, propertyName);
302         if (propertyDescriptor != null) {
303             Class clazz = propertyDescriptor.getPropertyType();
304 
305             if ((clazz != null) && Collection.class.isAssignableFrom(clazz)) {
306                 isCollectionPropertyOf = true;
307             }
308         }
309 
310         return isCollectionPropertyOf;
311     }
312 
313     public static PersistenceStructureService persistenceStructureService;
314     
315     /**
316      * @return the persistenceStructureService
317      */
318     public static PersistenceStructureService getPersistenceStructureService() {
319         if ( persistenceStructureService == null ) {
320             persistenceStructureService = KNSServiceLocator.getPersistenceStructureService();
321         }
322         return persistenceStructureService;
323     }
324     
325     /**
326      * This method determines the Class of the attributeName passed in. Null will be returned if the member is not available, or if
327      * a reflection exception is thrown.
328      * 
329      * @param rootClass - Class that the attributeName property exists in.
330      * @param attributeName - Name of the attribute you want a class for.
331      * @return The Class of the attributeName, if the attribute exists on the rootClass. Null otherwise.
332      */
333     public static Class getAttributeClass(Class boClass, String attributeName) {
334 
335         // fail loudly if the attributeName isnt a member of rootClass
336         if (!isPropertyOf(boClass, attributeName)) {
337             throw new AttributeValidationException("unable to find attribute '" + attributeName + "' in rootClass '" + boClass.getName() + "'");
338         }
339 
340     	//Implementing Externalizable Business Object Services...
341         //The boClass can be an interface, hence handling this separately, 
342         //since the original method was throwing exception if the class could not be instantiated.
343         if(boClass.isInterface())
344         	return getAttributeClassWhenBOIsInterface(boClass, attributeName);
345         else
346         	return getAttributeClassWhenBOIsClass(boClass, attributeName);        	
347 
348     }
349 
350     /**
351      * 
352      * This method gets the property type of the given attributeName when the bo class is a concrete class
353      * 
354      * @param boClass
355      * @param attributeName
356      * @return
357      */
358     private static Class getAttributeClassWhenBOIsClass(Class boClass, String attributeName){
359     	BusinessObject boInstance;
360         try {
361             boInstance = (BusinessObject) boClass.newInstance();
362         } catch (Exception e) {
363         	throw new RuntimeException("Unable to instantiate BO: " + boClass, e);
364         }
365 
366         // attempt to retrieve the class of the property
367         try {
368             return ObjectUtils.getPropertyType(boInstance, attributeName, getPersistenceStructureService());
369         } catch (Exception e) {
370             throw new RuntimeException("Unable to determine property type for: " + boClass.getName() + "." + attributeName, e);
371         }
372     }
373 
374     /**
375      * 
376      * This method gets the property type of the given attributeName when the bo class is an interface
377      * This method will also work if the bo class is not an interface, 
378      * but that case requires special handling, hence a separate method getAttributeClassWhenBOIsClass 
379      * 
380      * @param boClass
381      * @param attributeName
382      * @return
383      */
384     private static Class getAttributeClassWhenBOIsInterface(Class boClass, String attributeName){
385         if (boClass == null) {
386             throw new IllegalArgumentException("invalid (null) boClass");
387         }
388         if (StringUtils.isBlank(attributeName)) {
389             throw new IllegalArgumentException("invalid (blank) attributeName");
390         }
391 
392         PropertyDescriptor propertyDescriptor = null;
393 
394         String[] intermediateProperties = attributeName.split("\\.");
395         int lastLevel = intermediateProperties.length - 1;
396         Class currentClass = boClass;
397 
398         for (int i = 0; i <= lastLevel; ++i) {
399 
400             String currentPropertyName = intermediateProperties[i];
401             propertyDescriptor = buildSimpleReadDescriptor(currentClass, currentPropertyName);
402 
403             if (propertyDescriptor != null) {
404 
405                 Class propertyType = propertyDescriptor.getPropertyType();
406                 if ( propertyType.equals( PersistableBusinessObjectExtension.class ) ) {
407                     propertyType = getPersistenceStructureService().getBusinessObjectAttributeClass( currentClass, currentPropertyName );                    
408                 }
409                 if (Collection.class.isAssignableFrom(propertyType)) {
410                 	// TODO: determine property type using generics type definition
411                 	throw new AttributeValidationException("Can't determine the Class of Collection elements because when the business object is an (possibly ExternalizableBusinessObject) interface.");
412                 }
413                 else {
414                     currentClass = propertyType;
415                 }
416             }
417             else {
418             	throw new AttributeValidationException("Can't find getter method of " + boClass.getName() + " for property " + attributeName);
419             }
420         }
421         return currentClass;
422     }
423     
424     /**
425      * This method determines the Class of the elements in the collectionName passed in.
426      * 
427      * @param boClass Class that the collectionName collection exists in.
428      * @param collectionName the name of the collection you want the element class for
429      * @return
430      */
431     public static Class getCollectionElementClass(Class boClass, String collectionName) {
432         if (boClass == null) {
433             throw new IllegalArgumentException("invalid (null) boClass");
434         }
435         if (StringUtils.isBlank(collectionName)) {
436             throw new IllegalArgumentException("invalid (blank) collectionName");
437         }
438 
439         PropertyDescriptor propertyDescriptor = null;
440 
441         String[] intermediateProperties = collectionName.split("\\.");
442         Class currentClass = boClass;
443 
444         for (int i = 0; i <intermediateProperties.length; ++i) {
445 
446             String currentPropertyName = intermediateProperties[i];
447             propertyDescriptor = buildSimpleReadDescriptor(currentClass, currentPropertyName);
448 
449 
450                 if (propertyDescriptor != null) {
451 
452                     Class type = propertyDescriptor.getPropertyType();
453                     if (Collection.class.isAssignableFrom(type)) {
454 
455                         if (getPersistenceStructureService().isPersistable(currentClass)) {
456 
457                             Map<String, Class> collectionClasses = new HashMap<String, Class>();
458                             collectionClasses = getPersistenceStructureService().listCollectionObjectTypes(currentClass);
459                             currentClass = collectionClasses.get(currentPropertyName);
460 
461                         }
462                         else {
463                             throw new RuntimeException("Can't determine the Class of Collection elements because persistenceStructureService.isPersistable(" + currentClass.getName() + ") returns false.");
464                         }
465 
466                     }
467                     else {
468 
469                         currentClass = propertyDescriptor.getPropertyType();
470 
471                     }
472                 }
473             }
474 
475         return currentClass;
476     }
477 
478     static private Map<String, Map<String, PropertyDescriptor>> cache = new TreeMap<String, Map<String, PropertyDescriptor>>();
479 
480     /**
481      * @param propertyClass
482      * @param propertyName
483      * @return PropertyDescriptor for the getter for the named property of the given class, if one exists.
484      */
485     public static PropertyDescriptor buildReadDescriptor(Class propertyClass, String propertyName) {
486         if (propertyClass == null) {
487             throw new IllegalArgumentException("invalid (null) propertyClass");
488         }
489         if (StringUtils.isBlank(propertyName)) {
490             throw new IllegalArgumentException("invalid (blank) propertyName");
491         }
492 
493         PropertyDescriptor propertyDescriptor = null;
494 
495         String[] intermediateProperties = propertyName.split("\\.");
496         int lastLevel = intermediateProperties.length - 1;
497         Class currentClass = propertyClass;
498 
499         for (int i = 0; i <= lastLevel; ++i) {
500 
501             String currentPropertyName = intermediateProperties[i];
502             propertyDescriptor = buildSimpleReadDescriptor(currentClass, currentPropertyName);
503 
504             if (i < lastLevel) {
505 
506                 if (propertyDescriptor != null) {
507 
508                     Class propertyType = propertyDescriptor.getPropertyType();
509                     if ( propertyType.equals( PersistableBusinessObjectExtension.class ) ) {
510                         propertyType = getPersistenceStructureService().getBusinessObjectAttributeClass( currentClass, currentPropertyName );                    
511                     }
512                     if (Collection.class.isAssignableFrom(propertyType)) {
513 
514                         if (getPersistenceStructureService().isPersistable(currentClass)) {
515 
516                             Map<String, Class> collectionClasses = new HashMap<String, Class>();
517                             collectionClasses = getPersistenceStructureService().listCollectionObjectTypes(currentClass);
518                             currentClass = collectionClasses.get(currentPropertyName);
519 
520                         }
521                         else {
522 
523                             throw new RuntimeException("Can't determine the Class of Collection elements because persistenceStructureService.isPersistable(" + currentClass.getName() + ") returns false.");
524 
525                         }
526 
527                     }
528                     else {
529 
530                         currentClass = propertyType;
531 
532                     }
533 
534                 }
535 
536             }
537 
538         }
539 
540         return propertyDescriptor;
541     }
542 
543     /**
544      * @param propertyClass
545      * @param propertyName
546      * @return PropertyDescriptor for the getter for the named property of the given class, if one exists.
547      */
548     public static PropertyDescriptor buildSimpleReadDescriptor(Class propertyClass, String propertyName) {
549         if (propertyClass == null) {
550             throw new IllegalArgumentException("invalid (null) propertyClass");
551         }
552         if (StringUtils.isBlank(propertyName)) {
553             throw new IllegalArgumentException("invalid (blank) propertyName");
554         }
555 
556         PropertyDescriptor p = null;
557 
558         // check to see if we've cached this descriptor already. if yes, return true.
559         String propertyClassName = propertyClass.getName();
560         Map<String, PropertyDescriptor> m = cache.get(propertyClassName);
561         if (null != m) {
562             p = m.get(propertyName);
563             if (null != p) {
564                 return p;
565             }
566         }
567 
568         String prefix = StringUtils.capitalize(propertyName);
569         String getName = "get" + prefix;
570         String isName = "is" + prefix;
571 
572         try {
573 
574             p = new PropertyDescriptor(propertyName, propertyClass, getName, null);
575 
576         }
577         catch (IntrospectionException e) {
578             try {
579 
580                 p = new PropertyDescriptor(propertyName, propertyClass, isName, null);
581 
582             }
583             catch (IntrospectionException f) {
584                 // ignore it
585             }
586         }
587 
588         // cache the property descriptor if we found it.
589         if (null != p) {
590 
591             if (null == m) {
592 
593                 m = new TreeMap<String, PropertyDescriptor>();
594                 cache.put(propertyClassName, m);
595 
596             }
597 
598             m.put(propertyName, p);
599 
600         }
601 
602         return p;
603     }
604 
605     public Set<InactivationBlockingMetadata> getAllInactivationBlockingMetadatas(Class blockedClass) {
606     	return ddMapper.getAllInactivationBlockingMetadatas(ddIndex, blockedClass);
607     }
608     
609     /**
610      * This method gathers beans of type BeanOverride and invokes each one's performOverride() method.
611      */
612     // KULRICE-4513
613     public void performBeanOverrides()
614     {
615     	Collection<BeanOverride> beanOverrides = ddBeans.getBeansOfType(BeanOverride.class).values();
616     	
617     	if (beanOverrides.isEmpty()){
618     		LOG.info("DataDictionary.performOverrides(): No beans to override");
619     	}
620 		for (BeanOverride beanOverride : beanOverrides) {
621 			
622 			Object bean = ddBeans.getBean(beanOverride.getBeanName());
623 			beanOverride.performOverride(bean);
624 			LOG.info("DataDictionary.performOverrides(): Performing override on bean: " + bean.toString());
625 		}
626     }
627 }