package org.swellrt.model.generic;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import org.swellrt.model.ReadableModel;
import org.swellrt.model.shared.ModelUtils;
import org.waveprotocol.wave.media.model.AttachmentId;
import org.waveprotocol.wave.media.model.AttachmentIdGenerator;
import org.waveprotocol.wave.media.model.AttachmentIdGeneratorImpl;
import org.waveprotocol.wave.model.document.Doc;
import org.waveprotocol.wave.model.document.Doc.E;
import org.waveprotocol.wave.model.document.Doc.N;
import org.waveprotocol.wave.model.document.Doc.T;
import org.waveprotocol.wave.model.document.ObservableDocument;
import org.waveprotocol.wave.model.document.indexed.DocumentEvent;
import org.waveprotocol.wave.model.document.indexed.DocumentHandler;
import org.waveprotocol.wave.model.document.operation.DocInitialization;
import org.waveprotocol.wave.model.document.parser.XmlParseException;
import org.waveprotocol.wave.model.document.util.DefaultDocEventRouter;
import org.waveprotocol.wave.model.document.util.DocEventRouter;
import org.waveprotocol.wave.model.document.util.DocHelper;
import org.waveprotocol.wave.model.document.util.DocProviders;
import org.waveprotocol.wave.model.id.IdGenerator;
import org.waveprotocol.wave.model.id.IdGeneratorImpl;
import org.waveprotocol.wave.model.id.IdGeneratorImpl.Seed;
import org.waveprotocol.wave.model.id.WaveId;
import org.waveprotocol.wave.model.id.WaveletId;
import org.waveprotocol.wave.model.util.CopyOnWriteSet;
import org.waveprotocol.wave.model.util.Preconditions;
import org.waveprotocol.wave.model.version.HashedVersion;
import org.waveprotocol.wave.model.wave.Blip;
import org.waveprotocol.wave.model.wave.ObservableWavelet;
import org.waveprotocol.wave.model.wave.ParticipantId;
import org.waveprotocol.wave.model.wave.SourcesEvents;
import org.waveprotocol.wave.model.wave.WaveletListener;
import org.waveprotocol.wave.model.wave.data.WaveletData;
import org.waveprotocol.wave.model.wave.opbased.ObservableWaveView;
import org.waveprotocol.wave.model.waveref.WaveRef;
import com.google.common.collect.ImmutableMap;
/**
* A model is a Wavelet wrapper storing a tree-like structure of data objects of
* the Type hierarchy.
*
* <p>
* [version not provided] <br/>
* The original very buggy implementation. No longer supported.
*
* <p>
* version 0.1 <br/>
*
* Each <code>Type</code> instance stores values in a new <code>Document</code>
* but strings, they are stored in a separated document storing a string index.
* <br/>
*
* Simplified <code>Type</code> interface, only one <code>attach()</code>
* method. <br/>
*
* Improved class names to distinguish List and Map inner tools:
* ListElementFactory...<br/>
*
* The main document is now "map+root", supporting metadata, index of string and
* the root map.<br/>
*
* version 0.2 - SwellRT branding and new TextType<br/>
*
* Wave: s+XXXXXX <br/>
* Wavelet: swl+root <br/>
* Root Document : model+root <br/>
*
* version 1.0 - Data model supporting access control and metadata per blip
*
* Model metadata: model+root <br/>
* Root map: map+root <br/>
* Map blip: map+XXXX <br/>
* List blip: list+XXXX <br/>
* Text blip: b+XXXX <br/>
*
* Metadata attributes for model; <br/>
* v = model version <br/>
* t = model type id (for custom data types) <br/>
* a = app id <br />
*
*
* Metadata attributes for types (Map, List and future containers): <br/>
* pc = participant creator <br/>
* tc = timestamp creation <br/>
* pm = participant lastmod <br/>
* tm = timestamp lastmod <br/>
* ap = access policy <br/>
* acl = access control list <br/>
* p = path of this object in the wavelet <br/>
*
* Primitive values are stored in each container document. (Former string index
* is deprecated)
*
*/
public class Model implements ReadableModel, SourcesEvents<Model.Listener> {
/**
* The model version of the current source code. Check {@link ModelMigrator}
* for migration procedures.
*/
public static final String MODEL_VERSION = "1.0";
/**
* For future use.
*/
public static final String MODEL_TYPE_DEFAULT = "default";
/**
* For future use.
*/
public static final String MODEL_APP_DEFAULT = "default";
public interface Listener {
void onAddParticipant(ParticipantId participant);
void onRemoveParticipant(ParticipantId participant);
}
/**
* A prefix for SwellRT wavelets.
*/
public static final String WAVELET_SWELL_PREFIX = "swl";
/**
* Name of wavelet containing the public view (default) of a collaborative
* object.
*/
public static final String WAVELET_SWELL_ROOT = "swl+root";
/**
* Name of the blip/document storing collaborative object metadata
*/
public static final String DOC_MODEL_ROOT = "model+root";
/**
* Name of substrate document for the root of the collaborative object.
*/
public static final String DOC_MAP_ROOT = "map+root";
/**
* Tag name of the model section (metadata).
*/
public static final String TAG_MODEL = "model";
public static final String ATTR_VERSION_METADATA = "v";
public static final String ATTR_TYPE_METADATA = "t";
public static final String ATTR_APP_METADATA = "a";
/**
* Utility method to check for swellrt wavelet id.
*/
public static boolean isModelWaveletId(WaveletId waveletId) {
return waveletId.getId().startsWith(WAVELET_SWELL_PREFIX);
}
/**
* The Document substrate of this object, the model metadata info of the
* collaborative object.
*/
private final ObservableDocument doc;
/**
* Wavelet supporting the whole collaborative object
*/
private final ObservableWavelet wavelet;
/**
* Wavelet supporting the whole collaborative object
*/
private final WaveletData waveletData;
/**
* An id generator for swellrt blips
*/
private final TypeIdGenerator idGenerator;
/**
* An id generattor for attachments
*/
private final AttachmentIdGenerator attachmentIdGenerator;
/**
* The current participant accesing the model
*/
private final ParticipantId currentParticipant;
/**
* Reference to the root map
*/
private MapType rootMap = null;
private final CopyOnWriteSet<Listener> listeners = CopyOnWriteSet.create();
/**
* Create or load a new collaborative object (aka model). Blip-based data will
* be migrated to the current model version.
*
* @param wave
* @param domain
* @param loggedInUser
* @param isNewWave
* @param idGenerator
* @return
*/
public static Model create(ObservableWaveView wave, String domain, ParticipantId loggedInUser,
boolean isNewWave, IdGenerator idGenerator) {
WaveletId waveletId = WaveletId.of(domain, WAVELET_SWELL_ROOT);
ObservableWavelet wavelet = wave.getWavelet(waveletId);
// New
if (wavelet == null) {
wavelet = wave.createWavelet(waveletId);
wavelet.addParticipant(loggedInUser);
} else {
// Existing, check for migration
boolean wasOk = ModelMigrator.migrateIfNecessary(domain, wave);
// TODO Log migration result
if (!wasOk) return null;
}
//
// Set up the Root document
//
ObservableDocument modelDocument = wavelet.getDocument(DOC_MODEL_ROOT);
DocEventRouter router = DefaultDocEventRouter.create(modelDocument);
//
//
// <model v="1.0" a="default" t="default"> </model>
//
Doc.E metadataElement = DocHelper.getElementWithTagName(modelDocument, TAG_MODEL);
if (metadataElement == null) {
metadataElement =
modelDocument.createChildElement(modelDocument.getDocumentElement(), TAG_MODEL,
ImmutableMap.of(ATTR_VERSION_METADATA, MODEL_VERSION, ATTR_TYPE_METADATA,
MODEL_TYPE_DEFAULT, ATTR_APP_METADATA, MODEL_APP_DEFAULT));
}
return new Model(wavelet, TypeIdGenerator.get(idGenerator), loggedInUser);
}
/**
* Create a model from an existing wavelet. Don't perform integrity checks.
* This method is intended for server-side logic.
*
* @param wavelet the substrate of the object data model
* @param participantId the user who will operates the object
*/
public static Model create(ObservableWavelet wavelet, ParticipantId participantId, Seed seed) {
return new Model(wavelet,
TypeIdGenerator.get(new IdGeneratorImpl(participantId.getDomain(), seed)),
participantId);
}
/**
* Constructor.
*
* @param wavelet
* @param idGenerator
*/
protected Model(ObservableWavelet wavelet, TypeIdGenerator idGenerator,
ParticipantId currentParticipant) {
this.wavelet = wavelet;
this.wavelet.addListener(waveletListener);
this.waveletData = wavelet.getWaveletData();
this.idGenerator = idGenerator;
this.attachmentIdGenerator =
new AttachmentIdGeneratorImpl(idGenerator.getUnderlyingGenerator());
this.doc = wavelet.getDocument(DOC_MODEL_ROOT);
this.currentParticipant = currentParticipant;
}
public ParticipantId getCurrentParticipantId() {
return currentParticipant;
}
public String getId() {
return this.getWaveId().serialise();
}
public WaveId getWaveId() {
return this.waveletData.getWaveId();
}
public WaveletId getWaveletId() {
return wavelet.getId();
}
public WaveRef getWaveRef() {
return WaveRef.of(getWaveId(), getWaveletId());
}
protected String generateDocId(String prefix) {
return idGenerator.newDocumentId(prefix);
}
public AttachmentId generateAttachmentId() {
return attachmentIdGenerator.newAttachmentId();
}
protected ObservableDocument createDocument(String docId) {
Preconditions.checkArgument(!wavelet.getDocumentIds().contains(docId),
"Trying to create an existing substrate document");
return wavelet.getDocument(docId);
}
protected DocInitialization getTextDocInitialization(String text) {
DocInitialization op;
String initContent = "<body><line/>" + text + "</body>";
try {
op = DocProviders.POJO.parse(initContent).asOperation();
} catch (IllegalArgumentException e) {
if (e.getCause() instanceof XmlParseException) {
// GWT.log("Ill-formed XML string ", e.getCause());
// TODO How handle this?
} else {
// GWT.log("Error", e);
}
return null;
}
// DocumentSchema schema = ConversationSchemas.BLIP_SCHEMA_CONSTRAINTS;
// ViolationCollector vc = new ViolationCollector();
// if (!DocOpValidator.validate(vc, schema, op).isValid()) {
// GWT.log("That content does not conform to the schema: " + vc.toString());
// return;
// }
return op;
}
protected Blip createBlip(String docId) {
Preconditions.checkArgument(!wavelet.getDocumentIds().contains(docId),
"Trying to create an existing substrate document");
return wavelet.createBlip(docId);
}
protected ObservableDocument getDocument(String docId) {
Preconditions.checkArgument(wavelet.getDocumentIds().contains(docId),
"Trying to get a non existing substrate document");
return wavelet.getDocument(docId);
}
protected Blip getBlip(String docId) {
Preconditions.checkArgument(wavelet.getDocumentIds().contains(docId),
"Trying to get a non existing substrate document");
return wavelet.getBlip(docId);
}
//
// Listeners
//
@Override
public void addListener(Listener listener) {
listeners.add(listener);
}
@Override
public void removeListener(Listener listener) {
listeners.add(listener);
}
//
// Public operations
//
public Set<ParticipantId> getParticipants() {
return wavelet.getParticipantIds();
}
public void addParticipant(String address) {
wavelet.addParticipant(ParticipantId.ofUnsafe(address));
}
public void removeParticipant(String address) {
wavelet.removeParticipant(ParticipantId.ofUnsafe(address));
}
public MapType getRoot() {
// Lazy initialization of the root map
if (rootMap == null) {
rootMap = MapType.deserialize(this, DOC_MAP_ROOT);
if (rootMap.getPath().isEmpty()) rootMap.setPath("root");
}
return rootMap;
}
public MapType createMap() {
return new MapType(this);
}
public StringType createString(String value) {
return new StringType(value);
}
public ListType createList() {
return new ListType(this);
}
public TextType createText() {
return new TextType(this);
}
public TextType createText(String textOrXml) {
TextType tt = new TextType(this);
if (textOrXml != null) tt.setInitContent(textOrXml);
return tt;
}
public FileType createFile(AttachmentId attachmentId) {
return new FileType(attachmentId, null, this);
}
public FileType createFile(AttachmentId attachmentId, String contentType) {
return new FileType(attachmentId, contentType, this);
}
public NumberType createNumber(String value) {
return new NumberType(value);
}
public NumberType createNumber(int value) {
return new NumberType(value);
}
public NumberType createNumber(double value) {
return new NumberType(value);
}
public BooleanType createBoolean(boolean value) {
return new BooleanType(value);
}
public BooleanType createBoolean(String value) {
return new BooleanType(value);
}
@Override
public Type fromPath(String path) {
return (Type) ModelUtils.fromPath(this, path);
}
/**
* For debug purposes only
*/
public Set<String> getModelDocuments() {
return wavelet.getDocumentIds();
}
/**
* For debug purposes only
*/
public String getModelDocument(String documentId) {
return wavelet.getDocument(documentId).toXmlString();
}
private Map<String, DocumentHandler<Doc.N, Doc.E, Doc.T>> docHandlers =
new HashMap<String, DocumentHandler<Doc.N, Doc.E, Doc.T>>();
public void debugDocumentEvents(final String docId, boolean debug) {
if (!wavelet.getDocumentIds().contains(docId)) return;
ObservableDocument doc = getDocument(docId);
if (debug) {
if (!docHandlers.containsKey(docId)) {
DocumentHandler<Doc.N, Doc.E, Doc.T> handler = new DocumentHandler<Doc.N, Doc.E, Doc.T>() {
@Override
public void onDocumentEvents(
org.waveprotocol.wave.model.document.indexed.DocumentHandler.EventBundle<N, E, T> event) {
ModelUtils.log("-------- (" + docId + ") --------");
for (DocumentEvent<Doc.N, Doc.E, Doc.T> e : event.getEventComponents()) {
ModelUtils.log(e.toString());
}
ModelUtils.log("----------------------------------");
}
};
docHandlers.put(docId, handler);
doc.addListener(handler);
}
} else {
if (docHandlers.containsKey(docId)) docHandlers.remove(docId);
}
}
//
// Wavelet Listener
//
private final WaveletListener waveletListener = new WaveletListener() {
@Override
public void onParticipantRemoved(ObservableWavelet wavelet, ParticipantId participant) {
for (Listener l : listeners)
l.onRemoveParticipant(participant);
}
@Override
public void onParticipantAdded(ObservableWavelet wavelet, ParticipantId participant) {
for (Listener l : listeners)
l.onAddParticipant(participant);
}
@Override
public void onLastModifiedTimeChanged(ObservableWavelet wavelet, long oldTime, long newTime) {
// TODO Auto-generated method stub
}
@Override
public void onBlipAdded(ObservableWavelet wavelet, Blip blip) {
// TODO Auto-generated method stub
}
@Override
public void onBlipRemoved(ObservableWavelet wavelet, Blip blip) {
// TODO Auto-generated method stub
}
@Override
public void onBlipSubmitted(ObservableWavelet wavelet, Blip blip) {
// TODO Auto-generated method stub
}
@Override
public void onBlipTimestampModified(ObservableWavelet wavelet, Blip blip, long oldTime,
long newTime) {
// TODO Auto-generated method stub
}
@Override
public void onBlipVersionModified(ObservableWavelet wavelet, Blip blip, Long oldVersion,
Long newVersion) {
// TODO Auto-generated method stub
}
@Override
public void onBlipContributorAdded(ObservableWavelet wavelet, Blip blip,
ParticipantId contributor) {
// TODO Auto-generated method stub
}
@Override
public void onBlipContributorRemoved(ObservableWavelet wavelet, Blip blip,
ParticipantId contributor) {
// TODO Auto-generated method stub
}
@Override
public void onVersionChanged(ObservableWavelet wavelet, long oldVersion, long newVersion) {
// TODO Auto-generated method stub
}
@Override
public void onHashedVersionChanged(ObservableWavelet wavelet, HashedVersion oldHashedVersion,
HashedVersion newHashedVersion) {
// TODO Auto-generated method stub
}
@Override
public void onRemoteBlipContentModified(ObservableWavelet wavelet, Blip blip) {
// TODO Auto-generated method stub
}
};
public static Type getField(Type parent, String path) {
Preconditions.checkArgument(path != null, "Can't get field from null path");
Preconditions.checkArgument(parent != null, "Can't field from null parent");
if (path.isEmpty())
return parent;
int pathSeparatorIndex = path.indexOf(".");
String keyOrIndex = pathSeparatorIndex != -1 ? path.substring(0, pathSeparatorIndex) : path;
if (pathSeparatorIndex == -1) {
return getFromContainerField(parent, keyOrIndex);
} else {
String nextPath = path.substring(pathSeparatorIndex+1);
Type nextParent = getFromContainerField(parent, keyOrIndex);
return getField(nextParent, nextPath);
}
}
private static Type getFromContainerField(Type container, String keyOrIndex) {
if (container instanceof MapType) {
MapType map = (MapType) container;
return map.get(keyOrIndex);
} else if (container instanceof ListType) {
try {
int index = Integer.valueOf(keyOrIndex);
ListType list = (ListType) container;
return list.get(index);
} catch (NumberFormatException e) {
}
}
return null;
}
}