Package org.waveprotocol.wave.model.conversation

Source Code of org.waveprotocol.wave.model.conversation.WaveletBasedConversationBlip$LocatedReplyThread

/**
* 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.wave.model.conversation;

import com.google.common.annotations.VisibleForTesting;

import org.waveprotocol.wave.model.conversation.WaveletBasedConversation.ComponentHelper;
import org.waveprotocol.wave.model.document.Document;
import org.waveprotocol.wave.model.document.MutableDocument;
import org.waveprotocol.wave.model.document.MutableDocument.Action;
import org.waveprotocol.wave.model.document.util.DocIterate;
import org.waveprotocol.wave.model.document.util.Point;
import org.waveprotocol.wave.model.util.CollectionUtils;
import org.waveprotocol.wave.model.util.CopyOnWriteSet;
import org.waveprotocol.wave.model.util.Preconditions;
import org.waveprotocol.wave.model.util.StringMap;
import org.waveprotocol.wave.model.wave.Blip;
import org.waveprotocol.wave.model.wave.ObservableWavelet;
import org.waveprotocol.wave.model.wave.ParticipantId;
import org.waveprotocol.wave.model.wave.SourcesEvents;
import org.waveprotocol.wave.model.wave.WaveletListener;
import org.waveprotocol.wave.model.wave.opbased.WaveletListenerImpl;

import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
* A conversation blip backed by a region of a wavelet's manifest document.
*
* NOTE(anorth): at present this in in fact backed by a {@link Blip} but we are
* migrating all blip meta-data into documents.
*
* @author anorth@google.com (Alex North)
*/
final class WaveletBasedConversationBlip implements ObservableConversationBlip,
    SourcesEvents<WaveletBasedConversationBlip.Listener>, ObservableManifestBlip.Listener {

  /**
   * Receives events on a conversation blip.
   */
  interface Listener {
    /**
     * Notifies this listener that a reply thread has been added to this blip.
     *
     * @param reply the new thread
     */
    void onReplyAdded(WaveletBasedConversationThread reply);

    /**
     * Notifies this listener that a reply thread has been added to this blip.
     *
     * @param reply the new thread
     * @param location the location at which the thread is anchored
     */
    void onInlineReplyAdded(WaveletBasedConversationThread reply, int location);

    /**
     * Notifies this listener that the blip was removed from the conversation.
     * No further methods may be called on the blip.
     */
    void onDeleted();

    /**
     * Notifies this listener that a contributor was added to the blip.
     */
    void onContributorAdded(ParticipantId contributor);

    /**
     * Notifies this listener that a contributor was removed from the blip.
     */
    void onContributorRemoved(ParticipantId contributor);

    /**
     * Notifies the listener that the blip was submitted.
     */
    void onSumbitted();

    /**
     * Notifies the listener that the blip timestamp changed.
     */
    void onTimestampChanged(long oldTimestamp, long newTimestamp);
  }


  /**
   * A located reply thread  which specializes the thread type to
   * WaveletBasedConversationThread.
   */
  final class LocatedReplyThread
      extends ConversationBlip.LocatedReplyThread<WaveletBasedConversationThread>
      implements Comparable<LocatedReplyThread> {

    LocatedReplyThread(WaveletBasedConversationThread thread, int location) {
      super(thread, location);
    }

    @Override
    public int compareTo(WaveletBasedConversationBlip.LocatedReplyThread o) {
      if (getLocation() == o.getLocation()) {
        return 0;
      } else if (getLocation() == Blips.INVALID_INLINE_LOCATION) {
        return 1;
      } else if (o.getLocation() == Blips.INVALID_INLINE_LOCATION) {
        return -1;
      }
      return getLocation() - o.getLocation();
    }
  }

  /**
   * Redirects old-style blip events to the conversation listeners.
   */
  private final WaveletListener waveletListener = new WaveletListenerImpl() {
    @Override
    public void onBlipContributorAdded(ObservableWavelet wavelet, Blip eventBlip,
        ParticipantId contributor) {
      if (eventBlip == blip) {
        triggerOnContributorAdded(contributor);
      }
    }

    @Override
    public void onBlipContributorRemoved(ObservableWavelet wavelet, Blip eventBlip,
        ParticipantId contributor) {
      if (eventBlip == blip) {
        triggerOnContributorRemoved(contributor);
      }
    }

    @Override
    public void onBlipSubmitted(ObservableWavelet wavelet, Blip eventBlip) {
      if (eventBlip == blip) {
        triggerOnSubmitted();
      }
    }

    @Override
    public void onBlipTimestampModified(ObservableWavelet wavelet, Blip eventBlip, long oldTime,
        long newTime) {
      if (eventBlip == blip) {
        triggerOnTimestampModified(oldTime, newTime);
      }
    }
  };

  /** Manifest entry for this blip. */
  private final ObservableManifestBlip manifestBlip;

  /** Blip object containing content and metadata. */
  private final Blip blip;

  /** Thread containing this blip. */
  private final WaveletBasedConversationThread parentThread;

  /** Helper for wavelet access. */
  private final ComponentHelper helper;

  /** Replies keyed by id. */
  private final StringMap<WaveletBasedConversationThread> replies = CollectionUtils.createStringMap();

  /** Whether this blip is safe to access. Set false when deleted. */
  private boolean isUsable = true;

  private final CopyOnWriteSet<Listener> listeners = CopyOnWriteSet.create();

  static WaveletBasedConversationBlip create(ObservableManifestBlip manifestBlip, Blip backingBlip,
      WaveletBasedConversationThread thread, ComponentHelper helper) {
    WaveletBasedConversationBlip blip = new WaveletBasedConversationBlip(manifestBlip, backingBlip,
        thread, helper);

    for (ObservableManifestThread reply : manifestBlip.getReplies()) {
      blip.adaptThread(reply);
    }

    manifestBlip.addListener(blip);
    helper.getWaveletEventSource().addListener(blip.waveletListener);
    return blip;
  }

  private WaveletBasedConversationBlip(ObservableManifestBlip manifestBlip, Blip blip,
      WaveletBasedConversationThread thread, ComponentHelper helper) {
    Preconditions.checkNotNull(manifestBlip,
        "WaveletBasedConversationBlip received null manifest blip");
    if (blip == null) {
      Preconditions.nullPointer("WaveletBasedConversationBlip " + manifestBlip.getId()
          + " received null blip");
    }
    this.manifestBlip = manifestBlip;
    this.blip = blip;
    this.helper = helper;
    this.parentThread = thread;
  }

  @Override
  public WaveletBasedConversation getConversation() {
    return helper.getConversation();
  }

  @Override
  public WaveletBasedConversationThread getThread() {
    return parentThread;
  }


  @Override
  public Iterable<LocatedReplyThread> locateReplyThreads() {
    // NOTE(anorth): We must recalculate the anchor locations on each
    // call as the document does not provide stable elements. However, we
    // calculate the list of anchor locations on demand.
    Map<String, Integer> replyLocations = null;
    List<LocatedReplyThread> inlineReplyThreads = CollectionUtils.newArrayList();
    for (WaveletBasedConversationThread reply : getReplyThreads()) {
      if (replyLocations == null) {
        replyLocations = findAnchors();
      }
      Integer location = replyLocations.get(reply.getId());
      inlineReplyThreads.add(new LocatedReplyThread(reply,
          (location != null) ? location : Blips.INVALID_INLINE_LOCATION));
    }
    Collections.sort(inlineReplyThreads);
    return Collections.unmodifiableList(inlineReplyThreads);
  }

  /**
   * {@inheritDoc}
   *
   * The 'history of appends' corresponds to the manifest order of replies.
   */
  @Override
  public Iterable<WaveletBasedConversationThread> getReplyThreads() {
    final Iterable<? extends ObservableManifestThread> manifestBlips = manifestBlip.getReplies();
    return new Iterable<WaveletBasedConversationThread>() {
      @Override
      public Iterator<WaveletBasedConversationThread> iterator() {
        return WrapperIterator.create(manifestBlips.iterator(), replies);
      }
    };
  }

  @Override
  public WaveletBasedConversationThread addReplyThread() {
    checkIsUsable();
    String id = helper.createThreadId();
    manifestBlip.appendReply(id, false);
    return replies.get(id);
  }

  @Override
  public WaveletBasedConversationThread addReplyThread(final int location) {
    checkIsUsable();
    final String threadId = helper.createThreadId();
    createInlineReplyAnchor(threadId, location);
    manifestBlip.appendReply(threadId, true);
    return replies.get(threadId);
  }

  @Override
  public Document getContent() {
    return blip.getContent();
  }

  // TODO(anorth): migrate blip metadata to data stored in the blip document.

  @Override
  public ParticipantId getAuthorId() {
    return blip.getAuthorId();
  }

  @Override
  public Set<ParticipantId> getContributorIds() {
    return blip.getContributorIds();
  }

  @Override
  public long getLastModifiedTime() {
    return blip.getLastModifiedTime();
  }

  @Override
  public long getLastModifiedVersion() {
    return blip.getLastModifiedVersion();
  }

  @Override
  public void delete() {
    checkIsUsable();
    Collection<WaveletBasedConversationThread> allReplies = CollectionUtils.createQueue();
    CollectionUtils.copyValuesToJavaCollection(replies, allReplies);
    // Delete reply threads.
    // TODO(anorth): Move this loop to WBCT, where it can delete all the
    // inline reply anchors in one pass.
    for (WaveletBasedConversationThread replyThread : allReplies) {
      deleteThread(replyThread);
    }

    // All replies have been deleted, so remove this empty blip.
    parentThread.deleteBlip(this, true);
  }

  @Override
  public String getId() {
    return manifestBlip.getId();
  }

  @Override
  public boolean isRoot() {
    return parentThread == getConversation().getRootThread() &&
        this == parentThread.getFirstBlip();
  }

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

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

  //
  // ObservableManifestBlip.Listener
  // These methods update local data structures in response to changes in
  // the underlying data, either synchronously in local methods or from
  // remote changes. They don't make further changes to the data.
  //

  @Override
  public void onReplyAdded(ObservableManifestThread thread) {
    WaveletBasedConversationThread convThread = adaptThread(thread);
    triggerOnReplyAdded(convThread);
  }

  @Override
  public void onReplyRemoved(final ObservableManifestThread thread) {
    forgetThread(replies.get(thread.getId()));
  }

  @Override
  public String toString() {
    return "WaveletBasedConversationBlip(id = " + manifestBlip.getId() + ")";
  }

  public Blip getBlip() {
    return blip;
  }

  // TODO(anorth): remove this after porting client to conversation model.
  @Override
  public Blip hackGetRaw() {
    return getBlip();
  }

  /**
   * Deletes the content of this blip's document.
   */
  void clearContent() {
    if (blip.getContent().size() != 0) {
      blip.getContent().emptyElement(blip.getContent().getDocumentElement());
    }
  }

  @Override
  public ObservableConversationThread getReplyThread(String id) {
    return replies.get(id);
  }

  ManifestBlip getManifestBlip() {
    return manifestBlip;
  }

  // Package-private methods for WaveletBasedConversationThread.

  /**
   * Deletes a thread from this blip, deleting that thread's blips.
   */
  void deleteThread(WaveletBasedConversationThread threadToDelete) {
    threadToDelete.deleteBlips();
    manifestBlip.removeReply(threadToDelete.getManifestThread());
    clearInlineReplyAnchor(threadToDelete.getId());
  }

  /**
   * Deletes all threads from this blip.
   *
   * @see WaveletBasedConversationBlip#deleteThread(WaveletBasedConversationThread)
   */
  void deleteThreads() {
    // deleteThread() equivalent is inline here so we can do only one
    // document traversal to remove inline reply anchors.
    List<WaveletBasedConversationThread> threads =
        CollectionUtils.newArrayList(getReplyThreads());
    for (WaveletBasedConversationThread threadToDelete : threads) {
      threadToDelete.deleteBlips();
      manifestBlip.removeReply(threadToDelete.getManifestThread());
    }
    clearAllInlineReplyAnchors();
  }

  /**
   * Invalidates this blip. It may no longer be accessed.
   */
  void invalidate() {
    checkIsUsable();
    manifestBlip.removeListener(this);
    isUsable = false;
  }

  /**
   * Recursively invalidates this blip and its replies.
   */
  void destroy() {
    for (WaveletBasedConversationThread thread : CollectionUtils.valueList(replies)) {
      thread.destroy();
    }
    invalidate();
    listeners.clear();
  }

  /**
   * Checks that this blip is safe to access.
   */
  @VisibleForTesting
  void checkIsUsable() {
    if (!isUsable) {
      Preconditions.illegalState("Deleted blip is not usable: " + this);
    }
  }

  /**
   * Creates a conversation thread backed by a manifest thread and inserts it in
   * {@code replies}.
   */
  private WaveletBasedConversationThread adaptThread(ObservableManifestThread manifestThread) {
    WaveletBasedConversationThread thread =
        WaveletBasedConversationThread.create(manifestThread, this, helper);
    String id = thread.getId();
    replies.put(id, thread);
    return thread;
  }

  /**
   * Removes a thread from the internal list and triggers its deletion event.
   */
  private void forgetThread(WaveletBasedConversationThread threadToRemove) {
    String id = threadToRemove.getId();
    assert replies.containsKey(id);
    replies.remove(id);
    threadToRemove.triggerOnDeleted();
  }

  /**
   * Finds all thread anchor elements in the blip document.
   *
   * @return thread ids and their anchor locations
   */
  private Map<String, Integer> findAnchors() {
    final Map<String, Integer> anchors = CollectionUtils.newHashMap();
    blip.getContent().with(new Action() {
      @Override
      public <N, E extends N, T extends N> void exec(MutableDocument<N, E, T> doc) {
        for (E el : DocIterate.deepElements(doc, doc.getDocumentElement(), null)) {
          if (Blips.THREAD_INLINE_ANCHOR_TAGNAME.equals(doc.getTagName(el))) {
            String threadId = doc.getAttribute(el,
                Blips.THREAD_INLINE_ANCHOR_ID_ATTR);
            if ((threadId != null) && !anchors.containsKey(threadId)) {
              anchors.put(threadId, doc.getLocation(el));
            }
          }
        }
      }
    });
    return anchors;
  }

  /**
   * Inserts an inline reply anchor element in the blip document.
   *
   * @param threadId id of the reply thread
   * @param location location at which to insert anchor
   */
  private void createInlineReplyAnchor(final String threadId, final int location) {
    blip.getContent().with(new Action() {
      @Override
      public <N, E extends N, T extends N> void exec(MutableDocument<N, E, T> doc) {
        Point<N> point = doc.locate(location);
        doc.createElement(point, Blips.THREAD_INLINE_ANCHOR_TAGNAME,
            Collections.singletonMap(Blips.THREAD_INLINE_ANCHOR_ID_ATTR, threadId));
      }
    });
  }

  /**
   * Deletes all inline reply anchor elements for a thread from the blip
   * document.
   *
   * @param threadId id of the anchor(s) to delete
   */
  private void clearInlineReplyAnchor(final String threadId) {
    clearInlineReplyAnchors(CollectionUtils.immutableSet(threadId));
  }

  /**
   * Deletes all inline reply anchor elements for a set of threads from the blip
   * document.
   *
   * @param threadIds ids of the anchors to delete
   */
  private void clearInlineReplyAnchors(final Set<String> threadIds) {
    blip.getContent().with(new Action() {
      @Override
      public <N, E extends N, T extends N> void exec(MutableDocument<N, E, T> doc) {
        List<E> elementsToDelete = CollectionUtils.newArrayList();
        for (E el : DocIterate.deepElements(doc, doc.getDocumentElement(), null)) {
          if (Blips.THREAD_INLINE_ANCHOR_TAGNAME.equals(doc.getTagName(el))) {
            String elId = doc.getAttribute(el, Blips.THREAD_INLINE_ANCHOR_ID_ATTR);
            if (threadIds.contains(elId)) {
              elementsToDelete.add(el);
            }
          }
        }
        // Reverse elements to delete so we always delete bottom up if one
        // contains another (which would be really weird anyway).
        Collections.reverse(elementsToDelete);
        for (E el : elementsToDelete) {
          doc.deleteNode(el);
        }
      }
    });
  }

  /**
   * Deletes all inline reply anchor elements from the blip document.
   */
  private void clearAllInlineReplyAnchors() {
    blip.getContent().with(new Action() {
      @Override
      public <N, E extends N, T extends N> void exec(MutableDocument<N, E, T> doc) {
        List<E> elementsToDelete = CollectionUtils.newArrayList();
        for (E el : DocIterate.deepElements(doc, doc.getDocumentElement(), null)) {
          if (Blips.THREAD_INLINE_ANCHOR_TAGNAME.equals(doc.getTagName(el))) {
            elementsToDelete.add(el);
          }
        }
        // Reverse elements to delete so we always delete bottom up if one
        // contains another (which would be really weird anyway).
        Collections.reverse(elementsToDelete);
        for (E el : elementsToDelete) {
          doc.deleteNode(el);
        }
      }
    });
  }

  private void triggerOnReplyAdded(WaveletBasedConversationThread reply) {
    for (Listener l : listeners) {
      l.onReplyAdded(reply);
    }
  }

  // Package-private for access from WaveletBasedConversationThread.
  void triggerOnDeleted() {
    helper.getWaveletEventSource().removeListener(waveletListener);
    invalidate();
    for (Listener l : listeners) {
      l.onDeleted();
    }
  }

  private void triggerOnContributorAdded(ParticipantId contributor) {
    for (Listener l : listeners) {
      l.onContributorAdded(contributor);
    }
  }

  private void triggerOnContributorRemoved(ParticipantId contributor) {
    for (Listener l : listeners) {
      l.onContributorRemoved(contributor);
    }
  }

  private void triggerOnSubmitted() {
    for (Listener l : listeners) {
      l.onSumbitted();
    }
  }

  private void triggerOnTimestampModified(long oldTimestamp, long newTimestamp) {
    for (Listener l : listeners) {
      l.onTimestampChanged(oldTimestamp, newTimestamp);
    }
  }
}
TOP

Related Classes of org.waveprotocol.wave.model.conversation.WaveletBasedConversationBlip$LocatedReplyThread

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.