Package org.waveprotocol.box.server.robots.passive

Source Code of org.waveprotocol.box.server.robots.passive.EventGenerator$EventGeneratingDocumentHandler

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

Related Classes of org.waveprotocol.box.server.robots.passive.EventGenerator$EventGeneratingDocumentHandler

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.