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