blob: ad185917c27c71c99f521c36dcf126329392fa81 [file] [log] [blame]
/*
* 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.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;
import com.google.gwt.valuestore.shared.impl.RecordImpl;
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;
// 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;
}
public void addValidation() {
throw new UnsupportedOperationException("Auto-generated method stub");
}
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());
}
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() {
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);
RecordJsoImpl rawMasterRecord = master.records.get(recordKey);
WriteOperation priorOperation = operations.get(recordKey);
if (priorOperation == null) {
addNewChangeRecord(recordKey, recordImpl, property, value);
return;
}
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;
}
}
public DeltaValueStore spawnDeltaView() {
throw new UnsupportedOperationException("Auto-generated method stub");
}
public String toJson() {
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 : recordsMap.entrySet()) {
RecordJsoImpl impl = entry.getValue();
if (first) {
first = false;
} else {
requestData.append(",");
}
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();
}
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,
RecordJsoImpl rawMasterRecord) {
RecordJsoImpl masterRecord = null;
if (rawMasterRecord == null) {
return true;
}
masterRecord = rawMasterRecord.cast();
if (!masterRecord.isDefined(property.getName())) {
return true;
}
V masterValue = masterRecord.get(property);
if (masterValue == value) {
return false;
}
if ((masterValue != null)) {
return !masterValue.equals(value);
}
return true;
}
private RecordJsoImpl newChangeRecord(RecordImpl fromRecord) {
return RecordJsoImpl.emptyCopy(fromRecord);
}
}