Initial checkin of Sam Berlin's contribution for Module overrides. We still might want a small DSL to replace the single two-argument method.

git-svn-id: https://google-guice.googlecode.com/svn/trunk@486 d779f126-a31b-0410-b53b-1d3aecad763e
diff --git a/extensions/commands/test/com/google/inject/commands/intercepting/AllTests.java b/extensions/commands/test/com/google/inject/commands/intercepting/AllTests.java
index 2c0d24f..2001a2f 100644
--- a/extensions/commands/test/com/google/inject/commands/intercepting/AllTests.java
+++ b/extensions/commands/test/com/google/inject/commands/intercepting/AllTests.java
@@ -16,7 +16,6 @@
 
 package com.google.inject.commands.intercepting;
 
-import com.google.inject.commands.intercepting.InterceptingInjectorBuilderTest;
 import junit.framework.Test;
 import junit.framework.TestSuite;
 
diff --git a/src/com/google/inject/Guice.java b/src/com/google/inject/Guice.java
index 7c23e72..b834164 100644
--- a/src/com/google/inject/Guice.java
+++ b/src/com/google/inject/Guice.java
@@ -16,7 +16,13 @@
 
 package com.google.inject;
 
+import com.google.inject.commands.*;
+import com.google.inject.internal.UniqueAnnotations;
+
 import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
 
 /**
  * The entry point to the Guice framework. Creates {@link Injector}s from
@@ -135,4 +141,63 @@
         .build();
   }
 
+  /**
+   * Returns a new {@link Module} that overlays {@code overridesModule} over
+   * {@code module}. If a key is bound by both modules, only the binding in
+   * overrides is kept. This can be used to replace bindings in a production
+   * module with test bindings:
+   * <pre>
+   * Module functionalTestModule
+   *     = Guice.overrideModule(new ProductionModule(), new TestModule());
+   * </pre>
+   */
+  public static Module overrideModule(Module module, Module overridesModule) {
+    final FutureInjector futureInjector = new FutureInjector();
+    CommandRecorder commandRecorder = new CommandRecorder(futureInjector);
+    final List<Command> commands = commandRecorder.recordCommands(module);
+    final List<Command> overrideCommands = commandRecorder.recordCommands(overridesModule);
+
+    return new AbstractModule() {
+      public void configure() {
+        final Set<Key> overriddenKeys = new HashSet<Key>();
+
+        bind(Object.class).annotatedWith(UniqueAnnotations.create())
+            .toInstance(new Object() {
+              @Inject void initialize(Injector injector) {
+                futureInjector.initialize(injector);
+              }
+            });
+
+        // execute the overrides module, keeping track of which keys were bound
+        new CommandReplayer() {
+          @Override public <T> void replayBind(Binder binder, BindCommand<T> command) {
+            overriddenKeys.add(command.getKey());
+            super.replayBind(binder, command);
+          }
+          @Override public void replayBindConstant(Binder binder, BindConstantCommand command) {
+            overriddenKeys.add(command.getKey());
+            super.replayBindConstant(binder, command);
+          }
+        }.replay(binder(), overrideCommands);
+
+        // bind the regular module, skipping overridden keys. We only skip each
+        // overridden key once, so things still blow up if the module binds the
+        // same key multiple times
+        new CommandReplayer() {
+          @Override public <T> void replayBind(Binder binder, BindCommand<T> command) {
+            if (!overriddenKeys.remove(command.getKey())) {
+              super.replayBind(binder, command);
+            }
+          }
+          @Override public void replayBindConstant(Binder binder, BindConstantCommand command) {
+            if (!overriddenKeys.remove(command.getKey())) {
+              super.replayBindConstant(binder, command);
+            }
+          }
+        }.replay(binder(), commands);
+
+        // TODO: bind the overridden keys using multibinder
+      }
+    };
+  }
 }
diff --git a/src/com/google/inject/commands/CommandReplayer.java b/src/com/google/inject/commands/CommandReplayer.java
index d4326fb..f1a1b28 100644
--- a/src/com/google/inject/commands/CommandReplayer.java
+++ b/src/com/google/inject/commands/CommandReplayer.java
@@ -19,12 +19,12 @@
 import com.google.inject.Binder;
 import com.google.inject.Key;
 import com.google.inject.Module;
-import com.google.inject.spi.SourceProviders;
 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 com.google.inject.spi.SourceProviders;
 import org.aopalliance.intercept.MethodInterceptor;
 
 import java.util.List;
@@ -40,7 +40,7 @@
    * Returns a module that executes the specified commands
    * using this executing visitor.
    */
-  public Module createModule(final Iterable<Command> commands) {
+  public final Module createModule(final Iterable<Command> commands) {
     return new Module() {
       public void configure(Binder binder) {
         replay(binder, commands);
@@ -51,7 +51,7 @@
   /**
    * Replays {@code commands} against {@code binder}.
    */
-  public void replay(final Binder binder, Iterable<Command> commands) {
+  public final void replay(final Binder binder, Iterable<Command> commands) {
     Objects.nonNull(binder, "binder");
     Objects.nonNull(commands, "commands");
 
diff --git a/src/com/google/inject/commands/DefaultCommandVisitor.java b/src/com/google/inject/commands/DefaultCommandVisitor.java
new file mode 100644
index 0000000..08f433b
--- /dev/null
+++ b/src/com/google/inject/commands/DefaultCommandVisitor.java
@@ -0,0 +1,73 @@
+/**
+ * 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;
+
+/**
+ * No-op visitor for subclassing. All interface methods simply delegate to
+ * {@link #visitCommand(Command)}, returning its result.
+ *
+ * @author sberlin@gmail.com (Sam Berlin)
+ */
+public class DefaultCommandVisitor<V> implements Command.Visitor<V> {
+
+  protected DefaultCommandVisitor() {}
+
+  /**
+   * Visit {@code command} and return a result.
+   */
+  public V visitCommand(Command command) {
+    return null;
+  }
+
+  public V visitAddError(AddThrowableErrorCommand command) {
+    return visitCommand(command);
+  }
+
+  public V visitAddMessageError(AddMessageErrorCommand command) {
+    return visitCommand(command);
+  }
+
+  public <T> V visitBind(BindCommand<T> command) {
+    return visitCommand(command);
+  }
+
+  public V visitBindConstant(BindConstantCommand command) {
+    return visitCommand(command);
+  }
+
+  public V visitBindInterceptor(BindInterceptorCommand command) {
+    return visitCommand(command);
+  }
+
+  public V visitBindScope(BindScopeCommand command) {
+    return visitCommand(command);
+  }
+
+  public V visitConvertToTypes(ConvertToTypesCommand command) {
+    return visitCommand(command);
+  }
+
+  public <T> V visitGetProvider(GetProviderCommand<T> command) {
+    return visitCommand(command);
+  }
+
+  public V visitRequestStaticInjection(
+      RequestStaticInjectionCommand command) {
+    return visitCommand(command);
+  }
+}
diff --git a/test/com/google/inject/OverrideModuleTest.java b/test/com/google/inject/OverrideModuleTest.java
new file mode 100644
index 0000000..03c33be
--- /dev/null
+++ b/test/com/google/inject/OverrideModuleTest.java
@@ -0,0 +1,234 @@
+/**
+ * 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;
+
+import static com.google.inject.Guice.createInjector;
+import static com.google.inject.Guice.overrideModule;
+import static com.google.inject.name.Names.named;
+import junit.framework.TestCase;
+
+import java.util.Date;
+
+/**
+ * @author sberlin@gmail.com (Sam Berlin)
+ */
+public class OverrideModuleTest extends TestCase {
+
+  private static final Key<String> key2 = Key.get(String.class, named("2"));
+  private static final Key<String> key3 = Key.get(String.class, named("3"));
+
+  private static final Module EMPTY_MODULE = new Module() {
+    public void configure(Binder binder) {}
+  };
+
+
+  public void testOverride() {
+    Module original = new AbstractModule() {
+      protected void configure() {
+        bind(String.class).toInstance("A");
+      }
+    };
+
+    Module replacements = new AbstractModule() {
+      protected void configure() {
+        bind(String.class).toInstance("B");
+      }
+    };
+
+    Injector injector = createInjector(overrideModule(original, replacements));
+    assertEquals("B", injector.getInstance(String.class));
+  }
+
+  public void testOverrideUnmatchedTolerated() {
+    Module replacements = new AbstractModule() {
+      protected void configure() {
+        bind(String.class).toInstance("B");
+      }
+    };
+
+    Injector injector = createInjector(overrideModule(EMPTY_MODULE, replacements));
+    assertEquals("B", injector.getInstance(String.class));
+  }
+
+  public void testOverrideConstant() {
+    Module original = new AbstractModule() {
+      protected void configure() {
+        bindConstant().annotatedWith(named("Test")).to("A");
+      }
+    };
+
+    Module replacements = new AbstractModule() {
+      protected void configure() {
+        bindConstant().annotatedWith(named("Test")).to("B");
+      }
+    };
+
+    Injector injector = createInjector(overrideModule(original, replacements));
+    assertEquals("B", injector.getInstance(Key.get(String.class, named("Test"))));
+  }
+
+  public void testGetProviderInModule() {
+    Module original = new AbstractModule() {
+      protected void configure() {
+        bind(String.class).toInstance("A");
+        bind(key2).toProvider(getProvider(String.class));
+      }
+    };
+
+    Injector injector = createInjector(overrideModule(original, EMPTY_MODULE));
+    assertEquals("A", injector.getInstance(String.class));
+    assertEquals("A", injector.getInstance(key2));
+  }
+
+  public void testOverrideWhatGetProviderProvided() {
+    Module original = new AbstractModule() {
+      protected void configure() {
+        bind(String.class).toInstance("A");
+        bind(key2).toProvider(getProvider(String.class));
+      }
+    };
+
+    Module replacements = new AbstractModule() {
+      protected void configure() {
+        bind(String.class).toInstance("B");
+      }
+    };
+
+    Injector injector = createInjector(overrideModule(original, replacements));
+    assertEquals("B", injector.getInstance(String.class));
+    assertEquals("B", injector.getInstance(key2));
+  }
+
+  public void testOverrideUsingOriginalsGetProvider() {
+    Module original = new AbstractModule() {
+      protected void configure() {
+        bind(String.class).toInstance("A");
+        bind(key2).toInstance("B");
+      }
+    };
+
+    Module replacements = new AbstractModule() {
+      protected void configure() {
+        bind(String.class).toProvider(getProvider(key2));
+      }
+    };
+
+    Injector injector = createInjector(overrideModule(original, replacements));
+    assertEquals("B", injector.getInstance(String.class));
+    assertEquals("B", injector.getInstance(key2));
+  }
+
+  public void testOverrideOfOverride() {
+    Module original = new AbstractModule() {
+      protected void configure() {
+        bind(String.class).toInstance("A1");
+        bind(key2).toInstance("A2");
+        bind(key3).toInstance("A3");
+      }
+    };
+
+    Module replacements1 = new AbstractModule() {
+      protected void configure() {
+        bind(String.class).toInstance("B1");
+        bind(key2).toInstance("B2");
+      }
+    };
+
+    Module overrides = overrideModule(original, replacements1);
+
+    Module replacements2 = new AbstractModule() {
+      protected void configure() {
+        bind(String.class).toInstance("C1");
+        bind(key3).toInstance("C3");
+      }
+    };
+
+    Injector injector = createInjector(overrideModule(overrides, replacements2));
+    assertEquals("C1", injector.getInstance(String.class));
+    assertEquals("B2", injector.getInstance(key2));
+    assertEquals("C3", injector.getInstance(key3));
+  }
+
+  public void testOverridesTwiceFails() {
+    Module original = new AbstractModule() {
+      protected void configure() {
+        bind(String.class).toInstance("A");
+      }
+    };
+
+    Module replacements = new AbstractModule() {
+      protected void configure() {
+        bind(String.class).toInstance("B");
+        bind(String.class).toInstance("C");
+      }
+    };
+
+    Module module = overrideModule(original, replacements);
+    try {
+      createInjector(module);
+      fail();
+    } catch (CreationException expected) {
+      assertTrue(expected.getMessage().contains("Error at " + replacements.getClass().getName()));
+      assertTrue(expected.getMessage().contains(
+          "A binding to java.lang.String was already configured at "
+              + replacements.getClass().getName()));
+    }
+  }
+
+  public void testOverridesDoesntFixTwiceBoundInOriginal() {
+    Module original = new AbstractModule() {
+      protected void configure() {
+        bind(String.class).toInstance("A");
+        bind(String.class).toInstance("B");
+      }
+    };
+
+    Module replacements = new AbstractModule() {
+      protected void configure() {
+        bind(String.class).toInstance("C");
+      }
+    };
+
+    Module module = overrideModule(original, replacements);
+    try {
+      createInjector(module);
+      fail();
+    } catch (CreationException expected) {
+      assertTrue(expected.getMessage().contains("Error at " + original.getClass().getName()));
+      assertTrue(expected.getMessage().contains(
+          "A binding to java.lang.String was already configured at "
+              + replacements.getClass().getName()));
+    }
+  }
+
+  public void testOverrideUntargettedBinding() {
+    Module original = new AbstractModule() {
+      protected void configure() {
+        bind(Date.class);
+      }
+    };
+
+    Module replacements = new AbstractModule() {
+      protected void configure() {
+        bind(Date.class).toInstance(new Date(0));
+      }
+    };
+
+    Injector injector = createInjector(overrideModule(original, replacements));
+    assertEquals(0, injector.getInstance(Date.class).getTime());
+  }
+}