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