Private modules first draft. I'm not in love with the name "PrivateModules" or the "expose" method. But I think this is a reasonable first implementation of parent/child modules.

git-svn-id: https://google-guice.googlecode.com/svn/trunk@632 d779f126-a31b-0410-b53b-1d3aecad763e
diff --git a/build.properties b/build.properties
index ca5f2d9..6f12059 100644
--- a/build.properties
+++ b/build.properties
@@ -7,6 +7,7 @@
 commands.src.dir=extensions/commands/src
 throwingproviders.src.dir=extensions/throwingproviders/src
 multibindings.src.dir=extensions/multibindings/src
+privatemodules.src.dir=extensions/privatemodules/src
 build.dir=build
 javadoc.packagenames=com.google.inject,com.google.inject.spi,\
   com.google.inject.matcher,com.google.inject.servlet,com.google.inject.name,\
@@ -15,6 +16,7 @@
   com.google.inject.assistedinject,\
   com.google.inject.throwingproviders,\
   com.google.inject.multibindings,\
+  com.google.inject.privatemodules,\
   com.google.inject.commands
 test.class=com.google.inject.AllTests
 module=com.google.inject
diff --git a/build.xml b/build.xml
index e092ccf..dcea5ea 100644
--- a/build.xml
+++ b/build.xml
@@ -35,6 +35,7 @@
     <ant antfile="extensions/assistedinject/build.xml" target="distjars" inheritAll="false"/>
     <ant antfile="extensions/throwingproviders/build.xml" target="distjars" inheritAll="false"/>
     <ant antfile="extensions/multibindings/build.xml" target="distjars" inheritAll="false"/>
+    <ant antfile="extensions/privatemodules/build.xml" target="distjars" inheritAll="false"/>
     <ant antfile="extensions/commands/build.xml" target="distjars" inheritAll="false"/>
 
     <copy toDir="${build.dir}/dist"> 
@@ -56,6 +57,9 @@
       <fileset dir="extensions/multibindings/build" includes="*.jar"/>
     </copy>
     <copy toDir="${build.dir}/dist">
+      <fileset dir="extensions/privatemodules/build" includes="*.jar"/>
+    </copy>
+    <copy toDir="${build.dir}/dist">
       <fileset dir="extensions/commands/build" includes="*.jar"/>
     </copy>
 
@@ -115,6 +119,7 @@
         <pathelement location="${assistedinject.src.dir}"/>
         <pathelement location="${throwingproviders.src.dir}"/>
         <pathelement location="${multibindings.src.dir}"/>
+        <pathelement location="${privatemodules.src.dir}"/>
         <pathelement location="${commands.src.dir}"/>
       </sourcepath>
       <classpath refid="compile.classpath"/>
@@ -136,6 +141,7 @@
     <ant dir="extensions/assistedinject" antfile="build.xml" target="clean"/>
     <ant dir="extensions/throwingproviders" antfile="build.xml" target="clean"/>
     <ant dir="extensions/multibindings" antfile="build.xml" target="clean"/>
+    <ant dir="extensions/privatemodules" antfile="build.xml" target="clean"/>
     <ant dir="extensions/commands" antfile="build.xml" target="clean"/>
   </target>
   
diff --git a/extensions/privatemodules/build.properties b/extensions/privatemodules/build.properties
new file mode 100644
index 0000000..677b88f
--- /dev/null
+++ b/extensions/privatemodules/build.properties
@@ -0,0 +1,6 @@
+lib.dir=../../lib
+src.dir=src
+test.dir=test
+build.dir=build
+test.class=com.google.inject.privatemodules.AllTests
+module=com.google.inject.privatemodules
diff --git a/extensions/privatemodules/build.xml b/extensions/privatemodules/build.xml
new file mode 100644
index 0000000..99c7620
--- /dev/null
+++ b/extensions/privatemodules/build.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0"?>
+
+<project name="guice-privatemodules" 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, manifest"
+       description="Build jar.">
+    <mkdir dir="${build.dir}"/>
+    <jar destfile="${build.dir}/${ant.project.name}-${version}.jar"
+         manifest="${build.dir}/META-INF/MANIFEST.MF">
+      <fileset dir="${build.dir}/classes"/>
+    </jar>
+  </target>
+
+</project>
diff --git a/extensions/privatemodules/privatemodules.iml b/extensions/privatemodules/privatemodules.iml
new file mode 100644
index 0000000..bc42261
--- /dev/null
+++ b/extensions/privatemodules/privatemodules.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/privatemodules/src/com/google/inject/privatemodules/PrivateModule.java b/extensions/privatemodules/src/com/google/inject/privatemodules/PrivateModule.java
new file mode 100644
index 0000000..e6660b1
--- /dev/null
+++ b/extensions/privatemodules/src/com/google/inject/privatemodules/PrivateModule.java
@@ -0,0 +1,260 @@
+/**
+ * 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.privatemodules;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
+import com.google.common.collect.Sets;
+import com.google.inject.Binder;
+import com.google.inject.Inject;
+import com.google.inject.Injector;
+import com.google.inject.Key;
+import com.google.inject.Module;
+import com.google.inject.Provider;
+import com.google.inject.Scope;
+import com.google.inject.Scopes;
+import com.google.inject.Stage;
+import com.google.inject.TypeLiteral;
+import com.google.inject.binder.AnnotatedBindingBuilder;
+import com.google.inject.binder.AnnotatedConstantBindingBuilder;
+import com.google.inject.binder.LinkedBindingBuilder;
+import com.google.inject.internal.SourceProvider;
+import com.google.inject.internal.UniqueAnnotations;
+import com.google.inject.matcher.Matcher;
+import com.google.inject.spi.Elements;
+import com.google.inject.spi.Message;
+import com.google.inject.spi.ModuleWriter;
+import com.google.inject.spi.TypeConverter;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Method;
+import java.util.Set;
+import org.aopalliance.intercept.MethodInterceptor;
+
+/**
+ * @author jessewilson@google.com (Jesse Wilson)
+ */
+public abstract class PrivateModule implements Module {
+
+  private final SourceProvider sourceProvider
+      = new SourceProvider().plusSkippedClasses(PrivateModule.class);
+
+  /** When this provider returns, the private injector is ready. */
+  private Provider<Ready> readyProvider;
+
+  /** Keys exposed to the public injector */
+  private Set<Expose> exposes;
+
+  /** Like abstract module, the binder of the current private module */
+  private Binder privateBinder;
+
+  public final synchronized void configure(Binder binder) {
+    // when exposes is null, we're being run for the public injector
+    if (exposes == null) {
+      configurePublicBindings(binder);
+
+    // otherwise we're being run for the private injector
+    } else {
+      checkState(this.privateBinder == null, "Re-entry is not allowed.");
+      this.privateBinder = binder.skipSources(PrivateModule.class);
+      try {
+        configurePrivateBindings();
+
+        for (Expose<?> expose : exposes) {
+          expose.initPrivateProvider(binder);
+        }
+      } finally {
+        this.privateBinder = null;
+      }
+    }
+  }
+
+  private void configurePublicBindings(Binder publicBinder) {
+    exposes = Sets.newLinkedHashSet();
+    Key<Ready> readyKey = Key.get(Ready.class, UniqueAnnotations.create());
+    readyProvider = publicBinder.getProvider(readyKey);
+    try {
+      // gather elements and exposes from the private module by being re-entrant on configure()
+      final Module privateModule = new ModuleWriter().create(Elements.getElements(this));
+      for (Expose<?> expose : exposes) {
+        expose.configure(publicBinder);
+      }
+
+      // create the private injector while the public injector is injecting its members
+      publicBinder.bind(readyKey).toProvider(new Provider<Ready>() {
+        @Inject Injector publicInjector;
+        public Ready get() {
+          // this is necessary so the providers from getProvider() will work
+          publicInjector.createChildInjector(privateModule);
+          return new Ready();
+        }
+      }).in(Scopes.SINGLETON);
+
+    } finally {
+      readyProvider = null;
+      exposes = null;
+    }
+  }
+
+  private static class Ready {}
+
+  public abstract void configurePrivateBindings();
+
+  protected <T> void expose(Key<T> key) {
+    checkState(exposes != null, "Cannot expose %s, private module is not ready");
+    exposes.add(new Expose<T>(sourceProvider.get(), readyProvider, key));
+  }
+
+  protected <T> ExposedKeyBuilder expose(Class<T> type) {
+    checkState(exposes != null, "Cannot expose %s, private module is not ready");
+    Expose<T> expose = new Expose<T>(sourceProvider.get(), readyProvider, Key.get(type));
+    exposes.add(expose);
+    return expose;
+  }
+
+  protected <T> ExposedKeyBuilder expose(TypeLiteral<T> type) {
+    checkState(exposes != null, "Cannot expose %s, private module is not ready");
+    Expose<T> expose = new Expose<T>(sourceProvider.get(), readyProvider, Key.get(type));
+    exposes.add(expose);
+    return expose;
+  }
+
+  public interface ExposedKeyBuilder {
+    void annotatedWith(Class<? extends Annotation> annotationType);
+    void annotatedWith(Annotation annotation);
+  }
+
+  /**
+   * A binding from the private injector exposed to the public injector.
+   */
+  private static class Expose<T> implements ExposedKeyBuilder, Provider<T> {
+    private final Object source;
+    private final Provider<Ready> readyProvider;
+    private Key<T> key; // mutable, a binding annotation may be assigned after Expose creation
+    private Provider<T> privateProvider;
+
+    private Expose(Object source, Provider<Ready> readyProvider, Key<T> key) {
+      this.source = checkNotNull(source, "source");
+      this.readyProvider = checkNotNull(readyProvider, "readyProvider");
+      this.key = checkNotNull(key, "key");
+    }
+
+    public void annotatedWith(Class<? extends Annotation> annotationType) {
+      checkState(key.getAnnotationType() == null, "already annotated");
+      key = Key.get(key.getTypeLiteral(), annotationType);
+    }
+
+    public void annotatedWith(Annotation annotation) {
+      checkState(key.getAnnotationType() == null, "already annotated");
+      key = Key.get(key.getTypeLiteral(), annotation);
+    }
+
+    /** Sets the provider in the private injector, to be used by the public injector */
+    private void initPrivateProvider(Binder privateBinder) {
+      privateProvider = privateBinder.getProvider(key);
+    }
+
+    /** Creates a binding in the public binder */
+    private void configure(Binder publicBinder) {
+      publicBinder.withSource(source).bind(key).toProvider(this);
+    }
+
+    public T get() {
+      readyProvider.get(); // force creation of the private injector
+      return privateProvider.get();
+    }
+  }
+
+  // everything below is copied from AbstractModule
+
+  protected Binder binder() {
+    return privateBinder;
+  }
+
+  protected void bindScope(Class<? extends Annotation> scopeAnnotation, Scope scope) {
+    privateBinder.bindScope(scopeAnnotation, scope);
+  }
+
+  protected <T> LinkedBindingBuilder<T> bind(Key<T> key) {
+    return privateBinder.bind(key);
+  }
+
+  protected <T> AnnotatedBindingBuilder<T> bind(TypeLiteral<T> typeLiteral) {
+    return privateBinder.bind(typeLiteral);
+  }
+
+  protected <T> AnnotatedBindingBuilder<T> bind(Class<T> clazz) {
+    return privateBinder.bind(clazz);
+  }
+
+  protected AnnotatedConstantBindingBuilder bindConstant() {
+    return privateBinder.bindConstant();
+  }
+
+  protected void install(Module module) {
+    privateBinder.install(module);
+  }
+
+  protected void addError(String message, Object... arguments) {
+    privateBinder.addError(message, arguments);
+  }
+
+  protected void addError(Throwable t) {
+    privateBinder.addError(t);
+  }
+
+  protected void addError(Message message) {
+    privateBinder.addError(message);
+  }
+
+  protected void requestInjection(Object... objects) {
+    privateBinder.requestInjection(objects);
+  }
+
+  protected void requestStaticInjection(Class<?>... types) {
+    privateBinder.requestStaticInjection(types);
+  }
+
+  protected void bindInterceptor(Matcher<? super Class<?>> classMatcher,
+      Matcher<? super Method> methodMatcher, MethodInterceptor... interceptors) {
+    privateBinder.bindInterceptor(classMatcher, methodMatcher, interceptors);
+  }
+
+  protected void requireBinding(Key<?> key) {
+    privateBinder.getProvider(key);
+  }
+
+  protected void requireBinding(Class<?> type) {
+    privateBinder.getProvider(type);
+  }
+
+  protected <T> Provider<T> getProvider(Key<T> key) {
+    return privateBinder.getProvider(key);
+  }
+
+  protected <T> Provider<T> getProvider(Class<T> type) {
+    return privateBinder.getProvider(type);
+  }
+
+  protected void convertToTypes(Matcher<? super TypeLiteral<?>> typeMatcher,
+      TypeConverter converter) {
+    privateBinder.convertToTypes(typeMatcher, converter);
+  }
+
+  protected Stage currentStage() {
+    return privateBinder.currentStage();
+  }
+}
diff --git a/extensions/privatemodules/test/com/google/inject/privatemodules/AllTests.java b/extensions/privatemodules/test/com/google/inject/privatemodules/AllTests.java
new file mode 100644
index 0000000..2d947e8
--- /dev/null
+++ b/extensions/privatemodules/test/com/google/inject/privatemodules/AllTests.java
@@ -0,0 +1,31 @@
+/**
+ * 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.privatemodules;
+
+import junit.framework.Test;
+import junit.framework.TestSuite;
+
+/**
+ * @author jessewilson@google.com (Jesse Wilson)
+ */
+public class AllTests {
+  public static Test suite() {
+    TestSuite suite = new TestSuite();
+    suite.addTestSuite(PrivateModuleTest.class);
+    return suite;
+  }
+}
diff --git a/extensions/privatemodules/test/com/google/inject/privatemodules/PrivateModuleTest.java b/extensions/privatemodules/test/com/google/inject/privatemodules/PrivateModuleTest.java
new file mode 100644
index 0000000..913f7a8
--- /dev/null
+++ b/extensions/privatemodules/test/com/google/inject/privatemodules/PrivateModuleTest.java
@@ -0,0 +1,71 @@
+/**
+ * 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.privatemodules;
+
+import com.google.inject.AbstractModule;
+import com.google.inject.Guice;
+import com.google.inject.Inject;
+import com.google.inject.Injector;
+import com.google.inject.Key;
+import com.google.inject.name.Named;
+import static com.google.inject.name.Names.named;
+import junit.framework.TestCase;
+
+/**
+ * @author jessewilson@google.com (Jesse Wilson)
+ */
+public class PrivateModuleTest extends TestCase {
+
+  public void testBasicUsage() {
+    Injector injector = Guice.createInjector(new AbstractModule() {
+      protected void configure() {
+        bind(String.class).annotatedWith(named("a")).toInstance("public");
+
+        install(new PrivateModule() {
+          public void configurePrivateBindings() {
+            bind(String.class).annotatedWith(named("b")).toInstance("i");
+
+            bind(AB.class).annotatedWith(named("one")).to(AB.class);
+            expose(AB.class).annotatedWith(named("one"));
+          }
+        });
+
+        install(new PrivateModule() {
+          public void configurePrivateBindings() {
+            bind(String.class).annotatedWith(named("b")).toInstance("ii");
+
+            bind(AB.class).annotatedWith(named("two")).to(AB.class);
+            expose(AB.class).annotatedWith(named("two"));
+          }
+        });
+      }
+    });
+
+    AB ab1 = injector.getInstance(Key.get(AB.class, named("one")));
+    assertEquals("public", ab1.a);
+    assertEquals("i", ab1.b);
+
+    AB ab2 = injector.getInstance(Key.get(AB.class, named("two")));
+    assertEquals("public", ab2.a);
+    assertEquals("ii", ab2.b);
+  }
+
+  static class AB {
+    @Inject @Named("a") String a;
+    @Inject @Named("b") String b;
+  }
+}
diff --git a/src/com/google/inject/BindingProcessor.java b/src/com/google/inject/BindingProcessor.java
index a9f0386..18d1617 100644
--- a/src/com/google/inject/BindingProcessor.java
+++ b/src/com/google/inject/BindingProcessor.java
@@ -23,6 +23,7 @@
 import com.google.inject.internal.ErrorsException;
 import com.google.inject.spi.BindingScopingVisitor;
 import com.google.inject.spi.BindingTargetVisitor;
+import com.google.inject.spi.DefaultBindingTargetVisitor;
 import com.google.inject.spi.InjectionPoint;
 import java.lang.annotation.Annotation;
 import java.lang.reflect.Constructor;
@@ -38,6 +39,14 @@
  */
 class BindingProcessor extends AbstractProcessor {
 
+  private BindingTargetVisitor<Object, Object> GET_BINDING_PROVIDER
+      = new DefaultBindingTargetVisitor<Object, Object>() {
+    public Object visitProvider(
+        Provider<?> provider, Set<InjectionPoint> injectionPoints) {
+      return provider;
+    }
+  };
+
   private static final BindingScopingVisitor<LoadStrategy> LOAD_STRATEGY_VISITOR
       = new BindingScopingVisitor<LoadStrategy>() {
     public LoadStrategy visitEagerSingleton() {
@@ -247,7 +256,9 @@
     }
 
     Binding<?> original = state.getExplicitBinding(key);
-    if (original != null) {
+    if (original != null && !"com.google.inject.privatemodules.PrivateModule$Expose"
+        .equals(original.acceptTargetVisitor(GET_BINDING_PROVIDER).getClass().getName())) {
+      // the hard-coded class name is certainly lame, but it avoids an even lamer dependency... 
       errors.bindingAlreadySet(key, original.getSource());
       return;
     }
diff --git a/test/com/google/inject/ParentInjectorTest.java b/test/com/google/inject/ParentInjectorTest.java
index 953549b..f9aeaab 100644
--- a/test/com/google/inject/ParentInjectorTest.java
+++ b/test/com/google/inject/ParentInjectorTest.java
@@ -152,6 +152,33 @@
     assertSame(e.injector, parent);
   }
 
+  public void testSeveralLayersOfHierarchy() {
+    Injector top = Guice.createInjector(bindsA);
+    Injector left = top.createChildInjector();
+    Injector leftLeft = left.createChildInjector(bindsD);
+    Injector right = top.createChildInjector(bindsD);
+    
+    assertSame(leftLeft, leftLeft.getInstance(D.class).injector);
+    assertSame(right, right.getInstance(D.class).injector);
+    assertSame(top, leftLeft.getInstance(E.class).injector);
+    assertSame(top.getInstance(A.class), leftLeft.getInstance(A.class));
+
+    Injector leftRight = left.createChildInjector(bindsD);
+    assertSame(leftRight, leftRight.getInstance(D.class).injector);
+
+    try {
+      top.getInstance(D.class);
+      fail();
+    } catch (ProvisionException expected) {
+    }
+
+    try {
+      left.getInstance(D.class);
+      fail();
+    } catch (ProvisionException expected) {
+    }
+  }
+
   @Singleton
   static class A {}