package org.swellrt.server.box.events;
import java.util.HashMap;
import java.util.Map;
import org.swellrt.model.ReadableString;
import org.swellrt.model.ReadableType;
import org.swellrt.model.generic.ListType;
import org.swellrt.model.generic.MapType;
import org.swellrt.model.generic.Model;
import org.swellrt.model.shared.ModelUtils;
import org.swellrt.model.unmutable.UnmutableModel;
import org.swellrt.server.box.events.Event.Builder;
import org.swellrt.server.box.events.Event.Type;
import org.waveprotocol.box.common.DeltaSequence;
import org.waveprotocol.box.server.waveserver.WaveBus.Subscriber;
import org.waveprotocol.wave.model.document.Doc;
import org.waveprotocol.wave.model.document.Document;
import org.waveprotocol.wave.model.document.operation.AnnotationBoundaryMap;
import org.waveprotocol.wave.model.document.operation.Attributes;
import org.waveprotocol.wave.model.document.operation.AttributesUpdate;
import org.waveprotocol.wave.model.document.operation.DocOp;
import org.waveprotocol.wave.model.document.operation.DocOpCursor;
import org.waveprotocol.wave.model.document.util.DocHelper;
import org.waveprotocol.wave.model.id.WaveletName;
import org.waveprotocol.wave.model.operation.wave.AddParticipant;
import org.waveprotocol.wave.model.operation.wave.BlipContentOperation;
import org.waveprotocol.wave.model.operation.wave.BlipOperation;
import org.waveprotocol.wave.model.operation.wave.RemoveParticipant;
import org.waveprotocol.wave.model.operation.wave.TransformedWaveletDelta;
import org.waveprotocol.wave.model.operation.wave.WaveletBlipOperation;
import org.waveprotocol.wave.model.operation.wave.WaveletOperation;
import org.waveprotocol.wave.model.version.HashedVersion;
import org.waveprotocol.wave.model.wave.data.ReadableBlipData;
import org.waveprotocol.wave.model.wave.data.ReadableWaveletData;
import org.waveprotocol.wave.util.logging.Log;
import com.google.inject.Inject;
/**
* A processor which generates Data Model Events interpreting Document
* Operations received from the Wave Bus.
*
* Document operations sequences are generated by mutations in a swellrt data
* Model.
*
* Generated events can include context data from the mutated model according a
* set of provided paths by an event processor configurator.
*
*
*
* @author pablojan@gmail.com (Pablo Ojanguren)
*
*/
public class DeltaBasedEventSource implements Subscriber {
private static final Log LOG = Log.get(DeltaBasedEventSource.class);
/**
* A DocOp cursor that generates data model events. The same cursor must be
* used to process all the ops in the same delta.
*
* For now this cursor is used to generate events for maps, lists and docs but
* it might be clearer to have separated cursor by data type according the
* blip id.
*
* @author pablojan@gmail.com (Pablo Ojanguren)
*
*/
private class DocOpToEventCursor implements DocOpCursor {
static final String CREATE_ELEMENT = "create";
static final String DELETE_ELEMENT = "delete";
String lastMapDocOp = null;
String lastMapDocOpKey = null;
String lastMapDocOpValue = null;
/** The context of this op */
ReadableWaveletData wavelet;
/** The context of this op */
UnmutableModel dataModel;
/** An event builder */
Event.Builder eventBuilder;
/** The data model path of the object affected by the next op to process */
String path;
public void flushPendingOps() {
if (lastMapDocOp != null) triggerLastMapDocOp();
}
protected void setLastMapDocOp(String op, String key, String value) {
lastMapDocOp = op;
lastMapDocOpKey = key;
lastMapDocOpValue = value;
}
protected Event generateEventWithContext(Event.Type eventType, String path, String valueRef) {
Event e = null;
// Check which type of value of the event:
// primitive, or container = map, list
if (ModelUtils.isContainerId(valueRef)) {
// Check if the blip of this container still exists
if (wavelet.getDocumentIds().contains(valueRef)) {
String containerPath =
ModelUtils.getMetadataPath(wavelet.getDocument(valueRef).getContent()
.getMutableDocument());
// Evaluate context data based on the container's path
e = eventBuilder.build(eventType, path,
getContextData(dataModel, containerPath, eventBuilder.getContextData()));
}
} else {
// First evaluate context data's expressions based on the value's
// parent
Map<String, String> contextData = getContextData(dataModel, path.substring(0, path.lastIndexOf(".")), eventBuilder.getContextData());
// Second, add the event's value in the context
contextData.putAll(getContextData(wavelet.getDocument(eventBuilder.getBlipId()), path, valueRef));
e = eventBuilder.build(eventType, path, contextData);
}
return e;
}
protected void triggerLastMapDocOp() {
Event e = null;
if (lastMapDocOp.equals(CREATE_ELEMENT)) {
// TODO support new MAP_ENTRY_CREATED
e =
generateEventWithContext(Type.MAP_ENTRY_UPDATED, path + "." + lastMapDocOpKey,
lastMapDocOpValue);
} else if (lastMapDocOp.equals(DELETE_ELEMENT)) {
e =
generateEventWithContext(Type.MAP_ENTRY_REMOVED, path + "." + lastMapDocOpKey,
lastMapDocOpValue);
}
if (e != null) eventQueue.add(e);
setLastMapDocOp(null, null, null);
}
protected void triggerLastMapDocOpUpdate() {
Event e =
generateEventWithContext(Type.MAP_ENTRY_UPDATED, path + "." + lastMapDocOpKey, lastMapDocOpValue);
if (e != null) eventQueue.add(e);
setLastMapDocOp(null, null, null);
}
/**
* For map operations we need to check previous doc op to distiguish between
* new, update or remove map ops.
*
* @param op
* @param key
* @param value
*/
protected void processMapDocOp(String op, String key, String value) {
// LOG.info("Map DocOp = [" + op + ":" + key + "]");
// buffer op
if (lastMapDocOp == null) {
setLastMapDocOp(op, key, value);
} else {
if (lastMapDocOpKey.equals(key)) {
// a map update is a sequence of startElement - endElement or
// viceversa
if (!lastMapDocOp.equals(op)) {
triggerLastMapDocOpUpdate();
} else {
triggerLastMapDocOp();
setLastMapDocOp(op, key, value);
}
} else {
triggerLastMapDocOp();
setLastMapDocOp(op, key, value);
}
}
}
public DocOpToEventCursor(ReadableWaveletData wavelet, UnmutableModel dataModel,
Builder eventBuilder) {
super();
this.wavelet = wavelet;
this.dataModel = dataModel;
this.eventBuilder = eventBuilder;
}
public void setDocOpContext(String path) {
this.path = path;
}
@Override
public void annotationBoundary(AnnotationBoundaryMap map) {
}
@Override
public void characters(String chars) {
String blipId = eventBuilder.getBlipId();
if (ModelUtils.isTextBlip(blipId)) {
eventQueue.add(eventBuilder.buildDocChange(path, chars));
}
}
@Override
public void elementStart(String type, Attributes attrs) {
String blipId = eventBuilder.getBlipId();
//
// Map Op
//
if (ModelUtils.isMapBlip(blipId) && type.equals(MapType.TAG_ENTRY)) {
processMapDocOp(CREATE_ELEMENT, attrs.get(MapType.KEY_ATTR_NAME),
attrs.get(MapType.VALUE_ATTR_NAME));
return;
}
//
// List Op
//
if (ModelUtils.isListBlip(blipId) && type.equals(ListType.TAG_LIST_ITEM)) {
String ref = attrs.get(ListType.ATTR_LIST_ITEM_REF);
Event e = generateEventWithContext(Type.LIST_ITEM_ADDED, path + ".?", ref);
if (e != null) eventQueue.add(e);
}
}
@Override
public void elementEnd() {
}
@Override
public void retain(int itemCount) {
}
@Override
public void deleteCharacters(String chars) {
String blipId = eventBuilder.getBlipId();
if (ModelUtils.isTextBlip(blipId)) {
eventQueue.add(eventBuilder.buildDocChange(path, chars));
}
}
@Override
public void deleteElementStart(String type, Attributes attrs) {
String blipId = eventBuilder.getBlipId();
//
// Map Op
//
if (ModelUtils.isMapBlip(blipId) && type.equals(MapType.TAG_ENTRY)) {
String key = attrs.get(MapType.KEY_ATTR_NAME);
String value = attrs.get(MapType.VALUE_ATTR_NAME);
processMapDocOp(DELETE_ELEMENT, key, value);
return;
}
//
// List Op
//
if (type.equals(ListType.TAG_LIST_ITEM)) {
eventQueue.add(eventBuilder.build(Type.LIST_ITEM_REMOVED, path));
}
}
@Override
public void deleteElementEnd() {
}
@Override
public void replaceAttributes(Attributes oldAttrs, Attributes newAttrs) {
}
@Override
public void updateAttributes(AttributesUpdate attrUpdate) {
}
}
protected EventQueue eventQueue;
@Inject
public DeltaBasedEventSource(EventQueue eventQueue) {
this.eventQueue = eventQueue;
}
protected String getWaveletApp(ReadableWaveletData wavelet) {
Document docModelRoot =
wavelet.getDocument(Model.DOC_MODEL_ROOT).getContent().getMutableDocument();
Doc.E eltModel = DocHelper.getElementWithTagName(docModelRoot, Model.TAG_MODEL);
return docModelRoot.getAttribute(eltModel, Model.ATTR_APP_METADATA);
}
protected String getWaveletDataType(ReadableWaveletData wavelet) {
Document docModelRoot =
wavelet.getDocument(Model.DOC_MODEL_ROOT).getContent().getMutableDocument();
Doc.E eltModel = DocHelper.getElementWithTagName(docModelRoot, Model.TAG_MODEL);
return docModelRoot.getAttribute(eltModel, Model.ATTR_TYPE_METADATA);
}
/**
* Returns a context data map populated with values for paths matching the
* provided value path.
* <p>
* A context's path matches the provided path expression if it is partial or
* fully compatible: <br>
* <p>
* Example 1:<br>
* <p>
* ContextData["root.list.?.field"]
* <p>
* Value Path = "root.list.3"<br>
* <p>
* generates<br>
* <p>
* ContextData["root.list.?.field"] = value of "root.list.3.field"
* <p>
* <p>
* Example 2:<br>
* <p>
* ContextData["root.list.?.array.?.field"]
* <p>
* Value Path "root.list.2.array.5"<br>
* <p>
* generates<br>
* <p>
* ContextData["root.list.?.array.?.field"] = value of "root.list.2.array.5"
*
* @param dataModel the data model of the event
* @param valuePath the path to match with context paths
* @params contextData context data of the event
* @return a copy of the provided context populated with values
*
*/
protected Map<String, String> getContextData(UnmutableModel dataModel, String valuePath,
Map<String, String> contextData) {
Map<String, String> newContextData = new HashMap<String, String>(contextData);
for (String key : contextData.keySet()) {
String path = ExpressionParser.matchSubpath(key, valuePath);
if (path != null) {
ReadableType value = dataModel.fromPath(path);
if (value != null) {
ReadableString strValue = value.asString();
if (strValue != null) newContextData.put(key, strValue.getValue());
}
}
}
return newContextData;
}
/**
* Returns a context data map for the provided path and simple value within a
* container blip.
* <p>
* e.g.
* <p>
* dataPath = root.list.?
* <p>
* dataValueRef = str+3
* <p>
* returns
* <p>
* ["root.list.?"] = value of str+3 in the provided blip
*
*
*
* @param dataContainerBlip the blip containing the data
* @param dataPath the path pointing a value within the blip
* @param dataValueRef the pointer to a value within the blip
*
*/
protected Map<String, String> getContextData(ReadableBlipData dataContainerBlip,
String dataPath,
String dataValueRef) {
Map<String, String> opContextData = new HashMap<String, String>();
opContextData.put(dataPath, ModelUtils.getContainerValue(dataContainerBlip, dataValueRef));
return opContextData;
}
/**
* Populate map with values of static path expressions for the provided data
* model.<br>
* <p>
* Paths with wildcards are ignored, they should be evaluated on op event
* processing.
*
* @param dataModel
* @param contextData
*/
protected void populateStaticContextData(UnmutableModel dataModel, Map<String, String> contextData) {
for (String path : contextData.keySet()) {
if (!path.contains("?")) {
ReadableType value = dataModel.fromPath(path);
if (value != null) {
contextData.put(path, value.toString());
}
}
}
}
/**
* Create an map having paths expressions as keys.<br>
* <p>
* For static expressions (e.g. "root.key.field") value can be set before op
* event is processed.
* <p>
* For relative expressions (e.g. "root.?.list.?.field) value must be
* calculated in the context of the op event.
*
* @param app
* @param dataType
* @return
*/
protected Map<String, String> initializeContextData(String app, String dataType) {
Map<String, String> emptyContextData = new HashMap<String, String>();
for (String p : eventQueue.getExpressionPaths(app, dataType)) {
emptyContextData.put(p, null);
}
return emptyContextData;
}
@Override
public void waveletUpdate(ReadableWaveletData wavelet, DeltaSequence deltas) {
try {
// This only applies to SwellRT data model wavelets
if (!wavelet.getWaveletId().getId().equals(Model.WAVELET_SWELL_ROOT)) return;
String app = getWaveletApp(wavelet);
String dataType = getWaveletDataType(wavelet);
// Only process deltas if there are event rules for this app and data type
if (!eventQueue.hasEventsFor(app, dataType)) return;
UnmutableModel dataModel = null;
Map<String, String> contextData = initializeContextData(app, dataType);
// Avoid to generate the tree data model if no context data is required
if (!contextData.isEmpty()) {
dataModel = UnmutableModel.create(wavelet);
populateStaticContextData(dataModel, contextData);
}
Event.Builder eventBuilder =
new Event.Builder().app(app).dataType(dataType).contextData(contextData)
.waveId(wavelet.getWaveId()).waveletId(wavelet.getWaveletId());
for (TransformedWaveletDelta delta : deltas) {
eventBuilder.deltaVersion(delta.getResultingVersion()).author(delta.getAuthor().getAddress())
.timestamp(delta.getApplicationTimestamp());
DocOpToEventCursor cursor = new DocOpToEventCursor(wavelet, dataModel, eventBuilder);
for (WaveletOperation op : delta) {
if (op instanceof AddParticipant) {
eventQueue.add(eventBuilder.buildAddParticipant(((AddParticipant) op).getParticipantId()));
}
if (op instanceof RemoveParticipant) {
eventQueue.add(eventBuilder.buildRemoveParticipant(((RemoveParticipant) op)
.getParticipantId()));
}
if (op instanceof WaveletBlipOperation) {
WaveletBlipOperation wbop = (WaveletBlipOperation) op;
BlipOperation bop = wbop.getBlipOp();
ReadableBlipData blipData = wavelet.getDocument(wbop.getBlipId());
String path = "";
if (ModelUtils.isTextBlip(wbop.getBlipId())) {
// TODO get path of text objects from a suitable metadata attribute
// on <body> tag ?
path = ModelUtils.getTextTypePath(blipData.getContent().getMutableDocument());
} else {
path = ModelUtils.getMetadataPath(blipData.getContent().getMutableDocument());
}
eventBuilder.blipId(blipData.getId());
if (bop instanceof BlipContentOperation) {
BlipContentOperation bcop = (BlipContentOperation) bop;
DocOp docOp = bcop.getContentOp();
// Set specific contex data for this doc op.
cursor.setDocOpContext(path);
docOp.apply(cursor);
}
}
}
// Remove pending deletion events
cursor.flushPendingOps();
}
} catch (Exception e) {
LOG.info("Error generating event on delta received: " + e.getMessage());
}
}
@Override
public void waveletCommitted(WaveletName waveletName, HashedVersion version) {
// For now, we ignore this
}
}