Support and validation of null values in JSON Requests

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

Review by: cromwellian@google.com

git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@8776 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/user/src/com/google/gwt/requestfactory/server/JsonRequestProcessor.java b/user/src/com/google/gwt/requestfactory/server/JsonRequestProcessor.java
index a2f6f84..21f0c21 100644
--- a/user/src/com/google/gwt/requestfactory/server/JsonRequestProcessor.java
+++ b/user/src/com/google/gwt/requestfactory/server/JsonRequestProcessor.java
@@ -183,7 +183,8 @@
    * @throws InvocationTargetException 
    * @throws IllegalAccessException 
    * @throws JSONException 
-   * @throws SecurityException 
+   * @throws SecurityException
+   * @throws NullPointerException 
    */
   public Object decodeParameterValue(Type genericParameterType,
       String parameterValue) throws SecurityException, JSONException, IllegalAccessException, InvocationTargetException, NoSuchMethodException, InstantiationException {
@@ -197,6 +198,9 @@
         parameterType = (Class<?>) pType.getActualTypeArguments()[0];
       }
     }
+    if (parameterValue == null) {
+      return null;
+    }
     if (String.class == parameterType) {
       return parameterValue;
     }
@@ -283,7 +287,7 @@
 
   public Object encodePropertyValue(Object value) {
     if (value == null) {
-      return value;
+      return null;
     }
     Class<?> type = value.getClass();
     if (Boolean.class == type) {
@@ -385,7 +389,7 @@
           }
         } else {
           Object propertyValue = null;
-          if (EntityProxy.class.isAssignableFrom(dtoType)) {
+          if (!recordObject.isNull(key) && EntityProxy.class.isAssignableFrom(dtoType)) {
             EntityKey propKey = getEntityKey(recordObject.getString(key));
             Object cacheValue = cachedEntityLookup.get(propKey);
             if (cachedEntityLookup.containsKey(propKey)) {
@@ -599,7 +603,8 @@
    */
   public Object getPropertyValueFromRequest(JSONObject recordObject, String key,
       Class<?> propertyType) throws JSONException, SecurityException, IllegalAccessException, InvocationTargetException, NoSuchMethodException, InstantiationException {
-    return decodeParameterValue(propertyType, recordObject.get(key).toString());
+    return decodeParameterValue(propertyType,
+        recordObject.isNull(key) ? null : recordObject.get(key).toString());
   }
 
   @SuppressWarnings("unchecked")
@@ -745,8 +750,8 @@
     Iterator<?> keyIterator = before.keys();
     while (keyIterator.hasNext()) {
       String key = keyIterator.next().toString();
-      Object beforeValue = before.get(key);
-      Object afterValue = after.get(key);
+      Object beforeValue = before.isNull(key) ? null : before.get(key);
+      Object afterValue = after.isNull(key) ? null : after.get(key);
       if (beforeValue == null) {
         if (afterValue == null) {
           continue;
diff --git a/user/test/com/google/gwt/requestfactory/server/JsonRequestProcessorTest.java b/user/test/com/google/gwt/requestfactory/server/JsonRequestProcessorTest.java
index a46b852..9cdb252 100644
--- a/user/test/com/google/gwt/requestfactory/server/JsonRequestProcessorTest.java
+++ b/user/test/com/google/gwt/requestfactory/server/JsonRequestProcessorTest.java
@@ -16,6 +16,7 @@
 package com.google.gwt.requestfactory.server;
 
 import com.google.gwt.requestfactory.shared.RequestData;
+import com.google.gwt.requestfactory.shared.SimpleBarProxy;
 import com.google.gwt.requestfactory.shared.SimpleEnum;
 import com.google.gwt.requestfactory.shared.SimpleFooProxy;
 import com.google.gwt.requestfactory.shared.WriteOperation;
@@ -40,7 +41,7 @@
     BAR, BAZ
   }
 
-  private JsonRequestProcessor requestProcessor;;
+  private JsonRequestProcessor requestProcessor;
 
   @Override
   public void setUp() {
@@ -73,6 +74,20 @@
         dec.toBigInteger().toString());
     // enums
     assertTypeAndValueEquals(Foo.class, Foo.BAR, "" + Foo.BAR.ordinal());
+    
+    // nulls
+    assertTypeAndValueEquals(String.class, null, null);
+    assertTypeAndValueEquals(Integer.class, null, null);
+    assertTypeAndValueEquals(Byte.class, null, null);
+    assertTypeAndValueEquals(Short.class, null, null);
+    assertTypeAndValueEquals(Float.class, null, null);
+    assertTypeAndValueEquals(Double.class, null, null);
+    assertTypeAndValueEquals(Long.class, null, null);
+    assertTypeAndValueEquals(Boolean.class, null, null);
+    assertTypeAndValueEquals(Date.class, null, null);
+    assertTypeAndValueEquals(BigDecimal.class, null, null);
+    assertTypeAndValueEquals(BigInteger.class, null, null);
+    assertTypeAndValueEquals(Foo.class, null, null);
   }
 
   public void testEncodePropertyValue() {
@@ -91,102 +106,119 @@
     assertEncodedType(Double.class, Foo.BAR);
     assertEncodedType(Boolean.class, true);
     assertEncodedType(Boolean.class, false);
+    // nulls stay null
+    assertNull(requestProcessor.encodePropertyValue(null));
   }
 
-  public void testEndToEnd() {
+  public void testEndToEnd() throws Exception {
     com.google.gwt.requestfactory.server.SimpleFoo.reset();
-    try {
-      // fetch object
-      JSONObject foo = fetchVerifyAndGetInitialObject();
+    // fetch object
+    JSONObject foo = fetchVerifyAndGetInitialObject();
 
-      // modify fields and sync
-      foo.put("intId", 45);
-      foo.put("userName", "JSC");
-      foo.put("longField", "" + 9L);
-      foo.put("enumField", SimpleEnum.BAR.ordinal());
-      foo.put("boolField", false);
-      Date now = new Date();
-      foo.put("created", "" + now.getTime());
+    // modify fields and sync
+    foo.put("intId", 45);
+    foo.put("userName", "JSC");
+    foo.put("longField", "" + 9L);
+    foo.put("enumField", SimpleEnum.BAR.ordinal());
+    foo.put("boolField", false);
+    Date now = new Date();
+    foo.put("created", "" + now.getTime());
 
-      JSONObject result = getResultFromServer(foo);
+    JSONObject result = getResultFromServer(foo);
 
-      // check modified fields and no violations
-      SimpleFoo fooResult = SimpleFoo.getSingleton();
-      JSONArray updateArray = result.getJSONObject("sideEffects").getJSONArray(
-          "UPDATE");
-      assertEquals(1, updateArray.length());
-      assertEquals(1, updateArray.getJSONObject(0).length());
-      assertTrue(updateArray.getJSONObject(0).has("id"));
-      assertFalse(updateArray.getJSONObject(0).has("violations"));
-      assertEquals(45, (int) fooResult.getIntId());
-      assertEquals("JSC", fooResult.getUserName());
-      assertEquals(now, fooResult.getCreated());
-      assertEquals(9L, (long) fooResult.getLongField());
-      assertEquals(com.google.gwt.requestfactory.shared.SimpleEnum.BAR,
-          fooResult.getEnumField());
-      assertEquals(false, (boolean) fooResult.getBoolField());
-
-    } catch (Exception e) {
-      e.printStackTrace();
-      fail(e.toString());
-    }
+    // check modified fields and no violations
+    SimpleFoo fooResult = SimpleFoo.getSingleton();
+    JSONArray updateArray = result.getJSONObject("sideEffects").getJSONArray(
+        "UPDATE");
+    assertEquals(1, updateArray.length());
+    assertEquals(1, updateArray.getJSONObject(0).length());
+    assertTrue(updateArray.getJSONObject(0).has("id"));
+    assertFalse(updateArray.getJSONObject(0).has("violations"));
+    assertEquals(45, (int) fooResult.getIntId());
+    assertEquals("JSC", fooResult.getUserName());
+    assertEquals(now, fooResult.getCreated());
+    assertEquals(9L, (long) fooResult.getLongField());
+    assertEquals(com.google.gwt.requestfactory.shared.SimpleEnum.BAR,
+        fooResult.getEnumField());
+    assertEquals(false, (boolean) fooResult.getBoolField());
   }
 
-  public void testEndToEndSmartDiff_NoChange() {
+  public void testEndToEndSmartDiff_NoChange() throws Exception {
     com.google.gwt.requestfactory.server.SimpleFoo.reset();
-    try {
-      // fetch object
-      JSONObject foo = fetchVerifyAndGetInitialObject();
+    // fetch object
+    JSONObject foo = fetchVerifyAndGetInitialObject();
 
-      // change the value on the server behind the back
-      SimpleFoo fooResult = SimpleFoo.getSingleton();
-      fooResult.setUserName("JSC");
-      fooResult.setIntId(45);
+    // change the value on the server behind the back
+    SimpleFoo fooResult = SimpleFoo.getSingleton();
+    fooResult.setUserName("JSC");
+    fooResult.setIntId(45);
 
-      // modify fields and sync -- there should be no change on the server.
-      foo.put("intId", 45);
-      foo.put("userName", "JSC");
-      JSONObject result = getResultFromServer(foo);
+    // modify fields and sync -- there should be no change on the server.
+    foo.put("intId", 45);
+    foo.put("userName", "JSC");
+    JSONObject result = getResultFromServer(foo);
 
-      // check modified fields and no violations
-      assertFalse(result.getJSONObject("sideEffects").has("UPDATE"));
-      fooResult = SimpleFoo.getSingleton();
-      assertEquals(45, (int) fooResult.getIntId());
-      assertEquals("JSC", fooResult.getUserName());
-    } catch (Exception e) {
-      e.printStackTrace();
-      fail(e.toString());
-    }
+    // check modified fields and no violations
+    assertFalse(result.getJSONObject("sideEffects").has("UPDATE"));
+    fooResult = SimpleFoo.getSingleton();
+    assertEquals(45, (int) fooResult.getIntId());
+    assertEquals("JSC", fooResult.getUserName());
   }
 
-  public void testEndToEndSmartDiff_SomeChange() {
+  public void testEndToEndSmartDiff_SomeChangeWithNull() throws Exception {
     com.google.gwt.requestfactory.server.SimpleFoo.reset();
-    try {
-      // fetch object
-      JSONObject foo = fetchVerifyAndGetInitialObject();
+    // fetch object
+    JSONObject foo = fetchVerifyAndGetInitialObject();
 
-      // change some fields but don't change other fields.
-      SimpleFoo fooResult = SimpleFoo.getSingleton();
-      fooResult.setUserName("JSC");
-      fooResult.setIntId(45);
-      foo.put("userName", "JSC");
-      foo.put("intId", 45);
-      Date now = new Date();
-      long newTime = now.getTime() + 10000;
-      foo.put("created", "" + newTime);
+    // change some fields but don't change other fields.
+    SimpleFoo fooResult = SimpleFoo.getSingleton();
+    fooResult.setUserName("JSC");
+    fooResult.setIntId(45);
+    fooResult.setNullField(null);
+    fooResult.setBarField(SimpleBar.getSingleton());
+    fooResult.setBarNullField(null);
+    foo.put("userName", JSONObject.NULL);
+    foo.put("intId", 45);
+    foo.put("nullField", "test");
+    foo.put("barNullField", SimpleBar.getSingleton().getId() 
+        + "-IS-" + SimpleBarProxy.class.getName());
+    foo.put("barField", JSONObject.NULL);
 
-      JSONObject result = getResultFromServer(foo);
+    JSONObject result = getResultFromServer(foo);
 
-      // check modified fields and no violations
-      assertTrue(result.getJSONObject("sideEffects").has("UPDATE"));
-      fooResult = SimpleFoo.getSingleton();
-      assertEquals(45, (int) fooResult.getIntId());
-      assertEquals("JSC", fooResult.getUserName());
-      assertEquals(newTime, fooResult.getCreated().getTime());
-    } catch (Exception e) {
-      e.printStackTrace();
-      fail(e.toString());
-    }
+    // check modified fields and no violations
+    assertTrue(result.getJSONObject("sideEffects").has("UPDATE"));
+    fooResult = SimpleFoo.getSingleton();
+    assertEquals(45, (int) fooResult.getIntId());
+    assertNull(fooResult.getUserName());
+    assertEquals("test", fooResult.getNullField());
+    assertEquals(SimpleBar.getSingleton(), fooResult.getBarNullField());
+    assertNull(fooResult.getBarField());
+  }
+
+  public void testEndToEndSmartDiff_SomeChange() throws Exception {
+    com.google.gwt.requestfactory.server.SimpleFoo.reset();
+    // fetch object
+    JSONObject foo = fetchVerifyAndGetInitialObject();
+
+    // change some fields but don't change other fields.
+    SimpleFoo fooResult = SimpleFoo.getSingleton();
+    fooResult.setUserName("JSC");
+    fooResult.setIntId(45);
+    foo.put("userName", "JSC");
+    foo.put("intId", 45);
+    Date now = new Date();
+    long newTime = now.getTime() + 10000;
+    foo.put("created", "" + newTime);
+
+    JSONObject result = getResultFromServer(foo);
+
+    // check modified fields and no violations
+    assertTrue(result.getJSONObject("sideEffects").has("UPDATE"));
+    fooResult = SimpleFoo.getSingleton();
+    assertEquals(45, (int) fooResult.getIntId());
+    assertEquals("JSC", fooResult.getUserName());
+    assertEquals(newTime, fooResult.getCreated().getTime());
   }
 
   private void assertEncodedType(Class<?> expected, Object value) {
@@ -199,7 +231,9 @@
       JSONException, IllegalAccessException, InvocationTargetException,
       NoSuchMethodException, InstantiationException {
     Object val = requestProcessor.decodeParameterValue(expectedType, paramValue);
-    assertEquals(expectedType, val.getClass());
+    if (val != null) {
+      assertEquals(expectedType, val.getClass());
+    }
     assertEquals(expectedValue, val);
   }
 
diff --git a/user/test/com/google/gwt/requestfactory/server/SimpleFoo.java b/user/test/com/google/gwt/requestfactory/server/SimpleFoo.java
index 86cbbc6..ae2ded1 100644
--- a/user/test/com/google/gwt/requestfactory/server/SimpleFoo.java
+++ b/user/test/com/google/gwt/requestfactory/server/SimpleFoo.java
@@ -97,6 +97,9 @@
 
   private SimpleBar barField;
   private SimpleFoo fooField;
+  
+  private String nullField;
+  private SimpleBar barNullField;
 
   public SimpleFoo() {
     intId = 42;
@@ -107,6 +110,8 @@
     created = new Date();
     barField = SimpleBar.getSingleton();
     boolField = true;
+    nullField = null;
+    barNullField = null;
   }
 
   public Long countSimpleFooWithUserNameSideEffect() {
@@ -118,6 +123,10 @@
     return barField;
   }
 
+  public SimpleBar getBarNullField() {
+    return barNullField;
+  }
+
   /**
    * @return the bigDecimalField
    */
@@ -188,6 +197,10 @@
     return longField;
   }
 
+  public String getNullField() {
+    return nullField;
+  }
+
   /**
    * @return the otherBoolField
    */
@@ -230,6 +243,10 @@
     this.barField = barField;
   }
 
+  public void setBarNullField(SimpleBar barNullField) {
+    this.barNullField = barNullField;
+  }
+
   /**
    * @param bigDecimalField the bigDecimalField to set
    */
@@ -300,6 +317,10 @@
     this.longField = longField;
   }
 
+  public void setNullField(String nullField) {
+    this.nullField = nullField;
+  }
+
   /**
    * @param otherBoolField the otherBoolField to set
    */
diff --git a/user/test/com/google/gwt/requestfactory/shared/SimpleFooProxy.java b/user/test/com/google/gwt/requestfactory/shared/SimpleFooProxy.java
index 03af72d..b3851bf 100644
--- a/user/test/com/google/gwt/requestfactory/shared/SimpleFooProxy.java
+++ b/user/test/com/google/gwt/requestfactory/shared/SimpleFooProxy.java
@@ -58,11 +58,19 @@
   Property<SimpleBarProxy> barField = new Property<SimpleBarProxy>("barField",
       SimpleBarProxy.class);
 
+  Property<SimpleBarProxy> barNullField = new Property<SimpleBarProxy>("barNullField",
+      SimpleBarProxy.class);
+
   Property<SimpleFooProxy> fooField = new Property<SimpleFooProxy>("fooField",
       SimpleFooProxy.class);
 
+  Property<String> nullField = new Property<String>("nullField", "A nullable field",
+      String.class);
+  
   SimpleBarProxy getBarField();
   
+  SimpleBarProxy getBarNullField();
+  
   BigDecimal getBigDecimalField();
   
   BigInteger getBigIntField();
@@ -87,6 +95,8 @@
   
   Long getLongField();
   
+  String getNullField();
+  
   Boolean getOtherBoolField();
   
   String getPassword();
@@ -97,6 +107,8 @@
   
   void setBarField(SimpleBarProxy barField);
 
+  void setBarNullField(SimpleBarProxy barNullField);
+
   void setBigDecimalField(BigDecimal d);
 
   void setBigIntField(BigInteger i);
@@ -119,6 +131,8 @@
 
   void setLongField(Long longField);
 
+  void setNullField(String nullField);
+
   void setOtherBoolField(Boolean boolField);
 
   void setPassword(String password);