Trying to preserve the getCause() of ProvisionException to be the user's exception

git-svn-id: https://google-guice.googlecode.com/svn/trunk@347 d779f126-a31b-0410-b53b-1d3aecad763e
diff --git a/src/com/google/inject/BoundProviderFactory.java b/src/com/google/inject/BoundProviderFactory.java
index 6d1c442..61046a1 100644
--- a/src/com/google/inject/BoundProviderFactory.java
+++ b/src/com/google/inject/BoundProviderFactory.java
@@ -57,6 +57,15 @@
   }
 
   public T get(InternalContext context) {
-    return context.sanitize(providerFactory.get(context).get(), source);
+    Provider<? extends T> provider = providerFactory.get(context);
+    try {
+      return context.sanitize(provider.get(), source);
+    } catch(ProvisionException provisionException) {
+      provisionException.addContext(context.getExternalContext());
+      throw provisionException;
+    } catch(RuntimeException e) {
+      throw new ProvisionException(context.getExternalContext(), e,
+          ErrorMessages.ERROR_IN_PROVIDER);
+    }
   }
 }
diff --git a/src/com/google/inject/ConstructorInjector.java b/src/com/google/inject/ConstructorInjector.java
index cd87cf1..6660f3e 100644
--- a/src/com/google/inject/ConstructorInjector.java
+++ b/src/com/google/inject/ConstructorInjector.java
@@ -158,7 +158,9 @@
       return t;
     }
     catch (InvocationTargetException e) {
-      throw new RuntimeException(e);
+      Throwable cause = e.getCause() != null ? e.getCause() : e;
+      throw new ProvisionException(context.getExternalContext(), cause,
+          ErrorMessages.ERROR_INJECTING_CONSTRUCTOR);
     }
     finally {
       constructionContext.removeCurrentReference();
diff --git a/src/com/google/inject/ErrorMessages.java b/src/com/google/inject/ErrorMessages.java
index f31fb31..9b8e641 100644
--- a/src/com/google/inject/ErrorMessages.java
+++ b/src/com/google/inject/ErrorMessages.java
@@ -155,14 +155,24 @@
   static final String PRELOAD_NOT_ALLOWED = "Preloading is only supported for"
       + " singleton-scoped bindings.";
 
-  static final String EXCEPTION_WHILE_CREATING = "Error while locating"
-      + " instance%n  bound to %s%n  for member at %s";
-  
-  static final String CANNOT_INJECT_NULL_INTO_MEMBER =
-      "Null value returned by binding at %s, but %s is not @Nullable";
+  static final String ERROR_INJECTING_FIELD = "Error injecting field";
+  static final String ERROR_INJECTING_METHOD = "Error injecting method";
+  static final String ERROR_INJECTING_CONSTRUCTOR =
+      "Error injecting constructor";
+  static final String ERROR_IN_PROVIDER = "Error in custom provider";
 
+  static final String ERROR_WHILE_LOCATING_FIELD =
+      "  while locating %s%n    for field at %s";
+  static final String ERROR_WHILE_LOCATING_PARAMETER =
+      "  while locating %s%n    for parameter %s at %s";
+  static final String ERROR_WHILE_LOCATING_VALUE =
+      "  while locating %s";
   static final String CANNOT_INJECT_NULL =
-      "Null value returned by binding at %s";
+      "null returned by binding at %s";
+  static final String CANNOT_INJECT_NULL_INTO_MEMBER =
+      "null returned by binding at %s%n    but %s is not @Nullable";
+
+
 
   static String getRootMessage(Throwable t) {
     Throwable cause = t.getCause();
diff --git a/src/com/google/inject/InjectorImpl.java b/src/com/google/inject/InjectorImpl.java
index 6909cd9..970c045 100644
--- a/src/com/google/inject/InjectorImpl.java
+++ b/src/com/google/inject/InjectorImpl.java
@@ -477,8 +477,13 @@
       catch (ConfigurationException e) {
         throw e;
       }
-      catch (Throwable throwable) {
-        throw new ProvisionException(externalContext, throwable);
+      catch (ProvisionException provisionException) {
+        provisionException.addContext(externalContext);
+        throw provisionException;
+      }
+      catch (RuntimeException runtimeException) {
+        throw new ProvisionException(externalContext, runtimeException,
+            ErrorMessages.ERROR_INJECTING_FIELD);
       }
       finally {
         context.setExternalContext(previous);
@@ -571,14 +576,17 @@
       try {
         methodInvoker.invoke(o, getParameters(context, parameterInjectors));
       }
+      catch (IllegalAccessException e) {
+        throw new AssertionError(e);
+      }
       catch (ProvisionException e) {
+        e.addContext(context.getExternalContext());
         throw e;
       }
-      catch (IllegalAccessException e) {
-        throw new ProvisionException(context.getExternalContext(), e);
-      }
       catch (InvocationTargetException e) {
-        throw new ProvisionException(context.getExternalContext(), e);
+        Throwable cause = e.getCause() != null ? e.getCause() : e;
+        throw new ProvisionException(context.getExternalContext(), cause,
+            ErrorMessages.ERROR_INJECTING_METHOD);
       }
     }
   }
@@ -655,8 +663,13 @@
       catch (ConfigurationException e) {
         throw e;
       }
-      catch (Throwable throwable) {
-        throw new ProvisionException(externalContext, throwable);
+      catch (ProvisionException provisionException) {
+        provisionException.addContext(externalContext);
+        throw provisionException;
+      }
+      catch (RuntimeException runtimeException) {
+        throw new ProvisionException(externalContext, runtimeException,
+            ErrorMessages.ERROR_INJECTING_METHOD);
       }
       finally {
         context.setExternalContext(previous);
diff --git a/src/com/google/inject/InternalContext.java b/src/com/google/inject/InternalContext.java
index bfb1301..6b2a447 100644
--- a/src/com/google/inject/InternalContext.java
+++ b/src/com/google/inject/InternalContext.java
@@ -86,8 +86,10 @@
         ? String.format(ErrorMessages.CANNOT_INJECT_NULL_INTO_MEMBER, source,
             getExternalContext().getMember())
         : String.format(ErrorMessages.CANNOT_INJECT_NULL, source);
+
     throw new ProvisionException(externalContext,
-        new NullPointerException(message));
+        new NullPointerException(message),
+        String.format(ErrorMessages.CANNOT_INJECT_NULL, source));
   }
 
   // TODO(kevinb): gee, ya think we might want to remove this?
diff --git a/src/com/google/inject/ProvisionException.java b/src/com/google/inject/ProvisionException.java
index 689db4d..fa0fa9e 100644
--- a/src/com/google/inject/ProvisionException.java
+++ b/src/com/google/inject/ProvisionException.java
@@ -16,24 +16,93 @@
 
 package com.google.inject;
 
+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.List;
 
 /**
  * Used to rethrow exceptions that occur while providing instances, to add
  * additional contextual details.
  */
-class ProvisionException extends RuntimeException {
-  public ProvisionException(ExternalContext<?> externalContext,
-      Throwable cause) {
-    super(createMessage(externalContext), cause);
+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);
+
+  ProvisionException(ExternalContext<?> externalContext,
+      Throwable cause, String errorMessage) {
+    super(errorMessage, cause);
+    this.errorMessage = errorMessage;
+    contexts.add(externalContext);
   }
 
-  private static String createMessage(ExternalContext<?> 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);
+  }
+
+  @Override
+  public String getMessage() {
+    StringBuilder result = new StringBuilder();
+    result.append(errorMessage)
+        .append(NEWLINE);
+
+    for (Iterator<ExternalContext> e = contexts.iterator(); e.hasNext(); ) {
+      ExternalContext externalContext = e.next();
+      result.append(contextToSnippet(externalContext));
+      if (e.hasNext()) {
+        result.append(NEWLINE);
+      }
+    }
+
+    return result.toString();
+  }
+
+  /**
+   * Returns a snippet to include in the stacktrace message that describes the
+   * specified context.
+   */
+  private String contextToSnippet(ExternalContext externalContext) {
     Key<?> key = externalContext.getKey();
+    Object keyDescription = ErrorMessages.convert(key);
     Member member = externalContext.getMember();
-    return String.format(ErrorMessages.EXCEPTION_WHILE_CREATING,
-        ErrorMessages.convert(key),
-        StackTraceElements.forMember(member));
+
+    if (member instanceof Field) {
+      return String.format(ERROR_WHILE_LOCATING_FIELD,
+          keyDescription, StackTraceElements.forMember(member));
+
+    } else if (member instanceof Method || member instanceof Constructor) {
+      return String.format(ERROR_WHILE_LOCATING_PARAMETER,
+          keyDescription, externalContext.getParameterIndex(),
+          StackTraceElements.forMember(member));
+
+    } else {
+      return String.format(ERROR_WHILE_LOCATING_VALUE, keyDescription);
+    }
   }
 }
diff --git a/test/com/google/inject/NullableInjectionPointTest.java b/test/com/google/inject/NullableInjectionPointTest.java
index 413b419..f2ccc32 100644
--- a/test/com/google/inject/NullableInjectionPointTest.java
+++ b/test/com/google/inject/NullableInjectionPointTest.java
@@ -13,10 +13,10 @@
       fail("Injecting null should fail with an error");
     }
     catch (ProvisionException expected) {
-      Throwable rootCause = getRootCause(expected);
-      assertContains(rootCause.getMessage(), "Null value returned by binding");
-      assertContains(rootCause.getMessage(), "FooConstructor");
-      assertContains(rootCause.getMessage(), "is not @Nullable");
+      NullPointerException cause = (NullPointerException)expected.getCause();
+      assertContains(cause.getMessage(), "null returned by binding at");
+      assertContains(cause.getMessage(), "FooConstructor");
+      assertContains(cause.getMessage(), "is not @Nullable");
     }
   }
 
@@ -26,10 +26,10 @@
       fail("Injecting null should fail with an error");
     }
     catch (ProvisionException expected) {
-      Throwable rootCause = getRootCause(expected);
-      assertContains(rootCause.getMessage(), "Null value returned by binding");
-      assertContains(rootCause.getMessage(), "FooMethod.setFoo");
-      assertContains(rootCause.getMessage(), "is not @Nullable");
+      NullPointerException cause = (NullPointerException)expected.getCause();
+      assertContains(cause.getMessage(), "null returned by binding at");
+      assertContains(cause.getMessage(), "FooMethod.setFoo");
+      assertContains(cause.getMessage(), "is not @Nullable");
     }
   }
 
@@ -39,10 +39,10 @@
       fail("Injecting null should fail with an error");
     }
     catch (ProvisionException expected) {
-      Throwable rootCause = getRootCause(expected);
-      assertContains(rootCause.getMessage(), "Null value returned by binding");
-      assertContains(rootCause.getMessage(), "FooField.foo");
-      assertContains(rootCause.getMessage(), "is not @Nullable");
+      NullPointerException cause = (NullPointerException)expected.getCause();
+      assertContains(cause.getMessage(), "null returned by binding at");
+      assertContains(cause.getMessage(), "FooField.foo");
+      assertContains(cause.getMessage(), "is not @Nullable");
     }
   }
 
@@ -52,8 +52,8 @@
       fail("Getting an instance of null should fail with an error");
     }
     catch (ProvisionException expected) {
-      Throwable rootCause = getRootCause(expected);
-      assertContains(rootCause.getMessage(), "Null value returned by binding "
+      NullPointerException cause = (NullPointerException)expected.getCause();
+      assertContains(cause.getMessage(), "null returned by binding "
           + "at com.google.inject.NullableInjectionPointTest");
     }
   }
@@ -97,8 +97,8 @@
       injector.getInstance(FooField.class);
     }
     catch(ProvisionException expected) {
-      Throwable rootCause = getRootCause(expected);
-      assertContains(rootCause.getMessage(), "Null value returned by binding");
+      NullPointerException cause = (NullPointerException)expected.getCause();
+      assertContains(cause.getMessage(), "null returned by binding at");
     }
   }
 
@@ -118,8 +118,8 @@
       injector.getInstance(FooField.class);
     }
     catch(ProvisionException expected) {
-      Throwable rootCause = getRootCause(expected);
-      assertContains(rootCause.getMessage(), "Null value returned by binding");
+      NullPointerException cause = (NullPointerException)expected.getCause();
+      assertContains(cause.getMessage(), "null returned by binding at");
     }
   }
 
@@ -139,8 +139,8 @@
       injector.getInstance(FooField.class);
     }
     catch(ProvisionException expected) {
-      Throwable rootCause = getRootCause(expected);
-      assertContains(rootCause.getMessage(), "Null value returned by binding");
+      NullPointerException cause = (NullPointerException)expected.getCause();
+      assertContains(cause.getMessage(), "null returned by binding at");
     }
   }
 
@@ -160,19 +160,12 @@
       injector.getInstance(FooField.class);
     }
     catch(ProvisionException expected) {
-      Throwable rootCause = getRootCause(expected);
-      assertContains(rootCause.getMessage(), "Null value returned by binding "
+      NullPointerException cause = (NullPointerException)expected.getCause();
+      assertContains(cause.getMessage(), "null returned by binding "
           + "at com.google.inject.NullableInjectionPointTest");
     }
   }
 
-  private Throwable getRootCause(Throwable throwable) {
-    while (throwable.getCause() != null) {
-      throwable = throwable.getCause();
-    }
-    return throwable;
-  }
-
   private void assertContains(String text, String substring) {
     assertTrue(String.format("Expected \"%s\" to contain substring \"%s\"",
         text, substring), text.contains(substring));
diff --git a/test/com/google/inject/ProvisionExceptionTest.java b/test/com/google/inject/ProvisionExceptionTest.java
new file mode 100644
index 0000000..b1afc8f
--- /dev/null
+++ b/test/com/google/inject/ProvisionExceptionTest.java
@@ -0,0 +1,95 @@
+package com.google.inject;
+
+import junit.framework.TestCase;
+
+/**
+ * @author jessewilson
+ */
+public class ProvisionExceptionTest extends TestCase {
+
+  public void testExceptionsCollapsed() {
+    try {
+      Guice.createInjector().getInstance(A.class);
+      fail(); 
+    }
+    catch (ProvisionException e) {
+      assertTrue(e.getCause() instanceof UnsupportedOperationException);
+      assertContains(e.getMessage(), "Error injecting constructor");
+      assertContains(e.getMessage(), "while locating "
+          + "com.google.inject.ProvisionExceptionTest$D");
+      assertContains(e.getMessage(), "for parameter 0 at "
+          + "com.google.inject.ProvisionExceptionTest$C.setD");
+      assertContains(e.getMessage(), "while locating "
+          + "com.google.inject.ProvisionExceptionTest$C");
+      assertContains(e.getMessage(), "for field at "
+          + "com.google.inject.ProvisionExceptionTest$B.c");
+      assertContains(e.getMessage(), "while locating "
+          + "com.google.inject.ProvisionExceptionTest$B");
+      assertContains(e.getMessage(), "for parameter 0 at "
+          + "com.google.inject.ProvisionExceptionTest$A");
+    }
+  }
+
+  public void testMethodInjectionExceptions() {
+    try {
+      Guice.createInjector().getInstance(E.class);
+      fail();
+    }
+    catch (ProvisionException e) {
+      e.printStackTrace();
+      assertTrue(e.getCause() instanceof UnsupportedOperationException);
+      assertContains(e.getMessage(), "Error injecting method");
+      assertContains(e.getMessage(), "while locating "
+          + "com.google.inject.ProvisionExceptionTest$E");
+    }
+  }
+
+  public void testProviderExceptions() {
+    try {
+      Guice.createInjector(new AbstractModule() {
+        protected void configure() {
+          bind(D.class).toProvider(DProvider.class);
+        }
+      }).getInstance(D.class);
+      fail();
+    }
+    catch (ProvisionException e) {
+      assertTrue(e.getCause() instanceof UnsupportedOperationException);
+      assertContains(e.getMessage(), "Error in custom provider");
+      assertContains(e.getMessage(), "while locating "
+          + "com.google.inject.ProvisionExceptionTest$D");
+    }
+  }
+
+  static class A {
+    @Inject
+    A(B b) { }
+  }
+  static class B {
+    @Inject C c;
+  }
+  static class C {
+    @Inject
+    void setD(D d) { }
+  }
+  static class D {
+    D() {
+      throw new UnsupportedOperationException();
+    }
+  }
+  static class E {
+    @Inject void setObject(Object o) {
+      throw new UnsupportedOperationException();
+    }
+  }
+  static class DProvider implements Provider<D> {
+    public D get() {
+      return new D();
+    }
+  }
+
+  private void assertContains(String text, String substring) {
+    assertTrue(String.format("Expected \"%s\" to contain substring \"%s\"",
+        text, substring), text.contains(substring));
+  }
+}