Reimplement RequestFactory client vs. domain validation using classfile analysis to support better DevMode refresh semantics.
Add ProxyForName and ServiceName annotations to handle the case where the GWT compiler does not have access to the domain objects.
Patch by: bobv
Review by: rice
Review at http://gwt-code-reviews.appspot.com/1049801
git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@9137 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/servlet/build.xml b/servlet/build.xml
index 5a90138..e30dace 100755
--- a/servlet/build.xml
+++ b/servlet/build.xml
@@ -4,7 +4,9 @@
<import file="${gwt.root}/common.ant.xml" />
<property.ensure name="gwt.user.root" location="${gwt.root}/user" />
+ <property.ensure name="gwt.dev.build" location="${gwt.build.out}/dev" />
<property.ensure name="gwt.user.build" location="${gwt.build.out}/user" />
+ <property.ensure name="gwt.dev.bin" location="${gwt.dev.build}/bin" />
<property.ensure name="gwt.user.bin" location="${gwt.user.build}/bin" />
<property name="project.lib.deps"
@@ -21,6 +23,10 @@
<target name="-servlet" description="Packages this project into a jar">
<mkdir dir="${gwt.build.lib}" />
<gwt.jar>
+ <fileset dir="${gwt.dev.bin}">
+ <include name="com/google/gwt/dev/asm/**" />
+ <include name="com/google/gwt/dev/util/Name*.class" />
+ </fileset>
<fileset dir="${gwt.user.bin}">
<exclude name="**/rebind/**" />
<exclude name="**/tools/**" />
diff --git a/user/src/com/google/gwt/requestfactory/rebind/model/ContextMethod.java b/user/src/com/google/gwt/requestfactory/rebind/model/ContextMethod.java
index be744e4..9922620 100644
--- a/user/src/com/google/gwt/requestfactory/rebind/model/ContextMethod.java
+++ b/user/src/com/google/gwt/requestfactory/rebind/model/ContextMethod.java
@@ -52,17 +52,12 @@
public void setRequestMethods(List<RequestMethod> requestMethods) {
toReturn.requestMethods = requestMethods;
}
-
- public void setServiceClass(Class<?> serviceClass) {
- toReturn.serviceClass = serviceClass;
- }
}
private String interfaceName;
private String methodName;
private String packageName;
private List<RequestMethod> requestMethods;
- private Class<?> serviceClass;
private String simpleSourceName;
private ContextMethod() {
@@ -95,10 +90,6 @@
return Collections.unmodifiableList(requestMethods);
}
- public Class<?> getServiceClass() {
- return serviceClass;
- }
-
public String getSimpleSourceName() {
return simpleSourceName;
}
diff --git a/user/src/com/google/gwt/requestfactory/rebind/model/RequestFactoryModel.java b/user/src/com/google/gwt/requestfactory/rebind/model/RequestFactoryModel.java
index 6684536..be2eb96 100644
--- a/user/src/com/google/gwt/requestfactory/rebind/model/RequestFactoryModel.java
+++ b/user/src/com/google/gwt/requestfactory/rebind/model/RequestFactoryModel.java
@@ -28,13 +28,13 @@
import com.google.gwt.requestfactory.shared.EntityProxy;
import com.google.gwt.requestfactory.shared.InstanceRequest;
import com.google.gwt.requestfactory.shared.ProxyFor;
+import com.google.gwt.requestfactory.shared.ProxyForName;
import com.google.gwt.requestfactory.shared.Request;
import com.google.gwt.requestfactory.shared.RequestContext;
import com.google.gwt.requestfactory.shared.RequestFactory;
import com.google.gwt.requestfactory.shared.Service;
+import com.google.gwt.requestfactory.shared.ServiceName;
-import java.lang.reflect.Method;
-import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
@@ -159,13 +159,12 @@
private void buildContextMethod(ContextMethod.Builder contextBuilder,
JClassType contextType) throws UnableToCompleteException {
Service serviceAnnotation = contextType.getAnnotation(Service.class);
- if (serviceAnnotation == null) {
+ ServiceName serviceNameAnnotation = contextType.getAnnotation(ServiceName.class);
+ if (serviceAnnotation == null && serviceNameAnnotation == null) {
poison("RequestContext subtype %s is missing a @%s annotation",
contextType.getQualifiedSourceName(), Service.class.getSimpleName());
return;
}
- Class<?> serviceClass = serviceAnnotation.value();
- contextBuilder.setServiceClass(serviceClass);
List<RequestMethod> requestMethods = new ArrayList<RequestMethod>();
for (JMethod method : contextType.getInheritableMethods()) {
@@ -177,8 +176,7 @@
RequestMethod.Builder methodBuilder = new RequestMethod.Builder();
methodBuilder.setDeclarationMethod(method);
- if (!validateContextMethodAndSetDataType(methodBuilder, method,
- serviceClass)) {
+ if (!validateContextMethodAndSetDataType(methodBuilder, method)) {
continue;
}
@@ -193,20 +191,6 @@
throw new UnableToCompleteException();
}
- /**
- * Returns a list of public methods that match the given methodName.
- */
- private List<Method> findMethods(Class<?> domainType, String methodName) {
- List<Method> toReturn = new ArrayList<Method>();
- for (Method method : domainType.getMethods()) {
- if (methodName.equals(method.getName())
- && (method.getModifiers() & Modifier.PUBLIC) != 0) {
- toReturn.add(method);
- }
- }
- return toReturn;
- }
-
private EntityProxyModel getEntityProxyType(JClassType entityProxyType)
throws UnableToCompleteException {
entityProxyType = ModelUtils.ensureBaseType(entityProxyType);
@@ -225,16 +209,13 @@
// Get the server domain object type
ProxyFor proxyFor = entityProxyType.getAnnotation(ProxyFor.class);
- if (proxyFor == null) {
- poison("The %s type does not have a @%s annotation",
+ ProxyForName proxyForName = entityProxyType.getAnnotation(ProxyForName.class);
+ if (proxyFor == null && proxyForName == null) {
+ poison("The %s type does not have a @%s or @%s annotation",
entityProxyType.getQualifiedSourceName(),
- ProxyFor.class.getSimpleName());
+ ProxyFor.class.getSimpleName(), ProxyForName.class.getSimpleName());
// early exit, because further processing causes NPEs in numerous spots
die(poisonedMessage());
- } else {
- Class<?> domainType = proxyFor.value();
- builder.setProxyFor(domainType);
- validateDomainType(domainType);
}
// Look at the methods declared on the EntityProxy
@@ -267,9 +248,7 @@
}
validateTransportableType(methodBuilder, transportedType, false);
RequestMethod requestMethod = methodBuilder.build();
- if (validateDomainBeanMethod(requestMethod, builder)) {
- requestMethods.add(requestMethod);
- }
+ requestMethods.add(requestMethod);
}
builder.setRequestMethods(requestMethods);
@@ -280,20 +259,6 @@
return toReturn;
}
- private boolean isStatic(Method domainMethod) {
- return (domainMethod.getModifiers() & Modifier.STATIC) != 0;
- }
-
- private String methodLocation(Method domainMethod) {
- return domainMethod.getDeclaringClass().getName() + "."
- + domainMethod.getName();
- }
-
- private String methodLocation(JMethod proxyMethod) {
- return proxyMethod.getEnclosingType().getName() + "."
- + proxyMethod.getName();
- }
-
private void poison(String message, Object... args) {
logger.log(TreeLogger.ERROR, String.format(message, args));
poisoned = true;
@@ -303,7 +268,7 @@
* Examine a RequestContext method to see if it returns a transportable type.
*/
private boolean validateContextMethodAndSetDataType(
- RequestMethod.Builder methodBuilder, JMethod method, Class<?> serviceClass)
+ RequestMethod.Builder methodBuilder, JMethod method)
throws UnableToCompleteException {
JClassType requestReturnType = method.getReturnType().isInterface();
JClassType invocationReturnType;
@@ -314,40 +279,13 @@
return false;
}
- /*
- * TODO: bad assumption Implicit assumption is that the Service and ProxyFor
- * classes are the same. This is because an instance method should
- * technically be looked up on the class that is the instance parameter, and
- * not on the serviceClass, which consists of static service methods. Can't
- * be fixed until it is fixed in JsonRequestProcessor.
- */
- Method domainMethod = validateExistsAndNotOverriden(method, serviceClass,
- false);
- if (domainMethod == null) {
- return false;
- }
-
if (instanceRequestInterface.isAssignableFrom(requestReturnType)) {
- if (isStatic(domainMethod)) {
- poison("Method %s.%s is an instance method, "
- + "while the corresponding method on %s is static",
- method.getEnclosingType().getName(), method.getName(),
- serviceClass.getName());
- return false;
- }
// Instance method invocation
JClassType[] params = ModelUtils.findParameterizationOf(
instanceRequestInterface, requestReturnType);
methodBuilder.setInstanceType(getEntityProxyType(params[0]));
invocationReturnType = params[1];
} else if (requestInterface.isAssignableFrom(requestReturnType)) {
- if (!isStatic(domainMethod)) {
- poison("Method %s.%s is a static method, "
- + "while the corresponding method on %s is not",
- method.getEnclosingType().getName(), method.getName(),
- serviceClass.getName());
- return false;
- }
// Static method invocation
JClassType[] params = ModelUtils.findParameterizationOf(requestInterface,
requestReturnType);
@@ -363,152 +301,14 @@
// Validate the parameters
boolean paramsOk = true;
JParameter[] params = method.getParameters();
- Class<?>[] domainParams = domainMethod.getParameterTypes();
- if (params.length != domainParams.length) {
- poison("Method %s.%s parameters do not match same method on %s",
- method.getEnclosingType().getName(), method.getName(),
- serviceClass.getName());
- }
for (int i = 0; i < params.length; ++i) {
JParameter param = params[i];
- Class<?> domainParam = domainParams[i];
paramsOk = validateTransportableType(new RequestMethod.Builder(),
param.getType(), false)
&& paramsOk;
- paramsOk = validateProxyAndDomainTypeEquals(param.getType(), domainParam,
- i, methodLocation(method), methodLocation(domainMethod)) && paramsOk;
}
- return validateTransportableType(methodBuilder, invocationReturnType, true)
- && validateProxyAndDomainTypeEquals(invocationReturnType,
- domainMethod.getReturnType(), -1, methodLocation(method),
- methodLocation(domainMethod)) && paramsOk;
- }
-
- /**
- * Examine a domain method to see if it matches the proxy method.
- */
- private boolean validateDomainBeanMethod(RequestMethod requestMethod,
- EntityProxyModel.Builder entityBuilder) throws UnableToCompleteException {
- JMethod proxyMethod = requestMethod.getDeclarationMethod();
- // check if method exists on domain object
- Class<?> domainType = entityBuilder.peek().getProxyFor();
- Method domainMethod = validateExistsAndNotOverriden(proxyMethod,
- domainType, true);
- if (domainMethod == null) {
- return false;
- }
-
- boolean isGetter = proxyMethod.getName().startsWith("get");
- if (isGetter) {
- // compare return type of domain to proxy return type
- String returnTypeName = domainMethod.getReturnType().getName();
- // isEntityType() returns true for collections, but we want the Collection
- String propertyTypeName = requestMethod.isCollectionType()
- || requestMethod.isValueType()
- ? requestMethod.getDataType().getQualifiedBinaryName()
- : requestMethod.getEntityType().getProxyFor().getName();
- if (!returnTypeName.equals(propertyTypeName)) {
- poison("Method %s.%s return type %s does not match return type %s "
- + " of method %s.%s", domainType.getName(), domainMethod.getName(),
- returnTypeName, propertyTypeName,
- proxyMethod.getEnclosingType().getName(), proxyMethod.getName());
- return false;
- }
- }
- JParameter[] proxyParams = proxyMethod.getParameters();
- Class<?>[] domainParams = domainMethod.getParameterTypes();
- if (proxyParams.length != domainParams.length) {
- poison("Method %s.%s parameter mismatch with %s.%s",
- proxyMethod.getEnclosingType().getName(), proxyMethod.getName(),
- domainType.getName(), domainMethod.getName());
- return false;
- }
- for (int i = 0; i < proxyParams.length; i++) {
- JType proxyParam = proxyParams[i].getType();
- Class<?> domainParam = domainParams[i];
- if (!validateProxyAndDomainTypeEquals(proxyParam, domainParam, i,
- methodLocation(proxyMethod), methodLocation(domainMethod))) {
- poison("Parameter %d of %s.%s doesn't match method %s.%s", i,
- proxyMethod.getEnclosingType().getName(), proxyMethod.getName(),
- domainType.getName(), domainMethod.getName());
- return false;
- }
- }
- return true;
- }
-
- /**
- * Examine a domain type and see if it includes a getId() method.
- */
- private boolean validateDomainType(Class<?> domainType) {
- try {
- domainType.getMethod("getId");
- } catch (NoSuchMethodException e) {
- poison("The class %s is missing method getId()", domainType.getName());
- return false;
- }
- try {
- domainType.getMethod("getVersion");
- } catch (NoSuchMethodException e) {
- poison("The class %s is missing method getVersion()",
- domainType.getName());
- return false;
- }
- return true;
- }
-
- private Method validateExistsAndNotOverriden(JMethod clientMethod,
- Class<?> serverType, boolean isGetterOrSetter) {
- List<Method> domainMethods = findMethods(serverType, clientMethod.getName());
- if (domainMethods.size() == 0) {
- poison("Method %s.%s has no corresponding public method on %s",
- clientMethod.getEnclosingType().getQualifiedBinaryName(),
- clientMethod.getName(), serverType.getName());
- return null;
- }
- if (domainMethods.size() > 1) {
- poison("Method %s.%s is overloaded on %s",
- clientMethod.getEnclosingType().getQualifiedBinaryName(),
- clientMethod.getName(), serverType.getName());
- return null;
- }
- Method domainMethod = domainMethods.get(0);
- if (isGetterOrSetter && isStatic(domainMethod)) {
- poison("Method %s.%s is declared static", serverType.getName(),
- domainMethod.getName());
- return null;
- }
- return domainMethod;
- }
-
- /**
- * Compare type from Proxy and Domain.
- */
- private boolean validateProxyAndDomainTypeEquals(JType proxyType,
- Class<?> domainType, int paramNumber, String clientMethod,
- String serverMethod) throws UnableToCompleteException {
- boolean matchOk = false;
- if (ModelUtils.isValueType(oracle, proxyType)
- || collectionInterface.isAssignableFrom(proxyType.isClassOrInterface())) {
- // allow int to match int or Integer
- matchOk = proxyType.getQualifiedSourceName().equals(
- ModelUtils.maybeAutobox(domainType).getName())
- || proxyType.getQualifiedSourceName().equals(domainType.getName());
- } else {
- matchOk = getEntityProxyType(proxyType.isClassOrInterface()).getProxyFor().equals(
- domainType);
- }
- if (!matchOk) {
- if (paramNumber < 0) {
- poison("Return type of method %s does not match method %s",
- clientMethod, serverMethod);
- } else {
- poison("Parameter %d of method %s does not match method %s",
- paramNumber, clientMethod, serverMethod);
- }
- }
- return matchOk;
+ return validateTransportableType(methodBuilder, invocationReturnType, true);
}
/**
diff --git a/user/src/com/google/gwt/requestfactory/server/DefaultSecurityProvider.java b/user/src/com/google/gwt/requestfactory/server/DefaultSecurityProvider.java
index 7b60641..36ac70a 100644
--- a/user/src/com/google/gwt/requestfactory/server/DefaultSecurityProvider.java
+++ b/user/src/com/google/gwt/requestfactory/server/DefaultSecurityProvider.java
@@ -1,12 +1,12 @@
/*
* 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
@@ -16,6 +16,7 @@
package com.google.gwt.requestfactory.server;
import com.google.gwt.requestfactory.shared.Service;
+import com.google.gwt.requestfactory.shared.ServiceName;
/**
* A security provider that enforces
@@ -25,16 +26,21 @@
public void checkClass(Class<?> clazz) throws SecurityException {
Service service = clazz.getAnnotation(Service.class);
- if (service == null) {
- throw new SecurityException(
- "Class " + clazz.getName() + " does not have a @Service annotation.");
+ ServiceName serviceName = clazz.getAnnotation(ServiceName.class);
+ String className;
+ if (service != null) {
+ className = service.value().getName();
+ } else if (serviceName != null) {
+ className = serviceName.value();
+ } else {
+ throw new SecurityException("Class " + clazz.getName()
+ + " does not have a @Service annotation.");
}
try {
- Class.forName(service.value().getCanonicalName());
+ Class.forName(className);
} catch (ClassNotFoundException e) {
- throw new SecurityException(
- "Class " + service.value() + " from @Service annotation on " + clazz
- + " cannot be loaded.");
+ throw new SecurityException("Class " + className
+ + " from @Service annotation on " + clazz + " cannot be loaded.");
}
}
diff --git a/user/src/com/google/gwt/requestfactory/server/JsonRequestProcessor.java b/user/src/com/google/gwt/requestfactory/server/JsonRequestProcessor.java
index 328e8a9..27e8e93 100644
--- a/user/src/com/google/gwt/requestfactory/server/JsonRequestProcessor.java
+++ b/user/src/com/google/gwt/requestfactory/server/JsonRequestProcessor.java
@@ -18,6 +18,7 @@
import com.google.gwt.requestfactory.shared.EntityProxy;
import com.google.gwt.requestfactory.shared.EntityProxyId;
import com.google.gwt.requestfactory.shared.ProxyFor;
+import com.google.gwt.requestfactory.shared.ProxyForName;
import com.google.gwt.requestfactory.shared.ServerFailure;
import com.google.gwt.requestfactory.shared.WriteOperation;
import com.google.gwt.requestfactory.shared.impl.CollectionProperty;
@@ -162,7 +163,7 @@
/**
* Decodes a String encoded as web-safe base64.
- *
+ *
* @param encoded the encoded String
* @return a decoded String
*/
@@ -184,7 +185,7 @@
/**
* Encodes a String as web-safe base64.
- *
+ *
* @param decoded the decoded String
* @return an encoded String
*/
@@ -370,8 +371,9 @@
* 2. Merge the following and the object resolution code in getEntityKey.
* 3. Update the involvedKeys set.
*/
- ProxyFor service = parameterType.getAnnotation(ProxyFor.class);
- if (service != null) {
+ ProxyFor proxyFor = parameterType.getAnnotation(ProxyFor.class);
+ ProxyForName proxyForName = parameterType.getAnnotation(ProxyForName.class);
+ if (proxyFor != null || proxyForName != null) {
EntityKey entityKey = getEntityKey(parameterValue.toString());
DvsData dvsData = dvsDataMap.get(entityKey);
@@ -387,8 +389,9 @@
}
if (EntityProxyId.class.isAssignableFrom(parameterType)) {
EntityKey entityKey = getEntityKey(parameterValue.toString());
- ProxyFor service = entityKey.proxyType.getAnnotation(ProxyFor.class);
- if (service == null) {
+ ProxyFor proxyFor = entityKey.proxyType.getAnnotation(ProxyFor.class);
+ ProxyForName proxyForName = entityKey.proxyType.getAnnotation(ProxyForName.class);
+ if (proxyFor == null && proxyForName == null) {
throw new IllegalArgumentException("Unknown service, unable to decode "
+ parameterValue);
}
@@ -401,7 +404,7 @@
/**
* Encode a property value to be sent across the wire.
- *
+ *
* @param value a value Object
* @return an encoded value Object
*/
@@ -604,6 +607,16 @@
if (dtoAnn != null) {
return (Class<Object>) dtoAnn.value();
}
+ ProxyForName name = record.getAnnotation(ProxyForName.class);
+ if (name != null) {
+ try {
+ return (Class<Object>) Class.forName(name.value(), false,
+ Thread.currentThread().getContextClassLoader());
+ } catch (ClassNotFoundException e) {
+ throw new IllegalArgumentException("Could not find ProxyForName class",
+ e);
+ }
+ }
throw new IllegalArgumentException("Proxy class " + record.getName()
+ " missing ProxyFor annotation");
}
@@ -866,6 +879,21 @@
operation = getOperation(operationName);
Class<?> domainClass = Class.forName(operation.getDomainClassName());
+
+ /*
+ * The use of JRP's classloader mirrors the use of Class.forName elsewhere.
+ * It's wrong, but it's consistently wrong.
+ */
+ RequestFactoryInterfaceValidator validator = new RequestFactoryInterfaceValidator(
+ log, new RequestFactoryInterfaceValidator.ClassLoaderLoader(
+ JsonRequestProcessor.class.getClassLoader()));
+ validator.validateRequestContext(operationName.substring(0,
+ operationName.indexOf("::")));
+ if (validator.isPoisoned()) {
+ log.severe("Unable to validate RequestContext");
+ throw new RuntimeException();
+ }
+
Method domainMethod = domainClass.getMethod(
operation.getDomainMethod().getName(), operation.getParameterTypes());
if (Modifier.isStatic(domainMethod.getModifiers()) == operation.isInstance()) {
@@ -1597,6 +1625,14 @@
if (pFor != null) {
fieldType = pFor.value();
}
+ ProxyForName pFN = fieldType.getAnnotation(ProxyForName.class);
+ if (pFN != null) {
+ try {
+ fieldType = Class.forName(pFN.value(), false,
+ Thread.currentThread().getContextClassLoader());
+ } catch (ClassNotFoundException ignored) {
+ }
+ }
// TODO: remove override declared method return type with field type
if (!fieldType.equals(field.getType())) {
fieldType = field.getType();
diff --git a/user/src/com/google/gwt/requestfactory/server/ReflectionBasedOperationRegistry.java b/user/src/com/google/gwt/requestfactory/server/ReflectionBasedOperationRegistry.java
index 874643d..29daac2 100644
--- a/user/src/com/google/gwt/requestfactory/server/ReflectionBasedOperationRegistry.java
+++ b/user/src/com/google/gwt/requestfactory/server/ReflectionBasedOperationRegistry.java
@@ -1,12 +1,12 @@
/*
* 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
@@ -18,8 +18,10 @@
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.Service;
+import com.google.gwt.requestfactory.shared.ServiceName;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
@@ -30,13 +32,12 @@
/**
* OperationRegistry which uses the operation name as a convention for
- * reflection to a method on a class, and returns an appropriate {@link
- * com.google.gwt.requestfactory.server.RequestDefinition}.
+ * reflection to a method on a class, and returns an appropriate
+ * {@link com.google.gwt.requestfactory.server.RequestDefinition}.
*/
public class ReflectionBasedOperationRegistry implements OperationRegistry {
- class ReflectiveRequestDefinition
- implements RequestDefinition {
+ class ReflectiveRequestDefinition implements RequestDefinition {
private Class<?> requestClass;
@@ -49,7 +50,8 @@
private boolean isInstance;
public ReflectiveRequestDefinition(Class<?> requestClass,
- Method requestMethod, Class<?> domainClass, Method domainMethod, boolean isInstance) {
+ Method requestMethod, Class<?> domainClass, Method domainMethod,
+ boolean isInstance) {
this.requestClass = requestClass;
this.requestMethod = requestMethod;
this.domainClass = domainClass;
@@ -94,20 +96,29 @@
Class<?> requestReturnType = getReturnTypeFromParameter(requestMethod,
requestMethod.getGenericReturnType());
if (EntityProxy.class.isAssignableFrom(requestReturnType)) {
- ProxyFor annotation =
- requestReturnType.getAnnotation(ProxyFor.class);
+ ProxyFor annotation = requestReturnType.getAnnotation(ProxyFor.class);
+ ProxyForName nameAnnotation = requestReturnType.getAnnotation(ProxyForName.class);
+
+ Class<?> dtoClass = null;
if (annotation != null) {
- Class<?> dtoClass = annotation.value();
- if (!dtoClass.equals(domainReturnType)) {
+ dtoClass = annotation.value();
+ } else if (nameAnnotation != null) {
+ try {
+ dtoClass = Class.forName(nameAnnotation.value(), false,
+ Thread.currentThread().getContextClassLoader());
+ } catch (ClassNotFoundException e) {
throw new IllegalArgumentException(
- "Type mismatch between " + domainMethod + " return type, and "
- + requestReturnType + "'s ProxyFor annotation "
- + dtoClass);
+ "Unknown type specified in ProxyForName annotation", e);
}
} else {
throw new IllegalArgumentException(
"Missing ProxyFor annotation on proxy type " + requestReturnType);
}
+ if (!dtoClass.equals(domainReturnType)) {
+ throw new IllegalArgumentException("Type mismatch between "
+ + domainMethod + " return type, and " + requestReturnType
+ + "'s ProxyFor annotation " + dtoClass);
+ }
return requestReturnType;
}
// primitive ?
@@ -172,7 +183,7 @@
} else {
return null;
}
-
+
if (toExamine instanceof ParameterizedType) {
// if type is for example, RequestObject<List<T>> we return T
return getTypeArgument((ParameterizedType) toExamine);
@@ -190,9 +201,9 @@
private RequestSecurityProvider securityProvider;
/**
- * Constructs a {@link ReflectionBasedOperationRegistry} instance
- * with a given {@link RequestSecurityProvider}.
- *
+ * Constructs a {@link ReflectionBasedOperationRegistry} instance with a given
+ * {@link RequestSecurityProvider}.
+ *
* @param securityProvider a {@link RequestSecurityProvider} instance.
*/
public ReflectionBasedOperationRegistry(
@@ -219,25 +230,34 @@
this.getClass().getClassLoader());
securityProvider.checkClass(requestClass);
Service domainClassAnnotation = requestClass.getAnnotation(Service.class);
+ ServiceName domainClassNameAnnotation = requestClass.getAnnotation(ServiceName.class);
+ Class<?> domainClass;
if (domainClassAnnotation != null) {
- Class<?> domainClass = domainClassAnnotation.value();
- Method requestMethod = findMethod(requestClass, domainMethodName);
- Method domainMethod = findMethod(domainClass, domainMethodName);
- if (requestMethod != null && domainMethod != null) {
- boolean isInstance = InstanceRequest.class.isAssignableFrom(requestMethod.getReturnType());
- if (isInstance == Modifier.isStatic(domainMethod.getModifiers())) {
- throw new IllegalArgumentException("domain method " + domainMethod
- + " and interface method " + requestMethod
- + " don't match wrt instance/static");
- }
- return new ReflectiveRequestDefinition(requestClass, requestMethod,
- domainClass, domainMethod, isInstance);
- }
+ domainClass = domainClassAnnotation.value();
+ } else if (domainClassNameAnnotation != null) {
+ domainClass = Class.forName(domainClassNameAnnotation.value(), false,
+ Thread.currentThread().getContextClassLoader());
+ } else {
+ return null;
}
+
+ Method requestMethod = findMethod(requestClass, domainMethodName);
+ Method domainMethod = findMethod(domainClass, domainMethodName);
+ if (requestMethod != null && domainMethod != null) {
+ boolean isInstance = InstanceRequest.class.isAssignableFrom(requestMethod.getReturnType());
+ if (isInstance == Modifier.isStatic(domainMethod.getModifiers())) {
+ throw new IllegalArgumentException("domain method " + domainMethod
+ + " and interface method " + requestMethod
+ + " don't match wrt instance/static");
+ }
+ return new ReflectiveRequestDefinition(requestClass, requestMethod,
+ domainClass, domainMethod, isInstance);
+ }
+
return null;
} catch (ClassNotFoundException e) {
- throw new SecurityException(
- "Access to non-existent class " + reqClassName);
+ throw new SecurityException("Access to non-existent class "
+ + reqClassName);
}
}
diff --git a/user/src/com/google/gwt/requestfactory/server/RequestFactoryInterfaceValidator.java b/user/src/com/google/gwt/requestfactory/server/RequestFactoryInterfaceValidator.java
new file mode 100644
index 0000000..4903bea
--- /dev/null
+++ b/user/src/com/google/gwt/requestfactory/server/RequestFactoryInterfaceValidator.java
@@ -0,0 +1,1070 @@
+/*
+ * Copyright 2010 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.requestfactory.server;
+
+import com.google.gwt.dev.asm.AnnotationVisitor;
+import com.google.gwt.dev.asm.ClassReader;
+import com.google.gwt.dev.asm.ClassVisitor;
+import com.google.gwt.dev.asm.MethodVisitor;
+import com.google.gwt.dev.asm.Opcodes;
+import com.google.gwt.dev.asm.Type;
+import com.google.gwt.dev.asm.commons.EmptyVisitor;
+import com.google.gwt.dev.asm.commons.Method;
+import com.google.gwt.dev.asm.signature.SignatureReader;
+import com.google.gwt.dev.asm.signature.SignatureVisitor;
+import com.google.gwt.dev.util.Name;
+import com.google.gwt.dev.util.Name.BinaryName;
+import com.google.gwt.dev.util.Name.SourceOrBinaryName;
+import com.google.gwt.requestfactory.client.impl.FindRequest;
+import com.google.gwt.requestfactory.shared.EntityProxy;
+import com.google.gwt.requestfactory.shared.InstanceRequest;
+import com.google.gwt.requestfactory.shared.ProxyFor;
+import com.google.gwt.requestfactory.shared.ProxyForName;
+import com.google.gwt.requestfactory.shared.Request;
+import com.google.gwt.requestfactory.shared.RequestContext;
+import com.google.gwt.requestfactory.shared.RequestFactory;
+import com.google.gwt.requestfactory.shared.Service;
+import com.google.gwt.requestfactory.shared.ServiceName;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Encapsulates validation logic to determine if a {@link RequestFactory}
+ * interface, its {@link RequestContext}, and associated {@link EntityProxy}
+ * interfaces match their domain counterparts. This implementation examines the
+ * classfiles directly in order to avoid the need to load the types into the
+ * JVM.
+ */
+public class RequestFactoryInterfaceValidator {
+ /**
+ * An implementation of {@link Loader} that uses a {@link ClassLoader} to
+ * retrieve the class files.
+ */
+ public static class ClassLoaderLoader implements Loader {
+ private final ClassLoader loader;
+
+ public ClassLoaderLoader(ClassLoader loader) {
+ this.loader = loader;
+ }
+
+ public boolean exists(String resource) {
+ return loader.getResource(resource) != null;
+ }
+
+ public InputStream getResourceAsStream(String resource) {
+ return loader.getResourceAsStream(resource);
+ }
+ }
+
+ /**
+ * Abstracts the mechanism by which class files are loaded.
+ *
+ * @see ClassLoaderLoader
+ */
+ public interface Loader {
+ /**
+ * Returns true if the specified resource can be loaded.
+ */
+ boolean exists(String resource);
+
+ /**
+ * Returns an InputStream to access the specified resource, or
+ * <code>null</code> if no such resource exists.
+ */
+ InputStream getResourceAsStream(String resource);
+ }
+
+ /**
+ * Used internally as a placeholder for types that cannot be mapped to a
+ * domain object.
+ */
+ interface MissingDomainType {
+ }
+
+ /**
+ * Collects the ProxyFor or Service annotation from an EntityProxy or
+ * RequestContext type.
+ */
+ private class DomainMapper extends EmptyVisitor {
+ private final ErrorContext logger;
+ private String domainInternalName;
+
+ public DomainMapper(ErrorContext logger) {
+ this.logger = logger;
+ logger.spam("Finding domain mapping annotation");
+ }
+
+ public String getDomainInternalName() {
+ return domainInternalName;
+ }
+
+ @Override
+ public void visit(int version, int access, String name, String signature,
+ String superName, String[] interfaces) {
+ if ((access & Opcodes.ACC_INTERFACE) == 0) {
+ logger.poison("Type must be an interface");
+ }
+ }
+
+ @Override
+ public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
+ boolean foundProxy = desc.equals(Type.getDescriptor(ProxyFor.class));
+ boolean foundProxyName = desc.equals(Type.getDescriptor(ProxyForName.class));
+ boolean foundService = desc.equals(Type.getDescriptor(Service.class));
+ boolean foundServiceName = desc.equals(Type.getDescriptor(ServiceName.class));
+
+ if (foundProxy || foundService) {
+ return new EmptyVisitor() {
+
+ @Override
+ public void visit(String name, Object value) {
+ domainInternalName = ((Type) value).getInternalName();
+ }
+
+ };
+ }
+
+ if (foundProxyName || foundServiceName) {
+ return new EmptyVisitor() {
+ @Override
+ public void visit(String name, Object value) {
+ String sourceName = (String) value;
+
+ /*
+ * The input is a source name, so we need to convert it to an
+ * internal name. We'll do this by substituting dollar signs for the
+ * last slash in the name until there are no more slashes.
+ */
+ StringBuffer desc = new StringBuffer(sourceName.replace('.', '/'));
+ while (!loader.exists(desc.toString() + ".class")) {
+ logger.spam("Did not find " + desc.toString());
+ int idx = desc.lastIndexOf("/");
+ if (idx == -1) {
+ return;
+ }
+ desc.setCharAt(idx, '$');
+ }
+
+ domainInternalName = desc.toString();
+ logger.spam(domainInternalName);
+ }
+ };
+ }
+ return null;
+ }
+ }
+
+ /**
+ * Improves error messages by providing context for the user.
+ */
+ private class ErrorContext {
+ private final Logger logger;
+ private final ErrorContext parent;
+ private Type currentType;
+ private Method currentMethod;
+
+ public ErrorContext(Logger logger) {
+ this.logger = logger;
+ this.parent = null;
+ }
+
+ private ErrorContext(ErrorContext parent) {
+ this.logger = parent.logger;
+ this.parent = parent;
+ }
+
+ public void poison(String msg, Object... args) {
+ logger.logp(Level.SEVERE, currentType(), currentMethod(),
+ String.format(msg, args));
+ poisoned = true;
+ }
+
+ public void poison(String msg, Throwable t) {
+ logger.logp(Level.SEVERE, currentType(), currentMethod(), msg, t);
+ poisoned = true;
+ }
+
+ public ErrorContext setMethod(Method method) {
+ ErrorContext toReturn = new ErrorContext(this);
+ toReturn.currentMethod = method;
+ return toReturn;
+ }
+
+ public ErrorContext setType(Type type) {
+ ErrorContext toReturn = new ErrorContext(this);
+ toReturn.currentType = type;
+ return toReturn;
+ }
+
+ public void spam(String msg, Object... args) {
+ logger.logp(Level.FINEST, currentType(), currentMethod(),
+ String.format(msg, args));
+ }
+
+ private String currentMethod() {
+ if (currentMethod != null) {
+ return print(currentMethod);
+ }
+ if (parent != null) {
+ return parent.currentMethod();
+ }
+ return null;
+ }
+
+ private String currentType() {
+ if (currentType != null) {
+ return print(currentType);
+ }
+ if (parent != null) {
+ return parent.currentType();
+ }
+ return null;
+ }
+ }
+
+ /**
+ * Collects information about domain objects. This visitor is intended to be
+ * iteratively applied to collect all methods in a type hierarchy.
+ */
+ private class MethodsInHierarchyCollector extends EmptyVisitor {
+ private final ErrorContext logger;
+ private Set<RFMethod> methods = new LinkedHashSet<RFMethod>();
+ private Set<String> seen = new HashSet<String>();
+
+ private MethodsInHierarchyCollector(ErrorContext logger) {
+ this.logger = logger;
+ }
+
+ public Set<RFMethod> exec(String internalName) {
+ RequestFactoryInterfaceValidator.this.visit(logger, internalName, this);
+ return methods;
+ }
+
+ @Override
+ public void visit(int version, int access, String name, String signature,
+ String superName, String[] interfaces) {
+ if (!seen.add(name)) {
+ return;
+ }
+ if (!"java/lang/Object".equals(superName)) {
+ RequestFactoryInterfaceValidator.this.visit(logger, superName, this);
+ }
+ if (interfaces != null) {
+ for (String intf : interfaces) {
+ RequestFactoryInterfaceValidator.this.visit(logger, intf, this);
+ }
+ }
+ }
+
+ @Override
+ public MethodVisitor visitMethod(int access, String name, String desc,
+ String signature, String[] exceptions) {
+ RFMethod method = new RFMethod(name, desc);
+ method.setDeclaredStatic((access & Opcodes.ACC_STATIC) != 0);
+ method.setDeclaredSignature(signature);
+ methods.add(method);
+ return null;
+ }
+ }
+
+ private static class RFMethod extends Method {
+ private boolean isDeclaredStatic;
+ private String signature;
+
+ public RFMethod(String name, String desc) {
+ super(name, desc);
+ }
+
+ public String getSignature() {
+ return signature;
+ }
+
+ public boolean isDeclaredStatic() {
+ return isDeclaredStatic;
+ }
+
+ public void setDeclaredSignature(String signature) {
+ this.signature = signature;
+ }
+
+ public void setDeclaredStatic(boolean value) {
+ isDeclaredStatic = value;
+ }
+
+ @Override
+ public String toString() {
+ return (isDeclaredStatic ? "static " : "") + super.toString();
+ }
+ }
+
+ private class SupertypeCollector extends EmptyVisitor {
+ private final ErrorContext logger;
+ private final Set<String> seen = new HashSet<String>();
+ private final List<Type> supertypes = new ArrayList<Type>();
+
+ public SupertypeCollector(ErrorContext logger) {
+ this.logger = logger;
+ }
+
+ public List<Type> exec(Type type) {
+ RequestFactoryInterfaceValidator.this.visit(logger,
+ type.getInternalName(), this);
+ return supertypes;
+ }
+
+ @Override
+ public void visit(int version, int access, String name, String signature,
+ String superName, String[] interfaces) {
+ if (!seen.add(name)) {
+ return;
+ }
+ supertypes.add(Type.getObjectType(name));
+ if (!"java/lang/Object".equals(name)) {
+ RequestFactoryInterfaceValidator.this.visit(logger, superName, this);
+ }
+ if (interfaces != null) {
+ for (String intf : interfaces) {
+ RequestFactoryInterfaceValidator.this.visit(logger, intf, this);
+ }
+ }
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ static final Set<Class<?>> VALUE_TYPES = Collections.unmodifiableSet(new HashSet<Class<?>>(
+ Arrays.asList(Boolean.class, Character.class, Class.class, Date.class,
+ Enum.class, Number.class, String.class, Void.class)));
+
+ public static void main(String[] args) {
+ if (args.length == 0) {
+ System.err.println("Usage: java -cp gwt-servlet.jar:your-code.jar "
+ + RequestFactoryInterfaceValidator.class.getCanonicalName()
+ + " com.example.MyRequestFactory");
+ System.exit(1);
+ }
+ RequestFactoryInterfaceValidator validator = new RequestFactoryInterfaceValidator(
+ Logger.getLogger(RequestFactoryInterfaceValidator.class.getName()),
+ new ClassLoaderLoader(Thread.currentThread().getContextClassLoader()));
+ validator.validateRequestFactory(args[0]);
+ System.exit(validator.isPoisoned() ? 1 : 0);
+ }
+
+ private static String print(Method method) {
+ StringBuilder sb = new StringBuilder();
+ sb.append(print(method.getReturnType())).append(" ").append(
+ method.getName()).append("(");
+ for (Type t : method.getArgumentTypes()) {
+ sb.append(print(t)).append(" ");
+ }
+ sb.append(")");
+ return sb.toString();
+ }
+
+ private static String print(Type type) {
+ return SourceOrBinaryName.toSourceName(type.getClassName());
+ }
+
+ /**
+ * Maps client types (e.g. FooProxy) to server domain types (e.g. Foo).
+ */
+ private final Map<Type, Type> clientToDomainType = new HashMap<Type, Type>();
+ /**
+ * The type {@link EntityProxy}.
+ */
+ private final Type entityProxyIntf = Type.getType(EntityProxy.class);
+ /**
+ * A placeholder type for client types that could not be resolved to a domain
+ * type.
+ */
+ private final Type errorType = Type.getType(MissingDomainType.class);
+ /**
+ * The type {@link InstanceRequest}.
+ */
+ private final Type instanceRequestIntf = Type.getType(InstanceRequest.class);
+ private final Loader loader;
+ /**
+ * A cache of all methods defined in a type hierarchy.
+ */
+ private final Map<Type, Set<RFMethod>> methodsInHierarchy = new HashMap<Type, Set<RFMethod>>();
+ private final ErrorContext parentLogger;
+ private boolean poisoned;
+ /**
+ * The type {@link Request}.
+ */
+ private final Type requestIntf = Type.getType(Request.class);
+ /**
+ * The type {@link RequestContext}.
+ */
+ private final Type requestContextIntf = Type.getType(RequestContext.class);
+ /**
+ * A map of a type to all types that it could be assigned to.
+ */
+ private final Map<Type, List<Type>> supertypes = new HashMap<Type, List<Type>>();
+ /**
+ * A set to prevent re-validation of a type.
+ */
+ private final Set<String> validatedTypes = new HashSet<String>();
+
+ /**
+ * Contains vaue types (e.g. Integer).
+ */
+ private final Set<Type> valueTypes = new HashSet<Type>();
+
+ public RequestFactoryInterfaceValidator(Logger logger, Loader loader) {
+ this.parentLogger = new ErrorContext(logger);
+ this.loader = loader;
+ for (Class<?> clazz : VALUE_TYPES) {
+ valueTypes.add(Type.getType(clazz));
+ }
+ }
+
+ /**
+ * Returns true if validation failed.
+ */
+ public boolean isPoisoned() {
+ return poisoned;
+ }
+
+ /**
+ * This method checks an EntityProxy interface against its peer domain object
+ * to determine if the server code would be able to process a request using
+ * the methods defined in the EntityProxy interface. It does not perform any
+ * checks as to whether or not the EntityProxy could actually be generated by
+ * the Generator.
+ * <p>
+ * This method may be called repeatedly on a single instance of the validator.
+ * Doing so will amortize type calculation costs.
+ * <p>
+ * Checks implemented:
+ * <ul>
+ * <li> <code>binaryName</code> implements EntityProxy</li>
+ * <li><code>binaryName</code> has a {@link ProxyFor} or {@link ProxyForName}
+ * annotation</li>
+ * <li>The domain object has getId() and getVersion() methods</li>
+ * <li>All property methods in the EntityProxy can be mapped onto an
+ * equivalent domain method</li>
+ * <li>All referenced EntityProxy types are valid</li>
+ * </ul>
+ *
+ * @param binaryName the binary name (e.g. {@link Class#getName()}) of the
+ * EntityProxy subtype
+ */
+ public void validateEntityProxy(String binaryName) {
+ if (!Name.isBinaryName(binaryName)) {
+ parentLogger.poison("%s is not a binary name", binaryName);
+ return;
+ }
+
+ // Don't revalidate the same type
+ if (!validatedTypes.add(binaryName)) {
+ return;
+ }
+
+ Type proxyType = Type.getObjectType(BinaryName.toInternalName(binaryName));
+ ErrorContext logger = parentLogger.setType(proxyType);
+
+ // Quick sanity check for calling code
+ if (!isAssignable(logger, entityProxyIntf, proxyType)) {
+ parentLogger.poison("%s is not a %s", print(proxyType),
+ EntityProxy.class.getSimpleName());
+ return;
+ }
+
+ // Find the domain type
+ Type domainType = getDomainType(logger, proxyType);
+ if (domainType == errorType) {
+ logger.poison(
+ "The type %s must be annotated with a @%s or @%s annotation",
+ BinaryName.toSourceName(binaryName), ProxyFor.class.getSimpleName(),
+ ProxyForName.class.getSimpleName());
+ return;
+ }
+
+ // Check for getId() and getVersion() in domain
+ checkIdAndVersion(logger, domainType);
+
+ // Collect all methods in the client proxy type
+ Set<RFMethod> clientPropertyMethods = getMethodsInHierarchy(logger,
+ proxyType);
+
+ // Find the equivalent domain getter/setter method
+ for (RFMethod clientPropertyMethod : clientPropertyMethods) {
+ // Ignore stableId(). Can't use descriptor due to overrides
+ if ("stableId".equals(clientPropertyMethod.getName())
+ && clientPropertyMethod.getArgumentTypes().length == 0) {
+ continue;
+ }
+ checkPropertyMethod(logger, clientPropertyMethod, domainType);
+ maybeCheckReferredProxies(logger, clientPropertyMethod);
+ }
+ }
+
+ /**
+ * This method checks a RequestContext interface against its peer domain
+ * domain object to determine if the server code would be able to process a
+ * request using the the methods defined in the RequestContext interface. It
+ * does not perform any checks as to whether or not the RequestContext could
+ * actually be generated by the Generator.
+ * <p>
+ * This method may be called repeatedly on a single instance of the validator.
+ * Doing so will amortize type calculation costs.
+ * <p>
+ * Checks implemented:
+ * <ul>
+ * <li> <code>binaryName</code> implements RequestContext</li>
+ * <li><code>binaryName</code> has a {@link Service} or {@link ServiceName}
+ * annotation</li>
+ * <li>All service methods in the RequestContext can be mapped onto an
+ * equivalent domain method</li>
+ * <li>All referenced EntityProxy types are valid</li>
+ * </ul>
+ *
+ * @param binaryName the binary name (e.g. {@link Class#getName()}) of the
+ * RequestContext subtype
+ * @see #validateEntityProxy(String)
+ */
+ public void validateRequestContext(String binaryName) {
+ if (!Name.isBinaryName(binaryName)) {
+ parentLogger.poison("%s is not a binary name", binaryName);
+ return;
+ }
+
+ // Don't revalidate the same type
+ if (!validatedTypes.add(binaryName)) {
+ return;
+ }
+
+ if (FindRequest.class.getName().equals(binaryName)) {
+ // Ignore FindRequest, it's a huge hack
+ return;
+ }
+
+ Type requestContextType = Type.getObjectType(BinaryName.toInternalName(binaryName));
+ final ErrorContext logger = parentLogger.setType(requestContextType);
+
+ // Quick sanity check for calling code
+ if (!isAssignable(logger, requestContextIntf, requestContextType)) {
+ logger.poison("%s is not a %s", print(requestContextType),
+ RequestContext.class.getSimpleName());
+ return;
+ }
+
+ Type domainServiceType = getDomainType(logger, requestContextType);
+ if (domainServiceType == errorType) {
+ logger.poison(
+ "The type %s must be annotated with a @%s or @%s annotation",
+ BinaryName.toSourceName(binaryName), Service.class.getSimpleName(),
+ ServiceName.class.getSimpleName());
+ return;
+ }
+
+ for (RFMethod method : getMethodsInHierarchy(logger, requestContextType)) {
+ // Ignore methods in RequestContext itself
+ if (findCompatibleMethod(logger, requestContextIntf, method, false, true) != null) {
+ continue;
+ }
+
+ // Check the client method against the domain
+ checkClientMethodInDomain(logger, method, domainServiceType);
+ maybeCheckReferredProxies(logger, method);
+ }
+ }
+
+ /**
+ * This method checks a RequestFactory interface.
+ * <p>
+ * This method may be called repeatedly on a single instance of the validator.
+ * Doing so will amortize type calculation costs. It does not perform any
+ * checks as to whether or not the RequestContext could actually be generated
+ * by the Generator.
+ * <p>
+ * Checks implemented:
+ * <ul>
+ * <li> <code>binaryName</code> implements RequestFactory</li>
+ * <li>All referenced RequestContext types are valid</li>
+ * </ul>
+ *
+ * @param binaryName the binary name (e.g. {@link Class#getName()}) of the
+ * RequestContext subtype
+ * @see #validateRequestContext(String)
+ */
+ public void validateRequestFactory(String binaryName) {
+ if (!Name.isBinaryName(binaryName)) {
+ parentLogger.poison("%s is not a binary name", binaryName);
+ return;
+ }
+
+ // Don't revalidate the same type
+ if (!validatedTypes.add(binaryName)) {
+ return;
+ }
+
+ Type requestFactoryType = Type.getObjectType(BinaryName.toInternalName(binaryName));
+ ErrorContext logger = parentLogger.setType(requestFactoryType);
+
+ // Quick sanity check for calling code
+ if (!isAssignable(logger, Type.getType(RequestFactory.class),
+ requestFactoryType)) {
+ logger.poison("%s is not a %s", print(requestFactoryType),
+ RequestFactory.class.getSimpleName());
+ return;
+ }
+
+ // Validate each RequestContext method in the RF
+ for (Method contextMethod : getMethodsInHierarchy(logger,
+ requestFactoryType)) {
+ Type returnType = contextMethod.getReturnType();
+ if (isAssignable(logger, requestContextIntf, returnType)) {
+ validateRequestContext(returnType.getClassName());
+ }
+ }
+ }
+
+ /**
+ * Check that a given method RequestContext method declaration can be mapped
+ * to the server's domain type.
+ */
+ private void checkClientMethodInDomain(ErrorContext logger, RFMethod method,
+ Type domainServiceType) {
+ logger = logger.setMethod(method);
+
+ // Create a "translated" method declaration to search for
+ // Request<BlahProxy> foo(int a, BarProxy bar) -> Blah foo(int a, Bar bar);
+ Type returnType = getReturnType(logger, method);
+ Method searchFor = createDomainMethod(logger, new Method(method.getName(),
+ returnType, method.getArgumentTypes()));
+
+ RFMethod found = findCompatibleMethod(logger, domainServiceType, searchFor);
+
+ if (found != null) {
+ boolean isInstance = isAssignable(logger, instanceRequestIntf,
+ method.getReturnType());
+ if (isInstance && found.isDeclaredStatic()) {
+ logger.poison("The method %s is declared to return %s, but the"
+ + " service method is static", method.getName(),
+ InstanceRequest.class.getCanonicalName());
+ } else if (!isInstance && !found.isDeclaredStatic) {
+ logger.poison("The method %s is declared to return %s, but the"
+ + " service method is not static", method.getName(),
+ Request.class.getCanonicalName());
+ }
+ }
+ }
+
+ /**
+ * Check that the domain object has <code>getId()</code> and
+ * <code>getVersion</code> methods.
+ */
+ private void checkIdAndVersion(ErrorContext logger, Type domainType) {
+ logger = logger.setType(domainType);
+ Method getIdString = new Method("getId", "()Ljava/lang/String;");
+ Method getIdLong = new Method("getId", "()Ljava/lang/Long;");
+ if (findCompatibleMethod(logger, domainType, getIdString, false, true) == null
+ && findCompatibleMethod(logger, domainType, getIdLong, false, true) == null) {
+ logger.poison("Did not find a getId() method that"
+ + " returns a String or a Long");
+ }
+
+ Method getVersion = new Method("getVersion", "()Ljava/lang/Integer;");
+ if (findCompatibleMethod(logger, domainType, getVersion) == null) {
+ logger.poison("Did not find a getVersion() method"
+ + " that returns an Integer");
+ }
+ }
+
+ /**
+ * Ensure that the given property method on an EntityProxy exists on the
+ * domain object.
+ */
+ private void checkPropertyMethod(ErrorContext logger,
+ Method clientPropertyMethod, Type domainType) {
+ logger = logger.setMethod(clientPropertyMethod);
+
+ findCompatibleMethod(logger, domainType,
+ createDomainMethod(logger, clientPropertyMethod));
+ }
+
+ /**
+ * Convert a method declaration using client types (e.g. FooProxy) to domain
+ * types (e.g. Foo).
+ */
+ private Method createDomainMethod(ErrorContext logger, Method clientMethod) {
+ Type[] args = clientMethod.getArgumentTypes();
+ for (int i = 0, j = args.length; i < j; i++) {
+ args[i] = getDomainType(logger, args[i]);
+ }
+ Type returnType = getDomainType(logger, clientMethod.getReturnType());
+ return new Method(clientMethod.getName(), returnType, args);
+ }
+
+ /**
+ * Finds a compatible method declaration in <code>domainType</code>'s
+ * hierarchy that is assignment-compatible with the given Method.
+ */
+ private RFMethod findCompatibleMethod(final ErrorContext logger,
+ Type domainType, Method searchFor) {
+ return findCompatibleMethod(logger, domainType, searchFor, true, false);
+ }
+
+ /**
+ * Finds a compatible method declaration in <code>domainType</code>'s
+ * hierarchy that is assignment-compatible with the given Method.
+ */
+ private RFMethod findCompatibleMethod(final ErrorContext logger,
+ Type domainType, Method searchFor, boolean mustFind,
+ boolean allowOverloads) {
+ String methodName = searchFor.getName();
+ Type[] clientArgs = searchFor.getArgumentTypes();
+ Type clientReturnType = searchFor.getReturnType();
+ // Pull all methods out of the domain type
+ Map<String, List<RFMethod>> domainLookup = new LinkedHashMap<String, List<RFMethod>>();
+ for (RFMethod method : getMethodsInHierarchy(logger, domainType)) {
+ List<RFMethod> list = domainLookup.get(method.getName());
+ if (list == null) {
+ list = new ArrayList<RFMethod>();
+ domainLookup.put(method.getName(), list);
+ }
+ list.add(method);
+ }
+
+ // Find the matching method in the domain object
+ List<RFMethod> methods = domainLookup.get(methodName);
+ if (methods == null) {
+ if (mustFind) {
+ logger.poison("Could not find any methods named %s in %s", methodName,
+ print(domainType));
+ }
+ return null;
+ }
+ if (methods.size() > 1 && !allowOverloads) {
+ StringBuilder sb = new StringBuilder();
+ sb.append(String.format("Method overloads found in type %s named %s:\n",
+ print(domainType), methodName));
+ for (RFMethod method : methods) {
+ sb.append(" ").append(print(method)).append("\n");
+ }
+ logger.poison(sb.toString());
+ return null;
+ }
+
+ // Check each overloaded name
+ for (RFMethod domainMethod : methods) {
+ // Box any primitive types to simplify compotibility check
+ Type[] domainArgs = domainMethod.getArgumentTypes();
+ for (int i = 0, j = domainArgs.length; i < j; i++) {
+ domainArgs[i] = maybeBoxType(domainArgs[i]);
+ }
+ Type domainReturnType = maybeBoxType(domainMethod.getReturnType());
+
+ /*
+ * Make sure the client args can be passed into the domain args and the
+ * domain return type into the client return type.
+ */
+ if (isAssignable(logger, domainArgs, clientArgs)
+ && isAssignable(logger, clientReturnType, domainReturnType)) {
+
+ logger.spam("Mapped client method " + print(searchFor) + " to "
+ + print(domainMethod));
+ return domainMethod;
+ }
+ }
+ if (mustFind) {
+ StringBuilder sb = new StringBuilder();
+ sb.append(String.format(
+ "Could not find matching method in %s:\nPossible matches:\n",
+ print(domainType)));
+ for (RFMethod domainMethod : methods) {
+ sb.append(" ").append(print(domainMethod)).append("\n");
+ }
+ logger.poison(sb.toString());
+ }
+ return null;
+ }
+
+ /**
+ * This looks like it should be a utility method somewhere else, but I can't
+ * find it.
+ */
+ private Type getBoxedType(Type primitive) {
+ switch (primitive.getSort()) {
+ case Type.BOOLEAN:
+ return Type.getType(Boolean.class);
+ case Type.BYTE:
+ return Type.getType(Byte.class);
+ case Type.CHAR:
+ return Type.getType(Character.class);
+ case Type.DOUBLE:
+ return Type.getType(Double.class);
+ case Type.FLOAT:
+ return Type.getType(Float.class);
+ case Type.INT:
+ return Type.getType(Integer.class);
+ case Type.LONG:
+ return Type.getType(Long.class);
+ case Type.SHORT:
+ return Type.getType(Short.class);
+ case Type.VOID:
+ return Type.getType(Void.class);
+ }
+ throw new RuntimeException(primitive.getDescriptor()
+ + " is not a primitive type");
+ }
+
+ /**
+ * Convert the type used in a client-side EntityProxy or RequestContext
+ * declaration to the equivalent domain type. Value types and supported
+ * collections are a pass-through. EntityProxy types will be resolved to their
+ * domain object type. RequestContext types will be resolved to their service
+ * object.
+ */
+ private Type getDomainType(ErrorContext logger, Type clientType) {
+ Type toReturn = clientToDomainType.get(clientType);
+ if (toReturn != null) {
+ return toReturn;
+ }
+ if (isValueType(logger, clientType) || isCollectionType(logger, clientType)) {
+ toReturn = clientType;
+ } else {
+ logger = logger.setType(clientType);
+ DomainMapper pv = new DomainMapper(logger);
+ visit(logger, clientType.getInternalName(), pv);
+ if (pv.getDomainInternalName() == null) {
+ logger.poison("%s has no mapping to a domain type (e.g. @%s or @%s)",
+ print(clientType), ProxyFor.class.getSimpleName(),
+ Service.class.getSimpleName());
+ toReturn = errorType;
+ } else {
+ toReturn = Type.getObjectType(pv.getDomainInternalName());
+ }
+ }
+ clientToDomainType.put(clientType, toReturn);
+ return toReturn;
+ }
+
+ /**
+ * Collect all of the methods defined within a type hierarchy.
+ */
+ private Set<RFMethod> getMethodsInHierarchy(ErrorContext logger,
+ Type domainType) {
+ Set<RFMethod> toReturn = methodsInHierarchy.get(domainType);
+ if (toReturn == null) {
+ logger = logger.setType(domainType);
+ toReturn = new MethodsInHierarchyCollector(logger).exec(domainType.getInternalName());
+ methodsInHierarchy.put(domainType, Collections.unmodifiableSet(toReturn));
+ }
+ return toReturn;
+ }
+
+ /**
+ * Examines a generic RequestContext method declaration and determines the
+ * expected domain return type. This implementation is limited in that it will
+ * not attempt to resolve type bounds since that would essentially require
+ * implementing TypeOracle. In the case where the type bound cannot be
+ * resolved, this method will return Object's type.
+ */
+ private Type getReturnType(ErrorContext logger, RFMethod method) {
+ logger = logger.setMethod(method);
+ final String[] returnType = {"java/lang/Object"};
+ String signature = method.getSignature();
+
+ final int expectedCount;
+ if (method.getReturnType().equals(instanceRequestIntf)) {
+ expectedCount = 2;
+ } else if (method.getReturnType().equals(requestIntf)) {
+ expectedCount = 1;
+ } else {
+ logger.spam("Punting on " + signature);
+ return Type.getObjectType(returnType[0]);
+ }
+
+ // TODO(bobv): If a class-based TypeOracle is built, use that instead
+ new SignatureReader(signature).accept(new SignatureAdapter() {
+ @Override
+ public SignatureVisitor visitReturnType() {
+ return new SignatureAdapter() {
+ int count;
+
+ @Override
+ public SignatureVisitor visitTypeArgument(char wildcard) {
+ if (++count == expectedCount) {
+ return new SignatureAdapter() {
+ @Override
+ public void visitClassType(String name) {
+ returnType[0] = name;
+ }
+ };
+ }
+ return super.visitTypeArgument(wildcard);
+ }
+ };
+ }
+ });
+
+ logger.spam("Extracted " + returnType[0]);
+ return Type.getObjectType(returnType[0]);
+ }
+
+ private List<Type> getSupertypes(ErrorContext logger, Type type) {
+ if (type.getSort() != Type.OBJECT) {
+ return Collections.emptyList();
+ }
+ List<Type> toReturn = supertypes.get(type);
+ if (toReturn != null) {
+ return toReturn;
+ }
+ logger = logger.setType(type);
+ toReturn = new SupertypeCollector(logger).exec(type);
+ supertypes.put(type, Collections.unmodifiableList(toReturn));
+ return toReturn;
+ }
+
+ private boolean isAssignable(ErrorContext logger, Type possibleSupertype,
+ Type possibleSubtype) {
+ // Fast-path for same type
+ if (possibleSupertype.equals(possibleSubtype)) {
+ return true;
+ }
+
+ // Box primitive types to make this simple
+ if (possibleSupertype.getSort() != Type.OBJECT) {
+ possibleSupertype = getBoxedType(possibleSupertype);
+ }
+ if (possibleSubtype.getSort() != Type.OBJECT) {
+ possibleSubtype = getBoxedType(possibleSubtype);
+ }
+
+ // Supertype calculation is cached
+ List<Type> allSupertypes = getSupertypes(logger, possibleSubtype);
+ return allSupertypes.contains(possibleSupertype);
+ }
+
+ private boolean isAssignable(ErrorContext logger, Type[] possibleSupertypes,
+ Type[] possibleSubtypes) {
+ // Check the same number of types
+ if (possibleSupertypes.length != possibleSubtypes.length) {
+ return false;
+ }
+ for (int i = 0, j = possibleSupertypes.length; i < j; i++) {
+ if (!isAssignable(logger, possibleSupertypes[i], possibleSubtypes[i])) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private boolean isCollectionType(ErrorContext logger, Type type) {
+ return "java/util/List".equals(type.getInternalName())
+ || "java/util/Set".equals(type.getInternalName());
+ }
+
+ private boolean isValueType(ErrorContext logger, Type type) {
+ if (type.getSort() != Type.OBJECT) {
+ return true;
+ }
+ if (valueTypes.contains(type)) {
+ return true;
+ }
+ logger = logger.setType(type);
+ List<Type> types = getSupertypes(logger, type);
+ for (Type t : types) {
+ if (valueTypes.contains(t)) {
+ valueTypes.add(type);
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private Type maybeBoxType(Type maybePrimitive) {
+ if (maybePrimitive.getSort() == Type.OBJECT) {
+ return maybePrimitive;
+ }
+ return getBoxedType(maybePrimitive);
+ }
+
+ /**
+ * Examine an array of Types and call {@link #validateEntityProxy(String)} if
+ * the type is an EntityProxy.
+ */
+ private void maybeCheckEntityProxyType(ErrorContext logger, Type... types) {
+ for (Type type : types) {
+ if (isAssignable(logger, entityProxyIntf, type)) {
+ validateEntityProxy(type.getClassName());
+ }
+ }
+ }
+
+ /**
+ * Examine the arguments ond return value of a method and check any
+ * EntityProxies referred.
+ */
+ private void maybeCheckReferredProxies(ErrorContext logger, RFMethod method) {
+ Type[] argTypes = method.getArgumentTypes();
+ Type returnType = getReturnType(logger, method);
+
+ // Check EntityProxy args ond return types against the domain
+ maybeCheckEntityProxyType(logger, argTypes);
+ maybeCheckEntityProxyType(logger, returnType);
+ }
+
+ /**
+ * Load the classfile for the given binary name and apply the provided
+ * visitor.
+ *
+ * @return <code>true</code> if the visitor was successfully visited
+ */
+ private boolean visit(ErrorContext logger, String internalName,
+ ClassVisitor visitor) {
+ assert Name.isInternalName(internalName) : "internalName";
+ logger.spam("Visiting " + internalName);
+ InputStream inputStream = null;
+ try {
+ inputStream = loader.getResourceAsStream(internalName + ".class");
+ if (inputStream == null) {
+ logger.poison("Could not find class file for " + internalName);
+ return false;
+ }
+ ClassReader reader = new ClassReader(inputStream);
+ reader.accept(visitor, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG
+ | ClassReader.SKIP_FRAMES);
+ return true;
+ } catch (IOException e) {
+ logger.poison("Unable to open " + internalName, e);
+ } finally {
+ if (inputStream != null) {
+ try {
+ inputStream.close();
+ } catch (IOException ignored) {
+ }
+ }
+ }
+ return false;
+ }
+}
diff --git a/user/src/com/google/gwt/requestfactory/server/SignatureAdapter.java b/user/src/com/google/gwt/requestfactory/server/SignatureAdapter.java
new file mode 100644
index 0000000..ebd6299
--- /dev/null
+++ b/user/src/com/google/gwt/requestfactory/server/SignatureAdapter.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2010 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.requestfactory.server;
+
+import com.google.gwt.dev.asm.signature.SignatureVisitor;
+
+/**
+ * An empty implementation of SignatureVisitor, used by
+ * {@link RequestFactoryInterfaceValidator}. This is a copy of the dev package's
+ * EmptySignatureVisitor.
+ */
+class SignatureAdapter implements SignatureVisitor {
+
+ private static final SignatureAdapter ignore = new SignatureAdapter();
+
+ public SignatureVisitor visitArrayType() {
+ return ignore;
+ }
+
+ public void visitBaseType(char descriptor) {
+ }
+
+ public SignatureVisitor visitClassBound() {
+ return ignore;
+ }
+
+ public void visitClassType(String name) {
+ }
+
+ public void visitEnd() {
+ }
+
+ public SignatureVisitor visitExceptionType() {
+ return ignore;
+ }
+
+ public void visitFormalTypeParameter(String name) {
+ }
+
+ public void visitInnerClassType(String name) {
+ }
+
+ public SignatureVisitor visitInterface() {
+ return ignore;
+ }
+
+ public SignatureVisitor visitInterfaceBound() {
+ return ignore;
+ }
+
+ public SignatureVisitor visitParameterType() {
+ return ignore;
+ }
+
+ public SignatureVisitor visitReturnType() {
+ return ignore;
+ }
+
+ public SignatureVisitor visitSuperclass() {
+ return ignore;
+ }
+
+ public void visitTypeArgument() {
+ }
+
+ public SignatureVisitor visitTypeArgument(char wildcard) {
+ return ignore;
+ }
+
+ public void visitTypeVariable(String name) {
+ }
+}
\ No newline at end of file
diff --git a/user/src/com/google/gwt/requestfactory/shared/ProxyFor.java b/user/src/com/google/gwt/requestfactory/shared/ProxyFor.java
index 7a8c747..4da1afe 100644
--- a/user/src/com/google/gwt/requestfactory/shared/ProxyFor.java
+++ b/user/src/com/google/gwt/requestfactory/shared/ProxyFor.java
@@ -21,12 +21,13 @@
import java.lang.annotation.Target;
/**
- * Annotation on Record classes specifying 'type'. 'type'
- * represents the server-side counterpart of the Record.
+ * Annotation on EntityProxy classes specifying the domain (server-side) object
+ * type.
+ *
+ * @see ProxyForName
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface ProxyFor {
-
Class<?> value();
}
diff --git a/user/src/com/google/gwt/requestfactory/shared/ProxyForName.java b/user/src/com/google/gwt/requestfactory/shared/ProxyForName.java
new file mode 100644
index 0000000..fa3f5bd
--- /dev/null
+++ b/user/src/com/google/gwt/requestfactory/shared/ProxyForName.java
@@ -0,0 +1,32 @@
+/*
+ * 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.shared;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Annotation on EntityProxy classes specifying the domain (server-side) object
+ * type. This annotation can be used in place of {@link ProxyFor} if the domain
+ * object is not available to the GWT compiler or DevMode runtime.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+public @interface ProxyForName {
+ String value();
+}
diff --git a/user/src/com/google/gwt/requestfactory/shared/Service.java b/user/src/com/google/gwt/requestfactory/shared/Service.java
index 59f2cf5..29d8829 100644
--- a/user/src/com/google/gwt/requestfactory/shared/Service.java
+++ b/user/src/com/google/gwt/requestfactory/shared/Service.java
@@ -23,10 +23,11 @@
/**
* Annotation on Request classes specifying the server side implementations that
* back them.
+ *
+ * @see ServiceName
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Service {
-
Class<?> value();
}
diff --git a/user/src/com/google/gwt/requestfactory/shared/ServiceName.java b/user/src/com/google/gwt/requestfactory/shared/ServiceName.java
new file mode 100644
index 0000000..beb72b6
--- /dev/null
+++ b/user/src/com/google/gwt/requestfactory/shared/ServiceName.java
@@ -0,0 +1,32 @@
+/*
+ * 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.shared;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Annotation on Request classes specifying the server side implementations that
+ * back them.This annotation can be used in place of {@link Service} if the
+ * service type is not available to the GWT compiler or DevMode runtime.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+public @interface ServiceName {
+ String value();
+}
diff --git a/user/test/com/google/gwt/requestfactory/RequestFactoryJreSuite.java b/user/test/com/google/gwt/requestfactory/RequestFactoryJreSuite.java
index a6ad74a..3aa7f1b 100644
--- a/user/test/com/google/gwt/requestfactory/RequestFactoryJreSuite.java
+++ b/user/test/com/google/gwt/requestfactory/RequestFactoryJreSuite.java
@@ -19,6 +19,7 @@
import com.google.gwt.requestfactory.rebind.model.RequestFactoryModelTest;
import com.google.gwt.requestfactory.server.JsonRequestProcessorTest;
import com.google.gwt.requestfactory.server.ReflectionBasedOperationRegistryTest;
+import com.google.gwt.requestfactory.server.RequestFactoryInterfaceValidatorTest;
import com.google.gwt.requestfactory.server.RequestPropertyTest;
import junit.framework.Test;
@@ -34,6 +35,7 @@
suite.addTestSuite(SimpleEntityProxyIdTest.class);
suite.addTestSuite(JsonRequestProcessorTest.class);
suite.addTestSuite(ReflectionBasedOperationRegistryTest.class);
+ suite.addTestSuite(RequestFactoryInterfaceValidatorTest.class);
suite.addTestSuite(RequestFactoryModelTest.class);
suite.addTestSuite(RequestPropertyTest.class);
return suite;
diff --git a/user/test/com/google/gwt/requestfactory/rebind/model/RequestFactoryModelTest.java b/user/test/com/google/gwt/requestfactory/rebind/model/RequestFactoryModelTest.java
index 42a821b..74d268a 100644
--- a/user/test/com/google/gwt/requestfactory/rebind/model/RequestFactoryModelTest.java
+++ b/user/test/com/google/gwt/requestfactory/rebind/model/RequestFactoryModelTest.java
@@ -26,8 +26,6 @@
import com.google.gwt.dev.util.Util;
import com.google.gwt.dev.util.log.PrintWriterTreeLogger;
import com.google.gwt.requestfactory.server.TestContextImpl;
-import com.google.gwt.requestfactory.server.TestContextNoIdImpl;
-import com.google.gwt.requestfactory.server.TestContextNoVersionImpl;
import com.google.gwt.requestfactory.shared.EntityProxy;
import com.google.gwt.requestfactory.shared.InstanceRequest;
import com.google.gwt.requestfactory.shared.ProxyFor;
@@ -49,8 +47,9 @@
import java.util.SortedSet;
/**
- * Test case for {@link com.google.gwt.requestfactory.rebind.model.RequestFactoryModel}
- * that uses mock CompilationStates.
+ * Test case for
+ * {@link com.google.gwt.requestfactory.rebind.model.RequestFactoryModel} that
+ * uses mock CompilationStates.
*/
public class RequestFactoryModelTest extends TestCase {
@@ -64,8 +63,7 @@
public EmptyMockJavaResource(Class<?> clazz) {
super(clazz.getName());
- code.append("package ").append(clazz.getPackage().getName())
- .append(";\n");
+ code.append("package ").append(clazz.getPackage().getName()).append(";\n");
code.append("public interface ").append(clazz.getSimpleName());
int numParams = clazz.getTypeParameters().length;
@@ -103,15 +101,15 @@
@Override
protected CharSequence getContent() {
String resourceName = getTypeName().replace('.', '/') + ".java";
- InputStream stream = Thread.currentThread().getContextClassLoader()
- .getResourceAsStream(resourceName);
+ InputStream stream = Thread.currentThread().getContextClassLoader().getResourceAsStream(
+ resourceName);
return Util.readStreamAsString(stream);
}
}
private static TreeLogger createCompileLogger() {
- PrintWriterTreeLogger logger = new PrintWriterTreeLogger(
- new PrintWriter(System.err, true));
+ PrintWriterTreeLogger logger = new PrintWriterTreeLogger(new PrintWriter(
+ System.err, true));
logger.setMaxDetail(TreeLogger.ERROR);
return logger;
}
@@ -125,7 +123,8 @@
}
public void testBadCollectionType() {
- testModelWithMethodDecl("Request<SortedSet<Integer>> badReturnType();",
+ testModelWithMethodDecl(
+ "Request<SortedSet<Integer>> badReturnType();",
"Requests that return collections may be declared with java.util.List or java.util.Set only");
}
@@ -139,55 +138,10 @@
"Invalid Request parameterization java.lang.Iterable");
}
- public void testMismatchedArityInstance() {
- testModelWithMethodDecl(
- "InstanceRequest<TestProxy, String> mismatchedArityInstance(TestProxy p, int x);",
- "Parameter 0 of method TestContext.mismatchedArityInstance does not match method com.google.gwt.requestfactory.server.TestContextImpl.mismatchedArityInstance");
- }
-
- public void testMismatchedArityStatic() {
- testModelWithMethodDecl("Request<String> mismatchedArityStatic(int x);",
- "Method TestContext.mismatchedArityStatic parameters do not match same method on com.google.gwt.requestfactory.server.TestContextImpl");
- }
-
- public void testMismatchedModifierNonStatic() {
- testModelWithMethodDecl(
- "InstanceRequest<TestProxy, String> mismatchedNonStatic();",
- "Method TestContext.mismatchedNonStatic is an instance method, while the corresponding method on com.google.gwt.requestfactory.server.TestContextImpl is static");
- }
-
- public void testMismatchedModifierStatic() {
- testModelWithMethodDecl("Request<String> mismatchedStatic();",
- "Method TestContext.mismatchedStatic is a static method, while the corresponding method on com.google.gwt.requestfactory.server.TestContextImpl is not");
- }
-
- public void testMismatchedParamType() {
- testModelWithMethodDecl("Request<String> mismatchedParamType(Integer x);",
- "Parameter 0 of method TestContext.mismatchedParamType does not match method com.google.gwt.requestfactory.server.TestContextImpl.mismatchedParamType");
- }
-
- public void testMismatchedReturnType() {
- testModelWithMethodDecl("Request<String> mismatchedReturnType();",
- "Return type of method TestContext.mismatchedReturnType does not match method com.google.gwt.requestfactory.server.TestContextImpl.mismatchedReturnType");
- }
-
- public void testMissingId() {
- testModelWithMethodDeclArgs("Request<TestProxy> okMethodProxy();",
- TestContextNoIdImpl.class.getName(),
- TestContextNoIdImpl.class.getName(),
- "The class com.google.gwt.requestfactory.server.TestContextNoIdImpl is missing method getId()");
- }
-
- public void testMissingMethod() {
- testModelWithMethodDecl("Request<String> missingMethod();",
- "Method t.TestContext.missingMethod has no corresponding public method on"
- + " com.google.gwt.requestfactory.server.TestContextImpl");
- }
-
public void testMissingProxyFor() {
testModelWithMethodDeclArgs("Request<TestProxy> okMethodProxy();",
TestContextImpl.class.getName(), null,
- "The t.TestProxy type does not have a @ProxyFor annotation");
+ "The t.TestProxy type does not have a @ProxyFor or @ProxyForName annotation");
}
public void testMissingService() {
@@ -196,13 +150,6 @@
"RequestContext subtype t.TestContext is missing a @Service annotation");
}
- public void testMissingVersion() {
- testModelWithMethodDeclArgs("Request<TestProxy> okMethodProxy();",
- TestContextNoVersionImpl.class.getName(),
- TestContextNoVersionImpl.class.getName(),
- "The class com.google.gwt.requestfactory.server.TestContextNoVersionImpl is missing method getVersion()");
- }
-
public void testModelWithMethodDecl(final String clientMethodDecls,
String... expected) {
testModelWithMethodDeclArgs(clientMethodDecls,
@@ -250,8 +197,8 @@
}
});
- CompilationState state = CompilationStateBuilder
- .buildFrom(logger, javaResources);
+ CompilationState state = CompilationStateBuilder.buildFrom(logger,
+ javaResources);
UnitTestTreeLogger.Builder builder = new UnitTestTreeLogger.Builder();
builder.setLowestLogLevel(TreeLogger.ERROR);
@@ -261,19 +208,14 @@
builder.expectError(RequestFactoryModel.poisonedMessage(), null);
UnitTestTreeLogger testLogger = builder.createLogger();
try {
- new RequestFactoryModel(testLogger,
- state.getTypeOracle().findType("t.TestRequestFactory"));
+ new RequestFactoryModel(testLogger, state.getTypeOracle().findType(
+ "t.TestRequestFactory"));
fail("Should have complained");
} catch (UnableToCompleteException e) {
}
testLogger.assertCorrectLogEntries();
}
- public void testOverloadedMethod() {
- testModelWithMethodDecl("Request<String> overloadedMethod();",
- "Method t.TestContext.overloadedMethod is overloaded on com.google.gwt.requestfactory.server.TestContextImpl");
- }
-
private Set<Resource> getJavaResources(final String proxyClass) {
MockJavaResource[] javaFiles = {new MockJavaResource("t.AddressProxy") {
@Override
@@ -290,7 +232,7 @@
return code;
}
}, new MockJavaResource("java.util.List") {
- // Tests a Driver interface that extends more than RFED
+ // Tests a Driver interface that extends more than RFED
@Override
protected CharSequence getContent() {
StringBuilder code = new StringBuilder();
@@ -300,7 +242,7 @@
return code;
}
}, new MockJavaResource("java.util.Set") {
- // Tests a Driver interface that extends more than RFED
+ // Tests a Driver interface that extends more than RFED
@Override
protected CharSequence getContent() {
StringBuilder code = new StringBuilder();
@@ -310,7 +252,7 @@
return code;
}
}, new MockJavaResource("java.util.SortedSet") {
- // Tests a Driver interface that extends more than RFED
+ // Tests a Driver interface that extends more than RFED
@Override
protected CharSequence getContent() {
StringBuilder code = new StringBuilder();
@@ -323,18 +265,18 @@
Set<Resource> toReturn = new HashSet<Resource>(Arrays.asList(javaFiles));
- toReturn.addAll(Arrays.asList(
- new Resource[]{new EmptyMockJavaResource(Iterable.class),
- new EmptyMockJavaResource(Property.class),
- new EmptyMockJavaResource(EntityProxy.class),
- new EmptyMockJavaResource(InstanceRequest.class),
- new EmptyMockJavaResource(RequestFactory.class),
- new EmptyMockJavaResource(Receiver.class),
+ toReturn.addAll(Arrays.asList(new Resource[] {
+ new EmptyMockJavaResource(Iterable.class),
+ new EmptyMockJavaResource(Property.class),
+ new EmptyMockJavaResource(EntityProxy.class),
+ new EmptyMockJavaResource(InstanceRequest.class),
+ new EmptyMockJavaResource(RequestFactory.class),
+ new EmptyMockJavaResource(Receiver.class),
- new RealJavaResource(Request.class),
- new RealJavaResource(Service.class),
- new RealJavaResource(ProxyFor.class),
- new EmptyMockJavaResource(RequestContext.class),}));
+ new RealJavaResource(Request.class),
+ new RealJavaResource(Service.class),
+ new RealJavaResource(ProxyFor.class),
+ new EmptyMockJavaResource(RequestContext.class),}));
toReturn.addAll(Arrays.asList(JavaResourceBase.getStandardResources()));
return toReturn;
}
diff --git a/user/test/com/google/gwt/requestfactory/server/RequestFactoryInterfaceValidatorTest.java b/user/test/com/google/gwt/requestfactory/server/RequestFactoryInterfaceValidatorTest.java
new file mode 100644
index 0000000..0b1bad7
--- /dev/null
+++ b/user/test/com/google/gwt/requestfactory/server/RequestFactoryInterfaceValidatorTest.java
@@ -0,0 +1,169 @@
+/*
+ * 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.requestfactory.server.RequestFactoryInterfaceValidator.ClassLoaderLoader;
+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.Request;
+import com.google.gwt.requestfactory.shared.RequestContext;
+import com.google.gwt.requestfactory.shared.Service;
+import com.google.gwt.requestfactory.shared.SimpleRequestFactory;
+
+import junit.framework.TestCase;
+
+import java.util.logging.Logger;
+
+/**
+ * JRE tests for {@link RequestFactoryInterfaceValidator}.
+ */
+public class RequestFactoryInterfaceValidatorTest extends TestCase {
+ static class Domain {
+ static int fooStatic(int a) {
+ return 0;
+ }
+
+ int foo(int a) {
+ return 0;
+ }
+ }
+
+ @ProxyFor(Domain.class)
+ interface DomainProxy extends EntityProxy {
+ }
+
+ interface DomainProxyMissingAnnotation extends EntityProxy {
+ }
+
+ static class DomainWithOverloads {
+ void foo() {
+ }
+
+ void foo(int a) {
+ }
+
+ String getId() {
+ return null;
+ }
+
+ int getVersion() {
+ return 0;
+ }
+ }
+
+ @ProxyFor(DomainWithOverloads.class)
+ interface DomainWithOverloadsProxy extends EntityProxy {
+ void foo();
+ }
+
+ class Foo {
+ }
+
+ interface RequestContextMissingAnnotation extends RequestContext {
+ }
+
+ @Service(Domain.class)
+ interface ServiceRequestMismatchedArity extends RequestContext {
+ InstanceRequest<DomainProxy, Integer> foo(int a, int b);
+
+ Request<Integer> fooStatic(int a, int b);
+ }
+
+ @Service(Domain.class)
+ interface ServiceRequestMismatchedParam extends RequestContext {
+ Request<Integer> foo(long a);
+ }
+
+ @Service(Domain.class)
+ interface ServiceRequestMismatchedReturn extends RequestContext {
+ Request<Long> foo(int a);
+ }
+
+ @Service(Domain.class)
+ interface ServiceRequestMismatchedStatic extends RequestContext {
+ Request<Integer> foo(int a);
+
+ InstanceRequest<DomainProxy, Integer> fooStatic(int a);
+ }
+
+ @Service(Domain.class)
+ interface ServiceRequestMissingMethod extends RequestContext {
+ Request<Integer> doesNotExist(int a);
+ }
+
+ RequestFactoryInterfaceValidator v;
+
+ public void testMismatchedArity() {
+ v.validateRequestContext(ServiceRequestMismatchedArity.class.getName());
+ assertTrue(v.isPoisoned());
+ }
+
+ public void testMismatchedParamType() {
+ v.validateRequestContext(ServiceRequestMismatchedParam.class.getName());
+ assertTrue(v.isPoisoned());
+ }
+
+ public void testMismatchedReturnType() {
+ v.validateRequestContext(ServiceRequestMismatchedReturn.class.getName());
+ assertTrue(v.isPoisoned());
+ }
+
+ public void testMismatchedStatic() {
+ v.validateRequestContext(ServiceRequestMismatchedStatic.class.getName());
+ assertTrue(v.isPoisoned());
+ }
+
+ public void testMissingDomainAnnotations() {
+ v.validateEntityProxy(DomainProxyMissingAnnotation.class.getName());
+ assertTrue(v.isPoisoned());
+ }
+
+ public void testMissingServiceAnnotations() {
+ v.validateRequestContext(RequestContextMissingAnnotation.class.getName());
+ assertTrue(v.isPoisoned());
+ }
+
+ public void testMissingIdAndVersion() {
+ v.validateEntityProxy(DomainProxy.class.getName());
+ assertTrue(v.isPoisoned());
+ }
+
+ public void testMissingMethod() {
+ v.validateRequestContext(ServiceRequestMissingMethod.class.getName());
+ assertTrue(v.isPoisoned());
+ }
+
+ public void testOverloadedMethod() {
+ v.validateEntityProxy(DomainWithOverloadsProxy.class.getName());
+ assertTrue(v.isPoisoned());
+ }
+
+ /**
+ * Perform a full test of the RequestFactory used for most tests.
+ */
+ public void testTestCodeFactories() {
+ v.validateRequestFactory(SimpleRequestFactory.class.getName());
+ assertFalse(v.isPoisoned());
+ }
+
+ @Override
+ protected void setUp() throws Exception {
+ Logger logger = Logger.getLogger("");
+ v = new RequestFactoryInterfaceValidator(logger, new ClassLoaderLoader(
+ Thread.currentThread().getContextClassLoader()));
+ }
+}
diff --git a/user/test/com/google/gwt/requestfactory/shared/SimpleBarProxy.java b/user/test/com/google/gwt/requestfactory/shared/SimpleBarProxy.java
index e08a18d..6c8c1ae 100644
--- a/user/test/com/google/gwt/requestfactory/shared/SimpleBarProxy.java
+++ b/user/test/com/google/gwt/requestfactory/shared/SimpleBarProxy.java
@@ -15,13 +15,11 @@
*/
package com.google.gwt.requestfactory.shared;
-import com.google.gwt.requestfactory.server.SimpleBar;
-
/**
* A simple entity used for testing. Has an int field and date field. Add other
* data types as their support gets built in.
*/
-@ProxyFor(SimpleBar.class)
+@ProxyForName("com.google.gwt.requestfactory.server.SimpleBar")
public interface SimpleBarProxy extends EntityProxy {
Boolean getFindFails();
diff --git a/user/test/com/google/gwt/requestfactory/shared/SimpleBarRequest.java b/user/test/com/google/gwt/requestfactory/shared/SimpleBarRequest.java
index 0a8e7f2..f0196ad 100644
--- a/user/test/com/google/gwt/requestfactory/shared/SimpleBarRequest.java
+++ b/user/test/com/google/gwt/requestfactory/shared/SimpleBarRequest.java
@@ -21,7 +21,7 @@
/**
* Do nothing test interface.
*/
-@Service(com.google.gwt.requestfactory.server.SimpleBar.class)
+@ServiceName("com.google.gwt.requestfactory.server.SimpleBar")
public interface SimpleBarRequest extends RequestContext {
Request<Long> countSimpleBar();