/** * $Id: DeepUtils.java 41 2008-11-12 17:36:24Z azeckoski $ * $URL: http://reflectutils.googlecode.com/svn/trunk/src/main/java/org/azeckoski/reflectutils/DeepUtils.java $ * FieldUtils.java - genericdao - May 19, 2008 10:10:15 PM - azeckoski ************************************************************************** * Copyright (c) 2008 Aaron Zeckoski * Licensed under the Apache License, Version 2.0 * * A copy of the Apache License has been included in this * distribution and is available at: http://www.apache.org/licenses/LICENSE-2.0.txt * * Aaron Zeckoski (azeckoski @ gmail.com) (aaronz @ vt.edu) (aaron @ caret.cam.ac.uk) */ package org.azeckoski.reflectutils; import java.lang.ref.SoftReference; import java.lang.reflect.Array; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.Map.Entry; import org.azeckoski.reflectutils.ClassFields.FieldsFilter; import org.azeckoski.reflectutils.beanutils.FieldAdapter; import org.azeckoski.reflectutils.exceptions.FieldGetValueException; import org.azeckoski.reflectutils.exceptions.FieldSetValueException; import org.azeckoski.reflectutils.exceptions.FieldnameNotFoundException; import org.azeckoski.reflectutils.map.ArrayOrderedMap; /** * Class which provides methods for handling deep operations: <br/> * deep copy - make a copy of one object into another which traverses the entire object and duplicates all data down to immutable/simple fields <br/> * deep clone - clone an object by traversing it and making a copy of every field <br/> * deep map - turn an object into a map with only simple objects in it, all contained objects become nested maps <br/> * populate - turn a map into an object, can handle params maps (from a servlet request) and nested or non-nested string->object maps * * @author Aaron Zeckoski (azeckoski @ gmail.com) */ public class DeepUtils { /** * Empty constructor * <br/> * <b>WARNING:</b> use the {@link #getInstance()} method to get this rather than recreating it over and over */ public DeepUtils() { DeepUtils.setInstance(this); } protected ClassDataCacher getClassDataCacher() { return ClassDataCacher.getInstance(); } protected ConstructorUtils getConstructorUtils() { return ConstructorUtils.getInstance(); } protected ConversionUtils getConversionUtils() { return ConversionUtils.getInstance(); } protected FieldUtils getFieldUtils() { return FieldUtils.getInstance(); } protected FieldAdapter getFieldAdapter() { return getFieldUtils().getFieldAdapter(); } /** * Deep clone an object and all the values in it into a brand new object of the same type, * this will traverse the bean and will make new objects for all non-null values contained in the object * * @param <T> * @param object any java object, this can be a list, map, array, or any simple * object, it does not have to be a custom object or even a java bean, * also works with DynaBeans * @param maxDepth the number of objects to follow when traveling through the object and copying * the values from it, 0 means to only copy the simple values in the object, any objects will * be ignored and will end up as nulls, 1 means to follow the first objects found and copy all * of their simple values as well, and so forth * @param fieldNamesToSkip (optional) the names of fields to skip while cloning this object, * this only has an effect on the bottom level of the object, any fields found * on child objects will always be copied (if the maxDepth allows) * @return the cloned object */ @SuppressWarnings("unchecked") public <T> T deepClone(T object, int maxDepth, String[] fieldNamesToSkip) { Set<String> skip = ArrayUtils.makeSetFromArray(fieldNamesToSkip); T clone = (T) internalDeepClone(object, CopyDestination.ORIGINAL, maxDepth, skip, 0, true, false); return clone; } /** * Deep copies one object into another, this is primarily for copying between identical types of objects but * it can also handle copying between objects which are quite different, * this does not just do a reference copy of the values but actually creates new objects in the current classloader * * @param orig the original object to copy from * @param dest the object to copy the values to (must have the same fields with the same types) * @param maxDepth the number of objects to follow when traveling through the object and copying * the values from it, 0 means to only copy the simple values in the object, any objects will * be ignored and will end up as nulls, 1 means to follow the first objects found and copy all * of their simple values as well, and so forth * @param fieldNamesToSkip (optional) the names of fields to skip while cloning this object, * this only has an effect on the bottom level of the object, any fields found * on child objects will always be copied (if the maxDepth allows) * @param ignoreNulls if true then nulls are not copied and the destination retains the value it has, * if false then nulls are copied and the destination value will become a null if the original value is a null * @throws IllegalArgumentException if the copy cannot be completed because the objects to copy do not have matching fields or types */ public void deepCopy(Object orig, Object dest, int maxDepth, String[] fieldNamesToSkip, boolean ignoreNulls) { Set<String> skip = ArrayUtils.makeSetFromArray(fieldNamesToSkip); internalDeepCopy(orig, dest, maxDepth, skip, ignoreNulls, true); } /** * This handles the copying of objects to maps, it is recursive and is a deep operation which * traverses the entire object and clones every part of it. When converting to a map this will ensure * that there are no objects which are not part of java.lang or java.util in the new map<br/> * NOTE: This can handle simple objects (non-maps and non-beans) but will have to make up the initial map key * in the returned map, "data" will be used as the key<br/> * NOTE: Nulls are allowed to pass through this method (i.e. passing in a null object results in a null output) * * @param bean any java object * @param maxDepth the number of objects to follow when traveling through the object and copying * the values from it, 0 means to only copy the simple values in the object, any objects will * be ignored and will end up as nulls, 1 means to follow the first objects found and copy all * of their simple values as well, and so forth * @param fieldNamesToSkip (optional) the names of fields to skip while cloning this object, * this only has an effect on the bottom level of the object, any fields found * on child objects will always be copied (if the maxDepth allows) * @param ignoreNulls if true then nulls are not copied and the destination retains the value it has, * if false then nulls are copied and the destination value will become a null if the original value is a null * @param ignoreTransient if true then all transient fields will be skipped, useful when serializing * @param initialKey (optional) the initial key to use when simple objects are converted to maps, defaults to "data" * @return the resulting map which contains the cloned data from the object */ @SuppressWarnings("unchecked") public Map<String, Object> deepMap(Object object, int maxDepth, String[] fieldNamesToSkip, boolean ignoreNulls, boolean ignoreTransient, String initialKey) { Set<String> skip = ArrayUtils.makeSetFromArray(fieldNamesToSkip); Object clone = internalDeepClone(object, CopyDestination.MAP, maxDepth, skip, 0, ignoreNulls, ignoreTransient); Map<String, Object> map = null; if (clone != null) { if ( ConstructorUtils.isClassMap(clone.getClass())) { map = (Map<String, Object>) clone; } else { // handle the case of non-map/bean by wrapping in a map if (initialKey == null) { initialKey = "data"; } map = new HashMap<String, Object>(); map.put(initialKey, clone); } } return map; } /** * Populates an object with the values in the properties map, * this will not fail if the fieldName in the map is not a property on the * object or the fieldName cannot be written to with the value in the object. * This will attempt to convert the provided object values into the right values * to place in the object<br/> * <b>NOTE:</b> simple types like numbers and strings can almost always be converted from * just about anything though they will probably end up as 0 or ""<br/> * Setting fields supports simple, nested, indexed, and mapped values:<br/> * <b>Simple:</b> Get/set a field in a bean (or map), Example: "title", "id"<br/> * <b>Nested:</b> Get/set a field in a bean which is contained in another bean, Example: "someBean.title", "someBean.id"<br/> * <b>Indexed:</b> Get/set a list/array item by index in a bean, Example: "myList[1]", "anArray[2]"<br/> * <b>Mapped:</b> Get/set a map entry by key in a bean, Example: "myMap(key)", "someMap(thing)"<br/> * * @param object any object * @param properties a map of fieldNames -> Object * @return the list of fieldNames which were successfully written to the object * @throws IllegalArgumentException if the arguments are invalid */ public List<String> populate(Object object, Map<String, Object> properties) { if (object == null || properties == null) { throw new IllegalArgumentException("object and properties cannot be null"); } Map<String, Class<?>> fieldTypes = getFieldUtils().getFieldTypes(object.getClass(), FieldsFilter.ALL); List<String> writtenFields = new ArrayList<String>(); for (Entry<String, Object> entry : properties.entrySet()) { String fieldName = entry.getKey(); if (fieldName != null && ! "".equals(fieldName)) { Class<?> targetType = fieldTypes.get(fieldName); if (targetType != null) { Object value = entry.getValue(); try { if ( ConstructorUtils.isClassSimple(targetType) ) { // this should write the value or fail getFieldUtils().setFieldValue(object, fieldName, value, true); } else { // value is a bean so we will dig into it further Object dest = getFieldUtils().getFieldValue(object, fieldName); if (dest == null) { // need to construct the destination if (value.getClass().isArray() && targetType.isArray()) { // special case for array to array since the destination must match dest = ArrayUtils.template((Object[])value); } else { dest = getConstructorUtils().constructClass(targetType); } getFieldUtils().setFieldValue(object, fieldName, dest, true); } internalDeepCopy(value, dest, 5, null, false, true); } writtenFields.add(fieldName); // record that the value was written successfully } catch (RuntimeException e) { // this is OK, it just means we did not write the value continue; } } } } return writtenFields; } /** * Populates an object with the String array values in the params map, * this will not fail if the fieldName in the map is not a property on the * object or the fieldName cannot be written to with the value in the object<br/> * Arrays which are length 1 will be converted to a string before they are set on the target field <br/> * Setting fields supports simple, nested, indexed, and mapped values:<br/> * <b>Simple:</b> Get/set a field in a bean (or map), Example: "title", "id"<br/> * <b>Nested:</b> Get/set a field in a bean which is contained in another bean, Example: "someBean.title", "someBean.id"<br/> * <b>Indexed:</b> Get/set a list/array item by index in a bean, Example: "myList[1]", "anArray[2]"<br/> * <b>Mapped:</b> Get/set a map entry by key in a bean, Example: "myMap(key)", "someMap(thing)"<br/> * * @param object any object * @param params a map of fieldNames -> String[] (normally from an http request) * @return the list of fieldNames which were successfully written to the object * @throws IllegalArgumentException if the arguments are invalid */ public List<String> populateFromParams(Object object, Map<String, String[]> params) { if (object == null || params == null) { throw new IllegalArgumentException("object and params cannot be null"); } List<String> writtenFields = new ArrayList<String>(); for (Entry<String, String[]> entry : params.entrySet()) { String fieldName = entry.getKey(); if (fieldName != null && ! "".equals(fieldName)) { String[] value = entry.getValue(); try { // this should write the value or fail if (value != null) { Object toSet = value; if (value.length == 0) { toSet = ""; } else if (value.length == 1) { toSet = value[0]; } getFieldUtils().setFieldValue(object, fieldName, toSet, true); writtenFields.add(fieldName); // record that the value was written successfully } } catch (RuntimeException e) { // this is OK, it just means we did not write the value continue; } } } return writtenFields; } public static enum CopyDestination {ORIGINAL, MAP}; /** * This handles the cloning of objects to objects or maps, it is recursive and is a deep operation which * traverses the entire object and clones every part of it, when converting to a map this will ensure * that there are no objects which are not part of java.lang or java.util in the new map * @param bean any java object * @param dest the type of destination: ORIGINAL = an object of type matching bean, MAP = an ordered map * @param maxDepth the number of objects to follow when traveling through the object and copying * the values from it, 0 means to only copy the simple values in the object, any objects will * be ignored and will end up as nulls, 1 means to follow the first objects found and copy all * of their simple values as well, and so forth * @param fieldNamesToSkip the names of fields to skip while cloning this object, * this only has an effect on the bottom level of the object, any fields found * on child objects will always be copied (if the maxDepth allows) * @param currentDepth the current depth for recursion and halting when appropriate depth is reached * @param ignoreNulls if true then nulls are not copied and the destination retains the value it has, * if false then nulls are copied and the destination value will become a null if the original value is a null * @param ignoreTransient if true then all transient fields will be skipped, useful when serializing * @return the object or map which has been cloned */ @SuppressWarnings("unchecked") protected Object internalDeepClone(Object bean, CopyDestination dest, int maxDepth, Set<String> fieldNamesToSkip, int currentDepth, boolean ignoreNulls, boolean ignoreTransient) { Object copy = null; if ( bean != null ) { Class<?> beanClass = bean.getClass(); // always copy the simple types if possible if (ConstructorUtils.isClassSimple(beanClass) || ConstructorUtils.isClassSpecial(beanClass)) { // simple and special types are just ref copied copy = bean; } else { if (currentDepth <= maxDepth) { currentDepth++; try { // now do the cloning based on the thing to clone if (beanClass.isArray()) { // special case, use array reflection // make new array int length = ArrayUtils.size((Object[]) bean); Class<?> componentType = beanClass.getComponentType(); if ( ConstructorUtils.isClassBean(componentType) && CopyDestination.MAP.equals(dest)) { // special array component type for arrays of objects when map converting componentType = ArrayOrderedMap.class; } copy = ArrayUtils.create(componentType, length); // now copy the stuff into it for (int i = 0; i < length; i++) { Object value = Array.get(bean, i); Object clone = internalDeepClone(value, dest, maxDepth, null, currentDepth, ignoreNulls, ignoreTransient); Array.set(copy, i, clone); } } else if (Collection.class.isAssignableFrom(beanClass)) { // special case, clone everything in the list copy = getConstructorUtils().constructClass(beanClass); // make new list for (Object element : (Collection) bean) { Object clone = internalDeepClone(element, dest, maxDepth, null, currentDepth, ignoreNulls, ignoreTransient); ((Collection) copy).add( clone ); } } else if (Map.class.isAssignableFrom(beanClass)) { // special case, clone everything in the map except keys copy = getConstructorUtils().constructClass(beanClass); // make new map for (Object key : ((Map) bean).keySet()) { if ( fieldNamesToSkip != null && fieldNamesToSkip.contains(key) ) { continue; // skip to next } Object value = ((Map) bean).get(key); if (value == null && ignoreNulls) { continue; } try { ((Map) copy).put(key, internalDeepClone(value, dest, maxDepth, null, currentDepth, ignoreNulls, ignoreTransient) ); } catch (NullPointerException e) { // map does not support nulls so skip and keep going continue; } } } else if (getFieldAdapter().isAdaptableClass(beanClass)) { // special handling for dynabeans if (CopyDestination.MAP.equals(dest)) { copy = new ArrayOrderedMap<String, Object>(); } else { copy = getFieldAdapter().newInstance(bean); // make new dynabean } List<String> propertyNames = getFieldAdapter().getPropertyNames(bean); for (String name : propertyNames) { if ( fieldNamesToSkip != null && fieldNamesToSkip.contains(name) ) { continue; // skip to next } try { Object value = getFieldAdapter().getSimpleValue(bean, name); if (value == null && ignoreNulls) { continue; } if (CopyDestination.MAP.equals(dest)) { ((Map<String, Object>) copy).put(name, internalDeepClone(value, dest, maxDepth, null, currentDepth, ignoreNulls, ignoreTransient) ); } else { getFieldUtils().setFieldValue(copy, name, internalDeepClone(value, dest, maxDepth, null, currentDepth, ignoreNulls, ignoreTransient) ); } } catch (IllegalArgumentException e) { // this is ok, field might not be readable so we will not clone it, continue on } } } else { // regular javabean FieldsFilter filter = FieldsFilter.COMPLETE; if (CopyDestination.MAP.equals(dest)) { copy = new ArrayOrderedMap<String, Object>(); // maps should pick up all readable fields if (ignoreTransient) { filter = FieldsFilter.SERIALIZABLE; } else { filter = FieldsFilter.READABLE; } } else { copy = getConstructorUtils().constructClass(beanClass); // make new bean } ClassFields<?> cf = getFieldUtils().analyzeClass(beanClass); Map<String, Class<?>> types = cf.getFieldTypes(filter); for (String name : types.keySet()) { if (ClassFields.FIELD_CLASS.equals(name)) { continue; // No point in trying to set/get an object's class } if ( fieldNamesToSkip != null && fieldNamesToSkip.contains(name) ) { continue; // skip to next } try { Object value = getFieldUtils().getFieldValue(bean, name); if (value == null && ignoreNulls) { continue; } if (CopyDestination.MAP.equals(dest)) { ((Map<String, Object>) copy).put(name, internalDeepClone(value, dest, maxDepth, null, currentDepth, ignoreNulls, ignoreTransient) ); } else { getFieldUtils().setFieldValue(copy, name, internalDeepClone(value, dest, maxDepth, null, currentDepth, ignoreNulls, ignoreTransient) ); } } catch (FieldnameNotFoundException e) { // this is ok, field might not exist or might not be readable so we skip it } catch (FieldGetValueException e) { // this is ok, field might not be readable (it should be though) } catch (FieldSetValueException e) { // this is ok, field might not be writeable (should also not happen) } catch (IllegalArgumentException e) { // this is ok, failure should not stop the clone } } } } catch (Exception e) { // catch any possible thrown exception and translate (this is not a mistake so ignore findbugs) throw new RuntimeException("Failure during deep cloning ("+beanClass+") maxDepth="+maxDepth+", currentDepth="+currentDepth+": " + e.getMessage(), e); } } } } return copy; } /** * @param orig the original object to copy from * @param dest the object to copy the values to (must have the same fields with the same types) * @param maxDepth the number of objects to follow when traveling through the object and copying * the values from it, 0 means to only copy the simple values in the object, any objects will * be ignored and will end up as nulls, 1 means to follow the first objects found and copy all * of their simple values as well, and so forth * @param fieldNamesToSkip the names of fields to skip while cloning this object, * this only has an effect on the bottom level of the object, any fields found * on child objects will always be copied (if the maxDepth allows) * @param ignoreNulls if true then nulls are not copied and the destination retains the value it has, * if false then nulls are copied and the destination value will become a null if the original value is a null * @param autoConvert if true the the original will be converted to the dest type if needed * @throws IllegalArgumentException if the copy cannot be completed because the objects to copy do not have matching fields or types */ @SuppressWarnings("unchecked") protected void internalDeepCopy(Object orig, Object dest, int maxDepth, Set<String> fieldNamesToSkip, boolean ignoreNulls, boolean autoConvert) { if (orig == null || dest == null) { throw new IllegalArgumentException("original object and destination object must not be null, if you want to clone the object then use the clone method"); } int currentDepth = 1; Class<?> origClass = orig.getClass(); Class<?> destClass = dest.getClass(); // check if copy is possible first if (ConstructorUtils.getImmutableTypes().contains(destClass)) { // cannot copy to an immutable object throw new IllegalArgumentException("Cannot copy to an immutable object ("+destClass+")"); } else if (ConstructorUtils.isClassSpecial(origClass)) { // cannot copy special objects throw new IllegalArgumentException("Cannot copy a special object ("+origClass+")"); } if (autoConvert && ! ConstructorUtils.isClassBean(origClass) ) { // attempt to convert non-beans into the type we are copying to, fail if it does not work // This has to be done because the copy will simply fail if we try to copy between non-beans which are not equivalent try { orig = getConversionUtils().convert(orig, destClass); origClass = destClass; } catch (UnsupportedOperationException e) { throw new IllegalArgumentException("Cannot copy from an immutable object to a complex object without converter support"); } } // copy orig to dest try { if (origClass.isArray()) { // special case, copy and overwrite existing array values if (destClass.isArray()) { // TODO if the dest array is empty then fail try { for (int i = 0; i < Array.getLength(orig); i++) { Object value = Array.get(orig, i); if (ignoreNulls && value == null) { // don't copy this null over the existing value } else { Array.set(dest, i, internalDeepClone(value, CopyDestination.ORIGINAL, maxDepth, null, currentDepth, ignoreNulls, false)); } } } catch (ArrayIndexOutOfBoundsException e) { // partial copy is ok, continue on } } else { throw new IllegalArgumentException("Cannot copy a simple value to a complex object"); } } else if (Collection.class.isAssignableFrom(origClass)) { // special case, copy everything from orig and add to dest for (Object value : (Collection) orig) { if (ignoreNulls && value == null) { // don't copy this null over the existing value } else { ((Collection) dest).add( internalDeepClone(value, CopyDestination.ORIGINAL, maxDepth, null, currentDepth, ignoreNulls, false)); } } } else if (Map.class.isAssignableFrom(origClass)) { // special case clone everything in the map except keys for (Object key : ((Map) orig).keySet()) { if ( fieldNamesToSkip != null && fieldNamesToSkip.contains(key) ) { continue; // skip to next } Object value = ((Map) orig).get(key); if (ignoreNulls && value == null) { // don't copy this null over the existing value } else { ((Map) dest).put(key, internalDeepClone(value, CopyDestination.ORIGINAL, maxDepth, null, currentDepth, ignoreNulls, false)); } } } else if (getFieldAdapter().isAdaptableClass(origClass)) { List<String> propertyNames = getFieldAdapter().getPropertyNames(orig); for (String name : propertyNames) { if ( fieldNamesToSkip != null && fieldNamesToSkip.contains(name) ) { continue; // skip to next } try { Object value = getFieldAdapter().getSimpleValue(orig, name); if (ignoreNulls && value == null) { // don't copy this null over the existing value } else { getFieldUtils().setFieldValue(dest, name, internalDeepClone(value, CopyDestination.ORIGINAL, maxDepth, null, currentDepth, ignoreNulls, false)); } } catch (FieldnameNotFoundException e) { // it is ok for the objects to not be the same } catch (IllegalArgumentException e) { // it is ok for the objects to not be the same } } } else { // regular javabean Map<String, Object> values = getFieldUtils().getFieldValues(orig, FieldsFilter.READABLE, false); for (Entry<String, Object> entry : values.entrySet()) { String name = entry.getKey(); if (ClassFields.FIELD_CLASS.equals(name)) { continue; // No point in trying to set an object's class } if ( fieldNamesToSkip != null && fieldNamesToSkip.contains(name) ) { continue; // skip to next } try { Object value = getFieldUtils().getFieldValue(orig, name); if (ignoreNulls && value == null) { // don't copy this null over the existing value } else { getFieldUtils().setFieldValue(dest, name, internalDeepClone(value, CopyDestination.ORIGINAL, maxDepth, null, currentDepth, ignoreNulls, false)); } } catch (FieldnameNotFoundException e) { // this is ok, field might not exist or might not be readable so we skip it } catch (FieldGetValueException e) { // this is ok, field might not be readable } catch (FieldSetValueException e) { // this is ok, field might not be writeable } catch (IllegalArgumentException e) { // this is ok, failure should not stop the clone } } } } catch (Exception e) { // catch any possible thrown exception and translate (this is not a mistake so ignore findbugs) throw new IllegalArgumentException("Failure copying " + orig + " ("+origClass+") to " + dest + " ("+destClass+")" + e.getMessage(), e); } } @Override public String toString() { return "Deep:"+getClassDataCacher(); } // STATIC access protected static SoftReference<DeepUtils> instanceStorage; /** * Get a singleton instance of this class to work with (stored statically) <br/> * <b>WARNING</b>: do not hold onto this object or cache it yourself, call this method again if you need it again * @return a singleton instance of this class */ public static DeepUtils getInstance() { DeepUtils instance = (instanceStorage == null ? null : instanceStorage.get()); if (instance == null) { instance = DeepUtils.setInstance(null); } return instance; } /** * Set the singleton instance of the class which will be stored statically * @param instance the instance to use as the singleton instance */ public static DeepUtils setInstance(DeepUtils newInstance) { DeepUtils instance = newInstance; if (instance == null) { instance = new DeepUtils(); instance.singleton = true; } DeepUtils.timesCreated++; instanceStorage = new SoftReference<DeepUtils>(instance); return instance; } private static int timesCreated = 0; public static int getTimesCreated() { return timesCreated; } private boolean singleton = false; /** * @return true if this object is the singleton */ public boolean isSingleton() { return singleton; } }