aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorsameb <sameb@google.com>2015-02-20 10:44:10 -0800
committerSam Berlin <sameb@google.com>2015-02-24 18:28:17 -0500
commit0b33461e35fa3a769ce23a9812a80acdc281f62c (patch)
tree4ab012d38d46414f8cb194aad5df47d125ba0578
parent54da0e3ca924a5040e88a1c067f9f6760a14b20b (diff)
downloadguice-0b33461e35fa3a769ce23a9812a80acdc281f62c.tar.gz
Add a @ProvidesInto{Set,Map,Optional} & MultibindingsScanner that allow users
to annotate methods in a Module as elements that can contribute to a Multibinder, MapBinder, or OptionalBinder. ------------- Created by MOE: http://code.google.com/p/moe-java MOE_MIGRATED_REVID=86801706
-rw-r--r--extensions/multibindings/src/com/google/inject/multibindings/ClassMapKey.java35
-rw-r--r--extensions/multibindings/src/com/google/inject/multibindings/MapBinder.java38
-rw-r--r--extensions/multibindings/src/com/google/inject/multibindings/MapKey.java58
-rw-r--r--extensions/multibindings/src/com/google/inject/multibindings/Multibinder.java45
-rw-r--r--extensions/multibindings/src/com/google/inject/multibindings/MultibindingsScanner.java192
-rw-r--r--extensions/multibindings/src/com/google/inject/multibindings/OptionalBinder.java24
-rw-r--r--extensions/multibindings/src/com/google/inject/multibindings/ProvidesIntoMap.java59
-rw-r--r--extensions/multibindings/src/com/google/inject/multibindings/ProvidesIntoOptional.java63
-rw-r--r--extensions/multibindings/src/com/google/inject/multibindings/ProvidesIntoSet.java53
-rw-r--r--extensions/multibindings/src/com/google/inject/multibindings/StringMapKey.java35
-rw-r--r--extensions/multibindings/test/com/google/inject/multibindings/AllTests.java1
-rw-r--r--extensions/multibindings/test/com/google/inject/multibindings/ProvidesIntoTest.java373
12 files changed, 942 insertions, 34 deletions
diff --git a/extensions/multibindings/src/com/google/inject/multibindings/ClassMapKey.java b/extensions/multibindings/src/com/google/inject/multibindings/ClassMapKey.java
new file mode 100644
index 00000000..47c8c17d
--- /dev/null
+++ b/extensions/multibindings/src/com/google/inject/multibindings/ClassMapKey.java
@@ -0,0 +1,35 @@
+/**
+ * Copyright (C) 2015 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 static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+/**
+ * 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/extensions/multibindings/src/com/google/inject/multibindings/MapBinder.java b/extensions/multibindings/src/com/google/inject/multibindings/MapBinder.java
index 89df7713..fc3d74f5 100644
--- a/extensions/multibindings/src/com/google/inject/multibindings/MapBinder.java
+++ b/extensions/multibindings/src/com/google/inject/multibindings/MapBinder.java
@@ -143,7 +143,7 @@ public abstract class MapBinder<K, V> {
public static <K, V> MapBinder<K, V> newMapBinder(Binder binder,
TypeLiteral<K> keyType, TypeLiteral<V> valueType) {
binder = binder.skipSources(MapBinder.class, RealMapBinder.class);
- return newMapBinder(binder, keyType, valueType, Key.get(mapOf(keyType, valueType)),
+ return newRealMapBinder(binder, keyType, valueType, Key.get(mapOf(keyType, valueType)),
Multibinder.newSetBinder(binder, entryOfProviderOf(keyType, valueType)));
}
@@ -163,7 +163,7 @@ public abstract class MapBinder<K, V> {
public static <K, V> MapBinder<K, V> newMapBinder(Binder binder,
TypeLiteral<K> keyType, TypeLiteral<V> valueType, Annotation annotation) {
binder = binder.skipSources(MapBinder.class, RealMapBinder.class);
- return newMapBinder(binder, keyType, valueType,
+ return newRealMapBinder(binder, keyType, valueType,
Key.get(mapOf(keyType, valueType), annotation),
Multibinder.newSetBinder(binder, entryOfProviderOf(keyType, valueType), annotation));
}
@@ -184,7 +184,7 @@ public abstract class MapBinder<K, V> {
public static <K, V> MapBinder<K, V> newMapBinder(Binder binder, TypeLiteral<K> keyType,
TypeLiteral<V> valueType, Class<? extends Annotation> annotationType) {
binder = binder.skipSources(MapBinder.class, RealMapBinder.class);
- return newMapBinder(binder, keyType, valueType,
+ return newRealMapBinder(binder, keyType, valueType,
Key.get(mapOf(keyType, valueType), annotationType),
Multibinder.newSetBinder(binder, entryOfProviderOf(keyType, valueType), annotationType));
}
@@ -236,7 +236,19 @@ public abstract class MapBinder<K, V> {
Map.class, Entry.class, keyType.getType(), Types.providerOf(valueType.getType())));
}
- private static <K, V> MapBinder<K, V> newMapBinder(Binder binder,
+ // Note: We use valueTypeAndAnnotation effectively as a Pair<TypeLiteral, Annotation|Class>
+ // since it's an easy way to group a type and an optional annotation type or instance.
+ static <K, V> RealMapBinder<K, V> newRealMapBinder(Binder binder, TypeLiteral<K> keyType,
+ Key<V> valueTypeAndAnnotation) {
+ binder = binder.skipSources(MapBinder.class, RealMapBinder.class);
+ TypeLiteral<V> valueType = valueTypeAndAnnotation.getTypeLiteral();
+ return newRealMapBinder(binder, keyType, valueType,
+ valueTypeAndAnnotation.ofType(mapOf(keyType, valueType)),
+ Multibinder.newSetBinder(binder,
+ valueTypeAndAnnotation.ofType(entryOfProviderOf(keyType, valueType))));
+ }
+
+ private static <K, V> RealMapBinder<K, V> newRealMapBinder(Binder binder,
TypeLiteral<K> keyType, TypeLiteral<V> valueType, Key<Map<K, V>> mapKey,
Multibinder<Entry<K, Provider<V>>> entrySetBinder) {
RealMapBinder<K, V> mapBinder =
@@ -342,12 +354,8 @@ public abstract class MapBinder<K, V> {
multimapKey, providerMultimapKey, entrySetBinder.getSetKey()));
return this;
}
-
- /**
- * This creates two bindings. One for the {@code Map.Entry<K, Provider<V>>}
- * and another for {@code V}.
- */
- @Override public LinkedBindingBuilder<V> addBinding(K key) {
+
+ Key<V> getKeyForNewValue(K key) {
checkNotNull(key, "key");
checkConfiguration(!isInitialized(), "MapBinder was already initialized");
@@ -355,7 +363,15 @@ public abstract class MapBinder<K, V> {
new RealElement(entrySetBinder.getSetName(), MAPBINDER, keyType.toString()));
entrySetBinder.addBinding().toProvider(new ProviderMapEntry<K, V>(
key, binder.getProvider(valueKey), valueKey));
- return binder.bind(valueKey);
+ return valueKey;
+ }
+
+ /**
+ * This creates two bindings. One for the {@code Map.Entry<K, Provider<V>>}
+ * and another for {@code V}.
+ */
+ @Override public LinkedBindingBuilder<V> addBinding(K key) {
+ return binder.bind(getKeyForNewValue(key));
}
@Override public void configure(Binder binder) {
diff --git a/extensions/multibindings/src/com/google/inject/multibindings/MapKey.java b/extensions/multibindings/src/com/google/inject/multibindings/MapKey.java
new file mode 100644
index 00000000..bcb6a3b8
--- /dev/null
+++ b/extensions/multibindings/src/com/google/inject/multibindings/MapKey.java
@@ -0,0 +1,58 @@
+/**
+ * Copyright (C) 2015 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 static java.lang.annotation.ElementType.ANNOTATION_TYPE;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+/**
+ * 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}.
+ *
+ * <p>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:
+ *
+ * <pre>
+ * {@literal @}MapKey(unwrapValue = true)
+ * {@literal @}Retention(RUNTIME)
+ * public {@literal @}interface MyCustomEnumKey {
+ * MyCustomEnum value();
+ * }
+ * </pre>
+ *
+ * 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();
+}
diff --git a/extensions/multibindings/src/com/google/inject/multibindings/Multibinder.java b/extensions/multibindings/src/com/google/inject/multibindings/Multibinder.java
index 66a4951f..56433f78 100644
--- a/extensions/multibindings/src/com/google/inject/multibindings/Multibinder.java
+++ b/extensions/multibindings/src/com/google/inject/multibindings/Multibinder.java
@@ -121,10 +121,7 @@ public abstract class Multibinder<T> {
* itself bound with no binding annotation.
*/
public static <T> Multibinder<T> newSetBinder(Binder binder, TypeLiteral<T> type) {
- binder = binder.skipSources(RealMultibinder.class, Multibinder.class);
- RealMultibinder<T> result = new RealMultibinder<T>(binder, type, Key.get(setOf(type)));
- binder.install(result);
- return result;
+ return newRealSetBinder(binder, Key.get(type));
}
/**
@@ -132,7 +129,7 @@ public abstract class Multibinder<T> {
* itself bound with no binding annotation.
*/
public static <T> Multibinder<T> newSetBinder(Binder binder, Class<T> type) {
- return newSetBinder(binder, TypeLiteral.get(type));
+ return newRealSetBinder(binder, Key.get(type));
}
/**
@@ -141,11 +138,7 @@ public abstract class Multibinder<T> {
*/
public static <T> Multibinder<T> newSetBinder(
Binder binder, TypeLiteral<T> type, Annotation annotation) {
- binder = binder.skipSources(RealMultibinder.class, Multibinder.class);
- RealMultibinder<T> result =
- new RealMultibinder<T>(binder, type, Key.get(setOf(type), annotation));
- binder.install(result);
- return result;
+ return newRealSetBinder(binder, Key.get(type, annotation));
}
/**
@@ -154,7 +147,7 @@ public abstract class Multibinder<T> {
*/
public static <T> Multibinder<T> newSetBinder(
Binder binder, Class<T> type, Annotation annotation) {
- return newSetBinder(binder, TypeLiteral.get(type), annotation);
+ return newRealSetBinder(binder, Key.get(type, annotation));
}
/**
@@ -163,9 +156,24 @@ public abstract class Multibinder<T> {
*/
public static <T> Multibinder<T> newSetBinder(Binder binder, TypeLiteral<T> type,
Class<? extends Annotation> 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.
+ */
+ public static <T> Multibinder<T> newSetBinder(Binder binder, Key<T> key) {
+ return newRealSetBinder(binder, key);
+ }
+
+ /**
+ * Implementation of newSetBinder.
+ */
+ static <T> RealMultibinder<T> newRealSetBinder(Binder binder, Key<T> key) {
binder = binder.skipSources(RealMultibinder.class, Multibinder.class);
- RealMultibinder<T> result =
- new RealMultibinder<T>(binder, type, Key.get(setOf(type), annotationType));
+ RealMultibinder<T> result = new RealMultibinder<T>(binder, key.getTypeLiteral(),
+ key.ofType(setOf(key.getTypeLiteral())));
binder.install(result);
return result;
}
@@ -176,7 +184,7 @@ public abstract class Multibinder<T> {
*/
public static <T> Multibinder<T> newSetBinder(Binder binder, Class<T> type,
Class<? extends Annotation> annotationType) {
- return newSetBinder(binder, TypeLiteral.get(type), annotationType);
+ return newSetBinder(binder, Key.get(type, annotationType));
}
@SuppressWarnings("unchecked") // wrapping a T in a Set safely returns a Set<T>
@@ -295,11 +303,14 @@ public abstract class Multibinder<T> {
binder.install(new PermitDuplicatesModule(permitDuplicatesKey));
return this;
}
-
- @Override public LinkedBindingBuilder<T> addBinding() {
+
+ Key<T> getKeyForNewItem() {
checkConfiguration(!isInitialized(), "Multibinder was already initialized");
+ return Key.get(elementType, new RealElement(setName, MULTIBINDER, ""));
+ }
- return binder.bind(Key.get(elementType, new RealElement(setName, MULTIBINDER, "")));
+ @Override public LinkedBindingBuilder<T> addBinding() {
+ return binder.bind(getKeyForNewItem());
}
/**
diff --git a/extensions/multibindings/src/com/google/inject/multibindings/MultibindingsScanner.java b/extensions/multibindings/src/com/google/inject/multibindings/MultibindingsScanner.java
new file mode 100644
index 00000000..02fce320
--- /dev/null
+++ b/extensions/multibindings/src/com/google/inject/multibindings/MultibindingsScanner.java
@@ -0,0 +1,192 @@
+/**
+ * Copyright (C) 2015 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.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}.
+ *
+ * <p>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 class Scanner extends ModuleAnnotatedMethodScanner {
+ private static final Scanner INSTANCE = new Scanner();
+
+ @Override
+ public Set<? extends Class<? extends Annotation>> annotationClasses() {
+ return ImmutableSet.of(
+ ProvidesIntoSet.class, ProvidesIntoMap.class, ProvidesIntoOptional.class);
+ }
+
+ @SuppressWarnings({"unchecked", "rawtypes"}) // mapKey doesn't know its key type
+ @Override
+ public <T> Key<T> prepareMethod(Binder binder, Annotation annotation, Key<T> 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 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 TypeAndValue<T> {
+ final TypeLiteral<T> type;
+ final T value;
+
+ TypeAndValue(TypeLiteral<T> type, T value) {
+ this.type = type;
+ this.value = value;
+ }
+ }
+}
diff --git a/extensions/multibindings/src/com/google/inject/multibindings/OptionalBinder.java b/extensions/multibindings/src/com/google/inject/multibindings/OptionalBinder.java
index e4e12005..d18f974d 100644
--- a/extensions/multibindings/src/com/google/inject/multibindings/OptionalBinder.java
+++ b/extensions/multibindings/src/com/google/inject/multibindings/OptionalBinder.java
@@ -191,14 +191,18 @@ public abstract class OptionalBinder<T> {
private OptionalBinder() {}
public static <T> OptionalBinder<T> newOptionalBinder(Binder binder, Class<T> type) {
- return newOptionalBinder(binder, Key.get(type));
+ return newRealOptionalBinder(binder, Key.get(type));
}
public static <T> OptionalBinder<T> newOptionalBinder(Binder binder, TypeLiteral<T> type) {
- return newOptionalBinder(binder, Key.get(type));
+ return newRealOptionalBinder(binder, Key.get(type));
}
public static <T> OptionalBinder<T> newOptionalBinder(Binder binder, Key<T> type) {
+ return newRealOptionalBinder(binder, type);
+ }
+
+ static <T> RealOptionalBinder<T> newRealOptionalBinder(Binder binder, Key<T> type) {
binder = binder.skipSources(OptionalBinder.class, RealOptionalBinder.class);
RealOptionalBinder<T> optionalBinder = new RealOptionalBinder<T>(binder, type);
binder.install(optionalBinder);
@@ -349,16 +353,24 @@ public abstract class OptionalBinder<T> {
binder.bind(typeKey).toProvider(new RealDirectTypeProvider());
}
- @Override public LinkedBindingBuilder<T> setDefault() {
+ Key<T> getKeyForDefaultBinding() {
checkConfiguration(!isInitialized(), "already initialized");
addDirectTypeBinding(binder);
- return binder.bind(defaultKey);
+ return defaultKey;
}
- @Override public LinkedBindingBuilder<T> setBinding() {
+ @Override public LinkedBindingBuilder<T> setDefault() {
+ return binder.bind(getKeyForDefaultBinding());
+ }
+
+ Key<T> getKeyForActualBinding() {
checkConfiguration(!isInitialized(), "already initialized");
addDirectTypeBinding(binder);
- return binder.bind(actualKey);
+ return actualKey;
+ }
+
+ @Override public LinkedBindingBuilder<T> setBinding() {
+ return binder.bind(getKeyForActualBinding());
}
@Override public void configure(Binder binder) {
diff --git a/extensions/multibindings/src/com/google/inject/multibindings/ProvidesIntoMap.java b/extensions/multibindings/src/com/google/inject/multibindings/ProvidesIntoMap.java
new file mode 100644
index 00000000..b7147b4d
--- /dev/null
+++ b/extensions/multibindings/src/com/google/inject/multibindings/ProvidesIntoMap.java
@@ -0,0 +1,59 @@
+/**
+ * Copyright (C) 2015 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 static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import com.google.inject.Module;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+/**
+ * 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 contributes to. For example,
+ *
+ * <pre>
+ * {@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(); }
+ * </pre>
+ *
+ * will add two items to the {@code @Named("urls") Map<String, Plugin>} 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.
+ *
+ * <p>Because the key is specified as an annotation, only Strings, Classes, enums, primitive
+ * types and annotation instances are supported as keys.
+ *
+ * @author sameb@google.com (Sam Berlin)
+ * @since 4.0
+ */
+@Documented
+@Target(METHOD)
+@Retention(RUNTIME)
+public @interface ProvidesIntoMap {
+}
diff --git a/extensions/multibindings/src/com/google/inject/multibindings/ProvidesIntoOptional.java b/extensions/multibindings/src/com/google/inject/multibindings/ProvidesIntoOptional.java
new file mode 100644
index 00000000..384f2b76
--- /dev/null
+++ b/extensions/multibindings/src/com/google/inject/multibindings/ProvidesIntoOptional.java
@@ -0,0 +1,63 @@
+/**
+ * Copyright (C) 2015 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 static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import com.google.inject.Module;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+/**
+ * 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
+ * contributes to. For example,
+ *
+ * <pre>
+ * {@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(); }
+ * </pre>
+ *
+ * will set the default value of {@code @Named("url") Optional<String>} to foo's URL,
+ * and then override it to bar's URL.
+ *
+ * @author sameb@google.com (Sam Berlin)
+ * @since 4.0
+ */
+@Documented
+@Target(METHOD)
+@Retention(RUNTIME)
+public @interface ProvidesIntoOptional {
+ enum Type {
+ /** Corresponds to {@link OptionalBinder#setBinding}. */
+ ACTUAL,
+
+ /** Corresponds to {@link OptionalBinder#setDefault}. */
+ DEFAULT
+ }
+
+ /** Specifies if the binding is for the actual or default value. */
+ Type value();
+}
diff --git a/extensions/multibindings/src/com/google/inject/multibindings/ProvidesIntoSet.java b/extensions/multibindings/src/com/google/inject/multibindings/ProvidesIntoSet.java
new file mode 100644
index 00000000..6503ae73
--- /dev/null
+++ b/extensions/multibindings/src/com/google/inject/multibindings/ProvidesIntoSet.java
@@ -0,0 +1,53 @@
+/**
+ * Copyright (C) 2015 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 static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import com.google.inject.Module;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+/**
+ * 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
+ * contributes to. For example,
+ *
+ * <pre>
+ * {@literal @}ProvidesIntoSet
+ * {@literal @}Named("urls")
+ * String provideFooUrl(FooManager fm) { returm fm.getUrl(); }
+ *
+ * {@literal @}ProvidesIntoSet
+ * {@literal @}Named("urls")
+ * String provideBarUrl(BarManager bm) { return bm.getUrl(); }
+ * </pre>
+ *
+ * will add two items to the {@code @Named("urls") Set<String>} set. The items are bound as
+ * providers and will be evaluated at injection time.
+ *
+ * @author sameb@google.com (Sam Berlin)
+ * @since 4.0
+ */
+@Documented
+@Target(METHOD)
+@Retention(RUNTIME)
+public @interface ProvidesIntoSet {
+}
diff --git a/extensions/multibindings/src/com/google/inject/multibindings/StringMapKey.java b/extensions/multibindings/src/com/google/inject/multibindings/StringMapKey.java
new file mode 100644
index 00000000..dd4d99bc
--- /dev/null
+++ b/extensions/multibindings/src/com/google/inject/multibindings/StringMapKey.java
@@ -0,0 +1,35 @@
+/**
+ * Copyright (C) 2015 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 static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+/**
+ * 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/extensions/multibindings/test/com/google/inject/multibindings/AllTests.java b/extensions/multibindings/test/com/google/inject/multibindings/AllTests.java
index 9ea52db3..6806edde 100644
--- a/extensions/multibindings/test/com/google/inject/multibindings/AllTests.java
+++ b/extensions/multibindings/test/com/google/inject/multibindings/AllTests.java
@@ -30,6 +30,7 @@ public class AllTests {
suite.addTestSuite(MultibinderTest.class);
suite.addTestSuite(OptionalBinderTest.class);
suite.addTestSuite(RealElementTest.class);
+ suite.addTestSuite(ProvidesIntoTest.class);
return suite;
}
}
diff --git a/extensions/multibindings/test/com/google/inject/multibindings/ProvidesIntoTest.java b/extensions/multibindings/test/com/google/inject/multibindings/ProvidesIntoTest.java
new file mode 100644
index 00000000..62c7a58b
--- /dev/null
+++ b/extensions/multibindings/test/com/google/inject/multibindings/ProvidesIntoTest.java
@@ -0,0 +1,373 @@
+/**
+ * Copyright (C) 2015 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 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;
+
+/**
+ * Tests the various @ProvidesInto annotations.
+ *
+ * @author sameb@google.com (Sam Berlin)
+ */
+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(ProvidesIntoOptional.Type.DEFAULT)
+ @Named("foo")
+ String optionalDefaultFoo() { return "foo"; }
+
+ @ProvidesIntoOptional(ProvidesIntoOptional.Type.ACTUAL)
+ @Named("foo")
+ String optionalActualFoo() { return "foo2"; }
+
+ @ProvidesIntoOptional(ProvidesIntoOptional.Type.DEFAULT)
+ @Named("bar")
+ String optionalDefaultBar() { return "bar"; }
+
+ @ProvidesIntoOptional(ProvidesIntoOptional.Type.ACTUAL)
+ String optionalActualBar() { return "na2"; }
+ });
+
+ Set<String> fooSet = injector.getInstance(new Key<Set<String>>(named("foo")) {});
+ assertEquals(ImmutableSet.of("foo", "foo2"), fooSet);
+
+ Set<String> barSet = injector.getInstance(new Key<Set<String>>(named("bar")) {});
+ assertEquals(ImmutableSet.of("bar", "bar2"), barSet);
+
+ Set<String> noAnnotationSet = injector.getInstance(new Key<Set<String>>() {});
+ assertEquals(ImmutableSet.of("na", "na2"), noAnnotationSet);
+
+ Map<String, String> fooMap =
+ injector.getInstance(new Key<Map<String, String>>(named("foo")) {});
+ assertEquals(ImmutableMap.of("fooKey", "foo", "foo2Key", "foo2"), fooMap);
+
+ Map<Class<?>, String> barMap =
+ injector.getInstance(new Key<Map<Class<?>, String>>(named("bar")) {});
+ assertEquals(ImmutableMap.of(String.class, "bar", Number.class, "bar2"), barMap);
+
+ Map<TestEnum, String> noAnnotationMap =
+ injector.getInstance(new Key<Map<TestEnum, String>>() {});
+ assertEquals(ImmutableMap.of(TestEnum.A, "na", TestEnum.B, "na2"), noAnnotationMap);
+
+ Map<WrappedKey, Number> wrappedMap =
+ injector.getInstance(new Key<Map<WrappedKey, Number>>() {});
+ assertEquals(ImmutableMap.of(wrappedKeyFor(1), 11, wrappedKeyFor(2), 22), wrappedMap);
+
+ Optional<String> fooOptional =
+ injector.getInstance(new Key<Optional<String>>(named("foo")) {});
+ assertEquals("foo2", fooOptional.get());
+
+ Optional<String> barOptional =
+ injector.getInstance(new Key<Optional<String>>(named("bar")) {});
+ assertEquals("bar", barOptional.get());
+
+ Optional<String> noAnnotationOptional =
+ injector.getInstance(new Key<Optional<String>>() {});
+ 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<Set<String>>() {}));
+ }
+
+ @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<ArrayWrappedKey, String> map =
+ injector.getInstance(new Key<Map<ArrayWrappedKey, String>>() {});
+ 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());
+ }
+ }
+}