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() {