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