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<Object> { private final Class<?> clazzToInstanciate; public DeferredBindingProvider(Class<? extends Ginjector> 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<Class<?>, DeferredBindingModule> DEFERRED_BINDING_MODULES_CACHE = new HashMap<Class<?>, DeferredBindingModule>(); private static final Map<String, Class<Object>> GENERATED = new HashMap<String, Class<Object>>(); private static final Map<Class<?>, Boolean> HAS_INJECTION_ANNOTATION_CACHE = new HashMap<Class<?>, Boolean>(); private static final Logger LOGGER = LoggerFactory.getLogger(DeferredBindingModule.class); static final DeferredBindingModule getDeferredBindingModule( Class<? extends Ginjector> ginInjectorClass, Collection<Module> 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<Key<?>> bindedClasses; private final Set<Key<?>> classesToInstanciate; private final Class<? extends Ginjector> ginInjectorClass; private DeferredBindingModule(Class<? extends Ginjector> ginInjectorClass, Module[] modules) { this.ginInjectorClass = ginInjectorClass; List<Element> elements = Elements.getElements(modules); this.classesToInstanciate = collectClassesFromInjector(ginInjectorClass); this.classesToInstanciate.addAll(collectDependencies(elements)); this.bindedClasses = collectBindedClasses(elements); } @Override protected void configure() { Set<Key<?>> copy = new HashSet<Key<?>>(bindedClasses); addDeferredBindings(classesToInstanciate, copy); } @SuppressWarnings("unchecked") private void addDeferredBinding(final Key<?> toInstanciate, Set<Key<?>> bindedClasses) { bindedClasses.add(toInstanciate); if (isProviderKey(toInstanciate)) { Key<Object> providedKey = (Key<Object>) ReflectUtil.getProvidedKey(toInstanciate); if (!bindedClasses.contains(providedKey)) { bindedClasses.add(providedKey); Set<Key<?>> collected = new HashSet<Key<?>>(); collectDependencies(providedKey, collected); addDeferredBindings(collected, bindedClasses); } } else if (isAsyncProviderKey(toInstanciate)) { Class<Object> asyncProviderClass = getAsyncProvider(toInstanciate); bind((Key<Object>) toInstanciate).to(asyncProviderClass); Key<Object> providedKey = (Key<Object>) ReflectUtil.getProvidedKey(toInstanciate); if (!bindedClasses.contains(providedKey)) { bindedClasses.add(providedKey); Set<Key<?>> collected = new HashSet<Key<?>>(); 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<Object>) toInstanciate).toProvider( new DeferredBindingProvider(ginInjectorClass, toInstanciate)); } } private void addDeferredBindings(Set<Key<?>> classesToInstanciate, Set<Key<?>> bindedClasses) { for (final Key<?> toInstanciate : classesToInstanciate) { if (!bindedClasses.contains(toInstanciate)) { addDeferredBinding(toInstanciate, bindedClasses); } } } private Set<Key<?>> collectBindedClasses(List<Element> elements) { final Set<Key<?>> bindedClasses = new HashSet<Key<?>>(); for (Element e : elements) { e.acceptVisitor(new DefaultElementVisitor<Void>() { @Override public <T> Void visit(Binding<T> binding) { bindedClasses.add(binding.getKey()); return null; } }); } return bindedClasses; } private Set<Key<?>> collectClassesFromInjector(Class<?> injectorClass) { Set<Key<?>> classesToInstanciate = new HashSet<Key<?>>(); 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<Key<?>> collected) { if (collected.contains(current)) { return; } collected.add(current); Set<Key<?>> dependencies = getDependencies(current); for (Key<?> dependency : dependencies) { collectDependencies(dependency, collected); } } private Set<Key<?>> collectDependencies(List<Element> elements) { final Set<Key<?>> dependencies = new HashSet<Key<?>>(); for (Element e : elements) { e.acceptVisitor(new DefaultElementVisitor<Void>() { @Override public <T> Void visit(Binding<T> 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<Object> 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<Object> getAsyncProvider(Key<?> key) { Key<?> providedKey = ReflectUtil.getProvidedKey(key); String className = providedKey.getTypeLiteral().getRawType().getName() + "AsyncProvider"; Class<Object> clazz = GENERATED.get(className); if (clazz != null) { return clazz; } clazz = generatedAsyncProvider(className, providedKey); GENERATED.put(className, clazz); return clazz; } private Set<Key<?>> getDependencies(InjectionPoint point) { Set<Key<?>> dependencies = new HashSet<Key<?>>(); for (Dependency<?> d1 : point.getDependencies()) { dependencies.add(d1.getKey()); } return dependencies; } private Set<Key<?>> getDependencies(Key<?> clazz) { Set<Key<?>> dependencies = new HashSet<Key<?>>(); 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); } }