Correctly process @SkipInterfaceValidation on RequestContext methods

Also adds @Retention(RUNTIME) to @ExtraTypes to allow for external,
reflection-based, Deobfuscator.Builder generators.

Change-Id: I346b2b7f5688b765f9b9968d3ef065db4a236915
diff --git a/requestfactory/build.xml b/requestfactory/build.xml
index bbb9b87..89c1340 100755
--- a/requestfactory/build.xml
+++ b/requestfactory/build.xml
@@ -122,6 +122,7 @@
       <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.MethodProvidedByServiceLayerTest.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" />
diff --git a/user/src/com/google/web/bindery/requestfactory/apt/DeobfuscatorBuilder.java b/user/src/com/google/web/bindery/requestfactory/apt/DeobfuscatorBuilder.java
index e826efd..44cd2be 100644
--- a/user/src/com/google/web/bindery/requestfactory/apt/DeobfuscatorBuilder.java
+++ b/user/src/com/google/web/bindery/requestfactory/apt/DeobfuscatorBuilder.java
@@ -72,6 +72,7 @@
         String requestContextBinaryName =
             state.elements.getBinaryName(requestContextElement).toString();
         String clientMethodDescriptor = x.asType().accept(new DescriptorBuilder(), state);
+        String domainMethodDescriptor = null;
         ExecutableElement domainElement = (ExecutableElement) state.getClientToDomainMap().get(x);
         if (domainElement == null) {
           /*
@@ -80,13 +81,12 @@
            * the server by running ValidationTool.
            */
           if (state.mustResolveAllAnnotations()) {
-            state.poison(requestContextElement, Messages
+            state.poison(x, Messages
                 .deobfuscatorMissingContext(requestContextElement.getSimpleName()));
           }
-          return super.visitExecutable(x, state);
+        } else {
+          domainMethodDescriptor = domainElement.asType().accept(new DescriptorBuilder(), state);
         }
-        String domainMethodDescriptor =
-            domainElement.asType().accept(new DescriptorBuilder(), state);
         String methodName = x.getSimpleName().toString();
 
         OperationKey key =
@@ -94,7 +94,9 @@
         println("withOperation(new OperationKey(\"%s\"),", key.get());
         println("  new OperationData.Builder()");
         println("  .withClientMethodDescriptor(\"%s\")", clientMethodDescriptor);
-        println("  .withDomainMethodDescriptor(\"%s\")", domainMethodDescriptor);
+        if (domainMethodDescriptor != null) {
+          println("  .withDomainMethodDescriptor(\"%s\")", domainMethodDescriptor);
+        }
         println("  .withMethodName(\"%s\")", methodName);
         println("  .withRequestContext(\"%s\")", requestContextBinaryName);
         println("  .build());");
diff --git a/user/src/com/google/web/bindery/requestfactory/apt/DomainChecker.java b/user/src/com/google/web/bindery/requestfactory/apt/DomainChecker.java
index 4fcec55..d0fef2b 100644
--- a/user/src/com/google/web/bindery/requestfactory/apt/DomainChecker.java
+++ b/user/src/com/google/web/bindery/requestfactory/apt/DomainChecker.java
@@ -187,8 +187,13 @@
       // Did not find a service method
       StringBuilder sb = new StringBuilder();
       sb.append(returnType).append(" ").append(name).append("(");
+      boolean first = true;
       for (TypeMirror param : lookFor) {
+        if (!first) {
+          sb.append(", ");
+        }
         sb.append(param);
+        first = false;
       }
       sb.append(")");
 
diff --git a/user/src/com/google/web/bindery/requestfactory/shared/ExtraTypes.java b/user/src/com/google/web/bindery/requestfactory/shared/ExtraTypes.java
index 0273c97..557a5b1 100644
--- a/user/src/com/google/web/bindery/requestfactory/shared/ExtraTypes.java
+++ b/user/src/com/google/web/bindery/requestfactory/shared/ExtraTypes.java
@@ -16,6 +16,8 @@
 package com.google.web.bindery.requestfactory.shared;
 
 import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
 import java.lang.annotation.Target;
 
 /**
@@ -24,6 +26,7 @@
  * include additional polymorphic proxy types that are not explicitly
  * referenced.
  */
+@Retention(RetentionPolicy.RUNTIME)
 @Target(ElementType.TYPE)
 public @interface ExtraTypes {
   Class<? extends BaseProxy>[] value();
diff --git a/user/test/com/google/web/bindery/requestfactory/gwt/RequestFactorySuite.gwt.xml b/user/test/com/google/web/bindery/requestfactory/gwt/RequestFactorySuite.gwt.xml
index 5ec1e8b..e0fb9c2 100644
--- a/user/test/com/google/web/bindery/requestfactory/gwt/RequestFactorySuite.gwt.xml
+++ b/user/test/com/google/web/bindery/requestfactory/gwt/RequestFactorySuite.gwt.xml
@@ -20,5 +20,5 @@
   <inherits name='com.google.gwt.junit.JUnit'/>
   <inherits name='com.google.gwt.json.JSON'/>
   <servlet path='/gwtRequest'
-    class='com.google.web.bindery.requestfactory.server.RequestFactoryServlet' />
+    class='com.google.web.bindery.requestfactory.server.TestRequestFactoryServlet' />
 </module>
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 70dbf61..7d97052 100644
--- a/user/test/com/google/web/bindery/requestfactory/gwt/RequestFactorySuite.java
+++ b/user/test/com/google/web/bindery/requestfactory/gwt/RequestFactorySuite.java
@@ -30,6 +30,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.MethodProvidedByServiceLayerTest;
 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;
@@ -51,6 +52,7 @@
     suite.addTestSuite(FindServiceTest.class);
     suite.addTestSuite(JsonRpcRequestFactoryTest.class);
     suite.addTestSuite(LocatorTest.class);
+    suite.addTestSuite(MethodProvidedByServiceLayerTest.class);
     suite.addTestSuite(MultipleFactoriesTest.class);
     suite.addTestSuite(RequestFactoryTest.class);
     suite.addTestSuite(RequestFactoryChainedContextTest.class);
diff --git a/user/test/com/google/web/bindery/requestfactory/server/MethodProvidedByServiceLayerJreTest.java b/user/test/com/google/web/bindery/requestfactory/server/MethodProvidedByServiceLayerJreTest.java
new file mode 100644
index 0000000..0113e76
--- /dev/null
+++ b/user/test/com/google/web/bindery/requestfactory/server/MethodProvidedByServiceLayerJreTest.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright 2014 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.gwt.http.client.Request;
+import com.google.web.bindery.requestfactory.shared.InstanceRequest;
+import com.google.web.bindery.requestfactory.shared.MethodProvidedByServiceLayerTest;
+import com.google.web.bindery.requestfactory.vm.impl.OperationKey;
+
+import java.lang.reflect.Method;
+
+/**
+ * JRE version of {@link MethodProvidedByServiceLayerTest}.
+ */
+public class MethodProvidedByServiceLayerJreTest extends MethodProvidedByServiceLayerTest {
+
+  static class Decorator extends ServiceLayerDecorator {
+    private static final String MISSING_DOMAIN_METHOD;
+    private static final String MISSING_DOMAIN_TYPE;
+    private static final String MISSING_DOMAIN_TYPE_INSTANCE_METHOD;
+
+    private static String getTypeDescriptor(Class<?> clazz) {
+      return "L" + clazz.getName().replace('.', '/') + ";";
+    }
+
+    static {
+      String proxyTypeDescriptor = getTypeDescriptor(Proxy.class);
+      String requestTypeDescriptor = getTypeDescriptor(Request.class);
+      MISSING_DOMAIN_METHOD =
+          new OperationKey(Context.class.getName(), "missingDomainMethod", "("
+              + getTypeDescriptor(String.class) + ")" + requestTypeDescriptor).get();
+      MISSING_DOMAIN_TYPE =
+          new OperationKey(Context.class.getName(), "missingDomainType", "(" + proxyTypeDescriptor
+              + ")" + requestTypeDescriptor).get();
+      MISSING_DOMAIN_TYPE_INSTANCE_METHOD =
+          new OperationKey(Context.class.getName(), "missingDomainTypeInstanceMethod", "()"
+              + getTypeDescriptor(InstanceRequest.class)).get();
+    }
+
+    @Override
+    public Method resolveDomainMethod(String operation) {
+      if (MISSING_DOMAIN_METHOD.equals(operation)) {
+        try {
+          return getClass().getDeclaredMethod("echo", String.class);
+        } catch (NoSuchMethodException e) {
+          return this.die(e, "Cannot find " + getClass().getCanonicalName() + "::echo method");
+        }
+      } else if (MISSING_DOMAIN_TYPE.equals(operation)) {
+        try {
+          return SimpleFoo.class.getDeclaredMethod("echo", SimpleFoo.class);
+        } catch (NoSuchMethodException e) {
+          return this.die(e, "Cannot find " + SimpleFoo.class.getCanonicalName() + "::echo method");
+        }
+      } else if (MISSING_DOMAIN_TYPE_INSTANCE_METHOD.equals(operation)) {
+        try {
+          return SimpleFoo.class.getDeclaredMethod("persistAndReturnSelf");
+        } catch (NoSuchMethodException e) {
+          return this.die(e, "Cannot find " + SimpleFoo.class.getCanonicalName()
+              + "::persistAndReturnSelf method");
+        }
+      }
+      return super.resolveDomainMethod(operation);
+    }
+
+    @Override
+    public Class<?> resolveDomainClass(Class<?> clazz) {
+      if (Proxy.class.equals(clazz)) {
+        return SimpleFoo.class;
+      }
+      return super.resolveDomainClass(clazz);
+    }
+
+    @Override
+    public <T> Class<? extends T> resolveClientType(Class<?> domainClass, Class<T> clientType,
+        boolean required) {
+      if (SimpleFoo.class.equals(domainClass) && Proxy.class.equals(clientType)) {
+        return Proxy.class.asSubclass(clientType);
+      }
+      return super.resolveClientType(domainClass, clientType, required);
+    }
+
+    public static final String echo(String s) {
+      return s;
+    }
+  }
+
+  @Override
+  public String getModuleName() {
+    return null;
+  }
+
+  @Override
+  protected Factory createFactory() {
+    return RequestFactoryJreTest.createInProcess(Factory.class);
+  }
+}
diff --git a/user/test/com/google/web/bindery/requestfactory/server/RequestFactoryJreTest.java b/user/test/com/google/web/bindery/requestfactory/server/RequestFactoryJreTest.java
index 04a3685..4d531a2 100644
--- a/user/test/com/google/web/bindery/requestfactory/server/RequestFactoryJreTest.java
+++ b/user/test/com/google/web/bindery/requestfactory/server/RequestFactoryJreTest.java
@@ -44,7 +44,7 @@
         throw new RuntimeException(e);
       }
     } else {
-      ServiceLayer serviceLayer = ServiceLayer.create();
+      ServiceLayer serviceLayer = ServiceLayer.create(new MethodProvidedByServiceLayerJreTest.Decorator());
       SimpleRequestProcessor processor = new SimpleRequestProcessor(serviceLayer);
       req.initialize(eventBus, new InProcessRequestTransport(processor));
     }
diff --git a/user/test/com/google/web/bindery/requestfactory/server/TestRequestFactoryServlet.java b/user/test/com/google/web/bindery/requestfactory/server/TestRequestFactoryServlet.java
new file mode 100644
index 0000000..ea75e6f
--- /dev/null
+++ b/user/test/com/google/web/bindery/requestfactory/server/TestRequestFactoryServlet.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2014 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;
+
+/**
+ * RequestFactoryServlet with special ServiceLayerDecorator(s).
+ */
+public class TestRequestFactoryServlet extends RequestFactoryServlet {
+
+  public TestRequestFactoryServlet() {
+    super(new DefaultExceptionHandler(), new MethodProvidedByServiceLayerJreTest.Decorator());
+  }
+}
diff --git a/user/test/com/google/web/bindery/requestfactory/shared/MethodProvidedByServiceLayerTest.java b/user/test/com/google/web/bindery/requestfactory/shared/MethodProvidedByServiceLayerTest.java
new file mode 100644
index 0000000..894431e
--- /dev/null
+++ b/user/test/com/google/web/bindery/requestfactory/shared/MethodProvidedByServiceLayerTest.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright 2014 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;
+
+/**
+ * Tests advanced usage of RequestFactory where a ServiceLayerDecorator
+ * provides a service method at runtime, skipping interface validation at
+ * compile-time.
+ */
+public class MethodProvidedByServiceLayerTest extends GWTTestCase {
+
+  /** The factory under test. */
+  public interface Factory extends RequestFactory {
+    Context context();
+  }
+
+  /**
+   * RequestContext whose actual server-side methods will be provided
+   * dynamically at runtime by a ServiceLayerDecorator.
+   * <p>
+   * Note: the {@link SkipInterfaceValidation} is put on each method to test
+   * that it's actually looked up at that location (it was searched on the
+   * RequestContext only at some point).
+   */
+  @Service(ServiceImpl.class)
+  public interface Context extends RequestContext {
+    @SkipInterfaceValidation
+    Request<String> missingDomainMethod(String string);
+
+    // mapped to SimpleFoo#echo(SimpleFoo)
+    @SkipInterfaceValidation
+    Request<Proxy> missingDomainType(Proxy proxy);
+
+    // mapped to SimpleFoo#persistAndReturnSelf
+    @SkipInterfaceValidation
+    InstanceRequest<Proxy, Proxy> missingDomainTypeInstanceMethod();
+  }
+
+  /** Proxy for an inexistent domain class; mapped at runtime to SimpleFoo. */
+  @SkipInterfaceValidation
+  @ProxyForName("does.not.exist")
+  public interface Proxy extends EntityProxy {
+  }
+
+  /**
+   * Dummy service for interface validation.
+   * <p>
+   * All actual service methods are provided at runtime by
+   * MethodProvidedByServiceLayerJreTest.Decorator.
+   */
+  public static class ServiceImpl {
+  }
+
+  private static final int TEST_DELAY = 5000;
+
+  private Factory factory;
+
+  @Override
+  public String getModuleName() {
+    return "com.google.web.bindery.requestfactory.gwt.RequestFactorySuite";
+  }
+
+  protected Factory createFactory() {
+    Factory toReturn = GWT.create(Factory.class);
+    toReturn.initialize(new SimpleEventBus());
+    return toReturn;
+  }
+
+  public void testMissingDomainMethod() {
+    delayTestFinish(TEST_DELAY);
+    Context ctx = context();
+    ctx.missingDomainMethod("foo").fire(new Receiver<String>() {
+      @Override
+      public void onSuccess(String response) {
+        assertEquals("foo", response);
+        finishTest();
+      }
+    });
+  }
+
+  public void testMissingDomainType() {
+    delayTestFinish(TEST_DELAY);
+    Context ctx = context();
+    final Proxy proxy = ctx.create(Proxy.class);
+    ctx.missingDomainType(proxy).fire(new Receiver<Proxy>() {
+      @Override
+      public void onSuccess(Proxy response) {
+        // we only check that the call succeeds
+        finishTest();
+      }
+    });
+  }
+
+  public void testMissingDomainTypeInstanceMethod() {
+    delayTestFinish(TEST_DELAY);
+    Context ctx = context();
+    final Proxy proxy = ctx.create(Proxy.class);
+    ctx.missingDomainTypeInstanceMethod().using(proxy).fire(new Receiver<Proxy>() {
+      @Override
+      public void onSuccess(Proxy response) {
+        // we only check that the call succeeds
+        finishTest();
+      }
+    });
+  }
+
+  @Override
+  protected void gwtSetUp() {
+    factory = createFactory();
+  }
+
+  private Context context() {
+    return factory.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 591b92c..c5f80a7 100644
--- a/user/test/com/google/web/bindery/requestfactory/vm/RequestFactoryJreSuite.java
+++ b/user/test/com/google/web/bindery/requestfactory/vm/RequestFactoryJreSuite.java
@@ -21,6 +21,7 @@
 import com.google.web.bindery.requestfactory.server.FindServiceJreTest;
 import com.google.web.bindery.requestfactory.server.JsonRpcRequestFactoryJreTest;
 import com.google.web.bindery.requestfactory.server.LocatorJreTest;
+import com.google.web.bindery.requestfactory.server.MethodProvidedByServiceLayerJreTest;
 import com.google.web.bindery.requestfactory.server.MultipleFactoriesJreTest;
 import com.google.web.bindery.requestfactory.server.RequestFactoryChainedContextJreTest;
 import com.google.web.bindery.requestfactory.server.RequestFactoryExceptionPropagationJreTest;
@@ -50,6 +51,7 @@
     suite.addTestSuite(FindServiceJreTest.class);
     suite.addTestSuite(JsonRpcRequestFactoryJreTest.class);
     suite.addTestSuite(LocatorJreTest.class);
+    suite.addTestSuite(MethodProvidedByServiceLayerJreTest.class);
     suite.addTestSuite(MultipleFactoriesJreTest.class);
     suite.addTestSuite(RequestFactoryChainedContextJreTest.class);
     suite.addTestSuite(RequestFactoryExceptionPropagationJreTest.class);