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