diff options
author | sameb <sameb@google.com> | 2015-02-20 10:44:10 -0800 |
---|---|---|
committer | Sam Berlin <sameb@google.com> | 2015-02-24 18:28:17 -0500 |
commit | 0b33461e35fa3a769ce23a9812a80acdc281f62c (patch) | |
tree | 4ab012d38d46414f8cb194aad5df47d125ba0578 | |
parent | 54da0e3ca924a5040e88a1c067f9f6760a14b20b (diff) | |
download | guice-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
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()); + } + } +} |