Implements * globbing for RequestFactory with(), the simpler half of
the proposal in
http://code.google.com/p/google-web-toolkit/issues/detail?id=6697

Also fixes bugs exposed in using null members in collections

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

Review by: robertvawter@google.com

git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@10560 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/user/src/com/google/web/bindery/requestfactory/server/Resolver.java b/user/src/com/google/web/bindery/requestfactory/server/Resolver.java
index 4751d8d..22e7a31 100644
--- a/user/src/com/google/web/bindery/requestfactory/server/Resolver.java
+++ b/user/src/com/google/web/bindery/requestfactory/server/Resolver.java
@@ -198,6 +198,11 @@
      * not been previously resolved for the next batch of work.
      */
     public void addPaths(String prefix, Collection<String> requestedPaths) {
+      if (clientObject == null) {
+        // No point trying to follow paths past a null value
+        return;
+      }
+      
       // Identity comparison intentional
       if (toResolve == EMPTY) {
         toResolve = new TreeSet<String>();
@@ -207,6 +212,8 @@
       for (String path : requestedPaths) {
         if (path.startsWith(prefix)) {
           toResolve.add(path.substring(prefixLength));
+        } else if (path.startsWith("*.")) {
+          toResolve.add(path.substring("*.".length()));
         }
       }
       toResolve.removeAll(resolved);
@@ -310,7 +317,14 @@
    * references.
    */
   static boolean matchesPropertyRef(Set<String> propertyRefs, String newPrefix) {
-    return propertyRefs.contains(newPrefix.replaceAll("\\[\\d+\\]", ""));
+    /* 
+     * Match all fields for a wildcard
+     * 
+     * Also, remove list index suffixes. Not actually used, was in anticipation
+     * of OGNL type schemes. That said, Editor will slip in such things.
+     */
+    return propertyRefs.contains("*")
+        || propertyRefs.contains(newPrefix.replaceAll("\\[\\d+\\]", ""));
   }
 
   /**
@@ -567,7 +581,7 @@
    */
   private Resolution resolveClientValue(Object domainValue, Type clientType) {
     if (domainValue == null) {
-      return null;
+      return new Resolution(null);
     }
 
     boolean anyType = clientType == null;
diff --git a/user/src/com/google/web/bindery/requestfactory/shared/impl/ProxySerializerImpl.java b/user/src/com/google/web/bindery/requestfactory/shared/impl/ProxySerializerImpl.java
index 2b523cf..d07e43a 100644
--- a/user/src/com/google/web/bindery/requestfactory/shared/impl/ProxySerializerImpl.java
+++ b/user/src/com/google/web/bindery/requestfactory/shared/impl/ProxySerializerImpl.java
@@ -93,6 +93,10 @@
   }
 
   public String serialize(BaseProxy rootObject) {
+    if (rootObject == null) {
+      return "null";
+    }
+    
     final AutoBean<? extends BaseProxy> root = AutoBeanUtils.getAutoBean(rootObject);
     if (root == null) {
       // Unexpected, some kind of foreign implementation of the BaseProxy?
diff --git a/user/test/com/google/web/bindery/requestfactory/gwt/client/RequestFactoryPolymorphicTest.java b/user/test/com/google/web/bindery/requestfactory/gwt/client/RequestFactoryPolymorphicTest.java
index ffd546d..ba57c5a 100644
--- a/user/test/com/google/web/bindery/requestfactory/gwt/client/RequestFactoryPolymorphicTest.java
+++ b/user/test/com/google/web/bindery/requestfactory/gwt/client/RequestFactoryPolymorphicTest.java
@@ -55,6 +55,8 @@
 
     protected int id = idCount++;
 
+    private A nextA;
+
     public String getA() {
       return a;
     }
@@ -63,6 +65,13 @@
       return id;
     }
 
+    public A getNextA() {
+      if (nextA == null) {
+        nextA = new A();
+      }
+      return nextA;
+    }
+
     public int getVersion() {
       return 0;
     }
@@ -77,6 +86,8 @@
    */
   @ProxyFor(A.class)
   public interface AProxy extends EntityProxy, HasA {
+    AProxy getNextA();
+
     // Mix in getA() from HasA
     void setA(String value);
   }
@@ -146,10 +157,20 @@
 
     private String c = "c";
 
+    private C nextC;
+
     public String getC() {
       return c;
     }
 
+    public C getNextC() {
+      if (nextC == null) {
+        nextC = new C();
+      }
+
+      return nextC;
+    }
+
     public void setC(String value) {
       c = value;
     }
@@ -161,6 +182,8 @@
   public interface C1Proxy extends B1Proxy {
     String getC();
 
+    C1Proxy getNextC();
+
     void setC(String value);
   }
 
@@ -607,6 +630,50 @@
     return "com.google.web.bindery.requestfactory.gwt.RequestFactorySuite";
   }
 
+  public void testChain() {
+    delayTestFinish(TEST_DELAY);
+    Context ctx = factory.ctx();
+    ctx.CasA().with("nextA.nextA").fire(new Receiver<AProxy>() {
+      @Override
+      public void onSuccess(AProxy response) {
+        new CastAndCheckReceiver(AProxy.class).onSuccess(response.getNextA().getNextA());
+        assertNull(response.getNextA().getNextA().getNextA());
+        assertNull(((C1Proxy) response).getNextC());
+        finishTest();
+      }
+    });
+  }
+
+  public void testChainWithExtras() {
+    delayTestFinish(TEST_DELAY);
+    Context ctx = factory.ctx();
+    ctx.CasA().with("nextC.nextC").fire(new Receiver<AProxy>() {
+      @Override
+      public void onSuccess(AProxy response) {
+        assertNull(response.getNextA());
+        C1Proxy cast = (C1Proxy) response;
+        new CastAndCheckReceiver(C1Proxy.class).onSuccess(cast.getNextC().getNextC());
+        assertNull(cast.getNextC().getNextC().getNextC());
+        finishTest();
+      }
+    });
+  }
+
+  public void testChainWithWildcards() {
+    delayTestFinish(TEST_DELAY);
+    Context ctx = factory.ctx();
+    ctx.CasA().with("*.*").fire(new Receiver<AProxy>() {
+      @Override
+      public void onSuccess(AProxy response) {
+        new CastAndCheckReceiver(AProxy.class).onSuccess(response.getNextA().getNextA());
+        C1Proxy cast = (C1Proxy) response;
+        new CastAndCheckReceiver(C1Proxy.class).onSuccess(cast.getNextC().getNextC());
+        assertNull(cast.getNextC().getNextC().getNextC());
+        finishTest();
+      }
+    });
+  }
+
   public void testCreation() {
     delayTestFinish(TEST_DELAY);
     Context ctx = factory.ctx();
diff --git a/user/test/com/google/web/bindery/requestfactory/gwt/client/RequestFactoryTest.java b/user/test/com/google/web/bindery/requestfactory/gwt/client/RequestFactoryTest.java
index f9935a4..01da1c3 100644
--- a/user/test/com/google/web/bindery/requestfactory/gwt/client/RequestFactoryTest.java
+++ b/user/test/com/google/web/bindery/requestfactory/gwt/client/RequestFactoryTest.java
@@ -700,6 +700,23 @@
           }
         });
   }
+ 
+  public void testForwardReferenceWildcardDecode() {
+    delayTestFinish(DELAY_TEST_FINISH);
+    simpleFooRequest().getTripletReference().with("selfOneToManyField.*.fooField")
+        .fire(new Receiver<SimpleFooProxy>() {
+          @Override
+          public void onSuccess(SimpleFooProxy response) {
+            response = checkSerialization(response);
+            assertNotNull(response.getSelfOneToManyField().get(0));
+            assertNotNull(response.getSelfOneToManyField().get(0).getSelfOneToManyField());
+            assertNotNull(response.getSelfOneToManyField().get(0).getSelfOneToManyField().get(0));
+            assertNotNull(response.getSelfOneToManyField().get(0).getSelfOneToManyField().get(0)
+                .getFooField());
+            finishTestAndReset();
+          }
+        });
+  }
 
   public void testGetEventBus() {
     assertEquals(eventBus, req.getEventBus());
@@ -979,6 +996,19 @@
     delayTestFinish(DELAY_TEST_FINISH);
     simpleFooRequest().returnNullSimpleFoo().fire(new NullReceiver());
   }
+  
+  public void testNullEntityFieldResult() {
+    delayTestFinish(DELAY_TEST_FINISH);
+    simpleFooRequest().getSimpleFooWithNullRelationship().with("fooField.fooField.fooField").fire(
+        new Receiver<SimpleFooProxy>() {
+          @Override
+          public void onSuccess(SimpleFooProxy v) {
+            checkSerialization(v);
+            assertNull(v.getFooField());
+            finishTestAndReset();
+          }
+        });
+  }
 
   /**
    * Test that a null value can be sent in a request.
@@ -1084,6 +1114,63 @@
       }
     });
   }
+  
+  public void testNullValueInEntityListResponse() {
+    delayTestFinish(DELAY_TEST_FINISH);
+    final Request<SimpleFooProxy> fooReq =
+      req.simpleFooRequest().getNullInEntityList().with("selfOneToManyField");
+    fooReq.fire(new Receiver<SimpleFooProxy>() {
+      @Override
+      public void onSuccess(SimpleFooProxy v) {
+        List<SimpleFooProxy> manyFoos = v.getSelfOneToManyField();
+        assertEquals(3, manyFoos.size());
+        
+        assertNotNull(manyFoos.get(0));
+        assertNull(manyFoos.get(1));
+        assertNotNull(manyFoos.get(2));
+        
+        finishTestAndReset();
+      }
+    });
+  }
+
+  public void testNullValueInEntityListResponseWithWildcard() {
+    delayTestFinish(DELAY_TEST_FINISH);
+    final Request<SimpleFooProxy> fooReq =
+      req.simpleFooRequest().getNullInEntityList().with("selfOneToManyField.*.fooField");
+    fooReq.fire(new Receiver<SimpleFooProxy>() {
+      @Override
+      public void onSuccess(SimpleFooProxy foo0) {
+        List<SimpleFooProxy> manyFoos = foo0.getSelfOneToManyField();
+        assertEquals(3, manyFoos.size());
+        
+        assertSame(foo0, manyFoos.get(0).getSelfOneToManyField().get(0));
+        assertNull(manyFoos.get(1));
+        assertSame(foo0, manyFoos.get(2).getSelfOneToManyField().get(0));
+        assertSame(foo0, manyFoos.get(2).getFooField().getFooField());
+        
+        finishTestAndReset();
+      }
+    });
+  }
+
+  public void testNullValueInEntityListResponseWithLongResolvePaths() {
+    delayTestFinish(DELAY_TEST_FINISH);
+    final Request<SimpleFooProxy> fooReq =
+        req.simpleFooRequest().getNullInEntityList().with("selfOneToManyField.selfOneToManyField.selfOneToManyField");
+    fooReq.fire(new Receiver<SimpleFooProxy>() {
+      @Override
+      public void onSuccess(SimpleFooProxy v) {
+        assertEquals(3, v.getSelfOneToManyField().size());
+        
+        assertNotNull(v.getSelfOneToManyField().get(0));
+        assertNull(v.getSelfOneToManyField().get(1));
+        assertNotNull(v.getSelfOneToManyField().get(2));
+        
+        finishTestAndReset();
+      }
+    });
+  }
 
   /**
    * Test that a null value can be sent within a list of objects.
@@ -2044,6 +2131,31 @@
           }
         });
   }
+  
+  public void testPropertyRefsOnWildcardChain() {
+    delayTestFinish(DELAY_TEST_FINISH);
+    final Request<SimpleFooProxy> fooReq =
+      req.simpleFooRequest().getLongChain().with("fooField.*.*.fooField");
+    fooReq.fire(new Receiver<SimpleFooProxy>() {
+      @Override
+      public void onSuccess(SimpleFooProxy foo0) {
+        assertNull(foo0.getSelfOneToManyField()); // didn't ask for it
+        
+        SimpleFooProxy foo1 = foo0.getFooField(); // "fooField
+        SimpleFooProxy foo2 = foo1.getFooField(); //          .*
+        SimpleFooProxy foo3 = foo2.getFooField(); //            .*
+        SimpleFooProxy foo4 = foo3.getFooField(); //              .fooField"
+        SimpleFooProxy foo5 = foo4.getFooField(); 
+        
+        assertNotNull(foo1);
+        assertNotNull(foo2);
+        assertNotNull(foo3);
+        assertNotNull(foo4);
+        assertNull(foo5);
+        finishTestAndReset();
+      }
+    });
+  }
 
   public void testPropertyRefsOnSameObjectReturnedTwice() {
     delayTestFinish(DELAY_TEST_FINISH);
diff --git a/user/test/com/google/web/bindery/requestfactory/server/SimpleFoo.java b/user/test/com/google/web/bindery/requestfactory/server/SimpleFoo.java
index d3cceaa..fc20600 100644
--- a/user/test/com/google/web/bindery/requestfactory/server/SimpleFoo.java
+++ b/user/test/com/google/web/bindery/requestfactory/server/SimpleFoo.java
@@ -124,6 +124,53 @@
     foo3.persist();
     return Arrays.asList(foo1, foo2, foo3);
   }
+  
+  public static SimpleFoo getLongChain() {
+    SimpleFoo foo0 = new SimpleFoo();
+    SimpleFoo foo1 = new SimpleFoo();
+    SimpleFoo foo2 = new SimpleFoo();
+    SimpleFoo foo3 = new SimpleFoo();
+    SimpleFoo foo4 = new SimpleFoo();
+    SimpleFoo foo5 = new SimpleFoo();
+    
+    foo0.setSelfOneToManyField(Arrays.asList(foo1, foo2));
+    foo0.setFooField(foo1);
+    foo1.setFooField(foo2);
+    foo2.setFooField(foo3);
+    foo3.setFooField(foo4);
+    foo4.setFooField(foo5);
+    foo5.setFooField(foo5);
+    
+    foo0.persist();
+    foo1.persist();
+    foo2.persist();
+    foo3.persist();
+    foo4.persist();
+    foo5.persist();
+    
+    return foo0;
+  }
+  
+  public static SimpleFoo getNullInEntityList() {
+    SimpleFoo foo0 = new SimpleFoo();
+    SimpleFoo foo1 = new SimpleFoo();
+    SimpleFoo foo2 = new SimpleFoo();
+    SimpleFoo foo2FooField = new SimpleFoo();
+
+    foo0.setSelfOneToManyField(Arrays.asList(foo1, null, foo2));
+
+    foo1.setSelfOneToManyField(Arrays.asList(foo0));
+
+    foo2.setSelfOneToManyField(Arrays.asList(foo0));
+    foo2.setFooField(foo2FooField);
+    foo2FooField.setFooField(foo0);
+    
+    foo0.persist();
+    foo1.persist();
+    foo2.persist();
+    foo2FooField.persist();
+    return foo0;
+  }
 
   public static List<Integer> getNumberList() {
     ArrayList<Integer> list = new ArrayList<Integer>();
@@ -141,6 +188,12 @@
     return list;
   }
 
+  public static SimpleFoo getSimpleFooWithNullRelationship() {
+    SimpleFoo foo = new SimpleFoo();
+    foo.persist();
+    return foo;
+  }
+
   /**
    * This tests that the server detects and disallows the use of persisted
    * objects with a null version property.
diff --git a/user/test/com/google/web/bindery/requestfactory/shared/SimpleFooRequest.java b/user/test/com/google/web/bindery/requestfactory/shared/SimpleFooRequest.java
index 3c3290e..6183e0b 100644
--- a/user/test/com/google/web/bindery/requestfactory/shared/SimpleFooRequest.java
+++ b/user/test/com/google/web/bindery/requestfactory/shared/SimpleFooRequest.java
@@ -49,10 +49,16 @@
 
   Request<List<SimpleFooProxy>> getFlattenedTripletReference();
 
-  Request<List<Integer>> getNumberList();
+  Request<SimpleFooProxy> getLongChain();
 
+  Request<SimpleFooProxy> getNullInEntityList();
+
+  Request<List<Integer>> getNumberList();
+  
   Request<Set<Integer>> getNumberSet();
 
+  Request<SimpleFooProxy> getSimpleFooWithNullRelationship();
+
   Request<SimpleFooProxy> getSimpleFooWithNullVersion();
 
   Request<SimpleFooProxy> getSimpleFooWithSubPropertyCollection();