/* * 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.CircularDependenciesException; 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 com.shipdream.lib.poke.util.ReflectUtils; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import java.lang.annotation.Annotation; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import javax.inject.Named; import javax.inject.Qualifier; import javax.inject.Singleton; import static java.lang.annotation.RetentionPolicy.RUNTIME; public class TestProviderFinderByRegistry extends BaseTestCases { private Graph graph; private Component component; @Before public void setUp() throws Exception { component = new Component("AppSingleton"); graph = new Graph(); graph.setRootComponent(component); } @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 void registerByClass(Class type, Class impl) throws ProviderConflictException { component.register(new ProviderByClassType(type, impl)); } private void registerByName(Class type, String impl) throws ProviderConflictException, ClassNotFoundException { component.register(new ProviderByClassName(type, impl)); } @Test(expected = ProviderConflictException.class) public void shouldDetectConflictProviderException() throws PokeException { registerByClass(Os.class, iOs.class); registerByClass(Os.class, Android.class); registerByClass(Os.class, Android.class); } private static class Container { @MyInject private Os ios; @Google @MyInject private Os android; @Microsoft @MyInject private Os windows; } @Test public void shouldInjectQualifiedWithDifferentInstances() throws PokeException { Component c = new Component(false); c.register(new ProviderByClassType(Os.class, iOs.class)); c.register(new ProviderByClassType(Os.class, Android.class)); c.register(new ProviderByClassType(Os.class, Windows.class)); graph.setRootComponent(c); Container container = new Container(); graph.inject(container, MyInject.class); Container container2 = new Container(); graph.inject(container2, MyInject.class); Assert.assertEquals(container.ios.getClass(), iOs.class); Assert.assertEquals(container.android.getClass(), Android.class); Assert.assertEquals(container.windows.getClass(), Windows.class); Assert.assertTrue(container.ios != container2.ios); Assert.assertTrue(container.android != container2.android); Assert.assertTrue(container.windows != container2.windows); } @Test public void shouldInjectQualifiedSingletonInstance() throws PokeException { registerByClass(Os.class, iOs.class); registerByClass(Os.class, Android.class); registerByClass(Os.class, Windows.class); Container container = new Container(); graph.inject(container, MyInject.class); Container container2 = new Container(); graph.inject(container2, MyInject.class); Assert.assertEquals(container.ios.getClass(), iOs.class); Assert.assertEquals(container.android.getClass(), Android.class); Assert.assertEquals(container.windows.getClass(), Windows.class); Assert.assertTrue(container.ios == container2.ios); Assert.assertTrue(container.android == container2.android); Assert.assertTrue(container.windows == container2.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 componentProvidesQualifierShouldOverrideImplClassQualifier() throws PokeException { graph.setRootComponent(new Component(false).register(new ContainerModule())); Container container = new Container(); graph.inject(container, MyInject.class); Container container2 = new Container(); graph.inject(container2, MyInject.class); Assert.assertEquals(container.ios.getClass(), iOs.class); Assert.assertEquals(container.android.getClass(), Windows.class); Assert.assertEquals(container.windows.getClass(), Android.class); Assert.assertTrue(container.ios != container2.ios); Assert.assertTrue(container.android != container2.android); Assert.assertTrue(container.windows != container2.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; } registerByClass(Book.class, BookA.class); registerByClass(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; } registerByClass(Book.class, BookA.class); registerByClass(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 ProviderConflictException, ProvideException, CircularDependenciesException, ProviderMissingException { class Library { @MyInject @Named("C") private Book b1; } registerByClass(Book.class, BookA.class); registerByClass(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; } registerByClass(Book.class, BookA.class); registerByClass(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; } registerByClass(Food.class, Rice.class); registerByClass(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); } @Named static class Noodle implements Food { } static class Bread implements Food { } @Named static class Chicken implements Food { } static class Beef implements Food { } @Test public void overridingClassNameRegisteringShouldWorkAsExpected() throws PokeException, ClassNotFoundException { class Basket { @MyInject @Named private Food r; @MyInject private Food w; } Basket basket = new Basket(); registerByName(Food.class, Rice.class.getName()); registerByName(Food.class, Wheat.class.getName()); graph.inject(basket, MyInject.class); Assert.assertEquals(basket.r.getClass(), Rice.class); Assert.assertEquals(basket.w.getClass(), Wheat.class); boolean conflicted = false; try { registerByName(Food.class, Noodle.class.getName()); } catch (ProviderConflictException e) { conflicted = true; } Assert.assertTrue(conflicted); conflicted = false; try { registerByName(Food.class, Bread.class.getName()); } catch (ProviderConflictException e) { conflicted = true; } Assert.assertTrue(conflicted); component.unregister(new ProviderByClassName(Food.class, Chicken.class.getName())); component.unregister(new ProviderByClassName(Food.class, Bread.class.getName())); basket = new Basket(); boolean shouldCatchProviderMissingException = false; try { graph.inject(basket, MyInject.class); } catch (ProviderMissingException e) { shouldCatchProviderMissingException = true; } Assert.assertTrue(shouldCatchProviderMissingException); } static class BadModule { @Provides void provideNothing() { return; } } @Test public void should_throw_exception_when_there_is_void_function_in_component() throws PokeException { BadModule badComponent = new BadModule(); try { component.register(badComponent); } catch (ProvideException e) { Assert.assertTrue(e.getMessage().contains("must not return void")); return; } Assert.fail("Should raise ProvideException for provider returning void"); } @Qualifier @Retention(RUNTIME) @interface Qualifier1 {} @Qualifier @Retention(RUNTIME) @interface Qualifier2 {} static class DuplicateModule { @Provides @Qualifier1 @Qualifier2 String provideText() { return "123"; } } @Test public void should_throw_exception_when_provider_has_more_than_one_qualifier() throws PokeException { DuplicateModule module = new DuplicateModule(); try { component.register(module); } catch (ProvideException e) { Assert.assertTrue(e.getMessage().contains("Only one Qualifier")); return; } Assert.fail("Should raise ProvideException for provider with multiple qualifier"); } @Test public void should_throw_exception_if_provider_returns_null() throws PokeException { Provider<String> provider = new Provider(String.class) { @Override protected String createInstance() throws ProvideException { return null; } }; component.register(provider); class Container { @MyInject @Singleton private String name; } Container container = new Container(); try { graph.inject(container, MyInject.class); } catch (ProvideException e) { Assert.assertTrue(e.getMessage().contains("should not provide NULL as instance")); return; } Assert.fail("Should raise ProvideException for provider returns null"); } @Test public void overridingClassRegisteringShouldWorkAsExpected() throws PokeException { class Basket { @MyInject @Named private Food r; @MyInject private Food w; } Basket basket = new Basket(); registerByClass(Food.class, Rice.class); registerByClass(Food.class, Wheat.class); graph.inject(basket, MyInject.class); Assert.assertEquals(basket.r.getClass(), Rice.class); Assert.assertEquals(basket.w.getClass(), Wheat.class); boolean conflicted = false; try { registerByClass(Food.class, Noodle.class); } catch (ProviderConflictException e) { conflicted = true; } Assert.assertTrue(conflicted); conflicted = false; try { registerByClass(Food.class, Bread.class); } catch (ProviderConflictException e) { conflicted = true; } Assert.assertTrue(conflicted); component.unregister(new ProviderByClassType(Food.class, Noodle.class)); component.unregister(new ProviderByClassType(Food.class, Bread.class)); basket = new Basket(); boolean shouldCatchProviderMissingException = false; try { graph.inject(basket, MyInject.class); } catch (ProviderMissingException e) { shouldCatchProviderMissingException = true; } Assert.assertTrue(shouldCatchProviderMissingException); } @Test public void overridingProviderRegisteringShouldWorkAsExpected() throws ProviderConflictException, ProvideException, CircularDependenciesException, ProviderMissingException { Provider<Food> providerRice = new Provider<Food>(Food.class) { @Override protected Food createInstance() throws ProvideException { return new Rice(); } @Override public Annotation getQualifier() { return ReflectUtils.findFirstQualifierInAnnotations(Rice.class); } }; Provider<Food> providerWheat = new Provider<Food>(Food.class) { @Override protected Food createInstance() throws ProvideException { return new Wheat(); } @Override public Annotation getQualifier() { return ReflectUtils.findFirstQualifierInAnnotations(Wheat.class); } }; Provider<Food> providerNoodle = new Provider<Food>(Food.class) { @Override protected Food createInstance() throws ProvideException { return new Noodle(); } @Override public Annotation getQualifier() { return ReflectUtils.findFirstQualifierInAnnotations(Noodle.class); } }; Provider<Food> providerBread = new Provider<Food>(Food.class) { @Override protected Food createInstance() throws ProvideException { return new Bread(); } @Override public Annotation getQualifier() { return ReflectUtils.findFirstQualifierInAnnotations(Bread.class); } }; class Basket { @MyInject @Named private Food r; @MyInject private Food w; } Basket basket = new Basket(); component.register(providerRice); component.register(providerWheat); graph.inject(basket, MyInject.class); Assert.assertEquals(basket.r.getClass(), Rice.class); Assert.assertEquals(basket.w.getClass(), Wheat.class); boolean conflicted = false; try { component.register(providerNoodle); } catch (ProviderConflictException e) { conflicted = true; } Assert.assertTrue(conflicted); conflicted = false; try { component.register(providerBread); } catch (ProviderConflictException e) { conflicted = true; } Assert.assertTrue(conflicted); component.unregister(providerNoodle); component.unregister(providerWheat); basket = new Basket(); boolean shouldCatchProviderMissingException = false; try { graph.inject(basket, MyInject.class); } catch (ProviderMissingException e) { shouldCatchProviderMissingException = true; } Assert.assertTrue(shouldCatchProviderMissingException); } static class Apple implements Food{} static class Orange implements Food{} static class Banana implements Food{} static class FoodModuleA { @Provides @Named public Food provideApple() { return new Apple(); } @Provides public Food provideOrange() { return new Orange(); } } static class FoodModuleB { @Provides public Food provideApple() { return new Apple(); } @Provides @Named public Food provideOrange() { return new Orange(); } } static class FoodModuleC { @Provides @Named public Food provideApple() { return new Apple(); } @Provides public Food provideBanana() { return new Banana(); } } @Test public void overridingComponentRegisteringShouldWorkAsExpected() throws PokeException { class Basket { @MyInject @Named private Food r; @MyInject private Food w; } Basket basket = new Basket(); FoodModuleA foodModuleA = new FoodModuleA(); FoodModuleB foodModuleB = new FoodModuleB(); FoodModuleC foodModuleC = new FoodModuleC(); component.register(foodModuleA); graph.inject(basket, MyInject.class); Assert.assertEquals(basket.r.getClass(), Apple.class); Assert.assertEquals(basket.w.getClass(), Orange.class); boolean conflicted = false; try { component.register(new FoodModuleB()); } catch (ProviderConflictException e) { conflicted = true; } Assert.assertTrue(conflicted); component.unregister(foodModuleA); basket = new Basket(); boolean shouldCatchProviderMissingException = false; try { graph.inject(basket, MyInject.class); } catch (ProviderMissingException e) { shouldCatchProviderMissingException = true; } Assert.assertTrue(shouldCatchProviderMissingException); } @Test public void scopeCacheShouldRemoveUnregisteredBindings() throws ProviderConflictException, ProvideException, CircularDependenciesException, ProviderMissingException { class Basket { @MyInject @Named private Food r; @MyInject private Food w; } final Banana banana = new Banana(); final Orange orange = new Orange(); Provider<Food> namedProviderBanana = new Provider<Food>(Food.class) { @Override protected Food createInstance() throws ProvideException { return banana; } @Override public Annotation getQualifier() { return ReflectUtils.findFirstQualifierInAnnotations(Noodle.class); } }; Provider<Food> unnammedProviderOrange = new Provider<Food>(Food.class) { @Override protected Food createInstance() throws ProvideException { return orange; } @Override public Annotation getQualifier() { return null; } }; Basket basket = new Basket(); component.register(namedProviderBanana); component.register(unnammedProviderOrange); graph.inject(basket, MyInject.class); Assert.assertTrue(basket.r == banana); Assert.assertTrue(basket.w == orange); Assert.assertTrue(findCacheInstance(component.scopeCache, namedProviderBanana) == banana); Assert.assertTrue(findCacheInstance(component.scopeCache, unnammedProviderOrange) == orange); } @SuppressWarnings("unchecked") private <T> T findCacheInstance(ScopeCache scopeCache, Provider<T> provider) { T instance = scopeCache.findInstance(provider.type(), provider.getQualifier()); if(instance != null) { return instance; } return null; } }