Package org.waveprotocol.wave.client.state

Source Code of org.waveprotocol.wave.client.state.ThreadReadStateMonitorImpl

/**
* 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.wave.client.state;

import com.google.common.base.Preconditions;

import org.waveprotocol.wave.model.conversation.ConversationBlip;
import org.waveprotocol.wave.model.conversation.ConversationThread;
import org.waveprotocol.wave.model.conversation.ObservableConversation;
import org.waveprotocol.wave.model.conversation.ObservableConversationBlip;
import org.waveprotocol.wave.model.conversation.ObservableConversationThread;
import org.waveprotocol.wave.model.conversation.ObservableConversationView;
import org.waveprotocol.wave.model.supplement.ObservableSupplementedWave;
import org.waveprotocol.wave.model.supplement.SupplementedWave;
import org.waveprotocol.wave.model.util.CollectionUtils;
import org.waveprotocol.wave.model.util.CopyOnWriteSet;
import org.waveprotocol.wave.model.util.IdentityMap;
import org.waveprotocol.wave.model.util.IdentitySet;
import org.waveprotocol.wave.model.wave.ParticipantId;

/**
* Monitors the threads in a conversation for unread blips and efficiently
* maintains a count of them.
*
* Counts are lazily initialised such that the thread state will not be counted
* nor tracked unless the thread count has been explicitly queried.  This allows
* for nice optimisations such as not counting root threads (if this monitor is
* only used for inline reply counts), paging of threads, etc.
*/
public final class ThreadReadStateMonitorImpl extends ObservableSupplementedWave.ListenerImpl
    implements ThreadReadStateMonitor, ObservableConversation.Listener,
    ObservableConversationView.Listener {

  /**
   * Maintains the read/unread state of all blips in order to translate the
   * "maybe" events from the model/supplement into "definite" events.
   * Events must be pushed via the "handle" methods.
   */
  private class ReadStateBucket {
    private final IdentitySet<ConversationBlip> readBlips = CollectionUtils.createIdentitySet();
    private final IdentitySet<ConversationBlip> unreadBlips = CollectionUtils.createIdentitySet();

    /**
     * Notifies the bucket that a blip has been added.
     *
     * @return whether the bucket state changed as a result, i.e. if the blip
     *         was not already present in the bucket
     */
    public boolean handleBlipAdded(ConversationBlip blip) {
      if (!contains(blip)) {
        if (getSupplement().isUnread(blip)) {
          unreadBlips.add(blip);
        } else {
          readBlips.add(blip);
        }
        return true;
      } else {
        return false;
      }
    }

    /**
     * Notifies the bucket that a blip has been removed.
     *
     * @return whether the bucket state changed as a result, i.e. if the blip
     *         was not present in the bucket
     */
    public boolean handleBlipRemoved(ConversationBlip blip) {
      if (contains(blip)) {
        unreadBlips.remove(blip);
        readBlips.remove(blip);
        return true;
      } else {
        return false;
      }
    }

    /**
     * @return whether the bucket has seen a given blip
     */
    public boolean contains(ConversationBlip blip) {
      assert !(readBlips.contains(blip) && unreadBlips.contains(blip));
      return readBlips.contains(blip) || unreadBlips.contains(blip);
    }

    /**
     * Notifies the bucket that a blip read state may have changed.
     *
     * @return whether the read state of the blip definitely changed
     */
    public boolean handleMaybeBlipReadStateChanged(ConversationBlip blip) {
      if (getSupplement().isUnread(blip)) {
        if (readBlips.contains(blip)) {
          readBlips.remove(blip);
          unreadBlips.add(blip);
          return true;
        }
      } else {
        if (unreadBlips.contains(blip)) {
          unreadBlips.remove(blip);
          readBlips.add(blip);
          return true;
        }
      }
      return false;
    }

    /**
     * Notifies the bucket that a wavelet's read state may have changed.
     *
     * @return whether the read state of any blips changed
     */
    public boolean handleWaveletReadStateChanged() {
      // Calculate all blips which changed read state.
      final IdentitySet<ConversationBlip> changedBlips = CollectionUtils.createIdentitySet();

      readBlips.each(new IdentitySet.Proc<ConversationBlip>() {
        @Override
        public void apply(ConversationBlip blip) {
          if (getSupplement().isUnread(blip)) {
            changedBlips.add(blip);
          }
        }
      });

      unreadBlips.each(new IdentitySet.Proc<ConversationBlip>() {
        @Override
        public void apply(ConversationBlip blip) {
          if (!getSupplement().isUnread(blip)) {
            changedBlips.add(blip);
          }
        }
      });

      // Trigger events on changed blips.
      changedBlips.each(new IdentitySet.Proc<ConversationBlip>() {
        @Override
        public void apply(ConversationBlip blip) {
          boolean changed = handleMaybeBlipReadStateChanged(blip);
          assert changed;
        }
      });

      return !changedBlips.isEmpty();
    }
  }

  /**
   * Contains the state of all transitive child blips of a single thread.
   *
   * Events must be pushed via the "handle" methods. If a ThreadReadState exists
   * it *must* have all relevant events to it; the
   * {@link ThreadReadState#isMonitored} field is for efficiency where a
   * ThreadReadState might exist for a thread even though that thread is not
   * officially monitored; if so, callers must still push events but may query
   * {@link ThreadReadState#isMonitored()} to determine whether listeners should
   * receive read state changes for this thread.
   */
  private class ThreadReadState {
    private final ConversationThread thread;
    private int readCount = 0;
    private int unreadCount = 0;
    boolean isMonitored = false;
    boolean isDirty = true;

    public ThreadReadState(ConversationThread thread) {
      this.thread = thread;
    }

    /**
     * @return the current read count
     */
    public int getReadCount() {
      if (isDirty) {
        countBlips();
      }
      return readCount;
    }

    /**
     * @return the current unread count
     */
    public int getUnreadCount() {
      if (isDirty) {
        countBlips();
      }
      return unreadCount;
    }

    /**
     * Sets whether the thread should be included in events.
     */
    public void setIsMonitored(boolean isMonitored) {
      this.isMonitored = isMonitored;
    }

    /**
     * @return whether the thread should be included in events
     */
    public boolean isMonitored() {
      return isMonitored;
    }

    /**
     * Notifies the thread monitor that a blip has been added.
     */
    public void handleBlipAdded(ConversationBlip blip) {
      if (getSupplement().isUnread(blip)) {
        unreadCount++;
      } else {
        readCount++;
      }
    }

    /**
     * Notifies the thread monitor that a blip has been removed.
     */
    public void handleBlipRemoved(ConversationBlip blip) {
      if (getSupplement().isUnread(blip)) {
        unreadCount--;
      } else {
        readCount--;
      }
    }

    /**
     * Notifies the thread monitor that a blip's read state has
     * definitely changed.
     */
    public void handleBlipReadStateChanged(ConversationBlip blip) {
      if (getSupplement().isUnread(blip)) {
        unreadCount++;
        readCount--;
      } else {
        readCount++;
        unreadCount--;
      }
    }

    /**
     * Flags the read state as "dirty" such that it will be recomputed when next
     * queried.
     */
    public void flagAsDirty() {
      isDirty = true;
    }

    /**
     * Forces the thread monitor to count blips.
     */
    private void countBlips() {
      readCount = 0;
      unreadCount = 0;
      countBlipsInner(thread);
      isDirty = false;
    }

    private void countBlipsInner(ConversationThread thread) {
      for (ConversationBlip blip : thread.getBlips()) {
        if (knownBlips.contains(blip)) {
          if (getSupplement().isUnread(blip)) {
            unreadCount++;
          } else {
            readCount++;
          }
        }
        // Add blips from reply threads.
        for (ConversationThread replyThread : blip.getReplyThreads()) {
          // Always create monitors (for efficiency in initialisation); but
          // don't necessarily include them in events unless (via monitor/
          // getReadState/getUnreadState) they are explicitly included.
          ThreadReadState replyState = getOrCreateThreadState(replyThread);
          readCount += replyState.getReadCount();
          unreadCount += replyState.getUnreadCount();
        }
      }
    }
  }

  /** All blips known to the monitor. */
  private final ReadStateBucket knownBlips = new ReadStateBucket();

  /** Read/unread state of each thread. */
  private final IdentityMap<ConversationThread, ThreadReadState> threadStates =
      CollectionUtils.createIdentityMap();

  /** Event listeners. */
  private final CopyOnWriteSet<ThreadReadStateMonitor.Listener> listeners =
      CopyOnWriteSet.create();

  /**
   * A latch used to distribute events consistently; see comment near
   * {@link #countUpEvent} and {@link #countDownEvent} where the latch is
   * manipulated.
   */
  private int eventLatch = 0;

  /**
   * Threads which have events on them during a sequence of state changes.
   */
  private final IdentitySet<ConversationThread> eventThreads = CollectionUtils.createIdentitySet();

  private final ObservableSupplementedWave supplementedWave;
  private final ObservableConversationView conversationView;

  /** Whether this monitor is ready, set to true in {@link #init()}. */
  private boolean isReady = false;

  /**
   * Threads which have been {@link #monitor}ed before being ready. Set to null
   * in {@link #init()}.
   */
  private IdentitySet<ConversationThread> threadsToNotifyWhenReady =
      CollectionUtils.createIdentitySet();

  /**
   * Creates and initializes a ThreadReadStateMonitorImpl.
   *
   * @param conversationView the wave to monitor thread read state of
   */
  public static ThreadReadStateMonitorImpl create(ObservableSupplementedWave supplementedWave,
      ObservableConversationView conversationView) {
    ThreadReadStateMonitorImpl monitor = new ThreadReadStateMonitorImpl(
        supplementedWave, conversationView);
    monitor.init();
    return monitor;
  }

  private ThreadReadStateMonitorImpl(ObservableSupplementedWave supplementedWave,
      ObservableConversationView conversationView) {
    Preconditions.checkNotNull(supplementedWave, "supplementedWave cannot be null");
    Preconditions.checkNotNull(conversationView, "conversationView cannot be null");
    this.supplementedWave = supplementedWave;
    this.conversationView = conversationView;
  }

  private void init() {
    // Add existing conversations.  The monitor isn't ready through this, so no
    // events should be generated.
    for (ObservableConversation conversation : conversationView.getConversations()) {
      onConversationAdded(conversation);
    }

    // Listen for new converations.
    conversationView.addListener(this);

    // Listen to supplement read/unread events.
    supplementedWave.addListener(this);

    isReady = true;

    if (!threadsToNotifyWhenReady.isEmpty()) {
      // Initialise any queried threads.
      threadsToNotifyWhenReady.each(new IdentitySet.Proc<ConversationThread>() {
        @Override
        public void apply(ConversationThread thread) {
          monitor(thread);
        }
      });

      // Notify listeners of the queried threads.
      notifyListeners(threadsToNotifyWhenReady);
      threadsToNotifyWhenReady.clear();
      threadsToNotifyWhenReady = null;
    }
  }

  //
  // ThreadReadStateMonitor
  //

  @Override
  public void monitor(ConversationThread thread) {
    if (isReady) {
      getOrCreateMonitoredThreadState(thread);
    } else {
      threadsToNotifyWhenReady.add(thread);
    }
  }

  @Override
  public void ignore(ConversationThread thread) {
    if (isReady) {
      threadStates.remove(thread);
    } else {
      threadsToNotifyWhenReady.remove(thread);
    }
  }

  @Override
  public int getReadCount(ConversationThread thread) {
    Preconditions.checkState(isReady, "ThreadReadStateMonitor queried before ready");
    return getOrCreateMonitoredThreadState(thread).getReadCount();
  }

  @Override
  public int getUnreadCount(ConversationThread thread) {
    Preconditions.checkState(isReady, "ThreadReadStateMonitor queried before ready");
    return getOrCreateMonitoredThreadState(thread).getUnreadCount();
  }

  @Override
  public int getTotalCount(ConversationThread thread) {
    return getReadCount(thread) + getUnreadCount(thread);
  }

  @Override
  public boolean isReady() {
    return isReady;
  }

  @Override
  public void addListener(ThreadReadStateMonitor.Listener listener) {
    listeners.add(listener);
  }

  @Override
  public void removeListener(ThreadReadStateMonitor.Listener listener) {
    listeners.remove(listener);
  }

  //
  // ObservableConversation.Listener
  //

  @Override
  public void onThreadAdded(ObservableConversationThread thread) {
    handleThreadAdded(thread);
  }

  /**
   * Recursively adds a thread to be monitored.
   */
  private void handleThreadAdded(ObservableConversationThread thread) {
    // Note: thread state monitor added lazily.
    for (ObservableConversationBlip blip : thread.getBlips()) {
      handleBlipAdded(blip);
      // Recursion will continue for reply threads of the blip in handleBlipAdded.
    }
  }

  @Override
  public void onThreadDeleted(ObservableConversationThread thread) {
    handleThreadRemoved(thread);
    assert !threadStates.has(thread);
  }

  /**
   * Recursively removes a thread, the inverse of {@link #handleThreadAdded}.
   */
  private void handleThreadRemoved(ObservableConversationThread thread) {
    // Note: threadStates might not necessarily contain this thread due to laziness.
    threadStates.remove(thread);
    for (ObservableConversationBlip blip : thread.getBlips()) {
      handleBlipRemoved(blip);
    }
  }

  @Override
  public void onBlipAdded(ObservableConversationBlip blip) {
    handleBlipAdded(blip);
  }

  private void handleBlipAdded(ObservableConversationBlip blip) {
    // Check that this isn't a repeated event.
    if (!knownBlips.handleBlipAdded(blip)) {
      return;
    }

    countUpEvent();

    // Handle the event for all parent threads.
    ConversationThread thread = blip.getThread();
    while (thread != null) {
      ThreadReadState state = threadStates.get(thread);
      if (state != null) {
        state.handleBlipAdded(blip);
        registerEventIfMonitored(state);
      }
      thread = getParent(thread);
    }

    // Add any reply threads.
    for (ObservableConversationThread replyThread : blip.getReplyThreads()) {
      handleThreadAdded(replyThread);
    }

    countDownEvent();
  }

  @Override
  public void onBlipDeleted(ObservableConversationBlip blip) {
    handleBlipRemoved(blip);
  }

  private void handleBlipRemoved(ObservableConversationBlip blip) {
    // Check that this isn't a repeated event.
    if (!knownBlips.handleBlipRemoved(blip)) {
      return;
    }

    countUpEvent();

    // Handle the event for all parent threads.
    ConversationThread thread = blip.getThread();
    while (thread != null) {
      ThreadReadState state = threadStates.get(thread);
      if (state != null) {
        state.handleBlipRemoved(blip);
        registerEventIfMonitored(state);
      }
      thread = getParent(thread);
    }

    // Remove any inline child threads of this blip (non-inline replies will
    // just be reanchored).
    for (ObservableConversationThread replyThread : blip.getReplyThreads()) {
        handleThreadRemoved(replyThread);
    }

    countDownEvent();
  }

  //
  // ObservableConversationView.Listener
  //

  @Override
  public void onConversationAdded(ObservableConversation conversation) {
    handleThreadAdded(conversation.getRootThread());
    conversation.addListener(this);
  }

  @Override
  public void onConversationRemoved(ObservableConversation conversation) {
    conversation.removeListener(this);
    handleThreadRemoved(conversation.getRootThread());
  }

  //
  // ObservableSupplementedWave.Listener
  //

  @Override
  public void onMaybeBlipReadChanged(ObservableConversationBlip blip) {
    // Check that a blip really did change state.
    if (!knownBlips.handleMaybeBlipReadStateChanged(blip)) {
      return;
    }

    countUpEvent();

    ObservableConversationThread thread = blip.getThread();
    while (thread != null) {
      ThreadReadState state = threadStates.get(thread);
      if (state != null) {
        state.handleBlipReadStateChanged(blip);
        registerEventIfMonitored(state);
      }
      thread = getParent(thread);
    }

    countDownEvent();
  }

  @Override
  public void onMaybeWaveletReadChanged() {
    countUpEvent();

    // Bucket should be notified first.
    knownBlips.handleWaveletReadStateChanged();

    // Set all thread counts as "dirty" so that when next queried they will be
    // recalculated.
    threadStates.each(new IdentityMap.ProcV<ConversationThread, ThreadReadState>() {
      @Override
      public void apply(ConversationThread thread, ThreadReadState state) {
        state.flagAsDirty();
        registerEventIfMonitored(state);
      }
    });

    countDownEvent();
  }

  //
  // Event distribution helpers.
  //
  // The complicated nature of thread and blip adding/removing warrants some
  // specialised event distribution code.  E.g. if a blip is removed then
  //   - all parent threads must be notified of the deletion, and also
  //   - all inline child threads must be deleted, which in turn means
  //   - all parent threads must be notified, and all the while
  //   - listeners must be notified in a consistent state.
  // To solve this circular dependency without firing too many events, we set
  // up a "latch" for the events.  Methods should "count up" on the latch before
  // registering events, then "count down" when done.  If the latch reaches 0,
  // distribute events on the registered threads then clear them.
  //

  /**
   * Called by methods to register interest in an event.
   */
  private void countUpEvent() {
    eventLatch++;
    assert eventLatch > 0;
  }

  /**
   * Called by methods when their registered interest is completed.
   */
  private void countDownEvent() {
    assert eventLatch > 0;
    eventLatch--;
    if (eventLatch == 0 && !eventThreads.isEmpty()) {
      notifyListeners(eventThreads);
      eventThreads.clear();
    }
  }

  /**
   * Registers an event on a thread such that some time in the future a call
   * to {@link #countDownEvent} will cause listeners to be notified of changes on it.
   * An event is only registered if the thread state is actually monitored.
   */
  private void registerEventIfMonitored(ThreadReadState state) {
    assert eventLatch > 0;
    if (state.isMonitored()) {
      eventThreads.add(state.thread);
    }
  }

  /**
   * Notifies listeners of a change to a collection of threads.  Should only be
   * called from {@link #countDownEvent} apart from on initialisation.
   */
  private void notifyListeners(IdentitySet<ConversationThread> threads) {
    for (Listener listener : listeners) {
      listener.onReadStateChanged(threads);
    }
  }

  //
  // Helpers.
  //

  /**
   * @return the supplement, which must be ready
   */
  private SupplementedWave getSupplement() {
    ObservableSupplementedWave supplement = supplementedWave;
    assert supplement != null;
//    assert supplement.isReady();
    return supplement;
  }

  /**
   * Gets the ThreadReadState for a thread, creating one if necessary.
   */
  private ThreadReadState getOrCreateThreadState(ConversationThread thread) {
    assert thread != null;
    assert isReady;
    ThreadReadState state = threadStates.get(thread);
    if (state == null) {
      state = new ThreadReadState(thread);
      threadStates.put(thread, state);
    }
    return state;
  }

  /**
   * Gets the ThreadReadState for a thread, creating one if necessary, and
   * including it in events regardless of whether it was created or not.
   */
  private ThreadReadState getOrCreateMonitoredThreadState(ConversationThread thread) {
    ThreadReadState state = getOrCreateThreadState(thread);
    state.setIsMonitored(true);
    return state;
  }

  /**
   * @return the parent thread of a thread, or null if the thread is top-level
   */
  private ConversationThread getParent(ConversationThread thread) {
    return (thread.getParentBlip() != null) ? thread.getParentBlip().getThread() : null;
  }

  /**
   * @return the parent thread of a thread, or null if the thread is top-level
   */
  private ObservableConversationThread getParent(ObservableConversationThread thread) {
    return thread.getParentBlip() != null ? thread.getParentBlip().getThread() : null;
  }

  @Override public void onBlipContributorAdded(ObservableConversationBlip blip,
      ParticipantId contributor) {}
  @Override public void onBlipContributorRemoved(ObservableConversationBlip blip,
      ParticipantId contributor) {}
  @Override public void onBlipSumbitted(ObservableConversationBlip blip) {}
  @Override public void onBlipTimestampChanged(ObservableConversationBlip blip, long oldTimestamp,
      long newTimestamp) {}
  @Override public void onInlineThreadAdded(ObservableConversationThread thread, int location) {}
  @Override public void onParticipantAdded(ParticipantId participant) {}
  @Override public void onParticipantRemoved(ParticipantId participant) {}
}
TOP

Related Classes of org.waveprotocol.wave.client.state.ThreadReadStateMonitorImpl

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.