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  package org.kuali.rice.kns.util;
17  
18  import java.beans.PropertyDescriptor;
19  import java.io.ByteArrayInputStream;
20  import java.io.ByteArrayOutputStream;
21  import java.io.ObjectInputStream;
22  import java.io.ObjectOutputStream;
23  import java.io.Serializable;
24  import java.lang.reflect.Field;
25  import java.lang.reflect.InvocationTargetException;
26  import java.security.MessageDigest;
27  import java.util.Collection;
28  import java.util.Iterator;
29  import java.util.List;
30  import java.util.Map;
31  
32  import org.apache.commons.beanutils.NestedNullException;
33  import org.apache.commons.beanutils.PropertyUtils;
34  import org.apache.commons.lang.StringUtils;
35  import org.apache.log4j.Logger;
36  import org.apache.ojb.broker.core.proxy.ProxyHelper;
37  import org.hibernate.collection.PersistentBag;
38  import org.kuali.rice.kns.bo.BusinessObject;
39  import org.kuali.rice.kns.bo.ExternalizableBusinessObject;
40  import org.kuali.rice.kns.bo.PersistableBusinessObject;
41  import org.kuali.rice.kns.bo.PersistableBusinessObjectExtension;
42  import org.kuali.rice.kns.service.KNSServiceLocator;
43  import org.kuali.rice.kns.service.ModuleService;
44  import org.kuali.rice.kns.service.PersistenceStructureService;
45  import org.kuali.rice.kns.util.cache.CopiedObject;
46  import org.kuali.rice.kns.web.format.CollectionFormatter;
47  import org.kuali.rice.kns.web.format.FormatException;
48  import org.kuali.rice.kns.web.format.Formatter;
49  
50  /**
51   * This class contains various Object, Proxy, and serialization utilities.
52   */
53  public class ObjectUtils {
54      private static final Logger LOG = Logger.getLogger(ObjectUtils.class);
55  
56      private ObjectUtils() {
57      }
58  
59      /**
60       * Uses Serialization mechanism to create a deep copy of the given Object. As a special case, deepCopy of null returns null,
61       * just to make using this method simpler. For a detailed discussion see:
62       * http://www.javaworld.com/javaworld/javatips/jw-javatip76.html
63       * 
64       * @param src
65       * @return deep copy of the given Serializable
66       */
67      public static Serializable deepCopy(Serializable src) {
68          CopiedObject co = deepCopyForCaching(src);
69          return co.getContent();
70      }
71  
72  
73      /**
74       * Uses Serialization mechanism to create a deep copy of the given Object, and returns a CacheableObject instance containing the
75       * deepCopy and its size in bytes. As a special case, deepCopy of null returns a cacheableObject containing null and a size of
76       * 0, to make using this method simpler. For a detailed discussion see:
77       * http://www.javaworld.com/javaworld/javatips/jw-javatip76.html
78       * 
79       * @param src
80       * @return CopiedObject containing a deep copy of the given Serializable and its size in bytes
81       */
82      public static CopiedObject deepCopyForCaching(Serializable src) {
83          CopiedObject co = new CopiedObject();
84  
85          co.setContent( src );
86          
87          return co;
88      }
89  
90  
91      /**
92       * Converts the object to a byte array using the output stream.
93       * 
94       * @param object
95       * @return byte array of the object
96       */
97      public static byte[] toByteArray(Object object) throws Exception {
98          ObjectOutputStream oos = null;
99          try {
100             ByteArrayOutputStream bos = new ByteArrayOutputStream(); // A
101             oos = new ObjectOutputStream(bos); // B
102             // serialize and pass the object
103             oos.writeObject(object); // C
104             // oos.flush(); // D
105             return bos.toByteArray();
106         }
107         catch (Exception e) {
108             LOG.warn("Exception in ObjectUtil = " + e);
109             throw (e);
110         }
111         finally {
112             if (oos != null) {
113                 oos.close();
114             }
115         }
116     }
117 
118     /**
119      * reconsitiutes the object that was converted into a byte array by toByteArray
120      * @param bytes
121      * @return
122      * @throws Exception
123      */
124     public static Object fromByteArray(byte[] bytes) throws Exception {
125         ObjectInputStream ois = null;
126         try {
127             ByteArrayInputStream bis = new ByteArrayInputStream(bytes);
128             ois = new ObjectInputStream(bis);
129             Object obj = ois.readObject();
130             return obj;
131         }
132         catch (Exception e) {
133             LOG.warn("Exception in ObjectUtil = " + e);
134             throw (e);
135         }
136         finally {
137             if (ois != null) {
138                 ois.close();
139             }
140         }
141     }
142     
143     /**
144      * use MD5 to create a one way hash of an object
145      * 
146      * @param object
147      * @return
148      */
149     public static String getMD5Hash(Object object) throws Exception {
150         try {
151             MessageDigest md = MessageDigest.getInstance("MD5");
152             md.update(toByteArray(object));
153             return new String(md.digest());
154         }
155         catch (Exception e) {
156             LOG.warn(e);
157             throw e;
158         }
159     }
160 
161     /**
162      * Creates a new instance of a given BusinessObject, copying fields specified in template from the given source BO. For example,
163      * this can be used to create an AccountChangeDetail based on a particular Account.
164      * 
165      * @param template a map defining the relationships between the fields of the newly created BO, and the source BO.  For each K (key), V (value)
166      * entry, the value of property V on the source BO will be assigned to the K property of the newly created BO
167      * 
168      * @see org.kuali.rice.kns.maintenance.util.MaintenanceUtils
169      * 
170      * @throws NoSuchMethodException
171      * @throws InvocationTargetException
172      * @throws IllegalAccessException
173      * @throws FormatException
174      */
175 
176     public static BusinessObject createHybridBusinessObject(Class businessObjectClass, BusinessObject source, Map<String, String> template) throws FormatException, IllegalAccessException, InvocationTargetException, NoSuchMethodException {
177         BusinessObject obj = null;
178         try {
179     		ModuleService moduleService = KNSServiceLocator.getKualiModuleService().getResponsibleModuleService(businessObjectClass);
180     		if (moduleService != null && moduleService.isExternalizable(businessObjectClass))
181     			obj = (BusinessObject)moduleService.createNewObjectFromExternalizableClass(businessObjectClass);
182     		else
183     			obj = (BusinessObject) businessObjectClass.newInstance();
184         }
185         catch (Exception e) {
186             throw new RuntimeException("Cannot instantiate " + businessObjectClass.getName(), e);
187         }
188 
189         createHybridBusinessObject(obj, source, template);
190 
191         return obj;
192     }
193     
194     public static void createHybridBusinessObject(BusinessObject businessObject, BusinessObject source, Map<String, String> template) throws FormatException, IllegalAccessException, InvocationTargetException, NoSuchMethodException {
195         for (String name : template.keySet()) {
196             String sourcePropertyName = template.get(name);
197             setObjectProperty(businessObject, name, easyGetPropertyType(source, sourcePropertyName), getPropertyValue(source, sourcePropertyName));
198         }
199     }
200 
201 
202     /**
203      * This method simply uses PojoPropertyUtilsBean logic to get the Class of a Class property.
204      * This method does not have any of the logic needed to obtain the Class of an element of a Collection specified in the DataDictionary.
205      * 
206      * @param object An instance of the Class of which we're trying to get the property Class.
207      * @param propertyName The name of the property.
208 
209      * @return
210      * 
211      * @throws IllegalAccessException
212      * @throws NoSuchMethodException
213      * @throws InvocationTargetException
214      */
215     static public Class easyGetPropertyType(Object object, String propertyName) throws IllegalAccessException, NoSuchMethodException, InvocationTargetException {
216 
217         // FIXME (laran) This dependence should be inverted. Instead of having a core class
218         // depend on PojoPropertyUtilsBean, which is in the web layer, the web layer
219         // should depend downward to the core.
220         return PropertyUtils.getPropertyType(object, propertyName);
221 
222     }
223 
224     /**
225      * Returns the type of the property in the object. This implementation is not smart enough to look through a Collection to get the property type
226      * of an attribute of an element in the collection.
227      * 
228      * NOTE: A patch file attached to https://test.kuali.org/jira/browse/KULRNE-4435 contains a modified version of this method which IS smart enough
229      * to look through Collections. This patch is currently under review.
230      * 
231      * @param object An instance of the Class for which we're trying to get the property type.
232      * @param propertyName The name of the property of the Class the Class of which we're trying to get. Dot notation is used to separate properties.
233      *                     TODO: The rules about this dot notation needs to be explained in Confluence using examples. 
234      * @param persistenceStructureService Needed to get the type of elements in a Collection from OJB. 
235      * 
236      * @return Object will be null if any parent property for the given property is null.
237      */
238     public static Class getPropertyType(Object object, String propertyName, PersistenceStructureService persistenceStructureService) {
239 		if (object == null || propertyName == null) {
240 			throw new RuntimeException("Business object and property name can not be null");
241 		}
242 
243 		Class propertyType = null;
244 		try {
245 			try {
246 				// Try to simply use the default or simple way of getting the property type.
247 				propertyType = PropertyUtils.getPropertyType(object, propertyName);
248 			} catch (IllegalArgumentException ex) {
249 				// swallow the exception, propertyType stays null
250 			} catch (NoSuchMethodException nsme) {
251 				// swallow the exception, propertyType stays null
252 			}
253 
254 			// if the property type as determined from the object is PersistableBusinessObject,
255 			// then this must be an extension attribute -- attempt to get the property type from the
256 			// persistence structure service
257 			if (propertyType != null && propertyType.equals(PersistableBusinessObjectExtension.class)) {
258 				propertyType = persistenceStructureService.getBusinessObjectAttributeClass(
259 						ProxyHelper.getRealClass(object), propertyName);
260 			}
261 
262 			// If the easy way didn't work ...
263 			if (null == propertyType && -1 != propertyName.indexOf('.')) {
264 				if (null == persistenceStructureService) {
265 					LOG.info("PropertyType couldn't be determined simply and no PersistenceStructureService was given. If you pass in a PersistenceStructureService I can look in other places to try to determine the type of the property.");
266 				} else {
267 					String prePeriod = StringUtils.substringBefore(propertyName, ".");
268 					String postPeriod = StringUtils.substringAfter(propertyName, ".");
269 
270 					Class prePeriodClass = getPropertyType(object, prePeriod, persistenceStructureService);
271 					Object prePeriodClassInstance = prePeriodClass.newInstance();
272 					propertyType = getPropertyType(prePeriodClassInstance, postPeriod, persistenceStructureService);
273 				}
274 
275 			} else if (Collection.class.isAssignableFrom(propertyType)) {
276 				Map<String, Class> map = persistenceStructureService.listCollectionObjectTypes(object.getClass());
277 				propertyType = map.get(propertyName);
278 			}
279 
280 		} catch (Exception e) {
281 			LOG.debug("unable to get property type for " + propertyName + " " + e.getMessage());
282 			// continue and return null for propertyType
283 		}
284 
285 		return propertyType;
286 	}
287 
288     /**
289      * Returns the value of the property in the object.
290      * 
291      * @param businessObject
292      * @param propertyName
293      * 
294      * @return Object will be null if any parent property for the given property is null.
295      */
296     public static Object getPropertyValue(Object businessObject, String propertyName) {
297         if (businessObject == null || propertyName == null) {
298             throw new RuntimeException("Business object and property name can not be null");
299         }
300 
301         Object propertyValue = null;
302         try {
303             propertyValue = PropertyUtils.getProperty(businessObject, propertyName);
304         } catch (NestedNullException e) {
305             // continue and return null for propertyValue
306         } catch (IllegalAccessException e1) {
307             LOG.error("error getting property value for  " + businessObject.getClass() + "." + propertyName + " " + e1.getMessage());
308             throw new RuntimeException("error getting property value for  " + businessObject.getClass() + "." + propertyName + " " + e1.getMessage(), e1);
309         } catch (InvocationTargetException e1) {
310             // continue and return null for propertyValue
311         } catch (NoSuchMethodException e1) {
312             LOG.error("error getting property value for  " + businessObject.getClass() + "." + propertyName + " " + e1.getMessage());
313             throw new RuntimeException("error getting property value for  " + businessObject.getClass() + "." + propertyName + " " + e1.getMessage(),e1);
314         }
315 
316         return propertyValue;
317     }
318     
319 	/**
320 	 * Gets the property value from the business object, then based on the value
321 	 * type select a formatter and format the value
322 	 * 
323 	 * @param element
324 	 *            BusinessObject instance that contains the property
325 	 * @param propertyName
326 	 *            Name of property in BusinessObject to get value for
327 	 * @param formatter
328 	 *            Default formatter to use (or null)
329 	 * @return Formatted property value as String, or empty string if value is null
330 	 */
331 	public static String getFormattedPropertyValue(BusinessObject businessObject, String propertyName, Formatter formatter) {
332 		String propValue = KNSConstants.EMPTY_STRING;
333 
334 		Object prop = ObjectUtils.getPropertyValue(businessObject, propertyName);
335 		if (formatter == null) {
336 			propValue = formatPropertyValue(prop);
337 		} else {
338 			final Object formattedValue = formatter.format(prop);
339 			if (formattedValue != null) {
340 				propValue = String.valueOf(formattedValue);
341 			}
342 		}
343 
344 		return propValue;
345 	}
346 	
347 	/**
348 	 * References the data dictionary to find any registered formatter class then if not found checks for associated formatter for the
349 	 * property type. Value is then formatted using the found Formatter
350 	 * 
351 	 * @param element
352 	 *            BusinessObject instance that contains the property
353 	 * @param propertyName
354 	 *            Name of property in BusinessObject to get value for
355 	 * @return Formatted property value as String, or empty string if value is null
356 	 */
357 	public static String getFormattedPropertyValueUsingDataDictionary(BusinessObject businessObject, String propertyName) {
358 		Formatter formatter = getFormatterWithDataDictionary(businessObject, propertyName);
359 
360 		return getFormattedPropertyValue(businessObject, propertyName, formatter);
361 	}
362 
363 	/**
364 	 * Based on the value type selects a formatter and returns the formatted
365 	 * value as a string
366 	 * 
367 	 * @param propertyValue
368 	 *            Object value to be formatted
369 	 * @return formatted value as a String
370 	 */
371 	public static String formatPropertyValue(Object propertyValue) {
372 		Object propValue = KNSConstants.EMPTY_STRING;
373 
374 		Formatter formatter = null;
375 		if (propertyValue != null) {
376 			if (propertyValue instanceof Collection) {
377 				formatter = new CollectionFormatter();
378 			} else {
379 				formatter = Formatter.getFormatter(propertyValue.getClass());
380 			}
381 
382 			propValue = formatter != null ? formatter.format(propertyValue) : propertyValue;
383 		}
384 
385 		return propValue != null ? String.valueOf(propValue) : KNSConstants.EMPTY_STRING;
386 	}
387     
388     /**
389      * Sets the property of an object with the given value. Converts using the formatter of the type for the property.
390      * Note: propertyType does not need passed, is found by util method.
391      */
392     public static void setObjectProperty(Object bo, String propertyName, Object propertyValue) throws FormatException, IllegalAccessException, InvocationTargetException, NoSuchMethodException {
393         Class propertyType = easyGetPropertyType(bo, propertyName);
394         setObjectProperty(bo, propertyName, propertyType, propertyValue);
395     }
396 
397 
398 	/**
399 	 * Sets the property of an object with the given value. Converts using the formatter of the given type if one is found.
400 	 * 
401 	 * @param bo
402 	 * @param propertyName
403 	 * @param propertyType
404 	 * @param propertyValue
405 	 * 
406 	 * @throws NoSuchMethodException
407 	 * @throws InvocationTargetException
408 	 * @throws IllegalAccessException
409 	 */
410 	public static void setObjectProperty(Object bo, String propertyName, Class propertyType, Object propertyValue)
411 			throws FormatException, IllegalAccessException, InvocationTargetException, NoSuchMethodException {
412 		// reformat propertyValue, if necessary
413 		boolean reformat = false;
414 		if (propertyType != null) {
415 			if (propertyValue != null && propertyType.isAssignableFrom(String.class)) {
416 				// always reformat if the destination is a String
417 				reformat = true;
418 			} else if (propertyValue != null && !propertyType.isAssignableFrom(propertyValue.getClass())) {
419 				// otherwise, only reformat if the propertyValue can't be assigned into the property
420 				reformat = true;
421 			}
422 
423 			// attempting to set boolean fields to null throws an exception, set to false instead
424 			if (boolean.class.isAssignableFrom(propertyType) && propertyValue == null) {
425 				propertyValue = false;
426 			}
427 		}
428 
429 		Formatter formatter = getFormatterWithDataDictionary(bo, propertyName);
430 		if (reformat && formatter != null) {
431 			LOG.debug("reformatting propertyValue using Formatter " + formatter.getClass().getName());
432 			propertyValue = formatter.convertFromPresentationFormat(propertyValue);
433 		}
434 
435 		// set property in the object
436 		PropertyUtils.setNestedProperty(bo, propertyName, propertyValue);
437 	}
438 
439 
440     /**
441      * Sets the property of an object with the given value. Converts using the given formatter, if it isn't null.
442      * 
443      * @param formatter
444      * @param bo
445      * @param propertyName
446      * @param type
447      * @param propertyValue
448      * 
449      * @throws NoSuchMethodException
450      * @throws InvocationTargetException
451      * @throws IllegalAccessException
452      */
453     public static void setObjectProperty(Formatter formatter, Object bo, String propertyName, Class type, Object propertyValue) throws FormatException, IllegalAccessException, InvocationTargetException, NoSuchMethodException {
454 
455         // convert value using formatter for type
456         if (formatter != null) {
457             propertyValue = formatter.convertFromPresentationFormat(propertyValue);
458         }
459 
460         // set property in the object
461         PropertyUtils.setNestedProperty(bo, propertyName, propertyValue);
462     }
463 
464 	/**
465 	 * Returns a Formatter instance for the given property name in the given given business object. First
466 	 * checks if a formatter is defined for the attribute in the data dictionary, is not found then returns
467 	 * the registered formatter for the property type in Formatter
468 	 * 
469 	 * @param bo
470 	 *            - business object instance with property to get formatter for
471 	 * @param propertyName
472 	 *            - name of property to get formatter for
473 	 * @return Formatter instance
474 	 */
475 	public static Formatter getFormatterWithDataDictionary(Object bo, String propertyName) {
476 		Formatter formatter = null;
477 
478 		Class boClass = bo.getClass();
479 		String boPropertyName = propertyName;
480 
481 		// for collections, formatter should come from property on the collection type
482 		if (StringUtils.contains(propertyName, "]")) {
483 			Object collectionParent = getNestedValue(bo, StringUtils.substringBeforeLast(propertyName, "].") + "]");
484 			if (collectionParent != null) {
485 				boClass = collectionParent.getClass();
486 				boPropertyName = StringUtils.substringAfterLast(propertyName, "].");
487 			}
488 		}
489 
490 		Class<? extends Formatter> formatterClass = KNSServiceLocator.getDataDictionaryService().getAttributeFormatter(
491 				boClass, boPropertyName);
492 		if (formatterClass == null) {
493 			try {
494 				formatterClass = Formatter.findFormatter(getPropertyType(boClass.newInstance(), boPropertyName,
495 						KNSServiceLocator.getPersistenceStructureService()));
496 			} catch (InstantiationException e) {
497 				LOG.warn("Unable to find a formater for bo class " + boClass + " and property " + boPropertyName);
498 				// just swallow the exception and let formatter be null
499 			} catch (IllegalAccessException e) {
500 				LOG.warn("Unable to find a formater for bo class " + boClass + " and property " + boPropertyName);
501 				// just swallow the exception and let formatter be null
502 			}
503 		}
504 
505 		if (formatterClass != null) {
506 			try {
507 				formatter = formatterClass.newInstance();
508 			} catch (Exception e) {
509 				throw new RuntimeException(
510 						"cannot create new instance of formatter class " + formatterClass.toString(), e);
511 			}
512 		}
513 
514 		return formatter;
515 	}
516 
517     /**
518      * Recursive; sets all occurences of the property in the object, its nested objects and its object lists with the given value.
519      * 
520      * @param bo
521      * @param propertyName
522      * @param type
523      * @param propertyValue
524      * 
525      * @throws NoSuchMethodException
526      * @throws InvocationTargetException
527      * @throws IllegalAccessException
528      */
529     public static void setObjectPropertyDeep(Object bo, String propertyName, Class type, Object propertyValue) throws FormatException, IllegalAccessException, InvocationTargetException, NoSuchMethodException {
530 
531         // Base return cases to avoid null pointers & infinite loops
532         if (isNull(bo) || !PropertyUtils.isReadable(bo, propertyName) || (propertyValue != null && propertyValue.equals(getPropertyValue(bo, propertyName))) || (type != null && !type.equals(easyGetPropertyType(bo, propertyName)))) {
533             return;
534         }
535         
536         // need to materialize the updateable collections before resetting the property, because it may be used in the retrieval
537         materializeUpdateableCollections(bo);
538         
539         // Set the property in the BO
540         setObjectProperty(bo, propertyName, type, propertyValue);
541 
542         // Now drill down and check nested BOs and BO lists
543         PropertyDescriptor[] propertyDescriptors = PropertyUtils.getPropertyDescriptors(bo.getClass());
544         for (int i = 0; i < propertyDescriptors.length; i++) {
545 
546             PropertyDescriptor propertyDescriptor = propertyDescriptors[i];
547 
548             // Business Objects
549             if (propertyDescriptor.getPropertyType() != null && (BusinessObject.class).isAssignableFrom(propertyDescriptor.getPropertyType()) && PropertyUtils.isReadable(bo, propertyDescriptor.getName())) {
550                 Object nestedBo = getPropertyValue(bo, propertyDescriptor.getName());
551                 if ( nestedBo instanceof BusinessObject ) {
552                     setObjectPropertyDeep((BusinessObject)nestedBo, propertyName, type, propertyValue);
553                 }
554             }
555 
556             // Lists
557             else if (propertyDescriptor.getPropertyType() != null && (List.class).isAssignableFrom(propertyDescriptor.getPropertyType()) && getPropertyValue(bo, propertyDescriptor.getName()) != null) {
558 
559                 List propertyList = (List) getPropertyValue(bo, propertyDescriptor.getName());
560                 for(Object listedBo: propertyList) {
561                     if (listedBo != null && listedBo instanceof BusinessObject) {
562                         setObjectPropertyDeep(listedBo, propertyName, type, propertyValue);
563                     }
564                 } // end for
565             }
566         } // end for
567     }
568     /*
569     * Recursive up to a given depth; sets all occurences of the property in the object, its nested objects and its object lists with the given value.
570     */  
571     public static void setObjectPropertyDeep(Object bo, String propertyName, Class type, Object propertyValue, int depth) throws FormatException, IllegalAccessException, InvocationTargetException, NoSuchMethodException {
572         // Base return cases to avoid null pointers & infinite loops
573         if (depth == 0 || isNull(bo) || !PropertyUtils.isReadable(bo, propertyName)) {
574             return;
575         }
576         
577         // need to materialize the updateable collections before resetting the property, because it may be used in the retrieval
578         materializeUpdateableCollections(bo);
579         
580         // Set the property in the BO
581         setObjectProperty(bo, propertyName, type, propertyValue);
582 
583         // Now drill down and check nested BOs and BO lists
584         PropertyDescriptor[] propertyDescriptors = PropertyUtils.getPropertyDescriptors(bo.getClass());
585         for (int i = 0; i < propertyDescriptors.length; i++) {
586             PropertyDescriptor propertyDescriptor = propertyDescriptors[i];
587 
588             // Business Objects
589             if (propertyDescriptor.getPropertyType() != null && (BusinessObject.class).isAssignableFrom(propertyDescriptor.getPropertyType()) && PropertyUtils.isReadable(bo, propertyDescriptor.getName())) {
590                 Object nestedBo = getPropertyValue(bo, propertyDescriptor.getName());
591                 if ( nestedBo instanceof BusinessObject ) {
592                     setObjectPropertyDeep((BusinessObject)nestedBo, propertyName, type, propertyValue, depth - 1);
593                 }
594             }
595 
596             // Lists
597             else if (propertyDescriptor.getPropertyType() != null && (List.class).isAssignableFrom(propertyDescriptor.getPropertyType()) && getPropertyValue(bo, propertyDescriptor.getName()) != null) {
598 
599                 List propertyList = (List) getPropertyValue(bo, propertyDescriptor.getName());
600                 
601                 // Complete Hibernate Hack - fetches the proxied List into the PersistenceContext and sets it on the BO Copy.
602                 if (propertyList instanceof PersistentBag) {
603                 	try {
604 	                	PersistentBag bag = (PersistentBag) propertyList;
605 	                	PersistableBusinessObject pbo = (PersistableBusinessObject) KNSServiceLocator.getEntityManagerFactory().createEntityManager().find(bo.getClass(), bag.getKey());
606 	        			Field field1 = pbo.getClass().getDeclaredField(propertyDescriptor.getName());
607 	        			Field field2 = bo.getClass().getDeclaredField(propertyDescriptor.getName());
608 	        			field1.setAccessible(true);
609 	        			field2.setAccessible(true);
610 	        			field2.set(bo, field1.get(pbo));
611 	        			propertyList = (List) getPropertyValue(bo, propertyDescriptor.getName());;
612                 	} catch (Exception e) {
613                 		LOG.error(e.getMessage(), e);
614                 	}
615                 }
616                 // End Complete Hibernate Hack
617                 
618                 for(Object listedBo: propertyList) {
619                     if (listedBo != null && listedBo instanceof BusinessObject) {
620                         setObjectPropertyDeep(listedBo, propertyName, type, propertyValue, depth - 1);
621                     }
622                 } // end for
623             }
624         } // end for
625     }
626     
627     /**
628      * 
629      * This method checks for updateable collections on the business object provided and materializes the corresponding collection proxies
630      * 
631      * @param bo The business object for which you want unpdateable, proxied collections materialized
632      * @throws FormatException
633      * @throws IllegalAccessException
634      * @throws InvocationTargetException
635      * @throws NoSuchMethodException
636      */
637     public static void materializeUpdateableCollections(Object bo) throws FormatException, IllegalAccessException, InvocationTargetException, NoSuchMethodException {
638         if (isNotNull(bo)) {
639             PropertyDescriptor[] propertyDescriptors = PropertyUtils.getPropertyDescriptors(bo.getClass());
640             for (int i = 0; i < propertyDescriptors.length; i++) {
641                 if (KNSServiceLocator.getPersistenceStructureService().hasCollection(bo.getClass(), propertyDescriptors[i].getName()) && KNSServiceLocator.getPersistenceStructureService().isCollectionUpdatable(bo.getClass(), propertyDescriptors[i].getName())) {
642                     Collection updateableCollection = (Collection) getPropertyValue(bo, propertyDescriptors[i].getName());
643                     if ((updateableCollection != null) && ProxyHelper.isCollectionProxy(updateableCollection)) {
644                         materializeObjects(updateableCollection);
645                     }
646                 }
647             }
648         }
649     }
650 
651 
652     /**
653      * Removes all query characters from a string.
654      * 
655      * @param string
656      * 
657      * @return Cleaned string
658      */
659     public static String clean(String string) {
660         for (int i = 0; i < KNSConstants.QUERY_CHARACTERS.length; i++) {
661             string = StringUtils.replace(string, KNSConstants.QUERY_CHARACTERS[i], KNSConstants.EMPTY_STRING);
662         }
663         return string;
664     }
665 
666 
667     /**
668      * Compares two {@link PersistableBusinessObject} instances for equality of type and key values.
669      * 
670      * @param bo1
671      * @param bo2
672      *  
673      * @return boolean indicating whether the two objects are equal.
674      */
675     public static boolean equalByKeys(PersistableBusinessObject bo1, PersistableBusinessObject bo2) {
676         boolean equal = true;
677 
678         if (bo1 == null && bo2 == null) {
679             equal = true;
680         }
681         else if (bo1 == null || bo2 == null) {
682             equal = false;
683         }
684         else if (!bo1.getClass().getName().equals(bo2.getClass().getName())) {
685             equal = false;
686         }
687         else {
688             Map bo1Keys = KNSServiceLocator.getPersistenceService().getPrimaryKeyFieldValues(bo1);
689             Map bo2Keys = KNSServiceLocator.getPersistenceService().getPrimaryKeyFieldValues(bo2);
690             for (Iterator iter = bo1Keys.keySet().iterator(); iter.hasNext();) {
691                 String keyName = (String) iter.next();
692                 if (bo1Keys.get(keyName) != null && bo2Keys.get(keyName) != null) {
693                     if (!bo1Keys.get(keyName).toString().equals(bo2Keys.get(keyName).toString())) {
694                         equal = false;
695                     }
696                 } else {
697                     equal = false;
698                 }
699             }
700         }  
701 
702 
703         return equal;
704     }
705 
706     /**
707      * Compares a business object with a List of {@link PersistableBusinessObject}s to determine if an object with the same key as the BO exists in the list.
708      * 
709      * @param controlList - The list of items to check
710      * @param bo - The BO whose keys we are looking for in the controlList
711      * 
712      * @return boolean
713      */
714     public static boolean collectionContainsObjectWithIdentitcalKey(Collection<? extends PersistableBusinessObject> controlList, PersistableBusinessObject bo) {
715         boolean objectExistsInList = false;
716 
717         for (Iterator i = controlList.iterator(); i.hasNext();) {
718             if (equalByKeys((PersistableBusinessObject) i.next(), bo)) {
719                 return true;
720             }
721         }
722 
723         return objectExistsInList;
724     }
725 
726     /**
727      * Compares a business object with a Collection of {@link PersistableBusinessObject}s to count how many have the same key as the BO.
728      * 
729      * @param collection - The collection of items to check
730      * @param bo - The BO whose keys we are looking for in the collection
731      * 
732      * @return how many have the same keys
733      */
734     public static int countObjectsWithIdentitcalKey(Collection<? extends PersistableBusinessObject> collection, PersistableBusinessObject bo) {
735         // todo: genericize collectionContainsObjectWithIdentitcalKey() to leverage this method?
736         int n = 0;
737         for (PersistableBusinessObject item : collection) {
738             if (equalByKeys(item, bo)) {
739                 n++;
740             }
741         }
742         return n;
743     }
744 
745     /**
746      * Compares a business object with a List of {@link PersistableBusinessObject}s to determine if an object with the same key as the BO exists in the list. If it
747      * does, the item is removed from the List. This is functionally similar to List.remove() that operates only on Key values.
748      * 
749      * @param controlList - The list of items to check
750      * @param bo - The BO whose keys we are looking for in the controlList
751      */
752 
753     public static void removeObjectWithIdentitcalKey(Collection<? extends PersistableBusinessObject> controlList, PersistableBusinessObject bo) {
754         for (Iterator<? extends PersistableBusinessObject> i = controlList.iterator(); i.hasNext();) {
755         	PersistableBusinessObject listBo = i.next();
756             if (equalByKeys(listBo, bo)) {
757                 i.remove();
758             }
759         }
760     }
761 
762     /**
763      * Compares a business object with a List of BOs to determine if an object with the same key as the BO exists in the list. If it
764      * does, the item is returned.
765      * 
766      * @param controlList - The list of items to check
767      * @param bo - The BO whose keys we are looking for in the controlList
768      */
769 
770     public static BusinessObject retrieveObjectWithIdentitcalKey(Collection<? extends PersistableBusinessObject> controlList, PersistableBusinessObject bo) {
771         BusinessObject returnBo = null;
772 
773         for (Iterator<? extends PersistableBusinessObject> i = controlList.iterator(); i.hasNext();) {
774         	PersistableBusinessObject listBo = i.next();
775             if (equalByKeys(listBo, bo)) {
776                 returnBo = listBo;
777             }
778         }
779 
780         return returnBo;
781     }
782 
783     /**
784      * Determines if a given string could represent a nested attribute of an object.
785      * 
786      * @param attributeName
787      * 
788      * @return true if the attribute is nested
789      */
790     public static boolean isNestedAttribute(String attributeName) {
791         boolean isNested = false;
792 
793         if (StringUtils.contains(attributeName, ".")) {
794             isNested = true;
795         }
796 
797         return isNested;
798     }
799 
800     /**
801      * Returns the prefix of a nested attribute name, or the empty string if the attribute name is not nested.
802      * 
803      * @param attributeName
804      * 
805      * @return everything BEFORE the last "." character in attributeName
806      */
807     public static String getNestedAttributePrefix(String attributeName) {
808         String prefix = "";
809 
810         if (StringUtils.contains(attributeName, ".")) {
811             prefix = StringUtils.substringBeforeLast(attributeName, ".");
812         }
813 
814         return prefix;
815     }
816 
817     /**
818      * Returns the primitive part of an attribute name string.
819      * 
820      * @param attributeName
821      * 
822      * @return everything AFTER the last "." character in attributeName
823      */
824     public static String getNestedAttributePrimitive(String attributeName) {
825         String primitive = attributeName;
826 
827         if (StringUtils.contains(attributeName, ".")) {
828             primitive = StringUtils.substringAfterLast(attributeName, ".");
829         }
830 
831         return primitive;
832     }
833 
834     /**
835      * This method is a OJB Proxy-safe way to test for null on a proxied object that may or may not be materialized yet. It is safe
836      * to use on a proxy (materialized or non-materialized) or on a non-proxy (ie, regular object). Note that this will force a
837      * materialization of the proxy if the object is a proxy and unmaterialized.
838      * 
839      * @param object - any object, proxied or not, materialized or not
840      * 
841      * @return true if the object (or underlying materialized object) is null, false otherwise
842      */
843     public static boolean isNull(Object object) {
844 
845         // regardless, if its null, then its null
846         if (object == null) {
847             return true;
848         }
849 
850         // only try to materialize the object to see if its null if this is a
851         // proxy object
852         if (ProxyHelper.isProxy(object) || ProxyHelper.isCollectionProxy(object)) {
853             if (ProxyHelper.getRealObject(object) == null) {
854                 return true;
855             }
856         }
857 
858         return false;
859     }
860 
861     /**
862      * This method is a OJB Proxy-safe way to test for notNull on a proxied object that may or may not be materialized yet. It is
863      * safe to use on a proxy (materialized or non-materialized) or on a non-proxy (ie, regular object). Note that this will force a
864      * materialization of the proxy if the object is a proxy and unmaterialized.
865      * 
866      * @param object - any object, proxied or not, materialized or not
867      * 
868      * @return true if the object (or underlying materialized object) is not null, true if its null
869      */
870     public static boolean isNotNull(Object object) {
871         return !ObjectUtils.isNull(object);
872     }
873 
874     /**
875      * This method runs the ObjectUtils.isNotNull() method for each item in a list of BOs. ObjectUtils.isNotNull() will materialize
876      * the objects if they are currently OJB proxies.
877      * 
878      * @param possiblyProxiedObjects - a Collection of objects that may be proxies
879      */
880     public static void materializeObjects(Collection possiblyProxiedObjects) {
881         for (Iterator i = possiblyProxiedObjects.iterator(); i.hasNext();) {
882             ObjectUtils.isNotNull(i.next());
883         }
884     }
885 
886     /**
887      * This method attempts to materialize all of the proxied reference objects (ie, sub-objects) hanging off the passed-in BO
888      * object. It will do it down to the specified depth. An IllegalArgumentException will be thrown if the bo object passed in is
889      * itself a non-materialized proxy object. If the bo passed in has no proxied sub-objects, then the object will not be modified,
890      * and no errors will be thrown. WARNING: Be careful using depth any greater than 2. The number of DB hits, time, and memory
891      * consumed grows exponentially with each additional increment to depth. Make sure you really need that depth before doing so.
892      * 
893      * @param bo A valid, populated BusinessObject containing (possibly) proxied sub-objects. This object will be modified in place.
894      * @param depth int Value 0-5 indicating how deep to recurse the materialization. If a zero (0) is passed in, then no work will
895      *        be done.
896      */
897     public static void materializeSubObjectsToDepth(PersistableBusinessObject bo, int depth) {
898         if (bo == null) {
899             throw new IllegalArgumentException("The bo passed in was null.");
900         }
901         if (depth < 0 || depth > 5) {
902             throw new IllegalArgumentException("The depth passed in was out of bounds.  Only values " + "between 0 and 5, inclusively, are allowed.");
903         }
904 
905         // if depth is zero, then we're done recursing and can just exit
906         if (depth == 0) {
907             return;
908         }
909 
910         // deal with the possibility that the bo passed in (ie, the parent object) is an un-materialized proxy
911         if (ProxyHelper.isProxy(bo)) {
912             if (!ProxyHelper.isMaterialized(bo)) {
913                 throw new IllegalArgumentException("The bo passed in is an un-materialized proxy, and cannot be used.");
914             }
915         }
916 
917         // get the list of reference objects hanging off the parent BO
918         if ( KNSServiceLocator.getPersistenceStructureService().isPersistable( bo.getClass() ) ) {
919 	        Map<String, Class> references = KNSServiceLocator.getPersistenceStructureService().listReferenceObjectFields(bo);
920 	
921 	        // initialize our in-loop objects
922 	        String referenceName = "";
923 	        Class referenceClass = null;
924 	        Object referenceValue = null;
925 	        Object realReferenceValue = null;
926 	
927 	        // for each reference object on the parent bo
928 	        for (Iterator iter = references.keySet().iterator(); iter.hasNext();) {
929 	            referenceName = (String) iter.next();
930 	            referenceClass = references.get(referenceName);
931 	
932 	            // if its a proxy, replace it with a non-proxy
933 	            referenceValue = getPropertyValue(bo, referenceName);
934 	            if (referenceValue != null) {
935 	                if (ProxyHelper.isProxy(referenceValue)) {
936 	                    realReferenceValue = ProxyHelper.getRealObject(referenceValue);
937 	                    if (realReferenceValue != null) {
938 	                        try {
939 	                            setObjectProperty(bo, referenceName, referenceClass, realReferenceValue);
940 	                        }
941 	                        catch (FormatException e) {
942 	                            throw new RuntimeException("FormatException: could not set the property '" + referenceName + "'.", e);
943 	                        }
944 	                        catch (IllegalAccessException e) {
945 	                            throw new RuntimeException("IllegalAccessException: could not set the property '" + referenceName + "'.", e);
946 	                        }
947 	                        catch (InvocationTargetException e) {
948 	                            throw new RuntimeException("InvocationTargetException: could not set the property '" + referenceName + "'.", e);
949 	                        }
950 	                        catch (NoSuchMethodException e) {
951 	                            throw new RuntimeException("NoSuchMethodException: could not set the property '" + referenceName + "'.", e);
952 	                        }
953 	                    }
954 	                }
955 	
956 	                // recurse down through this reference object
957 	                if (realReferenceValue instanceof PersistableBusinessObject && depth > 1) {
958 	                    materializeSubObjectsToDepth((PersistableBusinessObject) realReferenceValue, depth - 1);
959 	                }
960 	            }
961 	
962 	        }
963         }
964     }
965 
966     /**
967      * This method attempts to materialize all of the proxied reference objects (ie, sub-objects) hanging off the passed-in BO
968      * object. It will do it just three levels down. In other words, it will only materialize the objects that are direct members of
969      * the bo, objects that are direct members of those bos, that one more time, and no further down. An IllegalArgumentException 
970      * will be thrown if the bo object passed in is itself a non-materialized proxy object. If the bo passed in has no proxied 
971      * sub-objects, then the object will not be modified, and no errors will be thrown.
972      * 
973      * @param bo A valid, populated BusinessObject containing (possibly) proxied sub-objects. This object will be modified in place.
974      */
975     public static void materializeAllSubObjects(PersistableBusinessObject bo) {
976         materializeSubObjectsToDepth(bo, 3);
977     }
978 
979     /**
980      * This method safely extracts either simple values OR nested values. For example, if the bo is SubAccount, and the fieldName is
981      * a21SubAccount.subAccountTypeCode, this thing makes sure it gets the value off the very end attribute, no matter how deeply
982      * nested it is. The code would be slightly simpler if this was done recursively, but this is safer, and consumes a constant
983      * amount of memory, no matter how deeply nested it goes.
984      * 
985      * @param bo
986      * @param fieldName
987      * 
988      * @return The field value if it exists. If it doesnt, and the name is invalid, and
989      */
990     public static Object getNestedValue(Object bo, String fieldName) {
991 
992         if (bo == null) {
993             throw new IllegalArgumentException("The bo passed in was null.");
994         }
995         if (StringUtils.isBlank(fieldName)) {
996             throw new IllegalArgumentException("The fieldName passed in was blank.");
997         }
998 
999         // okay, this section of code is to handle sub-object values, like
1000         // SubAccount.a21SubAccount.subAccountTypeCode. it basically walks
1001         // through the period-delimited list of names, and ends up with the
1002         // final value.
1003         String[] fieldNameParts = fieldName.split("\\.");
1004         Object currentObject = null;
1005         Object priorObject = bo;
1006         for (int i = 0; i < fieldNameParts.length; i++) {
1007             String fieldNamePart = fieldNameParts[i];
1008 
1009             try {
1010                 if (fieldNamePart.indexOf("]") > 0) {
1011                     currentObject = PropertyUtils.getIndexedProperty(priorObject, fieldNamePart);
1012                 }
1013                 else {
1014                     currentObject = PropertyUtils.getSimpleProperty(priorObject, fieldNamePart);
1015                 }
1016             }
1017             catch (IllegalAccessException e) {
1018                 throw new RuntimeException("Caller does not have access to the property accessor method.", e);
1019             }
1020             catch (InvocationTargetException e) {
1021                 throw new RuntimeException("Property accessor method threw an exception.", e);
1022             }
1023             catch (NoSuchMethodException e) {
1024                 throw new RuntimeException("The accessor method requested for this property cannot be found.", e);
1025             }
1026 
1027             // materialize the proxy, if it is a proxy
1028             if (ProxyHelper.isProxy(currentObject)) {
1029                 currentObject = ProxyHelper.getRealObject(currentObject);
1030             }
1031 
1032             // if a node or the leaf is null, then we're done, there's no need to
1033             // continue accessing null things
1034             if (currentObject == null) {
1035                 return currentObject;
1036             }
1037 
1038             priorObject = currentObject;
1039         }
1040         return currentObject;
1041     }
1042 
1043     /**
1044      * Returns the equality of the two given objects, automatically handling when one or both of the objects is null.
1045      * 
1046      * @param obj1
1047      * @param obj2
1048      * 
1049      * @return true if both objects are null or both are equal
1050      */
1051     public static boolean nullSafeEquals(Object obj1, Object obj2) {
1052         if (obj1 != null && obj2 != null) {
1053             return obj1.equals(obj2);
1054         }
1055         else {
1056             return (obj1 == obj2);
1057         }
1058     }
1059     
1060     /**
1061      * This method safely creates a object from a class
1062      * Convenience method to create new object and throw a runtime exception if it cannot
1063      * If the class is an {@link ExternalizableBusinessObject}, this method will determine the interface for the EBO and query the
1064 	 * appropriate module service to create a new instance.
1065      * 
1066      * @param boClass
1067      * 
1068      * @return a newInstance() of clazz
1069      */
1070     public static Object createNewObjectFromClass(Class clazz) {
1071 		if (clazz == null) {
1072 			throw new RuntimeException("BO class was passed in as null");
1073 		}
1074 		try {
1075 			if (ExternalizableBusinessObject.class.isAssignableFrom(clazz)) {
1076 				Class eboInterface = ExternalizableBusinessObjectUtils.determineExternalizableBusinessObjectSubInterface(clazz);
1077 				ModuleService moduleService = KNSServiceLocator.getKualiModuleService().getResponsibleModuleService(eboInterface);
1078 				return moduleService.createNewObjectFromExternalizableClass(eboInterface);
1079 			}
1080 			else {
1081 				return clazz.newInstance();
1082 			}
1083 		} catch (Exception e) {
1084 			throw new RuntimeException("Error occured while trying to create a new instance for class " + clazz,e);
1085 		}
1086     }
1087 
1088 	/**
1089 	 * Return whether or not an attribute is writeable. This method is aware that that Collections may be involved and handles them
1090 	 * consistently with the way in which OJB handles specifying the attributes of elements of a Collection.
1091 	 * 
1092 	 * @param o
1093 	 * @param p
1094 	 * @return
1095 	 * @throws IllegalArgumentException
1096 	 */
1097 	public static boolean isWriteable(Object o, String p, PersistenceStructureService persistenceStructureService)
1098 			throws IllegalArgumentException {
1099 		if (null == o || null == p) {
1100 			throw new IllegalArgumentException("Cannot check writeable status with null arguments.");
1101 		}
1102 
1103 		boolean b = false;
1104 
1105 		// Try the easy way.
1106 		if (!(PropertyUtils.isWriteable(o, p))) {
1107 			// If that fails lets try to be a bit smarter, understanding that Collections may be involved.
1108 			if (-1 != p.indexOf('.')) {
1109 				String[] parts = p.split("\\.");
1110 
1111 				// Get the type of the attribute.
1112 				Class c = ObjectUtils.getPropertyType(o, parts[0], persistenceStructureService);
1113 
1114 				if (c != null) {
1115 					Object i = null;
1116 
1117 					// If the next level is a Collection, look into the collection, to find out what type its elements are.
1118 					if (Collection.class.isAssignableFrom(c)) {
1119 						Map<String, Class> m = persistenceStructureService.listCollectionObjectTypes(o.getClass());
1120 						c = m.get(parts[0]);
1121 					}
1122 
1123 					// Look into the attribute class to see if it is writeable.
1124 					try {
1125 						i = c.newInstance();
1126 
1127 						StringBuffer sb = new StringBuffer();
1128 						for (int x = 1; x < parts.length; x++) {
1129 							sb.append(1 == x ? "" : ".").append(parts[x]);
1130 						}
1131 						b = isWriteable(i, sb.toString(), persistenceStructureService);
1132 
1133 					} catch (Exception ex) {
1134 						LOG.error("Skipping Criteria: " + p + " - Unable to instantiate class : " + c.getName(), ex);
1135 					}
1136 				} else {
1137 					LOG.error("Skipping Criteria: " + p + " - Unable to determine class for object: "
1138 							+ o.getClass().getName() + " - " + parts[0]);
1139 				}
1140 			}
1141 
1142 		} else {
1143 			b = true;
1144 		}
1145 
1146 		return b;
1147 	}
1148 }