Tag 2.4.0 release

git-svn-id: https://google-web-toolkit.googlecode.com/svn/tags/2.4.0@10590 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/eclipse/samples/DynaTableRf/.classpath b/eclipse/samples/DynaTableRf/.classpath
index f2c5ce7..3bdab67 100644
--- a/eclipse/samples/DynaTableRf/.classpath
+++ b/eclipse/samples/DynaTableRf/.classpath
@@ -7,5 +7,10 @@
 	<classpathentry kind="var" path="GWT_TOOLS/lib/javax/validation/validation-api-1.0.0.GA.jar" sourcepath="/GWT_TOOLS/lib/javax/validation/validation-api-1.0.0.GA-sources.jar"/>
 	<classpathentry kind="var" path="GWT_TOOLS/lib/javax/validation/validation-api-1.0.0.GA-sources.jar"/>
 	<classpathentry kind="var" path="GWT_TOOLS/redist/json/r2_20080312/json-1.5.jar"/>
+	<classpathentry kind="src" path=".apt_generated">
+		<attributes>
+			<attribute name="optional" value="true"/>
+		</attributes>
+	</classpathentry>
 	<classpathentry kind="output" path="war/WEB-INF/classes"/>
 </classpath>
diff --git a/eclipse/samples/DynaTableRf/.factorypath b/eclipse/samples/DynaTableRf/.factorypath
new file mode 100644
index 0000000..3602e92
--- /dev/null
+++ b/eclipse/samples/DynaTableRf/.factorypath
@@ -0,0 +1,3 @@
+<factorypath>
+    <factorypathentry kind="VARJAR" id="GWT_TOOLS/lib/requestfactory/requestfactory-apt.jar" enabled="true" runInBatchMode="false"/>
+</factorypath>
diff --git a/requestfactory/build.xml b/requestfactory/build.xml
index 8b3ed84..28b786e 100755
--- a/requestfactory/build.xml
+++ b/requestfactory/build.xml
@@ -12,13 +12,15 @@
     <delete file="${gwt.build.lib}/requestfactory-server.jar" />
     <delete file="${gwt.build.lib}/requestfactory-server-src.jar" />
     <delete file="${gwt.build.lib}/requestfactory-server+src.jar" />
-    <delete file="${gwt.build.lib}/requestfactory-test+src.jar" />
+    <delete file="${gwt.build.lib}/requestfactory-test.jar" />
+    <delete file="${gwt.build.lib}/requestfactory-test-src.jar" />
+    <delete file="${gwt.build.lib}/requestfactory-test-validated.jar" />
   </target>
 
   <!-- Build a jar file containing a subset of requestfactory -->
   <macrodef name="requestfactory-jar">
     <!--
-         "target" should be one of {client,server,all}[(+|-)src] or test+src.
+         "target" should be one of {client,server,all}[(+|-)src] or test[-src].
          -src includes .java files only, +src includes .java and .class files
     -->
     <attribute name="target" default="client"/>
@@ -45,6 +47,11 @@
     <requestfactory-jar target="apt"/>
   </target>
 
+  <!-- Useful for Android testing -->
+  <target name="requestfactory-all" description="Build RequestFactory all jar">
+    <requestfactory-jar target="all"/>
+  </target>
+
   <target name="requestfactory-client" description="Build RequestFactory client jar">
     <requestfactory-jar target="client"/>
   </target>
@@ -70,8 +77,9 @@
   </target>
 
   <!-- This target requires classes from ../build/{dev,user}/bin-test -->
-  <target name="requestfactory-test+src" description="Build RequestFactory test source/class jar">
-    <requestfactory-jar target="test+src" />
+  <target name="requestfactory-test" description="Build RequestFactory test source/class jar">
+    <requestfactory-jar target="test" />
+    <requestfactory-jar target="test-src" />
   </target>
 
   <!-- Build all client jars -->
@@ -88,7 +96,28 @@
   <!-- Run RequestFactoryJreSuite from the requestfactory-test+src jar.
        Assumes test classes have been built in the trunk directory
   -->
-  <target name="test" depends="requestfactory-test+src" description="Run RequestFactoryJreSuite">
+  <target name="test" depends="requestfactory-test" description="Run RequestFactoryJreSuite">
+    <!-- There's no direct dependency on the Deobfuscator builders, so this gives us an opportunity
+         to test the post-compilation ValidationTool to generate the metadata from the precompiled
+         class files. If a new RequestFactory types is added without the argument list below being
+         updated, a runtime error will occur giving the name of the RequestFactory that needs to
+         be validated.
+    -->
+    <java failonerror="true" fork="true"
+      classname="com.google.web.bindery.requestfactory.apt.ValidationTool" >
+      <classpath>
+        <fileset dir="${gwt.build.lib}" includes="requestfactory-apt.jar" />
+        <fileset dir="${gwt.build.lib}" includes="requestfactory-client.jar" />
+        <fileset dir="${gwt.build.lib}" includes="requestfactory-test.jar" />
+      </classpath>
+      <arg path="${gwt.build.lib}/requestfactory-test-validated.jar" />
+      <arg value="com.google.web.bindery.requestfactory.gwt.client.RequestFactoryPolymorphicTest.Factory" />
+      <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.ServiceInheritanceTest$Factory" />
+      <arg value="com.google.web.bindery.requestfactory.shared.SimpleRequestFactory" />
+    </java>
     <java failonerror="true" fork="true"
       classname="com.google.web.bindery.requestfactory.vm.RequestFactoryJreSuite">
       <jvmarg value="-Xss8m" />
@@ -100,9 +129,9 @@
         <fileset dir="${gwt.tools.lib}" includes="hibernate/validator/hibernate-validator-4.1.0.Final.jar" />
         <fileset dir="${gwt.tools.lib}" includes="javax/validation/validation-api-1.0.0.GA.jar" />
         <fileset dir="${gwt.tools.lib}" includes="javax/xml/bind/jaxb-api-2.1.jar" />
-        <fileset dir="${gwt.build.lib}" includes="requestfactory-apt.jar" />
-        <fileset dir="${gwt.build.lib}" includes="requestfactory-test+src.jar" />
-
+        <fileset dir="${gwt.build.lib}" includes="requestfactory-test.jar" />
+        <fileset dir="${gwt.build.lib}" includes="requestfactory-test-src.jar" />
+        <fileset dir="${gwt.build.lib}" includes="requestfactory-test-validated.jar" />
       </classpath>
     </java>
   </target>
diff --git a/samples/dynatablerf/build.xml b/samples/dynatablerf/build.xml
index 25d3a2d..9ffc6b7 100755
--- a/samples/dynatablerf/build.xml
+++ b/samples/dynatablerf/build.xml
@@ -5,6 +5,10 @@
   <import file="../common.ant.xml" />
   <!-- these are after the common.ant.xml so they have gwt.tools etc -->
   <path id="sample.extraclasspath">
+    <!-- Run the RequestFactory annotation processor when compiling the classes.
+         See http://code.google.com/p/google-web-toolkit/wiki/RequestFactoryInterfaceValidation
+    -->
+    <pathelement location="${gwt.build.lib}/requestfactory-apt.jar" />
     <pathelement location="${gwt.tools.lib}/javax/validation/validation-api-1.0.0.GA.jar" />
     <pathelement location="${gwt.tools.lib}/javax/validation/validation-api-1.0.0.GA-sources.jar" />
   </path>
diff --git a/samples/mobilewebapp/pom.xml b/samples/mobilewebapp/pom.xml
index 773988b..616b5b8 100644
--- a/samples/mobilewebapp/pom.xml
+++ b/samples/mobilewebapp/pom.xml
@@ -65,9 +65,7 @@
          of the GWT compiler.
 
          This dependency has a scope of "provided" so that it only gets used as a
-         compiler dependecy. The Maven GWT Plugin does not seem to honor this scoping,
-         though. For that reason we explicitly remove gwt-dev-*.jar from the produced
-         artifacts later on.
+         compiler dependecy.
     -->
     <dependency>
       <groupId>com.google.gwt</groupId>
@@ -190,8 +188,8 @@
       <plugin>
         <groupId>org.codehaus.mojo</groupId>
         <artifactId>gwt-maven-plugin</artifactId>
-	<version>2.2.0</version>
-	<dependencies>
+        <version>2.3.0-1</version>
+        <dependencies>
           <dependency>
             <groupId>com.google.gwt</groupId>
             <artifactId>gwt-user</artifactId>
@@ -207,7 +205,7 @@
             <artifactId>gwt-servlet</artifactId>
             <version>${gwtVersion}</version>
           </dependency>
-	</dependencies>
+        </dependencies>
         <!-- JS is only needed in the package phase, this speeds up testing --> 
         <executions>
           <execution>
@@ -226,7 +224,7 @@
           <hostedWebapp>target/www</hostedWebapp>
           <!-- Ask GWT to create the Story of Your Compile (SOYC) (gwt:compile) -->
           <compileReport>true</compileReport>
-	  <module>com.google.gwt.sample.mobilewebapp.MobileWebApp</module>
+          <module>com.google.gwt.sample.mobilewebapp.MobileWebApp</module>
 
           <appEngineVersion>${gae.version}</appEngineVersion>
           <appEngineHome>${gae.home}</appEngineHome>
@@ -258,7 +256,7 @@
       <!-- Add source folders to test classpath in order to run gwt-tests as normal junit-tests -->
       <plugin>
         <artifactId>maven-surefire-plugin</artifactId>
-	<version>2.5</version>
+        <version>2.5</version>
         <configuration>
           <additionalClasspathElements>
             <additionalClasspathElement>${project.build.sourceDirectory}</additionalClasspathElement>
@@ -276,7 +274,33 @@
           </systemProperties>
         </configuration>
       </plugin>
-      
+
+      <!-- Run the RequestFactory ValidationTool -->
+      <plugin>
+        <groupId>org.codehaus.mojo</groupId>
+        <artifactId>exec-maven-plugin</artifactId>
+        <version>1.2</version>
+        <executions>
+          <execution>
+            <phase>process-classes</phase>
+            <configuration>
+              <id>VerifyRequestFactoryInterfaces</id>
+              <executable>java</executable>
+              <arguments>
+                <argument>-cp</argument>
+                <classpath />
+                <argument>com.google.web.bindery.requestfactory.apt.ValidationTool</argument>
+                <argument>${project.build.outputDirectory}</argument>
+                <argument>com.google.gwt.sample.mobilewebapp.shared.MobileWebAppRequestFactory</argument>
+              </arguments>
+            </configuration>
+            <goals>
+              <goal>exec</goal>
+            </goals>
+          </execution>
+        </executions>
+      </plugin>
+
       <!-- Copy static web files before executing gwt:run -->
       <plugin>
         <artifactId>maven-resources-plugin</artifactId>
@@ -300,9 +324,9 @@
       </plugin>
 
       <plugin>
-	<artifactId>maven-eclipse-plugin</artifactId>
-	<version>2.8</version>
-	<configuration>
+        <artifactId>maven-eclipse-plugin</artifactId>
+        <version>2.8</version>
+        <configuration>
           <downloadSources>true</downloadSources>
           <downloadJavadocs>false</downloadJavadocs>
           <wtpversion>2.0</wtpversion>
@@ -315,45 +339,51 @@
             <projectnature>com.google.gwt.eclipse.core.gwtNature</projectnature>
             <projectnature>com.google.appengine.eclipse.core.gaeNature</projectnature>
           </additionalProjectnatures>
-	</configuration>
-      </plugin>
-
-      <plugin>
-        <!-- Don't deploy gwt-user nor gwt-dev jars to GAE.
-             These jars are needed to compile the
-             project and for DevMode, but not in AppEngine.
-        -->
-        <artifactId>maven-clean-plugin</artifactId>
-        <version>2.3</version>
-        <executions>
-          <execution>
-            <id>default-clean</id>
-            <phase>clean</phase>
-            <goals>
-              <goal>clean</goal>
-            </goals>
-          </execution>
-          <execution>
-            <id>remove-gwt-dev-jar</id>
-            <phase>process-classes</phase>
-            <goals>
-              <goal>clean</goal>
-            </goals>
-            <configuration>
-              <excludeDefaultDirectories>true</excludeDefaultDirectories>
-              <filesets>
-                <fileset>
-                  <directory>${project.build.directory}/${project.build.finalName}/WEB-INF/lib</directory>
-                  <includes>
-                    <include>gwt-dev*jar</include>
-                    <include>gwt-user*jar</include>
-                  </includes>
-                </fileset>
-              </filesets>
-            </configuration>
-          </execution>
-        </executions>
+        </configuration>
       </plugin>
     </plugins>
+
+    <pluginManagement>
+      <plugins>
+        <!--This plugin's configuration is used to store Eclipse m2e settings only. It has no influence on the Maven build itself.-->
+        <plugin>
+          <groupId>org.eclipse.m2e</groupId>
+          <artifactId>lifecycle-mapping</artifactId>
+          <version>1.0.0</version>
+          <configuration>
+            <lifecycleMappingMetadata>
+              <pluginExecutions>
+                <pluginExecution>
+                  <pluginExecutionFilter>
+                    <groupId>org.datanucleus</groupId>
+                    <artifactId>maven-datanucleus-plugin</artifactId>
+                    <versionRange>[1.1.4,)</versionRange>
+                    <goals>
+                      <goal>enhance</goal>
+                    </goals>
+                  </pluginExecutionFilter>
+                  <action>
+                    <ignore></ignore>
+                  </action>
+                </pluginExecution>
+                <pluginExecution>
+                  <pluginExecutionFilter>
+                    <groupId>net.kindleit</groupId>
+                    <artifactId>maven-gae-plugin</artifactId>
+                    <versionRange>[0.7.3,)</versionRange>
+                    <goals>
+                      <goal>unpack</goal>
+                    </goals>
+                  </pluginExecutionFilter>
+                  <action>
+                    <execute />
+                  </action>
+                </pluginExecution>
+              </pluginExecutions>
+            </lifecycleMappingMetadata>
+          </configuration>
+        </plugin>
+      </plugins>
+    </pluginManagement>
   </build>
 </project>
diff --git a/samples/mobilewebapp/src/main/webapp/WEB-INF/web.xml b/samples/mobilewebapp/src/main/webapp/WEB-INF/web.xml
index a8d039f..7490828 100644
--- a/samples/mobilewebapp/src/main/webapp/WEB-INF/web.xml
+++ b/samples/mobilewebapp/src/main/webapp/WEB-INF/web.xml
@@ -1,9 +1,9 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE web-app
-    PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
-    "http://java.sun.com/dtd/web-app_2_3.dtd">
-
-<web-app>
+<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee 
+              http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
+         version="2.5"
+         xmlns="http://java.sun.com/xml/ns/javaee">
 
   <!--
     Require login. We only require login for the MobileWebApp.html page (instead of *.html) because
@@ -36,10 +36,10 @@
 
   <filter>
     <filter-name>GaeAuthFilter</filter-name>
-    <description>
+    <!--
       This filter demonstrates making GAE authentication
       services visible to a RequestFactory client.
-    </description>
+    -->
     <filter-class>com.google.gwt.sample.gaerequest.server.GaeAuthFilter</filter-class>
   </filter>
   <filter-mapping>
diff --git a/user/build.xml b/user/build.xml
index 63a0eaa..69cfaaa 100755
--- a/user/build.xml
+++ b/user/build.xml
@@ -151,7 +151,9 @@
   <target name="precompile.modules" depends="compile">
     <outofdate>
       <sourcefiles>
-        <fileset dir="${gwt.root}/user/src" />
+        <fileset dir="${gwt.root}/user/src" >
+          <exclude name="com/google/web/bindery/requestfactory/apt/**"/>
+        </fileset>
         <fileset dir="${gwt.root}/user/super" />
         <fileset dir="${gwt.root}/dev/core/src" />
         <fileset dir="${gwt.root}/dev/core/super" />
diff --git a/user/src/com/google/gwt/user/tools/templates/sample/_warFolder_/WEB-INF/web.xmlsrc b/user/src/com/google/gwt/user/tools/templates/sample/_warFolder_/WEB-INF/web.xmlsrc
index fa54208..d35c414 100644
--- a/user/src/com/google/gwt/user/tools/templates/sample/_warFolder_/WEB-INF/web.xmlsrc
+++ b/user/src/com/google/gwt/user/tools/templates/sample/_warFolder_/WEB-INF/web.xmlsrc
@@ -1,10 +1,10 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE web-app
-    PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
-    "http://java.sun.com/dtd/web-app_2_3.dtd">
+<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee 
+              http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
+         version="2.5"
+         xmlns="http://java.sun.com/xml/ns/javaee">
 
-<web-app>
-  
   <!-- Servlets -->
   <servlet>
     <servlet-name>greetServlet</servlet-name>
diff --git a/user/src/com/google/web/bindery/requestfactory/apt/ClientToDomainMapper.java b/user/src/com/google/web/bindery/requestfactory/apt/ClientToDomainMapper.java
new file mode 100644
index 0000000..dd1f28c
--- /dev/null
+++ b/user/src/com/google/web/bindery/requestfactory/apt/ClientToDomainMapper.java
@@ -0,0 +1,154 @@
+/*
+ * 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
+ * 
+ * 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.apt;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+
+import javax.lang.model.element.ElementKind;
+import javax.lang.model.element.TypeElement;
+import javax.lang.model.type.DeclaredType;
+import javax.lang.model.type.NoType;
+import javax.lang.model.type.PrimitiveType;
+import javax.lang.model.type.TypeKind;
+import javax.lang.model.type.TypeMirror;
+import javax.lang.model.type.TypeVariable;
+import javax.lang.model.type.WildcardType;
+
+/**
+ * Uses information in a State object to convert client types to their domain
+ * equivalents. This types assumes that any incoming type has already been
+ * determined to be a transportable type.
+ */
+class ClientToDomainMapper extends TypeVisitorBase<TypeMirror> {
+  public static class UnmappedTypeException extends RuntimeException {
+    private final TypeMirror clientType;
+
+    public UnmappedTypeException() {
+      super();
+      clientType = null;
+    }
+
+    public UnmappedTypeException(TypeMirror clientType) {
+      super("No domain type resolved for " + clientType.toString());
+      this.clientType = clientType;
+    }
+
+    public TypeMirror getClientType() {
+      return clientType;
+    }
+  }
+
+  @Override
+  public TypeMirror visitDeclared(DeclaredType x, State state) {
+    if (x.asElement().getKind().equals(ElementKind.ENUM)) {
+      // Enums map to enums
+      return x;
+    }
+    if (state.types.isAssignable(x, state.entityProxyType)
+        || state.types.isAssignable(x, state.valueProxyType)) {
+      // FooProxy -> FooDomain
+      /*
+       * TODO(bobv): This if statement should be widened to baseProxy to support
+       * heterogenous collections of any proxy type. The BaseProxy interface
+       * would also need to be annotated with an @ProxyFor mapping. This can be
+       * done once RFIV is removed, since it only allows homogenous collections.
+       */
+      TypeElement domainType =
+          (TypeElement) state.getClientToDomainMap().get(state.types.asElement(x));
+      if (domainType == null) {
+        return defaultAction(x, state);
+      }
+      return domainType.asType();
+    }
+    if (state.types.isAssignable(x, state.entityProxyIdType)) {
+      // EntityProxyId<FooProxy> -> FooDomain
+      return convertSingleParamType(x, state.entityProxyIdType, 0, state);
+    }
+    if (state.types.isAssignable(x, state.requestType)) {
+      // Request<FooProxy> -> FooDomain
+      return convertSingleParamType(x, state.requestType, 0, state);
+    }
+    if (state.types.isAssignable(x, state.instanceRequestType)) {
+      // InstanceRequest<FooProxy, X> -> FooDomain
+      return convertSingleParamType(x, state.instanceRequestType, 1, state);
+    }
+    for (DeclaredType valueType : getValueTypes(state)) {
+      if (state.types.isAssignable(x, valueType)) {
+        // Value types map straight through
+        return x;
+      }
+    }
+    if (state.types.isAssignable(x, state.findType(List.class))
+        || state.types.isAssignable(x, state.findType(Set.class))) {
+      // Convert Set,List<FooProxy> to Set,List<FooDomain>
+      TypeMirror param = convertSingleParamType(x, state.findType(Collection.class), 0, state);
+      return state.types.getDeclaredType((TypeElement) state.types.asElement(x), param);
+    }
+    return defaultAction(x, state);
+  }
+
+  @Override
+  public TypeMirror visitNoType(NoType x, State state) {
+    if (x.getKind().equals(TypeKind.VOID)) {
+      // Pass void through
+      return x;
+    }
+    // Here, x would be NONE or PACKAGE, neither of which make sense
+    return defaultAction(x, state);
+  }
+
+  @Override
+  public TypeMirror visitPrimitive(PrimitiveType x, State state) {
+    // Primitives pass through
+    return x;
+  }
+
+  @Override
+  public TypeMirror visitTypeVariable(TypeVariable x, State state) {
+    // Convert <T extends FooProxy> to FooDomain
+    return x.getUpperBound().accept(this, state);
+  }
+
+  @Override
+  public TypeMirror visitWildcard(WildcardType x, State state) {
+    // Convert <? extends FooProxy> to FooDomain
+    return state.types.erasure(x).accept(this, state);
+  }
+
+  /**
+   * Utility method to convert a {@code Foo<BarProxy> -> BarDomain}. The
+   * {@code param} parameter specifies the index of the type paramater to
+   * extract.
+   */
+  protected TypeMirror convertSingleParamType(DeclaredType x, DeclaredType convertTo, int param,
+      State state) {
+    DeclaredType converted = (DeclaredType) State.viewAs(convertTo, x, state);
+    if (converted == null) {
+      return state.types.getNoType(TypeKind.NONE);
+    }
+    if (converted.getTypeArguments().isEmpty()) {
+      return defaultAction(x, state);
+    }
+    return converted.getTypeArguments().get(param).accept(this, state);
+  }
+
+  @Override
+  protected TypeMirror defaultAction(TypeMirror x, State state) {
+    throw new UnmappedTypeException(x);
+  }
+}
diff --git a/user/src/com/google/web/bindery/requestfactory/apt/DeobfuscatorBuilder.java b/user/src/com/google/web/bindery/requestfactory/apt/DeobfuscatorBuilder.java
new file mode 100644
index 0000000..2b8f2b1
--- /dev/null
+++ b/user/src/com/google/web/bindery/requestfactory/apt/DeobfuscatorBuilder.java
@@ -0,0 +1,237 @@
+/*
+ * 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
+ * 
+ * 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.apt;
+
+import com.google.web.bindery.requestfactory.vm.impl.Deobfuscator;
+import com.google.web.bindery.requestfactory.vm.impl.OperationData;
+import com.google.web.bindery.requestfactory.vm.impl.OperationKey;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.util.Arrays;
+import java.util.Map;
+import java.util.Set;
+import java.util.SortedSet;
+import java.util.TreeMap;
+import java.util.TreeSet;
+
+import javax.lang.model.element.Element;
+import javax.lang.model.element.ExecutableElement;
+import javax.lang.model.element.TypeElement;
+import javax.tools.JavaFileObject;
+
+/**
+ * Visits a RequestFactory to create its associated DeobfuscatorBuilder, a
+ * self-configuring subtype of
+ * {@link com.google.web.bindery.requestfactory.vm.impl.Deobfuscator.Builder}
+ * which provides the ServiceLayer with type- and method-mapping information.
+ */
+class DeobfuscatorBuilder extends ScannerBase<Void> {
+  private TypeElement requestFactoryElement;
+  private final StringBuilder sb = new StringBuilder();
+
+  public String toString() {
+    return sb.toString();
+  }
+
+  /**
+   * Examine a method defined within a RequestFactory.
+   */
+  @Override
+  public Void visitExecutable(ExecutableElement x, State state) {
+    if (shouldIgnore(x, state)) {
+      return null;
+    }
+    final TypeElement requestContextElement =
+        (TypeElement) state.types.asElement(x.getReturnType());
+    new ScannerBase<Void>() {
+
+      /**
+       * Scan a method within a RequestContext.
+       */
+      @Override
+      public Void visitExecutable(ExecutableElement x, State state) {
+        if (shouldIgnore(x, state)) {
+          return null;
+        }
+        String requestContextBinaryName =
+            state.elements.getBinaryName(requestContextElement).toString();
+        String clientMethodDescriptor = x.asType().accept(new DescriptorBuilder(), state);
+        ExecutableElement domainElement = (ExecutableElement) state.getClientToDomainMap().get(x);
+        if (domainElement == null) {
+          /*
+           * No mapping from the client to domain type, probably because of an
+           * unresolved ServiceName annotation. This can be fixed when building
+           * the server by running ValidationTool.
+           */
+          if (state.mustResolveAllAnnotations()) {
+            state.poison(requestContextElement, Messages
+                .deobfuscatorMissingContext(requestContextElement.getSimpleName()));
+          }
+          return super.visitExecutable(x, state);
+        }
+        String domainMethodDescriptor =
+            domainElement.asType().accept(new DescriptorBuilder(), state);
+        String methodName = x.getSimpleName().toString();
+
+        OperationKey key =
+            new OperationKey(requestContextBinaryName, methodName, clientMethodDescriptor);
+        println("withOperation(new OperationKey(\"%s\"),", key.get());
+        println("  new OperationData.Builder()");
+        println("  .withClientMethodDescriptor(\"%s\")", clientMethodDescriptor);
+        println("  .withDomainMethodDescriptor(\"%s\")", domainMethodDescriptor);
+        println("  .withMethodName(\"%s\")", methodName);
+        println("  .withRequestContext(\"%s\")", requestContextBinaryName);
+        println("  .build());");
+        return super.visitExecutable(x, state);
+      }
+
+      /**
+       * Scan a RequestContext.
+       */
+      @Override
+      public Void visitType(TypeElement x, State state) {
+        scanAllInheritedMethods(x, state);
+        return null;
+      }
+    }.scan(requestContextElement, state);
+    return null;
+  }
+
+  /**
+   * Scan a RequestFactory type.
+   */
+  @Override
+  public Void visitType(TypeElement x, State state) {
+    requestFactoryElement = x;
+    String simpleName = computeSimpleName(x, state);
+    String packageName = state.elements.getPackageOf(x).getQualifiedName().toString();
+
+    println("// Automatically Generated -- DO NOT EDIT");
+    println("// %s", state.elements.getBinaryName(x));
+    println("package %s;", packageName);
+    println("import %s;", Arrays.class.getCanonicalName());
+    println("import %s;", OperationData.class.getCanonicalName());
+    println("import %s;", OperationKey.class.getCanonicalName());
+    println("public final class %s extends %s {", simpleName, Deobfuscator.Builder.class
+        .getCanonicalName());
+    println("{");
+    scanAllInheritedMethods(x, state);
+    writeTypeAndTokenMap(state);
+    println("}}");
+
+    // Don't write the deobfuscator if something has gone wrong.
+    if (state.isPoisoned()) {
+      return null;
+    }
+
+    try {
+      JavaFileObject obj = state.filer.createSourceFile(packageName + "." + simpleName, x);
+      Writer w = obj.openWriter();
+      w.write(sb.toString());
+      w.close();
+    } catch (Exception e) {
+      StringWriter sw = new StringWriter();
+      e.printStackTrace(new PrintWriter(sw));
+      state.poison(x, sw.toString());
+    }
+    return null;
+  }
+
+  private String computeSimpleName(TypeElement x, State state) {
+    // See constants in Deobfuscator
+    String simpleName = state.elements.getBinaryName(x).toString() + "DeobfuscatorBuilder";
+    if (state.isClientOnly()) {
+      simpleName += "Lite";
+    }
+    simpleName = simpleName.substring(simpleName.lastIndexOf('.') + 1);
+    return simpleName;
+  }
+
+  private void println(String line, Object... args) {
+    sb.append(String.format(line, args)).append("\n");
+  }
+
+  /**
+   * Write calls to {@code withRawTypeToken} and
+   * {@code withClientToDomainMappings}.
+   */
+  private void writeTypeAndTokenMap(State state) {
+    /*
+     * A map and its comparator to build up the mapping between domain types and
+     * the client type(s) that it is mapped to.
+     */
+    TypeComparator comparator = new TypeComparator(state);
+    Map<TypeElement, SortedSet<TypeElement>> domainToClientMappings =
+        new TreeMap<TypeElement, SortedSet<TypeElement>>(comparator);
+    // Map accumulated by previous visitors
+    Map<Element, Element> clientToDomainMap = state.getClientToDomainMap();
+    // Get all types used by the RequestFactory
+    Set<TypeElement> referredTypes = ReferredTypesCollector.collect(requestFactoryElement, state);
+
+    for (TypeElement clientType : referredTypes) {
+      // Ignare non-proxy types
+      if (!state.types.isAssignable(clientType.asType(), state.baseProxyType)) {
+        continue;
+      }
+      String binaryName = state.elements.getBinaryName(clientType).toString();
+      // withRawTypeToken("1234ABC", "com.example.FooProxy");
+      println("withRawTypeToken(\"%s\", \"%s\");", OperationKey.hash(binaryName), binaryName);
+
+      TypeElement domainType = (TypeElement) clientToDomainMap.get(clientType);
+      if (domainType == null) {
+        /*
+         * Missing proxy mapping, probably due to an unresolved ProxyForName. If
+         * we're running as part of a build tool or an IDE, the
+         * mustResolveAllAnnotations() call below will return false. The
+         * isMappingRequired() check avoid false-positives on proxy supertypes
+         * that extend BaseProxy but that don't declare a ProxyFor annotation.
+         */
+        if (state.mustResolveAllAnnotations() && state.isMappingRequired(domainType)) {
+          state.poison(clientType, Messages.deobfuscatorMissingProxy(clientType.getSimpleName()));
+        }
+        continue;
+      }
+      // Generic get-and-add code
+      SortedSet<TypeElement> clientTypes = domainToClientMappings.get(domainType);
+      if (clientTypes == null) {
+        clientTypes = new TreeSet<TypeElement>(comparator);
+        domainToClientMappings.put(domainType, clientTypes);
+      }
+      clientTypes.add(clientType);
+    }
+
+    for (Map.Entry<TypeElement, SortedSet<TypeElement>> entry : domainToClientMappings.entrySet()) {
+      // Arrays.asList("com.example.FooView1Proxy", "com.example.FooView2Proxy")
+      StringBuilder list = new StringBuilder("Arrays.asList(");
+      boolean needsComma = false;
+      for (TypeElement elt : entry.getValue()) {
+        if (needsComma) {
+          list.append(", ");
+        } else {
+          needsComma = true;
+        }
+        list.append('"').append(state.elements.getBinaryName(elt)).append('"');
+      }
+      list.append(")");
+
+      // withClientToDomainMappings("com.example.Domain", Arrays.asList(...))
+      println("withClientToDomainMappings(\"%s\", %s);", state.elements.getBinaryName(entry
+          .getKey()), list);
+    }
+  }
+}
diff --git a/user/src/com/google/web/bindery/requestfactory/apt/DescriptorBuilder.java b/user/src/com/google/web/bindery/requestfactory/apt/DescriptorBuilder.java
new file mode 100644
index 0000000..0b2ac40
--- /dev/null
+++ b/user/src/com/google/web/bindery/requestfactory/apt/DescriptorBuilder.java
@@ -0,0 +1,117 @@
+/*
+ * 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
+ * 
+ * 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.apt;
+
+import com.google.gwt.core.ext.typeinfo.JniConstants;
+import com.google.gwt.dev.util.Name.BinaryName;
+
+import javax.lang.model.element.TypeElement;
+import javax.lang.model.type.ArrayType;
+import javax.lang.model.type.DeclaredType;
+import javax.lang.model.type.ExecutableType;
+import javax.lang.model.type.NoType;
+import javax.lang.model.type.PrimitiveType;
+import javax.lang.model.type.TypeKind;
+import javax.lang.model.type.TypeMirror;
+import javax.lang.model.type.TypeVariable;
+import javax.lang.model.type.WildcardType;
+import javax.lang.model.util.SimpleTypeVisitor6;
+
+/**
+ * Builds descriptors from TypeMirrors for both simple types and methods. Used
+ * by {@link DeobfuscatorBuilder} to construct client-to-server method mappings.
+ */
+class DescriptorBuilder extends SimpleTypeVisitor6<String, State> {
+
+  /**
+   * Arrays aren't actually used anywhere in RequestFactory, but it's trivial to
+   * implement and might be useful later on.
+   */
+  @Override
+  public String visitArray(ArrayType x, State state) {
+    return "[" + x.getComponentType().accept(this, state);
+  }
+
+  @Override
+  public String visitDeclared(DeclaredType x, State state) {
+    return "L"
+        + BinaryName.toInternalName(state.elements.getBinaryName((TypeElement) x.asElement())
+            .toString()) + ";";
+  }
+
+  /**
+   * Only generates the method descriptor, which does not include the method's
+   * name.
+   */
+  @Override
+  public String visitExecutable(ExecutableType x, State state) {
+    StringBuilder sb = new StringBuilder();
+    sb.append("(");
+    for (TypeMirror param : x.getParameterTypes()) {
+      sb.append(param.accept(this, state));
+    }
+    sb.append(")");
+    sb.append(x.getReturnType().accept(this, state));
+    return sb.toString();
+  }
+
+  @Override
+  public String visitNoType(NoType x, State state) {
+    if (x.getKind().equals(TypeKind.VOID)) {
+      return "V";
+    }
+    // The mythical NONE or PACKAGE type
+    return super.visitNoType(x, state);
+  }
+
+  @Override
+  public String visitPrimitive(PrimitiveType x, State state) {
+    switch (x.getKind()) {
+      case BOOLEAN:
+        return String.valueOf(JniConstants.DESC_BOOLEAN);
+      case BYTE:
+        return String.valueOf(JniConstants.DESC_BYTE);
+      case CHAR:
+        return String.valueOf(JniConstants.DESC_CHAR);
+      case DOUBLE:
+        return String.valueOf(JniConstants.DESC_DOUBLE);
+      case FLOAT:
+        return String.valueOf(JniConstants.DESC_FLOAT);
+      case INT:
+        return String.valueOf(JniConstants.DESC_INT);
+      case LONG:
+        return String.valueOf(JniConstants.DESC_LONG);
+      case SHORT:
+        return String.valueOf(JniConstants.DESC_SHORT);
+    }
+    return super.visitPrimitive(x, state);
+  }
+
+  @Override
+  public String visitTypeVariable(TypeVariable x, State state) {
+    return state.types.erasure(x).accept(this, state);
+  }
+
+  @Override
+  public String visitWildcard(WildcardType x, State state) {
+    return state.types.erasure(x).accept(this, state);
+  }
+
+  @Override
+  protected String defaultAction(TypeMirror x, State state) {
+    throw new RuntimeException("Unhandled type: " + x.toString());
+  }
+}
diff --git a/user/src/com/google/web/bindery/requestfactory/apt/DomainChecker.java b/user/src/com/google/web/bindery/requestfactory/apt/DomainChecker.java
new file mode 100644
index 0000000..5f5839f
--- /dev/null
+++ b/user/src/com/google/web/bindery/requestfactory/apt/DomainChecker.java
@@ -0,0 +1,394 @@
+/*
+ * 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
+ * 
+ * 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.apt;
+
+import com.google.web.bindery.requestfactory.apt.ClientToDomainMapper.UnmappedTypeException;
+import com.google.web.bindery.requestfactory.shared.ProxyFor;
+import com.google.web.bindery.requestfactory.shared.ProxyForName;
+import com.google.web.bindery.requestfactory.shared.Service;
+import com.google.web.bindery.requestfactory.shared.ServiceName;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+
+import javax.lang.model.element.ExecutableElement;
+import javax.lang.model.element.Modifier;
+import javax.lang.model.element.Name;
+import javax.lang.model.element.TypeElement;
+import javax.lang.model.type.ExecutableType;
+import javax.lang.model.type.MirroredTypeException;
+import javax.lang.model.type.TypeKind;
+import javax.lang.model.type.TypeMirror;
+import javax.lang.model.util.ElementFilter;
+
+/**
+ * Checks client to domain mappings.
+ */
+class DomainChecker extends ScannerBase<Void> {
+
+  /**
+   * Attempt to find the most specific method that conforms to a given
+   * signature.
+   */
+  static class MethodFinder extends ScannerBase<ExecutableElement> {
+    private TypeElement domainType;
+    private ExecutableElement found;
+    private final boolean boxReturnType;
+    private final CharSequence name;
+    private final TypeMirror returnType;
+    private final List<TypeMirror> params;
+
+    public MethodFinder(CharSequence name, TypeMirror returnType, List<TypeMirror> params,
+        boolean boxReturnType, State state) {
+      this.boxReturnType = boxReturnType;
+      this.name = name;
+      this.returnType = TypeSimplifier.simplify(returnType, boxReturnType, state);
+      List<TypeMirror> temp = new ArrayList<TypeMirror>(params.size());
+      for (TypeMirror param : params) {
+        temp.add(TypeSimplifier.simplify(param, false, state));
+      }
+      this.params = Collections.unmodifiableList(temp);
+    }
+
+    @Override
+    public ExecutableElement visitExecutable(ExecutableElement domainMethodElement, State state) {
+      // Quick check for name, paramer count, and return type assignability
+      if (domainMethodElement.getSimpleName().contentEquals(name)
+          && domainMethodElement.getParameters().size() == params.size()) {
+        // Pick up parameterizations in domain type
+        ExecutableType domainMethod = viewIn(domainType, domainMethodElement, state);
+
+        boolean returnTypeMatches;
+        if (returnType == null) {
+          /*
+           * This condition is for methods that we don't really care about the
+           * domain return types (for getId(), getVersion()).
+           */
+          returnTypeMatches = true;
+        } else {
+          TypeMirror domainReturn =
+              TypeSimplifier.simplify(domainMethod.getReturnType(), boxReturnType, state);
+          // The isSameType handles the NONE case.
+          returnTypeMatches = state.types.isSubtype(domainReturn, returnType);
+        }
+        if (returnTypeMatches) {
+          boolean paramsMatch = true;
+          Iterator<TypeMirror> lookFor = params.iterator();
+          Iterator<? extends TypeMirror> domainParam = domainMethod.getParameterTypes().iterator();
+          while (lookFor.hasNext()) {
+            assert domainParam.hasNext();
+            TypeMirror requestedType = lookFor.next();
+            TypeMirror paramType = TypeSimplifier.simplify(domainParam.next(), false, state);
+            if (!state.types.isSubtype(requestedType, paramType)) {
+              paramsMatch = false;
+            }
+          }
+
+          if (paramsMatch) {
+            // Keep most-specific method signature
+            if (found == null
+                || state.types.isSubsignature(domainMethod, (ExecutableType) found.asType())) {
+              found = domainMethodElement;
+            }
+          }
+        }
+      }
+
+      return found;
+    }
+
+    @Override
+    public ExecutableElement visitType(TypeElement domainType, State state) {
+      this.domainType = domainType;
+      return scanAllInheritedMethods(domainType, state);
+    }
+  }
+
+  /**
+   * This is used as the target for errors since generic methods show up as
+   * synthetic elements that don't correspond to any source.
+   */
+  private TypeElement checkedElement;
+  private boolean currentTypeIsProxy;
+  private TypeElement domainElement;
+  private boolean requireInstanceDomainMethods;
+  private boolean requireStaticDomainMethods;
+
+  @Override
+  public Void visitExecutable(ExecutableElement clientMethodElement, State state) {
+    if (shouldIgnore(clientMethodElement, state)) {
+      return null;
+    }
+    // Ignore overrides of stableId() in proxies
+    Name name = clientMethodElement.getSimpleName();
+    if (currentTypeIsProxy && name.contentEquals("stableId")
+        && clientMethodElement.getParameters().isEmpty()) {
+      return null;
+    }
+
+    ExecutableType clientMethod = viewIn(checkedElement, clientMethodElement, state);
+    List<TypeMirror> lookFor = new ArrayList<TypeMirror>();
+    // Convert client method signature to domain types
+    TypeMirror returnType;
+    try {
+      returnType = convertToDomainTypes(clientMethod, lookFor, clientMethodElement, state);
+    } catch (UnmappedTypeException e) {
+      /*
+       * Unusual: this would happen if a RequestContext for which we have a
+       * resolved domain service method uses unresolved proxy types. For
+       * example, the RequestContext uses a @Service annotation, while one or
+       * more proxy types use @ProxyForName("") and specify a domain type not
+       * available to the compiler.
+       */
+      return null;
+    }
+
+    ExecutableElement domainMethod;
+    if (currentTypeIsProxy && isSetter(clientMethodElement, state)) {
+      // Look for void setFoo(...)
+      domainMethod =
+          new MethodFinder(name, state.types.getNoType(TypeKind.VOID), lookFor, false, state).scan(
+              domainElement, state);
+      if (domainMethod == null) {
+        // Try a builder style
+        domainMethod =
+            new MethodFinder(name, domainElement.asType(), lookFor, false, state).scan(
+                domainElement, state);
+      }
+    } else {
+      /*
+       * The usual case for getters and all service methods. Only box return
+       * types when matching context methods since there's a significant
+       * semantic difference between a null Integer and 0.
+       */
+      domainMethod =
+          new MethodFinder(name, returnType, lookFor, !currentTypeIsProxy, state).scan(
+              domainElement, state);
+    }
+
+    if (domainMethod == null) {
+      // Did not find a service method
+      StringBuilder sb = new StringBuilder();
+      sb.append(returnType).append(" ").append(name).append("(");
+      for (TypeMirror param : lookFor) {
+        sb.append(param);
+      }
+      sb.append(")");
+
+      state.poison(clientMethodElement, Messages.domainMissingMethod(sb));
+      return null;
+    }
+    
+     /*
+     * Check the domain method for any requirements for it to be static.
+     * InstanceRequests assume instance methods on the domain type.
+     */
+    boolean isInstanceRequest =
+        state.types.isSubtype(clientMethod.getReturnType(), state.instanceRequestType);
+
+    if ((isInstanceRequest || requireInstanceDomainMethods)
+        && domainMethod.getModifiers().contains(Modifier.STATIC)) {
+      state.poison(checkedElement, Messages.domainMethodWrongModifier(false, domainMethod
+          .getSimpleName()));
+    }
+    if (!isInstanceRequest && requireStaticDomainMethods
+        && !domainMethod.getModifiers().contains(Modifier.STATIC)) {
+      state.poison(checkedElement, Messages.domainMethodWrongModifier(true, domainMethod
+          .getSimpleName()));
+    }
+
+    // Record the mapping
+    state.addMapping(clientMethodElement, domainMethod);
+    return null;
+  }
+
+  @Override
+  public Void visitType(TypeElement clientTypeElement, State state) {
+    TypeMirror clientType = clientTypeElement.asType();
+    checkedElement = clientTypeElement;
+    boolean isEntityProxy = state.types.isSubtype(clientType, state.entityProxyType);
+    currentTypeIsProxy = isEntityProxy || state.types.isSubtype(clientType, state.valueProxyType);
+    domainElement = (TypeElement) state.getClientToDomainMap().get(clientTypeElement);
+    if (domainElement == null) {
+      // A proxy with an unresolved domain type (e.g. ProxyForName(""))
+      return null;
+    }
+
+    requireInstanceDomainMethods = false;
+    requireStaticDomainMethods = false;
+
+    if (currentTypeIsProxy) {
+      // Require domain property methods to be instance methods
+      requireInstanceDomainMethods = true;
+      if (!hasProxyLocator(clientTypeElement, state)) {
+        // Domain types without a Locator should have a no-arg constructor
+        if (!hasNoArgConstructor(domainElement)) {
+          state.warn(clientTypeElement, Messages.domainNoDefaultConstructor(domainElement
+              .getSimpleName(), clientTypeElement.getSimpleName(), state.requestContextType
+              .asElement().getSimpleName()));
+        }
+
+        /*
+         * Check for getId(), getVersion(), and findFoo() for any type that
+         * extends EntityProxy, but not on EntityProxy itself, since EntityProxy
+         * is mapped to java.lang.Object.
+         */
+        if (isEntityProxy && !state.types.isSameType(clientType, state.entityProxyType)) {
+          checkDomainEntityMethods(state);
+        }
+      }
+    } else if (!hasServiceLocator(clientTypeElement, state)) {
+      /*
+       * Otherwise, we're looking at a RequestContext. If it doesn't have a
+       * ServiceLocator, all methods must be static.
+       */
+      requireStaticDomainMethods = true;
+    }
+
+    scanAllInheritedMethods(clientTypeElement, state);
+    return null;
+  }
+
+  /**
+   * Check that {@code getId()} and {@code getVersion()} exist and that they are
+   * non-static. Check that {@code findFoo()} exists, is static, returns an
+   * appropriate type, and its parameter is assignable from the return value
+   * from {@code getId()}.
+   */
+  private void checkDomainEntityMethods(State state) {
+    ExecutableElement getId =
+        new MethodFinder("getId", null, Collections.<TypeMirror> emptyList(), false, state).scan(
+            domainElement, state);
+    if (getId == null) {
+      state.poison(checkedElement, Messages.domainNoGetId(domainElement.asType()));
+    } else {
+      if (getId.getModifiers().contains(Modifier.STATIC)) {
+        state.poison(checkedElement, Messages.domainGetIdStatic());
+      }
+
+      // Can only check findFoo() if we have a getId
+      ExecutableElement find =
+          new MethodFinder("find" + domainElement.getSimpleName(), domainElement.asType(),
+              Collections.singletonList(getId.getReturnType()), false, state).scan(domainElement,
+              state);
+      if (find == null) {
+        state.warn(checkedElement, Messages.domainMissingFind(domainElement.asType(), domainElement
+            .getSimpleName(), getId.getReturnType(), checkedElement.getSimpleName()));
+      } else if (!find.getModifiers().contains(Modifier.STATIC)) {
+        state.poison(checkedElement, Messages.domainFindNotStatic(domainElement.getSimpleName()));
+      }
+    }
+
+    ExecutableElement getVersion =
+        new MethodFinder("getVersion", null, Collections.<TypeMirror> emptyList(), false, state)
+            .scan(domainElement, state);
+    if (getVersion == null) {
+      state.poison(checkedElement, Messages.domainNoGetVersion(domainElement.asType()));
+    } else if (getVersion.getModifiers().contains(Modifier.STATIC)) {
+      state.poison(checkedElement, Messages.domainGetVersionStatic());
+    }
+  }
+
+  /**
+   * Converts a client method's types to their domain counterparts.
+   * 
+   * @param clientMethod the RequestContext method to validate
+   * @param parameterAccumulator an out parameter that will be populated with
+   *          the converted paramater types
+   * @param warnTo The element to which warnings should be posted if one or more
+   *          client types cannot be converted to domain types for validation
+   * @param state the State object
+   * @throws UnmappedTypeException if one or more types used in
+   *           {@code clientMethod} cannot be resolved to domain types
+   */
+  private TypeMirror convertToDomainTypes(ExecutableType clientMethod,
+      List<TypeMirror> parameterAccumulator, ExecutableElement warnTo, State state)
+      throws UnmappedTypeException {
+    boolean error = false;
+    TypeMirror returnType;
+    try {
+      returnType = clientMethod.getReturnType().accept(new ClientToDomainMapper(), state);
+    } catch (UnmappedTypeException e) {
+      error = true;
+      returnType = null;
+      state.warn(warnTo, Messages.methodNoDomainPeer(e.getClientType(), false));
+    }
+    for (TypeMirror param : clientMethod.getParameterTypes()) {
+      try {
+        parameterAccumulator.add(param.accept(new ClientToDomainMapper(), state));
+      } catch (UnmappedTypeException e) {
+        parameterAccumulator.add(null);
+        error = true;
+        state.warn(warnTo, Messages.methodNoDomainPeer(e.getClientType(), true));
+      }
+    }
+    if (error) {
+      throw new UnmappedTypeException();
+    }
+    return returnType;
+  }
+
+  /**
+   * Looks for a no-arg constructor or no constructors at all. Instance
+   * initializers are ignored.
+   */
+  private boolean hasNoArgConstructor(TypeElement x) {
+    List<ExecutableElement> constructors = ElementFilter.constructorsIn(x.getEnclosedElements());
+    if (constructors.isEmpty()) {
+      return true;
+    }
+    for (ExecutableElement constructor : constructors) {
+      if (constructor.getParameters().isEmpty()) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  private boolean hasProxyLocator(TypeElement x, State state) {
+    ProxyFor proxyFor = x.getAnnotation(ProxyFor.class);
+    if (proxyFor != null) {
+      // See javadoc on getAnnotation
+      try {
+        proxyFor.locator();
+        throw new RuntimeException("Should not reach here");
+      } catch (MirroredTypeException expected) {
+        TypeMirror locatorType = expected.getTypeMirror();
+        return !state.types.asElement(locatorType).equals(state.locatorType.asElement());
+      }
+    }
+    ProxyForName proxyForName = x.getAnnotation(ProxyForName.class);
+    return proxyForName != null && !proxyForName.locator().isEmpty();
+  }
+
+  private boolean hasServiceLocator(TypeElement x, State state) {
+    Service service = x.getAnnotation(Service.class);
+    if (service != null) {
+      // See javadoc on getAnnotation
+      try {
+        service.locator();
+        throw new RuntimeException("Should not reach here");
+      } catch (MirroredTypeException expected) {
+        TypeMirror locatorType = expected.getTypeMirror();
+        return !state.types.asElement(locatorType).equals(state.serviceLocatorType.asElement());
+      }
+    }
+    ServiceName serviceName = x.getAnnotation(ServiceName.class);
+    return serviceName != null && !serviceName.locator().isEmpty();
+  }
+}
diff --git a/user/src/com/google/web/bindery/requestfactory/apt/ExtraTypesScanner.java b/user/src/com/google/web/bindery/requestfactory/apt/ExtraTypesScanner.java
new file mode 100644
index 0000000..a1d09f8
--- /dev/null
+++ b/user/src/com/google/web/bindery/requestfactory/apt/ExtraTypesScanner.java
@@ -0,0 +1,71 @@
+/*
+ * 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
+ * 
+ * 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.apt;
+
+import java.util.List;
+
+import javax.lang.model.element.AnnotationMirror;
+import javax.lang.model.element.AnnotationValue;
+import javax.lang.model.element.Element;
+import javax.lang.model.element.TypeElement;
+import javax.lang.model.type.TypeKind;
+import javax.lang.model.type.TypeMirror;
+
+/**
+ * Looks for {@code ExtraTypes} annotations and calls
+ * {@link #scanExtraType(TypeElement)}.
+ */
+abstract class ExtraTypesScanner<T> extends ScannerBase<T> {
+  /**
+   * Check an element for a declaration.
+   */
+  protected void checkForAnnotation(Element x, State state) {
+    // Bug similar to Eclipse 261969 makes ExtraTypes.value() unreliable.
+    for (AnnotationMirror mirror : x.getAnnotationMirrors()) {
+      if (!state.types.isSameType(mirror.getAnnotationType(), state.extraTypesAnnotation)) {
+        continue;
+      }
+      // The return of the Class[] value() method
+      AnnotationValue value = mirror.getElementValues().values().iterator().next();
+      // which is represented by a list
+      @SuppressWarnings("unchecked")
+      List<? extends AnnotationValue> valueList =
+          (List<? extends AnnotationValue>) value.getValue();
+      for (AnnotationValue clazz : valueList) {
+        TypeMirror type = (TypeMirror) clazz.getValue();
+        scanExtraType((TypeElement) state.types.asElement(type));
+      }
+    }
+  }
+
+  /**
+   * Check a type and all of its supertypes for the annotation.
+   */
+  protected void checkForAnnotation(TypeElement x, State state) {
+    // Check type's declaration
+    checkForAnnotation((Element) x, state);
+    // Look at superclass, if it exists
+    if (!x.getSuperclass().getKind().equals(TypeKind.NONE)) {
+      checkForAnnotation((TypeElement) state.types.asElement(x.getSuperclass()), state);
+    }
+    // Check super-interfaces
+    for (TypeMirror intf : x.getInterfaces()) {
+      checkForAnnotation((TypeElement) state.types.asElement(intf), state);
+    }
+  }
+
+  protected abstract void scanExtraType(TypeElement extraType);
+}
diff --git a/user/src/com/google/web/bindery/requestfactory/apt/Finder.java b/user/src/com/google/web/bindery/requestfactory/apt/Finder.java
new file mode 100644
index 0000000..0d5d79a
--- /dev/null
+++ b/user/src/com/google/web/bindery/requestfactory/apt/Finder.java
@@ -0,0 +1,44 @@
+/*
+ * 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
+ * 
+ * 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.apt;
+
+import javax.lang.model.element.ElementKind;
+import javax.lang.model.element.TypeElement;
+
+/**
+ * Looks for all types assignable to {@code RequestFactory} and adds them to the
+ * output state. This is necessary to support factory types declared as inner
+ * classes.
+ */
+class Finder extends ScannerBase<Void> {
+  @Override
+  public Void visitType(TypeElement x, State state) {
+    // Ignore anything other than interfaces
+    if (x.getKind().equals(ElementKind.INTERFACE)) {
+      if (state.types.isAssignable(x.asType(), state.requestFactoryType)) {
+        state.maybeScanFactory(x);
+      }
+      if (state.types.isAssignable(x.asType(), state.requestContextType)) {
+        state.maybeScanContext(x);
+      }
+      if (state.types.isAssignable(x.asType(), state.entityProxyType)
+          || state.types.isAssignable(x.asType(), state.valueProxyType)) {
+        state.maybeScanProxy(x);
+      }
+    }
+    return super.visitType(x, state);
+  }
+}
\ No newline at end of file
diff --git a/user/src/com/google/web/bindery/requestfactory/apt/HaltException.java b/user/src/com/google/web/bindery/requestfactory/apt/HaltException.java
new file mode 100644
index 0000000..98b0f46
--- /dev/null
+++ b/user/src/com/google/web/bindery/requestfactory/apt/HaltException.java
@@ -0,0 +1,22 @@
+/*
+ * 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
+ * 
+ * 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.apt;
+
+/**
+ * An un-logged RuntimeException used to abort processing.
+ */
+class HaltException extends RuntimeException {
+}
diff --git a/user/src/com/google/web/bindery/requestfactory/apt/Messages.java b/user/src/com/google/web/bindery/requestfactory/apt/Messages.java
new file mode 100644
index 0000000..1379596
--- /dev/null
+++ b/user/src/com/google/web/bindery/requestfactory/apt/Messages.java
@@ -0,0 +1,163 @@
+/*
+ * 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
+ * 
+ * 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.apt;
+
+import com.google.web.bindery.requestfactory.shared.JsonRpcProxy;
+import com.google.web.bindery.requestfactory.shared.JsonRpcService;
+import com.google.web.bindery.requestfactory.shared.ProxyFor;
+import com.google.web.bindery.requestfactory.shared.ProxyForName;
+import com.google.web.bindery.requestfactory.shared.Service;
+import com.google.web.bindery.requestfactory.shared.ServiceName;
+
+/**
+ * Contains string-formatting methods to produce error messages. This class
+ * exists to avoid the need to duplicate error messages in test code. All method
+ * parameters in this class accept {@code Object} so that the production code
+ * can pass {@code javax.lang.model} types and the test code can pass Strings.
+ */
+class Messages {
+  /*
+   * Note to maintainers: When new messages are added to this class, the
+   * RfValidatorTest.testErrorsAndWarnings() method should be updated to test
+   * the new message.
+   */
+
+  public static String contextMissingDomainType(Object domainTypeName) {
+    return String.format("Cannot fully validate context since domain type %s is not available.\n"
+        + "You must run the ValidationTool as part of your server build process.", domainTypeName);
+  }
+
+  public static String contextMustBeAnnotated(Object requestContextName) {
+    return String.format("The type %s must be annotated with %s, %s, or %s", requestContextName,
+        Service.class.getSimpleName(), ServiceName.class.getSimpleName(), JsonRpcService.class
+            .getSimpleName());
+  }
+
+  public static String contextRequiredReturnTypes(Object requestName, Object instanceRequestName) {
+    return String.format("The return type must be a %s or %s", requestName, instanceRequestName);
+  }
+
+  public static String deobfuscatorMissingContext(Object contextName) {
+    return String.format("Could not load domain mapping for context %s.\n"
+        + "Check that both the shared interfaces and server domain types are on the classpath.",
+        contextName);
+  }
+
+  public static String deobfuscatorMissingProxy(Object proxyName) {
+    return String.format("Could not load domain mapping for proxy %s.\n"
+        + "Check that both the shared interfaces and server domain types are on the classpath.",
+        proxyName);
+  }
+
+  public static String domainFindNotStatic(Object domainTypeName) {
+    return String.format("The domain object's find%s() method is not static", domainTypeName);
+  }
+
+  public static String domainGetIdStatic() {
+    return "The domain type's getId() method must not be static";
+  }
+
+  public static String domainGetVersionStatic() {
+    return "The domain type's getVersion() method must not be static";
+  }
+
+  public static String domainMethodWrongModifier(boolean expectStatic, Object domainMethodName) {
+    return String.format("Found %s domain method %s when %s method required", expectStatic
+        ? "instance" : "static", domainMethodName, expectStatic ? "static" : "instance");
+  }
+
+  public static String domainMissingFind(Object domainType, Object simpleName,
+      Object getIdReturnType, Object checkedTypeName) {
+    return String.format("The domain type %s has no %s find%s(%s) method. "
+        + "Attempting to send a %s to the server will result in a server error.", domainType,
+        simpleName, simpleName, getIdReturnType, checkedTypeName);
+  }
+
+  public static String domainMissingMethod(Object description) {
+    return String.format("Could not find domain method similar to %s", description);
+  }
+
+  public static String domainNoDefaultConstructor(Object domainName, Object proxyName,
+      Object requestContextName) {
+    return String.format("The domain type %s has no default constructor."
+        + " Calling %s.create(%s.class) will cause a server error.", domainName,
+        requestContextName, proxyName);
+  }
+
+  public static String domainNoGetId(Object domainType) {
+    return String.format("Domain type %s does not have a getId() method", domainType);
+  }
+
+  public static String domainNoGetVersion(Object domainType) {
+    return String.format("Domain type %s does not have a getVersion() method", domainType);
+  }
+
+  public static String factoryMustBeAssignable(Object assignableTo) {
+    return String.format("The return type of this method must return a %s", assignableTo);
+  }
+
+  public static String factoryMustReturnInterface(Object returnType) {
+    return String.format("The return type %s must be an interface", returnType);
+  }
+
+  public static String factoryNoMethodParameters() {
+    return "This method must have no parameters";
+  }
+
+  public static String methodNoDomainPeer(Object proxyTypeName, boolean isParameter) {
+    return String.format("Cannot validate this method because the domain mapping for "
+        + " %s type (%s) could not be resolved to a domain type", isParameter ? "a parameter of"
+        : "the return", proxyTypeName);
+  }
+
+  public static String noSuchType(String binaryTypeName) {
+    return String.format("Could not find root type %s", binaryTypeName);
+  }
+
+  public static String proxyMissingDomainType(Object missingDomainName) {
+    return String.format("Cannot fully validate proxy since type %s is not available",
+        missingDomainName);
+  }
+
+  public static String proxyMustBeAnnotated(Object typeName) {
+    return String.format("The proxy type %s must be annotated with %s, %s, or %s", typeName,
+        ProxyFor.class.getSimpleName(), ProxyForName.class.getSimpleName(), JsonRpcProxy.class
+            .getSimpleName());
+  }
+
+  public static String proxyOnlyGettersSetters() {
+    return "Only getters and setters allowed";
+  }
+
+  public static String rawType() {
+    return "A raw type may not be used here";
+  }
+
+  public static String redundantAnnotation(Object annotationName) {
+    return String.format("Redundant annotation: %s", annotationName);
+  }
+
+  public static String untransportableType(Object returnType) {
+    return String.format("The type %s cannot be used here", returnType);
+  }
+
+  public static String warnSuffix() {
+    return "\n\nAdd @SuppressWarnings(\"requestfactory\") to dismiss.";
+  }
+
+  private Messages() {
+  }
+}
diff --git a/user/src/com/google/web/bindery/requestfactory/apt/ProxyScanner.java b/user/src/com/google/web/bindery/requestfactory/apt/ProxyScanner.java
new file mode 100644
index 0000000..2b72edf
--- /dev/null
+++ b/user/src/com/google/web/bindery/requestfactory/apt/ProxyScanner.java
@@ -0,0 +1,101 @@
+/*
+ * 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
+ * 
+ * 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.apt;
+
+import com.google.gwt.dev.util.Name.BinaryName;
+import com.google.web.bindery.requestfactory.shared.JsonRpcProxy;
+import com.google.web.bindery.requestfactory.shared.ProxyFor;
+import com.google.web.bindery.requestfactory.shared.ProxyForName;
+
+import javax.lang.model.element.ExecutableElement;
+import javax.lang.model.element.TypeElement;
+import javax.lang.model.element.VariableElement;
+import javax.lang.model.type.MirroredTypeException;
+import javax.lang.model.type.TypeMirror;
+
+/**
+ * Examines the methods declared in a proxy interface. Also records the client
+ * to domain mapping for the proxy type.
+ */
+class ProxyScanner extends ScannerBase<Void> {
+
+  @Override
+  public Void visitExecutable(ExecutableElement x, State state) {
+    if (shouldIgnore(x, state)) {
+      return null;
+    }
+
+    if (isGetter(x, state)) {
+      TypeMirror returnType = x.getReturnType();
+      if (!state.isTransportableType(returnType)) {
+        state.poison(x, Messages.untransportableType(returnType));
+      }
+    } else if (!isSetter(x, state)) {
+      state.poison(x, Messages.proxyOnlyGettersSetters());
+    }
+    // Parameters checked by visitVariable
+    return super.visitExecutable(x, state);
+  }
+
+  @Override
+  public Void visitType(TypeElement x, State state) {
+    ProxyFor proxyFor = x.getAnnotation(ProxyFor.class);
+    ProxyForName proxyForName = x.getAnnotation(ProxyForName.class);
+    JsonRpcProxy jsonRpcProxy = x.getAnnotation(JsonRpcProxy.class);
+    if (proxyFor != null) {
+      poisonIfAnnotationPresent(state, x, proxyForName, jsonRpcProxy);
+
+      // See javadoc on Element.getAnnotation() for why it works this way
+      try {
+        proxyFor.value();
+        throw new RuntimeException("Should not reach here");
+      } catch (MirroredTypeException expected) {
+        TypeMirror type = expected.getTypeMirror();
+        state.addMapping(x, (TypeElement) state.types.asElement(type));
+      }
+    }
+    if (proxyForName != null) {
+      poisonIfAnnotationPresent(state, x, jsonRpcProxy);
+      TypeElement domain =
+          state.elements.getTypeElement(BinaryName.toSourceName(proxyForName.value()));
+      if (domain == null) {
+        state.warn(x, Messages.proxyMissingDomainType(proxyForName.value()));
+      }
+      state.addMapping(x, domain);
+    }
+
+    scanAllInheritedMethods(x, state);
+    state.checkExtraTypes(x);
+    return null;
+  }
+
+  @Override
+  public Void visitVariable(VariableElement x, State state) {
+    if (!state.isTransportableType(x.asType())) {
+      state.poison(x, Messages.untransportableType(x.asType()));
+    }
+    return super.visitVariable(x, state);
+  }
+
+  @Override
+  protected boolean shouldIgnore(ExecutableElement x, State state) {
+    // Ignore overrides of stableId()
+    if (x.getSimpleName().contentEquals("stableId") && x.getParameters().isEmpty()) {
+      return true;
+    }
+    return super.shouldIgnore(x, state);
+  }
+}
\ No newline at end of file
diff --git a/user/src/com/google/web/bindery/requestfactory/apt/ReferredTypesCollector.java b/user/src/com/google/web/bindery/requestfactory/apt/ReferredTypesCollector.java
new file mode 100644
index 0000000..a8eb5e1
--- /dev/null
+++ b/user/src/com/google/web/bindery/requestfactory/apt/ReferredTypesCollector.java
@@ -0,0 +1,145 @@
+/*
+ * 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
+ * 
+ * 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.apt;
+
+import java.util.Set;
+import java.util.SortedSet;
+import java.util.Stack;
+import java.util.TreeSet;
+
+import javax.lang.model.element.Element;
+import javax.lang.model.element.ExecutableElement;
+import javax.lang.model.element.TypeElement;
+import javax.lang.model.type.DeclaredType;
+import javax.lang.model.type.ExecutableType;
+import javax.lang.model.type.TypeMirror;
+import javax.lang.model.type.TypeVariable;
+import javax.lang.model.type.WildcardType;
+
+/**
+ * Given a RequestFactory interface, return all RequestContext and proxy types
+ * transitively referenced.
+ */
+class ReferredTypesCollector extends ExtraTypesScanner<Void> {
+
+  /**
+   * Finds TypeElements that we care about from TypeMirror API. This is used to
+   * handle generic type signatures and supertypes.
+   */
+  private class ElementFinder extends TypeVisitorBase<Void> {
+    @Override
+    public Void visitDeclared(DeclaredType x, State state) {
+      // Some generic types don't have elements
+      Element elt = state.types.asElement(x);
+      if (elt != null) {
+        ReferredTypesCollector.this.scan(elt, state);
+      }
+      // Capture List<FooProxy>
+      for (TypeMirror a : x.getTypeArguments()) {
+        a.accept(this, state);
+      }
+      return null;
+    }
+
+    @Override
+    public Void visitExecutable(ExecutableType x, State state) {
+      x.getReturnType().accept(this, state);
+      for (TypeMirror p : x.getParameterTypes()) {
+        p.accept(this, state);
+      }
+      for (TypeMirror t : x.getTypeVariables()) {
+        t.accept(this, state);
+      }
+      return null;
+    }
+
+    @Override
+    public Void visitTypeVariable(TypeVariable x, State state) {
+      return state.types.erasure(x).accept(this, state);
+    }
+
+    @Override
+    public Void visitWildcard(WildcardType x, State state) {
+      return state.types.erasure(x).accept(this, state);
+    }
+  }
+
+  /**
+   * Collect all RequestContext and proxy types reachable from the given
+   * RequestFactory.
+   */
+  public static Set<TypeElement> collect(TypeElement requestFactory, State state) {
+    ReferredTypesCollector c = new ReferredTypesCollector(state);
+    c.scan(requestFactory, state);
+    return c.seen;
+  }
+
+  private final Stack<TypeElement> currentType = new Stack<TypeElement>();
+  private final SortedSet<TypeElement> seen;
+  private final State state;
+
+  private ReferredTypesCollector(State state) {
+    seen = new TreeSet<TypeElement>(new TypeComparator(state));
+    this.state = state;
+  }
+
+  @Override
+  public Void visitExecutable(ExecutableElement x, State state) {
+    if (shouldIgnore(x, state)) {
+      return null;
+    }
+    ExecutableType xType = viewIn(currentType.peek(), x, state);
+    xType.accept(new ElementFinder(), state);
+    checkForAnnotation(x, state);
+    return null;
+  }
+
+  @Override
+  public Void visitType(TypeElement x, State state) {
+    // Only look at proxies and contexts
+    boolean isContext = state.types.isAssignable(x.asType(), state.requestContextType);
+    boolean isFactory = state.types.isAssignable(x.asType(), state.requestFactoryType);
+    boolean isProxy = state.types.isAssignable(x.asType(), state.baseProxyType);
+    if (isProxy || isFactory || isContext) {
+      currentType.push(x);
+      try {
+        // Ignore previously-seen types and factories
+        if ((isContext || isProxy) && !seen.add(x)) {
+          return null;
+        }
+        // Visit a proxy's supertypes
+        if (isProxy) {
+          x.getSuperclass().accept(new ElementFinder(), state);
+          for (TypeMirror intf : x.getInterfaces()) {
+            intf.accept(new ElementFinder(), state);
+          }
+        }
+        // Visit methods
+        scanAllInheritedMethods(x, state);
+        // Scan for extra types
+        checkForAnnotation(x, state);
+      } finally {
+        currentType.pop();
+      }
+    }
+    return null;
+  }
+
+  @Override
+  protected void scanExtraType(TypeElement extraType) {
+    scan(extraType, state);
+  }
+}
diff --git a/user/src/com/google/web/bindery/requestfactory/apt/RequestContextScanner.java b/user/src/com/google/web/bindery/requestfactory/apt/RequestContextScanner.java
new file mode 100644
index 0000000..8a07085
--- /dev/null
+++ b/user/src/com/google/web/bindery/requestfactory/apt/RequestContextScanner.java
@@ -0,0 +1,126 @@
+/*
+ * 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
+ * 
+ * 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.apt;
+
+import com.google.gwt.dev.util.Name.BinaryName;
+import com.google.web.bindery.requestfactory.shared.JsonRpcService;
+import com.google.web.bindery.requestfactory.shared.Service;
+import com.google.web.bindery.requestfactory.shared.ServiceName;
+
+import javax.lang.model.element.ExecutableElement;
+import javax.lang.model.element.TypeElement;
+import javax.lang.model.element.TypeParameterElement;
+import javax.lang.model.element.VariableElement;
+import javax.lang.model.type.DeclaredType;
+import javax.lang.model.type.MirroredTypeException;
+import javax.lang.model.type.TypeMirror;
+
+/**
+ * Scans a RequestContext declaration. This visitor will call out to the State
+ * object to validate the types that it encounters.
+ */
+class RequestContextScanner extends ScannerBase<Void> {
+
+  @Override
+  public Void visitExecutable(ExecutableElement x, State state) {
+    if (shouldIgnore(x, state)) {
+      return null;
+    }
+    TypeMirror returnType = x.getReturnType();
+    if (state.types.isAssignable(returnType, state.requestType)) {
+      // Extract Request<Foo> type
+      DeclaredType asRequest = (DeclaredType) State.viewAs(state.requestType, returnType, state);
+      if (asRequest.getTypeArguments().isEmpty()) {
+        state.poison(x, Messages.rawType());
+      } else {
+        TypeMirror requestReturn = asRequest.getTypeArguments().get(0);
+        if (!state.isTransportableType(requestReturn)) {
+          state.poison(x, Messages.untransportableType(requestReturn));
+        }
+      }
+    } else if (state.types.isAssignable(returnType, state.instanceRequestType)) {
+      // Extract InstanceRequest<FooProxy, String>
+      DeclaredType asInstanceRequest =
+          (DeclaredType) State.viewAs(state.instanceRequestType, returnType, state);
+      if (asInstanceRequest.getTypeArguments().isEmpty()) {
+        state.poison(x, Messages.rawType());
+      } else {
+        TypeMirror instanceType = asInstanceRequest.getTypeArguments().get(0);
+        state.maybeScanProxy((TypeElement) state.types.asElement(instanceType));
+        TypeMirror requestReturn = asInstanceRequest.getTypeArguments().get(1);
+        if (!state.isTransportableType(requestReturn)) {
+          state.poison(x, Messages.untransportableType(requestReturn));
+        }
+      }
+    } else if (isSetter(x, state)) {
+      // Parameter checked in visitVariable
+    } else {
+      state.poison(x, Messages.contextRequiredReturnTypes(state.requestType.asElement()
+          .getSimpleName(), state.instanceRequestType.asElement().getSimpleName()));
+    }
+    return super.visitExecutable(x, state);
+  }
+
+  @Override
+  public Void visitType(TypeElement x, State state) {
+    Service service = x.getAnnotation(Service.class);
+    ServiceName serviceName = x.getAnnotation(ServiceName.class);
+    JsonRpcService jsonRpcService = x.getAnnotation(JsonRpcService.class);
+    if (service != null) {
+      poisonIfAnnotationPresent(state, x, serviceName, jsonRpcService);
+
+      // See javadoc on Element.getAnnotation() for why it works this way
+      try {
+        service.value();
+        throw new RuntimeException("Should not reach here");
+      } catch (MirroredTypeException expected) {
+        TypeMirror type = expected.getTypeMirror();
+        state.addMapping(x, (TypeElement) state.types.asElement(type));
+      }
+    }
+    if (serviceName != null) {
+      poisonIfAnnotationPresent(state, x, jsonRpcService);
+      TypeElement domain =
+          state.elements.getTypeElement(BinaryName.toSourceName(serviceName.value()));
+      if (domain == null) {
+        state.warn(x, Messages.contextMissingDomainType(serviceName.value()));
+      }
+      state.addMapping(x, domain);
+    }
+
+    scanAllInheritedMethods(x, state);
+    state.checkExtraTypes(x);
+    return null;
+  }
+
+  @Override
+  public Void visitTypeParameter(TypeParameterElement x, State state) {
+    for (TypeMirror bound : x.getBounds()) {
+      if (!state.isTransportableType(bound)) {
+        state.poison(x, Messages.untransportableType(bound));
+      }
+    }
+    return super.visitTypeParameter(x, state);
+  }
+
+  @Override
+  public Void visitVariable(VariableElement x, State state) {
+    if (!state.isTransportableType(x.asType())) {
+      state.poison(x, Messages.untransportableType(x.asType()));
+    }
+    return super.visitVariable(x, state);
+  }
+}
\ No newline at end of file
diff --git a/user/src/com/google/web/bindery/requestfactory/apt/RequestFactoryScanner.java b/user/src/com/google/web/bindery/requestfactory/apt/RequestFactoryScanner.java
new file mode 100644
index 0000000..3f881f7
--- /dev/null
+++ b/user/src/com/google/web/bindery/requestfactory/apt/RequestFactoryScanner.java
@@ -0,0 +1,67 @@
+/*
+ * 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
+ * 
+ * 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.apt;
+
+import javax.lang.model.element.Element;
+import javax.lang.model.element.ElementKind;
+import javax.lang.model.element.ExecutableElement;
+import javax.lang.model.element.TypeElement;
+import javax.lang.model.type.TypeMirror;
+
+/**
+ * Scans a RequestFactory declaration for errors. This visitor will call out to
+ * the State object to validate the types that it encounters.
+ */
+class RequestFactoryScanner extends ScannerBase<Void> {
+  @Override
+  public Void visitExecutable(ExecutableElement x, State state) {
+    if (shouldIgnore(x, state)) {
+      // Ignore initializers and methods from Object and RequestFactory
+      return null;
+    }
+    if (!x.getParameters().isEmpty()) {
+      state.poison(x, Messages.factoryNoMethodParameters());
+    }
+    TypeMirror returnType = x.getReturnType();
+    if (state.types.isAssignable(returnType, state.requestContextType)) {
+      Element returnTypeElement = state.types.asElement(returnType);
+      if (!returnTypeElement.getKind().equals(ElementKind.INTERFACE)) {
+        state.poison(x, Messages.factoryMustReturnInterface(returnTypeElement.getSimpleName()));
+      } else {
+        TypeElement contextElement = (TypeElement) returnTypeElement;
+        state.maybeScanContext(contextElement);
+        state.requireMapping(contextElement);
+      }
+    } else {
+      state.poison(x, Messages.factoryMustBeAssignable(state.requestContextType.asElement()
+          .getSimpleName()));
+    }
+    return null;
+  }
+
+  @Override
+  public Void visitType(TypeElement x, State state) {
+    // Ignore RequestFactory itself
+    if (state.types.isSameType(state.requestFactoryType, x.asType())) {
+      return null;
+    }
+
+    scanAllInheritedMethods(x, state);
+    state.checkExtraTypes(x);
+    return null;
+  }
+
+}
\ No newline at end of file
diff --git a/user/src/com/google/web/bindery/requestfactory/apt/RfApt.java b/user/src/com/google/web/bindery/requestfactory/apt/RfApt.java
deleted file mode 100644
index 2afd346..0000000
--- a/user/src/com/google/web/bindery/requestfactory/apt/RfApt.java
+++ /dev/null
@@ -1,179 +0,0 @@
-/*
- * 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
- * 
- * 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.apt;
-
-import static com.google.web.bindery.requestfactory.vm.impl.TypeTokenResolver.TOKEN_MANIFEST;
-
-import com.google.web.bindery.requestfactory.shared.BaseProxy;
-import com.google.web.bindery.requestfactory.vm.impl.OperationKey;
-import com.google.web.bindery.requestfactory.vm.impl.TypeTokenResolver;
-
-import java.io.IOException;
-import java.io.PrintWriter;
-import java.util.Map;
-import java.util.Set;
-
-import javax.annotation.processing.AbstractProcessor;
-import javax.annotation.processing.Filer;
-import javax.annotation.processing.FilerException;
-import javax.annotation.processing.ProcessingEnvironment;
-import javax.annotation.processing.RoundEnvironment;
-import javax.annotation.processing.SupportedAnnotationTypes;
-import javax.annotation.processing.SupportedOptions;
-import javax.annotation.processing.SupportedSourceVersion;
-import javax.lang.model.SourceVersion;
-import javax.lang.model.element.Element;
-import javax.lang.model.element.TypeElement;
-import javax.lang.model.type.TypeMirror;
-import javax.lang.model.util.ElementFilter;
-import javax.lang.model.util.ElementScanner6;
-import javax.lang.model.util.Elements;
-import javax.lang.model.util.Types;
-import javax.tools.Diagnostic.Kind;
-import javax.tools.FileObject;
-import javax.tools.JavaFileObject;
-import javax.tools.StandardLocation;
-
-/**
- * An annotation processor that creates an obfuscated type manifest that is used
- * by {@link com.google.web.bindery.requestfactory.vm.RequestFactorySource} and
- * related implementation classes.
- */
-@SupportedAnnotationTypes("com.google.web.bindery.requestfactory.*")
-@SupportedSourceVersion(SourceVersion.RELEASE_6)
-@SupportedOptions("verbose")
-public class RfApt extends AbstractProcessor {
-
-  /**
-   * Looks for all types assignable to {@link BaseProxy} and adds them to the
-   * output state.
-   */
-  private class Finder extends ElementScanner6<Void, Void> {
-    // Only valid for a single round
-    TypeMirror baseProxyType = elements.getTypeElement(BaseProxy.class.getCanonicalName()).asType();
-
-    @Override
-    public Void visitType(TypeElement elt, Void arg1) {
-      String binaryName = elements.getBinaryName(elt).toString();
-      if (types.isSubtype(elt.asType(), baseProxyType)) {
-        String hash = OperationKey.hash(binaryName);
-        builder.addTypeToken(hash, binaryName);
-        log(elt, "Processed proxy %s %s", binaryName, hash);
-      }
-      return super.visitType(elt, arg1);
-    }
-  }
-
-  private TypeTokenResolver.Builder builder;
-  private Elements elements;
-  private Filer filer;
-  private boolean verbose;
-  private Types types;
-
-  @Override
-  public synchronized void init(ProcessingEnvironment processingEnv) {
-    super.init(processingEnv);
-    log("RfApt init");
-    elements = processingEnv.getElementUtils();
-    filer = processingEnv.getFiler();
-    types = processingEnv.getTypeUtils();
-    verbose = Boolean.parseBoolean(processingEnv.getOptions().get("verbose"));
-  }
-
-  @Override
-  public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
-    log("RequestFactory processing a round");
-
-    if (builder == null) {
-      builder = new TypeTokenResolver.Builder();
-      // Try not to obliterate existing data
-      try {
-        FileObject resource = filer.getResource(StandardLocation.CLASS_OUTPUT, "", TOKEN_MANIFEST);
-        builder.load(resource.openInputStream());
-        log("Reusing old data");
-      } catch (IOException e) {
-        // Likely because the file does not exist
-        log("Not reusing existing manifest file: " + e.getMessage());
-      }
-    }
-
-    // Extract data
-    new Finder().scan(ElementFilter.typesIn(roundEnv.getRootElements()), null);
-
-    // On the last round, write out accumulated data
-    if (roundEnv.processingOver()) {
-      TypeTokenResolver d = builder.build();
-      builder = null;
-      try {
-        FileObject res = filer.createResource(StandardLocation.CLASS_OUTPUT, "", TOKEN_MANIFEST);
-        d.store(res.openOutputStream());
-      } catch (IOException e) {
-        error("Could not write output: " + e.getMessage());
-      }
-
-      /*
-       * Getting the TOKEN_MANIFEST resource into an Android APK generated by
-       * the Android Eclipse plugin is non-trivial. (Users of ant and apkbuilder
-       * can just use -rf to include the relevant file). To support the common
-       * use-case, we'll generate a subclass of TypeTokenResolver.Builder that
-       * has all of the data already baked into it. This synthetic subtype is
-       * looked for first by TypeTokenResolver and any manifests that are on the
-       * classpath will be added on top.
-       */
-      try {
-        String packageName = TypeTokenResolver.class.getPackage().getName();
-        String simpleName = TypeTokenResolver.class.getSimpleName() + "BuilderImpl";
-        JavaFileObject classfile = filer.createSourceFile(packageName + "." + simpleName);
-        PrintWriter pw = new PrintWriter(classfile.openWriter());
-        pw.println("package " + packageName + ";");
-        pw.println("public class " + simpleName + " extends "
-            + TypeTokenResolver.Builder.class.getCanonicalName() + " {");
-        pw.println("public " + simpleName + "() {");
-        for (Map.Entry<String, String> entry : d.getAllTypeTokens().entrySet()) {
-          if (elements.getTypeElement(entry.getValue()) != null) {
-            pw.println("addTypeToken(\"" + entry.getKey() + "\", " + entry.getValue()
-                + ".class.getName());");
-          }
-        }
-        pw.println("}");
-        pw.println("}");
-        pw.close();
-      } catch (FilerException e) {
-        log("Ignoring exception: %s", e.getMessage());
-      } catch (IOException e) {
-        error("Could not write BuilderImpl: " + e.getMessage());
-      }
-      log("Finished!");
-    }
-    return false;
-  }
-
-  private void error(String message, Object... args) {
-    processingEnv.getMessager().printMessage(Kind.ERROR, "ERROR: " + String.format(message, args));
-  }
-
-  private void log(Element elt, String message, Object... args) {
-    if (verbose) {
-      processingEnv.getMessager().printMessage(Kind.NOTE, String.format(message, args), elt);
-    }
-  }
-
-  private void log(String message, Object... args) {
-    if (verbose) {
-      processingEnv.getMessager().printMessage(Kind.NOTE, String.format(message, args));
-    }
-  }
-}
diff --git a/user/src/com/google/web/bindery/requestfactory/apt/RfValidator.java b/user/src/com/google/web/bindery/requestfactory/apt/RfValidator.java
new file mode 100644
index 0000000..55b23db
--- /dev/null
+++ b/user/src/com/google/web/bindery/requestfactory/apt/RfValidator.java
@@ -0,0 +1,131 @@
+/*
+ * 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
+ * 
+ * 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.apt;
+
+import com.google.gwt.dev.util.Name.BinaryName;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import javax.annotation.processing.AbstractProcessor;
+import javax.annotation.processing.ProcessingEnvironment;
+import javax.annotation.processing.RoundEnvironment;
+import javax.annotation.processing.SupportedAnnotationTypes;
+import javax.annotation.processing.SupportedOptions;
+import javax.annotation.processing.SupportedSourceVersion;
+import javax.lang.model.SourceVersion;
+import javax.lang.model.element.TypeElement;
+import javax.lang.model.util.ElementFilter;
+
+/**
+ * The entry point for annotation validation.
+ */
+@SupportedAnnotationTypes("*")
+@SupportedSourceVersion(SourceVersion.RELEASE_6)
+@SupportedOptions({"rootOverride", "suppressErrors", "suppressWarnings", "verbose"})
+public class RfValidator extends AbstractProcessor {
+
+  private boolean clientOnly;
+  private boolean mustResolveAllMappings;
+  private List<String> rootOverride;
+  private boolean forceErrors;
+  private State state;
+
+  @Override
+  public synchronized void init(ProcessingEnvironment processingEnv) {
+    super.init(processingEnv);
+    String option = processingEnv.getOptions().get("rootOverride");
+    if (option != null) {
+      setRootOverride(Arrays.asList(option.split(",")));
+    }
+  }
+
+  @Override
+  public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
+    // Are we finished, if so, clean up
+    if (roundEnv.processingOver()) {
+      state = null;
+      return false;
+    }
+
+    // Newly initialized or being reused?
+    if (state == null) {
+      state = forceErrors ? new State.ForTesting(processingEnv) : new State(processingEnv);
+      if (state.isPoisoned()) {
+        // Could not initialize State object, bail out
+        return false;
+      }
+      // Produce a "lite" map just for JRE-only clients
+      state.setClientOnly(clientOnly);
+      // Disallow @ProxyForName or @ServiceName that can't be resolved
+      state.setMustResolveAllMappings(mustResolveAllMappings);
+    }
+
+    try {
+      // Bootstrap the State's work queue
+      new Finder().scan(getTypesToProcess(state, roundEnv), state);
+      // Execute the work items
+      state.executeJobs();
+    } catch (HaltException ignored) {
+      // Already logged. Let any unhandled RuntimeExceptions fall out.
+    }
+    return false;
+  }
+
+  public void setClientOnly(boolean clientOnly) {
+    this.clientOnly = clientOnly;
+  }
+
+  void setForceErrors(boolean forceErrors) {
+    this.forceErrors = forceErrors;
+  }
+
+  /**
+   * Make it an error to not resolve all ProxyForName and ServiceName mappings.
+   */
+  void setMustResolveAllMappings(boolean requireAll) {
+    this.mustResolveAllMappings = requireAll;
+  }
+
+  /**
+   * Instead of scanning the round's root elements, scan these type names
+   * instead. This is used by the ValidationTool to scan pre-compiled
+   * classfiles.
+   */
+  void setRootOverride(List<String> binaryTypeNames) {
+    this.rootOverride = binaryTypeNames;
+  }
+
+  private Set<TypeElement> getTypesToProcess(State state, RoundEnvironment roundEnv) {
+    if (rootOverride == null) {
+      return ElementFilter.typesIn(roundEnv.getRootElements());
+    }
+    Set<TypeElement> toScan = new HashSet<TypeElement>();
+    for (String binaryTypeName : rootOverride) {
+      TypeElement found =
+          state.elements.getTypeElement(BinaryName.toSourceName(binaryTypeName.trim()));
+      if (found == null) {
+        state.poison(null, Messages.noSuchType(binaryTypeName));
+      } else {
+        toScan.add(found);
+      }
+    }
+    rootOverride = null;
+    return toScan;
+  }
+}
diff --git a/user/src/com/google/web/bindery/requestfactory/apt/ScannerBase.java b/user/src/com/google/web/bindery/requestfactory/apt/ScannerBase.java
new file mode 100644
index 0000000..73efdb6
--- /dev/null
+++ b/user/src/com/google/web/bindery/requestfactory/apt/ScannerBase.java
@@ -0,0 +1,144 @@
+/*
+ * 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
+ * 
+ * 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.apt;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.lang.annotation.Annotation;
+import java.util.List;
+
+import javax.lang.model.element.Element;
+import javax.lang.model.element.ElementKind;
+import javax.lang.model.element.ExecutableElement;
+import javax.lang.model.element.TypeElement;
+import javax.lang.model.type.ExecutableType;
+import javax.lang.model.type.TypeKind;
+import javax.lang.model.type.TypeMirror;
+import javax.lang.model.util.ElementFilter;
+import javax.lang.model.util.ElementScanner6;
+
+/**
+ * Contains utility methods for traversing RequestFactory declarations.
+ */
+class ScannerBase<R> extends ElementScanner6<R, State> {
+
+  /**
+   * Poisons the given type if one or more of the annotation values are
+   * non-null.
+   */
+  protected static void poisonIfAnnotationPresent(State state, TypeElement x,
+      Annotation... annotations) {
+    for (Annotation a : annotations) {
+      if (a != null) {
+        state.poison(x, Messages.redundantAnnotation(a.annotationType().getSimpleName()));
+      }
+    }
+  }
+
+  protected static ExecutableType viewIn(TypeElement lookIn, ExecutableElement methodElement, State state) {
+    try {
+      return (ExecutableType) state.types.asMemberOf(state.types.getDeclaredType(lookIn),
+          methodElement);
+    } catch (IllegalArgumentException e) {
+      return (ExecutableType) methodElement.asType();
+    }
+  }
+
+  @Override
+  public final R scan(Element x, State state) {
+    try {
+      return super.scan(x, state);
+    } catch (HaltException e) {
+      throw e;
+    } catch (Throwable e) {
+      StringWriter sw = new StringWriter();
+      e.printStackTrace(new PrintWriter(sw));
+      state.poison(x, sw.toString());
+      throw new HaltException();
+    }
+  }
+
+  /**
+   * No parameters, name stars with "get" or is a boolean / Boolean isFoo hasFoo
+   * method.
+   */
+  protected boolean isGetter(ExecutableElement x, State state) {
+    String name = x.getSimpleName().toString();
+    TypeMirror returnType = x.getReturnType();
+    if (!x.getParameters().isEmpty()) {
+      return false;
+    }
+    if (name.startsWith("get")) {
+      return true;
+    }
+    if (name.startsWith("is") || name.startsWith("has")) {
+      TypeMirror javaLangBoolean =
+          state.types.boxedClass(state.types.getPrimitiveType(TypeKind.BOOLEAN)).asType();
+      if (returnType.getKind().equals(TypeKind.BOOLEAN)
+          || state.types.isSameType(returnType, javaLangBoolean)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  /**
+   * Name starts with set, has one parameter, returns either null or something
+   * assignable from the element's enclosing type.
+   */
+  protected boolean isSetter(ExecutableElement x, State state) {
+    String name = x.getSimpleName().toString();
+    TypeMirror returnType = x.getReturnType();
+
+    if (x.getParameters().size() != 1) {
+      return false;
+    }
+    if (!name.startsWith("set")) {
+      return false;
+    }
+    if (returnType.getKind().equals(TypeKind.VOID)) {
+      return true;
+    }
+    if (x.getEnclosingElement() != null
+        && state.types.isAssignable(x.getEnclosingElement().asType(), returnType)) {
+      return true;
+    }
+    return false;
+  }
+
+  protected R scanAllInheritedMethods(TypeElement x, State state) {
+    R toReturn = DEFAULT_VALUE;
+    List<ExecutableElement> methods = ElementFilter.methodsIn(state.elements.getAllMembers(x));
+    for (ExecutableElement method : methods) {
+      toReturn = scan(method, state);
+    }
+    return toReturn;
+  }
+
+  /**
+   * Ignore all static initializers and methods defined in the base
+   * RequestFactory interfaces
+   */
+  protected boolean shouldIgnore(ExecutableElement x, State state) {
+    TypeMirror enclosingType = x.getEnclosingElement().asType();
+    return x.getKind().equals(ElementKind.STATIC_INIT)
+        || state.types.isSameType(state.objectType, enclosingType)
+        || state.types.isSameType(state.requestFactoryType, enclosingType)
+        || state.types.isSameType(state.requestContextType, enclosingType)
+        || state.types.isSameType(state.entityProxyType, enclosingType)
+        || state.types.isSameType(state.valueProxyType, enclosingType);
+  }
+}
\ No newline at end of file
diff --git a/user/src/com/google/web/bindery/requestfactory/apt/State.java b/user/src/com/google/web/bindery/requestfactory/apt/State.java
new file mode 100644
index 0000000..2b2a2c4
--- /dev/null
+++ b/user/src/com/google/web/bindery/requestfactory/apt/State.java
@@ -0,0 +1,466 @@
+/*
+ * 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
+ * 
+ * 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.apt;
+
+import com.google.web.bindery.requestfactory.shared.SkipInterfaceValidation;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.SortedSet;
+import java.util.TreeSet;
+
+import javax.annotation.processing.Filer;
+import javax.annotation.processing.Messager;
+import javax.annotation.processing.ProcessingEnvironment;
+import javax.lang.model.element.Element;
+import javax.lang.model.element.ExecutableElement;
+import javax.lang.model.element.TypeElement;
+import javax.lang.model.type.DeclaredType;
+import javax.lang.model.type.TypeMirror;
+import javax.lang.model.type.TypeVariable;
+import javax.lang.model.util.Elements;
+import javax.lang.model.util.Types;
+import javax.tools.Diagnostic.Kind;
+
+class State {
+  /**
+   * Slightly tweaked implementation used when running tests.
+   */
+  static class ForTesting extends State {
+    public ForTesting(ProcessingEnvironment processingEnv) {
+      super(processingEnv);
+    }
+
+    @Override
+    boolean respectAnnotations() {
+      return false;
+    }
+  }
+
+  /**
+   * Implements comparable for priority ordering.
+   */
+  private static class Job implements Comparable<Job> {
+    private static long count;
+
+    public final TypeElement element;
+    public final ScannerBase<?> scanner;
+    private final long order = count++;
+    private final int priority;
+
+    public Job(TypeElement element, ScannerBase<?> scanner, int priority) {
+      this.element = element;
+      this.priority = priority;
+      this.scanner = scanner;
+    }
+
+    @Override
+    public int compareTo(Job o) {
+      int c = priority - o.priority;
+      if (c != 0) {
+        return c;
+      }
+      return Long.signum(order - o.order);
+    }
+
+    @Override
+    public String toString() {
+      return scanner.getClass().getSimpleName() + " " + element.getSimpleName();
+    }
+  }
+
+  /**
+   * Used to take a {@code FooRequest extends Request<Foo>} and find the
+   * {@code Request<Foo>} type.
+   */
+  static TypeMirror viewAs(DeclaredType desiredType, TypeMirror searchFrom, State state) {
+    if (!desiredType.getTypeArguments().isEmpty()) {
+      throw new IllegalArgumentException("Expecting raw type, received " + desiredType.toString());
+    }
+    Element searchElement = state.types.asElement(searchFrom);
+    switch (searchElement.getKind()) {
+      case CLASS:
+      case INTERFACE:
+      case ENUM: {
+        TypeMirror rawSearchFrom = state.types.getDeclaredType((TypeElement) searchElement);
+        if (state.types.isSameType(desiredType, rawSearchFrom)) {
+          return searchFrom;
+        }
+        for (TypeMirror s : state.types.directSupertypes(searchFrom)) {
+          TypeMirror maybe = viewAs(desiredType, s, state);
+          if (maybe != null) {
+            return maybe;
+          }
+        }
+        break;
+      }
+      case TYPE_PARAMETER: {
+        // Search <T extends Foo> as Foo
+        return viewAs(desiredType, ((TypeVariable) searchElement).getUpperBound(), state);
+      }
+    }
+    return null;
+  }
+
+  final TypeMirror baseProxyType;
+  final Elements elements;
+  final DeclaredType entityProxyIdType;
+  final DeclaredType entityProxyType;
+  final DeclaredType extraTypesAnnotation;
+  final Filer filer;
+  final DeclaredType instanceRequestType;
+  final DeclaredType locatorType;
+  final DeclaredType objectType;
+  final DeclaredType requestContextType;
+  final DeclaredType requestFactoryType;
+  final DeclaredType requestType;
+  final DeclaredType serviceLocatorType;
+  final Set<TypeElement> seen;
+  final Types types;
+  final DeclaredType valueProxyType;
+  private final Map<Element, Element> clientToDomainMain;
+  private final SortedSet<Job> jobs = new TreeSet<Job>();
+  private final Messager messager;
+  private boolean poisoned;
+  private boolean requireAllMappings;
+  private final boolean suppressErrors;
+  private final boolean suppressWarnings;
+  private final boolean verbose;
+  /**
+   * Prevents duplicate messages from being emitted.
+   */
+  private final Map<Element, Set<String>> previousMessages = new HashMap<Element, Set<String>>();
+
+  private final Set<TypeElement> typesRequiringMapping = new LinkedHashSet<TypeElement>();
+  private boolean clientOnly;
+
+  public State(ProcessingEnvironment processingEnv) {
+    clientToDomainMain = new HashMap<Element, Element>();
+    elements = processingEnv.getElementUtils();
+    filer = processingEnv.getFiler();
+    messager = processingEnv.getMessager();
+    types = processingEnv.getTypeUtils();
+    suppressErrors = Boolean.parseBoolean(processingEnv.getOptions().get("suppressErrors"));
+    suppressWarnings = Boolean.parseBoolean(processingEnv.getOptions().get("suppressWarnings"));
+    verbose = Boolean.parseBoolean(processingEnv.getOptions().get("verbose"));
+
+    baseProxyType = findType("BaseProxy");
+    entityProxyType = findType("EntityProxy");
+    entityProxyIdType = findType("EntityProxyId");
+    extraTypesAnnotation = findType("ExtraTypes");
+    instanceRequestType = findType("InstanceRequest");
+    locatorType = findType("Locator");
+    objectType = findType(Object.class);
+    requestType = findType("Request");
+    requestContextType = findType("RequestContext");
+    requestFactoryType = findType("RequestFactory");
+    seen = new HashSet<TypeElement>();
+    serviceLocatorType = findType("ServiceLocator");
+    valueProxyType = findType("ValueProxy");
+  }
+
+  /**
+   * Add a mapping from a client method to a domain method.
+   */
+  public void addMapping(ExecutableElement clientMethod, ExecutableElement domainMethod) {
+    if (domainMethod == null) {
+      debug(clientMethod, "No domain mapping");
+    } else {
+      debug(clientMethod, "Found domain method %s", domainMethod.toString());
+    }
+    clientToDomainMain.put(clientMethod, domainMethod);
+  }
+
+  /**
+   * Add a mapping from a client type to a domain type.
+   */
+  public void addMapping(TypeElement clientType, TypeElement domainType) {
+    if (domainType == null) {
+      debug(clientType, "No domain mapping");
+    } else {
+      debug(clientType, "Found domain type %s", domainType.toString());
+    }
+    clientToDomainMain.put(clientType, domainType);
+  }
+
+  /**
+   * Check an element for an {@code ExtraTypes} annotation. Handles both methods
+   * and types.
+   */
+  public void checkExtraTypes(Element x) {
+    (new ExtraTypesScanner<Void>() {
+      @Override
+      public Void visitExecutable(ExecutableElement x, State state) {
+        checkForAnnotation(x, state);
+        return null;
+      }
+
+      @Override
+      public Void visitType(TypeElement x, State state) {
+        checkForAnnotation(x, state);
+        return null;
+      }
+
+      @Override
+      protected void scanExtraType(TypeElement extraType) {
+        maybeScanProxy(extraType);
+      }
+    }).scan(x, this);
+  }
+
+  /**
+   * Print a warning message if verbose mode is enabled. A warning is used to
+   * ensure that the message shows up in Eclipse's editor (a note only makes it
+   * into the error console).
+   */
+  public void debug(Element elt, String message, Object... args) {
+    if (verbose) {
+      messager.printMessage(Kind.WARNING, String.format(message, args), elt);
+    }
+  }
+
+  public void executeJobs() {
+    while (!jobs.isEmpty()) {
+      Job job = jobs.first();
+      jobs.remove(job);
+      debug(job.element, "Scanning");
+      try {
+        job.scanner.scan(job.element, this);
+      } catch (HaltException ignored) {
+        // Already reported
+      } catch (Throwable e) {
+        StringWriter sw = new StringWriter();
+        e.printStackTrace(new PrintWriter(sw));
+        poison(job.element, sw.toString());
+      }
+    }
+    if (clientOnly) {
+      // Don't want to check for mappings in client-only mode
+      return;
+    }
+    for (TypeElement element : typesRequiringMapping) {
+      if (!getClientToDomainMap().containsKey(element)) {
+        if (types.isAssignable(element.asType(), requestContextType)) {
+          poison(element, Messages.contextMustBeAnnotated(element.getSimpleName()));
+        } else {
+          poison(element, Messages.proxyMustBeAnnotated(element.getSimpleName()));
+        }
+      }
+    }
+  }
+
+  /**
+   * Utility method to look up raw types from class literals.
+   */
+  public DeclaredType findType(Class<?> clazz) {
+    return types.getDeclaredType(elements.getTypeElement(clazz.getCanonicalName()));
+  }
+
+  /**
+   * Returns a map of client elements to their domain counterparts. The keys may
+   * be RequestContext or Proxy types or methods within those types.
+   */
+  public Map<Element, Element> getClientToDomainMap() {
+    return Collections.unmodifiableMap(clientToDomainMain);
+  }
+
+  public boolean isClientOnly() {
+    return clientOnly;
+  }
+
+  public boolean isMappingRequired(TypeElement element) {
+    return typesRequiringMapping.contains(element);
+  }
+
+  public boolean isPoisoned() {
+    return poisoned;
+  }
+
+  /**
+   * Verifies that the given type may be used with RequestFactory.
+   * 
+   * @see TransportableTypeVisitor
+   */
+  public boolean isTransportableType(TypeMirror asType) {
+    return asType.accept(new TransportableTypeVisitor(), this);
+  }
+
+  public void maybeScanContext(TypeElement requestContext) {
+    // Also ignore RequestContext itself
+    if (fastFail(requestContext) || types.isSameType(requestContextType, requestContext.asType())) {
+      return;
+    }
+    jobs.add(new Job(requestContext, new RequestContextScanner(), 0));
+    if (!clientOnly) {
+      jobs.add(new Job(requestContext, new DomainChecker(), 1));
+    }
+  }
+
+  public void maybeScanFactory(TypeElement factoryType) {
+    if (fastFail(factoryType) || types.isSameType(requestFactoryType, factoryType.asType())) {
+      return;
+    }
+    jobs.add(new Job(factoryType, new RequestFactoryScanner(), 0));
+    jobs.add(new Job(factoryType, new DeobfuscatorBuilder(), 2));
+  }
+
+  public void maybeScanProxy(TypeElement proxyType) {
+    if (fastFail(proxyType)) {
+      return;
+    }
+    jobs.add(new Job(proxyType, new ProxyScanner(), 0));
+    if (!clientOnly) {
+      jobs.add(new Job(proxyType, new DomainChecker(), 1));
+    }
+  }
+
+  public boolean mustResolveAllAnnotations() {
+    return requireAllMappings;
+  }
+
+  /**
+   * Emits a fatal error message attached to an element. If the element or an
+   * eclosing type is annotated with {@link SkipInterfaceValidation} the message
+   * will be dropped.
+   */
+  public void poison(Element elt, String message) {
+    if (suppressErrors) {
+      return;
+    }
+
+    if (squelchMessage(elt, message)) {
+      return;
+    }
+
+    if (respectAnnotations()) {
+      Element check = elt;
+      while (check != null) {
+        if (check.getAnnotation(SkipInterfaceValidation.class) != null) {
+          return;
+        }
+        check = check.getEnclosingElement();
+      }
+    }
+    poisoned = true;
+    if (elt == null) {
+      messager.printMessage(Kind.ERROR, message);
+    } else {
+      messager.printMessage(Kind.ERROR, message, elt);
+    }
+  }
+
+  public void requireMapping(TypeElement interfaceElement) {
+    typesRequiringMapping.add(interfaceElement);
+  }
+
+  /**
+   * Set to {@code true} to indicate that only JVM-client support code needs to
+   * be generated.
+   */
+  public void setClientOnly(boolean clientOnly) {
+    this.clientOnly = clientOnly;
+  }
+
+  /**
+   * Set to {@code true} if it is an error for unresolved ProxyForName and
+   * ServiceName annotations to be left over.
+   */
+  public void setMustResolveAllMappings(boolean requireAllMappings) {
+    this.requireAllMappings = requireAllMappings;
+  }
+
+  /**
+   * Emits a warning message, unless the element or an enclosing element are
+   * annotated with a {@code @SuppressWarnings("requestfactory")}.
+   */
+  public void warn(Element elt, String message) {
+    if (suppressWarnings) {
+      return;
+    }
+
+    if (squelchMessage(elt, message)) {
+      return;
+    }
+
+    if (respectAnnotations()) {
+      SuppressWarnings suppress;
+      Element check = elt;
+      while (check != null) {
+        if (check.getAnnotation(SkipInterfaceValidation.class) != null) {
+          return;
+        }
+        suppress = check.getAnnotation(SuppressWarnings.class);
+        if (suppress != null) {
+          if (Arrays.asList(suppress.value()).contains("requestfactory")) {
+            return;
+          }
+        }
+        check = check.getEnclosingElement();
+      }
+    }
+
+    messager.printMessage(Kind.WARNING, message + Messages.warnSuffix(), elt);
+  }
+
+  /**
+   * This switch allows the RfValidatorTest code to be worked on in the IDE
+   * without causing compilation failures.
+   */
+  boolean respectAnnotations() {
+    return true;
+  }
+
+  private boolean fastFail(TypeElement element) {
+    return !seen.add(element);
+  }
+
+  /**
+   * Utility method to look up raw types from the requestfactory.shared package.
+   * This method is used instead of class literals in order to minimize the
+   * number of dependencies that get packed into {@code requestfactoy-apt.jar}.
+   * If the requested type cannot be found, the State will be poisoned.
+   */
+  private DeclaredType findType(String simpleName) {
+    TypeElement element =
+        elements.getTypeElement("com.google.web.bindery.requestfactory.shared." + simpleName);
+    if (element == null) {
+      poison(null, "Unable to find RequestFactory built-in type. "
+          + "Is requestfactory-[client|server].jar on the classpath?");
+      return null;
+    }
+    return types.getDeclaredType(element);
+  }
+
+  /**
+   * Prevents duplicate messages from being emitted.
+   */
+  private boolean squelchMessage(Element elt, String message) {
+    Set<String> set = previousMessages.get(elt);
+    if (set == null) {
+      set = new HashSet<String>();
+      // HashMap allows the null key
+      previousMessages.put(elt, set);
+    }
+    return !set.add(message);
+  }
+}
\ No newline at end of file
diff --git a/user/src/com/google/web/bindery/requestfactory/apt/TransportableTypeVisitor.java b/user/src/com/google/web/bindery/requestfactory/apt/TransportableTypeVisitor.java
new file mode 100644
index 0000000..9d47dd1
--- /dev/null
+++ b/user/src/com/google/web/bindery/requestfactory/apt/TransportableTypeVisitor.java
@@ -0,0 +1,116 @@
+/*
+ * 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
+ * 
+ * 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.apt;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+
+import javax.lang.model.element.ElementKind;
+import javax.lang.model.element.TypeElement;
+import javax.lang.model.type.DeclaredType;
+import javax.lang.model.type.PrimitiveType;
+import javax.lang.model.type.TypeMirror;
+import javax.lang.model.type.TypeVariable;
+import javax.lang.model.type.WildcardType;
+
+/**
+ * Scans a TypeMirror to determine if it can be transported by RequestFactory.
+ */
+class TransportableTypeVisitor extends TypeVisitorBase<Boolean> {
+
+  /**
+   * AutoBeans supports arbitrary parameterizations, but there's work that needs
+   * to be done on the Request serialization code to support arbitrarily-complex
+   * parameterizations. For the moment, we'll disallow anything other than a
+   * one-level parameterization.
+   */
+  private boolean allowNestedParameterization = true;
+
+  @Override
+  public Boolean visitDeclared(DeclaredType t, State state) {
+    if (t.asElement().getKind().equals(ElementKind.ENUM)) {
+      return true;
+    }
+    if (state.types.isAssignable(t, state.entityProxyType)
+        || state.types.isAssignable(t, state.valueProxyType)) {
+      TypeElement proxyElement = (TypeElement) t.asElement();
+      state.maybeScanProxy(proxyElement);
+      state.requireMapping(proxyElement);
+      return true;
+    }
+    if (state.types.isAssignable(t, state.entityProxyIdType)) {
+      DeclaredType asId = (DeclaredType) State.viewAs(state.entityProxyIdType, t, state);
+      if (asId.getTypeArguments().isEmpty()) {
+        return false;
+      }
+      return asId.getTypeArguments().get(0).accept(this, state);
+    }
+    for (DeclaredType valueType : getValueTypes(state)) {
+      if (state.types.isAssignable(t, valueType)) {
+        return true;
+      }
+    }
+    if (state.types.isAssignable(t, state.findType(List.class))
+        || state.types.isAssignable(t, state.findType(Set.class))) {
+      if (!allowNestedParameterization) {
+        return false;
+      }
+      allowNestedParameterization = false;
+      DeclaredType asCollection =
+          (DeclaredType) State.viewAs(state.findType(Collection.class), t, state);
+      if (asCollection.getTypeArguments().isEmpty()) {
+        return false;
+      }
+      return t.getTypeArguments().get(0).accept(this, state);
+    }
+    return false;
+  }
+
+  @Override
+  public Boolean visitPrimitive(PrimitiveType x, State state) {
+    return true;
+  }
+
+  @Override
+  public Boolean visitTypeVariable(TypeVariable t, State state) {
+    if (t.equals(t.getUpperBound())) {
+      /*
+       * Weird case seen in Eclipse with self-parameterized type variables such
+       * as <T extends Enum<T>>.
+       * 
+       * TODO(bobv): Should intersection types be allowed at all? They don't
+       * seem to make much sense in the most-derived interface types, since the
+       * RF Generator won't know how to implement them.
+       */
+      return state.types.erasure(t).accept(this, state);
+    }
+    // Allow <T extends FooProxy>
+    return t.getUpperBound().accept(this, state);
+  }
+
+  @Override
+  public Boolean visitWildcard(WildcardType t, State state) {
+    // Allow List<? extends FooProxy>
+    return state.types.erasure(t).accept(this, state);
+  }
+
+  @Override
+  protected Boolean defaultAction(TypeMirror arg0, State arg1) {
+    return false;
+  }
+
+}
\ No newline at end of file
diff --git a/user/src/com/google/web/bindery/requestfactory/apt/TypeComparator.java b/user/src/com/google/web/bindery/requestfactory/apt/TypeComparator.java
new file mode 100644
index 0000000..4de2e8f
--- /dev/null
+++ b/user/src/com/google/web/bindery/requestfactory/apt/TypeComparator.java
@@ -0,0 +1,50 @@
+/*
+ * 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
+ * 
+ * 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.apt;
+
+import java.util.Comparator;
+
+import javax.lang.model.element.TypeElement;
+import javax.lang.model.type.DeclaredType;
+
+/**
+ * Orders TypeElements by assignability, with most-derived types ordered first,
+ * and then by name.
+ */
+class TypeComparator implements Comparator<TypeElement> {
+  private final State state;
+
+  public TypeComparator(State state) {
+    this.state = state;
+  }
+
+  @Override
+  public int compare(TypeElement a, TypeElement b) {
+    DeclaredType typeA = state.types.getDeclaredType(a);
+    DeclaredType typeB = state.types.getDeclaredType(b);
+    if (state.types.isSameType(typeA, typeB)) {
+      return 0;
+    }
+    if (state.types.isSubtype(typeA, typeB)) {
+      return -1;
+    }
+    if (state.types.isSubtype(typeB, typeA)) {
+      return 1;
+    }
+    return state.elements.getBinaryName(a).toString().compareTo(
+        state.elements.getBinaryName(b).toString());
+  }
+}
\ No newline at end of file
diff --git a/user/src/com/google/web/bindery/requestfactory/apt/TypeSimplifier.java b/user/src/com/google/web/bindery/requestfactory/apt/TypeSimplifier.java
new file mode 100644
index 0000000..676005f
--- /dev/null
+++ b/user/src/com/google/web/bindery/requestfactory/apt/TypeSimplifier.java
@@ -0,0 +1,112 @@
+/*
+ * 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
+ * 
+ * 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.apt;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.lang.model.element.TypeElement;
+import javax.lang.model.type.DeclaredType;
+import javax.lang.model.type.NoType;
+import javax.lang.model.type.PrimitiveType;
+import javax.lang.model.type.TypeKind;
+import javax.lang.model.type.TypeMirror;
+import javax.lang.model.type.TypeVariable;
+import javax.lang.model.type.WildcardType;
+import javax.lang.model.util.SimpleTypeVisitor6;
+
+/**
+ * Utility type for reducing complex type declarations to ones suitable for
+ * determining assignability based on RequestFactory's type-mapping semantics.
+ * <p>
+ * Rules:
+ * <ul>
+ * <li>primitive type {@code ->} boxed type (optional)</li>
+ * <li>{@code void -> Void} (optional)</li>
+ * <li>{@code <T extends Foo> -> Foo}</li>
+ * <li>{@code ? extends Foo -> Foo}</li>
+ * <li>{@code Foo<complex type> -> Foo<simplified type>}</li>
+ * </ul>
+ */
+public class TypeSimplifier extends SimpleTypeVisitor6<TypeMirror, State> {
+
+  public static TypeMirror simplify(TypeMirror toBox, boolean boxPrimitives, State state) {
+    if (toBox == null) {
+      return null;
+    }
+    return toBox.accept(new TypeSimplifier(boxPrimitives), state);
+  }
+
+  private final boolean boxPrimitives;
+
+  private TypeSimplifier(boolean boxPrimitives) {
+    this.boxPrimitives = boxPrimitives;
+  }
+
+  @Override
+  public TypeMirror visitDeclared(DeclaredType x, State state) {
+    if (x.getTypeArguments().isEmpty()) {
+      return x;
+    }
+    List<TypeMirror> newArgs = new ArrayList<TypeMirror>(x.getTypeArguments().size());
+    for (TypeMirror original : x.getTypeArguments()) {
+      // Are we looking at a self-parameterized type like Foo<T extends Foo<T>>?
+      if (original.getKind().equals(TypeKind.TYPEVAR) && state.types.isAssignable(original, x)) {
+        // If so, return a raw type
+        return state.types.getDeclaredType((TypeElement) x.asElement());
+      } else {
+        newArgs.add(original.accept(this, state));
+      }
+    }
+    return state.types.getDeclaredType((TypeElement) x.asElement(), newArgs
+        .toArray(new TypeMirror[newArgs.size()]));
+  }
+
+  @Override
+  public TypeMirror visitNoType(NoType x, State state) {
+    if (boxPrimitives) {
+      return state.findType(Void.class);
+    }
+    return x;
+  }
+
+  @Override
+  public TypeMirror visitPrimitive(PrimitiveType x, State state) {
+    if (boxPrimitives) {
+      return state.types.boxedClass(x).asType();
+    }
+    return x;
+  }
+
+  @Override
+  public TypeMirror visitTypeVariable(TypeVariable x, State state) {
+    if (x.equals(x.getUpperBound())) {
+      // See comment in TransportableTypeVisitor
+      return state.types.erasure(x);
+    }
+    return x.getUpperBound().accept(this, state);
+  }
+
+  @Override
+  public TypeMirror visitWildcard(WildcardType x, State state) {
+    return state.types.erasure(x).accept(this, state);
+  }
+
+  @Override
+  protected TypeMirror defaultAction(TypeMirror x, State state) {
+    return state.types.getNoType(TypeKind.NONE);
+  }
+}
diff --git a/user/src/com/google/web/bindery/requestfactory/apt/TypeVisitorBase.java b/user/src/com/google/web/bindery/requestfactory/apt/TypeVisitorBase.java
new file mode 100644
index 0000000..5468981
--- /dev/null
+++ b/user/src/com/google/web/bindery/requestfactory/apt/TypeVisitorBase.java
@@ -0,0 +1,63 @@
+/*
+ * 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
+ * 
+ * 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.apt;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+
+import javax.lang.model.element.TypeElement;
+import javax.lang.model.type.DeclaredType;
+import javax.lang.model.type.PrimitiveType;
+import javax.lang.model.type.TypeKind;
+import javax.lang.model.util.SimpleTypeVisitor6;
+
+/**
+ * Provides utility functions for type visitors.
+ * 
+ * @param <T> the return type for the visitor
+ */
+class TypeVisitorBase<T> extends SimpleTypeVisitor6<T, State> {
+  /**
+   * This method should be kept in sync with
+   * {@code ValueCodex.getAllValueTypes()}. It doesn't use
+   * {@code getAllValueTypes()} because a dependency on {@code ValueCodex} would
+   * pull in a large number of dependencies into the minimal
+   * {@code requestfactory-apt.jar}.
+   */
+  protected List<DeclaredType> getValueTypes(State state) {
+    List<DeclaredType> types = new ArrayList<DeclaredType>();
+    for (TypeKind kind : TypeKind.values()) {
+      if (kind.isPrimitive()) {
+        PrimitiveType primitiveType = state.types.getPrimitiveType(kind);
+        TypeElement boxedClass = state.types.boxedClass(primitiveType);
+        types.add((DeclaredType) boxedClass.asType());
+      }
+    }
+    types.add(state.findType(BigDecimal.class));
+    types.add(state.findType(BigInteger.class));
+    types.add(state.findType(Date.class));
+    types.add(state.findType(String.class));
+    types.add(state.findType(Void.class));
+    // Avoids compile-dependency bloat
+    types.add(state.types.getDeclaredType(state.elements
+        .getTypeElement("com.google.web.bindery.autobean.shared.Splittable")));
+    return Collections.unmodifiableList(types);
+  }
+}
diff --git a/user/src/com/google/web/bindery/requestfactory/apt/ValidationTool.java b/user/src/com/google/web/bindery/requestfactory/apt/ValidationTool.java
new file mode 100644
index 0000000..206b9e9
--- /dev/null
+++ b/user/src/com/google/web/bindery/requestfactory/apt/ValidationTool.java
@@ -0,0 +1,279 @@
+/*
+ * 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
+ * 
+ * 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.apt;
+
+import com.google.gwt.dev.util.Name.BinaryName;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.jar.JarOutputStream;
+import java.util.zip.ZipEntry;
+
+import javax.lang.model.SourceVersion;
+import javax.tools.FileObject;
+import javax.tools.ForwardingJavaFileManager;
+import javax.tools.JavaCompiler;
+import javax.tools.JavaCompiler.CompilationTask;
+import javax.tools.JavaFileManager;
+import javax.tools.JavaFileObject;
+import javax.tools.JavaFileObject.Kind;
+import javax.tools.SimpleJavaFileObject;
+import javax.tools.StandardLocation;
+import javax.tools.ToolProvider;
+
+/**
+ * Provides "late" validation services when server types aren't available to the
+ * shared-interface compilation process. This tool is provided the name of an
+ * output jar and the binary names of RequestFactory interfaces that should be
+ * validated. The validation process will provide pre-computed type map builders
+ * for use by the ServiceLayer.
+ * 
+ * @see http://code.google.com/p/google-web-toolkit/wiki/
+ *      RequestFactoryInterfaceValidation
+ */
+public class ValidationTool {
+  /**
+   * A JavaFileManager that writes the class outputs into a jar file or a
+   * directory.
+   */
+  static class JarOrDirectoryOutputFileManager extends ForwardingJavaFileManager<JavaFileManager> {
+    private final List<MemoryJavaFileObject> toOutput = new ArrayList<MemoryJavaFileObject>();
+    private final File output;
+
+    JarOrDirectoryOutputFileManager(File output, JavaFileManager fileManager) {
+      super(fileManager);
+      this.output = output;
+    }
+
+    @Override
+    public void close() throws IOException {
+      if (output.isDirectory()) {
+        writeToDirectory();
+      } else {
+        writeToJar();
+      }
+    }
+
+    /**
+     * Not expected to be called. Overridden to prevent accidental writes to
+     * disk.
+     */
+    @Override
+    public FileObject getFileForOutput(Location location, String packageName, String relativeName,
+        FileObject sibling) throws IOException {
+      throw new UnsupportedOperationException("Not expecting to write " + packageName + "/"
+          + relativeName);
+    }
+
+    /**
+     * This method will receive generated source and class files.
+     */
+    @Override
+    public JavaFileObject getJavaFileForOutput(Location location, String className, Kind kind,
+        FileObject sibling) throws IOException {
+      String path = BinaryName.toInternalName(className);
+      String suffix;
+      switch (kind) {
+        case CLASS:
+          suffix = ".class";
+          break;
+        case SOURCE:
+          suffix = ".java";
+          break;
+        default:
+          throw new UnsupportedOperationException("Unexpected kind " + kind);
+      }
+      MemoryJavaFileObject toReturn =
+          new MemoryJavaFileObject(uri("memory:///" + path + suffix), kind);
+      if (StandardLocation.CLASS_OUTPUT.equals(location) && Kind.CLASS.equals(kind)) {
+        toOutput.add(toReturn);
+      }
+      return toReturn;
+    }
+
+    @Override
+    public boolean isSameFile(FileObject a, FileObject b) {
+      if (a instanceof MemoryJavaFileObject && b instanceof MemoryJavaFileObject) {
+        MemoryJavaFileObject memoryA = (MemoryJavaFileObject) a;
+        MemoryJavaFileObject memoryB = (MemoryJavaFileObject) b;
+        return memoryA.getKind().equals(memoryB.getKind())
+            && memoryA.toUri().equals(memoryB.toUri());
+      }
+      if (a instanceof FakeJavaFileObject && b instanceof FakeJavaFileObject) {
+        // Only one file ever created
+        return true;
+      }
+      return super.isSameFile(a, b);
+    }
+
+    private void writeToDirectory() throws IOException {
+      for (MemoryJavaFileObject file : toOutput) {
+        String path = file.toUri().getPath();
+        if (path.equals("/fake/Fake.class")) {
+          // ignore dummy class
+          continue;
+        }
+        File target = new File(output, path);
+        target.getParentFile().mkdirs();
+        FileOutputStream out = new FileOutputStream(target);
+        out.write(file.bytes.toByteArray());
+        out.close();
+      }
+    }
+
+    private void writeToJar() throws IOException, FileNotFoundException {
+      JarOutputStream jar = new JarOutputStream(new FileOutputStream(output));
+      for (MemoryJavaFileObject file : toOutput) {
+        String path = file.toUri().getPath();
+        if (path.equals("/fake/Fake.class")) {
+          // ignore dummy class
+          continue;
+        }
+        // Strip leading /
+        ZipEntry entry = new ZipEntry(path.substring(1));
+        jar.putNextEntry(entry);
+        jar.write(file.bytes.toByteArray());
+      }
+      jar.close();
+    }
+  }
+
+  /**
+   * Provides a fake type to seed the compilation process with.
+   */
+  private static class FakeJavaFileObject extends SimpleJavaFileObject {
+    public FakeJavaFileObject() {
+      super(uri("fake:///fake/Fake.java"), JavaFileObject.Kind.SOURCE);
+    }
+
+    @Override
+    public CharSequence getCharContent(boolean arg0) throws IOException {
+      return "package fake; interface Fake {}";
+    }
+  }
+
+  /**
+   * An in-memory implementation of JavaFileObject.
+   */
+  private static class MemoryJavaFileObject extends SimpleJavaFileObject {
+    private ByteArrayOutputStream bytes;
+    private StringWriter charContents;
+
+    public MemoryJavaFileObject(URI uri, JavaFileObject.Kind kind) {
+      super(uri, kind);
+    }
+
+    @Override
+    public CharSequence getCharContent(boolean ignored) throws IOException {
+      return charContents.toString();
+    }
+
+    @Override
+    public OutputStream openOutputStream() throws IOException {
+      bytes = new ByteArrayOutputStream();
+      return bytes;
+    }
+
+    @Override
+    public Writer openWriter() throws IOException {
+      charContents = new StringWriter();
+      return charContents;
+    }
+  }
+
+  public static void main(String[] args) throws IOException {
+    System.exit(exec(args) ? 0 : -1);
+  }
+
+  /**
+   * A testable "main" method.
+   */
+  static boolean exec(String[] args) throws IOException {
+    if (args.length < 2) {
+      System.err.println("java -cp requestfactory-client.jar:your_server-code.jar "
+          + ValidationTool.class.getCanonicalName()
+          + " (/some/directory | output.jar) com.example.shared.MyRequestFactory");
+      System.err.println("See "
+          + "http://code.google.com/p/google-web-toolkit/wiki/RequestFactoryInterfaceValidation "
+          + "for more information.");
+      return false;
+    }
+    JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
+    if (compiler == null) {
+      System.err.println("This tool must be run with a JDK, not a JRE");
+      return false;
+    }
+    if (!compiler.getSourceVersions().contains(SourceVersion.RELEASE_6)) {
+      System.err.println("This tool must be run with a Java 1.6 compiler");
+      return false;
+    }
+
+    boolean clientOnly = false;
+    List<String> argList = new ArrayList<String>(Arrays.asList(args));
+    if (argList.get(0).equals("-client")) {
+      clientOnly = true;
+      argList.remove(0);
+    }
+
+    // Control how the compile process writes data to disk
+    JavaFileManager fileManager =
+        new JarOrDirectoryOutputFileManager(new File(argList.remove(0)), compiler
+            .getStandardFileManager(null, null, null));
+
+    // Create a validator and require it to process the specified types
+    RfValidator processor = new RfValidator();
+    if (clientOnly) {
+      processor.setClientOnly(true);
+    } else {
+      processor.setMustResolveAllMappings(true);
+    }
+    processor.setRootOverride(argList);
+
+    // Create the compilation task
+    CompilationTask task =
+        compiler.getTask(null, fileManager, null, null, null, Arrays
+            .asList(new FakeJavaFileObject()));
+    task.setProcessors(Arrays.asList(processor));
+    if (!task.call()) {
+      return false;
+    }
+    // Save data only on successful compilation
+    fileManager.close();
+    return true;
+  }
+
+  /**
+   * Convenience method for discarding {@link URISyntaxException}.
+   */
+  private static URI uri(String contents) {
+    try {
+      return new URI(contents);
+    } catch (URISyntaxException e) {
+      throw new RuntimeException(e);
+    }
+  }
+}
diff --git a/user/src/com/google/web/bindery/requestfactory/server/Deobfuscator.java b/user/src/com/google/web/bindery/requestfactory/server/Deobfuscator.java
deleted file mode 100644
index 22abf24..0000000
--- a/user/src/com/google/web/bindery/requestfactory/server/Deobfuscator.java
+++ /dev/null
@@ -1,172 +0,0 @@
-/*
- * 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
- * 
- * 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.dev.asm.Type;
-import com.google.web.bindery.requestfactory.vm.impl.OperationKey;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.SortedSet;
-
-/**
- * Provides access to payload deobfuscation services.
- */
-class Deobfuscator {
-  public static class Builder {
-    private Deobfuscator d = new Deobfuscator();
-    {
-      d.domainToClientType = new HashMap<String, List<String>>();
-      d.operationData = new HashMap<OperationKey, OperationData>();
-      d.typeTokens = new HashMap<String, String>();
-    }
-
-    public Builder addClientToDomainMapping(String domainBinaryName, SortedSet<Type> value) {
-      List<String> clientBinaryNames;
-      switch (value.size()) {
-        case 0:
-          clientBinaryNames = Collections.emptyList();
-          break;
-        case 1:
-          clientBinaryNames = Collections.singletonList(value.first().getClassName());
-          break;
-        default:
-          clientBinaryNames = new ArrayList<String>(value.size());
-          for (Type t : value) {
-            clientBinaryNames.add(t.getClassName());
-          }
-          clientBinaryNames = Collections.unmodifiableList(clientBinaryNames);
-      }
-      d.domainToClientType.put(domainBinaryName, clientBinaryNames);
-      return this;
-    }
-
-    public Builder addClientToDomainMappings(Map<Type, SortedSet<Type>> data) {
-      for (Map.Entry<Type, SortedSet<Type>> entry : data.entrySet()) {
-        String domainBinaryName = entry.getKey().getClassName();
-        SortedSet<Type> value = entry.getValue();
-        addClientToDomainMapping(domainBinaryName, value);
-      }
-      return this;
-    }
-
-    public Builder addOperation(OperationKey key, OperationData data) {
-      d.operationData.put(key, data);
-      return this;
-    }
-
-    public Builder addOperationData(Map<OperationKey, OperationData> operationData) {
-      d.operationData.putAll(operationData);
-      return this;
-    }
-
-    public Builder addRawTypeToken(String token, String binaryName) {
-      d.typeTokens.put(token, binaryName);
-      return this;
-    }
-
-    public Builder addRawTypeTokens(Map<String, String> typeTokens) {
-      for (Map.Entry<String, String> entry : typeTokens.entrySet()) {
-        addRawTypeToken(entry.getKey(), entry.getValue());
-      }
-      return this;
-    }
-
-    public Builder addTypeTokens(Map<String, Type> typeTokens) {
-      for (Map.Entry<String, Type> entry : typeTokens.entrySet()) {
-        d.typeTokens.put(entry.getKey(), entry.getValue().getClassName());
-      }
-      return this;
-    }
-
-    public Deobfuscator build() {
-      Deobfuscator toReturn = d;
-      toReturn.domainToClientType = Collections.unmodifiableMap(toReturn.domainToClientType);
-      toReturn.operationData = Collections.unmodifiableMap(toReturn.operationData);
-      toReturn.typeTokens = Collections.unmodifiableMap(toReturn.typeTokens);
-      d = null;
-      return toReturn;
-    }
-
-    public Builder merge(Deobfuscator deobfuscator) {
-      d.domainToClientType.putAll(deobfuscator.domainToClientType);
-      d.operationData.putAll(deobfuscator.operationData);
-      d.typeTokens.putAll(deobfuscator.typeTokens);
-      return this;
-    }
-  }
-
-  /**
-   * 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;
-  /**
-   * Map of obfuscated ids to binary class names.
-   */
-  private Map<String, String> typeTokens;
-
-  Deobfuscator() {
-  }
-
-  /**
-   * 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);
-  }
-
-  /**
-   * Returns a method descriptor that should be invoked on the service object.
-   */
-  public String getDomainMethodDescriptor(String operation) {
-    OperationData data = getData(operation);
-    return data == null ? null : data.getDomainMethodDescriptor();
-  }
-
-  public String getRequestContext(String operation) {
-    OperationData data = getData(operation);
-    return data == null ? null : data.getRequestContext();
-  }
-
-  public String getRequestContextMethodDescriptor(String operation) {
-    OperationData data = getData(operation);
-    return data == null ? null : data.getClientMethodDescriptor();
-  }
-
-  public String getRequestContextMethodName(String operation) {
-    OperationData data = getData(operation);
-    return data == null ? null : data.getMethodName();
-  }
-
-  /**
-   * Returns a type's binary name based on an obfuscated token.
-   */
-  public String getTypeFromToken(String token) {
-    return typeTokens.get(token);
-  }
-
-  private OperationData getData(String operation) {
-    OperationData data = operationData.get(new OperationKey(operation));
-    return data;
-  }
-}
\ No newline at end of file
diff --git a/user/src/com/google/web/bindery/requestfactory/server/RequestFactoryInterfaceValidator.java b/user/src/com/google/web/bindery/requestfactory/server/RequestFactoryInterfaceValidator.java
deleted file mode 100644
index f2c8b49..0000000
--- a/user/src/com/google/web/bindery/requestfactory/server/RequestFactoryInterfaceValidator.java
+++ /dev/null
@@ -1,1622 +0,0 @@
-/*
- * Copyright 2010 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.dev.asm.AnnotationVisitor;
-import com.google.gwt.dev.asm.ClassReader;
-import com.google.gwt.dev.asm.ClassVisitor;
-import com.google.gwt.dev.asm.MethodVisitor;
-import com.google.gwt.dev.asm.Opcodes;
-import com.google.gwt.dev.asm.Type;
-import com.google.gwt.dev.asm.commons.EmptyVisitor;
-import com.google.gwt.dev.asm.commons.Method;
-import com.google.gwt.dev.asm.signature.SignatureReader;
-import com.google.gwt.dev.asm.signature.SignatureVisitor;
-import com.google.gwt.dev.util.Name;
-import com.google.gwt.dev.util.Name.BinaryName;
-import com.google.gwt.dev.util.Name.SourceOrBinaryName;
-import com.google.web.bindery.autobean.shared.ValueCodex;
-import com.google.web.bindery.requestfactory.shared.BaseProxy;
-import com.google.web.bindery.requestfactory.shared.EntityProxy;
-import com.google.web.bindery.requestfactory.shared.ExtraTypes;
-import com.google.web.bindery.requestfactory.shared.InstanceRequest;
-import com.google.web.bindery.requestfactory.shared.ProxyFor;
-import com.google.web.bindery.requestfactory.shared.ProxyForName;
-import com.google.web.bindery.requestfactory.shared.Request;
-import com.google.web.bindery.requestfactory.shared.RequestContext;
-import com.google.web.bindery.requestfactory.shared.RequestFactory;
-import com.google.web.bindery.requestfactory.shared.Service;
-import com.google.web.bindery.requestfactory.shared.ServiceName;
-import com.google.web.bindery.requestfactory.shared.SkipInterfaceValidation;
-import com.google.web.bindery.requestfactory.shared.ValueProxy;
-import com.google.web.bindery.requestfactory.vm.impl.OperationKey;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.lang.annotation.Annotation;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.LinkedHashMap;
-import java.util.LinkedHashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.SortedSet;
-import java.util.TreeSet;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-
-/**
- * Encapsulates validation logic to determine if a {@link RequestFactory}
- * interface, its {@link RequestContext}, and associated {@link EntityProxy}
- * interfaces match their domain counterparts. This implementation examines the
- * classfiles directly in order to avoid the need to load the types into the
- * JVM.
- * <p>
- * This class is amenable to being used as a unit test:
- * 
- * <pre>
- * public void testRequestFactory() {
- *   Logger logger = Logger.getLogger("");
- *   RequestFactoryInterfaceValidator v = new RequestFactoryInterfaceValidator(
- *     logger, new ClassLoaderLoader(MyRequestContext.class.getClassLoader()));
- *   v.validateRequestContext(MyRequestContext.class.getName());
- *   assertFalse(v.isPoisoned());
- * }
- * </pre>
- * This class also has a {@code main} method and can be used as a build-time
- * tool:
- * 
- * <pre>
- * java -cp gwt-servlet.jar:your-code.jar \
- *   com.google.web.bindery.requestfactory.server.RequestFactoryInterfaceValidator \
- *   com.example.MyRequestFactory
- * </pre>
- */
-public class RequestFactoryInterfaceValidator {
-  /**
-   * An implementation of {@link Loader} that uses a {@link ClassLoader} to
-   * retrieve the class files.
-   */
-  public static class ClassLoaderLoader implements Loader {
-    private final ClassLoader loader;
-
-    public ClassLoaderLoader(ClassLoader loader) {
-      this.loader = loader;
-    }
-
-    public boolean exists(String resource) {
-      return loader.getResource(resource) != null;
-    }
-
-    public InputStream getResourceAsStream(String resource) {
-      return loader.getResourceAsStream(resource);
-    }
-  }
-
-  /**
-   * Abstracts the mechanism by which class files are loaded.
-   * 
-   * @see ClassLoaderLoader
-   */
-  public interface Loader {
-    /**
-     * Returns true if the specified resource can be loaded.
-     * 
-     * @param resource a resource name (e.g. <code>com/example/Foo.class</code>)
-     */
-    boolean exists(String resource);
-
-    /**
-     * Returns an InputStream to access the specified resource, or
-     * <code>null</code> if no such resource exists.
-     * 
-     * @param resource a resource name (e.g. <code>com/example/Foo.class</code>)
-     */
-    InputStream getResourceAsStream(String resource);
-  }
-
-  /**
-   * Improves error messages by providing context for the user.
-   * <p>
-   * Visible for testing.
-   */
-  static class ErrorContext {
-    private final Logger logger;
-    private final ErrorContext parent;
-    private Type currentType;
-    private Method currentMethod;
-    private RequestFactoryInterfaceValidator validator;
-
-    public ErrorContext(Logger logger) {
-      this.logger = logger;
-      this.parent = null;
-    }
-
-    protected ErrorContext(ErrorContext parent) {
-      this.logger = parent.logger;
-      this.parent = parent;
-      this.validator = parent.validator;
-    }
-
-    public void poison(String msg, Object... args) {
-      poison();
-      logger.logp(Level.SEVERE, currentType(), currentMethod(), String.format(msg, args));
-      validator.poisoned = true;
-    }
-
-    public void poison(String msg, Throwable t) {
-      poison();
-      logger.logp(Level.SEVERE, currentType(), currentMethod(), msg, t);
-      validator.poisoned = true;
-    }
-
-    public ErrorContext setMethod(Method method) {
-      ErrorContext toReturn = fork();
-      toReturn.currentMethod = method;
-      return toReturn;
-    }
-
-    public ErrorContext setType(Type type) {
-      ErrorContext toReturn = fork();
-      toReturn.currentType = type;
-      return toReturn;
-    }
-
-    public void spam(String msg, Object... args) {
-      logger.logp(Level.FINEST, currentType(), currentMethod(), String.format(msg, args));
-    }
-
-    protected ErrorContext fork() {
-      return new ErrorContext(this);
-    }
-
-    void setValidator(RequestFactoryInterfaceValidator validator) {
-      assert this.validator == null : "Cannot set validator twice";
-      this.validator = validator;
-    }
-
-    private String currentMethod() {
-      if (currentMethod != null) {
-        return print(currentMethod);
-      }
-      if (parent != null) {
-        return parent.currentMethod();
-      }
-      return null;
-    }
-
-    private String currentType() {
-      if (currentType != null) {
-        return print(currentType);
-      }
-      if (parent != null) {
-        return parent.currentType();
-      }
-      return null;
-    }
-
-    /**
-     * Populate {@link RequestFactoryInterfaceValidator#badTypes} with the
-     * current context.
-     */
-    private void poison() {
-      if (currentType != null) {
-        validator.badTypes.add(currentType.getClassName());
-      }
-      if (parent != null) {
-        parent.poison();
-      }
-    }
-  }
-
-  /**
-   * Used internally as a placeholder for types that cannot be mapped to a
-   * domain object.
-   */
-  interface MissingDomainType {
-  }
-
-  /**
-   * Collects the ProxyFor or Service annotation from an EntityProxy or
-   * RequestContext type.
-   */
-  private class DomainMapper extends EmptyVisitor {
-    private final ErrorContext logger;
-    private String domainInternalName;
-    private List<Class<? extends Annotation>> found = new ArrayList<Class<? extends Annotation>>();
-    private String locatorInternalName;
-
-    public DomainMapper(ErrorContext logger) {
-      this.logger = logger;
-      logger.spam("Finding domain mapping annotation");
-    }
-
-    public String getDomainInternalName() {
-      return domainInternalName;
-    }
-
-    public String getLocatorInternalName() {
-      return locatorInternalName;
-    }
-
-    @Override
-    public void visit(int version, int access, String name, String signature, String superName,
-        String[] interfaces) {
-      if ((access & Opcodes.ACC_INTERFACE) == 0) {
-        logger.poison("Type must be an interface");
-      }
-    }
-
-    /**
-     * This method examines one annotation at a time.
-     */
-    @Override
-    public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
-      // Set to true if the annotation should have class literal values
-      boolean expectClasses = false;
-      // Set to true if the annonation has string values
-      boolean expectNames = false;
-
-      if (desc.equals(Type.getDescriptor(ProxyFor.class))) {
-        expectClasses = true;
-        found.add(ProxyFor.class);
-      } else if (desc.equals(Type.getDescriptor(ProxyForName.class))) {
-        expectNames = true;
-        found.add(ProxyForName.class);
-      } else if (desc.equals(Type.getDescriptor(Service.class))) {
-        expectClasses = true;
-        found.add(Service.class);
-      } else if (desc.equals(Type.getDescriptor(ServiceName.class))) {
-        expectNames = true;
-        found.add(ServiceName.class);
-      }
-
-      if (expectClasses) {
-        return new EmptyVisitor() {
-          @Override
-          public void visit(String name, Object value) {
-            if ("value".equals(name)) {
-              domainInternalName = ((Type) value).getInternalName();
-            } else if ("locator".equals(name)) {
-              locatorInternalName = ((Type) value).getInternalName();
-            }
-          }
-        };
-      }
-
-      if (expectNames) {
-        return new EmptyVisitor() {
-          @Override
-          public void visit(String name, Object value) {
-            String sourceName;
-            boolean locatorRequired = "locator".equals(name);
-            boolean valueRequired = "value".equals(name);
-            if (valueRequired || locatorRequired) {
-              sourceName = (String) value;
-            } else {
-              return;
-            }
-
-            /*
-             * The input is a source name, so we need to convert it to an
-             * internal name. We'll do this by substituting dollar signs for the
-             * last slash in the name until there are no more slashes.
-             */
-            StringBuffer desc = new StringBuffer(sourceName.replace('.', '/'));
-            while (!loader.exists(desc.toString() + ".class")) {
-              logger.spam("Did not find " + desc.toString());
-              int idx = desc.lastIndexOf("/");
-              if (idx == -1) {
-                if (locatorRequired) {
-                  logger.poison("Cannot find locator named %s", value);
-                } else if (valueRequired) {
-                  logger.poison("Cannot find domain type named %s", value);
-                }
-                return;
-              }
-              desc.setCharAt(idx, '$');
-            }
-
-            if (locatorRequired) {
-              locatorInternalName = desc.toString();
-              logger.spam(locatorInternalName);
-            } else if (valueRequired) {
-              domainInternalName = desc.toString();
-              logger.spam(domainInternalName);
-            } else {
-              throw new RuntimeException("Should not reach here");
-            }
-          }
-        };
-      }
-      return null;
-    }
-
-    @Override
-    public void visitEnd() {
-      // Only allow one annotation
-      if (found.size() > 1) {
-        StringBuilder sb = new StringBuilder();
-        for (Class<?> clazz : found) {
-          sb.append(" @").append(clazz.getSimpleName());
-        }
-        logger.poison("Redundant domain mapping annotations present:%s", sb.toString());
-      }
-    }
-  }
-
-  private class ExtraTypesCollector extends EmptyVisitor {
-    private final ErrorContext logger;
-    private final Set<Type> collected = new HashSet<Type>();
-
-    public ExtraTypesCollector(ErrorContext logger) {
-      this.logger = logger;
-      logger.spam("Collecting extra types");
-    }
-
-    public Type[] exec(Type type) {
-      for (Type toExamine : getSupertypes(logger, type)) {
-        RequestFactoryInterfaceValidator.this.visit(logger, toExamine.getInternalName(), this);
-      }
-      return collected.toArray(new Type[collected.size()]);
-    }
-
-    @Override
-    public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
-      if (!desc.equals(Type.getDescriptor(ExtraTypes.class))) {
-        return null;
-      }
-
-      return new EmptyVisitor() {
-
-        @Override
-        public AnnotationVisitor visitArray(String name) {
-          if (!"value".equals(name)) {
-            return null;
-          }
-          return new EmptyVisitor() {
-            @Override
-            public void visit(String name, Object value) {
-              collected.add((Type) value);
-            }
-          };
-        }
-
-      };
-    }
-  }
-
-  /**
-   * Collects information about domain objects. This visitor is intended to be
-   * iteratively applied to collect all methods in a type hierarchy.
-   */
-  private class MethodsInHierarchyCollector extends EmptyVisitor {
-    private final ErrorContext logger;
-    private Set<RFMethod> methods = new LinkedHashSet<RFMethod>();
-    private Set<String> seen = new HashSet<String>();
-
-    private MethodsInHierarchyCollector(ErrorContext logger) {
-      this.logger = logger;
-    }
-
-    public Set<RFMethod> exec(String internalName) {
-      RequestFactoryInterfaceValidator.this.visit(logger, internalName, this);
-
-      Map<RFMethod, RFMethod> toReturn = new HashMap<RFMethod, RFMethod>();
-      // Return most-derived methods
-      for (RFMethod method : methods) {
-        RFMethod key =
-            new RFMethod(method.getName(), Type.getMethodDescriptor(Type.VOID_TYPE, method
-                .getArgumentTypes()));
-
-        RFMethod compareTo = toReturn.get(key);
-        if (compareTo == null) {
-          toReturn.put(key, method);
-        } else if (isAssignable(logger, compareTo.getReturnType(), method.getReturnType())) {
-          toReturn.put(key, method);
-        }
-      }
-
-      return new HashSet<RFMethod>(toReturn.values());
-    }
-
-    @Override
-    public void visit(int version, int access, String name, String signature, String superName,
-        String[] interfaces) {
-      if (!seen.add(name)) {
-        return;
-      }
-      if (!objectType.getInternalName().equals(superName)) {
-        RequestFactoryInterfaceValidator.this.visit(logger, superName, this);
-      }
-      if (interfaces != null) {
-        for (String intf : interfaces) {
-          RequestFactoryInterfaceValidator.this.visit(logger, intf, this);
-        }
-      }
-    }
-
-    @Override
-    public MethodVisitor visitMethod(int access, String name, String desc, String signature,
-        String[] exceptions) {
-      // Ignore initializers
-      if ("<clinit>".equals(name) || "<init>".equals(name)) {
-        return null;
-      }
-      final RFMethod method = new RFMethod(name, desc);
-      method.setDeclaredStatic((access & Opcodes.ACC_STATIC) != 0);
-      method.setDeclaredSignature(signature);
-      methods.add(method);
-
-      return new EmptyVisitor() {
-        @Override
-        public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
-          if (desc.equals(Type.getDescriptor(SkipInterfaceValidation.class))) {
-            method.setValidationSkipped(true);
-          }
-          return null;
-        }
-      };
-    }
-  }
-
-  private static class RFMethod extends Method {
-    private boolean isDeclaredStatic;
-    private String signature;
-    private boolean isValidationSkipped;
-
-    public RFMethod(String name, String desc) {
-      super(name, desc);
-    }
-
-    public String getSignature() {
-      return signature;
-    }
-
-    public boolean isDeclaredStatic() {
-      return isDeclaredStatic;
-    }
-
-    public boolean isValidationSkipped() {
-      return isValidationSkipped;
-    }
-
-    public void setDeclaredSignature(String signature) {
-      this.signature = signature;
-    }
-
-    public void setDeclaredStatic(boolean value) {
-      isDeclaredStatic = value;
-    }
-
-    public void setValidationSkipped(boolean isValidationSkipped) {
-      this.isValidationSkipped = isValidationSkipped;
-    }
-
-    @Override
-    public String toString() {
-      return (isDeclaredStatic ? "static " : "") + super.toString();
-    }
-  }
-
-  private class SupertypeCollector extends EmptyVisitor {
-    private final ErrorContext logger;
-    private final Set<String> seen = new HashSet<String>();
-    private final List<Type> supers = new ArrayList<Type>();
-
-    public SupertypeCollector(ErrorContext logger) {
-      this.logger = logger;
-    }
-
-    public List<Type> exec(Type type) {
-      RequestFactoryInterfaceValidator.this.visit(logger, type.getInternalName(), this);
-      return supers;
-    }
-
-    @Override
-    public void visit(int version, int access, String name, String signature, String superName,
-        String[] interfaces) {
-      if (!seen.add(name)) {
-        return;
-      }
-      supers.add(Type.getObjectType(name));
-      if (!objectType.getInternalName().equals(name)) {
-        RequestFactoryInterfaceValidator.this.visit(logger, superName, this);
-      }
-      if (interfaces != null) {
-        for (String intf : interfaces) {
-          RequestFactoryInterfaceValidator.this.visit(logger, intf, this);
-        }
-      }
-    }
-  }
-
-  /**
-   * Return all types referenced by a method signature.
-   */
-  private static class TypesInSignatureCollector extends SignatureAdapter {
-    private final Set<Type> found = new HashSet<Type>();
-
-    public Type[] getFound() {
-      return found.toArray(new Type[found.size()]);
-    }
-
-    @Override
-    public SignatureVisitor visitArrayType() {
-      return this;
-    }
-
-    @Override
-    public SignatureVisitor visitClassBound() {
-      return this;
-    }
-
-    @Override
-    public void visitClassType(String name) {
-      found.add(Type.getObjectType(name));
-    }
-
-    @Override
-    public SignatureVisitor visitExceptionType() {
-      return this;
-    }
-
-    @Override
-    public SignatureVisitor visitInterface() {
-      return this;
-    }
-
-    @Override
-    public SignatureVisitor visitInterfaceBound() {
-      return this;
-    }
-
-    @Override
-    public SignatureVisitor visitParameterType() {
-      return this;
-    }
-
-    @Override
-    public SignatureVisitor visitReturnType() {
-      return this;
-    }
-
-    @Override
-    public SignatureVisitor visitSuperclass() {
-      return this;
-    }
-
-    @Override
-    public SignatureVisitor visitTypeArgument(char wildcard) {
-      return this;
-    }
-  }
-
-  static final Set<Class<?>> VALUE_TYPES = ValueCodex.getAllValueTypes();
-
-  public static void main(String[] args) {
-    if (args.length == 0) {
-      System.err.println("Usage: java -cp gwt-servlet.jar:your-code.jar "
-          + RequestFactoryInterfaceValidator.class.getCanonicalName()
-          + " com.example.MyRequestFactory");
-      System.exit(1);
-    }
-    RequestFactoryInterfaceValidator validator =
-        new RequestFactoryInterfaceValidator(Logger
-            .getLogger(RequestFactoryInterfaceValidator.class.getName()), new ClassLoaderLoader(
-            Thread.currentThread().getContextClassLoader()));
-    validator.validateRequestFactory(args[0]);
-    System.exit(validator.isPoisoned() ? 1 : 0);
-  }
-
-  static String messageCouldNotFindMethod(Type domainType, List<? extends Method> methods) {
-    StringBuilder sb = new StringBuilder();
-    sb.append(String.format("Could not find matching method in %s.\nPossible matches:\n",
-        print(domainType)));
-    for (Method domainMethod : methods) {
-      sb.append("  ").append(print(domainMethod)).append("\n");
-    }
-    return sb.toString();
-  }
-
-  private static String print(Method method) {
-    StringBuilder sb = new StringBuilder();
-    sb.append(print(method.getReturnType())).append(" ").append(method.getName()).append("(");
-    for (Type t : method.getArgumentTypes()) {
-      sb.append(print(t)).append(" ");
-    }
-    sb.append(")");
-    return sb.toString();
-  }
-
-  private static String print(Type type) {
-    return SourceOrBinaryName.toSourceName(type.getClassName());
-  }
-
-  /**
-   * A set of binary type names that are known to be bad.
-   */
-  private final Set<String> badTypes = new HashSet<String>();
-  /**
-   * The type {@link BaseProxy}.
-   */
-  private final Type baseProxyIntf = Type.getType(BaseProxy.class);
-  /**
-   * Maps client types (e.g. FooProxy) to server domain types (e.g. Foo).
-   */
-  private final Map<Type, Type> clientToDomainType = new HashMap<Type, Type>();
-  /**
-   * Maps client types (e.g. FooProxy or FooContext) to their locator types
-   * (e.g. FooLocator or FooServiceLocator).
-   */
-  private final Map<Type, Type> clientToLocatorMap = new HashMap<Type, Type>();
-  /**
-   * Maps domain types (e.g Foo) to client proxy types (e.g. FooAProxy,
-   * FooBProxy).
-   */
-  private final Map<Type, SortedSet<Type>> domainToClientType =
-      new HashMap<Type, SortedSet<Type>>();
-  /**
-   * The type {@link EntityProxy}.
-   */
-  private final Type entityProxyIntf = Type.getType(EntityProxy.class);
-  /**
-   * The type {@link Enum}.
-   */
-  private final Type enumType = Type.getType(Enum.class);
-  /**
-   * A placeholder type for client types that could not be resolved to a domain
-   * type.
-   */
-  private final Type errorType = Type.getType(MissingDomainType.class);
-  /**
-   * The type {@link InstanceRequest}.
-   */
-  private final Type instanceRequestIntf = Type.getType(InstanceRequest.class);
-  private final Loader loader;
-  /**
-   * A cache of all methods defined in a type hierarchy.
-   */
-  private final Map<Type, Set<RFMethod>> methodsInHierarchy = new HashMap<Type, Set<RFMethod>>();
-  /**
-   * Not static because it depends on {@link #parentLogger}.
-   */
-  private final Comparator<Type> typeNameComparator = new Comparator<Type>() {
-    @Override
-    public int compare(Type a, Type b) {
-      if (isAssignable(parentLogger, a, b)) {
-        return 1;
-      } else if (isAssignable(parentLogger, b, a)) {
-        return -1;
-      }
-      return a.getInternalName().compareTo(b.getInternalName());
-    }
-  };
-  /**
-   * Used to resolve obfuscated type tokens.
-   */
-  private final Map<String, Type> typeTokens = new HashMap<String, Type>();
-  /**
-   * The type {@link Object}.
-   */
-  private final Type objectType = Type.getObjectType("java/lang/Object");
-  /**
-   * Maps obfuscated operation names to dispatch information.
-   */
-  private final Map<OperationKey, OperationData> operationData =
-      new HashMap<OperationKey, OperationData>();
-  private final ErrorContext parentLogger;
-  private boolean poisoned;
-  /**
-   * The type {@link Request}.
-   */
-  private final Type requestIntf = Type.getType(Request.class);
-
-  /**
-   * The type {@link RequestContext}.
-   */
-  private final Type requestContextIntf = Type.getType(RequestContext.class);
-
-  /**
-   * A map of a type to all types that it could be assigned to.
-   */
-  private final Map<Type, List<Type>> supertypes = new HashMap<Type, List<Type>>();
-
-  /**
-   * The type {@link ValueProxy}.
-   */
-  private final Type valueProxyIntf = Type.getType(ValueProxy.class);
-
-  /**
-   * A set to prevent re-validation of a type.
-   */
-  private final Set<String> validatedTypes = new HashSet<String>();
-
-  /**
-   * Contains vaue types (e.g. Integer).
-   */
-  private final Set<Type> valueTypes = new HashSet<Type>();
-
-  /**
-   * Maps a domain object to the type returned from its getId method.
-   */
-  private final Map<Type, Type> unresolvedKeyTypes = new HashMap<Type, Type>();
-
-  {
-    for (Class<?> clazz : VALUE_TYPES) {
-      valueTypes.add(Type.getType(clazz));
-    }
-  }
-
-  public RequestFactoryInterfaceValidator(Logger logger, Loader loader) {
-    this.parentLogger = new ErrorContext(logger);
-    parentLogger.setValidator(this);
-    this.loader = loader;
-  }
-
-  /**
-   * Visible for testing.
-   */
-  RequestFactoryInterfaceValidator(ErrorContext errorContext, Loader loader) {
-    this.parentLogger = errorContext;
-    this.loader = loader;
-    errorContext.setValidator(this);
-  }
-
-  /**
-   * Reset the poisoned status of the validator so that it may be reused without
-   * destroying cached state.
-   */
-  public void antidote() {
-    poisoned = false;
-  }
-
-  public Deobfuscator getDeobfuscator() {
-    return new Deobfuscator.Builder().addClientToDomainMappings(domainToClientType)
-        .addOperationData(operationData).addTypeTokens(typeTokens).build();
-  }
-
-  /**
-   * Returns true if validation failed.
-   */
-  public boolean isPoisoned() {
-    return poisoned;
-  }
-
-  /**
-   * This method checks an EntityProxy interface against its peer domain object
-   * to determine if the server code would be able to process a request using
-   * the methods defined in the EntityProxy interface. It does not perform any
-   * checks as to whether or not the EntityProxy could actually be generated by
-   * the Generator.
-   * <p>
-   * This method may be called repeatedly on a single instance of the validator.
-   * Doing so will amortize type calculation costs.
-   * <p>
-   * Checks implemented:
-   * <ul>
-   * <li> <code>binaryName</code> implements EntityProxy</li>
-   * <li><code>binaryName</code> has a {@link ProxyFor} or {@link ProxyForName}
-   * annotation</li>
-   * <li>The domain object has getId() and getVersion() methods</li>
-   * <li>All property methods in the EntityProxy can be mapped onto an
-   * equivalent domain method (unless validation is skipped for the method)</li>
-   * <li>All referenced proxy types are valid</li>
-   * </ul>
-   * 
-   * @param binaryName the binary name (e.g. {@link Class#getName()}) of the
-   *          EntityProxy subtype
-   */
-  public void validateEntityProxy(String binaryName) {
-    validateProxy(binaryName, entityProxyIntf, true);
-  }
-
-  /**
-   * Determine if the specified type implements a proxy interface and apply the
-   * appropriate validations. This can be used as a general-purpose entry method
-   * when writing unit tests.
-   * 
-   * @param binaryName the binary name (e.g. {@link Class#getName()}) of the
-   *          EntityProxy or ValueProxy subtype
-   */
-  public void validateProxy(String binaryName) {
-    /*
-     * Don't call fastFail() here or the proxy may not be validated, since
-     * validateXProxy delegates to validateProxy() which would re-check.
-     */
-    Type proxyType = Type.getObjectType(BinaryName.toInternalName(binaryName));
-    if (isAssignable(parentLogger, entityProxyIntf, proxyType)) {
-      validateEntityProxy(binaryName);
-    } else if (isAssignable(parentLogger, valueProxyIntf, proxyType)) {
-      validateValueProxy(binaryName);
-    } else {
-      parentLogger.poison("%s is neither an %s nor a %s", print(proxyType), print(entityProxyIntf),
-          print(valueProxyIntf));
-    }
-  }
-
-  /**
-   * This method checks a RequestContext interface against its peer domain
-   * domain object to determine if the server code would be able to process a
-   * request using the the methods defined in the RequestContext interface. It
-   * does not perform any checks as to whether or not the RequestContext could
-   * actually be generated by the Generator.
-   * <p>
-   * This method may be called repeatedly on a single instance of the validator.
-   * Doing so will amortize type calculation costs.
-   * <p>
-   * Checks implemented:
-   * <ul>
-   * <li> <code>binaryName</code> implements RequestContext</li>
-   * <li><code>binaryName</code> has a {@link Service} or {@link ServiceName}
-   * annotation</li>
-   * <li>All service methods in the RequestContext can be mapped onto an
-   * equivalent domain method (unless validation is skipped for the method)</li>
-   * <li>All referenced EntityProxy types are valid</li>
-   * </ul>
-   * 
-   * @param binaryName the binary name (e.g. {@link Class#getName()}) of the
-   *          RequestContext subtype
-   * @see #validateEntityProxy(String)
-   */
-  public void validateRequestContext(String binaryName) {
-    if (fastFail(binaryName)) {
-      return;
-    }
-
-    Type requestContextType = Type.getObjectType(BinaryName.toInternalName(binaryName));
-    final ErrorContext logger = parentLogger.setType(requestContextType);
-
-    // Quick sanity check for calling code
-    if (!isAssignable(logger, requestContextIntf, requestContextType)) {
-      logger.poison("%s is not a %s", print(requestContextType), RequestContext.class
-          .getSimpleName());
-      return;
-    }
-
-    Type domainServiceType = getDomainType(logger, requestContextType, false);
-    if (domainServiceType == errorType) {
-      logger.poison("The type %s must be annotated with a @%s or @%s annotation", BinaryName
-          .toSourceName(binaryName), Service.class.getSimpleName(), ServiceName.class
-          .getSimpleName());
-      return;
-    }
-
-    for (RFMethod method : getMethodsInHierarchy(logger, requestContextType)) {
-      // Ignore methods in RequestContext itself
-      if (findCompatibleMethod(logger, requestContextIntf, method, false, true, true) != null) {
-        continue;
-      }
-
-      // Check the client method against the domain
-      Method found =
-          checkClientMethodInDomain(logger, method, domainServiceType, !clientToLocatorMap
-              .containsKey(requestContextType));
-      if (found != null) {
-        OperationKey key = new OperationKey(binaryName, method.getName(), method.getDescriptor());
-        OperationData data =
-            new OperationData.Builder().setClientMethodDescriptor(method.getDescriptor())
-                .setDomainMethodDescriptor(found.getDescriptor()).setMethodName(method.getName())
-                .setRequestContext(requestContextType.getClassName()).build();
-        operationData.put(key, data);
-      }
-      maybeCheckReferredProxies(logger, method);
-    }
-
-    maybeCheckExtraTypes(logger, requestContextType);
-    checkUnresolvedKeyTypes(logger);
-  }
-
-  /**
-   * This method checks a RequestFactory interface.
-   * <p>
-   * This method may be called repeatedly on a single instance of the validator.
-   * Doing so will amortize type calculation costs. It does not perform any
-   * checks as to whether or not the RequestContext could actually be generated
-   * by the Generator.
-   * <p>
-   * Checks implemented:
-   * <ul>
-   * <li> <code>binaryName</code> implements RequestFactory</li>
-   * <li>All referenced RequestContext types are valid</li>
-   * </ul>
-   * 
-   * @param binaryName the binary name (e.g. {@link Class#getName()}) of the
-   *          RequestContext subtype
-   * @see #validateRequestContext(String)
-   */
-  public void validateRequestFactory(String binaryName) {
-    if (fastFail(binaryName)) {
-      return;
-    }
-
-    Type requestFactoryType = Type.getObjectType(BinaryName.toInternalName(binaryName));
-    ErrorContext logger = parentLogger.setType(requestFactoryType);
-
-    // Quick sanity check for calling code
-    if (!isAssignable(logger, Type.getType(RequestFactory.class), requestFactoryType)) {
-      logger.poison("%s is not a %s", print(requestFactoryType), RequestFactory.class
-          .getSimpleName());
-      return;
-    }
-
-    // Validate each RequestContext method in the RF
-    for (Method contextMethod : getMethodsInHierarchy(logger, requestFactoryType)) {
-      Type returnType = contextMethod.getReturnType();
-      if (isAssignable(logger, requestContextIntf, returnType)) {
-        validateRequestContext(returnType.getClassName());
-      }
-    }
-
-    maybeCheckExtraTypes(logger, requestFactoryType);
-  }
-
-  /**
-   * This method checks a ValueProxy interface against its peer domain object to
-   * determine if the server code would be able to process a request using the
-   * methods defined in the ValueProxy interface. It does not perform any checks
-   * as to whether or not the ValueProxy could actually be generated by the
-   * Generator.
-   * <p>
-   * This method may be called repeatedly on a single instance of the validator.
-   * Doing so will amortize type calculation costs.
-   * <p>
-   * Checks implemented:
-   * <ul>
-   * <li> <code>binaryName</code> implements ValueProxy</li>
-   * <li><code>binaryName</code> has a {@link ProxyFor} or {@link ProxyForName}
-   * annotation</li>
-   * <li>All property methods in the EntityProxy can be mapped onto an
-   * equivalent domain method (unless validation is skipped for the method)</li>
-   * <li>All referenced proxy types are valid</li>
-   * </ul>
-   * 
-   * @param binaryName the binary name (e.g. {@link Class#getName()}) of the
-   *          EntityProxy subtype
-   */
-  public void validateValueProxy(String binaryName) {
-    validateProxy(binaryName, valueProxyIntf, false);
-  }
-
-  /**
-   * Record the mapping of a domain type to a client type. Proxy types will be
-   * added to {@link #domainToClientType}.
-   */
-  private void addToDomainMap(ErrorContext logger, Type domainType, Type clientType) {
-    clientToDomainType.put(clientType, domainType);
-
-    if (isAssignable(logger, baseProxyIntf, clientType)) {
-      SortedSet<Type> list = domainToClientType.get(domainType);
-      if (list == null) {
-        list = new TreeSet<Type>(typeNameComparator);
-        domainToClientType.put(domainType, list);
-      }
-      list.add(clientType);
-    }
-  }
-
-  /**
-   * Check that a given method RequestContext method declaration can be mapped
-   * to the server's domain type.
-   */
-  private RFMethod checkClientMethodInDomain(ErrorContext logger, RFMethod method,
-      Type domainServiceType, boolean requireStaticMethodsForRequestType) {
-    logger = logger.setMethod(method);
-
-    // Create a "translated" method declaration to search for
-    // Request<BlahProxy> foo(int a, BarProxy bar) -> Blah foo(int a, Bar bar);
-    Type returnType = getReturnType(logger, method);
-    Method searchFor =
-        createDomainMethod(logger, new Method(method.getName(), returnType, method
-            .getArgumentTypes()));
-
-    RFMethod found =
-        findCompatibleServiceMethod(logger, domainServiceType, searchFor, !method
-            .isValidationSkipped());
-
-    if (found != null) {
-      boolean isInstance = isAssignable(logger, instanceRequestIntf, method.getReturnType());
-      if (isInstance && found.isDeclaredStatic()) {
-        logger.poison("The method %s is declared to return %s, but the"
-            + " service method is static", method.getName(), InstanceRequest.class
-            .getCanonicalName());
-      } else if (requireStaticMethodsForRequestType && !isInstance && !found.isDeclaredStatic()) {
-        logger.poison("The method %s is declared to return %s, but the"
-            + " service method is not static", method.getName(), Request.class.getCanonicalName());
-      }
-    }
-    return found;
-  }
-
-  /**
-   * Check that the domain object has <code>getId()</code> and
-   * <code>getVersion</code> methods.
-   */
-  private void checkIdAndVersion(ErrorContext logger, Type domainType) {
-    if (objectType.equals(domainType)) {
-      return;
-    }
-    logger = logger.setType(domainType);
-    String findMethodName = "find" + BinaryName.getShortClassName(domainType.getClassName());
-    Type keyType = null;
-    RFMethod findMethod = null;
-
-    boolean foundFind = false;
-    boolean foundId = false;
-    boolean foundVersion = false;
-    for (RFMethod method : getMethodsInHierarchy(logger, domainType)) {
-      if ("getId".equals(method.getName()) && method.getArgumentTypes().length == 0) {
-        foundId = true;
-        keyType = method.getReturnType();
-        if (!isResolvedKeyType(logger, keyType)) {
-          unresolvedKeyTypes.put(domainType, keyType);
-        }
-      } else if ("getVersion".equals(method.getName()) && method.getArgumentTypes().length == 0) {
-        foundVersion = true;
-        if (!isResolvedKeyType(logger, method.getReturnType())) {
-          unresolvedKeyTypes.put(domainType, method.getReturnType());
-        }
-      } else if (findMethodName.equals(method.getName()) && method.getArgumentTypes().length == 1) {
-        foundFind = true;
-        findMethod = method;
-      }
-      if (foundFind && foundId && foundVersion) {
-        break;
-      }
-    }
-    if (!foundId) {
-      logger.poison("There is no getId() method in type %s", print(domainType));
-    }
-    if (!foundVersion) {
-      logger.poison("There is no getVersion() method in type %s", print(domainType));
-    }
-
-    if (foundFind) {
-      if (keyType != null && !isAssignable(logger, findMethod.getArgumentTypes()[0], keyType)) {
-        logger.poison("The key type returned by %s getId()"
-            + " cannot be used as the argument to %s(%s)", print(keyType), findMethod.getName(),
-            print(findMethod.getArgumentTypes()[0]));
-      }
-      if (!findMethod.isDeclaredStatic()) {
-        logger.poison("The %s method must be static", findMethodName);
-      }
-    } else {
-      logger.poison("There is no %s method in type %s that returns %2$s", findMethodName,
-          print(domainType));
-    }
-  }
-
-  /**
-   * Ensure that the given property method on an EntityProxy exists on the
-   * domain object.
-   */
-  private void checkPropertyMethod(ErrorContext logger, RFMethod clientPropertyMethod,
-      Type domainType) {
-    logger = logger.setMethod(clientPropertyMethod);
-
-    findCompatiblePropertyMethod(logger, domainType, createDomainMethod(logger,
-        clientPropertyMethod), !clientPropertyMethod.isValidationSkipped());
-  }
-
-  private void checkUnresolvedKeyTypes(ErrorContext logger) {
-    unresolvedKeyTypes.values().removeAll(domainToClientType.keySet());
-    if (unresolvedKeyTypes.isEmpty()) {
-      return;
-    }
-
-    for (Map.Entry<Type, Type> type : unresolvedKeyTypes.entrySet()) {
-      logger.setType(type.getKey()).poison(
-          "The domain type %s uses  a non-simple key type (%s)"
-              + " in its getId() or getVersion() method that" + " does not have a proxy mapping.",
-          print(type.getKey()), print(type.getValue()));
-    }
-  }
-
-  /**
-   * Convert a method declaration using client types (e.g. FooProxy) to domain
-   * types (e.g. Foo).
-   */
-  private Method createDomainMethod(ErrorContext logger, Method clientMethod) {
-    Type[] args = clientMethod.getArgumentTypes();
-    for (int i = 0, j = args.length; i < j; i++) {
-      args[i] = getDomainType(logger, args[i], true);
-    }
-    Type returnType = getDomainType(logger, clientMethod.getReturnType(), true);
-    return new Method(clientMethod.getName(), returnType, args);
-  }
-
-  /**
-   * Common checks to quickly determine if a type needs to be checked.
-   */
-  private boolean fastFail(String binaryName) {
-    if (!Name.isBinaryName(binaryName)) {
-      parentLogger.poison("%s is not a binary name", binaryName);
-      return true;
-    }
-
-    // Allow the poisoned flag to be reset without losing data
-    if (badTypes.contains(binaryName)) {
-      parentLogger.poison("Type type %s was previously marked as bad", binaryName);
-      return true;
-    }
-
-    // Don't revalidate the same type
-    if (!validatedTypes.add(binaryName)) {
-      return true;
-    }
-    return false;
-  }
-
-  /**
-   * Finds a compatible method declaration in <code>domainType</code>'s
-   * hierarchy that is assignment-compatible with the given Method.
-   */
-  private RFMethod findCompatibleMethod(final ErrorContext logger, Type domainType,
-      Method searchFor, boolean mustFind, boolean allowOverloads, boolean boxReturnTypes) {
-    String methodName = searchFor.getName();
-    Type[] clientArgs = searchFor.getArgumentTypes();
-    Type clientReturnType = searchFor.getReturnType();
-    if (boxReturnTypes) {
-      clientReturnType = maybeBoxType(clientReturnType);
-    }
-    // Pull all methods out of the domain type
-    Map<String, List<RFMethod>> domainLookup = new LinkedHashMap<String, List<RFMethod>>();
-    for (RFMethod method : getMethodsInHierarchy(logger, domainType)) {
-      List<RFMethod> list = domainLookup.get(method.getName());
-      if (list == null) {
-        list = new ArrayList<RFMethod>();
-        domainLookup.put(method.getName(), list);
-      }
-      list.add(method);
-    }
-
-    // Find the matching method in the domain object
-    List<RFMethod> methods = domainLookup.get(methodName);
-    if (methods == null) {
-      if (mustFind) {
-        logger.poison("Could not find any methods named %s in %s", methodName, print(domainType));
-      }
-      return null;
-    }
-    if (methods.size() > 1 && !allowOverloads) {
-      StringBuilder sb = new StringBuilder();
-      sb.append(String.format("Method overloads found in type %s named %s:\n", print(domainType),
-          methodName));
-      for (RFMethod method : methods) {
-        sb.append("  ").append(print(method)).append("\n");
-      }
-      logger.poison(sb.toString());
-      return null;
-    }
-
-    // Check each overloaded name
-    for (RFMethod domainMethod : methods) {
-      Type[] domainArgs = domainMethod.getArgumentTypes();
-      Type domainReturnType = domainMethod.getReturnType();
-      if (boxReturnTypes) {
-        /*
-         * When looking for the implementation of a Request<Integer>, we want to
-         * match either int or Integer, so we'll box the domain method's return
-         * type.
-         */
-        domainReturnType = maybeBoxType(domainReturnType);
-      }
-
-      /*
-       * Make sure the client args can be passed into the domain args and the
-       * domain return type into the client return type.
-       */
-      if (isAssignable(logger, domainArgs, clientArgs)
-          && isAssignable(logger, clientReturnType, domainReturnType)) {
-
-        logger.spam("Mapped client method " + print(searchFor) + " to " + print(domainMethod));
-        return domainMethod;
-      }
-    }
-    if (mustFind) {
-      logger.poison(messageCouldNotFindMethod(domainType, methods));
-    }
-    return null;
-  }
-
-  /**
-   * Finds a compatible method declaration in <code>domainType</code>'s
-   * hierarchy that is assignment-compatible with the given Method.
-   */
-  private RFMethod findCompatiblePropertyMethod(final ErrorContext logger, Type domainType,
-      Method searchFor, boolean mustFind) {
-    return findCompatibleMethod(logger, domainType, searchFor, mustFind, false, false);
-  }
-
-  /**
-   * Finds a compatible method declaration in <code>domainType</code>'s
-   * hierarchy that is assignment-compatible with the given Method.
-   */
-  private RFMethod findCompatibleServiceMethod(final ErrorContext logger, Type domainType,
-      Method searchFor, boolean mustFind) {
-    return findCompatibleMethod(logger, domainType, searchFor, mustFind, true, true);
-  }
-
-  /**
-   * This looks like it should be a utility method somewhere else, but I can't
-   * find it.
-   */
-  private Type getBoxedType(Type primitive) {
-    switch (primitive.getSort()) {
-      case Type.BOOLEAN:
-        return Type.getType(Boolean.class);
-      case Type.BYTE:
-        return Type.getType(Byte.class);
-      case Type.CHAR:
-        return Type.getType(Character.class);
-      case Type.DOUBLE:
-        return Type.getType(Double.class);
-      case Type.FLOAT:
-        return Type.getType(Float.class);
-      case Type.INT:
-        return Type.getType(Integer.class);
-      case Type.LONG:
-        return Type.getType(Long.class);
-      case Type.SHORT:
-        return Type.getType(Short.class);
-      case Type.VOID:
-        return Type.getType(Void.class);
-    }
-    throw new RuntimeException(primitive.getDescriptor() + " is not a primitive type");
-  }
-
-  /**
-   * Convert the type used in a client-side EntityProxy or RequestContext
-   * declaration to the equivalent domain type. Value types and supported
-   * collections are a pass-through. EntityProxy types will be resolved to their
-   * domain object type. RequestContext types will be resolved to their service
-   * object.
-   */
-  private Type getDomainType(ErrorContext logger, Type clientType, boolean requireMapping) {
-    Type domainType = clientToDomainType.get(clientType);
-    if (domainType != null) {
-      return domainType;
-    }
-    if (isValueType(logger, clientType) || isCollectionType(logger, clientType)) {
-      domainType = clientType;
-    } else if (entityProxyIntf.equals(clientType) || valueProxyIntf.equals(clientType)) {
-      domainType = objectType;
-    } else {
-      logger = logger.setType(clientType);
-      DomainMapper pv = new DomainMapper(logger);
-      visit(logger, clientType.getInternalName(), pv);
-      if (pv.getDomainInternalName() == null) {
-        if (requireMapping) {
-          logger.poison("%s has no mapping to a domain type (e.g. @%s or @%s)", print(clientType),
-              ProxyFor.class.getSimpleName(), Service.class.getSimpleName());
-        }
-        domainType = errorType;
-      } else {
-        domainType = Type.getObjectType(pv.getDomainInternalName());
-      }
-      if (pv.getLocatorInternalName() != null) {
-        Type locatorType = Type.getObjectType(pv.getLocatorInternalName());
-        clientToLocatorMap.put(clientType, locatorType);
-      }
-    }
-    addToDomainMap(logger, domainType, clientType);
-    if (domainType != errorType) {
-      maybeCheckProxyType(logger, clientType);
-    }
-    return domainType;
-  }
-
-  /**
-   * Collect all of the methods defined within a type hierarchy.
-   */
-  private Set<RFMethod> getMethodsInHierarchy(ErrorContext logger, Type domainType) {
-    Set<RFMethod> toReturn = methodsInHierarchy.get(domainType);
-    if (toReturn == null) {
-      logger = logger.setType(domainType);
-      toReturn = new MethodsInHierarchyCollector(logger).exec(domainType.getInternalName());
-      methodsInHierarchy.put(domainType, Collections.unmodifiableSet(toReturn));
-    }
-    return toReturn;
-  }
-
-  /**
-   * Examines a generic RequestContext method declaration and determines the
-   * expected domain return type. This implementation is limited in that it will
-   * not attempt to resolve type bounds since that would essentially require
-   * implementing TypeOracle. In the case where the type bound cannot be
-   * resolved, this method will return Object's type.
-   */
-  private Type getReturnType(ErrorContext logger, RFMethod method) {
-    logger = logger.setMethod(method);
-    final String[] returnType = {objectType.getInternalName()};
-    String signature = method.getSignature();
-
-    final int expectedCount;
-    if (method.getReturnType().equals(instanceRequestIntf)) {
-      expectedCount = 2;
-    } else if (method.getReturnType().equals(requestIntf)) {
-      expectedCount = 1;
-    } else {
-      logger.spam("Punting on " + signature);
-      return Type.getObjectType(returnType[0]);
-    }
-
-    // TODO(bobv): If a class-based TypeOracle is built, use that instead
-    new SignatureReader(signature).accept(new SignatureAdapter() {
-      @Override
-      public SignatureVisitor visitReturnType() {
-        return new SignatureAdapter() {
-          int count;
-
-          @Override
-          public SignatureVisitor visitTypeArgument(char wildcard) {
-            if (++count == expectedCount) {
-              return new SignatureAdapter() {
-                @Override
-                public void visitClassType(String name) {
-                  returnType[0] = name;
-                }
-              };
-            }
-            return super.visitTypeArgument(wildcard);
-          }
-        };
-      }
-    });
-
-    logger.spam("Extracted " + returnType[0]);
-    return Type.getObjectType(returnType[0]);
-  }
-
-  private List<Type> getSupertypes(ErrorContext logger, Type type) {
-    if (type.getSort() != Type.OBJECT) {
-      return Collections.emptyList();
-    }
-    List<Type> toReturn = supertypes.get(type);
-    if (toReturn != null) {
-      return toReturn;
-    }
-
-    logger = logger.setType(type);
-
-    toReturn = new SupertypeCollector(logger).exec(type);
-    supertypes.put(type, Collections.unmodifiableList(toReturn));
-    return toReturn;
-  }
-
-  private boolean isAssignable(ErrorContext logger, Type possibleSupertype, Type possibleSubtype) {
-    // Fast-path for same type
-    if (possibleSupertype.equals(possibleSubtype)) {
-      return true;
-    }
-
-    // Supertype calculation is cached
-    List<Type> allSupertypes = getSupertypes(logger, possibleSubtype);
-    return allSupertypes.contains(possibleSupertype);
-  }
-
-  private boolean isAssignable(ErrorContext logger, Type[] possibleSupertypes,
-      Type[] possibleSubtypes) {
-    // Check the same number of types
-    if (possibleSupertypes.length != possibleSubtypes.length) {
-      return false;
-    }
-    for (int i = 0, j = possibleSupertypes.length; i < j; i++) {
-      if (!isAssignable(logger, possibleSupertypes[i], possibleSubtypes[i])) {
-        return false;
-      }
-    }
-    return true;
-  }
-
-  private boolean isCollectionType(@SuppressWarnings("unused") ErrorContext logger, Type type) {
-    // keeping the logger arg just for internal consistency for our small minds
-    return "java/util/List".equals(type.getInternalName())
-        || "java/util/Set".equals(type.getInternalName());
-  }
-
-  /**
-   * Keep in sync with {@code ReflectiveServiceLayer.isKeyType()}.
-   */
-  private boolean isResolvedKeyType(ErrorContext logger, Type type) {
-    if (isValueType(logger, type)) {
-      return true;
-    }
-
-    // We have already seen a mapping for the key type
-    if (domainToClientType.containsKey(type)) {
-      return true;
-    }
-
-    return false;
-  }
-
-  private boolean isValueType(ErrorContext logger, Type type) {
-    if (type.getSort() != Type.OBJECT) {
-      return true;
-    }
-    if (valueTypes.contains(type)) {
-      return true;
-    }
-    logger = logger.setType(type);
-    if (isAssignable(logger, enumType, type)) {
-      return true;
-    }
-    return false;
-  }
-
-  private Type maybeBoxType(Type maybePrimitive) {
-    if (maybePrimitive.getSort() == Type.OBJECT) {
-      return maybePrimitive;
-    }
-    return getBoxedType(maybePrimitive);
-  }
-
-  /**
-   * Examines a type for an {@link ExtraTypes} annotation and processes the
-   * referred types.
-   */
-  private void maybeCheckExtraTypes(ErrorContext logger, Type type) {
-    maybeCheckProxyType(logger, new ExtraTypesCollector(logger.setType(type)).exec(type));
-  }
-
-  /**
-   * Examine an array of Types and call {@link #validateEntityProxy(String)} or
-   * {@link #validateValueProxy(String)} if the type is a proxy.
-   */
-  private void maybeCheckProxyType(ErrorContext logger, Type... types) {
-    for (Type type : types) {
-      if (isAssignable(logger, entityProxyIntf, type)) {
-        validateEntityProxy(type.getClassName());
-      } else if (isAssignable(logger, valueProxyIntf, type)) {
-        validateValueProxy(type.getClassName());
-      } else if (isAssignable(logger, baseProxyIntf, type)) {
-        logger.poison(
-            "Invalid type hierarchy for %s. Only types derived from %s or %s may be used.",
-            print(type), print(entityProxyIntf), print(valueProxyIntf));
-      }
-    }
-  }
-
-  /**
-   * Examine the arguments and return value of a method and check any
-   * EntityProxies referred.
-   */
-  private void maybeCheckReferredProxies(ErrorContext logger, RFMethod method) {
-    if (method.getSignature() != null) {
-      TypesInSignatureCollector collector = new TypesInSignatureCollector();
-      SignatureReader reader = new SignatureReader(method.getSignature());
-      reader.accept(collector);
-      maybeCheckProxyType(logger, collector.getFound());
-    } else {
-      Type[] argTypes = method.getArgumentTypes();
-      Type returnType = getReturnType(logger, method);
-
-      // Check EntityProxy args ond return types against the domain
-      maybeCheckProxyType(logger, argTypes);
-      maybeCheckProxyType(logger, returnType);
-    }
-  }
-
-  /**
-   * Returns {@code true} if the type is assignable to EntityProxy or ValueProxy
-   * and has a mapping to a domain type.
-   * 
-   * @see com.google.web.bindery.requestfactory.gwt.rebind.model.RequestFactoryModel#shouldAttemptProxyValidation()
-   */
-  private boolean shouldAttemptProxyValidation(ErrorContext logger, Type type) {
-    logger = logger.setType(type);
-    if (!isAssignable(logger, entityProxyIntf, type) && !isAssignable(logger, valueProxyIntf, type)) {
-      return false;
-    }
-    if (getDomainType(logger, type, false) == errorType) {
-      return false;
-    }
-    return true;
-  }
-
-  private void validateProxy(String binaryName, Type expectedType, boolean requireId) {
-    if (fastFail(binaryName)) {
-      return;
-    }
-
-    Type proxyType = Type.getObjectType(BinaryName.toInternalName(binaryName));
-    typeTokens.put(OperationKey.hash(binaryName), proxyType);
-    ErrorContext logger = parentLogger.setType(proxyType);
-
-    // Quick sanity check for calling code
-    if (!isAssignable(logger, expectedType, proxyType)) {
-      parentLogger.poison("%s is not a %s", print(proxyType), print(expectedType));
-      return;
-    }
-
-    // Check supertypes first
-    for (Type supertype : getSupertypes(logger, proxyType)) {
-      if (shouldAttemptProxyValidation(logger, supertype)) {
-        maybeCheckProxyType(logger, supertype);
-      }
-    }
-
-    // Find the domain type
-    Type domainType = getDomainType(logger, proxyType, false);
-    if (domainType == errorType) {
-      logger.poison("The type %s must be annotated with a @%s or @%s annotation", BinaryName
-          .toSourceName(binaryName), ProxyFor.class.getSimpleName(), ProxyForName.class
-          .getSimpleName());
-      return;
-    }
-
-    // Check for getId() and getVersion() in domain if no locator is specified
-    if (requireId) {
-      Type locatorType = clientToLocatorMap.get(proxyType);
-      if (locatorType == null) {
-        checkIdAndVersion(logger, domainType);
-      }
-    }
-
-    // Collect all methods in the client proxy type
-    Set<RFMethod> clientPropertyMethods = getMethodsInHierarchy(logger, proxyType);
-
-    // Find the equivalent domain getter/setter method
-    for (RFMethod clientPropertyMethod : clientPropertyMethods) {
-      // Ignore stableId(). Can't use descriptor due to overrides
-      if ("stableId".equals(clientPropertyMethod.getName())
-          && clientPropertyMethod.getArgumentTypes().length == 0) {
-        continue;
-      }
-      checkPropertyMethod(logger, clientPropertyMethod, domainType);
-      maybeCheckReferredProxies(logger, clientPropertyMethod);
-    }
-    maybeCheckExtraTypes(logger, proxyType);
-  }
-
-  /**
-   * Load the classfile for the given binary name and apply the provided
-   * visitor.
-   * 
-   * @return <code>true</code> if the visitor was successfully visited
-   */
-  private boolean visit(ErrorContext logger, String internalName, ClassVisitor visitor) {
-    assert Name.isInternalName(internalName) : "internalName";
-    logger.spam("Visiting " + internalName);
-    InputStream inputStream = null;
-    try {
-      inputStream = loader.getResourceAsStream(internalName + ".class");
-      if (inputStream == null) {
-        logger.poison("Could not find class file for " + internalName);
-        return false;
-      }
-      ClassReader reader = new ClassReader(inputStream);
-      reader.accept(visitor, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG
-          | ClassReader.SKIP_FRAMES);
-      return true;
-    } catch (IOException e) {
-      logger.poison("Unable to open " + internalName, e);
-    } finally {
-      if (inputStream != null) {
-        try {
-          inputStream.close();
-        } catch (IOException ignored) {
-        }
-      }
-    }
-    return false;
-  }
-}
diff --git a/user/src/com/google/web/bindery/requestfactory/server/RequestFactoryJarExtractor.java b/user/src/com/google/web/bindery/requestfactory/server/RequestFactoryJarExtractor.java
index c4c82c2..4289ce5 100644
--- a/user/src/com/google/web/bindery/requestfactory/server/RequestFactoryJarExtractor.java
+++ b/user/src/com/google/web/bindery/requestfactory/server/RequestFactoryJarExtractor.java
@@ -29,17 +29,18 @@
 import com.google.gwt.dev.asm.Type;
 import com.google.gwt.dev.asm.commons.Method;
 import com.google.gwt.dev.util.Name;
+import com.google.gwt.dev.util.Name.SourceOrBinaryName;
 import com.google.gwt.dev.util.Util;
 import com.google.web.bindery.event.shared.SimpleEventBus;
-import com.google.web.bindery.requestfactory.apt.RfApt;
-import com.google.web.bindery.requestfactory.server.RequestFactoryInterfaceValidator.ClassLoaderLoader;
-import com.google.web.bindery.requestfactory.server.RequestFactoryInterfaceValidator.ErrorContext;
-import com.google.web.bindery.requestfactory.server.RequestFactoryInterfaceValidator.Loader;
+import com.google.web.bindery.requestfactory.apt.RfValidator;
+import com.google.web.bindery.requestfactory.apt.ValidationTool;
+import com.google.web.bindery.requestfactory.gwt.client.RequestBatcher;
 import com.google.web.bindery.requestfactory.shared.BaseProxy;
 import com.google.web.bindery.requestfactory.shared.DefaultProxyStore;
 import com.google.web.bindery.requestfactory.shared.EntityProxy;
 import com.google.web.bindery.requestfactory.shared.EntityProxyChange;
 import com.google.web.bindery.requestfactory.shared.EntityProxyId;
+import com.google.web.bindery.requestfactory.shared.ExtraTypes;
 import com.google.web.bindery.requestfactory.shared.InstanceRequest;
 import com.google.web.bindery.requestfactory.shared.JsonRpcContent;
 import com.google.web.bindery.requestfactory.shared.JsonRpcProxy;
@@ -63,11 +64,9 @@
 import com.google.web.bindery.requestfactory.shared.ValueProxy;
 import com.google.web.bindery.requestfactory.shared.WriteOperation;
 import com.google.web.bindery.requestfactory.vm.RequestFactorySource;
-import com.google.web.bindery.requestfactory.vm.impl.TypeTokenResolver;
 import com.google.web.bindery.requestfactory.vm.testing.UrlRequestTransport;
 
 import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
 import java.io.File;
 import java.io.FileOutputStream;
 import java.io.IOException;
@@ -92,6 +91,7 @@
 import java.util.concurrent.LinkedBlockingQueue;
 import java.util.jar.JarOutputStream;
 import java.util.jar.Manifest;
+import java.util.logging.Level;
 import java.util.logging.Logger;
 import java.util.zip.ZipEntry;
 
@@ -109,6 +109,26 @@
    */
 
   /**
+   * An implementation of {@link Loader} that uses a {@link ClassLoader} to
+   * retrieve the class files.
+   */
+  public static class ClassLoaderLoader implements Loader {
+    private final ClassLoader loader;
+
+    public ClassLoaderLoader(ClassLoader loader) {
+      this.loader = loader;
+    }
+
+    public boolean exists(String resource) {
+      return loader.getResource(resource) != null;
+    }
+
+    public InputStream getResourceAsStream(String resource) {
+      return loader.getResourceAsStream(resource);
+    }
+  }
+
+  /**
    * Describes a way to emit the contents of a classpath, typically into a JAR
    * or filesystem directory.
    */
@@ -155,6 +175,28 @@
   }
 
   /**
+   * Abstracts the mechanism by which class files are loaded.
+   * 
+   * @see ClassLoaderLoader
+   */
+  public interface Loader {
+    /**
+     * Returns true if the specified resource can be loaded.
+     * 
+     * @param resource a resource name (e.g. <code>com/example/Foo.class</code>)
+     */
+    boolean exists(String resource);
+
+    /**
+     * Returns an InputStream to access the specified resource, or
+     * <code>null</code> if no such resource exists.
+     * 
+     * @param resource a resource name (e.g. <code>com/example/Foo.class</code>)
+     */
+    InputStream getResourceAsStream(String resource);
+  }
+
+  /**
    * Controls what is emitted by the tool.
    */
   public enum Mode {
@@ -206,6 +248,104 @@
     protected abstract boolean matches(String target);
   }
 
+  /**
+   * Improves error messages by providing context for the user.
+   * <p>
+   * Visible for testing.
+   */
+  static class ErrorContext {
+    private static String print(Method method) {
+      StringBuilder sb = new StringBuilder();
+      sb.append(print(method.getReturnType())).append(" ").append(method.getName()).append("(");
+      for (Type t : method.getArgumentTypes()) {
+        sb.append(print(t)).append(" ");
+      }
+      sb.append(")");
+      return sb.toString();
+    }
+
+    private static String print(Type type) {
+      return SourceOrBinaryName.toSourceName(type.getClassName());
+    }
+
+    private final Logger logger;
+    private final ErrorContext parent;
+
+    private Type currentType;
+
+    private Method currentMethod;
+
+    public ErrorContext(Logger logger) {
+      this.logger = logger;
+      this.parent = null;
+    }
+
+    protected ErrorContext(ErrorContext parent) {
+      this.logger = parent.logger;
+      this.parent = parent;
+    }
+
+    public void poison(String msg, Object... args) {
+      poison();
+      logger.logp(Level.SEVERE, currentType(), currentMethod(), String.format(msg, args));
+    }
+
+    public void poison(String msg, Throwable t) {
+      poison();
+      logger.logp(Level.SEVERE, currentType(), currentMethod(), msg, t);
+    }
+
+    public ErrorContext setMethod(Method method) {
+      ErrorContext toReturn = fork();
+      toReturn.currentMethod = method;
+      return toReturn;
+    }
+
+    public ErrorContext setType(Type type) {
+      ErrorContext toReturn = fork();
+      toReturn.currentType = type;
+      return toReturn;
+    }
+
+    public void spam(String msg, Object... args) {
+      logger.logp(Level.FINEST, currentType(), currentMethod(), String.format(msg, args));
+    }
+
+    protected ErrorContext fork() {
+      return new ErrorContext(this);
+    }
+
+    private String currentMethod() {
+      if (currentMethod != null) {
+        return print(currentMethod);
+      }
+      if (parent != null) {
+        return parent.currentMethod();
+      }
+      return null;
+    }
+
+    private String currentType() {
+      if (currentType != null) {
+        return print(currentType);
+      }
+      if (parent != null) {
+        return parent.currentType();
+      }
+      return null;
+    }
+
+    /**
+     * Populate {@link RequestFactoryInterfaceValidator#badTypes} with the
+     * current context.
+     */
+    private void poison() {
+      if (parent != null) {
+        parent.poison();
+      }
+    }
+  }
+
   private class AnnotationProcessor implements AnnotationVisitor {
     private final String sourceType;
     private final AnnotationVisitor av;
@@ -631,11 +771,12 @@
   @SuppressWarnings("deprecation")
   private static final Class<?>[] SHARED_CLASSES = {
       BaseProxy.class, DefaultProxyStore.class, EntityProxy.class, EntityProxyChange.class,
-      EntityProxyId.class, InstanceRequest.class, JsonRpcContent.class, JsonRpcProxy.class,
-      JsonRpcService.class, JsonRpcWireName.class, Locator.class, ProxyFor.class,
-      ProxyForName.class, ProxySerializer.class, ProxyStore.class, Receiver.class, Request.class,
-      RequestContext.class, RequestFactory.class, RequestTransport.class, ServerFailure.class,
-      Service.class, ServiceLocator.class, ServiceName.class, ValueProxy.class,
+      EntityProxyId.class, ExtraTypes.class, InstanceRequest.class, JsonRpcContent.class,
+      JsonRpcProxy.class, JsonRpcService.class, JsonRpcWireName.class, Locator.class,
+      ProxyFor.class, ProxyForName.class, ProxySerializer.class, ProxyStore.class, Receiver.class,
+      Request.class, RequestBatcher.class, RequestContext.class, RequestFactory.class,
+      RequestTransport.class, ServerFailure.class, Service.class, ServiceLocator.class,
+      ServiceName.class, ValueProxy.class,
       com.google.web.bindery.requestfactory.shared.Violation.class, WriteOperation.class,
       RequestFactorySource.class, SimpleEventBus.class};
 
@@ -645,18 +786,20 @@
   private static final int MAX_THREADS = 4;
 
   static {
+    List<Class<?>> aptClasses =
+        Collections.unmodifiableList(Arrays.<Class<?>> asList(RfValidator.class,
+            ValidationTool.class));
     List<Class<?>> sharedClasses = Arrays.<Class<?>> asList(SHARED_CLASSES);
 
     List<Class<?>> clientClasses = new ArrayList<Class<?>>();
     clientClasses.addAll(sharedClasses);
-    clientClasses.add(RfApt.class);
     clientClasses.add(UrlRequestTransport.class);
 
     List<Class<?>> serverClasses = new ArrayList<Class<?>>();
     serverClasses.addAll(Arrays.<Class<?>> asList(SERVER_CLASSES));
     serverClasses.addAll(sharedClasses);
 
-    SEEDS.put("apt", Collections.unmodifiableList(Arrays.<Class<?>> asList(RfApt.class)));
+    SEEDS.put("apt", aptClasses);
     SEEDS.put("client", Collections.unmodifiableList(clientClasses));
     SEEDS.put("server", Collections.unmodifiableList(serverClasses));
 
@@ -676,9 +819,12 @@
      * lookup, since the gwt-user code is compiled separately from its tests.
      */
     try {
-      SEEDS.put("test" + CODE_AND_SOURCE, Collections.unmodifiableList(Arrays.<Class<?>> asList(
-          Class.forName("com.google.web.bindery.requestfactory.vm.RequestFactoryJreSuite"), Class
-              .forName("com.google.web.bindery.requestfactory.server.SimpleBar"))));
+      List<Class<?>> testClasses =
+          Collections.unmodifiableList(Arrays.<Class<?>> asList(Class
+              .forName("com.google.web.bindery.requestfactory.vm.RequestFactoryJreSuite"), Class
+              .forName("com.google.web.bindery.requestfactory.server.SimpleBar")));
+      SEEDS.put("test", testClasses);
+      SEEDS.put("test" + SOURCE_ONLY, testClasses);
     } catch (ClassNotFoundException ignored) {
     }
   }
@@ -699,11 +845,12 @@
       System.err.println("Unknown target: " + target);
       System.exit(1);
     }
-    Map<String, byte[]> resources = createResources(target, seeds);
+    Map<String, byte[]> resources = createResources(seeds);
     Mode mode = Mode.match(target);
     Logger errorContext = Logger.getLogger(RequestFactoryJarExtractor.class.getName());
-    ClassLoaderLoader classLoader =
-        new ClassLoaderLoader(Thread.currentThread().getContextClassLoader());
+    RequestFactoryJarExtractor.ClassLoaderLoader classLoader =
+        new RequestFactoryJarExtractor.ClassLoaderLoader(Thread.currentThread()
+            .getContextClassLoader());
     JarEmitter jarEmitter = new JarEmitter(new File(args[1]));
     RequestFactoryJarExtractor extractor =
         new RequestFactoryJarExtractor(errorContext, classLoader, jarEmitter, seeds, resources,
@@ -712,20 +859,14 @@
     System.exit(extractor.isExecutionFailed() ? 1 : 0);
   }
 
-  private static Map<String, byte[]> createResources(String target, List<Class<?>> seeds)
+  private static Map<String, byte[]> createResources(List<Class<?>> seeds)
       throws UnsupportedEncodingException, IOException {
     Map<String, byte[]> resources;
-    if (seeds.contains(RfApt.class)) {
+    if (seeds.contains(RfValidator.class)) {
       // Add the annotation processor manifest
       resources =
           Collections.singletonMap("META-INF/services/" + Processor.class.getCanonicalName(),
-              RfApt.class.getCanonicalName().getBytes("UTF-8"));
-    } else if (("test" + CODE_AND_SOURCE).equals(target)) {
-      // Combine all type token maps to run tests
-      ByteArrayOutputStream out = new ByteArrayOutputStream();
-      TypeTokenResolver resolver = TypeTokenResolver.loadFromClasspath();
-      resolver.store(out);
-      resources = Collections.singletonMap(TypeTokenResolver.TOKEN_MANIFEST, out.toByteArray());
+              RfValidator.class.getCanonicalName().getBytes("UTF-8"));
     } else {
       resources = Collections.emptyMap();
     }
@@ -746,8 +887,8 @@
    * 
    * @return <code>true</code> if the visitor was successfully visited
    */
-  private static boolean visit(ErrorContext logger, Loader loader, String internalName,
-      ClassVisitor visitor) {
+  private static boolean visit(RequestFactoryJarExtractor.ErrorContext logger,
+      RequestFactoryJarExtractor.Loader loader, String internalName, ClassVisitor visitor) {
     assert Name.isInternalName(internalName) : "internalName";
     logger.spam("Visiting " + internalName);
     InputStream inputStream = null;
@@ -778,8 +919,8 @@
   private final Emitter emitter;
   private final ExecutorService ex;
   private final BlockingQueue<Future<?>> inProcess = new LinkedBlockingQueue<Future<?>>();
-  private final ErrorContext logger;
-  private final Loader loader;
+  private final RequestFactoryJarExtractor.ErrorContext logger;
+  private final RequestFactoryJarExtractor.Loader loader;
   private final Mode mode;
   private final Map<String, byte[]> resources;
   private final List<Class<?>> seeds;
@@ -787,9 +928,9 @@
   private final Set<String> sources = new ConcurrentSkipListSet<String>();
   private final ExecutorService writerService;
 
-  public RequestFactoryJarExtractor(Logger logger, Loader loader, Emitter emitter,
-      List<Class<?>> seeds, Map<String, byte[]> resources, Mode mode) {
-    this.logger = new ErrorContext(logger);
+  public RequestFactoryJarExtractor(Logger logger, RequestFactoryJarExtractor.Loader loader,
+      Emitter emitter, List<Class<?>> seeds, Map<String, byte[]> resources, Mode mode) {
+    this.logger = new RequestFactoryJarExtractor.ErrorContext(logger);
     this.loader = loader;
     this.emitter = emitter;
     this.resources = resources;
diff --git a/user/src/com/google/web/bindery/requestfactory/server/ResolverServiceLayer.java b/user/src/com/google/web/bindery/requestfactory/server/ResolverServiceLayer.java
index e62d8b9..1c602bd 100644
--- a/user/src/com/google/web/bindery/requestfactory/server/ResolverServiceLayer.java
+++ b/user/src/com/google/web/bindery/requestfactory/server/ResolverServiceLayer.java
@@ -24,12 +24,12 @@
 import com.google.web.bindery.requestfactory.shared.RequestFactory;
 import com.google.web.bindery.requestfactory.shared.Service;
 import com.google.web.bindery.requestfactory.shared.ServiceName;
+import com.google.web.bindery.requestfactory.vm.impl.Deobfuscator;
 import com.google.web.bindery.requestfactory.vm.impl.OperationKey;
 
 import java.lang.reflect.Method;
 import java.util.List;
 import java.util.Set;
-import java.util.logging.Logger;
 
 /**
  * Implements all of the resolution methods in ServiceLayer.
@@ -37,24 +37,14 @@
 final class ResolverServiceLayer extends ServiceLayerDecorator {
 
   private static Deobfuscator deobfuscator;
-  private static final Logger log = Logger.getLogger(ServiceLayer.class.getName());
 
-  private static synchronized boolean updateDeobfuscator(ClassLoader classLoader, String binaryName) {
-    RequestFactoryInterfaceValidator validator =
-        new RequestFactoryInterfaceValidator(log,
-            new RequestFactoryInterfaceValidator.ClassLoaderLoader(classLoader));
-    validator.antidote();
-    validator.validateRequestFactory(binaryName);
-    if (validator.isPoisoned()) {
-      return false;
+  private static synchronized void updateDeobfuscator(Class<? extends RequestFactory> clazz,
+      ClassLoader resolveClassesWith) {
+    Deobfuscator.Builder builder = Deobfuscator.Builder.load(clazz, resolveClassesWith);
+    if (deobfuscator != null) {
+      builder.merge(deobfuscator);
     }
-    if (deobfuscator == null) {
-      deobfuscator = validator.getDeobfuscator();
-    } else {
-      deobfuscator =
-          new Deobfuscator.Builder().merge(deobfuscator).merge(validator.getDeobfuscator()).build();
-    }
-    return true;
+    deobfuscator = builder.build();
   }
 
   @Override
@@ -182,10 +172,9 @@
 
   @Override
   public Class<? extends RequestFactory> resolveRequestFactory(String binaryName) {
-    if (!updateDeobfuscator(getTop().getDomainClassLoader(), binaryName)) {
-      die(null, "The RequestFactory %s did not pass validation", binaryName);
-    }
-    return forName(binaryName).asSubclass(RequestFactory.class);
+    Class<? extends RequestFactory> toReturn = forName(binaryName).asSubclass(RequestFactory.class);
+    updateDeobfuscator(toReturn, getTop().getDomainClassLoader());
+    return toReturn;
   }
 
   @Override
diff --git a/user/src/com/google/web/bindery/requestfactory/shared/SkipInterfaceValidation.java b/user/src/com/google/web/bindery/requestfactory/shared/SkipInterfaceValidation.java
index c2b69c1..3bc051d 100644
--- a/user/src/com/google/web/bindery/requestfactory/shared/SkipInterfaceValidation.java
+++ b/user/src/com/google/web/bindery/requestfactory/shared/SkipInterfaceValidation.java
@@ -30,6 +30,6 @@
  */
 @Documented
 @Retention(RetentionPolicy.RUNTIME)
-@Target({ElementType.METHOD, ElementType.TYPE})
+@Target({ElementType.METHOD, ElementType.PACKAGE, ElementType.TYPE})
 public @interface SkipInterfaceValidation {
 }
diff --git a/user/src/com/google/web/bindery/requestfactory/shared/impl/ProxySerializerImpl.java b/user/src/com/google/web/bindery/requestfactory/shared/impl/ProxySerializerImpl.java
index f13942b..2b523cf 100644
--- a/user/src/com/google/web/bindery/requestfactory/shared/impl/ProxySerializerImpl.java
+++ b/user/src/com/google/web/bindery/requestfactory/shared/impl/ProxySerializerImpl.java
@@ -39,8 +39,7 @@
 /**
  * The default implementation of ProxySerializer.
  */
-class ProxySerializerImpl extends AbstractRequestContext implements
-    ProxySerializer {
+class ProxySerializerImpl extends AbstractRequestContext implements ProxySerializer {
 
   /**
    * Used internally to unwind the stack if data cannot be found in the backing
@@ -55,13 +54,15 @@
    * ValueProxy), we'll assign a synthetic id that is local to the store being
    * used.
    */
-  private final Map<SimpleProxyId<?>, SimpleProxyId<?>> syntheticIds = new HashMap<SimpleProxyId<?>, SimpleProxyId<?>>();
+  private final Map<SimpleProxyId<?>, SimpleProxyId<?>> syntheticIds =
+      new HashMap<SimpleProxyId<?>, SimpleProxyId<?>>();
 
   /**
    * The ids of proxies whose content has been reloaded.
    */
   private final Set<SimpleProxyId<?>> restored = new HashSet<SimpleProxyId<?>>();
-  private final Map<SimpleProxyId<?>, AutoBean<?>> serialized = new HashMap<SimpleProxyId<?>, AutoBean<?>>();
+  private final Map<SimpleProxyId<?>, AutoBean<?>> serialized =
+      new HashMap<SimpleProxyId<?>, AutoBean<?>>();
 
   public ProxySerializerImpl(AbstractRequestFactory factory, ProxyStore store) {
     super(factory, Dialect.STANDARD);
@@ -73,7 +74,7 @@
     if (store.get(key) == null) {
       return null;
     }
-    OperationMessage op = getOperation(proxyType, key);
+    OperationMessage op = getOperation(key);
     @SuppressWarnings("unchecked")
     SimpleProxyId<T> id = (SimpleProxyId<T>) getId(op);
     return doDeserialize(id);
@@ -123,13 +124,12 @@
         }
 
         @Override
-        public void endVisitCollectionProperty(String propertyName,
-            AutoBean<Collection<?>> value, CollectionPropertyContext ctx) {
+        public void endVisitCollectionProperty(String propertyName, AutoBean<Collection<?>> value,
+            CollectionPropertyContext ctx) {
           if (value == null) {
             return;
           }
-          if (isEntityType(ctx.getElementType())
-              || isValueType(ctx.getElementType())) {
+          if (isEntityType(ctx.getElementType()) || isValueType(ctx.getElementType())) {
             for (Object o : value.as()) {
               serialize((BaseProxy) o);
             }
@@ -150,23 +150,20 @@
   SimpleProxyId<BaseProxy> getId(IdMessage op) {
     if (Strength.SYNTHETIC.equals(op.getStrength())) {
       return getRequestFactory().allocateSyntheticId(
-          getRequestFactory().getTypeFromToken(op.getTypeToken()),
-          op.getSyntheticId());
+          getRequestFactory().getTypeFromToken(op.getTypeToken()), op.getSyntheticId());
     }
     return super.getId(op);
   }
 
   @Override
-  <Q extends BaseProxy> AutoBean<Q> getProxyForReturnPayloadGraph(
-      SimpleProxyId<Q> id) {
+  <Q extends BaseProxy> AutoBean<Q> getProxyForReturnPayloadGraph(SimpleProxyId<Q> id) {
     AutoBean<Q> toReturn = super.getProxyForReturnPayloadGraph(id);
     if (restored.add(id)) {
       /*
        * If we haven't seen the id before, use the data in the OperationMessage
        * to repopulate the properties of the canonical bean for this id.
        */
-      OperationMessage op = getOperation(id.getProxyClass(),
-          getRequestFactory().getHistoryToken(id));
+      OperationMessage op = getOperation(getRequestFactory().getHistoryToken(id));
       this.processReturnOperation(id, op);
       toReturn.setTag(Constants.STABLE_ID, super.getId(op));
     }
@@ -196,32 +193,29 @@
    * Load the OperationMessage containing the object state from the backing
    * store.
    */
-  private <T> OperationMessage getOperation(Class<T> proxyType, String key) {
+  private OperationMessage getOperation(String key) {
     Splittable data = store.get(key);
     if (data == null) {
       throw new NoDataException();
     }
 
-    OperationMessage op = AutoBeanCodex.decode(MessageFactoryHolder.FACTORY,
-        OperationMessage.class, data).as();
+    OperationMessage op =
+        AutoBeanCodex.decode(MessageFactoryHolder.FACTORY, OperationMessage.class, data).as();
     return op;
   }
 
   /**
    * Convert any non-persistent ids into store-local synthetic ids.
    */
-  private <T extends BaseProxy> SimpleProxyId<T> serializedId(
-      SimpleProxyId<T> stableId) {
+  private <T extends BaseProxy> SimpleProxyId<T> serializedId(SimpleProxyId<T> stableId) {
     assert !stableId.isSynthetic();
     if (stableId.isEphemeral()) {
       @SuppressWarnings("unchecked")
       SimpleProxyId<T> syntheticId = (SimpleProxyId<T>) syntheticIds.get(stableId);
       if (syntheticId == null) {
         int nextId = store.nextId();
-        assert nextId >= 0 : "ProxyStore.nextId() returned a negative number "
-            + nextId;
-        syntheticId = getRequestFactory().allocateSyntheticId(
-            stableId.getProxyClass(), nextId + 1);
+        assert nextId >= 0 : "ProxyStore.nextId() returned a negative number " + nextId;
+        syntheticId = getRequestFactory().allocateSyntheticId(stableId.getProxyClass(), nextId + 1);
         syntheticIds.put(stableId, syntheticId);
       }
       return syntheticId;
@@ -231,10 +225,9 @@
 
   private void serializeOneProxy(SimpleProxyId<?> idForSerialization,
       AutoBean<? extends BaseProxy> bean) {
-    AutoBean<OperationMessage> op = makeOperationMessage(
-        serializedId(BaseProxyCategory.stableId(bean)), bean, false);
+    AutoBean<OperationMessage> op =
+        makeOperationMessage(serializedId(BaseProxyCategory.stableId(bean)), bean, false);
 
-    store.put(getRequestFactory().getHistoryToken(idForSerialization),
-        AutoBeanCodex.encode(op));
+    store.put(getRequestFactory().getHistoryToken(idForSerialization), AutoBeanCodex.encode(op));
   }
 }
diff --git a/user/src/com/google/web/bindery/requestfactory/vm/InProcessRequestContext.java b/user/src/com/google/web/bindery/requestfactory/vm/InProcessRequestContext.java
index 6dcedca..86390c8 100644
--- a/user/src/com/google/web/bindery/requestfactory/vm/InProcessRequestContext.java
+++ b/user/src/com/google/web/bindery/requestfactory/vm/InProcessRequestContext.java
@@ -30,8 +30,8 @@
 import com.google.web.bindery.requestfactory.shared.impl.AbstractRequestContext;
 import com.google.web.bindery.requestfactory.shared.impl.RequestData;
 import com.google.web.bindery.requestfactory.shared.impl.SimpleProxyId;
+import com.google.web.bindery.requestfactory.vm.impl.Deobfuscator;
 import com.google.web.bindery.requestfactory.vm.impl.OperationKey;
-import com.google.web.bindery.requestfactory.vm.impl.TypeTokenResolver;
 
 import java.lang.annotation.Annotation;
 import java.lang.reflect.InvocationHandler;
@@ -184,14 +184,14 @@
 
   static final Object[] NO_ARGS = new Object[0];
   private final Class<? extends RequestContext> context;
+  private final Deobfuscator deobfuscator;
   private final Dialect dialect;
-  private final TypeTokenResolver tokenResolver;
 
   protected InProcessRequestContext(InProcessRequestFactory factory, Dialect dialect,
       Class<? extends RequestContext> context) {
     super(factory, dialect);
     this.context = context;
-    this.tokenResolver = factory.getTypeTokenResolver();
+    this.deobfuscator = factory.getDeobfuscator();
     this.dialect = dialect;
   }
 
@@ -205,7 +205,7 @@
   @Override
   protected <T extends BaseProxy> AutoBean<T> createProxy(Class<T> clazz, SimpleProxyId<T> id,
       boolean useAppendedContexts) {
-    if (tokenResolver.isReferencedType(clazz.getName())) {
+    if (deobfuscator.isReferencedType(clazz.getName())) {
       return super.createProxy(clazz, id, useAppendedContexts);
     }
     throw new IllegalArgumentException("Unknown proxy type " + clazz.getName());
diff --git a/user/src/com/google/web/bindery/requestfactory/vm/InProcessRequestFactory.java b/user/src/com/google/web/bindery/requestfactory/vm/InProcessRequestFactory.java
index fc3802b..05b3f03 100644
--- a/user/src/com/google/web/bindery/requestfactory/vm/InProcessRequestFactory.java
+++ b/user/src/com/google/web/bindery/requestfactory/vm/InProcessRequestFactory.java
@@ -31,10 +31,9 @@
 import com.google.web.bindery.requestfactory.shared.impl.EntityProxyCategory;
 import com.google.web.bindery.requestfactory.shared.impl.ValueProxyCategory;
 import com.google.web.bindery.requestfactory.vm.InProcessRequestContext.RequestContextHandler;
+import com.google.web.bindery.requestfactory.vm.impl.Deobfuscator;
 import com.google.web.bindery.requestfactory.vm.impl.OperationKey;
-import com.google.web.bindery.requestfactory.vm.impl.TypeTokenResolver;
 
-import java.io.IOException;
 import java.lang.reflect.InvocationHandler;
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
@@ -74,15 +73,17 @@
   }
 
   private final Class<? extends RequestFactory> requestFactoryInterface;
-  private final TypeTokenResolver deobfuscator;
+  private final Deobfuscator deobfuscator;
 
   public InProcessRequestFactory(Class<? extends RequestFactory> requestFactoryInterface) {
     this.requestFactoryInterface = requestFactoryInterface;
-    try {
-      deobfuscator = TypeTokenResolver.loadFromClasspath();
-    } catch (IOException e) {
-      throw new RuntimeException(e);
-    }
+    deobfuscator =
+        Deobfuscator.Builder.load(requestFactoryInterface,
+            Thread.currentThread().getContextClassLoader()).build();
+  }
+
+  public Deobfuscator getDeobfuscator() {
+    return deobfuscator;
   }
 
   @Override
@@ -131,8 +132,4 @@
   protected String getTypeToken(Class<? extends BaseProxy> clazz) {
     return isEntityType(clazz) || isValueType(clazz) ? OperationKey.hash(clazz.getName()) : null;
   }
-
-  TypeTokenResolver getTypeTokenResolver() {
-    return deobfuscator;
-  }
 }
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
new file mode 100644
index 0000000..5c16de1
--- /dev/null
+++ b/user/src/com/google/web/bindery/requestfactory/vm/impl/Deobfuscator.java
@@ -0,0 +1,188 @@
+/*
+ * 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
+ * 
+ * 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.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * 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 {
+  /**
+   * Creates Deobfuscators.
+   */
+  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.
+     * 
+     * @see com.google.web.bindery.requestfactory.apt.DeobfuscatorBuilder
+     * @see com.google.web.bindery.requestfactory.server.ResolverServiceLayer
+     */
+    public static Builder load(Class<?> clazz, ClassLoader resolveClassesWith) {
+      Throwable ex;
+      try {
+        Class<?> found;
+        try {
+          // Used by the server
+          found = Class.forName(clazz.getName() + GENERATED_SUFFIX, false, resolveClassesWith);
+        } catch (ClassNotFoundException ignored) {
+          // Used by JRE-only clients
+          found = Class.forName(clazz.getName() + GENERATED_SUFFIX_LITE, false, resolveClassesWith);
+        }
+        Class<? extends Builder> builderClass = found.asSubclass(Builder.class);
+        Builder builder = builderClass.newInstance();
+        return builder;
+      } catch (ClassNotFoundException e) {
+        throw new RuntimeException("The RequestFactory ValidationTool must be run for the "
+            + clazz.getCanonicalName() + " RequestFactory type");
+      } catch (InstantiationException e) {
+        ex = e;
+      } catch (IllegalAccessException e) {
+        ex = e;
+      }
+      throw new RuntimeException(ex);
+    }
+
+    private Deobfuscator d = new Deobfuscator();
+
+    {
+      d.domainToClientType = new HashMap<String, List<String>>();
+      d.operationData = new HashMap<OperationKey, OperationData>();
+      d.typeTokens = new HashMap<String, String>();
+    }
+
+    public Deobfuscator build() {
+      Deobfuscator toReturn = d;
+      toReturn.domainToClientType = Collections.unmodifiableMap(toReturn.domainToClientType);
+      toReturn.operationData = Collections.unmodifiableMap(toReturn.operationData);
+      toReturn.referencedTypes =
+          Collections.unmodifiableSet(new HashSet<String>(toReturn.typeTokens.values()));
+      toReturn.typeTokens = Collections.unmodifiableMap(toReturn.typeTokens);
+      d = null;
+      return toReturn;
+    }
+
+    public Builder merge(Deobfuscator existing) {
+      d.domainToClientType.putAll(existing.domainToClientType);
+      d.operationData.putAll(existing.operationData);
+      // referencedTypes recomputed in build()
+      d.typeTokens.putAll(existing.typeTokens);
+      return this;
+    }
+
+    public Builder withClientToDomainMappings(String domainBinaryName, List<String> value) {
+      List<String> clientBinaryNames;
+      switch (value.size()) {
+        case 0:
+          clientBinaryNames = Collections.emptyList();
+          break;
+        case 1:
+          clientBinaryNames = Collections.singletonList(value.get(0));
+          break;
+        default:
+          clientBinaryNames = Collections.unmodifiableList(new ArrayList<String>(value));
+      }
+      d.domainToClientType.put(domainBinaryName, clientBinaryNames);
+      return this;
+    }
+
+    public Builder withOperation(OperationKey key, OperationData data) {
+      d.operationData.put(key, data);
+      return this;
+    }
+
+    public Builder withRawTypeToken(String token, String binaryName) {
+      d.typeTokens.put(token, binaryName);
+      return this;
+    }
+  }
+
+  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).
+   */
+  private Map<String, List<String>> domainToClientType;
+  private Map<OperationKey, OperationData> operationData;
+  private Set<String> referencedTypes;
+  /**
+   * Map of obfuscated ids to binary class names.
+   */
+  private Map<String, String> typeTokens;
+
+  Deobfuscator() {
+  }
+
+  /**
+   * 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);
+  }
+
+  /**
+   * Returns a method descriptor that should be invoked on the service object.
+   */
+  public String getDomainMethodDescriptor(String operation) {
+    OperationData data = getData(operation);
+    return data == null ? null : data.getDomainMethodDescriptor();
+  }
+
+  public String getRequestContext(String operation) {
+    OperationData data = getData(operation);
+    return data == null ? null : data.getRequestContext();
+  }
+
+  public String getRequestContextMethodDescriptor(String operation) {
+    OperationData data = getData(operation);
+    return data == null ? null : data.getClientMethodDescriptor();
+  }
+
+  public String getRequestContextMethodName(String operation) {
+    OperationData data = getData(operation);
+    return data == null ? null : data.getMethodName();
+  }
+
+  /**
+   * Returns a type's binary name based on an obfuscated token.
+   */
+  public String getTypeFromToken(String token) {
+    return typeTokens.get(token);
+  }
+
+  public boolean isReferencedType(String name) {
+    return referencedTypes.contains(name);
+  }
+
+  private OperationData getData(String operation) {
+    OperationData data = operationData.get(new OperationKey(operation));
+    return data;
+  }
+}
diff --git a/user/src/com/google/web/bindery/requestfactory/server/OperationData.java b/user/src/com/google/web/bindery/requestfactory/vm/impl/OperationData.java
similarity index 87%
rename from user/src/com/google/web/bindery/requestfactory/server/OperationData.java
rename to user/src/com/google/web/bindery/requestfactory/vm/impl/OperationData.java
index 09a2aa6..04225bf 100644
--- a/user/src/com/google/web/bindery/requestfactory/server/OperationData.java
+++ b/user/src/com/google/web/bindery/requestfactory/vm/impl/OperationData.java
@@ -13,7 +13,7 @@
  * License for the specific language governing permissions and limitations under
  * the License.
  */
-package com.google.web.bindery.requestfactory.server;
+package com.google.web.bindery.requestfactory.vm.impl;
 
 import com.google.gwt.dev.asm.Type;
 import com.google.gwt.dev.asm.commons.Method;
@@ -21,7 +21,7 @@
 /**
  * Describes operations that the client may ask the server to perform.
  */
-class OperationData {
+public class OperationData {
   /**
    * Creates {@link OperationData} instances.
    */
@@ -49,22 +49,22 @@
       return toReturn;
     }
 
-    public Builder setClientMethodDescriptor(String clientMethodDescriptor) {
+    public Builder withClientMethodDescriptor(String clientMethodDescriptor) {
       d.clientMethodDescriptor = clientMethodDescriptor;
       return this;
     }
 
-    public Builder setDomainMethodDescriptor(String domainMethodDescriptor) {
+    public Builder withDomainMethodDescriptor(String domainMethodDescriptor) {
       d.domainMethodDescriptor = domainMethodDescriptor;
       return this;
     }
 
-    public Builder setMethodName(String methodName) {
+    public Builder withMethodName(String methodName) {
       d.methodName = methodName;
       return this;
     }
 
-    public Builder setRequestContext(String requestContext) {
+    public Builder withRequestContext(String requestContext) {
       d.requestContextBinaryName = requestContext;
       return this;
     }
diff --git a/user/src/com/google/web/bindery/requestfactory/vm/impl/TypeTokenResolver.java b/user/src/com/google/web/bindery/requestfactory/vm/impl/TypeTokenResolver.java
deleted file mode 100644
index 5699c9a..0000000
--- a/user/src/com/google/web/bindery/requestfactory/vm/impl/TypeTokenResolver.java
+++ /dev/null
@@ -1,141 +0,0 @@
-/*
- * 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
- * 
- * 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.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.net.URL;
-import java.util.Collections;
-import java.util.Enumeration;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Map;
-import java.util.Properties;
-import java.util.Set;
-import java.util.SortedMap;
-import java.util.TreeMap;
-
-/**
- * Resolves payload type tokens to binary class names.
- */
-public class TypeTokenResolver {
-  /**
-   * Constructs {@link TypeTokenResolver} instances.
-   */
-  public static class Builder {
-    private TypeTokenResolver d = new TypeTokenResolver();
-
-    /**
-     * To be removed as well.
-     */
-    public Builder addTypeToken(String token, String binaryName) {
-      d.typeTokens.put(token, binaryName);
-      return this;
-    }
-
-    public TypeTokenResolver build() {
-      TypeTokenResolver toReturn = d;
-      toReturn.typeTokens = Collections.unmodifiableMap(toReturn.typeTokens);
-      toReturn.referencedTypes =
-          Collections.unmodifiableSet(new HashSet<String>(toReturn.typeTokens.values()));
-      d = null;
-      return toReturn;
-    }
-
-    /**
-     * Adds data from a single InputStream to the state accumulated by the
-     * Builder.
-     */
-    public void load(InputStream in) throws IOException {
-      Properties props = new Properties();
-      props.load(in);
-      for (Map.Entry<Object, Object> entry : props.entrySet()) {
-        addTypeToken(entry.getKey().toString(), entry.getValue().toString());
-      }
-      in.close();
-    }
-  }
-
-  public static final String TOKEN_MANIFEST = "META-INF/requestFactory/typeTokens";
-
-  /**
-   * Loads all resources on path {@value #TOKEN_MANIFEST} and creates a
-   * TypeTokenResolver.
-   */
-  public static TypeTokenResolver loadFromClasspath() throws IOException {
-    Builder builder;
-    boolean mustLoad = true;
-    try {
-      // Look for a pre-cooked Builder type
-      Class<?> maybeBuilderImpl =
-          Class.forName(TypeTokenResolver.class.getName() + "BuilderImpl", false, Thread
-              .currentThread().getContextClassLoader());
-      builder = maybeBuilderImpl.asSubclass(Builder.class).newInstance();
-      mustLoad = false;
-    } catch (ClassNotFoundException ignored) {
-      // Try manifest-based approach
-      builder = new Builder();
-    } catch (InstantiationException e) {
-      throw new RuntimeException("Could not instantiate TypeTokenResolverImpl", e);
-    } catch (IllegalAccessException e) {
-      throw new RuntimeException("Could not instantiate TypeTokenResolverImpl", e);
-    }
-    Enumeration<URL> locations =
-        Thread.currentThread().getContextClassLoader().getResources(TOKEN_MANIFEST);
-    if (mustLoad && !locations.hasMoreElements()) {
-      throw new RuntimeException("No token manifest found.  Did the RequestFactory annotation"
-          + " processor run? Check classpath for " + TOKEN_MANIFEST + " file and ensure that"
-          + " your proxy types are compiled with the requestfactory-apt.jar on javac's classpath.");
-    }
-    while (locations.hasMoreElements()) {
-      builder.load(locations.nextElement().openStream());
-    }
-    return builder.build();
-  }
-
-  /**
-   * The values of {@link #typeTokens}.
-   */
-  private Set<String> referencedTypes;
-
-  /**
-   * Map of obfuscated ids to binary class names.
-   */
-  private Map<String, String> typeTokens = new HashMap<String, String>();
-
-  public SortedMap<String, String> getAllTypeTokens() {
-    return new TreeMap<String, String>(typeTokens);
-  }
-
-  public String getTypeFromToken(String typeToken) {
-    return typeTokens.get(typeToken);
-  }
-
-  public boolean isReferencedType(String binaryName) {
-    return referencedTypes.contains(binaryName);
-  }
-
-  /**
-   * Closes the OutputStream.
-   */
-  public void store(OutputStream out) throws IOException {
-    Properties props = new Properties();
-    props.putAll(typeTokens);
-    props.store(out, "GENERATED FILE -- DO NOT EDIT");
-    out.close();
-  }
-}
diff --git a/user/test/com/google/web/bindery/requestfactory/apt/DiagnosticComparator.java b/user/test/com/google/web/bindery/requestfactory/apt/DiagnosticComparator.java
new file mode 100644
index 0000000..80b1938
--- /dev/null
+++ b/user/test/com/google/web/bindery/requestfactory/apt/DiagnosticComparator.java
@@ -0,0 +1,43 @@
+/*
+ * 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
+ * 
+ * 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.apt;
+
+import java.util.Comparator;
+
+import javax.tools.Diagnostic;
+import javax.tools.JavaFileObject;
+
+/**
+ * Orders Diagnostic objects by filename, position, and message.
+ */
+class DiagnosticComparator implements Comparator<Diagnostic<? extends JavaFileObject>> {
+  @Override
+  public int compare(Diagnostic<? extends JavaFileObject> o1,
+      Diagnostic<? extends JavaFileObject> o2) {
+    int c;
+    if (o1.getSource() != null && o2.getSource() != null) {
+      c = o1.getSource().toUri().toString().compareTo(o2.getSource().toUri().toString());
+      if (c != 0) {
+        return c;
+      }
+    }
+    long p = o1.getPosition() - o2.getPosition();
+    if (p != 0) {
+      return Long.signum(p);
+    }
+    return o1.getMessage(null).compareTo(o2.getMessage(null));
+  }
+}
diff --git a/user/test/com/google/web/bindery/requestfactory/apt/EntityProxyCheckDomainMapping.java b/user/test/com/google/web/bindery/requestfactory/apt/EntityProxyCheckDomainMapping.java
new file mode 100644
index 0000000..6a1ee98
--- /dev/null
+++ b/user/test/com/google/web/bindery/requestfactory/apt/EntityProxyCheckDomainMapping.java
@@ -0,0 +1,70 @@
+/*
+ * 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
+ * 
+ * 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.apt;
+
+import com.google.web.bindery.requestfactory.shared.EntityProxy;
+import com.google.web.bindery.requestfactory.shared.ProxyFor;
+
+@Expected({
+    @Expect(method = "domainGetIdStatic"),
+    @Expect(method = "domainGetVersionStatic"),
+    @Expect(method = "domainFindNotStatic", args = "Domain"),
+    @Expect(method = "domainMethodWrongModifier", args = {"false", "getFoo"}),
+    @Expect(method = "domainNoDefaultConstructor", args = {
+        "Domain", "EntityProxyCheckDomainMapping", "RequestContext"}, warning = true)})
+@ProxyFor(EntityProxyCheckDomainMapping.Domain.class)
+@SuppressWarnings("requestfactory")
+interface EntityProxyCheckDomainMapping extends EntityProxy {
+  public static class Domain {
+    public static String getFoo() {
+      return null;
+    }
+
+    public static String getId() {
+      return null;
+    }
+
+    public static String getVersion() {
+      return null;
+    }
+
+    public Domain(@SuppressWarnings("unused") boolean ignored) {
+    }
+
+    public Domain findDomain(@SuppressWarnings("unused") String id) {
+      return null;
+    }
+  }
+
+  String getFoo();
+
+  @Expect(method = "domainMissingMethod", args = "java.lang.String getMissingProperty()")
+  String getMissingProperty();
+
+  @Expected({
+      @Expect(method = "methodNoDomainPeer", args = {"java.lang.Object", "false"}, warning = true),
+      @Expect(method = "untransportableType", args = "java.lang.Object")})
+  Object getUntransportable();
+
+  @Expect(method = "methodNoDomainPeer", args = {"java.lang.Object", "true"}, warning = true)
+  void setUntransportable(
+      @Expect(method = "untransportableType", args = "java.lang.Object") Object obj);
+
+  @Expected({
+      @Expect(method = "proxyOnlyGettersSetters"),
+      @Expect(method = "domainMissingMethod", args = "java.lang.String notAProperty()")})
+  String notAProperty();
+}
diff --git a/user/test/com/google/web/bindery/requestfactory/apt/EntityProxyMismatchedFind.java b/user/test/com/google/web/bindery/requestfactory/apt/EntityProxyMismatchedFind.java
new file mode 100644
index 0000000..ad943b8
--- /dev/null
+++ b/user/test/com/google/web/bindery/requestfactory/apt/EntityProxyMismatchedFind.java
@@ -0,0 +1,40 @@
+/*
+ * 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
+ * 
+ * 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.apt;
+
+import com.google.web.bindery.requestfactory.shared.EntityProxy;
+import com.google.web.bindery.requestfactory.shared.ProxyFor;
+
+@Expect(method = "domainMissingFind", args = {
+    "com.google.web.bindery.requestfactory.apt.EntityProxyMismatchedFind.Domain", "Domain",
+    "java.lang.String", "EntityProxyMismatchedFind"}, warning = true)
+@ProxyFor(EntityProxyMismatchedFind.Domain.class)
+@SuppressWarnings("requestfactory")
+interface EntityProxyMismatchedFind extends EntityProxy {
+  public static class Domain {
+    public static Domain findDomain(@SuppressWarnings("unused") Integer id) {
+      return null;
+    }
+
+    public String getId() {
+      return null;
+    }
+
+    public String getVersion() {
+      return null;
+    }
+  }
+}
diff --git a/user/test/com/google/web/bindery/requestfactory/apt/EntityProxyMissingDomainLocatorMethods.java b/user/test/com/google/web/bindery/requestfactory/apt/EntityProxyMissingDomainLocatorMethods.java
new file mode 100644
index 0000000..a222e01
--- /dev/null
+++ b/user/test/com/google/web/bindery/requestfactory/apt/EntityProxyMissingDomainLocatorMethods.java
@@ -0,0 +1,28 @@
+/*
+ * 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
+ * 
+ * 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.apt;
+
+import com.google.web.bindery.requestfactory.shared.EntityProxy;
+import com.google.web.bindery.requestfactory.shared.ProxyFor;
+
+@Expected({
+    @Expect(method = "domainNoGetId", args = "com.google.web.bindery.requestfactory.apt.EntityProxyMissingDomainLocatorMethods.Domain"),
+    @Expect(method = "domainNoGetVersion", args = "com.google.web.bindery.requestfactory.apt.EntityProxyMissingDomainLocatorMethods.Domain")})
+@ProxyFor(EntityProxyMissingDomainLocatorMethods.Domain.class)
+interface EntityProxyMissingDomainLocatorMethods extends EntityProxy {
+  public static class Domain {
+  }
+}
diff --git a/user/test/com/google/web/bindery/requestfactory/apt/EntityProxyMissingDomainType.java b/user/test/com/google/web/bindery/requestfactory/apt/EntityProxyMissingDomainType.java
new file mode 100644
index 0000000..2282bc8
--- /dev/null
+++ b/user/test/com/google/web/bindery/requestfactory/apt/EntityProxyMissingDomainType.java
@@ -0,0 +1,24 @@
+/*
+ * 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
+ * 
+ * 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.apt;
+
+import com.google.web.bindery.requestfactory.shared.EntityProxy;
+import com.google.web.bindery.requestfactory.shared.ProxyForName;
+
+@Expect(method = "proxyMissingDomainType", args = "does.not.exist", warning = true)
+@ProxyForName("does.not.exist")
+interface EntityProxyMissingDomainType extends EntityProxy {
+}
diff --git a/user/test/com/google/web/bindery/requestfactory/apt/Expect.java b/user/test/com/google/web/bindery/requestfactory/apt/Expect.java
new file mode 100644
index 0000000..953b2b5
--- /dev/null
+++ b/user/test/com/google/web/bindery/requestfactory/apt/Expect.java
@@ -0,0 +1,43 @@
+/*
+ * 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
+ * 
+ * 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.apt;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * This annotation is applied to any element that is expected to be the target
+ * of an error or a warning from the validator.
+ */
+@Documented
+@Retention(RetentionPolicy.RUNTIME)
+@interface Expect {
+  /**
+   * The arguments to be passed to {@code method}.
+   */
+  String[] args() default {};
+
+  /**
+   * The name of a method defined in {@link Messages}.
+   */
+  String method();
+
+  /**
+   * Specifies whether the diagnostic will be a warning or an error.
+   */
+  boolean warning() default false;
+}
\ No newline at end of file
diff --git a/user/test/com/google/web/bindery/requestfactory/apt/ExpectCollector.java b/user/test/com/google/web/bindery/requestfactory/apt/ExpectCollector.java
new file mode 100644
index 0000000..31f7cde
--- /dev/null
+++ b/user/test/com/google/web/bindery/requestfactory/apt/ExpectCollector.java
@@ -0,0 +1,115 @@
+/*
+ * 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
+ * 
+ * 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.apt;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.Set;
+
+import javax.annotation.processing.AbstractProcessor;
+import javax.annotation.processing.Messager;
+import javax.annotation.processing.RoundEnvironment;
+import javax.annotation.processing.SupportedAnnotationTypes;
+import javax.annotation.processing.SupportedSourceVersion;
+import javax.lang.model.SourceVersion;
+import javax.lang.model.element.Element;
+import javax.lang.model.element.TypeElement;
+import javax.lang.model.util.ElementScanner6;
+import javax.tools.Diagnostic.Kind;
+
+/**
+ * This is a trivial scanner that collects {@code @Expect} declarations on a
+ * type. It does not perform any type-chasing or supertype traversal. The named
+ * method on the {@link Messages} class is invoked with the provided arguments
+ * and the resulting message is emitted to the {@link Messager}.
+ */
+@SupportedAnnotationTypes({
+    "com.google.web.bindery.requestfactory.apt.Expect",
+    "com.google.web.bindery.requestfactory.apt.Expected"})
+@SupportedSourceVersion(SourceVersion.RELEASE_6)
+class ExpectCollector extends AbstractProcessor {
+  class Scanner extends ElementScanner6<Void, Void> {
+
+    private final Messager messager;
+
+    public Scanner(Messager messager) {
+      this.messager = messager;
+    }
+
+    @Override
+    public Void scan(Element e, Void p) {
+      Expect expect = e.getAnnotation(Expect.class);
+      if (expect != null) {
+        addExpect(expect, e);
+      }
+      Expected expected = e.getAnnotation(Expected.class);
+      if (expected != null) {
+        for (Expect v : expected.value()) {
+          addExpect(v, e);
+        }
+      }
+      return super.scan(e, p);
+    }
+
+    private void addExpect(Expect expect, Element accumulator) {
+      Method toInvoke = null;
+      for (Method m : Messages.class.getDeclaredMethods()) {
+        if (m.getName().equals(expect.method())) {
+          toInvoke = m;
+          break;
+        }
+      }
+      if (toInvoke == null) {
+        throw new RuntimeException("No method named " + expect.method());
+      }
+      String[] originalArgs = expect.args();
+      Object[] args = new Object[originalArgs.length];
+      for (int i = 0, j = args.length; i < j; i++) {
+        // Special case for domainMethodWrongModifier
+        if (boolean.class.equals(toInvoke.getParameterTypes()[i])) {
+          args[i] = Boolean.valueOf(originalArgs[i]);
+        } else {
+          args[i] = originalArgs[i];
+        }
+      }
+      String message;
+      Throwable ex;
+      try {
+        message = (String) toInvoke.invoke(null, args);
+        if (expect.warning()) {
+          message += Messages.warnSuffix();
+        }
+        messager.printMessage(expect.warning() ? Kind.WARNING : Kind.ERROR, message, accumulator);
+        return;
+      } catch (IllegalArgumentException e) {
+        ex = e;
+      } catch (IllegalAccessException e) {
+        ex = e;
+      } catch (InvocationTargetException e) {
+        ex = e.getCause();
+      }
+      throw new RuntimeException("Could not get test message", ex);
+    }
+  }
+
+  @Override
+  public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
+    Scanner scanner = new Scanner(processingEnv.getMessager());
+    scanner.scan(roundEnv.getElementsAnnotatedWith(Expect.class), null);
+    scanner.scan(roundEnv.getElementsAnnotatedWith(Expected.class), null);
+    return false;
+  }
+}
\ No newline at end of file
diff --git a/user/test/com/google/web/bindery/requestfactory/apt/Expected.java b/user/test/com/google/web/bindery/requestfactory/apt/Expected.java
new file mode 100644
index 0000000..5e73483
--- /dev/null
+++ b/user/test/com/google/web/bindery/requestfactory/apt/Expected.java
@@ -0,0 +1,27 @@
+/*
+ * 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
+ * 
+ * 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.apt;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Allows several {@link Expect} annotations to be applied to an element.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@interface Expected {
+  Expect[] value();
+}
\ No newline at end of file
diff --git a/user/test/com/google/web/bindery/requestfactory/apt/MyRequestContext.java b/user/test/com/google/web/bindery/requestfactory/apt/MyRequestContext.java
new file mode 100644
index 0000000..7f6b579
--- /dev/null
+++ b/user/test/com/google/web/bindery/requestfactory/apt/MyRequestContext.java
@@ -0,0 +1,75 @@
+/*
+ * 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
+ * 
+ * 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.apt;
+
+import com.google.web.bindery.requestfactory.shared.EntityProxy;
+import com.google.web.bindery.requestfactory.shared.InstanceRequest;
+import com.google.web.bindery.requestfactory.shared.JsonRpcProxy;
+import com.google.web.bindery.requestfactory.shared.ProxyFor;
+import com.google.web.bindery.requestfactory.shared.ProxyForName;
+import com.google.web.bindery.requestfactory.shared.Request;
+import com.google.web.bindery.requestfactory.shared.RequestContext;
+import com.google.web.bindery.requestfactory.shared.ValueProxy;
+
+/*
+ * No error about a missing mapping expected because this type isn't referenced
+ * from a RequestFactory.
+ */
+interface MyRequestContext extends RequestContext {
+  @Expect(method = "proxyMustBeAnnotated", args = "ProxyMissingAnnotation")
+  interface ProxyMissingAnnotation extends EntityProxy {
+  }
+
+  @Expected({
+      @Expect(method = "proxyMissingDomainType", args = "bad", warning = true),
+      @Expect(method = "redundantAnnotation", args = "ProxyForName"),
+      @Expect(method = "redundantAnnotation", args = "JsonRpcProxy")})
+  @ProxyFor(ProxyWithRedundantAnnotations.Domain.class)
+  @ProxyForName("bad")
+  @JsonRpcProxy
+  interface ProxyWithRedundantAnnotations extends ValueProxy {
+    static class Domain {
+    }
+  }
+
+  /**
+   * Because this is not referenced from the context, it shouldn't generate an
+   * error.
+   */
+  interface UnusedProxyBase extends EntityProxy {
+  }
+
+  @Expect(method = "untransportableType", args = "java.lang.Object")
+  InstanceRequest<ProxyWithRedundantAnnotations, Object> badInstanceReturn();
+
+  @Expect(method = "contextRequiredReturnTypes", args = {"Request", "InstanceRequest"})
+  String badMethod();
+
+  Request<Void> badParam(@Expect(method = "untransportableType", args = "java.lang.Object") Object o);
+
+  @Expect(method = "untransportableType", args = "java.lang.Object")
+  Request<Object> badReturn();
+
+  Request<ProxyMissingAnnotation> forceAnnotation();
+
+  @Expect(method = "rawType")
+  @SuppressWarnings("rawtypes")
+  InstanceRequest rawInstance();
+
+  @Expect(method = "rawType")
+  @SuppressWarnings("rawtypes")
+  Request rawRequest();
+}
\ No newline at end of file
diff --git a/user/test/com/google/web/bindery/requestfactory/apt/MyRequestFactory.java b/user/test/com/google/web/bindery/requestfactory/apt/MyRequestFactory.java
new file mode 100644
index 0000000..e5444c3
--- /dev/null
+++ b/user/test/com/google/web/bindery/requestfactory/apt/MyRequestFactory.java
@@ -0,0 +1,53 @@
+/*
+ * 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
+ * 
+ * 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.apt;
+
+import com.google.web.bindery.requestfactory.shared.JsonRpcService;
+import com.google.web.bindery.requestfactory.shared.RequestContext;
+import com.google.web.bindery.requestfactory.shared.RequestFactory;
+import com.google.web.bindery.requestfactory.shared.Service;
+import com.google.web.bindery.requestfactory.shared.ServiceName;
+import com.google.web.bindery.requestfactory.shared.impl.AbstractRequestContext;
+
+interface MyRequestFactory extends RequestFactory {
+
+  @Expect(method = "contextMustBeAnnotated", args = "ReferencedContextWithoutMapping")
+  interface ReferencedContextWithoutMapping extends RequestContext {
+  }
+
+  // No error expected
+  interface UnreferencedContextWithoutMapping extends RequestContext {
+  }
+
+  @Expected({
+      @Expect(method = "redundantAnnotation", args = "ServiceName"),
+      @Expect(method = "redundantAnnotation", args = "JsonRpcService")})
+  @Service(Object.class)
+  @ServiceName("java.lang.Object")
+  @JsonRpcService
+  interface UnreferencedContextWithRedundantMapping extends RequestContext {
+  }
+
+  @Expect(method = "factoryMustReturnInterface", args = "AbstractRequestContext")
+  AbstractRequestContext concreteContext();
+
+  ReferencedContextWithoutMapping shouldError();
+
+  @Expected({
+      @Expect(method = "factoryNoMethodParameters"),
+      @Expect(method = "factoryMustBeAssignable", args = "RequestContext")})
+  String stringMethod(String bad);
+}
\ No newline at end of file
diff --git a/user/test/com/google/web/bindery/requestfactory/apt/RequestContextMissingDomainType.java b/user/test/com/google/web/bindery/requestfactory/apt/RequestContextMissingDomainType.java
new file mode 100644
index 0000000..57de96a
--- /dev/null
+++ b/user/test/com/google/web/bindery/requestfactory/apt/RequestContextMissingDomainType.java
@@ -0,0 +1,24 @@
+/*
+ * 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
+ * 
+ * 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.apt;
+
+import com.google.web.bindery.requestfactory.shared.RequestContext;
+import com.google.web.bindery.requestfactory.shared.ServiceName;
+
+@Expect(method = "contextMissingDomainType", args = "does.not.exist", warning = true)
+@ServiceName("does.not.exist")
+interface RequestContextMissingDomainType extends RequestContext {
+}
diff --git a/user/test/com/google/web/bindery/requestfactory/apt/RequestContextUsingUnmappedProxy.java b/user/test/com/google/web/bindery/requestfactory/apt/RequestContextUsingUnmappedProxy.java
new file mode 100644
index 0000000..ddb0efd
--- /dev/null
+++ b/user/test/com/google/web/bindery/requestfactory/apt/RequestContextUsingUnmappedProxy.java
@@ -0,0 +1,33 @@
+/*
+ * 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
+ * 
+ * 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.apt;
+
+import com.google.web.bindery.requestfactory.shared.Request;
+import com.google.web.bindery.requestfactory.shared.RequestContext;
+import com.google.web.bindery.requestfactory.shared.Service;
+
+@Service(RequestContextUsingUnmappedProxy.Domain.class)
+interface RequestContextUsingUnmappedProxy extends RequestContext {
+  public static class Domain {
+  }
+
+  @Expected({
+      @Expect(method = "methodNoDomainPeer", args = {
+          "com.google.web.bindery.requestfactory.apt.EntityProxyMissingDomainType", "false"}, warning = true),
+      @Expect(method = "methodNoDomainPeer", args = {
+          "com.google.web.bindery.requestfactory.apt.EntityProxyMissingDomainType", "true"}, warning = true)})
+  Request<EntityProxyMissingDomainType> cannotVerify(EntityProxyMissingDomainType proxy);
+}
diff --git a/user/test/com/google/web/bindery/requestfactory/apt/RequestContextWithMismatchedBoxes.java b/user/test/com/google/web/bindery/requestfactory/apt/RequestContextWithMismatchedBoxes.java
new file mode 100644
index 0000000..ee854aa
--- /dev/null
+++ b/user/test/com/google/web/bindery/requestfactory/apt/RequestContextWithMismatchedBoxes.java
@@ -0,0 +1,60 @@
+/*
+ * 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
+ * 
+ * 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.apt;
+
+import com.google.web.bindery.requestfactory.shared.ProxyFor;
+import com.google.web.bindery.requestfactory.shared.Request;
+import com.google.web.bindery.requestfactory.shared.RequestContext;
+import com.google.web.bindery.requestfactory.shared.Service;
+import com.google.web.bindery.requestfactory.shared.ValueProxy;
+
+/**
+ * Checks that proxy property accessors match exactly and that context method
+ * arguments must match exactly.
+ */
+@Service(RequestContextWithMismatchedBoxes.Domain.class)
+interface RequestContextWithMismatchedBoxes extends RequestContext {
+  @Expect(method = "domainMissingMethod", args = "java.lang.Void checkBoxed(int)")
+  Request<Void> checkBoxed(int value);
+
+  @Expect(method = "domainMissingMethod", args = "java.lang.Void checkPrimitive(java.lang.Integer)")
+  Request<Void> checkPrimitive(Integer value);
+
+  @ProxyFor(Domain.class)
+  interface ProxyMismatchedGetterA extends ValueProxy {
+    @Expect(method = "domainMissingMethod", args = "int getBoxed()")
+    int getBoxed();
+
+    @Expect(method = "domainMissingMethod", args = "java.lang.Integer getPrimitive()")
+    Integer getPrimitive();
+  }
+
+  static class Domain {
+    static void checkBoxed(@SuppressWarnings("unused") Integer value) {
+    }
+
+    static void checkPrimitive(@SuppressWarnings("unused") int value) {
+    }
+
+    Integer getBoxed() {
+      return null;
+    }
+
+    int getPrimitive() {
+      return 0;
+    }
+  }
+}
diff --git a/user/test/com/google/web/bindery/requestfactory/apt/RequestContextWithMismatchedInstance.java b/user/test/com/google/web/bindery/requestfactory/apt/RequestContextWithMismatchedInstance.java
new file mode 100644
index 0000000..26c428d
--- /dev/null
+++ b/user/test/com/google/web/bindery/requestfactory/apt/RequestContextWithMismatchedInstance.java
@@ -0,0 +1,61 @@
+/*
+ * 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
+ * 
+ * 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.apt;
+
+import com.google.web.bindery.requestfactory.shared.EntityProxy;
+import com.google.web.bindery.requestfactory.shared.InstanceRequest;
+import com.google.web.bindery.requestfactory.shared.ProxyFor;
+import com.google.web.bindery.requestfactory.shared.Request;
+import com.google.web.bindery.requestfactory.shared.RequestContext;
+import com.google.web.bindery.requestfactory.shared.Service;
+
+/**
+ * Tests Request and InstanceRequest methods bound to methods with the wrong
+ * static modifier.
+ */
+@Expected({
+    @Expect(method = "domainMethodWrongModifier", args = {"true", "instanceMethod"}),
+    @Expect(method = "domainMethodWrongModifier", args = {"false", "staticMethod"})})
+@Service(RequestContextWithMismatchedInstance.Domain.class)
+interface RequestContextWithMismatchedInstance extends RequestContext {
+  static class Domain {
+    public static Domain findDomain(@SuppressWarnings("unused") String id) {
+      return null;
+    }
+
+    public static void staticMethod() {
+    }
+
+    public String getId() {
+      return null;
+    }
+
+    public String getVersion() {
+      return null;
+    }
+
+    public void instanceMethod() {
+    }
+  }
+
+  @ProxyFor(Domain.class)
+  interface Proxy extends EntityProxy {
+  }
+
+  Request<Void> instanceMethod();
+
+  InstanceRequest<Proxy, Void> staticMethod();
+}
diff --git a/user/test/com/google/web/bindery/requestfactory/apt/RfValidatorTest.java b/user/test/com/google/web/bindery/requestfactory/apt/RfValidatorTest.java
new file mode 100644
index 0000000..a6c3ec7
--- /dev/null
+++ b/user/test/com/google/web/bindery/requestfactory/apt/RfValidatorTest.java
@@ -0,0 +1,202 @@
+/*
+ * 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
+ * 
+ * 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.apt;
+
+import com.google.gwt.dev.util.Util;
+import com.google.web.bindery.requestfactory.gwt.client.RequestFactoryPolymorphicTest;
+import com.google.web.bindery.requestfactory.shared.LoggingRequest;
+import com.google.web.bindery.requestfactory.shared.SimpleRequestFactory;
+import com.google.web.bindery.requestfactory.shared.TestRequestFactory;
+import com.google.web.bindery.requestfactory.shared.impl.FindRequest;
+
+import junit.framework.TestCase;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.StringWriter;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.TreeSet;
+
+import javax.tools.Diagnostic;
+import javax.tools.DiagnosticCollector;
+import javax.tools.JavaCompiler;
+import javax.tools.JavaCompiler.CompilationTask;
+import javax.tools.JavaFileManager;
+import javax.tools.JavaFileObject;
+import javax.tools.SimpleJavaFileObject;
+import javax.tools.ToolProvider;
+
+/**
+ * Integration test of {@link RfValidator} using the Java6 tools API to invoke
+ * the Java compiler. This test requires that the gwt-user source is available
+ * on the classpath.
+ */
+public class RfValidatorTest extends TestCase {
+  /**
+   * Provides access to a source file from the classpath.
+   */
+  private static class UriJavaFileObject extends SimpleJavaFileObject {
+    public static UriJavaFileObject create(Class<?> toLoad) {
+      try {
+        String path = toLoad.getName().replace('.', '/') + ".java";
+        /*
+         * SimpleJavaFileObject does not like the URI's created from jar URLs
+         * since their path does not start with a leading forward-slash. The
+         * choice of "classpath" as the scheme is arbitrary and meaningless.
+         */
+        URI fakeLocation = new URI("classpath:/" + path);
+        return new UriJavaFileObject(fakeLocation, Thread.currentThread().getContextClassLoader()
+            .getResource(path));
+      } catch (URISyntaxException e) {
+        throw new RuntimeException(e);
+      }
+    }
+
+    private final URL contents;
+
+    public UriJavaFileObject(URI fakeLocation, URL contents) throws URISyntaxException {
+      super(fakeLocation, JavaFileObject.Kind.SOURCE);
+      this.contents = contents;
+    }
+
+    @Override
+    public CharSequence getCharContent(boolean ignored) throws IOException {
+      return Util.readStreamAsString(contents.openStream());
+    }
+  }
+
+  /**
+   * Smoke test to ensure that appropriate errors and warnings are emitted.
+   */
+  public void testErrorsAndWarnings() throws IOException {
+    testGeneratedMessages(EntityProxyCheckDomainMapping.class);
+    testGeneratedMessages(EntityProxyMismatchedFind.class);
+    testGeneratedMessages(EntityProxyMissingDomainLocatorMethods.class);
+    testGeneratedMessages(EntityProxyMissingDomainType.class);
+    testGeneratedMessages(MyRequestContext.class);
+    testGeneratedMessages(MyRequestFactory.class);
+    testGeneratedMessages(RequestContextMissingDomainType.class);
+    testGeneratedMessages(RequestContextUsingUnmappedProxy.class,
+        EntityProxyMissingDomainType.class);
+    testGeneratedMessages(RequestContextWithMismatchedBoxes.class);
+    testGeneratedMessages(RequestContextWithMismatchedInstance.class);
+  }
+
+  /**
+   * The target classes for this method don't contain any {@code @Expect}
+   * annotations, so this test will verify that they are error- and
+   * warning-free.
+   */
+  public void testTestClasses() throws IOException {
+    testGeneratedMessages(RequestFactoryPolymorphicTest.class);
+    testGeneratedMessages(FindRequest.class);
+    testGeneratedMessages(LoggingRequest.class);
+    testGeneratedMessages(SimpleRequestFactory.class);
+    testGeneratedMessages(TestRequestFactory.class);
+  }
+
+  /**
+   * The target classes for this method don't contain any {@code @Expect}
+   * annotations, so this test will verify that they are error- and
+   * warning-free.
+   * 
+   * @throws IOException
+   */
+  public void testTestClassesClientOnly() throws IOException {
+    testGeneratedMessages(true, RequestFactoryPolymorphicTest.class);
+    testGeneratedMessages(true, FindRequest.class);
+    testGeneratedMessages(true, LoggingRequest.class);
+    testGeneratedMessages(true, SimpleRequestFactory.class);
+    testGeneratedMessages(true, TestRequestFactory.class);
+  }
+
+  private void assertEquals(TreeSet<Diagnostic<? extends JavaFileObject>> expected,
+      TreeSet<Diagnostic<? extends JavaFileObject>> actual) {
+    List<Diagnostic<?>> unmatched = new ArrayList<Diagnostic<?>>();
+
+    // Remove actual elements from expect, saving any unexpected elements
+    for (Diagnostic<? extends JavaFileObject> d : actual) {
+      if (!expected.remove(d)) {
+        unmatched.add(d);
+      }
+    }
+
+    assertTrue("Did not see expected errors: " + expected + "\n\nLeftovers :" + unmatched, expected
+        .isEmpty());
+    assertTrue("Unexpected errors: " + unmatched, unmatched.isEmpty());
+  }
+
+  private void testGeneratedMessages(Class<?>... classes) throws IOException {
+    testGeneratedMessages(false, classes);
+  }
+
+  /**
+   * Run the annotation processor over one or more classes and verify that the
+   * appropriate messages are generated.
+   */
+  private void testGeneratedMessages(boolean clientOnly, Class<?>... classes) throws IOException {
+    JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
+    if (compiler == null) {
+      // This test is being run without a full JDK
+      return;
+    }
+
+    // Don't spray files in random locations
+    File tempFile = File.createTempFile(RfValidatorTest.class.getSimpleName(), ".jar");
+    tempFile.deleteOnExit();
+    JavaFileManager fileManager =
+        new ValidationTool.JarOrDirectoryOutputFileManager(tempFile, compiler
+            .getStandardFileManager(null, null, null));
+
+    List<JavaFileObject> files = new ArrayList<JavaFileObject>(classes.length);
+    for (Class<?> clazz : classes) {
+      JavaFileObject obj = UriJavaFileObject.create(clazz);
+      files.add(obj);
+    }
+    StringWriter errorWriter = new StringWriter();
+    RfValidator rfValidator = new RfValidator();
+    rfValidator.setForceErrors(true);
+    rfValidator.setClientOnly(clientOnly);
+
+    DiagnosticCollector<JavaFileObject> expectedCollector =
+        new DiagnosticCollector<JavaFileObject>();
+    CompilationTask expectedTask =
+        compiler.getTask(errorWriter, fileManager, expectedCollector, Arrays.asList("-proc:only"),
+            null, files);
+    expectedTask.setProcessors(Arrays.asList(new ExpectCollector()));
+    expectedTask.call();
+
+    DiagnosticCollector<JavaFileObject> actualCollector = new DiagnosticCollector<JavaFileObject>();
+    CompilationTask actualTask =
+        compiler.getTask(errorWriter, fileManager, actualCollector, Arrays.asList("-proc:only"),
+            null, files);
+    actualTask.setProcessors(Arrays.asList(rfValidator));
+    actualTask.call();
+
+    TreeSet<Diagnostic<? extends JavaFileObject>> expected =
+        new TreeSet<Diagnostic<? extends JavaFileObject>>(new DiagnosticComparator());
+    expected.addAll(expectedCollector.getDiagnostics());
+    TreeSet<Diagnostic<? extends JavaFileObject>> actual =
+        new TreeSet<Diagnostic<? extends JavaFileObject>>(new DiagnosticComparator());
+    actual.addAll(actualCollector.getDiagnostics());
+    assertEquals(expected, actual);
+  }
+}
diff --git a/user/test/com/google/web/bindery/requestfactory/apt/package-info.java b/user/test/com/google/web/bindery/requestfactory/apt/package-info.java
new file mode 100644
index 0000000..d2eab38
--- /dev/null
+++ b/user/test/com/google/web/bindery/requestfactory/apt/package-info.java
@@ -0,0 +1,25 @@
+/*
+ * 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
+ * 
+ * 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.
+ */
+
+/**
+ * By disabling the following annotation, a manual check can be made against the
+ * various {@link Expect} annotations and the IDE's behavior.
+ */
+@SkipInterfaceValidation
+package com.google.web.bindery.requestfactory.apt;
+
+import com.google.web.bindery.requestfactory.shared.SkipInterfaceValidation;
+
diff --git a/user/test/com/google/web/bindery/requestfactory/gwt/RequestFactoryGwtJreSuite.java b/user/test/com/google/web/bindery/requestfactory/gwt/RequestFactoryGwtJreSuite.java
index 5a2f008..2505961 100644
--- a/user/test/com/google/web/bindery/requestfactory/gwt/RequestFactoryGwtJreSuite.java
+++ b/user/test/com/google/web/bindery/requestfactory/gwt/RequestFactoryGwtJreSuite.java
@@ -15,6 +15,7 @@
  */
 package com.google.web.bindery.requestfactory.gwt;
 
+import com.google.web.bindery.requestfactory.apt.RfValidatorTest;
 import com.google.web.bindery.requestfactory.gwt.rebind.model.RequestFactoryModelTest;
 
 import junit.framework.Test;
@@ -33,6 +34,7 @@
     TestSuite suite = new TestSuite(
         "requestfactory package tests that require the JRE and gwt-user");
     suite.addTestSuite(RequestFactoryModelTest.class);
+    suite.addTestSuite(RfValidatorTest.class);
 
     return suite;
   }
diff --git a/user/test/com/google/web/bindery/requestfactory/server/BoxesAndPrimitivesJreTest.java b/user/test/com/google/web/bindery/requestfactory/server/BoxesAndPrimitivesJreTest.java
index 7ba01ad..fb15660 100644
--- a/user/test/com/google/web/bindery/requestfactory/server/BoxesAndPrimitivesJreTest.java
+++ b/user/test/com/google/web/bindery/requestfactory/server/BoxesAndPrimitivesJreTest.java
@@ -15,111 +15,20 @@
  */
 package com.google.web.bindery.requestfactory.server;
 
-import com.google.gwt.dev.asm.Type;
-import com.google.gwt.dev.asm.commons.Method;
-import com.google.web.bindery.requestfactory.server.RequestFactoryInterfaceValidatorTest.VisibleErrorContext;
 import com.google.web.bindery.requestfactory.shared.BoxesAndPrimitivesTest;
-import com.google.web.bindery.requestfactory.shared.EntityProxy;
-import com.google.web.bindery.requestfactory.shared.ProxyFor;
-import com.google.web.bindery.requestfactory.shared.Request;
-import com.google.web.bindery.requestfactory.shared.RequestContext;
-import com.google.web.bindery.requestfactory.shared.Service;
-import com.google.web.bindery.requestfactory.shared.SkipInterfaceValidation;
-
-import java.util.Arrays;
-import java.util.logging.Logger;
 
 /**
- * A JRE version of {@link BoxesAndPrimitivesTest} with additional validation
- * tests.
+ * A JRE version of {@link BoxesAndPrimitivesTest}.
  */
-@SkipInterfaceValidation
 public class BoxesAndPrimitivesJreTest extends BoxesAndPrimitivesTest {
 
-  @Service(ServiceImpl.class)
-  interface ContextMismatchedParameterA extends RequestContext {
-    Request<Void> checkBoxed(int value);
-  }
-
-  @Service(ServiceImpl.class)
-  interface ContextMismatchedParameterB extends RequestContext {
-    Request<Void> checkPrimitive(Integer value);
-  }
-
-  @ProxyFor(Entity.class)
-  interface ProxyMismatchedGetterA extends EntityProxy {
-    int getBoxed();
-  }
-
-  @ProxyFor(Entity.class)
-  interface ProxyMismatchedGetterB extends EntityProxy {
-    Integer getPrimitive();
-  }
-
-  private VisibleErrorContext errors;
-  private RequestFactoryInterfaceValidator v;
-
   @Override
   public String getModuleName() {
     return null;
   }
 
-  public void test() {
-    RequestFactoryInterfaceValidator v = new RequestFactoryInterfaceValidator(
-        Logger.getAnonymousLogger(),
-        new RequestFactoryInterfaceValidator.ClassLoaderLoader(
-            getClass().getClassLoader()));
-    v.validateRequestFactory(Factory.class.getName());
-    assertFalse(v.isPoisoned());
-  }
-
-  /**
-   * Tests that mismatched primitive verses boxed getters are correctly
-   * reported.
-   */
-  public void testMismatchedGetters() {
-    v.validateEntityProxy(ProxyMismatchedGetterA.class.getName());
-    v.validateEntityProxy(ProxyMismatchedGetterB.class.getName());
-    assertTrue(v.isPoisoned());
-
-    String getBoxedMessage = RequestFactoryInterfaceValidator.messageCouldNotFindMethod(
-        Type.getType(Entity.class),
-        Arrays.asList(new Method("getBoxed", "()Ljava/lang/Integer;")));
-    String getPrimitiveMessage = RequestFactoryInterfaceValidator.messageCouldNotFindMethod(
-        Type.getType(Entity.class),
-        Arrays.asList(new Method("getPrimitive", "()I")));
-    assertEquals(Arrays.asList(getBoxedMessage, getPrimitiveMessage),
-        errors.logs);
-  }
-
-  /**
-   * Tests that mismatched parameter types are correctly reported.
-   */
-  public void testMismatchedParameters() {
-    v.validateRequestContext(ContextMismatchedParameterA.class.getName());
-    v.validateRequestContext(ContextMismatchedParameterB.class.getName());
-
-    String checkBoxedMessage = RequestFactoryInterfaceValidator.messageCouldNotFindMethod(
-        Type.getType(ServiceImpl.class),
-        Arrays.asList(new Method("checkBoxed", "(Ljava/lang/Integer;)V")));
-    String checkPrimitiveMessage = RequestFactoryInterfaceValidator.messageCouldNotFindMethod(
-        Type.getType(ServiceImpl.class),
-        Arrays.asList(new Method("checkPrimitive", "(I)V")));
-    assertEquals(Arrays.asList(checkBoxedMessage, checkPrimitiveMessage),
-        errors.logs);
-  }
-
   @Override
   protected Factory createFactory() {
     return RequestFactoryJreTest.createInProcess(Factory.class);
   }
-
-  @Override
-  protected void gwtSetUp() {
-    super.gwtSetUp();
-    errors = new VisibleErrorContext(Logger.getAnonymousLogger());
-    v = new RequestFactoryInterfaceValidator(errors,
-        new RequestFactoryInterfaceValidator.ClassLoaderLoader(
-            getClass().getClassLoader()));
-  }
 }
diff --git a/user/test/com/google/web/bindery/requestfactory/server/RequestFactoryInterfaceValidatorTest.java b/user/test/com/google/web/bindery/requestfactory/server/RequestFactoryInterfaceValidatorTest.java
deleted file mode 100644
index 934ee07..0000000
--- a/user/test/com/google/web/bindery/requestfactory/server/RequestFactoryInterfaceValidatorTest.java
+++ /dev/null
@@ -1,522 +0,0 @@
-/*
- * Copyright 2010 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.server.RequestFactoryInterfaceValidator.ClassLoaderLoader;
-import com.google.web.bindery.requestfactory.shared.EntityProxy;
-import com.google.web.bindery.requestfactory.shared.InstanceRequest;
-import com.google.web.bindery.requestfactory.shared.Locator;
-import com.google.web.bindery.requestfactory.shared.ProxyFor;
-import com.google.web.bindery.requestfactory.shared.ProxyForName;
-import com.google.web.bindery.requestfactory.shared.Request;
-import com.google.web.bindery.requestfactory.shared.RequestContext;
-import com.google.web.bindery.requestfactory.shared.RequestFactory;
-import com.google.web.bindery.requestfactory.shared.Service;
-import com.google.web.bindery.requestfactory.shared.ServiceName;
-import com.google.web.bindery.requestfactory.shared.SimpleRequestFactory;
-import com.google.web.bindery.requestfactory.shared.SkipInterfaceValidation;
-import com.google.web.bindery.requestfactory.shared.ValueProxy;
-import com.google.web.bindery.requestfactory.shared.impl.FindRequest;
-
-import junit.framework.TestCase;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Random;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-
-/**
- * JRE tests for {@link RequestFactoryInterfaceValidator}.
- */
-@SkipInterfaceValidation
-public class RequestFactoryInterfaceValidatorTest extends TestCase {
-  static class ClinitEntity {
-    static ClinitEntity findClinitEntity(@SuppressWarnings("unused") String key) {
-      return null;
-    }
-
-    static ClinitEntity request() {
-      return null;
-    }
-
-    Object OBJECT = new Object();
-
-    String getId() {
-      return null;
-    }
-
-    int getVersion() {
-      return 0;
-    }
-  }
-  @ProxyFor(ClinitEntity.class)
-  interface ClinitEntityProxy extends EntityProxy {
-    Object OBJECT = new Object();
-  }
-  @Service(ClinitEntity.class)
-  interface ClinitRequestContext extends RequestContext {
-    Object OBJECT = new Object();
-
-    Request<ClinitEntityProxy> request();
-  }
-
-  interface ClinitRequestFactory extends RequestFactory {
-    Object OBJECT = new Object();
-
-    ClinitRequestContext context();
-  }
-
-  static class Domain {
-    static int fooStatic(@SuppressWarnings("unused") int a) {
-      return 0;
-    }
-
-    int foo(@SuppressWarnings("unused") int a) {
-      return 0;
-    }
-
-    java.sql.Date getSqlDate() {
-      return null;
-    }
-  }
-
-  @ProxyFor(Domain.class)
-  interface DomainProxy extends EntityProxy {
-  }
-
-  interface DomainProxyMissingAnnotation extends EntityProxy {
-  }
-
-  static class DomainWithOverloads {
-    void foo() {
-    }
-
-    void foo(@SuppressWarnings("unused") int a) {
-    }
-
-    String getId() {
-      return null;
-    }
-
-    int getVersion() {
-      return 0;
-    }
-  }
-
-  @ProxyFor(DomainWithOverloads.class)
-  interface DomainWithOverloadsProxy extends EntityProxy {
-    void foo();
-  }
-
-  @ProxyFor(Domain.class)
-  interface DomainWithSqlDateProxy extends EntityProxy {
-    java.sql.Date getSqlDate();
-  }
-
-  /**
-   * Tests that the validator reports non-static finder methods.
-   */
-  static class EntityWithInstanceFind {
-    public String getId() {
-      return null;
-    }
-
-    public int getVersion() {
-      return 0;
-    }
-
-    /**
-     * This method should be static.
-     */
-    EntityWithInstanceFind findEntityWithInstanceFind(@SuppressWarnings("unused") String key) {
-      return null;
-    }
-  }
-
-  @ProxyFor(EntityWithInstanceFind.class)
-  interface EntityWithInstanceFindProxy extends EntityProxy {
-  }
-
-  class Foo {
-  }
-
-  @ProxyFor(HasListDomain.class)
-  interface HasList extends EntityProxy {
-    List<ReachableOnlyThroughReturnedList> getList();
-
-    void setList(List<ReachableOnlyThroughParamaterList> list);
-  }
-
-  static class HasListDomain extends Domain {
-    public String getId() {
-      return null;
-    }
-
-    public int getVersion() {
-      return 0;
-    }
-
-    List<Domain> getList() {
-      return null;
-    }
-
-    void setList(@SuppressWarnings("unused") List<Domain> value) {
-    }
-  }
-
-  /**
-   * An entity type without the usual boilerplate.
-   */
-  class LocatorEntity {
-  }
-
-  class LocatorEntityLocator extends Locator<LocatorEntity, String> {
-    @Override
-    public LocatorEntity create(Class<? extends LocatorEntity> clazz) {
-      return null;
-    }
-
-    @Override
-    public LocatorEntity find(Class<? extends LocatorEntity> clazz, String id) {
-      return null;
-    }
-
-    @Override
-    public Class<LocatorEntity> getDomainType() {
-      return null;
-    }
-
-    @Override
-    public String getId(LocatorEntity domainObject) {
-      return null;
-    }
-
-    @Override
-    public Class<String> getIdType() {
-      return null;
-    }
-
-    @Override
-    public Object getVersion(LocatorEntity domainObject) {
-      return null;
-    }
-  }
-
-  @ProxyFor(value = LocatorEntity.class, locator = LocatorEntityLocator.class)
-  interface LocatorEntityProxy extends EntityProxy {
-  }
-
-  @ProxyForName(value = "com.google.web.bindery.requestfactory.server.RequestFactoryInterfaceValidatorTest.LocatorEntity", locator = "badLocator")
-  interface LocatorEntityProxyWithBadLocator extends EntityProxy {
-  }
-
-  @ProxyForName(value = "badDomainType", locator = "com.google.web.bindery.requestfactory.server.RequestFactoryInterfaceValidatorTest.LocatorEntityProxyWithBadServiceName")
-  interface LocatorEntityProxyWithBadServiceName extends EntityProxy {
-  }
-
-  @ProxyFor(value = Value.class)
-  interface MyValueProxy extends ValueProxy {
-  }
-
-  @ProxyFor(Domain.class)
-  interface ReachableOnlyThroughParamaterList extends EntityProxy {
-  }
-
-  @ProxyFor(Domain.class)
-  interface ReachableOnlyThroughReturnedList extends EntityProxy {
-  }
-
-  interface RequestContextMissingAnnotation extends RequestContext {
-  }
-
-  @Service(Domain.class)
-  interface ServiceRequestMismatchedArity extends RequestContext {
-    InstanceRequest<DomainProxy, Integer> foo(int a, int b);
-
-    Request<Integer> fooStatic(int a, int b);
-  }
-
-  @Service(Domain.class)
-  interface ServiceRequestMismatchedParam extends RequestContext {
-    Request<Integer> foo(long a);
-  }
-  @Service(Domain.class)
-  interface ServiceRequestMismatchedReturn extends RequestContext {
-    Request<Long> foo(int a);
-  }
-
-  @Service(Domain.class)
-  interface ServiceRequestMismatchedStatic extends RequestContext {
-    Request<Integer> foo(int a);
-
-    InstanceRequest<DomainProxy, Integer> fooStatic(int a);
-  }
-
-  @Service(Domain.class)
-  interface ServiceRequestMissingMethod extends RequestContext {
-    Request<Integer> doesNotExist(int a);
-  }
-
-  @Service(Domain.class)
-  interface SkipValidationChecksReferredProxies extends ValueProxy {
-    @SkipInterfaceValidation
-    // still validates other proxies
-    DomainProxyMissingAnnotation getDomainProxyMissingAnnotation();
-  }
-
-  @Service(Domain.class)
-  interface SkipValidationContext extends RequestContext {
-    @SkipInterfaceValidation
-    Request<Integer> doesNotExist(int a);
-
-    @SkipInterfaceValidation
-    Request<Long> foo(int a);
-  }
-
-  @Service(Domain.class)
-  interface SkipValidationProxy extends ValueProxy {
-    @SkipInterfaceValidation
-    boolean doesNotExist();
-  }
-
-  @ProxyFor(Domain.class)
-  @ProxyForName("Domain")
-  @Service(Domain.class)
-  @ServiceName("Domain")
-  interface TooManyAnnotations extends RequestContext {
-  }
-
-  static class UnexpectedIdAndVersionDomain {
-    Random getId() {
-      return null;
-    }
-
-    Random getVersion() {
-      return null;
-    }
-  }
-
-  @ProxyFor(UnexpectedIdAndVersionDomain.class)
-  interface UnexpectedIdAndVersionProxy extends EntityProxy {
-  }
-
-  static class Value {
-  }
-
-  static class VisibleErrorContext extends RequestFactoryInterfaceValidator.ErrorContext {
-    final List<String> logs;
-
-    public VisibleErrorContext(Logger logger) {
-      super(logger);
-      logs = new ArrayList<String>();
-    }
-
-    public VisibleErrorContext(VisibleErrorContext that) {
-      super(that);
-      this.logs = that.logs;
-    }
-
-    @Override
-    public void poison(String msg, Object... args) {
-      logs.add(String.format(msg, args));
-      super.poison(msg, args);
-    }
-
-    @Override
-    public void poison(String msg, Throwable t) {
-      logs.add(msg);
-      super.poison(msg, t);
-    }
-
-    @Override
-    protected VisibleErrorContext fork() {
-      return new VisibleErrorContext(this);
-    }
-  }
-
-  RequestFactoryInterfaceValidator v;;
-
-  private static final boolean DUMP_PAYLOAD = Boolean.getBoolean("gwt.rpc.dumpPayload");;
-
-  private VisibleErrorContext errors;
-
-  /**
-   * Ensure that calling {@link RequestFactoryInterfaceValidator#antidote()}
-   * doesn't cause information to be lost.
-   */
-  public void testAntidote() {
-    v.validateRequestContext(RequestContextMissingAnnotation.class.getName());
-    assertTrue(v.isPoisoned());
-    v.antidote();
-    assertFalse(v.isPoisoned());
-    v.validateRequestContext(RequestContextMissingAnnotation.class.getName());
-    assertTrue(v.isPoisoned());
-  }
-
-  public void testBadLocatorName() {
-    v.validateEntityProxy(LocatorEntityProxyWithBadLocator.class.getName());
-    assertTrue(v.isPoisoned());
-    assertTrue(errors.logs.contains("Cannot find locator named badLocator"));
-  }
-
-  public void testBadServiceName() {
-    v.validateEntityProxy(LocatorEntityProxyWithBadServiceName.class.getName());
-    assertTrue(v.isPoisoned());
-    assertTrue(errors.logs.contains("Cannot find domain type named badDomainType"));
-  }
-
-  /**
-   * Test that subclasses of {@code java.util.Date} are not transportable.
-   */
-  public void testDateSubclass() {
-    v.validateEntityProxy(DomainWithSqlDateProxy.class.getName());
-    assertTrue(v.isPoisoned());
-  }
-
-  public void testFindMustBeStatic() {
-    v.validateEntityProxy(EntityWithInstanceFindProxy.class.getName());
-    assertTrue(v.isPoisoned());
-    assertTrue(errors.logs.contains("The findEntityWithInstanceFind method must be static"));
-  }
-
-  /**
-   * Test the {@link FindRequest} context used to implement find().
-   */
-  public void testFindRequestContext() {
-    v.validateRequestContext(FindRequest.class.getName());
-  }
-
-  /**
-   * Make sure that proxy types referenced through type parameters of method
-   * return types and paramaters types are examined.
-   */
-  public void testFollowingTypeParameters() {
-    v.validateEntityProxy(HasList.class.getName());
-    Deobfuscator d = v.getDeobfuscator();
-    assertNotNull(d.getClientProxies(HasListDomain.class.getName()));
-    assertTrue(d.getClientProxies(Domain.class.getName()).contains(
-        ReachableOnlyThroughParamaterList.class.getName()));
-    assertTrue(d.getClientProxies(Domain.class.getName()).contains(
-        ReachableOnlyThroughReturnedList.class.getName()));
-  }
-
-  /**
-   * Ensure that the &lt;clinit> methods don't interfere with validation.
-   */
-  public void testIntecfacesWithClinits() {
-    v.validateRequestFactory(ClinitRequestFactory.class.getName());
-    assertFalse(v.isPoisoned());
-  }
-
-  public void testLocatorProxy() {
-    v.validateEntityProxy(LocatorEntityProxy.class.getName());
-    assertFalse(v.isPoisoned());
-  }
-
-  public void testMismatchedArity() {
-    v.validateRequestContext(ServiceRequestMismatchedArity.class.getName());
-    assertTrue(v.isPoisoned());
-  }
-
-  public void testMismatchedParamType() {
-    v.validateRequestContext(ServiceRequestMismatchedParam.class.getName());
-    assertTrue(v.isPoisoned());
-  }
-
-  public void testMismatchedReturnType() {
-    v.validateRequestContext(ServiceRequestMismatchedReturn.class.getName());
-    assertTrue(v.isPoisoned());
-  }
-
-  public void testMismatchedStatic() {
-    v.validateRequestContext(ServiceRequestMismatchedStatic.class.getName());
-    assertTrue(v.isPoisoned());
-  }
-
-  public void testMissingDomainAnnotations() {
-    v.validateEntityProxy(DomainProxyMissingAnnotation.class.getName());
-    assertTrue(v.isPoisoned());
-  }
-
-  public void testMissingIdAndVersion() {
-    v.validateEntityProxy(DomainProxy.class.getName());
-    assertTrue(v.isPoisoned());
-  }
-
-  public void testMissingMethod() {
-    v.validateRequestContext(ServiceRequestMissingMethod.class.getName());
-    assertTrue(v.isPoisoned());
-  }
-
-  public void testMissingServiceAnnotations() {
-    v.validateRequestContext(RequestContextMissingAnnotation.class.getName());
-    assertTrue(v.isPoisoned());
-  }
-
-  public void testOverloadedMethod() {
-    v.validateEntityProxy(DomainWithOverloadsProxy.class.getName());
-    assertTrue(v.isPoisoned());
-  }
-
-  public void testSkipValidationChecksReferredProxies() {
-    v.validateValueProxy(SkipValidationChecksReferredProxies.class.getName());
-    assertTrue(v.isPoisoned());
-  }
-
-  public void testSkipValidationContext() {
-    v.validateRequestContext(SkipValidationContext.class.getName());
-    assertFalse(v.isPoisoned());
-  }
-
-  public void testSkipValidationProxy() {
-    v.validateValueProxy(SkipValidationProxy.class.getName());
-    assertFalse(v.isPoisoned());
-  }
-
-  /**
-   * Perform a full test of the RequestFactory used for most tests.
-   */
-  public void testTestCodeFactories() {
-    v.validateRequestFactory(SimpleRequestFactory.class.getName());
-    assertFalse(v.isPoisoned());
-  }
-
-  public void testTooManyAnnotations() {
-    v.validateRequestContext(TooManyAnnotations.class.getName());
-    assertTrue(v.isPoisoned());
-  }
-
-  public void testUnexpectedIdAndVersion() {
-    v.validateEntityProxy(UnexpectedIdAndVersionProxy.class.getName());
-    assertTrue(v.isPoisoned());
-  }
-
-  public void testValueType() {
-    v.validateValueProxy(MyValueProxy.class.getName());
-    assertFalse(v.isPoisoned());
-  }
-
-  @Override
-  protected void setUp() throws Exception {
-    Logger logger = Logger.getLogger("");
-    logger.setLevel(DUMP_PAYLOAD ? Level.ALL : Level.OFF);
-    errors = new VisibleErrorContext(logger);
-    v =
-        new RequestFactoryInterfaceValidator(errors, new ClassLoaderLoader(Thread.currentThread()
-            .getContextClassLoader()));
-  }
-}
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 ad762f4..04a3685 100644
--- a/user/test/com/google/web/bindery/requestfactory/server/RequestFactoryJreTest.java
+++ b/user/test/com/google/web/bindery/requestfactory/server/RequestFactoryJreTest.java
@@ -19,17 +19,11 @@
 import com.google.web.bindery.event.shared.SimpleEventBus;
 import com.google.web.bindery.requestfactory.gwt.client.RequestFactoryTest;
 import com.google.web.bindery.requestfactory.server.testing.InProcessRequestTransport;
-import com.google.web.bindery.requestfactory.shared.BaseProxy;
 import com.google.web.bindery.requestfactory.shared.RequestFactory;
-import com.google.web.bindery.requestfactory.shared.SimpleBarProxy;
-import com.google.web.bindery.requestfactory.shared.SimpleFooProxy;
 import com.google.web.bindery.requestfactory.shared.SimpleRequestFactory;
 import com.google.web.bindery.requestfactory.vm.RequestFactorySource;
-import com.google.web.bindery.requestfactory.vm.impl.OperationKey;
-import com.google.web.bindery.requestfactory.vm.impl.TypeTokenResolver;
 import com.google.web.bindery.requestfactory.vm.testing.UrlRequestTransport;
 
-import java.io.IOException;
 import java.net.MalformedURLException;
 import java.net.URL;
 
@@ -37,8 +31,7 @@
  * Runs the RequestFactory tests in-process.
  */
 public class RequestFactoryJreTest extends RequestFactoryTest {
-  private static final String TEST_SERVER_ADDRESS = System
-      .getProperty("RequestFactory.testUrl");
+  private static final String TEST_SERVER_ADDRESS = System.getProperty("RequestFactory.testUrl");
 
   public static <T extends RequestFactory> T createInProcess(Class<T> clazz) {
     EventBus eventBus = new SimpleEventBus();
@@ -63,20 +56,8 @@
     return null;
   }
 
-  public void testTypeTokenResolver() throws IOException {
-    TypeTokenResolver resolver = TypeTokenResolver.loadFromClasspath();
-    testResolver(resolver, SimpleBarProxy.class);
-    testResolver(resolver, SimpleFooProxy.class);
-  }
-
   @Override
   protected SimpleRequestFactory createFactory() {
     return createInProcess(SimpleRequestFactory.class);
   }
-
-  private void testResolver(TypeTokenResolver resolver, Class<? extends BaseProxy> type) {
-    String token = OperationKey.hash(type.getName());
-    String typeName = resolver.getTypeFromToken(token);
-    assertEquals(type.getName(), typeName);
-  }
 }
diff --git a/user/test/com/google/web/bindery/requestfactory/server/RequestFactoryPolymorphicJreTest.java b/user/test/com/google/web/bindery/requestfactory/server/RequestFactoryPolymorphicJreTest.java
index bcf5a23..e10c68a 100644
--- a/user/test/com/google/web/bindery/requestfactory/server/RequestFactoryPolymorphicJreTest.java
+++ b/user/test/com/google/web/bindery/requestfactory/server/RequestFactoryPolymorphicJreTest.java
@@ -16,11 +16,10 @@
 package com.google.web.bindery.requestfactory.server;
 
 import com.google.web.bindery.requestfactory.gwt.client.RequestFactoryPolymorphicTest;
-import com.google.web.bindery.requestfactory.server.RequestFactoryInterfaceValidator.ClassLoaderLoader;
 import com.google.web.bindery.requestfactory.shared.BaseProxy;
+import com.google.web.bindery.requestfactory.vm.impl.Deobfuscator;
 
 import java.util.List;
-import java.util.logging.Logger;
 
 /**
  * A JRE version of {@link RequestFactoryPolymorphicTest} that includes
@@ -120,11 +119,7 @@
   @Override
   protected void gwtSetUp() throws Exception {
     super.gwtSetUp();
-    Logger logger = Logger.getLogger(getClass().getName());
-    ClassLoaderLoader loader = new ClassLoaderLoader(getClass().getClassLoader());
-    RequestFactoryInterfaceValidator v = new RequestFactoryInterfaceValidator(logger, loader);
-    v.validateRequestFactory(Factory.class.getName());
-    deobfuscator = v.getDeobfuscator();
+    deobfuscator = Deobfuscator.Builder.load(Factory.class, getClass().getClassLoader()).build();
   }
 
   private void check(Class<?> domainType, Class<? extends BaseProxy> declaredReturnType,
diff --git a/user/test/com/google/web/bindery/requestfactory/server/SimpleBar.java b/user/test/com/google/web/bindery/requestfactory/server/SimpleBar.java
index 5b8e40c..e313263 100644
--- a/user/test/com/google/web/bindery/requestfactory/server/SimpleBar.java
+++ b/user/test/com/google/web/bindery/requestfactory/server/SimpleBar.java
@@ -15,10 +15,9 @@
  */
 package com.google.web.bindery.requestfactory.server;
 
-import com.google.gwt.dev.util.collect.HashSet;
-
 import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
diff --git a/user/test/com/google/web/bindery/requestfactory/shared/TestFooPolymorphicRequest.java b/user/test/com/google/web/bindery/requestfactory/shared/TestFooPolymorphicRequest.java
index 5662c40..350b8c9 100644
--- a/user/test/com/google/web/bindery/requestfactory/shared/TestFooPolymorphicRequest.java
+++ b/user/test/com/google/web/bindery/requestfactory/shared/TestFooPolymorphicRequest.java
@@ -17,7 +17,8 @@
 
 /**
  * Just to test the
- * {@link com.google.web.bindery.requestfactory.gwt.rebind.RequestFactoryGenerator} code.
+ * {@link com.google.web.bindery.requestfactory.gwt.rebind.RequestFactoryGenerator}
+ * code.
  */
 @Service(com.google.web.bindery.requestfactory.server.SimpleFoo.class)
 public interface TestFooPolymorphicRequest extends RequestContext {
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 ba26e92..f0cb5a2 100644
--- a/user/test/com/google/web/bindery/requestfactory/vm/RequestFactoryJreSuite.java
+++ b/user/test/com/google/web/bindery/requestfactory/vm/RequestFactoryJreSuite.java
@@ -22,7 +22,6 @@
 import com.google.web.bindery.requestfactory.server.LocatorJreTest;
 import com.google.web.bindery.requestfactory.server.RequestFactoryChainedContextJreTest;
 import com.google.web.bindery.requestfactory.server.RequestFactoryExceptionPropagationJreTest;
-import com.google.web.bindery.requestfactory.server.RequestFactoryInterfaceValidatorTest;
 import com.google.web.bindery.requestfactory.server.RequestFactoryJreTest;
 import com.google.web.bindery.requestfactory.server.RequestFactoryPolymorphicJreTest;
 import com.google.web.bindery.requestfactory.server.RequestFactoryUnicodeEscapingJreTest;
@@ -49,7 +48,6 @@
     suite.addTestSuite(LocatorJreTest.class);
     suite.addTestSuite(RequestFactoryChainedContextJreTest.class);
     suite.addTestSuite(RequestFactoryExceptionPropagationJreTest.class);
-    suite.addTestSuite(RequestFactoryInterfaceValidatorTest.class);
     suite.addTestSuite(RequestFactoryJreTest.class);
     suite.addTestSuite(RequestFactoryPolymorphicJreTest.class);
     suite.addTestSuite(RequestFactoryUnicodeEscapingJreTest.class);