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 }