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    }