Add RequestFactory validator implemented as an annotation processor.
This will eventually replace the RequestFactoryInterfaceValidator and it's
classfile-based approach.
Patch by: bobv
Review by: t.broyer, pquitslund

Review at http://gwt-code-reviews.appspot.com/1467804


git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@10417 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/user/build.xml b/user/build.xml
index 5b39170..c37fbeb 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/web/bindery/requestfactory/apt/ClientToDomainMapper.java b/user/src/com/google/web/bindery/requestfactory/apt/ClientToDomainMapper.java
new file mode 100644
index 0000000..f4cf816
--- /dev/null
+++ b/user/src/com/google/web/bindery/requestfactory/apt/ClientToDomainMapper.java
@@ -0,0 +1,157 @@
+/*
+ * 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.autobean.shared.ValueCodex;
+
+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;
+import javax.lang.model.util.SimpleTypeVisitor6;
+
+/**
+ * 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 SimpleTypeVisitor6<TypeMirror, State> {
+  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
+      TypeElement domainType = 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 (Class<?> clazz : ValueCodex.getAllValueTypes()) {
+      if (clazz.isPrimitive()) {
+        continue;
+      }
+
+      if (state.types.isAssignable(x, state.findType(clazz))) {
+        // 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
+    if (x.getExtendsBound() != null) {
+      return x.getExtendsBound().accept(this, state);
+    }
+    return defaultAction(x, 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/DomainChecker.java b/user/src/com/google/web/bindery/requestfactory/apt/DomainChecker.java
new file mode 100644
index 0000000..7c53177
--- /dev/null
+++ b/user/src/com/google/web/bindery/requestfactory/apt/DomainChecker.java
@@ -0,0 +1,408 @@
+/*
+ * 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 CharSequence name;
+    private final TypeMirror returnType;
+    private final List<TypeMirror> params;
+
+    public MethodFinder(CharSequence name, TypeMirror returnType, List<TypeMirror> params,
+        State state) {
+      this.name = name;
+      this.returnType = TypeSimplifier.simplify(returnType, true, 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(), true, state);
+          // The isSameType handles the NONE case.
+          returnTypeMatches =
+              state.types.isSameType(domainReturn, returnType)
+                  || state.types.isAssignable(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.isSameType(requestedType, paramType)
+                && !state.types.isAssignable(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);
+    }
+  }
+
+  private 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();
+    }
+  }
+
+  /**
+   * 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 checkedType;
+  private boolean currentTypeIsProxy;
+  private boolean requireInstanceDomainMethods;
+  private boolean requireStaticDomainMethods;
+  private TypeElement domainType;
+
+  @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(checkedType, 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, state).scan(
+              domainType, state);
+      if (domainMethod == null) {
+        // Try a builder style
+        domainMethod =
+            new MethodFinder(name, domainType.asType(), lookFor, state).scan(domainType, state);
+      }
+    } else {
+      // The usual case for getters and all service methods
+      domainMethod = new MethodFinder(name, returnType, lookFor, state).scan(domainType, 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, "Could not find domain method similar to %s", 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.isAssignable(clientMethod.getReturnType(), state.instanceRequestType);
+
+    if ((isInstanceRequest || requireInstanceDomainMethods)
+        && domainMethod.getModifiers().contains(Modifier.STATIC)) {
+      state.poison(checkedType, "Found static domain method %s when instance method required",
+          domainMethod.getSimpleName());
+    }
+    if (!isInstanceRequest && requireStaticDomainMethods
+        && !domainMethod.getModifiers().contains(Modifier.STATIC)) {
+      state.poison(checkedType, "Found instance domain method %s when static method required",
+          domainMethod.getSimpleName());
+    }
+    if (state.verbose) {
+      state.warn(clientMethodElement, "Found domain method %s", domainMethod.toString());
+    }
+
+    return null;
+  }
+
+  @Override
+  public Void visitType(TypeElement clientTypeElement, State state) {
+    TypeMirror clientType = clientTypeElement.asType();
+    checkedType = clientTypeElement;
+    boolean isEntityProxy = state.types.isAssignable(clientType, state.entityProxyType);
+    currentTypeIsProxy =
+        isEntityProxy || state.types.isAssignable(clientType, state.valueProxyType);
+    domainType = state.getClientToDomainMap().get(clientTypeElement);
+    if (domainType == 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(domainType)) {
+          state.warn(clientTypeElement, "The domain type %s has no default constructor."
+              + " Calling %s.create(%s.class) will cause a server error.", domainType,
+              state.requestContextType.asElement().getSimpleName(), clientTypeElement
+                  .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(), state).scan(
+            domainType, state);
+    if (getId == null) {
+      state.poison(checkedType, "Domain type %s does not have a getId() method", domainType
+          .asType());
+    } else {
+      if (getId.getModifiers().contains(Modifier.STATIC)) {
+
+        state.poison(checkedType, "The domain type's getId() method must not be static");
+      }
+
+      // Can only check findFoo() if we have a getId
+      ExecutableElement find =
+          new MethodFinder("find" + domainType.getSimpleName(), domainType.asType(), Collections
+              .singletonList(getId.getReturnType()), state).scan(domainType, state);
+      if (find == null) {
+        state.warn(checkedType, "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
+            .asType(), domainType.getSimpleName(), domainType.getSimpleName(), getId
+            .getReturnType(), checkedType.getSimpleName());
+      } else if (!find.getModifiers().contains(Modifier.STATIC)) {
+        state.poison(checkedType, "The domain object's find%s() method is not static", domainType
+            .getSimpleName());
+      }
+    }
+
+    ExecutableElement getVersion =
+        new MethodFinder("getVersion", null, Collections.<TypeMirror> emptyList(), state).scan(
+            domainType, state);
+    if (getVersion == null) {
+      state.poison(checkedType, "Domain type %s does not have a getVersion() method", domainType
+          .asType());
+    } else if (getVersion.getModifiers().contains(Modifier.STATIC)) {
+      state.poison(checkedType, "The domain type's getVersion() method must not be static");
+    }
+  }
+
+  /**
+   * 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, "Cannot validate this method because the domain mapping for the"
+          + " return type (%s) could not be resolved to a domain type", e.getClientType());
+    }
+    for (TypeMirror param : clientMethod.getParameterTypes()) {
+      try {
+        parameterAccumulator.add(param.accept(new ClientToDomainMapper(), state));
+      } catch (UnmappedTypeException e) {
+        parameterAccumulator.add(null);
+        error = true;
+        state.warn(warnTo, "Cannot validate this method because the domain mapping for a"
+            + " parameter of type (%s) could not be resolved to a domain type", e.getClientType());
+      }
+    }
+    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/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/ProxyScanner.java b/user/src/com/google/web/bindery/requestfactory/apt/ProxyScanner.java
new file mode 100644
index 0000000..f3c85c7
--- /dev/null
+++ b/user/src/com/google/web/bindery/requestfactory/apt/ProxyScanner.java
@@ -0,0 +1,102 @@
+/*
+ * 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, "The return type %s cannot be used here", returnType.toString());
+      }
+    } else if (!isSetter(x, state)) {
+      state.poison(x, "Only getters and setters allowed");
+    }
+    // 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, "Cannot fully validate proxy since type %s is not available", 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, "The type %s cannot be used here", x.asType().toString());
+    }
+    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/RequestContextScanner.java b/user/src/com/google/web/bindery/requestfactory/apt/RequestContextScanner.java
new file mode 100644
index 0000000..3fe796a
--- /dev/null
+++ b/user/src/com/google/web/bindery/requestfactory/apt/RequestContextScanner.java
@@ -0,0 +1,136 @@
+/*
+ * 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, "A raw return type may not be used here");
+      } else {
+        TypeMirror requestReturn = asRequest.getTypeArguments().get(0);
+        if (!state.isTransportableType(requestReturn)) {
+          state.poison(x, "The type %s cannot be used as a return value", requestReturn.toString());
+        }
+      }
+    } 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, "A raw return type may not be used here");
+      } else {
+        TypeMirror instanceType = asInstanceRequest.getTypeArguments().get(0);
+        if (!state.isTransportableType(instanceType)) {
+          state.poison(x, "The type %s cannot be used as an invocation target", instanceType
+              .toString());
+        }
+        TypeMirror requestReturn = asInstanceRequest.getTypeArguments().get(1);
+        if (!state.isTransportableType(requestReturn)) {
+          state.poison(x, "The type %s cannot be used as a return value", requestReturn.toString());
+        }
+      }
+    } else if (isSetter(x, state)) {
+      // Parameter checked in visitVariable
+    } else {
+      state.poison(x, "The return type must be a %s or %s", 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 && serviceName == null && jsonRpcService == null) {
+      state.poison(x, "A %s must be annotated with %s, %s, or %s", state.types.asElement(
+          state.requestContextType).getSimpleName(), Service.class.getSimpleName(),
+          ServiceName.class.getSimpleName(), JsonRpcService.class.getSimpleName());
+    }
+    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, "Cannot fully validate context since type %s is not available", serviceName
+            .value());
+      } else {
+        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, "The type %s cannot be used here", bound);
+      }
+    }
+    return super.visitTypeParameter(x, state);
+  }
+
+  @Override
+  public Void visitVariable(VariableElement x, State state) {
+    if (!state.isTransportableType(x.asType())) {
+      state.poison(x, "The type %s cannot be used here", 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..e9cea1c
--- /dev/null
+++ b/user/src/com/google/web/bindery/requestfactory/apt/RequestFactoryScanner.java
@@ -0,0 +1,64 @@
+/*
+ * 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, "This method must have no parameters");
+    }
+    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, "The return type %s must be an interface", returnType.toString());
+      } else {
+        state.maybeScanContext((TypeElement) returnTypeElement);
+      }
+    } else {
+      state.poison(x, "This method must return a %s", state.requestContextType);
+    }
+    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
index 2afd346..66e60d4 100644
--- a/user/src/com/google/web/bindery/requestfactory/apt/RfApt.java
+++ b/user/src/com/google/web/bindery/requestfactory/apt/RfApt.java
@@ -113,7 +113,39 @@
     // Extract data
     new Finder().scan(ElementFilter.typesIn(roundEnv.getRootElements()), null);
 
-    // On the last round, write out accumulated data
+    /*
+     * 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 : builder.peek().getAllTypeTokens().entrySet()) {
+        if (elements.getTypeElement(entry.getValue()) != null) {
+          pw.println("addTypeToken(\"" + entry.getKey() + "\", \"" + entry.getValue() + "\");");
+        }
+      }
+      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());
+    }
+
+    // Write out accumulated data
     if (roundEnv.processingOver()) {
       TypeTokenResolver d = builder.build();
       builder = null;
@@ -123,41 +155,8 @@
       } 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!");
     }
+    log("Finished!");
     return false;
   }
 
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..332c6d9
--- /dev/null
+++ b/user/src/com/google/web/bindery/requestfactory/apt/RfValidator.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 java.util.Set;
+
+import javax.annotation.processing.AbstractProcessor;
+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({"suppressErrors", "suppressWarnings", "verbose"})
+public class RfValidator extends AbstractProcessor {
+
+  private State state;
+
+  @Override
+  public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
+    if (state == null) {
+      state = new State(processingEnv);
+    }
+
+    try {
+      // Bootstrap the State's work queue
+      new Finder().scan(ElementFilter.typesIn(roundEnv.getRootElements()), state);
+      // Execute the work items
+      state.executeJobs();
+      if (roundEnv.processingOver()) {
+        // Verify mappings
+        new DomainChecker().scan(state.getClientToDomainMap().keySet(), state);
+        state = null;
+      }
+    } catch (HaltException ignored) {
+      // Already logged. Let any unhandled RuntimeExceptions fall out.
+    }
+    return false;
+  }
+}
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..49268da
--- /dev/null
+++ b/user/src/com/google/web/bindery/requestfactory/apt/ScannerBase.java
@@ -0,0 +1,134 @@
+/*
+ * 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.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, "Redundant annotation: %s", a.annotationType().getSimpleName());
+      }
+    }
+  }
+
+  @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..52c6ef3
--- /dev/null
+++ b/user/src/com/google/web/bindery/requestfactory/apt/State.java
@@ -0,0 +1,365 @@
+/*
+ * 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.ProxyFor;
+import com.google.web.bindery.requestfactory.shared.ProxyForName;
+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.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.annotation.processing.Messager;
+import javax.annotation.processing.ProcessingEnvironment;
+import javax.lang.model.element.AnnotationMirror;
+import javax.lang.model.element.AnnotationValue;
+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.TypeKind;
+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 {
+  private static class Job {
+    public final TypeElement element;
+    public final ScannerBase<?> scanner;
+
+    public Job(TypeElement element, ScannerBase<?> scanner) {
+      this.element = element;
+      this.scanner = scanner;
+    }
+  }
+
+  /**
+   * 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 Elements elements;
+  final DeclaredType entityProxyIdType;
+  final DeclaredType entityProxyType;
+  final DeclaredType extraTypesAnnotation;
+  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 boolean suppressErrors;
+  final boolean suppressWarnings;
+  final Types types;
+  final DeclaredType valueProxyType;
+  final boolean verbose;
+  private final Map<TypeElement, TypeElement> clientToDomainMain;
+  private final List<Job> jobs = new LinkedList<Job>();
+  private final Messager messager;
+  private boolean poisoned;
+  /**
+   * Prevents duplicate messages from being emitted.
+   */
+  private final Map<Element, Set<String>> previousMessages = new HashMap<Element, Set<String>>();
+  private final Set<TypeElement> proxiesRequiringMapping = new LinkedHashSet<TypeElement>();
+
+  public State(ProcessingEnvironment processingEnv) {
+    clientToDomainMain = new HashMap<TypeElement, TypeElement>();
+    elements = processingEnv.getElementUtils();
+    messager = processingEnv.getMessager();
+    types = processingEnv.getTypeUtils();
+    verbose = Boolean.parseBoolean(processingEnv.getOptions().get("verbose"));
+    suppressErrors = Boolean.parseBoolean(processingEnv.getOptions().get("suppressErrors"));
+    suppressWarnings = Boolean.parseBoolean(processingEnv.getOptions().get("suppressWarnings"));
+
+    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 type to a domain type.
+   */
+  public void addMapping(TypeElement clientType, TypeElement domainType) {
+    clientToDomainMain.put(clientType, domainType);
+  }
+
+  /**
+   * Check an element, and, for types, its supertype hierarchy, for an
+   * {@code ExtraTypes} annotation.
+   */
+  public void checkExtraTypes(Element x) {
+    (new ScannerBase<Void>() {
+      @Override
+      public Void visitExecutable(ExecutableElement x, State state) {
+        // Check method declaration
+        checkForAnnotation(x);
+        return null;
+      }
+
+      @Override
+      public Void visitType(TypeElement x, State state) {
+        // Check type's declaration
+        checkForAnnotation(x);
+        // Look at superclass, if it exists
+        if (!x.getSuperclass().getKind().equals(TypeKind.NONE)) {
+          scan(state.types.asElement(x.getSuperclass()), state);
+        }
+        // Check super-interfaces
+        for (TypeMirror intf : x.getInterfaces()) {
+          scan(state.types.asElement(intf), state);
+        }
+        return null;
+      }
+
+      private void checkForAnnotation(Element x) {
+        // Bug similar to Eclipse 261969 makes ExtraTypes.value() unreliable.
+        for (AnnotationMirror mirror : x.getAnnotationMirrors()) {
+          if (!types.isSameType(mirror.getAnnotationType(), 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();
+            maybeScanProxy((TypeElement) types.asElement(type));
+          }
+        }
+      }
+    }).scan(x, this);
+  }
+
+  public void executeJobs() {
+    while (!jobs.isEmpty()) {
+      Job job = jobs.remove(0);
+      if (verbose) {
+        messager.printMessage(Kind.NOTE, String.format("Scanning %s", elements
+            .getBinaryName(job.element)));
+      }
+      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());
+      }
+    }
+    for (TypeElement proxyElement : proxiesRequiringMapping) {
+      if (!getClientToDomainMap().containsKey(proxyElement)) {
+        poison(proxyElement, "A proxy must be annotated with %s, %s, or %s", ProxyFor.class
+            .getSimpleName(), ProxyForName.class.getSimpleName(), JsonRpcProxy.class
+            .getSimpleName());
+      }
+    }
+  }
+
+  /**
+   * Utility method to look up raw types from class literals.
+   */
+  public DeclaredType findType(Class<?> clazz) {
+    return types.getDeclaredType(elements.getTypeElement(clazz.getCanonicalName()));
+  }
+
+  /**
+   * 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}.
+   */
+  public DeclaredType findType(String simpleName) {
+    return types.getDeclaredType(elements
+        .getTypeElement("com.google.web.bindery.requestfactory.shared." + simpleName));
+  }
+
+  /**
+   * Returns a map of client proxy elements to their domain counterparts.
+   */
+  public Map<TypeElement, TypeElement> getClientToDomainMap() {
+    return Collections.unmodifiableMap(clientToDomainMain);
+  }
+
+  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()));
+  }
+
+  public void maybeScanFactory(TypeElement factoryType) {
+    if (fastFail(factoryType) || types.isSameType(requestFactoryType, factoryType.asType())) {
+      return;
+    }
+    jobs.add(new Job(factoryType, new RequestFactoryScanner()));
+  }
+
+  public void maybeScanProxy(TypeElement proxyType) {
+    if (fastFail(proxyType)) {
+      return;
+    }
+    jobs.add(new Job(proxyType, new ProxyScanner()));
+  }
+
+  /**
+   * 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, Object... args) {
+    if (suppressErrors) {
+      return;
+    }
+
+    String formatted = String.format(message, args);
+    if (squelchMessage(elt, formatted)) {
+      return;
+    }
+
+    Element check = elt;
+    while (check != null) {
+      if (check.getAnnotation(SkipInterfaceValidation.class) != null) {
+        return;
+      }
+      check = check.getEnclosingElement();
+    }
+
+    if (elt == null) {
+      messager.printMessage(Kind.ERROR, formatted);
+    } else {
+      messager.printMessage(Kind.ERROR, formatted, elt);
+    }
+  }
+
+  public void requireMapping(TypeElement proxyElement) {
+    proxiesRequiringMapping.add(proxyElement);
+  }
+
+  /**
+   * 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, Object... args) {
+    if (suppressWarnings) {
+      return;
+    }
+
+    String formatted = String.format(message, args);
+    if (squelchMessage(elt, formatted)) {
+      return;
+    }
+
+    SuppressWarnings suppress;
+    Element check = elt;
+    while (check != null) {
+      suppress = check.getAnnotation(SuppressWarnings.class);
+      if (suppress != null) {
+        if (Arrays.asList(suppress.value()).contains("requestfactory")) {
+          return;
+        }
+      }
+      check = check.getEnclosingElement();
+    }
+
+    messager.printMessage(Kind.WARNING, formatted
+        + "\n\nAdd @SuppressWarnings(\"requestfactory\") to dismiss.", elt);
+  }
+
+  private boolean fastFail(TypeElement element) {
+    return !seen.add(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..e6d106d
--- /dev/null
+++ b/user/src/com/google/web/bindery/requestfactory/apt/TransportableTypeVisitor.java
@@ -0,0 +1,123 @@
+/*
+ * 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.autobean.shared.ValueCodex;
+
+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;
+import javax.lang.model.util.SimpleTypeVisitor6;
+
+/**
+ * Scans a TypeMirror to determine if it can be transported by RequestFactory.
+ */
+class TransportableTypeVisitor extends SimpleTypeVisitor6<Boolean, State> {
+
+  /**
+   * 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 (Class<?> clazz : ValueCodex.getAllValueTypes()) {
+      if (clazz.isPrimitive()) {
+        continue;
+      }
+
+      if (state.types.isAssignable(t, state.findType(clazz))) {
+        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 t.getExtendsBound() != null && t.getExtendsBound().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/TypeSimplifier.java b/user/src/com/google/web/bindery/requestfactory/apt/TypeSimplifier.java
new file mode 100644
index 0000000..f073734
--- /dev/null
+++ b/user/src/com/google/web/bindery/requestfactory/apt/TypeSimplifier.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.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) {
+    if (x.getExtendsBound() != null) {
+      return x.getExtendsBound().accept(this, state);
+    }
+    return state.objectType;
+  }
+
+  @Override
+  protected TypeMirror defaultAction(TypeMirror x, State state) {
+    return state.types.getNoType(TypeKind.NONE);
+  }
+}
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..49eda05 100644
--- a/user/src/com/google/web/bindery/requestfactory/server/RequestFactoryJarExtractor.java
+++ b/user/src/com/google/web/bindery/requestfactory/server/RequestFactoryJarExtractor.java
@@ -32,6 +32,7 @@
 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.apt.RfValidator;
 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;
@@ -40,6 +41,7 @@
 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;
@@ -631,13 +633,13 @@
   @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,
-      com.google.web.bindery.requestfactory.shared.Violation.class, WriteOperation.class,
-      RequestFactorySource.class, SimpleEventBus.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, 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};
 
   /**
    * Maximum number of threads to use to run the Extractor.
@@ -645,18 +647,20 @@
   private static final int MAX_THREADS = 4;
 
   static {
+    List<Class<?>> aptClasses =
+        Collections.unmodifiableList(Arrays.<Class<?>> asList(RfApt.class, RfValidator.class));
     List<Class<?>> sharedClasses = Arrays.<Class<?>> asList(SHARED_CLASSES);
 
     List<Class<?>> clientClasses = new ArrayList<Class<?>>();
     clientClasses.addAll(sharedClasses);
-    clientClasses.add(RfApt.class);
+    clientClasses.addAll(aptClasses);
     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));
 
@@ -719,7 +723,8 @@
       // Add the annotation processor manifest
       resources =
           Collections.singletonMap("META-INF/services/" + Processor.class.getCanonicalName(),
-              RfApt.class.getCanonicalName().getBytes("UTF-8"));
+              (RfApt.class.getCanonicalName() + "\n" + RfValidator.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();
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 ad6575f..c2b69c1 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)
+@Target({ElementType.METHOD, ElementType.TYPE})
 public @interface SkipInterfaceValidation {
 }
diff --git a/user/src/com/google/web/bindery/requestfactory/shared/impl/FindRequest.java b/user/src/com/google/web/bindery/requestfactory/shared/impl/FindRequest.java
index 5061b7f..1fcb1c4 100644
--- a/user/src/com/google/web/bindery/requestfactory/shared/impl/FindRequest.java
+++ b/user/src/com/google/web/bindery/requestfactory/shared/impl/FindRequest.java
@@ -31,5 +31,5 @@
   /**
    * Use the implicit lookup in passing EntityProxy types to service methods.
    */
-  Request<EntityProxy> find(EntityProxyId<?> proxy);
+  Request<EntityProxy> find(EntityProxyId<? extends EntityProxy> proxy);
 }
\ No newline at end of file
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/impl/TypeTokenResolver.java b/user/src/com/google/web/bindery/requestfactory/vm/impl/TypeTokenResolver.java
index 5699c9a..aedcb27 100644
--- a/user/src/com/google/web/bindery/requestfactory/vm/impl/TypeTokenResolver.java
+++ b/user/src/com/google/web/bindery/requestfactory/vm/impl/TypeTokenResolver.java
@@ -68,6 +68,10 @@
       }
       in.close();
     }
+
+    public TypeTokenResolver peek() {
+      return d;
+    }
   }
 
   public static final String TOKEN_MANIFEST = "META-INF/requestFactory/typeTokens";
diff --git a/user/test/com/google/web/bindery/requestfactory/server/RequestFactoryInterfaceValidatorTest.java b/user/test/com/google/web/bindery/requestfactory/server/RequestFactoryInterfaceValidatorTest.java
index 5fb2655..049935a 100644
--- a/user/test/com/google/web/bindery/requestfactory/server/RequestFactoryInterfaceValidatorTest.java
+++ b/user/test/com/google/web/bindery/requestfactory/server/RequestFactoryInterfaceValidatorTest.java
@@ -42,7 +42,12 @@
 /**
  * JRE tests for {@link RequestFactoryInterfaceValidator}.
  */
+@SkipInterfaceValidation
 public class RequestFactoryInterfaceValidatorTest extends TestCase {
+  /*
+   * The annotation is only used by the APT-based validator and is temporary
+   * until RFIV and this test are removed.
+   */
   static class ClinitEntity {
     static ClinitEntity findClinitEntity(@SuppressWarnings("unused") String key) {
       return null;
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..821f20a 100644
--- a/user/test/com/google/web/bindery/requestfactory/shared/TestFooPolymorphicRequest.java
+++ b/user/test/com/google/web/bindery/requestfactory/shared/TestFooPolymorphicRequest.java
@@ -17,9 +17,10 @@
 
 /**
  * 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 {
-  <P extends SimpleFooProxy> Request<P> echo(P proxy);
+  <P extends SimpleFooProxy> Request<P> echo(SimpleFooProxy proxy);
 }