Add graceful failure when a client sends a deleted EntityProxy in a request.
Resolves GWT issue 5403.
Patch by: bobv
Review by: rjrjr

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


git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@9034 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/user/src/com/google/gwt/requestfactory/server/DeadEntityException.java b/user/src/com/google/gwt/requestfactory/server/DeadEntityException.java
new file mode 100644
index 0000000..008fe4c
--- /dev/null
+++ b/user/src/com/google/gwt/requestfactory/server/DeadEntityException.java
@@ -0,0 +1,29 @@
+/*
+ * 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;
+
+/**
+ * Indicates the user attempted to perform an operation on an irretrievable
+ * entity
+ */
+public class DeadEntityException extends RuntimeException {
+  public DeadEntityException() {
+  }
+
+  public DeadEntityException(String message) {
+    super(message);
+  }
+}
diff --git a/user/src/com/google/gwt/requestfactory/server/JsonRequestProcessor.java b/user/src/com/google/gwt/requestfactory/server/JsonRequestProcessor.java
index d422b5c..ce0f692 100644
--- a/user/src/com/google/gwt/requestfactory/server/JsonRequestProcessor.java
+++ b/user/src/com/google/gwt/requestfactory/server/JsonRequestProcessor.java
@@ -237,6 +237,9 @@
       JSONObject exceptionResponse = buildExceptionResponse(e.getCause());
       throw new RequestProcessingException("Unexpected exception", e,
           exceptionResponse.toString());
+    } catch (DeadEntityException e) {
+      // This is a normal, if exceptional, condition
+      return buildExceptionResponse(e).toString();
     } catch (Exception e) {
       JSONObject exceptionResponse = buildExceptionResponse(e);
       throw new RequestProcessingException("Unexpected exception", e,
@@ -466,6 +469,10 @@
     Class<?> idType = getIdMethodForEntity(entityType).getReturnType();
     Object entityInstance = getEntityInstance(writeOperation, entityType,
         entityKey.decodedId(idType), idType);
+    if (entityInstance == null) {
+      throw new DeadEntityException(
+          "The requested entity is not available on the server");
+    }
     cachedEntityLookup.put(entityKey, entityInstance);
 
     Iterator<?> keys = recordObject.keys();
diff --git a/user/test/com/google/gwt/requestfactory/client/RequestFactoryTest.java b/user/test/com/google/gwt/requestfactory/client/RequestFactoryTest.java
index 158331b..048745d 100644
--- a/user/test/com/google/gwt/requestfactory/client/RequestFactoryTest.java
+++ b/user/test/com/google/gwt/requestfactory/client/RequestFactoryTest.java
@@ -924,7 +924,7 @@
    */
   public void testNullValueInIntegerListRequest() {
     delayTestFinish(DELAY_TEST_FINISH);
-    List<Integer> list = Arrays.asList(new Integer[]{1, 2, null});
+    List<Integer> list = Arrays.asList(new Integer[] {1, 2, null});
     final Request<Void> fooReq = req.simpleFooRequest().receiveNullValueInIntegerList(
         list);
     fooReq.fire(new Receiver<Void>() {
@@ -940,7 +940,7 @@
    */
   public void testNullValueInStringListRequest() {
     delayTestFinish(DELAY_TEST_FINISH);
-    List<String> list = Arrays.asList(new String[]{"nonnull", "null", null});
+    List<String> list = Arrays.asList(new String[] {"nonnull", "null", null});
     final Request<Void> fooReq = req.simpleFooRequest().receiveNullValueInStringList(
         list);
     fooReq.fire(new Receiver<Void>() {
@@ -1762,7 +1762,7 @@
       @Override
       public void onSuccess(SimpleFooProxy response) {
         assertEquals(2, response.getOneToManyField().size());
-        
+
         // Check lists of proxies returned from a mutable object are mutable
         response = simpleFooRequest().edit(response);
         response.getOneToManyField().get(0).setUserName("canMutate");
@@ -1960,6 +1960,64 @@
     });
   }
 
+  /**
+   * This is analagous to FindServiceTest.testFetchDeletedEntity() only we're
+   * trying to invoke a method on the deleted entity using a stale EntityProxy
+   * reference on the client.
+   */
+  public void testUseOfDeletedEntity() {
+    delayTestFinish(DELAY_TEST_FINISH);
+    SimpleBarRequest context = simpleBarRequest();
+    SimpleBarProxy willDelete = context.create(SimpleBarProxy.class);
+    willDelete.setUserName("A");
+
+    // Persist the newly-created object
+    context.persistAndReturnSelf().using(willDelete).fire(
+        new Receiver<SimpleBarProxy>() {
+          @Override
+          public void onSuccess(SimpleBarProxy response) {
+            assertEquals("A", response.getUserName());
+            // Mark the object as deleted
+            SimpleBarRequest context = simpleBarRequest();
+            response = context.edit(response);
+            response.setFindFails(true);
+            response.setUserName("B");
+            context.persistAndReturnSelf().using(response).fire(
+                new Receiver<SimpleBarProxy>() {
+
+                  @Override
+                  public void onSuccess(SimpleBarProxy response) {
+                    // The last-known state should be returned
+                    assertNotNull(response);
+                    assertEquals("B", response.getUserName());
+
+                    SimpleBarRequest context = simpleBarRequest();
+                    // Ensure attempts to mutate deleted objects don't blow up
+                    response = context.edit(response);
+                    response.setUserName("C");
+
+                    // Attempting to use the now-deleted object should fail
+                    context.persistAndReturnSelf().using(response).fire(
+                        new Receiver<SimpleBarProxy>() {
+                          @Override
+                          public void onFailure(ServerFailure error) {
+                            assertTrue(error.getMessage().contains(
+                                "The requested entity is not available on"
+                                    + " the server"));
+                            finishTestAndReset();
+                          }
+
+                          @Override
+                          public void onSuccess(SimpleBarProxy response) {
+                            fail();
+                          }
+                        });
+                  }
+                });
+          }
+        });
+  }
+
   public void testViolationAbsent() {
     delayTestFinish(DELAY_TEST_FINISH);