/*
* The Kuali Financial System, a comprehensive financial management system for higher education.
*
* Copyright 2005-2014 The Kuali Foundation
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.kuali.kfs.module.purap.util;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.kuali.kfs.module.purap.PurapConstants;
import org.kuali.kfs.sys.context.SpringContext;
import org.kuali.rice.core.web.format.FormatException;
import org.kuali.rice.krad.bo.BusinessObject;
import org.kuali.rice.krad.bo.ExternalizableBusinessObject;
import org.kuali.rice.krad.service.KualiModuleService;
import org.kuali.rice.krad.service.ModuleService;
import org.kuali.rice.krad.service.PersistenceService;
import org.kuali.rice.krad.util.ExternalizableBusinessObjectUtils;
import org.kuali.rice.krad.util.ObjectUtils;
/**
* Purap Object Utils.
* Similar to the nervous system ObjectUtils this class contains methods to reflectively set and get values on
* BusinessObjects that are passed in.
*/
public class PurApObjectUtils {
private static org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(PurApObjectUtils.class);
/**
*
* Populates a class using a base class to determine fields
*
* @param base the class to determine what fields to copy
* @param src the source class
* @param target the target class
* @param supplementalUncopyable a list of fields to never copy
*/
public static void populateFromBaseClass(Class base, BusinessObject src, BusinessObject target, Map supplementalUncopyable) {
List<String> fieldNames = new ArrayList<String>();
Field[] fields = base.getDeclaredFields();
for (Field field : fields) {
if (!Modifier.isTransient(field.getModifiers())) {
fieldNames.add(field.getName());
}
else {
if ( LOG.isDebugEnabled() ) {
LOG.debug("field " + field.getName() + " is transient, skipping ");
}
}
}
int counter = 0;
for (String fieldName : fieldNames) {
if ((isProcessableField(base, fieldName, PurapConstants.KNOWN_UNCOPYABLE_FIELDS)) && (isProcessableField(base, fieldName, supplementalUncopyable))) {
attemptCopyOfFieldName(base.getName(), fieldName, src, target, supplementalUncopyable);
counter++;
}
}
if ( LOG.isDebugEnabled() ) {
LOG.debug("Population complete for " + counter + " fields out of a total of " + fieldNames.size() + " potential fields in object with base class '" + base + "'");
}
}
/**
*
* True if a field is processable
*
* @param baseClass the base class
* @param fieldName the field name to detrmine if processable
* @param excludedFieldNames field names to exclude
* @return true if a field is processable
*/
protected static boolean isProcessableField(Class baseClass, String fieldName, Map excludedFieldNames) {
if (excludedFieldNames.containsKey(fieldName)) {
Class potentialClassName = (Class) excludedFieldNames.get(fieldName);
if ((ObjectUtils.isNull(potentialClassName)) || (potentialClassName.equals(baseClass))) {
return false;
}
}
return true;
}
/**
*
* Attempts to copy a field
* @param baseClass the base class
* @param fieldName the field name to determine if processable
* @param sourceObject source object
* @param targetObject target object
* @param supplementalUncopyable
*/
protected static void attemptCopyOfFieldName(String baseClassName, String fieldName, BusinessObject sourceObject, BusinessObject targetObject, Map supplementalUncopyable) {
try {
Object propertyValue = ObjectUtils.getPropertyValue(sourceObject, fieldName);
if ((ObjectUtils.isNotNull(propertyValue)) && (Collection.class.isAssignableFrom(propertyValue.getClass()))) {
if ( LOG.isDebugEnabled() ) {
LOG.debug("attempting to copy collection field '" + fieldName + "' using base class '" + baseClassName + "' and property value class '" + propertyValue.getClass() + "'");
}
copyCollection(fieldName, targetObject, (Collection)propertyValue, supplementalUncopyable);
}
else {
String propertyValueClass = (ObjectUtils.isNotNull(propertyValue)) ? propertyValue.getClass().toString() : "(null)";
if ( LOG.isDebugEnabled() ) {
LOG.debug("attempting to set field '" + fieldName + "' using base class '" + baseClassName + "' and property value class '" + propertyValueClass + "'");
}
ObjectUtils.setObjectProperty(targetObject, fieldName, propertyValue);
}
}
catch (Exception e) {
// purposefully skip for now
// (I wish objectUtils getPropertyValue threw named errors instead of runtime) so I could
// selectively skip
if ( LOG.isDebugEnabled() ) {
LOG.debug("couldn't set field '" + fieldName + "' using base class '" + baseClassName + "' due to exception with class name '" + e.getClass().getName() + "'", e);
}
}
}
/**
*
* Copies a collection
*
* @param fieldName field to copy
* @param targetObject the object of the collection
* @param propertyValue value to copy
* @param supplementalUncopyable uncopyable fields
* @throws FormatException
* @throws IllegalAccessException
* @throws InvocationTargetException
* @throws NoSuchMethodException
*/
protected static <T extends BusinessObject> void copyCollection(String fieldName, BusinessObject targetObject, Collection<T> sourceList, Map supplementalUncopyable) throws FormatException, IllegalAccessException, InvocationTargetException, NoSuchMethodException {
Collection listToSet = null;
// materialize collections
if (ObjectUtils.isNotNull(sourceList)) {
ObjectUtils.materializeObjects(sourceList);
}
// ArrayList requires argument so handle differently than below
try {
listToSet = sourceList.getClass().newInstance();
}
catch (Exception e) {
if ( LOG.isDebugEnabled() ) {
LOG.debug("couldn't set class '" + sourceList.getClass() + "' on collection..." + fieldName + " using " + sourceList.getClass());
}
listToSet = new ArrayList<T>();
}
for (Iterator iterator = sourceList.iterator(); iterator.hasNext();) {
BusinessObject sourceCollectionObject = (BusinessObject) iterator.next();
if ( LOG.isDebugEnabled() ) {
LOG.debug("attempting to copy collection member with class '" + sourceCollectionObject.getClass() + "'");
}
BusinessObject targetCollectionObject = (BusinessObject) createNewObjectFromClass(sourceCollectionObject.getClass());
populateFromBaseWithSuper(sourceCollectionObject, targetCollectionObject, supplementalUncopyable, new HashSet<Class>());
// BusinessObject targetCollectionObject = (BusinessObject)ObjectUtils.deepCopy((Serializable)sourceCollectionObject);
Map pkMap = SpringContext.getBean(PersistenceService.class).getPrimaryKeyFieldValues(targetCollectionObject);
Set<String> pkFields = pkMap.keySet();
for (String field : pkFields) {
ObjectUtils.setObjectProperty(targetCollectionObject, field, null);
}
listToSet.add(targetCollectionObject);
}
ObjectUtils.setObjectProperty(targetObject, fieldName, listToSet);
}
/**
* This method safely creates a object from a class
* Convenience method to create new object and throw a runtime exception if it cannot
* If the class is an {@link ExternalizableBusinessObject}, this method will determine the interface for the EBO and query the
* appropriate module service to create a new instance.
*
* @param boClass
*
* @return a newInstance() of clazz
*/
protected static Object createNewObjectFromClass(Class clazz) {
if (clazz == null) {
throw new RuntimeException("BO class was passed in as null");
}
try {
if (clazz.getSuperclass().equals(ExternalizableBusinessObject.class)) {
Class eboInterface = ExternalizableBusinessObjectUtils.determineExternalizableBusinessObjectSubInterface(clazz);
ModuleService moduleService = SpringContext.getBean(KualiModuleService.class).getResponsibleModuleService(eboInterface);
return moduleService.createNewObjectFromExternalizableClass(eboInterface);
}
else {
return clazz.newInstance();
}
} catch (Exception e) {
throw new RuntimeException("Error occured while trying to create a new instance for class " + clazz);
}
}
/**
* Copies based on a class template it does not copy fields in Known Uncopyable Fields
*
* @param base the base class
* @param src source
* @param target target
*/
public static void populateFromBaseClass(Class base, BusinessObject src, BusinessObject target) {
populateFromBaseClass(base, src, target, new HashMap());
}
/**
*
* Populates from a base class traversing up the object hierarchy.
*
* @param sourceObject object to copy from
* @param targetObject object to copy to
* @param supplementalUncopyableFieldNames fields to exclude
* @param classesToExclude classes to exclude
*/
public static void populateFromBaseWithSuper(BusinessObject sourceObject, BusinessObject targetObject, Map supplementalUncopyableFieldNames, Set<Class> classesToExclude) {
List<Class> classesToCopy = new ArrayList<Class>();
Class sourceObjectClass = sourceObject.getClass();
classesToCopy.add(sourceObjectClass);
while (sourceObjectClass.getSuperclass() != null) {
sourceObjectClass = sourceObjectClass.getSuperclass();
if (!classesToExclude.contains(sourceObjectClass)) {
classesToCopy.add(sourceObjectClass);
}
}
for (int i = (classesToCopy.size() - 1); i >= 0; i--) {
Class temp = classesToCopy.get(i);
populateFromBaseClass(temp, sourceObject, targetObject, supplementalUncopyableFieldNames);
}
}
// ***** following changes are to work around an ObjectUtils bug and are copied from ObjectUtils.java
/**
* 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
* does, the item is returned.
*
* @param controlList - The list of items to check
* @param bo - The BO whose keys we are looking for in the controlList
*/
public static BusinessObject retrieveObjectWithIdentitcalKey(Collection controlList, BusinessObject bo) {
BusinessObject returnBo = null;
for (Iterator i = controlList.iterator(); i.hasNext();) {
BusinessObject listBo = (BusinessObject) i.next();
if (equalByKeys(listBo, bo)) {
returnBo = listBo;
}
}
return returnBo;
}
/**
* Compares two business objects for equality of type and key values.
*
* @param bo1
* @param bo2
* @return boolean indicating whether the two objects are equal.
*/
public static boolean equalByKeys(BusinessObject bo1, BusinessObject bo2) {
boolean equal = true;
if (bo1 == null && bo2 == null) {
equal = true;
}
else if (bo1 == null || bo2 == null) {
equal = false;
}
else if (!bo1.getClass().getName().equals(bo2.getClass().getName())) {
equal = false;
}
else {
Map bo1Keys = SpringContext.getBean(PersistenceService.class).getPrimaryKeyFieldValues(bo1);
Map bo2Keys = SpringContext.getBean(PersistenceService.class).getPrimaryKeyFieldValues(bo2);
for (Iterator iter = bo1Keys.keySet().iterator(); iter.hasNext();) {
String keyName = (String) iter.next();
if (bo1Keys.get(keyName) != null && bo2Keys.get(keyName) != null) {
if (!bo1Keys.get(keyName).toString().equals(bo2Keys.get(keyName).toString())) {
equal = false;
}
}
else {
// CHANGE FOR PurapOjbCollectionHelper change if one is null we are likely looking at a new object (sequence) which is definitely
// not equal
equal = false;
}
}
}
return equal;
}
// ***** END copied from ObjectUtils.java changes
}