View Javadoc

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