/* * 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.assistedinject; import static com.google.inject.Asserts.assertContains; import static com.google.inject.name.Names.named; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.inject.AbstractModule; import com.google.inject.Binding; import com.google.inject.CreationException; import com.google.inject.Guice; import com.google.inject.Inject; import com.google.inject.Injector; import com.google.inject.Key; import com.google.inject.Module; import com.google.inject.Provides; import com.google.inject.Singleton; import com.google.inject.Stage; import com.google.inject.TypeLiteral; import com.google.inject.name.Named; import com.google.inject.name.Names; import com.google.inject.spi.Dependency; import com.google.inject.spi.Element; import com.google.inject.spi.Elements; import com.google.inject.spi.HasDependencies; import com.google.inject.spi.Message; import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Set; import junit.framework.TestCase; public class FactoryModuleBuilderTest extends TestCase { private enum Color { BLUE, GREEN, RED, GRAY, BLACK } public void testImplicitForwardingAssistedBindingFailsWithInterface() { try { Guice.createInjector( new AbstractModule() { @Override protected void configure() { bind(Car.class).to(Golf.class); install(new FactoryModuleBuilder().build(ColoredCarFactory.class)); } }); fail(); } catch (CreationException ce) { assertContains( ce.getMessage(), "1) " + Car.class.getName() + " is an interface, not a concrete class.", "Unable to create AssistedInject factory.", "while locating " + Car.class.getName(), "at " + ColoredCarFactory.class.getName() + ".create("); assertEquals(1, ce.getErrorMessages().size()); } } public void testImplicitForwardingAssistedBindingFailsWithAbstractClass() { try { Guice.createInjector( new AbstractModule() { @Override protected void configure() { bind(AbstractCar.class).to(ArtCar.class); install(new FactoryModuleBuilder().build(ColoredAbstractCarFactory.class)); } }); fail(); } catch (CreationException ce) { assertContains( ce.getMessage(), "1) " + AbstractCar.class.getName() + " is abstract, not a concrete class.", "Unable to create AssistedInject factory.", "while locating " + AbstractCar.class.getName(), "at " + ColoredAbstractCarFactory.class.getName() + ".create("); assertEquals(1, ce.getErrorMessages().size()); } } public void testImplicitForwardingAssistedBindingCreatesNewObjects() { final Mustang providedMustang = new Mustang(Color.BLUE); Injector injector = Guice.createInjector( new AbstractModule() { @Override protected void configure() { install(new FactoryModuleBuilder().build(MustangFactory.class)); } @Provides Mustang provide() { return providedMustang; } }); assertSame(providedMustang, injector.getInstance(Mustang.class)); MustangFactory factory = injector.getInstance(MustangFactory.class); Mustang created = factory.create(Color.GREEN); assertNotSame(providedMustang, created); assertEquals(Color.BLUE, providedMustang.color); assertEquals(Color.GREEN, created.color); } public void testExplicitForwardingAssistedBindingFailsWithInterface() { try { Guice.createInjector( new AbstractModule() { @Override protected void configure() { bind(Volkswagen.class).to(Golf.class); install( new FactoryModuleBuilder() .implement(Car.class, Volkswagen.class) .build(ColoredCarFactory.class)); } }); fail(); } catch (CreationException ce) { assertContains( ce.getMessage(), "1) " + Volkswagen.class.getName() + " is an interface, not a concrete class.", "Unable to create AssistedInject factory.", "while locating " + Volkswagen.class.getName(), "while locating " + Car.class.getName(), "at " + ColoredCarFactory.class.getName() + ".create("); assertEquals(1, ce.getErrorMessages().size()); } } public void testExplicitForwardingAssistedBindingFailsWithAbstractClass() { try { Guice.createInjector( new AbstractModule() { @Override protected void configure() { bind(AbstractCar.class).to(ArtCar.class); install( new FactoryModuleBuilder() .implement(Car.class, AbstractCar.class) .build(ColoredCarFactory.class)); } }); fail(); } catch (CreationException ce) { assertContains( ce.getMessage(), "1) " + AbstractCar.class.getName() + " is abstract, not a concrete class.", "Unable to create AssistedInject factory.", "while locating " + AbstractCar.class.getName(), "while locating " + Car.class.getName(), "at " + ColoredCarFactory.class.getName() + ".create("); assertEquals(1, ce.getErrorMessages().size()); } } public void testExplicitForwardingAssistedBindingCreatesNewObjects() { final Mustang providedMustang = new Mustang(Color.BLUE); Injector injector = Guice.createInjector( new AbstractModule() { @Override protected void configure() { install( new FactoryModuleBuilder() .implement(Car.class, Mustang.class) .build(ColoredCarFactory.class)); } @Provides Mustang provide() { return providedMustang; } }); assertSame(providedMustang, injector.getInstance(Mustang.class)); ColoredCarFactory factory = injector.getInstance(ColoredCarFactory.class); Mustang created = (Mustang) factory.create(Color.GREEN); assertNotSame(providedMustang, created); assertEquals(Color.BLUE, providedMustang.color); assertEquals(Color.GREEN, created.color); } public void testAnnotatedAndParentBoundReturnValue() { Injector injector = Guice.createInjector( new AbstractModule() { @Override protected void configure() { bind(Car.class).to(Golf.class); bind(Integer.class).toInstance(911); bind(Double.class).toInstance(5.0d); install( new FactoryModuleBuilder() .implement(Car.class, Names.named("german"), Beetle.class) .implement(Car.class, Names.named("american"), Mustang.class) .build(AnnotatedVersatileCarFactory.class)); } }); AnnotatedVersatileCarFactory factory = injector.getInstance(AnnotatedVersatileCarFactory.class); assertTrue(factory.getGermanCar(Color.BLACK) instanceof Beetle); assertTrue(injector.getInstance(Car.class) instanceof Golf); } public void testParentBoundReturnValue() { Injector injector = Guice.createInjector( new AbstractModule() { @Override protected void configure() { bind(Car.class).to(Golf.class); bind(Double.class).toInstance(5.0d); install( new FactoryModuleBuilder() .implement(Car.class, Mustang.class) .build(ColoredCarFactory.class)); } }); ColoredCarFactory factory = injector.getInstance(ColoredCarFactory.class); assertTrue(factory.create(Color.RED) instanceof Mustang); assertTrue(injector.getInstance(Car.class) instanceof Golf); } public void testConfigureAnnotatedReturnValue() { Injector injector = Guice.createInjector( new AbstractModule() { @Override protected void configure() { install( new FactoryModuleBuilder() .implement(Car.class, Names.named("german"), Beetle.class) .implement(Car.class, Names.named("american"), Mustang.class) .build(AnnotatedVersatileCarFactory.class)); } }); AnnotatedVersatileCarFactory factory = injector.getInstance(AnnotatedVersatileCarFactory.class); assertTrue(factory.getGermanCar(Color.GRAY) instanceof Beetle); assertTrue(factory.getAmericanCar(Color.BLACK) instanceof Mustang); } public void testNoBindingAssistedInject() { Injector injector = Guice.createInjector( new AbstractModule() { @Override protected void configure() { install(new FactoryModuleBuilder().build(MustangFactory.class)); } }); MustangFactory factory = injector.getInstance(MustangFactory.class); Mustang mustang = factory.create(Color.BLUE); assertEquals(Color.BLUE, mustang.color); } public void testBindingAssistedInject() { Injector injector = Guice.createInjector( new AbstractModule() { @Override protected void configure() { install( new FactoryModuleBuilder() .implement(Car.class, Mustang.class) .build(ColoredCarFactory.class)); } }); ColoredCarFactory factory = injector.getInstance(ColoredCarFactory.class); Mustang mustang = (Mustang) factory.create(Color.BLUE); assertEquals(Color.BLUE, mustang.color); } public void testDuplicateBindings() { Injector injector = Guice.createInjector( new AbstractModule() { @Override protected void configure() { install( new FactoryModuleBuilder() .implement(Car.class, Mustang.class) .build(ColoredCarFactory.class)); install( new FactoryModuleBuilder() .implement(Car.class, Mustang.class) .build(ColoredCarFactory.class)); } }); ColoredCarFactory factory = injector.getInstance(ColoredCarFactory.class); Mustang mustang = (Mustang) factory.create(Color.BLUE); assertEquals(Color.BLUE, mustang.color); } public void testSimilarBindingsWithConflictingImplementations() { try { Injector injector = Guice.createInjector( new AbstractModule() { @Override protected void configure() { install( new FactoryModuleBuilder() .implement(Car.class, Mustang.class) .build(ColoredCarFactory.class)); install( new FactoryModuleBuilder() .implement(Car.class, Golf.class) .build(ColoredCarFactory.class)); } }); injector.getInstance(ColoredCarFactory.class); fail(); } catch (CreationException ce) { assertContains( ce.getMessage(), "A binding to " + ColoredCarFactory.class.getName() + " was already configured"); assertEquals(1, ce.getErrorMessages().size()); } } public void testMultipleReturnTypes() { Injector injector = Guice.createInjector( new AbstractModule() { @Override protected void configure() { bind(Double.class).toInstance(5.0d); install(new FactoryModuleBuilder().build(VersatileCarFactory.class)); } }); VersatileCarFactory factory = injector.getInstance(VersatileCarFactory.class); Mustang mustang = factory.getMustang(Color.RED); assertEquals(Color.RED, mustang.color); Beetle beetle = factory.getBeetle(Color.GREEN); assertEquals(Color.GREEN, beetle.color); } public void testParameterizedClassesWithNoImplements() { Injector injector = Guice.createInjector( new AbstractModule() { @Override protected void configure() { install( new FactoryModuleBuilder().build(new TypeLiteral<Foo.Factory<String>>() {})); } }); Foo.Factory<String> factory = injector.getInstance(Key.get(new TypeLiteral<Foo.Factory<String>>() {})); @SuppressWarnings("unused") Foo<String> foo = factory.create(new Bar()); } public void testGenericErrorMessageMakesSense() { try { Guice.createInjector( new AbstractModule() { @Override protected void configure() { install(new FactoryModuleBuilder().build(Key.get(Foo.Factory.class))); } }); fail(); } catch (CreationException ce) { // Assert not only that it's the correct message, but also that it's the *only* message. Collection<Message> messages = ce.getErrorMessages(); assertEquals( Foo.Factory.class.getName() + " cannot be used as a key; It is not fully specified.", Iterables.getOnlyElement(messages).getMessage()); } } interface Car {} interface Volkswagen extends Car {} interface ColoredCarFactory { Car create(Color color); } interface MustangFactory { Mustang create(Color color); } interface VersatileCarFactory { Mustang getMustang(Color color); Beetle getBeetle(Color color); } interface AnnotatedVersatileCarFactory { @Named("german") Car getGermanCar(Color color); @Named("american") Car getAmericanCar(Color color); } public static class Golf implements Volkswagen {} public static class Mustang implements Car { private final Color color; @Inject public Mustang(@Assisted Color color) { this.color = color; } } public static class Beetle implements Car { private final Color color; @Inject public Beetle(@Assisted Color color) { this.color = color; } } public static class Foo<E> { static interface Factory<E> { Foo<E> create(Bar bar); } @SuppressWarnings("unused") @Inject Foo(@Assisted Bar bar, Baz<E> baz) {} } public static class Bar {} @SuppressWarnings("unused") public static class Baz<E> {} abstract static class AbstractCar implements Car {} interface ColoredAbstractCarFactory { AbstractCar create(Color color); } public static class ArtCar extends AbstractCar {} public void testFactoryBindingDependencies() { // validate dependencies work in all stages & as a raw element, // and that dependencies work for methods, fields, constructors, // and for @AssistedInject constructors too. Module module = new AbstractModule() { @Override protected void configure() { bind(Integer.class).toInstance(42); bind(Double.class).toInstance(4.2d); bind(Float.class).toInstance(4.2f); bind(String.class).annotatedWith(named("dog")).toInstance("dog"); bind(String.class).annotatedWith(named("cat1")).toInstance("cat1"); bind(String.class).annotatedWith(named("cat2")).toInstance("cat2"); bind(String.class).annotatedWith(named("cat3")).toInstance("cat3"); bind(String.class).annotatedWith(named("arbitrary")).toInstance("fail!"); install( new FactoryModuleBuilder() .implement(Animal.class, Dog.class) .build(AnimalHouse.class)); } }; Set<Key<?>> expectedKeys = ImmutableSet.<Key<?>>of( Key.get(Integer.class), Key.get(Double.class), Key.get(Float.class), Key.get(String.class, named("dog")), Key.get(String.class, named("cat1")), Key.get(String.class, named("cat2")), Key.get(String.class, named("cat3"))); Injector injector = Guice.createInjector(module); validateDependencies(expectedKeys, injector.getBinding(AnimalHouse.class)); injector = Guice.createInjector(Stage.TOOL, module); validateDependencies(expectedKeys, injector.getBinding(AnimalHouse.class)); List<Element> elements = Elements.getElements(module); boolean found = false; for (Element element : elements) { if (element instanceof Binding) { Binding<?> binding = (Binding<?>) element; if (binding.getKey().equals(Key.get(AnimalHouse.class))) { found = true; validateDependencies(expectedKeys, binding); break; } } } assertTrue(found); } private void validateDependencies(Set<Key<?>> expectedKeys, Binding<?> binding) { Set<Dependency<?>> dependencies = ((HasDependencies) binding).getDependencies(); Set<Key<?>> actualKeys = new HashSet<Key<?>>(); for (Dependency<?> dependency : dependencies) { actualKeys.add(dependency.getKey()); } assertEquals(expectedKeys, actualKeys); } interface AnimalHouse { Animal createAnimal(String name); Cat createCat(String name); Cat createCat(int age); } interface Animal {} @SuppressWarnings("unused") private static class Dog implements Animal { @Inject int a; @Inject Dog(@Assisted String a, double b) {} @Inject void register(@Named("dog") String a) {} } @SuppressWarnings("unused") private static class Cat implements Animal { @Inject float a; @AssistedInject Cat(@Assisted String a, @Named("cat1") String b) {} @AssistedInject Cat(@Assisted int a, @Named("cat2") String b) {} @AssistedInject Cat(@Assisted byte a, @Named("catfail") String b) {} // not a dependency! @Inject void register(@Named("cat3") String a) {} } public void testFactoryPublicAndReturnTypeNotPublic() { try { Guice.createInjector( new AbstractModule() { @Override protected void configure() { install( new FactoryModuleBuilder() .implement(Hidden.class, HiddenImpl.class) .build(NotHidden.class)); } }); fail("Expected CreationException"); } catch (CreationException ce) { assertEquals( NotHidden.class.getName() + " is public, but has a method that returns a non-public type: " + Hidden.class.getName() + ". Due to limitations with java.lang.reflect.Proxy, this is not allowed. " + "Please either make the factory non-public or the return type public.", Iterables.getOnlyElement(ce.getErrorMessages()).getMessage()); } } interface Hidden {} public static class HiddenImpl implements Hidden {} public interface NotHidden { Hidden create(); } public void testSingletonScopeOnAssistedClassIsIgnored() { try { Guice.createInjector( new AbstractModule() { @Override protected void configure() { install(new FactoryModuleBuilder().build(SingletonFactory.class)); } }); fail(); } catch (CreationException ce) { assertEquals(1, ce.getErrorMessages().size()); assertEquals( "Found scope annotation [" + Singleton.class.getName() + "]" + " on implementation class [" + AssistedSingleton.class.getName() + "]" + " of AssistedInject factory [" + SingletonFactory.class.getName() + "]." + "\nThis is not allowed, please remove the scope annotation.", Iterables.getOnlyElement(ce.getErrorMessages()).getMessage()); } } interface SingletonFactory { AssistedSingleton create(String string); } @SuppressWarnings("GuiceAssistedInjectScoping") @Singleton static class AssistedSingleton { @Inject public AssistedSingleton(@SuppressWarnings("unused") @Assisted String string) {} } }