Home | History | Annotate | Download | only in inject
      1 /**
      2  * Copyright (C) 2009 Google Inc.
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  * http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.google.inject;
     18 
     19 import static com.google.inject.Asserts.asModuleChain;
     20 import static com.google.inject.Asserts.assertContains;
     21 import static com.google.inject.Asserts.getDeclaringSourcePart;
     22 import static com.google.inject.matcher.Matchers.any;
     23 import static com.google.inject.matcher.Matchers.only;
     24 import static com.google.inject.name.Names.named;
     25 
     26 import com.google.common.collect.ImmutableList;
     27 import com.google.common.collect.Lists;
     28 import com.google.inject.matcher.Matcher;
     29 import com.google.inject.matcher.Matchers;
     30 import com.google.inject.spi.InjectionListener;
     31 import com.google.inject.spi.Message;
     32 import com.google.inject.spi.TypeEncounter;
     33 import com.google.inject.spi.TypeListener;
     34 
     35 import junit.framework.TestCase;
     36 
     37 import java.util.List;
     38 import java.util.concurrent.atomic.AtomicInteger;
     39 import java.util.concurrent.atomic.AtomicReference;
     40 
     41 /**
     42  * @author jessewilson (at) google.com (Jesse Wilson)
     43  */
     44 public class TypeListenerTest extends TestCase {
     45 
     46   private final Matcher<Object> onlyAbcd = Matchers.only(new TypeLiteral<A>() {})
     47       .or(only(new TypeLiteral<B>() {}))
     48       .or(only(new TypeLiteral<C>() {}))
     49       .or(only(new TypeLiteral<D>() {}));
     50 
     51   final TypeListener failingTypeListener = new TypeListener() {
     52     int failures = 0;
     53 
     54     public <I> void hear(TypeLiteral<I> type, TypeEncounter<I> encounter) {
     55       throw new ClassCastException("whoops, failure #" + (++failures));
     56     }
     57 
     58     @Override public String toString() {
     59       return "clumsy";
     60     }
     61   };
     62 
     63   final InjectionListener<Object> failingInjectionListener = new InjectionListener<Object>() {
     64     int failures = 0;
     65 
     66     public void afterInjection(Object injectee) {
     67       throw new ClassCastException("whoops, failure #" + (++failures));
     68     }
     69 
     70     @Override public String toString() {
     71       return "goofy";
     72     }
     73   };
     74 
     75   final MembersInjector<Object> failingMembersInjector = new MembersInjector<Object>() {
     76     int failures = 0;
     77 
     78     public void injectMembers(Object instance) {
     79       throw new ClassCastException("whoops, failure #" + (++failures));
     80     }
     81 
     82     @Override public String toString() {
     83       return "awkward";
     84     }
     85   };
     86 
     87   public void testTypeListenersAreFired() {
     88     final AtomicInteger firedCount = new AtomicInteger();
     89 
     90     final TypeListener typeListener = new TypeListener() {
     91       public <I> void hear(TypeLiteral<I> type, TypeEncounter<I> encounter) {
     92         assertEquals(new TypeLiteral<A>() {}, type);
     93         firedCount.incrementAndGet();
     94       }
     95     };
     96 
     97     Guice.createInjector(new AbstractModule() {
     98       @Override protected void configure() {
     99         bindListener(onlyAbcd, typeListener);
    100         bind(A.class);
    101       }
    102     });
    103 
    104     assertEquals(1, firedCount.get());
    105   }
    106 
    107   public void testInstallingInjectionListener() {
    108     final List<Object> injectees = Lists.newArrayList();
    109     final InjectionListener<Object> injectionListener = new InjectionListener<Object>() {
    110       public void afterInjection(Object injectee) {
    111         injectees.add(injectee);
    112       }
    113     };
    114 
    115     Injector injector = Guice.createInjector(new AbstractModule() {
    116       @Override protected void configure() {
    117         bindListener(onlyAbcd, new TypeListener() {
    118           public <I> void hear(TypeLiteral<I> type, TypeEncounter<I> encounter) {
    119             encounter.register(injectionListener);
    120           }
    121         });
    122         bind(A.class);
    123       }
    124     });
    125 
    126     assertEquals(ImmutableList.of(), injectees);
    127 
    128     Object a1 = injector.getInstance(A.class);
    129     assertEquals(ImmutableList.of(a1), injectees);
    130 
    131     Object a2 = injector.getInstance(A.class);
    132     assertEquals(ImmutableList.of(a1, a2), injectees);
    133 
    134     Object b1 = injector.getInstance(B.class);
    135     assertEquals(ImmutableList.of(a1, a2, b1), injectees);
    136 
    137     Provider<A> aProvider = injector.getProvider(A.class);
    138     assertEquals(ImmutableList.of(a1, a2, b1), injectees);
    139     A a3 = aProvider.get();
    140     A a4 = aProvider.get();
    141     assertEquals(ImmutableList.of(a1, a2, b1, a3, a4), injectees);
    142   }
    143 
    144   /*if[AOP]*/
    145   private static org.aopalliance.intercept.MethodInterceptor prefixInterceptor(
    146       final String prefix) {
    147     return new org.aopalliance.intercept.MethodInterceptor() {
    148       public Object invoke(org.aopalliance.intercept.MethodInvocation methodInvocation)
    149           throws Throwable {
    150         return prefix + methodInvocation.proceed();
    151       }
    152     };
    153   }
    154 
    155   public void testAddingInterceptors() throws NoSuchMethodException {
    156     final Matcher<Object> buzz = only(C.class.getMethod("buzz"));
    157 
    158     Injector injector = Guice.createInjector(new AbstractModule() {
    159       @Override protected void configure() {
    160         bindInterceptor(any(), buzz, prefixInterceptor("ka"));
    161         bindInterceptor(any(), any(), prefixInterceptor("fe"));
    162 
    163         bindListener(onlyAbcd, new TypeListener() {
    164           public <I> void hear(TypeLiteral<I> type, TypeEncounter<I> encounter) {
    165             encounter.bindInterceptor(any(), prefixInterceptor("li"));
    166             encounter.bindInterceptor(buzz, prefixInterceptor("no"));
    167           }
    168         });
    169       }
    170     });
    171 
    172     // interceptors must be invoked in the order they're bound.
    173     C c = injector.getInstance(C.class);
    174     assertEquals("kafelinobuzz", c.buzz());
    175     assertEquals("felibeep", c.beep());
    176   }
    177   /*end[AOP]*/
    178 
    179   class OuterThrowsModule extends AbstractModule {
    180     @Override protected void configure() {
    181       install(new InnerThrowsModule());
    182     }
    183   }
    184   class InnerThrowsModule extends AbstractModule {
    185     @Override protected void configure() {
    186       bindListener(onlyAbcd, failingTypeListener);
    187       bind(B.class);
    188       bind(C.class);
    189     }
    190   }
    191   public void testTypeListenerThrows() {
    192     try {
    193       Guice.createInjector(new OuterThrowsModule());
    194       fail();
    195     } catch (CreationException expected) {
    196       assertContains(expected.getMessage(),
    197           "1) Error notifying TypeListener clumsy (bound at " + getClass().getName(),
    198           getDeclaringSourcePart(getClass()),
    199           asModuleChain(OuterThrowsModule.class, InnerThrowsModule.class),
    200           "of " + B.class.getName(),
    201           "Reason: java.lang.ClassCastException: whoops, failure #1",
    202           "2) Error notifying TypeListener clumsy (bound at " + getClass().getName(),
    203           getDeclaringSourcePart(getClass()),
    204           asModuleChain(OuterThrowsModule.class, InnerThrowsModule.class),
    205           "of " + C.class.getName(),
    206           "Reason: java.lang.ClassCastException: whoops, failure #2");
    207     }
    208 
    209     Injector injector = Guice.createInjector(new AbstractModule() {
    210       @Override protected void configure() {
    211         bindListener(onlyAbcd, failingTypeListener);
    212       }
    213     });
    214     try {
    215       injector.getProvider(B.class);
    216       fail();
    217     } catch (ConfigurationException expected) {
    218       assertContains(expected.getMessage(),
    219           "1) Error notifying TypeListener clumsy (bound at " + getClass().getName(),
    220           getDeclaringSourcePart(getClass()),
    221           "of " + B.class.getName(),
    222           "Reason: java.lang.ClassCastException: whoops, failure #3");
    223     }
    224 
    225     // getting it again should yield the same exception #3
    226     try {
    227       injector.getInstance(B.class);
    228       fail();
    229     } catch (ConfigurationException expected) {
    230       assertContains(expected.getMessage(),
    231           "1) Error notifying TypeListener clumsy (bound at " + getClass().getName(),
    232           getDeclaringSourcePart(getClass()),
    233           "of " + B.class.getName(),
    234           "Reason: java.lang.ClassCastException: whoops, failure #3");
    235     }
    236 
    237     // non-injected types do not participate
    238     assertSame(Stage.DEVELOPMENT, injector.getInstance(Stage.class));
    239   }
    240 
    241   public void testInjectionListenerThrows() {
    242     Injector injector = Guice.createInjector(new AbstractModule() {
    243       @Override protected void configure() {
    244         bindListener(onlyAbcd, new TypeListener() {
    245           public <I> void hear(TypeLiteral<I> type, TypeEncounter<I> encounter) {
    246             encounter.register(failingInjectionListener);
    247           }
    248         });
    249         bind(B.class);
    250       }
    251     });
    252 
    253     try {
    254       injector.getInstance(A.class);
    255       fail();
    256     } catch (ProvisionException e) {
    257       assertContains(e.getMessage(),
    258           "1) Error notifying InjectionListener goofy of " + A.class.getName(),
    259           " Reason: java.lang.ClassCastException: whoops, failure #1");
    260     }
    261 
    262     // second time through should be a new cause (#2)
    263     try {
    264       injector.getInstance(A.class);
    265       fail();
    266     } catch (ProvisionException e) {
    267       assertContains(e.getMessage(),
    268           "1) Error notifying InjectionListener goofy of " + A.class.getName(),
    269           " Reason: java.lang.ClassCastException: whoops, failure #2");
    270     }
    271 
    272     // we should get errors for all types, but only on getInstance()
    273     Provider<B> bProvider = injector.getProvider(B.class);
    274     try {
    275       bProvider.get();
    276       fail();
    277     } catch (ProvisionException e) {
    278       assertContains(e.getMessage(),
    279           "1) Error notifying InjectionListener goofy of " + B.class.getName(),
    280           " Reason: java.lang.ClassCastException: whoops, failure #3");
    281     }
    282 
    283     // non-injected types do not participate
    284     assertSame(Stage.DEVELOPMENT, injector.getInstance(Stage.class));
    285   }
    286 
    287   public void testInjectMembersTypeListenerFails() {
    288     try {
    289       Guice.createInjector(new AbstractModule() {
    290         @Override protected void configure() {
    291           getMembersInjector(A.class);
    292           bindListener(onlyAbcd, failingTypeListener);
    293         }
    294       });
    295       fail();
    296     } catch (CreationException expected) {
    297       assertContains(expected.getMessage(),
    298           "1) Error notifying TypeListener clumsy (bound at ",
    299           TypeListenerTest.class.getName(), getDeclaringSourcePart(getClass()),
    300           "of " + A.class.getName(),
    301           " Reason: java.lang.ClassCastException: whoops, failure #1");
    302     }
    303   }
    304 
    305   public void testConstructedTypeListenerIsTheSameAsMembersInjectorListener() {
    306     final AtomicInteger typeEncounters = new AtomicInteger();
    307     final AtomicInteger injections = new AtomicInteger();
    308 
    309     final InjectionListener<A> listener = new InjectionListener<A>() {
    310       public void afterInjection(A injectee) {
    311         injections.incrementAndGet();
    312         assertNotNull(injectee.injector);
    313       }
    314     };
    315 
    316     Injector injector = Guice.createInjector(new AbstractModule() {
    317       @Override protected void configure() {
    318         bindListener(onlyAbcd, new TypeListener() {
    319           @SuppressWarnings("unchecked")
    320           public <I> void hear(TypeLiteral<I> type, TypeEncounter<I> encounter) {
    321             typeEncounters.incrementAndGet();
    322             encounter.register((InjectionListener) listener);
    323           }
    324         });
    325 
    326         bind(A.class);
    327         getMembersInjector(A.class);
    328       }
    329     });
    330 
    331     // creating the injector shouldn't trigger injections
    332     assertEquals(0, injections.getAndSet(0));
    333 
    334     // constructing an A should trigger an injection
    335     injector.getInstance(A.class);
    336     assertEquals(1, injections.getAndSet(0));
    337 
    338     // injecting an A should trigger an injection
    339     injector.injectMembers(new A());
    340     assertEquals(1, injections.getAndSet(0));
    341 
    342     // getting a provider shouldn't
    343     Provider<A> aProvider = injector.getProvider(A.class);
    344     MembersInjector<A> aMembersInjector = injector.getMembersInjector(A.class);
    345     assertEquals(0, injections.getAndSet(0));
    346 
    347     // exercise the provider
    348     aProvider.get();
    349     aProvider.get();
    350     assertEquals(2, injections.getAndSet(0));
    351 
    352     // exercise the members injector
    353     aMembersInjector.injectMembers(new A());
    354     aMembersInjector.injectMembers(new A());
    355     assertEquals(2, injections.getAndSet(0));
    356 
    357     // we should only have encountered one type
    358     assertEquals(1, typeEncounters.getAndSet(0));
    359   }
    360 
    361   public void testLookupsAtInjectorCreateTime() {
    362     final AtomicReference<Provider<B>> bProviderReference = new AtomicReference<Provider<B>>();
    363     final AtomicReference<MembersInjector<A>> aMembersInjectorReference
    364         = new AtomicReference<MembersInjector<A>>();
    365 
    366     final InjectionListener<Object> lookupsTester = new InjectionListener<Object>() {
    367       public void afterInjection(Object injectee) {
    368         assertNotNull(bProviderReference.get().get());
    369 
    370         A a = new A();
    371         aMembersInjectorReference.get().injectMembers(a);
    372         assertNotNull(a.injector);
    373       }
    374     };
    375 
    376     Guice.createInjector(new AbstractModule() {
    377       @Override protected void configure() {
    378         bindListener(only(TypeLiteral.get(C.class)), new TypeListener() {
    379           public <I> void hear(TypeLiteral<I> type, TypeEncounter<I> encounter) {
    380             Provider<B> bProvider = encounter.getProvider(B.class);
    381             try {
    382               bProvider.get();
    383               fail();
    384             } catch (IllegalStateException expected) {
    385               assertEquals("This Provider cannot be used until the Injector has been created.",
    386                   expected.getMessage());
    387             }
    388             bProviderReference.set(bProvider);
    389 
    390             MembersInjector<A> aMembersInjector = encounter.getMembersInjector(A.class);
    391             try {
    392               aMembersInjector.injectMembers(new A());
    393               fail();
    394             } catch (IllegalStateException expected) {
    395               assertEquals(
    396                   "This MembersInjector cannot be used until the Injector has been created.",
    397                   expected.getMessage());
    398             }
    399             aMembersInjectorReference.set(aMembersInjector);
    400 
    401             encounter.register(lookupsTester);
    402           }
    403         });
    404 
    405         // this ensures the type listener fires, and also the afterInjection() listener
    406         bind(C.class).asEagerSingleton();
    407       }
    408     });
    409 
    410     lookupsTester.afterInjection(null);
    411   }
    412 
    413   public void testLookupsPostCreate() {
    414     Injector injector = Guice.createInjector(new AbstractModule() {
    415       @Override protected void configure() {
    416         bindListener(only(TypeLiteral.get(C.class)), new TypeListener() {
    417           public <I> void hear(TypeLiteral<I> type, TypeEncounter<I> encounter) {
    418             assertNotNull(encounter.getProvider(B.class).get());
    419 
    420             A a = new A();
    421             encounter.getMembersInjector(A.class).injectMembers(a);
    422             assertNotNull(a.injector);
    423           }
    424         });
    425       }
    426     });
    427 
    428     injector.getInstance(C.class);
    429   }
    430 
    431   public void testMembersInjector() {
    432     final MembersInjector<D> membersInjector = new MembersInjector<D>() {
    433       public void injectMembers(D instance) {
    434         instance.userInjected++;
    435         assertEquals(instance.guiceInjected, instance.userInjected);
    436       }
    437     };
    438 
    439     final InjectionListener<D> injectionListener = new InjectionListener<D>() {
    440       public void afterInjection(D injectee) {
    441         assertTrue(injectee.userInjected > 0);
    442         injectee.listenersNotified++;
    443         assertEquals(injectee.guiceInjected, injectee.listenersNotified);
    444       }
    445     };
    446 
    447     Injector injector = Guice.createInjector(new AbstractModule() {
    448       @Override protected void configure() {
    449         bindListener(onlyAbcd, new TypeListener() {
    450           @SuppressWarnings("unchecked")
    451           public <I> void hear(TypeLiteral<I> type, TypeEncounter<I> encounter) {
    452             encounter.register((MembersInjector) membersInjector);
    453             encounter.register((InjectionListener) injectionListener);
    454           }
    455         });
    456 
    457         D boundThreeTimes = new D();
    458         bind(D.class).annotatedWith(named("i")).toInstance(boundThreeTimes);
    459         bind(D.class).annotatedWith(named("ii")).toInstance(boundThreeTimes);
    460         bind(D.class).annotatedWith(named("iii")).toInstance(boundThreeTimes);
    461       }
    462     });
    463 
    464     D boundThreeTimes = injector.getInstance(Key.get(D.class, named("iii")));
    465     boundThreeTimes.assertAllCounts(1);
    466 
    467     D getInstance = injector.getInstance(D.class);
    468     getInstance.assertAllCounts(1);
    469 
    470     D memberInjection = new D();
    471     injector.injectMembers(memberInjection);
    472     memberInjection.assertAllCounts(1);
    473 
    474     injector.injectMembers(memberInjection);
    475     injector.injectMembers(memberInjection);
    476     memberInjection.assertAllCounts(3);
    477 
    478     injector.getMembersInjector(D.class).injectMembers(memberInjection);
    479     memberInjection.assertAllCounts(4);
    480   }
    481 
    482   public void testMembersInjectorThrows() {
    483     Injector injector = Guice.createInjector(new AbstractModule() {
    484       @Override protected void configure() {
    485         bindListener(onlyAbcd, new TypeListener() {
    486           public <I> void hear(TypeLiteral<I> type, TypeEncounter<I> encounter) {
    487             encounter.register(failingMembersInjector);
    488           }
    489         });
    490         bind(B.class);
    491       }
    492     });
    493 
    494     try {
    495       injector.getInstance(A.class);
    496       fail();
    497     } catch (ProvisionException e) {
    498       assertContains(e.getMessage(),
    499           "1) Error injecting " + A.class.getName() + " using awkward.",
    500           "Reason: java.lang.ClassCastException: whoops, failure #1");
    501     }
    502 
    503     // second time through should be a new cause (#2)
    504     try {
    505       injector.getInstance(A.class);
    506       fail();
    507     } catch (ProvisionException e) {
    508       assertContains(e.getMessage(),
    509           "1) Error injecting " + A.class.getName() + " using awkward.",
    510           "Reason: java.lang.ClassCastException: whoops, failure #2");
    511     }
    512 
    513     // we should get errors for all types, but only on getInstance()
    514     Provider<B> bProvider = injector.getProvider(B.class);
    515     try {
    516       bProvider.get();
    517       fail();
    518     } catch (ProvisionException e) {
    519       assertContains(e.getMessage(),
    520           "1) Error injecting " + B.class.getName() + " using awkward.",
    521           "Reason: java.lang.ClassCastException: whoops, failure #3");
    522     }
    523 
    524     // non-injected types do not participate
    525     assertSame(Stage.DEVELOPMENT, injector.getInstance(Stage.class));
    526   }
    527 
    528   /**
    529    * We had a bug where we weren't notifying of types encountered for member injection when those
    530    * types had no members to be injected. Constructed types are always injected because they always
    531    * have at least one injection point: the class constructor.
    532    */
    533   public void testTypesWithNoInjectableMembersAreNotified() {
    534     final AtomicInteger notificationCount = new AtomicInteger();
    535 
    536     Guice.createInjector(new AbstractModule() {
    537       @Override protected void configure() {
    538         bindListener(onlyAbcd, new TypeListener() {
    539           public <I> void hear(TypeLiteral<I> type, TypeEncounter<I> encounter) {
    540             notificationCount.incrementAndGet();
    541           }
    542         });
    543 
    544         bind(C.class).toInstance(new C());
    545       }
    546     });
    547 
    548     assertEquals(1, notificationCount.get());
    549   }
    550 
    551   public void testEncounterCannotBeUsedAfterHearReturns() {
    552     final AtomicReference<TypeEncounter<?>> encounterReference = new AtomicReference<TypeEncounter<?>>();
    553 
    554     Guice.createInjector(new AbstractModule() {
    555       @Override protected void configure() {
    556         bindListener(any(), new TypeListener() {
    557           public <I> void hear(TypeLiteral<I> type, TypeEncounter<I> encounter) {
    558             encounterReference.set(encounter);
    559           }
    560         });
    561 
    562         bind(C.class);
    563       }
    564     });
    565     TypeEncounter<?> encounter = encounterReference.get();
    566 
    567     try {
    568       encounter.register(new InjectionListener<Object>() {
    569         public void afterInjection(Object injectee) {}
    570       });
    571       fail();
    572     } catch (IllegalStateException expected) {
    573     }
    574 
    575     /*if[AOP]*/
    576     try {
    577       encounter.bindInterceptor(any(), new org.aopalliance.intercept.MethodInterceptor() {
    578         public Object invoke(org.aopalliance.intercept.MethodInvocation methodInvocation)
    579             throws Throwable {
    580           return methodInvocation.proceed();
    581         }
    582       });
    583       fail();
    584     } catch (IllegalStateException expected) {
    585     }
    586     /*end[AOP]*/
    587 
    588     try {
    589       encounter.addError(new Exception());
    590       fail();
    591     } catch (IllegalStateException expected) {
    592     }
    593 
    594     try {
    595       encounter.getMembersInjector(A.class);
    596       fail();
    597     } catch (IllegalStateException expected) {
    598     }
    599 
    600     try {
    601       encounter.getProvider(B.class);
    602       fail();
    603     } catch (IllegalStateException expected) {
    604     }
    605   }
    606 
    607   public void testAddErrors() {
    608     try {
    609       Guice.createInjector(new AbstractModule() {
    610         @Override protected void configure() {
    611           requestInjection(new Object());
    612           bindListener(Matchers.any(), new TypeListener() {
    613             public <I> void hear(TypeLiteral<I> type, TypeEncounter<I> encounter) {
    614               encounter.addError("There was an error on %s", type);
    615               encounter.addError(new IllegalArgumentException("whoops!"));
    616               encounter.addError(new Message("And another problem"));
    617               encounter.addError(new IllegalStateException());
    618             }
    619           });
    620         }
    621       });
    622       fail();
    623     } catch (CreationException expected) {
    624       assertContains(expected.getMessage(),
    625           "1) There was an error on java.lang.Object",
    626           "2) An exception was caught and reported. Message: whoops!",
    627           "3) And another problem",
    628           "4) An exception was caught and reported. Message: null",
    629           "4 errors");
    630     }
    631   }
    632 
    633   private static class CountingMembersInjector implements MembersInjector<D> {
    634     public void injectMembers(D instance) {
    635       ++instance.userInjected;
    636     }
    637   }
    638 
    639   private static class CountingInjectionListener implements InjectionListener<D> {
    640     public void afterInjection(D injectee) {
    641       ++injectee.listenersNotified;
    642     }
    643   }
    644 
    645   private static class DuplicatingTypeListener implements TypeListener {
    646     int count = 0;
    647 
    648     @SuppressWarnings({"rawtypes", "unchecked"})
    649     public <I> void hear(TypeLiteral<I> type, TypeEncounter<I> encounter) {
    650       ++count;
    651 
    652       MembersInjector membersInjector = new CountingMembersInjector();
    653       encounter.register(membersInjector);
    654       encounter.register(membersInjector);
    655 
    656       InjectionListener injectionListener = new CountingInjectionListener();
    657       encounter.register(injectionListener);
    658       encounter.register(injectionListener);
    659     }
    660   }
    661 
    662   public void testDeDuplicateTypeListeners() {
    663     final DuplicatingTypeListener typeListener = new DuplicatingTypeListener();
    664     Injector injector = Guice.createInjector(new AbstractModule() {
    665       @Override
    666       protected void configure() {
    667         bindListener(any(), typeListener);
    668         bindListener(only(new TypeLiteral<D>() {}), typeListener);
    669       }
    670     });
    671     D d = injector.getInstance(D.class);
    672     d.assertAllCounts(1);
    673     assertEquals(1, typeListener.count);
    674   }
    675 
    676   // TODO: recursively accessing a lookup should fail
    677 
    678   static class A {
    679     @Inject Injector injector;
    680     @Inject Stage stage;
    681   }
    682 
    683   static class B {}
    684 
    685   public static class C {
    686     public String buzz() {
    687       return "buzz";
    688     }
    689 
    690     public String beep() {
    691       return "beep";
    692     }
    693   }
    694 
    695   static class D {
    696     int guiceInjected = 0;
    697     int userInjected = 0;
    698     int listenersNotified = 0;
    699 
    700     @Inject void guiceInjected() {
    701       guiceInjected++;
    702     }
    703 
    704     void assertAllCounts(int expected) {
    705       assertEquals(expected, guiceInjected);
    706       assertEquals(expected, userInjected);
    707       assertEquals(expected, listenersNotified);
    708     }
    709   }
    710 }
    711