/* * Copyright 2016 Kejun Xia * * 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.shipdream.lib.poke; import com.shipdream.lib.poke.exception.PokeException; import com.shipdream.lib.poke.exception.ProvideException; import com.shipdream.lib.poke.exception.ProviderConflictException; import com.shipdream.lib.poke.exception.ProviderMissingException; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import javax.inject.Named; import javax.inject.Qualifier; import static java.lang.annotation.RetentionPolicy.RUNTIME; import static org.mockito.Matchers.any; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; public class TestInjectionWithQualifier extends BaseTestCases { @Qualifier @Documented @Retention(RUNTIME) @interface Google { } @Qualifier @Documented @Retention(RUNTIME) @interface Microsoft { } interface Os { } static class iOs implements Os { } @Google static class Android implements Os { } @Microsoft static class Windows implements Os { } private ScopeCache scopeCache; private Component component; private Graph graph; @Before public void setUp() throws Exception { scopeCache = new ScopeCache(); component = new Component("AppSingleton"); graph = new Graph(); graph.setRootComponent(component); } @Test(expected = ProviderConflictException.class) public void shouldDetectConflictProviderException() throws PokeException { component.register(new ProviderByClassType(Os.class, iOs.class)); component.register(new ProviderByClassType(Os.class, Android.class)); component.register(new ProviderByClassType(Os.class, Android.class)); } private static class Device { @MyInject private Os ios; @Google @MyInject private Os android; @Microsoft @MyInject private Os windows; } @Test public void should_use_cached_instance_if_inject_instance_is_referenced_more_then_once() throws Exception { component.register(new ProviderByClassType(Os.class, iOs.class)); component.register(new ProviderByClassType(Os.class, Android.class)); component.register(new ProviderByClassType(Os.class, Windows.class)); //Retain = 0 final Device device = new Device(); graph.inject(device, MyInject.class); //Retain = 1 final Device device2 = new Device(); graph.inject(device2, MyInject.class); //Retain = 2 graph.use(Os.class, null, MyInject.class, new Consumer<Os>() { @Override public void consume(Os instance) { Assert.assertTrue(device.ios == instance); Assert.assertTrue(device2.ios == instance); } }); graph.release(device, MyInject.class); //Retain = 1 graph.use(Os.class, null, MyInject.class, new Consumer<Os>() { @Override public void consume(Os instance) { Assert.assertTrue(device.ios == instance); Assert.assertTrue(device2.ios == instance); } }); graph.release(device2, MyInject.class); //Retain = 0 graph.use(Os.class, null, MyInject.class, new Consumer<Os>() { @Override public void consume(Os instance) { Assert.assertTrue(device.ios != null); Assert.assertTrue(device.ios != instance); } }); } @Test public void should_retain_instance_in_use_method_until_exit() throws PokeException { ScopeCache scopeCache = new ScopeCache(); component.register(new ProviderByClassType(Os.class, iOs.class)); component.register(new ProviderByClassType(Os.class, Android.class)); component.register(new ProviderByClassType(Os.class, Windows.class)); final Device device = new Device(); graph.use(Os.class, null, MyInject.class, new Consumer<Os>() { @Override public void consume(Os instance) { try { graph.inject(device, MyInject.class); } catch (PokeException e) { throw new RuntimeException(e); } Assert.assertTrue(device.ios != null); Assert.assertTrue(device.ios == instance); } }); graph.use(Os.class, null, MyInject.class, new Consumer<Os>() { @Override public void consume(Os instance) { Assert.assertTrue(device.ios == instance); } }); graph.release(device, MyInject.class); graph.use(Os.class, null, MyInject.class, new Consumer<Os>() { @Override public void consume(Os instance) { //New instance has created Assert.assertTrue(device.ios != instance); } }); } @Test public void should_retain_instance_in_use_method_until_exit_without_qualifier() throws PokeException { ScopeCache scopeCache = new ScopeCache(); component.register(new ProviderByClassType(Os.class, iOs.class)); component.register(new ProviderByClassType(Os.class, Android.class)); component.register(new ProviderByClassType(Os.class, Windows.class)); final Device device = new Device(); graph.use(Os.class, MyInject.class, new Consumer<Os>() { @Override public void consume(Os instance) { try { graph.inject(device, MyInject.class); } catch (PokeException e) { throw new RuntimeException(e); } Assert.assertTrue(device.ios != null); Assert.assertTrue(device.ios == instance); } }); graph.use(Os.class, MyInject.class, new Consumer<Os>() { @Override public void consume(Os instance) { Assert.assertTrue(device.ios == instance); } }); graph.release(device, MyInject.class); graph.use(Os.class, MyInject.class, new Consumer<Os>() { @Override public void consume(Os instance) { //New instance has created Assert.assertTrue(device.ios != instance); } }); } @Test public void should_be_able_to_use_instance_injected_with_qualifier() throws PokeException { component.register(new ProviderByClassType(Os.class, iOs.class)); component.register(new ProviderByClassType(Os.class, Android.class)); component.register(new ProviderByClassType(Os.class, Windows.class)); final Device device = new Device(); @Google class GoogleHolder{} graph.use(Os.class, GoogleHolder.class.getAnnotation(Google.class), MyInject.class, new Consumer<Os>() { @Override public void consume(Os instance) { try { graph.inject(device, MyInject.class); } catch (PokeException e) { throw new RuntimeException(e); } Assert.assertTrue(device.android == instance); } }); } @Test public void use_method_should_notify_injection_and_freed() throws PokeException { final Provider<Os> provider = new Provider<Os>(Os.class) { @Override protected Os createInstance() throws ProvideException { return new Android(); } }; component.register(provider); class Phone { @MyInject private Os os; } final int[] onCreatedCalled = {0}; final Object[] injected = new Object[1]; final Provider.ReferencedListener<Os> injectListener = new Provider.ReferencedListener() { @Override public void onReferenced(Provider provider, Object instance) { onCreatedCalled[0]++; injected[0] = instance; provider.unregisterOnReferencedListener(this); } }; provider.registerOnReferencedListener(injectListener); final Provider.DereferenceListener osDereferenceListener = mock(Provider.DereferenceListener.class); graph.registerDereferencedListener(new Provider.DereferenceListener() { @Override public <T> void onDereferenced(Provider<T> provider, T instance) { if (provider.type() == Os.class && provider.getReferenceCount() == 0) { osDereferenceListener.onDereferenced(provider, instance); } } }); final Phone phone = new Phone(); graph.inject(phone, MyInject.class); graph.use(Os.class, MyInject.class, new Consumer<Os>() { @Override public void consume(Os instance) { Assert.assertTrue(injected[0] == phone.os); Assert.assertEquals(1, onCreatedCalled[0]); Assert.assertTrue(phone.os == instance); verify(osDereferenceListener, times(0)).onDereferenced(eq(provider), any(Os.class)); try { graph.release(phone, MyInject.class); } catch (ProviderMissingException e) { throw new RuntimeException(e); } verify(osDereferenceListener, times(0)).onDereferenced(eq(provider), any(Os.class)); } }); Assert.assertTrue(injected[0] == phone.os); Assert.assertEquals(1, onCreatedCalled[0]); verify(osDereferenceListener, times(1)).onDereferenced(eq(provider), any(Os.class)); //OnInject listener has been unregistered so the count should not increment graph.inject(phone, MyInject.class); Assert.assertEquals(1, onCreatedCalled[0]); } interface Connector{ } static class SamSungOs implements Os { @MyInject Connector connector; } static class TypeC implements Connector{ } @Test public void use_method_should_inject_fields_recursively() throws PokeException { ScopeCache scopeCache = new ScopeCache(); component.register(new ProviderByClassType(Os.class, SamSungOs.class)); component.register(new ProviderByClassType(Connector.class, TypeC.class)); graph.use(Os.class, MyInject.class, new Consumer<Os>() { @Override public void consume(Os instance) { Assert.assertNotNull(((SamSungOs) instance).connector); } }); } @Test public void use_method_should_release_fields_recursively() throws PokeException { component.register(new ProviderByClassType(Os.class, SamSungOs.class)); component.register(new ProviderByClassType(Connector.class, TypeC.class)); class Phone { @MyInject private Os os; } final Phone phone = new Phone(); class ConnectorHolder { Connector connector; } final ConnectorHolder connectorHolder = new ConnectorHolder(); graph.use(Os.class, MyInject.class, new Consumer<Os>() { @Override public void consume(Os instance) { Assert.assertNotNull(((SamSungOs) instance).connector); connectorHolder.connector = ((SamSungOs) instance).connector; } }); graph.inject(phone, MyInject.class); Assert.assertTrue(connectorHolder.connector != ((SamSungOs) phone.os).connector); } @Test public void inject_in_use_method_should_retain_instances() throws PokeException { component.register(new ProviderByClassType(Os.class, SamSungOs.class)); component.register(new ProviderByClassType(Connector.class, TypeC.class)); class Phone { @MyInject private Os os; } final Phone phone = new Phone(); class ConnectorHolder { Connector connector; } final ConnectorHolder connectorHolder = new ConnectorHolder(); graph.use(Os.class, MyInject.class, new Consumer<Os>() { @Override public void consume(Os instance) { Assert.assertNotNull(((SamSungOs) instance).connector); try { graph.inject(phone, MyInject.class); } catch (PokeException e) { throw new RuntimeException(e); } connectorHolder.connector = ((SamSungOs) instance).connector; } }); Assert.assertTrue(connectorHolder.connector == ((SamSungOs) phone.os).connector); } @Test public void shouldInjectQualifiedWithDifferentInstances() throws PokeException { Graph g = new Graph(); Component c; c = new Component(false); g.setRootComponent(c); c.register(new ProviderByClassType(Os.class, iOs.class)); c.register(new ProviderByClassType(Os.class, Android.class)); c.register(new ProviderByClassType(Os.class, Windows.class)); Device device = new Device(); g.inject(device, MyInject.class); Device device2 = new Device(); g.inject(device2, MyInject.class); Assert.assertEquals(device.ios.getClass(), iOs.class); Assert.assertEquals(device.android.getClass(), Android.class); Assert.assertEquals(device.windows.getClass(), Windows.class); Assert.assertTrue(device.ios != device2.ios); Assert.assertTrue(device.android != device2.android); Assert.assertTrue(device.windows != device2.windows); } @Test public void shouldInjectQualifiedSingletonInstance() throws PokeException { component.register(new ProviderByClassType(Os.class, iOs.class)); component.register(new ProviderByClassType(Os.class, Android.class)); component.register(new ProviderByClassType(Os.class, Windows.class)); Device device = new Device(); graph.inject(device, MyInject.class); Device device2 = new Device(); graph.inject(device2, MyInject.class); Assert.assertEquals(device.ios.getClass(), iOs.class); Assert.assertEquals(device.android.getClass(), Android.class); Assert.assertEquals(device.windows.getClass(), Windows.class); Assert.assertTrue(device.ios == device2.ios); Assert.assertTrue(device.android == device2.android); Assert.assertTrue(device.windows == device2.windows); } static class ContainerModule { @Provides public Os providesOs() { return new iOs(); } //Mismatch os intentionally to test if provides qualifier overrides qualifier of impl class @Microsoft @Provides public Os providesOs1() { return new Android(); } //Mismatch os intentionally to test if provides qualifier overrides qualifier of impl class @Google @Provides public Os providesOs2() { return new Windows(); } } @Test public void unscoped_commponent_should_always_create_new_instances() throws PokeException { Component unscopedComponenent = new Component(false); unscopedComponenent.register(new ContainerModule()); graph.setRootComponent(unscopedComponenent); Device device = new Device(); graph.inject(device, MyInject.class); Device device2 = new Device(); graph.inject(device2, MyInject.class); Assert.assertEquals(device.ios.getClass(), iOs.class); Assert.assertEquals(device.android.getClass(), Windows.class); Assert.assertEquals(device.windows.getClass(), Android.class); Assert.assertTrue(device.ios != device2.ios); Assert.assertTrue(device.android != device2.android); Assert.assertTrue(device.windows != device2.windows); } interface Book { } @Named("A") static class BookA implements Book { } @Named("B") static class BookB implements Book { } @Test public void namedQualifierShouldBeRecognized() throws PokeException { class Library { @MyInject @Named("A") private Book b1; @MyInject @Named("B") private Book b2; } component.register(new ProviderByClassType(Book.class, BookA.class)); component.register(new ProviderByClassType(Book.class, BookB.class)); Library library = new Library(); graph.inject(library, MyInject.class); Assert.assertEquals(library.b1.getClass(), BookA.class); Assert.assertEquals(library.b2.getClass(), BookB.class); } @Test public void incorrectNamedQualifierShouldBeRecognized() throws PokeException { class Library { @MyInject @Named("B") private Book b1; } component.register(new ProviderByClassType(Book.class, BookA.class)); component.register(new ProviderByClassType(Book.class, BookB.class)); Library library = new Library(); graph.inject(library, MyInject.class); Assert.assertFalse(library.b1.getClass() == BookA.class); } @Test(expected = ProviderMissingException.class) public void badNamedQualifierShouldBeTreatedAsMissing() throws PokeException { class Library { @MyInject @Named("C") private Book b1; } component.register(new ProviderByClassType(Book.class, BookA.class)); component.register(new ProviderByClassType(Book.class, BookB.class)); Library library = new Library(); graph.inject(library, MyInject.class); Assert.assertEquals(library.b1.getClass(), BookA.class); } @Test(expected = ProviderMissingException.class) public void badEmptyNamedQualifierShouldBeTreatedAsMissing() throws PokeException { class Library { //Empty named qualifier is allowed but will be different with any non empty string //Named qualifier @MyInject @Named private Book b1; } component.register(new ProviderByClassType(Book.class, BookA.class)); component.register(new ProviderByClassType(Book.class, BookB.class)); Library library = new Library(); graph.inject(library, MyInject.class); Assert.assertEquals(library.b1.getClass(), BookA.class); } interface Food { } @Named static class Rice implements Food { } static class Wheat implements Food { } @Test public void emptyNamedQualifierShouldBeTreatedAsNormalQualifier() throws PokeException { class Basket { @MyInject @Named private Food r; @MyInject private Food w; } component.register(new ProviderByClassType(Food.class, Rice.class)); component.register(new ProviderByClassType(Food.class, Wheat.class)); Basket basket = new Basket(); graph.inject(basket, MyInject.class); Assert.assertEquals(basket.r.getClass(), Rice.class); Assert.assertEquals(basket.w.getClass(), Wheat.class); } }