allow scopeRequest/continueRequest to seed with a null value, and also type-check the values immediately.

git-svn-id: https://google-guice.googlecode.com/svn/trunk@1424 d779f126-a31b-0410-b53b-1d3aecad763e
diff --git a/extensions/servlet/src/com/google/inject/servlet/ServletScopes.java b/extensions/servlet/src/com/google/inject/servlet/ServletScopes.java
index ea1dab5..6b19097 100644
--- a/extensions/servlet/src/com/google/inject/servlet/ServletScopes.java
+++ b/extensions/servlet/src/com/google/inject/servlet/ServletScopes.java
@@ -156,6 +156,9 @@
    *
    * @param callable code to be executed in another thread, which depends on
    *     the request scope.
+   * @param seedMap the initial set of scoped instances for Guice to seed the
+   *     request scope with.  To seed with null, use either a null
+   *     value or a value of {@link #nullObject()}.
    * @return a callable that will invoke the given callable, making the request
    *     context available to it.
    * @throws OutOfScopeException if this method is called from a non-request
@@ -172,7 +175,8 @@
     final ContinuingHttpServletRequest continuingRequest =
         new ContinuingHttpServletRequest(GuiceFilter.getRequest());
     for (Map.Entry<Key<?>, Object> entry : seedMap.entrySet()) {
-      continuingRequest.setAttribute(entry.getKey().toString(), entry.getValue());
+      Object value = validateAndCanonicalizeValue(entry.getKey(), entry.getValue());
+      continuingRequest.setAttribute(entry.getKey().toString(), value);
     }
 
     return new Callable<T>() {
@@ -216,7 +220,8 @@
    * @param callable code to be executed which depends on the request scope.
    *     Typically in another thread, but not necessarily so.
    * @param seedMap the initial set of scoped instances for Guice to seed the
-   *     request scope with.
+   *     request scope with.  To seed with null, use either a null
+   *     value or a value of {@link #nullObject()}.
    * @return a callable that when called will run inside the a request scope
    *     that exposes the instances in the {@code seedMap} as scoped keys.
    * @since 3.0
@@ -229,7 +234,8 @@
     // Copy the seed values into our local scope map.
     final Map<String, Object> scopeMap = Maps.newHashMap();
     for (Map.Entry<Key<?>, Object> entry : seedMap.entrySet()) {
-      scopeMap.put(entry.getKey().toString(), entry.getValue());
+      Object value = validateAndCanonicalizeValue(entry.getKey(), entry.getValue());
+      scopeMap.put(entry.getKey().toString(), value);
     }
 
     return new Callable<T>() {
@@ -249,4 +255,32 @@
       }
     };
   }
+
+  /**
+   * Returns an object that may be used in the seedMap of
+   * {@link #continueRequest} and {@link #scopeRequest} to indicate the value
+   * should remain null and not attempt to be created.
+   * 
+   * @since 3.0
+   */
+  public static Object nullObject() {
+    return NullObject.INSTANCE;
+  }
+
+  /**
+   * Validates the key and object, ensuring the value matches the key type, and
+   * canonicalizing null objects to the null sentinel.
+   */
+  private static Object validateAndCanonicalizeValue(Key<?> key, Object object) {
+    if (object == null || object == NullObject.INSTANCE) {
+      return NullObject.INSTANCE;
+    }
+
+    if (!key.getTypeLiteral().getRawType().isInstance(object)) {
+      throw new IllegalArgumentException("Value[" + object + "] of type["
+          + object.getClass().getName() + "] is not compatible with key[" + key + "]");
+    }
+
+    return object;
+  }
 }
diff --git a/extensions/servlet/test/com/google/inject/servlet/ScopeRequestIntegrationTest.java b/extensions/servlet/test/com/google/inject/servlet/ScopeRequestIntegrationTest.java
index 3f67ba1..53128ee 100644
--- a/extensions/servlet/test/com/google/inject/servlet/ScopeRequestIntegrationTest.java
+++ b/extensions/servlet/test/com/google/inject/servlet/ScopeRequestIntegrationTest.java
@@ -20,12 +20,17 @@
 import com.google.inject.Inject;
 import com.google.inject.Injector;
 import com.google.inject.Key;
+import com.google.inject.OutOfScopeException;
 import com.google.inject.Provider;
+import com.google.inject.ProvisionException;
 import com.google.inject.Singleton;
 import com.google.inject.internal.util.ImmutableMap;
+import com.google.inject.internal.util.Maps;
 import com.google.inject.name.Named;
 import com.google.inject.name.Names;
+
 import java.io.IOException;
+import java.util.Map;
 import java.util.concurrent.Callable;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
@@ -80,6 +85,52 @@
     executor.shutdown();
     executor.awaitTermination(2, TimeUnit.SECONDS);
   }
+  
+  public final void testWrongValueClasses() throws Exception {
+    Injector injector = Guice.createInjector(new ServletModule() {
+      @Override protected void configureServlets() {
+        bindConstant().annotatedWith(Names.named(SomeObject.INVALID)).to(SHOULDNEVERBESEEN);
+        bind(SomeObject.class).in(RequestScoped.class);
+      }
+    });
+    
+    OffRequestCallable offRequestCallable = injector.getInstance(OffRequestCallable.class);
+    try {
+      ServletScopes.scopeRequest(offRequestCallable,
+        ImmutableMap.<Key<?>, Object>of(Key.get(SomeObject.class), "Boo!"));
+      fail();
+    } catch(IllegalArgumentException iae) {
+      assertEquals("Value[Boo!] of type[java.lang.String] is not compatible with key[" + Key.get(SomeObject.class) + "]", iae.getMessage());
+    }
+  }
+  
+  public final void testNullReplacement() throws Exception {
+    Injector injector = Guice.createInjector(new ServletModule() {
+      @Override protected void configureServlets() {
+        bindConstant().annotatedWith(Names.named(SomeObject.INVALID)).to(SHOULDNEVERBESEEN);
+        bind(SomeObject.class).in(RequestScoped.class);
+      }
+    });
+    
+    Callable<SomeObject> callable = injector.getInstance(Caller.class);
+    try {
+      assertNotNull(callable.call());
+      fail();
+    } catch(ProvisionException pe) {
+      assertTrue(pe.getCause() instanceof OutOfScopeException);
+    }
+    
+    // First validate that an actual null entry gets replaced with the null sentinel.
+    Map<Key<?>, Object> map = Maps.newHashMap();
+    map.put(Key.get(SomeObject.class), null);
+    callable = ServletScopes.scopeRequest(injector.getInstance(Caller.class), map);
+    assertNull(callable.call());
+    
+    // Then validate that our nullObject entry also gets replaced.
+    callable = ServletScopes.scopeRequest(injector.getInstance(Caller.class), 
+        ImmutableMap.<Key<?>, Object>of(Key.get(SomeObject.class), ServletScopes.nullObject()));
+    assertNull(callable.call());
+  }
 
   @RequestScoped
   public static class SomeObject {
@@ -108,4 +159,12 @@
       return value;
     }
   }
+  
+  private static class Caller implements Callable<SomeObject> {
+    @Inject Provider<SomeObject> someObject;
+    
+    public SomeObject call() throws Exception {
+      return someObject.get();
+    }
+  }
 }