package org.swellrt.beta.model.remote;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import org.swellrt.beta.client.PlatformBasedFactory;
import org.swellrt.beta.client.WaveStatus;
import org.swellrt.beta.common.SException;
import org.swellrt.beta.model.SHandler;
import org.swellrt.beta.model.SList;
import org.swellrt.beta.model.SMap;
import org.swellrt.beta.model.SNode;
import org.swellrt.beta.model.SObject;
import org.swellrt.beta.model.SObservable;
import org.swellrt.beta.model.SPrimitive;
import org.swellrt.beta.model.SStatusEvent;
import org.swellrt.beta.model.SText;
import org.swellrt.beta.model.js.Proxy;
import org.swellrt.beta.model.js.SMapProxyHandler;
import org.swellrt.beta.model.remote.wave.DocumentBasedBasicRMap;
import org.waveprotocol.wave.model.adt.ObservableBasicMap;
import org.waveprotocol.wave.model.adt.ObservableElementList;
import org.waveprotocol.wave.model.adt.docbased.DocumentBasedElementList;
import org.waveprotocol.wave.model.adt.docbased.Initializer;
import org.waveprotocol.wave.model.document.Doc;
import org.waveprotocol.wave.model.document.Doc.E;
import org.waveprotocol.wave.model.document.Document;
import org.waveprotocol.wave.model.document.ObservableDocument;
import org.waveprotocol.wave.model.document.operation.DocInitialization;
import org.waveprotocol.wave.model.document.util.DefaultDocEventRouter;
import org.waveprotocol.wave.model.document.util.DocHelper;
import org.waveprotocol.wave.model.document.util.DocumentEventRouter;
import org.waveprotocol.wave.model.id.IdGenerator;
import org.waveprotocol.wave.model.id.ModernIdSerialiser;
import org.waveprotocol.wave.model.id.WaveletId;
import org.waveprotocol.wave.model.util.Preconditions;
import org.waveprotocol.wave.model.util.Serializer;
import org.waveprotocol.wave.model.wave.Blip;
import org.waveprotocol.wave.model.wave.InvalidParticipantAddress;
import org.waveprotocol.wave.model.wave.ObservableWavelet;
import org.waveprotocol.wave.model.wave.ParticipantId;
import org.waveprotocol.wave.model.wave.opbased.ObservableWaveView;
/**
*
* Main class providing object-based data model built on top of Wave data model.
* <p>
* A {@link SObject} represents a JSON-like data structure containing a nested structure of {@link SMap}, {@link SList} and {@link SPrimitive} values.
* <p>
* The underlying implementation based on the Wave data model provides:
* <ul>
* <li>Real-time sync of SObject state from changes of different users, even between different servers (see Wave Federation)</li>
* <li>In-flight persistence of changes with eventual consistency (see Wave Operational Transformations)</li>
* </ul>
* <p>
* The SObject data model is mapped with Wave data model as follows:
* <p>
* Wave => SObject instance
* <br>
* Wavelet => Container
* <br>
* Blip/Document => Substrate
* <br>
* <p>
* Implementation details based on the Wave data model follows:
* <p>
* Each node of a CObject is stored in a substrate Wave blip/document. Blips could be stored in different container Wavelets for
* performance reasons if it is necessary.
* <br>
* A map (see example below) is represented as a following XML structure within a blip. Primitive values are embedded in their container structure,
* and nested containers {@link SMap} or {@link SList} are represented as pointers to its substrate blip.
* <pre>
* {@code
*
* prop1 : "value1",
* prop2 : 12345,
* prop3 :
* prop31: "value31"
* prop32: "value32"
*
* prop4 : [ "a", "b", "c" ]
*
*
*
* <object>
* <prop1 t='s'>value1</prop1>
* <prop2 t='i'>12345</prop2>
* <prop3 t='o'>
* <prop31 t='s'>value31</prop31>
* <prop32 t='s'>value32</prop32>
* </prop3>
* <prop4 t='a'>
* <item t='s'>a</item>
* <item t='s'>b</item>
* <item t='s'>c</item>
* </prop4>
* </object>
*
* }
* </pre>
* @author pablojan@gmail.com (Pablo Ojanguren)
*
*/
public class SObjectRemote extends SNodeRemoteContainer implements SObject, SObservable {
private static String serialize(SNodeRemote x) {
// Order matters check SPrimitive first
if (x instanceof SPrimitive) {
SPrimitive p = (SPrimitive) x;
return p.serialize();
}
if (x instanceof SNodeRemote) {
SNodeRemote r = (SNodeRemote) x;
SubstrateId id = r.getSubstrateId();
if (id != null)
return id.serialize();
}
return null;
}
/**
* A serializer/deserializer of SNode objects to/from a Wave's list
*/
public class SubstrateListSerializer implements org.waveprotocol.wave.model.adt.docbased.Factory<Doc.E, SNodeRemote, SNodeRemote> {
@Override
public SNodeRemote adapt(DocumentEventRouter<? super E, E, ?> router, E element) {
Map<String, String> attributes = router.getDocument().getAttributes(element);
return deserialize(attributes.get(LIST_ENTRY_VALUE_ATTR));
}
@Override
public Initializer createInitializer(SNodeRemote node) {
return new org.waveprotocol.wave.model.adt.docbased.Initializer() {
@Override
public void initialize(Map<String, String> target) {
target.put(LIST_ENTRY_KEY_ATTR, String.valueOf(System.currentTimeMillis())); // temp
target.put(LIST_ENTRY_VALUE_ATTR, serialize(node));
}
};
}
}
/**
* A serializer/deserializer of SNode objects to/from a Wave's map
*/
public class SubstrateMapSerializer implements org.waveprotocol.wave.model.util.Serializer<SNodeRemote> {
@Override
public String toString(SNodeRemote x) {
return serialize(x);
}
@Override
public SNodeRemote fromString(String s) {
return deserialize(s);
}
@Override
public SNodeRemote fromString(String s, SNodeRemote defaultValue) {
return fromString(s);
}
}
private static final String MASTER_DATA_WAVELET_NAME = "data+master";
private static final String ROOT_SUBSTRATE_ID = "m+root";
private static final String MAP_TAG = "map";
private static final String MAP_ENTRY_TAG = "entry";
private static final String MAP_ENTRY_KEY_ATTR = "k";
private static final String MAP_ENTRY_VALUE_ATTR = "v";
private static final String LIST_TAG = "list";
private static final String LIST_ENTRY_TAG = "entry";
private static final String LIST_ENTRY_KEY_ATTR = "k";
private static final String LIST_ENTRY_VALUE_ATTR = "v";
private static final String USER_ROOT_SUBSTRATED_ID = "m+root";
private final String domain;
private final IdGenerator idGenerator;
private final ObservableWaveView wave;
private final WaveStatus waveStatus;
private ObservableWavelet masterWavelet;
private ObservableWavelet userWavelet;
private SubstrateMapSerializer mapSerializer;
private SMapRemote root;
private SMapRemote userRoot;
/** A factory for platform dependent objects */
private final PlatformBasedFactory factory;
private final Map<SubstrateId, SNodeRemote> nodeStore = new HashMap<SubstrateId, SNodeRemote>();
private SObject.StatusHandler statusHandler = null;
private final ParticipantId participant;
/**
* Get a MutableCObject instance with a substrate Wave.
* Initialize the Wave accordingly.
*
*/
public static SObjectRemote inflateFromWave(ParticipantId participant, IdGenerator idGenerator, String domain, ObservableWaveView wave, PlatformBasedFactory factory, WaveStatus waveStatus) {
Preconditions.checkArgument(domain != null && !domain.isEmpty(), "Domain is not provided");
Preconditions.checkArgument(wave != null, "Wave can't be null");
// Initialize master Wavelet if necessary
ObservableWavelet masterWavelet = wave.getWavelet(WaveletId.of(domain, MASTER_DATA_WAVELET_NAME));
if (masterWavelet == null) {
masterWavelet = wave.createWavelet(WaveletId.of(domain, MASTER_DATA_WAVELET_NAME));
}
SObjectRemote object = new SObjectRemote(participant, idGenerator, domain, wave, factory, waveStatus);
object.init();
masterWavelet.addListener(new SWaveletListener(object));
return object;
}
/**
* Private constructor.
*
* @param idGenerator
* @param domain
* @param wave
*/
private SObjectRemote(ParticipantId participant, IdGenerator idGenerator, String domain, ObservableWaveView wave, PlatformBasedFactory factory, WaveStatus waveStatus) {
this.wave = wave;
this.masterWavelet = wave.getWavelet(WaveletId.of(domain, MASTER_DATA_WAVELET_NAME));
this.mapSerializer = new SubstrateMapSerializer();
this.domain = domain;
this.idGenerator = idGenerator;
this.factory = factory;
this.waveStatus = waveStatus;
this.participant = participant;
}
/**
* Initialization tasks not suitable for constructors.
*/
private void init() {
root = loadMap(SubstrateId.ofMap(masterWavelet.getId(), ROOT_SUBSTRATE_ID));
root.attach(SNodeRemoteContainer.Void);
}
/**
* Checks if the status of the object or any underlying component (wave view, channel, websocket...)
* is normal.
* <p>
* Throws a {@link SException} otherwise.
* <p>
* This method should be called before doing any operation in the object.
*/
protected void check() throws SException {
if (waveStatus != null)
waveStatus.check();
}
/**
* Check if node can be written by the current log in user.
* <p>
* Actual check is delegated to the node.
* <p>
* TODO Only SPrimitives nodes support access control so far.
*
* @param node
* @throws SException throw exception if access is forbidden
*/
public void checkWritable(SNode node) throws SException {
if (node != null && node instanceof SPrimitive) {
if (!((SPrimitive) node).canWrite(participant))
throw new SException(SException.WRITE_FORBIDDEN, null, "Not allowed to write property");
}
}
/**
* Check if node can be read by the current log in user.
* <p>
* Actual check is delegated to the node.
* <p>
* TODO Only SPrimitives nodes support access control so far.
*
* @param node
* @throws SException throw exception if access is forbidden
*/
public void checkReadable(SNode node) throws SException {
if (node != null && node instanceof SPrimitive) {
if (!((SPrimitive) node).canRead(participant))
throw new SException(SException.READ_FORBIDDEN, null, "Not allowed to read property");
}
}
/**
* Transform a SNode object to SNodeRemote. Check first if the node is already a SNodeRemote.
* Transform otherwise.
* <p>
* If node is already attached to a mutable node, this method should raise an
* exception.
*
* @param node
* @param newContainer
* @return
*/
protected SNodeRemote transformToRemote(SNode node, SNodeRemoteContainer parentNode, boolean newContainer) throws SException {
if (node instanceof SNodeRemote)
return (SNodeRemote) node;
ObservableWavelet containerWavelet = masterWavelet;
if (newContainer) {
containerWavelet = createContainerWavelet();
}
return transformToRemote(node, parentNode, containerWavelet);
}
/**
* Unit test only.
* TODO fix visibility
*/
@Override
protected void clearCache() {
root.clearCache();
}
@Override
public void listen(SHandler h) {
root.listen(h);
}
@Override
public void unlisten(SHandler h) {
root.unlisten(h);
}
/**
* Transform a local SNode object to SNodeRemote recursively.
*
* @param node
* @param containerWavelet
* @return
*/
private SNodeRemote transformToRemote(SNode node, SNodeRemoteContainer parentNode, ObservableWavelet containerWavelet) throws SException {
if (node instanceof SList) {
@SuppressWarnings("unchecked")
SList<SNode> list = (SList<SNode>) node;
SListRemote remoteList = loadList(SubstrateId.createForList(containerWavelet.getId(), idGenerator));
remoteList.attach(parentNode);
remoteList.enableEvents(false);
for (SNode n: list.values()) {
remoteList.add(transformToRemote(n, remoteList, containerWavelet));
}
remoteList.enableEvents(true);
return remoteList;
} else if (node instanceof SMap) {
SMap map = (SMap) node;
SMapRemote remoteMap = loadMap(SubstrateId.createForMap(containerWavelet.getId(), idGenerator));
remoteMap.attach(parentNode);
remoteMap.enableEvents(false);
for (String k: map.keys()) {
SNode v = map.getNode(k);
remoteMap.put(k, transformToRemote(v, remoteMap, containerWavelet));
}
remoteMap.enableEvents(true);
return remoteMap;
} else if (node instanceof SText) {
SText text = (SText) node;
STextRemote remoteText = loadText(SubstrateId.createForText(containerWavelet.getId(), idGenerator), text.getInitContent());
remoteText.attach(parentNode);
return remoteText;
} else if (node instanceof SPrimitive) {
SPrimitive primitive = (SPrimitive) node;
primitive.attach(parentNode);
return primitive;
}
return null;
}
private ObservableWavelet createContainerWavelet() {
return wave.createWavelet();
}
/**
* Materialize a SMapRemote from a substrate Blip/Document (substrate)
* of a Wavelet (container). <br>The underlying substrate is created if it doesn't exist yet.
* The underlying Wavelet must exists.
* <p>
* TODO: manage auto-creation of Wavelets?
*
* @param substrateId
* @return
*/
private SMapRemote loadMap(SubstrateId substrateId) {
Preconditions.checkArgument(substrateId.isMap(), "Expected a map susbtrate id");
// Cache instances
if (nodeStore.containsKey(substrateId)) {
return (SMapRemote) nodeStore.get(substrateId);
}
// Create new instance
ObservableWavelet substrateContainer = wave.getWavelet(substrateId.getContainerId());
ObservableDocument document = substrateContainer.getDocument(substrateId.getDocumentId());
DefaultDocEventRouter router = DefaultDocEventRouter.create(document);
E mapElement = DocHelper.getElementWithTagName(document, MAP_TAG);
if (mapElement == null) {
mapElement = document.createChildElement(document.getDocumentElement(), MAP_TAG,
Collections.<String, String> emptyMap());
}
ObservableBasicMap<String, SNodeRemote> map =
DocumentBasedBasicRMap.create(router,
mapElement,
Serializer.STRING,
mapSerializer,
MAP_ENTRY_TAG,
MAP_ENTRY_KEY_ATTR,
MAP_ENTRY_VALUE_ATTR);
SMapRemote n = SMapRemote.create(this, substrateId, map);
nodeStore.put(substrateId, n);
return n;
}
private SNodeRemote deserialize(String s) {
Preconditions.checkNotNull(s, "Unable to deserialize a null value");
SubstrateId substrateId = SubstrateId.deserialize(s);
if (substrateId != null) {
if (substrateId.isList())
return loadList(substrateId);
if (substrateId.isMap())
return loadMap(substrateId);
if (substrateId.isText())
return loadText(substrateId, null);
return null;
} else {
return SPrimitive.deserialize(s);
}
}
private SListRemote loadList(SubstrateId substrateId) {
// Cache instances
if (nodeStore.containsKey(substrateId)) {
return (SListRemote) nodeStore.get(substrateId);
}
// Create new instance
ObservableWavelet substrateContainer = wave.getWavelet(substrateId.getContainerId());
ObservableDocument document = substrateContainer.getDocument(substrateId.getDocumentId());
DefaultDocEventRouter router = DefaultDocEventRouter.create(document);
E listElement = DocHelper.getElementWithTagName(document, LIST_TAG);
if (listElement == null) {
listElement = document.createChildElement(document.getDocumentElement(), LIST_TAG,
Collections.<String, String> emptyMap());
}
ObservableElementList<SNodeRemote, SNodeRemote> list =
DocumentBasedElementList.create(router, listElement, LIST_ENTRY_TAG, new SubstrateListSerializer());
SListRemote n = SListRemote.create(this, substrateId, list);
nodeStore.put(substrateId, n);
return n;
}
private STextRemote loadText(SubstrateId substrateId, DocInitialization docInit) {
Preconditions.checkArgument(substrateId.isText(), "Expected a text susbtrate id");
// Cache instances
if (nodeStore.containsKey(substrateId)) {
return (STextRemote) nodeStore.get(substrateId);
}
ObservableWavelet substrateContainer = wave.getWavelet(substrateId.getContainerId());
Blip blip = substrateContainer.getBlip(substrateId.getDocumentId());
if (blip == null) {
blip = substrateContainer.createBlip(substrateId.getDocumentId());
// TODO The docInit stuff seems not to work, check out LazyContentDocument
// blip = substrateContainer.createBlip(substrateId.getDocumentId(), docInit);
}
STextRemote textRemote = factory.getSTextRemote(this, substrateId, blip);
if (docInit != null) {
textRemote.setInitContent(docInit);
}
nodeStore.put(substrateId, textRemote);
return textRemote;
}
/**
* Delete reference of this node in the cache
* and delete substrate
* @param node
*/
protected void deleteNode(SNodeRemote node) {
if (node.getSubstrateId() != null) {
emptySubstrate(node.getSubstrateId());
nodeStore.remove(node.getSubstrateId());
}
}
/**
* Empty the substrate to mark it as deleted.
* <p>
* This method must be called after the substrate
* reference is removed from its container.
*
* @param substrateId subtrate id
*/
private void emptySubstrate(SubstrateId substrateId) {
ObservableWavelet w = wave.getWavelet(substrateId.getContainerId());
Document d = null;
if (SubstrateId.isText(substrateId.getDocumentId())) {
d = w.getBlip(substrateId.getDocumentId()).getContent();
} else {
d = w.getDocument(substrateId.getDocumentId());
}
Doc.E root = DocHelper.getFirstChildElement(d, d.getDocumentElement());
if (root != null)
d.deleteNode(root);
}
@Override
public String getId() {
return ModernIdSerialiser.INSTANCE.serialiseWaveId(wave.getWaveId());
}
@Override
public void addParticipant(String participantId) throws InvalidParticipantAddress {
masterWavelet.addParticipant(ParticipantId.of(participantId));
}
@Override
public void removeParticipant(String participantId) throws InvalidParticipantAddress {
masterWavelet.removeParticipant(ParticipantId.of(participantId));
}
@Override
public String[] getParticipants() {
String[] array = new String[masterWavelet.getParticipantIds().size()];
int i = 0;
for (ParticipantId p: masterWavelet.getParticipantIds()) {
array[i++] = p.getAddress();
}
return array;
}
@Override
public Object asNative() {
return new Proxy(root, new SMapProxyHandler());
}
@Override
public void setStatusHandler(StatusHandler h) {
this.statusHandler = h;
}
public void onStatusEvent(SStatusEvent e) {
if (statusHandler != null)
statusHandler.exec(e);
}
//
// SMap interface
//
@Override
public Object get(String key) throws SException {
return root.get(key);
}
@Override
public SNode getNode(String key) throws SException {
return root.getNode(key);
}
@Override
public SMap put(String key, SNode value) throws SException {
return root.put(key, value);
}
@Override
public SMap put(String key, Object object) throws SException {
return root.put(key, object);
}
@Override
public void remove(String key) throws SException {
root.remove(key);
}
@Override
public boolean has(String key) throws SException {
return root.has(key);
}
@Override
public String[] keys() throws SException {
return root.keys();
}
@Override
public void clear() throws SException {
root.clear();
}
@Override
public boolean isEmpty() {
return root.isEmpty();
}
@Override
public int size() {
return root.size();
}
@Override
public String[] _debug_getBlipList() {
return masterWavelet.getDocumentIds().toArray(new String[masterWavelet.getDocumentIds().size()]);
}
@Override
public String _debug_getBlip(String blipId) {
if (masterWavelet.getDocumentIds().contains(blipId)) {
if (SubstrateId.isText(blipId)) {
return masterWavelet.getBlip(blipId).getContent().toXmlString();
} else {
return masterWavelet.getDocument(blipId).toXmlString();
}
}
return null;
}
@Override
public void setPublic(boolean isPublic) {
try {
if (isPublic)
addParticipant("@"+domain);
else
removeParticipant("@"+domain);
} catch (InvalidParticipantAddress e) {
}
}
public boolean isPublic() {
try {
return masterWavelet.getParticipantIds().contains(ParticipantId.of("@"+domain));
} catch (InvalidParticipantAddress e) {
return false;
}
}
@Override
public SMap getPrivateArea() {
// Initialize user's private area
if (userRoot == null) {
// Initialize user wavelet
if (userWavelet == null) {
userWavelet = wave.getUserData();
if (userWavelet == null)
userWavelet = wave.createUserData();
}
userRoot = loadMap(SubstrateId.ofMap(userWavelet.getId(), USER_ROOT_SUBSTRATED_ID));
userRoot.attach(SNodeRemoteContainer.Void);
}
return userRoot;
}
public boolean isNew() {
return false;
}
}