diff --git a/src/main/java/org/springframework/guice/annotation/ModuleRegistryConfiguration.java b/src/main/java/org/springframework/guice/annotation/ModuleRegistryConfiguration.java index d38d5d8..a37d965 100644 --- a/src/main/java/org/springframework/guice/annotation/ModuleRegistryConfiguration.java +++ b/src/main/java/org/springframework/guice/annotation/ModuleRegistryConfiguration.java @@ -16,6 +16,9 @@ package org.springframework.guice.annotation; +import java.lang.annotation.Annotation; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -23,24 +26,30 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; -import java.util.Objects; import java.util.Optional; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Predicate; import java.util.stream.Collectors; +import java.util.stream.Stream; import com.google.inject.Binding; +import com.google.inject.ConfigurationException; import com.google.inject.Guice; import com.google.inject.Injector; import com.google.inject.Key; import com.google.inject.Module; import com.google.inject.Scopes; import com.google.inject.Stage; +import com.google.inject.TypeLiteral; +import com.google.inject.internal.BindingImpl; import com.google.inject.name.Named; import com.google.inject.spi.Element; import com.google.inject.spi.ElementSource; import com.google.inject.spi.Elements; import com.google.inject.spi.LinkedKeyBinding; +import com.google.inject.spi.Message; import com.google.inject.spi.PrivateElements; +import com.google.inject.spi.UntargettedBinding; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -86,12 +95,28 @@ class ModuleRegistryConfiguration implements BeanDefinitionRegistryPostProcessor private static final String SPRING_GUICE_STAGE_PROPERTY_NAME = "spring.guice.stage"; + private static final List SPRING_GUICE_IGNORED_ANNOTATION_PREFIXES = Arrays.asList( + "com.google.inject.multibindings", "com.google.inject.internal.Element", + "com.google.inject.internal.UniqueAnnotations", "com.google.inject.internal.RealOptionalBinder"); + private final Log logger = LogFactory.getLog(getClass()); private ApplicationContext applicationContext; private boolean enableJustInTimeBinding = true; + protected static final Method GUICE_BINDINGIMPL_WITHKEY; + + static { + try { + GUICE_BINDINGIMPL_WITHKEY = BindingImpl.class.getDeclaredMethod("withKey", Key.class); + GUICE_BINDINGIMPL_WITHKEY.setAccessible(true); + } + catch (NoSuchMethodException ex) { + throw new RuntimeException(ex); + } + } + @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; @@ -106,6 +131,11 @@ class ModuleRegistryConfiguration implements BeanDefinitionRegistryPostProcessor modules.add(new SpringModule((ConfigurableListableBeanFactory) registry, this.enableJustInTimeBinding)); Map, Binding> bindings = new HashMap, Binding>(); List elements = Elements.getElements(Stage.TOOL, modules); + List errors = elements.stream().filter((e) -> e instanceof Message).map((e) -> (Message) e) + .collect(Collectors.toList()); + if (!errors.isEmpty()) { + throw new ConfigurationException(errors); + } if (this.applicationContext.getEnvironment().getProperty(SPRING_GUICE_DEDUPE_BINDINGS_PROPERTY_NAME, Boolean.class, false)) { elements = removeDuplicates(elements); @@ -146,34 +176,53 @@ class ModuleRegistryConfiguration implements BeanDefinitionRegistryPostProcessor Stage stage = this.applicationContext.getEnvironment().getProperty(SPRING_GUICE_STAGE_PROPERTY_NAME, Stage.class, Stage.PRODUCTION); boolean ifLazyInit = stage.equals(Stage.DEVELOPMENT); - for (Entry, Binding> entry : bindings.entrySet()) { - if (entry.getKey().getTypeLiteral().getRawType().equals(Injector.class) || SpringModule.SPRING_GUICE_SOURCE - .equals(Optional.ofNullable(entry.getValue().getSource()).map(Object::toString).orElse(""))) { - continue; - } - if (entry.getKey().getAnnotationType() != null - && entry.getKey().getAnnotationType().getName().startsWith("com.google.inject.multibindings")) { - continue; - } - if (entry.getKey().getAnnotationType() != null && entry.getKey().getAnnotationType().getName() - .startsWith("com.google.inject.internal.UniqueAnnotations")) { - continue; - } + Map, List>> linkedBindingsByKey = bindings.values().stream() + .filter((e) -> e instanceof LinkedKeyBinding).map((e) -> ((LinkedKeyBinding) e)) + .collect(Collectors.groupingBy(LinkedKeyBinding::getLinkedKey)); + Map, ? extends Binding> guiceBindingsByKey = bindings.entrySet().stream() + .filter((entry) -> { + Binding binding = entry.getValue(); + Key key = entry.getKey(); + Object source = binding.getSource(); + + TypeLiteral typeLiteral = key.getTypeLiteral(); + Class annotationType = key.getAnnotationType(); + + if (binding instanceof UntargettedBinding && linkedBindingsByKey.containsKey(binding.getKey())) { + return false; + } + if (typeLiteral.getRawType().equals(Injector.class) || SpringModule.SPRING_GUICE_SOURCE + .equals(Optional.ofNullable(source).map(Object::toString).orElse(""))) { + return false; + } + if (annotationType != null) { + if (SPRING_GUICE_IGNORED_ANNOTATION_PREFIXES.stream() + .anyMatch((prefix) -> annotationType.getName().startsWith(prefix))) { + return false; + } + } + return true; + }).collect(Collectors.toMap(Entry::getKey, Entry::getValue)); + + for (Entry, ? extends Binding> entry : guiceBindingsByKey.entrySet()) { Binding binding = entry.getValue(); Key key = entry.getKey(); - Object source = binding.getSource(); + + TypeLiteral typeLiteral = key.getTypeLiteral(); + Class annotationType = key.getAnnotationType(); RootBeanDefinition bean = new RootBeanDefinition(GuiceFactoryBean.class); ConstructorArgumentValues args = new ConstructorArgumentValues(); - args.addIndexedArgumentValue(0, key.getTypeLiteral().getRawType()); + args.addIndexedArgumentValue(0, typeLiteral.getRawType()); args.addIndexedArgumentValue(1, key); args.addIndexedArgumentValue(2, Scopes.isSingleton(binding)); bean.setConstructorArgumentValues(args); - bean.setTargetType(ResolvableType.forType(key.getTypeLiteral().getType())); + bean.setTargetType(ResolvableType.forType(typeLiteral.getType())); if (!Scopes.isSingleton(binding)) { bean.setScope(ConfigurableBeanFactory.SCOPE_PROTOTYPE); } + Object source = binding.getSource(); if (source instanceof ElementSource) { bean.setResourceDescription(((ElementSource) source).getDeclaringSource().toString()); } @@ -181,10 +230,10 @@ class ModuleRegistryConfiguration implements BeanDefinitionRegistryPostProcessor bean.setResourceDescription(SpringModule.SPRING_GUICE_SOURCE); } bean.setAttribute(SpringModule.SPRING_GUICE_SOURCE, true); - if (key.getAnnotationType() != null) { - bean.addQualifier(new AutowireCandidateQualifier(Qualifier.class, getValueAttributeForNamed(key))); - bean.addQualifier( - new AutowireCandidateQualifier(key.getAnnotationType(), getValueAttributeForNamed(key))); + if (annotationType != null) { + String nameValue = getValueAttributeForNamed(key); + bean.addQualifier(new AutowireCandidateQualifier(Qualifier.class, nameValue)); + bean.addQualifier(new AutowireCandidateQualifier(annotationType, nameValue)); } if (ifLazyInit) { bean.setLazyInit(true); @@ -252,69 +301,62 @@ class ModuleRegistryConfiguration implements BeanDefinitionRegistryPostProcessor * @return de-duplicated list of bindings */ protected List removeDuplicates(List elements) { - List duplicateElements = elements.stream().filter((e) -> e instanceof Binding) - .map((e) -> (Binding) e) - .collect(Collectors.groupingBy(ModuleRegistryConfiguration::getLinkedKeyIfRequired)).entrySet().stream() - .filter((e) -> e.getValue().size() > 1 && e.getValue().stream() - .anyMatch((binding) -> binding.getSource() != null - && binding.getSource().toString().contains(SpringModule.SPRING_GUICE_SOURCE))) // find - // duplicates - .flatMap((e) -> e.getValue().stream()) - .filter((e) -> e.getSource() != null - && !e.getSource().toString().contains(SpringModule.SPRING_GUICE_SOURCE)) - .collect(Collectors.toList()); + Predicate hasSpringSource = ((element) -> element.getSource() != null + && element.getSource().toString().contains(SpringModule.SPRING_GUICE_SOURCE)); - @SuppressWarnings("unlikely-arg-type") - List dedupedElements = elements.stream().filter((e) -> { + List> bindings = elements.stream().filter((e) -> e instanceof Binding) + .map((e) -> (Binding) e).collect(Collectors.toList()); + + Map, ? extends Key> injectionKeys = bindings.stream() + .collect(Collectors.groupingBy(Binding::getKey)).entrySet().stream() + .collect(Collectors.toMap(Map.Entry::getKey, (e) -> { + List> keyBindings = e.getValue(); + if (keyBindings.size() == 1) { + // If a linked binding isn't duplicated by its key, try the linked + // injection key + Binding binding = keyBindings.get(0); + if (binding instanceof LinkedKeyBinding) { + return ((LinkedKeyBinding) binding).getLinkedKey(); + } + } + return e.getKey(); + })); + + Map, List>> duplicateBindings = bindings.stream() + .collect(Collectors.groupingBy((e) -> injectionKeys.get(e.getKey()))).entrySet().stream() + .filter((e) -> e.getValue().size() > 1 && e.getValue().stream().anyMatch(hasSpringSource)) + .collect(Collectors.toMap(Entry::getKey, Entry::getValue)); + + return elements.stream().flatMap((e) -> { if (e instanceof Binding) { - return !duplicateElements.contains(new SourceComparableBinding((Binding) e)); - } - else { - return true; + Binding b = (Binding) e; + Key key = injectionKeys.get(b.getKey()); + List> duplicates = duplicateBindings.get(key); + if (duplicates != null) { + if (hasSpringSource.test(b)) { + return duplicates.stream().filter(hasSpringSource.negate()) + .map((guiceBinding) -> withKey(b, guiceBinding.getKey())); + } + else { + // Remove the duplicate Guice binding + return Stream.empty(); + } + } } + return Stream.of(e); }).collect(Collectors.toList()); - return dedupedElements; } - private static Key getLinkedKeyIfRequired(Binding binding) { - if (binding == null) { - return null; + /* + * Re-key the Spring source binding with the Guice key for the built-in bindings. + */ + private Binding withKey(Binding binding, Key key) { + try { + return (BindingImpl) GUICE_BINDINGIMPL_WITHKEY.invoke(binding, key); } - - if (binding instanceof LinkedKeyBinding) { - LinkedKeyBinding linkedBinding = (LinkedKeyBinding) binding; - return linkedBinding.getLinkedKey(); + catch (IllegalAccessException | InvocationTargetException ex) { + throw new RuntimeException(ex); } - - return binding.getKey(); - } - - @SuppressWarnings("checkstyle:EqualsHashCode") - private static class SourceComparableBinding { - - private Binding binding; - - SourceComparableBinding(Binding binding) { - this.binding = binding; - } - - @Override - public boolean equals(Object obj) { - if (obj instanceof Binding) { - Binding compareTo = (Binding) obj; - if (compareTo.getSource() != null && this.binding != null) { - return this.binding.equals(compareTo) - && Objects.equals(this.binding.getSource(), compareTo.getSource()); - } - else { - return Objects.equals(this.binding, compareTo); - } - } - else { - return false; - } - } - } /** diff --git a/src/main/java/org/springframework/guice/module/SpringModule.java b/src/main/java/org/springframework/guice/module/SpringModule.java index 73881a4..f373d59 100644 --- a/src/main/java/org/springframework/guice/module/SpringModule.java +++ b/src/main/java/org/springframework/guice/module/SpringModule.java @@ -168,9 +168,6 @@ public class SpringModule extends AbstractModule { type = clazz; } - if (type == null) { - continue; - } Provider typeProvider = BeanFactoryProvider.typed(beanFactory, type, bindingAnnotation); Provider namedProvider = BeanFactoryProvider.named(beanFactory, name, type, bindingAnnotation); @@ -305,7 +302,8 @@ public class SpringModule extends AbstractModule { if (!this.matcher.matches(name, type)) { return; } - if (type.getTypeName().startsWith("com.google.inject")) { + String typeName = type.getTypeName(); + if (typeName.startsWith("com.google.inject") || typeName.startsWith("javax.inject.Provider")) { return; } if (type instanceof ParameterizedType) { @@ -318,13 +316,11 @@ public class SpringModule extends AbstractModule { } Key key = bindingAnnotation.map((a) -> (Key) Key.get(type, a)).orElse((Key) Key.get(type)); StageTypeKey stageTypeKey = new StageTypeKey(binder.currentStage(), key); - if (this.bound.get(stageTypeKey) == null) { - // Only bind one provider for each type - + // Only bind one provider for each type + if (this.bound.put(stageTypeKey, typeProvider) == null) { binder.withSource(SPRING_GUICE_SOURCE).bind(key).toProvider(typeProvider); - this.bound.put(stageTypeKey, typeProvider); } - // But allow binding to named beans if not already bound + // Allow binding to named beans if not already bound if (!name.equals(getNameFromBindingAnnotation(bindingAnnotation))) { binder.withSource(SPRING_GUICE_SOURCE).bind(TypeLiteral.get(type)).annotatedWith(Names.named(name)) .toProvider(namedProvider); @@ -377,6 +373,11 @@ public class SpringModule extends AbstractModule { return result; } + @Override + public String toString() { + return "StageTypeKey[key=" + this.key + ", stage=" + this.stage + "]"; + } + } @SuppressWarnings("checkstyle:FinalClass") diff --git a/src/test/java/org/springframework/guice/AdhocTestSuite.java b/src/test/java/org/springframework/guice/AdhocTestSuite.java index 29ea897..eb525e9 100644 --- a/src/test/java/org/springframework/guice/AdhocTestSuite.java +++ b/src/test/java/org/springframework/guice/AdhocTestSuite.java @@ -28,7 +28,7 @@ import org.springframework.guice.annotation.EnableGuiceModulesTests; * @author Dave Syer */ @Suite -@SelectClasses({ BindingDeduplicationTests.class, EnableGuiceModulesTests.class }) +@SelectClasses({ MapBindingDeduplicationTests.class, BindingDeduplicationTests.class, EnableGuiceModulesTests.class }) @Disabled public class AdhocTestSuite { diff --git a/src/test/java/org/springframework/guice/BindingAnnotationTests.java b/src/test/java/org/springframework/guice/BindingAnnotationTests.java index 1daf045..30a841e 100644 --- a/src/test/java/org/springframework/guice/BindingAnnotationTests.java +++ b/src/test/java/org/springframework/guice/BindingAnnotationTests.java @@ -21,6 +21,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import javax.inject.Inject; import javax.inject.Named; import javax.inject.Qualifier; @@ -35,6 +36,7 @@ import com.google.inject.throwingproviders.CheckedProvides; import com.google.inject.throwingproviders.ThrowingProviderBinder; import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.NoUniqueBeanDefinitionException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; @@ -42,6 +44,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.guice.annotation.EnableGuiceModules; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; public class BindingAnnotationTests { @@ -105,6 +108,15 @@ public class BindingAnnotationTests { context.close(); } + @Test + public void verifyBindingAnnotationsDuplicateBeans() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext( + BindingAnnotationTestsConfig.class); + assertThatExceptionOfType(NoUniqueBeanDefinitionException.class) + .isThrownBy(() -> assertThat(context.getBean(SomeService.class)).isNotNull()); + context.close(); + } + public static class SomeDependencyWithQualifierOnProvider { } @@ -145,7 +157,7 @@ public class BindingAnnotationTests { } @BindingAnnotation - @Target({ ElementType.TYPE, ElementType.METHOD, ElementType.FIELD }) + @Target({ ElementType.TYPE, ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER }) @Retention(RetentionPolicy.RUNTIME) @interface SomeBindingAnnotation { @@ -182,6 +194,32 @@ public class BindingAnnotationTests { } + interface SomeService { + + } + + static class BaseSomeService implements SomeService { + + } + + static class ShadowingSomeService implements SomeService { + + @Inject + ShadowingSomeService(@SomeBindingAnnotation SomeService baseService) { + + } + + } + + public static class SomeProvider implements javax.inject.Provider { + + @Override + public Object get() { + return null; + } + + } + @EnableGuiceModules @Configuration static class BindingAnnotationTestsConfig { @@ -245,6 +283,11 @@ public class BindingAnnotationTests { return new SomeStringHolder(); } + @Bean + SomeProvider someProvider() { + return new SomeProvider(); + } + @Bean static AbstractModule module() { return new AbstractModule() { @@ -253,6 +296,9 @@ public class BindingAnnotationTests { install(ThrowingProviderBinder.forModule(this)); bind(String.class).annotatedWith(SomeBindingAnnotation.class).toInstance("annotated"); bind(String.class).annotatedWith(SomeOtherBindingAnnotation.class).toInstance("other"); + + bind(SomeService.class).annotatedWith(SomeBindingAnnotation.class).to(BaseSomeService.class); + bind(SomeService.class).to(ShadowingSomeService.class); } @CheckedProvides(TestCheckedProvider.class) diff --git a/src/test/java/org/springframework/guice/BindingDeduplicationTests.java b/src/test/java/org/springframework/guice/BindingDeduplicationTests.java index 92dd967..7078200 100644 --- a/src/test/java/org/springframework/guice/BindingDeduplicationTests.java +++ b/src/test/java/org/springframework/guice/BindingDeduplicationTests.java @@ -19,10 +19,15 @@ package org.springframework.guice; import com.google.inject.AbstractModule; import com.google.inject.CreationException; import com.google.inject.Module; +import com.google.inject.Scopes; +import com.google.inject.multibindings.Multibinder; import com.google.inject.multibindings.OptionalBinder; -import org.junit.jupiter.api.AfterAll; +import com.google.inject.name.Names; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.NoUniqueBeanDefinitionException; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -33,20 +38,63 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType; public class BindingDeduplicationTests { - @AfterAll - public static void cleanUp() { + @BeforeEach + public void setup() { + System.setProperty("spring.guice.dedup", "true"); + } + + @AfterEach + public void cleanUp() { System.clearProperty("spring.guice.dedup"); } @Test public void verifyNoDuplicateBindingErrorWhenDedupeEnabled() { - System.setProperty("spring.guice.dedup", "true"); + try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext( + BindingDeduplicationTestsConfig.class)) { + Dependency dependency = context.getBean(Dependency.class); + assertThat(dependency).isNotNull(); + + OptionalDependency optionalDependency = context.getBean(OptionalDependency.class); + assertThat(optionalDependency).isNotNull(); + } + } + + @Test + public void annotatedBindingDoesNotDuplicate() { + try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext( + BindingDeduplicationTestsConfig.class)) { + FirstInterface firstInterface = context.getBean(FirstInterface.class); + assertThat(firstInterface).isNotNull(); + } + } + + @Test + public void untargettedBindingDoesNotDuplicate() { + try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext( + BindingDeduplicationTestsConfig.class)) { + UntargettedDependency untargettedDependency = context.getBean(UntargettedDependency.class); + assertThat(untargettedDependency).isNotNull(); + } + } + + @Test + public void setBindingDoesNotDuplicate() { + try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext( + BindingDeduplicationTestsConfig.class)) { + SetProvided setProvided = context.getBean(SetProvided.class); + assertThat(setProvided).isNotNull(); + } + } + + @Test + public void springBindingIsDuplicated() { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext( BindingDeduplicationTestsConfig.class); - SomeDependency someDependency = context.getBean(SomeDependency.class); - assertThat(someDependency).isNotNull(); - SomeOptionalDependency someOptionalDependency = context.getBean(SomeOptionalDependency.class); - assertThat(someOptionalDependency).isNotNull(); + + assertThatExceptionOfType(NoUniqueBeanDefinitionException.class) + .isThrownBy(() -> context.getBean(String.class)); + context.close(); } @@ -60,11 +108,47 @@ public class BindingDeduplicationTests { }); } - public static class SomeDependency { + public interface Dependency { } - public static class SomeOptionalDependency { + private static class PrivateDependency implements Dependency { + + } + + public static class SomeSingleton { + + } + + public interface OptionalDependency { + + } + + public static class SomeOptionalDependency implements OptionalDependency { + + } + + interface FirstInterface { + + } + + interface SecondInterface { + + } + + static class MultiInterfaceSingleton implements FirstInterface, SecondInterface { + + } + + static class UntargettedDependency { + + } + + interface SetProvided { + + } + + public static class SomeSetProvided implements SetProvided { } @@ -73,23 +157,53 @@ public class BindingDeduplicationTests { static class BindingDeduplicationTestsConfig { @Bean - SomeDependency someBean() { - return new SomeDependency(); + SomeSingleton someSingleton() { + return new SomeSingleton(); } @Bean - SomeOptionalDependency someOptionalBean() { + PrivateDependency privateDependency() { + return new PrivateDependency(); + } + + @Bean + OptionalDependency someOptionalDependency() { return new SomeOptionalDependency(); } + @Bean + String barString() { + return "bar"; + } + + @Bean + SomeSetProvided someSetProvided() { + return new SomeSetProvided(); + } + @Bean static Module module() { return new AbstractModule() { @Override protected void configure() { - bind(SomeDependency.class).asEagerSingleton(); - OptionalBinder.newOptionalBinder(binder(), SomeOptionalDependency.class).setDefault() + bind(Dependency.class).to(PrivateDependency.class); + bind(SomeSingleton.class).asEagerSingleton(); + + OptionalBinder.newOptionalBinder(binder(), OptionalDependency.class).setDefault() .to(SomeOptionalDependency.class); + + Multibinder setBinder = Multibinder.newSetBinder(binder(), SetProvided.class); + setBinder.addBinding().toInstance(new SomeSetProvided()); + + bind(UntargettedDependency.class); + + // Untargetted binding to provide a singleton for the interface + // bindings + bind(MultiInterfaceSingleton.class).in(Scopes.SINGLETON); + bind(FirstInterface.class).to(MultiInterfaceSingleton.class).in(Scopes.SINGLETON); + bind(SecondInterface.class).to(MultiInterfaceSingleton.class).in(Scopes.SINGLETON); + + bind(String.class).annotatedWith(Names.named("fooString")).toInstance("foo"); } }; } diff --git a/src/test/java/org/springframework/guice/MapBindingDeduplicationTests.java b/src/test/java/org/springframework/guice/MapBindingDeduplicationTests.java new file mode 100644 index 0000000..28c8ef6 --- /dev/null +++ b/src/test/java/org/springframework/guice/MapBindingDeduplicationTests.java @@ -0,0 +1,153 @@ +/* + * Copyright 2018-2022 the original author or authors. + * + * 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 + * + * https://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 org.springframework.guice; + +import java.util.Map; + +import com.google.inject.AbstractModule; +import com.google.inject.Module; +import com.google.inject.Provider; +import com.google.inject.multibindings.MapBinder; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Test; + +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.ResolvableType; +import org.springframework.guice.annotation.EnableGuiceModules; +import org.springframework.guice.module.SpringModule; + +import static org.assertj.core.api.Assertions.assertThat; + +public class MapBindingDeduplicationTests { + + @AfterAll + public static void cleanUp() { + System.clearProperty("spring.guice.dedup"); + } + + @Test + public void mapBindingGuiceOnly() { + System.setProperty("spring.guice.dedup", "false"); + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext( + MapBindingGuiceOnlyTestsConfig.class); + + String[] beanNamesForType = context + .getBeanNamesForType(ResolvableType.forClassWithGenerics(Map.class, String.class, Provider.class)); + @SuppressWarnings("unchecked") + Map> dependencyProvider = (Map>) context + .getBean(beanNamesForType[0]); + + assertThat(dependencyProvider.size()).isEqualTo(2); + assertThat(dependencyProvider.get("someQualifier").get()).isInstanceOf(SomeDependency.class); + + context.close(); + } + + @Test + public void mapBindingConflictingConcreteClass() { + System.setProperty("spring.guice.dedup", "true"); + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext( + MapBindingConcreteClassTestsConfig.class); + + String[] beanNamesForType = context + .getBeanNamesForType(ResolvableType.forClassWithGenerics(Map.class, String.class, Provider.class)); + @SuppressWarnings("unchecked") + Map> dependencyProvider = (Map>) context + .getBean(beanNamesForType[0]); + + assertThat(dependencyProvider.size()).isEqualTo(2); + assertThat(dependencyProvider.get("someQualifier").get()).isInstanceOf(SomeDependency.class); + + SomeDependency someDependency = context.getBean(SomeDependency.class); + assertThat(someDependency.getSource()).isEqualTo(SpringModule.SPRING_GUICE_SOURCE); + + context.close(); + } + + interface Dependency { + + } + + public static class SomeDependency implements Dependency { + + private String source = "guice"; + + public void setSource(String source) { + this.source = source; + } + + public String getSource() { + return this.source; + } + + } + + public static class SomeOptionalDependency implements Dependency { + + } + + @EnableGuiceModules + @Configuration + static class MapBindingGuiceOnlyTestsConfig { + + @Bean + static Module module() { + return new AbstractModule() { + @Override + protected void configure() { + MapBinder bindings = MapBinder.newMapBinder(binder(), String.class, + Dependency.class); + bindings.addBinding("someQualifier").to(SomeDependency.class); + bindings.addBinding("someOtherQualifier").to(SomeOptionalDependency.class); + } + }; + } + + } + + @EnableGuiceModules + @Configuration + static class MapBindingConcreteClassTestsConfig { + + @Bean + SomeDependency dependency() { + SomeDependency someDependency = new SomeDependency(); + someDependency.setSource(SpringModule.SPRING_GUICE_SOURCE); + return someDependency; + } + + @Bean + static Module module() { + return new AbstractModule() { + @Override + protected void configure() { + MapBinder bindings = MapBinder.newMapBinder(binder(), String.class, + Dependency.class); + bindings.addBinding("someQualifier").to(SomeDependency.class); + // Intentionally duplicate the binding to ensure that every key is + // available after deduplication + bindings.addBinding("someQualifier").to(SomeDependency.class); + bindings.addBinding("someOtherQualifier").to(SomeOptionalDependency.class); + } + }; + } + + } + +}