/* * Copyright (C) 2009 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.inject; import static com.google.inject.Asserts.asModuleChain; import static com.google.inject.Asserts.assertContains; import static com.google.inject.Asserts.getDeclaringSourcePart; import static com.google.inject.matcher.Matchers.any; import static com.google.inject.matcher.Matchers.only; import static com.google.inject.name.Names.named; import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; import com.google.inject.matcher.Matcher; import com.google.inject.matcher.Matchers; import com.google.inject.spi.InjectionListener; import com.google.inject.spi.Message; import com.google.inject.spi.TypeEncounter; import com.google.inject.spi.TypeListener; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import junit.framework.TestCase; /** @author jessewilson@google.com (Jesse Wilson) */ public class TypeListenerTest extends TestCase { private final Matcher<Object> onlyAbcd = Matchers.only(new TypeLiteral<A>() {}) .or(only(new TypeLiteral<B>() {})) .or(only(new TypeLiteral<C>() {})) .or(only(new TypeLiteral<D>() {})); final TypeListener failingTypeListener = new TypeListener() { int failures = 0; @Override public <I> void hear(TypeLiteral<I> type, TypeEncounter<I> encounter) { throw new ClassCastException("whoops, failure #" + (++failures)); } @Override public String toString() { return "clumsy"; } }; final InjectionListener<Object> failingInjectionListener = new InjectionListener<Object>() { int failures = 0; @Override public void afterInjection(Object injectee) { throw new ClassCastException("whoops, failure #" + (++failures)); } @Override public String toString() { return "goofy"; } }; final MembersInjector<Object> failingMembersInjector = new MembersInjector<Object>() { int failures = 0; @Override public void injectMembers(Object instance) { throw new ClassCastException("whoops, failure #" + (++failures)); } @Override public String toString() { return "awkward"; } }; public void testTypeListenersAreFired() { final AtomicInteger firedCount = new AtomicInteger(); final TypeListener typeListener = new TypeListener() { @Override public <I> void hear(TypeLiteral<I> type, TypeEncounter<I> encounter) { assertEquals(new TypeLiteral<A>() {}, type); firedCount.incrementAndGet(); } }; Guice.createInjector( new AbstractModule() { @Override protected void configure() { bindListener(onlyAbcd, typeListener); bind(A.class); } }); assertEquals(1, firedCount.get()); } public void testInstallingInjectionListener() { final List<Object> injectees = Lists.newArrayList(); final InjectionListener<Object> injectionListener = new InjectionListener<Object>() { @Override public void afterInjection(Object injectee) { injectees.add(injectee); } }; Injector injector = Guice.createInjector( new AbstractModule() { @Override protected void configure() { bindListener( onlyAbcd, new TypeListener() { @Override public <I> void hear(TypeLiteral<I> type, TypeEncounter<I> encounter) { encounter.register(injectionListener); } }); bind(A.class); } }); assertEquals(ImmutableList.of(), injectees); Object a1 = injector.getInstance(A.class); assertEquals(ImmutableList.of(a1), injectees); Object a2 = injector.getInstance(A.class); assertEquals(ImmutableList.of(a1, a2), injectees); Object b1 = injector.getInstance(B.class); assertEquals(ImmutableList.of(a1, a2, b1), injectees); Provider<A> aProvider = injector.getProvider(A.class); assertEquals(ImmutableList.of(a1, a2, b1), injectees); A a3 = aProvider.get(); A a4 = aProvider.get(); assertEquals(ImmutableList.of(a1, a2, b1, a3, a4), injectees); } /*if[AOP]*/ private static org.aopalliance.intercept.MethodInterceptor prefixInterceptor( final String prefix) { return new org.aopalliance.intercept.MethodInterceptor() { @Override public Object invoke(org.aopalliance.intercept.MethodInvocation methodInvocation) throws Throwable { return prefix + methodInvocation.proceed(); } }; } public void testAddingInterceptors() throws NoSuchMethodException { final Matcher<Object> buzz = only(C.class.getMethod("buzz")); Injector injector = Guice.createInjector( new AbstractModule() { @Override protected void configure() { bindInterceptor(any(), buzz, prefixInterceptor("ka")); bindInterceptor(any(), any(), prefixInterceptor("fe")); bindListener( onlyAbcd, new TypeListener() { @Override public <I> void hear(TypeLiteral<I> type, TypeEncounter<I> encounter) { encounter.bindInterceptor(any(), prefixInterceptor("li")); encounter.bindInterceptor(buzz, prefixInterceptor("no")); } }); } }); // interceptors must be invoked in the order they're bound. C c = injector.getInstance(C.class); assertEquals("kafelinobuzz", c.buzz()); assertEquals("felibeep", c.beep()); } /*end[AOP]*/ class OuterThrowsModule extends AbstractModule { @Override protected void configure() { install(new InnerThrowsModule()); } } class InnerThrowsModule extends AbstractModule { @Override protected void configure() { bindListener(onlyAbcd, failingTypeListener); bind(B.class); bind(C.class); } } public void testTypeListenerThrows() { try { Guice.createInjector(new OuterThrowsModule()); fail(); } catch (CreationException expected) { assertContains( expected.getMessage(), "1) Error notifying TypeListener clumsy (bound at " + getClass().getName(), getDeclaringSourcePart(getClass()), asModuleChain(OuterThrowsModule.class, InnerThrowsModule.class), "of " + B.class.getName(), "Reason: java.lang.ClassCastException: whoops, failure #1", "2) Error notifying TypeListener clumsy (bound at " + getClass().getName(), getDeclaringSourcePart(getClass()), asModuleChain(OuterThrowsModule.class, InnerThrowsModule.class), "of " + C.class.getName(), "Reason: java.lang.ClassCastException: whoops, failure #2"); } Injector injector = Guice.createInjector( new AbstractModule() { @Override protected void configure() { bindListener(onlyAbcd, failingTypeListener); } }); try { injector.getProvider(B.class); fail(); } catch (ConfigurationException expected) { assertContains( expected.getMessage(), "1) Error notifying TypeListener clumsy (bound at " + getClass().getName(), getDeclaringSourcePart(getClass()), "of " + B.class.getName(), "Reason: java.lang.ClassCastException: whoops, failure #3"); } // getting it again should yield the same exception #3 try { injector.getInstance(B.class); fail(); } catch (ConfigurationException expected) { assertContains( expected.getMessage(), "1) Error notifying TypeListener clumsy (bound at " + getClass().getName(), getDeclaringSourcePart(getClass()), "of " + B.class.getName(), "Reason: java.lang.ClassCastException: whoops, failure #3"); } // non-injected types do not participate assertSame(Stage.DEVELOPMENT, injector.getInstance(Stage.class)); } public void testInjectionListenerThrows() { Injector injector = Guice.createInjector( new AbstractModule() { @Override protected void configure() { bindListener( onlyAbcd, new TypeListener() { @Override public <I> void hear(TypeLiteral<I> type, TypeEncounter<I> encounter) { encounter.register(failingInjectionListener); } }); bind(B.class); } }); try { injector.getInstance(A.class); fail(); } catch (ProvisionException e) { assertContains( e.getMessage(), "1) Error notifying InjectionListener goofy of " + A.class.getName(), " Reason: java.lang.ClassCastException: whoops, failure #1"); } // second time through should be a new cause (#2) try { injector.getInstance(A.class); fail(); } catch (ProvisionException e) { assertContains( e.getMessage(), "1) Error notifying InjectionListener goofy of " + A.class.getName(), " Reason: java.lang.ClassCastException: whoops, failure #2"); } // we should get errors for all types, but only on getInstance() Provider<B> bProvider = injector.getProvider(B.class); try { bProvider.get(); fail(); } catch (ProvisionException e) { assertContains( e.getMessage(), "1) Error notifying InjectionListener goofy of " + B.class.getName(), " Reason: java.lang.ClassCastException: whoops, failure #3"); } // non-injected types do not participate assertSame(Stage.DEVELOPMENT, injector.getInstance(Stage.class)); } public void testInjectMembersTypeListenerFails() { try { Guice.createInjector( new AbstractModule() { @Override protected void configure() { getMembersInjector(A.class); bindListener(onlyAbcd, failingTypeListener); } }); fail(); } catch (CreationException expected) { assertContains( expected.getMessage(), "1) Error notifying TypeListener clumsy (bound at ", TypeListenerTest.class.getName(), getDeclaringSourcePart(getClass()), "of " + A.class.getName(), " Reason: java.lang.ClassCastException: whoops, failure #1"); } } public void testConstructedTypeListenerIsTheSameAsMembersInjectorListener() { final AtomicInteger typeEncounters = new AtomicInteger(); final AtomicInteger injections = new AtomicInteger(); final InjectionListener<A> listener = new InjectionListener<A>() { @Override public void afterInjection(A injectee) { injections.incrementAndGet(); assertNotNull(injectee.injector); } }; Injector injector = Guice.createInjector( new AbstractModule() { @Override protected void configure() { bindListener( onlyAbcd, new TypeListener() { @Override @SuppressWarnings("unchecked") public <I> void hear(TypeLiteral<I> type, TypeEncounter<I> encounter) { typeEncounters.incrementAndGet(); encounter.register((InjectionListener) listener); } }); bind(A.class); getMembersInjector(A.class); } }); // creating the injector shouldn't trigger injections assertEquals(0, injections.getAndSet(0)); // constructing an A should trigger an injection injector.getInstance(A.class); assertEquals(1, injections.getAndSet(0)); // injecting an A should trigger an injection injector.injectMembers(new A()); assertEquals(1, injections.getAndSet(0)); // getting a provider shouldn't Provider<A> aProvider = injector.getProvider(A.class); MembersInjector<A> aMembersInjector = injector.getMembersInjector(A.class); assertEquals(0, injections.getAndSet(0)); // exercise the provider aProvider.get(); aProvider.get(); assertEquals(2, injections.getAndSet(0)); // exercise the members injector aMembersInjector.injectMembers(new A()); aMembersInjector.injectMembers(new A()); assertEquals(2, injections.getAndSet(0)); // we should only have encountered one type assertEquals(1, typeEncounters.getAndSet(0)); } public void testLookupsAtInjectorCreateTime() { final AtomicReference<Provider<B>> bProviderReference = new AtomicReference<Provider<B>>(); final AtomicReference<MembersInjector<A>> aMembersInjectorReference = new AtomicReference<MembersInjector<A>>(); final InjectionListener<Object> lookupsTester = new InjectionListener<Object>() { @Override public void afterInjection(Object injectee) { assertNotNull(bProviderReference.get().get()); A a = new A(); aMembersInjectorReference.get().injectMembers(a); assertNotNull(a.injector); } }; Guice.createInjector( new AbstractModule() { @Override protected void configure() { bindListener( only(TypeLiteral.get(C.class)), new TypeListener() { @Override public <I> void hear(TypeLiteral<I> type, TypeEncounter<I> encounter) { Provider<B> bProvider = encounter.getProvider(B.class); try { bProvider.get(); fail(); } catch (IllegalStateException expected) { assertEquals( "This Provider cannot be used until the Injector has been created.", expected.getMessage()); } bProviderReference.set(bProvider); MembersInjector<A> aMembersInjector = encounter.getMembersInjector(A.class); try { aMembersInjector.injectMembers(new A()); fail(); } catch (IllegalStateException expected) { assertEquals( "This MembersInjector cannot be used until the Injector has been created.", expected.getMessage()); } aMembersInjectorReference.set(aMembersInjector); encounter.register(lookupsTester); } }); // this ensures the type listener fires, and also the afterInjection() listener bind(C.class).asEagerSingleton(); } }); lookupsTester.afterInjection(null); } public void testLookupsPostCreate() { Injector injector = Guice.createInjector( new AbstractModule() { @Override protected void configure() { bindListener( only(TypeLiteral.get(C.class)), new TypeListener() { @Override public <I> void hear(TypeLiteral<I> type, TypeEncounter<I> encounter) { assertNotNull(encounter.getProvider(B.class).get()); A a = new A(); encounter.getMembersInjector(A.class).injectMembers(a); assertNotNull(a.injector); } }); } }); injector.getInstance(C.class); } public void testMembersInjector() { final MembersInjector<D> membersInjector = new MembersInjector<D>() { @Override public void injectMembers(D instance) { instance.userInjected++; assertEquals(instance.guiceInjected, instance.userInjected); } }; final InjectionListener<D> injectionListener = new InjectionListener<D>() { @Override public void afterInjection(D injectee) { assertTrue(injectee.userInjected > 0); injectee.listenersNotified++; assertEquals(injectee.guiceInjected, injectee.listenersNotified); } }; Injector injector = Guice.createInjector( new AbstractModule() { @Override protected void configure() { bindListener( onlyAbcd, new TypeListener() { @Override @SuppressWarnings("unchecked") public <I> void hear(TypeLiteral<I> type, TypeEncounter<I> encounter) { encounter.register((MembersInjector) membersInjector); encounter.register((InjectionListener) injectionListener); } }); D boundThreeTimes = new D(); bind(D.class).annotatedWith(named("i")).toInstance(boundThreeTimes); bind(D.class).annotatedWith(named("ii")).toInstance(boundThreeTimes); bind(D.class).annotatedWith(named("iii")).toInstance(boundThreeTimes); } }); D boundThreeTimes = injector.getInstance(Key.get(D.class, named("iii"))); boundThreeTimes.assertAllCounts(1); D getInstance = injector.getInstance(D.class); getInstance.assertAllCounts(1); D memberInjection = new D(); injector.injectMembers(memberInjection); memberInjection.assertAllCounts(1); injector.injectMembers(memberInjection); injector.injectMembers(memberInjection); memberInjection.assertAllCounts(3); injector.getMembersInjector(D.class).injectMembers(memberInjection); memberInjection.assertAllCounts(4); } public void testMembersInjectorThrows() { Injector injector = Guice.createInjector( new AbstractModule() { @Override protected void configure() { bindListener( onlyAbcd, new TypeListener() { @Override public <I> void hear(TypeLiteral<I> type, TypeEncounter<I> encounter) { encounter.register(failingMembersInjector); } }); bind(B.class); } }); try { injector.getInstance(A.class); fail(); } catch (ProvisionException e) { assertContains( e.getMessage(), "1) Error injecting " + A.class.getName() + " using awkward.", "Reason: java.lang.ClassCastException: whoops, failure #1"); } // second time through should be a new cause (#2) try { injector.getInstance(A.class); fail(); } catch (ProvisionException e) { assertContains( e.getMessage(), "1) Error injecting " + A.class.getName() + " using awkward.", "Reason: java.lang.ClassCastException: whoops, failure #2"); } // we should get errors for all types, but only on getInstance() Provider<B> bProvider = injector.getProvider(B.class); try { bProvider.get(); fail(); } catch (ProvisionException e) { assertContains( e.getMessage(), "1) Error injecting " + B.class.getName() + " using awkward.", "Reason: java.lang.ClassCastException: whoops, failure #3"); } // non-injected types do not participate assertSame(Stage.DEVELOPMENT, injector.getInstance(Stage.class)); } /** * We had a bug where we weren't notifying of types encountered for member injection when those * types had no members to be injected. Constructed types are always injected because they always * have at least one injection point: the class constructor. */ public void testTypesWithNoInjectableMembersAreNotified() { final AtomicInteger notificationCount = new AtomicInteger(); Guice.createInjector( new AbstractModule() { @Override protected void configure() { bindListener( onlyAbcd, new TypeListener() { @Override public <I> void hear(TypeLiteral<I> type, TypeEncounter<I> encounter) { notificationCount.incrementAndGet(); } }); bind(C.class).toInstance(new C()); } }); assertEquals(1, notificationCount.get()); } public void testEncounterCannotBeUsedAfterHearReturns() { final AtomicReference<TypeEncounter<?>> encounterReference = new AtomicReference<TypeEncounter<?>>(); Guice.createInjector( new AbstractModule() { @Override protected void configure() { bindListener( any(), new TypeListener() { @Override public <I> void hear(TypeLiteral<I> type, TypeEncounter<I> encounter) { encounterReference.set(encounter); } }); bind(C.class); } }); TypeEncounter<?> encounter = encounterReference.get(); try { encounter.register( new InjectionListener<Object>() { @Override public void afterInjection(Object injectee) {} }); fail(); } catch (IllegalStateException expected) { } /*if[AOP]*/ try { encounter.bindInterceptor( any(), new org.aopalliance.intercept.MethodInterceptor() { @Override public Object invoke(org.aopalliance.intercept.MethodInvocation methodInvocation) throws Throwable { return methodInvocation.proceed(); } }); fail(); } catch (IllegalStateException expected) { } /*end[AOP]*/ try { encounter.addError(new Exception()); fail(); } catch (IllegalStateException expected) { } try { encounter.getMembersInjector(A.class); fail(); } catch (IllegalStateException expected) { } try { encounter.getProvider(B.class); fail(); } catch (IllegalStateException expected) { } } public void testAddErrors() { try { Guice.createInjector( new AbstractModule() { @Override protected void configure() { requestInjection(new Object()); bindListener( Matchers.any(), new TypeListener() { @Override public <I> void hear(TypeLiteral<I> type, TypeEncounter<I> encounter) { encounter.addError("There was an error on %s", type); encounter.addError(new IllegalArgumentException("whoops!")); encounter.addError(new Message("And another problem")); encounter.addError(new IllegalStateException()); } }); } }); fail(); } catch (CreationException expected) { assertContains( expected.getMessage(), "1) There was an error on java.lang.Object", "2) An exception was caught and reported. Message: whoops!", "3) And another problem", "4) An exception was caught and reported. Message: null", "4 errors"); } } private static class CountingMembersInjector implements MembersInjector<D> { @Override public void injectMembers(D instance) { ++instance.userInjected; } } private static class CountingInjectionListener implements InjectionListener<D> { @Override public void afterInjection(D injectee) { ++injectee.listenersNotified; } } private static class DuplicatingTypeListener implements TypeListener { int count = 0; @Override @SuppressWarnings({"rawtypes", "unchecked"}) public <I> void hear(TypeLiteral<I> type, TypeEncounter<I> encounter) { ++count; MembersInjector membersInjector = new CountingMembersInjector(); encounter.register(membersInjector); encounter.register(membersInjector); InjectionListener injectionListener = new CountingInjectionListener(); encounter.register(injectionListener); encounter.register(injectionListener); } } public void testDeDuplicateTypeListeners() { final DuplicatingTypeListener typeListener = new DuplicatingTypeListener(); Injector injector = Guice.createInjector( new AbstractModule() { @Override protected void configure() { bindListener(any(), typeListener); bindListener(only(new TypeLiteral<D>() {}), typeListener); } }); D d = injector.getInstance(D.class); d.assertAllCounts(1); assertEquals(1, typeListener.count); } // TODO: recursively accessing a lookup should fail static class A { @Inject Injector injector; @Inject Stage stage; } static class B {} public static class C { public String buzz() { return "buzz"; } public String beep() { return "beep"; } } static class D { int guiceInjected = 0; int userInjected = 0; int listenersNotified = 0; @Inject void guiceInjected() { guiceInjected++; } void assertAllCounts(int expected) { assertEquals(expected, guiceInjected); assertEquals(expected, userInjected); assertEquals(expected, listenersNotified); } } }