Monday, August 21, 2006

Mapping a Java bean

This is a straight-forward code post.

public class BeanMap<T>
        extends AbstractMap<String, Object> {
    private final T bean;
    private final Set<PropertyDescriptor> descriptors;

    public BeanMap(final T bean)
            throws IntrospectionException {
        this.bean = asNotNull(bean, "Missing bean");

        final Set<PropertyDescriptor> descriptors
                = new HashSet<PropertyDescriptor>();

        for (final PropertyDescriptor descriptor
                : getBeanInfo(bean.getClass()).getPropertyDescriptors())
            // Only support simple setter/getters.
            if (!(descriptor instanceof IndexedPropertyDescriptor))
                descriptors.add(descriptor);

        this.descriptors = unmodifiableSet(descriptors);
    }

    public Set<Entry<String, Object>> entrySet() {
        return new BeanSet();
    }

    @Override
    public Object get(final Object key) {
        return super.get(checkKey(key));
    }

    @Override
    public Object put(final String key, final Object value) {
        checkKey(key);

        for (final Entry<String, Object> entry : entrySet())
            if (entry.getKey().equals(key))
                return entry.setValue(value);

        return null;
    }

    @Override
    public Object remove(final Object key) {
        return super.remove(checkKey(key));
    }

    private String checkKey(final Object key) {
        // NB - the cast forces CCE if key is the wrong type.
        final String name = (String) key;

        if (!containsKey(asNotNull(name, "Missing key")))
            throw new IllegalArgumentException("Bad key: " + key);

        return name;
    }

    private class BeanSet
            extends AbstractSet<Entry<String, Object>> {
        public Iterator<Entry<String, Object>> iterator() {
            return new BeanIterator(descriptors.iterator());
        }

        public int size() {
            return descriptors.size();
        }
    }

    private class BeanIterator
            implements Iterator<Entry<String, Object>> {
        private final Iterator<PropertyDescriptor> it;

        public BeanIterator(final Iterator<PropertyDescriptor> it) {
            this.it = it;
        }

        public boolean hasNext() {
            return it.hasNext();
        }

        public Entry<String, Object> next() {
            return new BeanEntry(it.next());
        }

        public void remove() {
            it.remove();
        }
    }

    private class BeanEntry
            implements Entry<String, Object> {
        private final PropertyDescriptor descriptor;

        public BeanEntry(final PropertyDescriptor descriptor) {
            this.descriptor = descriptor;
        }

        public String getKey() {
            return descriptor.getName();
        }

        public Object getValue() {
            return unwrap(new Wrapped() {
                public Object run()
                        throws IllegalAccessException,
                        InvocationTargetException {
                    final Method method = descriptor.getReadMethod();
                    // A write-only bean.
                    if (null == method)
                        throw new UnsupportedOperationException(
                                "No getter: " + descriptor.getName());

                    return method.invoke(bean);
                }
            });
        }

        public Object setValue(final Object value) {
            return unwrap(new Wrapped() {
                public Object run()
                        throws IllegalAccessException,
                        InvocationTargetException {
                    final Method method = descriptor.getWriteMethod();
                    // A read-only bean.
                    if (null == method)
                        throw new UnsupportedOperationException(
                                "No setter: " + descriptor.getName());

                    final Object old = getValue();
                    method.invoke(bean, value);
                    return old;
                }
            });
        }
    }

    private static interface Wrapped {
        Object run()
                throws IllegalAccessException,
                InvocationTargetException;
    }

    private static Object unwrap(final Wrapped wrapped) {
        try {
            return wrapped.run();

        } catch (IllegalAccessException e) {
            throw new RuntimeException(e);

        } catch (final InvocationTargetException e) {
            // Javadocs for setValue indicate cast is ok.
            throw(RuntimeException) e.getCause();
        }
    }
}

The idea is simple: give the Java getter/setter idiom a Map interface. Commons Beanutils already does this but with a significant difference.

My example class is very brittle. It doesn't like null, wrong classes or missing keys. And in a brittle language like Java, this is a good thing. This kind of brittleness finds bugs quickly following the venerable fail-fast principle.

Avoid code which returns null or silently converts wrong-class arguments. You pay now in return for a clear conscience in the long term. I have better things to do with my time than debug NullPointerExceptions.

No comments: