From 3d4887f08cd9ff3bb31e2f305a2a22e8afc59318 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=CC=88rg=20Prante?= Date: Thu, 7 Jan 2016 23:04:58 +0100 Subject: [PATCH] added assistedinject and multibindings extension --- .../inject/assistedinject/Assisted.java | 26 + .../assistedinject/AssistedConstructor.java | 89 ++ .../inject/assistedinject/AssistedInject.java | 28 + .../assistedinject/AssistedInjectBinding.java | 23 + .../AssistedInjectTargetVisitor.java | 17 + .../inject/assistedinject/AssistedMethod.java | 37 + .../assistedinject/BindingCollector.java | 44 + .../assistedinject/FactoryModuleBuilder.java | 321 +++++ .../assistedinject/FactoryProvider.java | 377 +++++ .../assistedinject/FactoryProvider2.java | 932 +++++++++++++ .../inject/assistedinject/Parameter.java | 146 ++ .../assistedinject/ParameterListKey.java | 47 + .../inject/multibindings/ClassMapKey.java | 19 + .../google/inject/multibindings/Element.java | 31 + .../google/inject/multibindings/Indexer.java | 167 +++ .../inject/multibindings/MapBinder.java | 873 ++++++++++++ .../multibindings/MapBinderBinding.java | 82 ++ .../google/inject/multibindings/MapKey.java | 42 + .../inject/multibindings/Multibinder.java | 575 ++++++++ .../multibindings/MultibinderBinding.java | 63 + .../multibindings/MultibindingsScanner.java | 179 +++ .../MultibindingsTargetVisitor.java | 31 + .../inject/multibindings/OptionalBinder.java | 779 +++++++++++ .../multibindings/OptionalBinderBinding.java | 58 + .../inject/multibindings/ProvidesIntoMap.java | 40 + .../multibindings/ProvidesIntoOptional.java | 50 + .../inject/multibindings/ProvidesIntoSet.java | 34 + .../inject/multibindings/RealElement.java | 98 ++ .../inject/multibindings/StringMapKey.java | 19 + .../assistedinject/ExtensionSpiTest.java | 202 +++ .../FactoryModuleBuilderTest.java | 524 +++++++ .../assistedinject/FactoryProvider2Test.java | 1188 ++++++++++++++++ .../assistedinject/FactoryProviderTest.java | 840 +++++++++++ .../assistedinject/ManyConstructorsTest.java | 258 ++++ .../google/inject/multibindings/AllTests.java | 17 + .../inject/multibindings/Collector.java | 28 + .../inject/multibindings/MapBinderTest.java | 1013 ++++++++++++++ .../inject/multibindings/MultibinderTest.java | 1202 ++++++++++++++++ .../multibindings/OptionalBinderTest.java | 1222 +++++++++++++++++ .../multibindings/ProvidesIntoTest.java | 352 +++++ .../inject/multibindings/RealElementTest.java | 37 + .../google/inject/multibindings/SpiUtils.java | 1099 +++++++++++++++ 42 files changed, 13209 insertions(+) create mode 100644 src/main/java/com/google/inject/assistedinject/Assisted.java create mode 100644 src/main/java/com/google/inject/assistedinject/AssistedConstructor.java create mode 100644 src/main/java/com/google/inject/assistedinject/AssistedInject.java create mode 100644 src/main/java/com/google/inject/assistedinject/AssistedInjectBinding.java create mode 100644 src/main/java/com/google/inject/assistedinject/AssistedInjectTargetVisitor.java create mode 100644 src/main/java/com/google/inject/assistedinject/AssistedMethod.java create mode 100644 src/main/java/com/google/inject/assistedinject/BindingCollector.java create mode 100644 src/main/java/com/google/inject/assistedinject/FactoryModuleBuilder.java create mode 100644 src/main/java/com/google/inject/assistedinject/FactoryProvider.java create mode 100644 src/main/java/com/google/inject/assistedinject/FactoryProvider2.java create mode 100644 src/main/java/com/google/inject/assistedinject/Parameter.java create mode 100644 src/main/java/com/google/inject/assistedinject/ParameterListKey.java create mode 100644 src/main/java/com/google/inject/multibindings/ClassMapKey.java create mode 100644 src/main/java/com/google/inject/multibindings/Element.java create mode 100644 src/main/java/com/google/inject/multibindings/Indexer.java create mode 100644 src/main/java/com/google/inject/multibindings/MapBinder.java create mode 100644 src/main/java/com/google/inject/multibindings/MapBinderBinding.java create mode 100644 src/main/java/com/google/inject/multibindings/MapKey.java create mode 100644 src/main/java/com/google/inject/multibindings/Multibinder.java create mode 100644 src/main/java/com/google/inject/multibindings/MultibinderBinding.java create mode 100644 src/main/java/com/google/inject/multibindings/MultibindingsScanner.java create mode 100644 src/main/java/com/google/inject/multibindings/MultibindingsTargetVisitor.java create mode 100644 src/main/java/com/google/inject/multibindings/OptionalBinder.java create mode 100644 src/main/java/com/google/inject/multibindings/OptionalBinderBinding.java create mode 100644 src/main/java/com/google/inject/multibindings/ProvidesIntoMap.java create mode 100644 src/main/java/com/google/inject/multibindings/ProvidesIntoOptional.java create mode 100644 src/main/java/com/google/inject/multibindings/ProvidesIntoSet.java create mode 100644 src/main/java/com/google/inject/multibindings/RealElement.java create mode 100644 src/main/java/com/google/inject/multibindings/StringMapKey.java create mode 100644 src/test/java/com/google/inject/assistedinject/ExtensionSpiTest.java create mode 100644 src/test/java/com/google/inject/assistedinject/FactoryModuleBuilderTest.java create mode 100644 src/test/java/com/google/inject/assistedinject/FactoryProvider2Test.java create mode 100644 src/test/java/com/google/inject/assistedinject/FactoryProviderTest.java create mode 100644 src/test/java/com/google/inject/assistedinject/ManyConstructorsTest.java create mode 100644 src/test/java/com/google/inject/multibindings/AllTests.java create mode 100644 src/test/java/com/google/inject/multibindings/Collector.java create mode 100644 src/test/java/com/google/inject/multibindings/MapBinderTest.java create mode 100644 src/test/java/com/google/inject/multibindings/MultibinderTest.java create mode 100644 src/test/java/com/google/inject/multibindings/OptionalBinderTest.java create mode 100644 src/test/java/com/google/inject/multibindings/ProvidesIntoTest.java create mode 100644 src/test/java/com/google/inject/multibindings/RealElementTest.java create mode 100644 src/test/java/com/google/inject/multibindings/SpiUtils.java diff --git a/src/main/java/com/google/inject/assistedinject/Assisted.java b/src/main/java/com/google/inject/assistedinject/Assisted.java new file mode 100644 index 0000000..c44cf48 --- /dev/null +++ b/src/main/java/com/google/inject/assistedinject/Assisted.java @@ -0,0 +1,26 @@ +package com.google.inject.assistedinject; + +import com.google.inject.BindingAnnotation; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * Annotates an injected parameter or field whose value comes from an argument to a factory method. + */ +@BindingAnnotation +@Target({FIELD, PARAMETER, METHOD}) +@Retention(RUNTIME) +public @interface Assisted { + + /** + * The unique name for this parameter. This is matched to the {@literal @Assisted} constructor + * parameter with the same value. Names are not necessary when the parameter types are distinct. + */ + String value() default ""; +} \ No newline at end of file diff --git a/src/main/java/com/google/inject/assistedinject/AssistedConstructor.java b/src/main/java/com/google/inject/assistedinject/AssistedConstructor.java new file mode 100644 index 0000000..762d29b --- /dev/null +++ b/src/main/java/com/google/inject/assistedinject/AssistedConstructor.java @@ -0,0 +1,89 @@ +package com.google.inject.assistedinject; + +import com.google.common.collect.Lists; +import com.google.inject.Inject; +import com.google.inject.TypeLiteral; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Internal respresentation of a constructor annotated with + * {@link AssistedInject} + */ +class AssistedConstructor { + + private final Constructor constructor; + private final ParameterListKey assistedParameters; + private final List allParameters; + + private AssistedConstructor(Constructor constructor, List> parameterTypes) { + this.constructor = constructor; + + Annotation[][] annotations = constructor.getParameterAnnotations(); + + List typeList = Lists.newArrayList(); + allParameters = new ArrayList(); + + // categorize params as @Assisted or @Injected + for (int i = 0; i < parameterTypes.size(); i++) { + Parameter parameter = new Parameter(parameterTypes.get(i).getType(), annotations[i]); + allParameters.add(parameter); + if (parameter.isProvidedByFactory()) { + typeList.add(parameter.getType()); + } + } + this.assistedParameters = new ParameterListKey(typeList); + } + + public static AssistedConstructor create( + Constructor constructor, List> parameterTypes) { + return new AssistedConstructor(constructor, parameterTypes); + } + + /** + * Returns the {@link ParameterListKey} for this constructor. The + * {@link ParameterListKey} is created from the ordered list of {@link Assisted} + * constructor parameters. + */ + public ParameterListKey getAssistedParameters() { + return assistedParameters; + } + + /** + * Returns an ordered list of all constructor parameters (both + * {@link Assisted} and {@link Inject}ed). + */ + public List getAllParameters() { + return allParameters; + } + + public Set> getDeclaredExceptions() { + return new HashSet>(Arrays.asList(constructor.getExceptionTypes())); + } + + /** + * Returns an instance of T, constructed using this constructor, with the + * supplied arguments. + */ + public T newInstance(Object[] args) throws Throwable { + constructor.setAccessible(true); + try { + return constructor.newInstance(args); + } catch (InvocationTargetException e) { + throw e.getCause(); + } + } + + @Override + public String toString() { + return constructor.toString(); + } +} diff --git a/src/main/java/com/google/inject/assistedinject/AssistedInject.java b/src/main/java/com/google/inject/assistedinject/AssistedInject.java new file mode 100644 index 0000000..f1ef622 --- /dev/null +++ b/src/main/java/com/google/inject/assistedinject/AssistedInject.java @@ -0,0 +1,28 @@ +package com.google.inject.assistedinject; + +import com.google.inject.Inject; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.CONSTRUCTOR; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + *

+ * When used in tandem with {@link FactoryModuleBuilder}, constructors annotated with + * {@code @AssistedInject} indicate that multiple constructors can be injected, each with different + * parameters. AssistedInject annotations should not be mixed with {@literal @}{@link Inject} + * annotations. The assisted parameters must exactly match one corresponding factory method within + * the factory interface, but the parameters do not need to be in the same order. Constructors + * annotated with AssistedInject are created by Guice and receive all the benefits + * (such as AOP). + * + *

+ * Constructor parameters must be either supplied by the factory interface and marked with + * @Assisted, or they must be injectable. + */ +@Target({CONSTRUCTOR}) +@Retention(RUNTIME) +public @interface AssistedInject { +} diff --git a/src/main/java/com/google/inject/assistedinject/AssistedInjectBinding.java b/src/main/java/com/google/inject/assistedinject/AssistedInjectBinding.java new file mode 100644 index 0000000..722f719 --- /dev/null +++ b/src/main/java/com/google/inject/assistedinject/AssistedInjectBinding.java @@ -0,0 +1,23 @@ +package com.google.inject.assistedinject; + +import com.google.inject.Key; + +import java.util.Collection; + +/** + * A binding for a factory created by FactoryModuleBuilder. + * + * @param The fully qualified type of the factory. + */ +public interface AssistedInjectBinding { + + /** + * Returns the {@link Key} for the factory binding. + */ + Key getKey(); + + /** + * Returns an {@link AssistedMethod} for each method in the factory. + */ + Collection getAssistedMethods(); +} diff --git a/src/main/java/com/google/inject/assistedinject/AssistedInjectTargetVisitor.java b/src/main/java/com/google/inject/assistedinject/AssistedInjectTargetVisitor.java new file mode 100644 index 0000000..7eb673d --- /dev/null +++ b/src/main/java/com/google/inject/assistedinject/AssistedInjectTargetVisitor.java @@ -0,0 +1,17 @@ +package com.google.inject.assistedinject; + +import com.google.inject.spi.BindingTargetVisitor; + +/** + * A visitor for the AssistedInject extension. + *

+ * If your {@link BindingTargetVisitor} implements this interface, bindings created by using + * {@link FactoryModuleBuilder} will be visited through this interface. + */ +public interface AssistedInjectTargetVisitor extends BindingTargetVisitor { + + /** + * Visits an {@link AssistedInjectBinding} created through {@link FactoryModuleBuilder}. + */ + V visit(AssistedInjectBinding assistedInjectBinding); +} diff --git a/src/main/java/com/google/inject/assistedinject/AssistedMethod.java b/src/main/java/com/google/inject/assistedinject/AssistedMethod.java new file mode 100644 index 0000000..26acf55 --- /dev/null +++ b/src/main/java/com/google/inject/assistedinject/AssistedMethod.java @@ -0,0 +1,37 @@ +package com.google.inject.assistedinject; + +import com.google.inject.TypeLiteral; +import com.google.inject.spi.Dependency; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.util.Set; + +/** + * Details about how a method in an assisted inject factory will be assisted. + */ +public interface AssistedMethod { + + /** + * Returns the factory method that is being assisted. + */ + Method getFactoryMethod(); + + /** + * Returns the implementation type that will be created when the method is + * used. + */ + TypeLiteral getImplementationType(); + + /** + * Returns the constructor that will be used to construct instances of the + * implementation. + */ + Constructor getImplementationConstructor(); + + /** + * Returns all non-assisted dependencies required to construct and inject + * the implementation. + */ + Set> getDependencies(); +} diff --git a/src/main/java/com/google/inject/assistedinject/BindingCollector.java b/src/main/java/com/google/inject/assistedinject/BindingCollector.java new file mode 100644 index 0000000..42de817 --- /dev/null +++ b/src/main/java/com/google/inject/assistedinject/BindingCollector.java @@ -0,0 +1,44 @@ +package com.google.inject.assistedinject; + +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Maps; +import com.google.inject.ConfigurationException; +import com.google.inject.Key; +import com.google.inject.TypeLiteral; +import com.google.inject.spi.Message; + +import java.util.Collections; +import java.util.Map; + +/** + * Utility class for collecting factory bindings. Used for configuring {@link FactoryProvider2}. + */ +class BindingCollector { + + private final Map, TypeLiteral> bindings = Maps.newHashMap(); + + public BindingCollector addBinding(Key key, TypeLiteral target) { + if (bindings.containsKey(key)) { + throw new ConfigurationException(ImmutableSet.of( + new Message("Only one implementation can be specified for " + key))); + } + + bindings.put(key, target); + + return this; + } + + public Map, TypeLiteral> getBindings() { + return Collections.unmodifiableMap(bindings); + } + + @Override + public int hashCode() { + return bindings.hashCode(); + } + + @Override + public boolean equals(Object obj) { + return (obj instanceof BindingCollector) && bindings.equals(((BindingCollector) obj).bindings); + } +} diff --git a/src/main/java/com/google/inject/assistedinject/FactoryModuleBuilder.java b/src/main/java/com/google/inject/assistedinject/FactoryModuleBuilder.java new file mode 100644 index 0000000..84ca841 --- /dev/null +++ b/src/main/java/com/google/inject/assistedinject/FactoryModuleBuilder.java @@ -0,0 +1,321 @@ +package com.google.inject.assistedinject; + +import com.google.inject.AbstractModule; +import com.google.inject.Key; +import com.google.inject.Module; +import com.google.inject.Provider; +import com.google.inject.TypeLiteral; + +import java.lang.annotation.Annotation; + +/** + * Provides a factory that combines the caller's arguments with injector-supplied values to + * construct objects. + * + *

Defining a factory

+ * Create an interface whose methods return the constructed type, or any of its supertypes. The + * method's parameters are the arguments required to build the constructed type. + * + *
public interface PaymentFactory {
+ *   Payment create(Date startDate, Money amount);
+ * }
+ * + * You can name your factory methods whatever you like, such as create, createPayment + * or newPayment. + * + *

Creating a type that accepts factory parameters

+ * {@code constructedType} is a concrete class with an {@literal @}{@link com.google.inject.Inject + * Inject}-annotated constructor. In addition to injector-supplied parameters, the constructor + * should have parameters that match each of the factory method's parameters. Each factory-supplied + * parameter requires an {@literal @}{@link Assisted} annotation. This serves to document that the + * parameter is not bound by your application's modules. + * + *
public class RealPayment implements Payment {
+ *   {@literal @}Inject
+ *   public RealPayment(
+ *      CreditService creditService,
+ *      AuthService authService,
+ *      {@literal @}Assisted Date startDate,
+ *      {@literal @}Assisted Money amount) {
+ *     ...
+ *   }
+ * }
+ * + *

Multiple factory methods for the same type

+ * If the factory contains many methods that return the same type, you can create multiple + * constructors in your concrete class, each constructor marked with with + * {@literal @}{@link AssistedInject}, in order to match the different parameters types of the + * factory methods. + * + *
public interface PaymentFactory {
+ *    Payment create(Date startDate, Money amount);
+ *    Payment createWithoutDate(Money amount);
+ * }
+ *
+ * public class RealPayment implements Payment {
+ *  {@literal @}AssistedInject
+ *   public RealPayment(
+ *      CreditService creditService,
+ *      AuthService authService,
+ *     {@literal @}Assisted Date startDate,
+ *     {@literal @}Assisted Money amount) {
+ *     ...
+ *   }
+ *
+ *  {@literal @}AssistedInject
+ *   public RealPayment(
+ *      CreditService creditService,
+ *      AuthService authService,
+ *     {@literal @}Assisted Money amount) {
+ *     ...
+ *   }
+ * }
+ * + *

Configuring simple factories

+ * In your {@link Module module}, install a {@code FactoryModuleBuilder} that creates the + * factory: + * + *
install(new FactoryModuleBuilder()
+ *     .implement(Payment.class, RealPayment.class)
+ *     .build(PaymentFactory.class));
+ * + * As a side-effect of this binding, Guice will inject the factory to initialize it for use. The + * factory cannot be used until the injector has been initialized. + * + *

Configuring complex factories

+ * Factories can create an arbitrary number of objects, one per each method. Each factory + * method can be configured using .implement. + * + *
public interface OrderFactory {
+ *    Payment create(Date startDate, Money amount);
+ *    Shipment create(Customer customer, Item item);
+ *    Receipt create(Payment payment, Shipment shipment);
+ * }
+ *
+ * [...]
+ *
+ * install(new FactoryModuleBuilder()
+ *     .implement(Payment.class, RealPayment.class)
+ *     // excluding .implement for Shipment means the implementation class
+ *     // will be 'Shipment' itself, which is legal if it's not an interface.
+ *     .implement(Receipt.class, RealReceipt.class)
+ *     .build(OrderFactory.class));
+ * + * + *

Using the factory

+ * Inject your factory into your application classes. When you use the factory, your arguments + * will be combined with values from the injector to construct an instance. + * + *
public class PaymentAction {
+ * {@literal @}Inject private PaymentFactory paymentFactory;
+ *
+ * public void doPayment(Money amount) {
+ * Payment payment = paymentFactory.create(new Date(), amount);
+ * payment.apply();
+ * }
+ * }
+ * + *

Making parameter types distinct

+ * The types of the factory method's parameters must be distinct. To use multiple parameters of + * the same type, use a named {@literal @}{@link Assisted} annotation to disambiguate the + * parameters. The names must be applied to the factory method's parameters: + * + *
public interface PaymentFactory {
+ * Payment create(
+ * {@literal @}Assisted("startDate") Date startDate,
+ * {@literal @}Assisted("dueDate") Date dueDate,
+ * Money amount);
+ * } 
+ * + * ...and to the concrete type's constructor parameters: + * + *
public class RealPayment implements Payment {
+ * {@literal @}Inject
+ * public RealPayment(
+ * CreditService creditService,
+ * AuthService authService,
+ * {@literal @}Assisted("startDate") Date startDate,
+ * {@literal @}Assisted("dueDate") Date dueDate,
+ * {@literal @}Assisted Money amount) {
+ * ...
+ * }
+ * }
+ * + *

Values are created by Guice

+ * Returned factories use child injectors to create values. The values are eligible for method + * interception. In addition, {@literal @}{@literal Inject} members will be injected before they are + * returned. + * + *

More configuration options

+ * In addition to simply specifying an implementation class for any returned type, factories' return + * values can be automatic or can be configured to use annotations: + *

+ * If you just want to return the types specified in the factory, do not configure any + * implementations: + * + *

public interface FruitFactory {
+ * Apple getApple(Color color);
+ * }
+ * ...
+ * protected void configure() {
+ * install(new FactoryModuleBuilder().build(FruitFactory.class));
+ * }
+ * + * Note that any type returned by the factory in this manner needs to be an implementation class. + *

+ * To return two different implementations for the same interface from your factory, use binding + * annotations on your return types: + * + *

interface CarFactory {
+ * {@literal @}Named("fast") Car getFastCar(Color color);
+ * {@literal @}Named("clean") Car getCleanCar(Color color);
+ * }
+ * ...
+ * protected void configure() {
+ * install(new FactoryModuleBuilder()
+ * .implement(Car.class, Names.named("fast"), Porsche.class)
+ * .implement(Car.class, Names.named("clean"), Prius.class)
+ * .build(CarFactory.class));
+ * }
+ * + *

Implementation limitations

+ * As a limitation of the implementation, it is prohibited to declare a factory method that + * accepts a {@code Provider} as one of its arguments. + */ +public final class FactoryModuleBuilder { + + private final BindingCollector bindings = new BindingCollector(); + + /** + * See the factory configuration examples at {@link FactoryModuleBuilder}. + */ + public FactoryModuleBuilder implement(Class source, Class target) { + return implement(source, TypeLiteral.get(target)); + } + + /** + * See the factory configuration examples at {@link FactoryModuleBuilder}. + */ + public FactoryModuleBuilder implement(Class source, TypeLiteral target) { + return implement(TypeLiteral.get(source), target); + } + + /** + * See the factory configuration examples at {@link FactoryModuleBuilder}. + */ + public FactoryModuleBuilder implement(TypeLiteral source, Class target) { + return implement(source, TypeLiteral.get(target)); + } + + /** + * See the factory configuration examples at {@link FactoryModuleBuilder}. + */ + public FactoryModuleBuilder implement(TypeLiteral source, + TypeLiteral target) { + return implement(Key.get(source), target); + } + + /** + * See the factory configuration examples at {@link FactoryModuleBuilder}. + */ + public FactoryModuleBuilder implement(Class source, Annotation annotation, + Class target) { + return implement(source, annotation, TypeLiteral.get(target)); + } + + /** + * See the factory configuration examples at {@link FactoryModuleBuilder}. + */ + public FactoryModuleBuilder implement(Class source, Annotation annotation, + TypeLiteral target) { + return implement(TypeLiteral.get(source), annotation, target); + } + + /** + * See the factory configuration examples at {@link FactoryModuleBuilder}. + */ + public FactoryModuleBuilder implement(TypeLiteral source, Annotation annotation, + Class target) { + return implement(source, annotation, TypeLiteral.get(target)); + } + + /** + * See the factory configuration examples at {@link FactoryModuleBuilder}. + */ + public FactoryModuleBuilder implement(TypeLiteral source, Annotation annotation, + TypeLiteral target) { + return implement(Key.get(source, annotation), target); + } + + /** + * See the factory configuration examples at {@link FactoryModuleBuilder}. + */ + public FactoryModuleBuilder implement(Class source, + Class annotationType, Class target) { + return implement(source, annotationType, TypeLiteral.get(target)); + } + + /** + * See the factory configuration examples at {@link FactoryModuleBuilder}. + */ + public FactoryModuleBuilder implement(Class source, + Class annotationType, TypeLiteral target) { + return implement(TypeLiteral.get(source), annotationType, target); + } + + /** + * See the factory configuration examples at {@link FactoryModuleBuilder}. + */ + public FactoryModuleBuilder implement(TypeLiteral source, + Class annotationType, Class target) { + return implement(source, annotationType, TypeLiteral.get(target)); + } + + /** + * See the factory configuration examples at {@link FactoryModuleBuilder}. + */ + public FactoryModuleBuilder implement(TypeLiteral source, + Class annotationType, TypeLiteral target) { + return implement(Key.get(source, annotationType), target); + } + + /** + * See the factory configuration examples at {@link FactoryModuleBuilder}. + */ + public FactoryModuleBuilder implement(Key source, Class target) { + return implement(source, TypeLiteral.get(target)); + } + + /** + * See the factory configuration examples at {@link FactoryModuleBuilder}. + */ + public FactoryModuleBuilder implement(Key source, TypeLiteral target) { + bindings.addBinding(source, target); + return this; + } + + /** + * See the factory configuration examples at {@link FactoryModuleBuilder}. + */ + public Module build(Class factoryInterface) { + return build(TypeLiteral.get(factoryInterface)); + } + + /** + * See the factory configuration examples at {@link FactoryModuleBuilder}. + */ + public Module build(TypeLiteral factoryInterface) { + return build(Key.get(factoryInterface)); + } + + + public Module build(final Key factoryInterface) { + return new AbstractModule() { + @Override + protected void configure() { + Provider provider = new FactoryProvider2(factoryInterface, bindings); + bind(factoryInterface).toProvider(provider); + } + }; + } +} diff --git a/src/main/java/com/google/inject/assistedinject/FactoryProvider.java b/src/main/java/com/google/inject/assistedinject/FactoryProvider.java new file mode 100644 index 0000000..58330e0 --- /dev/null +++ b/src/main/java/com/google/inject/assistedinject/FactoryProvider.java @@ -0,0 +1,377 @@ +package com.google.inject.assistedinject; + +import com.google.common.base.Objects; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.inject.ConfigurationException; +import com.google.inject.Inject; +import com.google.inject.Injector; +import com.google.inject.Key; +import com.google.inject.Provider; +import com.google.inject.TypeLiteral; +import com.google.inject.internal.Errors; +import com.google.inject.internal.ErrorsException; +import com.google.inject.spi.Dependency; +import com.google.inject.spi.HasDependencies; +import com.google.inject.spi.Message; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.lang.reflect.Type; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static com.google.inject.internal.Annotations.getKey; + +/** + * Obsolete. Prefer {@link FactoryModuleBuilder} for its more concise API and + * additional capability. + * + *

Provides a factory that combines the caller's arguments with injector-supplied values to + * construct objects. + * + *

Defining a factory

+ * Create an interface whose methods return the constructed type, or any of its supertypes. The + * method's parameters are the arguments required to build the constructed type. + *
public interface PaymentFactory {
+ *   Payment create(Date startDate, Money amount);
+ * }
+ * You can name your factory methods whatever you like, such as create, createPayment + * or newPayment. + * + *

Creating a type that accepts factory parameters

+ * {@code constructedType} is a concrete class with an {@literal @}{@link Inject}-annotated + * constructor. In addition to injector-supplied parameters, the constructor should have + * parameters that match each of the factory method's parameters. Each factory-supplied parameter + * requires an {@literal @}{@link Assisted} annotation. This serves to document that the parameter + * is not bound by your application's modules. + *
public class RealPayment implements Payment {
+ *   {@literal @}Inject
+ *   public RealPayment(
+ *      CreditService creditService,
+ *      AuthService authService,
+ *      {@literal @}Assisted Date startDate,
+ *      {@literal @}Assisted Money amount) {
+ *     ...
+ *   }
+ * }
+ * Any parameter that permits a null value should also be annotated {@code @Nullable}. + * + *

Configuring factories

+ * In your {@link com.google.inject.Module module}, bind the factory interface to the returned + * factory: + *
bind(PaymentFactory.class).toProvider(
+ *     FactoryProvider.newFactory(PaymentFactory.class, RealPayment.class));
+ * As a side-effect of this binding, Guice will inject the factory to initialize it for use. The + * factory cannot be used until the injector has been initialized. + * + *

Using the factory

+ * Inject your factory into your application classes. When you use the factory, your arguments + * will be combined with values from the injector to construct an instance. + *
public class PaymentAction {
+ *   {@literal @}Inject private PaymentFactory paymentFactory;
+ *
+ *   public void doPayment(Money amount) {
+ *     Payment payment = paymentFactory.create(new Date(), amount);
+ *     payment.apply();
+ *   }
+ * }
+ * + *

Making parameter types distinct

+ * The types of the factory method's parameters must be distinct. To use multiple parameters of + * the same type, use a named {@literal @}{@link Assisted} annotation to disambiguate the + * parameters. The names must be applied to the factory method's parameters: + * + *
public interface PaymentFactory {
+ *   Payment create(
+ *       {@literal @}Assisted("startDate") Date startDate,
+ *       {@literal @}Assisted("dueDate") Date dueDate,
+ *       Money amount);
+ * } 
+ * ...and to the concrete type's constructor parameters: + *
public class RealPayment implements Payment {
+ *   {@literal @}Inject
+ *   public RealPayment(
+ *      CreditService creditService,
+ *      AuthService authService,
+ *      {@literal @}Assisted("startDate") Date startDate,
+ *      {@literal @}Assisted("dueDate") Date dueDate,
+ *      {@literal @}Assisted Money amount) {
+ *     ...
+ *   }
+ * }
+ * + *

Values are created by Guice

+ * Returned factories use child injectors to create values. The values are eligible for method + * interception. In addition, {@literal @}{@literal Inject} members will be injected before they are + * returned. + * + *

Backwards compatibility using {@literal @}AssistedInject

+ * Instead of the {@literal @}Inject annotation, you may annotate the constructed classes with + * {@literal @}{@link AssistedInject}. This triggers a limited backwards-compatability mode. + * + *

Instead of matching factory method arguments to constructor parameters using their names, the + * parameters are matched by their order. The first factory method argument is + * used for the first {@literal @}Assisted constructor parameter, etc.. Annotation names have no + * effect. + * + *

Returned values are not created by Guice. These types are not eligible for + * method interception. They do receive post-construction member injection. + * + * @param The factory interface + * @deprecated use {@link FactoryModuleBuilder} instead. + */ +@Deprecated +public class FactoryProvider implements Provider, HasDependencies { + + /* + * This class implements the old @AssistedInject implementation that manually matches constructors + * to factory methods. The new child injector implementation lives in FactoryProvider2. + */ + + private final TypeLiteral factoryType; + private final TypeLiteral implementationType; + private final Map> factoryMethodToConstructor; + private Injector injector; + + private FactoryProvider(TypeLiteral factoryType, + TypeLiteral implementationType, + Map> factoryMethodToConstructor) { + this.factoryType = factoryType; + this.implementationType = implementationType; + this.factoryMethodToConstructor = factoryMethodToConstructor; + checkDeclaredExceptionsMatch(); + } + + public static Provider newFactory(Class factoryType, Class implementationType) { + return newFactory(TypeLiteral.get(factoryType), TypeLiteral.get(implementationType)); + } + + public static Provider newFactory( + TypeLiteral factoryType, TypeLiteral implementationType) { + Map> factoryMethodToConstructor + = createMethodMapping(factoryType, implementationType); + + if (!factoryMethodToConstructor.isEmpty()) { + return new FactoryProvider(factoryType, implementationType, factoryMethodToConstructor); + } else { + BindingCollector collector = new BindingCollector(); + + // Preserving backwards-compatibility: Map all return types in a factory + // interface to the passed implementation type. + Errors errors = new Errors(); + Key implementationKey = Key.get(implementationType); + + try { + for (Method method : factoryType.getRawType().getMethods()) { + Key returnType = getKey(factoryType.getReturnType(method), method, + method.getAnnotations(), errors); + if (!implementationKey.equals(returnType)) { + collector.addBinding(returnType, implementationType); + } + } + } catch (ErrorsException e) { + throw new ConfigurationException(e.getErrors().getMessages()); + } + + return new FactoryProvider2(Key.get(factoryType), collector); + } + } + + private static Map> createMethodMapping( + TypeLiteral factoryType, TypeLiteral implementationType) { + List> constructors = Lists.newArrayList(); + + for (Constructor constructor : implementationType.getRawType().getDeclaredConstructors()) { + if (constructor.isAnnotationPresent(AssistedInject.class)) { + AssistedConstructor assistedConstructor = AssistedConstructor.create( + constructor, implementationType.getParameterTypes(constructor)); + constructors.add(assistedConstructor); + } + } + + if (constructors.isEmpty()) { + return ImmutableMap.of(); + } + + Method[] factoryMethods = factoryType.getRawType().getMethods(); + + if (constructors.size() != factoryMethods.length) { + throw newConfigurationException("Constructor mismatch: %s has %s @AssistedInject " + + "constructors, factory %s has %s creation methods", implementationType, + constructors.size(), factoryType, factoryMethods.length); + } + + Map> paramsToConstructor = Maps.newHashMap(); + + for (AssistedConstructor c : constructors) { + if (paramsToConstructor.containsKey(c.getAssistedParameters())) { + throw new RuntimeException("Duplicate constructor, " + c); + } + paramsToConstructor.put(c.getAssistedParameters(), c); + } + + Map> result = Maps.newHashMap(); + for (Method method : factoryMethods) { + if (!method.getReturnType().isAssignableFrom(implementationType.getRawType())) { + throw newConfigurationException("Return type of method %s is not assignable from %s", + method, implementationType); + } + + List parameterTypes = Lists.newArrayList(); + for (TypeLiteral parameterType : factoryType.getParameterTypes(method)) { + parameterTypes.add(parameterType.getType()); + } + ParameterListKey methodParams = new ParameterListKey(parameterTypes); + + if (!paramsToConstructor.containsKey(methodParams)) { + throw newConfigurationException("%s has no @AssistInject constructor that takes the " + + "@Assisted parameters %s in that order. @AssistInject constructors are %s", + implementationType, methodParams, paramsToConstructor.values()); + } + + method.getParameterAnnotations(); + for (Annotation[] parameterAnnotations : method.getParameterAnnotations()) { + for (Annotation parameterAnnotation : parameterAnnotations) { + if (parameterAnnotation.annotationType() == Assisted.class) { + throw newConfigurationException("Factory method %s has an @Assisted parameter, which " + + "is incompatible with the deprecated @AssistedInject annotation. Please replace " + + "@AssistedInject with @Inject on the %s constructor.", + method, implementationType); + } + } + } + + AssistedConstructor matchingConstructor = paramsToConstructor.remove(methodParams); + + result.put(method, matchingConstructor); + } + return result; + } + + private static ConfigurationException newConfigurationException(String format, Object... args) { + return new ConfigurationException(ImmutableSet.of(new Message(Errors.format(format, args)))); + } + + @Inject + void setInjectorAndCheckUnboundParametersAreInjectable(Injector injector) { + this.injector = injector; + for (AssistedConstructor c : factoryMethodToConstructor.values()) { + for (Parameter p : c.getAllParameters()) { + if (!p.isProvidedByFactory() && !paramCanBeInjected(p, injector)) { + // this is lame - we're not using the proper mechanism to add an + // error to the injector. Throughout this class we throw exceptions + // to add errors, which isn't really the best way in Guice + throw newConfigurationException("Parameter of type '%s' is not injectable or annotated " + + "with @Assisted for Constructor '%s'", p, c); + } + } + } + } + + private void checkDeclaredExceptionsMatch() { + for (Map.Entry> entry : factoryMethodToConstructor.entrySet()) { + for (Class constructorException : entry.getValue().getDeclaredExceptions()) { + if (!isConstructorExceptionCompatibleWithFactoryExeception( + constructorException, entry.getKey().getExceptionTypes())) { + throw newConfigurationException("Constructor %s declares an exception, but no compatible " + + "exception is thrown by the factory method %s", entry.getValue(), entry.getKey()); + } + } + } + } + + private boolean isConstructorExceptionCompatibleWithFactoryExeception( + Class constructorException, Class[] factoryExceptions) { + for (Class factoryException : factoryExceptions) { + if (factoryException.isAssignableFrom(constructorException)) { + return true; + } + } + return false; + } + + private boolean paramCanBeInjected(Parameter parameter, Injector injector) { + return parameter.isBound(injector); + } + + public Set> getDependencies() { + List> dependencies = Lists.newArrayList(); + for (AssistedConstructor constructor : factoryMethodToConstructor.values()) { + for (Parameter parameter : constructor.getAllParameters()) { + if (!parameter.isProvidedByFactory()) { + dependencies.add(Dependency.get(parameter.getPrimaryBindingKey())); + } + } + } + return ImmutableSet.copyOf(dependencies); + } + + public F get() { + InvocationHandler invocationHandler = new InvocationHandler() { + public Object invoke(Object proxy, Method method, Object[] creationArgs) throws Throwable { + // pass methods from Object.class to the proxy + if (method.getDeclaringClass().equals(Object.class)) { + if ("equals".equals(method.getName())) { + return proxy == creationArgs[0]; + } else if ("hashCode".equals(method.getName())) { + return System.identityHashCode(proxy); + } else { + return method.invoke(this, creationArgs); + } + } + + AssistedConstructor constructor = factoryMethodToConstructor.get(method); + Object[] constructorArgs = gatherArgsForConstructor(constructor, creationArgs); + Object objectToReturn = constructor.newInstance(constructorArgs); + injector.injectMembers(objectToReturn); + return objectToReturn; + } + + public Object[] gatherArgsForConstructor( + AssistedConstructor constructor, + Object[] factoryArgs) { + int numParams = constructor.getAllParameters().size(); + int argPosition = 0; + Object[] result = new Object[numParams]; + + for (int i = 0; i < numParams; i++) { + Parameter parameter = constructor.getAllParameters().get(i); + if (parameter.isProvidedByFactory()) { + result[i] = factoryArgs[argPosition]; + argPosition++; + } else { + result[i] = parameter.getValue(injector); + } + } + return result; + } + }; + + @SuppressWarnings("unchecked") // we imprecisely treat the class literal of T as a Class + Class factoryRawType = (Class) (Class) factoryType.getRawType(); + return factoryRawType.cast(Proxy.newProxyInstance(factoryRawType.getClassLoader(), + new Class[]{factoryRawType}, invocationHandler)); + } + + @Override + public int hashCode() { + return Objects.hashCode(factoryType, implementationType); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof FactoryProvider)) { + return false; + } + FactoryProvider other = (FactoryProvider) obj; + return factoryType.equals(other.factoryType) + && implementationType.equals(other.implementationType); + } +} \ No newline at end of file diff --git a/src/main/java/com/google/inject/assistedinject/FactoryProvider2.java b/src/main/java/com/google/inject/assistedinject/FactoryProvider2.java new file mode 100644 index 0000000..b637c92 --- /dev/null +++ b/src/main/java/com/google/inject/assistedinject/FactoryProvider2.java @@ -0,0 +1,932 @@ +package com.google.inject.assistedinject; + +import com.google.common.base.Objects; +import com.google.common.collect.HashMultimap; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; +import com.google.common.collect.Multimap; +import com.google.common.collect.Sets; +import com.google.inject.AbstractModule; +import com.google.inject.Binder; +import com.google.inject.Binding; +import com.google.inject.ConfigurationException; +import com.google.inject.Inject; +import com.google.inject.Injector; +import com.google.inject.Key; +import com.google.inject.Module; +import com.google.inject.Provider; +import com.google.inject.ProvisionException; +import com.google.inject.Scopes; +import com.google.inject.TypeLiteral; +import com.google.inject.internal.Annotations; +import com.google.inject.internal.Errors; +import com.google.inject.internal.ErrorsException; +import com.google.inject.internal.UniqueAnnotations; +import com.google.inject.internal.util.Classes; +import com.google.inject.spi.BindingTargetVisitor; +import com.google.inject.spi.Dependency; +import com.google.inject.spi.HasDependencies; +import com.google.inject.spi.InjectionPoint; +import com.google.inject.spi.Message; +import com.google.inject.spi.ProviderInstanceBinding; +import com.google.inject.spi.ProviderWithExtensionVisitor; +import com.google.inject.spi.Toolable; +import com.google.inject.util.Providers; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.Proxy; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; + +import static com.google.common.base.Preconditions.checkState; +import static com.google.common.collect.Iterables.getOnlyElement; + +/** + * The newer implementation of factory provider. This implementation uses a child injector to + * create values. + */ +final class FactoryProvider2 implements InvocationHandler, + ProviderWithExtensionVisitor, HasDependencies, AssistedInjectBinding { + + /** + * A constant annotation to denote the return value, instead of creating a new one each time. + */ + static final Annotation RETURN_ANNOTATION = UniqueAnnotations.create(); + + // use the logger under a well-known name, not FactoryProvider2 + static final Logger logger = Logger.getLogger(AssistedInject.class.getName()); + + /** + * if a factory method parameter isn't annotated, it gets this annotation. + */ + static final Assisted DEFAULT_ANNOTATION = new Assisted() { + public String value() { + return ""; + } + + public Class annotationType() { + return Assisted.class; + } + + @Override + public boolean equals(Object o) { + return o instanceof Assisted && ((Assisted) o).value().isEmpty(); + } + + @Override + public int hashCode() { + return 127 * "value".hashCode() ^ "".hashCode(); + } + + @Override + public String toString() { + return "@" + Assisted.class.getName() + "(value=)"; + } + }; + /** + * Mapping from method to the data about how the method will be assisted. + */ + private final ImmutableMap assistDataByMethod; + /** + * Mapping from method to method handle, for generated default methods. + */ + private final ImmutableMap methodHandleByMethod; + /** + * the factory interface, implemented and provided + */ + private final F factory; + /** + * The key that this is bound to. + */ + private final Key factoryKey; + /** + * The binding collector, for equality/hashing purposes. + */ + private final BindingCollector collector; + /** + * the hosting injector, or null if we haven't been initialized yet + */ + private Injector injector; + + /** + * @param factoryKey a key for a Java interface that defines one or more create methods. + * @param collector binding configuration that maps method return types to + * implementation types. + */ + FactoryProvider2(Key factoryKey, BindingCollector collector) { + this.factoryKey = factoryKey; + this.collector = collector; + + TypeLiteral factoryType = factoryKey.getTypeLiteral(); + Errors errors = new Errors(); + + @SuppressWarnings("unchecked") // we imprecisely treat the class literal of T as a Class + Class factoryRawType = (Class) (Class) factoryType.getRawType(); + + try { + if (!factoryRawType.isInterface()) { + throw errors.addMessage("%s must be an interface.", factoryRawType).toException(); + } + + Multimap defaultMethods = HashMultimap.create(); + Multimap otherMethods = HashMultimap.create(); + ImmutableMap.Builder assistDataBuilder = ImmutableMap.builder(); + // TODO: also grab methods from superinterfaces + for (Method method : factoryRawType.getMethods()) { + // Skip default methods that java8 may have created. + if (isDefault(method) && (method.isBridge() || method.isSynthetic())) { + // Even synthetic default methods need the return type validation... + // unavoidable consequence of javac8. :-( + validateFactoryReturnType(errors, method.getReturnType(), factoryRawType); + defaultMethods.put(method.getName(), method); + continue; + } + otherMethods.put(method.getName(), method); + + TypeLiteral returnTypeLiteral = factoryType.getReturnType(method); + Key returnType; + try { + returnType = Annotations.getKey(returnTypeLiteral, method, method.getAnnotations(), errors); + } catch (ConfigurationException ce) { + // If this was an error due to returnTypeLiteral not being specified, rephrase + // it as our factory not being specified, so it makes more sense to users. + if (isTypeNotSpecified(returnTypeLiteral, ce)) { + throw errors.keyNotFullySpecified(TypeLiteral.get(factoryRawType)).toException(); + } else { + throw ce; + } + } + validateFactoryReturnType(errors, returnType.getTypeLiteral().getRawType(), factoryRawType); + List> params = factoryType.getParameterTypes(method); + Annotation[][] paramAnnotations = method.getParameterAnnotations(); + int p = 0; + List> keys = Lists.newArrayList(); + for (TypeLiteral param : params) { + Key paramKey = Annotations.getKey(param, method, paramAnnotations[p++], errors); + Class underlylingType = paramKey.getTypeLiteral().getRawType(); + if (underlylingType.equals(Provider.class) + || underlylingType.equals(javax.inject.Provider.class)) { + errors.addMessage("A Provider may not be a type in a factory method of an AssistedInject." + + "\n Offending instance is parameter [%s] with key [%s] on method [%s]", + p, paramKey, method); + } + keys.add(assistKey(method, paramKey, errors)); + } + ImmutableList> immutableParamList = ImmutableList.copyOf(keys); + + // try to match up the method to the constructor + TypeLiteral implementation = collector.getBindings().get(returnType); + if (implementation == null) { + implementation = returnType.getTypeLiteral(); + } + Class scope = + Annotations.findScopeAnnotation(errors, implementation.getRawType()); + if (scope != null) { + errors.addMessage("Found scope annotation [%s] on implementation class " + + "[%s] of AssistedInject factory [%s].\nThis is not allowed, please" + + " remove the scope annotation.", + scope, implementation.getRawType(), factoryType); + } + + InjectionPoint ctorInjectionPoint; + try { + ctorInjectionPoint = + findMatchingConstructorInjectionPoint(method, returnType, implementation, immutableParamList); + } catch (ErrorsException ee) { + errors.merge(ee.getErrors()); + continue; + } + + Constructor constructor = (Constructor) ctorInjectionPoint.getMember(); + List providers = Collections.emptyList(); + Set> deps = getDependencies(ctorInjectionPoint, implementation); + boolean optimized = false; + // Now go through all dependencies of the implementation and see if it is OK to + // use an optimized form of assistedinject2. The optimized form requires that + // all injections directly inject the object itself (and not a Provider of the object, + // or an Injector), because it caches a single child injector and mutates the Provider + // of the arguments in a ThreadLocal. + if (isValidForOptimizedAssistedInject(deps, implementation.getRawType(), factoryType)) { + ImmutableList.Builder providerListBuilder = ImmutableList.builder(); + for (int i = 0; i < params.size(); i++) { + providerListBuilder.add(new ThreadLocalProvider()); + } + providers = providerListBuilder.build(); + optimized = true; + } + + AssistData data = new AssistData(constructor, + returnType, + immutableParamList, + implementation, + method, + removeAssistedDeps(deps), + optimized, + providers); + assistDataBuilder.put(method, data); + } + + factory = factoryRawType.cast(Proxy.newProxyInstance( + factoryRawType.getClassLoader(), new Class[]{factoryRawType}, this)); + + // Now go back through default methods. Try to use MethodHandles to make things + // work. If that doesn't work, fallback to trying to find compatible method + // signatures. + Map dataSoFar = assistDataBuilder.build(); + ImmutableMap.Builder methodHandleBuilder = ImmutableMap.builder(); + for (Map.Entry entry : defaultMethods.entries()) { + Method defaultMethod = entry.getValue(); + MethodHandleWrapper handle = MethodHandleWrapper.create(defaultMethod, factory); + if (handle != null) { + methodHandleBuilder.put(defaultMethod, handle); + } else { + boolean foundMatch = false; + for (Method otherMethod : otherMethods.get(defaultMethod.getName())) { + if (dataSoFar.containsKey(otherMethod) && isCompatible(defaultMethod, otherMethod)) { + if (foundMatch) { + errors.addMessage("Generated default method %s with parameters %s is" + + " signature-compatible with more than one non-default method." + + " Unable to create factory. As a workaround, remove the override" + + " so javac stops generating a default method.", + defaultMethod, Arrays.asList(defaultMethod.getParameterTypes())); + } else { + assistDataBuilder.put(defaultMethod, dataSoFar.get(otherMethod)); + foundMatch = true; + } + } + } + if (!foundMatch) { + throw new IllegalStateException("Can't find method compatible with: " + defaultMethod); + } + } + } + + // If we generated any errors (from finding matching constructors, for instance), throw an exception. + if (errors.hasErrors()) { + throw errors.toException(); + } + + assistDataByMethod = assistDataBuilder.build(); + methodHandleByMethod = methodHandleBuilder.build(); + } catch (ErrorsException e) { + throw new ConfigurationException(e.getErrors().getMessages()); + } + } + + static boolean isDefault(Method method) { + // Per the javadoc, default methods are non-abstract, public, non-static. + // They're also in interfaces, but we can guarantee that already since we only act + // on interfaces. + return (method.getModifiers() & (Modifier.ABSTRACT | Modifier.PUBLIC | Modifier.STATIC)) + == Modifier.PUBLIC; + } + + /** + * Returns true if {@code thrown} can be thrown by {@code invoked} without wrapping. + */ + static boolean canRethrow(Method invoked, Throwable thrown) { + if (thrown instanceof Error || thrown instanceof RuntimeException) { + return true; + } + + for (Class declared : invoked.getExceptionTypes()) { + if (declared.isInstance(thrown)) { + return true; + } + } + + return false; + } + + private boolean isCompatible(Method src, Method dst) { + if (!src.getReturnType().isAssignableFrom(dst.getReturnType())) { + return false; + } + Class[] srcParams = src.getParameterTypes(); + Class[] dstParams = dst.getParameterTypes(); + if (srcParams.length != dstParams.length) { + return false; + } + for (int i = 0; i < srcParams.length; i++) { + if (!srcParams[i].isAssignableFrom(dstParams[i])) { + return false; + } + } + return true; + } + + public F get() { + return factory; + } + + public Set> getDependencies() { + Set> combinedDeps = new HashSet>(); + for (AssistData data : assistDataByMethod.values()) { + combinedDeps.addAll(data.dependencies); + } + return ImmutableSet.copyOf(combinedDeps); + } + + public Key getKey() { + return factoryKey; + } + + // Safe cast because values are typed to AssistedData, which is an AssistedMethod, and + // the collection is immutable. + @SuppressWarnings("unchecked") + public Collection getAssistedMethods() { + return (Collection) (Collection) assistDataByMethod.values(); + } + + @SuppressWarnings("unchecked") + public V acceptExtensionVisitor(BindingTargetVisitor visitor, + ProviderInstanceBinding binding) { + if (visitor instanceof AssistedInjectTargetVisitor) { + return ((AssistedInjectTargetVisitor) visitor).visit((AssistedInjectBinding) this); + } + return visitor.visit(binding); + } + + private void validateFactoryReturnType(Errors errors, Class returnType, Class factoryType) { + if (Modifier.isPublic(factoryType.getModifiers()) + && !Modifier.isPublic(returnType.getModifiers())) { + errors.addMessage("%s is public, but has a method that returns a non-public type: %s. " + + "Due to limitations with java.lang.reflect.Proxy, this is not allowed. " + + "Please either make the factory non-public or the return type public.", + factoryType, returnType); + } + } + + /** + * Returns true if the ConfigurationException is due to an error of TypeLiteral not being fully + * specified. + */ + private boolean isTypeNotSpecified(TypeLiteral typeLiteral, ConfigurationException ce) { + Collection messages = ce.getErrorMessages(); + if (messages.size() == 1) { + Message msg = Iterables.getOnlyElement( + new Errors().keyNotFullySpecified(typeLiteral).getMessages()); + return msg.getMessage().equals(Iterables.getOnlyElement(messages).getMessage()); + } else { + return false; + } + } + + /** + * Finds a constructor suitable for the method. If the implementation contained any constructors + * marked with {@link AssistedInject}, this requires all {@link Assisted} parameters to exactly + * match the parameters (in any order) listed in the method. Otherwise, if no + * {@link AssistedInject} constructors exist, this will default to looking for an + * {@literal @}{@link Inject} constructor. + */ + private InjectionPoint findMatchingConstructorInjectionPoint( + Method method, Key returnType, TypeLiteral implementation, List> paramList) + throws ErrorsException { + Errors errors = new Errors(method); + if (returnType.getTypeLiteral().equals(implementation)) { + errors = errors.withSource(implementation); + } else { + errors = errors.withSource(returnType).withSource(implementation); + } + + Class rawType = implementation.getRawType(); + if (Modifier.isInterface(rawType.getModifiers())) { + errors.addMessage( + "%s is an interface, not a concrete class. Unable to create AssistedInject factory.", + implementation); + throw errors.toException(); + } else if (Modifier.isAbstract(rawType.getModifiers())) { + errors.addMessage( + "%s is abstract, not a concrete class. Unable to create AssistedInject factory.", + implementation); + throw errors.toException(); + } else if (Classes.isInnerClass(rawType)) { + errors.cannotInjectInnerClass(rawType); + throw errors.toException(); + } + + Constructor matchingConstructor = null; + boolean anyAssistedInjectConstructors = false; + // Look for AssistedInject constructors... + for (Constructor constructor : rawType.getDeclaredConstructors()) { + if (constructor.isAnnotationPresent(AssistedInject.class)) { + anyAssistedInjectConstructors = true; + if (constructorHasMatchingParams(implementation, constructor, paramList, errors)) { + if (matchingConstructor != null) { + errors + .addMessage( + "%s has more than one constructor annotated with @AssistedInject" + + " that matches the parameters in method %s. Unable to create " + + "AssistedInject factory.", + implementation, method); + throw errors.toException(); + } else { + matchingConstructor = constructor; + } + } + } + } + + if (!anyAssistedInjectConstructors) { + // If none existed, use @Inject. + try { + return InjectionPoint.forConstructorOf(implementation); + } catch (ConfigurationException e) { + errors.merge(e.getErrorMessages()); + throw errors.toException(); + } + } else { + // Otherwise, use it or fail with a good error message. + if (matchingConstructor != null) { + // safe because we got the constructor from this implementation. + @SuppressWarnings("unchecked") + InjectionPoint ip = InjectionPoint.forConstructor( + (Constructor) matchingConstructor, implementation); + return ip; + } else { + errors.addMessage( + "%s has @AssistedInject constructors, but none of them match the" + + " parameters in method %s. Unable to create AssistedInject factory.", + implementation, method); + throw errors.toException(); + } + } + } + + /** + * Matching logic for constructors annotated with AssistedInject. + * This returns true if and only if all @Assisted parameters in the + * constructor exactly match (in any order) all @Assisted parameters + * the method's parameter. + */ + private boolean constructorHasMatchingParams(TypeLiteral type, + Constructor constructor, List> paramList, Errors errors) + throws ErrorsException { + List> params = type.getParameterTypes(constructor); + Annotation[][] paramAnnotations = constructor.getParameterAnnotations(); + int p = 0; + List> constructorKeys = Lists.newArrayList(); + for (TypeLiteral param : params) { + Key paramKey = Annotations.getKey(param, constructor, paramAnnotations[p++], + errors); + constructorKeys.add(paramKey); + } + // Require that every key exist in the constructor to match up exactly. + for (Key key : paramList) { + // If it didn't exist in the constructor set, we can't use it. + if (!constructorKeys.remove(key)) { + return false; + } + } + // If any keys remain and their annotation is Assisted, we can't use it. + for (Key key : constructorKeys) { + if (key.getAnnotationType() == Assisted.class) { + return false; + } + } + // All @Assisted params match up to the method's parameters. + return true; + } + + /** + * Calculates all dependencies required by the implementation and constructor. + */ + private Set> getDependencies(InjectionPoint ctorPoint, TypeLiteral implementation) { + ImmutableSet.Builder> builder = ImmutableSet.builder(); + builder.addAll(ctorPoint.getDependencies()); + if (!implementation.getRawType().isInterface()) { + for (InjectionPoint ip : InjectionPoint.forInstanceMethodsAndFields(implementation)) { + builder.addAll(ip.getDependencies()); + } + } + return builder.build(); + } + + /** + * Return all non-assisted dependencies. + */ + private Set> removeAssistedDeps(Set> deps) { + ImmutableSet.Builder> builder = ImmutableSet.builder(); + for (Dependency dep : deps) { + Class annotationType = dep.getKey().getAnnotationType(); + if (annotationType == null || !annotationType.equals(Assisted.class)) { + builder.add(dep); + } + } + return builder.build(); + } + + /** + * Returns true if all dependencies are suitable for the optimized version of AssistedInject. The + * optimized version caches the binding & uses a ThreadLocal Provider, so can only be applied if + * the assisted bindings are immediately provided. This looks for hints that the values may be + * lazily retrieved, by looking for injections of Injector or a Provider for the assisted values. + */ + private boolean isValidForOptimizedAssistedInject(Set> dependencies, + Class implementation, TypeLiteral factoryType) { + Set> badDeps = null; // optimization: create lazily + for (Dependency dep : dependencies) { + if (isInjectorOrAssistedProvider(dep)) { + if (badDeps == null) { + badDeps = Sets.newHashSet(); + } + badDeps.add(dep); + } + } + if (badDeps != null && !badDeps.isEmpty()) { + logger.log(Level.WARNING, "AssistedInject factory {0} will be slow " + + "because {1} has assisted Provider dependencies or injects the Injector. " + + "Stop injecting @Assisted Provider (instead use @Assisted T) " + + "or Injector to speed things up. (It will be a ~6500% speed bump!) " + + "The exact offending deps are: {2}", + new Object[]{factoryType, implementation, badDeps}); + return false; + } + return true; + } + + /** + * Returns true if the dependency is for {@link Injector} or if the dependency + * is a {@link Provider} for a parameter that is {@literal @}{@link Assisted}. + */ + private boolean isInjectorOrAssistedProvider(Dependency dependency) { + Class annotationType = dependency.getKey().getAnnotationType(); + if (annotationType != null && annotationType.equals(Assisted.class)) { // If it's assisted.. + if (dependency.getKey().getTypeLiteral().getRawType().equals(Provider.class)) { // And a Provider... + return true; + } + } else if (dependency.getKey().getTypeLiteral().getRawType().equals(Injector.class)) { // If it's the Injector... + return true; + } + return false; + } + + /** + * Returns a key similar to {@code key}, but with an {@literal @}Assisted binding annotation. + * This fails if another binding annotation is clobbered in the process. If the key already has + * the {@literal @}Assisted annotation, it is returned as-is to preserve any String value. + */ + private Key assistKey(Method method, Key key, Errors errors) throws ErrorsException { + if (key.getAnnotationType() == null) { + return Key.get(key.getTypeLiteral(), DEFAULT_ANNOTATION); + } else if (key.getAnnotationType() == Assisted.class) { + return key; + } else { + errors.withSource(method).addMessage( + "Only @Assisted is allowed for factory parameters, but found @%s", + key.getAnnotationType()); + throw errors.toException(); + } + } + + /** + * At injector-creation time, we initialize the invocation handler. At this time we make sure + * all factory methods will be able to build the target types. + */ + @Inject + @Toolable + void initialize(Injector injector) { + if (this.injector != null) { + throw new ConfigurationException(ImmutableList.of(new Message(FactoryProvider2.class, + "Factories.create() factories may only be used in one Injector!"))); + } + + this.injector = injector; + + for (Map.Entry entry : assistDataByMethod.entrySet()) { + Method method = entry.getKey(); + AssistData data = entry.getValue(); + Object[] args; + if (!data.optimized) { + args = new Object[method.getParameterTypes().length]; + Arrays.fill(args, "dummy object for validating Factories"); + } else { + args = null; // won't be used -- instead will bind to data.providers. + } + getBindingFromNewInjector(method, args, data); // throws if the binding isn't properly configured + } + } + + /** + * Creates a child injector that binds the args, and returns the binding for the method's result. + */ + public Binding getBindingFromNewInjector( + final Method method, final Object[] args, final AssistData data) { + checkState(injector != null, + "Factories.create() factories cannot be used until they're initialized by Guice."); + + final Key returnType = data.returnType; + + // We ignore any pre-existing binding annotation. + final Key returnKey = Key.get(returnType.getTypeLiteral(), RETURN_ANNOTATION); + + Module assistedModule = new AbstractModule() { + @Override + @SuppressWarnings({ + "unchecked", "rawtypes"}) // raw keys are necessary for the args array and return value + protected void configure() { + Binder binder = binder().withSource(method); + + int p = 0; + if (!data.optimized) { + for (Key paramKey : data.paramTypes) { + // Wrap in a Provider to cover null, and to prevent Guice from injecting the parameter + binder.bind((Key) paramKey).toProvider(Providers.of(args[p++])); + } + } else { + for (Key paramKey : data.paramTypes) { + // Bind to our ThreadLocalProviders. + binder.bind((Key) paramKey).toProvider(data.providers.get(p++)); + } + } + + Constructor constructor = data.constructor; + // Constructor *should* always be non-null here, + // but if it isn't, we'll end up throwing a fairly good error + // message for the user. + if (constructor != null) { + binder.bind(returnKey) + .toConstructor(constructor, (TypeLiteral) data.implementationType) + .in(Scopes.NO_SCOPE); // make sure we erase any scope on the implementation type + } + } + }; + + Injector forCreate = injector.createChildInjector(assistedModule); + Binding binding = forCreate.getBinding(returnKey); + // If we have providers cached in data, cache the binding for future optimizations. + if (data.optimized) { + data.cachedBinding = binding; + } + return binding; + } + + /** + * When a factory method is invoked, we create a child injector that binds all parameters, then + * use that to get an instance of the return type. + */ + public Object invoke(Object proxy, final Method method, final Object[] args) throws Throwable { + // If we setup a method handle earlier for this method, call it. + // This is necessary for default methods that java8 creates, so we + // can call the default method implementation (and not our proxied version of it). + if (methodHandleByMethod.containsKey(method)) { + return methodHandleByMethod.get(method).invokeWithArguments(args); + } + + if (method.getDeclaringClass().equals(Object.class)) { + if ("equals".equals(method.getName())) { + return proxy == args[0]; + } else if ("hashCode".equals(method.getName())) { + return System.identityHashCode(proxy); + } else { + return method.invoke(this, args); + } + } + + AssistData data = assistDataByMethod.get(method); + checkState(data != null, "No data for method: %s", method); + Provider provider; + if (data.cachedBinding != null) { // Try to get optimized form... + provider = data.cachedBinding.getProvider(); + } else { + provider = getBindingFromNewInjector(method, args, data).getProvider(); + } + try { + int p = 0; + for (ThreadLocalProvider tlp : data.providers) { + tlp.set(args[p++]); + } + return provider.get(); + } catch (ProvisionException e) { + // if this is an exception declared by the factory method, throw it as-is + if (e.getErrorMessages().size() == 1) { + Message onlyError = getOnlyElement(e.getErrorMessages()); + Throwable cause = onlyError.getCause(); + if (cause != null && canRethrow(method, cause)) { + throw cause; + } + } + throw e; + } finally { + for (ThreadLocalProvider tlp : data.providers) { + tlp.remove(); + } + } + } + + @Override + public String toString() { + return factory.getClass().getInterfaces()[0].getName(); + } + + @Override + public int hashCode() { + return Objects.hashCode(factoryKey, collector); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof FactoryProvider2)) { + return false; + } + FactoryProvider2 other = (FactoryProvider2) obj; + return factoryKey.equals(other.factoryKey) && Objects.equal(collector, other.collector); + } + + /** + * All the data necessary to perform an assisted inject. + */ + private static class AssistData implements AssistedMethod { + /** + * the constructor the implementation is constructed with. + */ + final Constructor constructor; + /** + * the return type in the factory method that the constructor is bound to. + */ + final Key returnType; + /** + * the parameters in the factory method associated with this data. + */ + final ImmutableList> paramTypes; + /** + * the type of the implementation constructed + */ + final TypeLiteral implementationType; + + /** + * All non-assisted dependencies required by this method. + */ + final Set> dependencies; + /** + * The factory method associated with this data + */ + final Method factoryMethod; + + /** + * true if {@link #isValidForOptimizedAssistedInject} returned true. + */ + final boolean optimized; + /** + * the list of optimized providers, empty if not optimized. + */ + final List providers; + /** + * used to perform optimized factory creations. + */ + volatile Binding cachedBinding; // TODO: volatile necessary? + + AssistData(Constructor constructor, Key returnType, ImmutableList> paramTypes, + TypeLiteral implementationType, Method factoryMethod, + Set> dependencies, + boolean optimized, List providers) { + this.constructor = constructor; + this.returnType = returnType; + this.paramTypes = paramTypes; + this.implementationType = implementationType; + this.factoryMethod = factoryMethod; + this.dependencies = dependencies; + this.optimized = optimized; + this.providers = providers; + } + + @Override + public String toString() { + return Objects.toStringHelper(getClass()) + .add("ctor", constructor) + .add("return type", returnType) + .add("param type", paramTypes) + .add("implementation type", implementationType) + .add("dependencies", dependencies) + .add("factory method", factoryMethod) + .add("optimized", optimized) + .add("providers", providers) + .add("cached binding", cachedBinding) + .toString(); + } + + public Set> getDependencies() { + return dependencies; + } + + public Method getFactoryMethod() { + return factoryMethod; + } + + public Constructor getImplementationConstructor() { + return constructor; + } + + public TypeLiteral getImplementationType() { + return implementationType; + } + } + + // not because we'll never know and this is easier than suppressing warnings. + private static class ThreadLocalProvider extends ThreadLocal implements Provider { + @Override + protected Object initialValue() { + throw new IllegalStateException( + "Cannot use optimized @Assisted provider outside the scope of the constructor." + + " (This should never happen. If it does, please report it.)"); + } + } + + /** + * Wrapper around MethodHandles/MethodHandle, so we can compile+run on java6. + */ + private static class MethodHandleWrapper { + static final int ALL_MODES = Modifier.PRIVATE + | Modifier.STATIC /* package */ + | Modifier.PUBLIC + | Modifier.PROTECTED; + + static final Method unreflectSpecial; + static final Method bindTo; + static final Method invokeWithArguments; + static final Constructor lookupCxtor; + static final boolean valid; + + static { + Method unreflectSpecialTmp = null; + Method bindToTmp = null; + Method invokeWithArgumentsTmp = null; + boolean validTmp = false; + Constructor lookupCxtorTmp = null; + try { + Class lookupClass = Class.forName("java.lang.invoke.MethodHandles$Lookup"); + unreflectSpecialTmp = lookupClass.getMethod("unreflectSpecial", Method.class, Class.class); + Class methodHandleClass = Class.forName("java.lang.invoke.MethodHandle"); + bindToTmp = methodHandleClass.getMethod("bindTo", Object.class); + invokeWithArgumentsTmp = methodHandleClass.getMethod("invokeWithArguments", Object[].class); + lookupCxtorTmp = lookupClass.getDeclaredConstructor(Class.class, int.class); + lookupCxtorTmp.setAccessible(true); + validTmp = true; + } catch (Exception invalid) { + // Ignore the exception, store the values & exit early in create(..) if invalid. + } + + // Store refs to later. + valid = validTmp; + unreflectSpecial = unreflectSpecialTmp; + bindTo = bindToTmp; + invokeWithArguments = invokeWithArgumentsTmp; + lookupCxtor = lookupCxtorTmp; + } + + final Object handle; + + MethodHandleWrapper(Object handle) { + this.handle = handle; + } + + static MethodHandleWrapper create(Method method, Object proxy) { + if (!valid) { + return null; + } + try { + Class declaringClass = method.getDeclaringClass(); + // Note: this isn't a public API, but we need to use it in order to call default methods. + Object lookup = lookupCxtor.newInstance(declaringClass, ALL_MODES); + method.setAccessible(true); + // These are part of the public API, but we use reflection since we run on java6 + // and they were introduced in java7. + lookup = unreflectSpecial.invoke(lookup, method, declaringClass); + Object handle = bindTo.invoke(lookup, proxy); + return new MethodHandleWrapper(handle); + } catch (InvocationTargetException ite) { + return null; + } catch (IllegalAccessException iae) { + return null; + } catch (InstantiationException ie) { + return null; + } + } + + Object invokeWithArguments(Object[] args) throws Exception { + // We must cast the args to an object so the Object[] is the first param, + // as opposed to each individual varargs param. + return invokeWithArguments.invoke(handle, (Object) args); + } + + @Override + public String toString() { + return handle.toString(); + } + } +} diff --git a/src/main/java/com/google/inject/assistedinject/Parameter.java b/src/main/java/com/google/inject/assistedinject/Parameter.java new file mode 100644 index 0000000..336ca55 --- /dev/null +++ b/src/main/java/com/google/inject/assistedinject/Parameter.java @@ -0,0 +1,146 @@ +package com.google.inject.assistedinject; + +import com.google.inject.ConfigurationException; +import com.google.inject.Injector; +import com.google.inject.Key; +import com.google.inject.Provider; +import com.google.inject.internal.Annotations; + +import java.lang.annotation.Annotation; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; + +import static com.google.common.base.Preconditions.checkArgument; + +/** + * Models a method or constructor parameter. + */ +class Parameter { + + private final Type type; + private final boolean isAssisted; + private final Annotation bindingAnnotation; + private final boolean isProvider; + + private volatile Provider provider; + + public Parameter(Type type, Annotation[] annotations) { + this.type = type; + this.bindingAnnotation = getBindingAnnotation(annotations); + this.isAssisted = hasAssistedAnnotation(annotations); + this.isProvider = isProvider(type); + } + + public boolean isProvidedByFactory() { + return isAssisted; + } + + public Type getType() { + return type; + } + + @Override + public String toString() { + StringBuilder result = new StringBuilder(); + if (isAssisted) { + result.append("@Assisted "); + } + if (bindingAnnotation != null) { + result.append(bindingAnnotation).append(" "); + } + return result.append(type).toString(); + } + + private boolean hasAssistedAnnotation(Annotation[] annotations) { + for (Annotation annotation : annotations) { + if (annotation.annotationType().equals(Assisted.class)) { + return true; + } + } + return false; + } + + /** + * Returns the Guice {@link Key} for this parameter. + */ + public Object getValue(Injector injector) { + if (null == provider) { + synchronized (this) { + if (null == provider) { + provider = isProvider + ? injector.getProvider(getBindingForType(getProvidedType(type))) + : injector.getProvider(getPrimaryBindingKey()); + } + } + } + + return isProvider ? provider : provider.get(); + } + + public boolean isBound(Injector injector) { + return isBound(injector, getPrimaryBindingKey()) + || isBound(injector, fixAnnotations(getPrimaryBindingKey())); + } + + private boolean isBound(Injector injector, Key key) { + // This method is particularly lame - we really need an API that can test + // for any binding, implicit or explicit + try { + return injector.getBinding(key) != null; + } catch (ConfigurationException e) { + return false; + } + } + + /** + * Replace annotation instances with annotation types, this is only + * appropriate for testing if a key is bound and not for injecting. + * + * See Guice bug 125, + * https://github.com/google/guice/issues/125 + */ + public Key fixAnnotations(Key key) { + return key.getAnnotation() == null + ? key + : Key.get(key.getTypeLiteral(), key.getAnnotation().annotationType()); + } + + Key getPrimaryBindingKey() { + return isProvider + ? getBindingForType(getProvidedType(type)) + : getBindingForType(type); + } + + private Type getProvidedType(Type type) { + return ((ParameterizedType) type).getActualTypeArguments()[0]; + } + + private boolean isProvider(Type type) { + return type instanceof ParameterizedType + && ((ParameterizedType) type).getRawType() == Provider.class; + } + + private Key getBindingForType(Type type) { + return bindingAnnotation != null + ? Key.get(type, bindingAnnotation) + : Key.get(type); + } + + /** + * Returns the unique binding annotation from the specified list, or + * {@code null} if there are none. + * + * @throws IllegalStateException if multiple binding annotations exist. + */ + private Annotation getBindingAnnotation(Annotation[] annotations) { + Annotation bindingAnnotation = null; + for (Annotation annotation : annotations) { + if (Annotations.isBindingAnnotation(annotation.annotationType())) { + checkArgument(bindingAnnotation == null, + "Parameter has multiple binding annotations: %s and %s", bindingAnnotation, annotation); + bindingAnnotation = annotation; + } + } + return bindingAnnotation; + } +} diff --git a/src/main/java/com/google/inject/assistedinject/ParameterListKey.java b/src/main/java/com/google/inject/assistedinject/ParameterListKey.java new file mode 100644 index 0000000..766c69f --- /dev/null +++ b/src/main/java/com/google/inject/assistedinject/ParameterListKey.java @@ -0,0 +1,47 @@ +package com.google.inject.assistedinject; + +import com.google.inject.TypeLiteral; + +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * A list of {@link TypeLiteral}s to match an injectable Constructor's assited + * parameter types to the corresponding factory method. + */ +class ParameterListKey { + + private final List paramList; + + public ParameterListKey(List paramList) { + this.paramList = new ArrayList(paramList); + } + + public ParameterListKey(Type[] types) { + this(Arrays.asList(types)); + } + + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + if (!(o instanceof ParameterListKey)) { + return false; + } + ParameterListKey other = (ParameterListKey) o; + return paramList.equals(other.paramList); + } + + @Override + public int hashCode() { + return paramList.hashCode(); + } + + @Override + public String toString() { + return paramList.toString(); + } +} \ No newline at end of file diff --git a/src/main/java/com/google/inject/multibindings/ClassMapKey.java b/src/main/java/com/google/inject/multibindings/ClassMapKey.java new file mode 100644 index 0000000..0f28de3 --- /dev/null +++ b/src/main/java/com/google/inject/multibindings/ClassMapKey.java @@ -0,0 +1,19 @@ +package com.google.inject.multibindings; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * Allows {@literal @}{@link ProvidesIntoMap} to specify a class map key. + */ +@MapKey(unwrapValue = true) +@Documented +@Target(METHOD) +@Retention(RUNTIME) +public @interface ClassMapKey { + Class value(); +} diff --git a/src/main/java/com/google/inject/multibindings/Element.java b/src/main/java/com/google/inject/multibindings/Element.java new file mode 100644 index 0000000..031b1af --- /dev/null +++ b/src/main/java/com/google/inject/multibindings/Element.java @@ -0,0 +1,31 @@ +package com.google.inject.multibindings; + +import com.google.inject.BindingAnnotation; + +import java.lang.annotation.Retention; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * An internal binding annotation applied to each element in a multibinding. + * All elements are assigned a globally-unique id to allow different modules + * to contribute multibindings independently. + */ +@Retention(RUNTIME) +@BindingAnnotation +@interface Element { + + String setName(); + + int uniqueId(); + + Type type(); + + String keyType(); + + enum Type { + MAPBINDER, + MULTIBINDER, + OPTIONALBINDER; + } +} diff --git a/src/main/java/com/google/inject/multibindings/Indexer.java b/src/main/java/com/google/inject/multibindings/Indexer.java new file mode 100644 index 0000000..3086b9e --- /dev/null +++ b/src/main/java/com/google/inject/multibindings/Indexer.java @@ -0,0 +1,167 @@ +package com.google.inject.multibindings; + +import com.google.common.base.Objects; +import com.google.inject.Binding; +import com.google.inject.Injector; +import com.google.inject.Scope; +import com.google.inject.Scopes; +import com.google.inject.TypeLiteral; +import com.google.inject.spi.BindingScopingVisitor; +import com.google.inject.spi.ConstructorBinding; +import com.google.inject.spi.ConvertedConstantBinding; +import com.google.inject.spi.DefaultBindingTargetVisitor; +import com.google.inject.spi.ExposedBinding; +import com.google.inject.spi.InstanceBinding; +import com.google.inject.spi.LinkedKeyBinding; +import com.google.inject.spi.ProviderBinding; +import com.google.inject.spi.ProviderInstanceBinding; +import com.google.inject.spi.ProviderKeyBinding; +import com.google.inject.spi.UntargettedBinding; + +import java.lang.annotation.Annotation; + +/** + * Visits bindings to return a {@code IndexedBinding} that can be used to emulate the binding + * deduplication that Guice internally performs. + */ +class Indexer extends DefaultBindingTargetVisitor + implements BindingScopingVisitor { + private static final Object EAGER_SINGLETON = new Object(); + final Injector injector; + + Indexer(Injector injector) { + this.injector = injector; + } + + boolean isIndexable(Binding binding) { + return binding.getKey().getAnnotation() instanceof Element; + } + + private Object scope(Binding binding) { + return binding.acceptScopingVisitor(this); + } + + @Override + public IndexedBinding visit(ConstructorBinding binding) { + return new IndexedBinding(binding, BindingType.CONSTRUCTOR, scope(binding), + binding.getConstructor()); + } + + @Override + public IndexedBinding visit( + ConvertedConstantBinding binding) { + return new IndexedBinding(binding, BindingType.CONSTANT, scope(binding), + binding.getValue()); + } + + @Override + public IndexedBinding visit(ExposedBinding binding) { + return new IndexedBinding(binding, BindingType.EXPOSED, scope(binding), binding); + } + + @Override + public IndexedBinding visit(InstanceBinding binding) { + return new IndexedBinding(binding, BindingType.INSTANCE, scope(binding), + binding.getInstance()); + } + + @Override + public IndexedBinding visit(LinkedKeyBinding binding) { + return new IndexedBinding(binding, BindingType.LINKED_KEY, scope(binding), + binding.getLinkedKey()); + } + + @Override + public IndexedBinding visit(ProviderBinding binding) { + return new IndexedBinding(binding, BindingType.PROVIDED_BY, scope(binding), + injector.getBinding(binding.getProvidedKey())); + } + + @Override + public IndexedBinding visit(ProviderInstanceBinding binding) { + return new IndexedBinding(binding, BindingType.PROVIDER_INSTANCE, scope(binding), + binding.getUserSuppliedProvider()); + } + + @Override + public IndexedBinding visit(ProviderKeyBinding binding) { + return new IndexedBinding(binding, BindingType.PROVIDER_KEY, scope(binding), + binding.getProviderKey()); + } + + @Override + public IndexedBinding visit(UntargettedBinding binding) { + return new IndexedBinding(binding, BindingType.UNTARGETTED, scope(binding), null); + } + + @Override + public Object visitEagerSingleton() { + return EAGER_SINGLETON; + } + + @Override + public Object visitNoScoping() { + return Scopes.NO_SCOPE; + } + + @Override + public Object visitScope(Scope scope) { + return scope; + } + + @Override + public Object visitScopeAnnotation(Class scopeAnnotation) { + return scopeAnnotation; + } + + enum BindingType { + INSTANCE, + PROVIDER_INSTANCE, + PROVIDER_KEY, + LINKED_KEY, + UNTARGETTED, + CONSTRUCTOR, + CONSTANT, + EXPOSED, + PROVIDED_BY, + } + + static class IndexedBinding { + final String annotationName; + final Element.Type annotationType; + final TypeLiteral typeLiteral; + final Object scope; + final BindingType type; + final Object extraEquality; + + IndexedBinding(Binding binding, BindingType type, Object scope, Object extraEquality) { + this.scope = scope; + this.type = type; + this.extraEquality = extraEquality; + this.typeLiteral = binding.getKey().getTypeLiteral(); + Element annotation = (Element) binding.getKey().getAnnotation(); + this.annotationName = annotation.setName(); + this.annotationType = annotation.type(); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof IndexedBinding)) { + return false; + } + IndexedBinding o = (IndexedBinding) obj; + return type == o.type + && Objects.equal(scope, o.scope) + && typeLiteral.equals(o.typeLiteral) + && annotationType == o.annotationType + && annotationName.equals(o.annotationName) + && Objects.equal(extraEquality, o.extraEquality); + } + + @Override + public int hashCode() { + return Objects.hashCode(type, scope, typeLiteral, annotationType, annotationName, + extraEquality); + } + } +} diff --git a/src/main/java/com/google/inject/multibindings/MapBinder.java b/src/main/java/com/google/inject/multibindings/MapBinder.java new file mode 100644 index 0000000..8a40e7b --- /dev/null +++ b/src/main/java/com/google/inject/multibindings/MapBinder.java @@ -0,0 +1,873 @@ +package com.google.inject.multibindings; + +import com.google.common.base.Joiner; +import com.google.common.base.Objects; +import com.google.common.base.Supplier; +import com.google.common.collect.HashMultimap; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.common.collect.Multimap; +import com.google.common.collect.Multimaps; +import com.google.common.collect.Sets; +import com.google.inject.Binder; +import com.google.inject.Binding; +import com.google.inject.Inject; +import com.google.inject.Injector; +import com.google.inject.Key; +import com.google.inject.Module; +import com.google.inject.Provider; +import com.google.inject.TypeLiteral; +import com.google.inject.binder.LinkedBindingBuilder; +import com.google.inject.internal.Errors; +import com.google.inject.multibindings.Indexer.IndexedBinding; +import com.google.inject.multibindings.Multibinder.RealMultibinder; +import com.google.inject.spi.BindingTargetVisitor; +import com.google.inject.spi.Dependency; +import com.google.inject.spi.Element; +import com.google.inject.spi.HasDependencies; +import com.google.inject.spi.ProviderInstanceBinding; +import com.google.inject.spi.ProviderLookup; +import com.google.inject.spi.ProviderWithDependencies; +import com.google.inject.spi.ProviderWithExtensionVisitor; +import com.google.inject.spi.Toolable; +import com.google.inject.util.Types; + +import java.lang.annotation.Annotation; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import static com.google.inject.multibindings.Element.Type.MAPBINDER; +import static com.google.inject.multibindings.Multibinder.checkConfiguration; +import static com.google.inject.multibindings.Multibinder.checkNotNull; +import static com.google.inject.multibindings.Multibinder.setOf; +import static com.google.inject.util.Types.newParameterizedType; +import static com.google.inject.util.Types.newParameterizedTypeWithOwner; + +/** + * An API to bind multiple map entries separately, only to later inject them as + * a complete map. MapBinder is intended for use in your application's module: + *

+ * public class SnacksModule extends AbstractModule {
+ *   protected void configure() {
+ *     MapBinder<String, Snack> mapbinder
+ *         = MapBinder.newMapBinder(binder(), String.class, Snack.class);
+ *     mapbinder.addBinding("twix").toInstance(new Twix());
+ *     mapbinder.addBinding("snickers").toProvider(SnickersProvider.class);
+ *     mapbinder.addBinding("skittles").to(Skittles.class);
+ *   }
+ * }
+ * + *

With this binding, a {@link Map}{@code } can now be + * injected: + *


+ * class SnackMachine {
+ *   {@literal @}Inject
+ *   public SnackMachine(Map<String, Snack> snacks) { ... }
+ * }
+ * + *

In addition to binding {@code Map}, a mapbinder will also bind + * {@code Map>} for lazy value provision: + *


+ * class SnackMachine {
+ *   {@literal @}Inject
+ *   public SnackMachine(Map<String, Provider<Snack>> snackProviders) { ... }
+ * }
+ * + *

Contributing mapbindings from different modules is supported. For example, + * it is okay to have both {@code CandyModule} and {@code ChipsModule} both + * create their own {@code MapBinder}, and to each contribute + * bindings to the snacks map. When that map is injected, it will contain + * entries from both modules. + * + *

The map's iteration order is consistent with the binding order. This is + * convenient when multiple elements are contributed by the same module because + * that module can order its bindings appropriately. Avoid relying on the + * iteration order of elements contributed by different modules, since there is + * no equivalent mechanism to order modules. + * + *

The map is unmodifiable. Elements can only be added to the map by + * configuring the MapBinder. Elements can never be removed from the map. + * + *

Values are resolved at map injection time. If a value is bound to a + * provider, that provider's get method will be called each time the map is + * injected (unless the binding is also scoped, or a map of providers is injected). + * + *

Annotations are used to create different maps of the same key/value + * type. Each distinct annotation gets its own independent map. + * + *

Keys must be distinct. If the same key is bound more than + * once, map injection will fail. However, use {@link #permitDuplicates()} in + * order to allow duplicate keys; extra bindings to {@code Map>} and + * {@code Map>} will be added. + * + *

Keys must be non-null. {@code addBinding(null)} will + * throw an unchecked exception. + * + *

Values must be non-null to use map injection. If any + * value is null, map injection will fail (although injecting a map of providers + * will not). + */ +public abstract class MapBinder { + private MapBinder() { + } + + /** + * Returns a new mapbinder that collects entries of {@code keyType}/{@code valueType} in a + * {@link Map} that is itself bound with no binding annotation. + */ + public static MapBinder newMapBinder(Binder binder, + TypeLiteral keyType, TypeLiteral valueType) { + binder = binder.skipSources(MapBinder.class, RealMapBinder.class); + return newRealMapBinder(binder, keyType, valueType, Key.get(mapOf(keyType, valueType)), + Multibinder.newSetBinder(binder, entryOfProviderOf(keyType, valueType))); + } + + /** + * Returns a new mapbinder that collects entries of {@code keyType}/{@code valueType} in a + * {@link Map} that is itself bound with no binding annotation. + */ + public static MapBinder newMapBinder(Binder binder, + Class keyType, Class valueType) { + return newMapBinder(binder, TypeLiteral.get(keyType), TypeLiteral.get(valueType)); + } + + /** + * Returns a new mapbinder that collects entries of {@code keyType}/{@code valueType} in a + * {@link Map} that is itself bound with {@code annotation}. + */ + public static MapBinder newMapBinder(Binder binder, + TypeLiteral keyType, TypeLiteral valueType, Annotation annotation) { + binder = binder.skipSources(MapBinder.class, RealMapBinder.class); + return newRealMapBinder(binder, keyType, valueType, + Key.get(mapOf(keyType, valueType), annotation), + Multibinder.newSetBinder(binder, entryOfProviderOf(keyType, valueType), annotation)); + } + + /** + * Returns a new mapbinder that collects entries of {@code keyType}/{@code valueType} in a + * {@link Map} that is itself bound with {@code annotation}. + */ + public static MapBinder newMapBinder(Binder binder, + Class keyType, Class valueType, Annotation annotation) { + return newMapBinder(binder, TypeLiteral.get(keyType), TypeLiteral.get(valueType), annotation); + } + + /** + * Returns a new mapbinder that collects entries of {@code keyType}/{@code valueType} in a + * {@link Map} that is itself bound with {@code annotationType}. + */ + public static MapBinder newMapBinder(Binder binder, TypeLiteral keyType, + TypeLiteral valueType, Class annotationType) { + binder = binder.skipSources(MapBinder.class, RealMapBinder.class); + return newRealMapBinder(binder, keyType, valueType, + Key.get(mapOf(keyType, valueType), annotationType), + Multibinder.newSetBinder(binder, entryOfProviderOf(keyType, valueType), annotationType)); + } + + /** + * Returns a new mapbinder that collects entries of {@code keyType}/{@code valueType} in a + * {@link Map} that is itself bound with {@code annotationType}. + */ + public static MapBinder newMapBinder(Binder binder, Class keyType, + Class valueType, Class annotationType) { + return newMapBinder( + binder, TypeLiteral.get(keyType), TypeLiteral.get(valueType), annotationType); + } + + @SuppressWarnings("unchecked") // a map of is safely a Map + static TypeLiteral> mapOf( + TypeLiteral keyType, TypeLiteral valueType) { + return (TypeLiteral>) TypeLiteral.get( + Types.mapOf(keyType.getType(), valueType.getType())); + } + + @SuppressWarnings("unchecked") // a provider map is safely a Map> + static TypeLiteral>> mapOfProviderOf( + TypeLiteral keyType, TypeLiteral valueType) { + return (TypeLiteral>>) TypeLiteral.get( + Types.mapOf(keyType.getType(), Types.providerOf(valueType.getType()))); + } + + // provider map is safely a Map>> + @SuppressWarnings("unchecked") + static TypeLiteral>> mapOfJavaxProviderOf( + TypeLiteral keyType, TypeLiteral valueType) { + return (TypeLiteral>>) TypeLiteral.get( + Types.mapOf(keyType.getType(), + newParameterizedType(javax.inject.Provider.class, valueType.getType()))); + } + + @SuppressWarnings("unchecked") // a provider map > is safely a Map>> + static TypeLiteral>>> mapOfSetOfProviderOf( + TypeLiteral keyType, TypeLiteral valueType) { + return (TypeLiteral>>>) TypeLiteral.get( + Types.mapOf(keyType.getType(), Types.setOf(Types.providerOf(valueType.getType())))); + } + + @SuppressWarnings("unchecked") // a provider entry is safely a Map.Entry> + static TypeLiteral>> entryOfProviderOf( + TypeLiteral keyType, TypeLiteral valueType) { + return (TypeLiteral>>) TypeLiteral.get(newParameterizedTypeWithOwner( + Map.class, Entry.class, keyType.getType(), Types.providerOf(valueType.getType()))); + } + + // Note: We use valueTypeAndAnnotation effectively as a Pair + // since it's an easy way to group a type and an optional annotation type or instance. + static RealMapBinder newRealMapBinder(Binder binder, TypeLiteral keyType, + Key valueTypeAndAnnotation) { + binder = binder.skipSources(MapBinder.class, RealMapBinder.class); + TypeLiteral valueType = valueTypeAndAnnotation.getTypeLiteral(); + return newRealMapBinder(binder, keyType, valueType, + valueTypeAndAnnotation.ofType(mapOf(keyType, valueType)), + Multibinder.newSetBinder(binder, + valueTypeAndAnnotation.ofType(entryOfProviderOf(keyType, valueType)))); + } + + private static RealMapBinder newRealMapBinder(Binder binder, + TypeLiteral keyType, TypeLiteral valueType, Key> mapKey, + Multibinder>> entrySetBinder) { + RealMapBinder mapBinder = + new RealMapBinder(binder, keyType, valueType, mapKey, entrySetBinder); + binder.install(mapBinder); + return mapBinder; + } + + /** + * Configures the {@code MapBinder} to handle duplicate entries. + *

When multiple equal keys are bound, the value that gets included in the map is + * arbitrary. + *

In addition to the {@code Map} and {@code Map>} + * maps that are normally bound, a {@code Map>} and + * {@code Map>>} are also bound, which contain + * all values bound to each key. + *

+ * When multiple modules contribute elements to the map, this configuration + * option impacts all of them. + * + * @return this map binder + */ + public abstract MapBinder permitDuplicates(); + + /** + * Returns a binding builder used to add a new entry in the map. Each + * key must be distinct (and non-null). Bound providers will be evaluated each + * time the map is injected. + * + *

It is an error to call this method without also calling one of the + * {@code to} methods on the returned binding builder. + * + *

Scoping elements independently is supported. Use the {@code in} method + * to specify a binding scope. + */ + public abstract LinkedBindingBuilder addBinding(K key); + + /** + * The actual mapbinder plays several roles: + * + *

As a MapBinder, it acts as a factory for LinkedBindingBuilders for + * each of the map's values. It delegates to a {@link Multibinder} of + * entries (keys to value providers). + * + *

As a Module, it installs the binding to the map itself, as well as to + * a corresponding map whose values are providers. It uses the entry set + * multibinder to construct the map and the provider map. + * + *

As a module, this implements equals() and hashcode() in order to trick + * Guice into executing its configure() method only once. That makes it so + * that multiple mapbinders can be created for the same target map, but + * only one is bound. Since the list of bindings is retrieved from the + * injector itself (and not the mapbinder), each mapbinder has access to + * all contributions from all equivalent mapbinders. + * + *

Rather than binding a single Map.Entry<K, V>, the map binder + * binds keys and values independently. This allows the values to be properly + * scoped. + * + *

We use a subclass to hide 'implements Module' from the public API. + */ + static final class RealMapBinder extends MapBinder implements Module { + private final TypeLiteral keyType; + private final TypeLiteral valueType; + private final Key> mapKey; + private final Key>> javaxProviderMapKey; + private final Key>> providerMapKey; + private final Key>> multimapKey; + private final Key>>> providerMultimapKey; + private final RealMultibinder>> entrySetBinder; + private final Map duplicateKeyErrorMessages; + + /* the target injector's binder. non-null until initialization, null afterwards */ + private Binder binder; + + private boolean permitDuplicates; + private ImmutableList>> mapBindings; + + private RealMapBinder(Binder binder, TypeLiteral keyType, TypeLiteral valueType, + Key> mapKey, Multibinder>> entrySetBinder) { + this.keyType = keyType; + this.valueType = valueType; + this.mapKey = mapKey; + this.providerMapKey = mapKey.ofType(mapOfProviderOf(keyType, valueType)); + this.javaxProviderMapKey = mapKey.ofType(mapOfJavaxProviderOf(keyType, valueType)); + this.multimapKey = mapKey.ofType(mapOf(keyType, setOf(valueType))); + this.providerMultimapKey = mapKey.ofType(mapOfSetOfProviderOf(keyType, valueType)); + this.entrySetBinder = (RealMultibinder>>) entrySetBinder; + this.binder = binder; + this.duplicateKeyErrorMessages = Maps.newHashMap(); + } + + /** + * Sets the error message to be shown if the key had duplicate non-equal bindings. + */ + void updateDuplicateKeyMessage(K k, String errMsg) { + duplicateKeyErrorMessages.put(k, errMsg); + } + + @Override + public MapBinder permitDuplicates() { + entrySetBinder.permitDuplicates(); + binder.install(new MultimapBinder( + multimapKey, providerMultimapKey, entrySetBinder.getSetKey())); + return this; + } + + Key getKeyForNewValue(K key) { + checkNotNull(key, "key"); + checkConfiguration(!isInitialized(), "MapBinder was already initialized"); + + Key valueKey = Key.get(valueType, + new RealElement(entrySetBinder.getSetName(), MAPBINDER, keyType.toString())); + entrySetBinder.addBinding().toProvider(new ProviderMapEntry( + key, binder.getProvider(valueKey), valueKey)); + return valueKey; + } + + /** + * This creates two bindings. One for the {@code Map.Entry>} + * and another for {@code V}. + */ + @Override + public LinkedBindingBuilder addBinding(K key) { + return binder.bind(getKeyForNewValue(key)); + } + + @Override + public void configure(Binder binder) { + checkConfiguration(!isInitialized(), "MapBinder was already initialized"); + + ImmutableSet> dependencies + = ImmutableSet.>of(Dependency.get(entrySetBinder.getSetKey())); + + // Binds a Map> from a collection of Set>. + Provider>>> entrySetProvider = binder + .getProvider(entrySetBinder.getSetKey()); + + binder.bind(providerMapKey).toProvider( + new RealProviderMapProvider(dependencies, entrySetProvider)); + + // The map this exposes is internally an ImmutableMap, so it's OK to massage + // the guice Provider to javax Provider in the value (since Guice provider + // implements javax Provider). + @SuppressWarnings("unchecked") + Key massagedProviderMapKey = (Key) providerMapKey; + binder.bind(javaxProviderMapKey).to(massagedProviderMapKey); + + Provider>> mapProvider = binder.getProvider(providerMapKey); + binder.bind(mapKey).toProvider(new RealMapProvider(dependencies, mapProvider)); + } + + boolean containsElement(Element element) { + if (entrySetBinder.containsElement(element)) { + return true; + } else { + Key key; + if (element instanceof Binding) { + key = ((Binding) element).getKey(); + } else if (element instanceof ProviderLookup) { + key = ((ProviderLookup) element).getKey(); + } else { + return false; // cannot match; + } + + return key.equals(mapKey) + || key.equals(providerMapKey) + || key.equals(javaxProviderMapKey) + || key.equals(multimapKey) + || key.equals(providerMultimapKey) + || key.equals(entrySetBinder.getSetKey()) + || matchesValueKey(key); + } + } + + /** + * Returns true if the key indicates this is a value in the map. + */ + private boolean matchesValueKey(Key key) { + return key.getAnnotation() instanceof RealElement + && ((RealElement) key.getAnnotation()).setName().equals(entrySetBinder.getSetName()) + && ((RealElement) key.getAnnotation()).type() == MAPBINDER + && ((RealElement) key.getAnnotation()).keyType().equals(keyType.toString()) + && key.getTypeLiteral().equals(valueType); + } + + private boolean isInitialized() { + return binder == null; + } + + @Override + public boolean equals(Object o) { + return o instanceof RealMapBinder + && ((RealMapBinder) o).mapKey.equals(mapKey); + } + + @Override + public int hashCode() { + return mapKey.hashCode(); + } + + private Multimap newLinkedKeyArrayValueMultimap() { + return Multimaps.newListMultimap( + new LinkedHashMap>(), + new Supplier>() { + @Override + public List get() { + return Lists.newArrayList(); + } + }); + } + + /** + * Binds {@code Map>} and {{@code Map>>}. + */ + static final class MultimapBinder implements Module { + + private final Key>> multimapKey; + private final Key>>> providerMultimapKey; + private final Key>>> entrySetKey; + + public MultimapBinder( + Key>> multimapKey, + Key>>> providerMultimapKey, + Key>>> entrySetKey) { + this.multimapKey = multimapKey; + this.providerMultimapKey = providerMultimapKey; + this.entrySetKey = entrySetKey; + } + + @Override + public void configure(Binder binder) { + ImmutableSet> dependencies + = ImmutableSet.>of(Dependency.get(entrySetKey)); + + Provider>>> entrySetProvider = + binder.getProvider(entrySetKey); + // Binds a Map>> from a collection of Map> if + // permitDuplicates was called. + binder.bind(providerMultimapKey).toProvider( + new RealProviderMultimapProvider(dependencies, entrySetProvider)); + + Provider>>> multimapProvider = + binder.getProvider(providerMultimapKey); + binder.bind(multimapKey).toProvider( + new RealMultimapProvider(dependencies, multimapProvider)); + } + + @Override + public int hashCode() { + return multimapKey.hashCode(); + } + + @Override + public boolean equals(Object o) { + return o instanceof MultimapBinder + && ((MultimapBinder) o).multimapKey.equals(multimapKey); + } + + final class RealProviderMultimapProvider + extends RealMapBinderProviderWithDependencies>>> { + private final ImmutableSet> dependencies; + private final Provider>>> entrySetProvider; + private Map>> providerMultimap; + + private RealProviderMultimapProvider(ImmutableSet> dependencies, + Provider>>> entrySetProvider) { + super(multimapKey); + this.dependencies = dependencies; + this.entrySetProvider = entrySetProvider; + } + + @SuppressWarnings("unused") + @Inject + void initialize(Injector injector) { + Map>> providerMultimapMutable = + new LinkedHashMap>>(); + for (Entry> entry : entrySetProvider.get()) { + if (!providerMultimapMutable.containsKey(entry.getKey())) { + providerMultimapMutable.put( + entry.getKey(), ImmutableSet.>builder()); + } + providerMultimapMutable.get(entry.getKey()).add(entry.getValue()); + } + + ImmutableMap.Builder>> providerMultimapBuilder = + ImmutableMap.builder(); + for (Entry>> entry + : providerMultimapMutable.entrySet()) { + providerMultimapBuilder.put(entry.getKey(), entry.getValue().build()); + } + providerMultimap = providerMultimapBuilder.build(); + } + + @Override + public Map>> get() { + return providerMultimap; + } + + @Override + public Set> getDependencies() { + return dependencies; + } + } + + final class RealMultimapProvider + extends RealMapBinderProviderWithDependencies>> { + private final ImmutableSet> dependencies; + private final Provider>>> multimapProvider; + + RealMultimapProvider( + ImmutableSet> dependencies, + Provider>>> multimapProvider) { + super(multimapKey); + this.dependencies = dependencies; + this.multimapProvider = multimapProvider; + } + + @Override + public Map> get() { + ImmutableMap.Builder> multimapBuilder = ImmutableMap.builder(); + for (Entry>> entry : multimapProvider.get().entrySet()) { + K key = entry.getKey(); + ImmutableSet.Builder valuesBuilder = ImmutableSet.builder(); + for (Provider valueProvider : entry.getValue()) { + V value = valueProvider.get(); + checkConfiguration(value != null, + "Multimap injection failed due to null value for key \"%s\"", key); + valuesBuilder.add(value); + } + multimapBuilder.put(key, valuesBuilder.build()); + } + return multimapBuilder.build(); + } + + @Override + public Set> getDependencies() { + return dependencies; + } + } + } + + static final class ValueProvider implements Provider { + private final Provider delegate; + private final Binding binding; + + ValueProvider(Provider delegate, Binding binding) { + this.delegate = delegate; + this.binding = binding; + } + + @Override + public V get() { + return delegate.get(); + } + + public Binding getValueBinding() { + return binding; + } + } + + /** + * A Provider that Map.Entry that is also a Provider. The key is the entry in the + * map this corresponds to and the value is the provider of the user's binding. + * This returns itself as the Provider.get value. + */ + static final class ProviderMapEntry implements + ProviderWithDependencies>>, Entry> { + private final K key; + private final Provider provider; + private final Key valueKey; + + private ProviderMapEntry(K key, Provider provider, Key valueKey) { + this.key = key; + this.provider = provider; + this.valueKey = valueKey; + } + + @Override + public Entry> get() { + return this; + } + + @Override + public Set> getDependencies() { + return ((HasDependencies) provider).getDependencies(); + } + + public Key getValueKey() { + return valueKey; + } + + @Override + public K getKey() { + return key; + } + + @Override + public Provider getValue() { + return provider; + } + + @Override + public Provider setValue(Provider value) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof Entry) { + Entry o = (Entry) obj; + return Objects.equal(key, o.getKey()) + && Objects.equal(provider, o.getValue()); + } + return false; + } + + @Override + public int hashCode() { + return key.hashCode() ^ provider.hashCode(); + } + + @Override + public String toString() { + return "ProviderMapEntry(" + key + ", " + provider + ")"; + } + } + + private static abstract class RealMapWithExtensionProvider + extends RealMapBinderProviderWithDependencies + implements ProviderWithExtensionVisitor, MapBinderBinding { + public RealMapWithExtensionProvider(Object equality) { + super(equality); + } + } + + /** + * A base class for ProviderWithDependencies that need equality + * based on a specific object. + */ + private static abstract class RealMapBinderProviderWithDependencies implements ProviderWithDependencies { + private final Object equality; + + public RealMapBinderProviderWithDependencies(Object equality) { + this.equality = equality; + } + + @Override + public boolean equals(Object obj) { + return this.getClass() == obj.getClass() && + equality.equals(((RealMapBinderProviderWithDependencies) obj).equality); + } + + @Override + public int hashCode() { + return equality.hashCode(); + } + } + + final class RealProviderMapProvider + extends RealMapBinderProviderWithDependencies>> { + private final ImmutableSet> dependencies; + private final Provider>>> entrySetProvider; + private Map> providerMap; + + private RealProviderMapProvider( + ImmutableSet> dependencies, + Provider>>> entrySetProvider) { + super(mapKey); + this.dependencies = dependencies; + this.entrySetProvider = entrySetProvider; + } + + @Toolable + @Inject + void initialize(Injector injector) { + RealMapBinder.this.binder = null; + permitDuplicates = entrySetBinder.permitsDuplicates(injector); + + Map> providerMapMutable = new LinkedHashMap>(); + List>> bindingsMutable = Lists.newArrayList(); + Indexer indexer = new Indexer(injector); + Multimap index = HashMultimap.create(); + Set duplicateKeys = null; + for (Entry> entry : entrySetProvider.get()) { + ProviderMapEntry providerEntry = (ProviderMapEntry) entry; + Key valueKey = providerEntry.getValueKey(); + Binding valueBinding = injector.getBinding(valueKey); + // If this isn't a dup due to an exact same binding, add it. + if (index.put(providerEntry.getKey(), valueBinding.acceptTargetVisitor(indexer))) { + Provider previous = providerMapMutable.put(providerEntry.getKey(), + new ValueProvider(providerEntry.getValue(), valueBinding)); + if (previous != null && !permitDuplicates) { + if (duplicateKeys == null) { + duplicateKeys = Sets.newHashSet(); + } + duplicateKeys.add(providerEntry.getKey()); + } + bindingsMutable.add(Maps.immutableEntry(providerEntry.getKey(), valueBinding)); + } + } + if (duplicateKeys != null) { + // Must use a ListMultimap in case more than one binding has the same source + // and is listed multiple times. + Multimap dups = newLinkedKeyArrayValueMultimap(); + for (Entry> entry : bindingsMutable) { + if (duplicateKeys.contains(entry.getKey())) { + dups.put(entry.getKey(), "\t at " + Errors.convert(entry.getValue().getSource())); + } + } + StringBuilder sb = new StringBuilder("Map injection failed due to duplicated key "); + boolean first = true; + for (K key : dups.keySet()) { + if (first) { + first = false; + if (duplicateKeyErrorMessages.containsKey(key)) { + sb.setLength(0); + sb.append(duplicateKeyErrorMessages.get(key)); + } else { + sb.append("\"" + key + "\", from bindings:\n"); + } + } else { + if (duplicateKeyErrorMessages.containsKey(key)) { + sb.append("\n and " + duplicateKeyErrorMessages.get(key)); + } else { + sb.append("\n and key: \"" + key + "\", from bindings:\n"); + } + } + Joiner.on('\n').appendTo(sb, dups.get(key)).append("\n"); + } + checkConfiguration(false, sb.toString()); + } + + providerMap = ImmutableMap.copyOf(providerMapMutable); + mapBindings = ImmutableList.copyOf(bindingsMutable); + } + + @Override + public Map> get() { + return providerMap; + } + + @Override + public Set> getDependencies() { + return dependencies; + } + } + + final class RealMapProvider extends RealMapWithExtensionProvider> { + private final ImmutableSet> dependencies; + private final Provider>> mapProvider; + + private RealMapProvider( + ImmutableSet> dependencies, + Provider>> mapProvider) { + super(mapKey); + this.dependencies = dependencies; + this.mapProvider = mapProvider; + } + + @Override + public Map get() { + // We can initialize the internal table efficiently this way and then swap the values + // one by one. + Map map = new LinkedHashMap(mapProvider.get()); + for (Entry entry : map.entrySet()) { + @SuppressWarnings("unchecked") // we initialized the entries with providers + ValueProvider provider = (ValueProvider) entry.getValue(); + V value = provider.get(); + checkConfiguration(value != null, + "Map injection failed due to null value for key \"%s\", bound at: %s", + entry.getKey(), + provider.getValueBinding().getSource()); + entry.setValue(value); + } + @SuppressWarnings("unchecked") // if we exited the loop then we replaced all Providers + Map typedMap = (Map) map; + return Collections.unmodifiableMap(typedMap); + } + + @Override + public Set> getDependencies() { + return dependencies; + } + + @SuppressWarnings("unchecked") + @Override + public R acceptExtensionVisitor(BindingTargetVisitor visitor, + ProviderInstanceBinding binding) { + if (visitor instanceof MultibindingsTargetVisitor) { + return ((MultibindingsTargetVisitor, R>) visitor).visit(this); + } else { + return visitor.visit(binding); + } + } + + @Override + public Key> getMapKey() { + return mapKey; + } + + @Override + public TypeLiteral getKeyTypeLiteral() { + return keyType; + } + + @Override + public TypeLiteral getValueTypeLiteral() { + return valueType; + } + + @SuppressWarnings("unchecked") + @Override + public List>> getEntries() { + if (isInitialized()) { + return (List) mapBindings; // safe because mapBindings is immutable + } else { + throw new UnsupportedOperationException( + "getElements() not supported for module bindings"); + } + } + + @Override + public boolean permitsDuplicates() { + if (isInitialized()) { + return permitDuplicates; + } else { + throw new UnsupportedOperationException( + "permitsDuplicates() not supported for module bindings"); + } + } + + @Override + public boolean containsElement(Element element) { + return RealMapBinder.this.containsElement(element); + } + } + } +} diff --git a/src/main/java/com/google/inject/multibindings/MapBinderBinding.java b/src/main/java/com/google/inject/multibindings/MapBinderBinding.java new file mode 100644 index 0000000..bdaec02 --- /dev/null +++ b/src/main/java/com/google/inject/multibindings/MapBinderBinding.java @@ -0,0 +1,82 @@ +package com.google.inject.multibindings; + +import com.google.inject.Binding; +import com.google.inject.Key; +import com.google.inject.TypeLiteral; +import com.google.inject.spi.Element; +import com.google.inject.spi.Elements; + +import java.util.List; +import java.util.Map; + +/** + * A binding for a MapBinder. + *

+ * Although MapBinders may be injected through a variety of generic types (Map<K, V>, Map + * <K, Provider<V>>, Map<K, Set<V>>, Map>, and even Set<Map.Entry<K, Provider<V>>), a + * MapBinderBinding exists only on the Binding associated with the Map<K, V> key. Other + * bindings can be validated to be derived from this MapBinderBinding using + * {@link #containsElement(Element)}. + * + * @param The fully qualified type of the map, including Map. For example: + * MapBinderBinding<Map<String, Snack>> + */ +public interface MapBinderBinding { + + /** + * Returns the {@link Key} for the map. + */ + Key getMapKey(); + + /** + * Returns the TypeLiteral describing the keys of the map. + *

+ * The TypeLiteral will always match the type Map's generic type. For example, if getMapKey + * returns a key of Map<String, Snack>, then this will always return a + * TypeLiteral<String>. + */ + TypeLiteral getKeyTypeLiteral(); + + /** + * Returns the TypeLiteral describing the values of the map. + *

+ * The TypeLiteral will always match the type Map's generic type. For example, if getMapKey + * returns a key of Map<String, Snack>, then this will always return a + * TypeLiteral<Snack>. + */ + TypeLiteral getValueTypeLiteral(); + + /** + * Returns all entries in the Map. The returned list of Map.Entries contains the key and a binding + * to the value. Duplicate keys or values will exist as separate Map.Entries in the returned list. + * This is only supported on bindings returned from an injector. This will throw + * {@link UnsupportedOperationException} if it is called on an element retrieved from + * {@link Elements#getElements}. + *

+ * The elements will always match the type Map's generic type. For example, if getMapKey returns a + * key of Map<String, Snack>, then this will always return a list of type + * List<Map.Entry<String, Binding<Snack>>>. + */ + List>> getEntries(); + + /** + * Returns true if the MapBinder permits duplicates. This is only supported on bindings returned + * from an injector. This will throw {@link UnsupportedOperationException} if it is called on a + * MapBinderBinding retrieved from {@link Elements#getElements}. + */ + boolean permitsDuplicates(); + + /** + * Returns true if this MapBinder contains the given Element in order to build the map or uses the + * given Element in order to support building and injecting the map. This will work for + * MapBinderBindings retrieved from an injector and {@link Elements#getElements}. Usually this is + * only necessary if you are working with elements retrieved from modules (without an Injector), + * otherwise {@link #getEntries} and {@link #permitsDuplicates} are better options. + *

+ * If you need to introspect the details of the map, such as the keys, values or if it permits + * duplicates, it is necessary to pass the elements through an Injector and use + * {@link #getEntries()} and {@link #permitsDuplicates()}. + */ + boolean containsElement(Element element); +} diff --git a/src/main/java/com/google/inject/multibindings/MapKey.java b/src/main/java/com/google/inject/multibindings/MapKey.java new file mode 100644 index 0000000..3066461 --- /dev/null +++ b/src/main/java/com/google/inject/multibindings/MapKey.java @@ -0,0 +1,42 @@ +package com.google.inject.multibindings; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.ANNOTATION_TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * Allows users define customized key type annotations for map bindings by annotating an annotation + * of a {@code Map}'s key type. The custom key annotation can be applied to methods also annotated + * with {@literal @}{@link ProvidesIntoMap}. + * + *

A {@link StringMapKey} and {@link ClassMapKey} are provided for convenience with maps whose + * keys are strings or classes. For maps with enums or primitive types as keys, you must provide + * your own MapKey annotation, such as this one for an enum: + * + *

+ * {@literal @}MapKey(unwrapValue = true)
+ * {@literal @}Retention(RUNTIME)
+ * public {@literal @}interface MyCustomEnumKey {
+ *   MyCustomEnum value();
+ * }
+ * 
+ * + * You can also use the whole annotation as the key, if {@code unwrapValue=false}. + * When unwrapValue is false, the annotation type will be the key type for the injected map and + * the annotation instances will be the key values. If {@code unwrapValue=true}, the value() type + * will be the key type for injected map and the value() instances will be the keys values. + */ +@Documented +@Target(ANNOTATION_TYPE) +@Retention(RUNTIME) +public @interface MapKey { + /** + * if {@code unwrapValue} is false, then the whole annotation will be the type and annotation + * instances will be the keys. If {@code unwrapValue} is true, the value() type of key type + * annotation will be the key type for injected map and the value instances will be the keys. + */ + boolean unwrapValue() default true; +} diff --git a/src/main/java/com/google/inject/multibindings/Multibinder.java b/src/main/java/com/google/inject/multibindings/Multibinder.java new file mode 100644 index 0000000..0dda40a --- /dev/null +++ b/src/main/java/com/google/inject/multibindings/Multibinder.java @@ -0,0 +1,575 @@ +package com.google.inject.multibindings; + +import com.google.common.base.Objects; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Lists; +import com.google.common.collect.Sets; +import com.google.inject.AbstractModule; +import com.google.inject.Binder; +import com.google.inject.Binding; +import com.google.inject.ConfigurationException; +import com.google.inject.Inject; +import com.google.inject.Injector; +import com.google.inject.Key; +import com.google.inject.Module; +import com.google.inject.Provider; +import com.google.inject.TypeLiteral; +import com.google.inject.binder.LinkedBindingBuilder; +import com.google.inject.internal.Errors; +import com.google.inject.spi.BindingTargetVisitor; +import com.google.inject.spi.Dependency; +import com.google.inject.spi.HasDependencies; +import com.google.inject.spi.Message; +import com.google.inject.spi.ProviderInstanceBinding; +import com.google.inject.spi.ProviderWithDependencies; +import com.google.inject.spi.ProviderWithExtensionVisitor; +import com.google.inject.spi.Toolable; +import com.google.inject.util.Types; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static com.google.common.base.Predicates.equalTo; +import static com.google.common.collect.Iterables.filter; +import static com.google.common.collect.Iterables.getOnlyElement; +import static com.google.common.primitives.Ints.MAX_POWER_OF_TWO; +import static com.google.inject.multibindings.Element.Type.MULTIBINDER; +import static com.google.inject.name.Names.named; + +/** + * An API to bind multiple values separately, only to later inject them as a + * complete collection. Multibinder is intended for use in your application's + * module: + *

+ * public class SnacksModule extends AbstractModule {
+ *   protected void configure() {
+ *     Multibinder<Snack> multibinder
+ *         = Multibinder.newSetBinder(binder(), Snack.class);
+ *     multibinder.addBinding().toInstance(new Twix());
+ *     multibinder.addBinding().toProvider(SnickersProvider.class);
+ *     multibinder.addBinding().to(Skittles.class);
+ *   }
+ * }
+ * + *

With this binding, a {@link Set}{@code } can now be injected: + *


+ * class SnackMachine {
+ *   {@literal @}Inject
+ *   public SnackMachine(Set<Snack> snacks) { ... }
+ * }
+ * + * If desired, {@link Collection}{@code >} can also be injected. + * + *

Contributing multibindings from different modules is supported. For + * example, it is okay for both {@code CandyModule} and {@code ChipsModule} + * to create their own {@code Multibinder}, and to each contribute + * bindings to the set of snacks. When that set is injected, it will contain + * elements from both modules. + * + *

The set's iteration order is consistent with the binding order. This is + * convenient when multiple elements are contributed by the same module because + * that module can order its bindings appropriately. Avoid relying on the + * iteration order of elements contributed by different modules, since there is + * no equivalent mechanism to order modules. + * + *

The set is unmodifiable. Elements can only be added to the set by + * configuring the multibinder. Elements can never be removed from the set. + * + *

Elements are resolved at set injection time. If an element is bound to a + * provider, that provider's get method will be called each time the set is + * injected (unless the binding is also scoped). + * + *

Annotations are be used to create different sets of the same element + * type. Each distinct annotation gets its own independent collection of + * elements. + * + *

Elements must be distinct. If multiple bound elements + * have the same value, set injection will fail. + * + *

Elements must be non-null. If any set element is null, + * set injection will fail. + */ +public abstract class Multibinder { + private Multibinder() { + } + + /** + * Returns a new multibinder that collects instances of {@code type} in a {@link Set} that is + * itself bound with no binding annotation. + */ + public static Multibinder newSetBinder(Binder binder, TypeLiteral type) { + return newRealSetBinder(binder, Key.get(type)); + } + + /** + * Returns a new multibinder that collects instances of {@code type} in a {@link Set} that is + * itself bound with no binding annotation. + */ + public static Multibinder newSetBinder(Binder binder, Class type) { + return newRealSetBinder(binder, Key.get(type)); + } + + /** + * Returns a new multibinder that collects instances of {@code type} in a {@link Set} that is + * itself bound with {@code annotation}. + */ + public static Multibinder newSetBinder( + Binder binder, TypeLiteral type, Annotation annotation) { + return newRealSetBinder(binder, Key.get(type, annotation)); + } + + /** + * Returns a new multibinder that collects instances of {@code type} in a {@link Set} that is + * itself bound with {@code annotation}. + */ + public static Multibinder newSetBinder( + Binder binder, Class type, Annotation annotation) { + return newRealSetBinder(binder, Key.get(type, annotation)); + } + + /** + * Returns a new multibinder that collects instances of {@code type} in a {@link Set} that is + * itself bound with {@code annotationType}. + */ + public static Multibinder newSetBinder(Binder binder, TypeLiteral type, + Class annotationType) { + return newRealSetBinder(binder, Key.get(type, annotationType)); + } + + /** + * Returns a new multibinder that collects instances of the key's type in a {@link Set} that is + * itself bound with the annotation (if any) of the key. + * + * @since 4.0 + */ + public static Multibinder newSetBinder(Binder binder, Key key) { + return newRealSetBinder(binder, key); + } + + /** + * Implementation of newSetBinder. + */ + static RealMultibinder newRealSetBinder(Binder binder, Key key) { + binder = binder.skipSources(RealMultibinder.class, Multibinder.class); + RealMultibinder result = new RealMultibinder(binder, key.getTypeLiteral(), + key.ofType(setOf(key.getTypeLiteral()))); + binder.install(result); + return result; + } + + /** + * Returns a new multibinder that collects instances of {@code type} in a {@link Set} that is + * itself bound with {@code annotationType}. + */ + public static Multibinder newSetBinder(Binder binder, Class type, + Class annotationType) { + return newSetBinder(binder, Key.get(type, annotationType)); + } + + @SuppressWarnings("unchecked") // wrapping a T in a Set safely returns a Set + static TypeLiteral> setOf(TypeLiteral elementType) { + Type type = Types.setOf(elementType.getType()); + return (TypeLiteral>) TypeLiteral.get(type); + } + + @SuppressWarnings("unchecked") + static TypeLiteral>> collectionOfProvidersOf( + TypeLiteral elementType) { + Type providerType = Types.providerOf(elementType.getType()); + Type type = Types.newParameterizedType(Collection.class, providerType); + return (TypeLiteral>>) TypeLiteral.get(type); + } + + @SuppressWarnings("unchecked") + static TypeLiteral>> collectionOfJavaxProvidersOf( + TypeLiteral elementType) { + Type providerType = + Types.newParameterizedType(javax.inject.Provider.class, elementType.getType()); + Type type = Types.newParameterizedType(Collection.class, providerType); + return (TypeLiteral>>) TypeLiteral.get(type); + } + + static void checkConfiguration(boolean condition, String format, Object... args) { + if (condition) { + return; + } + + throw new ConfigurationException(ImmutableSet.of(new Message(Errors.format(format, args)))); + } + + private static ConfigurationException newDuplicateValuesException( + Map> existingBindings, + Binding binding, + final T newValue, + Binding duplicateBinding) { + T oldValue = getOnlyElement(filter(existingBindings.keySet(), equalTo(newValue))); + String oldString = oldValue.toString(); + String newString = newValue.toString(); + if (Objects.equal(oldString, newString)) { + // When the value strings match, just show the source of the bindings + return new ConfigurationException(ImmutableSet.of(new Message(Errors.format( + "Set injection failed due to duplicated element \"%s\"" + + "\n Bound at %s\n Bound at %s", + newValue, + duplicateBinding.getSource(), + binding.getSource())))); + } else { + // When the value strings don't match, include them both as they may be useful for debugging + return new ConfigurationException(ImmutableSet.of(new Message(Errors.format( + "Set injection failed due to multiple elements comparing equal:" + + "\n \"%s\"\n bound at %s" + + "\n \"%s\"\n bound at %s", + oldValue, + duplicateBinding.getSource(), + newValue, + binding.getSource())))); + } + } + + static T checkNotNull(T reference, String name) { + if (reference != null) { + return reference; + } + + NullPointerException npe = new NullPointerException(name); + throw new ConfigurationException(ImmutableSet.of( + new Message(npe.toString(), npe))); + } + + /** + * Configures the bound set to silently discard duplicate elements. When multiple equal values are + * bound, the one that gets included is arbitrary. When multiple modules contribute elements to + * the set, this configuration option impacts all of them. + * + * @return this multibinder + * @since 3.0 + */ + public abstract Multibinder permitDuplicates(); + + /** + * Returns a binding builder used to add a new element in the set. Each + * bound element must have a distinct value. Bound providers will be + * evaluated each time the set is injected. + * + *

It is an error to call this method without also calling one of the + * {@code to} methods on the returned binding builder. + * + *

Scoping elements independently is supported. Use the {@code in} method + * to specify a binding scope. + */ + public abstract LinkedBindingBuilder addBinding(); + + /** + * The actual multibinder plays several roles: + * + *

As a Multibinder, it acts as a factory for LinkedBindingBuilders for + * each of the set's elements. Each binding is given an annotation that + * identifies it as a part of this set. + * + *

As a Module, it installs the binding to the set itself. As a module, + * this implements equals() and hashcode() in order to trick Guice into + * executing its configure() method only once. That makes it so that + * multiple multibinders can be created for the same target collection, but + * only one is bound. Since the list of bindings is retrieved from the + * injector itself (and not the multibinder), each multibinder has access to + * all contributions from all multibinders. + * + *

As a Provider, this constructs the set instances. + * + *

We use a subclass to hide 'implements Module, Provider' from the public + * API. + */ + static final class RealMultibinder extends Multibinder + implements Module, ProviderWithExtensionVisitor>, HasDependencies, + MultibinderBinding> { + + private final TypeLiteral elementType; + private final String setName; + private final Key> setKey; + private final Key>> collectionOfProvidersKey; + private final Key>> collectionOfJavaxProvidersKey; + private final Key permitDuplicatesKey; + + /* the target injector's binder. non-null until initialization, null afterwards */ + private Binder binder; + + /* a binding for each element in the set. null until initialization, non-null afterwards */ + private ImmutableList> bindings; + private Set> dependencies; + + /** + * whether duplicates are allowed. Possibly configured by a different instance + */ + private boolean permitDuplicates; + + private RealMultibinder(Binder binder, TypeLiteral elementType, Key> setKey) { + this.binder = checkNotNull(binder, "binder"); + this.elementType = checkNotNull(elementType, "elementType"); + this.setKey = checkNotNull(setKey, "setKey"); + this.collectionOfProvidersKey = setKey.ofType(collectionOfProvidersOf(elementType)); + this.collectionOfJavaxProvidersKey = setKey.ofType(collectionOfJavaxProvidersOf(elementType)); + this.setName = RealElement.nameOf(setKey); + this.permitDuplicatesKey = Key.get(Boolean.class, named(toString() + " permits duplicates")); + } + + // This is forked from com.google.common.collect.Maps.capacity + private static int mapCapacity(int numBindings) { + if (numBindings < 3) { + return numBindings + 1; + } else if (numBindings < MAX_POWER_OF_TWO) { + return (int) (numBindings / 0.75F + 1.0F); + } + return Integer.MAX_VALUE; + } + + public void configure(Binder binder) { + checkConfiguration(!isInitialized(), "Multibinder was already initialized"); + + binder.bind(setKey).toProvider(this); + binder.bind(collectionOfProvidersKey).toProvider( + new RealMultibinderCollectionOfProvidersProvider()); + + // The collection this exposes is internally an ImmutableList, so it's OK to massage + // the guice Provider to javax Provider in the value (since the guice Provider implements + // javax Provider). + @SuppressWarnings("unchecked") + Key key = (Key) collectionOfProvidersKey; + binder.bind(collectionOfJavaxProvidersKey).to(key); + } + + @Override + public Multibinder permitDuplicates() { + binder.install(new PermitDuplicatesModule(permitDuplicatesKey)); + return this; + } + + Key getKeyForNewItem() { + checkConfiguration(!isInitialized(), "Multibinder was already initialized"); + return Key.get(elementType, new RealElement(setName, MULTIBINDER, "")); + } + + @Override + public LinkedBindingBuilder addBinding() { + return binder.bind(getKeyForNewItem()); + } + + /** + * Invoked by Guice at Injector-creation time to prepare providers for each + * element in this set. At this time the set's size is known, but its + * contents are only evaluated when get() is invoked. + */ + @Toolable + @Inject + void initialize(Injector injector) { + List> bindings = Lists.newArrayList(); + Set index = Sets.newHashSet(); + Indexer indexer = new Indexer(injector); + List> dependencies = Lists.newArrayList(); + for (Binding entry : injector.findBindingsByType(elementType)) { + if (keyMatches(entry.getKey())) { + @SuppressWarnings("unchecked") // protected by findBindingsByType() + Binding binding = (Binding) entry; + if (index.add(binding.acceptTargetVisitor(indexer))) { + bindings.add(binding); + dependencies.add(Dependency.get(binding.getKey())); + } + } + } + + this.bindings = ImmutableList.copyOf(bindings); + this.dependencies = ImmutableSet.copyOf(dependencies); + this.permitDuplicates = permitsDuplicates(injector); + this.binder = null; + } + + boolean permitsDuplicates(Injector injector) { + return injector.getBindings().containsKey(permitDuplicatesKey); + } + + private boolean keyMatches(Key key) { + return key.getTypeLiteral().equals(elementType) + && key.getAnnotation() instanceof Element + && ((Element) key.getAnnotation()).setName().equals(setName) + && ((Element) key.getAnnotation()).type() == MULTIBINDER; + } + + private boolean isInitialized() { + return binder == null; + } + + public Set get() { + checkConfiguration(isInitialized(), "Multibinder is not initialized"); + + Map> result = new LinkedHashMap>(mapCapacity(bindings.size())); + for (Binding binding : bindings) { + final T newValue = binding.getProvider().get(); + checkConfiguration(newValue != null, + "Set injection failed due to null element bound at: %s", + binding.getSource()); + Binding duplicateBinding = result.put(newValue, binding); + if (!permitDuplicates && duplicateBinding != null) { + throw newDuplicateValuesException(result, binding, newValue, duplicateBinding); + } + } + return ImmutableSet.copyOf(result.keySet()); + } + + @SuppressWarnings("unchecked") + public V acceptExtensionVisitor( + BindingTargetVisitor visitor, + ProviderInstanceBinding binding) { + if (visitor instanceof MultibindingsTargetVisitor) { + return ((MultibindingsTargetVisitor, V>) visitor).visit(this); + } else { + return visitor.visit(binding); + } + } + + String getSetName() { + return setName; + } + + public TypeLiteral getElementTypeLiteral() { + return elementType; + } + + public Key> getSetKey() { + return setKey; + } + + @SuppressWarnings("unchecked") + public List> getElements() { + if (isInitialized()) { + return (List>) (List) bindings; // safe because bindings is immutable. + } else { + throw new UnsupportedOperationException("getElements() not supported for module bindings"); + } + } + + public boolean permitsDuplicates() { + if (isInitialized()) { + return permitDuplicates; + } else { + throw new UnsupportedOperationException( + "permitsDuplicates() not supported for module bindings"); + } + } + + public boolean containsElement(com.google.inject.spi.Element element) { + if (element instanceof Binding) { + Binding binding = (Binding) element; + return keyMatches(binding.getKey()) + || binding.getKey().equals(permitDuplicatesKey) + || binding.getKey().equals(setKey) + || binding.getKey().equals(collectionOfProvidersKey) + || binding.getKey().equals(collectionOfJavaxProvidersKey); + } else { + return false; + } + } + + public Set> getDependencies() { + if (!isInitialized()) { + return ImmutableSet.>of(Dependency.get(Key.get(Injector.class))); + } else { + return dependencies; + } + } + + @Override + public boolean equals(Object o) { + return o instanceof RealMultibinder + && ((RealMultibinder) o).setKey.equals(setKey); + } + + @Override + public int hashCode() { + return setKey.hashCode(); + } + + @Override + public String toString() { + return (setName.isEmpty() ? "" : setName + " ") + "Multibinder<" + elementType + ">"; + } + + final class RealMultibinderCollectionOfProvidersProvider + implements ProviderWithDependencies>> { + @Override + public Collection> get() { + checkConfiguration(isInitialized(), "Multibinder is not initialized"); + int size = bindings.size(); + @SuppressWarnings("unchecked") // safe because we only put Provider into it. + Provider[] providers = new Provider[size]; + for (int i = 0; i < size; i++) { + providers[i] = bindings.get(i).getProvider(); + } + return ImmutableList.copyOf(providers); + } + + @Override + public Set> getDependencies() { + if (!isInitialized()) { + return ImmutableSet.>of(Dependency.get(Key.get(Injector.class))); + } + ImmutableSet.Builder> setBuilder = ImmutableSet.builder(); + for (Dependency dependency : dependencies) { + Key key = dependency.getKey(); + setBuilder.add( + Dependency.get(key.ofType(Types.providerOf(key.getTypeLiteral().getType())))); + } + return setBuilder.build(); + } + + Key getCollectionKey() { + return RealMultibinder.this.collectionOfProvidersKey; + } + + @Override + public boolean equals(Object o) { + return o instanceof RealMultibinder.RealMultibinderCollectionOfProvidersProvider + && ((RealMultibinderCollectionOfProvidersProvider) o) + .getCollectionKey().equals(getCollectionKey()); + } + + @Override + public int hashCode() { + return getCollectionKey().hashCode(); + } + } + } + + /** + * We install the permit duplicates configuration as its own binding, all by itself. This way, + * if only one of a multibinder's users remember to call permitDuplicates(), they're still + * permitted. + */ + private static class PermitDuplicatesModule extends AbstractModule { + private final Key key; + + PermitDuplicatesModule(Key key) { + this.key = key; + } + + @Override + protected void configure() { + bind(key).toInstance(true); + } + + @Override + public boolean equals(Object o) { + return o instanceof PermitDuplicatesModule + && ((PermitDuplicatesModule) o).key.equals(key); + } + + @Override + public int hashCode() { + return getClass().hashCode() ^ key.hashCode(); + } + } +} diff --git a/src/main/java/com/google/inject/multibindings/MultibinderBinding.java b/src/main/java/com/google/inject/multibindings/MultibinderBinding.java new file mode 100644 index 0000000..2451d3a --- /dev/null +++ b/src/main/java/com/google/inject/multibindings/MultibinderBinding.java @@ -0,0 +1,63 @@ +package com.google.inject.multibindings; + +import com.google.inject.Binding; +import com.google.inject.Key; +import com.google.inject.TypeLiteral; +import com.google.inject.spi.Element; +import com.google.inject.spi.Elements; + +import java.util.List; + +/** + * A binding for a Multibinder. + * + * @param The fully qualified type of the set, including Set. For example: + * MultibinderBinding<Set<Boolean>> + */ +public interface MultibinderBinding { + + /** + * Returns the key for the set. + */ + Key getSetKey(); + + /** + * Returns the TypeLiteral that describes the type of elements in the set. + *

+ * The elements will always match the type Set's generic type. For example, if getSetKey returns a + * key of Set<String>, then this will always return a + * TypeLiteral<String>. + */ + TypeLiteral getElementTypeLiteral(); + + /** + * Returns all bindings that make up the set. This is only supported on bindings returned from an + * injector. This will throw {@link UnsupportedOperationException} if it is called on an element + * retrieved from {@link Elements#getElements}. + *

+ * The elements will always match the type Set's generic type. For example, if getSetKey returns a + * key of Set<String>, then this will always return a list of type + * List<Binding<String>>. + */ + List> getElements(); + + /** + * Returns true if the multibinder permits duplicates. This is only supported on bindings returned + * from an injector. This will throw {@link UnsupportedOperationException} if it is called on a + * MultibinderBinding retrieved from {@link Elements#getElements}. + */ + boolean permitsDuplicates(); + + /** + * Returns true if this Multibinder uses the given Element. This will be true for bindings that + * derive the elements of the set and other bindings that Multibinder uses internally. This will + * work for MultibinderBindings retrieved from an injector and {@link Elements#getElements}. + * Usually this is only necessary if you are working with elements retrieved from modules (without + * an Injector), otherwise {@link #getElements} and {@link #permitsDuplicates} are better options. + *

+ * If you need to introspect the details of the set, such as the values or if it permits + * duplicates, it is necessary to pass the elements through an Injector and use + * {@link #getElements()} and {@link #permitsDuplicates()}. + */ + boolean containsElement(Element element); +} diff --git a/src/main/java/com/google/inject/multibindings/MultibindingsScanner.java b/src/main/java/com/google/inject/multibindings/MultibindingsScanner.java new file mode 100644 index 0000000..6521f52 --- /dev/null +++ b/src/main/java/com/google/inject/multibindings/MultibindingsScanner.java @@ -0,0 +1,179 @@ +package com.google.inject.multibindings; + +import com.google.common.collect.ImmutableSet; +import com.google.inject.AbstractModule; +import com.google.inject.Binder; +import com.google.inject.Key; +import com.google.inject.Module; +import com.google.inject.TypeLiteral; +import com.google.inject.spi.InjectionPoint; +import com.google.inject.spi.ModuleAnnotatedMethodScanner; + +import java.lang.annotation.Annotation; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Set; + +/** + * Scans a module for annotations that signal multibindings, mapbindings, and optional bindings. + */ +public class MultibindingsScanner { + + private MultibindingsScanner() { + } + + /** + * Returns a module that, when installed, will scan all modules for methods with the annotations + * {@literal @}{@link ProvidesIntoMap}, {@literal @}{@link ProvidesIntoSet}, and + * {@literal @}{@link ProvidesIntoOptional}. + * + *

This is a convenience method, equivalent to doing + * {@code binder().scanModulesForAnnotatedMethods(MultibindingsScanner.scanner())}. + */ + public static Module asModule() { + return new AbstractModule() { + @Override + protected void configure() { + binder().scanModulesForAnnotatedMethods(Scanner.INSTANCE); + } + }; + } + + /** + * Returns a {@link ModuleAnnotatedMethodScanner} that, when bound, will scan all modules for + * methods with the annotations {@literal @}{@link ProvidesIntoMap}, + * {@literal @}{@link ProvidesIntoSet}, and {@literal @}{@link ProvidesIntoOptional}. + */ + public static ModuleAnnotatedMethodScanner scanner() { + return Scanner.INSTANCE; + } + + private static AnnotationOrError findMapKeyAnnotation(Binder binder, Method method) { + Annotation foundAnnotation = null; + for (Annotation annotation : method.getAnnotations()) { + MapKey mapKey = annotation.annotationType().getAnnotation(MapKey.class); + if (mapKey != null) { + if (foundAnnotation != null) { + binder.addError("Found more than one MapKey annotations on %s.", method); + return AnnotationOrError.forError(); + } + if (mapKey.unwrapValue()) { + try { + // validate there's a declared method called "value" + Method valueMethod = annotation.annotationType().getDeclaredMethod("value"); + if (valueMethod.getReturnType().isArray()) { + binder.addError("Array types are not allowed in a MapKey with unwrapValue=true: %s", + annotation.annotationType()); + return AnnotationOrError.forError(); + } + } catch (NoSuchMethodException invalid) { + binder.addError("No 'value' method in MapKey with unwrapValue=true: %s", + annotation.annotationType()); + return AnnotationOrError.forError(); + } + } + foundAnnotation = annotation; + } + } + return AnnotationOrError.forPossiblyNullAnnotation(foundAnnotation); + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + static TypeAndValue typeAndValueOfMapKey(Annotation mapKeyAnnotation) { + if (!mapKeyAnnotation.annotationType().getAnnotation(MapKey.class).unwrapValue()) { + return new TypeAndValue(TypeLiteral.get(mapKeyAnnotation.annotationType()), mapKeyAnnotation); + } else { + try { + Method valueMethod = mapKeyAnnotation.annotationType().getDeclaredMethod("value"); + valueMethod.setAccessible(true); + TypeLiteral returnType = + TypeLiteral.get(mapKeyAnnotation.annotationType()).getReturnType(valueMethod); + return new TypeAndValue(returnType, valueMethod.invoke(mapKeyAnnotation)); + } catch (NoSuchMethodException e) { + throw new IllegalStateException(e); + } catch (SecurityException e) { + throw new IllegalStateException(e); + } catch (IllegalAccessException e) { + throw new IllegalStateException(e); + } catch (InvocationTargetException e) { + throw new IllegalStateException(e); + } + } + } + + private static class Scanner extends ModuleAnnotatedMethodScanner { + private static final Scanner INSTANCE = new Scanner(); + + @Override + public Set> annotationClasses() { + return ImmutableSet.of( + ProvidesIntoSet.class, ProvidesIntoMap.class, ProvidesIntoOptional.class); + } + + @SuppressWarnings({"unchecked", "rawtypes"}) // mapKey doesn't know its key type + @Override + public Key prepareMethod(Binder binder, Annotation annotation, Key key, + InjectionPoint injectionPoint) { + Method method = (Method) injectionPoint.getMember(); + AnnotationOrError mapKey = findMapKeyAnnotation(binder, method); + if (annotation instanceof ProvidesIntoSet) { + if (mapKey.annotation != null) { + binder.addError("Found a MapKey annotation on non map binding at %s.", method); + } + return Multibinder.newRealSetBinder(binder, key).getKeyForNewItem(); + } else if (annotation instanceof ProvidesIntoMap) { + if (mapKey.error) { + // Already failed on the MapKey, don't bother doing more work. + return key; + } + if (mapKey.annotation == null) { + // If no MapKey, make an error and abort. + binder.addError("No MapKey found for map binding at %s.", method); + return key; + } + TypeAndValue typeAndValue = typeAndValueOfMapKey(mapKey.annotation); + return MapBinder.newRealMapBinder(binder, typeAndValue.type, key) + .getKeyForNewValue(typeAndValue.value); + } else if (annotation instanceof ProvidesIntoOptional) { + if (mapKey.annotation != null) { + binder.addError("Found a MapKey annotation on non map binding at %s.", method); + } + switch (((ProvidesIntoOptional) annotation).value()) { + case DEFAULT: + return OptionalBinder.newRealOptionalBinder(binder, key).getKeyForDefaultBinding(); + case ACTUAL: + return OptionalBinder.newRealOptionalBinder(binder, key).getKeyForActualBinding(); + } + } + throw new IllegalStateException("Invalid annotation: " + annotation); + } + } + + private static class AnnotationOrError { + final Annotation annotation; + final boolean error; + + AnnotationOrError(Annotation annotation, boolean error) { + this.annotation = annotation; + this.error = error; + } + + static AnnotationOrError forPossiblyNullAnnotation(Annotation annotation) { + return new AnnotationOrError(annotation, false); + } + + static AnnotationOrError forError() { + return new AnnotationOrError(null, true); + } + } + + private static class TypeAndValue { + final TypeLiteral type; + final T value; + + TypeAndValue(TypeLiteral type, T value) { + this.type = type; + this.value = value; + } + } +} diff --git a/src/main/java/com/google/inject/multibindings/MultibindingsTargetVisitor.java b/src/main/java/com/google/inject/multibindings/MultibindingsTargetVisitor.java new file mode 100644 index 0000000..2bde9a2 --- /dev/null +++ b/src/main/java/com/google/inject/multibindings/MultibindingsTargetVisitor.java @@ -0,0 +1,31 @@ +package com.google.inject.multibindings; + +import com.google.inject.spi.BindingTargetVisitor; + +/** + * A visitor for the multibinder extension. + *

+ * If your {@link BindingTargetVisitor} implements this interface, bindings created by using + * {@link Multibinder}, {@link MapBinder} or {@link OptionalBinderBinding} will be visited through + * this interface. + */ +public interface MultibindingsTargetVisitor extends BindingTargetVisitor { + + /** + * Visits a binding created through {@link Multibinder}. + */ + V visit(MultibinderBinding multibinding); + + /** + * Visits a binding created through {@link MapBinder}. + */ + V visit(MapBinderBinding mapbinding); + + /** + * Visits a binding created through {@link OptionalBinder}. + * + * @since 4.0 + */ + V visit(OptionalBinderBinding optionalbinding); + +} diff --git a/src/main/java/com/google/inject/multibindings/OptionalBinder.java b/src/main/java/com/google/inject/multibindings/OptionalBinder.java new file mode 100644 index 0000000..5af595d --- /dev/null +++ b/src/main/java/com/google/inject/multibindings/OptionalBinder.java @@ -0,0 +1,779 @@ +package com.google.inject.multibindings; + +import com.google.common.base.Optional; +import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableSet; +import com.google.inject.Binder; +import com.google.inject.Binding; +import com.google.inject.Inject; +import com.google.inject.Injector; +import com.google.inject.Key; +import com.google.inject.Module; +import com.google.inject.Provider; +import com.google.inject.TypeLiteral; +import com.google.inject.binder.LinkedBindingBuilder; +import com.google.inject.spi.BindingTargetVisitor; +import com.google.inject.spi.Dependency; +import com.google.inject.spi.Element; +import com.google.inject.spi.ProviderInstanceBinding; +import com.google.inject.spi.ProviderLookup; +import com.google.inject.spi.ProviderWithDependencies; +import com.google.inject.spi.ProviderWithExtensionVisitor; +import com.google.inject.spi.Toolable; +import com.google.inject.util.Types; + +import javax.inject.Qualifier; +import java.io.Serializable; +import java.lang.annotation.Annotation; +import java.lang.annotation.Retention; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Type; +import java.util.Set; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; +import static com.google.inject.multibindings.Multibinder.checkConfiguration; +import static com.google.inject.util.Types.newParameterizedType; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + + +/** + * An API to bind optional values, optionally with a default value. + * OptionalBinder fulfills two roles:

    + *
  1. It allows a framework to define an injection point that may or + * may not be bound by users. + *
  2. It allows a framework to supply a default value that can be changed + * by users. + *
+ * + *

When an OptionalBinder is added, it will always supply the bindings: + * {@code Optional} and {@code Optional>}. If + * {@link #setBinding} or {@link #setDefault} are called, it will also + * bind {@code T}. + * + *

{@code setDefault} is intended for use by frameworks that need a default + * value. User code can call {@code setBinding} to override the default. + * Warning: Even if setBinding is called, the default binding + * will still exist in the object graph. If it is a singleton, it will be + * instantiated in {@code Stage.PRODUCTION}. + * + *

If setDefault or setBinding are linked to Providers, the Provider may return + * {@code null}. If it does, the Optional bindings will be absent. Binding + * setBinding to a Provider that returns null will not cause OptionalBinder + * to fall back to the setDefault binding. + * + *

If neither setDefault nor setBinding are called, it will try to link to a + * user-supplied binding of the same type. If no binding exists, the optionals + * will be absent. Otherwise, if a user-supplied binding of that type exists, + * or if setBinding or setDefault are called, the optionals will return present + * if they are bound to a non-null value. + * + *

Values are resolved at injection time. If a value is bound to a + * provider, that provider's get method will be called each time the optional + * is injected (unless the binding is also scoped, or an optional of provider is + * injected). + * + *

Annotations are used to create different optionals of the same key/value + * type. Each distinct annotation gets its own independent binding. + * + *


+ * public class FrameworkModule extends AbstractModule {
+ *   protected void configure() {
+ *     OptionalBinder.newOptionalBinder(binder(), Renamer.class);
+ *   }
+ * }
+ * + *

With this module, an {@link Optional}{@code } can now be + * injected. With no other bindings, the optional will be absent. + * Users can specify bindings in one of two ways: + * + *

Option 1: + *


+ * public class UserRenamerModule extends AbstractModule {
+ *   protected void configure() {
+ *     bind(Renamer.class).to(ReplacingRenamer.class);
+ *   }
+ * }
+ * + *

or Option 2: + *


+ * public class UserRenamerModule extends AbstractModule {
+ *   protected void configure() {
+ *     OptionalBinder.newOptionalBinder(binder(), Renamer.class)
+ *         .setBinding().to(ReplacingRenamer.class);
+ *   }
+ * }
+ * With both options, the {@code Optional} will be present and supply the + * ReplacingRenamer. + * + *

Default values can be supplied using: + *


+ * public class FrameworkModule extends AbstractModule {
+ *   protected void configure() {
+ *     OptionalBinder.newOptionalBinder(binder(), Key.get(String.class, LookupUrl.class))
+ *         .setDefault().toInstance(DEFAULT_LOOKUP_URL);
+ *   }
+ * }
+ * With the above module, code can inject an {@code @LookupUrl String} and it + * will supply the DEFAULT_LOOKUP_URL. A user can change this value by binding + *

+ * public class UserLookupModule extends AbstractModule {
+ *   protected void configure() {
+ *     OptionalBinder.newOptionalBinder(binder(), Key.get(String.class, LookupUrl.class))
+ *         .setBinding().toInstance(CUSTOM_LOOKUP_URL);
+ *   }
+ * }
+ * ... which will override the default value. + * + *

If one module uses setDefault the only way to override the default is to use setBinding. + * It is an error for a user to specify the binding without using OptionalBinder if + * setDefault or setBinding are called. For example, + *


+ * public class FrameworkModule extends AbstractModule {
+ *   protected void configure() {
+ *     OptionalBinder.newOptionalBinder(binder(), Key.get(String.class, LookupUrl.class))
+ *         .setDefault().toInstance(DEFAULT_LOOKUP_URL);
+ *   }
+ * }
+ * public class UserLookupModule extends AbstractModule {
+ *   protected void configure() {
+ *     bind(Key.get(String.class, LookupUrl.class)).toInstance(CUSTOM_LOOKUP_URL);
+ *   }
+ * }
+ * ... would generate an error, because both the framework and the user are trying to bind + * {@code @LookupUrl String}. + */ +public abstract class OptionalBinder { + + /* Reflectively capture java 8's Optional types so we can bind them if we're running in java8. */ + private static final Class JAVA_OPTIONAL_CLASS; + private static final Method JAVA_EMPTY_METHOD; + private static final Method JAVA_OF_NULLABLE_METHOD; + + static { + Class optional = null; + Method empty = null; + Method ofNullable = null; + boolean useJavaOptional = false; + try { + optional = Class.forName("java.util.Optional"); + empty = optional.getDeclaredMethod("empty"); + ofNullable = optional.getDeclaredMethod("ofNullable", Object.class); + useJavaOptional = true; + } catch (ClassNotFoundException ignored) { + } catch (NoSuchMethodException ignored) { + } catch (SecurityException ignored) { + } + JAVA_OPTIONAL_CLASS = useJavaOptional ? optional : null; + JAVA_EMPTY_METHOD = useJavaOptional ? empty : null; + JAVA_OF_NULLABLE_METHOD = useJavaOptional ? ofNullable : null; + } + + private OptionalBinder() { + } + + public static OptionalBinder newOptionalBinder(Binder binder, Class type) { + return newRealOptionalBinder(binder, Key.get(type)); + } + + public static OptionalBinder newOptionalBinder(Binder binder, TypeLiteral type) { + return newRealOptionalBinder(binder, Key.get(type)); + } + + public static OptionalBinder newOptionalBinder(Binder binder, Key type) { + return newRealOptionalBinder(binder, type); + } + + static RealOptionalBinder newRealOptionalBinder(Binder binder, Key type) { + binder = binder.skipSources(OptionalBinder.class, RealOptionalBinder.class); + RealOptionalBinder optionalBinder = new RealOptionalBinder(binder, type); + binder.install(optionalBinder); + return optionalBinder; + } + + @SuppressWarnings("unchecked") + static TypeLiteral> optionalOf( + TypeLiteral type) { + return (TypeLiteral>) TypeLiteral.get( + Types.newParameterizedType(Optional.class, type.getType())); + } + + static TypeLiteral javaOptionalOf( + TypeLiteral type) { + checkState(JAVA_OPTIONAL_CLASS != null, "java.util.Optional not found"); + return TypeLiteral.get(Types.newParameterizedType(JAVA_OPTIONAL_CLASS, type.getType())); + } + + @SuppressWarnings("unchecked") + static TypeLiteral>> optionalOfJavaxProvider( + TypeLiteral type) { + return (TypeLiteral>>) TypeLiteral.get( + Types.newParameterizedType(Optional.class, + newParameterizedType(javax.inject.Provider.class, type.getType()))); + } + + static TypeLiteral javaOptionalOfJavaxProvider( + TypeLiteral type) { + checkState(JAVA_OPTIONAL_CLASS != null, "java.util.Optional not found"); + return TypeLiteral.get(Types.newParameterizedType(JAVA_OPTIONAL_CLASS, + newParameterizedType(javax.inject.Provider.class, type.getType()))); + } + + @SuppressWarnings("unchecked") + static TypeLiteral>> optionalOfProvider(TypeLiteral type) { + return (TypeLiteral>>) TypeLiteral.get(Types.newParameterizedType( + Optional.class, newParameterizedType(Provider.class, type.getType()))); + } + + static TypeLiteral javaOptionalOfProvider(TypeLiteral type) { + checkState(JAVA_OPTIONAL_CLASS != null, "java.util.Optional not found"); + return TypeLiteral.get(Types.newParameterizedType(JAVA_OPTIONAL_CLASS, + newParameterizedType(Provider.class, type.getType()))); + } + + @SuppressWarnings("unchecked") + static Key> providerOf(Key key) { + Type providerT = Types.providerOf(key.getTypeLiteral().getType()); + return (Key>) key.ofType(providerT); + } + + /** + * Returns a binding builder used to set the default value that will be injected. + * The binding set by this method will be ignored if {@link #setBinding} is called. + * + *

It is an error to call this method without also calling one of the {@code to} + * methods on the returned binding builder. + */ + public abstract LinkedBindingBuilder setDefault(); + + + /** + * Returns a binding builder used to set the actual value that will be injected. + * This overrides any binding set by {@link #setDefault}. + * + *

It is an error to call this method without also calling one of the {@code to} + * methods on the returned binding builder. + */ + public abstract LinkedBindingBuilder setBinding(); + + enum Source {DEFAULT, ACTUAL} + + @Retention(RUNTIME) + @Qualifier + @interface Default { + String value(); + } + + @Retention(RUNTIME) + @Qualifier + @interface Actual { + String value(); + } + + /** + * The actual OptionalBinder plays several roles. It implements Module to hide that + * fact from the public API, and installs the various bindings that are exposed to the user. + */ + static final class RealOptionalBinder extends OptionalBinder implements Module { + private final Key typeKey; + private final Key> optionalKey; + private final Key>> optionalJavaxProviderKey; + private final Key>> optionalProviderKey; + private final Provider>> optionalProviderT; + private final Key defaultKey; + private final Key actualKey; + + private final Key javaOptionalKey; + private final Key javaOptionalJavaxProviderKey; + private final Key javaOptionalProviderKey; + + /** + * the target injector's binder. non-null until initialization, null afterwards + */ + private Binder binder; + /** + * the default binding, for the SPI. + */ + private Binding defaultBinding; + /** + * the actual binding, for the SPI + */ + private Binding actualBinding; + + /** + * the dependencies -- initialized with defaults & overridden when tooled. + */ + private Set> dependencies; + /** + * the dependencies -- initialized with defaults & overridden when tooled. + */ + private Set> providerDependencies; + + private RealOptionalBinder(Binder binder, Key typeKey) { + this.binder = binder; + this.typeKey = checkNotNull(typeKey); + TypeLiteral literal = typeKey.getTypeLiteral(); + this.optionalKey = typeKey.ofType(optionalOf(literal)); + this.optionalJavaxProviderKey = typeKey.ofType(optionalOfJavaxProvider(literal)); + this.optionalProviderKey = typeKey.ofType(optionalOfProvider(literal)); + this.optionalProviderT = binder.getProvider(optionalProviderKey); + String name = RealElement.nameOf(typeKey); + this.defaultKey = Key.get(typeKey.getTypeLiteral(), new DefaultImpl(name)); + this.actualKey = Key.get(typeKey.getTypeLiteral(), new ActualImpl(name)); + // Until the injector initializes us, we don't know what our dependencies are, + // so initialize to the whole Injector (like Multibinder, and MapBinder indirectly). + this.dependencies = ImmutableSet.>of(Dependency.get(Key.get(Injector.class))); + this.providerDependencies = + ImmutableSet.>of(Dependency.get(Key.get(Injector.class))); + + if (JAVA_OPTIONAL_CLASS != null) { + this.javaOptionalKey = typeKey.ofType(javaOptionalOf(literal)); + this.javaOptionalJavaxProviderKey = typeKey.ofType(javaOptionalOfJavaxProvider(literal)); + this.javaOptionalProviderKey = typeKey.ofType(javaOptionalOfProvider(literal)); + } else { + this.javaOptionalKey = null; + this.javaOptionalJavaxProviderKey = null; + this.javaOptionalProviderKey = null; + } + } + + /** + * Adds a binding for T. Multiple calls to this are safe, and will be collapsed as duplicate + * bindings. + */ + private void addDirectTypeBinding(Binder binder) { + binder.bind(typeKey).toProvider(new RealDirectTypeProvider()); + } + + Key getKeyForDefaultBinding() { + checkConfiguration(!isInitialized(), "already initialized"); + addDirectTypeBinding(binder); + return defaultKey; + } + + @Override + public LinkedBindingBuilder setDefault() { + return binder.bind(getKeyForDefaultBinding()); + } + + Key getKeyForActualBinding() { + checkConfiguration(!isInitialized(), "already initialized"); + addDirectTypeBinding(binder); + return actualKey; + } + + @Override + public LinkedBindingBuilder setBinding() { + return binder.bind(getKeyForActualBinding()); + } + + @Override + public void configure(Binder binder) { + checkConfiguration(!isInitialized(), "OptionalBinder was already initialized"); + + binder.bind(optionalProviderKey).toProvider(new RealOptionalProviderProvider()); + + // Optional is immutable, so it's safe to expose Optional> as + // Optional> (since Guice provider implements javax Provider). + @SuppressWarnings({"unchecked", "cast"}) + Key massagedOptionalProviderKey = (Key) optionalProviderKey; + binder.bind(optionalJavaxProviderKey).to(massagedOptionalProviderKey); + + binder.bind(optionalKey).toProvider(new RealOptionalKeyProvider()); + + // Bind the java-8 types if we know them. + bindJava8Optional(binder); + } + + @SuppressWarnings("unchecked") + private void bindJava8Optional(Binder binder) { + if (JAVA_OPTIONAL_CLASS != null) { + binder.bind(javaOptionalKey).toProvider(new JavaOptionalProvider()); + binder.bind(javaOptionalProviderKey).toProvider(new JavaOptionalProviderProvider()); + // for the javax version we reuse the guice version since they're type-compatible. + binder.bind(javaOptionalJavaxProviderKey).to(javaOptionalProviderKey); + } + } + + private Binding getActualBinding() { + if (isInitialized()) { + return actualBinding; + } else { + throw new UnsupportedOperationException( + "getActualBinding() not supported from Elements.getElements, requires an Injector."); + } + } + + private Binding getDefaultBinding() { + if (isInitialized()) { + return defaultBinding; + } else { + throw new UnsupportedOperationException( + "getDefaultBinding() not supported from Elements.getElements, requires an Injector."); + } + } + + private boolean containsElement(Element element) { + Key elementKey; + if (element instanceof Binding) { + elementKey = ((Binding) element).getKey(); + } else if (element instanceof ProviderLookup) { + elementKey = ((ProviderLookup) element).getKey(); + } else { + return false; // cannot match; + } + + return elementKey.equals(optionalKey) + || elementKey.equals(optionalProviderKey) + || elementKey.equals(optionalJavaxProviderKey) + || elementKey.equals(defaultKey) + || elementKey.equals(actualKey) + || matchesJ8Keys(elementKey) + || matchesTypeKey(element, elementKey); + } + + private boolean matchesJ8Keys(Key elementKey) { + if (JAVA_OPTIONAL_CLASS != null) { + return elementKey.equals(javaOptionalKey) + || elementKey.equals(javaOptionalProviderKey) + || elementKey.equals(javaOptionalJavaxProviderKey); + } + return false; + } + + /** + * Returns true if the key & element indicate they were bound by this OptionalBinder. + */ + private boolean matchesTypeKey(Element element, Key elementKey) { + // Just doing .equals(typeKey) isn't enough, because the user can bind that themselves. + return elementKey.equals(typeKey) + && element instanceof ProviderInstanceBinding + && (((ProviderInstanceBinding) element) + .getUserSuppliedProvider() instanceof RealOptionalBinderProviderWithDependencies); + } + + private boolean isInitialized() { + return binder == null; + } + + @Override + public boolean equals(Object o) { + return o instanceof RealOptionalBinder + && ((RealOptionalBinder) o).typeKey.equals(typeKey); + } + + @Override + public int hashCode() { + return typeKey.hashCode(); + } + + /** + * A base class for ProviderWithDependencies that need equality based on a specific object. + */ + private abstract static class RealOptionalBinderProviderWithDependencies implements + ProviderWithDependencies { + private final Object equality; + + public RealOptionalBinderProviderWithDependencies(Object equality) { + this.equality = equality; + } + + @Override + public boolean equals(Object obj) { + return this.getClass() == obj.getClass() + && equality.equals(((RealOptionalBinderProviderWithDependencies) obj).equality); + } + + @Override + public int hashCode() { + return equality.hashCode(); + } + } + + @SuppressWarnings("rawtypes") + final class JavaOptionalProvider extends RealOptionalBinderProviderWithDependencies + implements ProviderWithExtensionVisitor, OptionalBinderBinding { + private JavaOptionalProvider() { + super(typeKey); + } + + @Override + public Object get() { + Optional> optional = optionalProviderT.get(); + try { + if (optional.isPresent()) { + return JAVA_OF_NULLABLE_METHOD.invoke(JAVA_OPTIONAL_CLASS, optional.get().get()); + } else { + return JAVA_EMPTY_METHOD.invoke(JAVA_OPTIONAL_CLASS); + } + } catch (IllegalAccessException e) { + throw new SecurityException(e); + } catch (IllegalArgumentException e) { + throw new IllegalStateException(e); + } catch (InvocationTargetException e) { + throw Throwables.propagate(e.getCause()); + } + } + + @Override + public Set> getDependencies() { + return dependencies; + } + + @SuppressWarnings("unchecked") + @Override + public Object acceptExtensionVisitor(BindingTargetVisitor visitor, + ProviderInstanceBinding binding) { + if (visitor instanceof MultibindingsTargetVisitor) { + return ((MultibindingsTargetVisitor) visitor).visit(this); + } else { + return visitor.visit(binding); + } + } + + @Override + public boolean containsElement(Element element) { + return RealOptionalBinder.this.containsElement(element); + } + + @Override + public Binding getActualBinding() { + return RealOptionalBinder.this.getActualBinding(); + } + + @Override + public Binding getDefaultBinding() { + return RealOptionalBinder.this.getDefaultBinding(); + } + + @Override + public Key getKey() { + return javaOptionalKey; + } + } + + @SuppressWarnings("rawtypes") + final class JavaOptionalProviderProvider extends RealOptionalBinderProviderWithDependencies { + private JavaOptionalProviderProvider() { + super(typeKey); + } + + @Override + public Object get() { + Optional> optional = optionalProviderT.get(); + try { + if (optional.isPresent()) { + return JAVA_OF_NULLABLE_METHOD.invoke(JAVA_OPTIONAL_CLASS, optional.get()); + } else { + return JAVA_EMPTY_METHOD.invoke(JAVA_OPTIONAL_CLASS); + } + } catch (IllegalAccessException e) { + throw new SecurityException(e); + } catch (IllegalArgumentException e) { + throw new IllegalStateException(e); + } catch (InvocationTargetException e) { + throw Throwables.propagate(e.getCause()); + } + } + + @Override + public Set> getDependencies() { + return providerDependencies; + } + } + + final class RealDirectTypeProvider extends RealOptionalBinderProviderWithDependencies { + private RealDirectTypeProvider() { + super(typeKey); + } + + @Override + public T get() { + Optional> optional = optionalProviderT.get(); + if (optional.isPresent()) { + return optional.get().get(); + } + // Let Guice handle blowing up if the injection point doesn't have @Nullable + // (If it does have @Nullable, that's fine. This would only happen if + // setBinding/setDefault themselves were bound to 'null'). + return null; + } + + @Override + public Set> getDependencies() { + return dependencies; + } + } + + final class RealOptionalProviderProvider + extends RealOptionalBinderProviderWithDependencies>> { + private Optional> optional; + + private RealOptionalProviderProvider() { + super(typeKey); + } + + @Toolable + @Inject + void initialize(Injector injector) { + RealOptionalBinder.this.binder = null; + actualBinding = injector.getExistingBinding(actualKey); + defaultBinding = injector.getExistingBinding(defaultKey); + Binding userBinding = injector.getExistingBinding(typeKey); + Binding binding = null; + if (actualBinding != null) { + // TODO(sameb): Consider exposing an option that will allow + // ACTUAL to fallback to DEFAULT if ACTUAL's provider returns null. + // Right now, an ACTUAL binding can convert from present -> absent + // if it's bound to a provider that returns null. + binding = actualBinding; + } else if (defaultBinding != null) { + binding = defaultBinding; + } else if (userBinding != null) { + // If neither the actual or default is set, then we fallback + // to the value bound to the type itself and consider that the + // "actual binding" for the SPI. + binding = userBinding; + actualBinding = userBinding; + } + + if (binding != null) { + optional = Optional.of(binding.getProvider()); + RealOptionalBinder.this.dependencies = + ImmutableSet.>of(Dependency.get(binding.getKey())); + RealOptionalBinder.this.providerDependencies = + ImmutableSet.>of(Dependency.get(providerOf(binding.getKey()))); + } else { + optional = Optional.absent(); + RealOptionalBinder.this.dependencies = ImmutableSet.of(); + RealOptionalBinder.this.providerDependencies = ImmutableSet.of(); + } + } + + @Override + public Optional> get() { + return optional; + } + + @Override + public Set> getDependencies() { + return providerDependencies; + } + } + + final class RealOptionalKeyProvider + extends RealOptionalBinderProviderWithDependencies> + implements ProviderWithExtensionVisitor>, + OptionalBinderBinding>, + Provider> { + private RealOptionalKeyProvider() { + super(typeKey); + } + + @Override + public Optional get() { + Optional> optional = optionalProviderT.get(); + if (optional.isPresent()) { + return Optional.fromNullable(optional.get().get()); + } else { + return Optional.absent(); + } + } + + @Override + public Set> getDependencies() { + return dependencies; + } + + @SuppressWarnings("unchecked") + @Override + public R acceptExtensionVisitor(BindingTargetVisitor visitor, + ProviderInstanceBinding binding) { + if (visitor instanceof MultibindingsTargetVisitor) { + return ((MultibindingsTargetVisitor, R>) visitor).visit(this); + } else { + return visitor.visit(binding); + } + } + + @Override + public Key> getKey() { + return optionalKey; + } + + @Override + public Binding getActualBinding() { + return RealOptionalBinder.this.getActualBinding(); + } + + @Override + public Binding getDefaultBinding() { + return RealOptionalBinder.this.getDefaultBinding(); + } + + @Override + public boolean containsElement(Element element) { + return RealOptionalBinder.this.containsElement(element); + } + } + } + + static class DefaultImpl extends BaseAnnotation implements Default { + public DefaultImpl(String value) { + super(Default.class, value); + } + } + + static class ActualImpl extends BaseAnnotation implements Actual { + public ActualImpl(String value) { + super(Actual.class, value); + } + } + + abstract static class BaseAnnotation implements Serializable, Annotation { + + private static final long serialVersionUID = 0; + private final String value; + private final Class clazz; + + BaseAnnotation(Class clazz, String value) { + this.clazz = checkNotNull(clazz, "clazz"); + this.value = checkNotNull(value, "value"); + } + + public String value() { + return this.value; + } + + @Override + public int hashCode() { + // This is specified in java.lang.Annotation. + return (127 * "value".hashCode()) ^ value.hashCode(); + } + + @Override + public boolean equals(Object o) { + // We check against each annotation type instead of BaseAnnotation + // so that we can compare against generated annotation implementations. + if (o instanceof Actual && clazz == Actual.class) { + Actual other = (Actual) o; + return value.equals(other.value()); + } else if (o instanceof Default && clazz == Default.class) { + Default other = (Default) o; + return value.equals(other.value()); + } + return false; + } + + @Override + public String toString() { + return "@" + clazz.getName() + (value.isEmpty() ? "" : "(value=" + value + ")"); + } + + @Override + public Class annotationType() { + return clazz; + } + } +} diff --git a/src/main/java/com/google/inject/multibindings/OptionalBinderBinding.java b/src/main/java/com/google/inject/multibindings/OptionalBinderBinding.java new file mode 100644 index 0000000..4a0c80e --- /dev/null +++ b/src/main/java/com/google/inject/multibindings/OptionalBinderBinding.java @@ -0,0 +1,58 @@ +package com.google.inject.multibindings; + +import com.google.inject.Binding; +import com.google.inject.Key; +import com.google.inject.spi.Element; +import com.google.inject.spi.Elements; + +/** + * A binding for a OptionalBinder. + * + *

Although OptionalBinders may be injected through a variety of types + * {@code T}, {@code Optional}, {@code Optional>}, etc..), an + * OptionalBinderBinding exists only on the Binding associated with the + * {@code Optional} key. Other bindings can be validated to be derived from this + * OptionalBinderBinding using {@link #containsElement}. + * + * @param The fully qualified type of the optional binding, including Optional. + * For example: {@code Optional}. + */ +public interface OptionalBinderBinding { + + /** + * Returns the {@link Key} for this binding. + */ + Key getKey(); + + /** + * Returns the default binding (set by {@link OptionalBinder#setDefault}) if one exists or null + * if no default binding is set. This will throw {@link UnsupportedOperationException} if it is + * called on an element retrieved from {@link Elements#getElements}. + *

+ * The Binding's type will always match the type Optional's generic type. For example, if getKey + * returns a key of Optional<String>, then this will always return a + * Binding<String>. + */ + Binding getDefaultBinding(); + + /** + * Returns the actual binding (set by {@link OptionalBinder#setBinding}) or null if not set. + * This will throw {@link UnsupportedOperationException} if it is called on an element retrieved + * from {@link Elements#getElements}. + *

+ * The Binding's type will always match the type Optional's generic type. For example, if getKey + * returns a key of Optional<String>, then this will always return a + * Binding<String>. + */ + Binding getActualBinding(); + + /** + * Returns true if this OptionalBinder contains the given Element in order to build the optional + * binding or uses the given Element in order to support building and injecting its data. This + * will work for OptionalBinderBinding retrieved from an injector and + * {@link Elements#getElements}. Usually this is only necessary if you are working with elements + * retrieved from modules (without an Injector), otherwise {@link #getDefaultBinding} and + * {@link #getActualBinding} are better options. + */ + boolean containsElement(Element element); +} diff --git a/src/main/java/com/google/inject/multibindings/ProvidesIntoMap.java b/src/main/java/com/google/inject/multibindings/ProvidesIntoMap.java new file mode 100644 index 0000000..fd364b2 --- /dev/null +++ b/src/main/java/com/google/inject/multibindings/ProvidesIntoMap.java @@ -0,0 +1,40 @@ +package com.google.inject.multibindings; + +import com.google.inject.Module; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * Annotates methods of a {@link Module} to add items to a {@link MapBinder}. + * The method's return type, binding annotation and additional key annotation determines + * what Map this will contribute to. For example, + * + *

+ * {@literal @}ProvidesIntoMap
+ * {@literal @}StringMapKey("Foo")
+ * {@literal @}Named("plugins")
+ * Plugin provideFooUrl(FooManager fm) { returm fm.getPlugin(); }
+ *
+ * {@literal @}ProvidesIntoMap
+ * {@literal @}StringMapKey("Bar")
+ * {@literal @}Named("urls")
+ * Plugin provideBarUrl(BarManager bm) { return bm.getPlugin(); }
+ * 
+ * + * will add two items to the {@code @Named("urls") Map} map. The key 'Foo' + * will map to the provideFooUrl method, and the key 'Bar' will map to the provideBarUrl method. + * The values are bound as providers and will be evaluated at injection time. + * + *

Because the key is specified as an annotation, only Strings, Classes, enums, primitive + * types and annotation instances are supported as keys. + */ +@Documented +@Target(METHOD) +@Retention(RUNTIME) +public @interface ProvidesIntoMap { +} diff --git a/src/main/java/com/google/inject/multibindings/ProvidesIntoOptional.java b/src/main/java/com/google/inject/multibindings/ProvidesIntoOptional.java new file mode 100644 index 0000000..1251901 --- /dev/null +++ b/src/main/java/com/google/inject/multibindings/ProvidesIntoOptional.java @@ -0,0 +1,50 @@ +package com.google.inject.multibindings; + +import com.google.inject.Module; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * Annotates methods of a {@link Module} to add items to a {@link Multibinder}. + * The method's return type and binding annotation determines what Optional this will + * contribute to. For example, + * + *

+ * {@literal @}ProvidesIntoOptional(DEFAULT)
+ * {@literal @}Named("url")
+ * String provideFooUrl(FooManager fm) { returm fm.getUrl(); }
+ *
+ * {@literal @}ProvidesIntoOptional(ACTUAL)
+ * {@literal @}Named("url")
+ * String provideBarUrl(BarManager bm) { return bm.getUrl(); }
+ * 
+ * + * will set the default value of {@code @Named("url") Optional} to foo's URL, + * and then override it to bar's URL. + */ +@Documented +@Target(METHOD) +@Retention(RUNTIME) +public @interface ProvidesIntoOptional { + /** + * Specifies if the binding is for the actual or default value. + */ + Type value(); + + enum Type { + /** + * Corresponds to {@link OptionalBinder#setBinding}. + */ + ACTUAL, + + /** + * Corresponds to {@link OptionalBinder#setDefault}. + */ + DEFAULT + } +} diff --git a/src/main/java/com/google/inject/multibindings/ProvidesIntoSet.java b/src/main/java/com/google/inject/multibindings/ProvidesIntoSet.java new file mode 100644 index 0000000..96965a1 --- /dev/null +++ b/src/main/java/com/google/inject/multibindings/ProvidesIntoSet.java @@ -0,0 +1,34 @@ +package com.google.inject.multibindings; + +import com.google.inject.Module; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * Annotates methods of a {@link Module} to add items to a {@link Multibinder}. + * The method's return type and binding annotation determines what Set this will + * contribute to. For example, + * + *
+ * {@literal @}ProvidesIntoSet
+ * {@literal @}Named("urls")
+ * String provideFooUrl(FooManager fm) { returm fm.getUrl(); }
+ *
+ * {@literal @}ProvidesIntoSet
+ * {@literal @}Named("urls")
+ * String provideBarUrl(BarManager bm) { return bm.getUrl(); }
+ * 
+ * + * will add two items to the {@code @Named("urls") Set} set. The items are bound as + * providers and will be evaluated at injection time. + */ +@Documented +@Target(METHOD) +@Retention(RUNTIME) +public @interface ProvidesIntoSet { +} diff --git a/src/main/java/com/google/inject/multibindings/RealElement.java b/src/main/java/com/google/inject/multibindings/RealElement.java new file mode 100644 index 0000000..195f485 --- /dev/null +++ b/src/main/java/com/google/inject/multibindings/RealElement.java @@ -0,0 +1,98 @@ +package com.google.inject.multibindings; + +import com.google.inject.Key; +import com.google.inject.internal.Annotations; + +import java.lang.annotation.Annotation; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * An implementation of Element. + */ +// TODO(cgruber): Use AutoAnnotation when available, here & wherever else is makes sense. +class RealElement implements Element { + private static final AtomicInteger nextUniqueId = new AtomicInteger(1); + + private final int uniqueId; + private final String setName; + private final Type type; + private final String keyType; + + RealElement(String setName, Type type, String keyType) { + this(setName, type, keyType, nextUniqueId.incrementAndGet()); + } + + RealElement(String setName, Type type, String keyType, int uniqueId) { + this.uniqueId = uniqueId; + this.setName = setName; + this.type = type; + this.keyType = keyType; + } + + /** + * Returns the name the binding should use. This is based on the annotation. + * If the annotation has an instance and is not a marker annotation, + * we ask the annotation for its toString. If it was a marker annotation + * or just an annotation type, we use the annotation's name. Otherwise, + * the name is the empty string. + */ + static String nameOf(Key key) { + Annotation annotation = key.getAnnotation(); + Class annotationType = key.getAnnotationType(); + if (annotation != null && !Annotations.isMarker(annotationType)) { + return key.getAnnotation().toString(); + } else if (key.getAnnotationType() != null) { + return "@" + key.getAnnotationType().getName(); + } else { + return ""; + } + } + + @Override + public String setName() { + return setName; + } + + @Override + public int uniqueId() { + return uniqueId; + } + + @Override + public Type type() { + return type; + } + + @Override + public String keyType() { + return keyType; + } + + @Override + public Class annotationType() { + return Element.class; + } + + @Override + public String toString() { + return "@" + Element.class.getName() + "(setName=" + setName + + ",uniqueId=" + uniqueId + ", type=" + type + ", keyType=" + keyType + ")"; + } + + @Override + public boolean equals(Object o) { + return o instanceof Element + && ((Element) o).setName().equals(setName()) + && ((Element) o).uniqueId() == uniqueId() + && ((Element) o).type() == type() + && ((Element) o).keyType().equals(keyType()); + } + + @Override + public int hashCode() { + return ((127 * "setName".hashCode()) ^ setName.hashCode()) + + ((127 * "uniqueId".hashCode()) ^ uniqueId) + + ((127 * "type".hashCode()) ^ type.hashCode()) + + ((127 * "keyType".hashCode()) ^ keyType.hashCode()); + } +} diff --git a/src/main/java/com/google/inject/multibindings/StringMapKey.java b/src/main/java/com/google/inject/multibindings/StringMapKey.java new file mode 100644 index 0000000..6d631bc --- /dev/null +++ b/src/main/java/com/google/inject/multibindings/StringMapKey.java @@ -0,0 +1,19 @@ +package com.google.inject.multibindings; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * Allows {@literal @}{@link ProvidesIntoMap} to specify a string map key. + */ +@MapKey(unwrapValue = true) +@Documented +@Target(METHOD) +@Retention(RUNTIME) +public @interface StringMapKey { + String value(); +} diff --git a/src/test/java/com/google/inject/assistedinject/ExtensionSpiTest.java b/src/test/java/com/google/inject/assistedinject/ExtensionSpiTest.java new file mode 100644 index 0000000..ab6405f --- /dev/null +++ b/src/test/java/com/google/inject/assistedinject/ExtensionSpiTest.java @@ -0,0 +1,202 @@ +package com.google.inject.assistedinject; + +import static com.google.inject.name.Names.named; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; +import com.google.common.collect.Sets; +import com.google.inject.AbstractModule; +import com.google.inject.Binding; +import com.google.inject.Guice; +import com.google.inject.Inject; +import com.google.inject.Injector; +import com.google.inject.Key; +import com.google.inject.Stage; +import com.google.inject.name.Named; +import com.google.inject.spi.DefaultBindingTargetVisitor; +import com.google.inject.spi.Dependency; +import com.google.inject.spi.Element; +import com.google.inject.spi.Elements; + +import junit.framework.AssertionFailedError; +import junit.framework.TestCase; + +import java.util.List; +import java.util.Set; +import java.util.logging.Logger; + +/** + * Tests for AssistedInject Spi. + * + */ +public class ExtensionSpiTest extends TestCase { + + public final void testSpiOnElements() throws Exception { + AssistedInjectSpiVisitor visitor = new AssistedInjectSpiVisitor(); + Integer count = 0; + for(Element element : Elements.getElements(new Module())) { + if(element instanceof Binding) { + assertEquals(count++, ((Binding)element).acceptTargetVisitor(visitor)); + } + } + validateVisitor(visitor); + } + + public void testSpiOnVisitor() throws Exception { + AssistedInjectSpiVisitor visitor = new AssistedInjectSpiVisitor(); + Integer count = 0; + Injector injector = Guice.createInjector(new Module()); + for(Binding binding : injector.getBindings().values()) { + assertEquals(count++, binding.acceptTargetVisitor(visitor)); + } + validateVisitor(visitor); + } + + private void validateVisitor(AssistedInjectSpiVisitor visitor) throws Exception { + assertEquals(1, visitor.assistedBindingCount); + List assistedMethods = + Lists.newArrayList(Iterables.getOnlyElement( + visitor.assistedInjectBindings).getAssistedMethods()); + assertEquals(7, assistedMethods.size()); + assertEquals(1, visitor.assistedBindingCount); + assertEquals(1, visitor.assistedInjectBindings.size()); + + // Validate for each of the methods in AnimalFactory + + Set names = Sets.newHashSet(); + for (AssistedMethod method : assistedMethods) { + String name = method.getFactoryMethod().getName(); + names.add(name); + switch (name) { + case "createAStrangeCatAsAnimal": + validateAssistedMethod(method, name, StrangeCat.class, ImmutableList.>of()); + break; + case "createStrangeCatWithConstructorForOwner": + validateAssistedMethod(method, name, StrangeCat.class, ImmutableList.>of()); + break; + case "createStrangeCatWithConstructorForAge": + validateAssistedMethod(method, name, StrangeCat.class, ImmutableList.>of()); + break; + case "createCatWithANonAssistedDependency": + validateAssistedMethod(method, name, CatWithAName.class, + ImmutableList.>of(Key.get(String.class, named("catName2")))); + break; + case "createCat": + validateAssistedMethod(method, name, Cat.class, ImmutableList.>of()); + break; + case "createASimpleCatAsAnimal": + validateAssistedMethod(method, name, SimpleCat.class, ImmutableList.>of()); + break; + case "createCatWithNonAssistedDependencies": + List> dependencyKeys = ImmutableList.>of( + Key.get(String.class, named("catName1")), + Key.get(String.class, named("petName")), + Key.get(Integer.class, named("age"))); + validateAssistedMethod(method, name, ExplodingCat.class, dependencyKeys); + break; + default: + fail("Invalid method: " + method); + break; + } + } + assertEquals(names, ImmutableSet.of("createAStrangeCatAsAnimal", + "createStrangeCatWithConstructorForOwner", + "createStrangeCatWithConstructorForAge", + "createCatWithANonAssistedDependency", + "createCat", + "createASimpleCatAsAnimal", + "createCatWithNonAssistedDependencies")); + } + + private void validateAssistedMethod(AssistedMethod assistedMethod, String factoryMethodName, + Class clazz, List> dependencyKeys){ + assertEquals(factoryMethodName, assistedMethod.getFactoryMethod().getName()); + assertEquals(clazz, assistedMethod.getImplementationConstructor().getDeclaringClass()); + assertEquals(dependencyKeys.size(), assistedMethod.getDependencies().size()); + for (Dependency dependency : assistedMethod.getDependencies()) { + assertTrue(dependencyKeys.contains(dependency.getKey())); + } + assertEquals(clazz, assistedMethod.getImplementationType().getType()); + } + + + interface AnimalFactory { + Cat createCat(String owner); + CatWithAName createCatWithANonAssistedDependency(String owner); + @Named("SimpleCat") Animal createASimpleCatAsAnimal(String owner); + Animal createAStrangeCatAsAnimal(String owner); + StrangeCat createStrangeCatWithConstructorForOwner(String owner); + StrangeCat createStrangeCatWithConstructorForAge(Integer age); + ExplodingCat createCatWithNonAssistedDependencies(String owner); + } + + interface Animal {} + + private static class Cat implements Animal { + @Inject Cat(@Assisted String owner) {} + } + + private static class SimpleCat implements Animal { + @Inject SimpleCat(@Assisted String owner) { + } + } + + private static class StrangeCat implements Animal { + @AssistedInject StrangeCat(@Assisted String owner) {} + @AssistedInject StrangeCat(@Assisted Integer age) {} + } + + private static class ExplodingCat implements Animal { + @Inject public ExplodingCat(@Named("catName1") String name, @Assisted String owner, + @Named("age") Integer age, @Named("petName") String petName) {} + } + + private static class CatWithAName extends Cat { + @Inject CatWithAName(@Assisted String owner, @Named("catName2") String name) { + super(owner); + } + } + + public class Module extends AbstractModule{ + @Override + protected void configure() { + bind(String.class).annotatedWith(named("catName1")).toInstance("kitty1"); + bind(String.class).annotatedWith(named("catName2")).toInstance("kitty2"); + bind(String.class).annotatedWith(named("petName")).toInstance("pussy"); + bind(Integer.class).annotatedWith(named("age")).toInstance(12); + install(new FactoryModuleBuilder() + .implement(Animal.class, StrangeCat.class) + .implement(Animal.class, named("SimpleCat"), SimpleCat.class) + .build(AnimalFactory.class)); + } + } + + public class AssistedInjectSpiVisitor extends DefaultBindingTargetVisitor + implements AssistedInjectTargetVisitor { + + private final Set allowedClasses = + ImmutableSet. of( + Injector.class, Stage.class, Logger.class, + String.class, Integer.class); + + private int assistedBindingCount = 0; + private int currentCount = 0; + private List> assistedInjectBindings = Lists.newArrayList(); + + public Integer visit(AssistedInjectBinding assistedInjectBinding) { + assistedInjectBindings.add(assistedInjectBinding); + assistedBindingCount++; + return currentCount++; + } + + @Override + protected Integer visitOther(Binding binding) { + if(!allowedClasses.contains(binding.getKey().getTypeLiteral().getRawType())) { + throw new AssertionFailedError("invalid other binding: " + binding); + } + return currentCount++; + } + } +} diff --git a/src/test/java/com/google/inject/assistedinject/FactoryModuleBuilderTest.java b/src/test/java/com/google/inject/assistedinject/FactoryModuleBuilderTest.java new file mode 100644 index 0000000..f53e4e9 --- /dev/null +++ b/src/test/java/com/google/inject/assistedinject/FactoryModuleBuilderTest.java @@ -0,0 +1,524 @@ +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 junit.framework.TestCase; + +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +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 factory = injector.getInstance(Key.get(new TypeLiteral>() {})); + @SuppressWarnings("unused") + Foo 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 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 { + static interface Factory { + Foo create(Bar bar); + } + @SuppressWarnings("unused") + @Inject Foo(@Assisted Bar bar, Baz baz) {} + } + + public static class Bar {} + @SuppressWarnings("unused") + public static class Baz {} + + 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> expectedKeys = ImmutableSet.>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 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> expectedKeys, Binding binding) { + Set> dependencies = ((HasDependencies)binding).getDependencies(); + Set> actualKeys = new HashSet>(); + 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)); + } + }); + } 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) { + } + } + +} diff --git a/src/test/java/com/google/inject/assistedinject/FactoryProvider2Test.java b/src/test/java/com/google/inject/assistedinject/FactoryProvider2Test.java new file mode 100644 index 0000000..7803756 --- /dev/null +++ b/src/test/java/com/google/inject/assistedinject/FactoryProvider2Test.java @@ -0,0 +1,1188 @@ +/** + * 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.assistedinject; + +import static com.google.inject.Asserts.assertContains; +import static com.google.inject.Asserts.assertEqualsBothWays; + +import com.google.inject.AbstractModule; +import com.google.inject.ConfigurationException; +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.Provider; +import com.google.inject.Stage; +import com.google.inject.TypeLiteral; +import com.google.inject.assistedinject.FactoryProvider2Test.Equals.ComparisonMethod; +import com.google.inject.assistedinject.FactoryProvider2Test.Equals.Impl; +import com.google.inject.name.Named; +import com.google.inject.name.Names; + +import junit.framework.TestCase; + +import java.util.Collection; +import java.util.Collections; +import java.util.Set; + +@SuppressWarnings("deprecation") +public class FactoryProvider2Test extends TestCase { + + private enum Color { BLUE, GREEN, RED, GRAY, BLACK, ORANGE, PINK } + + public void testAssistedFactory() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + bind(Double.class).toInstance(5.0d); + bind(ColoredCarFactory.class).toProvider( + FactoryProvider.newFactory(ColoredCarFactory.class, Mustang.class)); + } + }); + ColoredCarFactory carFactory = injector.getInstance(ColoredCarFactory.class); + + Mustang blueMustang = (Mustang) carFactory.create(Color.BLUE); + assertEquals(Color.BLUE, blueMustang.color); + assertEquals(5.0d, blueMustang.engineSize); + + Mustang redMustang = (Mustang) carFactory.create(Color.RED); + assertEquals(Color.RED, redMustang.color); + assertEquals(5.0d, redMustang.engineSize); + } + + public void testAssistedFactoryWithAnnotations() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + bind(int.class).annotatedWith(Names.named("horsePower")).toInstance(250); + bind(int.class).annotatedWith(Names.named("modelYear")).toInstance(1984); + bind(ColoredCarFactory.class).toProvider( + FactoryProvider.newFactory(ColoredCarFactory.class, Camaro.class)); + } + }); + + ColoredCarFactory carFactory = injector.getInstance(ColoredCarFactory.class); + + Camaro blueCamaro = (Camaro) carFactory.create(Color.BLUE); + assertEquals(Color.BLUE, blueCamaro.color); + assertEquals(1984, blueCamaro.modelYear); + assertEquals(250, blueCamaro.horsePower); + + Camaro redCamaro = (Camaro) carFactory.create(Color.RED); + assertEquals(Color.RED, redCamaro.color); + assertEquals(1984, redCamaro.modelYear); + assertEquals(250, redCamaro.horsePower); + } + + public interface Car {} + + interface ColoredCarFactory { + Car create(Color color); + } + + public static class Mustang implements Car { + private final double engineSize; + private final Color color; + + @Inject + public Mustang(double engineSize, @Assisted Color color) { + this.engineSize = engineSize; + this.color = color; + } + + public void drive() {} + } + + public static class Camaro implements Car { + private final int horsePower; + private final int modelYear; + private final Color color; + + @Inject + public Camaro( + @Named("horsePower") int horsePower, + @Named("modelYear") int modelYear, + @Assisted Color color) { + this.horsePower = horsePower; + this.modelYear = modelYear; + this.color = color; + } + } + + interface SummerCarFactory { + Car create(Color color, boolean convertable); + } + + public void testFactoryUsesInjectedConstructor() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + bind(float.class).toInstance(140f); + bind(SummerCarFactory.class).toProvider( + FactoryProvider.newFactory(SummerCarFactory.class, Corvette.class)); + } + }); + + SummerCarFactory carFactory = injector.getInstance(SummerCarFactory.class); + + Corvette redCorvette = (Corvette) carFactory.create(Color.RED, false); + assertEquals(Color.RED, redCorvette.color); + assertEquals(140f, redCorvette.maxMph); + assertFalse(redCorvette.isConvertable); + } + + public static class Corvette implements Car { + private boolean isConvertable; + private Color color; + private float maxMph; + + @SuppressWarnings("unused") + public Corvette(Color color, boolean isConvertable) { + throw new IllegalStateException("Not an @AssistedInject constructor"); + } + + @Inject + public Corvette(@Assisted Color color, Float maxMph, @Assisted boolean isConvertable) { + this.isConvertable = isConvertable; + this.color = color; + this.maxMph = maxMph; + } + } + + public void testConstructorDoesntNeedAllFactoryMethodArguments() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + bind(SummerCarFactory.class).toProvider( + FactoryProvider.newFactory(SummerCarFactory.class, Beetle.class)); + } + }); + SummerCarFactory factory = injector.getInstance(SummerCarFactory.class); + + Beetle beetle = (Beetle) factory.create(Color.RED, true); + assertSame(Color.RED, beetle.color); + } + + public static class Beetle implements Car { + private final Color color; + @Inject + public Beetle(@Assisted Color color) { + this.color = color; + } + } + + public void testMethodsAndFieldsGetInjected() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + bind(String.class).toInstance("turbo"); + bind(int.class).toInstance(911); + bind(double.class).toInstance(50000d); + bind(ColoredCarFactory.class).toProvider( + FactoryProvider.newFactory(ColoredCarFactory.class, Porsche.class)); + } + }); + ColoredCarFactory carFactory = injector.getInstance(ColoredCarFactory.class); + + Porsche grayPorsche = (Porsche) carFactory.create(Color.GRAY); + assertEquals(Color.GRAY, grayPorsche.color); + assertEquals(50000d, grayPorsche.price); + assertEquals(911, grayPorsche.model); + assertEquals("turbo", grayPorsche.name); + } + + public static class Porsche implements Car { + private final Color color; + private final double price; + private @Inject String name; + private int model; + + @Inject + public Porsche(@Assisted Color color, double price) { + this.color = color; + this.price = price; + } + + @Inject void setModel(int model) { + this.model = model; + } + } + + public void testProviderInjection() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + bind(String.class).toInstance("trans am"); + bind(ColoredCarFactory.class).toProvider( + FactoryProvider.newFactory(ColoredCarFactory.class, Firebird.class)); + } + }); + ColoredCarFactory carFactory = injector.getInstance(ColoredCarFactory.class); + + Firebird blackFirebird = (Firebird) carFactory.create(Color.BLACK); + assertEquals(Color.BLACK, blackFirebird.color); + assertEquals("trans am", blackFirebird.modifiersProvider.get()); + } + + public static class Firebird implements Car { + private final Provider modifiersProvider; + private final Color color; + + @Inject + public Firebird(Provider modifiersProvider, @Assisted Color color) { + this.modifiersProvider = modifiersProvider; + this.color = color; + } + } + + public void testAssistedProviderInjection() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + bind(String.class).toInstance("trans am"); + bind(ColoredCarFactory.class).toProvider( + FactoryProvider.newFactory(ColoredCarFactory.class, Flamingbird.class)); + } + }); + ColoredCarFactory carFactory = injector.getInstance(ColoredCarFactory.class); + + Flamingbird flamingbird = (Flamingbird) carFactory.create(Color.BLACK); + assertEquals(Color.BLACK, flamingbird.colorProvider.get()); + assertEquals("trans am", flamingbird.modifiersProvider.get()); + + Flamingbird flamingbird2 = (Flamingbird) carFactory.create(Color.RED); + assertEquals(Color.RED, flamingbird2.colorProvider.get()); + assertEquals("trans am", flamingbird2.modifiersProvider.get()); + // Make sure the original flamingbird is black still. + assertEquals(Color.BLACK, flamingbird.colorProvider.get()); + } + + public static class Flamingbird implements Car { + private final Provider modifiersProvider; + private final Provider colorProvider; + + @Inject + public Flamingbird(Provider modifiersProvider, @Assisted Provider colorProvider) { + this.modifiersProvider = modifiersProvider; + this.colorProvider = colorProvider; + } + } + + public void testTypeTokenInjection() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + bind(new TypeLiteral>() {}).toInstance(Collections.singleton("Flux Capacitor")); + bind(new TypeLiteral>() {}).toInstance(Collections.singleton(88)); + bind(ColoredCarFactory.class).toProvider( + FactoryProvider.newFactory(ColoredCarFactory.class, DeLorean.class)); + } + }); + ColoredCarFactory carFactory = injector.getInstance(ColoredCarFactory.class); + + DeLorean deLorean = (DeLorean) carFactory.create(Color.GRAY); + assertEquals(Color.GRAY, deLorean.color); + assertEquals("Flux Capacitor", deLorean.features.iterator().next()); + assertEquals(new Integer(88), deLorean.featureActivationSpeeds.iterator().next()); + } + + public static class DeLorean implements Car { + private final Set features; + private final Set featureActivationSpeeds; + private final Color color; + + @Inject + public DeLorean( + Set extraFeatures, Set featureActivationSpeeds, @Assisted Color color) { + this.features = extraFeatures; + this.featureActivationSpeeds = featureActivationSpeeds; + this.color = color; + } + } + + public void testTypeTokenProviderInjection() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + bind(new TypeLiteral>() { }).toInstance(Collections.singleton("Datsun")); + bind(ColoredCarFactory.class).toProvider( + FactoryProvider.newFactory(ColoredCarFactory.class, Z.class)); + } + }); + ColoredCarFactory carFactory = injector.getInstance(ColoredCarFactory.class); + + Z orangeZ = (Z) carFactory.create(Color.ORANGE); + assertEquals(Color.ORANGE, orangeZ.color); + assertEquals("Datsun", orangeZ.manufacturersProvider.get().iterator().next()); + } + + public static class Z implements Car { + private final Provider> manufacturersProvider; + private final Color color; + + @Inject + public Z(Provider> manufacturersProvider, @Assisted Color color) { + this.manufacturersProvider = manufacturersProvider; + this.color = color; + } + } + + public static class Prius implements Car { + final Color color; + + @Inject + private Prius(@Assisted Color color) { + this.color = color; + } + } + + public void testAssistInjectionInNonPublicConstructor() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + bind(ColoredCarFactory.class).toProvider( + FactoryProvider.newFactory(ColoredCarFactory.class, Prius.class)); + } + }); + Prius prius = (Prius) injector.getInstance(ColoredCarFactory.class).create(Color.ORANGE); + assertEquals(prius.color, Color.ORANGE); + } + + public static class ExplodingCar implements Car { + @Inject + public ExplodingCar(@SuppressWarnings("unused") @Assisted Color color) { + throw new IllegalStateException("kaboom!"); + } + } + + public void testExceptionDuringConstruction() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + bind(ColoredCarFactory.class).toProvider( + FactoryProvider.newFactory(ColoredCarFactory.class, ExplodingCar.class)); + } + }); + try { + injector.getInstance(ColoredCarFactory.class).create(Color.ORANGE); + fail(); + } catch (IllegalStateException e) { + assertEquals("kaboom!", e.getMessage()); + } + } + + public static class DefectiveCar implements Car { + @Inject + public DefectiveCar() throws ExplosionException { + throw new ExplosionException(); + } + } + + public static class ExplosionException extends Exception { } + public static class FireException extends Exception { } + + public interface DefectiveCarFactoryWithNoExceptions { + Car createCar(); + } + + public interface DefectiveCarFactory { + Car createCar() throws FireException; + } + + public interface CorrectDefectiveCarFactory { + Car createCar() throws FireException, ExplosionException; + } + + public void testConstructorExceptionsAreThrownByFactory() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + bind(CorrectDefectiveCarFactory.class).toProvider( + FactoryProvider.newFactory(CorrectDefectiveCarFactory.class, DefectiveCar.class)); + } + }); + try { + injector.getInstance(CorrectDefectiveCarFactory.class).createCar(); + fail(); + } catch (FireException e) { + fail(); + } catch (ExplosionException expected) { + } + } + + public static class WildcardCollection { + + public interface Factory { + WildcardCollection create(Collection items); + } + + @Inject + public WildcardCollection(@SuppressWarnings("unused") @Assisted Collection items) { } + } + + public void testWildcardGenerics() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + bind(WildcardCollection.Factory.class).toProvider( + FactoryProvider.newFactory(WildcardCollection.Factory.class, WildcardCollection.class)); + } + }); + WildcardCollection.Factory factory = injector.getInstance(WildcardCollection.Factory.class); + factory.create(Collections.emptyList()); + } + + public static class SteeringWheel {} + + public static class Fiat implements Car { + private final SteeringWheel steeringWheel; + private final Color color; + + @Inject + public Fiat(SteeringWheel steeringWheel, @Assisted Color color) { + this.steeringWheel = steeringWheel; + this.color = color; + } + } + + public void testFactoryWithImplicitBindings() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + bind(ColoredCarFactory.class).toProvider( + FactoryProvider.newFactory(ColoredCarFactory.class, Fiat.class)); + } + }); + + ColoredCarFactory coloredCarFactory = injector.getInstance(ColoredCarFactory.class); + Fiat fiat = (Fiat) coloredCarFactory.create(Color.GREEN); + assertEquals(Color.GREEN, fiat.color); + assertNotNull(fiat.steeringWheel); + } + + public void testFactoryFailsWithMissingBinding() { + try { + Guice.createInjector(new AbstractModule() { + @Override protected void configure() { + bind(ColoredCarFactory.class).toProvider( + FactoryProvider.newFactory(ColoredCarFactory.class, Mustang.class)); + } + }); + fail(); + } catch (CreationException expected) { + assertContains(expected.getMessage(), + "Could not find a suitable constructor in java.lang.Double.", + "at " + ColoredCarFactory.class.getName() + ".create("); + } + } + + public void testFactoryFailsWithMissingBindingInToolStage() { + try { + Guice.createInjector(Stage.TOOL, new AbstractModule() { + @Override protected void configure() { + bind(ColoredCarFactory.class).toProvider( + FactoryProvider.newFactory(ColoredCarFactory.class, Mustang.class)); + } + }); + fail(); + } catch (CreationException expected) { + assertContains(expected.getMessage(), + "Could not find a suitable constructor in java.lang.Double.", + "at " + ColoredCarFactory.class.getName() + ".create("); + } + } + + public void testMethodsDeclaredInObject() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override protected void configure() { + bind(Double.class).toInstance(5.0d); + bind(ColoredCarFactory.class).toProvider( + FactoryProvider.newFactory(ColoredCarFactory.class, Mustang.class)); + } + }); + + ColoredCarFactory carFactory = injector.getInstance(ColoredCarFactory.class); + + assertEqualsBothWays(carFactory, carFactory); + } + + static class Subaru implements Car { + @Inject @Assisted Provider colorProvider; + } + + public void testInjectingProviderOfParameter() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override protected void configure() { + bind(ColoredCarFactory.class).toProvider( + FactoryProvider.newFactory(ColoredCarFactory.class, Subaru.class)); + } + }); + + ColoredCarFactory carFactory = injector.getInstance(ColoredCarFactory.class); + Subaru subaru = (Subaru) carFactory.create(Color.RED); + + assertSame(Color.RED, subaru.colorProvider.get()); + assertSame(Color.RED, subaru.colorProvider.get()); + + Subaru sedan = (Subaru) carFactory.create(Color.BLUE); + assertSame(Color.BLUE, sedan.colorProvider.get()); + assertSame(Color.BLUE, sedan.colorProvider.get()); + + // and make sure the subaru is still red + assertSame(Color.RED, subaru.colorProvider.get()); + } + + public void testInjectingNullParameter() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override protected void configure() { + bind(ColoredCarFactory.class).toProvider( + FactoryProvider.newFactory(ColoredCarFactory.class, Subaru.class)); + } + }); + + ColoredCarFactory carFactory = injector.getInstance(ColoredCarFactory.class); + Subaru subaru = (Subaru) carFactory.create(null); + + assertNull(subaru.colorProvider.get()); + assertNull(subaru.colorProvider.get()); + } + + interface ProviderBasedColoredCarFactory { + Car createCar(Provider colorProvider, Provider stringProvider); + Mustang createMustang(@Assisted("color") Provider colorProvider); + } + + public void testAssistedProviderIsDisallowed() { + try { + Guice.createInjector(new AbstractModule() { + @Override protected void configure() { + bind(ProviderBasedColoredCarFactory.class).toProvider( + FactoryProvider.newFactory(ProviderBasedColoredCarFactory.class, Subaru.class)); + } + }); + fail(); + } catch (CreationException expected) { + assertEquals(expected.getMessage(), 4, expected.getErrorMessages().size()); + // Assert each method individually, because JDK7 doesn't guarantee method ordering. + assertContains(expected.getMessage(), + ") A Provider may not be a type in a factory method of an AssistedInject." + + "\n Offending instance is parameter [1] with key" + + " [com.google.inject.Provider<" + Color.class.getName() + ">] on method [" + + ProviderBasedColoredCarFactory.class.getName() + ".createCar()]"); + assertContains(expected.getMessage(), + ") A Provider may not be a type in a factory method of an AssistedInject." + + "\n Offending instance is parameter [2] with key" + + " [com.google.inject.Provider] on method [" + + ProviderBasedColoredCarFactory.class.getName() + ".createCar()]"); + assertContains(expected.getMessage(), + ") A Provider may not be a type in a factory method of an AssistedInject." + + "\n Offending instance is parameter [1] with key" + + " [com.google.inject.Provider<" + Color.class.getName() + ">" + + " annotated with @com.google.inject.assistedinject.Assisted(value=color)]" + + " on method [" + ProviderBasedColoredCarFactory.class.getName() + ".createMustang()]" + ); + assertContains(expected.getMessage(), + ") No implementation for com.google.inject.assistedinject." + + "FactoryProvider2Test$ProviderBasedColoredCarFactory was bound."); + } + } + + interface JavaxProviderBasedColoredCarFactory { + Car createCar(javax.inject.Provider colorProvider, javax.inject.Provider stringProvider); + Mustang createMustang(@Assisted("color") javax.inject.Provider colorProvider); + } + + public void testAssistedJavaxProviderIsDisallowed() { + try { + Guice.createInjector(new AbstractModule() { + @Override protected void configure() { + bind(JavaxProviderBasedColoredCarFactory.class).toProvider( + FactoryProvider.newFactory(JavaxProviderBasedColoredCarFactory.class, Subaru.class)); + } + }); + fail(); + } catch (CreationException expected) { + assertEquals(expected.getMessage(), 4, expected.getErrorMessages().size()); + assertContains(expected.getMessage(), + ") A Provider may not be a type in a factory method of an AssistedInject." + + "\n Offending instance is parameter [1] with key" + + " [com.google.inject.Provider<" + Color.class.getName() + ">] on method [" + + JavaxProviderBasedColoredCarFactory.class.getName() + ".createCar()]"); + assertContains(expected.getMessage(), + ") A Provider may not be a type in a factory method of an AssistedInject." + + "\n Offending instance is parameter [2] with key" + + " [com.google.inject.Provider] on method [" + + JavaxProviderBasedColoredCarFactory.class.getName() + ".createCar()]"); + assertContains(expected.getMessage(), + ") A Provider may not be a type in a factory method of an AssistedInject." + + "\n Offending instance is parameter [1] with key" + + " [com.google.inject.Provider<" + Color.class.getName() + ">" + + " annotated with @com.google.inject.assistedinject.Assisted(value=color)]" + + " on method [" + JavaxProviderBasedColoredCarFactory.class.getName() + ".createMustang()]" + ); + assertContains(expected.getMessage(), + ") No implementation for com.google.inject.assistedinject." + + "FactoryProvider2Test$JavaxProviderBasedColoredCarFactory was bound."); + } + } + + public void testFactoryUseBeforeInitialization() { + ColoredCarFactory carFactory = FactoryProvider.newFactory(ColoredCarFactory.class, Subaru.class) + .get(); + try { + carFactory.create(Color.RED); + fail(); + } catch (IllegalStateException expected) { + assertContains(expected.getMessage(), + "Factories.create() factories cannot be used until they're initialized by Guice."); + } + } + + interface MustangFactory { + Mustang create(Color color); + } + + public void testFactoryBuildingConcreteTypes() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + bind(double.class).toInstance(5.0d); + // note there is no 'thatMakes()' call here: + bind(MustangFactory.class).toProvider( + FactoryProvider.newFactory(MustangFactory.class, Mustang.class)); + } + }); + MustangFactory factory = injector.getInstance(MustangFactory.class); + + Mustang mustang = factory.create(Color.RED); + assertSame(Color.RED, mustang.color); + assertEquals(5.0d, mustang.engineSize); + } + + static class Fleet { + @Inject Mustang mustang; + @Inject Camaro camaro; + } + + interface FleetFactory { + Fleet createFleet(Color color); + } + + public void testInjectDeepIntoConstructedObjects() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + bind(double.class).toInstance(5.0d); + bind(int.class).annotatedWith(Names.named("horsePower")).toInstance(250); + bind(int.class).annotatedWith(Names.named("modelYear")).toInstance(1984); + bind(FleetFactory.class).toProvider(FactoryProvider.newFactory(FleetFactory.class, + Fleet.class)); + } + }); + + FleetFactory fleetFactory = injector.getInstance(FleetFactory.class); + Fleet fleet = fleetFactory.createFleet(Color.RED); + + assertSame(Color.RED, fleet.mustang.color); + assertEquals(5.0d, fleet.mustang.engineSize); + assertSame(Color.RED, fleet.camaro.color); + assertEquals(250, fleet.camaro.horsePower); + assertEquals(1984, fleet.camaro.modelYear); + } + + interface TwoToneCarFactory { + Car create(@Assisted("paint") Color paint, @Assisted("fabric") Color fabric); + } + + static class Maxima implements Car { + @Inject @Assisted("paint") Color paint; + @Inject @Assisted("fabric") Color fabric; + } + + public void testDistinctKeys() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + bind(TwoToneCarFactory.class).toProvider( + FactoryProvider.newFactory(TwoToneCarFactory.class, Maxima.class)); + } + }); + + TwoToneCarFactory factory = injector.getInstance(TwoToneCarFactory.class); + Maxima maxima = (Maxima) factory.create(Color.BLACK, Color.GRAY); + assertSame(Color.BLACK, maxima.paint); + assertSame(Color.GRAY, maxima.fabric); + } + + interface DoubleToneCarFactory { + Car create(@Assisted("paint") Color paint, @Assisted("paint") Color morePaint); + } + + public void testDuplicateKeys() { + try { + Guice.createInjector(new AbstractModule() { + @Override protected void configure() { + bind(DoubleToneCarFactory.class).toProvider( + FactoryProvider.newFactory(DoubleToneCarFactory.class, Maxima.class)); + } + }); + fail(); + } catch (CreationException expected) { + assertContains(expected.getMessage(), "A binding to " + Color.class.getName() + " annotated with @" + + Assisted.class.getName() + "(value=paint) was already configured at"); + } + } + + /** + * Our factories aren't reusable across injectors. Although this behaviour isn't something we + * like, I have a test case to make sure the error message is pretty. + */ + public void testFactoryReuseErrorMessageIsPretty() { + final Provider factoryProvider + = FactoryProvider.newFactory(ColoredCarFactory.class, Mustang.class); + + Guice.createInjector(new AbstractModule() { + @Override protected void configure() { + bind(Double.class).toInstance(5.0d); + bind(ColoredCarFactory.class).toProvider(factoryProvider); + } + }); + + try { + Guice.createInjector(new AbstractModule() { + @Override protected void configure() { + bind(Double.class).toInstance(5.0d); + bind(ColoredCarFactory.class).toProvider(factoryProvider); + } + }); + fail(); + } catch(CreationException expected) { + assertContains(expected.getMessage(), + "Factories.create() factories may only be used in one Injector!"); + } + } + + public void testNonAssistedFactoryMethodParameter() { + try { + FactoryProvider.newFactory(NamedParameterFactory.class, Mustang.class); + fail(); + } catch(ConfigurationException expected) { + assertContains(expected.getMessage(), + "Only @Assisted is allowed for factory parameters, but found @" + Named.class.getName()); + } + } + + interface NamedParameterFactory { + Car create(@Named("seats") int seats, double engineSize); + } + + + public void testDefaultAssistedAnnotation() throws NoSuchFieldException { + Assisted plainAssisted + = Subaru.class.getDeclaredField("colorProvider").getAnnotation(Assisted.class); + assertEqualsBothWays(FactoryProvider2.DEFAULT_ANNOTATION, plainAssisted); + assertEquals(FactoryProvider2.DEFAULT_ANNOTATION.toString(), plainAssisted.toString()); + } + + interface GenericColoredCarFactory { + T create(Color color); + } + + public void testGenericAssistedFactory() { + final TypeLiteral> mustangTypeLiteral + = new TypeLiteral>() {}; + final TypeLiteral> camaroTypeLiteral + = new TypeLiteral>() {}; + + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + bind(Double.class).toInstance(5.0d); + bind(int.class).annotatedWith(Names.named("horsePower")).toInstance(250); + bind(int.class).annotatedWith(Names.named("modelYear")).toInstance(1984); + bind(mustangTypeLiteral) + .toProvider(FactoryProvider.newFactory(mustangTypeLiteral, TypeLiteral.get(Mustang.class))); + bind(camaroTypeLiteral) + .toProvider(FactoryProvider.newFactory(camaroTypeLiteral, TypeLiteral.get(Camaro.class))); + } + }); + + GenericColoredCarFactory mustangFactory + = injector.getInstance(Key.get(mustangTypeLiteral)); + GenericColoredCarFactory camaroFactory + = injector.getInstance(Key.get(camaroTypeLiteral)); + + Mustang blueMustang = mustangFactory.create(Color.BLUE); + assertEquals(Color.BLUE, blueMustang.color); + assertEquals(5.0d, blueMustang.engineSize); + + Camaro redCamaro = camaroFactory.create(Color.RED); + assertEquals(Color.RED, redCamaro.color); + assertEquals(1984, redCamaro.modelYear); + assertEquals(250, redCamaro.horsePower); + } + + @SuppressWarnings("unused") + public interface Insurance { + } + + public static class MustangInsurance implements Insurance { + private final double premium; + private final double limit; + @SuppressWarnings("unused") private Mustang car; + + @Inject + public MustangInsurance(@Named("lowLimit") double limit, @Assisted Mustang car, + @Assisted double premium) { + this.premium = premium; + this.limit = limit; + this.car = car; + } + + public void sell() {} + } + + public static class CamaroInsurance implements Insurance { + private final double premium; + private final double limit; + @SuppressWarnings("unused") private Camaro car; + + @Inject + public CamaroInsurance(@Named("highLimit") double limit, @Assisted Camaro car, + @Assisted double premium) { + this.premium = premium; + this.limit = limit; + this.car = car; + } + + public void sell() {} + } + + public interface MustangInsuranceFactory { + public Insurance create(Mustang car, double premium); + } + + public interface CamaroInsuranceFactory { + public Insurance create(Camaro car, double premium); + } + + public void testAssistedFactoryForConcreteType() { + + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + bind(Double.class).annotatedWith(Names.named("lowLimit")).toInstance(50000.0d); + bind(Double.class).annotatedWith(Names.named("highLimit")).toInstance(100000.0d); + bind(MustangInsuranceFactory.class).toProvider( + FactoryProvider.newFactory(MustangInsuranceFactory.class, MustangInsurance.class)); + bind(CamaroInsuranceFactory.class).toProvider( + FactoryProvider.newFactory(CamaroInsuranceFactory.class, CamaroInsurance.class)); + } + }); + + MustangInsuranceFactory mustangInsuranceFactory = + injector.getInstance(MustangInsuranceFactory.class); + CamaroInsuranceFactory camaroInsuranceFactory = + injector.getInstance(CamaroInsuranceFactory.class); + + Mustang mustang = new Mustang(5000d, Color.BLACK); + MustangInsurance mustangPolicy = + (MustangInsurance) mustangInsuranceFactory.create(mustang, 800.0d); + assertEquals(800.0d, mustangPolicy.premium); + assertEquals(50000.0d, mustangPolicy.limit); + + Camaro camaro = new Camaro(3000, 1967, Color.BLUE); + CamaroInsurance camaroPolicy = (CamaroInsurance) camaroInsuranceFactory.create(camaro, 800.0d); + assertEquals(800.0d, camaroPolicy.premium); + assertEquals(100000.0d, camaroPolicy.limit); + } + + public interface InsuranceFactory { + public Insurance create(T car, double premium); + } + + public void testAssistedFactoryForParameterizedType() { + final TypeLiteral> mustangInsuranceFactoryType = + new TypeLiteral>() {}; + final TypeLiteral> camaroInsuranceFactoryType = + new TypeLiteral>() {}; + + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + bind(Double.class).annotatedWith(Names.named("lowLimit")).toInstance(50000.0d); + bind(Double.class).annotatedWith(Names.named("highLimit")).toInstance(100000.0d); + bind(mustangInsuranceFactoryType).toProvider(FactoryProvider.newFactory( + mustangInsuranceFactoryType, TypeLiteral.get(MustangInsurance.class))); + bind(camaroInsuranceFactoryType).toProvider(FactoryProvider.newFactory( + camaroInsuranceFactoryType, TypeLiteral.get(CamaroInsurance.class))); + } + }); + + InsuranceFactory mustangInsuranceFactory = + injector.getInstance(Key.get(mustangInsuranceFactoryType)); + InsuranceFactory camaroInsuranceFactory = + injector.getInstance(Key.get(camaroInsuranceFactoryType)); + + Mustang mustang = new Mustang(5000d, Color.BLACK); + MustangInsurance mustangPolicy = + (MustangInsurance) mustangInsuranceFactory.create(mustang, 800.0d); + assertEquals(800.0d, mustangPolicy.premium); + assertEquals(50000.0d, mustangPolicy.limit); + + Camaro camaro = new Camaro(3000, 1967, Color.BLUE); + CamaroInsurance camaroPolicy = (CamaroInsurance) camaroInsuranceFactory.create(camaro, 800.0d); + assertEquals(800.0d, camaroPolicy.premium); + assertEquals(100000.0d, camaroPolicy.limit); + } + + public static class AutoInsurance implements Insurance { + private final double premium; + private final double limit; + private final T car; + + @Inject + public AutoInsurance(double limit, @Assisted T car, @Assisted double premium) { + this.limit = limit; + this.car = car; + this.premium = premium; + } + + public void sell() {} + } + + public void testAssistedFactoryForTypeVariableParameters() { + final TypeLiteral> camaroInsuranceFactoryType = + new TypeLiteral>() {}; + + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + bind(Double.class).toInstance(50000.0d); + bind(camaroInsuranceFactoryType).toProvider(FactoryProvider.newFactory( + camaroInsuranceFactoryType, new TypeLiteral>() {})); + } + }); + + InsuranceFactory camaroInsuranceFactory = + injector.getInstance(Key.get(camaroInsuranceFactoryType)); + + Camaro camaro = new Camaro(3000, 1967, Color.BLUE); + AutoInsurance camaroPolicy = + (AutoInsurance) camaroInsuranceFactory.create(camaro, 800.0d); + assertEquals(800.0d, camaroPolicy.premium); + assertEquals(50000.0d, camaroPolicy.limit); + assertEquals(camaro, camaroPolicy.car); + } + + public void testInjectingAndUsingInjector() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override protected void configure() { + bind(ColoredCarFactory.class).toProvider( + FactoryProvider.newFactory(ColoredCarFactory.class, Segway.class)); + } + }); + + ColoredCarFactory carFactory = injector.getInstance(ColoredCarFactory.class); + Segway green = (Segway)carFactory.create(Color.GREEN); + assertSame(Color.GREEN, green.getColor()); + assertSame(Color.GREEN, green.getColor()); + + Segway pink = (Segway)carFactory.create(Color.PINK); + assertSame(Color.PINK, pink.getColor()); + assertSame(Color.PINK, pink.getColor()); + assertSame(Color.GREEN, green.getColor()); + } + + public void testDuplicateAssistedFactoryBinding() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + bind(Double.class).toInstance(5.0d); + bind(ColoredCarFactory.class).toProvider( + FactoryProvider.newFactory(ColoredCarFactory.class, Mustang.class)); + bind(ColoredCarFactory.class).toProvider( + FactoryProvider.newFactory(ColoredCarFactory.class, Mustang.class)); + } + }); + ColoredCarFactory carFactory = injector.getInstance(ColoredCarFactory.class); + + Mustang blueMustang = (Mustang) carFactory.create(Color.BLUE); + assertEquals(Color.BLUE, blueMustang.color); + assertEquals(5.0d, blueMustang.engineSize); + + Mustang redMustang = (Mustang) carFactory.create(Color.RED); + assertEquals(Color.RED, redMustang.color); + assertEquals(5.0d, redMustang.engineSize); + } + + public interface Equals { + + enum ComparisonMethod { SHALLOW, DEEP; } + + interface Factory { + Equals equals(ComparisonMethod comparisonMethod); + } + + public static class Impl implements Equals { + private final double sigma; + private final ComparisonMethod comparisonMethod; + + @AssistedInject + public Impl(double sigma, @Assisted ComparisonMethod comparisonMethod) { + this.sigma = sigma; + this.comparisonMethod = comparisonMethod; + } + } + } + + public void testFactoryMethodCalledEquals() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + bind(Double.class).toInstance(0.01d); + bind(Equals.Factory.class).toProvider( + FactoryProvider.newFactory(Equals.Factory.class, Impl.class)); + } + }); + Equals.Factory equalsFactory = injector.getInstance(Equals.Factory.class); + Impl shallowEquals = (Impl) equalsFactory.equals(ComparisonMethod.SHALLOW); + assertEquals(ComparisonMethod.SHALLOW, shallowEquals.comparisonMethod); + assertEquals(0.01d, shallowEquals.sigma); + } + + static class Segway implements Car { + @Inject Injector injector; + + Color getColor() { return injector.getInstance(Key.get(Color.class, FactoryProvider2.DEFAULT_ANNOTATION)); } + } + + public void testReturnValueMatchesParamValue() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + public void configure() { + install(new FactoryModuleBuilder().build(Delegater.Factory.class)); + } + }); + Delegater delegate = new Delegater(); + Delegater user = injector.getInstance(Delegater.Factory.class).create(delegate); + assertSame(delegate, user.delegate); + } + + static class Delegater { + interface Factory { + Delegater create(Delegater delegate); + } + + private final Delegater delegate; + + @Inject Delegater(@Assisted Delegater delegater) { + this.delegate = delegater; + } + + Delegater() { + this.delegate = null; + } + } + + public static abstract class AbstractAssisted { + interface Factory { + O create(I string); + } + } + + static class ConcreteAssisted extends AbstractAssisted { + @Inject ConcreteAssisted(@SuppressWarnings("unused") @Assisted String string) {} + } + + static class ConcreteAssistedWithOverride extends AbstractAssisted { + @AssistedInject + ConcreteAssistedWithOverride(@SuppressWarnings("unused") @Assisted String string) {} + + @AssistedInject + ConcreteAssistedWithOverride(@SuppressWarnings("unused") @Assisted StringBuilder sb) {} + + interface Factory extends AbstractAssisted.Factory { + @Override ConcreteAssistedWithOverride create(String string); + } + + interface Factory2 extends AbstractAssisted.Factory { + @Override ConcreteAssistedWithOverride create(String string); + ConcreteAssistedWithOverride create(StringBuilder sb); + } + } + + static class ConcreteAssistedWithoutOverride extends AbstractAssisted { + @Inject ConcreteAssistedWithoutOverride(@SuppressWarnings("unused") @Assisted String string) {} + interface Factory extends AbstractAssisted.Factory {} + } + + public static class Public extends AbstractAssisted { + @AssistedInject Public(@SuppressWarnings("unused") @Assisted String string) {} + @AssistedInject Public(@SuppressWarnings("unused") @Assisted StringBuilder sb) {} + + public interface Factory extends AbstractAssisted.Factory { + @Override Public create(String string); + Public create(StringBuilder sb); + } + } + + // See https://github.com/google/guice/issues/904 + public void testGeneratedDefaultMethodsForwardCorrectly() { + final Key> concreteKey = + new Key>() {}; + Injector injector = Guice.createInjector(new AbstractModule() { + @Override protected void configure() { + install(new FactoryModuleBuilder().build(ConcreteAssistedWithOverride.Factory.class)); + install(new FactoryModuleBuilder().build(ConcreteAssistedWithOverride.Factory2.class)); + install(new FactoryModuleBuilder().build(ConcreteAssistedWithoutOverride.Factory.class)); + install(new FactoryModuleBuilder().build(Public.Factory.class)); + install(new FactoryModuleBuilder().build(concreteKey)); + } + }); + + ConcreteAssistedWithOverride.Factory factory1 = + injector.getInstance(ConcreteAssistedWithOverride.Factory.class); + factory1.create("foo"); + AbstractAssisted.Factory factory1Abstract = factory1; + factory1Abstract.create("foo"); + + ConcreteAssistedWithOverride.Factory2 factory2 = + injector.getInstance(ConcreteAssistedWithOverride.Factory2.class); + factory2.create("foo"); + factory2.create(new StringBuilder("foo")); + AbstractAssisted.Factory factory2Abstract = factory2; + factory2Abstract.create("foo"); + + ConcreteAssistedWithoutOverride.Factory factory3 = + injector.getInstance(ConcreteAssistedWithoutOverride.Factory.class); + factory3.create("foo"); + AbstractAssisted.Factory factory3Abstract = factory3; + factory3Abstract.create("foo"); + + Public.Factory factory4 = injector.getInstance(Public.Factory.class); + factory4.create("foo"); + factory4.create(new StringBuilder("foo")); + AbstractAssisted.Factory factory4Abstract = factory4; + factory4Abstract.create("foo"); + + AbstractAssisted.Factory factory5 = + injector.getInstance(concreteKey); + factory5.create("foo"); + } +} diff --git a/src/test/java/com/google/inject/assistedinject/FactoryProviderTest.java b/src/test/java/com/google/inject/assistedinject/FactoryProviderTest.java new file mode 100644 index 0000000..3684f7b --- /dev/null +++ b/src/test/java/com/google/inject/assistedinject/FactoryProviderTest.java @@ -0,0 +1,840 @@ +/** + * Copyright (C) 2007 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 com.google.common.collect.ImmutableSet; +import com.google.inject.AbstractModule; +import com.google.inject.Binding; +import com.google.inject.ConfigurationException; +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.Provider; +import com.google.inject.TypeLiteral; +import com.google.inject.assistedinject.FactoryProviderTest.Equals.ComparisonMethod; +import com.google.inject.assistedinject.FactoryProviderTest.Equals.Impl; +import com.google.inject.name.Named; +import com.google.inject.name.Names; +import com.google.inject.spi.Dependency; +import com.google.inject.spi.HasDependencies; + +import junit.framework.TestCase; + +import java.util.Collection; +import java.util.Collections; +import java.util.Set; + +/** + * @author jmourits@google.com (Jerome Mourits) + * @author jessewilson@google.com (Jesse Wilson) + */ +@SuppressWarnings("deprecation") +public class FactoryProviderTest extends TestCase { + + private enum Color { BLUE, GREEN, RED, GRAY, BLACK, ORANGE, PINK } + + public void testAssistedFactory() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + bind(Double.class).toInstance(5.0d); + bind(ColoredCarFactory.class) + .toProvider(FactoryProvider.newFactory(ColoredCarFactory.class, Mustang.class)); + } + }); + ColoredCarFactory carFactory = injector.getInstance(ColoredCarFactory.class); + + Mustang blueMustang = (Mustang) carFactory.create(Color.BLUE); + assertEquals(Color.BLUE, blueMustang.color); + assertEquals(5.0d, blueMustang.engineSize); + + Mustang redMustang = (Mustang) carFactory.create(Color.RED); + assertEquals(Color.RED, redMustang.color); + assertEquals(5.0d, redMustang.engineSize); + } + + public void testFactoryBindingDependencies() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + bind(Double.class).toInstance(5.0d); + bind(ColoredCarFactory.class) + .toProvider(FactoryProvider.newFactory(ColoredCarFactory.class, Mustang.class)); + } + }); + + Binding binding = injector.getBinding(ColoredCarFactory.class); + HasDependencies hasDependencies = (HasDependencies) binding; + assertEquals(ImmutableSet.>of(Dependency.get(Key.get(double.class))), + hasDependencies.getDependencies()); + } + + public void testAssistedFactoryWithAnnotations() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + bind(int.class).annotatedWith(Names.named("horsePower")).toInstance(250); + bind(int.class).annotatedWith(Names.named("modelYear")).toInstance(1984); + bind(ColoredCarFactory.class) + .toProvider(FactoryProvider.newFactory(ColoredCarFactory.class, Camaro.class)); + } + }); + + ColoredCarFactory carFactory = injector.getInstance(ColoredCarFactory.class); + + Camaro blueCamaro = (Camaro) carFactory.create(Color.BLUE); + assertEquals(Color.BLUE, blueCamaro.color); + assertEquals(1984, blueCamaro.modelYear); + assertEquals(250, blueCamaro.horsePower); + + Camaro redCamaro = (Camaro) carFactory.create(Color.RED); + assertEquals(Color.RED, redCamaro.color); + assertEquals(1984, redCamaro.modelYear); + assertEquals(250, redCamaro.horsePower); + } + + interface Car { + } + + interface ColoredCarFactory { + Car create(Color color); + } + + public static class Mustang implements Car { + private final double engineSize; + private final Color color; + + @AssistedInject + public Mustang(double engineSize, @Assisted Color color) { + this.engineSize = engineSize; + this.color = color; + } + } + + public static class Camaro implements Car { + private final int horsePower; + private final int modelYear; + private final Color color; + + @AssistedInject + public Camaro( + @Named("horsePower")int horsePower, + @Named("modelYear")int modelYear, + @Assisted Color color) { + this.horsePower = horsePower; + this.modelYear = modelYear; + this.color = color; + } + } + + interface SummerCarFactory { + Car create(Color color, boolean convertable); + Car createConvertible(Color color); + } + + public void testFactoryWithMultipleMethods() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + bind(float.class).toInstance(140f); + bind(SummerCarFactory.class).toProvider( + FactoryProvider.newFactory(SummerCarFactory.class, Corvette.class)); + } + }); + + SummerCarFactory carFactory = injector.getInstance(SummerCarFactory.class); + + Corvette blueCorvette = (Corvette) carFactory.createConvertible(Color.BLUE); + assertEquals(Color.BLUE, blueCorvette.color); + assertEquals(100f, blueCorvette.maxMph); + assertTrue(blueCorvette.isConvertable); + + Corvette redCorvette = (Corvette) carFactory.create(Color.RED, false); + assertEquals(Color.RED, redCorvette.color); + assertEquals(140f, redCorvette.maxMph); + assertFalse(redCorvette.isConvertable); + } + + public static class Corvette implements Car { + private boolean isConvertable; + private Color color; + private float maxMph; + + @AssistedInject + public Corvette(@Assisted Color color) { + this(color, 100f, true); + } + + @SuppressWarnings("unused") + public Corvette(@Assisted Color color, @Assisted boolean isConvertable) { + throw new IllegalStateException("Not an @AssistedInject constructor"); + } + + @AssistedInject + public Corvette(@Assisted Color color, Float maxMph, @Assisted boolean isConvertable) { + this.isConvertable = isConvertable; + this.color = color; + this.maxMph = maxMph; + } + } + + public void testFactoryMethodsMismatch() { + try { + FactoryProvider.newFactory(SummerCarFactory.class, Beetle.class); + fail(); + } catch(ConfigurationException e) { + assertContains(e.getMessage(), "Constructor mismatch"); + } + } + + public static class Beetle implements Car { + @AssistedInject + @SuppressWarnings("unused") + public Beetle(@Assisted Color color) { + throw new IllegalStateException("Conflicting constructors"); + } + @AssistedInject + @SuppressWarnings("unused") + public Beetle(@Assisted Color color, @Assisted boolean isConvertable) { + throw new IllegalStateException("Conflicting constructors"); + } + @AssistedInject + @SuppressWarnings("unused") + public Beetle(@Assisted Color color, @Assisted boolean isConvertable, float maxMph) { + throw new IllegalStateException("Conflicting constructors"); + } + } + + public void testMethodsAndFieldsGetInjected() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + bind(String.class).toInstance("turbo"); + bind(int.class).toInstance(911); + bind(double.class).toInstance(50000d); + bind(ColoredCarFactory.class) + .toProvider(FactoryProvider.newFactory(ColoredCarFactory.class, Porshe.class)); + } + }); + ColoredCarFactory carFactory = injector.getInstance(ColoredCarFactory.class); + + Porshe grayPorshe = (Porshe) carFactory.create(Color.GRAY); + assertEquals(Color.GRAY, grayPorshe.color); + assertEquals(50000d, grayPorshe.price); + assertEquals(911, grayPorshe.model); + assertEquals("turbo", grayPorshe.name); + } + + public static class Porshe implements Car { + private final Color color; + private final double price; + private @Inject String name; + private int model; + + @AssistedInject + public Porshe(@Assisted Color color, double price) { + this.color = color; + this.price = price; + } + + @Inject void setModel(int model) { + this.model = model; + } + } + + public void testProviderInjection() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + bind(String.class).toInstance("trans am"); + bind(ColoredCarFactory.class) + .toProvider(FactoryProvider.newFactory(ColoredCarFactory.class, Firebird.class)); + } + }); + ColoredCarFactory carFactory = injector.getInstance(ColoredCarFactory.class); + + Firebird blackFirebird = (Firebird) carFactory.create(Color.BLACK); + assertEquals(Color.BLACK, blackFirebird.color); + assertEquals("trans am", blackFirebird.modifiersProvider.get()); + } + + public static class Firebird implements Car { + private final Provider modifiersProvider; + private final Color color; + + @AssistedInject + public Firebird(Provider modifiersProvider, @Assisted Color color) { + this.modifiersProvider = modifiersProvider; + this.color = color; + } + } + + public void testTypeTokenInjection() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + bind(new TypeLiteral>() {}).toInstance(Collections.singleton("Flux Capacitor")); + bind(new TypeLiteral>() {}).toInstance(Collections.singleton(88)); + bind(ColoredCarFactory.class) + .toProvider(FactoryProvider.newFactory(ColoredCarFactory.class, DeLorean.class)); + } + }); + ColoredCarFactory carFactory = injector.getInstance(ColoredCarFactory.class); + + DeLorean deLorean = (DeLorean) carFactory.create(Color.GRAY); + assertEquals(Color.GRAY, deLorean.color); + assertEquals("Flux Capacitor", deLorean.features.iterator().next()); + assertEquals(new Integer(88), deLorean.featureActivationSpeeds.iterator().next()); + } + + public static class DeLorean implements Car { + private final Set features; + private final Set featureActivationSpeeds; + private final Color color; + + @AssistedInject + public DeLorean( + Set extraFeatures, Set featureActivationSpeeds, @Assisted Color color) { + this.features = extraFeatures; + this.featureActivationSpeeds = featureActivationSpeeds; + this.color = color; + } + } + + public void testTypeTokenProviderInjection() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + bind(new TypeLiteral>() { }).toInstance(Collections.singleton("Datsun")); + bind(ColoredCarFactory.class) + .toProvider(FactoryProvider.newFactory(ColoredCarFactory.class, Z.class)); + } + }); + ColoredCarFactory carFactory = injector.getInstance(ColoredCarFactory.class); + + Z orangeZ = (Z) carFactory.create(Color.ORANGE); + assertEquals(Color.ORANGE, orangeZ.color); + assertEquals("Datsun", orangeZ.manufacturersProvider.get().iterator().next()); + } + + public static class Z implements Car { + private final Provider> manufacturersProvider; + private final Color color; + + @AssistedInject + public Z(Provider> manufacturersProvider, @Assisted Color color) { + this.manufacturersProvider = manufacturersProvider; + this.color = color; + } + } + + public static class Prius implements Car { + @SuppressWarnings("unused") + private final Color color; + + @AssistedInject + private Prius(@Assisted Color color) { + this.color = color; + } + } + + public void testAssistInjectionInNonPublicConstructor() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + bind(ColoredCarFactory.class) + .toProvider(FactoryProvider.newFactory(ColoredCarFactory.class, Prius.class)); + } + }); + injector.getInstance(ColoredCarFactory.class).create(Color.ORANGE); + } + + public static class ExplodingCar implements Car { + @AssistedInject + public ExplodingCar(@SuppressWarnings("unused") @Assisted Color color) { + throw new IllegalStateException("kaboom!"); + } + } + + public void testExceptionDuringConstruction() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + bind(ColoredCarFactory.class).toProvider( + FactoryProvider.newFactory(ColoredCarFactory.class, ExplodingCar.class)); + } + }); + try { + injector.getInstance(ColoredCarFactory.class).create(Color.ORANGE); + fail(); + } catch (IllegalStateException e) { + assertEquals("kaboom!", e.getMessage()); + } + } + + public static class DefectiveCar implements Car { + @AssistedInject + public DefectiveCar() throws ExplosionException { + throw new ExplosionException(); + } + } + + public static class ExplosionException extends Exception { } + public static class FireException extends Exception { } + + public interface DefectiveCarFactoryWithNoExceptions { + Car createCar(); + } + + public interface DefectiveCarFactory { + Car createCar() throws FireException; + } + + public void testFactoryMethodMustDeclareAllConstructorExceptions() { + try { + FactoryProvider.newFactory(DefectiveCarFactoryWithNoExceptions.class, DefectiveCar.class); + fail(); + } catch (ConfigurationException expected) { + assertContains(expected.getMessage(), "no compatible exception is thrown"); + } + } + + public interface CorrectDefectiveCarFactory { + Car createCar() throws FireException, ExplosionException; + } + + public void testConstructorExceptionsAreThrownByFactory() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + bind(CorrectDefectiveCarFactory.class).toProvider( + FactoryProvider.newFactory( + CorrectDefectiveCarFactory.class, DefectiveCar.class)); + } + }); + try { + injector.getInstance(CorrectDefectiveCarFactory.class).createCar(); + fail(); + } catch (FireException e) { + fail(); + } catch (ExplosionException expected) { + } + } + + public static class MultipleConstructorDefectiveCar implements Car { + @AssistedInject + public MultipleConstructorDefectiveCar() throws ExplosionException { + throw new ExplosionException(); + } + + @AssistedInject + public MultipleConstructorDefectiveCar(@SuppressWarnings("unused") @Assisted Color c) + throws FireException { + throw new FireException(); + } + } + + public interface MultipleConstructorDefectiveCarFactory { + Car createCar() throws ExplosionException; + Car createCar(Color r) throws FireException; + } + + public void testMultipleConstructorExceptionMatching() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + bind(MultipleConstructorDefectiveCarFactory.class).toProvider( + FactoryProvider.newFactory( + MultipleConstructorDefectiveCarFactory.class, + MultipleConstructorDefectiveCar.class)); + } + }); + MultipleConstructorDefectiveCarFactory factory + = injector.getInstance(MultipleConstructorDefectiveCarFactory.class); + try { + factory.createCar(); + fail(); + } catch (ExplosionException expected) { + } + + try { + factory.createCar(Color.RED); + fail(); + } catch (FireException expected) { + } + } + + public static class WildcardCollection { + + public interface Factory { + WildcardCollection create(Collection items); + } + + @AssistedInject + public WildcardCollection(@SuppressWarnings("unused") @Assisted Collection items) { } + } + + public void testWildcardGenerics() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + bind(WildcardCollection.Factory.class).toProvider( + FactoryProvider.newFactory( + WildcardCollection.Factory.class, + WildcardCollection.class)); + } + }); + WildcardCollection.Factory factory = injector.getInstance(WildcardCollection.Factory.class); + factory.create(Collections.emptyList()); + } + + public static class SteeringWheel {} + + public static class Fiat implements Car { + @SuppressWarnings("unused") + private final SteeringWheel steeringWheel; + @SuppressWarnings("unused") + private final Color color; + + @AssistedInject + public Fiat(SteeringWheel steeringWheel, @Assisted Color color) { + this.steeringWheel = steeringWheel; + this.color = color; + } + } + + public void testFactoryWithImplicitBindings() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + bind(ColoredCarFactory.class).toProvider( + FactoryProvider.newFactory( + ColoredCarFactory.class, + Fiat.class)); + } + }); + + ColoredCarFactory coloredCarFactory = injector.getInstance(ColoredCarFactory.class); + Fiat fiat = (Fiat) coloredCarFactory.create(Color.GREEN); + assertEquals(Color.GREEN, fiat.color); + assertNotNull(fiat.steeringWheel); + } + + public void testFactoryFailsWithMissingBinding() { + try { + Guice.createInjector(new AbstractModule() { + @Override protected void configure() { + bind(ColoredCarFactory.class) + .toProvider(FactoryProvider.newFactory(ColoredCarFactory.class, Mustang.class)); + } + }); + fail(); + } catch (CreationException expected) { + assertContains(expected.getMessage(), + "1) Parameter of type 'double' is not injectable or annotated with @Assisted"); + } + } + + public void testMethodsDeclaredInObject() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override protected void configure() { + bind(Double.class).toInstance(5.0d); + bind(ColoredCarFactory.class) + .toProvider(FactoryProvider.newFactory(ColoredCarFactory.class, Mustang.class)); + } + }); + + ColoredCarFactory carFactory = injector.getInstance(ColoredCarFactory.class); + + carFactory.equals(carFactory); + carFactory.hashCode(); + carFactory.toString(); + } + + public void testAssistedInjectConstructorAndAssistedFactoryParameterMustNotMix() { + try { + Guice.createInjector(new AbstractModule() { + @Override protected void configure() { + bind(Double.class).toInstance(5.0d); + bind(AssistedParamsFactory.class) + .toProvider(FactoryProvider.newFactory(AssistedParamsFactory.class, Mustang.class)); + } + }); + fail(); + } catch (CreationException expected) { + assertContains(expected.getMessage(), "Factory method " + + AssistedParamsFactory.class.getName() + ".create() has an @Assisted parameter, which " + + "is incompatible with the deprecated @AssistedInject annotation."); + } + } + + interface AssistedParamsFactory { + Car create(@Assisted Color color); + } + + interface GenericColoredCarFactory { + T create(Color color); + } + + public void testGenericAssistedFactory() { + final TypeLiteral> mustangTypeLiteral + = new TypeLiteral>() {}; + final TypeLiteral> camaroTypeLiteral + = new TypeLiteral>() {}; + + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + bind(Double.class).toInstance(5.0d); + bind(int.class).annotatedWith(Names.named("horsePower")).toInstance(250); + bind(int.class).annotatedWith(Names.named("modelYear")).toInstance(1984); + bind(mustangTypeLiteral).toProvider( + FactoryProvider.newFactory(mustangTypeLiteral, TypeLiteral.get(Mustang.class))); + bind(camaroTypeLiteral).toProvider( + FactoryProvider.newFactory(camaroTypeLiteral, TypeLiteral.get(Camaro.class))); + } + }); + + GenericColoredCarFactory mustangFactory + = injector.getInstance(Key.get(mustangTypeLiteral)); + GenericColoredCarFactory camaroFactory + = injector.getInstance(Key.get(camaroTypeLiteral)); + + Mustang blueMustang = mustangFactory.create(Color.BLUE); + assertEquals(Color.BLUE, blueMustang.color); + assertEquals(5.0d, blueMustang.engineSize); + + Camaro redCamaro = camaroFactory.create(Color.RED); + assertEquals(Color.RED, redCamaro.color); + assertEquals(1984, redCamaro.modelYear); + assertEquals(250, redCamaro.horsePower); + } + + @SuppressWarnings("unused") + public interface Insurance { + } + + public static class MustangInsurance implements Insurance { + private final double premium; + private final double limit; + @SuppressWarnings("unused") private Mustang car; + + @AssistedInject + public MustangInsurance(@Named("lowLimit") double limit, @Assisted Mustang car, + @Assisted double premium) { + this.premium = premium; + this.limit = limit; + this.car = car; + } + + public void sell() {} + } + + public static class CamaroInsurance implements Insurance { + private final double premium; + private final double limit; + @SuppressWarnings("unused") private Camaro car; + + @AssistedInject + public CamaroInsurance(@Named("highLimit") double limit, @Assisted Camaro car, + @Assisted double premium) { + this.premium = premium; + this.limit = limit; + this.car = car; + } + + public void sell() {} + } + + public interface MustangInsuranceFactory { + public Insurance create(Mustang car, double premium); + } + + public interface CamaroInsuranceFactory { + public Insurance create(Camaro car, double premium); + } + + public void testAssistedFactoryForConcreteType() { + + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + bind(Double.class).annotatedWith(Names.named("lowLimit")).toInstance(50000.0d); + bind(Double.class).annotatedWith(Names.named("highLimit")).toInstance(100000.0d); + bind(MustangInsuranceFactory.class).toProvider( + FactoryProvider.newFactory(MustangInsuranceFactory.class, MustangInsurance.class)); + bind(CamaroInsuranceFactory.class).toProvider( + FactoryProvider.newFactory(CamaroInsuranceFactory.class, CamaroInsurance.class)); + } + }); + + MustangInsuranceFactory mustangInsuranceFactory = + injector.getInstance(MustangInsuranceFactory.class); + CamaroInsuranceFactory camaroInsuranceFactory = + injector.getInstance(CamaroInsuranceFactory.class); + + Mustang mustang = new Mustang(5000d, Color.BLACK); + MustangInsurance mustangPolicy = + (MustangInsurance) mustangInsuranceFactory.create(mustang, 800.0d); + assertEquals(800.0d, mustangPolicy.premium); + assertEquals(50000.0d, mustangPolicy.limit); + + Camaro camaro = new Camaro(3000, 1967, Color.BLUE); + CamaroInsurance camaroPolicy = (CamaroInsurance) camaroInsuranceFactory.create(camaro, 800.0d); + assertEquals(800.0d, camaroPolicy.premium); + assertEquals(100000.0d, camaroPolicy.limit); + } + + public interface InsuranceFactory { + public Insurance create(T car, double premium); + } + + public void testAssistedFactoryForParameterizedType() { + final TypeLiteral> mustangInsuranceFactoryType = + new TypeLiteral>() {}; + final TypeLiteral> camaroInsuranceFactoryType = + new TypeLiteral>() {}; + + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + bind(Double.class).annotatedWith(Names.named("lowLimit")).toInstance(50000.0d); + bind(Double.class).annotatedWith(Names.named("highLimit")).toInstance(100000.0d); + bind(mustangInsuranceFactoryType).toProvider(FactoryProvider.newFactory( + mustangInsuranceFactoryType, TypeLiteral.get(MustangInsurance.class))); + bind(camaroInsuranceFactoryType).toProvider(FactoryProvider.newFactory( + camaroInsuranceFactoryType, TypeLiteral.get(CamaroInsurance.class))); + } + }); + + InsuranceFactory mustangInsuranceFactory = + injector.getInstance(Key.get(mustangInsuranceFactoryType)); + InsuranceFactory camaroInsuranceFactory = + injector.getInstance(Key.get(camaroInsuranceFactoryType)); + + Mustang mustang = new Mustang(5000d, Color.BLACK); + MustangInsurance mustangPolicy = + (MustangInsurance) mustangInsuranceFactory.create(mustang, 800.0d); + assertEquals(800.0d, mustangPolicy.premium); + assertEquals(50000.0d, mustangPolicy.limit); + + Camaro camaro = new Camaro(3000, 1967, Color.BLUE); + CamaroInsurance camaroPolicy = (CamaroInsurance) camaroInsuranceFactory.create(camaro, 800.0d); + assertEquals(800.0d, camaroPolicy.premium); + assertEquals(100000.0d, camaroPolicy.limit); + } + + public static class AutoInsurance implements Insurance { + private final double premium; + private final double limit; + private final T car; + + @AssistedInject + public AutoInsurance(double limit, @Assisted T car, @Assisted double premium) { + this.limit = limit; + this.car = car; + this.premium = premium; + } + + public void sell() {} + } + + public void testAssistedFactoryForTypeVariableParameters() { + final TypeLiteral> camaroInsuranceFactoryType = + new TypeLiteral>() {}; + + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + bind(Double.class).toInstance(50000.0d); + bind(camaroInsuranceFactoryType).toProvider(FactoryProvider.newFactory( + camaroInsuranceFactoryType, new TypeLiteral>() {})); + } + }); + + InsuranceFactory camaroInsuranceFactory = + injector.getInstance(Key.get(camaroInsuranceFactoryType)); + + Camaro camaro = new Camaro(3000, 1967, Color.BLUE); + AutoInsurance camaroPolicy = + (AutoInsurance) camaroInsuranceFactory.create(camaro, 800.0d); + assertEquals(800.0d, camaroPolicy.premium); + assertEquals(50000.0d, camaroPolicy.limit); + assertEquals(camaro, camaroPolicy.car); + } + + public void testDuplicateAssistedFactoryBinding() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + bind(Double.class).toInstance(5.0d); + bind(ColoredCarFactory.class) + .toProvider(FactoryProvider.newFactory(ColoredCarFactory.class, Mustang.class)); + bind(ColoredCarFactory.class) + .toProvider(FactoryProvider.newFactory(ColoredCarFactory.class, Mustang.class)); + } + }); + ColoredCarFactory carFactory = injector.getInstance(ColoredCarFactory.class); + + Mustang blueMustang = (Mustang) carFactory.create(Color.BLUE); + assertEquals(Color.BLUE, blueMustang.color); + assertEquals(5.0d, blueMustang.engineSize); + + Mustang redMustang = (Mustang) carFactory.create(Color.RED); + assertEquals(Color.RED, redMustang.color); + assertEquals(5.0d, redMustang.engineSize); + } + + public interface Equals { + + enum ComparisonMethod { SHALLOW, DEEP; } + + interface Factory { + Equals equals(ComparisonMethod comparisonMethod); + } + + public static class Impl implements Equals { + private final double sigma; + private final ComparisonMethod comparisonMethod; + + @Inject + public Impl(double sigma, @Assisted ComparisonMethod comparisonMethod) { + this.sigma = sigma; + this.comparisonMethod = comparisonMethod; + } + } + } + + public void testFactoryMethodCalledEquals() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + bind(Double.class).toInstance(0.01d); + bind(Equals.Factory.class).toProvider( + FactoryProvider.newFactory(Equals.Factory.class, Impl.class)); + } + }); + Equals.Factory equalsFactory = injector.getInstance(Equals.Factory.class); + Impl shallowEquals = (Impl) equalsFactory.equals(ComparisonMethod.SHALLOW); + assertEquals(ComparisonMethod.SHALLOW, shallowEquals.comparisonMethod); + assertEquals(0.01d, shallowEquals.sigma); + } +} \ No newline at end of file diff --git a/src/test/java/com/google/inject/assistedinject/ManyConstructorsTest.java b/src/test/java/com/google/inject/assistedinject/ManyConstructorsTest.java new file mode 100644 index 0000000..9fd2657 --- /dev/null +++ b/src/test/java/com/google/inject/assistedinject/ManyConstructorsTest.java @@ -0,0 +1,258 @@ +package com.google.inject.assistedinject; + +import com.google.inject.AbstractModule; +import com.google.inject.Asserts; +import com.google.inject.CreationException; +import com.google.inject.Guice; +import com.google.inject.Injector; + +import junit.framework.TestCase; + +public class ManyConstructorsTest extends TestCase { + + public void testTwoConstructors() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + install(new FactoryModuleBuilder().build(Factory.class)); + } + }); + Factory factory = injector.getInstance(Factory.class); + Foo noIndex = factory.create("no index"); + assertEquals("no index", noIndex.name); + assertNull(noIndex.index); + Foo index = factory.create("index", 1); + assertEquals("index", index.name); + assertEquals(1, index.index.intValue()); + } + + public void testDifferentOrderParameters() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + install(new FactoryModuleBuilder().build(OtherFactory.class)); + } + }); + OtherFactory factory = injector.getInstance(OtherFactory.class); + Foo noIndex = factory.create("no index"); + assertEquals("no index", noIndex.name); + assertNull(noIndex.index); + Foo index = factory.create(1, "index"); + assertEquals("index", index.name); + assertEquals(1, index.index.intValue()); + Foo index2 = factory.create("index", 2); + assertEquals("index", index2.name); + assertEquals(2, index2.index.intValue()); + } + + public void testInterfaceToImpl() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + install(new FactoryModuleBuilder() + .implement(Bar.class, Foo.class) + .build(BarFactory.class)); + } + }); + BarFactory factory = injector.getInstance(BarFactory.class); + Bar noIndex = factory.create("no index"); + assertEquals("no index", noIndex.getName()); + assertNull(noIndex.getIndex()); + Bar index = factory.create("index", 1); + assertEquals("index", index.getName()); + assertEquals(1, index.getIndex().intValue()); + } + + public void testUsingOneConstructor() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + install(new FactoryModuleBuilder().build(SimpleFactory.class)); + } + }); + SimpleFactory factory = injector.getInstance(SimpleFactory.class); + Foo noIndex = factory.create("no index"); + assertEquals("no index", noIndex.name); + assertNull(noIndex.index); + + injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + install(new FactoryModuleBuilder().build(SimpleFactory2.class)); + } + }); + SimpleFactory2 factory2 = injector.getInstance(SimpleFactory2.class); + Foo index = factory2.create("index", 1); + assertEquals("index", index.name); + assertEquals(1, index.index.intValue()); + } + + public void testTooManyMatchingConstructors() { + try { + Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + install(new FactoryModuleBuilder() + .implement(Foo.class, TooManyMatches.class) + .build(SimpleFactory2.class)); + } + }); + fail("should have failed"); + } catch (CreationException expected) { + Asserts.assertContains(expected.getMessage(), "1) " + TooManyMatches.class.getName() + + " has more than one constructor annotated with @AssistedInject that " + + "matches the parameters in method " + SimpleFactory2.class.getName()); + } + } + + public void testNoMatchingConstructorsBecauseTooManyParams() { + try { + Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + install(new FactoryModuleBuilder().build(ComplexFactory.class)); + } + }); + fail("should have failed"); + } catch (CreationException expected) { + Asserts.assertContains(expected.getMessage(), "1) " + Foo.class.getName() + + " has @AssistedInject constructors, but none of them match the parameters in method " + + ComplexFactory.class.getName()); + } + } + + public void testNoMatchingConstrucotsBecauseTooLittleParams() { + try { + Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + install(new FactoryModuleBuilder().build(NullFactory.class)); + } + }); + fail("should have failed"); + } catch (CreationException expected) { + Asserts.assertContains(expected.getMessage(), "1) " + Foo.class.getName() + + " has @AssistedInject constructors, but none of them match the parameters in method " + + NullFactory.class.getName()); + } + } + + public static interface ComplexFactory { + Foo create(String name, int idx, float weight); + } + + public static interface NullFactory { + Foo create(); + } + + public static interface OtherFactory { + Foo create(String name, int idx); + Foo create(int idx, String name); + Foo create(String name); + } + + + public static interface Factory { + Foo create(String name); + Foo create(String name, int idx); + } + + public static interface BarFactory { + Bar create(String name); + Bar create(String name, int idx); + } + + public static interface SimpleFactory { + Foo create(String name); + } + + public static interface SimpleFactory2 { + Foo create(String name, int idx); + } + + public static class TooManyMatches extends Foo { + @AssistedInject TooManyMatches(@Assisted String name, @Assisted int index) { + } + + @AssistedInject TooManyMatches(@Assisted int index, @Assisted String name) { + } + } + + public static class Foo implements Bar { + private String name; + private Integer index; + + Foo() {} + + @AssistedInject Foo(@Assisted String name) { + this.name = name; + this.index = null; + } + + @AssistedInject Foo(@Assisted String name, @Assisted int index) { + this.name = name; + this.index = index; + } + + Foo(String a, String b, String c) { + + } + + public String getName() { return name; } + public Integer getIndex() { return index; } + } + + public static interface Bar { + String getName(); + Integer getIndex(); + } + + public void testDependenciesAndOtherAnnotations() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + install(new FactoryModuleBuilder().build(FamilyFarmFactory.class)); + } + }); + + FamilyFarmFactory factory = injector.getInstance(FamilyFarmFactory.class); + Farm pops = factory.popsFarm("Pop"); + assertEquals("Pop", pops.pop); + assertEquals(null, pops.mom); + Farm moms = factory.momsFarm("Mom"); + assertEquals(null, moms.pop); + assertEquals("Mom", moms.mom); + Farm momAndPop = factory.momAndPopsFarm("Mom", "Pop"); + assertEquals("Pop", momAndPop.pop); + assertEquals("Mom", momAndPop.mom); + } + + + public static interface FamilyFarmFactory { + Farm popsFarm(String pop); + Farm momsFarm(@Assisted("mom") String mom); + Farm momAndPopsFarm(@Assisted("mom") String mom, @Assisted("pop") String pop); + } + + public static class Farm { + String pop; + String mom; + + @AssistedInject Farm(@Assisted String pop, Dog dog) { + this.pop = pop; + } + + @AssistedInject Farm(@Assisted("mom") String mom, @Assisted("pop") String pop, Cow cow, Dog dog) { + this.pop = pop; + this.mom = mom; + } + + @AssistedInject Farm(@Assisted("mom") String mom, Cow cow) { + this.mom = mom; + } + } + + public static class Cow {} + public static class Dog {} + +} diff --git a/src/test/java/com/google/inject/multibindings/AllTests.java b/src/test/java/com/google/inject/multibindings/AllTests.java new file mode 100644 index 0000000..b75a19c --- /dev/null +++ b/src/test/java/com/google/inject/multibindings/AllTests.java @@ -0,0 +1,17 @@ +package com.google.inject.multibindings; + +import junit.framework.Test; +import junit.framework.TestSuite; + +public class AllTests { + + public static Test suite() { + TestSuite suite = new TestSuite(); + suite.addTestSuite(MapBinderTest.class); + suite.addTestSuite(MultibinderTest.class); + suite.addTestSuite(OptionalBinderTest.class); + suite.addTestSuite(RealElementTest.class); + suite.addTestSuite(ProvidesIntoTest.class); + return suite; + } +} diff --git a/src/test/java/com/google/inject/multibindings/Collector.java b/src/test/java/com/google/inject/multibindings/Collector.java new file mode 100644 index 0000000..1b7e048 --- /dev/null +++ b/src/test/java/com/google/inject/multibindings/Collector.java @@ -0,0 +1,28 @@ +package com.google.inject.multibindings; + +import com.google.inject.spi.DefaultBindingTargetVisitor; + +class Collector extends DefaultBindingTargetVisitor implements + MultibindingsTargetVisitor { + MapBinderBinding mapbinding; + MultibinderBinding setbinding; + OptionalBinderBinding optionalbinding; + + @Override + public Object visit(MapBinderBinding mapbinding) { + this.mapbinding = mapbinding; + return null; + } + + @Override + public Object visit(MultibinderBinding multibinding) { + this.setbinding = multibinding; + return null; + } + + @Override + public Object visit(OptionalBinderBinding optionalbinding) { + this.optionalbinding = optionalbinding; + return null; + } +} diff --git a/src/test/java/com/google/inject/multibindings/MapBinderTest.java b/src/test/java/com/google/inject/multibindings/MapBinderTest.java new file mode 100644 index 0000000..79a74cf --- /dev/null +++ b/src/test/java/com/google/inject/multibindings/MapBinderTest.java @@ -0,0 +1,1013 @@ +package com.google.inject.multibindings; + +import static com.google.inject.Asserts.asModuleChain; +import static com.google.inject.Asserts.assertContains; +import static com.google.inject.multibindings.SpiUtils.VisitType.BOTH; +import static com.google.inject.multibindings.SpiUtils.VisitType.MODULE; +import static com.google.inject.multibindings.SpiUtils.assertMapVisitor; +import static com.google.inject.multibindings.SpiUtils.instance; +import static com.google.inject.multibindings.SpiUtils.providerInstance; +import static com.google.inject.name.Names.named; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import com.google.common.base.Function; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; +import com.google.common.collect.Maps; +import com.google.common.collect.Sets; +import com.google.inject.AbstractModule; +import com.google.inject.Asserts; +import com.google.inject.Binding; +import com.google.inject.BindingAnnotation; +import com.google.inject.ConfigurationException; +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.Provider; +import com.google.inject.Provides; +import com.google.inject.ProvisionException; +import com.google.inject.Stage; +import com.google.inject.TypeLiteral; +import com.google.inject.internal.WeakKeySetUtils; +import com.google.inject.name.Names; +import com.google.inject.spi.Dependency; +import com.google.inject.spi.HasDependencies; +import com.google.inject.spi.InstanceBinding; +import com.google.inject.util.Modules; +import com.google.inject.util.Providers; +import com.google.inject.util.Types; + +import junit.framework.TestCase; + +import java.lang.annotation.Annotation; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.ref.WeakReference; +import java.lang.reflect.Method; +import java.lang.reflect.Type; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicReference; + +public class MapBinderTest extends TestCase { + + private static final Set> FRAMEWORK_KEYS = ImmutableSet.of( + //Key.get(java.util.logging.Logger.class), + Key.get(Stage.class), + Key.get(Injector.class) + ); + + final TypeLiteral>> mapOfStringJavaxProvider = + new TypeLiteral>>() {}; + final TypeLiteral>> mapOfStringProvider = + new TypeLiteral>>() {}; + final TypeLiteral> mapOfString = new TypeLiteral>() {}; + final TypeLiteral> mapOfIntString = + new TypeLiteral>() {}; + final TypeLiteral> mapOfInteger = new TypeLiteral>() {}; + final TypeLiteral>> mapOfSetOfString = + new TypeLiteral>>() {}; + + private final TypeLiteral stringType = TypeLiteral.get(String.class); + private final TypeLiteral intType = TypeLiteral.get(Integer.class); + + private Type javaxProviderOf(Type type) { + return Types.newParameterizedType(javax.inject.Provider.class, type); + } + + private Type mapEntryOf(Type keyType, Type valueType) { + return Types.newParameterizedTypeWithOwner(Map.class, Map.Entry.class, keyType, valueType); + } + + private Type collectionOf(Type type) { + return Types.newParameterizedType(Collection.class, type); + } + + public void testAllBindings() { + Module module = new AbstractModule() { + @Override + protected void configure() { + MapBinder.newMapBinder(binder(), String.class, String.class).permitDuplicates(); + } + }; + + Injector injector = Guice.createInjector(module); + + Map, Binding> bindings = injector.getBindings(); + + ImmutableSet> expectedBindings = ImmutableSet.>builder() + .add( + // Map + Key.get(Types.mapOf(String.class, String.class)), + // Map> + Key.get(Types.mapOf(String.class, Types.providerOf(String.class))), + // Map> + Key.get(Types.mapOf(String.class, javaxProviderOf(String.class))), + // Map> + Key.get(Types.mapOf(String.class, Types.setOf(String.class))), + // Map> + Key.get(Types.mapOf(String.class, Types.setOf(Types.providerOf(String.class)))), + // Set>> + Key.get(Types.setOf(mapEntryOf(String.class, Types.providerOf(String.class)))), + // Collection>>> + Key.get(collectionOf(Types.providerOf( + mapEntryOf(String.class, Types.providerOf(String.class))))), + // Collection>>> + Key.get(collectionOf(javaxProviderOf( + mapEntryOf(String.class, Types.providerOf(String.class))))), + // @Named(...) Boolean + Key.get(Boolean.class, + named("Multibinder>> permits duplicates")) + ) + .addAll(FRAMEWORK_KEYS).build(); + + Set> missingBindings = Sets.difference(expectedBindings, bindings.keySet()); + Set> extraBindings = Sets.difference(bindings.keySet(), expectedBindings); + + assertTrue("There should be no missing bindings. Missing: " + missingBindings, + missingBindings.isEmpty()); + assertTrue("There should be no extra bindings. Extra: " + extraBindings, + extraBindings.isEmpty()); + } + + public void testMapBinderAggregatesMultipleModules() { + Module abc = new AbstractModule() { + @Override protected void configure() { + MapBinder multibinder = MapBinder.newMapBinder( + binder(), String.class, String.class); + multibinder.addBinding("a").toInstance("A"); + multibinder.addBinding("b").toInstance("B"); + multibinder.addBinding("c").toInstance("C"); + } + }; + Module de = new AbstractModule() { + @Override protected void configure() { + MapBinder multibinder = MapBinder.newMapBinder( + binder(), String.class, String.class); + multibinder.addBinding("d").toInstance("D"); + multibinder.addBinding("e").toInstance("E"); + } + }; + + Injector injector = Guice.createInjector(abc, de); + Map abcde = injector.getInstance(Key.get(mapOfString)); + + assertEquals(mapOf("a", "A", "b", "B", "c", "C", "d", "D", "e", "E"), abcde); + assertMapVisitor(Key.get(mapOfString), stringType, stringType, setOf(abc, de), BOTH, false, 0, + instance("a", "A"), instance("b", "B"), instance("c", "C"), instance("d", "D"), instance("e", "E")); + + // just make sure these succeed + injector.getInstance(Key.get(mapOfStringProvider)); + injector.getInstance(Key.get(mapOfStringJavaxProvider)); + } + + public void testMapBinderAggregationForAnnotationInstance() { + Module module = new AbstractModule() { + @Override protected void configure() { + MapBinder multibinder = MapBinder.newMapBinder( + binder(), String.class, String.class, Names.named("abc")); + multibinder.addBinding("a").toInstance("A"); + multibinder.addBinding("b").toInstance("B"); + + multibinder = MapBinder.newMapBinder( + binder(), String.class, String.class, Names.named("abc")); + multibinder.addBinding("c").toInstance("C"); + } + }; + Injector injector = Guice.createInjector(module); + + Key> key = Key.get(mapOfString, Names.named("abc")); + Map abc = injector.getInstance(key); + assertEquals(mapOf("a", "A", "b", "B", "c", "C"), abc); + assertMapVisitor(key, stringType, stringType, setOf(module), BOTH, false, 0, + instance("a", "A"), instance("b", "B"), instance("c", "C")); + + // just make sure these succeed + injector.getInstance(Key.get(mapOfStringProvider, Names.named("abc"))); + injector.getInstance(Key.get(mapOfStringJavaxProvider, Names.named("abc"))); + } + + public void testMapBinderAggregationForAnnotationType() { + Module module = new AbstractModule() { + @Override protected void configure() { + MapBinder multibinder = MapBinder.newMapBinder( + binder(), String.class, String.class, Abc.class); + multibinder.addBinding("a").toInstance("A"); + multibinder.addBinding("b").toInstance("B"); + + multibinder = MapBinder.newMapBinder( + binder(), String.class, String.class, Abc.class); + multibinder.addBinding("c").toInstance("C"); + } + }; + Injector injector = Guice.createInjector(module); + + Key> key = Key.get(mapOfString, Abc.class); + Map abc = injector.getInstance(key); + assertEquals(mapOf("a", "A", "b", "B", "c", "C"), abc); + assertMapVisitor(key, stringType, stringType, setOf(module), BOTH, false, 0, + instance("a", "A"), instance("b", "B"), instance("c", "C")); + + // just make sure these succeed + injector.getInstance(Key.get(mapOfStringProvider, Abc.class)); + injector.getInstance(Key.get(mapOfStringJavaxProvider, Abc.class)); + } + + public void testMapBinderWithMultipleAnnotationValueSets() { + Module module = new AbstractModule() { + @Override protected void configure() { + MapBinder abcMapBinder = MapBinder.newMapBinder( + binder(), String.class, String.class, named("abc")); + abcMapBinder.addBinding("a").toInstance("A"); + abcMapBinder.addBinding("b").toInstance("B"); + abcMapBinder.addBinding("c").toInstance("C"); + + MapBinder deMapBinder = MapBinder.newMapBinder( + binder(), String.class, String.class, named("de")); + deMapBinder.addBinding("d").toInstance("D"); + deMapBinder.addBinding("e").toInstance("E"); + } + }; + Injector injector = Guice.createInjector(module); + + Key> abcKey = Key.get(mapOfString, named("abc")); + Map abc = injector.getInstance(abcKey); + Key> deKey = Key.get(mapOfString, named("de")); + Map de = injector.getInstance(deKey); + assertEquals(mapOf("a", "A", "b", "B", "c", "C"), abc); + assertEquals(mapOf("d", "D", "e", "E"), de); + assertMapVisitor(abcKey, stringType, stringType, setOf(module), BOTH, false, 1, + instance("a", "A"), instance("b", "B"), instance("c", "C")); + assertMapVisitor(deKey, stringType, stringType, setOf(module), BOTH, false, 1, + instance("d", "D"), instance("e", "E")); + + // just make sure these succeed + injector.getInstance(Key.get(mapOfStringProvider, named("abc"))); + injector.getInstance(Key.get(mapOfStringJavaxProvider, named("abc"))); + injector.getInstance(Key.get(mapOfStringProvider, named("de"))); + injector.getInstance(Key.get(mapOfStringJavaxProvider, named("de"))); + } + + public void testMapBinderWithMultipleAnnotationTypeSets() { + Module module = new AbstractModule() { + @Override protected void configure() { + MapBinder abcMapBinder = MapBinder.newMapBinder( + binder(), String.class, String.class, Abc.class); + abcMapBinder.addBinding("a").toInstance("A"); + abcMapBinder.addBinding("b").toInstance("B"); + abcMapBinder.addBinding("c").toInstance("C"); + + MapBinder deMapBinder = MapBinder.newMapBinder( + binder(), String.class, String.class, De.class); + deMapBinder.addBinding("d").toInstance("D"); + deMapBinder.addBinding("e").toInstance("E"); + } + }; + Injector injector = Guice.createInjector(module); + + Key> abcKey = Key.get(mapOfString, Abc.class); + Map abc = injector.getInstance(abcKey); + Key> deKey = Key.get(mapOfString, De.class); + Map de = injector.getInstance(deKey); + assertEquals(mapOf("a", "A", "b", "B", "c", "C"), abc); + assertEquals(mapOf("d", "D", "e", "E"), de); + assertMapVisitor(abcKey, stringType, stringType, setOf(module), BOTH, false, 1, + instance("a", "A"), instance("b", "B"), instance("c", "C")); + assertMapVisitor(deKey, stringType, stringType, setOf(module), BOTH, false, 1, + instance("d", "D"), instance("e", "E")); + + // just make sure these succeed + injector.getInstance(Key.get(mapOfStringProvider, Abc.class)); + injector.getInstance(Key.get(mapOfStringJavaxProvider, Abc.class)); + injector.getInstance(Key.get(mapOfStringProvider, De.class)); + injector.getInstance(Key.get(mapOfStringJavaxProvider, De.class)); + } + + public void testMapBinderWithMultipleTypes() { + Module module = new AbstractModule() { + @Override protected void configure() { + MapBinder.newMapBinder(binder(), String.class, String.class) + .addBinding("a").toInstance("A"); + MapBinder.newMapBinder(binder(), String.class, Integer.class) + .addBinding("1").toInstance(1); + } + }; + Injector injector = Guice.createInjector(module); + + assertEquals(mapOf("a", "A"), injector.getInstance(Key.get(mapOfString))); + assertEquals(mapOf("1", 1), injector.getInstance(Key.get(mapOfInteger))); + assertMapVisitor(Key.get(mapOfString), stringType, stringType, setOf(module), BOTH, false, 1, + instance("a", "A")); + assertMapVisitor(Key.get(mapOfInteger), stringType, intType, setOf(module), BOTH, false, 1, + instance("1", 1)); + } + + public void testMapBinderWithEmptyMap() { + Module module = new AbstractModule() { + @Override protected void configure() { + MapBinder.newMapBinder(binder(), String.class, String.class); + } + }; + Injector injector = Guice.createInjector(module); + + Map map = injector.getInstance(Key.get(mapOfString)); + assertEquals(Collections.emptyMap(), map); + assertMapVisitor(Key.get(mapOfString), stringType, stringType, setOf(module), BOTH, false, 0); + } + + public void testMapBinderMapIsUnmodifiable() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override protected void configure() { + MapBinder.newMapBinder(binder(), String.class, String.class) + .addBinding("a").toInstance("A"); + } + }); + + Map map = injector.getInstance(Key.get(mapOfString)); + try { + map.clear(); + fail(); + } catch(UnsupportedOperationException expected) { + } + } + + public void testMapBinderMapIsLazy() { + Module module = new AbstractModule() { + @Override protected void configure() { + MapBinder.newMapBinder(binder(), String.class, Integer.class) + .addBinding("num").toProvider(new Provider() { + int nextValue = 1; + @Override public Integer get() { + return nextValue++; + } + }); + } + }; + Injector injector = Guice.createInjector(module); + + assertEquals(mapOf("num", 1), injector.getInstance(Key.get(mapOfInteger))); + assertEquals(mapOf("num", 2), injector.getInstance(Key.get(mapOfInteger))); + assertEquals(mapOf("num", 3), injector.getInstance(Key.get(mapOfInteger))); + assertMapVisitor(Key.get(mapOfInteger), stringType, intType, setOf(module), BOTH, false, 0, + providerInstance("num", 1)); + } + + public void testMapBinderMapForbidsDuplicateKeys() { + Module module = new AbstractModule() { + @Override protected void configure() { + MapBinder multibinder = MapBinder.newMapBinder( + binder(), String.class, String.class); + multibinder.addBinding("a").toInstance("A"); + multibinder.addBinding("a").toInstance("B"); + } + }; + try { + Guice.createInjector(module); + fail(); + } catch(CreationException expected) { + assertContains(expected.getMessage(), + "Map injection failed due to duplicated key \"a\""); + } + + assertMapVisitor(Key.get(mapOfString), stringType, stringType, setOf(module), MODULE, false, 0, + instance("a", "A"), instance("a", "B")); + } + + public void testExhaustiveDuplicateErrorMessage() throws Exception { + class Module1 extends AbstractModule { + @Override protected void configure() { + MapBinder mapbinder = + MapBinder.newMapBinder(binder(), String.class, Object.class); + mapbinder.addBinding("a").to(String.class); + } + } + class Module2 extends AbstractModule { + @Override protected void configure() { + MapBinder mapbinder = + MapBinder.newMapBinder(binder(), String.class, Object.class); + mapbinder.addBinding("a").to(Integer.class); + mapbinder.addBinding("b").to(String.class); + } + } + class Module3 extends AbstractModule { + @Override protected void configure() { + MapBinder mapbinder = + MapBinder.newMapBinder(binder(), String.class, Object.class); + mapbinder.addBinding("b").to(Integer.class); + } + } + class Main extends AbstractModule { + @Override protected void configure() { + MapBinder.newMapBinder(binder(), String.class, Object.class); + install(new Module1()); + install(new Module2()); + install(new Module3()); + } + @Provides String provideString() { return "foo"; } + @Provides Integer provideInt() { return 42; } + } + try { + Guice.createInjector(new Main()); + fail(); + } catch(CreationException ce) { + assertContains(ce.getMessage(), + "Map injection failed due to duplicated key \"a\", from bindings:", + asModuleChain(Main.class, Module1.class), + asModuleChain(Main.class, Module2.class), + "and key: \"b\", from bindings:", + asModuleChain(Main.class, Module2.class), + asModuleChain(Main.class, Module3.class), + "at " + Main.class.getName() + ".configure(", + asModuleChain(Main.class, MapBinder.RealMapBinder.class)); + assertEquals(1, ce.getErrorMessages().size()); + } + } + + public void testMapBinderMapPermitDuplicateElements() { + Module ab = new AbstractModule() { + @Override protected void configure() { + MapBinder multibinder = MapBinder.newMapBinder( + binder(), String.class, String.class); + multibinder.addBinding("a").toInstance("A"); + multibinder.addBinding("b").toInstance("B"); + multibinder.permitDuplicates(); + } + }; + Module bc = new AbstractModule() { + @Override protected void configure() { + MapBinder multibinder = MapBinder.newMapBinder( + binder(), String.class, String.class); + multibinder.addBinding("b").toInstance("B"); + multibinder.addBinding("c").toInstance("C"); + multibinder.permitDuplicates(); + } + }; + Injector injector = Guice.createInjector(ab, bc); + + assertEquals(mapOf("a", "A", "b", "B", "c", "C"), injector.getInstance(Key.get(mapOfString))); + assertMapVisitor(Key.get(mapOfString), stringType, stringType, setOf(ab, bc), BOTH, true, 0, + instance("a", "A"), instance("b", "B"), instance("c", "C")); + } + + public void testMapBinderMapDoesNotDedupeDuplicateValues() { + class ValueType { + int keyPart; + int dataPart; + private ValueType(int keyPart, int dataPart) { + this.keyPart = keyPart; + this.dataPart = dataPart; + } + @Override + public boolean equals(Object obj) { + return (obj instanceof ValueType) && (keyPart == ((ValueType) obj).keyPart); + } + @Override + public int hashCode() { + return keyPart; + } + } + Module m1 = new AbstractModule() { + @Override protected void configure() { + MapBinder multibinder = MapBinder.newMapBinder( + binder(), String.class, ValueType.class); + multibinder.addBinding("a").toInstance(new ValueType(1, 2)); + } + }; + Module m2 = new AbstractModule() { + @Override protected void configure() { + MapBinder multibinder = MapBinder.newMapBinder( + binder(), String.class, ValueType.class); + multibinder.addBinding("b").toInstance(new ValueType(1, 3)); + } + }; + + Injector injector = Guice.createInjector(m1, m2); + Map map = injector.getInstance(new Key>() {}); + assertEquals(2, map.get("a").dataPart); + assertEquals(3, map.get("b").dataPart); + } + + public void testMapBinderMultimap() { + AbstractModule ab1c = new AbstractModule() { + @Override protected void configure() { + MapBinder multibinder = MapBinder.newMapBinder( + binder(), String.class, String.class); + multibinder.addBinding("a").toInstance("A"); + multibinder.addBinding("b").toInstance("B1"); + multibinder.addBinding("c").toInstance("C"); + } + }; + AbstractModule b2c = new AbstractModule() { + @Override protected void configure() { + MapBinder multibinder = MapBinder.newMapBinder( + binder(), String.class, String.class); + multibinder.addBinding("b").toInstance("B2"); + multibinder.addBinding("c").toInstance("C"); + multibinder.permitDuplicates(); + } + }; + Injector injector = Guice.createInjector(ab1c, b2c); + + assertEquals(mapOf("a", setOf("A"), "b", setOf("B1", "B2"), "c", setOf("C")), + injector.getInstance(Key.get(mapOfSetOfString))); + assertMapVisitor(Key.get(mapOfString), stringType, stringType, setOf(ab1c, b2c), BOTH, true, 0, + instance("a", "A"), instance("b", "B1"), instance("b", "B2"), instance("c", "C")); + } + + public void testMapBinderMultimapWithAnotation() { + AbstractModule ab1 = new AbstractModule() { + @Override protected void configure() { + MapBinder multibinder = MapBinder.newMapBinder( + binder(), String.class, String.class, Abc.class); + multibinder.addBinding("a").toInstance("A"); + multibinder.addBinding("b").toInstance("B1"); + } + }; + AbstractModule b2c = new AbstractModule() { + @Override protected void configure() { + MapBinder multibinder = MapBinder.newMapBinder( + binder(), String.class, String.class, Abc.class); + multibinder.addBinding("b").toInstance("B2"); + multibinder.addBinding("c").toInstance("C"); + multibinder.permitDuplicates(); + } + }; + Injector injector = Guice.createInjector(ab1, b2c); + + assertEquals(mapOf("a", setOf("A"), "b", setOf("B1", "B2"), "c", setOf("C")), + injector.getInstance(Key.get(mapOfSetOfString, Abc.class))); + try { + injector.getInstance(Key.get(mapOfSetOfString)); + fail(); + } catch (ConfigurationException expected) {} + + assertMapVisitor(Key.get(mapOfString, Abc.class), stringType, stringType, setOf(ab1, b2c), BOTH, true, 0, + instance("a", "A"), instance("b", "B1"), instance("b", "B2"), instance("c", "C")); + } + + public void testMapBinderMultimapIsUnmodifiable() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override protected void configure() { + MapBinder mapBinder = MapBinder.newMapBinder( + binder(), String.class, String.class); + mapBinder.addBinding("a").toInstance("A"); + mapBinder.permitDuplicates(); + } + }); + + Map> map = injector.getInstance(Key.get(mapOfSetOfString)); + try { + map.clear(); + fail(); + } catch(UnsupportedOperationException expected) { + } + try { + map.get("a").clear(); + fail(); + } catch(UnsupportedOperationException expected) { + } + } + + public void testMapBinderMapForbidsNullKeys() { + try { + Guice.createInjector(new AbstractModule() { + @Override protected void configure() { + MapBinder.newMapBinder(binder(), String.class, String.class).addBinding(null); + } + }); + fail(); + } catch (CreationException expected) {} + } + + public void testMapBinderMapForbidsNullValues() { + Module m = new AbstractModule() { + @Override protected void configure() { + MapBinder.newMapBinder(binder(), String.class, String.class) + .addBinding("null").toProvider(Providers.of(null)); + } + }; + Injector injector = Guice.createInjector(m); + + try { + injector.getInstance(Key.get(mapOfString)); + fail(); + } catch(ProvisionException expected) { + assertContains(expected.getMessage(), + "1) Map injection failed due to null value for key \"null\", bound at: " + + m.getClass().getName() + ".configure("); + } + } + + public void testMapBinderProviderIsScoped() { + final Provider counter = new Provider() { + int next = 1; + @Override public Integer get() { + return next++; + } + }; + + Injector injector = Guice.createInjector(new AbstractModule() { + @Override protected void configure() { + MapBinder.newMapBinder(binder(), String.class, Integer.class) + .addBinding("one").toProvider(counter).asEagerSingleton(); + } + }); + + assertEquals(1, (int) injector.getInstance(Key.get(mapOfInteger)).get("one")); + assertEquals(1, (int) injector.getInstance(Key.get(mapOfInteger)).get("one")); + } + + public void testSourceLinesInMapBindings() { + try { + Guice.createInjector(new AbstractModule() { + @Override protected void configure() { + MapBinder.newMapBinder(binder(), String.class, Integer.class) + .addBinding("one"); + } + }); + fail(); + } catch (CreationException expected) { + assertContains(expected.getMessage(), + "1) No implementation for java.lang.Integer", + "at " + getClass().getName()); + } + } + + /** We just want to make sure that mapbinder's binding depends on the underlying multibinder. */ + public void testMultibinderDependencies() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override protected void configure() { + MapBinder mapBinder + = MapBinder.newMapBinder(binder(), Integer.class, String.class); + mapBinder.addBinding(1).toInstance("A"); + mapBinder.addBinding(2).to(Key.get(String.class, Names.named("b"))); + + bindConstant().annotatedWith(Names.named("b")).to("B"); + } + }); + + Binding> binding = injector.getBinding(new Key>() {}); + HasDependencies withDependencies = (HasDependencies) binding; + Key setKey = new Key>>>() {}; + assertEquals(ImmutableSet.>of(Dependency.get(setKey)), + withDependencies.getDependencies()); + Set elements = Sets.newHashSet(); + elements.addAll(recurseForDependencies(injector, withDependencies)); + assertEquals(ImmutableSet.of("A", "B"), elements); + } + + private Set recurseForDependencies(Injector injector, HasDependencies hasDependencies) { + Set elements = Sets.newHashSet(); + for (Dependency dependency : hasDependencies.getDependencies()) { + Binding binding = injector.getBinding(dependency.getKey()); + HasDependencies deps = (HasDependencies) binding; + if (binding instanceof InstanceBinding) { + elements.add((String) ((InstanceBinding) binding).getInstance()); + } else { + elements.addAll(recurseForDependencies(injector, deps)); + } + } + return elements; + } + + /** We just want to make sure that mapbinder's binding depends on the underlying multibinder. */ + public void testMultibinderDependenciesInToolStage() { + Injector injector = Guice.createInjector(Stage.TOOL, new AbstractModule() { + @Override protected void configure() { + MapBinder mapBinder + = MapBinder.newMapBinder(binder(), Integer.class, String.class); + mapBinder.addBinding(1).toInstance("A"); + mapBinder.addBinding(2).to(Key.get(String.class, Names.named("b"))); + + bindConstant().annotatedWith(Names.named("b")).to("B"); + }}); + + Binding> binding = injector.getBinding(new Key>() {}); + HasDependencies withDependencies = (HasDependencies) binding; + Key setKey = new Key>>>() {}; + assertEquals(ImmutableSet.>of(Dependency.get(setKey)), + withDependencies.getDependencies()); + } + + + /** + * Our implementation maintains order, but doesn't guarantee it in the API spec. + * TODO: specify the iteration order? + */ + public void testBindOrderEqualsIterationOrder() { + Injector injector = Guice.createInjector( + new AbstractModule() { + @Override protected void configure() { + MapBinder mapBinder + = MapBinder.newMapBinder(binder(), String.class, String.class); + mapBinder.addBinding("leonardo").toInstance("blue"); + mapBinder.addBinding("donatello").toInstance("purple"); + install(new AbstractModule() { + @Override protected void configure() { + MapBinder.newMapBinder(binder(), String.class, String.class) + .addBinding("michaelangelo").toInstance("orange"); + } + }); + } + }, + new AbstractModule() { + @Override protected void configure() { + MapBinder.newMapBinder(binder(), String.class, String.class) + .addBinding("raphael").toInstance("red"); + } + }); + + Map map = injector.getInstance(new Key>() {}); + Iterator> iterator = map.entrySet().iterator(); + assertEquals(Maps.immutableEntry("leonardo", "blue"), iterator.next()); + assertEquals(Maps.immutableEntry("donatello", "purple"), iterator.next()); + assertEquals(Maps.immutableEntry("michaelangelo", "orange"), iterator.next()); + assertEquals(Maps.immutableEntry("raphael", "red"), iterator.next()); + } + + /** + * With overrides, we should get the union of all map bindings. + */ + public void testModuleOverrideAndMapBindings() { + Module ab = new AbstractModule() { + @Override protected void configure() { + MapBinder multibinder = MapBinder.newMapBinder(binder(), String.class, String.class); + multibinder.addBinding("a").toInstance("A"); + multibinder.addBinding("b").toInstance("B"); + } + }; + Module cd = new AbstractModule() { + @Override protected void configure() { + MapBinder multibinder = MapBinder.newMapBinder(binder(), String.class, String.class); + multibinder.addBinding("c").toInstance("C"); + multibinder.addBinding("d").toInstance("D"); + } + }; + Module ef = new AbstractModule() { + @Override protected void configure() { + MapBinder multibinder = MapBinder.newMapBinder(binder(), String.class, String.class); + multibinder.addBinding("e").toInstance("E"); + multibinder.addBinding("f").toInstance("F"); + } + }; + + Module abcd = Modules.override(ab).with(cd); + Injector injector = Guice.createInjector(abcd, ef); + assertEquals(mapOf("a", "A", "b", "B", "c", "C", "d", "D", "e", "E", "f", "F"), + injector.getInstance(Key.get(mapOfString))); + assertMapVisitor(Key.get(mapOfString), stringType, stringType, setOf(abcd, ef), BOTH, false, 0, + instance("a", "A"), instance("b", "B"), instance("c", "C"), instance("d", "D"), instance( + "e", "E"), instance("f", "F")); + } + + public void testDeduplicateMapBindings() { + Module module = new AbstractModule() { + @Override protected void configure() { + MapBinder mapbinder = + MapBinder.newMapBinder(binder(), String.class, String.class); + mapbinder.addBinding("a").toInstance("A"); + mapbinder.addBinding("a").toInstance("A"); + mapbinder.addBinding("b").toInstance("B"); + mapbinder.addBinding("b").toInstance("B"); + + } + }; + Injector injector = Guice.createInjector(module); + assertEquals(mapOf("a", "A", "b", "B"), + injector.getInstance(Key.get(mapOfString))); + assertMapVisitor(Key.get(mapOfString), stringType, stringType, setOf(module), BOTH, false, 0, + instance("a", "A"), instance("b", "B")); + } + + /** + * With overrides, we should get the union of all map bindings. + */ + public void testModuleOverrideAndMapBindingsWithPermitDuplicates() { + Module abc = new AbstractModule() { + @Override protected void configure() { + MapBinder multibinder = MapBinder.newMapBinder(binder(), String.class, String.class); + multibinder.addBinding("a").toInstance("A"); + multibinder.addBinding("b").toInstance("B"); + multibinder.addBinding("c").toInstance("C"); + multibinder.permitDuplicates(); + } + }; + Module cd = new AbstractModule() { + @Override protected void configure() { + MapBinder multibinder = MapBinder.newMapBinder(binder(), String.class, String.class); + multibinder.addBinding("c").toInstance("C"); + multibinder.addBinding("d").toInstance("D"); + multibinder.permitDuplicates(); + } + }; + Module ef = new AbstractModule() { + @Override protected void configure() { + MapBinder multibinder = MapBinder.newMapBinder(binder(), String.class, String.class); + multibinder.addBinding("e").toInstance("E"); + multibinder.addBinding("f").toInstance("F"); + multibinder.permitDuplicates(); + } + }; + + Module abcd = Modules.override(abc).with(cd); + Injector injector = Guice.createInjector(abcd, ef); + assertEquals(mapOf("a", "A", "b", "B", "c", "C", "d", "D", "e", "E", "f", "F"), + injector.getInstance(Key.get(mapOfString))); + assertMapVisitor(Key.get(mapOfString), stringType, stringType, setOf(abcd, ef), BOTH, true, 0, + instance("a", "A"), instance("b", "B"), instance("c", "C"), instance( + "d", "D"), instance("e", "E"), instance("f", "F")); + + } + + /** Ensure there are no initialization race conditions in basic map injection. */ + public void testBasicMapDependencyInjection() { + final AtomicReference> injectedMap = + new AtomicReference>(); + final Object anObject = new Object() { + @Inject void initialize(Map map) { + injectedMap.set(map); + } + }; + Module abc = new AbstractModule() { + @Override protected void configure() { + requestInjection(anObject); + MapBinder multibinder = + MapBinder.newMapBinder(binder(), String.class, String.class); + multibinder.addBinding("a").toInstance("A"); + multibinder.addBinding("b").toInstance("B"); + multibinder.addBinding("c").toInstance("C"); + } + }; + Guice.createInjector(abc); + assertEquals(mapOf("a", "A", "b", "B", "c", "C"), injectedMap.get()); + } + + /** Ensure there are no initialization race conditions in provider multimap injection. */ + public void testProviderMultimapDependencyInjection() { + final AtomicReference>>> injectedMultimap = + new AtomicReference>>>(); + final Object anObject = new Object() { + @Inject void initialize(Map>> multimap) { + injectedMultimap.set(multimap); + } + }; + Module abc = new AbstractModule() { + @Override protected void configure() { + requestInjection(anObject); + MapBinder multibinder = + MapBinder.newMapBinder(binder(), String.class, String.class); + multibinder.permitDuplicates(); + multibinder.addBinding("a").toInstance("A"); + multibinder.addBinding("b").toInstance("B"); + multibinder.addBinding("c").toInstance("C"); + } + }; + Guice.createInjector(abc); + Map map = Maps.transformValues(injectedMultimap.get(), + new Function>, String>() { + @Override public String apply(Set> stringProvidersSet) { + return Iterables.getOnlyElement(stringProvidersSet).get(); + } + }); + assertEquals(mapOf("a", "A", "b", "B", "c", "C"), map); + } + + @Retention(RUNTIME) @BindingAnnotation + @interface Abc {} + + @Retention(RUNTIME) @BindingAnnotation + @interface De {} + + @SuppressWarnings("unchecked") + private Map mapOf(Object... elements) { + Map result = new HashMap(); + for (int i = 0; i < elements.length; i += 2) { + result.put((K)elements[i], (V)elements[i+1]); + } + return result; + } + + @SuppressWarnings("unchecked") + private Set setOf(V... elements) { + return new HashSet(Arrays.asList(elements)); + } + + @BindingAnnotation + @Retention(RetentionPolicy.RUNTIME) + @Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD}) + private static @interface Marker {} + + @Marker + public void testMapBinderMatching() throws Exception { + Method m = MapBinderTest.class.getDeclaredMethod("testMapBinderMatching"); + assertNotNull(m); + final Annotation marker = m.getAnnotation(Marker.class); + Injector injector = Guice.createInjector(new AbstractModule() { + @Override public void configure() { + MapBinder mb1 = + MapBinder.newMapBinder(binder(), Integer.class, Integer.class, Marker.class); + MapBinder mb2 = + MapBinder.newMapBinder(binder(), Integer.class, Integer.class, marker); + mb1.addBinding(1).toInstance(1); + mb2.addBinding(2).toInstance(2); + + // This assures us that the two binders are equivalent, so we expect the instance added to + // each to have been added to one set. + assertEquals(mb1, mb2); + } + }); + TypeLiteral> t = new TypeLiteral>() {}; + Map s1 = injector.getInstance(Key.get(t, Marker.class)); + Map s2 = injector.getInstance(Key.get(t, marker)); + + // This assures us that the two sets are in fact equal. They may not be same set (as in Java + // object identical), but we shouldn't expect that, since probably Guice creates the set each + // time in case the elements are dependent on scope. + assertEquals(s1, s2); + + // This ensures that MultiBinder is internally using the correct set name -- + // making sure that instances of marker annotations have the same set name as + // MarkerAnnotation.class. + Map expected = new HashMap(); + expected.put(1, 1); + expected.put(2, 2); + assertEquals(expected, s1); + } + + public void testTwoMapBindersAreDistinct() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override protected void configure() { + MapBinder.newMapBinder(binder(), String.class, String.class) + .addBinding("A").toInstance("a"); + + MapBinder.newMapBinder(binder(), Integer.class, String.class) + .addBinding(1).toInstance("b"); + } + }); + Collector collector = new Collector(); + Binding> map1 = injector.getBinding(Key.get(mapOfString)); + map1.acceptTargetVisitor(collector); + assertNotNull(collector.mapbinding); + MapBinderBinding map1Binding = collector.mapbinding; + + Binding> map2 = injector.getBinding(Key.get(mapOfIntString)); + map2.acceptTargetVisitor(collector); + assertNotNull(collector.mapbinding); + MapBinderBinding map2Binding = collector.mapbinding; + + List> bindings = injector.findBindingsByType(stringType); + assertEquals("should have two elements: " + bindings, 2, bindings.size()); + Binding a = bindings.get(0); + Binding b = bindings.get(1); + assertEquals("a", ((InstanceBinding) a).getInstance()); + assertEquals("b", ((InstanceBinding) b).getInstance()); + + // Make sure the correct elements belong to their own sets. + assertTrue(map1Binding.containsElement(a)); + assertFalse(map1Binding.containsElement(b)); + + assertFalse(map2Binding.containsElement(a)); + assertTrue(map2Binding.containsElement(b)); + } + + // Tests for com.google.inject.internal.WeakKeySet not leaking memory. + public void testWeakKeySet_integration_mapbinder() { + Key> mapKey = Key.get(new TypeLiteral>() {}); + + Injector parentInjector = Guice.createInjector(new AbstractModule() { + @Override protected void configure() { + bind(String.class).toInstance("hi"); + } + }); + WeakKeySetUtils.assertNotBlacklisted(parentInjector, mapKey); + + Injector childInjector = parentInjector.createChildInjector(new AbstractModule() { + @Override protected void configure() { + MapBinder binder = + MapBinder.newMapBinder(binder(), String.class, String.class); + binder.addBinding("bar").toInstance("foo"); + } + }); + WeakReference weakRef = new WeakReference(childInjector); + WeakKeySetUtils.assertBlacklisted(parentInjector, mapKey); + + // Clear the ref, GC, and ensure that we are no longer blacklisting. + childInjector = null; + + Asserts.awaitClear(weakRef); + WeakKeySetUtils.assertNotBlacklisted(parentInjector, mapKey); + } +} diff --git a/src/test/java/com/google/inject/multibindings/MultibinderTest.java b/src/test/java/com/google/inject/multibindings/MultibinderTest.java new file mode 100644 index 0000000..3fd010c --- /dev/null +++ b/src/test/java/com/google/inject/multibindings/MultibinderTest.java @@ -0,0 +1,1202 @@ +package com.google.inject.multibindings; + +import static com.google.inject.Asserts.assertContains; +import static com.google.inject.multibindings.Multibinder.collectionOfJavaxProvidersOf; +import static com.google.inject.multibindings.SpiUtils.VisitType.BOTH; +import static com.google.inject.multibindings.SpiUtils.VisitType.MODULE; +import static com.google.inject.multibindings.SpiUtils.assertSetVisitor; +import static com.google.inject.multibindings.SpiUtils.instance; +import static com.google.inject.multibindings.SpiUtils.providerInstance; +import static com.google.inject.name.Names.named; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import com.google.common.base.Optional; +import com.google.common.base.Predicates; +import com.google.common.collect.FluentIterable; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; +import com.google.common.collect.Sets; +import com.google.inject.AbstractModule; +import com.google.inject.Binding; +import com.google.inject.BindingAnnotation; +import com.google.inject.CreationException; +import com.google.inject.Guice; +import com.google.inject.Injector; +import com.google.inject.Key; +import com.google.inject.Module; +import com.google.inject.Provider; +import com.google.inject.Provides; +import com.google.inject.ProvisionException; +import com.google.inject.Scopes; +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.InstanceBinding; +import com.google.inject.spi.LinkedKeyBinding; +import com.google.inject.util.Modules; +import com.google.inject.util.Providers; +import com.google.inject.util.Types; + +import junit.framework.TestCase; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.lang.annotation.Annotation; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.reflect.Method; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +public class MultibinderTest extends TestCase { + + final TypeLiteral> optionalOfString = + new TypeLiteral>() {}; + final TypeLiteral> mapOfStringString = + new TypeLiteral>() {}; + final TypeLiteral> setOfString = new TypeLiteral>() {}; + final TypeLiteral> setOfInteger = new TypeLiteral>() {}; + final TypeLiteral stringType = TypeLiteral.get(String.class); + final TypeLiteral intType = TypeLiteral.get(Integer.class); + final TypeLiteral> listOfStrings = new TypeLiteral>() {}; + final TypeLiteral>> setOfListOfStrings = new TypeLiteral>>() {}; + final TypeLiteral>> collectionOfProvidersOfStrings = + new TypeLiteral>>() {}; + + public void testMultibinderAggregatesMultipleModules() { + Module abc = new AbstractModule() { + @Override protected void configure() { + Multibinder multibinder = Multibinder.newSetBinder(binder(), String.class); + multibinder.addBinding().toInstance("A"); + multibinder.addBinding().toInstance("B"); + multibinder.addBinding().toInstance("C"); + } + }; + Module de = new AbstractModule() { + @Override protected void configure() { + Multibinder multibinder = Multibinder.newSetBinder(binder(), String.class); + multibinder.addBinding().toInstance("D"); + multibinder.addBinding().toInstance("E"); + } + }; + + Injector injector = Guice.createInjector(abc, de); + Key> setKey = Key.get(setOfString); + Set abcde = injector.getInstance(setKey); + Set results = setOf("A", "B", "C", "D", "E"); + + assertEquals(results, abcde); + assertSetVisitor(setKey, stringType, setOf(abc, de), BOTH, false, 0, + instance("A"), instance("B"), instance("C"), instance("D"), instance("E")); + } + + public void testMultibinderAggregationForAnnotationInstance() { + Module module = new AbstractModule() { + @Override protected void configure() { + Multibinder multibinder + = Multibinder.newSetBinder(binder(), String.class, Names.named("abc")); + multibinder.addBinding().toInstance("A"); + multibinder.addBinding().toInstance("B"); + + multibinder = Multibinder.newSetBinder(binder(), String.class, Names.named("abc")); + multibinder.addBinding().toInstance("C"); + } + }; + Injector injector = Guice.createInjector(module); + + Key> setKey = Key.get(setOfString, Names.named("abc")); + Set abc = injector.getInstance(setKey); + Set results = setOf("A", "B", "C"); + assertEquals(results, abc); + assertSetVisitor(setKey, stringType, setOf(module), BOTH, false, 0, + instance("A"), instance("B"), instance("C")); + } + + public void testMultibinderAggregationForAnnotationType() { + Module module = new AbstractModule() { + @Override protected void configure() { + Multibinder multibinder + = Multibinder.newSetBinder(binder(), String.class, Abc.class); + multibinder.addBinding().toInstance("A"); + multibinder.addBinding().toInstance("B"); + + multibinder = Multibinder.newSetBinder(binder(), String.class, Abc.class); + multibinder.addBinding().toInstance("C"); + } + }; + Injector injector = Guice.createInjector(module); + + Key> setKey = Key.get(setOfString, Abc.class); + Set abcde = injector.getInstance(setKey); + Set results = setOf("A", "B", "C"); + assertEquals(results, abcde); + assertSetVisitor(setKey, stringType, setOf(module), BOTH, false, 0, + instance("A"), instance("B"), instance("C")); + } + + public void testMultibinderWithMultipleAnnotationValueSets() { + Module module = new AbstractModule() { + @Override protected void configure() { + Multibinder abcMultibinder + = Multibinder.newSetBinder(binder(), String.class, named("abc")); + abcMultibinder.addBinding().toInstance("A"); + abcMultibinder.addBinding().toInstance("B"); + abcMultibinder.addBinding().toInstance("C"); + + Multibinder deMultibinder + = Multibinder.newSetBinder(binder(), String.class, named("de")); + deMultibinder.addBinding().toInstance("D"); + deMultibinder.addBinding().toInstance("E"); + } + }; + Injector injector = Guice.createInjector(module); + + Key> abcSetKey = Key.get(setOfString, named("abc")); + Set abc = injector.getInstance(abcSetKey); + Key> deSetKey = Key.get(setOfString, named("de")); + Set de = injector.getInstance(deSetKey); + Set abcResults = setOf("A", "B", "C"); + assertEquals(abcResults, abc); + Set deResults = setOf("D", "E"); + assertEquals(deResults, de); + assertSetVisitor(abcSetKey, stringType, setOf(module), BOTH, false, 1, + instance("A"), instance("B"), instance("C")); + assertSetVisitor(deSetKey, stringType, setOf(module), BOTH, false, 1, + instance("D"), instance("E")); + } + + public void testMultibinderWithMultipleAnnotationTypeSets() { + Module module = new AbstractModule() { + @Override protected void configure() { + Multibinder abcMultibinder + = Multibinder.newSetBinder(binder(), String.class, Abc.class); + abcMultibinder.addBinding().toInstance("A"); + abcMultibinder.addBinding().toInstance("B"); + abcMultibinder.addBinding().toInstance("C"); + + Multibinder deMultibinder + = Multibinder.newSetBinder(binder(), String.class, De.class); + deMultibinder.addBinding().toInstance("D"); + deMultibinder.addBinding().toInstance("E"); + } + }; + Injector injector = Guice.createInjector(module); + + Key> abcSetKey = Key.get(setOfString, Abc.class); + Set abc = injector.getInstance(abcSetKey); + Key> deSetKey = Key.get(setOfString, De.class); + Set de = injector.getInstance(deSetKey); + Set abcResults = setOf("A", "B", "C"); + assertEquals(abcResults, abc); + Set deResults = setOf("D", "E"); + assertEquals(deResults, de); + assertSetVisitor(abcSetKey, stringType, setOf(module), BOTH, false, 1, + instance("A"), instance("B"), instance("C")); + assertSetVisitor(deSetKey, stringType, setOf(module), BOTH, false, 1, + instance("D"), instance("E")); + } + + public void testMultibinderWithMultipleSetTypes() { + Module module = new AbstractModule() { + @Override protected void configure() { + Multibinder.newSetBinder(binder(), String.class) + .addBinding().toInstance("A"); + Multibinder.newSetBinder(binder(), Integer.class) + .addBinding().toInstance(1); + } + }; + Injector injector = Guice.createInjector(module); + + assertEquals(setOf("A"), injector.getInstance(Key.get(setOfString))); + assertEquals(setOf(1), injector.getInstance(Key.get(setOfInteger))); + assertSetVisitor(Key.get(setOfString), stringType, setOf(module), BOTH, false, 1, + instance("A")); + assertSetVisitor(Key.get(setOfInteger), intType, setOf(module), BOTH, false, 1, + instance(1)); + } + + public void testMultibinderWithEmptySet() { + Module module = new AbstractModule() { + @Override protected void configure() { + Multibinder.newSetBinder(binder(), String.class); + } + }; + Injector injector = Guice.createInjector(module); + + Set set = injector.getInstance(Key.get(setOfString)); + assertEquals(Collections.emptySet(), set); + assertSetVisitor(Key.get(setOfString), stringType, + setOf(module), BOTH, false, 0); + } + + public void testMultibinderSetIsUnmodifiable() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override protected void configure() { + Multibinder.newSetBinder(binder(), String.class) + .addBinding().toInstance("A"); + } + }); + + Set set = injector.getInstance(Key.get(setOfString)); + try { + set.clear(); + fail(); + } catch(UnsupportedOperationException expected) { + } + } + + public void testMultibinderSetIsSerializable() throws IOException, ClassNotFoundException { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override protected void configure() { + Multibinder.newSetBinder(binder(), String.class) + .addBinding().toInstance("A"); + } + }); + + Set set = injector.getInstance(Key.get(setOfString)); + ByteArrayOutputStream byteStream = new ByteArrayOutputStream(); + ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteStream); + try { + objectOutputStream.writeObject(set); + } finally { + objectOutputStream.close(); + } + ObjectInputStream objectInputStream = new ObjectInputStream( + new ByteArrayInputStream(byteStream.toByteArray())); + try { + Object setCopy = objectInputStream.readObject(); + assertEquals(set, setCopy); + } finally { + objectInputStream.close(); + } + } + + public void testMultibinderSetIsLazy() { + Module module = new AbstractModule() { + @Override protected void configure() { + Multibinder.newSetBinder(binder(), Integer.class) + .addBinding().toProvider(new Provider() { + int nextValue = 1; + public Integer get() { + return nextValue++; + } + }); + } + }; + Injector injector = Guice.createInjector(module); + + assertEquals(setOf(1), injector.getInstance(Key.get(setOfInteger))); + assertEquals(setOf(2), injector.getInstance(Key.get(setOfInteger))); + assertEquals(setOf(3), injector.getInstance(Key.get(setOfInteger))); + assertSetVisitor(Key.get(setOfInteger), intType, setOf(module), BOTH, false, 0, + providerInstance(1)); + } + + public void testMultibinderSetForbidsDuplicateElements() { + Module module1 = new AbstractModule() { + @Override protected void configure() { + final Multibinder multibinder = Multibinder.newSetBinder(binder(), String.class); + multibinder.addBinding().toProvider(Providers.of("A")); + } + }; + Module module2 = new AbstractModule() { + @Override protected void configure() { + final Multibinder multibinder = Multibinder.newSetBinder(binder(), String.class); + multibinder.addBinding().toInstance("A"); + } + }; + Injector injector = Guice.createInjector(module1, module2); + + try { + injector.getInstance(Key.get(setOfString)); + fail(); + } catch (ProvisionException expected) { + assertContains(expected.getMessage(), + "1) Set injection failed due to duplicated element \"A\"", + "Bound at " + module1.getClass().getName(), + "Bound at " + module2.getClass().getName()); + } + + // But we can still visit the module! + assertSetVisitor(Key.get(setOfString), stringType, setOf(module1, module2), MODULE, false, 0, + instance("A"), instance("A")); + } + + public void testMultibinderSetShowsBothElementsIfToStringDifferent() { + // A simple example of a type whose toString returns more information than its equals method + // considers. + class ValueType { + int a; + int b; + ValueType(int a, int b) { + this.a = a; + this.b = b; + } + @Override + public boolean equals(Object obj) { + return (obj instanceof ValueType) && (((ValueType) obj).a == a); + } + @Override + public int hashCode() { + return a; + } + @Override + public String toString() { + return String.format("ValueType(%d,%d)", a, b); + } + } + + Module module1 = new AbstractModule() { + @Override protected void configure() { + final Multibinder multibinder = + Multibinder.newSetBinder(binder(), ValueType.class); + multibinder.addBinding().toProvider(Providers.of(new ValueType(1, 2))); + } + }; + Module module2 = new AbstractModule() { + @Override protected void configure() { + final Multibinder multibinder = + Multibinder.newSetBinder(binder(), ValueType.class); + multibinder.addBinding().toInstance(new ValueType(1, 3)); + } + }; + Injector injector = Guice.createInjector(module1, module2); + + TypeLiteral valueType = TypeLiteral.get(ValueType.class); + TypeLiteral> setOfValueType = new TypeLiteral>() {}; + try { + injector.getInstance(Key.get(setOfValueType)); + fail(); + } catch (ProvisionException expected) { + assertContains(expected.getMessage(), + "1) Set injection failed due to multiple elements comparing equal:", + "\"ValueType(1,2)\"", + "bound at " + module1.getClass().getName(), + "\"ValueType(1,3)\"", + "bound at " + module2.getClass().getName()); + } + + // But we can still visit the module! + assertSetVisitor(Key.get(setOfValueType), valueType, setOf(module1, module2), MODULE, false, 0, + instance(new ValueType(1, 2)), instance(new ValueType(1, 3))); + } + + public void testMultibinderSetPermitDuplicateElements() { + Module ab = new AbstractModule() { + @Override protected void configure() { + Multibinder multibinder = Multibinder.newSetBinder(binder(), String.class); + multibinder.addBinding().toInstance("A"); + multibinder.addBinding().toInstance("B"); + } + }; + Module bc = new AbstractModule() { + @Override protected void configure() { + Multibinder multibinder = Multibinder.newSetBinder(binder(), String.class); + multibinder.permitDuplicates(); + multibinder.addBinding().toInstance("B"); + multibinder.addBinding().toInstance("C"); + } + }; + Injector injector = Guice.createInjector(ab, bc); + + assertEquals(setOf("A", "B", "C"), injector.getInstance(Key.get(setOfString))); + assertSetVisitor(Key.get(setOfString), stringType, setOf(ab, bc), BOTH, true, 0, + instance("A"), instance("B"), instance("C")); + } + + public void testMultibinderSetPermitDuplicateCallsToPermitDuplicates() { + Module ab = new AbstractModule() { + @Override protected void configure() { + Multibinder multibinder = Multibinder.newSetBinder(binder(), String.class); + multibinder.permitDuplicates(); + multibinder.addBinding().toInstance("A"); + multibinder.addBinding().toInstance("B"); + } + }; + Module bc = new AbstractModule() { + @Override protected void configure() { + Multibinder multibinder = Multibinder.newSetBinder(binder(), String.class); + multibinder.permitDuplicates(); + multibinder.addBinding().toInstance("B"); + multibinder.addBinding().toInstance("C"); + } + }; + Injector injector = Guice.createInjector(ab, bc); + + assertEquals(setOf("A", "B", "C"), injector.getInstance(Key.get(setOfString))); + assertSetVisitor(Key.get(setOfString), stringType, setOf(ab, bc), BOTH, true, 0, + instance("A"), instance("B"), instance("C")); + } + + public void testMultibinderSetForbidsNullElements() { + Module m = new AbstractModule() { + @Override protected void configure() { + Multibinder.newSetBinder(binder(), String.class) + .addBinding().toProvider(Providers.of(null)); + } + }; + Injector injector = Guice.createInjector(m); + + try { + injector.getInstance(Key.get(setOfString)); + fail(); + } catch(ProvisionException expected) { + assertContains(expected.getMessage(), + "1) Set injection failed due to null element bound at: " + + m.getClass().getName() + ".configure("); + } + } + + public void testSourceLinesInMultibindings() { + try { + Guice.createInjector(new AbstractModule() { + @Override protected void configure() { + Multibinder.newSetBinder(binder(), Integer.class).addBinding(); + } + }); + fail(); + } catch (CreationException expected) { + assertContains(expected.getMessage(), "No implementation for java.lang.Integer", + "at " + getClass().getName()); + } + } + + /** + * We just want to make sure that multibinder's binding depends on each of its values. We don't + * really care about the underlying structure of those bindings, which are implementation details. + */ + public void testMultibinderDependencies() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override protected void configure() { + Multibinder multibinder = Multibinder.newSetBinder(binder(), String.class); + multibinder.addBinding().toInstance("A"); + multibinder.addBinding().to(Key.get(String.class, Names.named("b"))); + + bindConstant().annotatedWith(Names.named("b")).to("B"); + } + }); + + Binding> binding = injector.getBinding(new Key>() {}); + HasDependencies withDependencies = (HasDependencies) binding; + Set elements = Sets.newHashSet(); + for (Dependency dependency : withDependencies.getDependencies()) { + elements.add((String) injector.getInstance(dependency.getKey())); + } + assertEquals(ImmutableSet.of("A", "B"), elements); + } + + /** + * We just want to make sure that multibinder's binding depends on each of its values. We don't + * really care about the underlying structure of those bindings, which are implementation details. + */ + public void testMultibinderDependenciesInToolStage() { + Injector injector = Guice.createInjector(Stage.TOOL, new AbstractModule() { + @Override protected void configure() { + Multibinder multibinder = Multibinder.newSetBinder(binder(), String.class); + multibinder.addBinding().toInstance("A"); + multibinder.addBinding().to(Key.get(String.class, Names.named("b"))); + + bindConstant().annotatedWith(Names.named("b")).to("B"); + } + }); + + Binding> binding = injector.getBinding(new Key>() {}); + HasDependencies withDependencies = (HasDependencies) binding; + InstanceBinding instanceBinding = null; + LinkedKeyBinding linkedBinding = null; + // The non-tool stage test can test this by calling injector.getInstance to ensure + // the right values are returned -- in tool stage we can't do that. It's also a + // little difficult to validate the dependencies & bindings, because they're + // bindings created internally within Multibinder. + // To workaround this, we just validate that the dependencies lookup to a single + // InstanceBinding whose value is "A" and another LinkedBinding whose target is + // the Key of @Named("b") String=B + for (Dependency dependency : withDependencies.getDependencies()) { + Binding b = injector.getBinding(dependency.getKey()); + if(b instanceof InstanceBinding) { + if(instanceBinding != null) { + fail("Already have an instance binding of: " + instanceBinding + ", and now want to add: " + b); + } else { + instanceBinding = (InstanceBinding)b; + } + } else if(b instanceof LinkedKeyBinding) { + if(linkedBinding != null) { + fail("Already have a linked binding of: " + linkedBinding + ", and now want to add: " + b); + } else { + linkedBinding = (LinkedKeyBinding)b; + } + } else { + fail("Unexpected dependency of: " + dependency); + } + } + + assertNotNull(instanceBinding); + assertNotNull(linkedBinding); + + assertEquals("A", instanceBinding.getInstance()); + assertEquals(Key.get(String.class, Names.named("b")), linkedBinding.getLinkedKey()); + } + + /** + * Our implementation maintains order, but doesn't guarantee it in the API spec. + * TODO: specify the iteration order? + */ + public void testBindOrderEqualsIterationOrder() { + Injector injector = Guice.createInjector( + new AbstractModule() { + @Override protected void configure() { + Multibinder multibinder = Multibinder.newSetBinder(binder(), String.class); + multibinder.addBinding().toInstance("leonardo"); + multibinder.addBinding().toInstance("donatello"); + install(new AbstractModule() { + @Override protected void configure() { + Multibinder.newSetBinder(binder(), String.class) + .addBinding().toInstance("michaelangelo"); + } + }); + } + }, + new AbstractModule() { + @Override protected void configure() { + Multibinder.newSetBinder(binder(), String.class).addBinding().toInstance("raphael"); + } + }); + + List inOrder = ImmutableList.copyOf(injector.getInstance(Key.get(setOfString))); + assertEquals(ImmutableList.of("leonardo", "donatello", "michaelangelo", "raphael"), inOrder); + } + + @Retention(RUNTIME) @BindingAnnotation + @interface Abc {} + + @Retention(RUNTIME) @BindingAnnotation + @interface De {} + + private Set setOf(T... elements) { + Set result = Sets.newHashSet(); + Collections.addAll(result, elements); + return result; + } + + /** + * With overrides, we should get the union of all multibindings. + */ + public void testModuleOverrideAndMultibindings() { + Module ab = new AbstractModule() { + @Override protected void configure() { + Multibinder multibinder = Multibinder.newSetBinder(binder(), String.class); + multibinder.addBinding().toInstance("A"); + multibinder.addBinding().toInstance("B"); + } + }; + Module cd = new AbstractModule() { + @Override protected void configure() { + Multibinder multibinder = Multibinder.newSetBinder(binder(), String.class); + multibinder.addBinding().toInstance("C"); + multibinder.addBinding().toInstance("D"); + } + }; + Module ef = new AbstractModule() { + @Override protected void configure() { + Multibinder multibinder = Multibinder.newSetBinder(binder(), String.class); + multibinder.addBinding().toInstance("E"); + multibinder.addBinding().toInstance("F"); + } + }; + + Module abcd = Modules.override(ab).with(cd); + Injector injector = Guice.createInjector(abcd, ef); + assertEquals(ImmutableSet.of("A", "B", "C", "D", "E", "F"), + injector.getInstance(Key.get(setOfString))); + + assertSetVisitor(Key.get(setOfString), stringType, setOf(abcd, ef), BOTH, false, 0, + instance("A"), instance("B"), instance("C"), instance("D"), instance("E"), instance("F")); + } + + /** + * With overrides, we should get the union of all multibindings. + */ + public void testModuleOverrideAndMultibindingsWithPermitDuplicates() { + Module abc = new AbstractModule() { + @Override protected void configure() { + Multibinder multibinder = Multibinder.newSetBinder(binder(), String.class); + multibinder.addBinding().toInstance("A"); + multibinder.addBinding().toInstance("B"); + multibinder.addBinding().toInstance("C"); + multibinder.permitDuplicates(); + } + }; + Module cd = new AbstractModule() { + @Override protected void configure() { + Multibinder multibinder = Multibinder.newSetBinder(binder(), String.class); + multibinder.addBinding().toInstance("C"); + multibinder.addBinding().toInstance("D"); + multibinder.permitDuplicates(); + } + }; + Module ef = new AbstractModule() { + @Override protected void configure() { + Multibinder multibinder = Multibinder.newSetBinder(binder(), String.class); + multibinder.addBinding().toInstance("E"); + multibinder.addBinding().toInstance("F"); + multibinder.permitDuplicates(); + } + }; + + Module abcd = Modules.override(abc).with(cd); + Injector injector = Guice.createInjector(abcd, ef); + assertEquals(ImmutableSet.of("A", "B", "C", "D", "E", "F"), + injector.getInstance(Key.get(setOfString))); + + assertSetVisitor(Key.get(setOfString), stringType, setOf(abcd, ef), BOTH, true, 0, + instance("A"), instance("B"), instance("C"), instance("D"), instance("E"), instance("F")); + } + + /** + * Doubly-installed modules should not conflict, even when one is overridden. + */ + public void testModuleOverrideRepeatedInstallsAndMultibindings_toInstance() { + Module ab = new AbstractModule() { + @Override protected void configure() { + Multibinder multibinder = Multibinder.newSetBinder(binder(), String.class); + multibinder.addBinding().toInstance("A"); + multibinder.addBinding().toInstance("B"); + } + }; + + // Guice guarantees this assertion, as the same module cannot be installed twice. + assertEquals(ImmutableSet.of("A", "B"), + Guice.createInjector(ab, ab).getInstance(Key.get(setOfString))); + + // Guice will only guarantee this assertion if Multibinder ensures the bindings match. + Injector injector = Guice.createInjector(ab, Modules.override(ab).with(ab)); + assertEquals(ImmutableSet.of("A", "B"), + injector.getInstance(Key.get(setOfString))); + } + + public void testModuleOverrideRepeatedInstallsAndMultibindings_toKey() { + Module ab = new AbstractModule() { + @Override protected void configure() { + Key aKey = Key.get(String.class, Names.named("A_string")); + Key bKey = Key.get(String.class, Names.named("B_string")); + bind(aKey).toInstance("A"); + bind(bKey).toInstance("B"); + + Multibinder multibinder = Multibinder.newSetBinder(binder(), String.class); + multibinder.addBinding().to(aKey); + multibinder.addBinding().to(bKey); + } + }; + + // Guice guarantees this assertion, as the same module cannot be installed twice. + assertEquals(ImmutableSet.of("A", "B"), + Guice.createInjector(ab, ab).getInstance(Key.get(setOfString))); + + // Guice will only guarantee this assertion if Multibinder ensures the bindings match. + Injector injector = Guice.createInjector(ab, Modules.override(ab).with(ab)); + assertEquals(ImmutableSet.of("A", "B"), + injector.getInstance(Key.get(setOfString))); + } + + public void testModuleOverrideRepeatedInstallsAndMultibindings_toProviderInstance() { + Module ab = new AbstractModule() { + @Override protected void configure() { + Multibinder multibinder = Multibinder.newSetBinder(binder(), String.class); + multibinder.addBinding().toProvider(Providers.of("A")); + multibinder.addBinding().toProvider(Providers.of("B")); + } + }; + + // Guice guarantees this assertion, as the same module cannot be installed twice. + assertEquals(ImmutableSet.of("A", "B"), + Guice.createInjector(ab, ab).getInstance(Key.get(setOfString))); + + // Guice will only guarantee this assertion if Multibinder ensures the bindings match. + Injector injector = Guice.createInjector(ab, Modules.override(ab).with(ab)); + assertEquals(ImmutableSet.of("A", "B"), + injector.getInstance(Key.get(setOfString))); + } + + private static class AStringProvider implements Provider { + public String get() { + return "A"; + } + } + + private static class BStringProvider implements Provider { + public String get() { + return "B"; + } + } + + public void testModuleOverrideRepeatedInstallsAndMultibindings_toProviderKey() { + Module ab = new AbstractModule() { + @Override protected void configure() { + Multibinder multibinder = Multibinder.newSetBinder(binder(), String.class); + multibinder.addBinding().toProvider(Key.get(AStringProvider.class)); + multibinder.addBinding().toProvider(Key.get(BStringProvider.class)); + } + }; + + // Guice guarantees this assertion, as the same module cannot be installed twice. + assertEquals(ImmutableSet.of("A", "B"), + Guice.createInjector(ab, ab).getInstance(Key.get(setOfString))); + + // Guice will only guarantee this assertion if Multibinder ensures the bindings match. + Injector injector = Guice.createInjector(ab, Modules.override(ab).with(ab)); + assertEquals(ImmutableSet.of("A", "B"), + injector.getInstance(Key.get(setOfString))); + } + + private static class StringGrabber { + private final String string; + + @SuppressWarnings("unused") // Found by reflection + public StringGrabber(@Named("A_string") String string) { + this.string = string; + } + + @SuppressWarnings("unused") // Found by reflection + public StringGrabber(@Named("B_string") String string, int unused) { + this.string = string; + } + + @Override + public int hashCode() { + return string.hashCode(); + } + + @Override + public boolean equals(Object obj) { + return (obj instanceof StringGrabber) && ((StringGrabber) obj).string.equals(string); + } + + @Override + public String toString() { + return "StringGrabber(" + string + ")"; + } + + static Set values(Iterable grabbers) { + Set result = new HashSet(); + for (StringGrabber grabber : grabbers) { + result.add(grabber.string); + } + return result; + } + } + + public void testModuleOverrideRepeatedInstallsAndMultibindings_toConstructor() { + TypeLiteral> setOfStringGrabber = new TypeLiteral>() {}; + Module ab = new AbstractModule() { + @Override protected void configure() { + Key aKey = Key.get(String.class, Names.named("A_string")); + Key bKey = Key.get(String.class, Names.named("B_string")); + bind(aKey).toInstance("A"); + bind(bKey).toInstance("B"); + bind(Integer.class).toInstance(0); // used to disambiguate constructors + + Multibinder multibinder = + Multibinder.newSetBinder(binder(), StringGrabber.class); + try { + multibinder.addBinding().toConstructor( + StringGrabber.class.getConstructor(String.class)); + multibinder.addBinding().toConstructor( + StringGrabber.class.getConstructor(String.class, int.class)); + } catch (NoSuchMethodException e) { + fail("No such method: " + e.getMessage()); + } + } + }; + + // Guice guarantees this assertion, as the same module cannot be installed twice. + assertEquals(ImmutableSet.of("A", "B"), + StringGrabber.values( + Guice.createInjector(ab, ab).getInstance(Key.get(setOfStringGrabber)))); + + // Guice will only guarantee this assertion if Multibinder ensures the bindings match. + Injector injector = Guice.createInjector(ab, Modules.override(ab).with(ab)); + assertEquals(ImmutableSet.of("A", "B"), + StringGrabber.values(injector.getInstance(Key.get(setOfStringGrabber)))); + } + + /** + * Unscoped bindings should not conflict, whether they were bound with no explicit scope, or + * explicitly bound in {@link Scopes#NO_SCOPE}. + */ + public void testDuplicateUnscopedBindings() { + Module singleBinding = new AbstractModule() { + @Override protected void configure() { + bind(Integer.class).to(Key.get(Integer.class, named("A"))); + bind(Integer.class).to(Key.get(Integer.class, named("A"))).in(Scopes.NO_SCOPE); + } + + @Provides @Named("A") + int provideInteger() { + return 5; + } + }; + Module multibinding = new AbstractModule() { + @Override protected void configure() { + Multibinder multibinder = Multibinder.newSetBinder(binder(), Integer.class); + multibinder.addBinding().to(Key.get(Integer.class, named("A"))); + multibinder.addBinding().to(Key.get(Integer.class, named("A"))).in(Scopes.NO_SCOPE); + } + }; + + assertEquals(5, + (int) Guice.createInjector(singleBinding).getInstance(Integer.class)); + assertEquals(ImmutableSet.of(5), + Guice.createInjector(singleBinding, multibinding).getInstance(Key.get(setOfInteger))); + } + + /** + * Ensure key hash codes are fixed at injection time, not binding time. + */ + public void testKeyHashCodesFixedAtInjectionTime() { + Module ab = new AbstractModule() { + @Override protected void configure() { + Multibinder> multibinder = Multibinder.newSetBinder(binder(), listOfStrings); + List list = Lists.newArrayList(); + multibinder.addBinding().toInstance(list); + list.add("A"); + list.add("B"); + } + }; + + Injector injector = Guice.createInjector(ab); + for (Entry, Binding> entry : injector.getAllBindings().entrySet()) { + Key bindingKey = entry.getKey(); + Key clonedKey; + if (bindingKey.getAnnotation() != null) { + clonedKey = Key.get(bindingKey.getTypeLiteral(), bindingKey.getAnnotation()); + } else if (bindingKey.getAnnotationType() != null) { + clonedKey = Key.get(bindingKey.getTypeLiteral(), bindingKey.getAnnotationType()); + } else { + clonedKey = Key.get(bindingKey.getTypeLiteral()); + } + assertEquals(bindingKey, clonedKey); + assertEquals("Incorrect hashcode for " + bindingKey + " -> " + entry.getValue(), + bindingKey.hashCode(), clonedKey.hashCode()); + } + } + + /** + * Ensure bindings do not rehash their keys once returned from {@link Elements#getElements}. + */ + public void testBindingKeysFixedOnReturnFromGetElements() { + final List list = Lists.newArrayList(); + Module ab = new AbstractModule() { + @Override protected void configure() { + Multibinder> multibinder = Multibinder.newSetBinder(binder(), listOfStrings); + multibinder.addBinding().toInstance(list); + list.add("A"); + list.add("B"); + } + }; + + InstanceBinding binding = Iterables.getOnlyElement( + Iterables.filter(Elements.getElements(ab), InstanceBinding.class)); + Key keyBefore = binding.getKey(); + assertEquals(listOfStrings, keyBefore.getTypeLiteral()); + + list.add("C"); + Key keyAfter = binding.getKey(); + assertSame(keyBefore, keyAfter); + } + + /* + * Verify through gratuitous mutation that key hashCode snapshots and whatnot happens at the right + * times, by binding two lists that are different at injector creation, but compare equal when the + * module is configured *and* when the set is instantiated. + */ + public void testConcurrentMutation_bindingsDiffentAtInjectorCreation() { + // We initially bind two equal lists + final List list1 = Lists.newArrayList(); + final List list2 = Lists.newArrayList(); + Module module = new AbstractModule() { + @Override protected void configure() { + Multibinder> multibinder = Multibinder.newSetBinder(binder(), listOfStrings); + multibinder.addBinding().toInstance(list1); + multibinder.addBinding().toInstance(list2); + } + }; + List elements = Elements.getElements(module); + + // Now we change the lists so they no longer match, and create the injector. + list1.add("A"); + list2.add("B"); + Injector injector = Guice.createInjector(Elements.getModule(elements)); + + // Now we change the lists so they compare equal again, and create the set. + list1.add(1, "B"); + list2.add(0, "A"); + try { + injector.getInstance(Key.get(setOfListOfStrings)); + fail(); + } catch (ProvisionException e) { + assertEquals(1, e.getErrorMessages().size()); + assertContains( + Iterables.getOnlyElement(e.getErrorMessages()).getMessage().toString(), + "Set injection failed due to duplicated element \"[A, B]\""); + } + + // Finally, we change the lists again so they are once more different, and ensure the set + // contains both. + list1.remove("A"); + list2.remove("B"); + Set> set = injector.getInstance(Key.get(setOfListOfStrings)); + assertEquals(ImmutableSet.of(ImmutableList.of("A"), ImmutableList.of("B")), set); + } + + /* + * Verify through gratuitous mutation that key hashCode snapshots and whatnot happen at the right + * times, by binding two lists that compare equal at injector creation, but are different when the + * module is configured *and* when the set is instantiated. + */ + public void testConcurrentMutation_bindingsSameAtInjectorCreation() { + // We initially bind two distinct lists + final List list1 = Lists.newArrayList("A"); + final List list2 = Lists.newArrayList("B"); + Module module = new AbstractModule() { + @Override protected void configure() { + Multibinder> multibinder = Multibinder.newSetBinder(binder(), listOfStrings); + multibinder.addBinding().toInstance(list1); + multibinder.addBinding().toInstance(list2); + } + }; + List elements = Elements.getElements(module); + + // Now we change the lists so they compare equal, and create the injector. + list1.add(1, "B"); + list2.add(0, "A"); + Injector injector = Guice.createInjector(Elements.getModule(elements)); + + // Now we change the lists again so they are once more different, and create the set. + list1.remove("A"); + list2.remove("B"); + Set> set = injector.getInstance(Key.get(setOfListOfStrings)); + + // The set will contain just one of the two lists. + // (In fact, it will be the first one we bound, but we don't promise that, so we won't test it.) + assertTrue(ImmutableSet.of(ImmutableList.of("A")).equals(set) + || ImmutableSet.of(ImmutableList.of("B")).equals(set)); + } + + @BindingAnnotation + @Retention(RetentionPolicy.RUNTIME) + @Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD}) + private static @interface Marker {} + + @Marker + public void testMultibinderMatching() throws Exception { + Method m = MultibinderTest.class.getDeclaredMethod("testMultibinderMatching"); + assertNotNull(m); + final Annotation marker = m.getAnnotation(Marker.class); + Injector injector = Guice.createInjector(new AbstractModule() { + @Override public void configure() { + Multibinder mb1 = Multibinder.newSetBinder(binder(), Integer.class, Marker.class); + Multibinder mb2 = Multibinder.newSetBinder(binder(), Integer.class, marker); + mb1.addBinding().toInstance(1); + mb2.addBinding().toInstance(2); + + // This assures us that the two binders are equivalent, so we expect the instance added to + // each to have been added to one set. + assertEquals(mb1, mb2); + } + }); + TypeLiteral> t = new TypeLiteral>() {}; + Set s1 = injector.getInstance(Key.get(t, Marker.class)); + Set s2 = injector.getInstance(Key.get(t, marker)); + + // This assures us that the two sets are in fact equal. They may not be same set (as in Java + // object identical), but we shouldn't expect that, since probably Guice creates the set each + // time in case the elements are dependent on scope. + assertEquals(s1, s2); + + // This ensures that MultiBinder is internally using the correct set name -- + // making sure that instances of marker annotations have the same set name as + // MarkerAnnotation.class. + Set expected = new HashSet(); + expected.add(1); + expected.add(2); + assertEquals(expected, s1); + } + + // See issue 670 + public void testSetAndMapValueAreDistinct() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override protected void configure() { + Multibinder.newSetBinder(binder(), String.class) + .addBinding().toInstance("A"); + + MapBinder.newMapBinder(binder(), String.class, String.class) + .addBinding("B").toInstance("b"); + + OptionalBinder.newOptionalBinder(binder(), String.class) + .setDefault().toInstance("C"); + OptionalBinder.newOptionalBinder(binder(), String.class) + .setBinding().toInstance("D"); + } + }); + + assertEquals(ImmutableSet.of("A"), injector.getInstance(Key.get(setOfString))); + assertEquals(ImmutableMap.of("B", "b"), injector.getInstance(Key.get(mapOfStringString))); + assertEquals(Optional.of("D"), injector.getInstance(Key.get(optionalOfString))); + } + + // See issue 670 + public void testSetAndMapValueAreDistinctInSpi() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override protected void configure() { + Multibinder.newSetBinder(binder(), String.class) + .addBinding().toInstance("A"); + + MapBinder.newMapBinder(binder(), String.class, String.class) + .addBinding("B").toInstance("b"); + + OptionalBinder.newOptionalBinder(binder(), String.class) + .setDefault().toInstance("C"); + } + }); + Collector collector = new Collector(); + Binding> mapbinding = injector.getBinding(Key.get(mapOfStringString)); + mapbinding.acceptTargetVisitor(collector); + assertNotNull(collector.mapbinding); + + Binding> setbinding = injector.getBinding(Key.get(setOfString)); + setbinding.acceptTargetVisitor(collector); + assertNotNull(collector.setbinding); + + Binding> optionalbinding = injector.getBinding(Key.get(optionalOfString)); + optionalbinding.acceptTargetVisitor(collector); + assertNotNull(collector.optionalbinding); + + // There should only be three instance bindings for string types + // (but because of the OptionalBinder, there's 2 ProviderInstanceBindings also). + // We also know the InstanceBindings will be in the order: A, b, C because that's + // how we bound them, and binding order is preserved. + List> bindings = FluentIterable.from(injector.findBindingsByType(stringType)) + .filter(Predicates.instanceOf(InstanceBinding.class)) + .toList(); + assertEquals(bindings.toString(), 3, bindings.size()); + Binding a = bindings.get(0); + Binding b = bindings.get(1); + Binding c = bindings.get(2); + assertEquals("A", ((InstanceBinding) a).getInstance()); + assertEquals("b", ((InstanceBinding) b).getInstance()); + assertEquals("C", ((InstanceBinding) c).getInstance()); + + // Make sure the correct elements belong to their own sets. + assertFalse(collector.mapbinding.containsElement(a)); + assertTrue(collector.mapbinding.containsElement(b)); + assertFalse(collector.mapbinding.containsElement(c)); + + assertTrue(collector.setbinding.containsElement(a)); + assertFalse(collector.setbinding.containsElement(b)); + assertFalse(collector.setbinding.containsElement(c)); + + assertFalse(collector.optionalbinding.containsElement(a)); + assertFalse(collector.optionalbinding.containsElement(b)); + assertTrue(collector.optionalbinding.containsElement(c)); + } + + public void testMultibinderCanInjectCollectionOfProviders() { + Module module = new AbstractModule() { + @Override protected void configure() { + final Multibinder multibinder = Multibinder.newSetBinder(binder(), String.class); + multibinder.addBinding().toProvider(Providers.of("A")); + multibinder.addBinding().toProvider(Providers.of("B")); + multibinder.addBinding().toInstance("C"); + } + }; + Collection expectedValues = ImmutableList.of("A", "B", "C"); + + Injector injector = Guice.createInjector(module); + + Collection> providers = + injector.getInstance(Key.get(collectionOfProvidersOfStrings)); + assertEquals(expectedValues, collectValues(providers)); + + Collection> javaxProviders = + injector.getInstance(Key.get(collectionOfJavaxProvidersOf(stringType))); + assertEquals(expectedValues, collectValues(javaxProviders)); + } + + public void testMultibinderCanInjectCollectionOfProvidersWithAnnotation() { + final Annotation ann = Names.named("foo"); + Module module = new AbstractModule() { + @Override protected void configure() { + final Multibinder multibinder = + Multibinder.newSetBinder(binder(), String.class, ann); + multibinder.addBinding().toProvider(Providers.of("A")); + multibinder.addBinding().toProvider(Providers.of("B")); + multibinder.addBinding().toInstance("C"); + } + }; + Collection expectedValues = ImmutableList.of("A", "B", "C"); + + Injector injector = Guice.createInjector(module); + + Collection> providers = + injector.getInstance(Key.get(collectionOfProvidersOfStrings, ann)); + Collection values = collectValues(providers); + assertEquals(expectedValues, values); + + Collection> javaxProviders = + injector.getInstance(Key.get(collectionOfJavaxProvidersOf(stringType), ann)); + assertEquals(expectedValues, collectValues(javaxProviders)); + } + + public void testMultibindingProviderDependencies() { + final Annotation setAnn = Names.named("foo"); + Injector injector = Guice.createInjector(new AbstractModule() { + @Override protected void configure() { + Multibinder multibinder = + Multibinder.newSetBinder(binder(), String.class, setAnn); + multibinder.addBinding().toInstance("a"); + multibinder.addBinding().toInstance("b"); + } + }); + HasDependencies providerBinding = + (HasDependencies) injector.getBinding(new Key>>(setAnn) {}); + HasDependencies setBinding = + (HasDependencies) injector.getBinding(new Key>(setAnn) {}); + // sanity check the size + assertEquals(setBinding.getDependencies().toString(), 2, setBinding.getDependencies().size()); + Set> expected = Sets.newHashSet(); + for (Dependency dep : setBinding.getDependencies()) { + Key key = dep.getKey(); + Dependency providerDependency = + Dependency.get(key.ofType(Types.providerOf(key.getTypeLiteral().getType()))); + expected.add(providerDependency); + } + assertEquals(expected, providerBinding.getDependencies()); + } + + private Collection collectValues( + Collection> providers) { + Collection values = Lists.newArrayList(); + for (javax.inject.Provider provider : providers) { + values.add(provider.get()); + } + return values; + } +} diff --git a/src/test/java/com/google/inject/multibindings/OptionalBinderTest.java b/src/test/java/com/google/inject/multibindings/OptionalBinderTest.java new file mode 100644 index 0000000..7d54b1f --- /dev/null +++ b/src/test/java/com/google/inject/multibindings/OptionalBinderTest.java @@ -0,0 +1,1222 @@ +package com.google.inject.multibindings; + +import static com.google.inject.Asserts.assertContains; +import static com.google.inject.multibindings.SpiUtils.assertOptionalVisitor; +import static com.google.inject.multibindings.SpiUtils.instance; +import static com.google.inject.multibindings.SpiUtils.linked; +import static com.google.inject.multibindings.SpiUtils.providerInstance; +import static com.google.inject.multibindings.SpiUtils.providerKey; +import static com.google.inject.name.Names.named; + +import com.google.common.base.Optional; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; +import com.google.common.collect.Sets; +import com.google.inject.AbstractModule; +import com.google.inject.Asserts; +import com.google.inject.Binding; +import com.google.inject.BindingAnnotation; +import com.google.inject.CreationException; +import com.google.inject.Guice; +import com.google.inject.Injector; +import com.google.inject.Key; +import com.google.inject.Module; +import com.google.inject.Provider; +import com.google.inject.Provides; +import com.google.inject.Scopes; +import com.google.inject.TypeLiteral; +import com.google.inject.internal.WeakKeySetUtils; +import com.google.inject.multibindings.OptionalBinder.Actual; +import com.google.inject.multibindings.OptionalBinder.Default; +import com.google.inject.multibindings.SpiUtils.VisitType; +import com.google.inject.name.Named; +import com.google.inject.name.Names; +import com.google.inject.spi.Dependency; +import com.google.inject.spi.Elements; +import com.google.inject.spi.HasDependencies; +import com.google.inject.spi.InstanceBinding; +import com.google.inject.util.Modules; +import com.google.inject.util.Providers; + +import junit.framework.TestCase; + +import java.lang.annotation.Annotation; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.ref.WeakReference; +import java.lang.reflect.Method; +import java.util.List; +import java.util.Map.Entry; +import java.util.Set; + +public class OptionalBinderTest extends TestCase { + + private static final boolean HAS_JAVA_OPTIONAL; + private static final Class JAVA_OPTIONAL_CLASS; + private static final Method JAVA_OPTIONAL_OR_ELSE; + static { + Class optional = null; + Method orElse = null; + try { + optional = Class.forName("java.util.Optional"); + orElse = optional.getDeclaredMethod("orElse", Object.class); + } catch (ClassNotFoundException ignored) { + } catch (NoSuchMethodException ignored) { + } catch (SecurityException ignored) { + } + HAS_JAVA_OPTIONAL = optional != null; + JAVA_OPTIONAL_CLASS = optional; + JAVA_OPTIONAL_OR_ELSE = orElse; + } + + final Key stringKey = Key.get(String.class); + final TypeLiteral> optionalOfString = new TypeLiteral>() {}; + final TypeLiteral javaOptionalOfString = HAS_JAVA_OPTIONAL ? + OptionalBinder.javaOptionalOf(stringKey.getTypeLiteral()) : null; + final TypeLiteral>> optionalOfProviderString = + new TypeLiteral>>() {}; + final TypeLiteral javaOptionalOfProviderString = HAS_JAVA_OPTIONAL ? + OptionalBinder.javaOptionalOfProvider(stringKey.getTypeLiteral()) : null; + final TypeLiteral>> optionalOfJavaxProviderString = + new TypeLiteral>>() {}; + final TypeLiteral javaOptionalOfJavaxProviderString = HAS_JAVA_OPTIONAL ? + OptionalBinder.javaOptionalOfJavaxProvider(stringKey.getTypeLiteral()) : null; + + final Key intKey = Key.get(Integer.class); + final TypeLiteral> optionalOfInteger = new TypeLiteral>() {}; + final TypeLiteral javaOptionalOfInteger = HAS_JAVA_OPTIONAL ? + OptionalBinder.javaOptionalOf(intKey.getTypeLiteral()) : null; + final TypeLiteral>> optionalOfProviderInteger = + new TypeLiteral>>() {}; + final TypeLiteral javaOptionalOfProviderInteger = HAS_JAVA_OPTIONAL ? + OptionalBinder.javaOptionalOfProvider(intKey.getTypeLiteral()) : null; + final TypeLiteral>> optionalOfJavaxProviderInteger = + new TypeLiteral>>() {}; + final TypeLiteral javaOptionalOfJavaxProviderInteger = HAS_JAVA_OPTIONAL ? + OptionalBinder.javaOptionalOfJavaxProvider(intKey.getTypeLiteral()) : null; + + final TypeLiteral> listOfStrings = new TypeLiteral>() {}; + + public void testTypeNotBoundByDefault() { + Module module = new AbstractModule() { + @Override protected void configure() { + OptionalBinder.newOptionalBinder(binder(), String.class); + requireBinding(new Key>() {}); // the above specifies this. + requireBinding(String.class); // but it doesn't specify this. + binder().requireExplicitBindings(); // need to do this, otherwise String will JIT + + if (HAS_JAVA_OPTIONAL) { + requireBinding(Key.get(javaOptionalOfString)); + } + } + }; + + try { + Guice.createInjector(module); + fail(); + } catch (CreationException ce) { + assertContains(ce.getMessage(), + "1) Explicit bindings are required and java.lang.String is not explicitly bound."); + assertEquals(1, ce.getErrorMessages().size()); + } + } + + public void testOptionalIsAbsentByDefault() throws Exception { + Module module = new AbstractModule() { + @Override protected void configure() { + OptionalBinder.newOptionalBinder(binder(), String.class); + } + }; + + Injector injector = Guice.createInjector(module); + Optional optional = injector.getInstance(Key.get(optionalOfString)); + assertFalse(optional.isPresent()); + + Optional> optionalP = injector.getInstance(Key.get(optionalOfProviderString)); + assertFalse(optionalP.isPresent()); + + Optional> optionalJxP = + injector.getInstance(Key.get(optionalOfJavaxProviderString)); + assertFalse(optionalJxP.isPresent()); + + assertOptionalVisitor(stringKey, setOf(module), VisitType.BOTH, 0, null, null, null); + + if (HAS_JAVA_OPTIONAL) { + optional = toOptional(injector.getInstance(Key.get(javaOptionalOfString))); + assertFalse(optional.isPresent()); + + optionalP = toOptional(injector.getInstance(Key.get(javaOptionalOfProviderString))); + assertFalse(optionalP.isPresent()); + + optionalJxP = toOptional(injector.getInstance(Key.get(javaOptionalOfJavaxProviderString))); + assertFalse(optionalJxP.isPresent()); + } + } + + public void testUsesUserBoundValue() throws Exception { + Module module = new AbstractModule() { + @Override protected void configure() { + OptionalBinder.newOptionalBinder(binder(), String.class); + } + @Provides String provideString() { return "foo"; } + }; + + Injector injector = Guice.createInjector(module); + assertEquals("foo", injector.getInstance(String.class)); + + Optional optional = injector.getInstance(Key.get(optionalOfString)); + assertEquals("foo", optional.get()); + + Optional> optionalP = injector.getInstance(Key.get(optionalOfProviderString)); + assertEquals("foo", optionalP.get().get()); + + Optional> optionalJxP = + injector.getInstance(Key.get(optionalOfJavaxProviderString)); + assertEquals("foo", optionalJxP.get().get()); + + assertOptionalVisitor(stringKey, + setOf(module), + VisitType.BOTH, + 0, + null, + null, + providerInstance("foo")); + + if (HAS_JAVA_OPTIONAL) { + optional = toOptional(injector.getInstance(Key.get(javaOptionalOfString))); + assertEquals("foo", optional.get()); + + optionalP = toOptional(injector.getInstance(Key.get(javaOptionalOfProviderString))); + assertEquals("foo", optionalP.get().get()); + + optionalJxP = toOptional(injector.getInstance(Key.get(javaOptionalOfJavaxProviderString))); + assertEquals("foo", optionalJxP.get().get()); + } + } + + public void testSetDefault() throws Exception { + Module module = new AbstractModule() { + @Override protected void configure() { + OptionalBinder.newOptionalBinder(binder(), String.class).setDefault().toInstance("a"); + } + }; + Injector injector = Guice.createInjector(module); + assertEquals("a", injector.getInstance(String.class)); + + Optional optional = injector.getInstance(Key.get(optionalOfString)); + assertTrue(optional.isPresent()); + assertEquals("a", optional.get()); + + Optional> optionalP = injector.getInstance(Key.get(optionalOfProviderString)); + assertTrue(optionalP.isPresent()); + assertEquals("a", optionalP.get().get()); + + Optional> optionalJxP = + injector.getInstance(Key.get(optionalOfJavaxProviderString)); + assertTrue(optionalJxP.isPresent()); + assertEquals("a", optionalJxP.get().get()); + + assertOptionalVisitor(stringKey, setOf(module), VisitType.BOTH, 0, instance("a"), null, null); + + if (HAS_JAVA_OPTIONAL) { + optional = toOptional(injector.getInstance(Key.get(javaOptionalOfString))); + assertTrue(optional.isPresent()); + assertEquals("a", optional.get()); + + optionalP = toOptional(injector.getInstance(Key.get(javaOptionalOfProviderString))); + assertTrue(optionalP.isPresent()); + assertEquals("a", optionalP.get().get()); + + optionalJxP = toOptional(injector.getInstance(Key.get(javaOptionalOfJavaxProviderString))); + assertTrue(optionalJxP.isPresent()); + assertEquals("a", optionalJxP.get().get()); + } + } + + public void testSetBinding() throws Exception { + Module module = new AbstractModule() { + @Override protected void configure() { + OptionalBinder.newOptionalBinder(binder(), String.class).setBinding().toInstance("a"); + } + }; + Injector injector = Guice.createInjector(module); + assertEquals("a", injector.getInstance(String.class)); + + Optional optional = injector.getInstance(Key.get(optionalOfString)); + assertTrue(optional.isPresent()); + assertEquals("a", optional.get()); + + Optional> optionalP = injector.getInstance(Key.get(optionalOfProviderString)); + assertTrue(optionalP.isPresent()); + assertEquals("a", optionalP.get().get()); + + Optional> optionalJxP = + injector.getInstance(Key.get(optionalOfJavaxProviderString)); + assertTrue(optionalJxP.isPresent()); + assertEquals("a", optionalJxP.get().get()); + + assertOptionalVisitor(stringKey, setOf(module), VisitType.BOTH, 0, null, instance("a"), null); + + if (HAS_JAVA_OPTIONAL) { + optional = toOptional(injector.getInstance(Key.get(javaOptionalOfString))); + assertTrue(optional.isPresent()); + assertEquals("a", optional.get()); + + optionalP = toOptional(injector.getInstance(Key.get(javaOptionalOfProviderString))); + assertTrue(optionalP.isPresent()); + assertEquals("a", optionalP.get().get()); + + optionalJxP = toOptional(injector.getInstance(Key.get(javaOptionalOfJavaxProviderString))); + assertTrue(optionalJxP.isPresent()); + assertEquals("a", optionalJxP.get().get()); + } + } + + public void testSetBindingOverridesDefault() throws Exception { + Module module = new AbstractModule() { + @Override protected void configure() { + OptionalBinder optionalBinder = + OptionalBinder.newOptionalBinder(binder(), String.class); + optionalBinder.setDefault().toInstance("a"); + optionalBinder.setBinding().toInstance("b"); + } + }; + Injector injector = Guice.createInjector(module); + assertEquals("b", injector.getInstance(String.class)); + + Optional optional = injector.getInstance(Key.get(optionalOfString)); + assertTrue(optional.isPresent()); + assertEquals("b", optional.get()); + + Optional> optionalP = injector.getInstance(Key.get(optionalOfProviderString)); + assertTrue(optionalP.isPresent()); + assertEquals("b", optionalP.get().get()); + + Optional> optionalJxP = + injector.getInstance(Key.get(optionalOfJavaxProviderString)); + assertTrue(optionalJxP.isPresent()); + assertEquals("b", optionalJxP.get().get()); + + assertOptionalVisitor(stringKey, + setOf(module), + VisitType.BOTH, + 0, + instance("a"), + instance("b"), + null); + + if (HAS_JAVA_OPTIONAL) { + optional = toOptional(injector.getInstance(Key.get(javaOptionalOfString))); + assertTrue(optional.isPresent()); + assertEquals("b", optional.get()); + + optionalP = toOptional(injector.getInstance(Key.get(javaOptionalOfProviderString))); + assertTrue(optionalP.isPresent()); + assertEquals("b", optionalP.get().get()); + + optionalJxP = toOptional(injector.getInstance(Key.get(javaOptionalOfJavaxProviderString))); + assertTrue(optionalJxP.isPresent()); + assertEquals("b", optionalJxP.get().get()); + } + } + + public void testSpreadAcrossModules() throws Exception { + Module module1 = new AbstractModule() { + @Override protected void configure() { + OptionalBinder.newOptionalBinder(binder(), String.class); + } + }; + Module module2 = new AbstractModule() { + @Override protected void configure() { + OptionalBinder.newOptionalBinder(binder(), String.class).setDefault().toInstance("a"); + } + }; + Module module3 = new AbstractModule() { + @Override protected void configure() { + OptionalBinder.newOptionalBinder(binder(), String.class).setBinding().toInstance("b"); + } + }; + + Injector injector = Guice.createInjector(module1, module2, module3); + assertEquals("b", injector.getInstance(String.class)); + + Optional optional = injector.getInstance(Key.get(optionalOfString)); + assertTrue(optional.isPresent()); + assertEquals("b", optional.get()); + + Optional> optionalP = injector.getInstance(Key.get(optionalOfProviderString)); + assertTrue(optionalP.isPresent()); + assertEquals("b", optionalP.get().get()); + + Optional> optionalJxP = + injector.getInstance(Key.get(optionalOfJavaxProviderString)); + assertTrue(optionalJxP.isPresent()); + assertEquals("b", optionalJxP.get().get()); + + assertOptionalVisitor(stringKey, + setOf(module1, module2, module3), + VisitType.BOTH, + 0, + instance("a"), + instance("b"), + null); + + if (HAS_JAVA_OPTIONAL) { + optional = toOptional(injector.getInstance(Key.get(javaOptionalOfString))); + assertTrue(optional.isPresent()); + assertEquals("b", optional.get()); + + optionalP = toOptional(injector.getInstance(Key.get(javaOptionalOfProviderString))); + assertTrue(optionalP.isPresent()); + assertEquals("b", optionalP.get().get()); + + optionalJxP = toOptional(injector.getInstance(Key.get(javaOptionalOfJavaxProviderString))); + assertTrue(optionalJxP.isPresent()); + assertEquals("b", optionalJxP.get().get()); + } + } + + public void testExactSameBindingCollapses_defaults() throws Exception { + Module module = new AbstractModule() { + @Override protected void configure() { + OptionalBinder.newOptionalBinder(binder(), String.class).setDefault() + .toInstance(new String("a")); // using new String to ensure .equals is checked. + OptionalBinder.newOptionalBinder(binder(), String.class).setDefault() + .toInstance(new String("a")); + } + }; + Injector injector = Guice.createInjector(module); + assertEquals("a", injector.getInstance(String.class)); + + Optional optional = injector.getInstance(Key.get(optionalOfString)); + assertTrue(optional.isPresent()); + assertEquals("a", optional.get()); + + Optional> optionalP = injector.getInstance(Key.get(optionalOfProviderString)); + assertTrue(optionalP.isPresent()); + assertEquals("a", optionalP.get().get()); + + Optional> optionalJxP = + injector.getInstance(Key.get(optionalOfJavaxProviderString)); + assertTrue(optionalJxP.isPresent()); + assertEquals("a", optionalJxP.get().get()); + + assertOptionalVisitor(stringKey, setOf(module), VisitType.BOTH, 0, instance("a"), null, null); + + if (HAS_JAVA_OPTIONAL) { + optional = toOptional(injector.getInstance(Key.get(javaOptionalOfString))); + assertTrue(optional.isPresent()); + assertEquals("a", optional.get()); + + optionalP = toOptional(injector.getInstance(Key.get(javaOptionalOfProviderString))); + assertTrue(optionalP.isPresent()); + assertEquals("a", optionalP.get().get()); + + optionalJxP = toOptional(injector.getInstance(Key.get(javaOptionalOfJavaxProviderString))); + assertTrue(optionalJxP.isPresent()); + assertEquals("a", optionalJxP.get().get()); + } + } + + public void testExactSameBindingCollapses_actual() throws Exception { + Module module = new AbstractModule() { + @Override protected void configure() { + OptionalBinder.newOptionalBinder(binder(), String.class).setBinding() + .toInstance(new String("a")); // using new String to ensure .equals is checked. + OptionalBinder.newOptionalBinder(binder(), String.class).setBinding() + .toInstance(new String("a")); + } + }; + Injector injector = Guice.createInjector(module); + assertEquals("a", injector.getInstance(String.class)); + + Optional optional = injector.getInstance(Key.get(optionalOfString)); + assertTrue(optional.isPresent()); + assertEquals("a", optional.get()); + + Optional> optionalP = injector.getInstance(Key.get(optionalOfProviderString)); + assertTrue(optionalP.isPresent()); + assertEquals("a", optionalP.get().get()); + + Optional> optionalJxP = + injector.getInstance(Key.get(optionalOfJavaxProviderString)); + assertTrue(optionalJxP.isPresent()); + assertEquals("a", optionalJxP.get().get()); + + assertOptionalVisitor(stringKey, setOf(module), VisitType.BOTH, 0, null, instance("a"), null); + + if (HAS_JAVA_OPTIONAL) { + optional = toOptional(injector.getInstance(Key.get(javaOptionalOfString))); + assertTrue(optional.isPresent()); + assertEquals("a", optional.get()); + + optionalP = toOptional(injector.getInstance(Key.get(javaOptionalOfProviderString))); + assertTrue(optionalP.isPresent()); + assertEquals("a", optionalP.get().get()); + + optionalJxP = toOptional(injector.getInstance(Key.get(javaOptionalOfJavaxProviderString))); + assertTrue(optionalJxP.isPresent()); + assertEquals("a", optionalJxP.get().get()); + } + } + + public void testDifferentBindingsFail_defaults() { + Module module = new AbstractModule() { + @Override protected void configure() { + OptionalBinder.newOptionalBinder(binder(), String.class).setDefault().toInstance("a"); + OptionalBinder.newOptionalBinder(binder(), String.class).setDefault().toInstance("b"); + } + }; + try { + Guice.createInjector(module); + fail(); + } catch (CreationException ce) { + assertEquals(ce.getMessage(), 1, ce.getErrorMessages().size()); + assertContains(ce.getMessage(), + "1) A binding to java.lang.String annotated with @" + + Default.class.getName() + " was already configured at " + + module.getClass().getName() + ".configure(", + "at " + module.getClass().getName() + ".configure("); + } + } + + public void testDifferentBindingsFail_actual() { + Module module = new AbstractModule() { + @Override protected void configure() { + OptionalBinder.newOptionalBinder(binder(), String.class).setBinding().toInstance("a"); + OptionalBinder.newOptionalBinder(binder(), String.class).setBinding().toInstance("b"); + } + }; + try { + Guice.createInjector(module); + fail(); + } catch (CreationException ce) { + assertEquals(ce.getMessage(), 1, ce.getErrorMessages().size()); + assertContains(ce.getMessage(), + "1) A binding to java.lang.String annotated with @" + + Actual.class.getName() + " was already configured at " + + module.getClass().getName() + ".configure(", + "at " + module.getClass().getName() + ".configure("); + } + } + + public void testDifferentBindingsFail_both() { + Module module = new AbstractModule() { + @Override protected void configure() { + OptionalBinder.newOptionalBinder(binder(), String.class).setDefault().toInstance("a"); + OptionalBinder.newOptionalBinder(binder(), String.class).setDefault().toInstance("b"); + OptionalBinder.newOptionalBinder(binder(), String.class).setBinding().toInstance("b"); + OptionalBinder.newOptionalBinder(binder(), String.class).setBinding().toInstance("c"); + } + }; + try { + Guice.createInjector(module); + fail(); + } catch (CreationException ce) { + assertEquals(ce.getMessage(), 2, ce.getErrorMessages().size()); + assertContains(ce.getMessage(), + "1) A binding to java.lang.String annotated with @" + + Default.class.getName() + " was already configured at " + + module.getClass().getName() + ".configure(", + "at " + module.getClass().getName() + ".configure(", + "2) A binding to java.lang.String annotated with @" + + Actual.class.getName() + " was already configured at " + + module.getClass().getName() + ".configure(", + "at " + module.getClass().getName() + ".configure("); + } + } + + public void testQualifiedAggregatesTogether() throws Exception { + Module module1 = new AbstractModule() { + @Override + protected void configure() { + OptionalBinder.newOptionalBinder(binder(), Key.get(String.class, Names.named("foo"))); + } + }; + Module module2 = new AbstractModule() { + @Override + protected void configure() { + OptionalBinder.newOptionalBinder(binder(), Key.get(String.class, Names.named("foo"))) + .setDefault().toInstance("a"); + } + }; + Module module3 = new AbstractModule() { + @Override + protected void configure() { + OptionalBinder.newOptionalBinder(binder(), Key.get(String.class, Names.named("foo"))) + .setBinding().toInstance("b"); + } + }; + + Injector injector = Guice.createInjector(module1, module2, module3); + assertEquals("b", injector.getInstance(Key.get(String.class, Names.named("foo")))); + + Optional optional = injector.getInstance(Key.get(optionalOfString, Names.named("foo"))); + assertTrue(optional.isPresent()); + assertEquals("b", optional.get()); + + Optional> optionalP = + injector.getInstance(Key.get(optionalOfProviderString, Names.named("foo"))); + assertTrue(optionalP.isPresent()); + assertEquals("b", optionalP.get().get()); + + Optional> optionalJxP = + injector.getInstance(Key.get(optionalOfJavaxProviderString, Names.named("foo"))); + assertTrue(optionalJxP.isPresent()); + assertEquals("b", optionalJxP.get().get()); + + assertOptionalVisitor(Key.get(String.class, Names.named("foo")), + setOf(module1, module2, module3), + VisitType.BOTH, + 0, + instance("a"), + instance("b"), + null); + + if (HAS_JAVA_OPTIONAL) { + optional = toOptional(injector.getInstance(Key.get(javaOptionalOfString, Names.named("foo")))); + assertTrue(optional.isPresent()); + assertEquals("b", optional.get()); + + optionalP = toOptional(injector.getInstance + (Key.get(javaOptionalOfProviderString, Names.named("foo")))); + assertTrue(optionalP.isPresent()); + assertEquals("b", optionalP.get().get()); + + optionalJxP = toOptional(injector.getInstance( + Key.get(javaOptionalOfJavaxProviderString, Names.named("foo")))); + assertTrue(optionalJxP.isPresent()); + assertEquals("b", optionalJxP.get().get()); + } + } + + public void testMultipleDifferentOptionals() { + final Key bKey = Key.get(String.class, named("b")); + final Key cKey = Key.get(String.class, named("c")); + Module module = new AbstractModule() { + @Override protected void configure() { + OptionalBinder.newOptionalBinder(binder(), String.class).setDefault().toInstance("a"); + OptionalBinder.newOptionalBinder(binder(), Integer.class).setDefault().toInstance(1); + + OptionalBinder.newOptionalBinder(binder(), bKey).setDefault().toInstance("b"); + OptionalBinder.newOptionalBinder(binder(), cKey).setDefault().toInstance("c"); + } + }; + Injector injector = Guice.createInjector(module); + assertEquals("a", injector.getInstance(String.class)); + assertEquals(1, injector.getInstance(Integer.class).intValue()); + assertEquals("b", injector.getInstance(bKey)); + assertEquals("c", injector.getInstance(cKey)); + + assertOptionalVisitor(stringKey, setOf(module), VisitType.BOTH, 3, instance("a"), null, null); + assertOptionalVisitor(intKey, setOf(module), VisitType.BOTH, 3, instance(1), null, null); + assertOptionalVisitor(bKey, setOf(module), VisitType.BOTH, 3, instance("b"), null, null); + assertOptionalVisitor(cKey, setOf(module), VisitType.BOTH, 3, instance("c"), null, null); + } + + public void testOptionalIsAppropriatelyLazy() throws Exception { + Module module = new AbstractModule() { + int nextValue = 1; + @Override protected void configure() { + OptionalBinder.newOptionalBinder(binder(), Integer.class) + .setDefault().to(Key.get(Integer.class, Names.named("foo"))); + } + @Provides @Named("foo") int provideInt() { + return nextValue++; + } + }; + Injector injector = Guice.createInjector(module); + + Optional> optionalP = + injector.getInstance(Key.get(optionalOfProviderInteger)); + Optional> optionalJxP = + injector.getInstance(Key.get(optionalOfJavaxProviderInteger)); + + assertEquals(1, injector.getInstance(Integer.class).intValue()); + assertEquals(2, injector.getInstance(Integer.class).intValue()); + + // Calling .get() on an Optional multiple times will keep giving the same thing + Optional optional = injector.getInstance(Key.get(optionalOfInteger)); + assertEquals(3, optional.get().intValue()); + assertEquals(3, optional.get().intValue()); + // But getting another Optional will give a new one. + assertEquals(4, injector.getInstance(Key.get(optionalOfInteger)).get().intValue()); + + // And the Optional will return a provider that gives a new value each time. + assertEquals(5, optionalP.get().get().intValue()); + assertEquals(6, optionalP.get().get().intValue()); + + assertEquals(7, optionalJxP.get().get().intValue()); + assertEquals(8, optionalJxP.get().get().intValue()); + + // and same rules with java.util.Optional + if (HAS_JAVA_OPTIONAL) { + optional = toOptional(injector.getInstance(Key.get(javaOptionalOfInteger))); + assertEquals(9, optional.get().intValue()); + assertEquals(9, optional.get().intValue()); + optional = toOptional(injector.getInstance(Key.get(javaOptionalOfInteger))); + assertEquals(10, optional.get().intValue()); + + optionalP = toOptional(injector.getInstance(Key.get(javaOptionalOfProviderInteger))); + assertEquals(11, optionalP.get().get().intValue()); + assertEquals(12, optionalP.get().get().intValue()); + + optionalJxP = toOptional(injector.getInstance(Key.get(javaOptionalOfJavaxProviderInteger))); + assertEquals(13, optionalJxP.get().get().intValue()); + assertEquals(14, optionalJxP.get().get().intValue()); + } + } + + public void testLinkedToNullProvidersMakeAbsentValuesAndPresentProviders_default() + throws Exception { + Module module = new AbstractModule() { + @Override protected void configure() { + OptionalBinder.newOptionalBinder(binder(), String.class) + .setDefault().toProvider(Providers.of(null)); + } + }; + Injector injector = Guice.createInjector(module); + assertNull(injector.getInstance(String.class)); + + Optional optional = injector.getInstance(Key.get(optionalOfString)); + assertFalse(optional.isPresent()); + + Optional> optionalP = injector.getInstance(Key.get(optionalOfProviderString)); + assertTrue(optionalP.isPresent()); + assertNull(optionalP.get().get()); + + Optional> optionalJxP = + injector.getInstance(Key.get(optionalOfJavaxProviderString)); + assertTrue(optionalJxP.isPresent()); + assertNull(optionalJxP.get().get()); + + assertOptionalVisitor(stringKey, + setOf(module), + VisitType.BOTH, + 0, + SpiUtils.providerInstance(null), + null, + null); + + if (HAS_JAVA_OPTIONAL) { + optional = toOptional(injector.getInstance(Key.get(javaOptionalOfString))); + assertFalse(optional.isPresent()); + + optionalP = toOptional(injector.getInstance(Key.get(javaOptionalOfProviderString))); + assertTrue(optionalP.isPresent()); + assertNull(optionalP.get().get()); + + optionalJxP = toOptional(injector.getInstance(Key.get(javaOptionalOfJavaxProviderString))); + assertTrue(optionalJxP.isPresent()); + assertNull(optionalJxP.get().get()); + } + } + + public void testLinkedToNullProvidersMakeAbsentValuesAndPresentProviders_actual() + throws Exception { + Module module = new AbstractModule() { + @Override protected void configure() { + OptionalBinder.newOptionalBinder(binder(), String.class) + .setBinding().toProvider(Providers.of(null)); + } + }; + Injector injector = Guice.createInjector(module); + assertNull(injector.getInstance(String.class)); + + Optional optional = injector.getInstance(Key.get(optionalOfString)); + assertFalse(optional.isPresent()); + + Optional> optionalP = injector.getInstance(Key.get(optionalOfProviderString)); + assertTrue(optionalP.isPresent()); + assertNull(optionalP.get().get()); + + Optional> optionalJxP = + injector.getInstance(Key.get(optionalOfJavaxProviderString)); + assertTrue(optionalJxP.isPresent()); + assertNull(optionalJxP.get().get()); + + assertOptionalVisitor(stringKey, + setOf(module), + VisitType.BOTH, + 0, + null, + SpiUtils.providerInstance(null), + null); + + if (HAS_JAVA_OPTIONAL) { + optional = toOptional(injector.getInstance(Key.get(javaOptionalOfString))); + assertFalse(optional.isPresent()); + + optionalP = toOptional(injector.getInstance(Key.get(javaOptionalOfProviderString))); + assertTrue(optionalP.isPresent()); + assertNull(optionalP.get().get()); + + optionalJxP = toOptional(injector.getInstance(Key.get(javaOptionalOfJavaxProviderString))); + assertTrue(optionalJxP.isPresent()); + assertNull(optionalJxP.get().get()); + } + } + + // TODO(sameb): Maybe change this? + public void testLinkedToNullActualDoesntFallbackToDefault() throws Exception { + Module module = new AbstractModule() { + @Override protected void configure() { + OptionalBinder.newOptionalBinder(binder(), String.class).setDefault().toInstance("a"); + OptionalBinder.newOptionalBinder(binder(), String.class) + .setBinding().toProvider(Providers.of(null)); + } + }; + Injector injector = Guice.createInjector(module); + assertNull(injector.getInstance(String.class)); + + Optional optional = injector.getInstance(Key.get(optionalOfString)); + assertFalse(optional.isPresent()); + + Optional> optionalP = injector.getInstance(Key.get(optionalOfProviderString)); + assertTrue(optionalP.isPresent()); + assertNull(optionalP.get().get()); + + Optional> optionalJxP = + injector.getInstance(Key.get(optionalOfJavaxProviderString)); + assertTrue(optionalJxP.isPresent()); + assertNull(optionalP.get().get()); + + assertOptionalVisitor(stringKey, + setOf(module), + VisitType.BOTH, + 0, + instance("a"), + SpiUtils.providerInstance(null), + null); + + if (HAS_JAVA_OPTIONAL) { + optional = toOptional(injector.getInstance(Key.get(javaOptionalOfString))); + assertFalse(optional.isPresent()); + + optionalP = toOptional(injector.getInstance(Key.get(javaOptionalOfProviderString))); + assertTrue(optionalP.isPresent()); + assertNull(optionalP.get().get()); + + optionalJxP = toOptional(injector.getInstance(Key.get(javaOptionalOfJavaxProviderString))); + assertTrue(optionalJxP.isPresent()); + assertNull(optionalJxP.get().get()); + } + } + + public void testSourceLinesInException() { + try { + Guice.createInjector(new AbstractModule() { + @Override protected void configure() { + OptionalBinder.newOptionalBinder(binder(), Integer.class).setDefault(); + } + }); + fail(); + } catch (CreationException expected) { + assertContains(expected.getMessage(), "No implementation for java.lang.Integer", + "at " + getClass().getName()); + } + } + + public void testDependencies_both() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override protected void configure() { + OptionalBinder optionalbinder = + OptionalBinder.newOptionalBinder(binder(), String.class); + optionalbinder.setDefault().toInstance("A"); + optionalbinder.setBinding().to(Key.get(String.class, Names.named("b"))); + bindConstant().annotatedWith(Names.named("b")).to("B"); + } + }); + + Binding binding = injector.getBinding(Key.get(String.class)); + HasDependencies withDependencies = (HasDependencies) binding; + Set elements = Sets.newHashSet(); + elements.addAll(recurseForDependencies(injector, withDependencies)); + assertEquals(ImmutableSet.of("B"), elements); + } + + public void testDependencies_actual() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override protected void configure() { + OptionalBinder optionalbinder = + OptionalBinder.newOptionalBinder(binder(), String.class); + optionalbinder.setBinding().to(Key.get(String.class, Names.named("b"))); + bindConstant().annotatedWith(Names.named("b")).to("B"); + } + }); + + Binding binding = injector.getBinding(Key.get(String.class)); + HasDependencies withDependencies = (HasDependencies) binding; + Set elements = Sets.newHashSet(); + elements.addAll(recurseForDependencies(injector, withDependencies)); + assertEquals(ImmutableSet.of("B"), elements); + } + + public void testDependencies_default() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override protected void configure() { + OptionalBinder optionalbinder = + OptionalBinder.newOptionalBinder(binder(), String.class); + optionalbinder.setDefault().toInstance("A"); + } + }); + + Binding binding = injector.getBinding(Key.get(String.class)); + HasDependencies withDependencies = (HasDependencies) binding; + Set elements = Sets.newHashSet(); + elements.addAll(recurseForDependencies(injector, withDependencies)); + assertEquals(ImmutableSet.of("A"), elements); + } + + @SuppressWarnings("rawtypes") + private Set recurseForDependencies(Injector injector, HasDependencies hasDependencies) { + Set elements = Sets.newHashSet(); + for (Dependency dependency : hasDependencies.getDependencies()) { + Binding binding = injector.getBinding(dependency.getKey()); + HasDependencies deps = (HasDependencies) binding; + if (binding instanceof InstanceBinding) { + elements.add((String) ((InstanceBinding) binding).getInstance()); + } else { + elements.addAll(recurseForDependencies(injector, deps)); + } + } + return elements; + } + + /** + * Doubly-installed modules should not conflict, even when one is overridden. + */ + public void testModuleOverrideRepeatedInstalls_toInstance() { + Module m = new AbstractModule() { + @Override protected void configure() { + OptionalBinder b = OptionalBinder.newOptionalBinder(binder(), String.class); + b.setDefault().toInstance("A"); + b.setBinding().toInstance("B"); + } + }; + + assertEquals("B", Guice.createInjector(m, m).getInstance(Key.get(String.class))); + + Injector injector = Guice.createInjector(m, Modules.override(m).with(m)); + assertEquals("B", injector.getInstance(Key.get(String.class))); + + assertOptionalVisitor(stringKey, + setOf(m, Modules.override(m).with(m)), + VisitType.BOTH, + 0, + instance("A"), + instance("B"), + null); + } + + public void testModuleOverrideRepeatedInstalls_toKey() { + final Key aKey = Key.get(String.class, Names.named("A_string")); + final Key bKey = Key.get(String.class, Names.named("B_string")); + Module m = new AbstractModule() { + @Override protected void configure() { + bind(aKey).toInstance("A"); + bind(bKey).toInstance("B"); + + OptionalBinder b = OptionalBinder.newOptionalBinder(binder(), String.class); + b.setDefault().to(aKey); + b.setBinding().to(bKey); + } + }; + + assertEquals("B", Guice.createInjector(m, m).getInstance(Key.get(String.class))); + + Injector injector = Guice.createInjector(m, Modules.override(m).with(m)); + assertEquals("B", injector.getInstance(Key.get(String.class))); + + assertOptionalVisitor(stringKey, + setOf(m, Modules.override(m).with(m)), + VisitType.BOTH, + 0, + linked(aKey), + linked(bKey), + null); + } + + public void testModuleOverrideRepeatedInstalls_toProviderInstance() { + // Providers#of() does not redefine equals/hashCode, so use the same one both times. + final Provider aProvider = Providers.of("A"); + final Provider bProvider = Providers.of("B"); + Module m = new AbstractModule() { + @Override protected void configure() { + OptionalBinder b = OptionalBinder.newOptionalBinder(binder(), String.class); + b.setDefault().toProvider(aProvider); + b.setBinding().toProvider(bProvider); + } + }; + + assertEquals("B", Guice.createInjector(m, m).getInstance(Key.get(String.class))); + + Injector injector = Guice.createInjector(m, Modules.override(m).with(m)); + assertEquals("B", injector.getInstance(Key.get(String.class))); + + assertOptionalVisitor(stringKey, + setOf(m, Modules.override(m).with(m)), + VisitType.BOTH, + 0, + providerInstance("A"), + providerInstance("B"), + null); + } + + private static class AStringProvider implements Provider { + public String get() { + return "A"; + } + } + + private static class BStringProvider implements Provider { + public String get() { + return "B"; + } + } + + public void testModuleOverrideRepeatedInstalls_toProviderKey() { + Module m = new AbstractModule() { + @Override protected void configure() { + OptionalBinder b = OptionalBinder.newOptionalBinder(binder(), String.class); + b.setDefault().toProvider(Key.get(AStringProvider.class)); + b.setBinding().toProvider(Key.get(BStringProvider.class)); + } + }; + + assertEquals("B", Guice.createInjector(m, m).getInstance(Key.get(String.class))); + + Injector injector = Guice.createInjector(m, Modules.override(m).with(m)); + assertEquals("B", injector.getInstance(Key.get(String.class))); + + assertOptionalVisitor(stringKey, + setOf(m, Modules.override(m).with(m)), + VisitType.BOTH, + 0, + providerKey(Key.get(AStringProvider.class)), + providerKey(Key.get(BStringProvider.class)), + null); + } + + private static class StringGrabber { + private final String string; + + @SuppressWarnings("unused") // Found by reflection + public StringGrabber(@Named("A_string") String string) { + this.string = string; + } + + @SuppressWarnings("unused") // Found by reflection + public StringGrabber(@Named("B_string") String string, int unused) { + this.string = string; + } + + @Override + public int hashCode() { + return string.hashCode(); + } + + @Override + public boolean equals(Object obj) { + return (obj instanceof StringGrabber) && ((StringGrabber) obj).string.equals(string); + } + + @Override + public String toString() { + return "StringGrabber(" + string + ")"; + } + } + + public void testModuleOverrideRepeatedInstalls_toConstructor() { + Module m = new AbstractModule() { + @Override protected void configure() { + Key aKey = Key.get(String.class, Names.named("A_string")); + Key bKey = Key.get(String.class, Names.named("B_string")); + bind(aKey).toInstance("A"); + bind(bKey).toInstance("B"); + bind(Integer.class).toInstance(0); // used to disambiguate constructors + + + OptionalBinder b = + OptionalBinder.newOptionalBinder(binder(), StringGrabber.class); + try { + b.setDefault().toConstructor( + StringGrabber.class.getConstructor(String.class)); + b.setBinding().toConstructor( + StringGrabber.class.getConstructor(String.class, int.class)); + } catch (NoSuchMethodException e) { + fail("No such method: " + e.getMessage()); + } + } + }; + + assertEquals("B", Guice.createInjector(m, m).getInstance(Key.get(StringGrabber.class)).string); + + Injector injector = Guice.createInjector(m, Modules.override(m).with(m)); + assertEquals("B", injector.getInstance(Key.get(StringGrabber.class)).string); + } + + /** + * Unscoped bindings should not conflict, whether they were bound with no explicit scope, or + * explicitly bound in {@link Scopes#NO_SCOPE}. + */ + public void testDuplicateUnscopedBindings() { + Module m = new AbstractModule() { + @Override protected void configure() { + OptionalBinder b = OptionalBinder.newOptionalBinder(binder(), Integer.class); + b.setDefault().to(Key.get(Integer.class, named("foo"))); + b.setDefault().to(Key.get(Integer.class, named("foo"))).in(Scopes.NO_SCOPE); + b.setBinding().to(Key.get(Integer.class, named("foo"))); + b.setBinding().to(Key.get(Integer.class, named("foo"))).in(Scopes.NO_SCOPE); + } + @Provides @Named("foo") int provideInt() { return 5; } + }; + assertEquals(5, Guice.createInjector(m).getInstance(Integer.class).intValue()); + } + + /** + * Ensure key hash codes are fixed at injection time, not binding time. + */ + public void testKeyHashCodesFixedAtInjectionTime() { + Module m = new AbstractModule() { + @Override protected void configure() { + OptionalBinder> b = OptionalBinder.newOptionalBinder(binder(), listOfStrings); + List list = Lists.newArrayList(); + b.setDefault().toInstance(list); + b.setBinding().toInstance(list); + list.add("A"); + list.add("B"); + } + }; + + Injector injector = Guice.createInjector(m); + for (Entry, Binding> entry : injector.getAllBindings().entrySet()) { + Key bindingKey = entry.getKey(); + Key clonedKey; + if (bindingKey.getAnnotation() != null) { + clonedKey = Key.get(bindingKey.getTypeLiteral(), bindingKey.getAnnotation()); + } else if (bindingKey.getAnnotationType() != null) { + clonedKey = Key.get(bindingKey.getTypeLiteral(), bindingKey.getAnnotationType()); + } else { + clonedKey = Key.get(bindingKey.getTypeLiteral()); + } + assertEquals(bindingKey, clonedKey); + assertEquals("Incorrect hashcode for " + bindingKey + " -> " + entry.getValue(), + bindingKey.hashCode(), clonedKey.hashCode()); + } + } + + /** + * Ensure bindings do not rehash their keys once returned from {@link Elements#getElements}. + */ + public void testBindingKeysFixedOnReturnFromGetElements() { + final List list = Lists.newArrayList(); + Module m = new AbstractModule() { + @Override protected void configure() { + OptionalBinder> b = OptionalBinder.newOptionalBinder(binder(), listOfStrings); + b.setDefault().toInstance(list); + list.add("A"); + list.add("B"); + } + }; + + InstanceBinding binding = Iterables.getOnlyElement( + Iterables.filter(Elements.getElements(m), InstanceBinding.class)); + Key keyBefore = binding.getKey(); + assertEquals(listOfStrings, keyBefore.getTypeLiteral()); + + list.add("C"); + Key keyAfter = binding.getKey(); + assertSame(keyBefore, keyAfter); + } + + @BindingAnnotation + @Retention(RetentionPolicy.RUNTIME) + @Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD}) + private static @interface Marker {} + + @Marker + public void testMatchingMarkerAnnotations() throws Exception { + Method m = OptionalBinderTest.class.getDeclaredMethod("testMatchingMarkerAnnotations"); + assertNotNull(m); + final Annotation marker = m.getAnnotation(Marker.class); + Injector injector = Guice.createInjector(new AbstractModule() { + @Override public void configure() { + OptionalBinder mb1 = + OptionalBinder.newOptionalBinder(binder(), Key.get(Integer.class, Marker.class)); + OptionalBinder mb2 = + OptionalBinder.newOptionalBinder(binder(), Key.get(Integer.class, marker)); + mb1.setDefault().toInstance(1); + mb2.setBinding().toInstance(2); + + // This assures us that the two binders are equivalent, so we expect the instance added to + // each to have been added to one set. + assertEquals(mb1, mb2); + } + }); + Integer i1 = injector.getInstance(Key.get(Integer.class, Marker.class)); + Integer i2 = injector.getInstance(Key.get(Integer.class, marker)); + + // These must be identical, because the marker annotations collapsed to the same thing. + assertSame(i1, i2); + assertEquals(2, i2.intValue()); + } + + // Tests for com.google.inject.internal.WeakKeySet not leaking memory. + public void testWeakKeySet_integration() { + Injector parentInjector = Guice.createInjector(new AbstractModule() { + @Override protected void configure() { + bind(String.class).toInstance("hi"); + } + }); + WeakKeySetUtils.assertNotBlacklisted(parentInjector, Key.get(Integer.class)); + + Injector childInjector = parentInjector.createChildInjector(new AbstractModule() { + @Override protected void configure() { + OptionalBinder.newOptionalBinder(binder(), Integer.class).setDefault().toInstance(4); + } + }); + WeakReference weakRef = new WeakReference(childInjector); + WeakKeySetUtils.assertBlacklisted(parentInjector, Key.get(Integer.class)); + + // Clear the ref, GC, and ensure that we are no longer blacklisting. + childInjector = null; + + Asserts.awaitClear(weakRef); + WeakKeySetUtils.assertNotBlacklisted(parentInjector, Key.get(Integer.class)); + } + + public void testCompareEqualsAgainstOtherAnnotation() { + Actual impl1 = new OptionalBinder.ActualImpl("foo"); + Actual other1 = Dummy.class.getAnnotation(Actual.class); + assertEquals(impl1, other1); + + Default impl2 = new OptionalBinder.DefaultImpl("foo"); + Default other2 = Dummy.class.getAnnotation(Default.class); + assertEquals(impl2, other2); + + assertFalse(impl1.equals(impl2)); + assertFalse(impl1.equals(other2)); + assertFalse(impl2.equals(other1)); + assertFalse(other1.equals(other2)); + } + + @Actual("foo") + @Default("foo") + static class Dummy {} + + @SuppressWarnings("unchecked") + private Set setOf(V... elements) { + return ImmutableSet.copyOf(elements); + } + + @SuppressWarnings("unchecked") + private Optional toOptional(Object object) throws Exception { + assertTrue("not a java.util.Optional: " + object.getClass(), + JAVA_OPTIONAL_CLASS.isInstance(object)); + return Optional.fromNullable((T) JAVA_OPTIONAL_OR_ELSE.invoke(object, (Void) null)); + } +} diff --git a/src/test/java/com/google/inject/multibindings/ProvidesIntoTest.java b/src/test/java/com/google/inject/multibindings/ProvidesIntoTest.java new file mode 100644 index 0000000..bb1cca7 --- /dev/null +++ b/src/test/java/com/google/inject/multibindings/ProvidesIntoTest.java @@ -0,0 +1,352 @@ +package com.google.inject.multibindings; + +import static com.google.inject.Asserts.assertContains; +import static com.google.inject.name.Names.named; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import com.google.common.base.Optional; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.inject.AbstractModule; +import com.google.inject.CreationException; +import com.google.inject.Guice; +import com.google.inject.Injector; +import com.google.inject.Key; +import com.google.inject.Module; +import com.google.inject.multibindings.ProvidesIntoOptional.Type; +import com.google.inject.name.Named; + +import junit.framework.TestCase; + +import java.lang.annotation.Retention; +import java.lang.reflect.Field; +import java.util.Map; +import java.util.Set; + +public class ProvidesIntoTest extends TestCase { + + public void testAnnotation() throws Exception { + Injector injector = Guice.createInjector(MultibindingsScanner.asModule(), new AbstractModule() { + @Override protected void configure() {} + + @ProvidesIntoSet + @Named("foo") + String setFoo() { return "foo"; } + + @ProvidesIntoSet + @Named("foo") + String setFoo2() { return "foo2"; } + + @ProvidesIntoSet + @Named("bar") + String setBar() { return "bar"; } + + @ProvidesIntoSet + @Named("bar") + String setBar2() { return "bar2"; } + + @ProvidesIntoSet + String setNoAnnotation() { return "na"; } + + @ProvidesIntoSet + String setNoAnnotation2() { return "na2"; } + + @ProvidesIntoMap + @StringMapKey("fooKey") + @Named("foo") + String mapFoo() { return "foo"; } + + @ProvidesIntoMap + @StringMapKey("foo2Key") + @Named("foo") + String mapFoo2() { return "foo2"; } + + @ProvidesIntoMap + @ClassMapKey(String.class) + @Named("bar") + String mapBar() { return "bar"; } + + @ProvidesIntoMap + @ClassMapKey(Number.class) + @Named("bar") + String mapBar2() { return "bar2"; } + + @ProvidesIntoMap + @TestEnumKey(TestEnum.A) + String mapNoAnnotation() { return "na"; } + + @ProvidesIntoMap + @TestEnumKey(TestEnum.B) + String mapNoAnnotation2() { return "na2"; } + + @ProvidesIntoMap + @WrappedKey(number = 1) + Number wrapped1() { return 11; } + + @ProvidesIntoMap + @WrappedKey(number = 2) + Number wrapped2() { return 22; } + + @ProvidesIntoOptional(Type.DEFAULT) + @Named("foo") + String optionalDefaultFoo() { return "foo"; } + + @ProvidesIntoOptional(Type.ACTUAL) + @Named("foo") + String optionalActualFoo() { return "foo2"; } + + @ProvidesIntoOptional(Type.DEFAULT) + @Named("bar") + String optionalDefaultBar() { return "bar"; } + + @ProvidesIntoOptional(Type.ACTUAL) + String optionalActualBar() { return "na2"; } + }); + + Set fooSet = injector.getInstance(new Key>(named("foo")) {}); + assertEquals(ImmutableSet.of("foo", "foo2"), fooSet); + + Set barSet = injector.getInstance(new Key>(named("bar")) {}); + assertEquals(ImmutableSet.of("bar", "bar2"), barSet); + + Set noAnnotationSet = injector.getInstance(new Key>() {}); + assertEquals(ImmutableSet.of("na", "na2"), noAnnotationSet); + + Map fooMap = + injector.getInstance(new Key>(named("foo")) {}); + assertEquals(ImmutableMap.of("fooKey", "foo", "foo2Key", "foo2"), fooMap); + + Map, String> barMap = + injector.getInstance(new Key, String>>(named("bar")) {}); + assertEquals(ImmutableMap.of(String.class, "bar", Number.class, "bar2"), barMap); + + Map noAnnotationMap = + injector.getInstance(new Key>() {}); + assertEquals(ImmutableMap.of(TestEnum.A, "na", TestEnum.B, "na2"), noAnnotationMap); + + Map wrappedMap = + injector.getInstance(new Key>() {}); + assertEquals(ImmutableMap.of(wrappedKeyFor(1), 11, wrappedKeyFor(2), 22), wrappedMap); + + Optional fooOptional = + injector.getInstance(new Key>(named("foo")) {}); + assertEquals("foo2", fooOptional.get()); + + Optional barOptional = + injector.getInstance(new Key>(named("bar")) {}); + assertEquals("bar", barOptional.get()); + + Optional noAnnotationOptional = + injector.getInstance(new Key>() {}); + assertEquals("na2", noAnnotationOptional.get()); + } + + enum TestEnum { + A, B + } + + @MapKey(unwrapValue = true) + @Retention(RUNTIME) + @interface TestEnumKey { + TestEnum value(); + } + + @MapKey(unwrapValue = false) + @Retention(RUNTIME) + @interface WrappedKey { + int number(); + } + + @SuppressWarnings("unused") @WrappedKey(number=1) private static Object wrappedKey1Holder; + @SuppressWarnings("unused") @WrappedKey(number=2) private static Object wrappedKey2Holder; + WrappedKey wrappedKeyFor(int number) throws Exception { + Field field; + switch (number) { + case 1: + field = ProvidesIntoTest.class.getDeclaredField("wrappedKey1Holder"); + break; + case 2: + field = ProvidesIntoTest.class.getDeclaredField("wrappedKey2Holder"); + break; + default: + throw new IllegalArgumentException("only 1 or 2 supported"); + } + return field.getAnnotation(WrappedKey.class); + } + + public void testDoubleScannerIsIgnored() { + Injector injector = Guice.createInjector( + MultibindingsScanner.asModule(), + MultibindingsScanner.asModule(), + new AbstractModule() { + @Override protected void configure() {} + @ProvidesIntoSet String provideFoo() { return "foo"; } + } + ); + assertEquals(ImmutableSet.of("foo"), injector.getInstance(new Key>() {})); + } + + @MapKey(unwrapValue = true) + @Retention(RUNTIME) + @interface ArrayUnwrappedKey { + int[] value(); + } + + public void testArrayKeys_unwrapValuesTrue() { + Module m = new AbstractModule() { + @Override protected void configure() {} + @ProvidesIntoMap @ArrayUnwrappedKey({1, 2}) String provideFoo() { return "foo"; } + }; + try { + Guice.createInjector(MultibindingsScanner.asModule(), m); + fail(); + } catch (CreationException ce) { + assertEquals(1, ce.getErrorMessages().size()); + assertContains(ce.getMessage(), + "Array types are not allowed in a MapKey with unwrapValue=true: " + + ArrayUnwrappedKey.class.getName(), + "at " + m.getClass().getName() + ".provideFoo("); + } + } + + @MapKey(unwrapValue = false) + @Retention(RUNTIME) + @interface ArrayWrappedKey { + int[] number(); + } + + @SuppressWarnings("unused") @ArrayWrappedKey(number={1, 2}) private static Object arrayWrappedKeyHolder12; + @SuppressWarnings("unused") @ArrayWrappedKey(number={3, 4}) private static Object arrayWrappedKeyHolder34; + ArrayWrappedKey arrayWrappedKeyFor(int number) throws Exception { + Field field; + switch (number) { + case 12: + field = ProvidesIntoTest.class.getDeclaredField("arrayWrappedKeyHolder12"); + break; + case 34: + field = ProvidesIntoTest.class.getDeclaredField("arrayWrappedKeyHolder34"); + break; + default: + throw new IllegalArgumentException("only 1 or 2 supported"); + } + return field.getAnnotation(ArrayWrappedKey.class); + } + + public void testArrayKeys_unwrapValuesFalse() throws Exception { + Module m = new AbstractModule() { + @Override protected void configure() {} + @ProvidesIntoMap @ArrayWrappedKey(number = {1, 2}) String provideFoo() { return "foo"; } + @ProvidesIntoMap @ArrayWrappedKey(number = {3, 4}) String provideBar() { return "bar"; } + }; + Injector injector = Guice.createInjector(MultibindingsScanner.asModule(), m); + Map map = + injector.getInstance(new Key>() {}); + ArrayWrappedKey key12 = arrayWrappedKeyFor(12); + ArrayWrappedKey key34 = arrayWrappedKeyFor(34); + assertEquals("foo", map.get(key12)); + assertEquals("bar", map.get(key34)); + assertEquals(2, map.size()); + } + + public void testProvidesIntoSetWithMapKey() { + Module m = new AbstractModule() { + @Override protected void configure() {} + @ProvidesIntoSet @TestEnumKey(TestEnum.A) String provideFoo() { return "foo"; } + }; + try { + Guice.createInjector(MultibindingsScanner.asModule(), m); + fail(); + } catch (CreationException ce) { + assertEquals(1, ce.getErrorMessages().size()); + assertContains(ce.getMessage(), "Found a MapKey annotation on non map binding at " + + m.getClass().getName() + ".provideFoo"); + } + } + + public void testProvidesIntoOptionalWithMapKey() { + Module m = new AbstractModule() { + @Override protected void configure() {} + + @ProvidesIntoOptional(Type.ACTUAL) + @TestEnumKey(TestEnum.A) + String provideFoo() { + return "foo"; + } + }; + try { + Guice.createInjector(MultibindingsScanner.asModule(), m); + fail(); + } catch (CreationException ce) { + assertEquals(1, ce.getErrorMessages().size()); + assertContains(ce.getMessage(), "Found a MapKey annotation on non map binding at " + + m.getClass().getName() + ".provideFoo"); + } + } + + public void testProvidesIntoMapWithoutMapKey() { + Module m = new AbstractModule() { + @Override protected void configure() {} + @ProvidesIntoMap String provideFoo() { return "foo"; } + }; + try { + Guice.createInjector(MultibindingsScanner.asModule(), m); + fail(); + } catch (CreationException ce) { + assertEquals(1, ce.getErrorMessages().size()); + assertContains(ce.getMessage(), "No MapKey found for map binding at " + + m.getClass().getName() + ".provideFoo"); + } + } + + @MapKey(unwrapValue = true) + @Retention(RUNTIME) + @interface TestEnumKey2 { + TestEnum value(); + } + + public void testMoreThanOneMapKeyAnnotation() { + Module m = new AbstractModule() { + @Override protected void configure() {} + + @ProvidesIntoMap + @TestEnumKey(TestEnum.A) + @TestEnumKey2(TestEnum.B) + String provideFoo() { + return "foo"; + } + }; + try { + Guice.createInjector(MultibindingsScanner.asModule(), m); + fail(); + } catch (CreationException ce) { + assertEquals(1, ce.getErrorMessages().size()); + assertContains(ce.getMessage(), "Found more than one MapKey annotations on " + + m.getClass().getName() + ".provideFoo"); + } + } + + @MapKey(unwrapValue = true) + @Retention(RUNTIME) + @interface MissingValueMethod { + } + + public void testMapKeyMissingValueMethod() { + Module m = new AbstractModule() { + @Override protected void configure() {} + + @ProvidesIntoMap + @MissingValueMethod + String provideFoo() { + return "foo"; + } + }; + try { + Guice.createInjector(MultibindingsScanner.asModule(), m); + fail(); + } catch (CreationException ce) { + assertEquals(1, ce.getErrorMessages().size()); + assertContains(ce.getMessage(), "No 'value' method in MapKey with unwrapValue=true: " + + MissingValueMethod.class.getName()); + } + } +} diff --git a/src/test/java/com/google/inject/multibindings/RealElementTest.java b/src/test/java/com/google/inject/multibindings/RealElementTest.java new file mode 100644 index 0000000..a6d395e --- /dev/null +++ b/src/test/java/com/google/inject/multibindings/RealElementTest.java @@ -0,0 +1,37 @@ +package com.google.inject.multibindings; + +import com.google.inject.multibindings.Element.Type; + +import junit.framework.TestCase; + +public class RealElementTest extends TestCase { + + private Element systemElement; + private RealElement realElement; + + @Override protected void setUp() throws Exception { + this.systemElement = Holder.class.getAnnotation(Element.class); + this.realElement = new RealElement("b", Type.MULTIBINDER, "a", 1); + } + + public void testEquals() { + assertEquals(systemElement, realElement); + assertEquals(realElement, systemElement); + } + + public void testHashCode() { + assertEquals(systemElement.hashCode(), realElement.hashCode()); + } + + public void testProperties() { + assertEquals("a", realElement.keyType()); + assertEquals("b", realElement.setName()); + assertEquals(Type.MULTIBINDER, realElement.type()); + assertEquals(1, realElement.uniqueId()); + } + + + @Element(keyType = "a", setName = "b", type = Type.MULTIBINDER, uniqueId = 1) + static class Holder {} + +} diff --git a/src/test/java/com/google/inject/multibindings/SpiUtils.java b/src/test/java/com/google/inject/multibindings/SpiUtils.java new file mode 100644 index 0000000..f13fe83 --- /dev/null +++ b/src/test/java/com/google/inject/multibindings/SpiUtils.java @@ -0,0 +1,1099 @@ +package com.google.inject.multibindings; + +import static com.google.inject.multibindings.MapBinder.entryOfProviderOf; +import static com.google.inject.multibindings.MapBinder.mapOf; +import static com.google.inject.multibindings.MapBinder.mapOfJavaxProviderOf; +import static com.google.inject.multibindings.MapBinder.mapOfProviderOf; +import static com.google.inject.multibindings.MapBinder.mapOfSetOfProviderOf; +import static com.google.inject.multibindings.Multibinder.collectionOfJavaxProvidersOf; +import static com.google.inject.multibindings.Multibinder.collectionOfProvidersOf; +import static com.google.inject.multibindings.Multibinder.setOf; +import static com.google.inject.multibindings.OptionalBinder.javaOptionalOfJavaxProvider; +import static com.google.inject.multibindings.OptionalBinder.javaOptionalOfProvider; +import static com.google.inject.multibindings.OptionalBinder.optionalOfJavaxProvider; +import static com.google.inject.multibindings.OptionalBinder.optionalOfProvider; +import static com.google.inject.multibindings.SpiUtils.BindType.INSTANCE; +import static com.google.inject.multibindings.SpiUtils.BindType.LINKED; +import static com.google.inject.multibindings.SpiUtils.BindType.PROVIDER_INSTANCE; +import static com.google.inject.multibindings.SpiUtils.BindType.PROVIDER_KEY; +import static com.google.inject.multibindings.SpiUtils.VisitType.BOTH; +import static com.google.inject.multibindings.SpiUtils.VisitType.INJECTOR; +import static com.google.inject.multibindings.SpiUtils.VisitType.MODULE; +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertFalse; +import static junit.framework.Assert.assertNotNull; +import static junit.framework.Assert.assertNull; +import static junit.framework.Assert.assertTrue; +import static junit.framework.Assert.fail; + +import com.google.common.base.Objects; +import com.google.common.base.Optional; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.common.collect.Multimap; +import com.google.common.collect.MultimapBuilder; +import com.google.common.collect.Sets; +import com.google.inject.Binding; +import com.google.inject.Guice; +import com.google.inject.Injector; +import com.google.inject.Key; +import com.google.inject.Module; +import com.google.inject.Provider; +import com.google.inject.TypeLiteral; +import com.google.inject.multibindings.Indexer.IndexedBinding; +import com.google.inject.multibindings.MapBinder.RealMapBinder.ProviderMapEntry; +import com.google.inject.multibindings.OptionalBinder.Source; +import com.google.inject.spi.DefaultBindingTargetVisitor; +import com.google.inject.spi.Element; +import com.google.inject.spi.Elements; +import com.google.inject.spi.InstanceBinding; +import com.google.inject.spi.LinkedKeyBinding; +import com.google.inject.spi.ProviderInstanceBinding; +import com.google.inject.spi.ProviderKeyBinding; +import com.google.inject.spi.ProviderLookup; + +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Utilities for testing the Multibinder & MapBinder extension SPI. + */ +public class SpiUtils { + + private static final boolean HAS_JAVA_OPTIONAL; + static { + Class optional = null; + try { + optional = Class.forName("java.util.Optional"); + } catch (ClassNotFoundException ignored) {} + HAS_JAVA_OPTIONAL = optional != null; + } + + /** The kind of test we should perform. A live Injector, a raw Elements (Module) test, or both. */ + enum VisitType { INJECTOR, MODULE, BOTH } + + /** + * Asserts that MapBinderBinding visitors for work correctly. + * + * @param The type of the binding + * @param mapKey The key the map belongs to. + * @param keyType the TypeLiteral of the key of the map + * @param valueType the TypeLiteral of the value of the map + * @param modules The modules that define the mapbindings + * @param visitType The kind of test we should perform. A live Injector, a raw Elements (Module) test, or both. + * @param allowDuplicates If duplicates are allowed. + * @param expectedMapBindings The number of other mapbinders we expect to see. + * @param results The kind of bindings contained in the mapbinder. + */ + static void assertMapVisitor(Key mapKey, TypeLiteral keyType, TypeLiteral valueType, + Iterable modules, VisitType visitType, boolean allowDuplicates, + int expectedMapBindings, MapResult... results) { + if(visitType == null) { + fail("must test something"); + } + + if (visitType == BOTH || visitType == INJECTOR) { + mapInjectorTest(mapKey, keyType, valueType, modules, allowDuplicates, expectedMapBindings, + results); + } + + if (visitType == BOTH || visitType == MODULE) { + mapModuleTest(mapKey, keyType, valueType, modules, allowDuplicates, expectedMapBindings, + results); + } + } + + @SuppressWarnings("unchecked") + private static void mapInjectorTest(Key mapKey, TypeLiteral keyType, + TypeLiteral valueType, Iterable modules, boolean allowDuplicates, + int expectedMapBindings, MapResult... results) { + Injector injector = Guice.createInjector(modules); + Visitor visitor = new Visitor(); + Binding mapBinding = injector.getBinding(mapKey); + MapBinderBinding mapbinder = (MapBinderBinding)mapBinding.acceptTargetVisitor(visitor); + assertNotNull(mapbinder); + assertEquals(keyType, mapbinder.getKeyTypeLiteral()); + assertEquals(valueType, mapbinder.getValueTypeLiteral()); + assertEquals(allowDuplicates, mapbinder.permitsDuplicates()); + List>> entries = Lists.newArrayList(mapbinder.getEntries()); + List mapResults = Lists.newArrayList(results); + assertEquals("wrong entries, expected: " + mapResults + ", but was: " + entries, + mapResults.size(), entries.size()); + + for(MapResult result : mapResults) { + Map.Entry> found = null; + for(Map.Entry> entry : entries) { + Object key = entry.getKey(); + Binding value = entry.getValue(); + if(key.equals(result.k) && matches(value, result.v)) { + found = entry; + break; + } + } + if(found == null) { + fail("Could not find entry: " + result + " in remaining entries: " + entries); + } else { + assertTrue("mapBinder doesn't contain: " + found.getValue(), + mapbinder.containsElement(found.getValue())); + entries.remove(found); + } + } + + if(!entries.isEmpty()) { + fail("Found all entries of: " + mapResults + ", but more were left over: " + entries); + } + + Key mapOfJavaxProvider = mapKey.ofType(mapOfJavaxProviderOf(keyType, valueType)); + Key mapOfProvider = mapKey.ofType(mapOfProviderOf(keyType, valueType)); + Key mapOfSetOfProvider = mapKey.ofType(mapOfSetOfProviderOf(keyType, valueType)); + Key mapOfSet = mapKey.ofType(mapOf(keyType, setOf(valueType))); + Key setOfEntry = mapKey.ofType(setOf(entryOfProviderOf(keyType, valueType))); + Key collectionOfProvidersOfEntryOfProvider = + mapKey.ofType(collectionOfProvidersOf(entryOfProviderOf(keyType, valueType))); + Key collectionOfJavaxProvidersOfEntryOfProvider = + mapKey.ofType(collectionOfJavaxProvidersOf(entryOfProviderOf(keyType, valueType))); + boolean entrySetMatch = false; + boolean mapJavaxProviderMatch = false; + boolean mapProviderMatch = false; + boolean mapSetMatch = false; + boolean mapSetProviderMatch = false; + boolean collectionOfProvidersOfEntryOfProviderMatch = false; + boolean collectionOfJavaxProvidersOfEntryOfProviderMatch = false; + List otherMapBindings = Lists.newArrayList(); + List otherMatches = Lists.newArrayList(); + Multimap indexedEntries = + MultimapBuilder.hashKeys().hashSetValues().build(); + Indexer indexer = new Indexer(injector); + int duplicates = 0; + for(Binding b : injector.getAllBindings().values()) { + boolean contains = mapbinder.containsElement(b); + Object visited = b.acceptTargetVisitor(visitor); + if(visited instanceof MapBinderBinding) { + if(visited.equals(mapbinder)) { + assertTrue(contains); + } else { + otherMapBindings.add(visited); + } + } else if(b.getKey().equals(mapOfProvider)) { + assertTrue(contains); + mapProviderMatch = true; + } else if (b.getKey().equals(mapOfJavaxProvider)) { + assertTrue(contains); + mapJavaxProviderMatch = true; + } else if(b.getKey().equals(mapOfSet)) { + assertTrue(contains); + mapSetMatch = true; + } else if(b.getKey().equals(mapOfSetOfProvider)) { + assertTrue(contains); + mapSetProviderMatch = true; + } else if(b.getKey().equals(setOfEntry)) { + assertTrue(contains); + entrySetMatch = true; + // Validate that this binding is also a MultibinderBinding. + assertTrue(b.acceptTargetVisitor(visitor) instanceof MultibinderBinding); + } else if(b.getKey().equals(collectionOfProvidersOfEntryOfProvider)) { + assertTrue(contains); + collectionOfProvidersOfEntryOfProviderMatch = true; + } else if(b.getKey().equals(collectionOfJavaxProvidersOfEntryOfProvider)) { + assertTrue(contains); + collectionOfJavaxProvidersOfEntryOfProviderMatch = true; + } else if (contains) { + if (b instanceof ProviderInstanceBinding) { + ProviderInstanceBinding pib = (ProviderInstanceBinding)b; + if (pib.getUserSuppliedProvider() instanceof ProviderMapEntry) { + // weird casting required to workaround compilation issues with jdk6 + ProviderMapEntry pme = + (ProviderMapEntry) (Provider) pib.getUserSuppliedProvider(); + Binding valueBinding = injector.getBinding(pme.getValueKey()); + if (indexer.isIndexable(valueBinding) + && !indexedEntries.put(pme.getKey(), valueBinding.acceptTargetVisitor(indexer))) { + duplicates++; + } + } + } + otherMatches.add(b); + } + } + + int sizeOfOther = otherMatches.size(); + if(allowDuplicates) { + sizeOfOther--; // account for 1 duplicate binding + } + // Multiply by two because each has a value and Map.Entry. + int expectedSize = 2 * (mapResults.size() + duplicates); + assertEquals("Incorrect other matches: " + otherMatches, expectedSize, sizeOfOther); + assertTrue(entrySetMatch); + assertTrue(mapProviderMatch); + assertTrue(mapJavaxProviderMatch); + assertTrue(collectionOfProvidersOfEntryOfProviderMatch); + assertTrue(collectionOfJavaxProvidersOfEntryOfProviderMatch); + assertEquals(allowDuplicates, mapSetMatch); + assertEquals(allowDuplicates, mapSetProviderMatch); + assertEquals("other MapBindings found: " + otherMapBindings, expectedMapBindings, + otherMapBindings.size()); + } + + @SuppressWarnings("unchecked") + private static void mapModuleTest(Key mapKey, TypeLiteral keyType, + TypeLiteral valueType, Iterable modules, boolean allowDuplicates, + int expectedMapBindings, MapResult... results) { + Set elements = ImmutableSet.copyOf(Elements.getElements(modules)); + Visitor visitor = new Visitor(); + MapBinderBinding mapbinder = null; + Map, Binding> keyMap = Maps.newHashMap(); + for(Element element : elements) { + if(element instanceof Binding) { + Binding binding = (Binding)element; + keyMap.put(binding.getKey(), binding); + if (binding.getKey().equals(mapKey)) { + mapbinder = (MapBinderBinding)((Binding)binding).acceptTargetVisitor(visitor); + } + } + } + assertNotNull(mapbinder); + + assertEquals(keyType, mapbinder.getKeyTypeLiteral()); + assertEquals(valueType, mapbinder.getValueTypeLiteral()); + List mapResults = Lists.newArrayList(results); + + Key mapOfProvider = mapKey.ofType(mapOfProviderOf(keyType, valueType)); + Key mapOfJavaxProvider = mapKey.ofType(mapOfJavaxProviderOf(keyType, valueType)); + Key mapOfSetOfProvider = mapKey.ofType(mapOfSetOfProviderOf(keyType, valueType)); + Key mapOfSet = mapKey.ofType(mapOf(keyType, setOf(valueType))); + Key setOfEntry = mapKey.ofType(setOf(entryOfProviderOf(keyType, valueType))); + Key collectionOfProvidersOfEntry = + mapKey.ofType(collectionOfProvidersOf(entryOfProviderOf(keyType, valueType))); + Key collectionOfJavaxProvidersOfEntry = + mapKey.ofType(collectionOfJavaxProvidersOf(entryOfProviderOf(keyType, valueType))); + boolean entrySetMatch = false; + boolean mapProviderMatch = false; + boolean mapJavaxProviderMatch = false; + boolean mapSetMatch = false; + boolean mapSetProviderMatch = false; + boolean collectionOfProvidersOfEntryMatch = false; + boolean collectionOfJavaxProvidersOfEntryMatch = false; + List otherMapBindings = Lists.newArrayList(); + List otherMatches = Lists.newArrayList(); + List otherElements = Lists.newArrayList(); + Indexer indexer = new Indexer(null); + Multimap indexedEntries = + MultimapBuilder.hashKeys().hashSetValues().build(); + int duplicates = 0; + for(Element element : elements) { + boolean contains = mapbinder.containsElement(element); + if(!contains) { + otherElements.add(element); + } + boolean matched = false; + Key key = null; + Binding b = null; + if(element instanceof Binding) { + b = (Binding)element; + if (b instanceof ProviderInstanceBinding) { + ProviderInstanceBinding pb = (ProviderInstanceBinding) b; + if (pb.getUserSuppliedProvider() instanceof ProviderMapEntry) { + // weird casting required to workaround jdk6 compilation problems + ProviderMapEntry pme = + (ProviderMapEntry) (Provider) pb.getUserSuppliedProvider(); + Binding valueBinding = keyMap.get(pme.getValueKey()); + if (indexer.isIndexable(valueBinding) + && !indexedEntries.put(pme.getKey(), valueBinding.acceptTargetVisitor(indexer))) { + duplicates++; + } + } + } + + key = b.getKey(); + Object visited = b.acceptTargetVisitor(visitor); + if(visited instanceof MapBinderBinding) { + matched = true; + if(visited.equals(mapbinder)) { + assertTrue(contains); + } else { + otherMapBindings.add(visited); + } + } + } else if(element instanceof ProviderLookup) { + key = ((ProviderLookup)element).getKey(); + } + + if(!matched && key != null) { + if(key.equals(mapOfProvider)) { + matched = true; + assertTrue(contains); + mapProviderMatch = true; + } else if(key.equals(mapOfJavaxProvider)) { + matched = true; + assertTrue(contains); + mapJavaxProviderMatch = true; + } else if(key.equals(mapOfSet)) { + matched = true; + assertTrue(contains); + mapSetMatch = true; + } else if(key.equals(mapOfSetOfProvider)) { + matched = true; + assertTrue(contains); + mapSetProviderMatch = true; + } else if(key.equals(setOfEntry)) { + matched = true; + assertTrue(contains); + entrySetMatch = true; + // Validate that this binding is also a MultibinderBinding. + if(b != null) { + assertTrue(b.acceptTargetVisitor(visitor) instanceof MultibinderBinding); + } + } else if(key.equals(collectionOfProvidersOfEntry)) { + matched = true; + assertTrue(contains); + collectionOfProvidersOfEntryMatch = true; + } else if(key.equals(collectionOfJavaxProvidersOfEntry)) { + matched = true; + assertTrue(contains); + collectionOfJavaxProvidersOfEntryMatch = true; + } + } + + if (!matched && contains) { + otherMatches.add(element); + } + } + + int otherMatchesSize = otherMatches.size(); + if (allowDuplicates) { + otherMatchesSize--; // allow for 1 duplicate binding + } + // Multiply by 3 because each has a value, ProviderLookup, and Map.Entry + int expectedSize = (mapResults.size() + duplicates) * 3; + assertEquals("incorrect number of contains, leftover matches: " + otherMatches, + expectedSize, otherMatchesSize); + + assertTrue(entrySetMatch); + assertTrue(mapProviderMatch); + assertTrue(mapJavaxProviderMatch); + assertTrue(collectionOfProvidersOfEntryMatch); + assertTrue(collectionOfJavaxProvidersOfEntryMatch); + assertEquals(allowDuplicates, mapSetMatch); + assertEquals(allowDuplicates, mapSetProviderMatch); + assertEquals("other MapBindings found: " + otherMapBindings, expectedMapBindings, + otherMapBindings.size()); + + // Validate that we can construct an injector out of the remaining bindings. + Guice.createInjector(Elements.getModule(otherElements)); + } + + /** + * Asserts that MultibinderBinding visitors work correctly. + * + * @param The type of the binding + * @param setKey The key the set belongs to. + * @param elementType the TypeLiteral of the element + * @param modules The modules that define the multibindings + * @param visitType The kind of test we should perform. A live Injector, a raw Elements (Module) test, or both. + * @param allowDuplicates If duplicates are allowed. + * @param expectedMultibindings The number of other multibinders we expect to see. + * @param results The kind of bindings contained in the multibinder. + */ + static void assertSetVisitor(Key> setKey, TypeLiteral elementType, + Iterable modules, VisitType visitType, boolean allowDuplicates, + int expectedMultibindings, BindResult... results) { + if(visitType == null) { + fail("must test something"); + } + + if(visitType == BOTH || visitType == INJECTOR) { + setInjectorTest(setKey, elementType, modules, allowDuplicates, + expectedMultibindings, results); + } + + if(visitType == BOTH || visitType == MODULE) { + setModuleTest(setKey, elementType, modules, allowDuplicates, + expectedMultibindings, results); + } + } + + @SuppressWarnings("unchecked") + private static void setInjectorTest(Key> setKey, TypeLiteral elementType, + Iterable modules, boolean allowDuplicates, int otherMultibindings, + BindResult... results) { + Key collectionOfProvidersKey = setKey.ofType(collectionOfProvidersOf(elementType)); + Key collectionOfJavaxProvidersKey = + setKey.ofType(collectionOfJavaxProvidersOf(elementType)); + Injector injector = Guice.createInjector(modules); + Visitor> visitor = new Visitor>(); + Binding> binding = injector.getBinding(setKey); + MultibinderBinding> multibinder = + (MultibinderBinding>)binding.acceptTargetVisitor(visitor); + assertNotNull(multibinder); + assertEquals(elementType, multibinder.getElementTypeLiteral()); + assertEquals(allowDuplicates, multibinder.permitsDuplicates()); + List> elements = Lists.newArrayList(multibinder.getElements()); + List bindResults = Lists.newArrayList(results); + assertEquals("wrong bind elements, expected: " + bindResults + + ", but was: " + multibinder.getElements(), + bindResults.size(), elements.size()); + + for(BindResult result : bindResults) { + Binding found = null; + for(Binding item : elements) { + if (matches(item, result)) { + found = item; + break; + } + } + if(found == null) { + fail("Could not find element: " + result + " in remaining elements: " + elements); + } else { + elements.remove(found); + } + } + + if(!elements.isEmpty()) { + fail("Found all elements of: " + bindResults + ", but more were left over: " + elements); + } + + Set setOfElements = new HashSet(multibinder.getElements()); + Set setOfIndexed = Sets.newHashSet(); + Indexer indexer = new Indexer(injector); + for (Binding oneBinding : setOfElements) { + setOfIndexed.add(oneBinding.acceptTargetVisitor(indexer)); + } + + List otherMultibinders = Lists.newArrayList(); + List otherContains = Lists.newArrayList(); + boolean collectionOfProvidersMatch = false; + boolean collectionOfJavaxProvidersMatch = false; + for(Binding b : injector.getAllBindings().values()) { + boolean contains = multibinder.containsElement(b); + Key key = b.getKey(); + Object visited = b.acceptTargetVisitor(visitor); + if(visited != null) { + if(visited.equals(multibinder)) { + assertTrue(contains); + } else { + otherMultibinders.add(visited); + } + } else if(setOfElements.contains(b)) { + assertTrue(contains); + } else if (key.equals(collectionOfProvidersKey)) { + assertTrue(contains); + collectionOfProvidersMatch = true; + } else if (key.equals(collectionOfJavaxProvidersKey)) { + assertTrue(contains); + collectionOfJavaxProvidersMatch = true; + } else if (contains) { + if (!indexer.isIndexable(b) || !setOfIndexed.contains(b.acceptTargetVisitor(indexer))) { + otherContains.add(b); + } + } + } + + assertTrue(collectionOfProvidersMatch); + assertTrue(collectionOfJavaxProvidersMatch); + + if(allowDuplicates) { + assertEquals("contained more than it should: " + otherContains, 1, otherContains.size()); + } else { + assertTrue("contained more than it should: " + otherContains, otherContains.isEmpty()); + } + assertEquals("other multibindings found: " + otherMultibinders, otherMultibindings, + otherMultibinders.size()); + + } + + @SuppressWarnings("unchecked") + private static void setModuleTest(Key> setKey, TypeLiteral elementType, + Iterable modules, boolean allowDuplicates, int otherMultibindings, + BindResult... results) { + Key collectionOfProvidersKey = setKey.ofType(collectionOfProvidersOf(elementType)); + Key collectionOfJavaxProvidersKey = + setKey.ofType(collectionOfJavaxProvidersOf(elementType)); + List bindResults = Lists.newArrayList(results); + List elements = Elements.getElements(modules); + Visitor visitor = new Visitor(); + MultibinderBinding> multibinder = null; + for(Element element : elements) { + if(element instanceof Binding && ((Binding)element).getKey().equals(setKey)) { + multibinder = (MultibinderBinding>)((Binding)element).acceptTargetVisitor(visitor); + break; + } + } + assertNotNull(multibinder); + + assertEquals(elementType, multibinder.getElementTypeLiteral()); + List otherMultibinders = Lists.newArrayList(); + Set otherContains = new HashSet(); + List otherElements = Lists.newArrayList(); + int duplicates = 0; + Set setOfIndexed = Sets.newHashSet(); + Indexer indexer = new Indexer(null); + boolean collectionOfProvidersMatch = false; + boolean collectionOfJavaxProvidersMatch = false; + for(Element element : elements) { + boolean contains = multibinder.containsElement(element); + if(!contains) { + otherElements.add(element); + } + boolean matched = false; + Key key = null; + if(element instanceof Binding) { + Binding binding = (Binding)element; + if (indexer.isIndexable(binding) + && !setOfIndexed.add((IndexedBinding) binding.acceptTargetVisitor(indexer))) { + duplicates++; + } + key = binding.getKey(); + Object visited = binding.acceptTargetVisitor(visitor); + if(visited != null) { + matched = true; + if(visited.equals(multibinder)) { + assertTrue(contains); + } else { + otherMultibinders.add(visited); + } + } + } + + if (collectionOfProvidersKey.equals(key)) { + assertTrue(contains); + assertFalse(matched); + collectionOfProvidersMatch = true; + } else if (collectionOfJavaxProvidersKey.equals(key)) { + assertTrue(contains); + assertFalse(matched); + collectionOfJavaxProvidersMatch = true; + } else if (!matched && contains) { + otherContains.add(element); + } + } + + if(allowDuplicates) { + assertEquals("wrong contained elements: " + otherContains, + bindResults.size() + 1 + duplicates, otherContains.size()); + } else { + assertEquals("wrong contained elements: " + otherContains, + bindResults.size() + duplicates, otherContains.size()); + } + + assertEquals("other multibindings found: " + otherMultibinders, otherMultibindings, + otherMultibinders.size()); + assertTrue(collectionOfProvidersMatch); + assertTrue(collectionOfJavaxProvidersMatch); + + // Validate that we can construct an injector out of the remaining bindings. + Guice.createInjector(Elements.getModule(otherElements)); + } + + /** + * Asserts that OptionalBinderBinding visitors for work correctly. + * + * @param The type of the binding + * @param keyType The key OptionalBinder is binding + * @param modules The modules that define the bindings + * @param visitType The kind of test we should perform. A live Injector, a raw Elements (Module) + * test, or both. + * @param expectedOtherOptionalBindings the # of other optional bindings we expect to see. + * @param expectedDefault the expected default binding, or null if none + * @param expectedActual the expected actual binding, or null if none + * @param expectedUserLinkedActual the user binding that is the actual binding, used if + * neither the default nor actual are set and a user binding existed for the type. + */ + static void assertOptionalVisitor(Key keyType, + Iterable modules, + VisitType visitType, + int expectedOtherOptionalBindings, + BindResult expectedDefault, + BindResult expectedActual, + BindResult expectedUserLinkedActual) { + if (visitType == null) { + fail("must test something"); + } + + // if java.util.Optional is bound, there'll be twice as many as we expect. + if (HAS_JAVA_OPTIONAL) { + expectedOtherOptionalBindings *= 2; + } + + if (visitType == BOTH || visitType == INJECTOR) { + optionalInjectorTest(keyType, modules, expectedOtherOptionalBindings, expectedDefault, + expectedActual, expectedUserLinkedActual); + } + + if (visitType == BOTH || visitType == MODULE) { + optionalModuleTest(keyType, modules, expectedOtherOptionalBindings, expectedDefault, + expectedActual, expectedUserLinkedActual); + } + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + private static void optionalInjectorTest(Key keyType, + Iterable modules, + int expectedOtherOptionalBindings, + BindResult expectedDefault, + BindResult expectedActual, + BindResult expectedUserLinkedActual) { + if (expectedUserLinkedActual != null) { + assertNull("cannot have actual if expecting user binding", expectedActual); + assertNull("cannot have default if expecting user binding", expectedDefault); + } + + Key> optionalKey = + keyType.ofType(OptionalBinder.optionalOf(keyType.getTypeLiteral())); + Key javaOptionalKey = HAS_JAVA_OPTIONAL ? + keyType.ofType(OptionalBinder.javaOptionalOf(keyType.getTypeLiteral())) : null; + Injector injector = Guice.createInjector(modules); + Binding> optionalBinding = injector.getBinding(optionalKey); + Visitor visitor = new Visitor(); + OptionalBinderBinding> optionalBinder = + (OptionalBinderBinding>) optionalBinding.acceptTargetVisitor(visitor); + assertNotNull(optionalBinder); + assertEquals(optionalKey, optionalBinder.getKey()); + + Binding javaOptionalBinding = null; + OptionalBinderBinding javaOptionalBinder = null; + if (HAS_JAVA_OPTIONAL) { + javaOptionalBinding = injector.getBinding(javaOptionalKey); + javaOptionalBinder = (OptionalBinderBinding) javaOptionalBinding.acceptTargetVisitor(visitor); + assertNotNull(javaOptionalBinder); + assertEquals(javaOptionalKey, javaOptionalBinder.getKey()); + } + + if (expectedDefault == null) { + assertNull("did not expect a default binding", optionalBinder.getDefaultBinding()); + if (HAS_JAVA_OPTIONAL) { + assertNull("did not expect a default binding", javaOptionalBinder.getDefaultBinding()); + } + } else { + assertTrue("expectedDefault: " + expectedDefault + ", actualDefault: " + + optionalBinder.getDefaultBinding(), + matches(optionalBinder.getDefaultBinding(), expectedDefault)); + if (HAS_JAVA_OPTIONAL) { + assertTrue("expectedDefault: " + expectedDefault + ", actualDefault: " + + javaOptionalBinder.getDefaultBinding(), + matches(javaOptionalBinder.getDefaultBinding(), expectedDefault)); + } + } + + if (expectedActual == null && expectedUserLinkedActual == null) { + assertNull(optionalBinder.getActualBinding()); + if (HAS_JAVA_OPTIONAL) { + assertNull(javaOptionalBinder.getActualBinding()); + } + } else if (expectedActual != null) { + assertTrue("expectedActual: " + expectedActual + ", actualActual: " + + optionalBinder.getActualBinding(), + matches(optionalBinder.getActualBinding(), expectedActual)); + if (HAS_JAVA_OPTIONAL) { + assertTrue("expectedActual: " + expectedActual + ", actualActual: " + + javaOptionalBinder.getActualBinding(), + matches(javaOptionalBinder.getActualBinding(), expectedActual)); + } + } else if (expectedUserLinkedActual != null) { + assertTrue("expectedUserLinkedActual: " + expectedUserLinkedActual + ", actualActual: " + + optionalBinder.getActualBinding(), + matches(optionalBinder.getActualBinding(), expectedUserLinkedActual)); + if (HAS_JAVA_OPTIONAL) { + assertTrue("expectedUserLinkedActual: " + expectedUserLinkedActual + ", actualActual: " + + javaOptionalBinder.getActualBinding(), + matches(javaOptionalBinder.getActualBinding(), expectedUserLinkedActual)); + } + } + + + Key>> optionalJavaxProviderKey = + keyType.ofType(optionalOfJavaxProvider(keyType.getTypeLiteral())); + Key javaOptionalJavaxProviderKey = HAS_JAVA_OPTIONAL ? + keyType.ofType(javaOptionalOfJavaxProvider(keyType.getTypeLiteral())) : null; + Key>> optionalProviderKey = + keyType.ofType(optionalOfProvider(keyType.getTypeLiteral())); + Key javaOptionalProviderKey = HAS_JAVA_OPTIONAL ? + keyType.ofType(javaOptionalOfProvider(keyType.getTypeLiteral())) : null; + + boolean keyMatch = false; + boolean optionalKeyMatch = false; + boolean javaOptionalKeyMatch = false; + boolean optionalJavaxProviderKeyMatch = false; + boolean javaOptionalJavaxProviderKeyMatch = false; + boolean optionalProviderKeyMatch = false; + boolean javaOptionalProviderKeyMatch = false; + boolean defaultMatch = false; + boolean actualMatch = false; + List otherOptionalBindings = Lists.newArrayList(); + List otherMatches = Lists.newArrayList(); + for (Binding b : injector.getAllBindings().values()) { + boolean contains = optionalBinder.containsElement(b); + if (HAS_JAVA_OPTIONAL) { + assertEquals(contains, javaOptionalBinder.containsElement(b)); + } + Object visited = b.acceptTargetVisitor(visitor); + if (visited instanceof OptionalBinderBinding) { + if (visited.equals(optionalBinder)) { + assertTrue(contains); + } else if (HAS_JAVA_OPTIONAL && visited.equals(javaOptionalBinder)) { + assertTrue(contains); + } else { + otherOptionalBindings.add(visited); + } + } + if (b.getKey().equals(keyType)) { + // keyType might match because a user bound it + // (which is possible in a purely absent OptionalBinder) + assertEquals(expectedDefault != null || expectedActual != null, contains); + if (contains) { + keyMatch = true; + } + } else if (b.getKey().equals(optionalKey)) { + assertTrue(contains); + optionalKeyMatch = true; + } else if (b.getKey().equals(javaOptionalKey)) { + assertTrue(contains); + javaOptionalKeyMatch = true; + } else if (b.getKey().equals(optionalJavaxProviderKey)) { + assertTrue(contains); + optionalJavaxProviderKeyMatch = true; + } else if (b.getKey().equals(javaOptionalJavaxProviderKey)) { + assertTrue(contains); + javaOptionalJavaxProviderKeyMatch = true; + } else if (b.getKey().equals(optionalProviderKey)) { + assertTrue(contains); + optionalProviderKeyMatch = true; + } else if (b.getKey().equals(javaOptionalProviderKey)) { + assertTrue(contains); + javaOptionalProviderKeyMatch = true; + } else if (expectedDefault != null && matches(b, expectedDefault)) { + assertTrue(contains); + defaultMatch = true; + } else if (expectedActual != null && matches(b, expectedActual)) { + assertTrue(contains); + actualMatch = true; + } else if (contains) { + otherMatches.add(b); + } + } + + assertEquals(otherMatches.toString(), 0, otherMatches.size()); + // only expect a keymatch if either default or actual are set + assertEquals(expectedDefault != null || expectedActual != null, keyMatch); + assertTrue(optionalKeyMatch); + assertTrue(optionalJavaxProviderKeyMatch); + assertTrue(optionalProviderKeyMatch); + assertEquals(HAS_JAVA_OPTIONAL, javaOptionalKeyMatch); + assertEquals(HAS_JAVA_OPTIONAL, javaOptionalJavaxProviderKeyMatch); + assertEquals(HAS_JAVA_OPTIONAL, javaOptionalProviderKeyMatch); + assertEquals(expectedDefault != null, defaultMatch); + assertEquals(expectedActual != null, actualMatch); + assertEquals("other OptionalBindings found: " + otherOptionalBindings, + expectedOtherOptionalBindings, otherOptionalBindings.size()); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + private static void optionalModuleTest(Key keyType, + Iterable modules, + int expectedOtherOptionalBindings, + BindResult expectedDefault, + BindResult expectedActual, + BindResult expectedUserLinkedActual) { + if (expectedUserLinkedActual != null) { + assertNull("cannot have actual if expecting user binding", expectedActual); + assertNull("cannot have default if expecting user binding", expectedDefault); + } + Set elements = ImmutableSet.copyOf(Elements.getElements(modules)); + Map, Binding> indexed = index(elements); + Key> optionalKey = + keyType.ofType(OptionalBinder.optionalOf(keyType.getTypeLiteral())); + Key javaOptionalKey = HAS_JAVA_OPTIONAL ? + keyType.ofType(OptionalBinder.javaOptionalOf(keyType.getTypeLiteral())) : null; + Visitor visitor = new Visitor(); + OptionalBinderBinding> optionalBinder = null; + OptionalBinderBinding javaOptionalBinder = null; + Key defaultKey = null; + Key actualKey = null; + + Binding optionalBinding = indexed.get(optionalKey); + optionalBinder = + (OptionalBinderBinding>) optionalBinding.acceptTargetVisitor(visitor); + + if (HAS_JAVA_OPTIONAL) { + Binding javaOptionalBinding = indexed.get(javaOptionalKey); + javaOptionalBinder = (OptionalBinderBinding) javaOptionalBinding.acceptTargetVisitor(visitor); + } + + // Locate the defaultKey & actualKey + for (Element element : elements) { + if (optionalBinder.containsElement(element) && element instanceof Binding) { + Binding binding = (Binding) element; + if (isSourceEntry(binding, Source.DEFAULT)) { + defaultKey = binding.getKey(); + } else if (isSourceEntry(binding, Source.ACTUAL)) { + actualKey = binding.getKey(); + } + } + } + assertNotNull(optionalBinder); + if (HAS_JAVA_OPTIONAL) { + assertNotNull(javaOptionalBinder); + } + assertEquals(expectedDefault == null, defaultKey == null); + assertEquals(expectedActual == null, actualKey == null); + + Key>> optionalJavaxProviderKey = + keyType.ofType(optionalOfJavaxProvider(keyType.getTypeLiteral())); + Key javaOptionalJavaxProviderKey = HAS_JAVA_OPTIONAL ? + keyType.ofType(javaOptionalOfJavaxProvider(keyType.getTypeLiteral())) : null; + Key>> optionalProviderKey = + keyType.ofType(optionalOfProvider(keyType.getTypeLiteral())); + Key javaOptionalProviderKey = HAS_JAVA_OPTIONAL ? + keyType.ofType(javaOptionalOfProvider(keyType.getTypeLiteral())) : null; + boolean keyMatch = false; + boolean optionalKeyMatch = false; + boolean javaOptionalKeyMatch = false; + boolean optionalJavaxProviderKeyMatch = false; + boolean javaOptionalJavaxProviderKeyMatch = false; + boolean optionalProviderKeyMatch = false; + boolean javaOptionalProviderKeyMatch = false; + boolean defaultMatch = false; + boolean actualMatch = false; + List otherOptionalElements = Lists.newArrayList(); + List otherContains = Lists.newArrayList(); + List nonContainedElements = Lists.newArrayList(); + for (Element element : elements) { + boolean contains = optionalBinder.containsElement(element); + if (HAS_JAVA_OPTIONAL) { + assertEquals(contains, javaOptionalBinder.containsElement(element)); + } + if (!contains) { + nonContainedElements.add(element); + } + Key key = null; + Binding b = null; + if (element instanceof Binding) { + b = (Binding) element; + key = b.getKey(); + Object visited = b.acceptTargetVisitor(visitor); + if (visited instanceof OptionalBinderBinding) { + if (visited.equals(optionalBinder)) { + assertTrue(contains); + } else if (HAS_JAVA_OPTIONAL && visited.equals(javaOptionalBinder)) { + assertTrue(contains); + } else { + otherOptionalElements.add(visited); + } + } + } else if (element instanceof ProviderLookup) { + key = ((ProviderLookup) element).getKey(); + } + + if (key != null && key.equals(keyType)) { + // keyType might match because a user bound it + // (which is possible in a purely absent OptionalBinder) + assertEquals(expectedDefault != null || expectedActual != null, contains); + if (contains) { + keyMatch = true; + } + } else if (key != null && key.equals(optionalKey)) { + assertTrue(contains); + optionalKeyMatch = true; + } else if (key != null && key.equals(javaOptionalKey)) { + assertTrue(contains); + javaOptionalKeyMatch = true; + } else if (key != null && key.equals(optionalJavaxProviderKey)) { + assertTrue(contains); + optionalJavaxProviderKeyMatch = true; + } else if (key != null && key.equals(javaOptionalJavaxProviderKey)) { + assertTrue(contains); + javaOptionalJavaxProviderKeyMatch = true; + } else if (key != null && key.equals(optionalProviderKey)) { + assertTrue(contains); + optionalProviderKeyMatch = true; + } else if (key != null && key.equals(javaOptionalProviderKey)) { + assertTrue(contains); + javaOptionalProviderKeyMatch = true; + } else if (key != null && key.equals(defaultKey)) { + assertTrue(contains); + if (b != null) { // otherwise it might just be a ProviderLookup into it + assertTrue("expected: " + expectedDefault + ", but was: " + b, + matches(b, expectedDefault)); + defaultMatch = true; + } + } else if (key != null && key.equals(actualKey)) { + assertTrue(contains); + if (b != null) { // otherwise it might just be a ProviderLookup into it + assertTrue("expected: " + expectedActual + ", but was: " + b, matches(b, expectedActual)); + actualMatch = true; + } + } else if (contains) { + otherContains.add(element); + } + } + + // only expect a keymatch if either default or actual are set + assertEquals(expectedDefault != null || expectedActual != null, keyMatch); + assertTrue(optionalKeyMatch); + assertTrue(optionalJavaxProviderKeyMatch); + assertTrue(optionalProviderKeyMatch); + assertEquals(HAS_JAVA_OPTIONAL, javaOptionalKeyMatch); + assertEquals(HAS_JAVA_OPTIONAL, javaOptionalJavaxProviderKeyMatch); + assertEquals(HAS_JAVA_OPTIONAL, javaOptionalProviderKeyMatch); + assertEquals(expectedDefault != null, defaultMatch); + assertEquals(expectedActual != null, actualMatch); + assertEquals(otherContains.toString(), 0, otherContains.size()); + assertEquals("other OptionalBindings found: " + otherOptionalElements, + expectedOtherOptionalBindings, otherOptionalElements.size()); + + // Validate that we can construct an injector out of the remaining bindings. + Guice.createInjector(Elements.getModule(nonContainedElements)); + } + + private static boolean isSourceEntry(Binding b, Source type) { + switch(type) { + case ACTUAL: + return b.getKey().getAnnotation() instanceof OptionalBinder.Actual; + case DEFAULT: + return b.getKey().getAnnotation() instanceof OptionalBinder.Default; + default: + throw new IllegalStateException("invalid type: " + type); + } + } + + /** Returns the subset of elements that have keys, indexed by them. */ + private static Map, Binding> index(Iterable elements) { + ImmutableMap.Builder, Binding> builder = ImmutableMap.builder(); + for (Element element : elements) { + if (element instanceof Binding) { + builder.put(((Binding) element).getKey(), (Binding) element); + } + } + return builder.build(); + } + + static MapResult instance(K k, V v) { + return new MapResult(k, new BindResult(INSTANCE, v, null)); + } + + static MapResult linked(K k, Class clazz) { + return new MapResult(k, new BindResult(LINKED, null, Key.get(clazz))); + } + + static MapResult linked(K k, Key key) { + return new MapResult(k, new BindResult(LINKED, null, key)); + } + + static MapResult providerInstance(K k, V v) { + return new MapResult(k, new BindResult(PROVIDER_INSTANCE, v, null)); + } + + static class MapResult { + private final K k; + private final BindResult v; + + MapResult(K k, BindResult v) { + this.k = k; + this.v = v; + } + + @Override + public String toString() { + return "entry[key[" + k + "],value[" + v + "]]"; + } + } + + private static boolean matches(Binding item, BindResult result) { + switch (result.type) { + case INSTANCE: + if (item instanceof InstanceBinding + && ((InstanceBinding) item).getInstance().equals(result.instance)) { + return true; + } + break; + case LINKED: + if (item instanceof LinkedKeyBinding + && ((LinkedKeyBinding) item).getLinkedKey().equals(result.key)) { + return true; + } + break; + case PROVIDER_INSTANCE: + if (item instanceof ProviderInstanceBinding + && Objects.equal(((ProviderInstanceBinding) item).getUserSuppliedProvider().get(), + result.instance)) { + return true; + } + break; + case PROVIDER_KEY: + if (item instanceof ProviderKeyBinding + && ((ProviderKeyBinding) item).getProviderKey().equals(result.key)) { + return true; + } + break; + } + return false; + } + + static BindResult instance(T t) { + return new BindResult(INSTANCE, t, null); + } + + static BindResult linked(Class clazz) { + return new BindResult(LINKED, null, Key.get(clazz)); + } + + static BindResult linked(Key key) { + return new BindResult(LINKED, null, key); + } + + static BindResult providerInstance(T t) { + return new BindResult(PROVIDER_INSTANCE, t, null); + } + + static BindResult providerKey(Key key) { + return new BindResult(PROVIDER_KEY, null, key); + } + + /** The kind of binding. */ + static enum BindType { INSTANCE, LINKED, PROVIDER_INSTANCE, PROVIDER_KEY } + /** The result of the binding. */ + static class BindResult { + private final BindType type; + private final Key key; + private final T instance; + + private BindResult(BindType type, T instance, Key key) { + this.type = type; + this.instance = instance; + this.key = key; + } + + @Override + public String toString() { + switch(type) { + case INSTANCE: + return "instance[" + instance + "]"; + case LINKED: + return "linkedKey[" + key + "]"; + case PROVIDER_INSTANCE: + return "providerInstance[" + instance + "]"; + case PROVIDER_KEY: + return "providerKey[" + key + "]"; + } + return null; + } + } + + private static class Visitor extends + DefaultBindingTargetVisitor implements MultibindingsTargetVisitor { + + public Object visit(MultibinderBinding multibinding) { + return multibinding; + } + + public Object visit(MapBinderBinding mapbinding) { + return mapbinding; + } + + public Object visit(OptionalBinderBinding optionalbinding) { + return optionalbinding; + } + } +}