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