| /** |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you 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 org.waveprotocol.box.server.robots.passive; |
| |
| import com.google.common.base.Preconditions; |
| import com.google.common.base.Strings; |
| import com.google.common.collect.Lists; |
| import com.google.common.collect.Maps; |
| import com.google.wave.api.BlipData; |
| import com.google.wave.api.Context; |
| import com.google.wave.api.Gadget; |
| import com.google.wave.api.data.converter.ContextResolver; |
| import com.google.wave.api.data.converter.EventDataConverter; |
| import com.google.wave.api.event.*; |
| import com.google.wave.api.impl.EventMessageBundle; |
| import com.google.wave.api.robot.Capability; |
| import com.google.wave.api.robot.RobotName; |
| import org.waveprotocol.box.server.robots.util.ConversationUtil; |
| import org.waveprotocol.box.server.util.WaveletDataUtil; |
| import org.waveprotocol.wave.model.conversation.*; |
| 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.DocHandler; |
| import org.waveprotocol.wave.model.document.ObservableDocument; |
| import org.waveprotocol.wave.model.document.indexed.DocumentEvent; |
| import org.waveprotocol.wave.model.document.indexed.DocumentEvent.AnnotationChanged; |
| import org.waveprotocol.wave.model.document.indexed.DocumentEvent.AttributesModified; |
| import org.waveprotocol.wave.model.document.indexed.DocumentEvent.ContentInserted; |
| import org.waveprotocol.wave.model.document.raw.impl.Node; |
| import org.waveprotocol.wave.model.operation.OperationException; |
| import org.waveprotocol.wave.model.operation.SilentOperationSink; |
| import org.waveprotocol.wave.model.operation.wave.BasicWaveletOperationContextFactory; |
| 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.wave.*; |
| import org.waveprotocol.wave.model.wave.data.ObservableWaveletData; |
| import org.waveprotocol.wave.model.wave.opbased.OpBasedWavelet; |
| import org.waveprotocol.wave.model.wave.opbased.WaveletListenerImpl; |
| |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| |
| /** |
| * Generates Robot API Events from operations applied to a Wavelet. |
| * |
| * <p> |
| * Events that exist in the API: |
| * <li>WaveletBlipCreated (DONE)</li> |
| * <li>WaveletBlipRemoved (DONE)</li> |
| * <li>WaveletParticipantsChanged (DONE)</li> |
| * <li>WaveletSelfAdded (DONE)</li> |
| * <li>WaveletSelfRemoved (DONE)</li> |
| * <li>DocumentChanged (DONE)</li> |
| * <li>AnnotatedTextChanged (DONE)</li> |
| * <li>FormButtonClicked (TBD)</li> |
| * <li>GadgetStateChanged (DONE)</li> |
| * <li>BlipContributorChanged (TBD)</li> |
| * <li>WaveletTagsChanged (TBD)</li> |
| * <li>WaveletTitleChanged (TBD)</li> |
| * <li>BlipSubmitted (Will not be supported, submit ops will be phased out)</li> |
| * |
| * @author ljvderijk@google.com (Lennard de Rijk) |
| */ |
| public class EventGenerator { |
| |
| private static class EventGeneratingWaveletListener extends WaveletListenerImpl { |
| @SuppressWarnings("unused") |
| private final Map<EventType, Capability> capabilities; |
| |
| /** |
| * Creates a {@link WaveletListener} which will generate events according to |
| * the capabilities. |
| * |
| * @param capabilities the capabilities which we are interested in. |
| */ |
| public EventGeneratingWaveletListener(Map<EventType, Capability> capabilities) { |
| this.capabilities = capabilities; |
| } |
| // TODO(ljvderijk): implement more events. This class should listen for |
| // non-conversational blip changes and robot data documents as indicated by |
| // IdConstants.ROBOT_PREFIX |
| } |
| |
| private class EventGeneratingConversationListener extends ConversationListenerImpl { |
| private final Map<EventType, Capability> capabilities; |
| private final Conversation conversation; |
| private final EventMessageBundle messages; |
| |
| // Event collectors |
| private final List<String> participantsAdded = Lists.newArrayList(); |
| private final List<String> participantsRemoved = Lists.newArrayList(); |
| |
| // Changes for each delta |
| private ParticipantId deltaAuthor; |
| private Long deltaTimestamp; |
| |
| /** |
| * Creates a {@link ObservableConversation.Listener} which will generate |
| * events according to the capabilities. |
| * |
| * @param conversation the conversation we are observing. |
| * @param capabilities the capabilities which we are interested in. |
| * @param messages the bundle to put the events in. |
| */ |
| public EventGeneratingConversationListener(Conversation conversation, |
| Map<EventType, Capability> capabilities, EventMessageBundle messages, RobotName robotName) { |
| this.conversation = conversation; |
| this.capabilities = capabilities; |
| this.messages = messages; |
| } |
| |
| /** |
| * Prepares this listener for events coming from a single delta. |
| * |
| * @param author the author of the delta. |
| * @param timestamp the timestamp of the delta. |
| */ |
| public void deltaBegin(ParticipantId author, long timestamp) { |
| Preconditions.checkState( |
| deltaAuthor == null && deltaTimestamp == null, "DeltaEnd wasn't called"); |
| Preconditions.checkNotNull(author, "Author should not be null"); |
| Preconditions.checkNotNull(timestamp, "Timestamp should not be null"); |
| |
| deltaAuthor = author; |
| deltaTimestamp = timestamp; |
| } |
| |
| @Override |
| public void onParticipantAdded(ParticipantId participant) { |
| if (capabilities.containsKey(EventType.WAVELET_PARTICIPANTS_CHANGED)) { |
| boolean removedBefore = participantsRemoved.remove(participant.getAddress()); |
| if (!removedBefore) { |
| participantsAdded.add(participant.getAddress()); |
| } |
| } |
| |
| // This deviates from Google Wave production which always sends this |
| // event, even if it wasn't present in your capabilities. |
| if (capabilities.containsKey(EventType.WAVELET_SELF_ADDED) && participant.equals(robotId)) { |
| // The robot has been added |
| String rootBlipId = ConversationUtil.getRootBlipId(conversation); |
| WaveletSelfAddedEvent event = new WaveletSelfAddedEvent( |
| null, null, deltaAuthor.getAddress(), deltaTimestamp, rootBlipId); |
| addEvent(event, capabilities, rootBlipId, messages); |
| } |
| } |
| |
| @Override |
| public void onParticipantRemoved(ParticipantId participant) { |
| if (capabilities.containsKey(EventType.WAVELET_PARTICIPANTS_CHANGED)) { |
| participantsRemoved.add(participant.getAddress()); |
| } |
| |
| if (capabilities.containsKey(EventType.WAVELET_SELF_REMOVED) && participant.equals(robotId)) { |
| String rootBlipId = ConversationUtil.getRootBlipId(conversation); |
| WaveletSelfRemovedEvent event = new WaveletSelfRemovedEvent( |
| null, null, deltaAuthor.getAddress(), deltaTimestamp, rootBlipId); |
| addEvent(event, capabilities, rootBlipId, messages); |
| } |
| } |
| |
| @Override |
| public void onBlipAdded(ObservableConversationBlip blip) { |
| if (capabilities.containsKey(EventType.WAVELET_BLIP_CREATED)) { |
| String rootBlipId = ConversationUtil.getRootBlipId(conversation); |
| WaveletBlipCreatedEvent event = new WaveletBlipCreatedEvent( |
| null, null, deltaAuthor.getAddress(), deltaTimestamp, rootBlipId, blip.getId()); |
| addEvent(event, capabilities, rootBlipId, messages); |
| } |
| } |
| |
| @Override |
| public void onBlipDeleted(ObservableConversationBlip blip) { |
| if (capabilities.containsKey(EventType.WAVELET_BLIP_REMOVED)) { |
| String rootBlipId = ConversationUtil.getRootBlipId(conversation); |
| WaveletBlipRemovedEvent event = new WaveletBlipRemovedEvent( |
| null, null, deltaAuthor.getAddress(), deltaTimestamp, rootBlipId, blip.getId()); |
| addEvent(event, capabilities, rootBlipId, messages); |
| } |
| } |
| |
| /** |
| * Generates the events that are collected over the span of one delta. |
| */ |
| public void deltaEnd() { |
| if (!participantsAdded.isEmpty() || !participantsRemoved.isEmpty()) { |
| String rootBlipId = ConversationUtil.getRootBlipId(conversation); |
| |
| WaveletParticipantsChangedEvent event = |
| new WaveletParticipantsChangedEvent(null, null, deltaAuthor.getAddress(), |
| deltaTimestamp, rootBlipId, participantsAdded, participantsRemoved); |
| addEvent(event, capabilities, rootBlipId, messages); |
| } |
| clearOncePerDeltaCollectors(); |
| |
| deltaAuthor = null; |
| deltaTimestamp = null; |
| } |
| |
| /** |
| * Clear the data structures responsible for collecting data for events that |
| * should only be fired once per delta. |
| */ |
| private void clearOncePerDeltaCollectors() { |
| participantsAdded.clear(); |
| participantsRemoved.clear(); |
| } |
| } |
| |
| private class EventGeneratingDocumentHandler implements DocHandler { |
| |
| /** Public so we can manage the subscription */ |
| public final ObservableDocument doc; |
| |
| private final ConversationBlip blip; |
| private final Map<EventType, Capability> capabilities; |
| private final EventMessageBundle messages; |
| private ParticipantId deltaAuthor; |
| private Long deltaTimestamp; |
| |
| /** |
| * Set to true if a {@link DocumentChangedEvent} has been generated by this |
| * handler. |
| */ |
| private boolean documentChangedEventGenerated; |
| |
| private EventDataConverter converter; |
| private Wavelet wavelet; |
| |
| public EventGeneratingDocumentHandler(ObservableDocument doc, ConversationBlip blip, |
| Map<EventType, Capability> capabilities, EventMessageBundle messages, |
| ParticipantId deltaAuthor, Long deltaTimestamp, Wavelet wavelet, |
| EventDataConverter converter) { |
| this.doc = doc; |
| this.blip = blip; |
| this.capabilities = capabilities; |
| this.messages = messages; |
| this.converter = converter; |
| this.wavelet = wavelet; |
| setAuthorAndTimeStamp(deltaAuthor, deltaTimestamp); |
| } |
| |
| @Override |
| public void onDocumentEvents(EventBundle<N, E, T> event) { |
| Iterable<DocumentEvent<N, E, T>> eventComponents = event.getEventComponents(); |
| |
| for (DocumentEvent<N, E, T> eventComponent : eventComponents) { |
| if (eventComponent.getType() == DocumentEvent.Type.ANNOTATION_CHANGED) { |
| if (capabilities.containsKey(EventType.ANNOTATED_TEXT_CHANGED)) { |
| AnnotationChanged<N, E, T> anotationChangedEvent = |
| (AnnotationChanged<N, E, T>) eventComponent; |
| AnnotatedTextChangedEvent apiEvent = |
| new AnnotatedTextChangedEvent(null, null, deltaAuthor.getAddress(), deltaTimestamp, |
| blip.getId(), anotationChangedEvent.key, anotationChangedEvent.newValue); |
| addEvent(apiEvent, capabilities, blip.getId(), messages); |
| } |
| } else { |
| // used to distinguish between attribute changes and gadget state |
| // changes |
| Boolean gadgetStateChangeEvent = false; |
| if (eventComponent.getType() == DocumentEvent.Type.ATTRIBUTES) { |
| if (capabilities.containsKey(EventType.GADGET_STATE_CHANGED)) { |
| Map<String, String> oldState = new HashMap<>(); |
| Integer index = -1; |
| try { |
| AttributesModified<N, E, T> attributesModified = |
| (AttributesModified<N, E, T>) eventComponent; |
| // When a gadget state changes, the AttributesModifies event has |
| // always |
| // an oldValue map of the form {"value", something} (key is |
| // always value). |
| // To obtain the key of the changed state, the attribute "name" |
| // has to be obtained |
| // from the Element of the AttributesModified event. |
| String name = |
| ((org.waveprotocol.wave.model.document.raw.impl.Element) attributesModified |
| .getElement()).getAttribute("name"); |
| String oldValue = attributesModified.getOldValues().get("value"); |
| if (name != null || oldValue != null) { |
| oldState.put(name, oldValue); |
| } |
| BlipData b = converter.toBlipData(blip, wavelet, messages); |
| Map<Integer, com.google.wave.api.Element> elements = b.getElements(); |
| Set<Integer> keys = elements.keySet(); |
| // The gadget element provided by the eventComponent |
| org.waveprotocol.wave.model.document.raw.impl.Element rawGadget = |
| ((Node) attributesModified.getElement()).getParentElement(); |
| for (Integer key : keys) { |
| try { |
| Gadget gadget = (Gadget) elements.get(key); |
| if (sameGadgets(rawGadget, gadget)) { |
| index = key; |
| break; |
| } |
| } catch (ClassCastException e) { |
| // if it is not a gadget we do not compare them |
| } |
| } |
| } catch (ClassCastException e) { |
| e.printStackTrace(); |
| } |
| if (oldState.size() != 0 && index != -1) { |
| // if the attribute changed belongs to a gadget |
| gadgetStateChangeEvent = true; |
| final GadgetStateChangedEvent gadgetEvent = |
| new GadgetStateChangedEvent(null, messages, deltaAuthor.getAddress(), |
| deltaTimestamp, blip.getId(), index, oldState); |
| addEvent(gadgetEvent, capabilities, blip.getId(), messages); |
| } |
| } |
| } |
| if (capabilities.containsKey(EventType.FORM_BUTTON_CLICKED)) { |
| if (eventComponent.getType() == DocumentEvent.Type.CONTENT_INSERTED) { |
| ContentInserted<N, E, T> contentInserted = (ContentInserted<N, E, T>) eventComponent; |
| org.waveprotocol.wave.model.document.raw.impl.Element elementInserted = ((org.waveprotocol.wave.model.document.raw.impl.Element) contentInserted.getSubtreeElement()); |
| if (elementInserted.getTagName().equals("click")) { |
| FormButtonClickedEvent buttonClickedEvent = |
| new FormButtonClickedEvent(null, null, |
| elementInserted.getAttribute("clicker"), Long.decode(elementInserted |
| .getAttribute("time")), blip.getId(), elementInserted |
| .getParentElement().getAttribute("name")); |
| addEvent(buttonClickedEvent, capabilities, blip.getId(), messages); |
| } |
| } |
| |
| } else |
| if (capabilities.containsKey(EventType.DOCUMENT_CHANGED) |
| && !documentChangedEventGenerated && !gadgetStateChangeEvent) { |
| DocumentChangedEvent apiEvent = |
| new DocumentChangedEvent(null, null, deltaAuthor.getAddress(), deltaTimestamp, |
| blip.getId()); |
| addEvent(apiEvent, capabilities, blip.getId(), messages); |
| // Only one documentChangedEvent should be generated per bundle. |
| documentChangedEventGenerated = true; |
| } |
| } |
| } |
| } |
| |
| /** |
| * Sets the author and timestamp for the events that will be coming in. |
| * Should be changed at least for every delta that will touch the document |
| * that the handler is listening to. |
| * |
| * @param author the author of the delta. |
| * @param timestamp the timestamp at which the delta is applied. |
| */ |
| public void setAuthorAndTimeStamp(ParticipantId author, long timestamp) { |
| Preconditions.checkNotNull(author, "Author should not be null"); |
| Preconditions.checkNotNull(timestamp, "Timestamp should not be null"); |
| this.deltaAuthor = author; |
| this.deltaTimestamp = timestamp; |
| } |
| |
| /** |
| * Check if an {@link org.waveprotocol.wave.model.document.raw.impl.Element} |
| * is and a {@link Gadget} |
| * |
| * @param rawElement |
| * @param element |
| * @return |
| */ |
| private boolean sameGadgets(org.waveprotocol.wave.model.document.raw.impl.Element rawElement, |
| Gadget element) { |
| String ifr1 = rawElement.getAttribute("ifr"); |
| String ifr2 = element.getProperty("ifr"); |
| return (ifr1 != null && ifr1.equals(ifr2)); |
| } |
| } |
| |
| /** |
| * Adds an {@link Event} to the given {@link EventMessageBundle}. |
| * |
| * If a blip id is specified this will be added to the |
| * {@link EventMessageBundle}'s required blips list with the context given by |
| * the robot's capabilities. If a robot does not specify a context for this |
| * event the default context will be used. Ergo this code is not responsible |
| * for filtering operations that a robot is not interested in. |
| * |
| * @param event to add. |
| * @param capabilities the capabilities to get the context from. |
| * @param blipId id of the blip this event is related to, may be null. |
| * @param messages {@link EventMessageBundle} to edit. |
| */ |
| private void addEvent(Event event, Map<EventType, Capability> capabilities, String blipId, |
| EventMessageBundle messages) { |
| if (!isEventFilteredOut(event)) { |
| // Add the given blip to the required blip lists with the context |
| // specified by the robot's capabilities. |
| if (!Strings.isNullOrEmpty(blipId)) { |
| Capability capability = capabilities.get(event.getType()); |
| List<Context> contexts; |
| if (capability == null) { |
| contexts = Capability.DEFAULT_CONTEXT; |
| } else { |
| contexts = capability.getContexts(); |
| } |
| messages.requireBlip(blipId, contexts); |
| } |
| // Add the event to the bundle. |
| messages.addEvent(event); |
| } |
| } |
| |
| /** |
| * Checks whether the event should be filtered out. It can happen |
| * if the robot received several deltas where in some delta it is added to |
| * the wavelet but it didn't receive the WAVELET_SELF_ADDED event yet. |
| * Or if robot already received WAVELET_SELF_REMOVED |
| * event - then it should not receive events after that. |
| * |
| * @param event the event to filter. |
| * @return true if the event should be filtered out |
| */ |
| protected boolean isEventFilteredOut(Event event) { |
| boolean isEventSuspensionOveriden = false; |
| if (event.getType().equals(EventType.WAVELET_SELF_REMOVED)) { |
| // Stop processing events. |
| isEventProcessingSuspended = true; |
| // Allow robot receive WAVELET_SELF_REMOVED event, but suspend after that. |
| isEventSuspensionOveriden = true; |
| } |
| if (event.getType().equals(EventType.WAVELET_SELF_ADDED)) { |
| // Start processing events. |
| isEventProcessingSuspended = false; |
| } |
| if ((isEventProcessingSuspended && !isEventSuspensionOveriden) |
| || event.getModifiedBy().equals(robotName.toParticipantAddress())) { |
| // Robot was removed from wave or this is self generated event. |
| return true; |
| } |
| return false; |
| } |
| |
| /** |
| * The name of the Robot to which this {@link EventGenerator} belongs. Used |
| * for events where "self" is important. |
| */ |
| private final RobotName robotName; |
| |
| /** Used to create conversations. */ |
| private final ConversationUtil conversationUtil; |
| |
| /** |
| * Indicates that robot was removed from wavelet and thus event processing |
| * should be suspended. |
| */ |
| private boolean isEventProcessingSuspended; |
| |
| private final ParticipantId robotId; |
| |
| /** |
| * Constructs a new {@link EventGenerator} for the robot with the given name. |
| * |
| * @param robotName the name of the robot. |
| * @param conversationUtil used to create conversations. |
| */ |
| public EventGenerator(RobotName robotName, ConversationUtil conversationUtil) { |
| this.robotName = robotName; |
| this.conversationUtil = conversationUtil; |
| this.robotId = ParticipantId.ofUnsafe(robotName.toParticipantAddress()); |
| } |
| |
| /** |
| * Generates the {@link EventMessageBundle} for the specified capabilities. |
| * |
| * @param waveletAndDeltas for which the events are to be generated |
| * @param capabilities the capabilities to filter events on |
| * @param converter converter for generating the API implementations of |
| * WaveletData and BlipData. |
| * @returns true if an event was generated, false otherwise |
| */ |
| public EventMessageBundle generateEvents(WaveletAndDeltas waveletAndDeltas, |
| Map<EventType, Capability> capabilities, EventDataConverter converter) { |
| EventMessageBundle messages = new EventMessageBundle(robotName.toEmailAddress(), ""); |
| ObservableWaveletData snapshot = |
| WaveletDataUtil.copyWavelet(waveletAndDeltas.getSnapshotBeforeDeltas()); |
| isEventProcessingSuspended = !snapshot.getParticipants().contains(robotId); |
| |
| if (robotName.hasProxyFor()) { |
| // This robot is proxying so set the proxy field. |
| messages.setProxyingFor(robotName.getProxyFor()); |
| } |
| |
| // Sending any operations will cause an exception. |
| OpBasedWavelet wavelet = |
| new OpBasedWavelet(snapshot.getWaveId(), snapshot, |
| // This doesn't thrown an exception, the sinks will |
| new BasicWaveletOperationContextFactory(null), |
| ParticipationHelper.DEFAULT, SilentOperationSink.VOID, SilentOperationSink.VOID); |
| |
| ObservableConversation conversation = getRootConversation(wavelet); |
| |
| if (conversation == null) { |
| return messages; |
| } |
| |
| // Start listening |
| EventGeneratingConversationListener conversationListener = |
| new EventGeneratingConversationListener(conversation, capabilities, messages, robotName); |
| conversation.addListener(conversationListener); |
| EventGeneratingWaveletListener waveletListener = |
| new EventGeneratingWaveletListener(capabilities); |
| wavelet.addListener(waveletListener); |
| |
| Map<String, EventGeneratingDocumentHandler> docHandlers = Maps.newHashMap(); |
| try { |
| for (TransformedWaveletDelta delta : waveletAndDeltas.getDeltas()) { |
| // TODO(ljvderijk): Set correct timestamp and hashed version once |
| // wavebus sends them along |
| long timestamp = 0L; |
| conversationListener.deltaBegin(delta.getAuthor(), timestamp); |
| |
| for (WaveletOperation op : delta) { |
| // Check if we need to attach a doc handler. |
| if ((op instanceof WaveletBlipOperation)) { |
| attachDocHandler(conversation, op, docHandlers, capabilities, messages, |
| delta.getAuthor(), timestamp, wavelet, converter); |
| } |
| op.apply(snapshot); |
| } |
| conversationListener.deltaEnd(); |
| } |
| } catch (OperationException e) { |
| throw new IllegalStateException("Operation failed to apply when generating events", e); |
| } finally { |
| conversation.removeListener(conversationListener); |
| wavelet.removeListener(waveletListener); |
| for (EventGeneratingDocumentHandler docHandler : docHandlers.values()) { |
| docHandler.doc.removeListener(docHandler); |
| } |
| } |
| |
| if (messages.getEvents().isEmpty()) { |
| // No events found, no need to resolve contexts |
| return messages; |
| } |
| |
| // Resolve the context of the bundle now that all events have been |
| // processed. |
| ContextResolver.resolveContext(messages, wavelet, conversation, converter); |
| |
| return messages; |
| } |
| |
| /** |
| * Attaches a doc handler to the blip the operation applies to. |
| * |
| * @param conversation the conversation the op is to be applied to. |
| * @param op the op to be applied |
| * @param docHandlers the list of attached dochandlers. |
| * @param capabilities the capabilities of the robot. |
| * @param messages the bundle to put the generated events in. |
| * @param deltaAuthor the author of the events generated. |
| * @param timestamp the timestamp at which these events occurred. |
| */ |
| private void attachDocHandler(ObservableConversation conversation, WaveletOperation op, |
| Map<String, EventGeneratingDocumentHandler> docHandlers, |
| Map<EventType, Capability> capabilities, EventMessageBundle messages, |
| ParticipantId deltaAuthor, long timestamp, Wavelet wavelet, EventDataConverter converter) { |
| WaveletBlipOperation blipOp = (WaveletBlipOperation) op; |
| String blipId = blipOp.getBlipId(); |
| // Ignoring the documents outside the conversation such as tags |
| // and robot data docs. |
| ObservableConversationBlip blip = conversation.getBlip(blipId); |
| if (blip != null) { |
| String blipId1 = blip.getId(); |
| |
| EventGeneratingDocumentHandler docHandler = docHandlers.get(blipId1); |
| if (docHandler == null) { |
| ObservableDocument doc = (ObservableDocument) blip.getContent(); |
| docHandler = |
| new EventGeneratingDocumentHandler(doc, blip, capabilities, messages, deltaAuthor, |
| timestamp, wavelet, converter); |
| doc.addListener(docHandler); |
| docHandlers.put(blipId1, docHandler); |
| } else { |
| docHandler.setAuthorAndTimeStamp(deltaAuthor, timestamp); |
| } |
| } |
| } |
| |
| /** |
| * Returns the root conversation from the given wavelet. Or null if there is |
| * none. |
| * |
| * @param wavelet the wavelet to get the conversation from. |
| */ |
| private ObservableConversation getRootConversation(ObservableWavelet wavelet) { |
| if (!WaveletBasedConversation.waveletHasConversation(wavelet)) { |
| // No conversation present, bail. |
| return null; |
| } |
| |
| ObservableConversation conversation = conversationUtil.buildConversation(wavelet).getRoot(); |
| if (conversation.getRootThread().getFirstBlip() == null) { |
| // No root blip is present, this will cause Robot API code |
| // to fail when resolving the context of events. This might be fixed later |
| // on by making changes to the ContextResolver. |
| return null; |
| } |
| return conversation; |
| } |
| } |