/** * Copyright 2010 Google Inc. * * Licensed 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.Context; import com.google.wave.api.data.converter.ContextResolver; import com.google.wave.api.data.converter.EventDataConverter; import com.google.wave.api.event.AnnotatedTextChangedEvent; import com.google.wave.api.event.DocumentChangedEvent; import com.google.wave.api.event.Event; import com.google.wave.api.event.EventType; import com.google.wave.api.event.WaveletBlipCreatedEvent; import com.google.wave.api.event.WaveletBlipRemovedEvent; import com.google.wave.api.event.WaveletParticipantsChangedEvent; import com.google.wave.api.event.WaveletSelfAddedEvent; import com.google.wave.api.event.WaveletSelfRemovedEvent; 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.Conversation; import org.waveprotocol.wave.model.conversation.ConversationBlip; import org.waveprotocol.wave.model.conversation.ConversationListenerImpl; import org.waveprotocol.wave.model.conversation.ObservableConversation; import org.waveprotocol.wave.model.conversation.ObservableConversationBlip; import org.waveprotocol.wave.model.conversation.WaveletBasedConversation; import org.waveprotocol.wave.model.document.DocHandler; import org.waveprotocol.wave.model.document.ObservableDocument; 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.indexed.DocumentEvent; import org.waveprotocol.wave.model.document.indexed.DocumentEvent.AnnotationChanged; 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.ObservableWavelet; import org.waveprotocol.wave.model.wave.ParticipantId; import org.waveprotocol.wave.model.wave.ParticipationHelper; import org.waveprotocol.wave.model.wave.WaveletListener; 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.List; import java.util.Map; /** * 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 (TBD)</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; public EventGeneratingDocumentHandler(ObservableDocument doc, ConversationBlip blip, Map<EventType, Capability> capabilities, EventMessageBundle messages, ParticipantId deltaAuthor, Long deltaTimestamp) { this.doc = doc; this.blip = blip; this.capabilities = capabilities; this.messages = messages; 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 { if (capabilities.containsKey(EventType.DOCUMENT_CHANGED) && !documentChangedEventGenerated) { 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; } } /** * 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); } 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) { 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); 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; } }