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&lt;String, Snack&gt; 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&lt;String, Snack&gt; 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&lt;String, Provider&lt;Snack&gt;&gt; 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&lt;K, V&gt;, 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