New API: Scopes.isSingleton(Binding)

The only thing interesting about this API is that I downcast the binding to its internal type in order to follow linked bindings.

git-svn-id: https://google-guice.googlecode.com/svn/trunk@1033 d779f126-a31b-0410-b53b-1d3aecad763e
diff --git a/servlet/src/com/google/inject/servlet/FilterDefinition.java b/servlet/src/com/google/inject/servlet/FilterDefinition.java
index dff810b..8097713 100755
--- a/servlet/src/com/google/inject/servlet/FilterDefinition.java
+++ b/servlet/src/com/google/inject/servlet/FilterDefinition.java
@@ -17,8 +17,8 @@
 
 import com.google.inject.Injector;
 import com.google.inject.Key;
+import com.google.inject.Scopes;
 import com.google.inject.internal.Iterators;
-import static com.google.inject.servlet.ServletScopes.isSingletonBinding;
 import java.io.IOException;
 import java.util.Collections;
 import java.util.Enumeration;
@@ -63,7 +63,7 @@
       Set<Filter> initializedSoFar) throws ServletException {
 
     // This absolutely must be a singleton, and so is only initialized once.
-    if (!isSingletonBinding(injector.getBinding(filterKey))) {
+    if (!Scopes.isSingleton(injector.getBinding(filterKey))) {
       throw new ServletException("Filters must be bound as singletons. "
         + filterKey + " was not bound in singleton scope.");
     }
diff --git a/servlet/src/com/google/inject/servlet/ServletDefinition.java b/servlet/src/com/google/inject/servlet/ServletDefinition.java
index 355b594..2941fad 100755
--- a/servlet/src/com/google/inject/servlet/ServletDefinition.java
+++ b/servlet/src/com/google/inject/servlet/ServletDefinition.java
@@ -17,8 +17,8 @@
 
 import com.google.inject.Injector;
 import com.google.inject.Key;
+import com.google.inject.Scopes;
 import com.google.inject.internal.Iterators;
-import static com.google.inject.servlet.ServletScopes.isSingletonBinding;
 import java.io.IOException;
 import java.util.Collections;
 import java.util.Enumeration;
@@ -66,7 +66,7 @@
       Set<HttpServlet> initializedSoFar) throws ServletException {
 
     // This absolutely must be a singleton, and so is only initialized once.
-    if (!isSingletonBinding(injector.getBinding(servletKey))) {
+    if (!Scopes.isSingleton(injector.getBinding(servletKey))) {
       throw new ServletException("Servlets must be bound as singletons. "
         + servletKey + " was not bound in singleton scope.");
     }
diff --git a/servlet/src/com/google/inject/servlet/ServletScopes.java b/servlet/src/com/google/inject/servlet/ServletScopes.java
index df6e505..c907946 100644
--- a/servlet/src/com/google/inject/servlet/ServletScopes.java
+++ b/servlet/src/com/google/inject/servlet/ServletScopes.java
@@ -16,15 +16,9 @@
 
 package com.google.inject.servlet;
 
-import com.google.inject.Binding;
 import com.google.inject.Key;
 import com.google.inject.Provider;
 import com.google.inject.Scope;
-import com.google.inject.Scopes;
-import com.google.inject.Singleton;
-import com.google.inject.spi.DefaultBindingScopingVisitor;
-import java.lang.annotation.Annotation;
-import java.util.concurrent.atomic.AtomicBoolean;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpSession;
 
@@ -97,36 +91,4 @@
       return "ServletScopes.SESSION";
     }
   };
-
-  static boolean isSingletonBinding(Binding<?> binding) {
-    final AtomicBoolean isSingleton = new AtomicBoolean(true);
-    binding.acceptScopingVisitor(new DefaultBindingScopingVisitor<Void>() {
-      @Override
-      public Void visitNoScoping() {
-        isSingleton.set(false);
-
-        return null;
-      }
-
-      @Override
-      public Void visitScopeAnnotation(Class<? extends Annotation> scopeAnnotation) {
-        if (null != scopeAnnotation && !Singleton.class.equals(scopeAnnotation)) {
-          isSingleton.set(false);
-        }
-
-        return null;
-      }
-
-      @Override
-      public Void visitScope(Scope scope) {
-        if (null != scope && !Scopes.SINGLETON.equals(scope)) {
-          isSingleton.set(false);
-        }
-
-        return null;
-      }
-    });
-
-    return isSingleton.get();
-  }
 }
diff --git a/src/com/google/inject/Scopes.java b/src/com/google/inject/Scopes.java
index 7bdaa4f..bb4316f 100644
--- a/src/com/google/inject/Scopes.java
+++ b/src/com/google/inject/Scopes.java
@@ -17,6 +17,9 @@
 package com.google.inject;
 
 import com.google.inject.internal.InjectorBuilder;
+import com.google.inject.internal.LinkedBindingImpl;
+import com.google.inject.spi.BindingScopingVisitor;
+import java.lang.annotation.Annotation;
 
 /**
  * Built-in scope implementations.
@@ -86,4 +89,49 @@
       return "Scopes.NO_SCOPE";
     }
   };
+
+  /**
+   * Returns true if {@code binding} is singleton-scoped. If the binding is a {@link
+   * com.google.inject.spi.LinkedKeyBinding linked key binding} and belongs to an injector (ie. it
+   * was retrieved via {@link Injector#getBinding Injector.getBinding()}), then this method will
+   * also true if the target binding is singleton-scoped.
+   *
+   * @since 2.1
+   */
+  public static boolean isSingleton(Binding<?> binding) {
+    do {
+      boolean singleton = binding.acceptScopingVisitor(new BindingScopingVisitor<Boolean>() {
+        public Boolean visitNoScoping() {
+          return false;
+        }
+
+        public Boolean visitScopeAnnotation(Class<? extends Annotation> scopeAnnotation) {
+          return Singleton.class == scopeAnnotation;
+        }
+
+        public Boolean visitScope(Scope scope) {
+          return scope == Scopes.SINGLETON;
+        }
+
+        public Boolean visitEagerSingleton() {
+          return true;
+        }
+      });
+
+      if (singleton) {
+        return true;
+      }
+
+      if (binding instanceof LinkedBindingImpl) {
+        LinkedBindingImpl<?> linkedBinding = (LinkedBindingImpl) binding;
+        Injector injector = (Injector) linkedBinding.getInjector();
+        if (injector != null) {
+          binding = injector.getBinding(linkedBinding.getLinkedKey());
+          continue;
+        }
+      }
+
+      return false;
+    } while (true);
+  }
 }
diff --git a/src/com/google/inject/internal/LinkedBindingImpl.java b/src/com/google/inject/internal/LinkedBindingImpl.java
index f897230..a61f781 100644
--- a/src/com/google/inject/internal/LinkedBindingImpl.java
+++ b/src/com/google/inject/internal/LinkedBindingImpl.java
@@ -21,7 +21,7 @@
 import com.google.inject.spi.BindingTargetVisitor;
 import com.google.inject.spi.LinkedKeyBinding;
 
-final class LinkedBindingImpl<T> extends BindingImpl<T> implements LinkedKeyBinding<T> {
+public final class LinkedBindingImpl<T> extends BindingImpl<T> implements LinkedKeyBinding<T> {
 
   final Key<? extends T> targetKey;
 
diff --git a/test/com/google/inject/ScopesTest.java b/test/com/google/inject/ScopesTest.java
index ca354fc..3b53ffd 100644
--- a/test/com/google/inject/ScopesTest.java
+++ b/test/com/google/inject/ScopesTest.java
@@ -17,7 +17,13 @@
 package com.google.inject;
 
 import static com.google.inject.Asserts.assertContains;
+import com.google.inject.internal.ImmutableMap;
 import com.google.inject.internal.Maps;
+import com.google.inject.name.Named;
+import static com.google.inject.name.Names.named;
+import com.google.inject.spi.Element;
+import com.google.inject.spi.Elements;
+import com.google.inject.util.Providers;
 import java.io.IOException;
 import java.lang.annotation.ElementType;
 import java.lang.annotation.Retention;
@@ -442,4 +448,102 @@
       };
     }
   }
+
+  public void testIsSingletonPositive() {
+    final Key<String> a = Key.get(String.class, named("A"));
+    final Key<String> b = Key.get(String.class, named("B"));
+    final Key<String> c = Key.get(String.class, named("C"));
+    final Key<String> d = Key.get(String.class, named("D"));
+    final Key<String> e = Key.get(String.class, named("E"));
+    final Key<String> f = Key.get(String.class, named("F"));
+    final Key<String> g = Key.get(String.class, named("G"));
+    final Key<Object> h = Key.get(Object.class, named("H"));
+
+    Module singletonBindings = new AbstractModule() {
+      protected void configure() {
+        bind(a).to(b);
+        bind(b).to(c);
+        bind(c).toProvider(Providers.of("c")).in(Scopes.SINGLETON);
+        bind(d).toInstance("d");
+        bind(e).toProvider(Providers.of("e")).asEagerSingleton();
+        bind(f).toProvider(Providers.of("f")).in(Singleton.class);
+        bind(h).to(AnnotatedSingleton.class);
+      }
+
+      @Provides @Named("G") @Singleton String provideG() {
+        return "g";
+      }
+    };
+
+    @SuppressWarnings("unchecked") // we know the module contains only bindings
+    List<Element> moduleBindings = Elements.getElements(singletonBindings);
+    ImmutableMap<Key<?>, Binding<?>> map = indexBindings(moduleBindings);
+    assertFalse(Scopes.isSingleton(map.get(a))); // linked bindings are not followed by modules
+    assertFalse(Scopes.isSingleton(map.get(b)));
+    assertTrue(Scopes.isSingleton(map.get(c)));
+    assertTrue(Scopes.isSingleton(map.get(d)));
+    assertTrue(Scopes.isSingleton(map.get(e)));
+    assertTrue(Scopes.isSingleton(map.get(f)));
+    assertTrue(Scopes.isSingleton(map.get(g)));
+    assertFalse(Scopes.isSingleton(map.get(h))); // annotated classes are not followed by modules
+
+    Injector injector = Guice.createInjector(singletonBindings);
+    assertTrue(Scopes.isSingleton(injector.getBinding(a)));
+    assertTrue(Scopes.isSingleton(injector.getBinding(b)));
+    assertTrue(Scopes.isSingleton(injector.getBinding(c)));
+    assertTrue(Scopes.isSingleton(injector.getBinding(d)));
+    assertTrue(Scopes.isSingleton(injector.getBinding(e)));
+    assertTrue(Scopes.isSingleton(injector.getBinding(f)));
+    assertTrue(Scopes.isSingleton(injector.getBinding(g)));
+    assertTrue(Scopes.isSingleton(injector.getBinding(h)));
+  }
+  
+  public void testIsSingletonNegative() {
+    final Key<String> a = Key.get(String.class, named("A"));
+    final Key<String> b = Key.get(String.class, named("B"));
+    final Key<String> c = Key.get(String.class, named("C"));
+    final Key<String> d = Key.get(String.class, named("D"));
+    final Key<String> e = Key.get(String.class, named("E"));
+
+    Module singletonBindings = new AbstractModule() {
+      protected void configure() {
+        bind(a).to(b);
+        bind(b).to(c);
+        bind(c).toProvider(Providers.of("c")).in(Scopes.NO_SCOPE);
+        bind(d).toProvider(Providers.of("d")).in(CustomScoped.class);
+        bindScope(CustomScoped.class, Scopes.NO_SCOPE);
+      }
+
+      @Provides @Named("E") @CustomScoped String provideE() {
+        return "e";
+      }
+    };
+
+    @SuppressWarnings("unchecked") // we know the module contains only bindings
+    List<Element> moduleBindings = Elements.getElements(singletonBindings);
+    ImmutableMap<Key<?>, Binding<?>> map = indexBindings(moduleBindings);
+    assertFalse(Scopes.isSingleton(map.get(a)));
+    assertFalse(Scopes.isSingleton(map.get(b)));
+    assertFalse(Scopes.isSingleton(map.get(c)));
+    assertFalse(Scopes.isSingleton(map.get(d)));
+    assertFalse(Scopes.isSingleton(map.get(e)));
+
+    Injector injector = Guice.createInjector(singletonBindings);
+    assertFalse(Scopes.isSingleton(injector.getBinding(a)));
+    assertFalse(Scopes.isSingleton(injector.getBinding(b)));
+    assertFalse(Scopes.isSingleton(injector.getBinding(c)));
+    assertFalse(Scopes.isSingleton(injector.getBinding(d)));
+    assertFalse(Scopes.isSingleton(injector.getBinding(e)));
+  }
+
+  ImmutableMap<Key<?>, Binding<?>> indexBindings(Iterable<Element> elements) {
+    ImmutableMap.Builder<Key<?>, Binding<?>> builder = ImmutableMap.builder();
+    for (Element element : elements) {
+      if (element instanceof Binding) {
+        Binding<?> binding = (Binding<?>) element;
+        builder.put(binding.getKey(), binding);
+      }
+    }
+    return builder.build();
+  }
 }