001 /* 002 * Copyright 2011 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.kfs.module.purap.util; 017 018 import java.lang.reflect.Field; 019 import java.lang.reflect.InvocationTargetException; 020 import java.lang.reflect.Modifier; 021 import java.util.ArrayList; 022 import java.util.Collection; 023 import java.util.HashMap; 024 import java.util.HashSet; 025 import java.util.Iterator; 026 import java.util.List; 027 import java.util.Map; 028 import java.util.Set; 029 030 import org.kuali.kfs.module.purap.PurapConstants; 031 import org.kuali.kfs.sys.context.SpringContext; 032 import org.kuali.rice.kns.bo.BusinessObject; 033 import org.kuali.rice.kns.bo.ExternalizableBusinessObject; 034 import org.kuali.rice.kns.service.KualiModuleService; 035 import org.kuali.rice.kns.service.ModuleService; 036 import org.kuali.rice.kns.service.PersistenceService; 037 import org.kuali.rice.kns.util.ExternalizableBusinessObjectUtils; 038 import org.kuali.rice.kns.util.ObjectUtils; 039 import org.kuali.rice.kns.util.TypedArrayList; 040 import org.kuali.rice.kns.web.format.FormatException; 041 /** 042 * Purap Object Utils. 043 * Similar to the nervous system ObjectUtils this class contains methods to reflectively set and get values on 044 * BusinessObjects that are passed in. 045 */ 046 public class PurApObjectUtils { 047 private static org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(PurApObjectUtils.class); 048 049 /** 050 * 051 * Populates a class using a base class to determine fields 052 * 053 * @param base the class to determine what fields to copy 054 * @param src the source class 055 * @param target the target class 056 * @param supplementalUncopyable a list of fields to never copy 057 */ 058 public static void populateFromBaseClass(Class base, BusinessObject src, BusinessObject target, Map supplementalUncopyable) { 059 List<String> fieldNames = new ArrayList<String>(); 060 Field[] fields = base.getDeclaredFields(); 061 062 063 for (Field field : fields) { 064 if (!Modifier.isTransient(field.getModifiers())) { 065 fieldNames.add(field.getName()); 066 } 067 else { 068 if ( LOG.isDebugEnabled() ) { 069 LOG.debug("field " + field.getName() + " is transient, skipping "); 070 } 071 } 072 } 073 int counter = 0; 074 for (String fieldName : fieldNames) { 075 if ((isProcessableField(base, fieldName, PurapConstants.KNOWN_UNCOPYABLE_FIELDS)) && (isProcessableField(base, fieldName, supplementalUncopyable))) { 076 attemptCopyOfFieldName(base.getName(), fieldName, src, target, supplementalUncopyable); 077 counter++; 078 } 079 } 080 if ( LOG.isDebugEnabled() ) { 081 LOG.debug("Population complete for " + counter + " fields out of a total of " + fieldNames.size() + " potential fields in object with base class '" + base + "'"); 082 } 083 } 084 085 /** 086 * 087 * True if a field is processable 088 * 089 * @param baseClass the base class 090 * @param fieldName the field name to detrmine if processable 091 * @param excludedFieldNames field names to exclude 092 * @return true if a field is processable 093 */ 094 protected static boolean isProcessableField(Class baseClass, String fieldName, Map excludedFieldNames) { 095 if (excludedFieldNames.containsKey(fieldName)) { 096 Class potentialClassName = (Class) excludedFieldNames.get(fieldName); 097 if ((ObjectUtils.isNull(potentialClassName)) || (potentialClassName.equals(baseClass))) { 098 return false; 099 } 100 } 101 return true; 102 } 103 104 /** 105 * 106 * Attempts to copy a field 107 * @param baseClass the base class 108 * @param fieldName the field name to determine if processable 109 * @param sourceObject source object 110 * @param targetObject target object 111 * @param supplementalUncopyable 112 */ 113 protected static void attemptCopyOfFieldName(String baseClassName, String fieldName, BusinessObject sourceObject, BusinessObject targetObject, Map supplementalUncopyable) { 114 try { 115 116 Object propertyValue = ObjectUtils.getPropertyValue(sourceObject, fieldName); 117 if ((ObjectUtils.isNotNull(propertyValue)) && (Collection.class.isAssignableFrom(propertyValue.getClass()))) { 118 if ( LOG.isDebugEnabled() ) { 119 LOG.debug("attempting to copy collection field '" + fieldName + "' using base class '" + baseClassName + "' and property value class '" + propertyValue.getClass() + "'"); 120 } 121 copyCollection(fieldName, targetObject, propertyValue, supplementalUncopyable); 122 } 123 else { 124 String propertyValueClass = (ObjectUtils.isNotNull(propertyValue)) ? propertyValue.getClass().toString() : "(null)"; 125 if ( LOG.isDebugEnabled() ) { 126 LOG.debug("attempting to set field '" + fieldName + "' using base class '" + baseClassName + "' and property value class '" + propertyValueClass + "'"); 127 } 128 ObjectUtils.setObjectProperty(targetObject, fieldName, propertyValue); 129 } 130 } 131 catch (Exception e) { 132 // purposefully skip for now 133 // (I wish objectUtils getPropertyValue threw named errors instead of runtime) so I could 134 // selectively skip 135 if ( LOG.isDebugEnabled() ) { 136 LOG.debug("couldn't set field '" + fieldName + "' using base class '" + baseClassName + "' due to exception with class name '" + e.getClass().getName() + "'", e); 137 } 138 } 139 } 140 141 /** 142 * 143 * Copies a collection 144 * 145 * @param fieldName field to copy 146 * @param targetObject the object of the collection 147 * @param propertyValue value to copy 148 * @param supplementalUncopyable uncopyable fields 149 * @throws FormatException 150 * @throws IllegalAccessException 151 * @throws InvocationTargetException 152 * @throws NoSuchMethodException 153 */ 154 protected static void copyCollection(String fieldName, BusinessObject targetObject, Object propertyValue, Map supplementalUncopyable) throws FormatException, IllegalAccessException, InvocationTargetException, NoSuchMethodException { 155 Collection sourceList = (Collection) propertyValue; 156 Collection listToSet = null; 157 158 // materialize collections 159 if (ObjectUtils.isNotNull(sourceList)) { 160 ObjectUtils.materializeObjects(sourceList); 161 } 162 163 // TypedArrayList requires argument so handle differently than below 164 if (sourceList instanceof TypedArrayList) { 165 TypedArrayList typedArray = (TypedArrayList) sourceList; 166 if ( LOG.isDebugEnabled() ) { 167 LOG.debug("collection will be typed using class '" + typedArray.getListObjectType() + "'"); 168 } 169 try { 170 listToSet = new TypedArrayList(typedArray.getListObjectType()); 171 } 172 catch (Exception e) { 173 if ( LOG.isDebugEnabled() ) { 174 LOG.debug("couldn't set class '" + propertyValue.getClass() + "' on collection... using TypedArrayList using ", e); 175 } 176 listToSet = new ArrayList(); 177 } 178 } 179 else { 180 try { 181 listToSet = sourceList.getClass().newInstance(); 182 } 183 catch (Exception e) { 184 if ( LOG.isDebugEnabled() ) { 185 LOG.debug("couldn't set class '" + propertyValue.getClass() + "' on collection..." + fieldName + " using " + sourceList.getClass()); 186 } 187 listToSet = new ArrayList(); 188 } 189 } 190 191 192 for (Iterator iterator = sourceList.iterator(); iterator.hasNext();) { 193 BusinessObject sourceCollectionObject = (BusinessObject) iterator.next(); 194 if ( LOG.isDebugEnabled() ) { 195 LOG.debug("attempting to copy collection member with class '" + sourceCollectionObject.getClass() + "'"); 196 } 197 BusinessObject targetCollectionObject = (BusinessObject) createNewObjectFromClass(sourceCollectionObject.getClass()); 198 populateFromBaseWithSuper(sourceCollectionObject, targetCollectionObject, supplementalUncopyable, new HashSet<Class>()); 199 // BusinessObject targetCollectionObject = (BusinessObject)ObjectUtils.deepCopy((Serializable)sourceCollectionObject); 200 Map pkMap = SpringContext.getBean(PersistenceService.class).getPrimaryKeyFieldValues(targetCollectionObject); 201 Set<String> pkFields = pkMap.keySet(); 202 for (String field : pkFields) { 203 ObjectUtils.setObjectProperty(targetCollectionObject, field, null); 204 } 205 listToSet.add(targetCollectionObject); 206 } 207 ObjectUtils.setObjectProperty(targetObject, fieldName, listToSet); 208 } 209 210 /** 211 * This method safely creates a object from a class 212 * Convenience method to create new object and throw a runtime exception if it cannot 213 * If the class is an {@link ExternalizableBusinessObject}, this method will determine the interface for the EBO and query the 214 * appropriate module service to create a new instance. 215 * 216 * @param boClass 217 * 218 * @return a newInstance() of clazz 219 */ 220 protected static Object createNewObjectFromClass(Class clazz) { 221 if (clazz == null) { 222 throw new RuntimeException("BO class was passed in as null"); 223 } 224 try { 225 if (clazz.getSuperclass().equals(ExternalizableBusinessObject.class)) { 226 Class eboInterface = ExternalizableBusinessObjectUtils.determineExternalizableBusinessObjectSubInterface(clazz); 227 ModuleService moduleService = SpringContext.getBean(KualiModuleService.class).getResponsibleModuleService(eboInterface); 228 return moduleService.createNewObjectFromExternalizableClass(eboInterface); 229 } 230 else { 231 return clazz.newInstance(); 232 } 233 } catch (Exception e) { 234 throw new RuntimeException("Error occured while trying to create a new instance for class " + clazz); 235 } 236 } 237 238 /** 239 * Copies based on a class template it does not copy fields in Known Uncopyable Fields 240 * 241 * @param base the base class 242 * @param src source 243 * @param target target 244 */ 245 public static void populateFromBaseClass(Class base, BusinessObject src, BusinessObject target) { 246 populateFromBaseClass(base, src, target, new HashMap()); 247 } 248 249 /** 250 * 251 * Populates from a base class traversing up the object hierarchy. 252 * 253 * @param sourceObject object to copy from 254 * @param targetObject object to copy to 255 * @param supplementalUncopyableFieldNames fields to exclude 256 * @param classesToExclude classes to exclude 257 */ 258 public static void populateFromBaseWithSuper(BusinessObject sourceObject, BusinessObject targetObject, Map supplementalUncopyableFieldNames, Set<Class> classesToExclude) { 259 List<Class> classesToCopy = new ArrayList<Class>(); 260 Class sourceObjectClass = sourceObject.getClass(); 261 classesToCopy.add(sourceObjectClass); 262 while (sourceObjectClass.getSuperclass() != null) { 263 sourceObjectClass = sourceObjectClass.getSuperclass(); 264 if (!classesToExclude.contains(sourceObjectClass)) { 265 classesToCopy.add(sourceObjectClass); 266 } 267 } 268 for (int i = (classesToCopy.size() - 1); i >= 0; i--) { 269 Class temp = classesToCopy.get(i); 270 populateFromBaseClass(temp, sourceObject, targetObject, supplementalUncopyableFieldNames); 271 } 272 } 273 274 // ***** following changes are to work around an ObjectUtils bug and are copied from ObjectUtils.java 275 /** 276 * 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 277 * does, the item is returned. 278 * 279 * @param controlList - The list of items to check 280 * @param bo - The BO whose keys we are looking for in the controlList 281 */ 282 public static BusinessObject retrieveObjectWithIdentitcalKey(Collection controlList, BusinessObject bo) { 283 BusinessObject returnBo = null; 284 285 for (Iterator i = controlList.iterator(); i.hasNext();) { 286 BusinessObject listBo = (BusinessObject) i.next(); 287 if (equalByKeys(listBo, bo)) { 288 returnBo = listBo; 289 } 290 } 291 292 return returnBo; 293 } 294 295 /** 296 * Compares two business objects for equality of type and key values. 297 * 298 * @param bo1 299 * @param bo2 300 * @return boolean indicating whether the two objects are equal. 301 */ 302 public static boolean equalByKeys(BusinessObject bo1, BusinessObject bo2) { 303 boolean equal = true; 304 305 if (bo1 == null && bo2 == null) { 306 equal = true; 307 } 308 else if (bo1 == null || bo2 == null) { 309 equal = false; 310 } 311 else if (!bo1.getClass().getName().equals(bo2.getClass().getName())) { 312 equal = false; 313 } 314 else { 315 Map bo1Keys = SpringContext.getBean(PersistenceService.class).getPrimaryKeyFieldValues(bo1); 316 Map bo2Keys = SpringContext.getBean(PersistenceService.class).getPrimaryKeyFieldValues(bo2); 317 for (Iterator iter = bo1Keys.keySet().iterator(); iter.hasNext();) { 318 String keyName = (String) iter.next(); 319 if (bo1Keys.get(keyName) != null && bo2Keys.get(keyName) != null) { 320 if (!bo1Keys.get(keyName).toString().equals(bo2Keys.get(keyName).toString())) { 321 equal = false; 322 } 323 } 324 else { 325 // CHANGE FOR PurapOjbCollectionHelper change if one is null we are likely looking at a new object (sequence) which is definitely 326 // not equal 327 equal = false; 328 } 329 } 330 } 331 332 333 return equal; 334 } 335 // ***** END copied from ObjectUtils.java changes 336 }