From e2e9034c3a69e8107d42a35b6702d9cfa295c724 Mon Sep 17 00:00:00 2001 From: Brendan Ralston Date: Sat, 24 Jan 2026 14:58:25 -0500 Subject: [PATCH] feat(multibindings): Enable spanning injectors for Multibinder, MapBinder, and OptionalBinder This commit introduces a new 'contributor' API for Multibinder, MapBinder, and OptionalBinder, allowing PrivateModules to contribute bindings to an extension point defined in a parent injector. Key Changes: - Introduced , , and methods. - Added to handle automatic key exposure in PrivateModules. - Updated , , and to support a non-binding container mode. - Fixed and to correctly unwrap instances from private injectors. - Added reproduction and verification tests: and . --- .../google/inject/internal/RealMapBinder.java | 146 ++++++++++++++---- .../inject/internal/RealMultibinder.java | 61 ++++++-- .../inject/internal/RealOptionalBinder.java | 133 ++++++++++------ .../inject/multibindings/MapBinder.java | 92 +++++++++++ .../inject/multibindings/Multibinder.java | 93 +++++++++++ .../inject/multibindings/OptionalBinder.java | 58 ++++++- .../MultibindingsSpanningInjectorsTest.java | 82 ++++++++++ .../OptionalBinderSpanningInjectorsTest.java | 84 ++++++++++ 8 files changed, 658 insertions(+), 91 deletions(-) create mode 100644 core/test/com/google/inject/multibindings/MultibindingsSpanningInjectorsTest.java create mode 100644 core/test/com/google/inject/multibindings/OptionalBinderSpanningInjectorsTest.java diff --git a/core/src/com/google/inject/internal/RealMapBinder.java b/core/src/com/google/inject/internal/RealMapBinder.java index 63703ff8e4..e95cafd313 100644 --- a/core/src/com/google/inject/internal/RealMapBinder.java +++ b/core/src/com/google/inject/internal/RealMapBinder.java @@ -38,6 +38,7 @@ import com.google.inject.spi.BindingTargetVisitor; import com.google.inject.spi.Dependency; import com.google.inject.spi.Element; +import com.google.inject.spi.ExposedBinding; import com.google.inject.spi.ProviderInstanceBinding; import com.google.inject.spi.ProviderWithExtensionVisitor; import com.google.inject.util.Types; @@ -119,6 +120,62 @@ public static RealMapBinder newRealMapBinder( binder, Key.get(entryOfProviderOf(keyType, valueType), annotationType))); } + /** + * Returns a new mapbinder that collects entries of {@code keyType}/{@code valueType} in a {@link + * Map} that is itself bound with {@code annotationType}. + */ + public static RealMapBinder newRealMapBinder( + Binder binder, + TypeLiteral keyType, + TypeLiteral valueType, + Class annotationType, + boolean bindMap) { + return newRealMapBinder( + binder, + keyType, + valueType, + Key.get(mapOf(keyType, valueType), annotationType), + RealMultibinder.newRealSetBinder( + binder, Key.get(entryOfProviderOf(keyType, valueType), annotationType), bindMap), + bindMap); + } + + /** + * Returns a new mapbinder that collects entries of {@code keyType}/{@code valueType} in a {@link + * Map} that is itself bound with {@code annotation}. + */ + public static RealMapBinder newRealMapBinder( + Binder binder, + TypeLiteral keyType, + TypeLiteral valueType, + Annotation annotation, + boolean bindMap) { + return newRealMapBinder( + binder, + keyType, + valueType, + Key.get(mapOf(keyType, valueType), annotation), + RealMultibinder.newRealSetBinder( + binder, Key.get(entryOfProviderOf(keyType, valueType), annotation), bindMap), + bindMap); + } + + /** + * Returns a new mapbinder that collects entries of {@code keyType}/{@code valueType} in a {@link + * Map} that is itself bound with no binding annotation. + */ + public static RealMapBinder newRealMapBinder( + Binder binder, TypeLiteral keyType, TypeLiteral valueType, boolean bindMap) { + return newRealMapBinder( + binder, + keyType, + valueType, + Key.get(mapOf(keyType, valueType)), + RealMultibinder.newRealSetBinder( + binder, Key.get(entryOfProviderOf(keyType, valueType)), bindMap), + bindMap); + } + @SuppressWarnings("unchecked") // a map of is safely a Map static TypeLiteral> mapOf(TypeLiteral keyType, TypeLiteral valueType) { return (TypeLiteral>) @@ -240,8 +297,18 @@ private static RealMapBinder newRealMapBinder( TypeLiteral valueType, Key> mapKey, RealMultibinder>> entrySetBinder) { + return newRealMapBinder(binder, keyType, valueType, mapKey, entrySetBinder, true); + } + + private static RealMapBinder newRealMapBinder( + Binder binder, + TypeLiteral keyType, + TypeLiteral valueType, + Key> mapKey, + RealMultibinder>> entrySetBinder, + boolean bindMap) { RealMapBinder mapBinder = - new RealMapBinder<>(binder, keyType, valueType, mapKey, entrySetBinder); + new RealMapBinder<>(binder, keyType, valueType, mapKey, entrySetBinder, bindMap); binder.install(mapBinder); return mapBinder; } @@ -253,6 +320,8 @@ private static RealMapBinder newRealMapBinder( private final BindingSelection bindingSelection; private final Binder binder; + private final boolean bindMap; + private boolean permitSpanInjectors; private final RealMultibinder>> entrySetBinder; @@ -261,10 +330,12 @@ private RealMapBinder( TypeLiteral keyType, TypeLiteral valueType, Key> mapKey, - RealMultibinder>> entrySetBinder) { + RealMultibinder>> entrySetBinder, + boolean bindMap) { this.bindingSelection = new BindingSelection<>(keyType, valueType, mapKey, entrySetBinder); this.binder = binder; this.entrySetBinder = entrySetBinder; + this.bindMap = bindMap; } public void permitDuplicates() { @@ -273,6 +344,11 @@ public void permitDuplicates() { binder.install(new MultimapBinder(bindingSelection)); } + public void permitSpanInjectors() { + this.permitSpanInjectors = true; + entrySetBinder.permitSpanInjectors(); + } + /** Adds a binding to the map for the given key. */ Key getKeyForNewValue(K key) { checkNotNull(key, "key"); @@ -294,7 +370,12 @@ Key getKeyForNewValue(K key) { * V}. */ public LinkedBindingBuilder addBinding(K key) { - return binder.bind(getKeyForNewValue(key)); + Key valueKey = getKeyForNewValue(key); + LinkedBindingBuilder builder = binder.bind(valueKey); + if (permitSpanInjectors && binder instanceof com.google.inject.PrivateBinder) { + ((com.google.inject.PrivateBinder) binder).expose(valueKey); + } + return builder; } @SuppressWarnings({"unchecked", "rawtypes"}) // we use raw Key to link bindings. @@ -302,32 +383,34 @@ public LinkedBindingBuilder addBinding(K key) { public void configure(Binder binder) { checkConfiguration(!bindingSelection.isInitialized(), "MapBinder was already initialized"); - // Binds a Map> - binder - .bind(bindingSelection.getProviderMapKey()) - .toProvider(new RealProviderMapProvider<>(bindingSelection)); - - // The map this exposes is internally an ImmutableMap, so it's OK to massage - // the guice Provider to jakarta Provider in the value (since Guice provider - // implements jakarta Provider). - binder - .bind(bindingSelection.getJakartaProviderMapKey()) - .to((Key) bindingSelection.getProviderMapKey()); - - // Bind Map to the provider w/ extension support. - binder - .bind(bindingSelection.getMapKey()) - .toProvider(new ExtensionRealMapProvider<>(bindingSelection)); - // Bind Map to the provider w/o the extension support. - binder - .bind(bindingSelection.getMapOfKeyExtendsValueKey()) - .to((Key) bindingSelection.getMapKey()); - - // The Map.Entries are all ProviderMapEntry instances which do not allow setValue, so it is - // safe to massage the return type like this - binder - .bind(bindingSelection.getEntrySetJakartaProviderKey()) - .to((Key) bindingSelection.getEntrySetBinder().getSetKey()); + if (bindMap) { + // Binds a Map> + binder + .bind(bindingSelection.getProviderMapKey()) + .toProvider(new RealProviderMapProvider<>(bindingSelection)); + + // The map this exposes is internally an ImmutableMap, so it's OK to massage + // the guice Provider to jakarta Provider in the value (since Guice provider + // implements jakarta Provider). + binder + .bind(bindingSelection.getJakartaProviderMapKey()) + .to((Key) bindingSelection.getProviderMapKey()); + + // Bind Map to the provider w/ extension support. + binder + .bind(bindingSelection.getMapKey()) + .toProvider(new ExtensionRealMapProvider<>(bindingSelection)); + // Bind Map to the provider w/o the extension support. + binder + .bind(bindingSelection.getMapOfKeyExtendsValueKey()) + .to((Key) bindingSelection.getMapKey()); + + // The Map.Entries are all ProviderMapEntry instances which do not allow setValue, so it is + // safe to massage the return type like this + binder + .bind(bindingSelection.getEntrySetJakartaProviderKey()) + .to((Key) bindingSelection.getEntrySetBinder().getSetKey()); + } } @Override @@ -443,6 +526,11 @@ private boolean tryInitialize(InjectorImpl injector, Errors errors) { injector.findBindingsByType(entrySetBinder.getElementTypeLiteral())) { if (entrySetBinder.containsElement(binding)) { + if (binding instanceof ExposedBinding) { + ExposedBinding>> exposed = (ExposedBinding) binding; + binding = exposed.getPrivateElements().getInjector().getBinding(binding.getKey()); + } + // Protected by findBindingByType() and the fact that all providers are added by us // in addBinding(). It would theoretically be possible for someone to directly // add their own binding to the entrySetBinder, but they shouldn't do that. diff --git a/core/src/com/google/inject/internal/RealMultibinder.java b/core/src/com/google/inject/internal/RealMultibinder.java index 8953da4550..6f736dbb8b 100644 --- a/core/src/com/google/inject/internal/RealMultibinder.java +++ b/core/src/com/google/inject/internal/RealMultibinder.java @@ -64,6 +64,15 @@ public static RealMultibinder newRealSetBinder(Binder binder, Key key) return result; } + /** + * Implementation of newSetBinder that avoids binding the set itself. + */ + public static RealMultibinder newRealSetBinder(Binder binder, Key key, boolean bindSet) { + RealMultibinder result = new RealMultibinder<>(binder, key, bindSet); + binder.install(result); + return result; + } + @SuppressWarnings("unchecked") // wrapping a T in a Set safely returns a Set static TypeLiteral> setOf(TypeLiteral elementType) { Type type = Types.setOf(elementType.getType()); @@ -96,10 +105,17 @@ static TypeLiteral> setOfExtendsOf(TypeLiteral elementTy private final BindingSelection bindingSelection; private final Binder binder; + private final boolean bindSet; + private boolean permitSpanInjectors; RealMultibinder(Binder binder, Key key) { + this(binder, key, true); + } + + RealMultibinder(Binder binder, Key key, boolean bindSet) { this.binder = checkNotNull(binder, "binder"); this.bindingSelection = new BindingSelection<>(key); + this.bindSet = bindSet; } @SuppressWarnings({"unchecked", "rawtypes"}) // we use raw Key to link bindings together. @@ -107,28 +123,34 @@ static TypeLiteral> setOfExtendsOf(TypeLiteral elementTy public void configure(Binder binder) { checkConfiguration(!bindingSelection.isInitialized(), "Multibinder was already initialized"); - // Bind the setKey to the provider wrapped w/ extension support. - binder - .bind(bindingSelection.getSetKey()) - .toProvider(new RealMultibinderProvider<>(bindingSelection)); - binder.bind(bindingSelection.getSetOfExtendsKey()).to(bindingSelection.getSetKey()); - - binder - .bind(bindingSelection.getCollectionOfProvidersKey()) - .toProvider(new RealMultibinderCollectionOfProvidersProvider(bindingSelection)); - - // The collection this exposes is internally an ImmutableList, so it's OK to massage - // the guice Provider to jakarta Provider in the value (since the guice Provider implements - // jakarta Provider). - binder - .bind(bindingSelection.getCollectionOfJakartaProvidersKey()) - .to((Key) bindingSelection.getCollectionOfProvidersKey()); + if (bindSet) { + // Bind the setKey to the provider wrapped w/ extension support. + binder + .bind(bindingSelection.getSetKey()) + .toProvider(new RealMultibinderProvider<>(bindingSelection)); + binder.bind(bindingSelection.getSetOfExtendsKey()).to(bindingSelection.getSetKey()); + + binder + .bind(bindingSelection.getCollectionOfProvidersKey()) + .toProvider(new RealMultibinderCollectionOfProvidersProvider(bindingSelection)); + + // The collection this exposes is internally an ImmutableList, so it's OK to massage + // the guice Provider to jakarta Provider in the value (since the guice Provider implements + // jakarta Provider). + binder + .bind(bindingSelection.getCollectionOfJakartaProvidersKey()) + .to((Key) bindingSelection.getCollectionOfProvidersKey()); + } } public void permitDuplicates() { binder.install(new PermitDuplicatesModule(bindingSelection.getPermitDuplicatesKey())); } + public void permitSpanInjectors() { + this.permitSpanInjectors = true; + } + /** Adds a new entry to the set and returns the key for it. */ Key getKeyForNewItem() { checkConfiguration(!bindingSelection.isInitialized(), "Multibinder was already initialized"); @@ -138,7 +160,12 @@ Key getKeyForNewItem() { } public LinkedBindingBuilder addBinding() { - return binder.bind(getKeyForNewItem()); + Key key = getKeyForNewItem(); + LinkedBindingBuilder builder = binder.bind(key); + if (permitSpanInjectors && binder instanceof com.google.inject.PrivateBinder) { + ((com.google.inject.PrivateBinder) binder).expose(key); + } + return builder; } // These methods are used by RealMapBinder diff --git a/core/src/com/google/inject/internal/RealOptionalBinder.java b/core/src/com/google/inject/internal/RealOptionalBinder.java index 49415cee02..af4fffa0c4 100644 --- a/core/src/com/google/inject/internal/RealOptionalBinder.java +++ b/core/src/com/google/inject/internal/RealOptionalBinder.java @@ -40,6 +40,7 @@ import com.google.inject.spi.BindingTargetVisitor; import com.google.inject.spi.Dependency; import com.google.inject.spi.Element; +import com.google.inject.spi.ExposedBinding; import com.google.inject.spi.ProviderInstanceBinding; import com.google.inject.spi.ProviderWithExtensionVisitor; import com.google.inject.util.Types; @@ -59,8 +60,18 @@ */ public final class RealOptionalBinder implements Module { public static RealOptionalBinder newRealOptionalBinder(Binder binder, Key type) { + return newRealOptionalBinder(binder, type, true); + } + + /** + * Returns a new OptionalBinder. + * + * @param bindOptional if true, the Optional and java.util.Optional keys will be bound. + */ + public static RealOptionalBinder newRealOptionalBinder( + Binder binder, Key type, boolean bindOptional) { binder = binder.skipSources(RealOptionalBinder.class); - RealOptionalBinder optionalBinder = new RealOptionalBinder<>(binder, type); + RealOptionalBinder optionalBinder = new RealOptionalBinder<>(binder, type, bindOptional); binder.install(optionalBinder); return optionalBinder; } @@ -140,10 +151,13 @@ enum Source { private final BindingSelection bindingSelection; private final Binder binder; + private final boolean bindOptional; + private boolean permitSpanInjectors; - private RealOptionalBinder(Binder binder, Key typeKey) { + private RealOptionalBinder(Binder binder, Key typeKey, boolean bindOptional) { this.bindingSelection = new BindingSelection<>(typeKey); this.binder = binder; + this.bindOptional = bindOptional; } /** @@ -164,12 +178,19 @@ private void addDirectTypeBinding(Binder binder) { */ Key getKeyForDefaultBinding() { bindingSelection.checkNotInitialized(); - addDirectTypeBinding(binder); + if (bindOptional) { + addDirectTypeBinding(binder); + } return bindingSelection.getKeyForDefaultBinding(); } public LinkedBindingBuilder setDefault() { - return binder.bind(getKeyForDefaultBinding()); + Key key = getKeyForDefaultBinding(); + LinkedBindingBuilder builder = binder.bind(key); + if (permitSpanInjectors && binder instanceof com.google.inject.PrivateBinder) { + ((com.google.inject.PrivateBinder) binder).expose(key); + } + return builder; } /** @@ -180,56 +201,69 @@ public LinkedBindingBuilder setDefault() { */ Key getKeyForActualBinding() { bindingSelection.checkNotInitialized(); - addDirectTypeBinding(binder); + if (bindOptional) { + addDirectTypeBinding(binder); + } return bindingSelection.getKeyForActualBinding(); } public LinkedBindingBuilder setBinding() { - return binder.bind(getKeyForActualBinding()); + Key key = getKeyForActualBinding(); + LinkedBindingBuilder builder = binder.bind(key); + if (permitSpanInjectors && binder instanceof com.google.inject.PrivateBinder) { + ((com.google.inject.PrivateBinder) binder).expose(key); + } + return builder; + } + + public void permitSpanInjectors() { + this.permitSpanInjectors = true; } @SuppressWarnings({"unchecked", "rawtypes"}) // we use raw Key to link bindings together. @Override public void configure(Binder binder) { bindingSelection.checkNotInitialized(); - Key key = bindingSelection.getDirectKey(); - TypeLiteral typeLiteral = key.getTypeLiteral(); - // Every OptionalBinder gets the following types bound - // * {cgcb,ju}.Optional> - // * {cgcb,ju}.Optional> - // * {cgcb,ju}.Optional - // If setDefault() or setBinding() is called then also - // * T is bound - - // cgcb.Optional> - Key>> guavaOptProviderKey = key.ofType(optionalOfProvider(typeLiteral)); - binder - .bind(guavaOptProviderKey) - .toProvider(new RealOptionalProviderProvider<>(bindingSelection)); - // ju.Optional> - Key>> javaOptProviderKey = - key.ofType(javaOptionalOfProvider(typeLiteral)); - binder - .bind(javaOptProviderKey) - .toProvider(new JavaOptionalProviderProvider<>(bindingSelection)); - - // Provider is assignable to jakarta.inject.Provider and the provider that the factory contains - // cannot be modified so we can use some rawtypes hackery to share the same implementation. - // cgcb.Optional> - binder.bind(key.ofType(optionalOfJakartaProvider(typeLiteral))).to((Key) guavaOptProviderKey); - // ju.Optional> - binder - .bind(key.ofType(javaOptionalOfJakartaProvider(typeLiteral))) - .to((Key) javaOptProviderKey); - - // cgcb.Optional - Key> guavaOptKey = key.ofType(optionalOf(typeLiteral)); - binder - .bind(guavaOptKey) - .toProvider(new RealOptionalKeyProvider<>(bindingSelection, guavaOptKey)); - // ju.Optional - Key> javaOptKey = key.ofType(javaOptionalOf(typeLiteral)); - binder.bind(javaOptKey).toProvider(new JavaOptionalProvider<>(bindingSelection, javaOptKey)); + if (bindOptional) { + Key key = bindingSelection.getDirectKey(); + TypeLiteral typeLiteral = key.getTypeLiteral(); + // Every OptionalBinder gets the following types bound + // * {cgcb,ju}.Optional> + // * {cgcb,ju}.Optional> + // * {cgcb,ju}.Optional + // If setDefault() or setBinding() is called then also + // * T is bound + + // cgcb.Optional> + Key>> guavaOptProviderKey = key.ofType(optionalOfProvider(typeLiteral)); + binder + .bind(guavaOptProviderKey) + .toProvider(new RealOptionalProviderProvider<>(bindingSelection)); + // ju.Optional> + Key>> javaOptProviderKey = + key.ofType(javaOptionalOfProvider(typeLiteral)); + binder + .bind(javaOptProviderKey) + .toProvider(new JavaOptionalProviderProvider<>(bindingSelection)); + + // Provider is assignable to jakarta.inject.Provider and the provider that the factory contains + // cannot be modified so we can use some rawtypes hackery to share the same implementation. + // cgcb.Optional> + binder.bind(key.ofType(optionalOfJakartaProvider(typeLiteral))).to((Key) guavaOptProviderKey); + // ju.Optional> + binder + .bind(key.ofType(javaOptionalOfJakartaProvider(typeLiteral))) + .to((Key) javaOptProviderKey); + + // cgcb.Optional + Key> guavaOptKey = key.ofType(optionalOf(typeLiteral)); + binder + .bind(guavaOptKey) + .toProvider(new RealOptionalKeyProvider<>(bindingSelection, guavaOptKey)); + // ju.Optional + Key> javaOptKey = key.ofType(javaOptionalOf(typeLiteral)); + binder.bind(javaOptKey).toProvider(new JavaOptionalProvider<>(bindingSelection, javaOptKey)); + } } /** Provides the binding for {@code java.util.Optional}. */ @@ -662,7 +696,18 @@ void initialize(InjectorImpl injector, Errors errors) throws ErrorsException { } state = InitializationState.INITIALIZING; actualBinding = injector.getExistingBinding(getKeyForActualBinding()); + if (actualBinding instanceof ExposedBinding) { + ExposedBinding exposed = (ExposedBinding) actualBinding; + actualBinding = + (BindingImpl) exposed.getPrivateElements().getInjector().getBinding(actualBinding.getKey()); + } defaultBinding = injector.getExistingBinding(getKeyForDefaultBinding()); + if (defaultBinding instanceof ExposedBinding) { + ExposedBinding exposed = (ExposedBinding) defaultBinding; + defaultBinding = + (BindingImpl) + exposed.getPrivateElements().getInjector().getBinding(defaultBinding.getKey()); + } // We should never create Jit bindings, but we can use them if some other binding created it. BindingImpl userBinding = injector.getExistingBinding(key); if (actualBinding != null) { diff --git a/core/src/com/google/inject/multibindings/MapBinder.java b/core/src/com/google/inject/multibindings/MapBinder.java index 0d534ba788..6b53bc2d9b 100644 --- a/core/src/com/google/inject/multibindings/MapBinder.java +++ b/core/src/com/google/inject/multibindings/MapBinder.java @@ -154,6 +154,86 @@ public static MapBinder newMapBinder( binder, TypeLiteral.get(keyType), TypeLiteral.get(valueType), annotationType); } + /** + * Returns a new mapbinder that contributes to an existing map binding (typically in a parent + * injector) without creating a new map binding itself. This is necessary when contributing to a + * MapBinder from a PrivateModule where the parent injector already has the MapBinder. + * + * @since 7.0 + */ + public static MapBinder newMapContributor( + Binder binder, TypeLiteral keyType, TypeLiteral valueType) { + return new MapBinder(newRealMapBinder(binder, keyType, valueType, false)); + } + + /** + * Returns a new mapbinder that contributes to an existing map binding (typically in a parent + * injector) without creating a new map binding itself. This is necessary when contributing to a + * MapBinder from a PrivateModule where the parent injector already has the MapBinder. + * + * @since 7.0 + */ + public static MapBinder newMapContributor( + Binder binder, Class keyType, Class valueType) { + return newMapContributor(binder, TypeLiteral.get(keyType), TypeLiteral.get(valueType)); + } + + /** + * Returns a new mapbinder that contributes to an existing map binding (typically in a parent + * injector) without creating a new map binding itself. This is necessary when contributing to a + * MapBinder from a PrivateModule where the parent injector already has the MapBinder. + * + * @since 7.0 + */ + public static MapBinder newMapContributor( + Binder binder, TypeLiteral keyType, TypeLiteral valueType, Annotation annotation) { + return new MapBinder(newRealMapBinder(binder, keyType, valueType, annotation, false)); + } + + /** + * Returns a new mapbinder that contributes to an existing map binding (typically in a parent + * injector) without creating a new map binding itself. This is necessary when contributing to a + * MapBinder from a PrivateModule where the parent injector already has the MapBinder. + * + * @since 7.0 + */ + public static MapBinder newMapContributor( + Binder binder, Class keyType, Class valueType, Annotation annotation) { + return newMapContributor( + binder, TypeLiteral.get(keyType), TypeLiteral.get(valueType), annotation); + } + + /** + * Returns a new mapbinder that contributes to an existing map binding (typically in a parent + * injector) without creating a new map binding itself. This is necessary when contributing to a + * MapBinder from a PrivateModule where the parent injector already has the MapBinder. + * + * @since 7.0 + */ + public static MapBinder newMapContributor( + Binder binder, + TypeLiteral keyType, + TypeLiteral valueType, + Class annotationType) { + return new MapBinder(newRealMapBinder(binder, keyType, valueType, annotationType, false)); + } + + /** + * Returns a new mapbinder that contributes to an existing map binding (typically in a parent + * injector) without creating a new map binding itself. This is necessary when contributing to a + * MapBinder from a PrivateModule where the parent injector already has the MapBinder. + * + * @since 7.0 + */ + public static MapBinder newMapContributor( + Binder binder, + Class keyType, + Class valueType, + Class annotationType) { + return newMapContributor( + binder, TypeLiteral.get(keyType), TypeLiteral.get(valueType), annotationType); + } + private final RealMapBinder delegate; private MapBinder(RealMapBinder delegate) { @@ -180,6 +260,18 @@ public MapBinder permitDuplicates() { return this; } + /** + * Configures the mapbinder to automatically expose the binding for each added entry. This is useful + * when contributing to a parent injector's MapBinder from a PrivateModule. + * + * @return this map binder + * @since 7.0 + */ + public MapBinder permitSpanInjectors() { + delegate.permitSpanInjectors(); + return this; + } + /** * 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. diff --git a/core/src/com/google/inject/multibindings/Multibinder.java b/core/src/com/google/inject/multibindings/Multibinder.java index a858655db1..39061d8eae 100644 --- a/core/src/com/google/inject/multibindings/Multibinder.java +++ b/core/src/com/google/inject/multibindings/Multibinder.java @@ -137,6 +137,87 @@ public static Multibinder newSetBinder(Binder binder, Key key) { return new Multibinder(newRealSetBinder(binder, key)); } + /** + * Returns a new multibinder that contributes to an existing set binding (typically in a parent + * injector) without creating a new set binding itself. This is necessary when contributing to a + * Multibinder from a PrivateModule where the parent injector already has the Multibinder. + * + * @since 7.0 + */ + public static Multibinder newSetContributor(Binder binder, TypeLiteral type) { + return newSetContributor(binder, Key.get(type)); + } + + /** + * Returns a new multibinder that contributes to an existing set binding (typically in a parent + * injector) without creating a new set binding itself. This is necessary when contributing to a + * Multibinder from a PrivateModule where the parent injector already has the Multibinder. + * + * @since 7.0 + */ + public static Multibinder newSetContributor(Binder binder, Class type) { + return newSetContributor(binder, Key.get(type)); + } + + /** + * Returns a new multibinder that contributes to an existing set binding (typically in a parent + * injector) without creating a new set binding itself. This is necessary when contributing to a + * Multibinder from a PrivateModule where the parent injector already has the Multibinder. + * + * @since 7.0 + */ + public static Multibinder newSetContributor( + Binder binder, TypeLiteral type, Annotation annotation) { + return newSetContributor(binder, Key.get(type, annotation)); + } + + /** + * Returns a new multibinder that contributes to an existing set binding (typically in a parent + * injector) without creating a new set binding itself. This is necessary when contributing to a + * Multibinder from a PrivateModule where the parent injector already has the Multibinder. + * + * @since 7.0 + */ + public static Multibinder newSetContributor( + Binder binder, Class type, Annotation annotation) { + return newSetContributor(binder, Key.get(type, annotation)); + } + + /** + * Returns a new multibinder that contributes to an existing set binding (typically in a parent + * injector) without creating a new set binding itself. This is necessary when contributing to a + * Multibinder from a PrivateModule where the parent injector already has the Multibinder. + * + * @since 7.0 + */ + public static Multibinder newSetContributor( + Binder binder, TypeLiteral type, Class annotationType) { + return newSetContributor(binder, Key.get(type, annotationType)); + } + + /** + * Returns a new multibinder that contributes to an existing set binding (typically in a parent + * injector) without creating a new set binding itself. This is necessary when contributing to a + * Multibinder from a PrivateModule where the parent injector already has the Multibinder. + * + * @since 7.0 + */ + public static Multibinder newSetContributor( + Binder binder, Class type, Class annotationType) { + return newSetContributor(binder, Key.get(type, annotationType)); + } + + /** + * Returns a new multibinder that contributes to an existing set binding (typically in a parent + * injector) without creating a new set binding itself. This is necessary when contributing to a + * Multibinder from a PrivateModule where the parent injector already has the Multibinder. + * + * @since 7.0 + */ + public static Multibinder newSetContributor(Binder binder, Key key) { + return new Multibinder(newRealSetBinder(binder, key, false)); + } + /** * Returns a new multibinder that collects instances of {@code type} in a {@link Set} that is * itself bound with {@code annotationType}. @@ -165,6 +246,18 @@ public Multibinder permitDuplicates() { return this; } + /** + * Configures the multibinder to automatically expose the binding for each added element. This is + * useful when contributing to a parent injector's Multibinder from a PrivateModule. + * + * @return this multibinder + * @since 7.0 + */ + public Multibinder permitSpanInjectors() { + delegate.permitSpanInjectors(); + return this; + } + /** * 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. diff --git a/core/src/com/google/inject/multibindings/OptionalBinder.java b/core/src/com/google/inject/multibindings/OptionalBinder.java index 4f1554e36a..7c7c0b3fa9 100644 --- a/core/src/com/google/inject/multibindings/OptionalBinder.java +++ b/core/src/com/google/inject/multibindings/OptionalBinder.java @@ -148,7 +148,51 @@ public static OptionalBinder newOptionalBinder(Binder binder, TypeLiteral } public static OptionalBinder newOptionalBinder(Binder binder, Key type) { - return new OptionalBinder(newRealOptionalBinder(binder, type)); + return newOptionalBinder(binder, type, true); + } + + /** + * Internal constructor for RealOptionalBinder to use. + */ + private static OptionalBinder newOptionalBinder( + Binder binder, Key type, boolean bindOptional) { + return new OptionalBinder(newRealOptionalBinder(binder, type, bindOptional)); + } + + /** + * Returns a new OptionalBinder that contributes to an existing OptionalBinder binding (typically + * in a parent injector) without creating a new OptionalBinder binding itself. This is necessary + * when contributing to an OptionalBinder from a PrivateModule where the parent injector already + * has the OptionalBinder. + * + * @since 7.0 + */ + public static OptionalBinder newOptionalContributor(Binder binder, Class type) { + return newOptionalContributor(binder, Key.get(type)); + } + + /** + * Returns a new OptionalBinder that contributes to an existing OptionalBinder binding (typically + * in a parent injector) without creating a new OptionalBinder binding itself. This is necessary + * when contributing to an OptionalBinder from a PrivateModule where the parent injector already + * has the OptionalBinder. + * + * @since 7.0 + */ + public static OptionalBinder newOptionalContributor(Binder binder, TypeLiteral type) { + return newOptionalContributor(binder, Key.get(type)); + } + + /** + * Returns a new OptionalBinder that contributes to an existing OptionalBinder binding (typically + * in a parent injector) without creating a new OptionalBinder binding itself. This is necessary + * when contributing to an OptionalBinder from a PrivateModule where the parent injector already + * has the OptionalBinder. + * + * @since 7.0 + */ + public static OptionalBinder newOptionalContributor(Binder binder, Key type) { + return newOptionalBinder(binder, type, false); } private final RealOptionalBinder delegate; @@ -157,6 +201,18 @@ private OptionalBinder(RealOptionalBinder delegate) { this.delegate = delegate; } + /** + * Configures the OptionalBinder to automatically expose the binding for each added entry. This is + * useful when contributing to a parent injector's OptionalBinder from a PrivateModule. + * + * @return this optional binder + * @since 7.0 + */ + public OptionalBinder permitSpanInjectors() { + delegate.permitSpanInjectors(); + return this; + } + /** * Returns a binding builder used to set the default value that will be injected. The binding set * by this method will be ignored if {@link #setBinding} is called. diff --git a/core/test/com/google/inject/multibindings/MultibindingsSpanningInjectorsTest.java b/core/test/com/google/inject/multibindings/MultibindingsSpanningInjectorsTest.java new file mode 100644 index 0000000000..13714f6f05 --- /dev/null +++ b/core/test/com/google/inject/multibindings/MultibindingsSpanningInjectorsTest.java @@ -0,0 +1,82 @@ +package com.google.inject.multibindings; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.inject.AbstractModule; +import com.google.inject.Guice; +import com.google.inject.Injector; +import com.google.inject.Key; +import com.google.inject.PrivateModule; +import com.google.inject.TypeLiteral; +import java.util.Map; +import java.util.Set; +import junit.framework.TestCase; + +public class MultibindingsSpanningInjectorsTest extends TestCase { + + public static class MainModule extends AbstractModule { + @Override + protected void configure() { + Multibinder.newSetBinder(binder(), String.class).addBinding().toInstance("Main"); + install(new ChildModule()); + } + } + + public static class ChildModule extends PrivateModule { + @Override + protected void configure() { + // Use the new API to contribute to the parent's Multibinder + Multibinder.newSetContributor(binder(), String.class) + .permitSpanInjectors() + .addBinding().toInstance("Private"); + } + } + + public void testPrivateModuleContributionExposed() { + Injector injector = Guice.createInjector(new MainModule()); + + Set set = injector.getInstance(Key.get(new TypeLiteral>() {})); + + assertThat(set).containsExactly("Main", "Private").inOrder(); + } + + public static class PrivateModuleA extends PrivateModule { + @Override protected void configure() { + Multibinder.newSetBinder(binder(), String.class).addBinding().toInstance("A"); + } + } + + public static class PrivateModuleB extends PrivateModule { + @Override protected void configure() { + Multibinder.newSetBinder(binder(), String.class).addBinding().toInstance("B"); + } + } + + public void testSiblings() { + Injector injector = Guice.createInjector(new PrivateModuleA(), new PrivateModuleB()); + // Siblings are isolated, so this just verifies we didn't break existing isolation/instantiation + } + + public static class MapMainModule extends AbstractModule { + @Override + protected void configure() { + MapBinder.newMapBinder(binder(), String.class, String.class).addBinding("KeyMain").toInstance("ValueMain"); + install(new MapChildModule()); + } + } + + public static class MapChildModule extends PrivateModule { + @Override + protected void configure() { + MapBinder.newMapContributor(binder(), String.class, String.class) + .permitSpanInjectors() + .addBinding("KeyPrivate").toInstance("ValuePrivate"); + } + } + + public void testMapBinderContribution() { + Injector injector = Guice.createInjector(new MapMainModule()); + Map map = injector.getInstance(Key.get(new TypeLiteral>() {})); + assertThat(map).containsExactly("KeyMain", "ValueMain", "KeyPrivate", "ValuePrivate"); + } +} diff --git a/core/test/com/google/inject/multibindings/OptionalBinderSpanningInjectorsTest.java b/core/test/com/google/inject/multibindings/OptionalBinderSpanningInjectorsTest.java new file mode 100644 index 0000000000..fabcd400f9 --- /dev/null +++ b/core/test/com/google/inject/multibindings/OptionalBinderSpanningInjectorsTest.java @@ -0,0 +1,84 @@ +package com.google.inject.multibindings; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.common.base.Optional; +import com.google.inject.AbstractModule; +import com.google.inject.Guice; +import com.google.inject.Injector; +import com.google.inject.Key; +import com.google.inject.PrivateModule; +import com.google.inject.TypeLiteral; +import junit.framework.TestCase; + +public class OptionalBinderSpanningInjectorsTest extends TestCase { + + public void testDefaultInParentActualInChild() { + Injector injector = Guice.createInjector( + new AbstractModule() { + @Override + protected void configure() { + OptionalBinder.newOptionalBinder(binder(), String.class).setDefault().toInstance("Default"); + + install(new PrivateModule() { + @Override + protected void configure() { + // Use new API to contribute to the parent's OptionalBinder + OptionalBinder.newOptionalContributor(binder(), String.class) + .permitSpanInjectors() + .setBinding().toInstance("Actual"); + } + }); + } + }); + + Optional optional = injector.getInstance(Key.get(new TypeLiteral>() {})); + assertThat(optional.get()).isEqualTo("Actual"); + } + + public void testNoDefaultInParentActualInChild() { + Injector injector = Guice.createInjector( + new AbstractModule() { + @Override + protected void configure() { + OptionalBinder.newOptionalBinder(binder(), String.class); + + install(new PrivateModule() { + @Override + protected void configure() { + OptionalBinder.newOptionalContributor(binder(), String.class) + .permitSpanInjectors() + .setBinding().toInstance("Actual"); + } + }); + } + }); + + Optional optional = injector.getInstance(Key.get(new TypeLiteral>() {})); + assertThat(optional.isPresent()).isTrue(); + assertThat(optional.get()).isEqualTo("Actual"); + } + + public void testDefaultInChildActualInParent() { + Injector injector = Guice.createInjector( + new AbstractModule() { + @Override + protected void configure() { + OptionalBinder.newOptionalBinder(binder(), String.class).setBinding().toInstance("Actual"); + + install(new PrivateModule() { + @Override + protected void configure() { + OptionalBinder.newOptionalContributor(binder(), String.class) + .permitSpanInjectors() + .setDefault().toInstance("Default"); + } + }); + } + }); + + Optional optional = injector.getInstance(Key.get(new TypeLiteral>() {})); + // Actual overrides Default even if Default is in child. + assertThat(optional.get()).isEqualTo("Actual"); + } +} \ No newline at end of file