com.google.gwt.requestfactory.server.RequestFactoryInterfaceValidator Maven / Gradle / Ivy
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.gwt.requestfactory.server;
import com.google.gwt.autobean.shared.ValueCodex;
import com.google.gwt.dev.asm.AnnotationVisitor;
import com.google.gwt.dev.asm.ClassReader;
import com.google.gwt.dev.asm.ClassVisitor;
import com.google.gwt.dev.asm.MethodVisitor;
import com.google.gwt.dev.asm.Opcodes;
import com.google.gwt.dev.asm.Type;
import com.google.gwt.dev.asm.commons.EmptyVisitor;
import com.google.gwt.dev.asm.commons.Method;
import com.google.gwt.dev.asm.signature.SignatureReader;
import com.google.gwt.dev.asm.signature.SignatureVisitor;
import com.google.gwt.dev.util.Name;
import com.google.gwt.dev.util.Name.BinaryName;
import com.google.gwt.dev.util.Name.SourceOrBinaryName;
import com.google.gwt.requestfactory.shared.BaseProxy;
import com.google.gwt.requestfactory.shared.EntityProxy;
import com.google.gwt.requestfactory.shared.InstanceRequest;
import com.google.gwt.requestfactory.shared.ProxyFor;
import com.google.gwt.requestfactory.shared.ProxyForName;
import com.google.gwt.requestfactory.shared.Request;
import com.google.gwt.requestfactory.shared.RequestContext;
import com.google.gwt.requestfactory.shared.RequestFactory;
import com.google.gwt.requestfactory.shared.Service;
import com.google.gwt.requestfactory.shared.ServiceName;
import com.google.gwt.requestfactory.shared.ValueProxy;
import java.io.IOException;
import java.io.InputStream;
import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Encapsulates validation logic to determine if a {@link RequestFactory}
* interface, its {@link RequestContext}, and associated {@link EntityProxy}
* interfaces match their domain counterparts. This implementation examines the
* classfiles directly in order to avoid the need to load the types into the
* JVM.
*
* This class is amenable to being used as a unit test:
*
*
* 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());
* }
*
* This class also has a {@code main} method and can be used as a build-time
* tool:
*
*
* java -cp gwt-servlet.jar:your-code.jar \
* com.google.gwt.requestfactory.server.RequestFactoryInterfaceValidator \
* com.example.MyRequestFactory
*
*
* RequestFactory has moved to
* com.google.web.bindery.requestfactory
. This package will be
* removed in a future version of GWT.
*/
@Deprecated
public class RequestFactoryInterfaceValidator {
/**
* An implementation of {@link Loader} that uses a {@link ClassLoader} to
* retrieve the class files.
*
* RequestFactory has moved to
* com.google.web.bindery.requestfactory
. This package will be
* removed in a future version of GWT.
*/
@Deprecated
public static class ClassLoaderLoader implements Loader {
private final ClassLoader loader;
public ClassLoaderLoader(ClassLoader loader) {
this.loader = loader;
}
public boolean exists(String resource) {
return loader.getResource(resource) != null;
}
public InputStream getResourceAsStream(String resource) {
return loader.getResourceAsStream(resource);
}
}
/**
* Abstracts the mechanism by which class files are loaded.
*
* RequestFactory has moved to
* com.google.web.bindery.requestfactory
. This package will be
* removed in a future version of GWT.
*
* @see ClassLoaderLoader
*/
@Deprecated
public interface Loader {
/**
* Returns true if the specified resource can be loaded.
*
* @param resource a resource name (e.g. com/example/Foo.class
)
*/
boolean exists(String resource);
/**
* Returns an InputStream to access the specified resource, or
* null
if no such resource exists.
*
* @param resource a resource name (e.g. com/example/Foo.class
)
*/
InputStream getResourceAsStream(String resource);
}
/**
* Improves error messages by providing context for the user.
*
* Visible for testing.
*
*
RequestFactory has moved to
* com.google.web.bindery.requestfactory
. This package will be
* removed in a future version of GWT.
*/
@Deprecated
static class ErrorContext {
private final Logger logger;
private final ErrorContext parent;
private Type currentType;
private Method currentMethod;
private RequestFactoryInterfaceValidator validator;
public ErrorContext(Logger logger) {
this.logger = logger;
this.parent = null;
}
protected ErrorContext(ErrorContext parent) {
this.logger = parent.logger;
this.parent = parent;
this.validator = parent.validator;
}
public void poison(String msg, Object... args) {
poison();
logger.logp(Level.SEVERE, currentType(), currentMethod(),
String.format(msg, args));
validator.poisoned = true;
}
public void poison(String msg, Throwable t) {
poison();
logger.logp(Level.SEVERE, currentType(), currentMethod(), msg, t);
validator.poisoned = true;
}
public ErrorContext setMethod(Method method) {
ErrorContext toReturn = fork();
toReturn.currentMethod = method;
return toReturn;
}
public ErrorContext setType(Type type) {
ErrorContext toReturn = fork();
toReturn.currentType = type;
return toReturn;
}
public void spam(String msg, Object... args) {
logger.logp(Level.FINEST, currentType(), currentMethod(),
String.format(msg, args));
}
protected ErrorContext fork() {
return new ErrorContext(this);
}
void setValidator(RequestFactoryInterfaceValidator validator) {
assert this.validator == null : "Cannot set validator twice";
this.validator = validator;
}
private String currentMethod() {
if (currentMethod != null) {
return print(currentMethod);
}
if (parent != null) {
return parent.currentMethod();
}
return null;
}
private String currentType() {
if (currentType != null) {
return print(currentType);
}
if (parent != null) {
return parent.currentType();
}
return null;
}
/**
* Populate {@link RequestFactoryInterfaceValidator#badTypes} with the
* current context.
*/
private void poison() {
if (currentType != null) {
validator.badTypes.add(currentType.getClassName());
}
if (parent != null) {
parent.poison();
}
}
}
/**
* Used internally as a placeholder for types that cannot be mapped to a
* domain object.
*
* RequestFactory has moved to
* com.google.web.bindery.requestfactory
. This package will be
* removed in a future version of GWT.
*/
@Deprecated
interface MissingDomainType {
}
/**
* Collects the ProxyFor or Service annotation from an EntityProxy or
* RequestContext type.
*/
private class DomainMapper extends EmptyVisitor {
private final ErrorContext logger;
private String domainInternalName;
private List> found = new ArrayList>();
private String locatorInternalName;
public DomainMapper(ErrorContext logger) {
this.logger = logger;
logger.spam("Finding domain mapping annotation");
}
public String getDomainInternalName() {
return domainInternalName;
}
public String getLocatorInternalName() {
return locatorInternalName;
}
@Override
public void visit(int version, int access, String name, String signature,
String superName, String[] interfaces) {
if ((access & Opcodes.ACC_INTERFACE) == 0) {
logger.poison("Type must be an interface");
}
}
/**
* This method examines one annotation at a time.
*/
@Override
public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
// Set to true if the annotation should have class literal values
boolean expectClasses = false;
// Set to true if the annonation has string values
boolean expectNames = false;
if (desc.equals(Type.getDescriptor(ProxyFor.class))) {
expectClasses = true;
found.add(ProxyFor.class);
} else if (desc.equals(Type.getDescriptor(ProxyForName.class))) {
expectNames = true;
found.add(ProxyForName.class);
} else if (desc.equals(Type.getDescriptor(Service.class))) {
expectClasses = true;
found.add(Service.class);
} else if (desc.equals(Type.getDescriptor(ServiceName.class))) {
expectNames = true;
found.add(ServiceName.class);
}
if (expectClasses) {
return new EmptyVisitor() {
@Override
public void visit(String name, Object value) {
if ("value".equals(name)) {
domainInternalName = ((Type) value).getInternalName();
} else if ("locator".equals(name)) {
locatorInternalName = ((Type) value).getInternalName();
}
}
};
}
if (expectNames) {
return new EmptyVisitor() {
@Override
public void visit(String name, Object value) {
String sourceName;
boolean locatorRequired = "locator".equals(name);
boolean valueRequired = "value".equals(name);
if (valueRequired || locatorRequired) {
sourceName = (String) value;
} else {
return;
}
/*
* The input is a source name, so we need to convert it to an
* internal name. We'll do this by substituting dollar signs for the
* last slash in the name until there are no more slashes.
*/
StringBuffer desc = new StringBuffer(sourceName.replace('.', '/'));
while (!loader.exists(desc.toString() + ".class")) {
logger.spam("Did not find " + desc.toString());
int idx = desc.lastIndexOf("/");
if (idx == -1) {
if (locatorRequired) {
logger.poison("Cannot find locator named %s", value);
} else if (valueRequired) {
logger.poison("Cannot find domain type named %s", value);
}
return;
}
desc.setCharAt(idx, '$');
}
if (locatorRequired) {
locatorInternalName = desc.toString();
logger.spam(locatorInternalName);
} else if (valueRequired) {
domainInternalName = desc.toString();
logger.spam(domainInternalName);
} else {
throw new RuntimeException("Should not reach here");
}
}
};
}
return null;
}
@Override
public void visitEnd() {
// Only allow one annotation
if (found.size() > 1) {
StringBuilder sb = new StringBuilder();
for (Class> clazz : found) {
sb.append(" @").append(clazz.getSimpleName());
}
logger.poison("Redundant domain mapping annotations present:%s",
sb.toString());
}
}
}
/**
* Collects information about domain objects. This visitor is intended to be
* iteratively applied to collect all methods in a type hierarchy.
*/
private class MethodsInHierarchyCollector extends EmptyVisitor {
private final ErrorContext logger;
private Set methods = new LinkedHashSet();
private Set seen = new HashSet();
private MethodsInHierarchyCollector(ErrorContext logger) {
this.logger = logger;
}
public Set exec(String internalName) {
RequestFactoryInterfaceValidator.this.visit(logger, internalName, this);
Map toReturn = new HashMap();
// 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(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 ("".equals(name) || "".equals(name)) {
return null;
}
RFMethod method = new RFMethod(name, desc);
method.setDeclaredStatic((access & Opcodes.ACC_STATIC) != 0);
method.setDeclaredSignature(signature);
methods.add(method);
return null;
}
}
private static class RFMethod extends Method {
private boolean isDeclaredStatic;
private String signature;
public RFMethod(String name, String desc) {
super(name, desc);
}
public String getSignature() {
return signature;
}
public boolean isDeclaredStatic() {
return isDeclaredStatic;
}
public void setDeclaredSignature(String signature) {
this.signature = signature;
}
public void setDeclaredStatic(boolean value) {
isDeclaredStatic = value;
}
@Override
public String toString() {
return (isDeclaredStatic ? "static " : "") + super.toString();
}
}
private class SupertypeCollector extends EmptyVisitor {
private final ErrorContext logger;
private final Set seen = new HashSet();
private final List supers = new ArrayList();
public SupertypeCollector(ErrorContext logger) {
this.logger = logger;
}
public List 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 found = new HashSet();
public Type[] getFound() {
return found.toArray(new Type[found.size()]);
}
public SignatureVisitor visitArrayType() {
return this;
}
public SignatureVisitor visitClassBound() {
return this;
}
@Override
public void visitClassType(String name) {
found.add(Type.getObjectType(name));
}
public SignatureVisitor visitExceptionType() {
return this;
}
public SignatureVisitor visitInterface() {
return this;
}
public SignatureVisitor visitInterfaceBound() {
return this;
}
public SignatureVisitor visitParameterType() {
return this;
}
public SignatureVisitor visitReturnType() {
return this;
}
public SignatureVisitor visitSuperclass() {
return this;
}
public SignatureVisitor visitTypeArgument(char wildcard) {
return this;
}
}
static final Set> 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 badTypes = new HashSet();
/**
* 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 clientToDomainType = new HashMap();
/**
* Maps client types (e.g. FooProxy or FooContext) to their locator types
* (e.g. FooLocator or FooServiceLocator).
*/
private final Map clientToLocatorMap = new HashMap();
/**
* Maps domain types (e.g Foo) to client proxy types (e.g. FooAProxy,
* FooBProxy).
*/
private final Map> domainToClientType = new HashMap>();
/**
* 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> methodsInHierarchy = new HashMap>();
/**
* The type {@link Object}.
*/
private final Type objectType = Type.getObjectType("java/lang/Object");
private final ErrorContext parentLogger;
private boolean poisoned;
/**
* The type {@link Request}.
*/
private final Type requestIntf = Type.getType(Request.class);
/**
* The type {@link RequestContext}.
*/
private final Type requestContextIntf = Type.getType(RequestContext.class);
/**
* A map of a type to all types that it could be assigned to.
*/
private final Map> supertypes = new HashMap>();
/**
* The type {@link ValueProxy}.
*/
private final Type valueProxyIntf = Type.getType(ValueProxy.class);
/**
* A set to prevent re-validation of a type.
*/
private final Set validatedTypes = new HashSet();
/**
* Contains vaue types (e.g. Integer).
*/
private final Set valueTypes = new HashSet();
/**
* Maps a domain object to the type returned from its getId method.
*/
private final Map unresolvedKeyTypes = new HashMap();
{
for (Class> clazz : VALUE_TYPES) {
valueTypes.add(Type.getType(clazz));
}
}
public RequestFactoryInterfaceValidator(Logger logger, Loader loader) {
this.parentLogger = new ErrorContext(logger);
parentLogger.setValidator(this);
this.loader = loader;
}
/**
* Visible for testing.
*/
RequestFactoryInterfaceValidator(ErrorContext errorContext, Loader loader) {
this.parentLogger = errorContext;
this.loader = loader;
errorContext.setValidator(this);
}
/**
* Reset the poisoned status of the validator so that it may be reused without
* destroying cached state.
*/
public void antidote() {
poisoned = false;
}
/**
* Returns true if validation failed.
*/
public boolean isPoisoned() {
return poisoned;
}
/**
* This method checks an EntityProxy interface against its peer domain object
* to determine if the server code would be able to process a request using
* the methods defined in the EntityProxy interface. It does not perform any
* checks as to whether or not the EntityProxy could actually be generated by
* the Generator.
*
* This method may be called repeatedly on a single instance of the validator.
* Doing so will amortize type calculation costs.
*
* Checks implemented:
*
* -
binaryName
implements EntityProxy
* binaryName
has a {@link ProxyFor} or {@link ProxyForName}
* annotation
* - The domain object has getId() and getVersion() methods
* - All property methods in the EntityProxy can be mapped onto an
* equivalent domain method
* - All referenced proxy types are valid
*
*
* @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.
*
* This method may be called repeatedly on a single instance of the validator.
* Doing so will amortize type calculation costs.
*
* Checks implemented:
*
* -
binaryName
implements RequestContext
* binaryName
has a {@link Service} or {@link ServiceName}
* annotation
* - All service methods in the RequestContext can be mapped onto an
* equivalent domain method
* - All referenced EntityProxy types are valid
*
*
* @param binaryName the binary name (e.g. {@link Class#getName()}) of the
* RequestContext subtype
* @see #validateEntityProxy(String)
*/
public void validateRequestContext(String binaryName) {
if (fastFail(binaryName)) {
return;
}
Type requestContextType = Type.getObjectType(BinaryName.toInternalName(binaryName));
final ErrorContext logger = parentLogger.setType(requestContextType);
// Quick sanity check for calling code
if (!isAssignable(logger, requestContextIntf, requestContextType)) {
logger.poison("%s is not a %s", print(requestContextType),
RequestContext.class.getSimpleName());
return;
}
Type domainServiceType = getDomainType(logger, requestContextType);
if (domainServiceType == errorType) {
logger.poison(
"The type %s must be annotated with a @%s or @%s annotation",
BinaryName.toSourceName(binaryName), Service.class.getSimpleName(),
ServiceName.class.getSimpleName());
return;
}
for (RFMethod method : getMethodsInHierarchy(logger, requestContextType)) {
// Ignore methods in RequestContext itself
if (findCompatibleMethod(logger, requestContextIntf, method, false, true,
true) != null) {
continue;
}
// Check the client method against the domain
checkClientMethodInDomain(logger, method, domainServiceType,
!clientToLocatorMap.containsKey(requestContextType));
maybeCheckReferredProxies(logger, method);
}
checkUnresolvedKeyTypes(logger);
}
/**
* This method checks a RequestFactory interface.
*
* 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.
*
* Checks implemented:
*
* -
binaryName
implements RequestFactory
* - All referenced RequestContext types are valid
*
*
* @param binaryName the binary name (e.g. {@link Class#getName()}) of the
* RequestContext subtype
* @see #validateRequestContext(String)
*/
public void validateRequestFactory(String binaryName) {
if (fastFail(binaryName)) {
return;
}
Type requestFactoryType = Type.getObjectType(BinaryName.toInternalName(binaryName));
ErrorContext logger = parentLogger.setType(requestFactoryType);
// Quick sanity check for calling code
if (!isAssignable(logger, Type.getType(RequestFactory.class),
requestFactoryType)) {
logger.poison("%s is not a %s", print(requestFactoryType),
RequestFactory.class.getSimpleName());
return;
}
// Validate each RequestContext method in the RF
for (Method contextMethod : getMethodsInHierarchy(logger,
requestFactoryType)) {
Type returnType = contextMethod.getReturnType();
if (isAssignable(logger, requestContextIntf, returnType)) {
validateRequestContext(returnType.getClassName());
}
}
}
/**
* This method checks a ValueProxy interface against its peer domain object to
* determine if the server code would be able to process a request using the
* methods defined in the ValueProxy interface. It does not perform any checks
* as to whether or not the ValueProxy could actually be generated by the
* Generator.
*
* This method may be called repeatedly on a single instance of the validator.
* Doing so will amortize type calculation costs.
*
* Checks implemented:
*
* -
binaryName
implements ValueProxy
* binaryName
has a {@link ProxyFor} or {@link ProxyForName}
* annotation
* - All property methods in the EntityProxy can be mapped onto an
* equivalent domain method
* - All referenced proxy types are valid
*
*
* @param binaryName the binary name (e.g. {@link Class#getName()}) of the
* EntityProxy subtype
*/
public void validateValueProxy(String binaryName) {
validateProxy(binaryName, valueProxyIntf, false);
}
/**
* Given the binary name of a domain type, return the BaseProxy type that is
* assignable to {@code clientType}. This method allows multiple proxy types
* to be assigned to a domain type for use in different contexts (e.g. API
* slices). If there are multiple client types mapped to
* {@code domainTypeBinaryName} and assignable to {@code clientTypeBinaryName}
* , the first matching type will be returned.
*/
String getEntityProxyTypeName(String domainTypeBinaryName,
String clientTypeBinaryName) {
Type key = Type.getObjectType(BinaryName.toInternalName(domainTypeBinaryName));
List found = domainToClientType.get(key);
/*
* If nothing was found look for proxyable supertypes the domain object can
* be upcast to.
*/
if (found == null || found.isEmpty()) {
List types = getSupertypes(parentLogger, key);
for (Type type : types) {
if (objectType.equals(type)) {
break;
}
found = domainToClientType.get(type);
if (found != null && !found.isEmpty()) {
break;
}
}
}
if (found == null || found.isEmpty()) {
return null;
}
Type typeToReturn = null;
// Common case
if (found.size() == 1) {
typeToReturn = found.get(0);
} else {
// Search for the first assignable type
Type assignableTo = Type.getObjectType(BinaryName.toInternalName(clientTypeBinaryName));
for (Type t : found) {
if (isAssignable(parentLogger, assignableTo, t)) {
typeToReturn = t;
break;
}
}
}
return typeToReturn == null ? null : typeToReturn.getClassName();
}
/**
* Record the mapping of a domain type to a client type. Proxy types will be
* added to {@link #domainToClientType}.
*/
private void addToDomainMap(ErrorContext logger, Type domainType,
Type clientType) {
clientToDomainType.put(clientType, domainType);
if (isAssignable(logger, baseProxyIntf, clientType)) {
maybeCheckProxyType(logger, clientType);
List list = domainToClientType.get(domainType);
if (list == null) {
list = new ArrayList();
domainToClientType.put(domainType, list);
}
list.add(clientType);
}
}
/**
* Check that a given method RequestContext method declaration can be mapped
* to the server's domain type.
*/
private void checkClientMethodInDomain(ErrorContext logger, RFMethod method,
Type domainServiceType, boolean requireStaticMethodsForRequestType) {
logger = logger.setMethod(method);
// Create a "translated" method declaration to search for
// Request foo(int a, BarProxy bar) -> Blah foo(int a, Bar bar);
Type returnType = getReturnType(logger, method);
Method searchFor = createDomainMethod(logger, new Method(method.getName(),
returnType, method.getArgumentTypes()));
RFMethod found = findCompatibleServiceMethod(logger, domainServiceType,
searchFor);
if (found != null) {
boolean isInstance = isAssignable(logger, instanceRequestIntf,
method.getReturnType());
if (isInstance && found.isDeclaredStatic()) {
logger.poison("The method %s is declared to return %s, but the"
+ " service method is static", method.getName(),
InstanceRequest.class.getCanonicalName());
} else if (requireStaticMethodsForRequestType && !isInstance
&& !found.isDeclaredStatic()) {
logger.poison("The method %s is declared to return %s, but the"
+ " service method is not static", method.getName(),
Request.class.getCanonicalName());
}
}
}
/**
* Check that the domain object has getId()
and
* getVersion
methods.
*/
private void checkIdAndVersion(ErrorContext logger, Type domainType) {
if (objectType.equals(domainType)) {
return;
}
logger = logger.setType(domainType);
String findMethodName = "find"
+ BinaryName.getShortClassName(domainType.getClassName());
Type keyType = null;
RFMethod findMethod = null;
boolean foundFind = false;
boolean foundId = false;
boolean foundVersion = false;
for (RFMethod method : getMethodsInHierarchy(logger, domainType)) {
if ("getId".equals(method.getName())
&& method.getArgumentTypes().length == 0) {
foundId = true;
keyType = method.getReturnType();
if (!isResolvedKeyType(logger, keyType)) {
unresolvedKeyTypes.put(domainType, keyType);
}
} else if ("getVersion".equals(method.getName())
&& method.getArgumentTypes().length == 0) {
foundVersion = true;
if (!isResolvedKeyType(logger, method.getReturnType())) {
unresolvedKeyTypes.put(domainType, method.getReturnType());
}
} else if (findMethodName.equals(method.getName())
&& method.getArgumentTypes().length == 1) {
foundFind = true;
findMethod = method;
}
if (foundFind && foundId && foundVersion) {
break;
}
}
if (!foundId) {
logger.poison("There is no getId() method in type %s", print(domainType));
}
if (!foundVersion) {
logger.poison("There is no getVersion() method in type %s",
print(domainType));
}
if (foundFind) {
if (keyType != null
&& !isAssignable(logger, findMethod.getArgumentTypes()[0], keyType)) {
logger.poison("The key type returned by %s getId()"
+ " cannot be used as the argument to %s(%s)", print(keyType),
findMethod.getName(), print(findMethod.getArgumentTypes()[0]));
}
} else {
logger.poison("There is no %s method in type %s that returns %2$s",
findMethodName, print(domainType));
}
}
/**
* Ensure that the given property method on an EntityProxy exists on the
* domain object.
*/
private void checkPropertyMethod(ErrorContext logger,
Method clientPropertyMethod, Type domainType) {
logger = logger.setMethod(clientPropertyMethod);
findCompatiblePropertyMethod(logger, domainType,
createDomainMethod(logger, clientPropertyMethod));
}
private void checkUnresolvedKeyTypes(ErrorContext logger) {
unresolvedKeyTypes.values().removeAll(domainToClientType.keySet());
if (unresolvedKeyTypes.isEmpty()) {
return;
}
for (Map.Entry type : unresolvedKeyTypes.entrySet()) {
logger.setType(type.getKey()).poison(
"The domain type %s uses a non-simple key type (%s)"
+ " in its getId() or getVersion() method that"
+ " does not have a proxy mapping.", print(type.getKey()),
print(type.getValue()));
}
}
/**
* Convert a method declaration using client types (e.g. FooProxy) to domain
* types (e.g. Foo).
*/
private Method createDomainMethod(ErrorContext logger, Method clientMethod) {
Type[] args = clientMethod.getArgumentTypes();
for (int i = 0, j = args.length; i < j; i++) {
args[i] = getDomainType(logger, args[i]);
}
Type returnType = getDomainType(logger, clientMethod.getReturnType());
return new Method(clientMethod.getName(), returnType, args);
}
/**
* Common checks to quickly determine if a type needs to be checked.
*/
private boolean fastFail(String binaryName) {
if (!Name.isBinaryName(binaryName)) {
parentLogger.poison("%s is not a binary name", binaryName);
return true;
}
// Allow the poisoned flag to be reset without losing data
if (badTypes.contains(binaryName)) {
parentLogger.poison("Type type %s was previously marked as bad",
binaryName);
return true;
}
// Don't revalidate the same type
if (!validatedTypes.add(binaryName)) {
return true;
}
return false;
}
/**
* Finds a compatible method declaration in domainType
'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> domainLookup = new LinkedHashMap>();
for (RFMethod method : getMethodsInHierarchy(logger, domainType)) {
List list = domainLookup.get(method.getName());
if (list == null) {
list = new ArrayList();
domainLookup.put(method.getName(), list);
}
list.add(method);
}
// Find the matching method in the domain object
List 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, 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 domainType
's
* hierarchy that is assignment-compatible with the given Method.
*/
private RFMethod findCompatiblePropertyMethod(final ErrorContext logger,
Type domainType, Method searchFor) {
return findCompatibleMethod(logger, domainType, searchFor, true, false,
false);
}
/**
* Finds a compatible method declaration in domainType
's
* hierarchy that is assignment-compatible with the given Method.
*/
private RFMethod findCompatibleServiceMethod(final ErrorContext logger,
Type domainType, Method searchFor) {
return findCompatibleMethod(logger, domainType, searchFor, true, false,
true);
}
/**
* This looks like it should be a utility method somewhere else, but I can't
* find it.
*/
private Type getBoxedType(Type primitive) {
switch (primitive.getSort()) {
case Type.BOOLEAN:
return Type.getType(Boolean.class);
case Type.BYTE:
return Type.getType(Byte.class);
case Type.CHAR:
return Type.getType(Character.class);
case Type.DOUBLE:
return Type.getType(Double.class);
case Type.FLOAT:
return Type.getType(Float.class);
case Type.INT:
return Type.getType(Integer.class);
case Type.LONG:
return Type.getType(Long.class);
case Type.SHORT:
return Type.getType(Short.class);
case Type.VOID:
return Type.getType(Void.class);
}
throw new RuntimeException(primitive.getDescriptor()
+ " is not a primitive type");
}
/**
* Convert the type used in a client-side EntityProxy or RequestContext
* declaration to the equivalent domain type. Value types and supported
* collections are a pass-through. EntityProxy types will be resolved to their
* domain object type. RequestContext types will be resolved to their service
* object.
*/
private Type getDomainType(ErrorContext logger, Type clientType) {
Type domainType = clientToDomainType.get(clientType);
if (domainType != null) {
return domainType;
}
if (isValueType(logger, clientType) || isCollectionType(logger, clientType)) {
domainType = clientType;
} else if (entityProxyIntf.equals(clientType)
|| valueProxyIntf.equals(clientType)) {
domainType = objectType;
} else {
logger = logger.setType(clientType);
DomainMapper pv = new DomainMapper(logger);
visit(logger, clientType.getInternalName(), pv);
if (pv.getDomainInternalName() == null) {
logger.poison("%s has no mapping to a domain type (e.g. @%s or @%s)",
print(clientType), ProxyFor.class.getSimpleName(),
Service.class.getSimpleName());
domainType = errorType;
} else {
domainType = Type.getObjectType(pv.getDomainInternalName());
}
if (pv.getLocatorInternalName() != null) {
Type locatorType = Type.getObjectType(pv.getLocatorInternalName());
clientToLocatorMap.put(clientType, locatorType);
}
}
addToDomainMap(logger, domainType, clientType);
maybeCheckProxyType(logger, clientType);
return domainType;
}
/**
* Collect all of the methods defined within a type hierarchy.
*/
private Set getMethodsInHierarchy(ErrorContext logger,
Type domainType) {
Set 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 getSupertypes(ErrorContext logger, Type type) {
if (type.getSort() != Type.OBJECT) {
return Collections.emptyList();
}
List 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 allSupertypes = getSupertypes(logger, possibleSubtype);
return allSupertypes.contains(possibleSupertype);
}
private boolean isAssignable(ErrorContext logger, Type[] possibleSupertypes,
Type[] possibleSubtypes) {
// Check the same number of types
if (possibleSupertypes.length != possibleSubtypes.length) {
return false;
}
for (int i = 0, j = possibleSupertypes.length; i < j; i++) {
if (!isAssignable(logger, possibleSupertypes[i], possibleSubtypes[i])) {
return false;
}
}
return true;
}
private boolean isCollectionType(
@SuppressWarnings("unused") ErrorContext logger, Type type) {
// keeping the logger arg just for internal consistency for our small minds
return "java/util/List".equals(type.getInternalName())
|| "java/util/Set".equals(type.getInternalName());
}
/**
* Keep in sync with {@code ReflectiveServiceLayer.isKeyType()}.
*/
private boolean isResolvedKeyType(ErrorContext logger, Type type) {
if (isValueType(logger, type)) {
return true;
}
// We have already seen a mapping for the key type
if (domainToClientType.containsKey(type)) {
return true;
}
return false;
}
private boolean isValueType(ErrorContext logger, Type type) {
if (type.getSort() != Type.OBJECT) {
return true;
}
if (valueTypes.contains(type)) {
return true;
}
logger = logger.setType(type);
if (isAssignable(logger, enumType, type)) {
return true;
}
return false;
}
private Type maybeBoxType(Type maybePrimitive) {
if (maybePrimitive.getSort() == Type.OBJECT) {
return maybePrimitive;
}
return getBoxedType(maybePrimitive);
}
/**
* Examine an array of Types and call {@link #validateEntityProxy(String)} or
* {@link #validateValueProxy(String)} if the type is a proxy.
*/
private void maybeCheckProxyType(ErrorContext logger, Type... types) {
for (Type type : types) {
if (isAssignable(logger, entityProxyIntf, type)) {
validateEntityProxy(type.getClassName());
} else if (isAssignable(logger, valueProxyIntf, type)) {
validateValueProxy(type.getClassName());
} else if (isAssignable(logger, baseProxyIntf, type)) {
logger.poison(
"Invalid type hierarchy for %s. Only types derived from %s or %s may be used.",
print(type), print(entityProxyIntf), print(valueProxyIntf));
}
}
}
/**
* Examine the arguments and return value of a method and check any
* EntityProxies referred.
*/
private void maybeCheckReferredProxies(ErrorContext logger, RFMethod method) {
if (method.getSignature() != null) {
TypesInSignatureCollector collector = new TypesInSignatureCollector();
SignatureReader reader = new SignatureReader(method.getSignature());
reader.accept(collector);
maybeCheckProxyType(logger, collector.getFound());
} else {
Type[] argTypes = method.getArgumentTypes();
Type returnType = getReturnType(logger, method);
// Check EntityProxy args ond return types against the domain
maybeCheckProxyType(logger, argTypes);
maybeCheckProxyType(logger, returnType);
}
}
private void validateProxy(String binaryName, Type expectedType,
boolean requireId) {
if (fastFail(binaryName)) {
return;
}
Type proxyType = Type.getObjectType(BinaryName.toInternalName(binaryName));
ErrorContext logger = parentLogger.setType(proxyType);
// Quick sanity check for calling code
if (!isAssignable(logger, expectedType, proxyType)) {
parentLogger.poison("%s is not a %s", print(proxyType),
print(expectedType));
return;
}
// Find the domain type
Type domainType = getDomainType(logger, proxyType);
if (domainType == errorType) {
logger.poison(
"The type %s must be annotated with a @%s or @%s annotation",
BinaryName.toSourceName(binaryName), ProxyFor.class.getSimpleName(),
ProxyForName.class.getSimpleName());
return;
}
// Check for getId() and getVersion() in domain if no locator is specified
if (requireId) {
Type locatorType = clientToLocatorMap.get(proxyType);
if (locatorType == null) {
checkIdAndVersion(logger, domainType);
}
}
// Collect all methods in the client proxy type
Set clientPropertyMethods = getMethodsInHierarchy(logger,
proxyType);
// Find the equivalent domain getter/setter method
for (RFMethod clientPropertyMethod : clientPropertyMethods) {
// Ignore stableId(). Can't use descriptor due to overrides
if ("stableId".equals(clientPropertyMethod.getName())
&& clientPropertyMethod.getArgumentTypes().length == 0) {
continue;
}
checkPropertyMethod(logger, clientPropertyMethod, domainType);
maybeCheckReferredProxies(logger, clientPropertyMethod);
}
}
/**
* Load the classfile for the given binary name and apply the provided
* visitor.
*
* @return true
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;
}
}