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}