Fix issue with two RequestFactories with two different Proxies on the same domain class

Review at https://gwt-code-reviews.appspot.com/1712803/
Contributed by alexandre.ardhuin.dw
Fixes issue 7381

Review by: cromwellian@google.com

git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@11258 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/requestfactory/build.xml b/requestfactory/build.xml
index 9769aa0..9215bfe 100755
--- a/requestfactory/build.xml
+++ b/requestfactory/build.xml
@@ -122,6 +122,8 @@
       <arg value="com.google.web.bindery.requestfactory.shared.BoxesAndPrimitivesTest.Factory" />
       <arg value="com.google.web.bindery.requestfactory.shared.ComplexKeysTest.Factory" />
       <arg value="com.google.web.bindery.requestfactory.shared.LocatorTest.Factory" />
+      <arg value="com.google.web.bindery.requestfactory.shared.MultipleFactoriesTest.Factory1" />
+      <arg value="com.google.web.bindery.requestfactory.shared.MultipleFactoriesTest.Factory2" />
       <arg value="com.google.web.bindery.requestfactory.shared.ServiceInheritanceTest$Factory" />
       <arg value="com.google.web.bindery.requestfactory.shared.SimpleRequestFactory" />
     </java>
diff --git a/user/src/com/google/web/bindery/requestfactory/vm/impl/ClassComparator.java b/user/src/com/google/web/bindery/requestfactory/vm/impl/ClassComparator.java
new file mode 100644
index 0000000..a3f142b
--- /dev/null
+++ b/user/src/com/google/web/bindery/requestfactory/vm/impl/ClassComparator.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2012 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 License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.web.bindery.requestfactory.vm.impl;
+
+import java.util.Comparator;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Orders classes by assignability, with most-derived types ordered first, and then by name.
+ */
+class ClassComparator implements Comparator<String> {
+  private static final Logger log = Logger.getLogger(ClassComparator.class.getName());
+
+  private final ClassLoader resolveClassesWith;
+
+  public ClassComparator(ClassLoader resolveClassesWith) {
+    this.resolveClassesWith = resolveClassesWith;
+  }
+
+  @Override
+  public int compare(String className1, String className2) {
+    Class<?> class1 = forName(className1);
+    Class<?> class2 = forName(className2);
+    if (class1.equals(class2)) {
+      return 0;
+    } else if (class1.isAssignableFrom(class2)) {
+      return 1;
+    } else if (class2.isAssignableFrom(class1)) {
+      return -1;
+    }
+    return className1.compareTo(className2);
+  }
+
+  private Class<?> forName(String name) {
+    try {
+      return Class.forName(name, false, resolveClassesWith);
+    } catch (ClassNotFoundException e) {
+      String msg = "Could not locate class " + name;
+      log.log(Level.SEVERE, msg, e);
+      throw new RuntimeException(msg, e);
+    }
+  }
+};
diff --git a/user/src/com/google/web/bindery/requestfactory/vm/impl/Deobfuscator.java b/user/src/com/google/web/bindery/requestfactory/vm/impl/Deobfuscator.java
index 5c16de1..c70b63e 100644
--- a/user/src/com/google/web/bindery/requestfactory/vm/impl/Deobfuscator.java
+++ b/user/src/com/google/web/bindery/requestfactory/vm/impl/Deobfuscator.java
@@ -1,16 +1,14 @@
 /*
  * Copyright 2011 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
+ * 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
- * License for the specific language governing permissions and limitations under
+ * 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 License for the specific language governing permissions and limitations under
  * the License.
  */
 package com.google.web.bindery.requestfactory.vm.impl;
@@ -22,11 +20,11 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.TreeSet;
 
 /**
- * Provides access to payload deobfuscation services for server and JVM-based
- * clients. The deobfuscation data is baked into GWT-based clients by the
- * generator.
+ * Provides access to payload deobfuscation services for server and JVM-based clients. The
+ * deobfuscation data is baked into GWT-based clients by the generator.
  */
 public class Deobfuscator {
   /**
@@ -34,9 +32,8 @@
    */
   public static class Builder {
     /**
-     * Load a pre-computed Builder from the classpath. The builder
-     * implementation is expected to have been generated by the annotation
-     * processor as part of the build process.
+     * Load a pre-computed Builder from the classpath. The builder implementation is expected to
+     * have been generated by the annotation processor as part of the build process.
      * 
      * @see com.google.web.bindery.requestfactory.apt.DeobfuscatorBuilder
      * @see com.google.web.bindery.requestfactory.server.ResolverServiceLayer
@@ -54,6 +51,7 @@
         }
         Class<? extends Builder> builderClass = found.asSubclass(Builder.class);
         Builder builder = builderClass.newInstance();
+        builder.resolveClassesWith = resolveClassesWith;
         return builder;
       } catch (ClassNotFoundException e) {
         throw new RuntimeException("The RequestFactory ValidationTool must be run for the "
@@ -66,6 +64,8 @@
       throw new RuntimeException(ex);
     }
 
+    private ClassLoader resolveClassesWith;
+
     private Deobfuscator d = new Deobfuscator();
 
     {
@@ -86,7 +86,7 @@
     }
 
     public Builder merge(Deobfuscator existing) {
-      d.domainToClientType.putAll(existing.domainToClientType);
+      d.domainToClientType.putAll(merge(d.domainToClientType, existing.domainToClientType));
       d.operationData.putAll(existing.operationData);
       // referencedTypes recomputed in build()
       d.typeTokens.putAll(existing.typeTokens);
@@ -118,14 +118,51 @@
       d.typeTokens.put(token, binaryName);
       return this;
     }
+
+    /**
+     * Merges two domainToClientType into one. Merged map's values are still ordering by
+     * assignability, with most-derived types ordered first.
+     * 
+     * @see ClassComparator
+     */
+    private Map<String, List<String>> merge(Map<String, List<String>> domainToClientType1,
+        Map<String, List<String>> domainToClientType2) {
+      Map<String, List<String>> result = new HashMap<String, List<String>>();
+      Set<String> domains = new HashSet<String>();
+      domains.addAll(domainToClientType1.keySet());
+      domains.addAll(domainToClientType2.keySet());
+      for (String domain : domains) {
+        List<String> clientTypes1 = domainToClientType1.get(domain);
+        List<String> clientTypes2 = domainToClientType2.get(domain);
+        List<String> clientTypes = mergeClientTypes(clientTypes1, clientTypes2);
+        result.put(domain, clientTypes);
+      }
+      return result;
+    }
+
+    /**
+     * Merges two clientType lists into one. Merged values are still ordering by assignability, with
+     * most-derived types ordered first.
+     * 
+     * @see ClassComparator
+     */
+    private List<String> mergeClientTypes(List<String> clientTypes1, List<String> clientTypes2) {
+      Set<String> clientTypes = new TreeSet<String>(new ClassComparator(resolveClassesWith));
+      if (clientTypes1 != null) {
+        clientTypes.addAll(clientTypes1);
+      }
+      if (clientTypes2 != null) {
+        clientTypes.addAll(clientTypes2);
+      }
+      return Collections.unmodifiableList(new ArrayList<String>(clientTypes));
+    }
   }
 
   private static final String GENERATED_SUFFIX = "DeobfuscatorBuilder";
   private static final String GENERATED_SUFFIX_LITE = GENERATED_SUFFIX + "Lite";
 
   /**
-   * Maps domain types (e.g Foo) to client proxy types (e.g. FooAProxy,
-   * FooBProxy).
+   * Maps domain types (e.g Foo) to client proxy types (e.g. FooAProxy, FooBProxy).
    */
   private Map<String, List<String>> domainToClientType;
   private Map<OperationKey, OperationData> operationData;
@@ -139,9 +176,8 @@
   }
 
   /**
-   * Returns the client proxy types whose {@code @ProxyFor} is exactly
-   * {@code binaryTypeName}. Ordered such that the most-derived types will be
-   * iterated over first.
+   * Returns the client proxy types whose {@code @ProxyFor} is exactly {@code binaryTypeName}.
+   * Ordered such that the most-derived types will be iterated over first.
    */
   public List<String> getClientProxies(String binaryTypeName) {
     return domainToClientType.get(binaryTypeName);
diff --git a/user/test/com/google/web/bindery/requestfactory/gwt/RequestFactorySuite.java b/user/test/com/google/web/bindery/requestfactory/gwt/RequestFactorySuite.java
index fb0f93b..0d4db18 100644
--- a/user/test/com/google/web/bindery/requestfactory/gwt/RequestFactorySuite.java
+++ b/user/test/com/google/web/bindery/requestfactory/gwt/RequestFactorySuite.java
@@ -16,8 +16,8 @@
 package com.google.web.bindery.requestfactory.gwt;
 
 import com.google.gwt.junit.tools.GWTTestSuite;
-import com.google.web.bindery.requestfactory.gwt.client.RequestBatcherTest;
 import com.google.web.bindery.requestfactory.gwt.client.FindServiceTest;
+import com.google.web.bindery.requestfactory.gwt.client.RequestBatcherTest;
 import com.google.web.bindery.requestfactory.gwt.client.RequestFactoryChainedContextTest;
 import com.google.web.bindery.requestfactory.gwt.client.RequestFactoryExceptionHandlerTest;
 import com.google.web.bindery.requestfactory.gwt.client.RequestFactoryExceptionPropagationTest;
@@ -29,6 +29,7 @@
 import com.google.web.bindery.requestfactory.shared.ComplexKeysTest;
 import com.google.web.bindery.requestfactory.shared.FanoutReceiverTest;
 import com.google.web.bindery.requestfactory.shared.LocatorTest;
+import com.google.web.bindery.requestfactory.shared.MultipleFactoriesTest;
 import com.google.web.bindery.requestfactory.shared.ServiceInheritanceTest;
 import com.google.web.bindery.requestfactory.shared.impl.RequestPayloadTest;
 
@@ -48,6 +49,7 @@
     suite.addTestSuite(FanoutReceiverTest.class);
     suite.addTestSuite(FindServiceTest.class);
     suite.addTestSuite(LocatorTest.class);
+    suite.addTestSuite(MultipleFactoriesTest.class);
     suite.addTestSuite(RequestFactoryTest.class);
     suite.addTestSuite(RequestFactoryChainedContextTest.class);
     suite.addTestSuite(RequestFactoryExceptionHandlerTest.class);
diff --git a/user/test/com/google/web/bindery/requestfactory/server/MultipleFactoriesJreTest.java b/user/test/com/google/web/bindery/requestfactory/server/MultipleFactoriesJreTest.java
new file mode 100644
index 0000000..4da66a7
--- /dev/null
+++ b/user/test/com/google/web/bindery/requestfactory/server/MultipleFactoriesJreTest.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2012 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 License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.web.bindery.requestfactory.server;
+
+import com.google.web.bindery.requestfactory.shared.MultipleFactoriesTest;
+
+/**
+ * A JRE version of {@link MultipleFactoriesTest}.
+ */
+public class MultipleFactoriesJreTest extends MultipleFactoriesTest {
+
+  @Override
+  public String getModuleName() {
+    return null;
+  }
+
+  @Override
+  protected Factory1 createFactory1() {
+    return RequestFactoryJreTest.createInProcess(Factory1.class);
+  }
+
+  @Override
+  protected Factory2 createFactory2() {
+    return RequestFactoryJreTest.createInProcess(Factory2.class);
+  }
+}
diff --git a/user/test/com/google/web/bindery/requestfactory/shared/MultipleFactoriesTest.java b/user/test/com/google/web/bindery/requestfactory/shared/MultipleFactoriesTest.java
new file mode 100644
index 0000000..481aa4e
--- /dev/null
+++ b/user/test/com/google/web/bindery/requestfactory/shared/MultipleFactoriesTest.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright 2012 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 License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.web.bindery.requestfactory.shared;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.junit.client.GWTTestCase;
+import com.google.web.bindery.event.shared.SimpleEventBus;
+
+/**
+ * Contains a set of checks of using multiple request factories simultaneously.
+ */
+public class MultipleFactoriesTest extends GWTTestCase {
+
+  /**
+   * The domain type.
+   */
+  protected static class Entity {
+    static final Entity SINGLETON = new Entity();
+
+    public String getString1() {
+      return EXPECTED_STRING_1;
+    }
+
+    public String getString2() {
+      return EXPECTED_STRING_2;
+    }
+  }
+
+  /**
+   * The first RequestFactory.
+   */
+  protected interface Factory1 extends RequestFactory {
+    Context1 context();
+  }
+
+  /**
+   * The second RequestFactory.
+   */
+  protected interface Factory2 extends RequestFactory {
+    Context2 context();
+  }
+
+  /**
+   * The service method implementations.
+   */
+  protected static class ServiceImpl {
+    public static Entity getEntity() {
+      return Entity.SINGLETON;
+    }
+  }
+
+  @Service(ServiceImpl.class)
+  interface Context1 extends RequestContext {
+    Request<Proxy1> getEntity();
+  }
+
+  @Service(ServiceImpl.class)
+  interface Context2 extends RequestContext {
+    Request<Proxy2> getEntity();
+  }
+
+  @ProxyFor(Entity.class)
+  interface Proxy1 extends ValueProxy {
+    String getString1();
+  }
+  @ProxyFor(Entity.class)
+  interface Proxy2 extends ValueProxy {
+    String getString2();
+  }
+
+  static abstract class TestReceiver<T> extends Receiver<T> {
+    @Override
+    public void onFailure(ServerFailure error) {
+      fail(error.getMessage());
+    }
+  }
+
+  private static final String EXPECTED_STRING_1 = "hello world 1";
+  private static final String EXPECTED_STRING_2 = "hello world 2";
+  private static final int TEST_DELAY = 5000;
+
+  private Factory1 factory1;
+  private Factory2 factory2;
+
+  @Override
+  public String getModuleName() {
+    return "com.google.web.bindery.requestfactory.gwt.RequestFactorySuite";
+  }
+
+  /**
+   * Tests that the 2 calls with 2 RequestFactory with 2 differents Proxy on the same domain class
+   * succeed.
+   */
+  public void test() {
+    delayTestFinish(TEST_DELAY);
+    context1().getEntity().fire(new TestReceiver<Proxy1>() {
+      @Override
+      public void onSuccess(Proxy1 response) {
+        assertEquals(EXPECTED_STRING_1, response.getString1());
+
+        // test 2
+        context2().getEntity().to(new TestReceiver<Proxy2>() {
+          @Override
+          public void onSuccess(Proxy2 response) {
+            assertEquals(EXPECTED_STRING_2, response.getString2());
+          }
+        }).fire(new TestReceiver<Void>() {
+          @Override
+          public void onSuccess(Void response) {
+            finishTest();
+          }
+        });
+      }
+    });
+  }
+
+  protected Factory1 createFactory1() {
+    Factory1 toReturn = GWT.create(Factory1.class);
+    toReturn.initialize(new SimpleEventBus());
+    return toReturn;
+  }
+
+  protected Factory2 createFactory2() {
+    Factory2 toReturn = GWT.create(Factory2.class);
+    toReturn.initialize(new SimpleEventBus());
+    return toReturn;
+  }
+
+  @Override
+  protected void gwtSetUp() {
+    factory1 = createFactory1();
+    factory2 = createFactory2();
+  }
+
+  private Context1 context1() {
+    return factory1.context();
+  }
+
+  private Context2 context2() {
+    return factory2.context();
+  }
+}
diff --git a/user/test/com/google/web/bindery/requestfactory/vm/RequestFactoryJreSuite.java b/user/test/com/google/web/bindery/requestfactory/vm/RequestFactoryJreSuite.java
index 503d026..e928579 100644
--- a/user/test/com/google/web/bindery/requestfactory/vm/RequestFactoryJreSuite.java
+++ b/user/test/com/google/web/bindery/requestfactory/vm/RequestFactoryJreSuite.java
@@ -20,6 +20,7 @@
 import com.google.web.bindery.requestfactory.server.FanoutReceiverJreTest;
 import com.google.web.bindery.requestfactory.server.FindServiceJreTest;
 import com.google.web.bindery.requestfactory.server.LocatorJreTest;
+import com.google.web.bindery.requestfactory.server.MultipleFactoriesJreTest;
 import com.google.web.bindery.requestfactory.server.RequestFactoryChainedContextJreTest;
 import com.google.web.bindery.requestfactory.server.RequestFactoryExceptionPropagationJreTest;
 import com.google.web.bindery.requestfactory.server.RequestFactoryJreTest;
@@ -47,6 +48,7 @@
     suite.addTestSuite(FanoutReceiverJreTest.class);
     suite.addTestSuite(FindServiceJreTest.class);
     suite.addTestSuite(LocatorJreTest.class);
+    suite.addTestSuite(MultipleFactoriesJreTest.class);
     suite.addTestSuite(RequestFactoryChainedContextJreTest.class);
     suite.addTestSuite(RequestFactoryExceptionPropagationJreTest.class);
     suite.addTestSuite(RequestFactoryJreTest.class);
diff --git a/user/test/com/google/web/bindery/requestfactory/vm/impl/ClassComparatorTest.java b/user/test/com/google/web/bindery/requestfactory/vm/impl/ClassComparatorTest.java
new file mode 100644
index 0000000..2732997
--- /dev/null
+++ b/user/test/com/google/web/bindery/requestfactory/vm/impl/ClassComparatorTest.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2012 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 License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.web.bindery.requestfactory.vm.impl;
+
+import junit.framework.TestCase;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.TreeSet;
+
+/**
+ * Tests class comparison used to merge {@link Deobfuscator}.
+ */
+public class ClassComparatorTest extends TestCase {
+  public void testCompare() {
+    List<Class<?>> classes = new ArrayList<Class<?>>();
+    classes.add(String.class);
+    classes.add(Integer.class);
+    classes.add(Object.class);
+    classes.add(CharSequence.class);
+    classes.add(Object.class);
+    classes.add(Number.class);
+    classes.add(Long.class);
+    classes.add(Number.class);
+
+    // add class names to treeset
+    ClassLoader classLoader = this.getClass().getClassLoader();
+    TreeSet<String> orderedClasses = new TreeSet<String>(new ClassComparator(classLoader));
+    for (Class<?> clazz : classes) {
+      orderedClasses.add(clazz.getName());
+    }
+
+    // check ordering and duplication
+    assertEquals(6, orderedClasses.size());
+    Iterator<String> it = orderedClasses.iterator();
+    assertEquals(Integer.class.getName(), it.next());
+    assertEquals(Long.class.getName(), it.next());
+    assertEquals(Number.class.getName(), it.next());
+    assertEquals(String.class.getName(), it.next());
+    assertEquals(CharSequence.class.getName(), it.next());
+    assertEquals(Object.class.getName(), it.next());
+  }
+}