Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Recursive BeanUtils.describe()

Is there a version of BeanUtils.describe(customer) that recursively calls the describe() method on the complex attributes of 'customer'.

class Customer {

String id;
Address address;

}

Here, I would like the describe method to retrieve the contents of the address attribute as well.

Currently, all I have can see the name of the class as follows:

{id=123, address=com.test.entities.Address@2a340e}
like image 440
TheLameProgrammer Avatar asked May 26 '11 04:05

TheLameProgrammer


People also ask

What is BeanUtils copyProperties () Java?

BeanUtils class provides a copyProperties method that copies the properties of source object to target object where the property name is same in both objects.

What is the use of BeanUtils copyProperties?

copyProperties. Copy the property values of the given source bean into the given target bean, only setting properties defined in the given "editable" class (or interface).

Does BeanUtils use reflection?

As they both ultimately use Reflection you aren't likely to notice much difference, unless the higher-level API is doing things you don't need done. See also java. beans.


2 Answers

Funny, I would like the describe method to retrieve the contents of nested attributes as well, I don't understand why it doesn't. I went ahead and rolled my own, though. Here it is, you can just call:

Map<String,String> beanMap = BeanUtils.recursiveDescribe(customer); 

A couple of caveats.

  1. I'm wasn't sure how commons BeanUtils formatted attributes in collections, so i went with "attribute[index]".
  2. I'm wasn't sure how it formatted attributes in maps, so i went with "attribute[key]".
  3. For name collisions the precedence is this: First properties are loaded from the fields of super classes, then the class, then from the getter methods.
  4. I haven't analyzed the performance of this method. If you have objects with large collections of objects that also contain collections, you might have some issues.
  5. This is alpha code, not garunteed to be bug free.
  6. I am assuming that you have the latest version of commons beanutils

Also, fyi, this is roughly taken from a project I've been working on called, affectionately, java in jails so you could just download it and then run:

Map<String, String[]> beanMap = new SimpleMapper().toMap(customer);

Though, you'll notice that it returns a String[], instead of a String, which may not work for your needs. Anyway, the below code should work, so have at it!

public class BeanUtils {
    public static Map<String, String> recursiveDescribe(Object object) {
        Set cache = new HashSet();
        return recursiveDescribe(object, null, cache);
    }

    private static Map<String, String> recursiveDescribe(Object object, String prefix, Set cache) {
        if (object == null || cache.contains(object)) return Collections.EMPTY_MAP;
        cache.add(object);
        prefix = (prefix != null) ? prefix + "." : "";

        Map<String, String> beanMap = new TreeMap<String, String>();

        Map<String, Object> properties = getProperties(object);
        for (String property : properties.keySet()) {
            Object value = properties.get(property);
            try {
                if (value == null) {
                    //ignore nulls
                } else if (Collection.class.isAssignableFrom(value.getClass())) {
                    beanMap.putAll(convertAll((Collection) value, prefix + property, cache));
                } else if (value.getClass().isArray()) {
                    beanMap.putAll(convertAll(Arrays.asList((Object[]) value), prefix + property, cache));
                } else if (Map.class.isAssignableFrom(value.getClass())) {
                    beanMap.putAll(convertMap((Map) value, prefix + property, cache));
                } else {
                    beanMap.putAll(convertObject(value, prefix + property, cache));
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return beanMap;
    }

    private static Map<String, Object> getProperties(Object object) {
        Map<String, Object> propertyMap = getFields(object);
        //getters take precedence in case of any name collisions
        propertyMap.putAll(getGetterMethods(object));
        return propertyMap;
    }

    private static Map<String, Object> getGetterMethods(Object object) {
        Map<String, Object> result = new HashMap<String, Object>();
        BeanInfo info;
        try {
            info = Introspector.getBeanInfo(object.getClass());
            for (PropertyDescriptor pd : info.getPropertyDescriptors()) {
                Method reader = pd.getReadMethod();
                if (reader != null) {
                    String name = pd.getName();
                    if (!"class".equals(name)) {
                        try {
                            Object value = reader.invoke(object);
                            result.put(name, value);
                        } catch (Exception e) {
                            //you can choose to do something here
                        }
                    }
                }
            }
        } catch (IntrospectionException e) {
            //you can choose to do something here
        } finally {
            return result;
        }

    }

    private static Map<String, Object> getFields(Object object) {
        return getFields(object, object.getClass());
    }

    private static Map<String, Object> getFields(Object object, Class<?> classType) {
        Map<String, Object> result = new HashMap<String, Object>();

        Class superClass = classType.getSuperclass();
        if (superClass != null) result.putAll(getFields(object, superClass));

        //get public fields only
        Field[] fields = classType.getFields();
        for (Field field : fields) {
            try {
                result.put(field.getName(), field.get(object));
            } catch (IllegalAccessException e) {
                //you can choose to do something here
            }
        }
        return result;
    }

    private static Map<String, String> convertAll(Collection<Object> values, String key, Set cache) {
        Map<String, String> valuesMap = new HashMap<String, String>();
        Object[] valArray = values.toArray();
        for (int i = 0; i < valArray.length; i++) {
            Object value = valArray[i];
            if (value != null) valuesMap.putAll(convertObject(value, key + "[" + i + "]", cache));
        }
        return valuesMap;
    }

    private static Map<String, String> convertMap(Map<Object, Object> values, String key, Set cache) {
        Map<String, String> valuesMap = new HashMap<String, String>();
        for (Object thisKey : values.keySet()) {
            Object value = values.get(thisKey);
            if (value != null) valuesMap.putAll(convertObject(value, key + "[" + thisKey + "]", cache));
        }
        return valuesMap;
    }

    private static ConvertUtilsBean converter = BeanUtilsBean.getInstance().getConvertUtils();

    private static Map<String, String> convertObject(Object value, String key, Set cache) {
        //if this type has a registered converted, then get the string and return
        if (converter.lookup(value.getClass()) != null) {
            String stringValue = converter.convert(value);
            Map<String, String> valueMap = new HashMap<String, String>();
            valueMap.put(key, stringValue);
            return valueMap;
        } else {
            //otherwise, treat it as a nested bean that needs to be described itself
            return recursiveDescribe(value, key, cache);
        }
    }
}
like image 53
Peter Anthony Avatar answered Oct 05 '22 22:10

Peter Anthony


The challenge (or show stopper) is problem that we have to deal with an object graph instead of a simple tree. A graph may contain cycles and that requires to develop some custom rules or requirements for the stop criteria inside the recursive algorithm.

Have a look at a dead simple bean (a tree structure, getters are assumed but not shown):

public class Node {
   private Node parent;
   private Node left;
   private Node right;
}

and initialize it like this:

        root
        /  \
       A    B

Now call a describe on root. A non-recursive call would result in

{parent=null, left=A, right=B}

A recursive call instead would do a

1: describe(root) =>
2: {parent=describe(null), left=describe(A), right=describe(B)} =>
3: {parent=null, 
     {A.parent=describe(root), A.left=describe(null), A.right= describe(null)}
     {B.parent=describe(root), B.left=describe(null), B.right= describe(null)}}

and run into a StackOverflowError because describe is called with objects root, A and B over and over again.

One solution for a custom implementation could be to remember all objects that have been described so far (record those instances in a set, stop if set.contains(bean) return true) and store some kind of link in your result object.

like image 41
Andreas Dolk Avatar answered Oct 05 '22 23:10

Andreas Dolk