Preliminary lifecycle and broadcasting extension experiment. Some servlet test cleanups bundled.

git-svn-id: https://google-guice.googlecode.com/svn/trunk@1073 d779f126-a31b-0410-b53b-1d3aecad763e
diff --git a/lifecycle/build.properties b/lifecycle/build.properties
new file mode 100644
index 0000000..c0a2be4
--- /dev/null
+++ b/lifecycle/build.properties
@@ -0,0 +1,5 @@
+src.dir=src
+test.dir=test
+build.dir=build
+test.class=com.google.inject.lifecycle.LifecycleTest
+module=com.google.inject.lifecycle
diff --git a/lifecycle/build.xml b/lifecycle/build.xml
new file mode 100644
index 0000000..f22a1c0
--- /dev/null
+++ b/lifecycle/build.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0"?>
+
+<project name="guice-lifecycle" basedir="." default="jar">
+
+  <import file="../common.xml"/>
+  
+  <path id="compile.classpath">
+    <fileset dir="../lib" includes="*.jar"/>
+    <fileset dir="../lib/build" includes="*.jar"/>
+    <fileset dir="../build/dist" includes="*.jar"/>
+  </path>
+
+  <target name="jar" depends="jar.withdeps, manifest" description="Build jar.">
+    <jar destfile="${build.dir}/${ant.project.name}-${version}.jar"
+        manifest="${build.dir}/META-INF/MANIFEST.MF">
+      <zipfileset src="${build.dir}/${ant.project.name}-with-deps.jar"
+          excludes="com/google/inject/internal/**"/>
+    </jar>
+  </target>
+
+</project>
diff --git a/lifecycle/lifecycle.iml b/lifecycle/lifecycle.iml
new file mode 100644
index 0000000..6906ee5
--- /dev/null
+++ b/lifecycle/lifecycle.iml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<module relativePaths="true" type="JAVA_MODULE" version="4">
+  <component name="NewModuleRootManager" LANGUAGE_LEVEL="JDK_1_5" inherit-compiler-output="true">
+    <exclude-output />
+    <content url="file://$MODULE_DIR$">
+      <sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
+      <sourceFolder url="file://$MODULE_DIR$/test" isTestSource="true" />
+    </content>
+    <orderEntry type="inheritedJdk" />
+    <orderEntry type="sourceFolder" forTests="false" />
+    <orderEntry type="module" module-name="guice" />
+    <orderEntry type="module-library">
+      <library>
+        <CLASSES>
+          <root url="jar://$MODULE_DIR$/../lib/build/junit.jar!/" />
+        </CLASSES>
+        <JAVADOC />
+        <SOURCES />
+      </library>
+    </orderEntry>
+    <orderEntry type="module-library">
+      <library>
+        <CLASSES>
+          <root url="jar://$MODULE_DIR$/../lib/build/easymock.jar!/" />
+        </CLASSES>
+        <JAVADOC />
+        <SOURCES />
+      </library>
+    </orderEntry>
+  </component>
+</module>
+
diff --git a/lifecycle/src/com/google/inject/lifecycle/BroadcastingLifecycle.java b/lifecycle/src/com/google/inject/lifecycle/BroadcastingLifecycle.java
new file mode 100644
index 0000000..74cb6af
--- /dev/null
+++ b/lifecycle/src/com/google/inject/lifecycle/BroadcastingLifecycle.java
@@ -0,0 +1,133 @@
+package com.google.inject.lifecycle;
+
+import com.google.inject.Binding;
+import com.google.inject.Inject;
+import com.google.inject.Injector;
+import com.google.inject.Key;
+import com.google.inject.Scopes;
+import com.google.inject.Singleton;
+import com.google.inject.internal.Lists;
+import com.google.inject.internal.Maps;
+import com.google.inject.internal.Preconditions;
+import com.google.inject.matcher.Matcher;
+import static com.google.inject.matcher.Matchers.any;
+import java.lang.reflect.Method;
+import java.util.List;
+import java.util.Map;
+import net.sf.cglib.proxy.InvocationHandler;
+import net.sf.cglib.proxy.Proxy;
+
+/** @author dhanji@google.com (Dhanji R. Prasanna) */
+@Singleton class BroadcastingLifecycle implements Lifecycle {
+  private final Injector injector;
+  private final List<Class<?>> callableClasses;
+
+  // @GuardedBy(this)
+  private Map<Class<?>, List<Key<?>>> callableKeys;
+
+  private volatile boolean started = false;
+
+  @Inject
+  public BroadcastingLifecycle(Injector injector, @ListOfMatchers List<Class<?>> callableClasses) {
+    this.injector = injector;
+    this.callableClasses = callableClasses;
+
+    // Self start. Eventually we may want to move this to a hook-in from Guice-core.
+    start();
+  }
+
+  public void start() {
+    if (started) {
+      // throw? log warning?
+      return;
+    }
+
+    // OK to start the startables now.
+    // Guaranteed to return in order of module binding..
+    Map<Key<?>, Binding<?>> allBindings = injector.getBindings();
+
+    List<Binding<Startable>> startables = Lists.newArrayList();
+    Map<Class<?>, List<Key<?>>> callableKeys = Maps.newLinkedHashMap();
+
+    // Do not collapse into loop below (in synchronized block). Time complexity is still linear.
+    for (Binding<?> binding : allBindings.values()) {
+
+      Class<?> bindingType = binding.getKey().getTypeLiteral().getRawType();
+
+      // inner loop N*M complexity
+      for (Class<?> callable : callableClasses) {
+        if (callable.isAssignableFrom(bindingType)) {
+
+          // we don't want to instantiate these right now...
+          List<Key<?>> list = callableKeys.get(callable);
+
+          // Multimap put.
+          if (null == list) {
+            list = Lists.newArrayList();
+            callableKeys.put(callable, list);
+          }
+
+          list.add(binding.getKey());
+        }
+      }
+
+      // check startables now.
+      if (Startable.class.isAssignableFrom(bindingType)) {
+
+        // First make sure this is a singleton.
+        Preconditions.checkState(Scopes.isSingleton(binding),
+            "Egregious error, all Startables must be scopes as singletons!");
+
+        //noinspection unchecked
+        startables.add((Binding<Startable>) binding);
+      }
+    }
+
+    synchronized (this) {
+      for (Binding<Startable> binding : startables) {
+
+        // Go go zilla go! (sequential startup)
+        injector.getInstance(binding.getKey()).start();
+      }
+
+      // Safely publish keymap.
+      this.callableKeys = callableKeys;
+
+      // success!
+      started = true;
+    }
+  }
+
+  public <T> T broadcast(Class<T> clazz) {
+    return broadcast(clazz, any());
+  }
+
+  public <T> T broadcast(Class<T> clazz, Matcher<? super T> matcher) {
+    final List<T> ts = Lists.newArrayList();
+    for (Key<?> key : callableKeys.get(clazz)) {
+      // Should this get instancing happen during method call?
+      @SuppressWarnings("unchecked") // Guarded by getInstance
+          T t = (T) injector.getInstance(key);
+
+      if (matcher.matches(t)) {
+        ts.add(t);
+      }
+    }
+
+    @SuppressWarnings("unchecked") T caster = (T) Proxy
+        .newProxyInstance(clazz.getClassLoader(), new Class[] { clazz }, new InvocationHandler() {
+          public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
+
+            // propagate the method call with the same arg list to all instances.
+            for (T t : ts) {
+              method.invoke(t, objects);
+            }
+
+            // We can't return from multiple instances, so just return null.
+            return null;
+          }
+        });
+
+    return caster;
+  }
+}
\ No newline at end of file
diff --git a/lifecycle/src/com/google/inject/lifecycle/Lifecycle.java b/lifecycle/src/com/google/inject/lifecycle/Lifecycle.java
new file mode 100644
index 0000000..0ec38b4
--- /dev/null
+++ b/lifecycle/src/com/google/inject/lifecycle/Lifecycle.java
@@ -0,0 +1,17 @@
+package com.google.inject.lifecycle;
+
+import com.google.inject.ImplementedBy;
+import com.google.inject.matcher.Matcher;
+
+/**
+ *  @author dhanji@google.com (Dhanji R. Prasanna)
+ */
+@ImplementedBy(BroadcastingLifecycle.class)
+public interface Lifecycle {
+
+  void start();
+
+  <T> T broadcast(Class<T> clazz);
+
+  <T> T broadcast(Class<T> clazz, Matcher<? super T> matcher);
+}
diff --git a/lifecycle/src/com/google/inject/lifecycle/LifecycleModule.java b/lifecycle/src/com/google/inject/lifecycle/LifecycleModule.java
new file mode 100644
index 0000000..7c780bf
--- /dev/null
+++ b/lifecycle/src/com/google/inject/lifecycle/LifecycleModule.java
@@ -0,0 +1,45 @@
+package com.google.inject.lifecycle;
+
+import com.google.inject.AbstractModule;
+import com.google.inject.TypeLiteral;
+import com.google.inject.internal.ImmutableList;
+import com.google.inject.internal.Lists;
+import java.util.List;
+
+/**
+ * Use this module to configure lifecycle and multicasting support
+ * for your Guice applications.
+ *
+ * @author dhanji@gmail.com (Dhanji R. Prasanna)
+// */
+public abstract class LifecycleModule extends AbstractModule {
+
+  private final List<Class<?>> callables = Lists.newArrayList();
+  private boolean autostart = false;
+
+  @Override
+  protected final void configure() {
+
+    // Call down into module.
+    configureLifecycle();
+
+    // The only real purpose of this is to do some error checking.
+    bind(new TypeLiteral<List<Class<?>>>() { })
+        .annotatedWith(ListOfMatchers.class)
+        .toInstance(ImmutableList.copyOf(callables));
+  }
+
+  protected abstract void configureLifecycle();
+  
+  protected void callable(Class<?> type) {
+    callables.add(type);
+  }
+
+  protected void autostart() {
+    this.autostart = true;
+
+    // This is a cool method that will execute after injector creating
+    // completes, and thus much better than the eager singleton hack.
+    throw new UnsupportedOperationException("Asplode!");
+  }
+}
\ No newline at end of file
diff --git a/lifecycle/src/com/google/inject/lifecycle/ListOfMatchers.java b/lifecycle/src/com/google/inject/lifecycle/ListOfMatchers.java
new file mode 100644
index 0000000..847b1e1
--- /dev/null
+++ b/lifecycle/src/com/google/inject/lifecycle/ListOfMatchers.java
@@ -0,0 +1,11 @@
+package com.google.inject.lifecycle;
+
+import com.google.inject.BindingAnnotation;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/** @author dhanji@google.com (Dhanji R. Prasanna) */
+@Retention(RetentionPolicy.RUNTIME)
+@BindingAnnotation
+@interface ListOfMatchers {
+}
diff --git a/lifecycle/src/com/google/inject/lifecycle/Startable.java b/lifecycle/src/com/google/inject/lifecycle/Startable.java
new file mode 100644
index 0000000..beae593
--- /dev/null
+++ b/lifecycle/src/com/google/inject/lifecycle/Startable.java
@@ -0,0 +1,22 @@
+package com.google.inject.lifecycle;
+
+/**
+ * A convenience lifecycle interface. Any class the exposes
+ * this interface will be started if the lifecycle module is
+ * installed. The lifecycle module guarantees that the order
+ * in which Startables are called will match the order in which
+ * modules are installed.
+ *
+ * All instances that wish to use startable *must* be bound as
+ * singletons.
+ *
+ * @author dhanji@google.com (Dhanji R. Prasanna)
+ */
+public interface Startable {
+  /**
+   * Called once the injector has been created completely.
+   * In PRODUCTION mode, this means when all singletons
+   * have been instantiated.
+   */
+  void start();
+}
diff --git a/lifecycle/src/com/google/inject/lifecycle/package-info.java b/lifecycle/src/com/google/inject/lifecycle/package-info.java
new file mode 100644
index 0000000..afe0e97
--- /dev/null
+++ b/lifecycle/src/com/google/inject/lifecycle/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright (C) 2009 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.
+ */
+
+/**
+ * Lifecycle extension; this extension requires {@code guice-lifecycle-2.0.jar}.
+ */
+package com.google.inject.lifecycle;
\ No newline at end of file
diff --git a/lifecycle/test/com/google/inject/lifecycle/ArbitraryBroadcastTest.java b/lifecycle/test/com/google/inject/lifecycle/ArbitraryBroadcastTest.java
new file mode 100644
index 0000000..0751807
--- /dev/null
+++ b/lifecycle/test/com/google/inject/lifecycle/ArbitraryBroadcastTest.java
@@ -0,0 +1,37 @@
+package com.google.inject.lifecycle;
+
+import com.google.inject.Guice;
+import com.google.inject.Singleton;
+import junit.framework.TestCase;
+
+/** @author dhanji@google.com (Dhanji R. Prasanna) */
+public class ArbitraryBroadcastTest extends TestCase {
+  private static int called;
+
+  public final void testCallable() {
+    called = 0;
+    Lifecycle lifecycle = Guice.createInjector(new LifecycleModule() {
+
+      @Override
+      protected void configureLifecycle() {
+        bind(Runnable.class).to(AClass.class).in(Singleton.class);
+        bind(AClass.class).in(Singleton.class);
+
+        callable(Runnable.class);
+      }
+
+    }).getInstance(Lifecycle.class);
+
+    lifecycle
+      .broadcast(Runnable.class)
+      .run();
+
+    assertEquals(2, called);
+  }
+
+  public static class AClass implements Runnable {
+    public void run() {
+      called++;
+    }
+  }
+}
\ No newline at end of file
diff --git a/lifecycle/test/com/google/inject/lifecycle/MultipleStartableTest.java b/lifecycle/test/com/google/inject/lifecycle/MultipleStartableTest.java
new file mode 100644
index 0000000..500bced
--- /dev/null
+++ b/lifecycle/test/com/google/inject/lifecycle/MultipleStartableTest.java
@@ -0,0 +1,51 @@
+package com.google.inject.lifecycle;
+
+import com.google.inject.Guice;
+import com.google.inject.Singleton;
+import junit.framework.TestCase;
+
+/** @author dhanji@google.com (Dhanji R. Prasanna) */
+public class MultipleStartableTest extends TestCase {
+  private static int started;
+
+  public final void testMultiStartable() {
+    started = 0;
+    Guice.createInjector(new LifecycleModule() {
+
+      @Override
+      protected void configureLifecycle() {
+        bind(AClass.class).in(Singleton.class);
+        bind(Startable.class)
+            .annotatedWith(ListOfMatchers.class)
+            .to(BClass.class)
+            .in(Singleton.class);
+        bind(Startable.class).to(CClass.class).in(Singleton.class);
+      }
+
+    }).getInstance(Lifecycle.class)
+      .start();
+
+    assertEquals(3, started);
+  }
+
+  public static class AClass implements Startable {
+
+    public void start() {
+      started++;
+    }
+  }
+
+  public static class BClass implements Startable {
+
+    public void start() {
+      started++;
+    }
+  }
+
+  public static class CClass implements Startable {
+
+    public void start() {
+      started++;
+    }
+  }
+}
\ No newline at end of file
diff --git a/lifecycle/test/com/google/inject/lifecycle/StartableTest.java b/lifecycle/test/com/google/inject/lifecycle/StartableTest.java
new file mode 100644
index 0000000..7f197b8
--- /dev/null
+++ b/lifecycle/test/com/google/inject/lifecycle/StartableTest.java
@@ -0,0 +1,32 @@
+package com.google.inject.lifecycle;
+
+import com.google.inject.Guice;
+import com.google.inject.Singleton;
+import junit.framework.TestCase;
+
+/** @author dhanji@google.com (Dhanji R. Prasanna) */
+public class StartableTest extends TestCase {
+  private static boolean started;
+
+  public final void testStartable() {
+    started = false;
+    Guice.createInjector(new LifecycleModule() {
+
+      @Override
+      protected void configureLifecycle() {
+        bind(AClass.class).in(Singleton.class);
+      }
+
+    }).getInstance(Lifecycle.class)
+      .start();
+
+    assertTrue(started);
+  }
+
+  public static class AClass implements Startable {
+
+    public void start() {
+      started = true;
+    }
+  }
+}
diff --git a/servlet/test/com/google/inject/servlet/DummyServlet.java b/servlet/test/com/google/inject/servlet/DummyServlet.java
index bfcfb94..3fc1548 100644
--- a/servlet/test/com/google/inject/servlet/DummyServlet.java
+++ b/servlet/test/com/google/inject/servlet/DummyServlet.java
@@ -15,6 +15,7 @@
  */
 package com.google.inject.servlet;
 
+import com.google.inject.Singleton;
 import javax.servlet.http.HttpServlet;
 
 /**
@@ -22,6 +23,7 @@
  *
  * @author Dhanji R. Prasanna (dhanji@gmail com)
  */
+@Singleton
 public class DummyServlet extends HttpServlet {
 
 }
diff --git a/servlet/test/com/google/inject/servlet/ServletDispatchIntegrationTest.java b/servlet/test/com/google/inject/servlet/ServletDispatchIntegrationTest.java
index 22e294b..df0a62e 100644
--- a/servlet/test/com/google/inject/servlet/ServletDispatchIntegrationTest.java
+++ b/servlet/test/com/google/inject/servlet/ServletDispatchIntegrationTest.java
@@ -30,6 +30,7 @@
 import javax.servlet.ServletResponse;
 import javax.servlet.http.HttpServlet;
 import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
 import junit.framework.TestCase;
 import static org.easymock.EasyMock.createMock;
 import static org.easymock.EasyMock.expect;
@@ -188,4 +189,50 @@
       destroys++;
     }
   }
+
+
+  @Singleton
+  public static class ForwardingServlet extends HttpServlet {
+    public void service(ServletRequest servletRequest, ServletResponse servletResponse)
+        throws IOException, ServletException {
+      final HttpServletRequest request = (HttpServletRequest) servletRequest;
+
+      request.getRequestDispatcher("/blah.jsp")
+          .forward(servletRequest, servletResponse);
+    }
+  }
+
+  @Singleton
+  public static class ForwardedServlet extends HttpServlet {
+    public void service(ServletRequest servletRequest, ServletResponse servletResponse)
+        throws IOException, ServletException {
+      final HttpServletRequest request = (HttpServletRequest) servletRequest;
+
+      System.out.println(request.getRequestURI());
+ }
+  }
+
+  public void testForwardUsingRequestDispatcher() throws IOException, ServletException {
+    Guice.createInjector(new ServletModule() {
+      @Override
+      protected void configureServlets() {
+        serve("/*").with(ForwardingServlet.class);
+        serve("/blah.jsp").with(ForwardedServlet.class);
+      }
+    });
+
+    final HttpServletRequest requestMock = createMock(HttpServletRequest.class);
+    HttpServletResponse responseMock = createMock(HttpServletResponse.class);
+    expect(requestMock.getServletPath())
+        .andReturn("/")
+        .anyTimes();
+
+    expect(responseMock.isCommitted()).andReturn(false);
+
+    replay(requestMock, responseMock);
+
+    new GuiceFilter()
+        .doFilter(requestMock, responseMock,
+            createMock(FilterChain.class));
+  }
 }
\ No newline at end of file