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());
+ }
+}