org.getobjects.foundation.kvc.KVCWrapper.java Source code

Java tutorial

Introduction

Here is the source code for org.getobjects.foundation.kvc.KVCWrapper.java

Source

//THIS CODE IS DERIVED FROM THE TAPESTRY WEB APPLICATION FRAMEWORK
//BY HOWARD LEWIS SHIP. EXCELLENT CODE.

//ALL EXTENSIONS AND MODIFICATIONS BY MARCUS MUELLER <znek@mulle-kybernetik.com>,
//EVERYTHING AVAILABLE UNDER THE TERMS AND CONDITIONS OF
//THE GNU LESSER GENERAL PUBLIC LICENSE (LGPL). SEE BELOW FOR MORE DETAILS.

// Modified by Helge Hess <helge.hess@opengroupware.org> (2007)

//Tapestry Web Application Framework
//Copyright (c) 2000-2002 by Howard Lewis Ship

//Howard Lewis Ship
//http://sf.net/projects/tapestry
//mailto:hship@users.sf.net

//This library is free software.

//You may redistribute it and/or modify it under the terms of the GNU
//Lesser General Public License as published by the Free Software Foundation.

//Version 2.1 of the license should be included with this distribution in
//the file LICENSE, as well as License.html. If the license is not
//included with this distribution, you may find a copy at the FSF web
//site at 'www.gnu.org' or 'www.fsf.org', or you may write to the
//Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139 USA.

//This library is distributed in the hope that it will be useful,
//but WITHOUT ANY WARRANTY; without even the implied waranty of
//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
//Lesser General Public License for more details.

package org.getobjects.foundation.kvc;

import java.beans.PropertyDescriptor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 *  Streamlines access to all the properties of a given
 *  JavaBean.  Static methods acts as a factory for PropertyHelper instances,
 *  which are specific to a particular Bean class.
 *
 *  <p>A <code>PropertyHelper</code> for a bean class simplifies getting and
 *  setting properties on the bean, handling (and caching) the lookup of methods
 *  as well as the dynamic invocation of those methods.  It uses an instance of
 *  {@link IPropertyAccessor} for each property.
 *
 *  <p>PropertyHelper allows properties to be specified in terms of a path.  A
 *  path is a series of property names seperate by periods.  So a property path
 *  of 'visit.user.address.street' is effectively the same as
 *  the code <code>getVisit().getUser().getAddress().getStreet()</code>
 *  (and just as likely to throw a <code>NullPointerException</code>).
 *
 *  <p>Typical usage:
 *
 *  <pre>
 *  ProperyHelper helper = PropertyHelper.forInstance(instance);
 *  helper.set(instance, "propertyName", newValue);
 *  </pre>
 *
 *  <p>Only single-valued properties (not indexed properties) are supported, and a minimum
 *  of type checking is performed.
 *
 *  <p>A mechanism exists to register custom <code>PropertyHelper</code>
 *  subclasses for specific classes.  The two default registrations are
 *  {@link PublicBeanPropertyHelper} for the {@link IPublicBean} interface, and
 *  {@link MapHelper} for the {@link Map} interface.
 *
 *  @version $Id: KVCWrapper.java,v 1.4 2002/11/20 14:47:27 znek Exp $
 *  @author Howard Lewis Ship
 *
 **/

public class KVCWrapper extends Object {

    /**
     *  Cache of helpers, keyed on the Class of the bean.
     **/
    private static ConcurrentHashMap<Class, KVCWrapper> helpers = new ConcurrentHashMap<Class, KVCWrapper>(16);

    //static {
    //  register(Map.class, MapKVCWrapper.class);
    //}

    private static final Log logger = LogFactory.getLog(KVCWrapper.class);

    private static ConcurrentHashMap<Class, Method[]> declaredMethodCache = new ConcurrentHashMap<Class, Method[]>(
            16);

    /**
     *  Map of PropertyAccessors for the helper's
     *  bean class. The keys are the names of the properties.
     **/

    protected ConcurrentHashMap<String, IPropertyAccessor> accessors;

    /**
     *  The Java Beans class for which this helper is configured.
     **/
    protected Class clazz;

    /**
     *  The separator character used to divide up different
     *  properties in a nested property name.
     **/

    /**
     * A {@link StringSplitter} used for parsing apart property paths.
     **/

    protected KVCWrapper(Class _class) {
        this.clazz = _class;
    }

    @SuppressWarnings("unchecked")
    private static Method[] getPublicDeclaredMethods(Class _class) {
        Method methods[] = declaredMethodCache.get(_class);
        if (methods != null)
            return methods;

        final Class fclz = _class;

        methods = (Method[]) AccessController.doPrivileged(new PrivilegedAction() {
            public Object run() {
                return fclz.getMethods();
            }
        });

        for (int i = 0; i < methods.length; i++) {
            Method method = methods[i];
            int j = method.getModifiers();
            if (!Modifier.isPublic(j))
                methods[i] = null;
        }

        declaredMethodCache.put(_class, methods);
        return methods;
    }

    public PropertyDescriptor[] getPropertyDescriptors(Class _class) throws Exception {
        /**
         * Our idea of KVC differs from what the Bean API proposes. Instead of
         * having get<name> and set<name> methods, we expect <name> and 
         * set<name> methods.
         * 
         * HH: changed to allow for getXYZ style accessors.
         */

        Map<String, Method> settersMap = new HashMap<String, Method>();
        Map<String, Method> gettersMap = new HashMap<String, Method>();

        Method methods[] = getPublicDeclaredMethods(_class);

        for (int i = 0; i < methods.length; i++) {
            Method method = methods[i];
            if (method == null)
                continue;

            String name = method.getName();
            int nameLen = name.length();
            int paraCount = method.getParameterTypes().length;

            if (name.startsWith("set")) {
                if (method.getReturnType() != Void.TYPE)
                    continue;
                if (paraCount != 1)
                    continue;
                if (nameLen == 3)
                    continue;

                char[] chars = name.substring(3).toCharArray();
                chars[0] = Character.toLowerCase(chars[0]);
                String decapsedName = new String(chars);

                if (logger.isDebugEnabled()) {
                    logger.debug("Recording setter method [" + method + "] for name \"" + decapsedName + "\"");
                }
                settersMap.put(decapsedName, method);
            } else {
                /* register as a getter */
                if (method.getReturnType() == Void.TYPE)
                    continue;
                if (paraCount > 0)
                    continue;

                if (name.startsWith("get")) {
                    char[] chars = name.substring(3).toCharArray();
                    chars[0] = Character.toLowerCase(chars[0]);
                    name = new String(chars);
                }

                if (logger.isDebugEnabled()) {
                    logger.debug("Recording getter method [" + method + "] for name \"" + name + "\"");
                }
                gettersMap.put(name, method);
            }

        }

        Set<PropertyDescriptor> pds = new HashSet<PropertyDescriptor>();

        /* merge all names from getters and setters */
        Set<String> names = new HashSet<String>(gettersMap.keySet());
        names.addAll(settersMap.keySet());

        for (String name : names) {
            Method getter = gettersMap.get(name);
            Method setter = settersMap.get(name);
            if (getter == null && setter == null)
                continue;

            /* this is JavaBeans stuff */
            PropertyDescriptor descriptor = new PropertyDescriptor(name, getter, setter);
            pds.add(descriptor);
        }
        return pds.toArray(new PropertyDescriptor[0]);
    }

    /**
     *  Uses JavaBeans introspection to find all the properties of the
     *  bean class.  This method sets the {@link #accessors} variable (it will
     *  have been null), and adds all the well-defined JavaBeans properties.
     *
     *  <p>Subclasses may invoke this method before adding thier own accessors.
     *
     *  <p>This method is invoked from within a synchronized block.  Subclasses
     *  do not have to worry about synchronization.
     **/

    protected void buildPropertyAccessors() {
        /*
         * Acquire all usable field accessors first.
         */

        if (this.accessors != null)
            return;

        /**
         * Construct field accessors for names which aren't occupied
         * by properties, yet. Imagine this as a "last resort".
         */

        final Map<String, FieldAccessor> propertyFieldAccessorMap = new HashMap<String, FieldAccessor>();
        final Field fields[] = this.clazz.getFields();

        for (Field field : fields) {
            final int mods = field.getModifiers();

            // Skip static variables and non-public instance variables.
            if ((Modifier.isPublic(mods) == false) || (Modifier.isStatic(mods)))
                continue;

            propertyFieldAccessorMap.put(field.getName(), new FieldAccessor(field));
        }

        /**
         * Retrieve all property descriptors now
         */
        PropertyDescriptor[] props;

        try {
            props = this.getPropertyDescriptors(this.clazz);
        } catch (Exception e) {
            logger.error("Error during getPropertyDescriptors()", e);
            throw new DynamicInvocationException(e);
        }

        // TBD: instead build the table locally, and then apply to an
        //      atomic reference?!
        this.accessors = new ConcurrentHashMap<String, IPropertyAccessor>(16);

        if (logger.isDebugEnabled())
            logger.debug("Recording properties for \"" + this.clazz.getName() + "\"");

        for (PropertyDescriptor pd : props) {
            final String name = pd.getName();

            if (logger.isDebugEnabled())
                logger.debug("Recording property \"" + name + "\"");

            final Method getter = pd.getReadMethod();
            final Method setter = pd.getWriteMethod();
            final FieldAccessor fa = propertyFieldAccessorMap.get(name);
            final Class type = pd.getPropertyType();

            final PropertyAccessor pa = PropertyAccessor.getPropertyAccessor(name, type, getter, setter, fa);
            this.accessors.put(name, pa);
        }

        /**
         * Use field accessors for names which are not occupied, yet.
         * This is the default fallback.
         */
        for (String name : propertyFieldAccessorMap.keySet()) {
            if (!this.accessors.containsKey(name))
                this.accessors.put(name, propertyFieldAccessorMap.get(name));
        }
    }

    /**
     *  Factory method which returns a <code>KVCWrapper</code> for the given
     *  JavaBean class.
     *
     *  <p>Finding the right helper class involves a sequential lookup, first for an
     *  exact match, then for an exact match on the superclass, the a search
     *  by interface.  If no specific
     *  match is found, then <code>KVCWrapper</code> itself is used, which is
     *  the most typical case.
     *
     *  @see #register(Class, Class)
     **/
    public static KVCWrapper forClass(Class _class) {
        // TBD: replace this method

        if (logger.isDebugEnabled()) // TBD: expensive
            logger.debug("Getting property helper for class " + _class.getName());

        KVCWrapper helper = helpers.get(_class);
        if (helper != null)
            return helper;

        // hh: Previously there was a synchronized registry of helpers. I removed
        //     that because it only contained MapKVCWrapper in Go ..., hence it
        //     was unnecessarily expensive.
        //     We might want to replicate this at the Go level.

        if (Map.class.isAssignableFrom(_class))
            helper = new MapKVCWrapper(_class);
        else
            helper = new KVCWrapper(_class);

        // We don't want to go through this again, so record permanently the correct
        // helper for this class.
        helpers.put(_class, helper);
        return helper;
    }

    /**
     *  Finds an accessor for the given property name.  Returns the
     *  accessor if the class has the named property, or null
     *  otherwise.
     *
     *  @param _key the <em>simple</em> property name of the property to
     *  get.
     *
     **/
    public IPropertyAccessor getAccessor(final Object _self, final String _key) {
        synchronized (this) {
            if (this.accessors == null)
                buildPropertyAccessors();
        }

        // hh: before this was iterating an array over names, hardcoded that for
        //     speed (no array creation, no String ops for exact matches)
        // this.accessors is a concurrent hashmap, hence no synchronized necessary
        IPropertyAccessor accessor;

        /* first check exact match, eg 'item' */

        if ((accessor = this.accessors.get(_key)) != null)
            return accessor;

        /* next check 'getItem' */

        final int len = _key.length();
        final char[] chars = new char[3 /* get */ + len];
        chars[0] = 'g';
        chars[1] = 'e';
        chars[2] = 't';

        _key.getChars(0, len, chars, 3 /* skip 'get' */);
        final char c0 = chars[3];
        if (c0 > 96 && c0 < 123 /* lowercase ASCII range */)
            chars[3] = (char) (c0 - 32); /* make uppercase */

        String s = new String(chars);
        if ((accessor = this.accessors.get(s)) != null)
            return accessor;

        /* finally with leading underscore */

        chars[3] = c0; /* restore lowercase char */
        chars[2] = '_';
        s = new String(chars, 2, len + 1);
        return this.accessors.get(s);
    }

    public String toString() {
        StringBuilder sb = new StringBuilder("<KVCWrapper @");
        sb.append(this.hashCode());
        sb.append(": ");
        sb.append(this.clazz.getName());
        sb.append('>');

        return sb.toString();
    }
}