Adding more unit tests to RequestFactoryTest. Refactoring SimpleFoo to use a HashMap to store records instead of a singleton.

Review at http://gwt-code-reviews.appspot.com/931802

Review by: rjrjr@google.com

git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@8909 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/user/src/com/google/gwt/requestfactory/client/impl/ClientRequestHelper.java b/user/src/com/google/gwt/requestfactory/client/impl/ClientRequestHelper.java
index 0a1305e..dcd532a 100644
--- a/user/src/com/google/gwt/requestfactory/client/impl/ClientRequestHelper.java
+++ b/user/src/com/google/gwt/requestfactory/client/impl/ClientRequestHelper.java
@@ -43,7 +43,7 @@
       // TODO(jgw): Find a better way to do this. Occasionally a js-wrapped
       // string ends up in 'value', which breaks the json2.js implementation
       // of JSON.stringify().
-      this[key] = String(value);
+      this[key] = (value == null) ? null : String(value);
     }-*/;
 
     private native String toJsonString()/*-{
diff --git a/user/src/com/google/gwt/requestfactory/client/impl/ProxyImpl.java b/user/src/com/google/gwt/requestfactory/client/impl/ProxyImpl.java
index 4685f7b..fc277b4 100644
--- a/user/src/com/google/gwt/requestfactory/client/impl/ProxyImpl.java
+++ b/user/src/com/google/gwt/requestfactory/client/impl/ProxyImpl.java
@@ -1,12 +1,12 @@
 /*
  * Copyright 2010 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
@@ -96,7 +96,7 @@
    * <p>
    * If the ProxyImpl is mutable, any EntityProxies reachable from the return
    * value will have already been made mutable.
-   * 
+   *
    * @param <V> the type of the property's value
    * @param property the property to fetch
    * @return the value
@@ -131,13 +131,6 @@
     return jso.<V> get(propertyName, propertyType);
   }
 
-  public boolean hasChanged() {
-    if (deltaValueStore == null) {
-      return false;
-    }
-    return deltaValueStore.isChanged();
-  }
-
   @Override
   public int hashCode() {
     return stableId().hashCode() * 13
diff --git a/user/src/com/google/gwt/requestfactory/server/JsonRequestProcessor.java b/user/src/com/google/gwt/requestfactory/server/JsonRequestProcessor.java
index 654f9ba..f50787d 100644
--- a/user/src/com/google/gwt/requestfactory/server/JsonRequestProcessor.java
+++ b/user/src/com/google/gwt/requestfactory/server/JsonRequestProcessor.java
@@ -1,12 +1,12 @@
 /*
  * Copyright 2010 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
@@ -196,11 +196,11 @@
   /*
    * <li>Request comes in. Construct the involvedKeys, dvsDataMap and
    * beforeDataMap, using DVS and parameters.
-   * 
+   *
    * <li>Apply the DVS and construct the afterDvsDataMqp.
-   * 
+   *
    * <li>Invoke the method noted in the operation.
-   * 
+   *
    * <li>Find the changes that need to be sent back.
    */
   private final Map<EntityKey, Object> cachedEntityLookup = new HashMap<EntityKey, Object>();
@@ -243,6 +243,9 @@
       String parameterValue) throws SecurityException, JSONException,
       IllegalAccessException, InvocationTargetException, NoSuchMethodException,
       InstantiationException {
+    if (parameterValue == null) {
+      return null;
+    }
     Class<?> parameterType = null;
     if (genericParameterType instanceof Class<?>) {
       parameterType = (Class<?>) genericParameterType;
@@ -258,17 +261,15 @@
           if (collection != null) {
             JSONArray array = new JSONArray(parameterValue);
             for (int i = 0; i < array.length(); i++) {
+              String value = array.isNull(i) ? null : array.getString(i);
               collection.add(decodeParameterValue(
-                  pType.getActualTypeArguments()[0], array.getString(i)));
+                  pType.getActualTypeArguments()[0], value));
             }
             return collection;
           }
         }
       }
     }
-    if (parameterValue == null) {
-      return null;
-    }
     if (String.class == parameterType) {
       return parameterValue;
     }
@@ -328,7 +329,7 @@
       /*
        * TODO: 1. Don't resolve in this step, just get EntityKey. May need to
        * use DVS.
-       * 
+       *
        * 2. Merge the following and the object resolution code in getEntityKey.
        * 3. Update the involvedKeys set.
        */
@@ -342,6 +343,8 @@
               entityKey, dvsData.jsonObject, dvsData.writeOperation);
           return entityData.entityInstance;
         } else {
+          // TODO(rjrjr): This results in a ConcurrentModificationException.
+          // involvedKeys loops in constructAfterDvsDataMapAfterCallingSetters.
           involvedKeys.add(entityKey);
           return getEntityInstance(entityKey);
         }
@@ -354,6 +357,8 @@
         throw new IllegalArgumentException("Unknown service, unable to decode "
             + parameterValue);
       }
+      // TODO(rjrjr): This results in a ConcurrentModificationException.
+      // involvedKeys loops in constructAfterDvsDataMapAfterCallingSetters.
       involvedKeys.add(entityKey);
       return getEntityInstance(entityKey);
     }
@@ -434,7 +439,7 @@
   /**
    * Generate an ID for a new record. The default behavior is to return null and
    * let the data store generate the ID automatically.
-   * 
+   *
    * @param key the key of the record field
    * @return the ID of the new record, or null to auto generate
    */
@@ -579,7 +584,7 @@
 
   /**
    * Converts the returnValue of a 'get' method to a JSONArray.
-   * 
+   *
    * @param resultList object returned by a 'get' method, must be of type
    *          List<?>
    * @param entityKeyClass the class type of the contained value
@@ -641,7 +646,7 @@
   /**
    * Returns methodName corresponding to the propertyName that can be invoked on
    * an entity.
-   * 
+   *
    * Example: "userName" returns prefix + "UserName". "version" returns prefix +
    * "Version"
    */
@@ -702,7 +707,8 @@
     while (keys.hasNext()) {
       String key = keys.next().toString();
       if (key.startsWith(PARAM_TOKEN)) {
-        parameterMap.put(key, jsonObject.getString(key));
+        String value = jsonObject.isNull(key) ? null : jsonObject.getString(key);
+        parameterMap.put(key, value);
       }
     }
     return parameterMap;
@@ -779,7 +785,7 @@
   /**
    * Returns the property value, in the specified type, from the request object.
    * The value is put in the DataStore.
-   * 
+   *
    * @throws InstantiationException
    * @throws NoSuchMethodException
    * @throws InvocationTargetException
@@ -1026,7 +1032,7 @@
       relatedObjects.put(keyRef, null);
       Object jsonObject = getJsonObject(returnValue, propertyType,
           propertyContext);
-      if (jsonObject != JSONObject.NULL) { 
+      if (jsonObject != JSONObject.NULL) {
         // put real value
         relatedObjects.put(keyRef, (JSONObject) jsonObject);
       }
@@ -1097,7 +1103,7 @@
 
   /**
    * Constructs the beforeDataMap.
-   * 
+   *
    * <p>
    * Algorithm: go through the involvedKeys, and find the entityData
    * corresponding to each.
@@ -1418,7 +1424,7 @@
   /**
    * returns true if the property has been requested. TODO: use the properties
    * that should be coming with the request.
-   * 
+   *
    * @param p the field of entity ref
    * @param propertyContext the root of the current dotted property reference
    * @return has the property value been requested
diff --git a/user/src/com/google/gwt/requestfactory/shared/impl/RequestData.java b/user/src/com/google/gwt/requestfactory/shared/impl/RequestData.java
index f369ce8..5cbdbd1 100644
--- a/user/src/com/google/gwt/requestfactory/shared/impl/RequestData.java
+++ b/user/src/com/google/gwt/requestfactory/shared/impl/RequestData.java
@@ -127,7 +127,7 @@
 
   private String asJsonString(Object value) {
     if (value == null) {
-      return "null";
+      return null;
     }
 
     if (value instanceof Iterable<?>) {
@@ -140,14 +140,18 @@
         } else {
           first = false;
         }
-        toReturn.append(asJsonString(val));
+        if (val == null) {
+          toReturn.append("null");
+        } else {
+          toReturn.append("\"" + asJsonString(val) + "\"");
+        }
       }
       toReturn.append(']');
       return toReturn.toString();
     }
 
     if (value instanceof HasWireFormatId) {
-      return "\"" + ((HasWireFormatId) value).wireFormatId() + "\"";
+      return ((HasWireFormatId) value).wireFormatId();
     }
 
     /* 
diff --git a/user/test/com/google/gwt/requestfactory/client/EditorTest.java b/user/test/com/google/gwt/requestfactory/client/EditorTest.java
index 32161dd..46f1684 100644
--- a/user/test/com/google/gwt/requestfactory/client/EditorTest.java
+++ b/user/test/com/google/gwt/requestfactory/client/EditorTest.java
@@ -104,7 +104,7 @@
     final SimpleFooDriver driver = GWT.create(SimpleFooDriver.class);
     driver.initialize(req, editor);
 
-    req.simpleFooRequest().findSimpleFooById(0L).with(driver.getPaths()).fire(
+    req.simpleFooRequest().findSimpleFooById(1L).with(driver.getPaths()).fire(
         new Receiver<SimpleFooProxy>() {
           @Override
           public void onSuccess(SimpleFooProxy response) {
@@ -143,7 +143,7 @@
     assertEquals(Arrays.asList("barField.userName", "barField"),
         Arrays.asList(driver.getPaths()));
 
-    req.simpleFooRequest().findSimpleFooById(0L).with(driver.getPaths()).fire(
+    req.simpleFooRequest().findSimpleFooById(1L).with(driver.getPaths()).fire(
         new Receiver<SimpleFooProxy>() {
           @Override
           public void onSuccess(SimpleFooProxy response) {
@@ -190,7 +190,7 @@
     final SimpleFooDriver driver = GWT.create(SimpleFooDriver.class);
     driver.initialize(req, editor);
 
-    req.simpleFooRequest().findSimpleFooById(0L).with(driver.getPaths()).fire(
+    req.simpleFooRequest().findSimpleFooById(1L).with(driver.getPaths()).fire(
         new Receiver<SimpleFooProxy>() {
           @Override
           public void onSuccess(SimpleFooProxy response) {
diff --git a/user/test/com/google/gwt/requestfactory/client/RequestFactoryTest.java b/user/test/com/google/gwt/requestfactory/client/RequestFactoryTest.java
index ebb77d9..07ac4dc 100644
--- a/user/test/com/google/gwt/requestfactory/client/RequestFactoryTest.java
+++ b/user/test/com/google/gwt/requestfactory/client/RequestFactoryTest.java
@@ -1,12 +1,12 @@
 /*
  * Copyright 2010 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
@@ -113,7 +113,7 @@
     public void onSuccess(T response) {
       /*
        * Make sure your class path includes:
-       * 
+       *
        * tools/lib/apache/log4j/log4j-1.2.16.jar
        * tools/lib/hibernate/validator/hibernate-validator-4.1.0.Final.jar
        * tools/lib/slf4j/slf4j-api/slf4j-api-1.6.1.jar
@@ -184,6 +184,38 @@
     return "com.google.gwt.requestfactory.RequestFactorySuite";
   }
 
+  /**
+   * Test that we can commit child objects.
+   *
+   * TODO: Causes a ConcurrentModificationException
+   */
+  public void disabledTestCascadingCommit() {
+    delayTestFinish(5000);
+    final SimpleFooProxy foo = req.create(SimpleFooProxy.class);
+    final SimpleBarProxy bar0 = req.create(SimpleBarProxy.class);
+    final SimpleBarProxy bar1 = req.create(SimpleBarProxy.class);
+    List<SimpleBarProxy> bars = new ArrayList<SimpleBarProxy>();
+    bars.add(bar0);
+    bars.add(bar1);
+
+    final SimpleFooEventHandler<SimpleBarProxy> handler = new SimpleFooEventHandler<SimpleBarProxy>();
+    EntityProxyChange.registerForProxyType(req.getEventBus(),
+        SimpleBarProxy.class, handler);
+
+    Request<SimpleFooProxy> request = req.simpleFooRequest().persistCascadingAndReturnSelf(foo);
+    SimpleFooProxy editFoo = request.edit(foo);
+    editFoo.setOneToManyField(bars);
+    request.fire(new Receiver<SimpleFooProxy>() {
+      @Override
+      public void onSuccess(SimpleFooProxy response) {
+        assertFalse(((ProxyImpl) response).unpersisted());
+        assertEquals(2, handler.persistEventCount); // two bars persisted.
+        assertEquals(2, handler.totalEventCount);
+        finishTestAndReset();
+      }
+    });
+  }
+
   public void testClassToken() {
     String token = req.getHistoryToken(SimpleFooProxy.class);
     assertEquals(SimpleFooProxy.class, req.getProxyClass(token));
@@ -383,7 +415,7 @@
         new Receiver<List<SimpleFooProxy>>() {
           @Override
           public void onSuccess(List<SimpleFooProxy> response) {
-            assertEquals(1, response.size());
+            assertEquals(2, response.size());
             for (SimpleFooProxy foo : response) {
               assertNotNull(foo.stableId());
               assertEquals("FOO", foo.getBarField().getUserName());
@@ -451,6 +483,125 @@
   }
 
   /**
+   * Test that a null value can be sent in a request.
+   */
+  public void testNullListRequest() {
+    delayTestFinish(5000);
+    final Request<Void> fooReq = req.simpleFooRequest().receiveNullList(null);
+    fooReq.fire(new Receiver<Void>() {
+      @Override
+      public void onSuccess(Void v) {
+        finishTestAndReset();
+      }
+    });
+  }
+
+  /**
+   * Test that a null value can be sent in a request.
+   */
+  public void disabledTestNullSimpleFooRequest() {
+    delayTestFinish(5000);
+    final Request<Void> fooReq = req.simpleFooRequest().receiveNullSimpleFoo(null);
+    fooReq.fire(new Receiver<Void>() {
+      @Override
+      public void onSuccess(Void v) {
+        finishTestAndReset();
+      }
+    });
+  }
+
+  /**
+   * Test that a null value can be sent to an instance method.
+   */
+  public void testNullStringInstanceRequest() {
+    delayTestFinish(5000);
+
+    // Get a valid proxy entity.
+    req.simpleFooRequest().findSimpleFooById(999L).fire(new Receiver<SimpleFooProxy>() {
+      @Override
+      public void onSuccess(SimpleFooProxy response) {
+        final Request<Void> fooReq = req.simpleFooRequest().receiveNull(response, null);
+        fooReq.fire(new Receiver<Void>() {
+          @Override
+          public void onSuccess(Void v) {
+            finishTestAndReset();
+          }
+        });
+      }
+    });
+  }
+
+  /**
+   * Test that a null value can be sent in a request.
+   */
+  public void testNullStringRequest() {
+    delayTestFinish(5000);
+    final Request<Void> fooReq = req.simpleFooRequest().receiveNullString(null);
+    fooReq.fire(new Receiver<Void>() {
+      @Override
+      public void onSuccess(Void v) {
+        finishTestAndReset();
+      }
+    });
+  }
+
+  /**
+   * Test that a null value can be sent within a list of entities.
+   */
+  public void testNullValueInEntityListRequest() {
+    delayTestFinish(5000);
+
+    // Get a valid proxy entity.
+    req.simpleFooRequest().findSimpleFooById(999L).fire(new Receiver<SimpleFooProxy>() {
+      @Override
+      public void onSuccess(SimpleFooProxy response) {
+        List<SimpleFooProxy> list = new ArrayList<SimpleFooProxy>();
+        list.add(response); // non-null
+        list.add(null); // null
+        final Request<Void> fooReq = req.simpleFooRequest().receiveNullValueInEntityList(list);
+        fooReq.fire(new Receiver<Void>() {
+          @Override
+          public void onSuccess(Void v) {
+            finishTestAndReset();
+          }
+        });
+      }
+    });
+  }
+
+  /**
+   * Test that a null value can be sent within a list of objects.
+   */
+  public void testNullValueInIntegerListRequest() {
+    delayTestFinish(5000);
+    List<Integer> list = Arrays.asList(new Integer[]{1, 2, null});
+    final Request<Void> fooReq = req.simpleFooRequest().receiveNullValueInIntegerList(
+        list);
+    fooReq.fire(new Receiver<Void>() {
+      @Override
+      public void onSuccess(Void v) {
+        finishTestAndReset();
+      }
+    });
+  }
+
+  /**
+   * Test that a null value can be sent within a list of strings.
+   */
+  public void testNullValueInStringListRequest() {
+    delayTestFinish(5000);
+    List<String> list = Arrays.asList(new String[]{"nonnull", "null", null});
+    final Request<Void> fooReq = req.simpleFooRequest().receiveNullValueInStringList(
+        list);
+    fooReq.fire(new Receiver<Void>() {
+      @Override
+      public void onSuccess(Void v) {
+        finishTestAndReset();
+      }
+    });
+  }
+
+  /**
    * Ensures that a service method can respond with a null value.
    */
   public void testNullListResult() {
@@ -501,7 +652,7 @@
               @Override
               public void onSuccess(Long response) {
                 assertCannotFire(mutateRequest);
-                assertEquals(new Long(1L), response);
+                assertEquals(new Long(2L), response);
                 assertEquals(2, handler.updateEventCount);
                 assertEquals(2, handler.totalEventCount);
 
@@ -531,6 +682,63 @@
         });
   }
 
+  /**
+   * Test that removing a parent entity and implicitly removing the child sends
+   * an event to the client that the child was removed.
+   * 
+   * TODO(rjrjr): Should cascading deletes be detected?  
+   */
+  public void disableTestMethodWithSideEffectDeleteChild() {
+    delayTestFinish(5000);
+
+    // Persist bar.
+    final SimpleBarProxy bar = req.create(SimpleBarProxy.class);
+    req.simpleBarRequest().persistAndReturnSelf(bar).fire(new Receiver<SimpleBarProxy>() {
+      @Override
+      public void onSuccess(SimpleBarProxy persistentBar) {
+        // Persist foo with bar as a child.
+        SimpleFooProxy foo = req.create(SimpleFooProxy.class);
+        final Request<SimpleFooProxy> persistRequest =
+            req.simpleFooRequest().persistAndReturnSelf(foo);
+        foo = persistRequest.edit(foo);
+        foo.setUserName("John");
+        foo.setBarField(bar);
+        persistRequest.fire(new Receiver<SimpleFooProxy>() {
+          @Override
+          public void onSuccess(SimpleFooProxy persistentFoo) {
+            // Handle changes to SimpleFooProxy.
+            final SimpleFooEventHandler<SimpleFooProxy> fooHandler =
+                new SimpleFooEventHandler<SimpleFooProxy>();
+            EntityProxyChange.registerForProxyType(
+                req.getEventBus(), SimpleFooProxy.class, fooHandler);
+
+            // Handle changes to SimpleBarProxy.
+            final SimpleFooEventHandler<SimpleBarProxy> barHandler =
+                new SimpleFooEventHandler<SimpleBarProxy>();
+            EntityProxyChange.registerForProxyType(
+                req.getEventBus(), SimpleBarProxy.class, barHandler);
+
+            // Delete bar.
+            final Request<Void> deleteRequest = req.simpleFooRequest().deleteBar(persistentFoo);
+            SimpleFooProxy editable = deleteRequest.edit(persistentFoo);
+            editable.setBarField(bar);
+            deleteRequest.fire(new Receiver<Void>() {
+              @Override
+              public void onSuccess(Void response) {
+                assertEquals(1, fooHandler.updateEventCount); // set bar to null
+                assertEquals(1, fooHandler.totalEventCount);
+
+                assertEquals(1, barHandler.deleteEventCount); // deleted bar
+                assertEquals(1, barHandler.totalEventCount);
+                finishTestAndReset();
+              }
+            });
+          }
+        });
+      }
+    });
+  }
+
   /*
    * TODO: all these tests should check the final values. It will be easy when
    * we have better persistence than the singleton pattern.
@@ -709,7 +917,7 @@
             fooReq2.fire(new Receiver<Void>() {
               @Override
               public void onSuccess(Void response) {
-                req.simpleFooRequest().findSimpleFooById(999L).with(
+                req.simpleFooRequest().findSimpleFooById(persistedFoo.getId()).with(
                     "barField.userName").fire(new Receiver<SimpleFooProxy>() {
                   @Override
                   public void onSuccess(SimpleFooProxy finalFooProxy) {
diff --git a/user/test/com/google/gwt/requestfactory/server/JsonRequestProcessorTest.java b/user/test/com/google/gwt/requestfactory/server/JsonRequestProcessorTest.java
index b6ad267..8f98ea1 100644
--- a/user/test/com/google/gwt/requestfactory/server/JsonRequestProcessorTest.java
+++ b/user/test/com/google/gwt/requestfactory/server/JsonRequestProcessorTest.java
@@ -130,7 +130,7 @@
     JSONObject result = getResultFromServer(foo);
 
     // check modified fields and no violations
-    SimpleFoo fooResult = SimpleFoo.getSingleton();
+    SimpleFoo fooResult = SimpleFoo.findSimpleFooById(999L);
     JSONArray updateArray = result.getJSONObject("sideEffects").getJSONArray(
         "UPDATE");
     assertEquals(1, updateArray.length());
@@ -152,7 +152,7 @@
     JSONObject foo = fetchVerifyAndGetInitialObject();
 
     // change the value on the server behind the back
-    SimpleFoo fooResult = SimpleFoo.getSingleton();
+    SimpleFoo fooResult = SimpleFoo.findSimpleFooById(999L);
     fooResult.setUserName("JSC");
     fooResult.setIntId(45);
 
@@ -163,7 +163,7 @@
 
     // check modified fields and no violations
     assertFalse(result.getJSONObject("sideEffects").has("UPDATE"));
-    fooResult = SimpleFoo.getSingleton();
+    fooResult = SimpleFoo.findSimpleFooById(999L);
     assertEquals(45, (int) fooResult.getIntId());
     assertEquals("JSC", fooResult.getUserName());
   }
@@ -174,7 +174,7 @@
     JSONObject foo = fetchVerifyAndGetInitialObject();
 
     // change some fields but don't change other fields.
-    SimpleFoo fooResult = SimpleFoo.getSingleton();
+    SimpleFoo fooResult = SimpleFoo.findSimpleFooById(999L);
     fooResult.setUserName("JSC");
     fooResult.setIntId(45);
     fooResult.setNullField(null);
@@ -191,7 +191,7 @@
 
     // check modified fields and no violations
     assertTrue(result.getJSONObject("sideEffects").has("UPDATE"));
-    fooResult = SimpleFoo.getSingleton();
+    fooResult = SimpleFoo.findSimpleFooById(999L);
     assertEquals(45, (int) fooResult.getIntId());
     assertNull(fooResult.getUserName());
     assertEquals("test", fooResult.getNullField());
@@ -205,7 +205,7 @@
     JSONObject foo = fetchVerifyAndGetInitialObject();
 
     // change some fields but don't change other fields.
-    SimpleFoo fooResult = SimpleFoo.getSingleton();
+    SimpleFoo fooResult = SimpleFoo.findSimpleFooById(999L);
     fooResult.setUserName("JSC");
     fooResult.setIntId(45);
     foo.put("userName", "JSC");
@@ -218,7 +218,7 @@
 
     // check modified fields and no violations
     assertTrue(result.getJSONObject("sideEffects").has("UPDATE"));
-    fooResult = SimpleFoo.getSingleton();
+    fooResult = SimpleFoo.findSimpleFooById(999L);
     assertEquals(45, (int) fooResult.getIntId());
     assertEquals("JSC", fooResult.getUserName());
     assertEquals(newTime, fooResult.getCreated().getTime());
diff --git a/user/test/com/google/gwt/requestfactory/server/SimpleBar.java b/user/test/com/google/gwt/requestfactory/server/SimpleBar.java
index f6e2fd6..fd47a98 100644
--- a/user/test/com/google/gwt/requestfactory/server/SimpleBar.java
+++ b/user/test/com/google/gwt/requestfactory/server/SimpleBar.java
@@ -1,12 +1,12 @@
 /*
  * Copyright 2010 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
@@ -62,7 +62,7 @@
    */
   public static SimpleBar findSimpleBarById(String id) {
     SimpleBar toReturn = get().get(id);
-    return toReturn.findFails ? null : toReturn;
+    return (toReturn == null || toReturn.findFails) ? null : toReturn;
   }
 
   @SuppressWarnings("unchecked")
@@ -131,6 +131,10 @@
     userName = "FOO";
   }
 
+  public void delete() {
+    get().remove(getId());
+  }
+
   public Boolean getFindFails() {
     return findFails;
   }
diff --git a/user/test/com/google/gwt/requestfactory/server/SimpleFoo.java b/user/test/com/google/gwt/requestfactory/server/SimpleFoo.java
index f596d25..4756f89 100644
--- a/user/test/com/google/gwt/requestfactory/server/SimpleFoo.java
+++ b/user/test/com/google/gwt/requestfactory/server/SimpleFoo.java
@@ -1,12 +1,12 @@
 /*
  * Copyright 2010 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
@@ -21,10 +21,11 @@
 import java.math.BigDecimal;
 import java.math.BigInteger;
 import java.util.ArrayList;
-import java.util.Collections;
 import java.util.Date;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 
 import javax.servlet.http.HttpServletRequest;
@@ -37,12 +38,12 @@
   /**
    * DO NOT USE THIS UGLY HACK DIRECTLY! Call {@link #get} instead.
    */
-  private static SimpleFoo jreTestSingleton = new SimpleFoo();
+  private static Map<Long, SimpleFoo> jreTestSingleton = new HashMap<Long, SimpleFoo>();
 
   private static Long nextId = 1L;
 
   public static Long countSimpleFoo() {
-    return 1L;
+    return (long) get().size();
   }
 
   public static SimpleFoo echo(SimpleFoo simpleFoo) {
@@ -55,7 +56,7 @@
   }
 
   public static List<SimpleFoo> findAll() {
-    return Collections.singletonList(get());
+    return new ArrayList<SimpleFoo>(get().values());
   }
 
   public static SimpleFoo findSimpleFoo(Long id) {
@@ -63,11 +64,11 @@
   }
 
   public static SimpleFoo findSimpleFooById(Long id) {
-    get().setId(id);
-    return get();
+    return get().get(id);
   }
 
-  public static synchronized SimpleFoo get() {
+  @SuppressWarnings("unchecked")
+  public static synchronized Map<Long, SimpleFoo> get() {
     HttpServletRequest req = RequestFactoryServlet.getThreadLocalRequest();
     if (req == null) {
       // May be in a JRE test case, use the singleton
@@ -78,7 +79,7 @@
        * that doesn't allow any requests to be processed unless they're
        * associated with an existing session.
        */
-      SimpleFoo value = (SimpleFoo) req.getSession().getAttribute(
+      Map<Long, SimpleFoo> value = (Map<Long, SimpleFoo>) req.getSession().getAttribute(
           SimpleFoo.class.getCanonicalName());
       if (value == null) {
         value = reset();
@@ -103,10 +104,6 @@
     return list;
   }
 
-  public static SimpleFoo getSingleton() {
-    return get();
-  }
-
   public static Boolean processBooleanList(List<Boolean> values) {
     return values.get(0);
   }
@@ -123,8 +120,79 @@
     return string;
   }
 
-  public static synchronized SimpleFoo reset() {
-    SimpleFoo instance = new SimpleFoo();
+  public static void receiveNullList(List<SimpleFoo> value) {
+    if (value != null) {
+      throw new IllegalArgumentException(
+          "Expected value to be null. Actual value: \"" + value + "\"");
+    }
+  }
+
+  public static void receiveNullSimpleFoo(SimpleFoo value) {
+    if (value != null) {
+      throw new IllegalArgumentException(
+          "Expected value to be null. Actual value: \"" + value + "\"");
+    }
+  }
+
+  public static void receiveNullString(String value) {
+    if (value != null) {
+      throw new IllegalArgumentException(
+          "Expected value to be null. Actual value: \"" + value + "\"");
+    }
+  }
+
+  public static void receiveNullValueInEntityList(List<SimpleFoo> list) {
+    if (list == null) {
+      throw new IllegalArgumentException("Expected list to be non null.");
+    } else if (list.size() != 2) {
+      throw new IllegalArgumentException("Expected list to contain two items.");
+    } else if (list.get(0) == null) {
+      throw new IllegalArgumentException("Expected list.get(0) to return non null.");
+    } else if (list.get(1) != null) {
+      throw new IllegalArgumentException(
+          "Expected list.get(1) to return null. Actual: " + list.get(1));
+    }
+  }
+
+  public static void receiveNullValueInIntegerList(List<Integer> list) {
+    if (list == null) {
+      throw new IllegalArgumentException("Expected list to be non null.");
+    } else if (list.size() != 3) {
+      throw new IllegalArgumentException("Expected list to contain three items.");
+    } else if (list.get(0) == null || list.get(1) == null) {
+      throw new IllegalArgumentException("Expected list.get(0)/get(1) to return non null.");
+    } else if (list.get(2) != null) {
+      throw new IllegalArgumentException(
+          "Expected list.get(2) to return null. Actual: \"" + list.get(2) + "\"");
+    }
+  }
+
+  public static void receiveNullValueInStringList(List<String> list) {
+    if (list == null) {
+      throw new IllegalArgumentException("Expected list to be non null.");
+    } else if (list.size() != 3) {
+      throw new IllegalArgumentException("Expected list to contain three items.");
+    } else if (list.get(0) == null || list.get(1) == null) {
+      throw new IllegalArgumentException("Expected list.get(0)/get(1) to return non null.");
+    } else if (list.get(2) != null) {
+      throw new IllegalArgumentException(
+          "Expected list.get(2) to return null. Actual: \"" + list.get(2) + "\"");
+    }
+  }
+
+  public static synchronized Map<Long, SimpleFoo> reset() {
+    Map<Long, SimpleFoo> instance = new HashMap<Long, SimpleFoo>();
+    // fixtures
+    SimpleFoo s1 = new SimpleFoo();
+    s1.setId(1L);
+    s1.isNew = false;
+    instance.put(s1.getId(), s1);
+
+    SimpleFoo s2 = new SimpleFoo();
+    s2.setId(999L);
+    s2.isNew = false;
+    instance.put(s2.getId(), s2);
+
     HttpServletRequest req = RequestFactoryServlet.getThreadLocalRequest();
     if (req == null) {
       jreTestSingleton = instance;
@@ -156,6 +224,7 @@
 
   @Id
   private Long id = 1L;
+  private boolean isNew = true;
 
   @Size(min = 3, max = 30)
   private String userName;
@@ -220,8 +289,15 @@
   }
 
   public Long countSimpleFooWithUserNameSideEffect() {
-    get().setUserName(userName);
-    return 1L;
+    findSimpleFoo(1L).setUserName(userName);
+    return countSimpleFoo();
+  }
+
+  public void deleteBar() {
+    if (barField != null) {
+      barField.delete();
+    }
+    barField = null;
   }
 
   public SimpleBar getBarField() {
@@ -357,7 +433,11 @@
   }
 
   public void persist() {
-    setId(nextId++);
+    if (isNew) {
+      setId(nextId++);
+      isNew = false;
+      get().put(getId(), this);
+    }
   }
 
   public SimpleFoo persistAndReturnSelf() {
@@ -365,6 +445,11 @@
     return this;
   }
 
+  public SimpleFoo persistCascadingAndReturnSelf() {
+    persistCascadingAndReturnSelfImpl(new HashSet<SimpleFoo>());
+    return this;
+  }
+
   public String processList(List<SimpleFoo> values) {
     String result = "";
     for (SimpleFoo n : values) {
@@ -373,6 +458,13 @@
     return result;
   }
 
+  public void receiveNull(String value) {
+    if (value != null) {
+      throw new IllegalArgumentException(
+          "Expected value to be null. Actual value: \"" + value + "\"");
+    }
+  }
+
   public void setBarField(SimpleBar barField) {
     this.barField = barField;
   }
@@ -515,4 +607,55 @@
     }
     return sum;
   }
+
+  /**
+   * Persist this entity and all child entities. This method can handle loops.
+   * 
+   * @param processed the entities that have been processed
+   */
+  private void persistCascadingAndReturnSelfImpl(Set<SimpleFoo> processed) {
+    if (processed.contains(this)) {
+      return;
+    }
+
+    // Persist this entity.
+    processed.add(this);
+    persist();
+
+    // Persist SimpleBar children.
+    // We don't need to keep track of the processed SimpleBars because persist()
+    // is a no-op if the SimpleBar has already been persisted.
+    if (barField != null) {
+      barField.persist();
+    }
+    if (barNullField != null) {
+      barNullField.persist();
+    }
+    if (oneToManySetField != null) {
+      for (SimpleBar child : oneToManySetField) {
+        if (child != null) {
+          child.persist();
+        }
+      }
+    }
+    if (oneToManyField != null) {
+      for (SimpleBar child : oneToManyField) {
+        if (child != null) {
+          child.persist();
+        }
+      }
+    }
+
+    // Persist SimpleFoo children.
+    if (fooField != null) {
+      fooField.persistCascadingAndReturnSelfImpl(processed);
+    }
+    if (selfOneToManyField != null) {
+      for (SimpleFoo child : selfOneToManyField) {
+        if (child != null) {
+          child.persistCascadingAndReturnSelfImpl(processed);
+        }
+      }
+    }
+  }
 }
diff --git a/user/test/com/google/gwt/requestfactory/shared/SimpleFooRequest.java b/user/test/com/google/gwt/requestfactory/shared/SimpleFooRequest.java
index 3ccdf7d..b58c8ea 100644
--- a/user/test/com/google/gwt/requestfactory/shared/SimpleFooRequest.java
+++ b/user/test/com/google/gwt/requestfactory/shared/SimpleFooRequest.java
@@ -1,12 +1,12 @@
 /*
  * Copyright 2010 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
@@ -29,6 +29,9 @@
   @Instance
   Request<Long> countSimpleFooWithUserNameSideEffect(SimpleFooProxy proxy);
 
+  @Instance
+  Request<Void> deleteBar(SimpleFooProxy proxy);
+
   Request<SimpleFooProxy> echo(SimpleFooProxy proxy);
 
   Request<SimpleFooProxy> echoComplex(SimpleFooProxy fooProxy, SimpleBarProxy barProxy);
@@ -46,10 +49,13 @@
 
   @Instance
   Request<Void> persist(SimpleFooProxy proxy);
-  
+
   @Instance
   Request<SimpleFooProxy> persistAndReturnSelf(SimpleFooProxy proxy);
 
+  @Instance
+  Request<SimpleFooProxy> persistCascadingAndReturnSelf(SimpleFooProxy proxy);
+
   Request<Boolean> processBooleanList(List<Boolean> values);
 
   Request<Date> processDateList(List<Date> values);
@@ -60,7 +66,22 @@
 
   @Instance
   Request<String> processList(SimpleFooProxy instance, List<SimpleFooProxy> values);
-  
+
+  @Instance
+  Request<Void> receiveNull(SimpleFooProxy instance, String value);
+
+  Request<Void> receiveNullList(List<SimpleFooProxy> value);
+
+  Request<Void> receiveNullSimpleFoo(SimpleFooProxy value);
+
+  Request<Void> receiveNullString(String value);
+
+  Request<Void> receiveNullValueInEntityList(List<SimpleFooProxy> value);
+
+  Request<Void> receiveNullValueInIntegerList(List<Integer> value);
+
+  Request<Void> receiveNullValueInStringList(List<String> value);
+
   Request<Void> reset();
 
   Request<List<SimpleFooProxy>> returnNullList();