All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.googlecode.gwt.test.gin.DeferredBindingModule Maven / Gradle / Ivy

There is a newer version: 0.63
Show newest version
package com.googlecode.gwt.test.gin;

import com.google.gwt.core.client.GWT;
import com.google.gwt.inject.client.AsyncProvider;
import com.google.gwt.inject.client.Ginjector;
import com.google.gwt.inject.rebind.reflect.ReflectUtil;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.inject.*;
import com.google.inject.spi.*;
import com.googlecode.gwt.test.exceptions.GwtTestPatchException;
import com.googlecode.gwt.test.internal.GwtClassPool;
import com.googlecode.gwt.test.utils.GwtReflectionUtils;
import javassist.CannotCompileException;
import javassist.CtClass;
import javassist.CtField;
import javassist.CtMethod;
import javassist.bytecode.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.*;

/**
 * Additional Guice module class which must be added when replacing a GIN Injector with a Guice
 * Injector, in order to add all required bindings to call GWT's deferred binding fallback which
 * happens in GIN.
 *
 * @author Alex Dobjanschi
 * @author Gael Lazzari
 */
class DeferredBindingModule extends AbstractModule {

    private static class DeferredBindingProvider implements Provider {

        private final Class clazzToInstanciate;

        public DeferredBindingProvider(Class ginInjectorClass, Key key) {
            Class rawType = key.getTypeLiteral().getRawType();
            if (rawType.getName().endsWith("Async")) {
                try {
                    this.clazzToInstanciate = GwtReflectionUtils.getClass(rawType.getName().substring(0,
                            rawType.getName().length() - 5));
                } catch (ClassNotFoundException e) {
                    throw new GwtTestPatchException(
                            "Error while trying to create a Guice provider for injector '"
                                    + ginInjectorClass.getName() + "'", e);
                }
            } else {
                this.clazzToInstanciate = rawType;
            }
        }

        public Object get() {
            // call GWT deferred binding, which is patch by gwt-test-utils to call
            // GwtCreateHandlerManager
            return GWT.create(clazzToInstanciate);
        }

    }

    private static final Map, DeferredBindingModule> DEFERRED_BINDING_MODULES_CACHE = new HashMap, DeferredBindingModule>();

    private static final Map> GENERATED = new HashMap>();
    private static final Map, Boolean> HAS_INJECTION_ANNOTATION_CACHE = new HashMap, Boolean>();

    private static final Logger LOGGER = LoggerFactory.getLogger(DeferredBindingModule.class);

    static final DeferredBindingModule getDeferredBindingModule(
            Class ginInjectorClass, Collection modules) {
        DeferredBindingModule deferredBindingModule = DEFERRED_BINDING_MODULES_CACHE.get(ginInjectorClass);
        if (deferredBindingModule == null) {
            deferredBindingModule = new DeferredBindingModule(ginInjectorClass,
                    modules.toArray(new Module[modules.size()]));
            DEFERRED_BINDING_MODULES_CACHE.put(ginInjectorClass, deferredBindingModule);
        }

        return deferredBindingModule;
    }

    private final Set> bindedClasses;

    private final Set> classesToInstanciate;

    private final Class ginInjectorClass;

    private DeferredBindingModule(Class ginInjectorClass, Module[] modules) {

        this.ginInjectorClass = ginInjectorClass;
        List elements = Elements.getElements(modules);
        this.classesToInstanciate = collectClassesFromInjector(ginInjectorClass);
        this.classesToInstanciate.addAll(collectDependencies(elements));
        this.bindedClasses = collectBindedClasses(elements);
    }

    @Override
    protected void configure() {
        Set> copy = new HashSet>(bindedClasses);
        addDeferredBindings(classesToInstanciate, copy);

    }

    @SuppressWarnings("unchecked")
    private void addDeferredBinding(final Key toInstanciate, Set> bindedClasses) {
        bindedClasses.add(toInstanciate);

        if (isProviderKey(toInstanciate)) {
            Key providedKey = (Key) ReflectUtil.getProvidedKey(toInstanciate);
            if (!bindedClasses.contains(providedKey)) {
                bindedClasses.add(providedKey);
                Set> collected = new HashSet>();
                collectDependencies(providedKey, collected);
                addDeferredBindings(collected, bindedClasses);
            }
        } else if (isAsyncProviderKey(toInstanciate)) {
            Class asyncProviderClass = getAsyncProvider(toInstanciate);
            bind((Key) toInstanciate).to(asyncProviderClass);

            Key providedKey = (Key) ReflectUtil.getProvidedKey(toInstanciate);
            if (!bindedClasses.contains(providedKey)) {
                bindedClasses.add(providedKey);
                Set> collected = new HashSet>();
                collectDependencies(providedKey, collected);
                addDeferredBindings(collected, bindedClasses);
            }

        } else if (hasAnyGuiceAnnotation(toInstanciate.getTypeLiteral().getRawType())) {
            // bind to itself, to tell guice there are some injection to proceed although the binding
            // is not declared in the module
            bind(toInstanciate);
        } else {
            // by default use GWT deferred binding to create leaf instances to be injected
            bind((Key) toInstanciate).toProvider(
                    new DeferredBindingProvider(ginInjectorClass, toInstanciate));
        }

    }

    private void addDeferredBindings(Set> classesToInstanciate, Set> bindedClasses) {
        for (final Key toInstanciate : classesToInstanciate) {
            if (!bindedClasses.contains(toInstanciate)) {
                addDeferredBinding(toInstanciate, bindedClasses);
            }
        }
    }

    private Set> collectBindedClasses(List elements) {
        final Set> bindedClasses = new HashSet>();

        for (Element e : elements) {
            e.acceptVisitor(new DefaultElementVisitor() {
                @Override
                public  Void visit(Binding binding) {
                    bindedClasses.add(binding.getKey());
                    return null;
                }
            });
        }

        return bindedClasses;
    }

    private Set> collectClassesFromInjector(Class injectorClass) {

        Set> classesToInstanciate = new HashSet>();

        for (Method m : injectorClass.getMethods()) {
            if (m.getGenericParameterTypes().length > 0) {
                // This method has non-zero argument list. We cannot do anything
                // about it, so inform developer and continue
                LOGGER.warn("skipping method '" + m.toGenericString()
                        + "' because it has non-zero argument list");
                continue;
            }

            Class literal = m.getReturnType();

            collectDependencies(Key.get(literal), classesToInstanciate);
        }

        return classesToInstanciate;
    }

    private void collectDependencies(Key current, Set> collected) {
        if (collected.contains(current)) {
            return;
        }

        collected.add(current);

        Set> dependencies = getDependencies(current);

        for (Key dependency : dependencies) {
            collectDependencies(dependency, collected);
        }
    }

    private Set> collectDependencies(List elements) {
        final Set> dependencies = new HashSet>();
        for (Element e : elements) {
            e.acceptVisitor(new DefaultElementVisitor() {
                @Override
                public  Void visit(Binding binding) {
                    LOGGER.debug("visiting binding " + binding.toString());

                    if (binding instanceof HasDependencies) {
                        HasDependencies deps = (HasDependencies) binding;
                        for (Dependency d : deps.getDependencies()) {
                            collectDependencies(d.getKey(), dependencies);
                        }
                    } else {
                        collectDependencies(binding.getKey(), dependencies);
                        dependencies.addAll(getDependencies(binding.getKey()));
                    }

                    return null;
                }
            });
        }

        return dependencies;
    }

    @SuppressWarnings("unchecked")
    private Class generatedAsyncProvider(String className, Key providedKey) {

        CtClass providedCtClass = GwtClassPool.getCtClass(providedKey.getTypeLiteral().getRawType());

        CtClass c = GwtClassPool.get().makeClass(className);
        c.addInterface(GwtClassPool.getCtClass(AsyncProvider.class));

        try {
            ClassFile classFile = c.getClassFile();
            classFile.setVersionToJava5();
            ConstPool constantPool = classFile.getConstPool();

            CtField provider = CtField.make("private " + Provider.class.getName() + " provider;", c);
            c.addField(provider);
            FieldInfo fieldInfo = provider.getFieldInfo();

            // Make it generic
            SignatureAttribute signatureAttribute = new SignatureAttribute(fieldInfo.getConstPool(),
                    "Lcom/google/inject/Provider<" + Descriptor.of(providedCtClass) + ">;");
            fieldInfo.addAttribute(signatureAttribute);

            AnnotationsAttribute attr = new AnnotationsAttribute(constantPool,
                    AnnotationsAttribute.visibleTag);
            javassist.bytecode.annotation.Annotation a = new javassist.bytecode.annotation.Annotation(
                    Inject.class.getName(), constantPool);
            attr.setAnnotation(a);
            provider.getFieldInfo().addAttribute(attr);

            CtMethod get = CtMethod.make("public void get(" + AsyncCallback.class.getName()
                    + " callback) { callback.onSuccess(provider.get()); }", c);
            c.addMethod(get);

            return c.toClass();
        } catch (CannotCompileException e) {
            throw new GwtTestGinException("Error while creating AsyncProvider subclass [" + className
                    + "]", e);
        }

    }

    private Class getAsyncProvider(Key key) {

        Key providedKey = ReflectUtil.getProvidedKey(key);
        String className = providedKey.getTypeLiteral().getRawType().getName() + "AsyncProvider";

        Class clazz = GENERATED.get(className);

        if (clazz != null) {
            return clazz;
        }

        clazz = generatedAsyncProvider(className, providedKey);
        GENERATED.put(className, clazz);

        return clazz;
    }

    private Set> getDependencies(InjectionPoint point) {
        Set> dependencies = new HashSet>();
        for (Dependency d1 : point.getDependencies()) {
            dependencies.add(d1.getKey());
        }

        return dependencies;
    }

    private Set> getDependencies(Key clazz) {

        Set> dependencies = new HashSet>();

        if (clazz.getTypeLiteral().getRawType().isInterface()) {
            dependencies.add(clazz);
            return dependencies;
        }

        try {
            dependencies.addAll(getDependencies(InjectionPoint.forConstructorOf(clazz.getTypeLiteral())));
        } catch (ConfigurationException e) {
            // nothing to do
        }

        for (InjectionPoint point : InjectionPoint.forInstanceMethodsAndFields(clazz.getTypeLiteral())) {
            dependencies.addAll(getDependencies(point));
        }

        for (InjectionPoint point : InjectionPoint.forStaticMethodsAndFields(clazz.getTypeLiteral())) {
            dependencies.addAll(getDependencies(point));
        }

        return dependencies;

    }

    private boolean hasAnyGuiceAnnotation(Class toInstanciate) {
        Boolean hasAnyGuiceAnnotation = HAS_INJECTION_ANNOTATION_CACHE.get(toInstanciate);
        if (hasAnyGuiceAnnotation != null) {
            return hasAnyGuiceAnnotation;
        }

        if (GwtReflectionUtils.getAnnotation(toInstanciate, Singleton.class) != null) {
            hasAnyGuiceAnnotation = true;
        } else if (GwtReflectionUtils.getAnnotation(toInstanciate, ProvidedBy.class) != null) {
            hasAnyGuiceAnnotation = true;
        } else if (hasInjectAnnotatedConstructor(toInstanciate)) {
            hasAnyGuiceAnnotation = true;
        } else {
            hasAnyGuiceAnnotation = GwtReflectionUtils.getAnnotatedField(toInstanciate, Inject.class).size() > 0;
        }

        HAS_INJECTION_ANNOTATION_CACHE.put(toInstanciate, hasAnyGuiceAnnotation);

        return hasAnyGuiceAnnotation;
    }

    private boolean hasInjectAnnotatedConstructor(Class toInstanciate) {
        for (Constructor cons : toInstanciate.getDeclaredConstructors()) {
            if (cons.getAnnotation(Inject.class) != null) {
                return true;
            }
        }
        return false;
    }

    private boolean isAsyncProviderKey(Key key) {
        Type keyType = key.getTypeLiteral().getType();
        return keyType instanceof ParameterizedType
                && ((ParameterizedType) keyType).getRawType() == AsyncProvider.class;
    }

    private boolean isProviderKey(Key key) {
        Type keyType = key.getTypeLiteral().getType();
        return keyType instanceof ParameterizedType
                && (((ParameterizedType) keyType).getRawType() == Provider.class || ((ParameterizedType) keyType).getRawType() == com.google.inject.Provider.class);
    }
}