Revision: 8938
Author: [email protected]
Date: Tue Oct  5 10:23:33 2010
Log: Original change by Ray Cromwell

Add validation of context methods and proxy against domain objects.
Add verbose diagnostic messages.
Add test cases for RequestFactoryModel.

Review at http://gwt-code-reviews.appspot.com/951801

http://code.google.com/p/google-web-toolkit/source/detail?r=8938

Added:
 /trunk/user/test/com/google/gwt/requestfactory/rebind
 /trunk/user/test/com/google/gwt/requestfactory/rebind/model
/trunk/user/test/com/google/gwt/requestfactory/rebind/model/RequestFactoryModelTest.java
 /trunk/user/test/com/google/gwt/requestfactory/server/TestContextImpl.java
/trunk/user/test/com/google/gwt/requestfactory/server/TestContextNoIdImpl.java /trunk/user/test/com/google/gwt/requestfactory/server/TestContextNoVersionImpl.java
Modified:
 /trunk/user/src/com/google/gwt/editor/rebind/model/ModelUtils.java
/trunk/user/src/com/google/gwt/requestfactory/rebind/model/RequestFactoryModel.java
 /trunk/user/test/com/google/gwt/requestfactory/RequestFactoryJreSuite.java
 /trunk/user/test/com/google/gwt/requestfactory/server/SimpleBar.java
 /trunk/user/test/com/google/gwt/requestfactory/server/SimpleFoo.java
 /trunk/user/test/com/google/gwt/requestfactory/server/SimpleFooString.java

=======================================
--- /dev/null
+++ /trunk/user/test/com/google/gwt/requestfactory/rebind/model/RequestFactoryModelTest.java Tue Oct 5 10:23:33 2010
@@ -0,0 +1,341 @@
+/*
+ * 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.rebind.model;
+
+import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.core.ext.UnableToCompleteException;
+import com.google.gwt.dev.javac.CompilationState;
+import com.google.gwt.dev.javac.CompilationStateBuilder;
+import com.google.gwt.dev.javac.impl.JavaResourceBase;
+import com.google.gwt.dev.javac.impl.MockJavaResource;
+import com.google.gwt.dev.resource.Resource;
+import com.google.gwt.dev.util.UnitTestTreeLogger;
+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;
+import com.google.gwt.requestfactory.shared.Receiver;
+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.impl.Property;
+
+import junit.framework.TestCase;
+
+import java.io.InputStream;
+import java.io.PrintWriter;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.SortedSet;
+
+/**
+ * Test case for {...@link com.google.gwt.requestfactory.rebind.model.RequestFactoryModel}
+ * that uses mock CompilationStates.
+ */
+public class RequestFactoryModelTest extends TestCase {
+
+  /**
+   * Constructs an empty interface representation of a type.
+   */
+  private static class EmptyMockJavaResource extends MockJavaResource {
+
+    private final StringBuilder code = new StringBuilder();
+
+    public EmptyMockJavaResource(Class<?> clazz) {
+      super(clazz.getName());
+
+      code.append("package ").append(clazz.getPackage().getName())
+          .append(";\n");
+      code.append("public interface ").append(clazz.getSimpleName());
+
+      int numParams = clazz.getTypeParameters().length;
+      if (numParams != 0) {
+        code.append("<");
+        for (int i = 0; i < numParams; i++) {
+          if (i != 0) {
+            code.append(",");
+          }
+          code.append("T").append(i);
+        }
+        code.append(">");
+      }
+
+      code.append("{}\n");
+    }
+
+    @Override
+    protected CharSequence getContent() {
+      return code;
+    }
+  }
+
+  /**
+   * Loads the actual source of a type. This should be used only for types
+ * directly tested by this test. Note that use of this class requires your
+   * source files to be on your classpath.
+   */
+  private static class RealJavaResource extends MockJavaResource {
+
+    public RealJavaResource(Class<?> clazz) {
+      super(clazz.getName());
+    }
+
+    @Override
+    protected CharSequence getContent() {
+      String resourceName = getTypeName().replace('.', '/') + ".java";
+      InputStream stream = Thread.currentThread().getContextClassLoader()
+          .getResourceAsStream(resourceName);
+      return Util.readStreamAsString(stream);
+    }
+  }
+
+  private static TreeLogger createCompileLogger() {
+    PrintWriterTreeLogger logger = new PrintWriterTreeLogger(
+        new PrintWriter(System.err, true));
+    logger.setMaxDetail(TreeLogger.ERROR);
+    return logger;
+  }
+
+  private TreeLogger logger;
+
+  @Override
+  public void setUp() throws Exception {
+    super.setUp();
+    logger = createCompileLogger();
+  }
+
+  public void testBadCollectionType() {
+    testModelWithMethodDecl("Request<SortedSet<Integer>> badReturnType();",
+ "Requests that return collections may be declared with java.util.List or java.util.Set only");
+  }
+
+  public void testBadCollectionTypeNotParameterized() {
+    testModelWithMethodDecl("Request<SortedSet> badReturnType();",
+ "Requests that return collections of List or Set must be parameterized");
+  }
+
+  public void testBadReturnType() {
+    testModelWithMethodDecl("Request<Iterable> badReturnType();",
+        "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");
+  }
+
+  public void testMissingService() {
+    testModelWithMethodDeclArgs("Request<String> okMethod();", null,
+        TestContextImpl.class.getName(),
+ "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,
+        TestContextImpl.class.getName(), TestContextImpl.class.getName(),
+        expected);
+  }
+
+  public void testModelWithMethodDeclArgs(final String clientMethodDecls,
+      final String serviceClass, String proxyClass, String... expected) {
+    Set<Resource> javaResources = getJavaResources(proxyClass);
+    javaResources.add(new MockJavaResource("t.TestRequestFactory") {
+      @Override
+      protected CharSequence getContent() {
+        StringBuilder code = new StringBuilder();
+        code.append("package t;\n");
+        code.append("import " + RequestFactory.class.getName() + ";\n");
+ code.append("interface TestRequestFactory extends RequestFactory {\n");
+        code.append("TestContext testContext();");
+        code.append("}");
+        return code;
+      }
+    });
+    javaResources.add(new MockJavaResource("t.TestContext") {
+      @Override
+      protected CharSequence getContent() {
+        StringBuilder code = new StringBuilder();
+        code.append("package t;\n");
+        code.append("import " + Request.class.getName() + ";\n");
+        code.append("import " + InstanceRequest.class.getName() + ";\n");
+
+        code.append("import " + RequestContext.class.getName() + ";\n");
+        code.append("import " + SortedSet.class.getName() + ";\n");
+        code.append("import " + List.class.getName() + ";\n");
+        code.append("import " + Set.class.getName() + ";\n");
+        code.append("import " + Service.class.getName() + ";\n");
+        code.append("import " + TestContextImpl.class.getName() + ";\n");
+
+        if (serviceClass != null) {
+          code.append("@Service(" + serviceClass + ".class)");
+        }
+        code.append("interface TestContext extends RequestContext {\n");
+        code.append(clientMethodDecls);
+        code.append("}");
+        return code;
+      }
+    });
+
+    CompilationState state = CompilationStateBuilder
+        .buildFrom(logger, javaResources);
+
+    UnitTestTreeLogger.Builder builder = new UnitTestTreeLogger.Builder();
+    builder.setLowestLogLevel(TreeLogger.ERROR);
+    for (String expectedMsg : expected) {
+      builder.expectError(expectedMsg, null);
+    }
+    builder.expectError(RequestFactoryModel.poisonedMessage(), null);
+    UnitTestTreeLogger testLogger = builder.createLogger();
+    try {
+      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
+      protected CharSequence getContent() {
+        StringBuilder code = new StringBuilder();
+        code.append("package t;\n");
+        code.append("import " + ProxyFor.class.getName() + ";\n");
+        code.append("import " + EntityProxy.class.getName() + ";\n");
+        if (proxyClass != null) {
+          code.append("@ProxyFor(" + proxyClass + ".class)");
+        }
+        code.append("interface TestProxy extends EntityProxy {\n");
+        code.append("}");
+        return code;
+      }
+    }, new MockJavaResource("java.util.List") {
+      // Tests a Driver interface that extends more than RFED
+      @Override
+      protected CharSequence getContent() {
+        StringBuilder code = new StringBuilder();
+        code.append("package java.util;\n");
+        code.append("public interface List<T> extends Collection<T> {\n");
+        code.append("}");
+        return code;
+      }
+    }, new MockJavaResource("java.util.Set") {
+      // Tests a Driver interface that extends more than RFED
+      @Override
+      protected CharSequence getContent() {
+        StringBuilder code = new StringBuilder();
+        code.append("package java.util;\n");
+        code.append("public interface Set<T> extends Collection<T> {\n");
+        code.append("}");
+        return code;
+      }
+    }, new MockJavaResource("java.util.SortedSet") {
+      // Tests a Driver interface that extends more than RFED
+      @Override
+      protected CharSequence getContent() {
+        StringBuilder code = new StringBuilder();
+        code.append("package java.util;\n");
+        code.append("public interface SortedSet<T> extends Set<T> {\n");
+        code.append("}");
+        return code;
+      }
+    }};
+
+ 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),
+
+            new RealJavaResource(Request.class),
+            new RealJavaResource(Service.class),
+            new RealJavaResource(ProxyFor.class),
+            new EmptyMockJavaResource(RequestContext.class),}));
+ toReturn.addAll(Arrays.asList(JavaResourceBase.getStandardResources()));
+    return toReturn;
+  }
+}
=======================================
--- /dev/null
+++ /trunk/user/test/com/google/gwt/requestfactory/server/TestContextImpl.java Tue Oct 5 10:23:33 2010
@@ -0,0 +1,74 @@
+/*
+ * 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;
+
+/**
+ * Bad service method declarations.
+ */
+public class TestContextImpl {
+
+  public static Class badReturnType() {
+    return null;
+  }
+
+  public static String mismatchedArityStatic(int x, int y, int z) {
+    return null;
+  }
+
+  public static String mismatchedParamType(String param) {
+    return null;
+  }
+
+  public static Integer mismatchedReturnType() {
+    return null;
+  }
+
+  public static String overloadedMethod() {
+    return null;
+  }
+
+  public static String overloadedMethod(String foo) {
+    return null;
+  }
+  public String getId() {
+    return null;
+  }
+
+  public String getVersion() {
+    return null;
+  }
+
+  public String mismatchedArityInstance(int x, int y) {
+    return null;
+  }
+
+  public String mismatchedStatic(String param) {
+    return null;
+  }
+
+  public static String okMethod() {
+    return null;
+  }
+
+  public static TestContextImpl okMethodProxy() {
+    return null;
+  }
+
+  public static String mismatchedNonStatic(String param) {
+    return null;
+  }
+}
+
=======================================
--- /dev/null
+++ /trunk/user/test/com/google/gwt/requestfactory/server/TestContextNoIdImpl.java Tue Oct 5 10:23:33 2010
@@ -0,0 +1,31 @@
+/*
+ * 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;
+
+/**
+ * Bad proxy, no id.
+ */
+public class TestContextNoIdImpl {
+
+  public static TestContextNoIdImpl okMethodProxy() {
+    return null;
+  }
+
+  public String getVersion() {
+    return null;
+  }
+}
+
=======================================
--- /dev/null
+++ /trunk/user/test/com/google/gwt/requestfactory/server/TestContextNoVersionImpl.java Tue Oct 5 10:23:33 2010
@@ -0,0 +1,31 @@
+/*
+ * 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;
+
+/**
+ * Bad proxy, no version.
+ */
+public class TestContextNoVersionImpl {
+
+  public static TestContextNoVersionImpl okMethodProxy() {
+    return null;
+  }
+
+  public String getId() {
+    return null;
+  }
+}
+
=======================================
--- /trunk/user/src/com/google/gwt/editor/rebind/model/ModelUtils.java Fri Oct 1 18:15:55 2010 +++ /trunk/user/src/com/google/gwt/editor/rebind/model/ModelUtils.java Tue Oct 5 10:23:33 2010
@@ -19,11 +19,13 @@
 import com.google.gwt.core.ext.typeinfo.JParameterizedType;
 import com.google.gwt.core.ext.typeinfo.JType;
 import com.google.gwt.core.ext.typeinfo.TypeOracle;
+import com.google.gwt.dev.util.collect.HashMap;

 import java.util.Arrays;
 import java.util.Collections;
 import java.util.Date;
 import java.util.HashSet;
+import java.util.Map;
 import java.util.Set;

 /**
@@ -31,11 +33,27 @@
  */
 public class ModelUtils {

+  static final Map<Class<?>, Class<?>> AUTOBOX_MAP;
+
static final Set<String> VALUE_TYPES = Collections.unmodifiableSet(new HashSet<String>(
       Arrays.asList(Boolean.class.getName(), Character.class.getName(),
Class.class.getName(), Date.class.getName(), Enum.class.getName(), Number.class.getName(), String.class.getName(), Void.class.getName())));

+  static {
+    Map<Class<?>, Class<?>> autoBoxMap = new HashMap<Class<?>, Class<?>>();
+    autoBoxMap.put(byte.class, Byte.class);
+    autoBoxMap.put(char.class, Character.class);
+    autoBoxMap.put(double.class, Double.class);
+    autoBoxMap.put(float.class, Float.class);
+    autoBoxMap.put(int.class, Integer.class);
+    autoBoxMap.put(long.class, Long.class);
+    autoBoxMap.put(short.class, Short.class);
+    autoBoxMap.put(void.class, Void.class);
+    AUTOBOX_MAP = Collections.unmodifiableMap(autoBoxMap);
+  }
+
+
   public static JClassType[] findParameterizationOf(JClassType intfType,
       JClassType subType) {
assert intfType.isAssignableFrom(subType) : subType.getParameterizedQualifiedSourceName()
@@ -69,6 +87,11 @@
     }
     return false;
   }
+
+  public static Class<?> maybeAutobox(Class<?> domainType) {
+    Class<?> autoBoxType = AUTOBOX_MAP.get(domainType);
+    return autoBoxType == null ? domainType : autoBoxType;
+  }

   private ModelUtils() {
   }
=======================================
--- /trunk/user/src/com/google/gwt/requestfactory/rebind/model/RequestFactoryModel.java Fri Oct 1 18:15:55 2010 +++ /trunk/user/src/com/google/gwt/requestfactory/rebind/model/RequestFactoryModel.java Tue Oct 5 10:23:33 2010
@@ -20,6 +20,7 @@
 import com.google.gwt.core.ext.typeinfo.JClassType;
 import com.google.gwt.core.ext.typeinfo.JMethod;
 import com.google.gwt.core.ext.typeinfo.JParameter;
+import com.google.gwt.core.ext.typeinfo.JParameterizedType;
 import com.google.gwt.core.ext.typeinfo.JType;
 import com.google.gwt.core.ext.typeinfo.TypeOracle;
 import com.google.gwt.editor.rebind.model.ModelUtils;
@@ -32,6 +33,8 @@
 import com.google.gwt.requestfactory.shared.RequestFactory;
 import com.google.gwt.requestfactory.shared.Service;

+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
@@ -53,6 +56,10 @@
         requestInterface.getSimpleSourceName(),
         instanceRequestInterface.getSimpleSourceName());
   }
+
+  static String poisonedMessage() {
+ return "Unable to create RequestFactoryModel model due to previous errors";
+  }

   private final TreeLogger logger;
   private final JClassType collectionInterface;
@@ -118,10 +125,10 @@
     }

     if (poisoned) {
-      die("Unable to complete due to previous errors");
+      die(poisonedMessage());
     }
   }
-
+
   public Collection<EntityProxyModel> getAllProxyModels() {
     return Collections.unmodifiableCollection(peers.values());
   }
@@ -150,14 +157,15 @@
    * Examine a RequestContext subtype to populate a ContextMethod.
    */
   private void buildContextMethod(ContextMethod.Builder contextBuilder,
-      JClassType contextType) {
+      JClassType contextType) throws UnableToCompleteException {
     Service serviceAnnotation = contextType.getAnnotation(Service.class);
     if (serviceAnnotation == null) {
       poison("RequestContext subtype %s is missing a @%s annotation",
contextType.getQualifiedSourceName(), Service.class.getSimpleName());
       return;
     }
-    contextBuilder.setServiceClass(serviceAnnotation.value());
+      Class<?> serviceClass = serviceAnnotation.value();
+      contextBuilder.setServiceClass(serviceClass);

     List<RequestMethod> requestMethods = new ArrayList<RequestMethod>();
     for (JMethod method : contextType.getInheritableMethods()) {
@@ -169,7 +177,7 @@
       RequestMethod.Builder methodBuilder = new RequestMethod.Builder();
       methodBuilder.setDeclarationMethod(method);

-      if (!validateContextMethodAndSetDataType(methodBuilder, method)) {
+ if (!validateContextMethodAndSetDataType(methodBuilder, method, serviceClass)) {
         continue;
       }

@@ -184,7 +192,22 @@
     throw new UnableToCompleteException();
   }

-  private EntityProxyModel getEntityProxyType(JClassType entityProxyType) {
+  /**
+   * Return 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 {
     EntityProxyModel toReturn = peers.get(entityProxyType);
     if (toReturn == null) {
EntityProxyModel.Builder inProgress = peerBuilders.get(entityProxyType);
@@ -204,8 +227,12 @@
         poison("The %s type does not have a @%s annotation",
             entityProxyType.getQualifiedSourceName(),
             ProxyFor.class.getSimpleName());
+ // early exit, because further processing causes NPEs in numerous spots
+        die(poisonedMessage());
       } else {
-        builder.setProxyFor(proxyFor.value());
+        Class<?> domainType = proxyFor.value();
+        builder.setProxyFor(domainType);
+        validateDomainType(domainType);
       }

       // Look at the methods declared on the EntityProxy
@@ -237,7 +264,10 @@
           continue;
         }
         validateTransportableType(methodBuilder, transportedType, false);
-        requestMethods.add(methodBuilder.build());
+        RequestMethod requestMethod = methodBuilder.build();
+        if (validateDomainBeanMethod(requestMethod, builder)) {
+          requestMethods.add(requestMethod);
+        }
       }
       builder.setRequestMethods(requestMethods);

@@ -247,6 +277,20 @@
     }
     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));
@@ -257,7 +301,8 @@
* Examine a RequestContext method to see if it returns a transportable type.
    */
   private boolean validateContextMethodAndSetDataType(
-      RequestMethod.Builder methodBuilder, JMethod method) {
+ RequestMethod.Builder methodBuilder, JMethod method, Class<?> serviceClass)
+      throws UnableToCompleteException {
     JClassType requestReturnType = method.getReturnType().isInterface();
     JClassType invocationReturnType;
     if (requestReturnType == null) {
@@ -266,15 +311,45 @@
           instanceRequestInterface));
       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);
@@ -289,21 +364,168 @@

     // Validate the parameters
     boolean paramsOk = true;
-    for (JParameter param : method.getParameters()) {
+    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;
+  }

   /**
    * Examines a type to see if it can be transported.
    */
   private boolean validateTransportableType(
- RequestMethod.Builder methodBuilder, JType type, boolean requireObject) { + RequestMethod.Builder methodBuilder, JType type, boolean requireObject)
+      throws UnableToCompleteException {
     JClassType transportedClass = type.isClassOrInterface();
     if (transportedClass == null) {
       if (requireObject) {
@@ -324,9 +546,14 @@
       methodBuilder.setEntityType(getEntityProxyType(transportedClass));
     } else if (collectionInterface.isAssignableFrom(transportedClass)) {
       // Only allow certain collections for now
- if (listInterface.equals(transportedClass.isParameterized().getBaseType())) { + JParameterizedType parameterized = transportedClass.isParameterized();
+      if (parameterized == null) {
+ poison("Requests that return collections of List or Set must be parameterized");
+        return false;
+      }
+      if (listInterface.equals(parameterized.getBaseType())) {
         methodBuilder.setCollectionType(CollectionType.LIST);
- } else if (setInterface.equals(transportedClass.isParameterized().getBaseType())) {
+      } else if (setInterface.equals(parameterized.getBaseType())) {
         methodBuilder.setCollectionType(CollectionType.SET);
       } else {
         poison("Requests that return collections may be declared with"
=======================================
--- /trunk/user/test/com/google/gwt/requestfactory/RequestFactoryJreSuite.java Sun Oct 3 11:34:32 2010 +++ /trunk/user/test/com/google/gwt/requestfactory/RequestFactoryJreSuite.java Tue Oct 5 10:23:33 2010
@@ -16,6 +16,7 @@
 package com.google.gwt.requestfactory;

 import com.google.gwt.requestfactory.client.impl.SimpleEntityProxyIdTest;
+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.RequestPropertyTest;
@@ -33,6 +34,7 @@
     suite.addTestSuite(SimpleEntityProxyIdTest.class);
     suite.addTestSuite(JsonRequestProcessorTest.class);
     suite.addTestSuite(ReflectionBasedOperationRegistryTest.class);
+    suite.addTestSuite(RequestFactoryModelTest.class);
     suite.addTestSuite(RequestPropertyTest.class);
     return suite;
   }
=======================================
--- /trunk/user/test/com/google/gwt/requestfactory/server/SimpleBar.java Mon Oct 4 15:35:00 2010 +++ /trunk/user/test/com/google/gwt/requestfactory/server/SimpleBar.java Tue Oct 5 10:23:33 2010
@@ -80,7 +80,7 @@
Map<String, SimpleBar> value = (Map<String, SimpleBar>) req.getSession().getAttribute(
           SimpleBar.class.getCanonicalName());
       if (value == null) {
-        value = reset();
+        value = resetImpl();
       }
       return value;
     }
@@ -95,7 +95,11 @@
     return toReturn;
   }

-  public static synchronized Map<String, SimpleBar> reset() {
+  public static void reset() {
+    resetImpl();
+  }
+
+  public static synchronized Map<String, SimpleBar> resetImpl() {
     Map<String, SimpleBar> instance = new HashMap<String, SimpleBar>();
     // fixtures
     SimpleBar s1 = new SimpleBar();
=======================================
--- /trunk/user/test/com/google/gwt/requestfactory/server/SimpleFoo.java Tue Oct 5 09:11:00 2010 +++ /trunk/user/test/com/google/gwt/requestfactory/server/SimpleFoo.java Tue Oct 5 10:23:33 2010
@@ -97,7 +97,7 @@
Map<Long, SimpleFoo> value = (Map<Long, SimpleFoo>) req.getSession().getAttribute(
           SimpleFoo.class.getCanonicalName());
       if (value == null) {
-        value = reset();
+        value = resetImpl();
       }
       return value;
     }
@@ -232,7 +232,11 @@
     }
   }

-  public static synchronized Map<Long, SimpleFoo> reset() {
+  public static void reset() {
+    resetImpl();
+  }
+
+  public static synchronized Map<Long, SimpleFoo> resetImpl() {
     Map<Long, SimpleFoo> instance = new HashMap<Long, SimpleFoo>();
     // fixtures
     SimpleFoo s1 = new SimpleFoo();
=======================================
--- /trunk/user/test/com/google/gwt/requestfactory/server/SimpleFooString.java Mon Oct 4 15:35:00 2010 +++ /trunk/user/test/com/google/gwt/requestfactory/server/SimpleFooString.java Tue Oct 5 10:23:33 2010
@@ -73,7 +73,7 @@
SimpleFooString value = (SimpleFooString) req.getSession().getAttribute(
           SimpleFooString.class.getCanonicalName());
       if (value == null) {
-        value = reset();
+        value = resetImpl();
       }
       return value;
     }
@@ -99,7 +99,11 @@
     return get();
   }

-  public static synchronized SimpleFooString reset() {
+  public static void reset() {
+    resetImpl();
+  }
+
+  public static synchronized SimpleFooString resetImpl() {
     SimpleFooString instance = new SimpleFooString();
     HttpServletRequest req = RequestFactoryServlet.getThreadLocalRequest();
     if (req == null) {

--
http://groups.google.com/group/Google-Web-Toolkit-Contributors

Reply via email to