blob: b45a0f85bba1b50f4ba7d7ee29a573abd8638255 [file] [log] [blame]
/**
* 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;
}
}