Package com.google.gwt.requestfactory.server

Source Code of com.google.gwt.requestfactory.server.RequestFactoryInterfaceValidator$DomainMapper

/*
* 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.gwt.requestfactory.server;

import com.google.gwt.autobean.shared.ValueCodex;
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.gwt.requestfactory.shared.BaseProxy;
import com.google.gwt.requestfactory.shared.EntityProxy;
import com.google.gwt.requestfactory.shared.InstanceRequest;
import com.google.gwt.requestfactory.shared.ProxyFor;
import com.google.gwt.requestfactory.shared.ProxyForName;
import com.google.gwt.requestfactory.shared.Request;
import com.google.gwt.requestfactory.shared.RequestContext;
import com.google.gwt.requestfactory.shared.RequestFactory;
import com.google.gwt.requestfactory.shared.Service;
import com.google.gwt.requestfactory.shared.ServiceName;
import com.google.gwt.requestfactory.shared.ValueProxy;

import java.io.IOException;
import java.io.InputStream;
import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.Collections;
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.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.gwt.requestfactory.server.RequestFactoryInterfaceValidator \
*   com.example.MyRequestFactory
* </pre>
*
* <p><span style='color:red'>RequestFactory has moved to
* <code>com.google.web.bindery.requestfactory</code>.  This package will be
* removed in a future version of GWT.</span></p>
*/
@Deprecated
public class RequestFactoryInterfaceValidator {
  /**
   * An implementation of {@link Loader} that uses a {@link ClassLoader} to
   * retrieve the class files.
   *
   * <p><span style='color:red'>RequestFactory has moved to
   * <code>com.google.web.bindery.requestfactory</code>.  This package will be
   * removed in a future version of GWT.</span></p>
   */
  @Deprecated
  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.
   *
   * <p><span style='color:red'>RequestFactory has moved to
   * <code>com.google.web.bindery.requestfactory</code>.  This package will be
   * removed in a future version of GWT.</span></p>
   *
   * @see ClassLoaderLoader
   */
  @Deprecated
  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.
   *
   * <p><span style='color:red'>RequestFactory has moved to
   * <code>com.google.web.bindery.requestfactory</code>.  This package will be
   * removed in a future version of GWT.</span></p>
   */
  @Deprecated
  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.
   *
   * <p><span style='color:red'>RequestFactory has moved to
   * <code>com.google.web.bindery.requestfactory</code>.  This package will be
   * removed in a future version of GWT.</span></p>
   */
  @Deprecated
  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());
      }
    }
  }

  /**
   * 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;
      }
      RFMethod method = new RFMethod(name, desc);
      method.setDeclaredStatic((access & Opcodes.ACC_STATIC) != 0);
      method.setDeclaredSignature(signature);
      methods.add(method);
      return null;
    }
  }

  private static class RFMethod extends Method {
    private boolean isDeclaredStatic;
    private String signature;

    public RFMethod(String name, String desc) {
      super(name, desc);
    }

    public String getSignature() {
      return signature;
    }

    public boolean isDeclaredStatic() {
      return isDeclaredStatic;
    }

    public void setDeclaredSignature(String signature) {
      this.signature = signature;
    }

    public void setDeclaredStatic(boolean value) {
      isDeclaredStatic = value;
    }

    @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()]);
    }

    public SignatureVisitor visitArrayType() {
      return this;
    }

    public SignatureVisitor visitClassBound() {
      return this;
    }

    @Override
    public void visitClassType(String name) {
      found.add(Type.getObjectType(name));
    }

    public SignatureVisitor visitExceptionType() {
      return this;
    }

    public SignatureVisitor visitInterface() {
      return this;
    }

    public SignatureVisitor visitInterfaceBound() {
      return this;
    }

    public SignatureVisitor visitParameterType() {
      return this;
    }

    public SignatureVisitor visitReturnType() {
      return this;
    }

    public SignatureVisitor visitSuperclass() {
      return this;
    }

    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, List<Type>> domainToClientType = new HashMap<Type, List<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>>();
  /**
   * The type {@link Object}.
   */
  private final Type objectType = Type.getObjectType("java/lang/Object");
  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;
  }

  /**
   * 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</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</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);
    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
      checkClientMethodInDomain(logger, method, domainServiceType,
          !clientToLocatorMap.containsKey(requestContextType));
      maybeCheckReferredProxies(logger, method);
    }

    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());
      }
    }
  }

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

  /**
   * Given the binary name of a domain type, return the BaseProxy type that is
   * assignable to {@code clientType}. This method allows multiple proxy types
   * to be assigned to a domain type for use in different contexts (e.g. API
   * slices). If there are multiple client types mapped to
   * {@code domainTypeBinaryName} and assignable to {@code clientTypeBinaryName}
   * , the first matching type will be returned.
   */
  String getEntityProxyTypeName(String domainTypeBinaryName,
      String clientTypeBinaryName) {
    Type key = Type.getObjectType(BinaryName.toInternalName(domainTypeBinaryName));
    List<Type> found = domainToClientType.get(key);

    /*
     * If nothing was found look for proxyable supertypes the domain object can
     * be upcast to.
     */
    if (found == null || found.isEmpty()) {
      List<Type> types = getSupertypes(parentLogger, key);
      for (Type type : types) {
        if (objectType.equals(type)) {
          break;
        }

        found = domainToClientType.get(type);
        if (found != null && !found.isEmpty()) {
          break;
        }
      }
    }

    if (found == null || found.isEmpty()) {
      return null;
    }

    Type typeToReturn = null;

    // Common case
    if (found.size() == 1) {
      typeToReturn = found.get(0);
    } else {
      // Search for the first assignable type
      Type assignableTo = Type.getObjectType(BinaryName.toInternalName(clientTypeBinaryName));
      for (Type t : found) {
        if (isAssignable(parentLogger, assignableTo, t)) {
          typeToReturn = t;
          break;
        }
      }
    }

    return typeToReturn == null ? null : typeToReturn.getClassName();
  }

  /**
   * 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)) {
      maybeCheckProxyType(logger, clientType);
      List<Type> list = domainToClientType.get(domainType);
      if (list == null) {
        list = new ArrayList<Type>();
        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 void 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);

    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());
      }
    }
  }

  /**
   * 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]));
      }
    } 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,
      Method clientPropertyMethod, Type domainType) {
    logger = logger.setMethod(clientPropertyMethod);

    findCompatiblePropertyMethod(logger, domainType,
        createDomainMethod(logger, clientPropertyMethod));
  }

  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]);
    }
    Type returnType = getDomainType(logger, clientMethod.getReturnType());
    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) {
    return findCompatibleMethod(logger, domainType, searchFor, true, 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) {
    return findCompatibleMethod(logger, domainType, searchFor, true, false,
        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) {
    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) {
        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);
    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);
  }

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

  private void validateProxy(String binaryName, Type expectedType,
      boolean requireId) {
    if (fastFail(binaryName)) {
      return;
    }

    Type proxyType = Type.getObjectType(BinaryName.toInternalName(binaryName));
    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;
    }

    // Find the domain type
    Type domainType = getDomainType(logger, proxyType);
    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);
    }
  }

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

Related Classes of com.google.gwt.requestfactory.server.RequestFactoryInterfaceValidator$DomainMapper

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.