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