Sync is for real now. Notes:
- introduced an annotation ServerType that is to be put on all *Record objects. It is not visible to the client.
- the client sends the 'token' in the annotation to the server. If there is no token, it sends the Record name.
- DeltaValueStore can handle a sequence of CREATE, DELETE, UPDATE calls on records.
- ExpenseDataServlet now just does database initialization, all sync functionality is carried out using reflection in the generic servlet.

Patch by: amitmanjhi
Review by: rjrjr (desk review)

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


git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@7970 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/bikeshed/src/com/google/gwt/requestfactory/client/impl/RequestFactoryJsonImpl.java b/bikeshed/src/com/google/gwt/requestfactory/client/impl/RequestFactoryJsonImpl.java
index f22ac86..2541c90 100644
--- a/bikeshed/src/com/google/gwt/requestfactory/client/impl/RequestFactoryJsonImpl.java
+++ b/bikeshed/src/com/google/gwt/requestfactory/client/impl/RequestFactoryJsonImpl.java
@@ -98,10 +98,9 @@
 
           public void onResponseReceived(Request request, Response response) {
             if (200 == response.getStatusCode()) {
-              // String text = response.getText();
               // parse the return value.
 
-              jsonDeltas.commit();
+              jsonDeltas.commit(response.getText());
             } else {
               // shell.error.setInnerText(SERVER_ERROR + " ("
               // + response.getStatusText() + ")");
diff --git a/bikeshed/src/com/google/gwt/requestfactory/rebind/RequestFactoryGenerator.java b/bikeshed/src/com/google/gwt/requestfactory/rebind/RequestFactoryGenerator.java
index 7e1d279..4f2bf96 100644
--- a/bikeshed/src/com/google/gwt/requestfactory/rebind/RequestFactoryGenerator.java
+++ b/bikeshed/src/com/google/gwt/requestfactory/rebind/RequestFactoryGenerator.java
@@ -31,7 +31,9 @@
 import com.google.gwt.requestfactory.client.impl.AbstractListJsonRequestObject;
 import com.google.gwt.requestfactory.client.impl.RequestFactoryJsonImpl;
 import com.google.gwt.requestfactory.shared.ServerOperation;
+import com.google.gwt.requestfactory.shared.RequestFactory.WriteOperation;
 import com.google.gwt.requestfactory.shared.impl.RequestDataManager;
+import com.google.gwt.sample.expenses.gwt.request.ServerType;
 import com.google.gwt.user.rebind.ClassSourceFileComposerFactory;
 import com.google.gwt.user.rebind.PrintWriterManager;
 import com.google.gwt.user.rebind.SourceWriter;
@@ -126,6 +128,7 @@
       f.addImport(RecordImpl.class.getName());
       f.addImport(RecordJsoImpl.class.getName());
       f.addImport(RecordSchema.class.getName());
+      f.addImport(WriteOperation.class.getName().replace("$", "."));
 
       f.addImport(Collections.class.getName());
       f.addImport(HashSet.class.getName());
@@ -183,14 +186,26 @@
 
       sw.println();
       sw.println("@Override");
-      sw.println(String.format("public %s createChangeEvent(Record record) {",
+      sw.println(String.format("public %s createChangeEvent(Record record, WriteOperation writeOperation) {",
           eventType.getName()));
       sw.indent();
-      sw.println(String.format("return new %s((%s) record);",
+      sw.println(String.format("return new %s((%s) record, writeOperation);",
           eventType.getName(), publicRecordType.getName()));
       sw.outdent();
       sw.println("}");
 
+      sw.println();
+      sw.println("public String getToken() {");
+      sw.indent();
+      ServerType serverType = publicRecordType.getAnnotation(ServerType.class);
+      String token = serverType.token();
+      if ("[UNASSIGNED]".equals(token)) {
+        token = publicRecordType.getName();
+      }
+      sw.println("return \"" + token + "\";");
+      sw.outdent();
+      sw.println("}");
+
       sw.outdent();
       sw.println("}");
 
diff --git a/bikeshed/src/com/google/gwt/requestfactory/server/RequestFactoryServlet.java b/bikeshed/src/com/google/gwt/requestfactory/server/RequestFactoryServlet.java
index f1c7115..92d999a 100644
--- a/bikeshed/src/com/google/gwt/requestfactory/server/RequestFactoryServlet.java
+++ b/bikeshed/src/com/google/gwt/requestfactory/server/RequestFactoryServlet.java
@@ -18,7 +18,9 @@
 import com.google.gwt.requestfactory.shared.RequestFactory;
 import com.google.gwt.requestfactory.shared.RequestFactory.Config;
 import com.google.gwt.requestfactory.shared.RequestFactory.RequestDefinition;
+import com.google.gwt.requestfactory.shared.RequestFactory.WriteOperation;
 import com.google.gwt.requestfactory.shared.impl.RequestDataManager;
+import com.google.gwt.sample.expenses.gwt.request.ServerType;
 import com.google.gwt.valuestore.shared.Property;
 import com.google.gwt.valuestore.shared.Record;
 
@@ -63,24 +65,41 @@
 @SuppressWarnings("serial")
 public class RequestFactoryServlet extends HttpServlet {
 
-  private static final String SERVER_OPERATION_CONTEXT_PARAM = "servlet.serverOperation";
+  /**
+   * A class representing the pair of a domain entity and its corresponding
+   * record class on the client side.
+   */
+  protected static class EntityRecordPair {
+    public final Class<?> entity;
+    public final Class<? extends Record> record;
 
+    EntityRecordPair(Class<?> entity, Class<? extends Record> record) {
+      this.entity = entity;
+      this.record = record;
+    }
+  }
+
+  private static final String SERVER_OPERATION_CONTEXT_PARAM = "servlet.serverOperation";
   // TODO: Remove this hack
   private static final Set<String> PROPERTY_SET = new HashSet<String>();
+
   static {
-    for (String str : new String[]{
+    for (String str : new String[] {
         "id", "version", "displayName", "userName", "purpose", "created"}) {
       PROPERTY_SET.add(str);
     }
   }
 
-  private Config config;
+  private Config config = null;
+
+  protected Map<String, EntityRecordPair> tokenToEntityRecord;
 
   @Override
   protected void doPost(HttpServletRequest request, HttpServletResponse response)
       throws IOException {
 
     initDb(); // temporary place-holder
+    ensureConfig();
 
     RequestDefinition operation = null;
     try {
@@ -128,6 +147,8 @@
       throw new IllegalArgumentException(e);
     } catch (IllegalArgumentException e) {
       throw new RuntimeException(e);
+    } catch (InstantiationException e) {
+      throw new RuntimeException(e);
     }
   }
 
@@ -138,15 +159,60 @@
   }
 
   /**
-   * Allows subclass to provide hack implementation.
+   * Persist a recordObject of token "recordToken" and return useful information
+   * as a JSONObject to return back.
    * <p>
-   * TODO real reflection based implementation.
-   *
-   * @param content
-   * @param writer
+   * Example: recordToken = "Employee", entity = Employee.class, record =
+   * EmployeeRecord.class
+   *<p>
+   * Steps:
+   * <ol>
+   * <li>assert that each property is present in "EmployeeRecord"
+   * <li>invoke "findEmployee (id)" OR new Employee()
+   * <li>set various fields on the attached entity and persist OR remove()
+   * <li>return data
+   * </ol>
    */
-  protected void sync(String content, PrintWriter writer) {
-    return;
+  JSONObject updateRecordInDataStore(String recordToken,
+      JSONObject recordObject, WriteOperation writeOperation)
+      throws SecurityException, NoSuchMethodException, IllegalAccessException,
+      InvocationTargetException, JSONException, InstantiationException {
+
+    Class<?> entity = tokenToEntityRecord.get(recordToken).entity;
+    Class<? extends Record> record = tokenToEntityRecord.get(recordToken).record;
+    Map<String, Class<?>> propertiesInRecord = getPropertiesFromRecord(record);
+    validateKeys(recordObject, propertiesInRecord);
+
+    // get entityInstance
+    Object entityInstance = getEntityInstance(writeOperation, entity,
+        recordObject.getString("id"), propertiesInRecord.get("id"));
+
+    // persist
+    if (writeOperation == WriteOperation.DELETE) {
+      entity.getMethod("remove").invoke(entityInstance);
+    } else {
+      Iterator<?> keys = recordObject.keys();
+      while (keys.hasNext()) {
+        String key = (String) keys.next();
+        Object value = recordObject.getString(key);
+        Class<?> propertyType = propertiesInRecord.get(key);
+        // TODO: hack to work around the GAE integer bug.
+        if ("version".equals(key)) {
+          propertyType = Long.class;
+          value = new Long(value.toString());
+        }
+        if (writeOperation == WriteOperation.CREATE && ("id".equals(key))) {
+          // ignored. id is assigned by default.
+        } else {
+          entity.getMethod(getMethodNameFromPropertyName(key, "set"),
+              propertyType).invoke(entityInstance, value);
+        }
+      }
+      entity.getMethod("persist").invoke(entityInstance);
+    }
+
+    // return data back.
+    return getReturnRecord(writeOperation, entity, entityInstance, recordObject);
   }
 
   private Collection<Property<?>> allProperties(Class<? extends Record> clazz) {
@@ -180,6 +246,18 @@
           Class<?> clazz = Class.forName(serverOperation);
           if (Config.class.isAssignableFrom(clazz)) {
             config = ((Class<? extends Config>) clazz).newInstance();
+
+            // initialize tokenToEntity map
+            tokenToEntityRecord = new HashMap<String, EntityRecordPair>();
+            for (Class<? extends Record> recordClass : config.recordTypes()) {
+              ServerType serverType = recordClass.getAnnotation(ServerType.class);
+              String token = serverType.token();
+              if ("[UNASSIGNED]".equals(token)) {
+                token = recordClass.getSimpleName();
+              }
+              tokenToEntityRecord.put(token, new EntityRecordPair(
+                  serverType.type(), recordClass));
+            }
           }
 
         } catch (ClassNotFoundException e) {
@@ -225,6 +303,20 @@
     }
   }
 
+  private Object getEntityInstance(WriteOperation writeOperation,
+      Class<?> entity, String idValue, Class<?> idType)
+      throws SecurityException, InstantiationException, IllegalAccessException,
+      InvocationTargetException, NoSuchMethodException {
+
+    if (writeOperation == WriteOperation.CREATE) {
+      return entity.getConstructor().newInstance();
+    }
+
+    // TODO: check "version" validity.
+    return entity.getMethod("find" + entity.getSimpleName(), idType).invoke(
+        null, idValue);
+  }
+
   /**
    * Converts the returnValue of a 'get' method to a JSONArray.
    *
@@ -260,14 +352,16 @@
    * Returns methodName corresponding to the propertyName that can be invoked on
    * an entity.
    *
-   * Example: "userName" returns "getUserName". "version" returns "getVersion"
+   * Example: "userName" returns prefix + "UserName". "version" returns prefix +
+   * "Version"
    */
-  private String getMethodNameFromPropertyName(String propertyName) {
+  private String getMethodNameFromPropertyName(String propertyName,
+      String prefix) {
     if (propertyName == null) {
       throw new NullPointerException("propertyName must not be null");
     }
 
-    StringBuffer methodName = new StringBuffer("get");
+    StringBuffer methodName = new StringBuffer(prefix);
     methodName.append(propertyName.substring(0, 1).toUpperCase());
     methodName.append(propertyName.substring(1));
     return methodName.toString();
@@ -275,7 +369,6 @@
 
   private RequestDefinition getOperation(String operationName) {
     RequestDefinition operation;
-    ensureConfig();
     operation = config.requestDefinitions().get(operationName);
     if (null == operation) {
       throw new IllegalArgumentException("Unknown operation " + operationName);
@@ -302,6 +395,23 @@
   }
 
   /**
+   * Returns the property fields (name => type) for a record.
+   */
+  private Map<String, Class<?>> getPropertiesFromRecord(
+      Class<? extends Record> record) throws SecurityException,
+      IllegalAccessException, InvocationTargetException, NoSuchMethodException {
+    Map<String, Class<?>> properties = new HashMap<String, Class<?>>();
+    for (Field f : record.getFields()) {
+      if (Property.class.isAssignableFrom(f.getType())) {
+        Class<?> propertyType = (Class<?>) f.getType().getMethod("getType").invoke(
+            f.get(null));
+        properties.put(f.getName(), propertyType);
+      }
+    }
+    return properties;
+  }
+
+  /**
    * @param entityElement
    * @param property
    * @return
@@ -309,7 +419,7 @@
   private Object getPropertyValue(Object entityElement, String propertyName)
       throws SecurityException, NoSuchMethodException, IllegalAccessException,
       InvocationTargetException {
-    String methodName = getMethodNameFromPropertyName(propertyName);
+    String methodName = getMethodNameFromPropertyName(propertyName, "get");
     Method method = entityElement.getClass().getMethod(methodName);
     Object returnValue = method.invoke(entityElement);
     /*
@@ -325,6 +435,21 @@
     return returnValue;
   }
 
+  private JSONObject getReturnRecord(WriteOperation writeOperation,
+      Class<?> entity, Object entityInstance, JSONObject recordObject)
+      throws SecurityException, JSONException, IllegalAccessException,
+      InvocationTargetException, NoSuchMethodException {
+
+    JSONObject returnObject = new JSONObject();
+    returnObject.put("id", entity.getMethod("getId").invoke(entityInstance));
+    returnObject.put("version", entity.getMethod("getVersion").invoke(
+        entityInstance));
+    if (writeOperation == WriteOperation.CREATE) {
+      returnObject.put("futureId", recordObject.getString("id"));
+    }
+    return returnObject;
+  }
+
   /**
    * returns true if the property has been requested. TODO: fix this hack.
    *
@@ -334,4 +459,59 @@
   private boolean requestedProperty(Property<?> p) {
     return PROPERTY_SET.contains(p.getName());
   }
+
+  private void sync(String content, PrintWriter writer)
+      throws SecurityException, NoSuchMethodException, IllegalAccessException,
+      InvocationTargetException, InstantiationException {
+
+    try {
+      JSONObject jsonObject = new JSONObject(content);
+      JSONObject returnJsonObject = new JSONObject();
+      for (WriteOperation writeOperation : WriteOperation.values()) {
+        if (!jsonObject.has(writeOperation.name())) {
+          continue;
+        }
+        JSONArray reportArray = new JSONArray(
+            jsonObject.getString(writeOperation.name()));
+        JSONArray returnArray = new JSONArray();
+
+        int length = reportArray.length();
+        if (length == 0) {
+          throw new IllegalArgumentException("No json array for "
+              + writeOperation.name() + " should have been sent");
+        }
+        for (int i = 0; i < length; i++) {
+          JSONObject recordWithSchema = reportArray.getJSONObject(i);
+          // iterator has just one element.
+          Iterator<?> iterator = recordWithSchema.keys();
+          iterator.hasNext();
+          String recordToken = (String) iterator.next();
+          JSONObject recordObject = recordWithSchema.getJSONObject(recordToken);
+          JSONObject returnObject = updateRecordInDataStore(recordToken,
+              recordObject, writeOperation);
+          returnArray.put(returnObject);
+          if (iterator.hasNext()) {
+            throw new IllegalArgumentException(
+                "There cannot be more than one record token");
+          }
+        }
+        returnJsonObject.put(writeOperation.name(), returnArray);
+      }
+      writer.print(returnJsonObject.toString());
+    } catch (JSONException e) {
+      throw new IllegalArgumentException("sync failed: ", e);
+    }
+  }
+
+  private void validateKeys(JSONObject recordObject,
+      Map<String, Class<?>> declaredProperties) {
+    Iterator<?> keys = recordObject.keys();
+    while (keys.hasNext()) {
+      String key = (String) keys.next();
+      if (declaredProperties.get(key) == null) {
+        throw new IllegalArgumentException("key " + key
+            + " is not permitted to be set");
+      }
+    }
+  }
 }
diff --git a/bikeshed/src/com/google/gwt/requestfactory/shared/RequestFactory.java b/bikeshed/src/com/google/gwt/requestfactory/shared/RequestFactory.java
index 67cb6ac..6872cfb 100644
--- a/bikeshed/src/com/google/gwt/requestfactory/shared/RequestFactory.java
+++ b/bikeshed/src/com/google/gwt/requestfactory/shared/RequestFactory.java
@@ -21,6 +21,7 @@
 import com.google.gwt.valuestore.shared.ValueStore;
 
 import java.util.Map;
+import java.util.Set;
 
 /**
  * Marker interface for the RequestFactory code generator.
@@ -34,6 +35,7 @@
    */
   interface Config {
     Map<String, RequestDefinition> requestDefinitions();
+    Set<Class<? extends Record>> recordTypes();
   }
 
   /**
@@ -91,4 +93,11 @@
   void init(HandlerManager handlerManager);
 
   SyncRequest syncRequest(DeltaValueStore deltaValueStore);
+
+  /**
+   * The write operation enum used in DeltaValueStore.
+   */
+  enum WriteOperation {
+    CREATE, UPDATE, DELETE
+  }
 }
diff --git a/bikeshed/src/com/google/gwt/sample/expenses/gwt/request/EmployeeRecord.java b/bikeshed/src/com/google/gwt/sample/expenses/gwt/request/EmployeeRecord.java
index dc478e7..8a21d16 100644
--- a/bikeshed/src/com/google/gwt/sample/expenses/gwt/request/EmployeeRecord.java
+++ b/bikeshed/src/com/google/gwt/sample/expenses/gwt/request/EmployeeRecord.java
@@ -24,6 +24,7 @@
  * <p>
  * IRL this class will be generated by a JPA-savvy tool run before compilation.
  */
+@ServerType(type = com.google.gwt.sample.expenses.server.domain.Employee.class, token = "Employee")
 public interface EmployeeRecord extends Record {
   Property<String> userName = new Property<String>("userName", String.class);
   Property<String> displayName = new Property<String>("displayName",
diff --git a/bikeshed/src/com/google/gwt/sample/expenses/gwt/request/EmployeeRecordChanged.java b/bikeshed/src/com/google/gwt/sample/expenses/gwt/request/EmployeeRecordChanged.java
index cf7c294..06ce746 100644
--- a/bikeshed/src/com/google/gwt/sample/expenses/gwt/request/EmployeeRecordChanged.java
+++ b/bikeshed/src/com/google/gwt/sample/expenses/gwt/request/EmployeeRecordChanged.java
@@ -18,6 +18,7 @@
 import com.google.gwt.event.shared.EventHandler;
 import com.google.gwt.event.shared.GwtEvent;
 import com.google.gwt.valuestore.shared.RecordChangedEvent;
+import com.google.gwt.requestfactory.shared.RequestFactory.WriteOperation;
 
 /**
  * "API Generated" event posted when the values of a {@link EmployeeRecord}
@@ -37,8 +38,8 @@
 
   public static final Type<Handler> TYPE = new Type<Handler>();
 
-  public EmployeeRecordChanged(EmployeeRecord record) {
-    super(record);
+  public EmployeeRecordChanged(EmployeeRecord record, WriteOperation writeOperation) {
+    super(record, writeOperation);
   }
 
   @Override
diff --git a/bikeshed/src/com/google/gwt/sample/expenses/gwt/request/ExpensesServerSideOperations.java b/bikeshed/src/com/google/gwt/sample/expenses/gwt/request/ExpensesServerSideOperations.java
index 80eca80..c08a2ed 100644
--- a/bikeshed/src/com/google/gwt/sample/expenses/gwt/request/ExpensesServerSideOperations.java
+++ b/bikeshed/src/com/google/gwt/sample/expenses/gwt/request/ExpensesServerSideOperations.java
@@ -17,10 +17,13 @@
 
 import com.google.gwt.requestfactory.shared.RequestFactory.Config;
 import com.google.gwt.requestfactory.shared.RequestFactory.RequestDefinition;
+import com.google.gwt.valuestore.shared.Record;
 
 import java.util.Collections;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.Map;
+import java.util.Set;
 
 /**
  * "API Generated" configuration class for
@@ -47,6 +50,13 @@
     map = Collections.unmodifiableMap(newMap);
   }
 
+  public Set<Class<? extends Record>> recordTypes() {
+    Set<Class<? extends Record>> records = new HashSet<Class<? extends Record>>();
+    records.add(EmployeeRecord.class);
+    records.add(ReportRecord.class);
+    return records;
+  }
+
   public Map<String, RequestDefinition> requestDefinitions() {
     return map;
   }
diff --git a/bikeshed/src/com/google/gwt/sample/expenses/gwt/request/ReportRecord.java b/bikeshed/src/com/google/gwt/sample/expenses/gwt/request/ReportRecord.java
index 2829fb9..617e306 100644
--- a/bikeshed/src/com/google/gwt/sample/expenses/gwt/request/ReportRecord.java
+++ b/bikeshed/src/com/google/gwt/sample/expenses/gwt/request/ReportRecord.java
@@ -26,6 +26,7 @@
  * <p>
  * IRL this class will be generated by a JPA-savvy tool run before compilation.
  */
+@ServerType(type = com.google.gwt.sample.expenses.server.domain.Report.class)
 public interface ReportRecord extends Record {
 
   Property<Date> created = new Property<Date>("created", Date.class);
diff --git a/bikeshed/src/com/google/gwt/sample/expenses/gwt/request/ReportRecordChanged.java b/bikeshed/src/com/google/gwt/sample/expenses/gwt/request/ReportRecordChanged.java
index b4c058d..061f577 100644
--- a/bikeshed/src/com/google/gwt/sample/expenses/gwt/request/ReportRecordChanged.java
+++ b/bikeshed/src/com/google/gwt/sample/expenses/gwt/request/ReportRecordChanged.java
@@ -18,6 +18,7 @@
 import com.google.gwt.event.shared.EventHandler;
 import com.google.gwt.event.shared.GwtEvent;
 import com.google.gwt.valuestore.shared.RecordChangedEvent;
+import com.google.gwt.requestfactory.shared.RequestFactory.WriteOperation;
 
 /**
  * "API Generated" event posted when the values of a {@link EmployeeRecord}
@@ -37,8 +38,8 @@
 
   public static final Type<Handler> TYPE = new Type<Handler>();
 
-  public ReportRecordChanged(ReportRecord record) {
-    super(record);
+  public ReportRecordChanged(ReportRecord record, WriteOperation writeOperation) {
+    super(record, writeOperation);
   }
 
   @Override
diff --git a/bikeshed/src/com/google/gwt/sample/expenses/gwt/request/ServerType.java b/bikeshed/src/com/google/gwt/sample/expenses/gwt/request/ServerType.java
new file mode 100644
index 0000000..920027d
--- /dev/null
+++ b/bikeshed/src/com/google/gwt/sample/expenses/gwt/request/ServerType.java
@@ -0,0 +1,36 @@
+/*
+ * 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.sample.expenses.gwt.request;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Annotation on Record classes specifying 'type' and 'token'. 'type' represents
+ * the server-side counterpart of the Record. 'token' is an optional String that
+ * is sent in sync requests to the server. server and their type. If 'token' is
+ * absent, the record name is used instead.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+public @interface ServerType {
+
+  String token() default "[UNASSIGNED]";
+
+  Class<?> type();
+}
diff --git a/bikeshed/src/com/google/gwt/sample/expenses/server/ExpensesDataServlet.java b/bikeshed/src/com/google/gwt/sample/expenses/server/ExpensesDataServlet.java
index 218c41f..1d9cdeb 100644
--- a/bikeshed/src/com/google/gwt/sample/expenses/server/ExpensesDataServlet.java
+++ b/bikeshed/src/com/google/gwt/sample/expenses/server/ExpensesDataServlet.java
@@ -16,16 +16,9 @@
 package com.google.gwt.sample.expenses.server;
 
 import com.google.gwt.requestfactory.server.RequestFactoryServlet;
-import com.google.gwt.sample.expenses.gwt.request.ReportRecord;
-import com.google.gwt.sample.expenses.server.domain.Report;
 import com.google.gwt.sample.expenses.server.domain.Employee;
-import com.google.gwt.valuestore.shared.Record;
+import com.google.gwt.sample.expenses.server.domain.Report;
 
-import org.json.JSONArray;
-import org.json.JSONException;
-import org.json.JSONObject;
-
-import java.io.PrintWriter;
 import java.util.Date;
 
 /**
@@ -87,28 +80,4 @@
       report.persist();
     }
   }
-
-  @Override
-  protected void sync(String content, PrintWriter writer) {
-
-    try {
-      JSONArray reportArray = new JSONArray(content);
-      int length = reportArray.length();
-      if (length > 0) {
-        JSONObject report = reportArray.getJSONObject(0);
-        Report r = Report.findReport(report.getString(Record.id.getName()));
-        r.setPurpose(report.getString(ReportRecord.purpose.getName()));
-        r.persist();
-        report.put(Record.version.getName(), r.getVersion());
-        JSONArray returnArray = new JSONArray();
-        // TODO: don't echo back everything.
-        returnArray.put(report);
-        writer.print(returnArray.toString());
-      }
-    } catch (JSONException e) {
-      e.printStackTrace();
-      // TODO: return an error.
-    }
-    return;
-  }
 }
diff --git a/bikeshed/src/com/google/gwt/valuestore/ValueStore.gwt.xml b/bikeshed/src/com/google/gwt/valuestore/ValueStore.gwt.xml
index f47a206..42a432c 100644
--- a/bikeshed/src/com/google/gwt/valuestore/ValueStore.gwt.xml
+++ b/bikeshed/src/com/google/gwt/valuestore/ValueStore.gwt.xml
@@ -19,5 +19,5 @@
   <inherits name='com.google.gwt.user.User'/>
 
   <source path="client"/>
-  <source path="shared"/>
+  <source path="shared" excludes="ServerType" />
 </module>
diff --git a/bikeshed/src/com/google/gwt/valuestore/client/DeltaValueStoreJsonImpl.java b/bikeshed/src/com/google/gwt/valuestore/client/DeltaValueStoreJsonImpl.java
index 4072857..ad18591 100644
--- a/bikeshed/src/com/google/gwt/valuestore/client/DeltaValueStoreJsonImpl.java
+++ b/bikeshed/src/com/google/gwt/valuestore/client/DeltaValueStoreJsonImpl.java
@@ -15,6 +15,9 @@
  */
 package com.google.gwt.valuestore.client;
 
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.core.client.JsArray;
+import com.google.gwt.requestfactory.shared.RequestFactory.WriteOperation;
 import com.google.gwt.valuestore.shared.DeltaValueStore;
 import com.google.gwt.valuestore.shared.Property;
 import com.google.gwt.valuestore.shared.Record;
@@ -22,15 +25,87 @@
 import com.google.gwt.valuestore.shared.impl.RecordJsoImpl;
 
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.Map;
+import java.util.Set;
 
 /**
  * {@link DeltaValueStore} implementation based on {@link ValuesImpl}.
  */
 public class DeltaValueStoreJsonImpl implements DeltaValueStore {
 
+  static class ReturnRecord extends JavaScriptObject {
+
+    public static final native JsArray<ReturnRecord> getRecords(
+        JavaScriptObject response, String operation) /*-{
+      return response[operation];
+    }-*/;
+
+    private static native void fillKeys(JavaScriptObject jso, HashSet<String> s) /*-{
+      for (key in jso) {
+        if (jso.hasOwnProperty(key)) {
+          s.@java.util.HashSet::add(Ljava/lang/Object;)(key);
+        }
+      }
+    }-*/;
+
+    private static native JavaScriptObject getJsoResponse(String response) /*-{
+      // TODO: clean this
+      eval("xyz=" + response);
+      return xyz;
+    }-*/;
+
+    protected ReturnRecord() {
+    }
+
+    public final native String getFutureId()/*-{
+      return this.futureId;
+    }-*/;
+
+    public final native String getId() /*-{
+      return this.id;
+    }-*/;
+
+    public final native String getVersion()/*-{
+      return this.version;
+    }-*/;
+
+    public final native boolean hasFutureId()/*-{
+      return 'futureId' in this;
+    }-*/;
+  }
+
+  private static class FutureIdGenerator {
+    Set<String> idsInTransit = new HashSet<String>();
+    int maxId = 1;
+
+    void delete(String id) {
+      idsInTransit.remove(id);
+    }
+
+    String getFutureId() {
+      int futureId = maxId++;
+      if (maxId == Integer.MAX_VALUE) {
+        maxId = 1;
+      }
+      assert !idsInTransit.contains(futureId);
+      return new String(futureId + "");
+    }
+  }
+
+  private static final String INITIAL_VERSION = "1";
+
+  private boolean used = false;
+  private final FutureIdGenerator futureIdGenerator = new FutureIdGenerator();
+
   private final ValueStoreJsonImpl master;
-  private final Map<RecordKey, RecordJsoImpl> changes = new HashMap<RecordKey, RecordJsoImpl>();
+
+  // track C-U-D of CRUD operations
+  private final Map<RecordKey, RecordJsoImpl> creates = new HashMap<RecordKey, RecordJsoImpl>();
+  private final Map<RecordKey, RecordJsoImpl> updates = new HashMap<RecordKey, RecordJsoImpl>();
+  private final Map<RecordKey, RecordJsoImpl> deletes = new HashMap<RecordKey, RecordJsoImpl>();
+
+  private final Map<RecordKey, WriteOperation> operations = new HashMap<RecordKey, WriteOperation>();
 
   DeltaValueStoreJsonImpl(ValueStoreJsonImpl master) {
     this.master = master;
@@ -40,61 +115,181 @@
     throw new UnsupportedOperationException("Auto-generated method stub");
   }
 
-  public void commit() {
-    // TODO This will drop new verison numbers (and whatever else) returned
-    // by the server.
-    for (Map.Entry<RecordKey, RecordJsoImpl> entry : changes.entrySet()) {
-      final RecordKey key = entry.getKey();
-      RecordJsoImpl masterRecord = master.records.get(key);
-      if (masterRecord == null) {
-        master.records.put(key, entry.getValue());
-        masterRecord = entry.getValue();
-      } else {
-        masterRecord.merge(entry.getValue());
+  public void commit(String response) {
+    JavaScriptObject returnedJso = ReturnRecord.getJsoResponse(response);
+    HashSet<String> keys = new HashSet<String>();
+    ReturnRecord.fillKeys(returnedJso, keys);
+
+    if (keys.contains(WriteOperation.CREATE.name())) {
+      JsArray<ReturnRecord> newRecords = ReturnRecord.getRecords(returnedJso,
+          WriteOperation.CREATE.name());
+      // construct a map from futureId to the datastore Id
+      Map<Object, Object> futureToDatastoreId = new HashMap<Object, Object>();
+      int length = newRecords.length();
+      for (int i = 0; i < length; i++) {
+        ReturnRecord sync = newRecords.get(i);
+        futureToDatastoreId.put(sync.getFutureId(), sync.getId());
       }
-      master.eventBus.fireEvent(masterRecord.getSchema().createChangeEvent(
-          masterRecord));
+
+      for (Map.Entry<RecordKey, RecordJsoImpl> entry : creates.entrySet()) {
+        final RecordKey futureKey = entry.getKey();
+        Object datastoreId = futureToDatastoreId.get(futureKey.id);
+        assert datastoreId != null;
+        futureIdGenerator.delete(futureKey.id.toString());
+
+        final RecordKey key = new RecordKey(datastoreId, futureKey.schema);
+        RecordJsoImpl value = entry.getValue();
+        value.set(Record.id, datastoreId.toString());
+        RecordJsoImpl masterRecord = master.records.get(key);
+        assert masterRecord == null;
+        master.records.put(key, value);
+        masterRecord = value;
+        master.eventBus.fireEvent(masterRecord.getSchema().createChangeEvent(
+            masterRecord, WriteOperation.CREATE));
+      }
+    }
+
+    if (keys.contains(WriteOperation.DELETE.name())) {
+      JsArray<ReturnRecord> deletedRecords = ReturnRecord.getRecords(
+          returnedJso, WriteOperation.DELETE.name());
+      Set<String> returnedKeys = getKeySet(deletedRecords);
+      for (Map.Entry<RecordKey, RecordJsoImpl> entry : deletes.entrySet()) {
+        final RecordKey key = entry.getKey();
+        assert returnedKeys.contains(key.id);
+        RecordJsoImpl masterRecord = master.records.get(key);
+        assert masterRecord != null;
+        master.records.remove(key);
+        master.eventBus.fireEvent(masterRecord.getSchema().createChangeEvent(
+            masterRecord, WriteOperation.DELETE));
+      }
+    }
+
+    if (keys.contains(WriteOperation.UPDATE.name())) {
+      JsArray<ReturnRecord> updatedRecords = ReturnRecord.getRecords(
+          returnedJso, WriteOperation.UPDATE.name());
+      Set<String> returnedKeys = getKeySet(updatedRecords);
+      for (Map.Entry<RecordKey, RecordJsoImpl> entry : updates.entrySet()) {
+        final RecordKey key = entry.getKey();
+        assert returnedKeys.contains(key.id.toString());
+        RecordJsoImpl masterRecord = master.records.get(key);
+        assert masterRecord != null;
+        masterRecord.merge(entry.getValue());
+        master.eventBus.fireEvent(masterRecord.getSchema().createChangeEvent(
+            masterRecord, WriteOperation.UPDATE));
+      }
+    }
+  }
+
+  // TODO: don't use RecordSchema
+  public Record create(Record record) {
+    assert !used;
+    assert record instanceof RecordImpl;
+    RecordImpl recordImpl = (RecordImpl) record;
+    String futureId = futureIdGenerator.getFutureId();
+    RecordJsoImpl newRecord = RecordJsoImpl.newCopy(recordImpl.getSchema(),
+        futureId, INITIAL_VERSION);
+    RecordKey recordKey = new RecordKey(newRecord);
+    assert operations.get(recordKey) == null;
+    operations.put(recordKey, WriteOperation.CREATE);
+    creates.put(recordKey, newRecord);
+    return newRecord;
+  }
+
+  public void delete(Record record) {
+    assert !used;
+    assert record instanceof RecordImpl;
+    RecordImpl recordImpl = (RecordImpl) record;
+    RecordKey recordKey = new RecordKey(recordImpl);
+    WriteOperation priorOperation = operations.get(recordKey);
+    if (priorOperation == null) {
+      operations.put(recordKey, WriteOperation.DELETE);
+      deletes.put(recordKey, recordImpl.asJso());
+      return;
+    }
+    Record priorRecord = null;
+    switch (priorOperation) {
+      case CREATE:
+        priorRecord = creates.remove(recordKey);
+        assert priorRecord != null;
+        operations.remove(recordKey);
+        break;
+      case DELETE:
+        // nothing to do here.
+        break;
+      case UPDATE:
+        // undo update
+        priorRecord = updates.remove(recordKey);
+        assert priorRecord != null;
+        operations.remove(recordKey);
+
+        // actually delete
+        operations.put(recordKey, WriteOperation.DELETE);
+        deletes.put(recordKey, recordImpl.asJso());
+        break;
+      default:
+        throw new IllegalStateException("unknown prior WriteOperation "
+            + priorOperation.name());
     }
   }
 
   public boolean isChanged() {
-    return !changes.isEmpty();
+    assert !used;
+    return !operations.isEmpty();
   }
 
   public <V> void set(Property<V> property, Record record, V value) {
+    assert !used;
     assert record instanceof RecordImpl;
     RecordImpl recordImpl = (RecordImpl) record;
+    RecordKey recordKey = new RecordKey(recordImpl);
 
-    RecordKey key = new RecordKey(recordImpl);
-    RecordJsoImpl rawMasterRecord = master.records.get(key);
-    RecordJsoImpl rawChangeRecord = changes.get(key);
-
-    RecordJsoImpl changeRecord;
-
-    if (rawChangeRecord == null) {
-      // TODO will need to mark this as a sync record, not a new record
-      changeRecord = newChangeRecord(recordImpl);
-    } else {
-      changeRecord = rawChangeRecord.cast();
-    }
-
-    if (isRealChange(property, value, rawMasterRecord)) {
-      changeRecord.set(property, value);
-      changes.put(key, changeRecord);
+    RecordJsoImpl rawMasterRecord = master.records.get(recordKey);
+    WriteOperation priorOperation = operations.get(recordKey);
+    if (priorOperation == null) {
+      addNewChangeRecord(recordKey, recordImpl, property, value);
       return;
-    } 
-
-    /*
-     * Not done yet. If the user has changed the value back to the original
-     * value, we should eliminate the previous value from the changeRecord. And
-     * if the changeRecord is now empty, we should drop it entirely.
-     */
-
-    if (changeRecord.isDefined(property.getName())) {
-      changeRecord.delete(property.getName());
     }
-    if (changes.containsKey(key) && changeRecord.isEmpty()) {
-      changes.remove(key);
+
+    RecordJsoImpl priorRecord = null;
+    switch (priorOperation) {
+      case CREATE:
+        // nothing to do here.
+        priorRecord = creates.get(recordKey);
+        assert priorRecord != null;
+        priorRecord.set(property, value);
+        break;
+      case DELETE:
+        // undo delete
+        RecordJsoImpl recordJsoImpl = deletes.remove(recordKey);
+        assert recordJsoImpl != null;
+        operations.remove(recordKey);
+
+        // add new change record
+        addNewChangeRecord(recordKey, recordImpl, property, value);
+        break;
+      case UPDATE:
+        priorRecord = updates.get(recordKey);
+        assert priorRecord != null;
+
+        if (isRealChange(property, value, rawMasterRecord)) {
+          priorRecord.set(property, value);
+          updates.put(recordKey, priorRecord);
+          return;
+        }
+        /*
+         * Not done yet. If the user has changed the value back to the original
+         * value, we should eliminate the previous value from the changeRecord.
+         * And if the changeRecord is now empty, we should drop it entirely.
+         */
+
+        if (priorRecord.isDefined(property.getName())) {
+          priorRecord.delete(property.getName());
+        }
+        if (updates.containsKey(recordKey) && priorRecord.isEmpty()) {
+          updates.remove(recordKey);
+          operations.remove(recordKey);
+        }
+        break;
     }
   }
 
@@ -103,23 +298,92 @@
   }
 
   public String toJson() {
-    StringBuffer requestData = new StringBuffer("[");
+    used = true;
+    StringBuffer jsonData = new StringBuffer("{");
+    for (WriteOperation writeOperation : WriteOperation.values()) {
+      String jsonDataForOperation = getJsonForOperation(writeOperation);
+      if (jsonDataForOperation.equals("")) {
+        continue;
+      }
+      if (jsonData.length() > 1) {
+        jsonData.append(",");
+      }
+      jsonData.append(jsonDataForOperation);
+    }
+    jsonData.append("}");
+    return jsonData.toString();
+  }
+
+  public boolean validate() {
+    throw new UnsupportedOperationException("Auto-generated method stub");
+  }
+
+  /**
+   * returns true if a new change record has been added.
+   */
+  private <V> boolean addNewChangeRecord(RecordKey recordKey,
+      RecordImpl recordImpl, Property<V> property, V value) {
+    RecordJsoImpl rawMasterRecord = master.records.get(recordKey);
+    RecordJsoImpl changeRecord = newChangeRecord(recordImpl);
+    if (isRealChange(property, value, rawMasterRecord)) {
+      changeRecord.set(property, value);
+      updates.put(recordKey, changeRecord);
+      operations.put(recordKey, WriteOperation.UPDATE);
+      return true;
+    }
+    return false;
+  }
+
+  private String getJsonForOperation(WriteOperation writeOperation) {
+    Map<RecordKey, RecordJsoImpl> recordsMap = getRecordsMap(writeOperation);
+    if (recordsMap.size() == 0) {
+      return "";
+    }
+    StringBuffer requestData = new StringBuffer("\"" + writeOperation.name()
+        + "\":[");
     boolean first = true;
-    for (Map.Entry<RecordKey, RecordJsoImpl> entry : changes.entrySet()) {
+    for (Map.Entry<RecordKey, RecordJsoImpl> entry : recordsMap.entrySet()) {
       RecordJsoImpl impl = entry.getValue();
       if (first) {
         first = false;
       } else {
         requestData.append(",");
       }
-      requestData.append(impl.toJson());
+      requestData.append("{\"" + entry.getValue().getSchema().getToken()
+          + "\":");
+      if (writeOperation != WriteOperation.DELETE) {
+        requestData.append(impl.toJson());
+      } else {
+        requestData.append(impl.toJsonIdVersion());
+      }
+      requestData.append("}");
     }
     requestData.append("]");
     return requestData.toString();
   }
 
-  public boolean validate() {
-    throw new UnsupportedOperationException("Auto-generated method stub");
+  private Set<String> getKeySet(JsArray<ReturnRecord> records) {
+    Set<String> returnSet = new HashSet<String>();
+    int length = records.length();
+    for (int i = 0; i < length; i++) {
+      returnSet.add(records.get(i).getId());
+    }
+    return returnSet;
+  }
+
+  private Map<RecordKey, RecordJsoImpl> getRecordsMap(
+      WriteOperation writeOperation) {
+    switch (writeOperation) {
+      case CREATE:
+        return creates;
+      case DELETE:
+        return deletes;
+      case UPDATE:
+        return updates;
+      default:
+        throw new IllegalStateException("unknow writeOperation "
+            + writeOperation.name());
+    }
   }
 
   private <V> boolean isRealChange(Property<V> property, V value,
diff --git a/bikeshed/src/com/google/gwt/valuestore/client/RecordKey.java b/bikeshed/src/com/google/gwt/valuestore/client/RecordKey.java
index 2bde533..0f10975 100644
--- a/bikeshed/src/com/google/gwt/valuestore/client/RecordKey.java
+++ b/bikeshed/src/com/google/gwt/valuestore/client/RecordKey.java
@@ -35,7 +35,7 @@
     this(record.getId(), record.getSchema());
   }
 
-  private RecordKey(Object id, RecordSchema<?> schema) {
+  protected RecordKey(Object id, RecordSchema<?> schema) {
     assert id != null;
     assert schema != null;
 
diff --git a/bikeshed/src/com/google/gwt/valuestore/client/ValueStoreJsonImpl.java b/bikeshed/src/com/google/gwt/valuestore/client/ValueStoreJsonImpl.java
index bebbfc1..341717f 100644
--- a/bikeshed/src/com/google/gwt/valuestore/client/ValueStoreJsonImpl.java
+++ b/bikeshed/src/com/google/gwt/valuestore/client/ValueStoreJsonImpl.java
@@ -17,6 +17,7 @@
 
 import com.google.gwt.core.client.JsArray;
 import com.google.gwt.event.shared.HandlerManager;
+import com.google.gwt.requestfactory.shared.RequestFactory.WriteOperation;
 import com.google.gwt.valuestore.shared.ValueStore;
 import com.google.gwt.valuestore.shared.impl.RecordJsoImpl;
 
@@ -55,7 +56,8 @@
         newRecord = oldRecord.cast();
         newRecords.set(i, newRecord);
         if (changed) {
-          eventBus.fireEvent(newRecord.getSchema().createChangeEvent(newRecord));
+          eventBus.fireEvent(newRecord.getSchema().createChangeEvent(newRecord,
+              WriteOperation.UPDATE));
         }
       }
     }
diff --git a/bikeshed/src/com/google/gwt/valuestore/shared/DeltaValueStore.java b/bikeshed/src/com/google/gwt/valuestore/shared/DeltaValueStore.java
index 02ecd94..5e8fe51 100644
--- a/bikeshed/src/com/google/gwt/valuestore/shared/DeltaValueStore.java
+++ b/bikeshed/src/com/google/gwt/valuestore/shared/DeltaValueStore.java
@@ -19,6 +19,15 @@
  * Set of changes to a ValueStore.
  */
 public interface DeltaValueStore extends ValueStore {
+  Record create(Record existingRecord);
+
+  void delete(Record record);
+
+  /**
+   * Return true if there are outstanding changes that have not been
+   * communicated to the server yet. Note that it is illegal to call this method
+   * after a request using it has been fired.
+   */
   boolean isChanged();
 
   <V> void set(Property<V> property, Record record, V value);
diff --git a/bikeshed/src/com/google/gwt/valuestore/shared/RecordChangedEvent.java b/bikeshed/src/com/google/gwt/valuestore/shared/RecordChangedEvent.java
index 1a32786..1471864 100644
--- a/bikeshed/src/com/google/gwt/valuestore/shared/RecordChangedEvent.java
+++ b/bikeshed/src/com/google/gwt/valuestore/shared/RecordChangedEvent.java
@@ -17,6 +17,7 @@
 
 import com.google.gwt.event.shared.EventHandler;
 import com.google.gwt.event.shared.GwtEvent;
+import com.google.gwt.requestfactory.shared.RequestFactory.WriteOperation;
 
 /**
  * Abstract base class for an event announcing changes to a {@link Record}.
@@ -29,12 +30,18 @@
 public abstract class RecordChangedEvent<R extends Record, H extends EventHandler>
     extends GwtEvent<H> {
   R record;
+  WriteOperation writeOperation;
 
-  public RecordChangedEvent(R record) {
+  public RecordChangedEvent(R record, WriteOperation writeOperation) {
     this.record = record;
+    this.writeOperation = writeOperation;
   }
 
   public R getRecord() {
     return record;
   }
+
+  public WriteOperation writeOperation() {
+    return writeOperation;
+  }
 }
diff --git a/bikeshed/src/com/google/gwt/valuestore/shared/impl/RecordJsoImpl.java b/bikeshed/src/com/google/gwt/valuestore/shared/impl/RecordJsoImpl.java
index 67152ed..43cdb32 100644
--- a/bikeshed/src/com/google/gwt/valuestore/shared/impl/RecordJsoImpl.java
+++ b/bikeshed/src/com/google/gwt/valuestore/shared/impl/RecordJsoImpl.java
@@ -42,6 +42,15 @@
     return copy;
   }
 
+  public static RecordJsoImpl newCopy(RecordSchema<?> schema, String id,
+      String version) {
+    RecordJsoImpl newCopy = create();
+    newCopy.setSchema(schema);
+    newCopy.set(Record.id, id);
+    newCopy.set(Record.version, version);
+    return newCopy;
+  }
+
   private static native RecordJsoImpl create() /*-{
     return {};
   }-*/;
@@ -158,6 +167,22 @@
     return JSON.stringify(this, replacer);
   }-*/;
 
+  /**
+   * Return JSON representation of just id and version fields, using org.json
+   * library.
+   *
+   * @return returned string.
+   */
+  public final native String toJsonIdVersion() /*-{
+    var replacer = function(key, value) {
+      if (key == 'id' || key == 'version') {
+        return value;
+      }
+      return;
+    }
+    return JSON.stringify(this, replacer);
+  }-*/;
+
   private native boolean copyPropertyIfDifferent(String name, RecordJsoImpl from) /*-{
     if (this[name] == from[name]) {
       return false;
diff --git a/bikeshed/src/com/google/gwt/valuestore/shared/impl/RecordSchema.java b/bikeshed/src/com/google/gwt/valuestore/shared/impl/RecordSchema.java
index 99d6b6b..6810626 100644
--- a/bikeshed/src/com/google/gwt/valuestore/shared/impl/RecordSchema.java
+++ b/bikeshed/src/com/google/gwt/valuestore/shared/impl/RecordSchema.java
@@ -15,6 +15,7 @@
  */
 package com.google.gwt.valuestore.shared.impl;
 
+import com.google.gwt.requestfactory.shared.RequestFactory.WriteOperation;
 import com.google.gwt.valuestore.shared.Property;
 import com.google.gwt.valuestore.shared.Record;
 import com.google.gwt.valuestore.shared.RecordChangedEvent;
@@ -47,10 +48,15 @@
 
   public abstract R create(RecordJsoImpl jso);
 
-  public abstract RecordChangedEvent<?, ?> createChangeEvent(Record record);
+  public abstract RecordChangedEvent<?, ?> createChangeEvent(Record record,
+      WriteOperation writeOperation);
 
-  public RecordChangedEvent<?, ?> createChangeEvent(RecordJsoImpl jsoRecord) {
+  public RecordChangedEvent<?, ?> createChangeEvent(RecordJsoImpl jsoRecord,
+      WriteOperation writeOperation) {
     R record = create(jsoRecord);
-    return createChangeEvent(record);
+    return createChangeEvent(record, writeOperation);
   }
+
+  public abstract String getToken();
+
 }
\ No newline at end of file