Revision: 8898
Author: [email protected]
Date: Wed Sep 29 21:51:38 2010
Log: Add verbose validation and diagnostics to RequestFactoryGenerator
Review at http://gwt-code-reviews.appspot.com/929801
http://code.google.com/p/google-web-toolkit/source/detail?r=8898
Modified:
/trunk/user/src/com/google/gwt/requestfactory/rebind/RequestFactoryGenerator.java
/trunk/user/src/com/google/gwt/requestfactory/shared/impl/TypeLibrary.java
/trunk/user/test/com/google/gwt/requestfactory/shared/SimpleFooRequest.java
/trunk/user/test/com/google/gwt/requestfactory/shared/SimpleFooStringRequest.java
=======================================
---
/trunk/user/src/com/google/gwt/requestfactory/rebind/RequestFactoryGenerator.java
Wed Sep 29 11:01:14 2010
+++
/trunk/user/src/com/google/gwt/requestfactory/rebind/RequestFactoryGenerator.java
Wed Sep 29 21:51:38 2010
@@ -56,18 +56,24 @@
import
com.google.gwt.requestfactory.server.ReflectionBasedOperationRegistry;
import com.google.gwt.requestfactory.shared.EntityProxy;
import com.google.gwt.requestfactory.shared.EntityProxyId;
+import com.google.gwt.requestfactory.shared.Instance;
+import com.google.gwt.requestfactory.shared.ProxyFor;
import com.google.gwt.requestfactory.shared.Request;
import com.google.gwt.requestfactory.shared.RequestFactory;
+import com.google.gwt.requestfactory.shared.Service;
import com.google.gwt.requestfactory.shared.WriteOperation;
import com.google.gwt.requestfactory.shared.impl.CollectionProperty;
import com.google.gwt.requestfactory.shared.impl.EnumProperty;
import com.google.gwt.requestfactory.shared.impl.Property;
import com.google.gwt.requestfactory.shared.impl.RequestData;
+import com.google.gwt.requestfactory.shared.impl.TypeLibrary;
import com.google.gwt.user.rebind.ClassSourceFileComposerFactory;
import com.google.gwt.user.rebind.SourceWriter;
import java.beans.Introspector;
import java.io.PrintWriter;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.ArrayList;
@@ -113,6 +119,12 @@
return requestClass;
}
}
+
+ private static class DiagnosticException extends Exception {
+ DiagnosticException(String s) {
+ super(s);
+ }
+ }
private static class EntityProperty {
private final String name;
@@ -131,10 +143,14 @@
return type;
}
}
+
+ private JClassType findRequestType;
private JClassType listType;
private JClassType setType;
private JClassType entityProxyType;
+ private Set<JType> allowedTypes = new HashSet<JType>();
+ private Set<JType> allowedRequestParamTypes = new HashSet<JType>();
private JClassType mainRequestType;
private final Set<JClassType> generatedProxyTypes = new
HashSet<JClassType>();
@@ -148,6 +164,28 @@
listType = typeOracle.findType(List.class.getName());
setType = typeOracle.findType(Set.class.getName());
entityProxyType = typeOracle.findType(EntityProxy.class.getName());
+ findRequestType = typeOracle.findType(FindRequest.class.getName());
+ for (Class<?> type : TypeLibrary.VALUE_TYPES) {
+ allowedTypes.add(typeOracle.findType(type.getName()));
+ }
+
+ allowedTypes.add(JPrimitiveType.VOID);
+ allowedTypes.add(typeOracle.findType("java.lang.Void"));
+ allowedTypes.add(listType.getErasedType());
+ allowedTypes.add(setType.getErasedType());
+ allowedTypes.add(entityProxyType.getErasedType());
+
allowedTypes.add(typeOracle.findType(EntityProxyId.class.getName()).getErasedType());
+
+ allowedRequestParamTypes.addAll(allowedTypes);
+ allowedRequestParamTypes.add(JPrimitiveType.INT);
+ allowedRequestParamTypes.add(JPrimitiveType.BYTE);
+ allowedRequestParamTypes.add(JPrimitiveType.DOUBLE);
+ allowedRequestParamTypes.add(JPrimitiveType.FLOAT);
+ allowedRequestParamTypes.add(JPrimitiveType.LONG);
+ allowedRequestParamTypes.add(JPrimitiveType.SHORT);
+ allowedRequestParamTypes.add(JPrimitiveType.INT);
+ allowedRequestParamTypes.add(JPrimitiveType.CHAR);
+
mainRequestType = typeOracle.findType(Request.class.getName());
// Get a reference to the type that the generator should implement
@@ -224,6 +262,71 @@
}
return entityProperties;
+ }
+
+ private JClassType decodeToBaseType(JType type) {
+ if (type.isTypeParameter() != null) {
+ return type.isTypeParameter().getBaseType();
+ }
+ return type.isClassOrInterface();
+ }
+
+ private void diagnosticIf(boolean cond, String fmt, Object... args)
+ throws DiagnosticException {
+ if (cond) {
+ throw new DiagnosticException(String.format(fmt, args));
+ }
+ }
+
+ private void diagnosticIfBannedType(JType origType, String entityName,
+ String methodName) throws DiagnosticException {
+ diagnosticIfBannedType(allowedTypes, origType, entityName, methodName);
+ }
+
+ /*
+ * No primitives other than void, no arrays, no concrete collections,
+ * no maps, no unknown value types.
+ */
+ private void diagnosticIfBannedType(Set<JType> allowed, JType origType,
+ String entityName, String methodName) throws DiagnosticException {
+ diagnosticIf(origType.isArray() != null,
+ "Method %s.%s: signature not permitted to use array type",
+ entityName, methodName);
+ JClassType cType = decodeToBaseType(origType);
+ if (cType == null) {
+ return;
+ }
+
+ diagnosticIf(isSubtype(setType, cType) || isSubtype(listType, cType),
+ "Method %s.%s: signature must use Set or List, not subtypes",
+ entityName, methodName);
+ // not an allowed value type, not a proxy, and not an enum
+ diagnosticIf(!allowed.contains(cType.getErasedType())
+ && !entityProxyType.isAssignableFrom(cType)
+ && origType.isEnum() == null,
+ "Method %s.%s: signature uses unsupported type %s",
+ entityName, methodName, origType.getQualifiedBinaryName());
+
+ if (isSupportedCollectionType(cType)) {
+ diagnosticIf(origType.isParameterized() == null,
+ "Method %s.%s: collections must have type parameters specified",
+ entityName, methodName);
+ JClassType typeArg = origType.isParameterized().getTypeArgs()[0];
+ if (isSupportedCollectionType(typeArg)) {
+ diagnosticIf(true, "Method %s.%s: signature cannot use collection"
+ + " which contain other collection", entityName, methodName);
+ }
+ // check for value types and arrays
+ diagnosticIfBannedType(typeArg, entityName, methodName);
+ }
+ }
+
+ /*
+ * Primitives permitted for request params.
+ */
+ private void diagnosticIfBannedTypeRequestParam(JType origType, String
entityName, String name)
+ throws DiagnosticException {
+ diagnosticIfBannedType(allowedRequestParamTypes, origType, entityName,
name);
};
private void ensureProxyType(TreeLogger logger,
@@ -235,12 +338,22 @@
// handle publicProxyType = List<EmployeeProxy>
publicProxyType = publicProxyType.isParameterized().getTypeArgs()[0];
}
- if (!publicProxyType.isAssignableTo(entityProxyType)) {
+ // don't generate proxies for the EntityProxy impl itself
+ if (!publicProxyType.isAssignableTo(entityProxyType)
+ || publicProxyType.equals(entityProxyType)) {
return;
}
if (generatedProxyTypes.contains(publicProxyType)) {
return;
}
+
+ try {
+ validateProxyType(publicProxyType, typeOracle);
+ } catch (DiagnosticException e) {
+ logger.log(TreeLogger.ERROR, e.getMessage());
+ throw new UnableToCompleteException();
+ }
+
// Hack
if ("P".equals(publicProxyType.getName())) {
return;
@@ -370,9 +483,10 @@
sw.println("}");
/*
* Because a Proxy A may relate to B which relates to C, we need to
- * ensure transitively.
+ * ensure transitively. But skip EnityProxy itself.
*/
- if (isProxyType(returnType)) {
+ if (!returnType.equals(entityProxyType)
+ && isProxyType(returnType)) {
transitiveDeps.add(returnType);
}
}
@@ -388,6 +502,27 @@
ensureProxyType(logger, generatorContext, type);
}
}
+
+ private List<JMethod> findMethods(JClassType domainType, String name) {
+ List<JMethod> toReturn = new ArrayList<JMethod>();
+ for (JMethod meth : domainType.getOverridableMethods()) {
+ if (meth.getName().equals(name) && meth.isPublic()) {
+ toReturn.add(meth);
+ }
+ }
+ return toReturn;
+ }
+
+ private List<Method> findMethods(Class<?> domainType, String name) {
+ List<Method> toReturn = new ArrayList<Method>();
+ for (Method meth : domainType.getDeclaredMethods()) {
+ if (meth.getName().equals(name)
+ && (meth.getModifiers() & Modifier.PUBLIC) != 0) {
+ toReturn.add(meth);
+ }
+ }
+ return toReturn;
+ }
private void generateOnce(JClassType requestFactoryType, TreeLogger
logger,
GeneratorContext generatorContext, PrintWriter out,
@@ -412,12 +547,12 @@
sw.println();
// Find the request builder methods
-
// TODO allow getRequest type methods to live directly on the factory,
w/o
// requiring a builder to get to them
Set<JMethod> requestBuilders = new LinkedHashSet<JMethod>();
for (JMethod method : interfaceType.getOverridableMethods()) {
+ // skip validating methods in EntityProxy supertype
if (method.getEnclosingType().equals(requestFactoryType)) {
continue;
}
@@ -527,8 +662,13 @@
PrintWriter pw = generatorContext.tryCreate(logger,
nestedImplPackage,
nestedImplName);
if (pw != null) {
- generateRequestSelectorImplementation(logger, generatorContext, pw,
- requestSelector, interfaceType, nestedImplPackage,
nestedImplName);
+ try {
+ generateRequestSelectorImplementation(logger, generatorContext,
pw,
+ requestSelector, interfaceType, nestedImplPackage,
nestedImplName);
+ } catch (DiagnosticException e) {
+ logger.log(TreeLogger.ERROR, e.getMessage());
+ throw new UnableToCompleteException();
+ }
}
}
@@ -627,7 +767,7 @@
private void generateRequestSelectorImplementation(TreeLogger logger,
GeneratorContext generatorContext, PrintWriter out,
JMethod selectorMethod, JClassType mainType, String packageName,
- String implName) throws UnableToCompleteException {
+ String implName) throws UnableToCompleteException,
DiagnosticException {
JClassType selectorInterface =
selectorMethod.getReturnType().isInterface();
logger = logger.branch(TreeLogger.DEBUG, String.format(
"Generating implementation of %s", selectorInterface.getName()));
@@ -654,6 +794,9 @@
// write each method.
for (JMethod method : selectorInterface.getOverridableMethods()) {
+ if (method.getEnclosingType().equals(entityProxyType)) {
+ continue;
+ }
JParameterizedType parameterizedType =
method.getReturnType().isParameterized();
if (parameterizedType == null) {
logger.log(
@@ -663,7 +806,11 @@
method.getName(), selectorInterface.getName()));
throw new UnableToCompleteException();
}
+
JClassType returnType = parameterizedType.getTypeArgs()[0];
+ // check if for Request<T>, we support type T
+ diagnosticIfBannedType(returnType, selectorInterface.getName(),
+ method.getName());
ensureProxyType(logger, generatorContext, returnType);
@@ -675,25 +822,53 @@
TypeOracle typeOracle = generatorContext.getTypeOracle();
String extraArgs = "";
- // TODO: refactor this into some kind of extensible map lookup
- // check for ProxyListRequest<T> or ProxySetRequest<T>
- if (method.getReturnType().isArray() != null) {
- logger.log(TreeLogger.ERROR, String.format(
- "Illegal return type for %s. Methods of %s cannot return
array.",
- method.getName(), selectorInterface.getName()));
- throw new UnableToCompleteException();
- }
+
for (JParameter param : method.getParameters()) {
- if (param.getType().isArray() != null) {
- logger.log(
- TreeLogger.ERROR,
- String.format(
- "Illegal param type %s for %s. Methods of %s cannot take
array parameters.",
- param.getName(), method.getName(),
- selectorInterface.getName()));
- throw new UnableToCompleteException();
- }
- }
+ diagnosticIfBannedTypeRequestParam(param.getType(),
selectorInterface.getName(),
+ method.getName());
+ }
+
+ Instance instanceAnn = method.getAnnotation(Instance.class);
+ JParameter[] params = method.getParameters();
+
+ // assert first arg must be entity if it's an @Instance
+ diagnosticIf(instanceAnn != null && (params.length == 0
+ || !entityProxyType.isAssignableFrom(
+ params[0].getType().isClassOrInterface())),
+ "Message %s.s: @Instance method, but first parameter not an
EntityProxy",
+ selectorInterface.getName(), method.getName());
+
+ // instance method verification
+ if (instanceAnn != null) {
+ // assert domain method must be non-static for @Instance
+ Class<?> domainType =
params[0].getType().isClassOrInterface().getAnnotation(ProxyFor.class).value();
+
+ List<Method> domainMethods = findMethods(domainType,
method.getName());
+ diagnosticIf(domainMethods.size() == 0,
+ "Instance Method %s.%s not found on type %s",
+ selectorInterface.getName(), method.getName(),
domainType.getName());
+ diagnosticIf(domainMethods.size() > 1,
+ "Instance Method %s.%s is overloaded, unsupported feature",
+ selectorInterface.getName(), method.getName());
+ diagnosticIf((domainMethods.get(0).getModifiers() &
Modifier.STATIC) != 0,
+ "Instance Method %s.%s is declared static on %s",
+ selectorInterface.getName(), method.getName(),
domainType.getName());
+ } else {
+ // static method verification
+ Service ann = selectorInterface.getAnnotation(Service.class);
+ Class<?> domainType = ann.value();
+ List<Method> domainMethods = findMethods(domainType,
method.getName());
+ diagnosticIf(domainMethods.size() == 0,
+ "Static Method %s.%s not found on type %s",
+ selectorInterface.getName(), method.getName(),
domainType.getName());
+ diagnosticIf(domainMethods.size() > 1,
+ "Static Method %s.%s is overloaded, unsupported feature",
+ selectorInterface.getName(), method.getName());
+ diagnosticIf((domainMethods.get(0).getModifiers() &
Modifier.STATIC) == 0,
+ "Static Method %s.%s is not declared static on %s",
+ selectorInterface.getName(), method.getName(),
domainType.getName());
+ }
+
if (isRequestObjectCollectionRequest(typeOracle, requestType)) {
Class<?> colType = getCollectionType(typeOracle, requestType);
assert colType != null;
@@ -972,6 +1147,16 @@
private boolean isStringRequest(TypeOracle typeOracle, JClassType
requestType) {
return
requestType.isParameterized().getTypeArgs()[0].isAssignableTo(typeOracle.findType(String.class.getName()));
}
+
+ private boolean isSubtype(JClassType baseType, JClassType type) {
+ return baseType.isAssignableFrom(type.isClassOrInterface())
+ && !matchesErasedType(baseType, type);
+ }
+
+ private boolean isSupportedCollectionType(JClassType cType) {
+ return setType.isAssignableFrom(cType)
+ || listType.isAssignableFrom(cType);
+ }
private boolean isValueListRequest(JClassType requestType) {
JClassType retType = requestType.isParameterized().getTypeArgs()[0];
@@ -987,6 +1172,10 @@
private boolean isVoidRequest(TypeOracle typeOracle, JClassType
requestType) {
return
requestType.isParameterized().getTypeArgs()[0].isAssignableTo(typeOracle.findType(Void.class.getName()));
}
+
+ private boolean matchesErasedType(JClassType type1, JClassType type2) {
+ return type1.getErasedType().equals(type2.getErasedType());
+ }
/**
* Returns an {...@link EntityProperty} if the method looks like a bean
property
@@ -1028,6 +1217,23 @@
return null;
}
+
+ private String maybeDecodeProxyTypeNameFor(JType proxyParam, TypeOracle
typeOracle)
+ throws DiagnosticException {
+ JClassType cType = decodeToBaseType(proxyParam);
+
+ if (cType != null && entityProxyType.isAssignableFrom(cType)) {
+ ProxyFor pFor = proxyParam.isClassOrInterface().getAnnotation(
+ ProxyFor.class);
+ diagnosticIf(pFor == null,
+ "Missing @ProxyFor annotation on type %s",
+ proxyParam.getQualifiedBinaryName());
+ if (pFor != null) {
+ return pFor.value().getName();
+ }
+ }
+ return proxyParam.getQualifiedBinaryName();
+ }
private boolean mayContainEntityProxy(JType paramType) {
JClassType paramClass = paramType.isClassOrInterface();
@@ -1159,4 +1365,78 @@
}
return qualifiedSourceName;
}
-}
+
+ /**
+ * Enforce design rules for EntityProxy, report diagnostics.
+ */
+ @SuppressWarnings("unchecked")
+ private void validateProxyType(JClassType entityProxyType,
+ TypeOracle typeOracle) throws DiagnosticException {
+ // deal with stuff like Request<T extends FooProxy>
+ entityProxyType = decodeToBaseType(entityProxyType);
+ // skip validating base interface
+ if (this.entityProxyType.equals(entityProxyType)) {
+ return;
+ }
+
+ // 1. EntityProxy must have @ProxyFor annotation
+ ProxyFor proxyFor = entityProxyType.getAnnotation(ProxyFor.class);
+ String entityName = entityProxyType.getName();
+ diagnosticIf(proxyFor == null,
+ "Proxy %s is missing @ProxyFor annotation", entityName);
+
+ // 2. find domain object
+ Class<?> domainType = proxyFor.value();
+ String domainName = domainType.getName();
+
+ // assert domain class has getId() method
+ diagnosticIf(findMethods(domainType, "getId").size() == 0,
+ "Domain classes %s missing getId() method for proxy %s",
+ domainName, entityName);
+
+ for (JMethod method : entityProxyType.getOverridableMethods()) {
+ // skip non-bean methods
+ if (!method.getName().startsWith("get")
+ && !method.getName().startsWith("set")) {
+ continue;
+ }
+ // 2. Method must exist on domain object
+ List<Method> domainMethods = findMethods(domainType,
method.getName());
+ diagnosticIf(domainMethods.size() == 0,
+ "Method %s on %s does not exist, but present in proxy %s",
+ method.getName(), domainName, entityName);
+ diagnosticIf(domainMethods.size() > 1,
+ "Method %s on %s is overridden, this is currently unsupported",
+ method.getName(), domainName);
+
+ Method domainMethod = domainMethods.get(0);
+ // 3. type signature must agree starting with return type
+ diagnosticIf(!maybeDecodeProxyTypeNameFor(method.getReturnType(),
+ typeOracle).equals(domainMethod.getReturnType().getName()),
+ "Return type of %s.%s doesn't match that of %s.%s",
+ entityName, method.getName(), domainMethod.getName());
+
+ JType proxyReturnType = method.getReturnType();
+
+ // can't be non-void primitive, array, or unsupported collection type
+ diagnosticIfBannedType(proxyReturnType, entityName,
method.getName());
+
+ JParameter[] proxyParams = method.getParameters();
+ Class<?>[] domainParams = domainMethod.getParameterTypes();
+
+ // # params and param types must agree
+ diagnosticIf(proxyParams.length != domainParams.length,
+ "Parameters do not match between %s.s% and %s.%s",
+ entityName, method.getName(), domainName,
domainMethod.getName());
+ for (int i = 0; i < proxyParams.length; i++) {
+ diagnosticIf(!maybeDecodeProxyTypeNameFor(proxyParams[i].getType(),
+ typeOracle).equals(
+ domainParams[i].getName()),
+ "Parameters do not match between %s.s% and %s.%s",
+ entityName, method.getName(), domainName,
domainMethod.getName());
+ diagnosticIfBannedType(proxyParams[i].getType(), method.getName(),
+ entityName);
+ }
+ }
+ }
+}
=======================================
---
/trunk/user/src/com/google/gwt/requestfactory/shared/impl/TypeLibrary.java
Wed Sep 22 04:32:28 2010
+++
/trunk/user/src/com/google/gwt/requestfactory/shared/impl/TypeLibrary.java
Wed Sep 29 21:51:38 2010
@@ -33,7 +33,7 @@
*/
public class TypeLibrary {
- static final Collection<Class<?>> VALUE_TYPES;
+ public static final Collection<Class<?>> VALUE_TYPES;
static {
HashSet<Class<?>> valueTypes = new HashSet<Class<?>>();
=======================================
---
/trunk/user/test/com/google/gwt/requestfactory/shared/SimpleFooRequest.java
Tue Sep 28 08:39:15 2010
+++
/trunk/user/test/com/google/gwt/requestfactory/shared/SimpleFooRequest.java
Wed Sep 29 21:51:38 2010
@@ -46,8 +46,6 @@
@Instance
Request<SimpleFooProxy> persistAndReturnSelf(SimpleFooProxy proxy);
- Request<Integer> privateMethod();
-
Request<Boolean> processBooleanList(List<Boolean> values);
Request<Date> processDateList(List<Date> values);
=======================================
---
/trunk/user/test/com/google/gwt/requestfactory/shared/SimpleFooStringRequest.java
Tue Sep 28 08:39:15 2010
+++
/trunk/user/test/com/google/gwt/requestfactory/shared/SimpleFooStringRequest.java
Wed Sep 29 21:51:38 2010
@@ -31,8 +31,6 @@
Request<SimpleFooStringProxy> findSimpleFooStringById(String id);
- Request<Integer> privateMethod();
-
@Instance
Request<Void> persist(SimpleFooStringProxy proxy);
--
http://groups.google.com/group/Google-Web-Toolkit-Contributors