New API: Multibinder.permitDuplicates() and MapBinder.permitDuplicates()

When a map binder is configured to permit duplicate keys, the value chosen is arbitrary! This may be frustrating; but otherwise it would be necessary to actually call Provider.get() on the duplicated values to compare 'em; I think the cure would be worse than the disease

git-svn-id: https://google-guice.googlecode.com/svn/trunk@1034 d779f126-a31b-0410-b53b-1d3aecad763e
diff --git a/extensions/multibindings/src/com/google/inject/multibindings/MapBinder.java b/extensions/multibindings/src/com/google/inject/multibindings/MapBinder.java
index bc5e3a2..4f92dc6 100644
--- a/extensions/multibindings/src/com/google/inject/multibindings/MapBinder.java
+++ b/extensions/multibindings/src/com/google/inject/multibindings/MapBinder.java
@@ -18,6 +18,7 @@
 
 import com.google.inject.Binder;
 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;
@@ -195,6 +196,15 @@
   }
 
   /**
+   * Configures the bound map to silently discard duplicate entries. When multiple equal keys are
+   * bound, the value that gets included is arbitrary. When multible modules contribute elements to
+   * the map, this configuration option impacts all of them.
+   *
+   * @return this map binder
+   */
+  public abstract MapBinder<K, V> 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.
@@ -250,6 +260,11 @@
       this.binder = binder;
     }
 
+    public MapBinder<K, V> permitDuplicates() {
+      entrySetBinder.permitDuplicates();
+      return this;
+    }
+
     /**
      * This creates two bindings. One for the {@code Map.Entry<K, Provider<V>>}
      * and another for {@code V}.
@@ -277,12 +292,14 @@
         private Map<K, Provider<V>> providerMap;
 
         @SuppressWarnings("unused")
-        @Inject void initialize() {
+        @Inject void initialize(Injector injector) {
           RealMapBinder.this.binder = null;
+          boolean permitDuplicates = entrySetBinder.permitsDuplicates(injector);
 
           Map<K, Provider<V>> providerMapMutable = new LinkedHashMap<K, Provider<V>>();
           for (Entry<K, Provider<V>> entry : entrySetProvider.get()) {
-            checkConfiguration(providerMapMutable.put(entry.getKey(), entry.getValue()) == null,
+            Provider<V> previous = providerMapMutable.put(entry.getKey(), entry.getValue());
+            checkConfiguration(previous == null || permitDuplicates,
                 "Map injection failed due to duplicated key \"%s\"", entry.getKey());
           }
 
diff --git a/extensions/multibindings/src/com/google/inject/multibindings/Multibinder.java b/extensions/multibindings/src/com/google/inject/multibindings/Multibinder.java
index 3ddc443..70ee9ce 100644
--- a/extensions/multibindings/src/com/google/inject/multibindings/Multibinder.java
+++ b/extensions/multibindings/src/com/google/inject/multibindings/Multibinder.java
@@ -16,6 +16,7 @@
 
 package com.google.inject.multibindings;
 
+import com.google.inject.AbstractModule;
 import com.google.inject.Binder;
 import com.google.inject.Binding;
 import com.google.inject.ConfigurationException;
@@ -30,6 +31,7 @@
 import com.google.inject.internal.ImmutableList;
 import com.google.inject.internal.ImmutableSet;
 import com.google.inject.internal.Lists;
+import static com.google.inject.name.Names.named;
 import com.google.inject.spi.Dependency;
 import com.google.inject.spi.HasDependencies;
 import com.google.inject.spi.Message;
@@ -159,6 +161,15 @@
   }
 
   /**
+   * Configures the bound set to silently discard duplicate elements. When multiple equal values are
+   * bound, the one that gets included is arbitrary. When multible modules contribute elements to
+   * the set, this configuration option impacts all of them.
+   *
+   * @return this multibinder
+   */
+  public abstract Multibinder<T> 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.
@@ -197,6 +208,7 @@
     private final TypeLiteral<T> elementType;
     private final String setName;
     private final Key<Set<T>> setKey;
+    private final Key<Boolean> permitDuplicatesKey;
 
     /* the target injector's binder. non-null until initialization, null afterwards */
     private Binder binder;
@@ -205,12 +217,16 @@
     private List<Provider<T>> providers;
     private Set<Dependency<?>> dependencies;
 
+    /** whether duplicates are allowed. Possibly configured by a different instance */
+    private boolean permitDuplicates;
+
     private RealMultibinder(Binder binder, TypeLiteral<T> elementType,
         String setName, Key<Set<T>> setKey) {
       this.binder = checkNotNull(binder, "binder");
       this.elementType = checkNotNull(elementType, "elementType");
       this.setName = checkNotNull(setName, "setName");
       this.setKey = checkNotNull(setKey, "setKey");
+      this.permitDuplicatesKey = Key.get(Boolean.class, named(toString() + " permits duplicates"));
     }
 
     @SuppressWarnings("unchecked")
@@ -220,6 +236,11 @@
       binder.bind(setKey).toProvider(this);
     }
 
+    public Multibinder<T> permitDuplicates() {
+      binder.install(new PermitDuplicatesModule(permitDuplicatesKey));
+      return this;
+    }
+
     @Override public LinkedBindingBuilder<T> addBinding() {
       checkConfiguration(!isInitialized(), "Multibinder was already initialized");
 
@@ -245,9 +266,14 @@
       }
 
       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
@@ -265,7 +291,7 @@
       for (Provider<T> provider : providers) {
         final T newValue = provider.get();
         checkConfiguration(newValue != null, "Set injection failed due to null element");
-        checkConfiguration(result.add(newValue),
+        checkConfiguration(result.add(newValue) || permitDuplicates,
             "Set injection failed due to duplicated element \"%s\"", newValue);
       }
       return Collections.unmodifiableSet(result);
@@ -303,6 +329,32 @@
     }
   }
 
+  /**
+   * We install the permit duplicates configuration as its own binding, all by itself. This way,
+   * if only half of a multibinder user's remember to call permitDuplicates(), they're still
+   * permitted.
+   */
+  private static class PermitDuplicatesModule extends AbstractModule {
+    private final Key<Boolean> key;
+
+    PermitDuplicatesModule(Key<Boolean> key) {
+      this.key = key;
+    }
+
+    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();
+    }
+  }
+
   static void checkConfiguration(boolean condition, String format, Object... args) {
     if (condition) {
       return;
diff --git a/extensions/multibindings/test/com/google/inject/multibindings/MapBinderTest.java b/extensions/multibindings/test/com/google/inject/multibindings/MapBinderTest.java
index c40ddd8..de63c3c 100644
--- a/extensions/multibindings/test/com/google/inject/multibindings/MapBinderTest.java
+++ b/extensions/multibindings/test/com/google/inject/multibindings/MapBinderTest.java
@@ -233,6 +233,29 @@
     }
   }
 
+  public void testMapBinderMapPermitDuplicateElements() {
+    Injector injector = 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("b").toInstance("B");
+          }
+        },
+        new AbstractModule() {
+          @Override protected void configure() {
+            MapBinder<String, String> multibinder = MapBinder.newMapBinder(
+                binder(), String.class, String.class);
+            multibinder.addBinding("b").toInstance("B");
+            multibinder.addBinding("c").toInstance("C");
+            multibinder.permitDuplicates();
+          }
+        });
+
+    assertEquals(mapOf("a", "A", "b", "B", "c", "C"), injector.getInstance(Key.get(mapOfString)));
+  }
+
   public void testMapBinderMapForbidsNullKeys() {
     try {
       Guice.createInjector(new AbstractModule() {
diff --git a/extensions/multibindings/test/com/google/inject/multibindings/MultibinderTest.java b/extensions/multibindings/test/com/google/inject/multibindings/MultibinderTest.java
index d2dc32a..f65a125 100644
--- a/extensions/multibindings/test/com/google/inject/multibindings/MultibinderTest.java
+++ b/extensions/multibindings/test/com/google/inject/multibindings/MultibinderTest.java
@@ -229,6 +229,49 @@
           "1) Set injection failed due to duplicated element \"A\"");
     }
   }
+  
+  public void testMultibinderSetPermitDuplicateElements() {
+    Injector injector = Guice.createInjector(
+        new AbstractModule() {
+          protected void configure() {
+            Multibinder<String> multibinder = Multibinder.newSetBinder(binder(), String.class);
+            multibinder.addBinding().toInstance("A");
+            multibinder.addBinding().toInstance("B");
+          }
+        },
+        new AbstractModule() {
+          protected void configure() {
+            Multibinder<String> multibinder = Multibinder.newSetBinder(binder(), String.class);
+            multibinder.permitDuplicates();
+            multibinder.addBinding().toInstance("B");
+            multibinder.addBinding().toInstance("C");
+          }
+        });
+
+    assertEquals(setOf("A", "B", "C"), injector.getInstance(Key.get(setOfString)));
+  }
+
+  public void testMultibinderSetPermitDuplicateCallsToPermitDuplicates() {
+    Injector injector = Guice.createInjector(
+        new AbstractModule() {
+          protected void configure() {
+            Multibinder<String> multibinder = Multibinder.newSetBinder(binder(), String.class);
+            multibinder.permitDuplicates();
+            multibinder.addBinding().toInstance("A");
+            multibinder.addBinding().toInstance("B");
+          }
+        },
+        new AbstractModule() {
+          protected void configure() {
+            Multibinder<String> multibinder = Multibinder.newSetBinder(binder(), String.class);
+            multibinder.permitDuplicates();
+            multibinder.addBinding().toInstance("B");
+            multibinder.addBinding().toInstance("C");
+          }
+        });
+
+    assertEquals(setOf("A", "B", "C"), injector.getInstance(Key.get(setOfString)));
+  }
 
   public void testMultibinderSetForbidsNullElements() {
     Injector injector = Guice.createInjector(new AbstractModule() {