com.google.web.bindery.requestfactory.server.RequestFactoryInterfaceValidator.java Source code

Java tutorial

Introduction

Here is the source code for com.google.web.bindery.requestfactory.server.RequestFactoryInterfaceValidator.java

Source

/*
 * 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;
    }
}