Throw OrmConcurrencyException when optimistic locking fails

If an entity is using @RowVersion to protect itself from concurrent
updates we may be able to detect concurrent updates when the UPDATE
SQL returns 0 rows affected.  In such a case the caller may need to
handle the exception specially by retrying the operation.

Signed-off-by: Shawn O. Pearce <sop@google.com>
diff --git a/src/main/java/com/google/gwtorm/client/OrmConcurrencyException.java b/src/main/java/com/google/gwtorm/client/OrmConcurrencyException.java
new file mode 100644
index 0000000..e8da3d3
--- /dev/null
+++ b/src/main/java/com/google/gwtorm/client/OrmConcurrencyException.java
@@ -0,0 +1,30 @@
+// Copyright 2009 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.gwtorm.client;
+
+/** Indicates one or more entities were modified concurrently. */
+public class OrmConcurrencyException extends OrmException {
+  public OrmConcurrencyException() {
+    super("Concurrent modification detected");
+  }
+
+  public OrmConcurrencyException(final String message) {
+    super(message);
+  }
+
+  public OrmConcurrencyException(final String message, final Throwable why) {
+    super(message, why);
+  }
+}
diff --git a/src/main/java/com/google/gwtorm/jdbc/JdbcAccess.java b/src/main/java/com/google/gwtorm/jdbc/JdbcAccess.java
index 4cc35e5..bd08a98 100644
--- a/src/main/java/com/google/gwtorm/jdbc/JdbcAccess.java
+++ b/src/main/java/com/google/gwtorm/jdbc/JdbcAccess.java
@@ -16,6 +16,7 @@
 
 import com.google.gwtorm.client.Access;
 import com.google.gwtorm.client.Key;
+import com.google.gwtorm.client.OrmConcurrencyException;
 import com.google.gwtorm.client.OrmException;
 import com.google.gwtorm.client.impl.AbstractAccess;
 import com.google.gwtorm.client.impl.ListResultSet;
@@ -225,7 +226,7 @@
   }
 
   private static void execute(final PreparedStatement ps, final int cnt)
-      throws SQLException {
+      throws SQLException, OrmConcurrencyException {
     if (cnt == 0) {
       return;
     }
@@ -234,13 +235,9 @@
     if (states == null) {
       throw new SQLException("No rows affected; expected " + cnt + " rows");
     }
-    if (states.length != cnt) {
-      throw new SQLException("Expected " + cnt + " rows affected, received "
-          + states.length + " instead");
-    }
     for (int i = 0; i < cnt; i++) {
-      if (states[i] != 1) {
-        throw new SQLException("Entity " + (i + 1) + " not affected by update");
+      if (states.length <= i || states[i] != 1) {
+        throw new OrmConcurrencyException();
       }
     }
   }
diff --git a/src/test/java/com/google/gwtorm/server/PhoneBookDbTestCase.java b/src/test/java/com/google/gwtorm/server/PhoneBookDbTestCase.java
index ba19836..f9afcfe 100644
--- a/src/test/java/com/google/gwtorm/server/PhoneBookDbTestCase.java
+++ b/src/test/java/com/google/gwtorm/server/PhoneBookDbTestCase.java
@@ -14,6 +14,7 @@
 
 package com.google.gwtorm.server;
 
+import com.google.gwtorm.client.OrmConcurrencyException;
 import com.google.gwtorm.client.OrmException;
 import com.google.gwtorm.client.Transaction;
 import com.google.gwtorm.data.PersonAccess;
@@ -261,10 +262,8 @@
     try {
       schema.people().update(Collections.singleton(bob));
       fail("Update of missing person succeeded");
-    } catch (OrmException e) {
-      assertEquals("Update failure: people", e.getMessage());
-      assertTrue(e.getCause() instanceof SQLException);
-      assertEquals("Entity 1 not affected by update", e.getCause().getMessage());
+    } catch (OrmConcurrencyException e) {
+      assertEquals("Concurrent modification detected", e.getMessage());
     }
   }