Add a utility function for atomic updates

Doing a single-row atomic update is often possible in distributed
databases like Apache Cassandra, but more complex multi-row updates
aren't supported.  Create a variant of updating that makes the
single row case easy to use and identify.

Change-Id: Ibd500d93e5a771e370d78c7f88c10f5d439feb32
Signed-off-by: Shawn O. Pearce <sop@google.com>
diff --git a/src/main/java/com/google/gwtorm/client/Access.java b/src/main/java/com/google/gwtorm/client/Access.java
index 1cf5331..b8d3721 100644
--- a/src/main/java/com/google/gwtorm/client/Access.java
+++ b/src/main/java/com/google/gwtorm/client/Access.java
@@ -178,4 +178,22 @@
    * @throws UnsupportedOperationException no PrimaryKey was declared.
    */
   void delete(Iterable<T> instances, Transaction txn) throws OrmException;
+
+  /**
+   * Atomically update a single entity.
+   * <p>
+   * If the entity does not exist, the method returns {@code null} without
+   * invoking {@code update}.
+   * <p>
+   * If the entity exists, the method invokes {@code update} with a current copy
+   * of the entity. The update function should edit the passed instance
+   * in-place. The return value will be returned to the caller, but is otherwise
+   * ignored by this update function.
+   *
+   * @param key key which identifies the entity.
+   * @param update the update function.
+   * @return the updated copy of the entity; or {@code null}.
+   * @throws OrmException data update failed.
+   */
+  T atomicUpdate(K key, AtomicUpdate<T> update) throws OrmException;
 }
diff --git a/src/main/java/com/google/gwtorm/client/AtomicUpdate.java b/src/main/java/com/google/gwtorm/client/AtomicUpdate.java
new file mode 100644
index 0000000..3ef0554
--- /dev/null
+++ b/src/main/java/com/google/gwtorm/client/AtomicUpdate.java
@@ -0,0 +1,33 @@
+// 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;
+
+/**
+ * Atomically updates exactly one entity.
+ */
+public interface AtomicUpdate<T> {
+  /**
+   * Update the one object, in place.
+   * <p>
+   * This method may be called multiple times, up until the retry limit.
+   *
+   * @param row a fresh copy of the object. The updater should modify it in
+   *        place and return, the caller will attempt to rewrite it into the
+   *        database.
+   * @return return value for the application code calling the atomic update;
+   *         should be either {@code instance} or {@code null}.
+   */
+  T update(T row);
+}
diff --git a/src/main/java/com/google/gwtorm/client/impl/AbstractAccess.java b/src/main/java/com/google/gwtorm/client/impl/AbstractAccess.java
index 56e976b..bd66e34 100644
--- a/src/main/java/com/google/gwtorm/client/impl/AbstractAccess.java
+++ b/src/main/java/com/google/gwtorm/client/impl/AbstractAccess.java
@@ -15,6 +15,7 @@
 package com.google.gwtorm.client.impl;
 
 import com.google.gwtorm.client.Access;
+import com.google.gwtorm.client.AtomicUpdate;
 import com.google.gwtorm.client.Key;
 import com.google.gwtorm.client.OrmException;
 import com.google.gwtorm.client.ResultSet;
diff --git a/src/main/java/com/google/gwtorm/jdbc/JdbcAccess.java b/src/main/java/com/google/gwtorm/jdbc/JdbcAccess.java
index 0304a2e..239d0ec 100644
--- a/src/main/java/com/google/gwtorm/jdbc/JdbcAccess.java
+++ b/src/main/java/com/google/gwtorm/jdbc/JdbcAccess.java
@@ -15,9 +15,12 @@
 package com.google.gwtorm.jdbc;
 
 import com.google.gwtorm.client.Access;
+import com.google.gwtorm.client.AtomicUpdate;
 import com.google.gwtorm.client.Key;
 import com.google.gwtorm.client.OrmConcurrencyException;
 import com.google.gwtorm.client.OrmException;
+import com.google.gwtorm.client.OrmRunnable;
+import com.google.gwtorm.client.Transaction;
 import com.google.gwtorm.client.impl.AbstractAccess;
 import com.google.gwtorm.client.impl.ListResultSet;
 
@@ -217,7 +220,7 @@
         final int[] states = ps.executeBatch();
         if (states == null) {
           inserts = new ArrayList<T>(cnt);
-          for(T o : instances){
+          for (T o : instances) {
             inserts.add(o);
           }
         } else {
@@ -285,6 +288,24 @@
     }
   }
 
+  @Override
+  public T atomicUpdate(final K key, final AtomicUpdate<T> update)
+      throws OrmException {
+    return schema.run(new OrmRunnable<T, JdbcSchema>() {
+      @Override
+      public T run(JdbcSchema db, Transaction txn, boolean retry)
+          throws OrmException {
+        final T obj = get(key);
+        if (obj == null) {
+          return null;
+        }
+        final T res = update.update(obj);
+        update(Collections.singleton(obj), txn);
+        return res;
+      }
+    });
+  }
+
   private OrmException convertError(final String op, final SQLException err) {
     if (err.getCause() == null && err.getNextException() != null) {
       err.initCause(err.getNextException());