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 } }