com.zenesis.qx.remote.ProxyTypeImpl.java Source code

Java tutorial

Introduction

Here is the source code for com.zenesis.qx.remote.ProxyTypeImpl.java

Source

/**
 * ************************************************************************
 * 
 *    server-objects - a contrib to the Qooxdoo project that makes server 
 *    and client objects operate seamlessly; like Qooxdoo, server objects 
 *    have properties, events, and methods all of which can be access from
 *    either server or client, regardless of where the original object was
 *    created.
 * 
 *    http://qooxdoo.org
 * 
 *    Copyright:
 *      2010 Zenesis Limited, http://www.zenesis.com
 * 
 *    License:
 *      LGPL: http://www.gnu.org/licenses/lgpl.html
 *      EPL: http://www.eclipse.org/org/documents/epl-v10.php
 *      
 *      This software is provided under the same licensing terms as Qooxdoo,
 *      please see the LICENSE file in the Qooxdoo project's top-level directory 
 *      for details.
 * 
 *    Authors:
 *      * John Spackman (john.spackman@zenesis.com)
 * 
 * ************************************************************************
 */
package com.zenesis.qx.remote;

import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.jsontype.TypeSerializer;
import com.zenesis.qx.remote.annotations.AlwaysProxy;
import com.zenesis.qx.remote.annotations.DoNotProxy;
import com.zenesis.qx.remote.annotations.Event;
import com.zenesis.qx.remote.annotations.Events;
import com.zenesis.qx.remote.annotations.ExplicitProxyOnly;
import com.zenesis.qx.remote.annotations.FactoryMethod;
import com.zenesis.qx.remote.annotations.Properties;
import com.zenesis.qx.remote.annotations.Property;

public class ProxyTypeImpl extends AbstractProxyType {

    private final static class MethodSig {
        private final Method method;

        public MethodSig(Method method) {
            super();
            this.method = method;
        }

        /* (non-Javadoc)
         * @see java.lang.Object#hashCode()
         */
        @Override
        public int hashCode() {
            return method.getName().hashCode();
        }

        /* (non-Javadoc)
         * @see java.lang.Object#equals(java.lang.Object)
         */
        @Override
        public boolean equals(Object obj) {
            MethodSig that = (MethodSig) obj;
            return that.method.getName().equals(method.getName()) && hasSameSignature(method, that.method);
        }

        /* (non-Javadoc)
         * @see java.lang.Object#toString()
         */
        @Override
        public String toString() {
            return method.toString();
        }
    }

    /*
     * Helper class to track methods
     */
    private final class MethodsCompiler {
        // Methods we found
        final HashMap<String, Method> methods = new HashMap<String, Method>();

        // Methods that should not be proxied
        final HashSet<MethodSig> doNotProxyMethods = new HashSet<ProxyTypeImpl.MethodSig>();

        /**
         * Adds all methods from a given class
         * @param fromClass
         * @param defaultProxy
         */
        public void addMethods(Class fromClass, boolean defaultProxy) {
            boolean explicitOnly = fromClass.isAnnotationPresent(ExplicitProxyOnly.class);
            Method[] ifcMethods = fromClass.getDeclaredMethods();
            for (Method method : ifcMethods) {
                int mods = method.getModifiers();

                // Public and protected only
                if (!Modifier.isPublic(mods) && !Modifier.isProtected(mods))
                    continue;

                method.setAccessible(true);// Short cut access controls validation
                if (explicitOnly && !method.isAnnotationPresent(AlwaysProxy.class)
                        && !method.isAnnotationPresent(com.zenesis.qx.remote.annotations.Method.class))
                    continue;

                if (method.isAnnotationPresent(DoNotProxy.class)) {
                    doNotProxyMethods.add(new MethodSig(method));
                    continue;
                }

                Method existing = methods.get(method.getName());
                if (existing != null) {
                    // Method overloading is not possible
                    if (isConflicting(method, existing))
                        throw new IllegalArgumentException("Cannot create a proxy for " + clazz
                                + " because it has overloaded method " + method + " first seen in " + existing);
                }
                Boolean canProxy = canProxy(fromClass, method);
                if (canProxy == null)
                    canProxy = defaultProxy;
                if (canProxy)
                    methods.put(method.getName(), method);
            }
        }

        /**
         * Removes all methods listed in the super type (because super type is already
         * defined)
         */
        public void removeSuperTypeMethods() {
            for (ProxyType tmpType = superType; tmpType != null; tmpType = tmpType.getSuperType()) {
                for (ProxyMethod method : tmpType.getMethods())
                    methods.remove(method.getName());
            }
        }

        /**
         * Removes any methods which are property accessor methods
         * @param prop
         */
        public void removePropertyAccessors(ProxyPropertyImpl prop) {
            String upname = Character.toUpperCase(prop.getName().charAt(0)) + prop.getName().substring(1);
            methods.remove("get" + upname);
            methods.remove("set" + upname);
            methods.remove("is" + upname);
            if (prop.getSerializeMethod() != null)
                methods.remove(prop.getSerializeMethod().getName());
            if (prop.getDeserializeMethod() != null)
                methods.remove(prop.getDeserializeMethod().getName());
        }

        /**
         * Checks that the list of methods is valid, i.e. that there are no conflicts with DoNotProxy
         * @throws IllegalArgumentException
         */
        public void checkValid() throws IllegalArgumentException {
            for (MethodSig sig : doNotProxyMethods) {
                Method method = methods.get(sig.method.getName());
                if (method != null && hasSameSignature(method, sig.method)) {
                    throw new IllegalArgumentException("Cannot create a proxy for " + clazz
                            + " because it has conflicting DoNotProxy for method " + method);
                }
            }
        }

        /**
         * Converts the list of methods to a sorted array of ProxyMethods
         * @return
         */
        public ProxyMethod[] toArray() {
            ArrayList<ProxyMethod> proxyMethods = new ArrayList<ProxyMethod>();
            for (Method method : methods.values())
                proxyMethods.add(new ProxyMethod(method));
            Collections.sort(proxyMethods, ProxyMethod.ALPHA_COMPARATOR);

            return proxyMethods.toArray(new ProxyMethod[proxyMethods.size()]);
        }
    }

    // The class being represented
    private final Class clazz;

    // Base ProxyType that represents the class that clazz is derived from
    private final ProxyType superType;

    // Interfaces implements by clazz 
    private final Set<ProxyType> interfaces;

    // Methods (not including property accessors)
    private final ProxyMethod[] methods;

    // Properties
    private final HashMap<String, ProxyProperty> properties;

    // Events
    private final HashMap<String, ProxyEvent> events;

    // Factory method for newInstance()
    private Method factoryMethod;

    /**
     * Constructor, used for defining interfaces which are to be proxied
     * @param className
     * @param methods
     */
    public ProxyTypeImpl(ProxyType superType, Class clazz, Set<ProxyType> interfaces) {
        super();
        if (interfaces == null)
            interfaces = Collections.EMPTY_SET;
        this.superType = superType;
        this.interfaces = interfaces;
        this.clazz = clazz;

        MethodsCompiler methodsCompiler = new MethodsCompiler();

        // Get a complete list of methods from the interfaces that the new class has to 
        //   implement; we include methods marked as DoNotProxy so that we can check for 
        //   conflicting instructions
        if (!clazz.isInterface()) {
            // Get a full list of the interfaces which our class has to implement
            HashSet<ProxyType> allInterfaces = new HashSet<ProxyType>();
            getAllInterfaces(allInterfaces, interfaces);

            for (ProxyType ifcType : allInterfaces) {
                try {
                    methodsCompiler.addMethods(Class.forName(ifcType.getClassName()), true);
                } catch (ClassNotFoundException e) {
                    throw new IllegalStateException("Cannot find class " + ifcType.getClassName());
                }
            }
        }

        boolean defaultProxy = false;
        if (clazz.isInterface())
            defaultProxy = true;
        else {
            for (Class tmp = clazz; tmp != null; tmp = tmp.getSuperclass()) {
                if (factoryMethod == null) {
                    for (Method method : tmp.getDeclaredMethods()) {
                        if (method.isAnnotationPresent(FactoryMethod.class)) {
                            if (!Modifier.isStatic(method.getModifiers()))
                                throw new IllegalStateException("Cannot use method " + method
                                        + " as FactoryMethod because it is not static");
                            factoryMethod = method;
                            method.setAccessible(true);
                            break;
                        }
                    }
                }
                if (tmp.isAnnotationPresent(AlwaysProxy.class)) {
                    defaultProxy = true;
                    break;
                } else if (tmp.isAnnotationPresent(ExplicitProxyOnly.class)) {
                    break;
                }
            }
        }

        // If the class does not have any proxied interfaces or the class is marked with
        //   the AlwaysProxy annotation, then we take methods from the class definition
        methodsCompiler.addMethods(clazz, defaultProxy);

        methodsCompiler.checkValid();
        methodsCompiler.removeSuperTypeMethods();

        // Load properties
        HashMap<String, ProxyEvent> events = new HashMap<String, ProxyEvent>();
        HashMap<String, ProxyProperty> properties = new HashMap<String, ProxyProperty>();
        Properties annoProperties = (Properties) clazz.getAnnotation(Properties.class);
        if (annoProperties != null) {
            for (Property anno : annoProperties.value()) {
                ProxyProperty property = new ProxyPropertyImpl(clazz, anno.value(), anno, annoProperties);
                properties.put(property.getName(), property);
                ProxyEvent event = property.getEvent();
                if (event != null)
                    events.put(event.getName(), event);
            }
        }
        for (Field field : clazz.getDeclaredFields()) {
            Property anno = field.getAnnotation(Property.class);
            if (anno != null) {
                ProxyProperty property = new ProxyPropertyImpl(clazz,
                        anno.value().length() > 0 ? anno.value() : field.getName(), anno, annoProperties);
                properties.put(property.getName(), property);
                ProxyEvent event = property.getEvent();
                if (event != null)
                    events.put(event.getName(), event);
            }
        }

        for (Method method : clazz.getDeclaredMethods()) {
            String name = method.getName();
            if (name.length() < 4 || !name.startsWith("get") || !Character.isUpperCase(name.charAt(3)))
                continue;
            Property anno = method.getAnnotation(Property.class);
            if (anno == null)
                continue;

            name = Character.toLowerCase(name.charAt(3)) + name.substring(4);
            if (properties.containsKey(name))
                continue;

            ProxyProperty property = new ProxyPropertyImpl(clazz, anno.value().length() > 0 ? anno.value() : name,
                    anno, annoProperties);
            properties.put(property.getName(), property);
            ProxyEvent event = property.getEvent();
            if (event != null)
                events.put(event.getName(), event);
        }

        // Classes need to have all inherited properties added
        if (!clazz.isInterface()) {
            for (ProxyType ifc : interfaces)
                addProperties((ProxyTypeImpl) ifc, properties);
        }

        // Remove property accessors
        for (ProxyProperty prop : properties.values())
            methodsCompiler.removePropertyAccessors((ProxyPropertyImpl) prop);

        // Load events
        if (clazz.isAnnotationPresent(Events.class)) {
            Events annoEvents = (Events) clazz.getAnnotation(Events.class);
            for (Event annoEvent : annoEvents.value()) {
                if (!events.containsKey(annoEvent.value()))
                    events.put(annoEvent.value(), new ProxyEvent(annoEvent));
            }
        }

        // Classes need to have all inherited events added
        if (!clazz.isInterface()) {
            for (ProxyType type : interfaces)
                addEvents((ProxyTypeImpl) type, events);
        }

        // Save
        this.properties = properties.isEmpty() ? null : properties;
        this.events = events.isEmpty() ? null : events;
        this.methods = methodsCompiler.toArray();
    }

    protected boolean isConflicting(Method method, Method existing) {
        // The same method can appear more than once, but only if they are
        //   identical - we just ignore it
        if (existing != null) {
            Class[] epts = existing.getParameterTypes();
            Class[] mpts = method.getParameterTypes();
            if (epts.length == mpts.length) {
                for (int i = 0; i < epts.length; i++)
                    if (epts[i] != mpts[i])
                        return true;
                return false;
            }
        }

        return true;
    }

    protected static boolean hasSameSignature(Method method, Method existing) {
        Class[] epts = existing.getParameterTypes();
        Class[] mpts = method.getParameterTypes();
        if (epts.length == mpts.length) {
            for (int i = 0; i < epts.length; i++)
                if (epts[i] != mpts[i])
                    return false;
            return true;
        }

        return false;
    }

    protected Boolean canProxy(Class clazz, Method method) {
        if (method.isAnnotationPresent(AlwaysProxy.class)
                || method.isAnnotationPresent(com.zenesis.qx.remote.annotations.Method.class))
            return true;
        for (Class tmp : clazz.getInterfaces())
            if (Proxied.class.isAssignableFrom(tmp)) {
                Boolean test = canProxy(tmp, method);
                if (test != null)
                    return test;
            }
        Class superClazz = clazz.getSuperclass();
        if (superClazz != null && Proxied.class.isAssignableFrom(superClazz))
            return canProxy(superClazz, method);
        return null;
    }

    /**
     * Recursively adds to addInterfaces to get a list of all interfaces which can be proxied.
     * @param allInterfaces
     * @param interfaces
     * @return
     */
    protected HashSet<ProxyType> getAllInterfaces(HashSet<ProxyType> allInterfaces, Set<ProxyType> interfaces) {
        for (ProxyType type : interfaces) {
            allInterfaces.add(type);
            getAllInterfaces(allInterfaces, type.getInterfaces());
        }
        return allInterfaces;
    }

    /**
     * Detects whether the method is a property accessor
     * @param method
     * @return
     */
    protected boolean isPropertyAccessor(Method method) {
        String name = method.getName();
        if (!name.startsWith("set") && !name.startsWith("get"))
            return false;
        name = Character.toLowerCase(name.charAt(3)) + name.substring(4);
        return isProperty(name);
    }

    @Override
    public boolean isProperty(String name) {
        if (properties != null && properties.containsKey(name))
            return true;
        if (interfaces != null)
            for (ProxyType type : interfaces)
                if (type.isProperty(name))
                    return true;
        return false;
    }

    /**
     * Adds all properties recursively
     * @param properties
     */
    public void addProperties(ProxyTypeImpl proxyType, HashMap<String, ProxyProperty> properties) {
        if (proxyType.properties != null)
            for (ProxyProperty prop : proxyType.properties.values())
                properties.put(prop.getName(), prop);
        if (proxyType.interfaces != null)
            for (ProxyType type : proxyType.interfaces)
                addProperties((ProxyTypeImpl) type, properties);
    }

    /**
     * Adds all events recursively
     * @param events
     */
    public void addEvents(ProxyTypeImpl proxyType, HashMap<String, ProxyEvent> events) {
        if (proxyType.events != null)
            for (ProxyEvent event : proxyType.events.values())
                events.put(event.getName(), event);
        if (proxyType.interfaces != null)
            for (ProxyType type : proxyType.interfaces)
                addEvents((ProxyTypeImpl) type, events);
    }

    /* (non-Javadoc)
     * @see org.codehaus.jackson.map.JsonSerializable#serialize(org.codehaus.jackson.JsonGenerator, org.codehaus.jackson.map.SerializerProvider)
     */
    @Override
    public void serialize(JsonGenerator gen, SerializerProvider sp) throws IOException, JsonProcessingException {
        serialize(gen, sp, null);
    }

    /* (non-Javadoc)
     * @see com.fasterxml.jackson.databind.JsonSerializable#serializeWithType(com.fasterxml.jackson.core.JsonGenerator, com.fasterxml.jackson.databind.SerializerProvider, com.fasterxml.jackson.databind.jsontype.TypeSerializer)
     */
    @Override
    public void serializeWithType(JsonGenerator gen, SerializerProvider sp, TypeSerializer ts)
            throws IOException, JsonProcessingException {
        serialize(gen, sp);
    }

    @Override
    public Proxied newInstance(Class<? extends Proxied> clazz)
            throws InstantiationException, IllegalAccessException, InvocationTargetException {
        if (factoryMethod != null) {
            if (factoryMethod.getParameterTypes().length == 0)
                return (Proxied) factoryMethod.invoke(null);
            return (Proxied) factoryMethod.invoke(null, clazz);
        }
        return clazz.newInstance();
    }

    /**
     * @return the superType
     */
    @Override
    public ProxyType getSuperType() {
        return superType;
    }

    @Override
    public boolean isInterface() {
        return clazz.isInterface();
    }

    @Override
    public String getClassName() {
        return clazz.getName();
    }

    @Override
    public Class getClazz() {
        return clazz;
    }

    @Override
    public ProxyMethod[] getMethods() {
        return methods;
    }

    @Override
    public Set<ProxyType> getInterfaces() {
        return interfaces;
    }

    @Override
    public Map<String, ProxyProperty> getProperties() {
        if (properties == null)
            return Collections.EMPTY_MAP;
        return properties;
    }

    @Override
    public Map<String, ProxyEvent> getEvents() {
        if (events == null)
            return Collections.EMPTY_MAP;
        return events;
    }

    @Override
    public boolean supportsEvent(String eventName) {
        if (events == null)
            return false;
        return events.containsKey(eventName);
    }

    @Override
    public ProxyEvent getEvent(String eventName) {
        if (events == null)
            return null;
        return events.get(eventName);
    }

    /* (non-Javadoc)
     * @see java.lang.Object#equals(java.lang.Object)
     */
    @Override
    public boolean equals(Object obj) {
        return obj instanceof ProxyType && ((ProxyTypeImpl) obj).getClazz() == clazz;
    }

    /* (non-Javadoc)
     * @see java.lang.Object#hashCode()
     */
    @Override
    public int hashCode() {
        return clazz.hashCode();
    }

    /* (non-Javadoc)
     * @see java.lang.Object#toString()
     */
    @Override
    public String toString() {
        return clazz.toString();
    }
}