Introducing the idea of a stack of ExternalContexts.

This means that when we throw an ProvisionException, we can include the entire stack, rather than catching and adding context as necessary.

It also means it's slightly easier to push a new ExternalContext onto the stack.

git-svn-id: https://google-guice.googlecode.com/svn/trunk@348 d779f126-a31b-0410-b53b-1d3aecad763e
diff --git a/src/com/google/inject/BinderImpl.java b/src/com/google/inject/BinderImpl.java
index 001d183..8710414 100644
--- a/src/com/google/inject/BinderImpl.java
+++ b/src/com/google/inject/BinderImpl.java
@@ -482,16 +482,14 @@
     }
 
     public Void call(InternalContext context) {
-      ExternalContext<?> externalContext
-          = ExternalContext.newInstance(null, NULLABLE, key,
-              context.getInjectorImpl());
-      context.setExternalContext(externalContext);
+      context.pushExternalContext(ExternalContext.newInstance(
+          null, NULLABLE, key, context.getInjectorImpl()));
       try {
         factory.get(context);
         return null;
       }
       finally {
-        context.setExternalContext(null);
+        context.popExternalContext();
       }
     }
   }
diff --git a/src/com/google/inject/BoundProviderFactory.java b/src/com/google/inject/BoundProviderFactory.java
index 61046a1..058a1f8 100644
--- a/src/com/google/inject/BoundProviderFactory.java
+++ b/src/com/google/inject/BoundProviderFactory.java
@@ -60,12 +60,11 @@
     Provider<? extends T> provider = providerFactory.get(context);
     try {
       return context.sanitize(provider.get(), source);
-    } catch(ProvisionException provisionException) {
-      provisionException.addContext(context.getExternalContext());
-      throw provisionException;
+    } catch(ProvisionException e) {
+      throw e;
     } catch(RuntimeException e) {
-      throw new ProvisionException(context.getExternalContext(), e,
-          ErrorMessages.ERROR_IN_PROVIDER);
+      throw new ProvisionException(context.getExternalContextStack(),
+          e, ErrorMessages.ERROR_IN_PROVIDER);
     }
   }
 }
diff --git a/src/com/google/inject/ConstructorInjector.java b/src/com/google/inject/ConstructorInjector.java
index 6660f3e..f1e0cb7 100644
--- a/src/com/google/inject/ConstructorInjector.java
+++ b/src/com/google/inject/ConstructorInjector.java
@@ -159,8 +159,8 @@
     }
     catch (InvocationTargetException e) {
       Throwable cause = e.getCause() != null ? e.getCause() : e;
-      throw new ProvisionException(context.getExternalContext(), cause,
-          ErrorMessages.ERROR_INJECTING_CONSTRUCTOR);
+      throw new ProvisionException(context.getExternalContextStack(),
+          cause, ErrorMessages.ERROR_INJECTING_CONSTRUCTOR);
     }
     finally {
       constructionContext.removeCurrentReference();
diff --git a/src/com/google/inject/InjectorImpl.java b/src/com/google/inject/InjectorImpl.java
index 970c045..d63adba 100644
--- a/src/com/google/inject/InjectorImpl.java
+++ b/src/com/google/inject/InjectorImpl.java
@@ -215,9 +215,14 @@
     BindingImpl<String> stringBinding = getBinding(stringKey);
     if (stringBinding != null && stringBinding.isConstant()) {
       InternalContext context = new InternalContext(this);
-      context.setExternalContext(ExternalContext.newInstance(
+      String value;
+      context.pushExternalContext(ExternalContext.newInstance(
           member, Nullability.NOT_NULLABLE, key, this));
-      String value = stringBinding.getInternalFactory().get(context);
+      try {
+        value = stringBinding.getInternalFactory().get(context);
+      } finally {
+        context.popExternalContext();
+      }
 
       // TODO: Generalize everything below here and enable users to plug in
       // their own converters.
@@ -465,8 +470,7 @@
     }
 
     public void inject(InternalContext context, Object o) {
-      ExternalContext<?> previous = context.getExternalContext();
-      context.setExternalContext(externalContext);
+      context.pushExternalContext(externalContext);
       try {
         Object value = factory.get(context);
         field.set(o, value);
@@ -478,15 +482,14 @@
         throw e;
       }
       catch (ProvisionException provisionException) {
-        provisionException.addContext(externalContext);
         throw provisionException;
       }
       catch (RuntimeException runtimeException) {
-        throw new ProvisionException(externalContext, runtimeException,
-            ErrorMessages.ERROR_INJECTING_FIELD);
+        throw new ProvisionException(context.getExternalContextStack(),
+            runtimeException, ErrorMessages.ERROR_INJECTING_FIELD);
       }
       finally {
-        context.setExternalContext(previous);
+        context.popExternalContext();
       }
     }
   }
@@ -527,9 +530,8 @@
       throw new MissingDependencyException(key, member);
     }
 
-    ExternalContext<T> externalContext
-        = ExternalContext.newInstance(member, index,
-        Nullability.forAnnotations(annotations), key, this);
+    ExternalContext<T> externalContext = ExternalContext.newInstance(
+        member, index, Nullability.forAnnotations(annotations), key, this);
     return new SingleParameterInjector<T>(externalContext, factory);
   }
 
@@ -580,13 +582,12 @@
         throw new AssertionError(e);
       }
       catch (ProvisionException e) {
-        e.addContext(context.getExternalContext());
         throw e;
       }
       catch (InvocationTargetException e) {
         Throwable cause = e.getCause() != null ? e.getCause() : e;
-        throw new ProvisionException(context.getExternalContext(), cause,
-            ErrorMessages.ERROR_INJECTING_METHOD);
+        throw new ProvisionException(context.getExternalContextStack(),
+            cause, ErrorMessages.ERROR_INJECTING_METHOD);
       }
     }
   }
@@ -655,24 +656,22 @@
     }
 
     T inject(InternalContext context) {
-      ExternalContext<?> previous = context.getExternalContext();
-      context.setExternalContext(externalContext);
+      context.pushExternalContext(externalContext);
       try {
         return factory.get(context);
       }
       catch (ConfigurationException e) {
         throw e;
       }
-      catch (ProvisionException provisionException) {
-        provisionException.addContext(externalContext);
-        throw provisionException;
+      catch (ProvisionException e) {
+        throw e;
       }
       catch (RuntimeException runtimeException) {
-        throw new ProvisionException(externalContext, runtimeException,
-            ErrorMessages.ERROR_INJECTING_METHOD);
+        throw new ProvisionException(context.getExternalContextStack(),
+            runtimeException, ErrorMessages.ERROR_INJECTING_METHOD);
       }
       finally {
-        context.setExternalContext(previous);
+        context.popExternalContext();
       }
     }
   }
@@ -731,15 +730,13 @@
       public T get() {
         return callInContext(new ContextualCallable<T>() {
           public T call(InternalContext context) {
-            ExternalContext<?> previous = context.getExternalContext();
-            context.setExternalContext(
-                ExternalContext.newInstance(null, Nullability.NOT_NULLABLE,
-                    key, InjectorImpl.this));
+            context.pushExternalContext(ExternalContext.newInstance(
+                null, Nullability.NOT_NULLABLE, key, InjectorImpl.this));
             try {
               return factory.get(context);
             }
             finally {
-              context.setExternalContext(previous);
+              context.popExternalContext();
             }
           }
         });
diff --git a/src/com/google/inject/InternalContext.java b/src/com/google/inject/InternalContext.java
index 6b2a447..b24037c 100644
--- a/src/com/google/inject/InternalContext.java
+++ b/src/com/google/inject/InternalContext.java
@@ -18,7 +18,9 @@
 
 import java.util.HashMap;
 import java.util.Map;
-import com.google.inject.spi.SourceProviders;
+import java.util.List;
+import java.util.ArrayList;
+import java.util.Collections;
 
 /**
  * Internal context. Used to coordinate injections and support circular
@@ -30,7 +32,8 @@
 
   final InjectorImpl injector;
   Map<Object, ConstructionContext<?>> constructionContexts;
-  ExternalContext<?> externalContext;
+  final List<ExternalContext<?>> externalContextStack =
+      new ArrayList<ExternalContext<?>>(5);
 
   InternalContext(InjectorImpl injector) {
     this.injector = injector;
@@ -61,15 +64,36 @@
 
   @SuppressWarnings("unchecked")
   <T> ExternalContext<T> getExternalContext() {
-    return (ExternalContext<T>) externalContext;
+    if (externalContextStack.isEmpty()) {
+      throw new IllegalStateException("No external context on stack");
+    }
+    return (ExternalContext<T>) externalContextStack.get(
+        externalContextStack.size() - 1);
+  }
+
+  public List<ExternalContext<?>> getExternalContextStack() {
+    return Collections.unmodifiableList(
+        new ArrayList<ExternalContext<?>>(externalContextStack));
   }
 
   Class<?> getExpectedType() {
-    return externalContext.getKey().getRawType();
+    return getExternalContext().getKey().getRawType();
   }
 
-  void setExternalContext(ExternalContext<?> externalContext) {
-    this.externalContext = externalContext;
+  /**
+   * Push a new external context onto the stack. Each call to {@code #push()}
+   * requires a matching call to {@code #pop()} so that the contexts are
+   * balanced.
+   */
+  void pushExternalContext(ExternalContext<?> externalContext) {
+    externalContextStack.add(externalContext);
+  }
+
+  /**
+   * Pop the external context off the stack.
+   */
+  void popExternalContext() {
+    externalContextStack.remove(externalContextStack.size() - 1);
   }
 
   /**
@@ -87,7 +111,7 @@
             getExternalContext().getMember())
         : String.format(ErrorMessages.CANNOT_INJECT_NULL, source);
 
-    throw new ProvisionException(externalContext,
+    throw new ProvisionException(getExternalContextStack(), 
         new NullPointerException(message),
         String.format(ErrorMessages.CANNOT_INJECT_NULL, source));
   }
diff --git a/src/com/google/inject/ProvisionException.java b/src/com/google/inject/ProvisionException.java
index fa0fa9e..87e75f1 100644
--- a/src/com/google/inject/ProvisionException.java
+++ b/src/com/google/inject/ProvisionException.java
@@ -19,14 +19,13 @@
 import static com.google.inject.ErrorMessages.ERROR_WHILE_LOCATING_FIELD;
 import static com.google.inject.ErrorMessages.ERROR_WHILE_LOCATING_PARAMETER;
 import static com.google.inject.ErrorMessages.ERROR_WHILE_LOCATING_VALUE;
-import com.google.inject.internal.Objects;
 import com.google.inject.internal.StackTraceElements;
 import java.lang.reflect.Constructor;
 import java.lang.reflect.Field;
 import java.lang.reflect.Member;
 import java.lang.reflect.Method;
 import java.util.ArrayList;
-import java.util.Iterator;
+import java.util.Collections;
 import java.util.List;
 
 /**
@@ -35,49 +34,26 @@
  */
 public class ProvisionException extends RuntimeException {
 
-  private static final String NEWLINE = String.format("%n");
-
   private final String errorMessage;
-  private final List<ExternalContext> contexts =
-      new ArrayList<ExternalContext>(4);
+  private final List<ExternalContext<?>> contexts;
 
-  ProvisionException(ExternalContext<?> externalContext,
+  ProvisionException(List<ExternalContext<?>> externalContextStack,
       Throwable cause, String errorMessage) {
     super(errorMessage, cause);
     this.errorMessage = errorMessage;
-    contexts.add(externalContext);
-  }
-
-  /**
-   * Add more context to this exception, to be included in the exception
-   * message. This allows nested contexts to be displayed more concisely than
-   * with exception chaining.
-   */
-  void addContext(ExternalContext<?> externalContext) {
-    // deduplicate contexts.
-    if (!contexts.isEmpty()) {
-      ExternalContext last = contexts.get(contexts.size() - 1);
-      if (Objects.equal(last.getKey(), externalContext.getKey())
-          && Objects.equal(last.getMember(), externalContext.getMember())) {
-        return;
-      }
-    }
-    
-    contexts.add(externalContext);
+    this.contexts = Collections.unmodifiableList(
+        new ArrayList<ExternalContext<?>>(externalContextStack));
   }
 
   @Override
   public String getMessage() {
     StringBuilder result = new StringBuilder();
-    result.append(errorMessage)
-        .append(NEWLINE);
+    result.append(errorMessage);
 
-    for (Iterator<ExternalContext> e = contexts.iterator(); e.hasNext(); ) {
-      ExternalContext externalContext = e.next();
+    for (int i = contexts.size() - 1; i >= 0; i--) {
+      ExternalContext externalContext = contexts.get(i);
+      result.append(String.format("%n"));
       result.append(contextToSnippet(externalContext));
-      if (e.hasNext()) {
-        result.append(NEWLINE);
-      }
     }
 
     return result.toString();