Add a test for circular references via a CustomFieldSerializer.
Update deRPC code to pass this test.

Patch by: bobv
Review by: jgw

git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@6604 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/user/src/com/google/gwt/rpc/client/impl/CommandClientSerializationStreamWriter.java b/user/src/com/google/gwt/rpc/client/impl/CommandClientSerializationStreamWriter.java
index 5756565..83c69ee 100644
--- a/user/src/com/google/gwt/rpc/client/impl/CommandClientSerializationStreamWriter.java
+++ b/user/src/com/google/gwt/rpc/client/impl/CommandClientSerializationStreamWriter.java
@@ -51,13 +51,19 @@
     anObject.hashCode();
   }
 
-  private final Map<Object, IdentityValueCommand> identityMap = new IdentityHashMap<Object, IdentityValueCommand>();
+  private final Map<Object, IdentityValueCommand> identityMap;
   private final TypeOverrides serializer;
 
   public CommandClientSerializationStreamWriter(TypeOverrides serializer,
       CommandSink sink) {
+    this(serializer, sink, new IdentityHashMap<Object, IdentityValueCommand>());
+  }
+
+  private CommandClientSerializationStreamWriter(TypeOverrides serializer,
+      CommandSink sink, Map<Object, IdentityValueCommand> identityMap) {
     super(sink);
     this.serializer = serializer;
+    this.identityMap = identityMap;
   }
 
   /**
@@ -141,8 +147,13 @@
     InvokeCustomFieldSerializerCommand command = new InvokeCustomFieldSerializerCommand(
         type, null, null);
     identityMap.put(value, command);
+
+    /*
+     * Pass the current identityMap into the new writer to allow circular
+     * references through the graph emitted by the CFS.
+     */
     CommandClientSerializationStreamWriter subWriter = new CommandClientSerializationStreamWriter(
-        serializer, new HasValuesCommandSink(command));
+        serializer, new HasValuesCommandSink(command), identityMap);
 
     serializeFunction.serialize(subWriter, value);
     if (serializer.hasExtraFields(type.getName())) {
diff --git a/user/src/com/google/gwt/rpc/server/CommandServerSerializationStreamReader.java b/user/src/com/google/gwt/rpc/server/CommandServerSerializationStreamReader.java
index 8f89687..bf20c17 100644
--- a/user/src/com/google/gwt/rpc/server/CommandServerSerializationStreamReader.java
+++ b/user/src/com/google/gwt/rpc/server/CommandServerSerializationStreamReader.java
@@ -177,7 +177,8 @@
     public boolean visit(InvokeCustomFieldSerializerCommand x, Context ctx) {
       if (maybePushBackRef(x)) {
 
-        CommandServerSerializationStreamReader subReader = new CommandServerSerializationStreamReader();
+        CommandServerSerializationStreamReader subReader = new CommandServerSerializationStreamReader(
+            backRefs);
         subReader.prepareToRead(x.getValues());
 
         Class<?> serializerClass = x.getSerializerClass();
@@ -267,9 +268,18 @@
     }
   }
 
-  Map<IdentityValueCommand, Object> backRefs = new HashMap<IdentityValueCommand, Object>();
+  final Map<IdentityValueCommand, Object> backRefs;
   Iterator<ValueCommand> values;
 
+  public CommandServerSerializationStreamReader() {
+    this(new HashMap<IdentityValueCommand, Object>());
+  }
+
+  private CommandServerSerializationStreamReader(
+      Map<IdentityValueCommand, Object> backRefs) {
+    this.backRefs = backRefs;
+  }
+
   public void prepareToRead(List<ValueCommand> commands) {
     values = commands.iterator();
     assert values.hasNext() : "No commands";
diff --git a/user/src/com/google/gwt/rpc/server/CommandServerSerializationStreamWriter.java b/user/src/com/google/gwt/rpc/server/CommandServerSerializationStreamWriter.java
index d3b4b0f..bfae11e 100644
--- a/user/src/com/google/gwt/rpc/server/CommandServerSerializationStreamWriter.java
+++ b/user/src/com/google/gwt/rpc/server/CommandServerSerializationStreamWriter.java
@@ -48,7 +48,7 @@
     CommandSerializationStreamWriterBase {
 
   private final ClientOracle clientOracle;
-  private final Map<Object, IdentityValueCommand> identityMap = new IdentityHashMap<Object, IdentityValueCommand>();
+  private final Map<Object, IdentityValueCommand> identityMap;
 
   public CommandServerSerializationStreamWriter(CommandSink sink) {
     this(new HostedModeClientOracle(), sink);
@@ -56,8 +56,14 @@
 
   public CommandServerSerializationStreamWriter(ClientOracle oracle,
       CommandSink sink) {
+    this(oracle, sink, new IdentityHashMap<Object, IdentityValueCommand>());
+  }
+
+  private CommandServerSerializationStreamWriter(ClientOracle oracle,
+      CommandSink sink, Map<Object, IdentityValueCommand> identityMap) {
     super(sink);
     this.clientOracle = oracle;
+    this.identityMap = identityMap;
   }
 
   /**
@@ -70,14 +76,17 @@
       return NullValueCommand.INSTANCE;
     }
 
+    /*
+     * Check accessor map before the identity map because we don't want to
+     * recurse on wrapped primitive values.
+     */
     Accessor accessor;
-
-    if (identityMap.containsKey(value)) {
-      return identityMap.get(value);
-
-    } else if ((accessor = CommandSerializationUtil.getAccessor(type)).canMakeValueCommand()) {
+    if ((accessor = CommandSerializationUtil.getAccessor(type)).canMakeValueCommand()) {
       return accessor.makeValueCommand(value);
 
+    } else if (identityMap.containsKey(value)) {
+      return identityMap.get(value);
+
     } else if (type.isArray()) {
       return makeArray(type, value);
 
@@ -92,6 +101,7 @@
   private ArrayValueCommand makeArray(Class<?> type, Object value)
       throws SerializationException {
     ArrayValueCommand toReturn = new ArrayValueCommand(type.getComponentType());
+    identityMap.put(value, toReturn);
     for (int i = 0, j = Array.getLength(value); i < j; i++) {
       Object arrayValue = Array.get(value, i);
       if (arrayValue == null) {
@@ -102,7 +112,6 @@
         toReturn.add(makeValue(valueType, arrayValue));
       }
     }
-    identityMap.put(value, toReturn);
     return toReturn;
   }
 
@@ -202,8 +211,12 @@
               instanceClass, customSerializer, manuallySerializedType);
           identityMap.put(instance, toReturn);
 
+          /*
+           * Pass the current identityMap into the new writer to allow circular
+           * references through the graph emitted by the CFS.
+           */
           CommandServerSerializationStreamWriter subWriter = new CommandServerSerializationStreamWriter(
-              clientOracle, new HasValuesCommandSink(toReturn));
+              clientOracle, new HasValuesCommandSink(toReturn), identityMap);
           method.invoke(null, subWriter, instance);
 
           return toReturn;
diff --git a/user/src/com/google/gwt/rpc/server/WebModePayloadSink.java b/user/src/com/google/gwt/rpc/server/WebModePayloadSink.java
index b7718f1..a93e308 100644
--- a/user/src/com/google/gwt/rpc/server/WebModePayloadSink.java
+++ b/user/src/com/google/gwt/rpc/server/WebModePayloadSink.java
@@ -295,102 +295,102 @@
 
     @Override
     public boolean visit(InvokeCustomFieldSerializerCommand x, Context ctx) {
-      byte[] currentBackRef = null;
-      // TODO Extract the commands as an inline function
-      if (!isStarted(x)) {
-        InstantiateCommand makeReader = new InstantiateCommand(
-            CommandClientSerializationStreamReader.class);
-        /*
-         * Ensure that the reader will stick around for both instantiate and
-         * deserialize calls.
-         */
-        makeBackRef(makeReader);
-
-        ArrayValueCommand payload = new ArrayValueCommand(Object.class);
-        for (ValueCommand value : x.getValues()) {
-          payload.add(value);
-        }
-        makeReader.set(CommandClientSerializationStreamReader.class, "payload",
-            payload);
-
-        currentBackRef = begin(x);
-
-        String instantiateIdent = clientOracle.getMethodId(
-            x.getSerializerClass(), "instantiate",
-            SerializationStreamReader.class);
-
-        // x = $Foo(new Foo);
-        // x = instantiate(reader);
-        push(currentBackRef);
-        eq();
-        if (instantiateIdent == null) {
-          // No instantiate method, we'll have to invoke the constructor
-
-          instantiateIdent = clientOracle.getSeedName(x.getTargetClass());
-          assert instantiateIdent != null : "instantiateIdent";
-
-          // $Foo()
-          String constructorMethodName;
-          if (x.getTargetClass().getEnclosingClass() == null) {
-            constructorMethodName = "$" + x.getTargetClass().getSimpleName();
-          } else {
-            String name = x.getTargetClass().getName();
-            constructorMethodName = "$"
-                + name.substring(name.lastIndexOf('.') + 1);
-          }
-
-          String constructorIdent = clientOracle.getMethodId(
-              x.getTargetClass(), constructorMethodName, x.getTargetClass());
-          assert constructorIdent != null : "constructorIdent "
-              + constructorMethodName;
-
-          // constructor(new Seed);
-          push(constructorIdent);
-          lparen();
-          _new();
-          push(instantiateIdent);
-          rparen();
-          semi();
-        } else {
-          // instantiate(reader);
-          push(instantiateIdent);
-          lparen();
-          accept(makeReader);
-          rparen();
-          semi();
-        }
-
-        // Call the deserialize method if it exists
-        String deserializeIdent = clientOracle.getMethodId(
-            x.getSerializerClass(), "deserialize",
-            SerializationStreamReader.class, x.getManuallySerializedType());
-        if (deserializeIdent != null) {
-          // deserialize(reader, obj);
-          push(deserializeIdent);
-          lparen();
-          accept(makeReader);
-          comma();
-          push(currentBackRef);
-          rparen();
-          semi();
-        }
-
-        // If there are extra fields, set them
-        for (SetCommand setter : x.getSetters()) {
-          accept(setter);
-          semi();
-        }
-
-        commit(x);
-        forget(makeReader);
-      }
-
-      if (currentBackRef == null) {
+      if (isStarted(x)) {
         push(makeBackRef(x));
-      } else {
-        push(currentBackRef);
+        return false;
       }
 
+      // ( backref = instantiate(), deserialize(), setter, ..., backref )
+      byte[] currentBackRef = begin(x);
+
+      lparen();
+
+      InstantiateCommand makeReader = new InstantiateCommand(
+          CommandClientSerializationStreamReader.class);
+      /*
+       * Ensure that the reader will stick around for both instantiate and
+       * deserialize calls.
+       */
+      makeBackRef(makeReader);
+
+      ArrayValueCommand payload = new ArrayValueCommand(Object.class);
+      for (ValueCommand value : x.getValues()) {
+        payload.add(value);
+      }
+      makeReader.set(CommandClientSerializationStreamReader.class, "payload",
+          payload);
+
+      String instantiateIdent = clientOracle.getMethodId(
+          x.getSerializerClass(), "instantiate",
+          SerializationStreamReader.class);
+
+      // x = $Foo(new Foo),
+      // x = instantiate(reader),
+      push(currentBackRef);
+      eq();
+      if (instantiateIdent == null) {
+        // No instantiate method, we'll have to invoke the constructor
+
+        instantiateIdent = clientOracle.getSeedName(x.getTargetClass());
+        assert instantiateIdent != null : "instantiateIdent";
+
+        // $Foo()
+        String constructorMethodName;
+        if (x.getTargetClass().getEnclosingClass() == null) {
+          constructorMethodName = "$" + x.getTargetClass().getSimpleName();
+        } else {
+          String name = x.getTargetClass().getName();
+          constructorMethodName = "$"
+              + name.substring(name.lastIndexOf('.') + 1);
+        }
+
+        String constructorIdent = clientOracle.getMethodId(x.getTargetClass(),
+            constructorMethodName, x.getTargetClass());
+        assert constructorIdent != null : "constructorIdent "
+            + constructorMethodName;
+
+        // constructor(new Seed),
+        push(constructorIdent);
+        lparen();
+        _new();
+        push(instantiateIdent);
+        rparen();
+        comma();
+      } else {
+        // instantiate(reader),
+        push(instantiateIdent);
+        lparen();
+        accept(makeReader);
+        rparen();
+        comma();
+      }
+
+      // Call the deserialize method if it exists
+      String deserializeIdent = clientOracle.getMethodId(
+          x.getSerializerClass(), "deserialize",
+          SerializationStreamReader.class, x.getManuallySerializedType());
+      if (deserializeIdent != null) {
+        // deserialize(reader, obj),
+        push(deserializeIdent);
+        lparen();
+        accept(makeReader);
+        comma();
+        push(currentBackRef);
+        rparen();
+        comma();
+      }
+
+      // If there are extra fields, set them
+      for (SetCommand setter : x.getSetters()) {
+        accept(setter);
+        comma();
+      }
+
+      push(currentBackRef);
+      rparen();
+      commit(x, false);
+      forget(makeReader);
+
       return false;
     }
 
@@ -696,7 +696,7 @@
         } while (leafType.getComponentType() != null);
         assert dims > 0;
         // leafType cannot be null here
-        
+
         String s = getJavahSignatureName(leafType);
         for (int i = 0; i < dims; ++i) {
           s = "_3" + s;
diff --git a/user/test/com/google/gwt/user/client/rpc/ObjectGraphTest.java b/user/test/com/google/gwt/user/client/rpc/ObjectGraphTest.java
index ce6b964..291605b 100644
--- a/user/test/com/google/gwt/user/client/rpc/ObjectGraphTest.java
+++ b/user/test/com/google/gwt/user/client/rpc/ObjectGraphTest.java
@@ -18,6 +18,7 @@
 import com.google.gwt.core.client.GWT;
 import com.google.gwt.junit.client.GWTTestCase;
 import com.google.gwt.user.client.rpc.TestSetFactory.SerializableDoublyLinkedNode;
+import com.google.gwt.user.client.rpc.TestSetFactory.SerializableGraphWithCFS;
 import com.google.gwt.user.client.rpc.TestSetFactory.SerializablePrivateNoArg;
 import com.google.gwt.user.client.rpc.TestSetFactory.SerializableWithTwoArrays;
 import com.google.gwt.user.client.rpc.impl.AbstractSerializationStream;
@@ -68,6 +69,25 @@
         });
   }
 
+  public void testComplexCyclicGraphWithCFS() {
+    delayTestFinish(TEST_DELAY);
+
+    ObjectGraphTestServiceAsync service = getServiceAsync();
+    service.echo_ComplexCyclicGraphWithCFS(
+        TestSetFactory.createComplexCyclicGraphWithCFS(),
+        new AsyncCallback<SerializableGraphWithCFS>() {
+          public void onFailure(Throwable caught) {
+            TestSetValidator.rethrowException(caught);
+          }
+
+          public void onSuccess(SerializableGraphWithCFS result) {
+            assertNotNull(result);
+            assertTrue(TestSetValidator.isValidComplexCyclicGraphWithCFS(result));
+            finishTest();
+          }
+        });
+  }
+
   public void testComplexCyclicGraph2() {
     delayTestFinish(TEST_DELAY);
 
diff --git a/user/test/com/google/gwt/user/client/rpc/ObjectGraphTestService.java b/user/test/com/google/gwt/user/client/rpc/ObjectGraphTestService.java
index f806677..aa9825a 100644
--- a/user/test/com/google/gwt/user/client/rpc/ObjectGraphTestService.java
+++ b/user/test/com/google/gwt/user/client/rpc/ObjectGraphTestService.java
@@ -16,6 +16,7 @@
 package com.google.gwt.user.client.rpc;
 
 import com.google.gwt.user.client.rpc.TestSetFactory.SerializableDoublyLinkedNode;
+import com.google.gwt.user.client.rpc.TestSetFactory.SerializableGraphWithCFS;
 import com.google.gwt.user.client.rpc.TestSetFactory.SerializablePrivateNoArg;
 import com.google.gwt.user.client.rpc.TestSetFactory.SerializableWithTwoArrays;
 
@@ -33,9 +34,13 @@
   SerializableDoublyLinkedNode echo_ComplexCyclicGraph(
       SerializableDoublyLinkedNode node1, SerializableDoublyLinkedNode node2);
 
+  SerializableGraphWithCFS echo_ComplexCyclicGraphWithCFS(
+      SerializableGraphWithCFS createComplexCyclicGraphWithArrays);
+
   SerializablePrivateNoArg echo_PrivateNoArg(SerializablePrivateNoArg node);
 
-  SerializableWithTwoArrays echo_SerializableWithTwoArrays(SerializableWithTwoArrays node);
+  SerializableWithTwoArrays echo_SerializableWithTwoArrays(
+      SerializableWithTwoArrays node);
 
   SerializableDoublyLinkedNode echo_TrivialCyclicGraph(
       SerializableDoublyLinkedNode node);
diff --git a/user/test/com/google/gwt/user/client/rpc/ObjectGraphTestServiceAsync.java b/user/test/com/google/gwt/user/client/rpc/ObjectGraphTestServiceAsync.java
index 4b4d42b..b277bba 100644
--- a/user/test/com/google/gwt/user/client/rpc/ObjectGraphTestServiceAsync.java
+++ b/user/test/com/google/gwt/user/client/rpc/ObjectGraphTestServiceAsync.java
@@ -16,6 +16,7 @@
 package com.google.gwt.user.client.rpc;
 
 import com.google.gwt.user.client.rpc.TestSetFactory.SerializableDoublyLinkedNode;
+import com.google.gwt.user.client.rpc.TestSetFactory.SerializableGraphWithCFS;
 import com.google.gwt.user.client.rpc.TestSetFactory.SerializablePrivateNoArg;
 import com.google.gwt.user.client.rpc.TestSetFactory.SerializableWithTwoArrays;
 
@@ -34,8 +35,13 @@
 
   void echo_PrivateNoArg(SerializablePrivateNoArg node, AsyncCallback callback);
 
-  void echo_SerializableWithTwoArrays(SerializableWithTwoArrays node, AsyncCallback callback);
+  void echo_SerializableWithTwoArrays(SerializableWithTwoArrays node,
+      AsyncCallback callback);
 
   void echo_TrivialCyclicGraph(SerializableDoublyLinkedNode node,
       AsyncCallback callback);
+
+  void echo_ComplexCyclicGraphWithCFS(
+      SerializableGraphWithCFS createComplexCyclicGraphWithArrays,
+      AsyncCallback<SerializableGraphWithCFS> asyncCallback);
 }
diff --git a/user/test/com/google/gwt/user/client/rpc/TestSetFactory.java b/user/test/com/google/gwt/user/client/rpc/TestSetFactory.java
index 5166bf2..069a7fb 100644
--- a/user/test/com/google/gwt/user/client/rpc/TestSetFactory.java
+++ b/user/test/com/google/gwt/user/client/rpc/TestSetFactory.java
@@ -257,6 +257,32 @@
   }
 
   /**
+   * Test reference cycles where the reference graph passes through a
+   * CustomFieldSerializer.
+   */
+  public static final class SerializableGraphWithCFS implements Serializable {
+    private ArrayList<SerializableGraphWithCFS> array;
+    private SerializableGraphWithCFS parent;
+
+    public SerializableGraphWithCFS() {
+      array = new ArrayList<SerializableGraphWithCFS>();
+      array.add(new SerializableGraphWithCFS(this));
+    }
+
+    public SerializableGraphWithCFS(SerializableGraphWithCFS parent) {
+      this.parent = parent;
+    }
+
+    public ArrayList<SerializableGraphWithCFS> getArray() {
+      return array;
+    }
+
+    public SerializableGraphWithCFS getParent() {
+      return parent;
+    }
+  }
+
+  /**
    * Tests that classes with a private no-arg constructor can be serialized.
    */
   public static class SerializablePrivateNoArg implements IsSerializable {
@@ -352,6 +378,10 @@
         new Character(Character.MAX_VALUE), new Character(Character.MIN_VALUE)};
   }
 
+  public static SerializableGraphWithCFS createComplexCyclicGraphWithCFS() {
+    return new SerializableGraphWithCFS();
+  }
+
   @SuppressWarnings("deprecation")
   public static Date[] createDateArray() {
     return new Date[] {
diff --git a/user/test/com/google/gwt/user/client/rpc/TestSetValidator.java b/user/test/com/google/gwt/user/client/rpc/TestSetValidator.java
index adbf839..58390a0 100644
--- a/user/test/com/google/gwt/user/client/rpc/TestSetValidator.java
+++ b/user/test/com/google/gwt/user/client/rpc/TestSetValidator.java
@@ -15,17 +15,18 @@
  */
 package com.google.gwt.user.client.rpc;
 
-import com.google.gwt.user.client.rpc.TestSetFactory.MarkerTypeTreeMap;
-import com.google.gwt.user.client.rpc.TestSetFactory.MarkerTypeTreeSet;
-import com.google.gwt.user.client.rpc.TestSetFactory.SerializableDoublyLinkedNode;
-import com.google.gwt.user.client.rpc.TestSetFactory.SerializablePrivateNoArg;
-import com.google.gwt.user.client.rpc.TestSetFactory.SerializableWithTwoArrays;
-
 import static junit.framework.Assert.assertEquals;
 import static junit.framework.Assert.assertFalse;
 import static junit.framework.Assert.assertNotNull;
 import static junit.framework.Assert.assertSame;
 
+import com.google.gwt.user.client.rpc.TestSetFactory.MarkerTypeTreeMap;
+import com.google.gwt.user.client.rpc.TestSetFactory.MarkerTypeTreeSet;
+import com.google.gwt.user.client.rpc.TestSetFactory.SerializableDoublyLinkedNode;
+import com.google.gwt.user.client.rpc.TestSetFactory.SerializableGraphWithCFS;
+import com.google.gwt.user.client.rpc.TestSetFactory.SerializablePrivateNoArg;
+import com.google.gwt.user.client.rpc.TestSetFactory.SerializableWithTwoArrays;
+
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.HashSet;
@@ -41,8 +42,8 @@
 import java.util.Map.Entry;
 
 /**
- * Misnamed set of static validation methods used by various
- * collection class tests.
+ * Misnamed set of static validation methods used by various collection class
+ * tests.
  * <p>
  * TODO: could add generics to require args to be of the same type
  */
@@ -247,7 +248,7 @@
     return reference.equals(list);
   }
 
-  public static boolean isValid(HashMap<?,?> expected, HashMap<?,?> map) {
+  public static boolean isValid(HashMap<?, ?> expected, HashMap<?, ?> map) {
     if (map == null) {
       return false;
     }
@@ -259,7 +260,7 @@
     Set<?> entries = expected.entrySet();
     Iterator<?> entryIter = entries.iterator();
     while (entryIter.hasNext()) {
-      Entry<?,?> entry = (Entry<?,?>) entryIter.next();
+      Entry<?, ?> entry = (Entry<?, ?>) entryIter.next();
 
       Object value = map.get(entry.getKey());
 
@@ -298,8 +299,9 @@
     return true;
   }
 
-  public static boolean isValid(LinkedHashMap<?,?> expected, LinkedHashMap<?,?> map) {
-    if (isValid((HashMap<?,?>) expected, (HashMap<?,?>) map)) {
+  public static boolean isValid(LinkedHashMap<?, ?> expected,
+      LinkedHashMap<?, ?> map) {
+    if (isValid((HashMap<?, ?>) expected, (HashMap<?, ?>) map)) {
       Iterator<?> expectedEntries = expected.entrySet().iterator();
       Iterator<?> actualEntries = map.entrySet().iterator();
       return equals(expectedEntries, actualEntries);
@@ -494,6 +496,19 @@
     return true;
   }
 
+  public static boolean isValidComplexCyclicGraphWithCFS(
+      SerializableGraphWithCFS result) {
+    assertNotNull(result);
+    List<SerializableGraphWithCFS> array = result.getArray();
+    assertNotNull(array);
+    assertEquals(1, array.size());
+
+    SerializableGraphWithCFS child = array.get(0);
+    assertFalse(result == child);
+    assertSame(result, child.getParent());
+    return true;
+  }
+
   public static boolean isValidTrivialCyclicGraph(
       SerializableDoublyLinkedNode actual) {
     if (actual == null) {
@@ -538,5 +553,4 @@
   private static boolean equalsWithNullCheck(Object a, Object b) {
     return a == b || (a != null && a.equals(b));
   }
-
 }
diff --git a/user/test/com/google/gwt/user/server/rpc/ObjectGraphTestServiceImpl.java b/user/test/com/google/gwt/user/server/rpc/ObjectGraphTestServiceImpl.java
index c86f7b2..4d1262a 100644
--- a/user/test/com/google/gwt/user/server/rpc/ObjectGraphTestServiceImpl.java
+++ b/user/test/com/google/gwt/user/server/rpc/ObjectGraphTestServiceImpl.java
@@ -18,6 +18,7 @@
 import com.google.gwt.user.client.rpc.ObjectGraphTestService;
 import com.google.gwt.user.client.rpc.TestSetValidator;
 import com.google.gwt.user.client.rpc.TestSetFactory.SerializableDoublyLinkedNode;
+import com.google.gwt.user.client.rpc.TestSetFactory.SerializableGraphWithCFS;
 import com.google.gwt.user.client.rpc.TestSetFactory.SerializablePrivateNoArg;
 import com.google.gwt.user.client.rpc.TestSetFactory.SerializableWithTwoArrays;
 
@@ -45,6 +46,15 @@
     return root;
   }
 
+  public SerializableGraphWithCFS echo_ComplexCyclicGraphWithCFS(
+      SerializableGraphWithCFS root) {
+    if (!TestSetValidator.isValidComplexCyclicGraphWithCFS(root)) {
+      throw new RuntimeException();
+    }
+
+    return root;
+  }
+
   public SerializableDoublyLinkedNode echo_TrivialCyclicGraph(
       SerializableDoublyLinkedNode root) {
     if (!TestSetValidator.isValidTrivialCyclicGraph(root)) {