Checking in David Baker's MapBinder implementation.
git-svn-id: https://google-guice.googlecode.com/svn/trunk@470 d779f126-a31b-0410-b53b-1d3aecad763e
diff --git a/extensions/multibindings/src/com/google/inject/multibindings/Element.java b/extensions/multibindings/src/com/google/inject/multibindings/Element.java
new file mode 100644
index 0000000..9d5bfa6
--- /dev/null
+++ b/extensions/multibindings/src/com/google/inject/multibindings/Element.java
@@ -0,0 +1,37 @@
+/**
+ * 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.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.
+ *
+ * @author jessewilson@google.com (Jesse Wilson)
+ */
+@Retention(RUNTIME) @BindingAnnotation
+@interface Element {
+ public abstract String setName();
+ public abstract String role();
+ public abstract int uniqueId();
+}
diff --git a/extensions/multibindings/src/com/google/inject/multibindings/MapBinder.java b/extensions/multibindings/src/com/google/inject/multibindings/MapBinder.java
new file mode 100644
index 0000000..4f69c1b
--- /dev/null
+++ b/extensions/multibindings/src/com/google/inject/multibindings/MapBinder.java
@@ -0,0 +1,286 @@
+/**
+ * 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.multibindings;
+
+import com.google.inject.*;
+import com.google.inject.binder.LinkedBindingBuilder;
+import com.google.inject.internal.Objects;
+import com.google.inject.internal.TypeWithArgument;
+import com.google.inject.multibindings.Multibinder.RealMultibinder;
+
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Type;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * 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:
+ * <pre><code>
+ * 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);
+ * }
+ * }</code></pre>
+ *
+ * <p>With this binding, a {@link Map}{@code <String, Snack>} can now be
+ * injected:
+ * <pre><code>
+ * class SnackMachine {
+ * {@literal @}Inject
+ * public SnackMachine(Map<String, Snack> snacks) { ... }
+ * }</code></pre>
+ *
+ * <p>In addition to binding {@code Map<K, V>}, a mapbinder will also bind
+ * {@code Map<K, Provider<V>>} for lazy value provision:
+ * <pre><code>
+ * class SnackMachine {
+ * {@literal @}Inject
+ * public SnackMachine(Map<String, Provider<Snack>> snackProviders) { ... }
+ * }</code></pre>
+ *
+ * <p>Creating 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<String, Snack>}, and to each contribute
+ * bindings to the snacks map. When that map is injected, it will contain
+ * entries from both modules.
+ *
+ * <p>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).
+ *
+ * <p>Annotations are be used to create different maps of the same key/value
+ * type. Each distinct annotation gets its own independent map.
+ *
+ * <p><strong>Keys must be distinct.</strong> If the same key is bound more than
+ * once, map injection will fail.
+ *
+ * <p><strong>Keys must be non-null.</strong> {@code addBinding(null)} will
+ * throw an unchecked exception.
+ *
+ * <p><strong>Values must be non-null to use map injection.</strong> If any
+ * value is null, map injection will fail (although injecting a map of providers
+ * will not).
+ *
+ * @author dpb@google.com (David P. Baker)
+ * @author jessewilson@google.com (Jesse Wilson)
+ */
+public abstract class MapBinder<K, V> {
+ 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 <K, V> MapBinder<K, V> newMapBinder(Binder binder,
+ Type keyType, Type valueType) {
+ return newMapBinder(binder,
+ Key.get(MapBinder.<K, V>mapOf(keyType, valueType)),
+ Key.get(MapBinder.<K, V>mapOfProviderOf(keyType, valueType)),
+ Multibinder.<K>newSetBinder(binder, keyType),
+ Multibinder.<V>newSetBinder(binder, 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 <K, V> MapBinder<K, V> newMapBinder(Binder binder,
+ Type keyType, Type valueType, Annotation annotation) {
+ return newMapBinder(binder,
+ Key.get(MapBinder.<K, V>mapOf(keyType, valueType), annotation),
+ Key.get(MapBinder.<K, V>mapOfProviderOf(keyType, valueType), annotation),
+ Multibinder.<K>newSetBinder(binder, keyType, annotation),
+ Multibinder.<V>newSetBinder(binder, 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 <K, V> MapBinder<K, V> newMapBinder(Binder binder,
+ Type keyType, Type valueType, Class<? extends Annotation> annotationType) {
+ return newMapBinder(binder,
+ Key.get(MapBinder.<K, V>mapOf(keyType, valueType), annotationType),
+ Key.get(MapBinder.<K, V>mapOfProviderOf(keyType, valueType), annotationType),
+ Multibinder.<K>newSetBinder(binder, keyType, annotationType),
+ Multibinder.<V>newSetBinder(binder, valueType, annotationType));
+ }
+
+ @SuppressWarnings("unchecked")
+ private static <K, V> TypeLiteral<Map<K, V>> mapOf(Type keyType, Type valueType) {
+ Type type = new TypeWithArgument(Map.class, keyType, valueType);
+ return (TypeLiteral<Map<K, V>>) TypeLiteral.get(type);
+ }
+
+ private static <K, V> TypeLiteral<Map<K, Provider<V>>> mapOfProviderOf(
+ Type keyType, Type valueType) {
+ return mapOf(keyType, new TypeWithArgument(Provider.class, valueType));
+ }
+
+ private static <K, V> MapBinder<K, V> newMapBinder(Binder binder,
+ Key<Map<K, V>> mapKey, Key<Map<K, Provider<V>>> valueKey,
+ Multibinder<K> keyMultibinder, Multibinder<V> valueMultibinder) {
+ RealMapBinder<K, V> realMapBinder = new RealMapBinder<K, V>(
+ mapKey, valueKey, keyMultibinder, valueMultibinder);
+ binder.install(realMapBinder);
+ return realMapBinder;
+ }
+
+ /**
+ * 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.
+ *
+ * <p>It is an error to call this method without also calling one of the
+ * {@code to} methods on the returned binding builder.
+ *
+ * <p>Scoping elements independently is supported. Use the {@code in} method
+ * to specify a binding scope.
+ */
+ public abstract LinkedBindingBuilder<V> addBinding(K key);
+
+ /**
+ * The actual mapbinder plays several roles:
+ *
+ * <p>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).
+ *
+ * <p>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.
+ *
+ * <p>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.
+ *
+ * <p>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.
+ *
+ * <p>We use a subclass to hide 'implements Module' from the public API.
+ */
+ private static final class RealMapBinder<K, V> extends MapBinder<K, V> implements Module {
+ private final Key<Map<K, V>> mapKey;
+ private final Key<Map<K, Provider<V>>> providerMapKey;
+ private final RealMultibinder<K> keyBinder;
+ private final RealMultibinder<V> valueBinder;
+
+ private RealMapBinder(Key<Map<K, V>> mapKey, Key<Map<K, Provider<V>>> providerMapKey,
+ Multibinder<K> keyBinder, Multibinder<V> valueBinder) {
+ this.mapKey = mapKey;
+ this.providerMapKey = providerMapKey;
+ this.keyBinder = (RealMultibinder<K>) keyBinder;
+ this.valueBinder = (RealMultibinder<V>) valueBinder;
+ }
+
+ public LinkedBindingBuilder<V> addBinding(K key) {
+ // this code is currently quite crufty - we depend on the fact that the
+ // key and the value have the same unique ID. A better approach would be
+ // to create an element annotation that knows the Map.Entry type
+ Objects.nonNull(key, "key");
+ int uniqueId = RealMultibinder.nextUniqueId.getAndIncrement();
+ keyBinder.addBinding("key", uniqueId).toInstance(key);
+ return valueBinder.addBinding("value", uniqueId);
+ }
+
+ public void configure(Binder binder) {
+ final Provider<Map<K, Provider<V>>> providerMapProvider
+ = new Provider<Map<K, Provider<V>>>() {
+ private Map<K, Provider<V>> providerMap;
+
+ @Inject void init(Injector injector) {
+ Map<Integer, K> keys = new LinkedHashMap<Integer, K>();
+ Map<Integer, Provider<V>> valueProviders = new HashMap<Integer, Provider<V>>();
+
+ // find the bindings
+ for (Map.Entry<Key<?>, Binding<?>> entry : injector.getBindings().entrySet()) {
+ if (keyBinder.keyMatches(entry.getKey(), "key")) {
+ Element element = (Element) entry.getKey().getAnnotation();
+ @SuppressWarnings("unchecked")
+ Binding<K> binding = (Binding<K>) entry.getValue();
+ keys.put(element.uniqueId(), binding.getProvider().get());
+ } else if (valueBinder.keyMatches(entry.getKey(), "value")) {
+ Element element = (Element) entry.getKey().getAnnotation();
+ @SuppressWarnings("unchecked")
+ Binding<V> binding = (Binding<V>) entry.getValue();
+ valueProviders.put(element.uniqueId(), binding.getProvider());
+ }
+ }
+
+ // build the map
+ Map<K, Provider<V>> providerMapMutable = new LinkedHashMap<K, Provider<V>>();
+ for (Map.Entry<Integer, K> entry : keys.entrySet()) {
+ K key = entry.getValue();
+ Provider<V> valueProvider = valueProviders.get(entry.getKey());
+ if (valueProvider == null) {
+ continue;
+ }
+ if (providerMapMutable.put(key, valueProvider) != null) {
+ throw new IllegalStateException("Map injection failed due to duplicated key \""
+ + key + "\"");
+ }
+ }
+
+ providerMap = Collections.unmodifiableMap(providerMapMutable);
+ }
+
+ public Map<K, Provider<V>> get() {
+ return providerMap;
+ }
+ };
+
+ binder.bind(providerMapKey).toProvider(providerMapProvider);
+
+ binder.bind(mapKey).toProvider(new Provider<Map<K, V>>() {
+ public Map<K, V> get() {
+ Map<K, V> map = new LinkedHashMap<K, V>();
+ for (Map.Entry<K, Provider<V>> entry : providerMapProvider.get().entrySet()) {
+ V value = entry.getValue().get();
+ K key = entry.getKey();
+ if (value == null) {
+ throw new IllegalStateException("Map injection failed due to null value for key \""
+ + key + "\"");
+ }
+ map.put(key, value);
+ }
+ return Collections.unmodifiableMap(map);
+ }
+ });
+ }
+
+ @Override public boolean equals(Object o) {
+ return o instanceof RealMapBinder
+ && ((RealMapBinder) o).mapKey.equals(mapKey);
+ }
+
+ @Override public int hashCode() {
+ return mapKey.hashCode();
+ }
+ }
+}
diff --git a/extensions/multibindings/src/com/google/inject/multibindings/Multibinder.java b/extensions/multibindings/src/com/google/inject/multibindings/Multibinder.java
index 83ba87e..e94c6bc 100644
--- a/extensions/multibindings/src/com/google/inject/multibindings/Multibinder.java
+++ b/extensions/multibindings/src/com/google/inject/multibindings/Multibinder.java
@@ -22,8 +22,6 @@
import com.google.inject.internal.TypeWithArgument;
import java.lang.annotation.Annotation;
-import java.lang.annotation.Retention;
-import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.reflect.Type;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
@@ -148,8 +146,10 @@
* <p>We use a subclass to hide 'implements Module, Provider' from the public
* API.
*/
- private static final class RealMultibinder<T>
+ static final class RealMultibinder<T>
extends Multibinder<T> implements Module, Provider<Set<T>> {
+ static final AtomicInteger nextUniqueId = new AtomicInteger(1);
+
private final Type elementType;
private final String setName;
private final Key<Set<T>> setKey;
@@ -177,14 +177,18 @@
binder.bind(setKey).toProvider(this);
}
- @SuppressWarnings("unchecked")
public LinkedBindingBuilder<T> addBinding() {
+ return addBinding("element", nextUniqueId.getAndIncrement());
+ }
+
+ @SuppressWarnings("unchecked")
+ LinkedBindingBuilder<T> addBinding(String role, int uniqueId) {
if (isInitialized()) {
throw new IllegalStateException("Multibinder was already initialized");
}
return (LinkedBindingBuilder<T>) binder.bind(
- Key.get(elementType, new RealElement(setName)));
+ Key.get(elementType, new RealElement(setName, role, uniqueId)));
}
/**
@@ -195,7 +199,7 @@
@Inject void initialize(Injector injector) {
providers = new ArrayList<Provider<T>>();
for (Map.Entry<Key<?>, Binding<?>> entry : injector.getBindings().entrySet()) {
- if (keyMatches(entry.getKey())) {
+ if (keyMatches(entry.getKey(), "element")) {
@SuppressWarnings("unchecked")
Binding<T> binding = (Binding<T>) entry.getValue();
providers.add(binding.getProvider());
@@ -205,10 +209,11 @@
this.binder = null;
}
- private boolean keyMatches(Key<?> key) {
+ boolean keyMatches(Key<?> key, String role) {
return key.getTypeLiteral().getType().equals(elementType)
&& key.getAnnotation() instanceof Element
- && ((Element) key.getAnnotation()).setName().equals(setName);
+ && ((Element) key.getAnnotation()).setName().equals(setName)
+ && ((Element) key.getAnnotation()).role().equals(role);
}
private boolean isInitialized() {
@@ -236,8 +241,7 @@
@Override public boolean equals(Object o) {
return o instanceof RealMultibinder
- && ((RealMultibinder)o ).elementType.equals(elementType)
- && ((RealMultibinder)o ).setName.equals(setName);
+ && ((RealMultibinder) o).setKey.equals(setKey);
}
@Override public int hashCode() {
@@ -253,55 +257,9 @@
.append(">")
.toString();
}
- }
- /**
- * 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
- private @interface Element {
- String setName();
- int uniqueId();
- }
-
- private static class RealElement implements Element {
- private static final AtomicInteger nextUniqueId = new AtomicInteger(1);
-
- private final int uniqueId = nextUniqueId.getAndIncrement();
- private final String setName;
-
- RealElement(String setName) {
- this.setName = setName;
- }
-
- public String setName() {
- return setName;
- }
-
- public int uniqueId() {
- return uniqueId;
- }
-
- public Class<? extends Annotation> annotationType() {
- return Element.class;
- }
-
- @Override public String toString() {
- return "@" + Element.class.getName() + "(uniqueId=" + uniqueId
- + ",setName=" + setName + ")";
- }
-
- @Override public boolean equals(Object o) {
- return o instanceof Element
- && ((Element) o).uniqueId() == uniqueId()
- && ((Element) o).setName().equals(setName());
- }
-
- @Override public int hashCode() {
- return 127 * ("uniqueId".hashCode() ^ uniqueId)
- + 127 * ("setName".hashCode() ^ setName.hashCode());
+ public Type getElementType() {
+ return elementType;
}
}
}
\ No newline at end of file
diff --git a/extensions/multibindings/src/com/google/inject/multibindings/RealElement.java b/extensions/multibindings/src/com/google/inject/multibindings/RealElement.java
new file mode 100644
index 0000000..49cd350
--- /dev/null
+++ b/extensions/multibindings/src/com/google/inject/multibindings/RealElement.java
@@ -0,0 +1,68 @@
+/**
+ * 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.multibindings;
+
+import java.lang.annotation.Annotation;
+
+/**
+ * @author jessewilson@google.com (Jesse Wilson)
+ */
+class RealElement implements Element {
+ private final String setName;
+ private final String role;
+ private final int uniqueId;
+
+ RealElement(String setName, String role, int uniqueId) {
+ this.setName = setName;
+ this.role = role;
+ this.uniqueId = uniqueId;
+ }
+
+ public String setName() {
+ return setName;
+ }
+
+ public String role() {
+ return role;
+ }
+
+ public int uniqueId() {
+ return uniqueId;
+ }
+
+ public Class<? extends Annotation> annotationType() {
+ return Element.class;
+ }
+
+ @Override public String toString() {
+ return "@" + Element.class.getName() + "(setName=" + setName
+ + ",role=" + role + ",uniqueId=" + uniqueId + ")";
+ }
+
+ @Override public boolean equals(Object o) {
+ return o instanceof Element
+ && ((Element) o).setName().equals(setName())
+ && ((Element) o).role().equals(role())
+ && ((Element) o).uniqueId() == uniqueId();
+ }
+
+ @Override public int hashCode() {
+ return 127 * ("setName".hashCode() ^ setName.hashCode())
+ + 127 * ("role".hashCode() ^ role.hashCode())
+ + 127 * ("uniqueId".hashCode() ^ uniqueId);
+ }
+}
diff --git a/extensions/multibindings/test/com/google/inject/multibindings/MapBinderTest.java b/extensions/multibindings/test/com/google/inject/multibindings/MapBinderTest.java
new file mode 100644
index 0000000..526f192
--- /dev/null
+++ b/extensions/multibindings/test/com/google/inject/multibindings/MapBinderTest.java
@@ -0,0 +1,283 @@
+/**
+ * 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.multibindings;
+
+import com.google.inject.*;
+import com.google.inject.name.Names;
+import static com.google.inject.name.Names.named;
+import com.google.inject.util.Providers;
+import junit.framework.TestCase;
+
+import java.lang.annotation.Retention;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author dpb@google.com (David P. Baker)
+ * @author jessewilson@google.com (Jesse Wilson)
+ */
+public class MapBinderTest extends TestCase {
+
+ final TypeLiteral<Map<String, String>> mapOfString = new TypeLiteral<Map<String, String>>() {};
+ final TypeLiteral<Map<String, Integer>> mapOfInteger = new TypeLiteral<Map<String, Integer>>() {};
+
+ public void testMapBinderAggregatesMultipleModules() {
+ Module abc = new AbstractModule() {
+ @Override protected void configure() {
+ MapBinder<String, String> 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<String, String> 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<String, String> abcde = injector.getInstance(Key.get(mapOfString));
+
+ assertEquals(mapOf("a", "A", "b", "B", "c", "C", "d", "D", "e", "E"), abcde);
+ }
+
+ public void testMapBinderAggregationForAnnotationInstance() {
+ Injector injector = Guice.createInjector(new AbstractModule() {
+ @Override protected void configure() {
+ MapBinder<String, String> 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");
+ }
+ });
+
+ Map<String, String> abc = injector.getInstance(Key.get(mapOfString, Names.named("abc")));
+ assertEquals(mapOf("a", "A", "b", "B", "c", "C"), abc);
+ }
+
+ public void testMapBinderAggregationForAnnotationType() {
+ Injector injector = Guice.createInjector(new AbstractModule() {
+ @Override protected void configure() {
+ MapBinder<String, String> 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");
+ }
+ });
+
+ Map<String, String> abc = injector.getInstance(Key.get(mapOfString, Abc.class));
+ assertEquals(mapOf("a", "A", "b", "B", "c", "C"), abc);
+ }
+
+ public void testMapBinderWithMultipleAnnotationValueSets() {
+ Injector injector = Guice.createInjector(new AbstractModule() {
+ @Override protected void configure() {
+ MapBinder<String, String> 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<String, String> deMapBinder = MapBinder.newMapBinder(
+ binder(), String.class, String.class, named("de"));
+ deMapBinder.addBinding("d").toInstance("D");
+ deMapBinder.addBinding("e").toInstance("E");
+ }
+ });
+
+ Map<String, String> abc = injector.getInstance(Key.get(mapOfString, named("abc")));
+ Map<String, String> de = injector.getInstance(Key.get(mapOfString, named("de")));
+ assertEquals(mapOf("a", "A", "b", "B", "c", "C"), abc);
+ assertEquals(mapOf("d", "D", "e", "E"), de);
+ }
+
+ public void testMapBinderWithMultipleAnnotationTypeSets() {
+ Injector injector = Guice.createInjector(new AbstractModule() {
+ @Override protected void configure() {
+ MapBinder<String, String> 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<String, String> deMapBinder = MapBinder.newMapBinder(
+ binder(), String.class, String.class, De.class);
+ deMapBinder.addBinding("d").toInstance("D");
+ deMapBinder.addBinding("e").toInstance("E");
+ }
+ });
+
+ Map<String, String> abc = injector.getInstance(Key.get(mapOfString, Abc.class));
+ Map<String, String> de = injector.getInstance(Key.get(mapOfString, De.class));
+ assertEquals(mapOf("a", "A", "b", "B", "c", "C"), abc);
+ assertEquals(mapOf("d", "D", "e", "E"), de);
+ }
+
+ public void testMapBinderWithMultipleTypes() {
+ Injector injector = Guice.createInjector(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);
+ }
+ });
+
+ assertEquals(mapOf("a", "A"), injector.getInstance(Key.get(mapOfString)));
+ assertEquals(mapOf("1", 1), injector.getInstance(Key.get(mapOfInteger)));
+ }
+
+ public void testMapBinderWithEmptyMap() {
+ Injector injector = Guice.createInjector(new AbstractModule() {
+ @Override protected void configure() {
+ MapBinder.newMapBinder(binder(), String.class, String.class);
+ }
+ });
+
+ Map<String, String> map = injector.getInstance(Key.get(mapOfString));
+ assertEquals(Collections.emptyMap(), map);
+ }
+
+ 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<String, String> map = injector.getInstance(Key.get(mapOfString));
+ try {
+ map.clear();
+ fail();
+ } catch(UnsupportedOperationException expected) {
+ }
+ }
+
+ public void testMapBinderMapIsLazy() {
+ Injector injector = Guice.createInjector(new AbstractModule() {
+ @Override protected void configure() {
+ MapBinder.newMapBinder(binder(), String.class, Integer.class)
+ .addBinding("num").toProvider(new Provider<Integer>() {
+ int nextValue = 1;
+ public Integer get() {
+ return nextValue++;
+ }
+ });
+ }
+ });
+
+ 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)));
+ }
+
+ public void testMapBinderMapForbidsDuplicateKeys() {
+ try {
+ Guice.createInjector(new AbstractModule() {
+ @Override protected void configure() {
+ MapBinder<String, String> multibinder = MapBinder.newMapBinder(
+ binder(), String.class, String.class);
+ multibinder.addBinding("a").toInstance("A");
+ multibinder.addBinding("a").toInstance("B");
+ }
+ });
+ fail();
+ } catch(ProvisionException expected) {
+ assertEquals("Map injection failed due to duplicated key \"a\"",
+ expected.getCause().getMessage());
+ }
+ }
+
+ public void testMapBinderMapForbidsNullKeys() {
+ try {
+ Guice.createInjector(new AbstractModule() {
+ @Override protected void configure() {
+ MapBinder.newMapBinder(binder(), String.class, String.class)
+ .addBinding(null);
+ }
+ });
+ fail();
+ } catch (NullPointerException expected) {}
+ }
+
+ public void testMapBinderMapForbidsNullValues() {
+ Injector injector = Guice.createInjector(new AbstractModule() {
+ @Override protected void configure() {
+ MapBinder.newMapBinder(binder(), String.class, String.class)
+ .addBinding("null").toProvider(Providers.<String>of(null));
+ }
+ });
+
+ try {
+ injector.getInstance(Key.get(mapOfString));
+ fail();
+ } catch(IllegalStateException expected) {
+ assertEquals("Map injection failed due to null value for key \"null\"",
+ expected.getMessage());
+ }
+ }
+
+ public void testMapBinderProviderIsScoped() {
+ final Provider<Integer> counter = new Provider<Integer>() {
+ int next = 1;
+ 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"));
+ }
+
+ @Retention(RUNTIME) @BindingAnnotation
+ @interface Abc {}
+
+ @Retention(RUNTIME) @BindingAnnotation
+ @interface De {}
+
+ @SuppressWarnings("unchecked")
+ private <K, V> Map<K, V> mapOf(Object... elements) {
+ Map<K, V> result = new HashMap<K, V>();
+ for (int i = 0; i < elements.length; i += 2) {
+ result.put((K)elements[i], (V)elements[i+1]);
+ }
+ return result;
+ }
+}
diff --git a/src/com/google/inject/internal/TypeWithArgument.java b/src/com/google/inject/internal/TypeWithArgument.java
index 8ad8d66..cf64f9f 100644
--- a/src/com/google/inject/internal/TypeWithArgument.java
+++ b/src/com/google/inject/internal/TypeWithArgument.java
@@ -59,4 +59,27 @@
&& Arrays.equals(getActualTypeArguments(), that.getActualTypeArguments())
&& that.getOwnerType() == null;
}
+
+ @Override public String toString() {
+ return toString(this);
+ }
+
+ private String toString(Type type) {
+ if (type instanceof Class<?>) {
+ return ((Class) type).getSimpleName();
+ } else if (type instanceof ParameterizedType) {
+ ParameterizedType parameterizedType = (ParameterizedType) type;
+ Type[] arguments = parameterizedType.getActualTypeArguments();
+ StringBuilder stringBuilder = new StringBuilder()
+ .append(toString(parameterizedType.getRawType()))
+ .append("<")
+ .append(toString(arguments[0]));
+ for (int i = 1; i < arguments.length; i++) {
+ stringBuilder.append(", ").append(toString(arguments[i]));
+ }
+ return stringBuilder.append(">").toString();
+ } else {
+ return type.toString();
+ }
+ }
}
\ No newline at end of file