visitable to commands

git-svn-id: https://google-guice.googlecode.com/svn/trunk@405 d779f126-a31b-0410-b53b-1d3aecad763e
diff --git a/extensions/commands/build.properties b/extensions/commands/build.properties
new file mode 100644
index 0000000..e85ddc4
--- /dev/null
+++ b/extensions/commands/build.properties
@@ -0,0 +1,5 @@
+lib.dir=../../lib
+src.dir=src
+test.dir=test
+build.dir=build
+test.class=com.google.inject.commands.CommandRecorderTest
diff --git a/extensions/commands/build.xml b/extensions/commands/build.xml
new file mode 100644
index 0000000..12f94a8
--- /dev/null
+++ b/extensions/commands/build.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0"?>
+
+<project name="guice-visitable" basedir="." default="jar">
+
+  <import file="../../common.xml"/>
+  
+  <path id="compile.classpath">
+    <fileset dir="${lib.dir}" includes="*.jar"/>
+    <fileset dir="${lib.dir}/build" includes="*.jar"/>
+    <fileset dir="../../build/dist" includes="*.jar"/>
+  </path>
+
+  <target name="jar" depends="compile"
+       description="Build jar.">
+    <mkdir dir="${build.dir}"/>
+    <jar destfile="${build.dir}/${ant.project.name}-${version}.jar">
+      <fileset dir="${build.dir}/classes"/>
+    </jar>
+  </target>
+
+</project>
diff --git a/extensions/commands/commands.iml b/extensions/commands/commands.iml
new file mode 100644
index 0000000..bc42261
--- /dev/null
+++ b/extensions/commands/commands.iml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<module relativePaths="true" type="JAVA_MODULE" version="4">
+  <component name="NewModuleRootManager" 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" />
+    <orderEntryProperties />
+  </component>
+</module>
+
diff --git a/extensions/commands/src/com/google/inject/commands/AddMessageErrorCommand.java b/extensions/commands/src/com/google/inject/commands/AddMessageErrorCommand.java
new file mode 100644
index 0000000..d6f0077
--- /dev/null
+++ b/extensions/commands/src/com/google/inject/commands/AddMessageErrorCommand.java
@@ -0,0 +1,48 @@
+/**
+ * Copyright (C) 2008 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.
+ */
+
+package com.google.inject.commands;
+
+import java.util.Arrays;
+import static java.util.Collections.unmodifiableList;
+import java.util.List;
+
+/**
+ * Immutable snapshot of a request to add a string message.
+ *
+ * @author jessewilson@google.com (Jesse Wilson)
+ */
+public final class AddMessageErrorCommand implements Command {
+  private final String message;
+  private final List<Object> arguments;
+
+  AddMessageErrorCommand(String message, Object[] arguments) {
+    this.message = message;
+    this.arguments = unmodifiableList(Arrays.asList(arguments.clone()));
+  }
+
+  public <T> T acceptVisitor(Visitor<T> visitor) {
+    return visitor.visitAddMessageError(this);
+  }
+
+  public String getMessage() {
+    return message;
+  }
+
+  public List<Object> getArguments() {
+    return arguments;
+  }
+}
diff --git a/extensions/commands/src/com/google/inject/commands/AddThrowableErrorCommand.java b/extensions/commands/src/com/google/inject/commands/AddThrowableErrorCommand.java
new file mode 100644
index 0000000..38be96e
--- /dev/null
+++ b/extensions/commands/src/com/google/inject/commands/AddThrowableErrorCommand.java
@@ -0,0 +1,38 @@
+/**
+ * Copyright (C) 2008 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.
+ */
+
+package com.google.inject.commands;
+
+/**
+ * Immutable snapshot of a request to add a throwable message.
+ *
+ * @author jessewilson@google.com (Jesse Wilson)
+ */
+public final class AddThrowableErrorCommand implements Command {
+  private final Throwable throwable;
+
+  AddThrowableErrorCommand(Throwable throwable) {
+    this.throwable = throwable;
+  }
+
+  public <T> T acceptVisitor(Visitor<T> visitor) {
+    return visitor.visitAddError(this);
+  }
+
+  public Throwable getThrowable() {
+    return throwable;
+  }
+}
diff --git a/extensions/commands/src/com/google/inject/commands/BindCommand.java b/extensions/commands/src/com/google/inject/commands/BindCommand.java
new file mode 100644
index 0000000..c8ef971
--- /dev/null
+++ b/extensions/commands/src/com/google/inject/commands/BindCommand.java
@@ -0,0 +1,272 @@
+/**
+ * Copyright (C) 2008 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.
+ */
+
+package com.google.inject.commands;
+
+import com.google.inject.Key;
+import com.google.inject.Provider;
+import com.google.inject.Scope;
+import com.google.inject.TypeLiteral;
+import com.google.inject.binder.AnnotatedBindingBuilder;
+import com.google.inject.binder.ConstantBindingBuilder;
+import com.google.inject.binder.LinkedBindingBuilder;
+import com.google.inject.binder.ScopedBindingBuilder;
+import static com.google.inject.internal.Objects.nonNull;
+
+import java.lang.annotation.Annotation;
+
+/**
+ * Immutable snapshot of a request to bind a value.
+ *
+ * @author jessewilson@google.com (Jesse Wilson)
+ */
+public final class BindCommand<T> implements Command {
+  private Key<T> key;
+  private BindTarget<T> bindTarget;
+  private BindScoping bindScoping;
+
+  BindCommand(Key<T> key) {
+    this.key = nonNull(key, "key");
+  }
+
+  public <V> V acceptVisitor(Visitor<V> visitor) {
+    return visitor.visitBind(this);
+  }
+
+  public Key<T> getKey() {
+    return key;
+  }
+
+  public BindTarget<T> getTarget() {
+    return bindTarget;
+  }
+
+  public BindScoping getScoping() {
+    return bindScoping;
+  }
+
+  @Override public String toString() {
+    return "bind " + key
+        + (bindTarget == null ? "" : (" to " + bindTarget))
+        + (bindScoping == null ? "" : (" in " + bindScoping));
+  }
+
+  private static abstract class AbstractTarget<T> implements BindTarget<T> {
+    public void execute(ConstantBindingBuilder builder) {
+      throw new UnsupportedOperationException();
+    }
+    public T get(T defaultValue) {
+      return defaultValue;
+    }
+    public Key<? extends Provider<? extends T>> getProviderKey(
+        Key<Provider<? extends T>> defaultValue) {
+      return defaultValue;
+    }
+    public Provider<? extends T> getProvider(Provider<? extends T> defaultValue) {
+      return defaultValue;
+    }
+    public Key<? extends T> getKey(Key<? extends T> defaultValue) {
+      return defaultValue;
+    }
+  }
+
+  private static abstract class AbstractScoping implements BindScoping {
+    public boolean isEagerSingleton() {
+      return false;
+    }
+    public Scope getScope(Scope defaultValue) {
+      return defaultValue;
+    }
+    public Class<? extends Annotation> getScopeAnnotation(
+        Class<? extends Annotation> defaultValue) {
+      return defaultValue;
+    }
+  }
+
+  BindingBuilder bindingBuilder() {
+    return new BindingBuilder();
+  }
+
+  /**
+   * Package-private write access to the internal state of this command.
+   */
+  class BindingBuilder implements AnnotatedBindingBuilder<T> {
+    public LinkedBindingBuilder<T> annotatedWith(
+        Class<? extends Annotation> annotationType) {
+      assertNotAnnotated();
+      key = Key.get(key.getTypeLiteral(), annotationType);
+      return this;
+    }
+
+    public LinkedBindingBuilder<T> annotatedWith(Annotation annotation) {
+      nonNull(annotation, "annotation");
+      assertNotAnnotated();
+      key = Key.get(key.getTypeLiteral(), annotation);
+      return this;
+    }
+
+    public ScopedBindingBuilder to(final Class<? extends T> implementation) {
+      return to(Key.get(implementation));
+    }
+
+    public ScopedBindingBuilder to(
+        final TypeLiteral<? extends T> implementation) {
+      return to(Key.get(implementation));
+    }
+
+    public ScopedBindingBuilder to(final Key<? extends T> targetKey) {
+      nonNull(targetKey, "targetKey");
+      assertNoTarget();
+      bindTarget = new AbstractTarget<T>() {
+        public ScopedBindingBuilder execute(LinkedBindingBuilder<T> linkedBindingBuilder) {
+          return linkedBindingBuilder.to(targetKey);
+        }
+        @Override public Key<? extends T> getKey(Key<? extends T> defaultValue) {
+          return targetKey;
+        }
+        @Override public String toString() {
+          return String.valueOf(targetKey);
+        }
+      };
+      return this;
+    }
+
+    public void toInstance(final T instance) {
+      nonNull(instance, "instance"); // might someday want to tolerate null here
+      assertNoTarget();
+      bindTarget = new AbstractTarget<T>() {
+        public ScopedBindingBuilder execute(LinkedBindingBuilder<T> linkedBindingBuilder) {
+          linkedBindingBuilder.toInstance(instance);
+          return null;
+        }
+        @Override public T get(T defaultValue) {
+          return instance;
+        }
+        @Override public String toString() {
+          return "instance " + instance;
+        }
+      };
+    }
+
+    public ScopedBindingBuilder toProvider(final Provider<? extends T> provider) {
+      nonNull(provider, "provider");
+      assertNoTarget();
+      bindTarget = new AbstractTarget<T>() {
+        public ScopedBindingBuilder execute(LinkedBindingBuilder<T> linkedBindingBuilder) {
+          return linkedBindingBuilder.toProvider(provider);
+        }
+        @Override public Provider<? extends T> getProvider(Provider<? extends T> defaultValue) {
+          return provider;
+        }
+        @Override public String toString() {
+          return "provider " + provider;
+        }
+      };
+      return this;
+    }
+
+    public ScopedBindingBuilder toProvider(
+        Class<? extends Provider<? extends T>> providerType) {
+      return toProvider(Key.get(providerType));
+    }
+
+    public ScopedBindingBuilder toProvider(
+        final Key<? extends Provider<? extends T>> providerKey) {
+      nonNull(providerKey, "providerKey");
+      assertNoTarget();
+      bindTarget = new AbstractTarget<T>() {
+        public ScopedBindingBuilder execute(LinkedBindingBuilder<T> linkedBindingBuilder) {
+          return linkedBindingBuilder.toProvider(providerKey);
+        }
+        @Override public Key<? extends Provider<? extends T>> getProviderKey(
+            Key<Provider<? extends T>> defaultValue) {
+          return providerKey;
+        }
+        @Override public String toString() {
+          return "provider " + providerKey;
+        }
+      };
+      return this;
+    }
+
+    public void in(final Class<? extends Annotation> scopeAnnotation) {
+      nonNull(scopeAnnotation, "scopeAnnotation");
+      assertNoScope();
+
+      bindScoping = new AbstractScoping() {
+        public void execute(ScopedBindingBuilder scopedBindingBuilder) {
+          scopedBindingBuilder.in(scopeAnnotation);
+        }
+        @Override public Class<? extends Annotation> getScopeAnnotation(
+            Class<? extends Annotation> defaultValue) {
+          return scopeAnnotation;
+        }
+        @Override public String toString() {
+          return scopeAnnotation.getName();
+        }
+      };
+    }
+
+    public void in(final Scope scope) {
+      nonNull(scope, "scope");
+      assertNoScope();
+      bindScoping = new AbstractScoping() {
+        public void execute(ScopedBindingBuilder scopedBindingBuilder) {
+          scopedBindingBuilder.in(scope);
+        }
+        @Override public Scope getScope(Scope defaultValue) {
+          return scope;
+        }
+        @Override public String toString() {
+          return String.valueOf(scope);
+        }
+      };
+    }
+
+    public void asEagerSingleton() {
+      assertNoScope();
+      bindScoping = new AbstractScoping() {
+        public void execute(ScopedBindingBuilder scopedBindingBuilder) {
+          scopedBindingBuilder.asEagerSingleton();
+        }
+        @Override public boolean isEagerSingleton() {
+          return true;
+        }
+        @Override public String toString() {
+          return "eager singleton";
+        }
+      };
+    }
+
+    private void assertNoTarget() {
+      if (bindTarget != null) {
+        throw new IllegalStateException("Already targetted to " + bindTarget);
+      }
+    }
+
+    private void assertNotAnnotated() {
+      if (BindCommand.this.key.getAnnotationType() != null) {
+        throw new IllegalStateException("Already annotated with " + key.getAnnotationType());
+      }
+    }
+
+    private void assertNoScope() {
+      if (bindScoping != null) {
+        throw new IllegalStateException("Already scoped by " + bindScoping);
+      }
+    }
+  }
+}
diff --git a/extensions/commands/src/com/google/inject/commands/BindConstantCommand.java b/extensions/commands/src/com/google/inject/commands/BindConstantCommand.java
new file mode 100644
index 0000000..142bb19
--- /dev/null
+++ b/extensions/commands/src/com/google/inject/commands/BindConstantCommand.java
@@ -0,0 +1,334 @@
+/**
+ * Copyright (C) 2008 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.
+ */
+
+package com.google.inject.commands;
+
+import com.google.inject.Key;
+import com.google.inject.Provider;
+import com.google.inject.binder.AnnotatedConstantBindingBuilder;
+import com.google.inject.binder.ConstantBindingBuilder;
+import com.google.inject.binder.LinkedBindingBuilder;
+import com.google.inject.binder.ScopedBindingBuilder;
+import com.google.inject.internal.Objects;
+
+import java.lang.annotation.Annotation;
+
+/**
+ * Immutable snapshot of a request to bind a constant.
+ *
+ * @author jessewilson@google.com (Jesse Wilson)
+ */
+public final class BindConstantCommand implements Command {
+  private BindingAnnotation bindingAnnotation;
+  private ConstantTarget<?> target;
+
+  BindConstantCommand() {
+    // hide public constructor
+  }
+
+  public <T> T acceptVisitor(Visitor<T> visitor) {
+    return visitor.visitBindConstant(this);
+  }
+
+  public BindTarget<?> getTarget() {
+    return target;
+  }
+
+  public <T> Key<T> getKey() {
+    return bindingAnnotation.getKey();
+  }
+
+  /**
+   * Target API for bindConstant().
+   */
+  private static abstract class ConstantTarget<T> implements BindTarget<T> {
+
+    /**
+     * Returns the type of constant, such as {@code int.class} or
+     * {@code Enum.class}.
+     */
+    abstract Class getType();
+
+    public boolean hasInstance() {
+      return true;
+    }
+    public ScopedBindingBuilder execute(LinkedBindingBuilder linkedBindingBuilder) {
+      throw new UnsupportedOperationException();
+    }
+    public Provider<? extends T> getProvider(Provider<? extends T> defaultValue) {
+      return defaultValue;
+    }
+    public Key<? extends Provider<? extends T>> getProviderKey(
+        Key<Provider<? extends T>> defaultValue) {
+      return defaultValue;
+    }
+    public Key<? extends T> getKey(Key<? extends T> defaultValue) {
+      return defaultValue;
+    }
+  }
+
+  /**
+   * Internal annotation API.
+   */
+  private abstract class BindingAnnotation {
+    abstract ConstantBindingBuilder execute(AnnotatedConstantBindingBuilder builder);
+    abstract <T> Key<T> getKey();
+  }
+
+  BindingBuilder bindingBuilder() {
+    return new BindingBuilder();
+  }
+
+  /**
+   * Package-private write access to the internal state of this command.
+   */
+  class BindingBuilder
+      implements AnnotatedConstantBindingBuilder, ConstantBindingBuilder {
+
+    private void assertNoBindingAnnotation() {
+      if (bindingAnnotation != null) {
+        throw new IllegalStateException("Already annotated with " + bindingAnnotation);
+      }
+    }
+
+    private void assertNoTarget() {
+      if (target != null) {
+        throw new IllegalStateException("Already targetted to " + target);
+      }
+    }
+
+    public ConstantBindingBuilder annotatedWith(final Class<? extends Annotation> annotationType) {
+      assertNoBindingAnnotation();
+
+      bindingAnnotation = new BindingAnnotation() {
+        public ConstantBindingBuilder execute(AnnotatedConstantBindingBuilder builder) {
+          return builder.annotatedWith(annotationType);
+        }
+        @SuppressWarnings({"unchecked"})
+        public <T> Key<T> getKey() {
+          return Key.get((Class<T>) target.getType(), annotationType);
+        }
+      };
+      return this;
+    }
+
+    public ConstantBindingBuilder annotatedWith(final Annotation annotation) {
+      assertNoBindingAnnotation();
+
+      bindingAnnotation = new BindingAnnotation() {
+        public ConstantBindingBuilder execute(AnnotatedConstantBindingBuilder builder) {
+          return builder.annotatedWith(annotation);
+        }
+        @SuppressWarnings({"unchecked"})
+        public <T> Key<T> getKey() {
+          return Key.get((Class<T>) target.getType(), annotation);
+        }
+      };
+      return this;
+    }
+
+    public void to(final String value) {
+      assertNoTarget();
+
+      BindConstantCommand.this.target = new ConstantTarget() {
+        public void execute(ConstantBindingBuilder builder) {
+          builder.to(value);
+        }
+        public Object get(Object defaultValue) {
+          return value;
+        }
+        public Class getType() {
+          return String.class;
+        }
+        @Override public String toString() {
+          return value;
+        }
+      };
+    }
+
+    public void to(final int value) {
+      assertNoTarget();
+
+      BindConstantCommand.this.target = new ConstantTarget() {
+        public void execute(ConstantBindingBuilder builder) {
+          builder.to(value);
+        }
+        public Object get(Object defaultValue) {
+          return value;
+        }
+        public Class getType() {
+          return Integer.class;
+        }
+        @Override public String toString() {
+          return String.valueOf(value);
+        }
+      };
+    }
+
+    public void to(final long value) {
+      assertNoTarget();
+
+      BindConstantCommand.this.target = new ConstantTarget() {
+        public void execute(ConstantBindingBuilder builder) {
+          builder.to(value);
+        }
+        public Object get(Object defaultValue) {
+          return value;
+        }
+        public Class getType() {
+          return Long.class;
+        }
+        @Override public String toString() {
+          return String.valueOf(value);
+        }
+      };
+    }
+
+    public void to(final boolean value) {
+      assertNoTarget();
+
+      BindConstantCommand.this.target = new ConstantTarget() {
+        public void execute(ConstantBindingBuilder builder) {
+          builder.to(value);
+        }
+        public Object get(Object defaultValue) {
+          return value;
+        }
+        public Class getType() {
+          return Boolean.class;
+        }
+        @Override public String toString() {
+          return String.valueOf(value);
+        }
+      };
+    }
+
+    public void to(final double value) {
+      assertNoTarget();
+
+      BindConstantCommand.this.target = new ConstantTarget() {
+        public void execute(ConstantBindingBuilder builder) {
+          builder.to(value);
+        }
+        public Object get(Object defaultValue) {
+          return value;
+        }
+        public Class getType() {
+          return Double.class;
+        }
+        @Override public String toString() {
+          return String.valueOf(value);
+        }
+      };
+    }
+
+    public void to(final float value) {
+      assertNoTarget();
+
+      BindConstantCommand.this.target = new ConstantTarget() {
+        public void execute(ConstantBindingBuilder builder) {
+          builder.to(value);
+        }
+        public Object get(Object defaultValue) {
+          return value;
+        }
+        public Class getType() {
+          return Float.class;
+        }
+        @Override public String toString() {
+          return String.valueOf(value);
+        }
+      };
+    }
+
+    public void to(final short value) {
+      assertNoTarget();
+
+      BindConstantCommand.this.target = new ConstantTarget() {
+        public void execute(ConstantBindingBuilder builder) {
+          builder.to(value);
+        }
+        public Object get(Object defaultValue) {
+          return value;
+        }
+        public Class getType() {
+          return Short.class;
+        }
+        @Override public String toString() {
+          return String.valueOf(value);
+        }
+      };
+    }
+
+    public void to(final char value) {
+      assertNoTarget();
+
+      BindConstantCommand.this.target = new ConstantTarget() {
+        public void execute(ConstantBindingBuilder builder) {
+          builder.to(value);
+        }
+        public Object get(Object defaultValue) {
+          return value;
+        }
+        public Class getType() {
+          return Character.class;
+        }
+        @Override public String toString() {
+          return String.valueOf(value);
+        }
+      };
+    }
+
+    public void to(final Class<?> value) {
+      assertNoTarget();
+
+      BindConstantCommand.this.target = new ConstantTarget() {
+        public void execute(ConstantBindingBuilder builder) {
+          builder.to(value);
+        }
+        public Object get(Object defaultValue) {
+          return value;
+        }
+        public Class getType() {
+          return Class.class;
+        }
+        @Override public String toString() {
+          return String.valueOf(value);
+        }
+      };
+    }
+
+    public <E extends Enum<E>> void to(final E value) {
+      Objects.nonNull(value, "value");
+      assertNoTarget();
+
+      BindConstantCommand.this.target = new ConstantTarget() {
+        public void execute(ConstantBindingBuilder builder) {
+          builder.to(value);
+        }
+        public Object get(Object defaultValue) {
+          return value;
+        }
+        public Class getType() {
+          return value.getDeclaringClass();
+        }
+        @Override public String toString() {
+          return String.valueOf(value);
+        }
+      };
+    }
+  }
+}
diff --git a/extensions/commands/src/com/google/inject/commands/BindInterceptorCommand.java b/extensions/commands/src/com/google/inject/commands/BindInterceptorCommand.java
new file mode 100644
index 0000000..ad12345
--- /dev/null
+++ b/extensions/commands/src/com/google/inject/commands/BindInterceptorCommand.java
@@ -0,0 +1,61 @@
+/**
+ * Copyright (C) 2008 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.
+ */
+
+package com.google.inject.commands;
+
+import com.google.inject.matcher.Matcher;
+import org.aopalliance.intercept.MethodInterceptor;
+
+import java.lang.reflect.Method;
+import java.util.Arrays;
+import static java.util.Collections.unmodifiableList;
+import java.util.List;
+
+/**
+ * Immutable snapshot of a request to bind an interceptor.
+ *
+ * @author jessewilson@google.com (Jesse Wilson)
+ */
+public final class BindInterceptorCommand implements Command {
+  private final Matcher<? super Class<?>> classMatcher;
+  private final Matcher<? super Method> methodMatcher;
+  private final List<MethodInterceptor> interceptors;
+
+  BindInterceptorCommand(
+      Matcher<? super Class<?>> classMatcher,
+      Matcher<? super Method> methodMatcher,
+      MethodInterceptor[] interceptors) {
+    this.classMatcher = classMatcher;
+    this.methodMatcher = methodMatcher;
+    this.interceptors = unmodifiableList(Arrays.asList(interceptors.clone()));
+  }
+
+  public Matcher<? super Class<?>> getClassMatcher() {
+    return classMatcher;
+  }
+
+  public Matcher<? super Method> getMethodMatcher() {
+    return methodMatcher;
+  }
+
+  public List<MethodInterceptor> getInterceptors() {
+    return interceptors;
+  }
+
+  public <T> T acceptVisitor(Visitor<T> visitor) {
+    return visitor.visitBindInterceptor(this);
+  }
+}
diff --git a/extensions/commands/src/com/google/inject/commands/BindScopeCommand.java b/extensions/commands/src/com/google/inject/commands/BindScopeCommand.java
new file mode 100644
index 0000000..639f2c5
--- /dev/null
+++ b/extensions/commands/src/com/google/inject/commands/BindScopeCommand.java
@@ -0,0 +1,49 @@
+/**
+ * Copyright (C) 2008 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.
+ */
+
+package com.google.inject.commands;
+
+import com.google.inject.Scope;
+
+import java.lang.annotation.Annotation;
+
+/**
+ * Immutable snapshot of a request to bind a scope.
+ *
+ * @author jessewilson@google.com (Jesse Wilson)
+ */
+public final class BindScopeCommand implements Command {
+  private final Class<? extends Annotation> annotationType;
+  private final Scope scope;
+
+  BindScopeCommand(
+      Class<? extends Annotation> annotationType, Scope scope) {
+    this.annotationType = annotationType;
+    this.scope = scope;
+  }
+
+  public Class<? extends Annotation> getAnnotationType() {
+    return annotationType;
+  }
+
+  public Scope getScope() {
+    return scope;
+  }
+
+  public <T> T acceptVisitor(Visitor<T> visitor) {
+    return visitor.visitBindScope(this);
+  }
+}
diff --git a/extensions/commands/src/com/google/inject/commands/BindScoping.java b/extensions/commands/src/com/google/inject/commands/BindScoping.java
new file mode 100644
index 0000000..52e162d
--- /dev/null
+++ b/extensions/commands/src/com/google/inject/commands/BindScoping.java
@@ -0,0 +1,35 @@
+/**
+ * Copyright (C) 2008 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.
+ */
+
+package com.google.inject.commands;
+
+import com.google.inject.Scope;
+import com.google.inject.binder.ScopedBindingBuilder;
+
+import java.lang.annotation.Annotation;
+
+
+/**
+ * Immutable snapshot of a binding scope.
+ *
+ * @author jessewilson@google.com (Jesse Wilson)
+ */
+public interface BindScoping {
+  void execute(ScopedBindingBuilder scopedBindingBuilder);
+  boolean isEagerSingleton();
+  Scope getScope(Scope defaultValue);
+  Class<? extends Annotation> getScopeAnnotation(Class<? extends Annotation> defaultValue);
+}
diff --git a/extensions/commands/src/com/google/inject/commands/BindTarget.java b/extensions/commands/src/com/google/inject/commands/BindTarget.java
new file mode 100644
index 0000000..99bf418
--- /dev/null
+++ b/extensions/commands/src/com/google/inject/commands/BindTarget.java
@@ -0,0 +1,54 @@
+/**
+ * Copyright (C) 2008 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.
+ */
+
+package com.google.inject.commands;
+
+import com.google.inject.Key;
+import com.google.inject.Provider;
+import com.google.inject.binder.ConstantBindingBuilder;
+import com.google.inject.binder.LinkedBindingBuilder;
+import com.google.inject.binder.ScopedBindingBuilder;
+
+
+/**
+ * A binding target, which provides instances from a specific key. 
+ *
+ * @author jessewilson@google.com (Jesse Wilson)
+ */
+public interface BindTarget<T> {
+
+  /**
+   * Execute this target against the linked binding builder.
+   */
+  ScopedBindingBuilder execute(LinkedBindingBuilder<T> linkedBindingBuilder);
+
+  /**
+   * Execute this target against the constant binding builder.
+   */
+  void execute(ConstantBindingBuilder builder);
+
+  /**
+   * Returns the bound instance, if it exists, or {@code defaultValue}
+   * if no bound value exists.
+   */
+  T get(T defaultValue);
+
+  Provider<? extends T> getProvider(Provider<? extends T> defaultValue);
+
+  Key<? extends Provider<? extends T>> getProviderKey(Key<Provider<? extends T>> defaultValue);
+
+  Key<? extends T> getKey(Key<? extends T> defaultValue);
+}
diff --git a/extensions/commands/src/com/google/inject/commands/Command.java b/extensions/commands/src/com/google/inject/commands/Command.java
new file mode 100644
index 0000000..15c1729
--- /dev/null
+++ b/extensions/commands/src/com/google/inject/commands/Command.java
@@ -0,0 +1,41 @@
+/**
+ * Copyright (C) 2008 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.
+ */
+
+package com.google.inject.commands;
+
+/**
+ * Immutable snapshot of a binding command.
+ *
+ * @author jessewilson@google.com (Jesse Wilson)
+ */
+public interface Command {
+  <T> T acceptVisitor(Visitor<T> visitor);
+
+  /**
+   * Visit commands.
+   */
+  public interface Visitor<V> {
+    V visitAddMessageError(AddMessageErrorCommand command);
+    V visitAddError(AddThrowableErrorCommand command);
+    V visitBindInterceptor(BindInterceptorCommand command);
+    V visitBindScope(BindScopeCommand command);
+    V visitRequestStaticInjection(RequestStaticInjectionCommand command);
+    V visitBindConstant(BindConstantCommand command);
+    V visitConvertToTypes(ConvertToTypesCommand command);
+    <T> V visitBind(BindCommand<T> command);
+    <T> V visitGetProvider(GetProviderCommand<T> command);
+  }
+}
diff --git a/extensions/commands/src/com/google/inject/commands/CommandRecorder.java b/extensions/commands/src/com/google/inject/commands/CommandRecorder.java
new file mode 100644
index 0000000..9c3ed9b
--- /dev/null
+++ b/extensions/commands/src/com/google/inject/commands/CommandRecorder.java
@@ -0,0 +1,142 @@
+/**
+ * Copyright (C) 2008 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.
+ */
+
+package com.google.inject.commands;
+
+import com.google.inject.*;
+import com.google.inject.binder.AnnotatedBindingBuilder;
+import com.google.inject.binder.AnnotatedConstantBindingBuilder;
+import com.google.inject.matcher.Matcher;
+import com.google.inject.spi.TypeConverter;
+import org.aopalliance.intercept.MethodInterceptor;
+
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Records commands executed by a module so they can be inspected or
+ * {@link CommandReplayer replayed}.
+ *
+ * @author jessewilson@google.com (Jesse Wilson)
+ */
+public final class CommandRecorder {
+  private final Stage stage = Stage.DEVELOPMENT;
+  private final EarlyRequestsProvider earlyRequestsProvider;
+
+  /**
+   * @param earlyRequestsProvider satisfies requests to
+   *     {@link Binder#getProvider} at module execution time. For modules that
+   *     will be used to create an injector, use {@link FutureInjector}.
+   */
+  public CommandRecorder(EarlyRequestsProvider earlyRequestsProvider) {
+    this.earlyRequestsProvider = earlyRequestsProvider;
+  }
+
+  /**
+   * Records the commands executed by {@code modules}.
+   */
+  public List<Command> recordCommands(Module... modules) {
+    return recordCommands(Arrays.asList(modules));
+  }
+
+  /**
+   * Records the commands executed by {@code modules}.
+   */
+  public List<Command> recordCommands(Iterable<Module> modules) {
+    RecordingBinder binder = new RecordingBinder();
+    for (Module module : modules) {
+      module.configure(binder);
+    }
+    return Collections.unmodifiableList(binder.commands);
+  }
+
+  private class RecordingBinder implements Binder {
+    private final List<Command> commands = new ArrayList<Command>();
+
+    public void bindInterceptor(
+        Matcher<? super Class<?>> classMatcher,
+        Matcher<? super Method> methodMatcher,
+        MethodInterceptor... interceptors) {
+      commands.add(new BindInterceptorCommand(classMatcher, methodMatcher, interceptors));
+    }
+
+    public void bindScope(Class<? extends Annotation> annotationType, Scope scope) {
+      commands.add(new BindScopeCommand(annotationType, scope));
+    }
+
+    public void requestStaticInjection(Class<?>... types) {
+      commands.add(new RequestStaticInjectionCommand(types));
+    }
+
+    public void install(Module module) {
+      module.configure(this);
+    }
+
+    public Stage currentStage() {
+      return stage;
+    }
+
+    public void addError(String message, Object... arguments) {
+      commands.add(new AddMessageErrorCommand(message, arguments));
+    }
+
+    public void addError(Throwable t) {
+      commands.add(new AddThrowableErrorCommand(t));
+    }
+
+    public <T> BindCommand<T>.BindingBuilder bind(Key<T> key) {
+      BindCommand<T> bindCommand = new BindCommand<T>(key);
+      commands.add(bindCommand);
+      return bindCommand.bindingBuilder();
+    }
+
+    public <T> AnnotatedBindingBuilder<T> bind(TypeLiteral<T> typeLiteral) {
+      return bind(Key.get(typeLiteral));
+    }
+
+    public <T> AnnotatedBindingBuilder<T> bind(Class<T> type) {
+      return bind(Key.get(type));
+    }
+
+    public AnnotatedConstantBindingBuilder bindConstant() {
+      BindConstantCommand bindConstantCommand = new BindConstantCommand();
+      commands.add(bindConstantCommand);
+      return bindConstantCommand.bindingBuilder();
+    }
+
+    public <T> Provider<T> getProvider(final Key<T> key) {
+      commands.add(new GetProviderCommand<T>(key, earlyRequestsProvider));
+      return new Provider<T>() {
+        public T get() {
+          return earlyRequestsProvider.get(key);
+        }
+      };
+    }
+
+    public <T> Provider<T> getProvider(Class<T> type) {
+      return getProvider(Key.get(type));
+    }
+
+    public void convertToTypes(Matcher<? super TypeLiteral<?>> typeMatcher,
+                               TypeConverter converter) {
+      commands.add(new ConvertToTypesCommand(typeMatcher, converter));
+    }
+  }
+}
diff --git a/extensions/commands/src/com/google/inject/commands/CommandReplayer.java b/extensions/commands/src/com/google/inject/commands/CommandReplayer.java
new file mode 100644
index 0000000..4b06890
--- /dev/null
+++ b/extensions/commands/src/com/google/inject/commands/CommandReplayer.java
@@ -0,0 +1,164 @@
+/**
+ * Copyright (C) 2008 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.
+ */
+
+package com.google.inject.commands;
+
+import com.google.inject.Binder;
+import com.google.inject.Key;
+import com.google.inject.Module;
+import com.google.inject.binder.AnnotatedConstantBindingBuilder;
+import com.google.inject.binder.ConstantBindingBuilder;
+import com.google.inject.binder.LinkedBindingBuilder;
+import com.google.inject.binder.ScopedBindingBuilder;
+import com.google.inject.internal.Objects;
+import org.aopalliance.intercept.MethodInterceptor;
+
+import java.util.List;
+
+/**
+ * Executes commands against a binder.
+ *
+ * @author jessewilson@google.com (Jesse Wilson)
+ */
+public class CommandReplayer {
+
+  /**
+   * Returns a module that executes the specified commands
+   * using this executing visitor.
+   */
+  public Module createModule(final Iterable<Command> commands) {
+    return new Module() {
+      public void configure(Binder binder) {
+        replay(binder, commands);
+      }
+    };
+  }
+
+  /**
+   * Replays {@code commands} against {@code binder}.
+   */
+  public void replay(final Binder binder, Iterable<Command> commands) {
+    Objects.nonNull(binder, "binder");
+    Objects.nonNull(commands, "commands");
+
+    Command.Visitor<Void> visitor = new Command.Visitor<Void>() {
+      public Void visitAddMessageError(AddMessageErrorCommand command) {
+        replayAddMessageError(binder, command);
+        return null;
+      }
+
+      public Void visitAddError(AddThrowableErrorCommand command) {
+        replayAddError(binder, command);
+        return null;
+      }
+
+      public Void visitBindInterceptor(BindInterceptorCommand command) {
+        replayBindInterceptor(binder, command);
+        return null;
+      }
+
+      public Void visitBindScope(BindScopeCommand command) {
+        replayBindScope(binder, command);
+        return null;
+      }
+
+      public Void visitRequestStaticInjection(RequestStaticInjectionCommand command) {
+        replayRequestStaticInjection(binder, command);
+        return null;
+      }
+
+      public Void visitBindConstant(BindConstantCommand command) {
+        replayBindConstant(binder, command);
+        return null;
+      }
+
+      public Void visitConvertToTypes(ConvertToTypesCommand command) {
+        replayConvertToTypes(binder, command);
+        return null;
+      }
+
+      public <T> Void visitBind(BindCommand<T> command) {
+        replayBind(binder, command);
+        return null;
+      }
+
+      public <T> Void visitGetProvider(GetProviderCommand<T> command) {
+        replayGetProvider(binder, command);
+        return null;
+      }
+    };
+
+    for (Command command : commands) {
+      command.acceptVisitor(visitor);
+    }
+  }
+
+  public void replayAddMessageError(Binder binder, AddMessageErrorCommand command) {
+    binder.addError(command.getMessage(), command.getArguments().toArray());
+  }
+
+  public void replayAddError(Binder binder, AddThrowableErrorCommand command) {
+    binder.addError(command.getThrowable());
+  }
+
+  public void replayBindInterceptor(Binder binder, BindInterceptorCommand command) {
+    List<MethodInterceptor> interceptors = command.getInterceptors();
+    binder.bindInterceptor(command.getClassMatcher(), command.getMethodMatcher(),
+        interceptors.toArray(new MethodInterceptor[interceptors.size()]));
+  }
+
+  public void replayBindScope(Binder binder, BindScopeCommand command) {
+    binder.bindScope(command.getAnnotationType(), command.getScope());
+  }
+
+  public void replayRequestStaticInjection(Binder binder, RequestStaticInjectionCommand command) {
+    List<Class> types = command.getTypes();
+    binder.requestStaticInjection(types.toArray(new Class[types.size()]));
+  }
+
+  public void replayBindConstant(Binder binder, BindConstantCommand command) {
+    AnnotatedConstantBindingBuilder constantBindingBuilder = binder.bindConstant();
+
+    Key<Object> key = command.getKey();
+    ConstantBindingBuilder builder = key.getAnnotation() != null
+        ? constantBindingBuilder.annotatedWith(key.getAnnotation())
+        : constantBindingBuilder.annotatedWith(key.getAnnotationType());
+
+    command.getTarget().execute(builder);
+  }
+
+  public void replayConvertToTypes(Binder binder, ConvertToTypesCommand command) {
+    binder.convertToTypes(command.getTypeMatcher(), command.getTypeConverter());
+  }
+
+  public <T> void replayBind(Binder binder, BindCommand<T> command) {
+    LinkedBindingBuilder<T> lbb = binder.bind(command.getKey());
+
+    BindTarget<T> bindTarget = command.getTarget();
+    ScopedBindingBuilder sbb = bindTarget != null
+        ? bindTarget.execute(lbb)
+        : lbb;
+
+    BindScoping scoping = command.getScoping();
+    if (scoping != null) {
+      scoping.execute(sbb);
+    }
+  }
+
+  public <T> void replayGetProvider(Binder binder, GetProviderCommand<T> command) {
+    binder.getProvider(command.getKey());
+  }
+}
diff --git a/extensions/commands/src/com/google/inject/commands/ConvertToTypesCommand.java b/extensions/commands/src/com/google/inject/commands/ConvertToTypesCommand.java
new file mode 100644
index 0000000..a02536e
--- /dev/null
+++ b/extensions/commands/src/com/google/inject/commands/ConvertToTypesCommand.java
@@ -0,0 +1,50 @@
+/**
+ * Copyright (C) 2008 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.
+ */
+
+package com.google.inject.commands;
+
+import com.google.inject.TypeLiteral;
+import com.google.inject.matcher.Matcher;
+import com.google.inject.spi.TypeConverter;
+
+
+/**
+ * Immutable snapshot of a request to convert binder types.
+ *
+ * @author jessewilson@google.com (Jesse Wilson)
+ */
+public final class ConvertToTypesCommand implements Command {
+  private final Matcher<? super TypeLiteral<?>> typeMatcher;
+  private final TypeConverter typeConverter;
+
+  ConvertToTypesCommand(Matcher<? super TypeLiteral<?>> typeMatcher,
+      TypeConverter typeConverter) {
+    this.typeMatcher = typeMatcher;
+    this.typeConverter = typeConverter;
+  }
+
+  public Matcher<? super TypeLiteral<?>> getTypeMatcher() {
+    return typeMatcher;
+  }
+
+  public TypeConverter getTypeConverter() {
+    return typeConverter;
+  }
+
+  public <T> T acceptVisitor(Visitor<T> visitor) {
+    return visitor.visitConvertToTypes(this);
+  }
+}
diff --git a/extensions/commands/src/com/google/inject/commands/EarlyRequestsProvider.java b/extensions/commands/src/com/google/inject/commands/EarlyRequestsProvider.java
new file mode 100644
index 0000000..bf3f4c3
--- /dev/null
+++ b/extensions/commands/src/com/google/inject/commands/EarlyRequestsProvider.java
@@ -0,0 +1,28 @@
+/**
+ * Copyright (C) 2008 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.
+ */
+
+package com.google.inject.commands;
+
+import com.google.inject.Key;
+
+/**
+ * Satisfies requests of the binder.
+ *
+ * @author jessewilson@google.com (Jesse Wilson)
+ */
+public interface EarlyRequestsProvider {
+  public <T> T get(Key<T> key);
+}
diff --git a/extensions/commands/src/com/google/inject/commands/FutureInjector.java b/extensions/commands/src/com/google/inject/commands/FutureInjector.java
new file mode 100644
index 0000000..e846ec1
--- /dev/null
+++ b/extensions/commands/src/com/google/inject/commands/FutureInjector.java
@@ -0,0 +1,60 @@
+/**
+ * Copyright (C) 2008 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.
+ */
+
+package com.google.inject.commands;
+
+import com.google.inject.Injector;
+import com.google.inject.Key;
+
+
+/**
+ * Satisfies binding requests using an eventually-created Injector. To use:
+ *
+ * <ol><li>Record commands using a {@link CommandRecorder}.</li>
+ * <li>Create an injector by replaying those commands (and possibly rewriting
+ * them) using {@link CommandReplayer}.</li>
+ * <li>Initialize the injector in the {@code FutureInjector}.</li></ol>
+ *
+ * <pre>
+ * FutureInjector futureInjector = new FutureInjector();
+ * List&lt;Command&gt; commands = new CommandRecorder(futureInjector).recordCommands(modules);
+ * Module module = new CommandRewriter().createModule(commands);
+ * Injector injector = Guice.createInjector(module);
+ * futureInjector.initialize(injector);
+ * </pre>
+ *
+ * @author jessewilson@google.com (Jesse Wilson)
+ */
+public final class FutureInjector implements EarlyRequestsProvider {
+  /** manually initialized later */
+  private Injector injector;
+
+  public void initialize(Injector injector) {
+    if (this.injector != null) {
+      throw new IllegalStateException("Already initialized");
+    }
+
+    this.injector = injector;
+  }
+
+  public <T> T get(Key<T> key) {
+    if (injector == null) {
+      throw new IllegalStateException("Not yet initialized");
+    }
+
+    return injector.getInstance(key);
+  }
+}
diff --git a/extensions/commands/src/com/google/inject/commands/GetProviderCommand.java b/extensions/commands/src/com/google/inject/commands/GetProviderCommand.java
new file mode 100644
index 0000000..e155cd7
--- /dev/null
+++ b/extensions/commands/src/com/google/inject/commands/GetProviderCommand.java
@@ -0,0 +1,46 @@
+/**
+ * Copyright (C) 2008 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.
+ */
+
+package com.google.inject.commands;
+
+import com.google.inject.Key;
+
+/**
+ * Immutable snapshot of a request for a provider.
+ *
+ * @author jessewilson@google.com (Jesse Wilson)
+ */
+public final class GetProviderCommand<T> implements Command {
+  private final Key<T> key;
+  private final EarlyRequestsProvider earlyRequestsProvider;
+
+  GetProviderCommand(Key<T> key, EarlyRequestsProvider earlyRequestsProvider) {
+    this.key = key;
+    this.earlyRequestsProvider = earlyRequestsProvider;
+  }
+
+  public Key<T> getKey() {
+    return key;
+  }
+
+  public <T> T acceptVisitor(Visitor<T> visitor) {
+    return visitor.visitGetProvider(this);
+  }
+
+  public EarlyRequestsProvider getEarlyRequestsProvider() {
+    return earlyRequestsProvider;
+  }
+}
diff --git a/extensions/commands/src/com/google/inject/commands/RequestStaticInjectionCommand.java b/extensions/commands/src/com/google/inject/commands/RequestStaticInjectionCommand.java
new file mode 100644
index 0000000..677bd34
--- /dev/null
+++ b/extensions/commands/src/com/google/inject/commands/RequestStaticInjectionCommand.java
@@ -0,0 +1,42 @@
+/**
+ * Copyright (C) 2008 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.
+ */
+
+package com.google.inject.commands;
+
+import java.util.Arrays;
+import static java.util.Collections.unmodifiableList;
+import java.util.List;
+
+/**
+ * Immutable snapshot of a request for static injection.
+ * 
+ * @author jessewilson@google.com (Jesse Wilson)
+ */
+public final class RequestStaticInjectionCommand implements Command {
+  private final List<Class> types;
+
+  RequestStaticInjectionCommand(Class[] types) {
+    this.types = unmodifiableList(Arrays.asList(types.clone()));
+  }
+
+  public List<Class> getTypes() {
+    return types;
+  }
+
+  public <T> T acceptVisitor(Visitor<T> visitor) {
+    return visitor.visitRequestStaticInjection(this);
+  }
+}
diff --git a/extensions/commands/src/com/google/inject/commands/intercepting/InterceptingInjectorBuilder.java b/extensions/commands/src/com/google/inject/commands/intercepting/InterceptingInjectorBuilder.java
new file mode 100644
index 0000000..0214355
--- /dev/null
+++ b/extensions/commands/src/com/google/inject/commands/intercepting/InterceptingInjectorBuilder.java
@@ -0,0 +1,211 @@
+/**
+ * Copyright (C) 2008 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.
+ */
+
+package com.google.inject.commands.intercepting;
+
+import com.google.inject.*;
+import com.google.inject.binder.LinkedBindingBuilder;
+import com.google.inject.binder.ScopedBindingBuilder;
+import static com.google.inject.internal.Objects.nonNull;
+import com.google.inject.commands.*;
+
+import java.lang.annotation.Annotation;
+import java.lang.annotation.Retention;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+import java.util.*;
+
+/**
+ * Builds an {@link Injector} that intercepts provision.
+ *
+ * <h3>Limitations of the current implementation</h3>
+ *
+ * <p>All intercepted bindings must have binding targets - for example, a type
+ * that is bound to itself cannot be intercepted:
+ * <pre class="code">bind(MyServiceClass.class);</pre>
+ *
+ * <p>All intercepted bindings must be bound explicitly. Interception cannot
+ * be applied to implicit bindings, or bindings that depend on
+ * {@literal @}{@link ProvidedBy}, {@literal @}{@link ImplementedBy}
+ * annotations.
+ *
+ * <p><strong>Implementation note:</strong> To intercept provision, an
+ * additional, internal binding is created for each intercepted key. This is
+ * used to bind the original (non-intercepted) provisioning strategy, and an
+ * intercepting binding is created for the original key. This shouldn't have
+ * any side-effects on the behaviour of the injector, but may confuse tools
+ * that depend on {@link Injector#getBindings()} and similar methods.
+ *
+ * @author jessewilson@google.com (Jesse Wilson)
+ * @author jmourits@google.com (Jerome Mourits)
+ */
+public final class InterceptingInjectorBuilder {
+
+  private static final Key<ProvisionInterceptor> INJECTION_INTERCEPTOR_KEY
+      = Key.get(ProvisionInterceptor.class);
+
+  private final Collection<Module> modules = new ArrayList<Module>();
+  private final Set<Key<?>> keysToIntercept = new HashSet<Key<?>>();
+  private boolean tolerateUnmatchedInterceptions = false;
+
+  public InterceptingInjectorBuilder install(Module... modules) {
+    this.modules.addAll(Arrays.asList(modules));
+    return this;
+  }
+
+  public InterceptingInjectorBuilder install(Collection<Module> modules) {
+    this.modules.addAll(modules);
+    return this;
+  }
+
+  public InterceptingInjectorBuilder intercept(Key<?>... keys) {
+    this.keysToIntercept.addAll(Arrays.asList(keys));
+    return this;
+  }
+
+  public InterceptingInjectorBuilder intercept(Collection<Key<?>> keys) {
+    if (keys.contains(INJECTION_INTERCEPTOR_KEY)) {
+      throw new IllegalArgumentException("Cannot intercept the interceptor!");
+    }
+
+    keysToIntercept.addAll(keys);
+    return this;
+  }
+
+  public InterceptingInjectorBuilder intercept(Class<?>... classes) {
+    List<Key<?>> keysAsList = new ArrayList<Key<?>>(classes.length);
+    for (Class<?> clas : classes) {
+      keysAsList.add(Key.get(clas));
+    }
+
+    return intercept(keysAsList);
+  }
+
+  public InterceptingInjectorBuilder tolerateUnmatchedInterceptions() {
+    this.tolerateUnmatchedInterceptions = true;
+    return this;
+  }
+
+  public Injector build() {
+    FutureInjector futureInjector = new FutureInjector();
+
+    // record commands from the modules
+    List<Command> commands = new CommandRecorder(futureInjector).recordCommands(modules);
+
+    // rewrite the commands to insert interception
+    CommandRewriter rewriter = new CommandRewriter();
+    Module module = rewriter.createModule(commands);
+
+    // create and injector with the rewritten commands
+    Injector injector = Guice.createInjector(module);
+
+    // fail if any interceptions were missing
+    if (!tolerateUnmatchedInterceptions 
+        && !rewriter.keysIntercepted.equals(keysToIntercept)) {
+      Set<Key> keysNotIntercepted = new HashSet<Key>(keysToIntercept);
+      keysNotIntercepted.removeAll(rewriter.keysIntercepted);
+      throw new IllegalArgumentException("An explicit binding is required for "
+          + "all intercepted keys, but was not found for " + keysNotIntercepted);
+    }
+
+    // make the injector available for callbacks from early providers
+    futureInjector.initialize(injector);
+
+    return injector;
+  }
+
+  /**
+   * Replays commands, inserting the InterceptingProvider where necessary.
+   */
+  private class CommandRewriter extends CommandReplayer {
+    private Set<Key> keysIntercepted = new HashSet<Key>();
+
+    @Override public <T> void replayBind(Binder binder, BindCommand<T> command) {
+      Key<T> key = command.getKey();
+
+      if (!keysToIntercept.contains(key)) {
+        super.replayBind(binder, command);
+        return;
+      }
+
+      if (command.getTarget() == null) {
+        throw new UnsupportedOperationException(
+            String.format("Cannot intercept bare binding of %s.", key));
+      }
+
+      Key<T> anonymousKey = Key.get(key.getTypeLiteral(), uniqueAnnotation());
+      binder.bind(key).toProvider(new InterceptingProvider<T>(key, anonymousKey));
+
+      LinkedBindingBuilder<T> linkedBindingBuilder = binder.bind(anonymousKey);
+      ScopedBindingBuilder scopedBindingBuilder = command.getTarget().execute(linkedBindingBuilder);
+
+      // we scope the user's provider, not the interceptor. This is dangerous,
+      // but convenient. It means that although the user's provider will live
+      // in its proper scope, the intereptor gets invoked without a scope
+      BindScoping scoping = command.getScoping();
+      if (scoping != null) {
+        scoping.execute(scopedBindingBuilder);
+      }
+
+      keysIntercepted.add(key);
+    }
+  }
+
+  /**
+   * Provide {@code T}, with a hook for an {@link ProvisionInterceptor}.
+   */
+  private static class InterceptingProvider<T> implements Provider<T> {
+    private final Key<T> key;
+    private final Key<T> anonymousKey;
+    private Provider<ProvisionInterceptor> injectionInterceptorProvider;
+    private Provider<? extends T> delegateProvider;
+
+    public InterceptingProvider(Key<T> key, Key<T> anonymousKey) {
+      this.key = key;
+      this.anonymousKey = anonymousKey;
+    }
+
+    @Inject void initialize(Injector injector,
+        Provider<ProvisionInterceptor> injectionInterceptorProvider) {
+      this.injectionInterceptorProvider = nonNull(
+          injectionInterceptorProvider, "injectionInterceptorProvider");
+      this.delegateProvider = nonNull(
+          injector.getProvider(anonymousKey), "delegateProvider");
+    }
+
+    public T get() {
+      nonNull(injectionInterceptorProvider, "injectionInterceptorProvider");
+      nonNull(delegateProvider, "delegateProvider");
+      return injectionInterceptorProvider.get().intercept(key, delegateProvider);
+    }
+  }
+
+  /**
+   * Returns an annotation instance that is not equal to any other annotation
+   * instances, for use in creating distinct {@link Key}s.
+   */
+  private static Annotation uniqueAnnotation() {
+    return new Annotation() {
+      public Class<? extends Annotation> annotationType() {
+        return Internal.class;
+      }
+      @Override public String toString() {
+        return "InterceptingBinderPrivate";
+      }
+    };
+  }
+  @Retention(RUNTIME) @BindingAnnotation
+  private @interface Internal { }
+}
diff --git a/extensions/commands/src/com/google/inject/commands/intercepting/ProvisionInterceptor.java b/extensions/commands/src/com/google/inject/commands/intercepting/ProvisionInterceptor.java
new file mode 100644
index 0000000..dc8737d
--- /dev/null
+++ b/extensions/commands/src/com/google/inject/commands/intercepting/ProvisionInterceptor.java
@@ -0,0 +1,30 @@
+/**
+ * Copyright (C) 2008 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.
+ */
+
+package com.google.inject.commands.intercepting;
+
+import com.google.inject.Key;
+import com.google.inject.Provider;
+
+/**
+ * Intercepts object provision.
+ *
+ * @author jessewilson@google.com (Jesse Wilson)
+ * @author jmourits@google.com (Jerome Mourits)
+ */
+public interface ProvisionInterceptor {
+  <T> T intercept(Key<T> key, Provider<? extends T> delegate);
+}
diff --git a/extensions/commands/src/com/google/inject/commands/intercepting/package-info.java b/extensions/commands/src/com/google/inject/commands/intercepting/package-info.java
new file mode 100644
index 0000000..e7fdfb9
--- /dev/null
+++ b/extensions/commands/src/com/google/inject/commands/intercepting/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright (C) 2008 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.
+ */
+
+/**
+ * Extension for intercepting object provision.
+ */
+package com.google.inject.commands.intercepting;
\ No newline at end of file
diff --git a/extensions/commands/src/com/google/inject/commands/package-info.java b/extensions/commands/src/com/google/inject/commands/package-info.java
new file mode 100644
index 0000000..c4fd670
--- /dev/null
+++ b/extensions/commands/src/com/google/inject/commands/package-info.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2008 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.
+ */
+
+/**
+ * Extension for recording, reviewing and instrumenting the commands executed
+ * by a module.
+ */
+package com.google.inject.commands;
\ No newline at end of file
diff --git a/extensions/commands/src/com/google/inject/injectioncontroller/InjectionController.java b/extensions/commands/src/com/google/inject/injectioncontroller/InjectionController.java
new file mode 100644
index 0000000..b78f169
--- /dev/null
+++ b/extensions/commands/src/com/google/inject/injectioncontroller/InjectionController.java
@@ -0,0 +1,144 @@
+/**
+ * Copyright (C) 2008 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.
+ */
+
+package com.google.inject.injectioncontroller;
+
+import com.google.inject.AbstractModule;
+import com.google.inject.Key;
+import com.google.inject.Module;
+import com.google.inject.Provider;
+import com.google.inject.commands.intercepting.ProvisionInterceptor;
+import com.google.inject.internal.Objects;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Allows bound objects to be substituted at runtime. To use:
+ *
+ * <ol><li>Create a module that binds {@link ProvisionInterceptor} to the
+ * result of {@link #getProvisionInterceptor()} in the desired scope. Or use
+ * {@link #createModule()} which binds it with no scope.</li>
+ * <li>Create an {@code InterceptingInjectorBuilder} that installs your
+ * application modules, plus the module from the previous step.</li>
+ * <li>Configure the builder to intercept each type that you would like to
+ * substitute.</li>
+ * <li>Build the injector. Whenever the injector needs an instance of a
+ * controlled type, the injection controller will disregard that binding if a
+ * value for that type as been substituted.</li></ul>
+ *
+ * <pre>
+ * InjectionController injectionController = new InjectionController();
+ *
+ * Injector injector = new InterceptingInjectorBuilder()
+ *     .install(new MyApplicationModule(), injectionController.createModule());
+ *     .intercept(PersistenceEngine.class)
+ *     .intercept(DeliveryRequestService.class)
+ *     .build();
+ *
+ * injectionController.substitute(PersistenceEngine.class, new MockPersistenceEngine());
+ * </pre>
+ *
+ * @author jessewilson@google.com (Jesse Wilson)
+ * @author jmourits@google.com (Jerome Mourits)
+ */
+public class InjectionController {
+  private final Map<Key<?>, Object> mapWritable = new HashMap<Key<?>, Object>();
+  private final Map<Key<?>, Object> map = Collections.unmodifiableMap(mapWritable);
+
+  private final ProvisionInterceptor provisionInterceptor = new ProvisionInterceptor() {
+    @SuppressWarnings({"unchecked"})
+    public <T> T intercept(Key<T> key, Provider<? extends T> delegate) {
+      T mockT = (T) map.get(key);
+      return (mockT == null)
+          // This always happens in production
+          ? delegate.get()
+          // This will happen when running tests that "control" a <T>'s injection
+          : mockT;
+    }
+  };
+
+  /**
+   * Returns the injection interceptor for binding
+   */
+  public ProvisionInterceptor getProvisionInterceptor() {
+    return provisionInterceptor;
+  }
+
+  /**
+   * Returns a module that binds the provision interceptor without a scope.
+   */
+  public final Module createModule() {
+    return new AbstractModule() {
+      protected void configure() {
+        bind(ProvisionInterceptor.class)
+            .toInstance(provisionInterceptor);
+      }
+    };
+  }
+
+  /**
+   * Substitutes the injector's existing binding for {@code key} with
+   * {@code instance}.
+   */
+  public <T> InjectionController substitute(Key<T> key, T instance) {
+    Objects.nonNull(key, "key");
+
+    if (map.containsKey(key)) {
+      throw new IllegalStateException(key + " was already being doubled.");
+    }
+
+    mapWritable.put(key, instance);
+    return this;
+  }
+
+  /**
+   * Substitutes the injector's existing binding for {@code type} with
+   * {@code instance}.
+   */
+  public <T> InjectionController substitute(Class<T> type, T instance) {
+    return substitute(Key.get(type), instance);
+  }
+
+  /**
+   * Restores the original binding for {@code key}.
+   */
+  public <T> InjectionController remove(Key<T> key) {
+    Objects.nonNull(key, "key");
+
+    if (!map.containsKey(key)) {
+      throw new IllegalStateException(key + " was not being doubled.");
+    }
+
+    mapWritable.remove(key);
+    return this;
+  }
+
+  /**
+   * Restores the original binding for {@code type}.
+   */
+  public <T> InjectionController remove(Class<T> type) {
+    return remove(Key.get(type));
+  }
+
+  /**
+   * Returns an unmodifiable, mutable map with the substituted bindings.
+   */
+  public Map<Key<?>, Object> getSubstitutesMap() {
+    return map;
+  }
+}
diff --git a/extensions/commands/src/com/google/inject/injectioncontroller/package-info.java b/extensions/commands/src/com/google/inject/injectioncontroller/package-info.java
new file mode 100644
index 0000000..41a5b4d
--- /dev/null
+++ b/extensions/commands/src/com/google/inject/injectioncontroller/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright (C) 2008 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.
+ */
+
+/**
+ * Extension for overriding bindings.
+ */
+package com.google.inject.injectioncontroller;
\ No newline at end of file
diff --git a/extensions/commands/test/com/google/inject/commands/CommandRecorderTest.java b/extensions/commands/test/com/google/inject/commands/CommandRecorderTest.java
new file mode 100644
index 0000000..a6a566d
--- /dev/null
+++ b/extensions/commands/test/com/google/inject/commands/CommandRecorderTest.java
@@ -0,0 +1,681 @@
+/**
+ * Copyright (C) 2008 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.
+ */
+
+package com.google.inject.commands;
+
+import com.google.inject.*;
+import com.google.inject.binder.AnnotatedBindingBuilder;
+import com.google.inject.binder.ScopedBindingBuilder;
+import com.google.inject.binder.AnnotatedConstantBindingBuilder;
+import com.google.inject.binder.ConstantBindingBuilder;
+import com.google.inject.matcher.Matcher;
+import com.google.inject.matcher.Matchers;
+import com.google.inject.name.Names;
+import com.google.inject.spi.TypeConverter;
+import junit.framework.AssertionFailedError;
+import junit.framework.TestCase;
+import org.aopalliance.intercept.MethodInterceptor;
+import org.aopalliance.intercept.MethodInvocation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+import java.lang.annotation.Target;
+import java.util.*;
+
+/**
+ * @author jessewilson@google.com (Jesse Wilson)
+ */
+public class CommandRecorderTest extends TestCase {
+
+  protected EarlyRequestsProvider earlyRequestProvider = new EarlyRequestsProvider() {
+    public <T> T get(Key<T> key) {
+      throw new AssertionFailedError();
+    }
+  };
+
+  private CommandRecorder commandRecorder = new CommandRecorder(earlyRequestProvider);
+
+  public void testAddMessageErrorCommand() {
+    checkModule(
+        new AbstractModule() {
+          protected void configure() {
+            addError("Message", "A", "B", "C");
+          }
+        },
+
+        new FailingVisitor() {
+          @Override public Void visitAddMessageError(AddMessageErrorCommand command) {
+            assertEquals(Arrays.asList("A", "B", "C"), command.getArguments());
+            assertEquals("Message", command.getMessage());
+            return null;
+          }
+        }
+    );
+  }
+
+  public void testAddThrowableErrorCommand() {
+    checkModule(
+        new AbstractModule() {
+          protected void configure() {
+            addError(new Exception("A"));
+          }
+        },
+
+        new FailingVisitor() {
+          @Override public Void visitAddError(AddThrowableErrorCommand command) {
+            assertEquals("A", command.getThrowable().getMessage());
+            return null;
+          }
+        }
+    );
+  }
+
+  public void testBindConstantAnnotations() {
+    checkModule(
+        new AbstractModule() {
+          protected void configure() {
+            bindConstant().annotatedWith(SampleAnnotation.class).to("A");
+            bindConstant().annotatedWith(Names.named("Bee")).to("B");
+          }
+        },
+
+        new FailingVisitor() {
+          @Override public Void visitBindConstant(BindConstantCommand command) {
+            assertEquals(Key.get(String.class, SampleAnnotation.class), command.getKey());
+            assertEquals("A", command.getTarget().get(null));
+            return null;
+          }
+        },
+
+        new FailingVisitor() {
+          @Override public Void visitBindConstant(BindConstantCommand command) {
+            assertEquals(Key.get(String.class, Names.named("Bee")), command.getKey());
+            assertEquals("B", command.getTarget().get(null));
+            return null;
+          }
+        }
+    );
+  }
+
+  public void testBindConstantTypes() {
+    checkModule(
+        new AbstractModule() {
+          protected void configure() {
+            bindConstant().annotatedWith(Names.named("String")).to("A");
+            bindConstant().annotatedWith(Names.named("int")).to(2);
+            bindConstant().annotatedWith(Names.named("long")).to(3L);
+            bindConstant().annotatedWith(Names.named("boolean")).to(false);
+            bindConstant().annotatedWith(Names.named("double")).to(5.0d);
+            bindConstant().annotatedWith(Names.named("float")).to(6.0f);
+            bindConstant().annotatedWith(Names.named("short")).to((short) 7);
+            bindConstant().annotatedWith(Names.named("char")).to('h');
+            bindConstant().annotatedWith(Names.named("Class")).to(Iterator.class);
+            bindConstant().annotatedWith(Names.named("Enum")).to(CoinSide.TAILS);
+          }
+        },
+
+        new FailingVisitor() {
+          @Override public Void visitBindConstant(BindConstantCommand command) {
+            assertEquals(Key.get(String.class, Names.named("String")), command.getKey());
+            assertEquals("A", command.getTarget().get(null));
+            return null;
+          }
+        },
+
+        new FailingVisitor() {
+          @Override public Void visitBindConstant(BindConstantCommand command) {
+            assertEquals(Key.get(Integer.class, Names.named("int")), command.getKey());
+            assertEquals(2, command.getTarget().get(null));
+            return null;
+          }
+        },
+
+        new FailingVisitor() {
+          @Override public Void visitBindConstant(BindConstantCommand command) {
+            assertEquals(Key.get(Long.class, Names.named("long")), command.getKey());
+            assertEquals(3L, command.getTarget().get(null));
+            return null;
+          }
+        },
+
+        new FailingVisitor() {
+          @Override public Void visitBindConstant(BindConstantCommand command) {
+            assertEquals(Key.get(Boolean.class, Names.named("boolean")), command.getKey());
+            assertEquals(false, command.getTarget().get(null));
+            return null;
+          }
+        },
+
+        new FailingVisitor() {
+          @Override public Void visitBindConstant(BindConstantCommand command) {
+            assertEquals(Key.get(Double.class, Names.named("double")), command.getKey());
+            assertEquals(5.0d, command.getTarget().get(null));
+            return null;
+          }
+        },
+
+        new FailingVisitor() {
+          @Override public Void visitBindConstant(BindConstantCommand command) {
+            assertEquals(Key.get(Float.class, Names.named("float")), command.getKey());
+            assertEquals(6.0f, command.getTarget().get(null));
+            return null;
+          }
+        },
+
+        new FailingVisitor() {
+          @Override public Void visitBindConstant(BindConstantCommand command) {
+            assertEquals(Key.get(Short.class, Names.named("short")), command.getKey());
+            assertEquals((short) 7, command.getTarget().get(null));
+            return null;
+          }
+        },
+
+        new FailingVisitor() {
+          @Override public Void visitBindConstant(BindConstantCommand command) {
+            assertEquals(Key.get(Character.class, Names.named("char")), command.getKey());
+            assertEquals('h', command.getTarget().get(null));
+            return null;
+          }
+        },
+
+        new FailingVisitor() {
+          @Override public Void visitBindConstant(BindConstantCommand command) {
+            assertEquals(Key.get(Class.class, Names.named("Class")), command.getKey());
+            assertEquals(Iterator.class, command.getTarget().get(null));
+            return null;
+          }
+        },
+
+        new FailingVisitor() {
+          @Override public Void visitBindConstant(BindConstantCommand command) {
+            assertEquals(Key.get(CoinSide.class, Names.named("Enum")), command.getKey());
+            assertEquals(CoinSide.TAILS, command.getTarget().get(null));
+            return null;
+          }
+        }
+    );
+  }
+
+  public void testBindKeysNoAnnotations() {
+    FailingVisitor keyChecker = new FailingVisitor() {
+      @Override public Void visitBind(BindCommand command) {
+        assertEquals(Key.get(String.class), command.getKey());
+        return null;
+      }
+    };
+
+    checkModule(
+        new AbstractModule() {
+          protected void configure() {
+            bind(String.class).toInstance("A");
+            bind(new TypeLiteral<String>() {
+            }).toInstance("B");
+            bind(Key.get(String.class)).toInstance("C");
+          }
+        },
+        keyChecker,
+        keyChecker,
+        keyChecker
+    );
+  }
+
+  public void testBindKeysWithAnnotationType() {
+    FailingVisitor annotationChecker = new FailingVisitor() {
+      @Override public Void visitBind(BindCommand command) {
+        assertEquals(Key.get(String.class, SampleAnnotation.class), command.getKey());
+        return null;
+      }
+    };
+
+    checkModule(
+        new AbstractModule() {
+          protected void configure() {
+            bind(String.class).annotatedWith(SampleAnnotation.class).toInstance("A");
+            bind(new TypeLiteral<String>() {
+            }).annotatedWith(SampleAnnotation.class).toInstance("B");
+          }
+        },
+        annotationChecker,
+        annotationChecker
+    );
+  }
+
+  public void testBindKeysWithAnnotationInstance() {
+    FailingVisitor annotationChecker = new FailingVisitor() {
+      @Override public Void visitBind(BindCommand command) {
+        assertEquals(Key.get(String.class, Names.named("a")), command.getKey());
+        return null;
+      }
+    };
+
+
+    checkModule(
+        new AbstractModule() {
+          protected void configure() {
+            bind(String.class).annotatedWith(Names.named("a")).toInstance("B");
+            bind(new TypeLiteral<String>() {
+            }).annotatedWith(Names.named("a")).toInstance("C");
+          }
+        },
+        annotationChecker,
+        annotationChecker
+    );
+  }
+
+  public void testBindToProvider() {
+    checkModule(
+        new AbstractModule() {
+          protected void configure() {
+            bind(String.class).toProvider(new Provider<String>() {
+              public String get() {
+                return "A";
+              }
+            });
+            bind(List.class).toProvider(ListProvider.class);
+            bind(Collection.class).toProvider(Key.get(ListProvider.class));
+          }
+        },
+
+        new FailingVisitor() {
+          @Override public <T> Void visitBind(BindCommand<T> command) {
+            assertEquals(Key.get(String.class), command.getKey());
+            assertEquals("A", command.getTarget().getProvider(null).get());
+            return null;
+          }
+        },
+
+        new FailingVisitor() {
+          @Override public <T> Void visitBind(BindCommand<T> command) {
+            assertEquals(Key.get(List.class), command.getKey());
+            assertNull(command.getTarget().get(null));
+            assertEquals(Key.get(ListProvider.class), command.getTarget().getProviderKey(null));
+            return null;
+          }
+        },
+
+        new FailingVisitor() {
+          @Override public <T> Void visitBind(BindCommand<T> command) {
+            assertEquals(Key.get(Collection.class), command.getKey());
+            assertNull(command.getTarget().get(null));
+            assertEquals(Key.get(ListProvider.class), command.getTarget().getProviderKey(null));
+            return null;
+          }
+        }
+    );
+  }
+
+  public void testBindToLinkedBinding() {
+    checkModule(
+        new AbstractModule() {
+          protected void configure() {
+            bind(List.class).to(ArrayList.class);
+            bind(Map.class).to(new TypeLiteral<HashMap<Integer, String>>() { });
+            bind(Set.class).to(Key.get(TreeSet.class, SampleAnnotation.class));
+          }
+        },
+
+        new FailingVisitor() {
+          @Override public <T> Void visitBind(BindCommand<T> command) {
+            assertEquals(Key.get(List.class), command.getKey());
+            assertEquals(Key.get(ArrayList.class), command.getTarget().getKey(null));
+            return null;
+          }
+        },
+
+        new FailingVisitor() {
+          @Override public <T> Void visitBind(BindCommand<T> command) {
+            assertEquals(Key.get(Map.class), command.getKey());
+            assertEquals(Key.get(new TypeLiteral<HashMap<Integer, String>>() {}), command.getTarget().getKey(null));
+            return null;
+          }
+        },
+
+        new FailingVisitor() {
+          @Override public <T> Void visitBind(BindCommand<T> command) {
+            assertEquals(Key.get(Set.class), command.getKey());
+            assertEquals(Key.get(TreeSet.class, SampleAnnotation.class), command.getTarget().getKey(null));
+            return null;
+          }
+        }
+    );
+  }
+
+  public void testBindToInstance() {
+    checkModule(
+        new AbstractModule() {
+          protected void configure() {
+            bind(String.class).toInstance("A");
+          }
+        },
+
+        new FailingVisitor() {
+          @Override public <T> Void visitBind(BindCommand<T> command) {
+            assertEquals(Key.get(String.class), command.getKey());
+            assertEquals("A", command.getTarget().get(null));
+            return null;
+          }
+        }
+    );
+  }
+
+  public void testBindInScopes() {
+    checkModule(
+        new AbstractModule() {
+          protected void configure() {
+            bind(List.class).to(ArrayList.class).in(Scopes.SINGLETON);
+            bind(Map.class).to(HashMap.class).in(Singleton.class);
+            bind(Set.class).to(TreeSet.class).asEagerSingleton();
+          }
+        },
+
+        new FailingVisitor() {
+          @Override public <T> Void visitBind(BindCommand<T> command) {
+            assertEquals(Key.get(List.class), command.getKey());
+            assertEquals(Scopes.SINGLETON, command.getScoping().getScope(null));
+            assertNull(command.getScoping().getScopeAnnotation(null));
+            assertFalse(command.getScoping().isEagerSingleton());
+            return null;
+          }
+        },
+
+        new FailingVisitor() {
+          @Override public <T> Void visitBind(BindCommand<T> command) {
+            assertEquals(Key.get(Map.class), command.getKey());
+            assertEquals(Singleton.class, command.getScoping().getScopeAnnotation(null));
+            assertNull(command.getScoping().getScope(null));
+            assertFalse(command.getScoping().isEagerSingleton());
+            return null;
+          }
+        },
+
+        new FailingVisitor() {
+          @Override public <T> Void visitBind(BindCommand<T> command) {
+            assertEquals(Key.get(Set.class), command.getKey());
+            assertNull(command.getScoping().getScopeAnnotation(null));
+            assertNull(command.getScoping().getScope(null));
+            assertTrue(command.getScoping().isEagerSingleton());
+            return null;
+          }
+        }
+    );
+  }
+
+  public void testBindIntercepor() {
+    final Matcher<Class> classMatcher = Matchers.subclassesOf(List.class);
+    final Matcher<Object> methodMatcher = Matchers.any();
+    final MethodInterceptor methodInterceptor = new MethodInterceptor() {
+      public Object invoke(MethodInvocation methodInvocation) {
+        return null;
+      }
+    };
+
+    checkModule(
+        new AbstractModule() {
+          protected void configure() {
+            bindInterceptor(classMatcher, methodMatcher, methodInterceptor);
+          }
+        },
+
+        new FailingVisitor() {
+          @Override public Void visitBindInterceptor(BindInterceptorCommand command) {
+            assertSame(classMatcher, command.getClassMatcher());
+            assertSame(methodMatcher, command.getMethodMatcher());
+            assertEquals(Arrays.asList(methodInterceptor), command.getInterceptors());
+            return null;
+          }
+        }
+    );
+  }
+
+  public void testBindScope() {
+    checkModule(
+        new AbstractModule() {
+          protected void configure() {
+            bindScope(SampleAnnotation.class, Scopes.NO_SCOPE);
+          }
+        },
+
+        new FailingVisitor() {
+          @Override public Void visitBindScope(BindScopeCommand command) {
+            assertSame(SampleAnnotation.class, command.getAnnotationType());
+            assertSame(Scopes.NO_SCOPE, command.getScope());
+            return null;
+          }
+        }
+    );
+  }
+
+  public void testConvertToTypes() {
+    final TypeConverter typeConverter = new TypeConverter() {
+      public Object convert(String value, TypeLiteral<?> toType) {
+        return value;
+      }
+    };
+
+    checkModule(
+        new AbstractModule() {
+          protected void configure() {
+            convertToTypes(Matchers.any(), typeConverter);
+          }
+        },
+
+        new FailingVisitor() {
+          @Override public Void visitConvertToTypes(ConvertToTypesCommand command) {
+            assertSame(typeConverter, command.getTypeConverter());
+            assertSame(Matchers.any(), command.getTypeMatcher());
+            return null;
+          }
+        }
+    );
+  }
+
+  public void testGetProvider() {
+    final List<Key> calls = new ArrayList<Key>();
+
+    earlyRequestProvider = new EarlyRequestsProvider() {
+      @SuppressWarnings({"unchecked"})
+      public <T> T get(Key<T> key) {
+        calls.add(key);
+        return (T) "A";
+      }
+    };
+
+    commandRecorder = new CommandRecorder(earlyRequestProvider);
+
+    checkModule(
+        new AbstractModule() {
+          protected void configure() {
+            Provider<String> keyGetProvider = getProvider(Key.get(String.class, SampleAnnotation.class));
+            assertEquals("A", keyGetProvider.get());
+            assertEquals(Key.get(String.class, SampleAnnotation.class), calls.get(0));
+
+            Provider<String> typeGetProvider = getProvider(String.class);
+            assertEquals("A", typeGetProvider.get());
+            assertEquals(Key.get(String.class), calls.get(1));
+            assertEquals(2, calls.size());
+          }
+        },
+
+        new FailingVisitor() {
+          @Override public Void visitGetProvider(GetProviderCommand command) {
+            assertEquals(Key.get(String.class, SampleAnnotation.class), command.getKey());
+            assertEquals(earlyRequestProvider, command.getEarlyRequestsProvider());
+            return null;
+          }
+        },
+
+        new FailingVisitor() {
+          @Override public Void visitGetProvider(GetProviderCommand command) {
+            assertEquals(Key.get(String.class), command.getKey());
+            assertEquals(earlyRequestProvider, command.getEarlyRequestsProvider());
+            return null;
+          }
+        }
+    );
+  }
+
+  public void testRequestStaticInjection() {
+    checkModule(
+        new AbstractModule() {
+          protected void configure() {
+            requestStaticInjection(ArrayList.class);
+          }
+        },
+
+        new FailingVisitor() {
+          @Override public Void visitRequestStaticInjection(RequestStaticInjectionCommand command) {
+            assertEquals(Arrays.asList(ArrayList.class), command.getTypes());
+            return null;
+          }
+        }
+    );
+  }
+
+  public void testMalformedBindCommands() {
+    CommandRecorder recorder = new CommandRecorder(earlyRequestProvider);
+
+    recorder.recordCommands(new AbstractModule() {
+      protected void configure() {
+        AnnotatedBindingBuilder<String> abb = bind(String.class);
+        abb.annotatedWith(SampleAnnotation.class);
+        try {
+          abb.annotatedWith(Names.named("A"));
+          fail();
+        } catch(IllegalStateException expected) {
+        }
+      }
+    });
+
+    recorder.recordCommands(new AbstractModule() {
+      protected void configure() {
+        AnnotatedBindingBuilder<String> abb = bind(String.class);
+        abb.toInstance("A");
+        try {
+          abb.toInstance("B");
+          fail();
+        } catch(IllegalStateException expected) {
+        }
+      }
+    });
+
+    recorder.recordCommands(new AbstractModule() {
+      protected void configure() {
+        ScopedBindingBuilder sbb = bind(List.class).to(ArrayList.class);
+        sbb.in(Scopes.NO_SCOPE);
+        try {
+          sbb.asEagerSingleton();
+          fail();
+        } catch(IllegalStateException expected) {
+        }
+      }
+    });
+  }
+
+  public void testMalformedBindConstantCommands() {
+    CommandRecorder recorder = new CommandRecorder(earlyRequestProvider);
+
+    recorder.recordCommands(new AbstractModule() {
+      protected void configure() {
+        AnnotatedConstantBindingBuilder cbb = bindConstant();
+        cbb.annotatedWith(SampleAnnotation.class);
+        try {
+          cbb.annotatedWith(Names.named("A"));
+          fail();
+        } catch(IllegalStateException expected) {
+        }
+      }
+    });
+
+    recorder.recordCommands(new AbstractModule() {
+      protected void configure() {
+        ConstantBindingBuilder cbb = bindConstant().annotatedWith(SampleAnnotation.class);
+        cbb.to("A");
+        try {
+          cbb.to("B");
+          fail();
+        } catch(IllegalStateException expected) {
+        }
+      }
+    });
+  }
+
+  /**
+   * Ensures the module performs the commands consistent with {@code visitors}.
+   */
+  protected void checkModule(Module module, Command.Visitor<?>... visitors) {
+    List<Command> commands = commandRecorder.recordCommands(module);
+
+    assertEquals(commands.size(), visitors.length);
+
+    for (int i = 0; i < visitors.length; i++) {
+      Command.Visitor<?> visitor = visitors[i];
+      Command command = commands.get(i);
+      command.acceptVisitor(visitor);
+    }
+  }
+
+  private static class ListProvider implements Provider<List> {
+    public List get() {
+      return new ArrayList();
+    }
+  }
+
+  private static class FailingVisitor implements Command.Visitor<Void> {
+    public Void visitAddMessageError(AddMessageErrorCommand command) {
+      throw new AssertionFailedError();
+    }
+
+    public Void visitAddError(AddThrowableErrorCommand command) {
+      throw new AssertionFailedError();
+    }
+
+    public Void visitBindInterceptor(BindInterceptorCommand command) {
+      throw new AssertionFailedError();
+    }
+
+    public Void visitBindScope(BindScopeCommand command) {
+      throw new AssertionFailedError();
+    }
+
+    public Void visitRequestStaticInjection(RequestStaticInjectionCommand command) {
+      throw new AssertionFailedError();
+    }
+
+    public Void visitBindConstant(BindConstantCommand command) {
+      throw new AssertionFailedError();
+    }
+
+    public Void visitConvertToTypes(ConvertToTypesCommand command) {
+      throw new AssertionFailedError();
+    }
+
+    public <T> Void visitBind(BindCommand<T> command) {
+      throw new AssertionFailedError();
+    }
+
+    public Void visitGetProvider(GetProviderCommand command) {
+      throw new AssertionFailedError();
+    }
+  }
+
+  @Retention(RUNTIME)
+  @Target({ ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD })
+  @BindingAnnotation
+  public @interface SampleAnnotation { }
+
+  public enum CoinSide { HEADS, TAILS }
+}
diff --git a/extensions/commands/test/com/google/inject/commands/CommandReplayerTest.java b/extensions/commands/test/com/google/inject/commands/CommandReplayerTest.java
new file mode 100644
index 0000000..2ef21a1
--- /dev/null
+++ b/extensions/commands/test/com/google/inject/commands/CommandReplayerTest.java
@@ -0,0 +1,45 @@
+/**
+ * Copyright (C) 2008 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.
+ */
+
+package com.google.inject.commands;
+
+import com.google.inject.Module;
+
+import java.util.List;
+
+
+/**
+ * @author jessewilson@google.com (Jesse Wilson)
+ */
+public class CommandReplayerTest extends CommandRecorderTest {
+
+  protected void checkModule(Module module, Command.Visitor<?>... visitors) {
+    // get some commands to replay
+    List<Command> commands = new CommandRecorder(earlyRequestProvider).recordCommands(module);
+
+    // replay the recorded commands, and record them again!
+    List<Command> replayedCommands = new CommandRecorder(earlyRequestProvider)
+        .recordCommands(new CommandReplayer().createModule(commands));
+
+    // verify that the replayed commands are as expected
+    assertEquals(replayedCommands.size(), visitors.length);
+    for (int i = 0; i < visitors.length; i++) {
+      Command.Visitor<?> visitor = visitors[i];
+      Command command = replayedCommands.get(i);
+      command.acceptVisitor(visitor);
+    }
+  }
+}
diff --git a/extensions/commands/test/com/google/inject/commands/CommandRewriteTest.java b/extensions/commands/test/com/google/inject/commands/CommandRewriteTest.java
new file mode 100644
index 0000000..7515b4b
--- /dev/null
+++ b/extensions/commands/test/com/google/inject/commands/CommandRewriteTest.java
@@ -0,0 +1,62 @@
+/**
+ * Copyright (C) 2008 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.
+ */
+
+package com.google.inject.commands;
+
+import com.google.inject.*;
+import junit.framework.TestCase;
+
+import java.util.List;
+
+
+/**
+ * @author jessewilson@google.com (Jesse Wilson)
+ */
+public class CommandRewriteTest extends TestCase {
+
+  public void testRewriteBindings() {
+    // create a module the binds String.class and CharSequence.class
+    Module module = new AbstractModule() {
+      protected void configure() {
+        bind(String.class).toInstance("Pizza");
+        bind(CharSequence.class).toInstance("Wine");
+      }
+    };
+
+    // record the commands from that module
+    CommandRecorder commandRecorder = new CommandRecorder(new FutureInjector());
+    List<Command> commands = commandRecorder.recordCommands(module);
+
+    // create a rewriter that rewrites the binding to 'Wine' with a binding to 'Beer'
+    CommandReplayer rewriter = new CommandReplayer() {
+      @Override public <T> void replayBind(Binder binder, BindCommand<T> command) {
+        if ("Wine".equals(command.getTarget().get(null))) {
+          binder.bind(CharSequence.class).toInstance("Beer");
+        } else {
+          super.replayBind(binder, command);
+        }
+      }
+    };
+
+    // create a module from the original list of commands and the rewriter
+    Module rewrittenModule = rewriter.createModule(commands);
+
+    // it all works
+    Injector injector = Guice.createInjector(rewrittenModule);
+    assertEquals("Pizza", injector.getInstance(String.class));
+    assertEquals("Beer", injector.getInstance(CharSequence.class));
+  }
+}
diff --git a/extensions/commands/test/com/google/inject/commands/intercepting/InterceptingInjectorBuilderTest.java b/extensions/commands/test/com/google/inject/commands/intercepting/InterceptingInjectorBuilderTest.java
new file mode 100644
index 0000000..367e77d
--- /dev/null
+++ b/extensions/commands/test/com/google/inject/commands/intercepting/InterceptingInjectorBuilderTest.java
@@ -0,0 +1,191 @@
+/**
+ * Copyright (C) 2008 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.
+ */
+
+package com.google.inject.commands.intercepting;
+
+import com.google.inject.*;
+import junit.framework.AssertionFailedError;
+import junit.framework.TestCase;
+
+import java.util.Collection;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.LinkedList;
+
+
+/**
+ * @author jessewilson@google.com (Jesse Wilson)
+ */
+public class InterceptingInjectorBuilderTest extends TestCase {
+
+  private final ProvisionInterceptor failingInterceptor = new ProvisionInterceptor() {
+    public <T> T intercept(Key<T> key, Provider<? extends T> delegate) {
+      throw new AssertionFailedError();
+    }
+  };
+
+  public void testInterceptProvisionInterceptor() {
+    InterceptingInjectorBuilder builder = new InterceptingInjectorBuilder();
+
+    try {
+      builder.intercept(ProvisionInterceptor.class);
+      fail();
+    } catch(IllegalArgumentException expected) {
+    }
+  }
+
+  public void testProvisionInterception() {
+    final ProvisionInterceptor interceptor = new ProvisionInterceptor() {
+      @SuppressWarnings({"unchecked"})
+      public <T> T intercept(Key<T> key, Provider<? extends T> delegate) {
+        assertEquals(Key.get(String.class), key);
+        assertEquals("A", delegate.get());
+        return (T) "B";
+      }
+    };
+
+    Module module = new AbstractModule() {
+      protected void configure() {
+        bind(String.class).toInstance("A");
+        bind(ProvisionInterceptor.class).toInstance(interceptor);
+      }
+    };
+
+    Injector injector = new InterceptingInjectorBuilder()
+        .intercept(String.class)
+        .install(module)
+        .build();
+
+    assertEquals("B", injector.getInstance(String.class));
+  }
+
+  /**
+   * The user's provider is scoped but the interceptor is not. As this testcase
+   * demonstrates, the user's provider gets called only once (in singleton
+   * scope) but the interceptor gets called for each provision.
+   */
+  public void testInterceptionIsNotScoped() {
+    final Provider<Integer> sequenceProvider = new Provider<Integer>() {
+      private int next = 100;
+      public Integer get() {
+        return next++;
+      }
+    };
+
+    final ProvisionInterceptor interceptor = new ProvisionInterceptor() {
+      private int next = 1;
+      @SuppressWarnings({"unchecked"})
+      public <T> T intercept(Key<T> key, Provider<? extends T> delegate) {
+        assertEquals(100, delegate.get());
+        return (T) new Integer(next++);
+      }
+    };
+
+    Module module = new AbstractModule() {
+      protected void configure() {
+        bind(Integer.class).toProvider(sequenceProvider).in(Scopes.SINGLETON);
+        bind(ProvisionInterceptor.class).toInstance(interceptor);
+      }
+    };
+
+    Injector injector = new InterceptingInjectorBuilder()
+        .intercept(Integer.class)
+        .install(module)
+        .build();
+
+    assertEquals(1, (int) injector.getInstance(Integer.class));
+    assertEquals(2, (int) injector.getInstance(Integer.class));
+  }
+
+  public void testInterceptionIsWhitelistedKeysOnly() {
+    final ProvisionInterceptor interceptor = new ProvisionInterceptor() {
+      @SuppressWarnings({"unchecked"})
+      public <T> T intercept(Key<T> key, Provider<? extends T> delegate) {
+        assertEquals(ArrayList.class, delegate.get().getClass());
+        return (T) new LinkedList();
+      }
+    };
+
+    Module module = new AbstractModule() {
+      protected void configure() {
+        bind(Collection.class).to(ArrayList.class);
+        bind(List.class).to(ArrayList.class);
+        bind(ProvisionInterceptor.class).toInstance(interceptor);
+      }
+    };
+
+    Injector injector = new InterceptingInjectorBuilder()
+        .intercept(List.class)
+        .install(module)
+        .build();
+
+    assertEquals(LinkedList.class, injector.getInstance(List.class).getClass());
+    assertEquals(ArrayList.class, injector.getInstance(Collection.class).getClass());
+  }
+  
+  public void testCannotInterceptBareBinding() {
+    Module module = new AbstractModule() {
+      protected void configure() {
+        bind(ArrayList.class);
+      }
+    };
+
+    InterceptingInjectorBuilder builder = new InterceptingInjectorBuilder()
+        .intercept(ArrayList.class)
+        .install(module);
+
+    try {
+      builder.build();
+      fail();
+    } catch(UnsupportedOperationException expected) {
+    }
+  }
+
+  public void testAllInterceptedKeysMustBeBound() {
+    Module module = new AbstractModule() {
+      protected void configure() {
+        bind(ProvisionInterceptor.class).toInstance(failingInterceptor);
+      }
+    };
+
+    InterceptingInjectorBuilder builder = new InterceptingInjectorBuilder()
+        .intercept(ArrayList.class)
+        .install(module);
+
+    try {
+      builder.build();
+      fail();
+    } catch(IllegalArgumentException expected) {
+    }
+  }
+
+  public void testTolerateUnmatchedInterceptions() {
+    Module module = new AbstractModule() {
+      protected void configure() {
+        bind(ProvisionInterceptor.class).toInstance(failingInterceptor);
+      }
+    };
+
+    Injector injector = new InterceptingInjectorBuilder()
+        .intercept(ArrayList.class)
+        .tolerateUnmatchedInterceptions()
+        .install(module)
+        .build();
+
+    assertEquals(new ArrayList(), injector.getInstance(ArrayList.class));
+  }
+
+}
diff --git a/extensions/commands/test/com/google/inject/injectioncontroller/InjectionControllerTest.java b/extensions/commands/test/com/google/inject/injectioncontroller/InjectionControllerTest.java
new file mode 100644
index 0000000..347c682
--- /dev/null
+++ b/extensions/commands/test/com/google/inject/injectioncontroller/InjectionControllerTest.java
@@ -0,0 +1,89 @@
+/**
+ * Copyright (C) 2008 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.
+ */
+
+package com.google.inject.injectioncontroller;
+
+import com.google.inject.AbstractModule;
+import com.google.inject.Injector;
+import com.google.inject.commands.intercepting.InterceptingInjectorBuilder;
+import junit.framework.TestCase;
+
+import java.util.ArrayList;
+
+/**
+ * @author jessewilson@google.com (Jesse Wilson)
+ * @author jmourits@google.com (Jerome Mourits)
+ */
+public class InjectionControllerTest extends TestCase {
+
+  private InjectionController injectionController = new InjectionController();
+
+  public void testSimpleOverride() throws Exception {
+    Injector injector = new InterceptingInjectorBuilder()
+        .install(injectionController.createModule(),
+            new AbstractModule() {
+              protected void configure() {
+                bind(String.class).toInstance("a");
+              }
+            })
+        .intercept(String.class)
+        .build();
+
+    assertEquals("a", injector.getInstance(String.class));
+    injectionController.substitute(String.class, "b");
+    assertEquals("b", injector.getInstance(String.class));
+  }
+
+  public void testOverrideRequiresWhitelist() throws Exception {
+    Injector injector = new InterceptingInjectorBuilder()
+        .install(injectionController.createModule(),
+            new AbstractModule() {
+              protected void configure() {
+                bind(String.class).toInstance("a");
+              }
+            })
+        .build();
+
+    injectionController.substitute(String.class, "b");
+    assertEquals("a", injector.getInstance(String.class));
+  }
+
+  public void testBareBindingFails() throws Exception {
+    InterceptingInjectorBuilder builder = new InterceptingInjectorBuilder()
+        .install(injectionController.createModule(),
+            new AbstractModule() {
+              protected void configure() {
+                bind(ArrayList.class);
+              }
+            })
+        .intercept(ArrayList.class);
+
+    try {
+      builder.build();
+      fail();
+    } catch (UnsupportedOperationException expected) {
+    }
+  }
+
+  public void testCannotOverrideDouble() throws Exception {
+    injectionController.substitute(String.class, "b");
+    try {
+      injectionController.substitute(String.class, "c");
+      fail();
+    } catch(IllegalStateException expected) {
+    }
+  }
+}