Add support for RowVersion columns for optimistic locking

The row version is necessary to implement optimistic locking at the
application level, where an entity is read and then later updated.
If the row version differs in the data store we know that changes
were made.

Signed-off-by: Shawn O. Pearce <sop@google.com>
diff --git a/src/com/google/gwtorm/client/RowVersion.java b/src/com/google/gwtorm/client/RowVersion.java
new file mode 100644
index 0000000..3cae45c
--- /dev/null
+++ b/src/com/google/gwtorm/client/RowVersion.java
@@ -0,0 +1,36 @@
+// Copyright 2008 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;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Annotation marking a field as the row version used for optimistic locking.
+ * <p>
+ * Fields marked with <code>RowVersion</code> must also be marked with
+ * {@link Column} and must be of type <code>int</code>. The field will be
+ * automatically incremented during INSERT and UPDATE operations, and will be
+ * tested during UPDATE and DELETE operations. Concurrent modifications of the
+ * same entity fail as the row version won't match.
+ * <p>
+ * At most one RowVersion annotation should appear in any entity.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.FIELD)
+public @interface RowVersion {
+}
diff --git a/src/com/google/gwtorm/jdbc/gen/AccessGen.java b/src/com/google/gwtorm/jdbc/gen/AccessGen.java
index 3a1e8b2..2fd4c60 100644
--- a/src/com/google/gwtorm/jdbc/gen/AccessGen.java
+++ b/src/com/google/gwtorm/jdbc/gen/AccessGen.java
@@ -36,6 +36,7 @@
 import java.sql.PreparedStatement;
 import java.sql.ResultSet;
 import java.sql.SQLException;
+import java.util.ArrayList;
 import java.util.Iterator;
 import java.util.List;
 
@@ -270,8 +271,22 @@
 
     final CodeGenSupport cgs = new CodeGenSupport(mv);
     cgs.setEntityType(entityType);
+
+    for (final ColumnModel col : model.getRowVersionColumns()) {
+      cgs.setFieldReference(col);
+      cgs.fieldSetBegin();
+      cgs.pushFieldValue();
+      mv.visitInsn(ICONST_1);
+      mv.visitInsn(IADD);
+      cgs.fieldSetEnd();
+    }
+    cgs.resetColumnIndex(0);
+
     if (type != DmlType.DELETE) {
-      for (final ColumnModel field : model.getDependentFields()) {
+      final List<ColumnModel> cols = new ArrayList<ColumnModel>();
+      cols.addAll(model.getDependentFields());
+      cols.addAll(model.getRowVersionFields());
+      for (final ColumnModel field : cols) {
         if (field.isNested() && field.getColumnAnnotation().notNull()) {
           for (final ColumnModel c : field.getAllLeafColumns()) {
             cgs.setFieldReference(c);
@@ -311,6 +326,17 @@
       cgs.setFieldReference(col);
       dialect.getSqlTypeInfo(col).generatePreparedStatementSet(cgs);
     }
+    if (type != DmlType.INSERT) {
+      for (final ColumnModel col : model.getRowVersionColumns()) {
+        cgs.setFieldReference(col);
+        cgs.pushSqlHandle();
+        cgs.pushColumnIndex();
+        cgs.pushFieldValue();
+        mv.visitInsn(ICONST_1);
+        mv.visitInsn(ISUB);
+        cgs.invokePreparedStatementSet("Int");
+      }
+    }
 
     mv.visitInsn(RETURN);
     mv.visitMaxs(-1, -1);
@@ -348,7 +374,11 @@
       cgs.resetColumnIndex(oldIdx);
     }
 
-    for (final ColumnModel field : model.getDependentFields()) {
+    final List<ColumnModel> cols = new ArrayList<ColumnModel>();
+    cols.addAll(model.getDependentFields());
+    cols.addAll(model.getRowVersionFields());
+    cols.addAll(model.getPrimaryKeyColumns());
+    for (final ColumnModel field : cols) {
       if (field.isNested()) {
         int oldIdx = cgs.getColumnIndex();
         final Type vType = CodeGenSupport.toType(field);
@@ -387,10 +417,6 @@
         dialect.getSqlTypeInfo(field).generateResultSetGet(cgs);
       }
     }
-    for (final ColumnModel col : model.getPrimaryKeyColumns()) {
-      cgs.setFieldReference(col);
-      dialect.getSqlTypeInfo(col).generateResultSetGet(cgs);
-    }
 
     mv.visitInsn(RETURN);
     mv.visitMaxs(-1, -1);
diff --git a/src/com/google/gwtorm/schema/ColumnModel.java b/src/com/google/gwtorm/schema/ColumnModel.java
index 754bb8a..6a56949 100644
--- a/src/com/google/gwtorm/schema/ColumnModel.java
+++ b/src/com/google/gwtorm/schema/ColumnModel.java
@@ -27,6 +27,7 @@
   protected String columnName;
   protected Column column;
   protected Collection<ColumnModel> nestedColumns;
+  protected boolean rowVersion;
   protected boolean inPrimaryKey;
 
   protected ColumnModel() {
@@ -120,6 +121,10 @@
     return getPrimitiveType() == null;
   }
 
+  public boolean isRowVersion() {
+    return rowVersion;
+  }
+
   public Column getColumnAnnotation() {
     return column;
   }
diff --git a/src/com/google/gwtorm/schema/RelationModel.java b/src/com/google/gwtorm/schema/RelationModel.java
index a8e8018..1d8a004 100644
--- a/src/com/google/gwtorm/schema/RelationModel.java
+++ b/src/com/google/gwtorm/schema/RelationModel.java
@@ -25,6 +25,7 @@
 import java.util.Collections;
 import java.util.Iterator;
 import java.util.LinkedHashMap;
+import java.util.List;
 
 public abstract class RelationModel {
   protected String methodName;
@@ -125,7 +126,7 @@
   public Collection<ColumnModel> getDependentFields() {
     final ArrayList<ColumnModel> r = new ArrayList<ColumnModel>();
     for (final ColumnModel c : fieldsByFieldName.values()) {
-      if (primaryKey == null || primaryKey.getField() != c) {
+      if (!c.rowVersion && (primaryKey == null || primaryKey.getField() != c)) {
         r.add(c);
       }
     }
@@ -135,7 +136,27 @@
   public Collection<ColumnModel> getDependentColumns() {
     final ArrayList<ColumnModel> r = new ArrayList<ColumnModel>();
     for (final ColumnModel c : columnsByColumnName.values()) {
-      if (!c.inPrimaryKey) {
+      if (!c.inPrimaryKey && !c.rowVersion) {
+        r.add(c);
+      }
+    }
+    return r;
+  }
+
+  public Collection<ColumnModel> getRowVersionFields() {
+    final ArrayList<ColumnModel> r = new ArrayList<ColumnModel>();
+    for (final ColumnModel c : fieldsByFieldName.values()) {
+      if (c.rowVersion) {
+        r.add(c);
+      }
+    }
+    return r;
+  }
+
+  public Collection<ColumnModel> getRowVersionColumns() {
+    final ArrayList<ColumnModel> r = new ArrayList<ColumnModel>();
+    for (final ColumnModel c : columnsByColumnName.values()) {
+      if (c.rowVersion) {
         r.add(c);
       }
     }
@@ -164,6 +185,7 @@
   public Collection<ColumnModel> getColumns() {
     final ArrayList<ColumnModel> r = new ArrayList<ColumnModel>();
     r.addAll(getDependentColumns());
+    r.addAll(getRowVersionColumns());
     r.addAll(getPrimaryKeyColumns());
     return r;
   }
@@ -272,9 +294,13 @@
     r.append("UPDATE ");
     r.append(relationName);
     r.append(" SET ");
+    List<ColumnModel> cols;
     int nth = 1;
-    for (final Iterator<ColumnModel> i = getDependentColumns().iterator(); i
-        .hasNext();) {
+
+    cols = new ArrayList<ColumnModel>();
+    cols.addAll(getDependentColumns());
+    cols.addAll(getRowVersionColumns());
+    for (final Iterator<ColumnModel> i = cols.iterator(); i.hasNext();) {
       final ColumnModel col = i.next();
       r.append(col.getColumnName());
       r.append("=");
@@ -283,9 +309,12 @@
         r.append(",");
       }
     }
+
     r.append(" WHERE ");
-    for (final Iterator<ColumnModel> i = getPrimaryKeyColumns().iterator(); i
-        .hasNext();) {
+    cols = new ArrayList<ColumnModel>();
+    cols.addAll(getPrimaryKeyColumns());
+    cols.addAll(getRowVersionColumns());
+    for (final Iterator<ColumnModel> i = cols.iterator(); i.hasNext();) {
       final ColumnModel col = i.next();
       r.append(col.getColumnName());
       r.append("=");
@@ -303,8 +332,10 @@
     r.append(relationName);
     int nth = 1;
     r.append(" WHERE ");
-    for (final Iterator<ColumnModel> i = getPrimaryKeyColumns().iterator(); i
-        .hasNext();) {
+    final List<ColumnModel> cols = new ArrayList<ColumnModel>();
+    cols.addAll(getPrimaryKeyColumns());
+    cols.addAll(getRowVersionColumns());
+    for (final Iterator<ColumnModel> i = cols.iterator(); i.hasNext();) {
       final ColumnModel col = i.next();
       r.append(col.getColumnName());
       r.append("=");
diff --git a/src/com/google/gwtorm/schema/gwt/GwtColumnModel.java b/src/com/google/gwtorm/schema/gwt/GwtColumnModel.java
index c4ecc8e..e2c2605 100644
--- a/src/com/google/gwtorm/schema/gwt/GwtColumnModel.java
+++ b/src/com/google/gwtorm/schema/gwt/GwtColumnModel.java
@@ -20,6 +20,7 @@
 import com.google.gwt.core.ext.typeinfo.JType;
 import com.google.gwtorm.client.Column;
 import com.google.gwtorm.client.OrmException;
+import com.google.gwtorm.client.RowVersion;
 import com.google.gwtorm.schema.ColumnModel;
 
 import java.util.ArrayList;
@@ -96,6 +97,12 @@
     }
 
     primitiveType = toClass(field.getType());
+    rowVersion = field.getAnnotation(RowVersion.class) != null;
+    if (rowVersion && primitiveType != Integer.TYPE) {
+      throw new OrmException("Field " + field.getName() + " of "
+          + field.getEnclosingType().getQualifiedSourceName()
+          + " must have type 'int'");
+    }
 
     if (isNested()) {
       final List<GwtColumnModel> col = new ArrayList<GwtColumnModel>();
diff --git a/src/com/google/gwtorm/schema/java/JavaColumnModel.java b/src/com/google/gwtorm/schema/java/JavaColumnModel.java
index 47c79f5..082a6a2 100644
--- a/src/com/google/gwtorm/schema/java/JavaColumnModel.java
+++ b/src/com/google/gwtorm/schema/java/JavaColumnModel.java
@@ -16,6 +16,7 @@
 
 import com.google.gwtorm.client.Column;
 import com.google.gwtorm.client.OrmException;
+import com.google.gwtorm.client.RowVersion;
 import com.google.gwtorm.schema.ColumnModel;
 import com.google.gwtorm.schema.Util;
 
@@ -41,6 +42,12 @@
           + field.getDeclaringClass().getName() + " must not be final");
     }
 
+    rowVersion = field.getAnnotation(RowVersion.class) != null;
+    if (rowVersion && field.getType() != Integer.TYPE) {
+      throw new OrmException("Field " + field.getName() + " of "
+          + field.getDeclaringClass().getName() + " must have type 'int'");
+    }
+
     if (isNested()) {
       final List<JavaColumnModel> col = new ArrayList<JavaColumnModel>();
       Class<?> in = field.getType();