/* * Copyright (C) 2008 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.name.Names.named; import com.google.common.collect.ImmutableSet; import com.google.inject.name.Named; import com.google.inject.name.Names; import com.google.inject.spi.Dependency; import com.google.inject.spi.ExposedBinding; import com.google.inject.spi.PrivateElements; import com.google.inject.util.Types; import java.util.ArrayList; import java.util.Collection; import java.util.List; import junit.framework.TestCase; /** @author jessewilson@google.com (Jesse Wilson) */ public class PrivateModuleTest extends TestCase { public void testBasicUsage() { Injector injector = Guice.createInjector( new AbstractModule() { @Override protected void configure() { bind(String.class).annotatedWith(named("a")).toInstance("public"); install( new PrivateModule() { @Override public void configure() { bind(String.class).annotatedWith(named("b")).toInstance("i"); bind(AB.class).annotatedWith(named("one")).to(AB.class); expose(AB.class).annotatedWith(named("one")); } }); install( new PrivateModule() { @Override public void configure() { bind(String.class).annotatedWith(named("b")).toInstance("ii"); bind(AB.class).annotatedWith(named("two")).to(AB.class); expose(AB.class).annotatedWith(named("two")); } }); } }); AB ab1 = injector.getInstance(Key.get(AB.class, named("one"))); assertEquals("public", ab1.a); assertEquals("i", ab1.b); AB ab2 = injector.getInstance(Key.get(AB.class, named("two"))); assertEquals("public", ab2.a); assertEquals("ii", ab2.b); } public void testWithoutPrivateModules() { Injector injector = Guice.createInjector( new AbstractModule() { @Override protected void configure() { PrivateBinder bindA = binder().newPrivateBinder(); bindA.bind(String.class).annotatedWith(named("a")).toInstance("i"); bindA.expose(String.class).annotatedWith(named("a")); bindA.bind(String.class).annotatedWith(named("c")).toInstance("private to A"); PrivateBinder bindB = binder().newPrivateBinder(); bindB.bind(String.class).annotatedWith(named("b")).toInstance("ii"); bindB.expose(String.class).annotatedWith(named("b")); bindB.bind(String.class).annotatedWith(named("c")).toInstance("private to B"); } }); assertEquals("i", injector.getInstance(Key.get(String.class, named("a")))); assertEquals("ii", injector.getInstance(Key.get(String.class, named("b")))); } public void testMisplacedExposedAnnotation() { try { Guice.createInjector( new AbstractModule() { @Override protected void configure() {} @Provides @Exposed String provideString() { return "i"; } }); fail(); } catch (CreationException expected) { assertContains( expected.getMessage(), "Cannot expose java.lang.String on a standard binder. ", "Exposed bindings are only applicable to private binders.", " at " + PrivateModuleTest.class.getName(), "provideString(PrivateModuleTest.java:"); } } public void testMisplacedExposeStatement() { try { Guice.createInjector( new AbstractModule() { @Override protected void configure() { ((PrivateBinder) binder()).expose(String.class).annotatedWith(named("a")); } }); fail(); } catch (CreationException expected) { assertContains( expected.getMessage(), "Cannot expose java.lang.String on a standard binder. ", "Exposed bindings are only applicable to private binders.", " at " + PrivateModuleTest.class.getName(), getDeclaringSourcePart(getClass())); } } public void testPrivateModulesAndProvidesMethods() { Injector injector = Guice.createInjector( new AbstractModule() { @Override protected void configure() { install( new PrivateModule() { @Override public void configure() { expose(String.class).annotatedWith(named("a")); } @Provides @Named("a") String providePublicA() { return "i"; } @Provides @Named("b") String providePrivateB() { return "private"; } }); install( new PrivateModule() { @Override public void configure() {} @Provides @Named("c") String providePrivateC() { return "private"; } @Provides @Exposed @Named("d") String providePublicD() { return "ii"; } }); } }); assertEquals("i", injector.getInstance(Key.get(String.class, named("a")))); try { injector.getInstance(Key.get(String.class, named("b"))); fail(); } catch (ConfigurationException expected) { } try { injector.getInstance(Key.get(String.class, named("c"))); fail(); } catch (ConfigurationException expected) { } assertEquals("ii", injector.getInstance(Key.get(String.class, named("d")))); } public void testCannotBindAKeyExportedByASibling() { try { Guice.createInjector( new AbstractModule() { @Override protected void configure() { install( new PrivateModule() { @Override public void configure() { bind(String.class).toInstance("public"); expose(String.class); } }); install( new PrivateModule() { @Override public void configure() { bind(String.class).toInstance("private"); } }); } }); fail(); } catch (CreationException expected) { assertContains( expected.getMessage(), "A binding to java.lang.String was already configured at ", getClass().getName(), getDeclaringSourcePart(getClass()), " at " + getClass().getName(), getDeclaringSourcePart(getClass())); } } public void testExposeButNoBind() { try { Guice.createInjector( new AbstractModule() { @Override protected void configure() { bind(String.class).annotatedWith(named("a")).toInstance("a"); bind(String.class).annotatedWith(named("b")).toInstance("b"); install( new PrivateModule() { @Override public void configure() { expose(AB.class); } }); } }); fail("AB was exposed but not bound"); } catch (CreationException expected) { assertContains( expected.getMessage(), "Could not expose() " + AB.class.getName() + ", it must be explicitly bound", getDeclaringSourcePart(getClass())); } } /** * Ensure that when we've got errors in different private modules, Guice presents all errors in a * unified message. */ public void testMessagesFromPrivateModulesAreNicelyIntegrated() { try { Guice.createInjector( new PrivateModule() { @Override public void configure() { bind(C.class); } }, new PrivateModule() { @Override public void configure() { bind(AB.class); } }); fail(); } catch (CreationException expected) { assertContains( expected.getMessage(), "1) No implementation for " + C.class.getName() + " was bound.", "at " + getClass().getName(), getDeclaringSourcePart(getClass()), "2) No implementation for " + String.class.getName(), "Named(value=a) was bound.", "for field at " + AB.class.getName() + ".a(PrivateModuleTest.java:", "3) No implementation for " + String.class.getName(), "Named(value=b) was bound.", "for field at " + AB.class.getName() + ".b(PrivateModuleTest.java:", "3 errors"); } } public void testNestedPrivateInjectors() { Injector injector = Guice.createInjector( new PrivateModule() { @Override public void configure() { expose(String.class); install( new PrivateModule() { @Override public void configure() { bind(String.class).toInstance("nested"); expose(String.class); } }); } }); assertEquals("nested", injector.getInstance(String.class)); } public void testInstallingRegularModulesFromPrivateModules() { Injector injector = Guice.createInjector( new PrivateModule() { @Override public void configure() { expose(String.class); install( new AbstractModule() { @Override protected void configure() { bind(String.class).toInstance("nested"); } }); } }); assertEquals("nested", injector.getInstance(String.class)); } public void testNestedPrivateModulesWithSomeKeysUnexposed() { Injector injector = Guice.createInjector( new PrivateModule() { @Override public void configure() { bind(String.class) .annotatedWith(named("bound outer, exposed outer")) .toInstance("boeo"); expose(String.class).annotatedWith(named("bound outer, exposed outer")); bind(String.class) .annotatedWith(named("bound outer, exposed none")) .toInstance("boen"); expose(String.class).annotatedWith(named("bound inner, exposed both")); install( new PrivateModule() { @Override public void configure() { bind(String.class) .annotatedWith(named("bound inner, exposed both")) .toInstance("bieb"); expose(String.class).annotatedWith(named("bound inner, exposed both")); bind(String.class) .annotatedWith(named("bound inner, exposed none")) .toInstance("bien"); } }); } }); assertEquals( "boeo", injector.getInstance(Key.get(String.class, named("bound outer, exposed outer")))); assertEquals( "bieb", injector.getInstance(Key.get(String.class, named("bound inner, exposed both")))); try { injector.getInstance(Key.get(String.class, named("bound outer, exposed none"))); fail(); } catch (ConfigurationException expected) { } try { injector.getInstance(Key.get(String.class, named("bound inner, exposed none"))); fail(); } catch (ConfigurationException expected) { } } public void testDependenciesBetweenPrivateAndPublic() { Injector injector = Guice.createInjector( new PrivateModule() { @Override protected void configure() {} @Provides @Exposed @Named("a") String provideA() { return "A"; } @Provides @Exposed @Named("abc") String provideAbc(@Named("ab") String ab) { return ab + "C"; } }, new AbstractModule() { @Override protected void configure() {} @Provides @Named("ab") String provideAb(@Named("a") String a) { return a + "B"; } @Provides @Named("abcd") String provideAbcd(@Named("abc") String abc) { return abc + "D"; } }); assertEquals("ABCD", injector.getInstance(Key.get(String.class, named("abcd")))); } public void testDependenciesBetweenPrivateAndPublicWithPublicEagerSingleton() { Injector injector = Guice.createInjector( new PrivateModule() { @Override protected void configure() {} @Provides @Exposed @Named("a") String provideA() { return "A"; } @Provides @Exposed @Named("abc") String provideAbc(@Named("ab") String ab) { return ab + "C"; } }, new AbstractModule() { @Override protected void configure() { bind(String.class) .annotatedWith(named("abcde")) .toProvider( new Provider<String>() { @Inject @Named("abcd") String abcd; @Override public String get() { return abcd + "E"; } }) .asEagerSingleton(); } @Provides @Named("ab") String provideAb(@Named("a") String a) { return a + "B"; } @Provides @Named("abcd") String provideAbcd(@Named("abc") String abc) { return abc + "D"; } }); assertEquals("ABCDE", injector.getInstance(Key.get(String.class, named("abcde")))); } public void testDependenciesBetweenPrivateAndPublicWithPrivateEagerSingleton() { Injector injector = Guice.createInjector( new AbstractModule() { @Override protected void configure() {} @Provides @Named("ab") String provideAb(@Named("a") String a) { return a + "B"; } @Provides @Named("abcd") String provideAbcd(@Named("abc") String abc) { return abc + "D"; } }, new PrivateModule() { @Override protected void configure() { bind(String.class) .annotatedWith(named("abcde")) .toProvider( new Provider<String>() { @Inject @Named("abcd") String abcd; @Override public String get() { return abcd + "E"; } }) .asEagerSingleton(); expose(String.class).annotatedWith(named("abcde")); } @Provides @Exposed @Named("a") String provideA() { return "A"; } @Provides @Exposed @Named("abc") String provideAbc(@Named("ab") String ab) { return ab + "C"; } }); assertEquals("ABCDE", injector.getInstance(Key.get(String.class, named("abcde")))); } static class AB { @Inject @Named("a") String a; @Inject @Named("b") String b; } interface C {} public void testSpiAccess() { Injector injector = Guice.createInjector( new PrivateModule() { @Override public void configure() { bind(String.class).annotatedWith(named("a")).toInstance("private"); bind(String.class).annotatedWith(named("b")).toInstance("exposed"); expose(String.class).annotatedWith(named("b")); } }); ExposedBinding<?> binding = (ExposedBinding<?>) injector.getBinding(Key.get(String.class, Names.named("b"))); assertEquals( ImmutableSet.<Dependency<?>>of(Dependency.get(Key.get(Injector.class))), binding.getDependencies()); PrivateElements privateElements = binding.getPrivateElements(); assertEquals( ImmutableSet.<Key<?>>of(Key.get(String.class, named("b"))), privateElements.getExposedKeys()); assertContains( privateElements.getExposedSource(Key.get(String.class, named("b"))).toString(), PrivateModuleTest.class.getName(), getDeclaringSourcePart(getClass())); Injector privateInjector = privateElements.getInjector(); assertEquals("private", privateInjector.getInstance(Key.get(String.class, Names.named("a")))); } public void testParentBindsSomethingInPrivate() { try { Guice.createInjector(new FailingModule()); fail(); } catch (CreationException expected) { assertEquals(1, expected.getErrorMessages().size()); assertContains( expected.toString(), "Unable to create binding for java.util.List.", "It was already configured on one or more child injectors or private modules", "bound at " + FailingPrivateModule.class.getName() + ".configure(", asModuleChain(FailingModule.class, ManyPrivateModules.class, FailingPrivateModule.class), "bound at " + SecondFailingPrivateModule.class.getName() + ".configure(", asModuleChain( FailingModule.class, ManyPrivateModules.class, SecondFailingPrivateModule.class), "If it was in a PrivateModule, did you forget to expose the binding?", "at " + FailingModule.class.getName() + ".configure("); } } public void testParentBindingToPrivateLinkedJitBinding() { Injector injector = Guice.createInjector(new ManyPrivateModules()); try { injector.getBinding(Key.get(Types.providerOf(List.class))); fail(); } catch (ConfigurationException expected) { assertEquals(1, expected.getErrorMessages().size()); assertContains( expected.toString(), "Unable to create binding for com.google.inject.Provider<java.util.List>.", "It was already configured on one or more child injectors or private modules", "bound at " + FailingPrivateModule.class.getName() + ".configure(", asModuleChain(ManyPrivateModules.class, FailingPrivateModule.class), "bound at " + SecondFailingPrivateModule.class.getName() + ".configure(", asModuleChain(ManyPrivateModules.class, SecondFailingPrivateModule.class), "If it was in a PrivateModule, did you forget to expose the binding?", "while locating com.google.inject.Provider<java.util.List>"); } } public void testParentBindingToPrivateJitBinding() { Injector injector = Guice.createInjector(new ManyPrivateModules()); try { injector.getBinding(PrivateFoo.class); fail(); } catch (ConfigurationException expected) { assertEquals(1, expected.getErrorMessages().size()); assertContains( expected.toString(), "Unable to create binding for " + PrivateFoo.class.getName(), "It was already configured on one or more child injectors or private modules", "(bound by a just-in-time binding)", "If it was in a PrivateModule, did you forget to expose the binding?", "while locating " + PrivateFoo.class.getName()); } } private static class FailingModule extends AbstractModule { @Override protected void configure() { bind(Collection.class).to(List.class); install(new ManyPrivateModules()); } } private static class ManyPrivateModules extends AbstractModule { @Override protected void configure() { // make sure duplicate sources are collapsed install(new FailingPrivateModule()); install(new FailingPrivateModule()); // but additional sources are listed install(new SecondFailingPrivateModule()); } } private static class FailingPrivateModule extends PrivateModule { @Override protected void configure() { bind(List.class).toInstance(new ArrayList()); // Add the Provider<List> binding, created just-in-time, // to make sure our linked JIT bindings have the correct source. getProvider(Key.get(Types.providerOf(List.class))); // Request a JIT binding for PrivateFoo, which can only // be created in the private module because it depends // on List. getProvider(PrivateFoo.class); } } /** A second class, so we can see another name in the source list. */ private static class SecondFailingPrivateModule extends PrivateModule { @Override protected void configure() { bind(List.class).toInstance(new ArrayList()); // Add the Provider<List> binding, created just-in-time, // to make sure our linked JIT bindings have the correct source. getProvider(Key.get(Types.providerOf(List.class))); // Request a JIT binding for PrivateFoo, which can only // be created in the private module because it depends // on List. getProvider(PrivateFoo.class); } } private static class PrivateFoo { @Inject List list; } }