/**
* 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;
}
}