| /* |
| * 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.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.client.impl.FindRequest; |
| 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 java.io.IOException; |
| import java.io.InputStream; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collections; |
| import java.util.Date; |
| 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. |
| */ |
| 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. |
| */ |
| boolean exists(String resource); |
| |
| /** |
| * Returns an InputStream to access the specified resource, or |
| * <code>null</code> if no such resource exists. |
| */ |
| InputStream getResourceAsStream(String resource); |
| } |
| |
| /** |
| * 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; |
| |
| public DomainMapper(ErrorContext logger) { |
| this.logger = logger; |
| logger.spam("Finding domain mapping annotation"); |
| } |
| |
| public String getDomainInternalName() { |
| return domainInternalName; |
| } |
| |
| @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"); |
| } |
| } |
| |
| @Override |
| public AnnotationVisitor visitAnnotation(String desc, boolean visible) { |
| boolean foundProxy = desc.equals(Type.getDescriptor(ProxyFor.class)); |
| boolean foundProxyName = desc.equals(Type.getDescriptor(ProxyForName.class)); |
| boolean foundService = desc.equals(Type.getDescriptor(Service.class)); |
| boolean foundServiceName = desc.equals(Type.getDescriptor(ServiceName.class)); |
| |
| if (foundProxy || foundService) { |
| return new EmptyVisitor() { |
| |
| @Override |
| public void visit(String name, Object value) { |
| domainInternalName = ((Type) value).getInternalName(); |
| } |
| |
| }; |
| } |
| |
| if (foundProxyName || foundServiceName) { |
| return new EmptyVisitor() { |
| @Override |
| public void visit(String name, Object value) { |
| String sourceName = (String) value; |
| |
| /* |
| * 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) { |
| return; |
| } |
| desc.setCharAt(idx, '$'); |
| } |
| |
| domainInternalName = desc.toString(); |
| logger.spam(domainInternalName); |
| } |
| }; |
| } |
| return null; |
| } |
| } |
| |
| /** |
| * Improves error messages by providing context for the user. |
| */ |
| private class ErrorContext { |
| private final Logger logger; |
| private final ErrorContext parent; |
| private Type currentType; |
| private Method currentMethod; |
| |
| public ErrorContext(Logger logger) { |
| this.logger = logger; |
| this.parent = null; |
| } |
| |
| private ErrorContext(ErrorContext parent) { |
| this.logger = parent.logger; |
| this.parent = parent; |
| } |
| |
| public void poison(String msg, Object... args) { |
| logger.logp(Level.SEVERE, currentType(), currentMethod(), |
| String.format(msg, args)); |
| poisoned = true; |
| } |
| |
| public void poison(String msg, Throwable t) { |
| logger.logp(Level.SEVERE, currentType(), currentMethod(), msg, t); |
| poisoned = true; |
| } |
| |
| public ErrorContext setMethod(Method method) { |
| ErrorContext toReturn = new ErrorContext(this); |
| toReturn.currentMethod = method; |
| return toReturn; |
| } |
| |
| public ErrorContext setType(Type type) { |
| ErrorContext toReturn = new ErrorContext(this); |
| toReturn.currentType = type; |
| return toReturn; |
| } |
| |
| public void spam(String msg, Object... args) { |
| logger.logp(Level.FINEST, currentType(), currentMethod(), |
| String.format(msg, args)); |
| } |
| |
| 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; |
| } |
| } |
| |
| /** |
| * 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); |
| return methods; |
| } |
| |
| @Override |
| public void visit(int version, int access, String name, String signature, |
| String superName, String[] interfaces) { |
| if (!seen.add(name)) { |
| return; |
| } |
| if (!"java/lang/Object".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> supertypes = new ArrayList<Type>(); |
| |
| public SupertypeCollector(ErrorContext logger) { |
| this.logger = logger; |
| } |
| |
| public List<Type> exec(Type type) { |
| RequestFactoryInterfaceValidator.this.visit(logger, |
| type.getInternalName(), this); |
| return supertypes; |
| } |
| |
| @Override |
| public void visit(int version, int access, String name, String signature, |
| String superName, String[] interfaces) { |
| if (!seen.add(name)) { |
| return; |
| } |
| supertypes.add(Type.getObjectType(name)); |
| if (!"java/lang/Object".equals(name)) { |
| RequestFactoryInterfaceValidator.this.visit(logger, superName, this); |
| } |
| if (interfaces != null) { |
| for (String intf : interfaces) { |
| RequestFactoryInterfaceValidator.this.visit(logger, intf, this); |
| } |
| } |
| } |
| } |
| |
| @SuppressWarnings("unchecked") |
| static final Set<Class<?>> VALUE_TYPES = Collections.unmodifiableSet(new HashSet<Class<?>>( |
| Arrays.asList(Boolean.class, Character.class, Class.class, Date.class, |
| Enum.class, Number.class, String.class, Void.class))); |
| |
| 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); |
| } |
| |
| 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()); |
| } |
| |
| /** |
| * Maps client types (e.g. FooProxy) to server domain types (e.g. Foo). |
| */ |
| private final Map<Type, Type> clientToDomainType = new HashMap<Type, Type>(); |
| /** |
| * The type {@link EntityProxy}. |
| */ |
| private final Type entityProxyIntf = Type.getType(EntityProxy.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>>(); |
| 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>>(); |
| /** |
| * 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>(); |
| |
| public RequestFactoryInterfaceValidator(Logger logger, Loader loader) { |
| this.parentLogger = new ErrorContext(logger); |
| this.loader = loader; |
| for (Class<?> clazz : VALUE_TYPES) { |
| valueTypes.add(Type.getType(clazz)); |
| } |
| } |
| |
| /** |
| * 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 EntityProxy types are valid</li> |
| * </ul> |
| * |
| * @param binaryName the binary name (e.g. {@link Class#getName()}) of the |
| * EntityProxy subtype |
| */ |
| public void validateEntityProxy(String binaryName) { |
| if (!Name.isBinaryName(binaryName)) { |
| parentLogger.poison("%s is not a binary name", binaryName); |
| return; |
| } |
| |
| // Don't revalidate the same type |
| if (!validatedTypes.add(binaryName)) { |
| return; |
| } |
| |
| Type proxyType = Type.getObjectType(BinaryName.toInternalName(binaryName)); |
| ErrorContext logger = parentLogger.setType(proxyType); |
| |
| // Quick sanity check for calling code |
| if (!isAssignable(logger, entityProxyIntf, proxyType)) { |
| parentLogger.poison("%s is not a %s", print(proxyType), |
| EntityProxy.class.getSimpleName()); |
| 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 |
| 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); |
| } |
| } |
| |
| /** |
| * 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 (!Name.isBinaryName(binaryName)) { |
| parentLogger.poison("%s is not a binary name", binaryName); |
| return; |
| } |
| |
| // Don't revalidate the same type |
| if (!validatedTypes.add(binaryName)) { |
| return; |
| } |
| |
| if (FindRequest.class.getName().equals(binaryName)) { |
| // Ignore FindRequest, it's a huge hack |
| 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) != null) { |
| continue; |
| } |
| |
| // Check the client method against the domain |
| checkClientMethodInDomain(logger, method, domainServiceType); |
| maybeCheckReferredProxies(logger, method); |
| } |
| } |
| |
| /** |
| * 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 (!Name.isBinaryName(binaryName)) { |
| parentLogger.poison("%s is not a binary name", binaryName); |
| return; |
| } |
| |
| // Don't revalidate the same type |
| if (!validatedTypes.add(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()); |
| } |
| } |
| } |
| |
| /** |
| * 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) { |
| 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 = findCompatibleMethod(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 (!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) { |
| logger = logger.setType(domainType); |
| Method getIdString = new Method("getId", "()Ljava/lang/String;"); |
| Method getIdLong = new Method("getId", "()Ljava/lang/Long;"); |
| if (findCompatibleMethod(logger, domainType, getIdString, false, true) == null |
| && findCompatibleMethod(logger, domainType, getIdLong, false, true) == null) { |
| logger.poison("Did not find a getId() method that" |
| + " returns a String or a Long"); |
| } |
| |
| Method getVersion = new Method("getVersion", "()Ljava/lang/Integer;"); |
| if (findCompatibleMethod(logger, domainType, getVersion) == null) { |
| logger.poison("Did not find a getVersion() method" |
| + " that returns an Integer"); |
| } |
| } |
| |
| /** |
| * 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); |
| |
| findCompatibleMethod(logger, domainType, |
| createDomainMethod(logger, clientPropertyMethod)); |
| } |
| |
| /** |
| * 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); |
| } |
| |
| /** |
| * 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) { |
| return findCompatibleMethod(logger, domainType, searchFor, true, 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) { |
| String methodName = searchFor.getName(); |
| Type[] clientArgs = searchFor.getArgumentTypes(); |
| Type clientReturnType = searchFor.getReturnType(); |
| // 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) { |
| // Box any primitive types to simplify compotibility check |
| Type[] domainArgs = domainMethod.getArgumentTypes(); |
| for (int i = 0, j = domainArgs.length; i < j; i++) { |
| domainArgs[i] = maybeBoxType(domainArgs[i]); |
| } |
| Type domainReturnType = maybeBoxType(domainMethod.getReturnType()); |
| |
| /* |
| * 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) { |
| StringBuilder sb = new StringBuilder(); |
| sb.append(String.format( |
| "Could not find matching method in %s:\nPossible matches:\n", |
| print(domainType))); |
| for (RFMethod domainMethod : methods) { |
| sb.append(" ").append(print(domainMethod)).append("\n"); |
| } |
| logger.poison(sb.toString()); |
| } |
| return null; |
| } |
| |
| /** |
| * 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 toReturn = clientToDomainType.get(clientType); |
| if (toReturn != null) { |
| return toReturn; |
| } |
| if (isValueType(logger, clientType) || isCollectionType(logger, clientType)) { |
| toReturn = clientType; |
| } 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()); |
| toReturn = errorType; |
| } else { |
| toReturn = Type.getObjectType(pv.getDomainInternalName()); |
| } |
| } |
| clientToDomainType.put(clientType, toReturn); |
| return toReturn; |
| } |
| |
| /** |
| * 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 = {"java/lang/Object"}; |
| 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; |
| } |
| |
| // Box primitive types to make this simple |
| if (possibleSupertype.getSort() != Type.OBJECT) { |
| possibleSupertype = getBoxedType(possibleSupertype); |
| } |
| if (possibleSubtype.getSort() != Type.OBJECT) { |
| possibleSubtype = getBoxedType(possibleSubtype); |
| } |
| |
| // 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(ErrorContext logger, Type type) { |
| return "java/util/List".equals(type.getInternalName()) |
| || "java/util/Set".equals(type.getInternalName()); |
| } |
| |
| private boolean isValueType(ErrorContext logger, Type type) { |
| if (type.getSort() != Type.OBJECT) { |
| return true; |
| } |
| if (valueTypes.contains(type)) { |
| return true; |
| } |
| logger = logger.setType(type); |
| List<Type> types = getSupertypes(logger, type); |
| for (Type t : types) { |
| if (valueTypes.contains(t)) { |
| valueTypes.add(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)} if |
| * the type is an EntityProxy. |
| */ |
| private void maybeCheckEntityProxyType(ErrorContext logger, Type... types) { |
| for (Type type : types) { |
| if (isAssignable(logger, entityProxyIntf, type)) { |
| validateEntityProxy(type.getClassName()); |
| } |
| } |
| } |
| |
| /** |
| * Examine the arguments ond return value of a method and check any |
| * EntityProxies referred. |
| */ |
| private void maybeCheckReferredProxies(ErrorContext logger, RFMethod method) { |
| Type[] argTypes = method.getArgumentTypes(); |
| Type returnType = getReturnType(logger, method); |
| |
| // Check EntityProxy args ond return types against the domain |
| maybeCheckEntityProxyType(logger, argTypes); |
| maybeCheckEntityProxyType(logger, returnType); |
| } |
| |
| /** |
| * 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; |
| } |
| } |