Integrate r10346 into GWT 2.4 branch.


git-svn-id: https://google-web-toolkit.googlecode.com/svn/releases/2.4@10368 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 38e5a97..4751d8d 100644
--- a/user/src/com/google/web/bindery/requestfactory/server/Resolver.java
+++ b/user/src/com/google/web/bindery/requestfactory/server/Resolver.java
@@ -22,7 +22,6 @@
 import com.google.web.bindery.autobean.shared.ValueCodex;
 import com.google.web.bindery.autobean.vm.impl.TypeUtils;
 import com.google.web.bindery.requestfactory.shared.BaseProxy;
-import com.google.web.bindery.requestfactory.shared.EntityProxy;
 import com.google.web.bindery.requestfactory.shared.EntityProxyId;
 import com.google.web.bindery.requestfactory.shared.impl.Constants;
 import com.google.web.bindery.requestfactory.shared.impl.SimpleProxyId;
@@ -34,9 +33,11 @@
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
+import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.SortedSet;
 import java.util.TreeSet;
 
 /**
@@ -87,6 +88,163 @@
   }
 
   /**
+   * Copies values and references from a domain object to a client object. This
+   * type does not descend into referenced objects.
+   */
+  private class PropertyResolver extends AutoBeanVisitor {
+    private final Object domainEntity;
+    private final boolean isOwnerValueProxy;
+    private final boolean needsSimpleValues;
+    private final Set<String> propertyRefs;
+
+    private PropertyResolver(Resolution resolution) {
+      ResolutionKey key = resolution.getResolutionKey();
+      this.domainEntity = key.getDomainObject();
+      this.isOwnerValueProxy = state.isValueType(TypeUtils.ensureBaseType(key.requestedType));
+      this.needsSimpleValues = resolution.needsSimpleValues();
+      this.propertyRefs = resolution.takeWork();
+    }
+
+    @Override
+    public boolean visitReferenceProperty(String propertyName, AutoBean<?> value,
+        PropertyContext ctx) {
+      /*
+       * Send the property if the enclosing type is a ValueProxy, if the owner
+       * requested the property, or if the property is a list of values.
+       */
+      Class<?> elementType =
+          ctx instanceof CollectionPropertyContext ? ((CollectionPropertyContext) ctx)
+              .getElementType() : null;
+      boolean shouldSend =
+          isOwnerValueProxy || matchesPropertyRef(propertyRefs, propertyName)
+              || elementType != null && ValueCodex.canDecode(elementType);
+
+      if (!shouldSend) {
+        return false;
+      }
+
+      // Call the getter
+      Object domainValue = service.getProperty(domainEntity, propertyName);
+      if (domainValue == null) {
+        return false;
+      }
+
+      // Turn the domain object into something usable on the client side
+      Type type;
+      if (elementType == null) {
+        type = ctx.getType();
+      } else {
+        type = new CollectionType(ctx.getType(), elementType);
+      }
+      Resolution resolution = resolveClientValue(domainValue, type);
+      addPathsToResolution(resolution, propertyName, propertyRefs);
+      ctx.set(resolution.getClientObject());
+      return false;
+    }
+
+    @Override
+    public boolean visitValueProperty(String propertyName, Object value, PropertyContext ctx) {
+      /*
+       * Only call the getter for simple values once since they're not
+       * explicitly enumerated.
+       */
+      if (needsSimpleValues) {
+        // Limit unrequested value properties?
+        value = service.getProperty(domainEntity, propertyName);
+        ctx.set(value);
+      }
+      return false;
+    }
+  }
+
+  /**
+   * Tracks the state of resolving a single client object.
+   */
+  private static class Resolution {
+    /**
+     * There's no Collections shortcut for this.
+     */
+    private static final SortedSet<String> EMPTY = Collections
+        .unmodifiableSortedSet(new TreeSet<String>());
+
+    /**
+     * The client object.
+     */
+    private final Object clientObject;
+
+    /**
+     * A one-shot flag for {@link #hasWork()} to ensure that simple properties
+     * will be resolved, even when there's no requested property set.
+     */
+    private boolean needsSimpleValues;
+    private SortedSet<String> toResolve = EMPTY;
+    private final SortedSet<String> resolved = new TreeSet<String>();
+    private final ResolutionKey key;
+
+    public Resolution(Object simpleValue) {
+      assert !(simpleValue instanceof Resolution);
+      this.clientObject = simpleValue;
+      this.key = null;
+    }
+
+    public Resolution(ResolutionKey key, BaseProxy clientObject) {
+      this.clientObject = clientObject;
+      this.key = key;
+      needsSimpleValues = true;
+    }
+
+    /**
+     * Removes the prefix from each requested path and enqueues paths that have
+     * not been previously resolved for the next batch of work.
+     */
+    public void addPaths(String prefix, Collection<String> requestedPaths) {
+      // Identity comparison intentional
+      if (toResolve == EMPTY) {
+        toResolve = new TreeSet<String>();
+      }
+      prefix = prefix.isEmpty() ? prefix : (prefix + ".");
+      int prefixLength = prefix.length();
+      for (String path : requestedPaths) {
+        if (path.startsWith(prefix)) {
+          toResolve.add(path.substring(prefixLength));
+        }
+      }
+      toResolve.removeAll(resolved);
+      if (toResolve.isEmpty()) {
+        toResolve = EMPTY;
+      }
+    }
+
+    public Object getClientObject() {
+      return clientObject;
+    }
+
+    public ResolutionKey getResolutionKey() {
+      return key;
+    }
+
+    public boolean hasWork() {
+      return needsSimpleValues || !toResolve.isEmpty();
+    }
+
+    public boolean needsSimpleValues() {
+      return needsSimpleValues;
+    }
+
+    /**
+     * Returns client-object-relative reference paths that should be further
+     * resolved.
+     */
+    public SortedSet<String> takeWork() {
+      needsSimpleValues = false;
+      SortedSet<String> toReturn = toResolve;
+      resolved.addAll(toReturn);
+      toResolve = EMPTY;
+      return toReturn;
+    }
+  }
+
+  /**
    * Used to map the objects being resolved and its API slice to the client-side
    * value. This handles the case where a domain object is returned to the
    * client mapped to two proxies of differing types.
@@ -118,6 +276,10 @@
       return true;
     }
 
+    public Object getDomainObject() {
+      return domainObject;
+    }
+
     @Override
     public int hashCode() {
       return hashCode;
@@ -163,12 +325,40 @@
   }
 
   /**
+   * Expand the property references in an InvocationMessage into a
+   * fully-expanded list of properties. For example, <code>[foo.bar.baz]</code>
+   * will be converted into <code>[foo, foo.bar, foo.bar.baz]</code>.
+   */
+  private static Set<String> expandPropertyRefs(Set<String> refs) {
+    if (refs == null) {
+      return Collections.emptySet();
+    }
+
+    Set<String> toReturn = new TreeSet<String>();
+    for (String raw : refs) {
+      for (int idx = raw.length(); idx >= 0; idx = raw.lastIndexOf('.', idx - 1)) {
+        toReturn.add(raw.substring(0, idx));
+      }
+    }
+    return toReturn;
+  }
+
+  /**
+   * Maps proxy instances to the Resolution objects.
+   */
+  private Map<BaseProxy, Resolution> clientObjectsToResolutions =
+      new HashMap<BaseProxy, Resolution>();
+  /**
    * Maps domain values to client values. This map prevents cycles in the object
    * graph from causing infinite recursion.
    */
-  private final Map<ResolutionKey, Object> resolved = new HashMap<ResolutionKey, Object>();
+  private final Map<ResolutionKey, Resolution> resolved = new HashMap<ResolutionKey, Resolution>();
   private final ServiceLayer service;
   private final RequestState state;
+  /**
+   * Contains Resolutions with path references that have not yet been resolved.
+   */
+  private Set<Resolution> toProcess = new LinkedHashSet<Resolution>();
   private int syntheticId;
 
   /**
@@ -190,7 +380,23 @@
    * @param propertyRefs the property references requested by the client
    */
   public Object resolveClientValue(Object domainValue, Type assignableTo, Set<String> propertyRefs) {
-    return resolveClientValue(domainValue, assignableTo, getPropertyRefs(propertyRefs), "");
+    Resolution toReturn = resolveClientValue(domainValue, assignableTo);
+    if (toReturn == null) {
+      return null;
+    }
+    addPathsToResolution(toReturn, "", expandPropertyRefs(propertyRefs));
+    while (!toProcess.isEmpty()) {
+      List<Resolution> working = new ArrayList<Resolution>(toProcess);
+      toProcess.clear();
+      for (Resolution resolution : working) {
+        if (resolution.hasWork()) {
+          AutoBean<BaseProxy> bean =
+              AutoBeanUtils.getAutoBean((BaseProxy) resolution.getClientObject());
+          bean.accept(new PropertyResolver(resolution));
+        }
+      }
+    }
+    return toReturn.getClientObject();
   }
 
   /**
@@ -229,30 +435,69 @@
   }
 
   /**
-   * Expand the property references in an InvocationMessage into a
-   * fully-expanded list of properties. For example, <code>[foo.bar.baz]</code>
-   * will be converted into <code>[foo, foo.bar, foo.bar.baz]</code>.
+   * Calls {@link Resolution#addPaths(String, Collection)}, enqueuing
+   * {@code key} if {@link Resolution#hasWork()} returns {@code true}. This
+   * method will also expand paths on the members of Collections.
    */
-  private Set<String> getPropertyRefs(Set<String> refs) {
-    if (refs == null) {
-      return Collections.emptySet();
+  private void addPathsToResolution(Resolution resolution, String prefix, Set<String> propertyRefs) {
+    if (propertyRefs.isEmpty()) {
+      // No work to do
+      return;
     }
-
-    Set<String> toReturn = new TreeSet<String>();
-    for (String raw : refs) {
-      for (int idx = raw.length(); idx >= 0; idx = raw.lastIndexOf('.', idx - 1)) {
-        toReturn.add(raw.substring(0, idx));
+    if (resolution.getResolutionKey() != null) {
+      // Working on a proxied type
+      assert resolution.getClientObject() instanceof BaseProxy : "Expecting BaseProxy, found "
+          + resolution.getClientObject().getClass().getCanonicalName();
+      resolution.addPaths(prefix, propertyRefs);
+      if (resolution.hasWork()) {
+        toProcess.add(resolution);
       }
+      return;
     }
-    return toReturn;
+    if (resolution.getClientObject() instanceof Collection) {
+      // Pass the paths onto the Resolutions for the contained elements
+      Collection<?> collection = (Collection<?>) resolution.getClientObject();
+      for (Object obj : collection) {
+        Resolution subResolution = clientObjectsToResolutions.get(obj);
+        // subResolution will be null for List<Integer>, etc.
+        if (subResolution != null) {
+          addPathsToResolution(subResolution, prefix, propertyRefs);
+        }
+      }
+      return;
+    }
+    assert false : "Should not add paths to client type "
+        + resolution.getClientObject().getClass().getCanonicalName();
   }
 
   /**
-   * Converts a domain entity into an EntityProxy that will be sent to the
-   * client.
+   * Creates a resolution for a simple value.
    */
-  private <T extends BaseProxy> T resolveClientProxy(final Object domainEntity, Class<T> proxyType,
-      final Set<String> propertyRefs, ResolutionKey key, final String prefix) {
+  private Resolution makeResolution(Object domainValue) {
+    assert !state.isEntityType(domainValue.getClass())
+        && !state.isValueType(domainValue.getClass()) : "Not a simple value type";
+    return new Resolution(domainValue);
+  }
+
+  /**
+   * Create or reuse a Resolution for a proxy object.
+   */
+  private Resolution makeResolution(ResolutionKey key, BaseProxy clientObject) {
+    Resolution resolution = resolved.get(key);
+    if (resolution == null) {
+      resolution = new Resolution(key, clientObject);
+      clientObjectsToResolutions.put(clientObject, resolution);
+      toProcess.add(resolution);
+      resolved.put(key, resolution);
+    }
+    return resolution;
+  }
+
+  /**
+   * Creates a proxy instance held by a Resolution for a given domain type.
+   */
+  private <T extends BaseProxy> Resolution resolveClientProxy(Object domainEntity,
+      Class<T> proxyType, ResolutionKey key) {
     if (domainEntity == null) {
       return null;
     }
@@ -260,7 +505,6 @@
     SimpleProxyId<? extends BaseProxy> id = state.getStableId(domainEntity);
 
     boolean isEntityProxy = state.isEntityType(proxyType);
-    final boolean isOwnerValueProxy = state.isValueType(proxyType);
     Object domainVersion;
 
     // Create the id or update an ephemeral id by calculating its address
@@ -305,7 +549,6 @@
 
     @SuppressWarnings("unchecked")
     AutoBean<T> bean = (AutoBean<T>) state.getBeanForPayload(id, domainEntity);
-    resolved.put(key, bean.as());
     bean.setTag(Constants.IN_RESPONSE, true);
     if (domainVersion != null) {
       Splittable flatVersion = state.flatten(domainVersion);
@@ -313,80 +556,31 @@
           .getPayload()));
     }
 
-    bean.accept(new AutoBeanVisitor() {
-
-      @Override
-      public boolean visitReferenceProperty(String propertyName, AutoBean<?> value,
-          PropertyContext ctx) {
-        // Does the user care about the property?
-        String newPrefix = (prefix.length() > 0 ? (prefix + ".") : "") + propertyName;
-
-        /*
-         * Send the property if the enclosing type is a ValueProxy, if the owner
-         * requested the property, or if the property is a list of values.
-         */
-        Class<?> elementType =
-            ctx instanceof CollectionPropertyContext ? ((CollectionPropertyContext) ctx)
-                .getElementType() : null;
-        boolean shouldSend =
-            isOwnerValueProxy || matchesPropertyRef(propertyRefs, newPrefix) || elementType != null
-                && ValueCodex.canDecode(elementType);
-
-        if (!shouldSend) {
-          return false;
-        }
-
-        // Call the getter
-        Object domainValue = service.getProperty(domainEntity, propertyName);
-        if (domainValue == null) {
-          return false;
-        }
-
-        // Turn the domain object into something usable on the client side
-        Type type;
-        if (elementType == null) {
-          type = ctx.getType();
-        } else {
-          type = new CollectionType(ctx.getType(), elementType);
-        }
-        Object clientValue = resolveClientValue(domainValue, type, propertyRefs, newPrefix);
-
-        ctx.set(clientValue);
-        return false;
-      }
-
-      @Override
-      public boolean visitValueProperty(String propertyName, Object value, PropertyContext ctx) {
-        // Limit unrequested value properties?
-        value = service.getProperty(domainEntity, propertyName);
-        ctx.set(value);
-        return false;
-      }
-    });
-
-    return bean.as();
+    T clientObject = bean.as();
+    return makeResolution(key, clientObject);
   }
 
   /**
-   * Recursive-descent implementation.
+   * Creates a Resolution object that holds a client value that represents the
+   * given domain value. The resolved client value will be assignable to
+   * {@code clientType}.
    */
-  private Object resolveClientValue(Object domainValue, Type returnType, Set<String> propertyRefs,
-      String prefix) {
+  private Resolution resolveClientValue(Object domainValue, Type clientType) {
     if (domainValue == null) {
       return null;
     }
 
-    boolean anyType = returnType == null;
+    boolean anyType = clientType == null;
     if (anyType) {
-      returnType = Object.class;
+      clientType = Object.class;
     }
 
-    Class<?> assignableTo = TypeUtils.ensureBaseType(returnType);
-    ResolutionKey key = new ResolutionKey(domainValue, returnType);
+    Class<?> assignableTo = TypeUtils.ensureBaseType(clientType);
+    ResolutionKey key = new ResolutionKey(domainValue, clientType);
 
-    Object previous = resolved.get(key);
-    if (previous != null && assignableTo.isInstance(previous)) {
-      return assignableTo.cast(previous);
+    Resolution previous = resolved.get(key);
+    if (previous != null && assignableTo.isInstance(previous.getClientObject())) {
+      return previous;
     }
 
     Class<?> returnClass = service.resolveClientType(domainValue.getClass(), assignableTo, true);
@@ -397,7 +591,7 @@
 
     // Pass simple values through
     if (ValueCodex.canDecode(returnClass)) {
-      return assignableTo.cast(domainValue);
+      return makeResolution(domainValue);
     }
 
     // Convert entities to EntityProxies or EntityProxyIds
@@ -405,11 +599,7 @@
     boolean isId = EntityProxyId.class.isAssignableFrom(returnClass);
     if (isProxy || isId) {
       Class<? extends BaseProxy> proxyClass = returnClass.asSubclass(BaseProxy.class);
-      BaseProxy entity = resolveClientProxy(domainValue, proxyClass, propertyRefs, key, prefix);
-      if (isId) {
-        return assignableTo.cast(((EntityProxy) entity).stableId());
-      }
-      return assignableTo.cast(entity);
+      return resolveClientProxy(domainValue, proxyClass, key);
     }
 
     // Convert collections
@@ -422,13 +612,12 @@
       } else {
         throw new ReportableException("Unsupported collection type" + returnClass.getName());
       }
-      resolved.put(key, accumulator);
 
-      Type elementType = TypeUtils.getSingleParameterization(Collection.class, returnType);
+      Type elementType = TypeUtils.getSingleParameterization(Collection.class, clientType);
       for (Object o : (Collection<?>) domainValue) {
-        accumulator.add(resolveClientValue(o, elementType, propertyRefs, prefix));
+        accumulator.add(resolveClientValue(o, elementType).getClientObject());
       }
-      return assignableTo.cast(accumulator);
+      return makeResolution(accumulator);
     }
 
     throw new ReportableException("Unsupported domain type " + returnClass.getCanonicalName());
diff --git a/user/src/com/google/web/bindery/requestfactory/server/SimpleRequestProcessor.java b/user/src/com/google/web/bindery/requestfactory/server/SimpleRequestProcessor.java
index 0df5c46..078ad2a 100644
--- a/user/src/com/google/web/bindery/requestfactory/server/SimpleRequestProcessor.java
+++ b/user/src/com/google/web/bindery/requestfactory/server/SimpleRequestProcessor.java
@@ -60,6 +60,8 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.SortedSet;
+import java.util.TreeSet;
 
 import javax.validation.ConstraintViolation;
 
@@ -416,7 +418,12 @@
       // No method invocations which can happen via RequestContext.fire()
       return;
     }
+    List<Method> contextMethods = new ArrayList<Method>(invocations.size());
+    List<Object> invocationResults = new ArrayList<Object>(invocations.size());
+    Map<Object, SortedSet<String>> allPropertyRefs = new HashMap<Object, SortedSet<String>>();
     for (InvocationMessage invocation : invocations) {
+      Object domainReturnValue;
+      boolean ok;
       try {
         // Find the Method
         String operation = invocation.getOperation();
@@ -425,6 +432,7 @@
           throw new UnexpectedException("Cannot resolve operation " + invocation.getOperation(),
               null);
         }
+        contextMethods.add(contextMethod);
         Method domainMethod = service.resolveDomainMethod(operation);
         if (domainMethod == null) {
           throw new UnexpectedException(
@@ -440,20 +448,42 @@
           args.add(0, serviceInstance);
         }
         // Invoke it
-        Object returnValue = service.invoke(domainMethod, args.toArray());
-
+        domainReturnValue = service.invoke(domainMethod, args.toArray());
+        if (invocation.getPropertyRefs() != null) {
+          SortedSet<String> paths = allPropertyRefs.get(domainReturnValue);
+          if (paths == null) {
+            paths = new TreeSet<String>();
+            allPropertyRefs.put(domainReturnValue, paths);
+          }
+          paths.addAll(invocation.getPropertyRefs());
+        }
+        ok = true;
+      } catch (ReportableException e) {
+        domainReturnValue = AutoBeanCodex.encode(createFailureMessage(e));
+        ok = false;
+      }
+      invocationResults.add(domainReturnValue);
+      success.add(ok);
+    }
+    Iterator<Method> contextMethodIt = contextMethods.iterator();
+    Iterator<Object> objects = invocationResults.iterator();
+    Iterator<Boolean> successes = success.iterator();
+    while (successes.hasNext()) {
+      assert contextMethodIt.hasNext();
+      assert objects.hasNext();
+      Method contextMethod = contextMethodIt.next();
+      Object returnValue = objects.next();
+      if (successes.next()) {
         // Convert domain object to client object
         Type requestReturnType = service.getRequestReturnType(contextMethod);
         returnValue =
             state.getResolver().resolveClientValue(returnValue, requestReturnType,
-                invocation.getPropertyRefs());
+                allPropertyRefs.get(returnValue));
 
         // Convert the client object to a string
         results.add(EntityCodex.encode(returnState, returnValue));
-        success.add(true);
-      } catch (ReportableException e) {
-        results.add(AutoBeanCodex.encode(createFailureMessage(e)));
-        success.add(false);
+      } else {
+        results.add((Splittable) returnValue);
       }
     }
   }
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 df32514..fdff661 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
@@ -1122,7 +1122,7 @@
    */
   public void testNullValueInIntegerListRequest() {
     delayTestFinish(DELAY_TEST_FINISH);
-    List<Integer> list = Arrays.asList(new Integer[]{1, 2, null});
+    List<Integer> list = Arrays.asList(new Integer[] {1, 2, null});
     final Request<Void> fooReq = req.simpleFooRequest().receiveNullValueInIntegerList(list);
     fooReq.fire(new Receiver<Void>() {
       @Override
@@ -1137,7 +1137,7 @@
    */
   public void testNullValueInStringListRequest() {
     delayTestFinish(DELAY_TEST_FINISH);
-    List<String> list = Arrays.asList(new String[]{"nonnull", "null", null});
+    List<String> list = Arrays.asList(new String[] {"nonnull", "null", null});
     final Request<Void> fooReq = req.simpleFooRequest().receiveNullValueInStringList(list);
     fooReq.fire(new Receiver<Void>() {
       @Override
@@ -1148,6 +1148,14 @@
   }
 
   /**
+   * Test that a proxy only referenced via a parameterization is available.
+   */
+  public void testOnlyUsedInList() {
+    OnlyUsedInListProxy proxy = simpleFooRequest().create(OnlyUsedInListProxy.class);
+    assertNotNull(proxy);
+  }
+
+  /**
    * Tests a message consisting only of operations, with no invocations.
    */
   public void testOperationOnlyMessage() {
@@ -1325,11 +1333,6 @@
     });
   }
 
-  /*
-   * TODO: all these tests should check the final values. It will be easy when
-   * we have better persistence than the singleton pattern.
-   */
-
   /**
    * Ensure that a relationship can be set up between two newly-created objects.
    */
@@ -1547,6 +1550,11 @@
     });
   }
 
+  /*
+   * TODO: all these tests should check the final values. It will be easy when
+   * we have better persistence than the singleton pattern.
+   */
+
   public void testPersistSelfOneToManyExistingEntityExistingRelation() {
     delayTestFinish(DELAY_TEST_FINISH);
 
@@ -1606,11 +1614,6 @@
    * TODO: all these tests should check the final values. It will be easy when
    * we have better persistence than the singleton pattern.
    */
-
-  /*
-   * TODO: all these tests should check the final values. It will be easy when
-   * we have better persistence than the singleton pattern.
-   */
   public void testPersistValueListNull() {
     delayTestFinish(DELAY_TEST_FINISH);
     simpleFooRequest().findSimpleFooById(999L).fire(new Receiver<SimpleFooProxy>() {
@@ -2050,6 +2053,48 @@
     });
   }
 
+  public void testPropertyRefsOnRecursiveProxyStructures() {
+    delayTestFinish(DELAY_TEST_FINISH);
+    simpleFooRequest().getFlattenedTripletReference().with("selfOneToManyField").fire(
+        new Receiver<List<SimpleFooProxy>>() {
+          @Override
+          public void onSuccess(List<SimpleFooProxy> response) {
+            for (int i = 0; i < response.size(); i++) {
+              SimpleFooProxy proxy = response.get(i);
+              checkSerialization(proxy); // do not reassign proxy as we
+                                         // assertSame() later
+              assertNotNull("Missing selfOneToManyField for item at index " + i, proxy
+                  .getSelfOneToManyField());
+              assertEquals(1, proxy.getSelfOneToManyField().size());
+              // last one references itself
+              SimpleFooProxy next = response.get(Math.min(i + 1, response.size() - 1));
+              assertSame("Item at index " + i
+                  + " does not link the following item in its selfOneToManyField", proxy
+                  .getSelfOneToManyField().get(0), next);
+            }
+            finishTestAndReset();
+          }
+        });
+  }
+
+  public void testPropertyRefsOnSameObjectReturnedTwice() {
+    delayTestFinish(DELAY_TEST_FINISH);
+    SimpleFooRequest request = simpleFooRequest();
+    request.findSimpleFooById(1L);
+    request.findAll().with("barField").to(new Receiver<List<SimpleFooProxy>>() {
+      @Override
+      public void onSuccess(List<SimpleFooProxy> response) {
+        for (SimpleFooProxy proxy : response) {
+          proxy = checkSerialization(proxy);
+          assertNotNull("barField has not been retrieved on id=" + proxy.getId(), proxy
+              .getBarField());
+        }
+        finishTestAndReset();
+      }
+    });
+    request.fire();
+  }
+
   public void testProxyList() {
     delayTestFinish(DELAY_TEST_FINISH);
     final Request<SimpleFooProxy> fooReq =
@@ -2260,14 +2305,6 @@
   }
 
   /**
-   * Test that a proxy only referenced via a parameterization is available.
-   */
-  public void testOnlyUsedInList() {
-    OnlyUsedInListProxy proxy = simpleFooRequest().create(OnlyUsedInListProxy.class);
-    assertNotNull(proxy);
-  }
-
-  /**
    * Check if a graph of unpersisted objects can be echoed.
    */
   public void testUnpersistedEchoComplexGraph() {
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 46ce807..d3cceaa 100644
--- a/user/test/com/google/web/bindery/requestfactory/server/SimpleFoo.java
+++ b/user/test/com/google/web/bindery/requestfactory/server/SimpleFoo.java
@@ -104,8 +104,8 @@
        * that doesn't allow any requests to be processed unless they're
        * associated with an existing session.
        */
-      Map<Long, SimpleFoo> value = (Map<Long, SimpleFoo>) req.getSession().getAttribute(
-          SimpleFoo.class.getCanonicalName());
+      Map<Long, SimpleFoo> value =
+          (Map<Long, SimpleFoo>) req.getSession().getAttribute(SimpleFoo.class.getCanonicalName());
       if (value == null) {
         value = resetImpl();
       }
@@ -113,6 +113,18 @@
     }
   }
 
+  public static List<SimpleFoo> getFlattenedTripletReference() {
+    SimpleFoo foo1 = new SimpleFoo();
+    SimpleFoo foo2 = new SimpleFoo();
+    SimpleFoo foo3 = new SimpleFoo();
+    foo1.setSelfOneToManyField(Arrays.asList(foo2));
+    foo2.setSelfOneToManyField(Arrays.asList(foo3));
+    foo1.persist();
+    foo2.persist();
+    foo3.persist();
+    return Arrays.asList(foo1, foo2, foo3);
+  }
+
   public static List<Integer> getNumberList() {
     ArrayList<Integer> list = new ArrayList<Integer>();
     list.add(1);
@@ -179,8 +191,7 @@
 
   public static void pleaseCrash(Integer crashIf42or43) throws Exception {
     if (crashIf42or43 == 42) {
-      throw new UnsupportedOperationException(
-          "THIS EXCEPTION IS EXPECTED BY A TEST");
+      throw new UnsupportedOperationException("THIS EXCEPTION IS EXPECTED BY A TEST");
     }
     if (crashIf42or43 == 43) {
       throw new Exception("THIS EXCEPTION IS EXPECTED BY A TEST");
@@ -247,8 +258,8 @@
       long expectedTime = expectedDate.getTime();
       long actualTime = actual.next().getTime();
       if (expectedTime != actualTime) {
-        throw new IllegalArgumentException(expectedDate.getClass().getName()
-            + " " + expectedTime + " != " + actualTime);
+        throw new IllegalArgumentException(expectedDate.getClass().getName() + " " + expectedTime
+            + " != " + actualTime);
       }
     }
 
@@ -271,22 +282,22 @@
 
   public static void receiveNullList(List<SimpleFoo> value) {
     if (value != null) {
-      throw new IllegalArgumentException(
-          "Expected value to be null. Actual value: \"" + value + "\"");
+      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 + "\"");
+      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 + "\"");
+      throw new IllegalArgumentException("Expected value to be null. Actual value: \"" + value
+          + "\"");
     }
   }
 
@@ -296,11 +307,10 @@
     } 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.");
+      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));
+      throw new IllegalArgumentException("Expected list.get(1) to return null. Actual: "
+          + list.get(1));
     }
   }
 
@@ -308,15 +318,12 @@
     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.");
+      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.");
+      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)
-              + "\"");
+      throw new IllegalArgumentException("Expected list.get(2) to return null. Actual: \""
+          + list.get(2) + "\"");
     }
   }
 
@@ -324,15 +331,12 @@
     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.");
+      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.");
+      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)
-              + "\"");
+      throw new IllegalArgumentException("Expected list.get(2) to return null. Actual: \""
+          + list.get(2) + "\"");
     }
   }
 
@@ -357,8 +361,7 @@
     if (req == null) {
       jreTestSingleton = instance;
     } else {
-      req.getSession().setAttribute(SimpleFoo.class.getCanonicalName(),
-          instance);
+      req.getSession().setAttribute(SimpleFoo.class.getCanonicalName(), instance);
     }
     return instance;
   }
@@ -666,8 +669,8 @@
 
   public void receiveNull(String value) {
     if (value != null) {
-      throw new IllegalArgumentException(
-          "Expected value to be null. Actual value: \"" + value + "\"");
+      throw new IllegalArgumentException("Expected value to be null. Actual value: \"" + value
+          + "\"");
     }
   }
 
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 ebc02c0..3c3290e 100644
--- a/user/test/com/google/web/bindery/requestfactory/shared/SimpleFooRequest.java
+++ b/user/test/com/google/web/bindery/requestfactory/shared/SimpleFooRequest.java
@@ -39,8 +39,7 @@
 
   Request<SimpleFooProxy> echo(SimpleFooProxy proxy);
 
-  Request<SimpleFooProxy> echoComplex(SimpleFooProxy fooProxy,
-      SimpleBarProxy barProxy);
+  Request<SimpleFooProxy> echoComplex(SimpleFooProxy fooProxy, SimpleBarProxy barProxy);
 
   Request<SimpleFooProxy> fetchDoubleReference();
 
@@ -48,6 +47,8 @@
 
   Request<SimpleFooProxy> findSimpleFooById(Long id);
 
+  Request<List<SimpleFooProxy>> getFlattenedTripletReference();
+
   Request<List<Integer>> getNumberList();
 
   Request<Set<Integer>> getNumberSet();
@@ -80,8 +81,7 @@
 
   Request<SimpleEnum> processEnumList(List<SimpleEnum> values);
 
-  InstanceRequest<SimpleFooProxy, String> processList(
-      List<SimpleFooProxy> values);
+  InstanceRequest<SimpleFooProxy, String> processList(List<SimpleFooProxy> values);
 
   Request<String> processString(String value);
 
@@ -109,8 +109,7 @@
 
   Request<String> returnNullString();
 
-  Request<Void> returnOnlyUsedInParameterization(
-      List<OnlyUsedInListProxy> values);
+  Request<Void> returnOnlyUsedInParameterization(List<OnlyUsedInListProxy> values);
 
   Request<SimpleFooProxy> returnSimpleFooSubclass();