Package org.waveprotocol.wave.client.editor.content

Source Code of org.waveprotocol.wave.client.editor.content.ContentNode

/**
* 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.client.editor.content;

import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.Node;

import org.waveprotocol.wave.client.common.util.VolatileComparable;
import org.waveprotocol.wave.client.debug.logger.DomLogger;
import org.waveprotocol.wave.client.debug.logger.LogLevel;
import org.waveprotocol.wave.client.editor.extract.Repairer;
import org.waveprotocol.wave.client.editor.extract.TypingExtractor;
import org.waveprotocol.wave.client.editor.impl.HtmlView;
import org.waveprotocol.wave.client.editor.selection.content.SelectionHelper;
import org.waveprotocol.wave.client.editor.sugg.SuggestionsManager;
import org.waveprotocol.wave.client.scheduler.ScheduleCommand;
import org.waveprotocol.wave.client.scheduler.Scheduler.Task;
import org.waveprotocol.wave.common.logging.LoggerBundle;
import org.waveprotocol.wave.model.document.Doc;
import org.waveprotocol.wave.model.document.ReadableDocument;
import org.waveprotocol.wave.model.document.indexed.LocationMapper;
import org.waveprotocol.wave.model.document.raw.RawDocument;
import org.waveprotocol.wave.model.document.util.Point;
import org.waveprotocol.wave.model.document.util.Pretty;
import org.waveprotocol.wave.model.util.OffsetList;
import org.waveprotocol.wave.model.util.Preconditions;

import java.util.HashMap;
import java.util.Map;

/**
* Content node. Base class for ContentTextNode and ContentElement.
*
* TODO(danilatos): Thoroughly update the javadoc for this class
*
* See {@link ContentDocument} for more...
*
* In this context, the word "nodelet" is used to refer to the JSO dom nodes, to
* avoid confusion with the word "node", which when unqualified, refers to
* subclasses of ContentNode.
*
* The handleXXX methods with boolean return values are called to see if this
* node will handle a given event. If the method handles the event, it will
* return true, and no further processing of the event is needed. Otherwise, it
* will return false. The methods may trigger an operation event if they handle
* the browser event. (They might not, for example to simply cause the browser
* event to be ignored, and prevent the default handling by returning true)
*
* TODO(danilatos): Extract out an interface for these methods.
*
* NOTE(danilatos): By default, here and in subclasses, if a method's name is
* ambiguous as to whether it refers to the content or the html, it refers to
* the html.
*
*/
public abstract class ContentNode implements Doc.N,
    VolatileComparable<ContentNode>, MutatingNode<ContentNode, ContentElement> {

  /**
   * Debug logger
   */
  protected static LoggerBundle logger = new DomLogger("editor-node");

  private final ExtendedClientDocumentContext context;

  private Node implNodelet;
  private ContentElement parent = null;
  private ContentNode next = null;
  private ContentNode prev = null;
  protected static final int MAX_REPAIR_ATTEMPTS = 50;

  private OffsetList.Container<ContentNode> indexingContainer;

  /**
   * @param implNodelet The wrapped nodelet
   * @param context
   */
  public ContentNode(Node implNodelet, ExtendedClientDocumentContext context) {
    this.context = context;
    this.implNodelet = implNodelet;
  }

  /**
   * @return The top-level wrapped implementation html nodelet. It might be null
   *         (either because we are halfway through repairing, or because this
   *         ContentNode is a meta-node that has no corresponding HTML
   *         implementation)
   */
  public Node getImplNodelet() {
    return this.implNodelet;
  }

  /**
   * Same as {@link #getImplNodelet()}, but traverses the filtered view righwards
   * until it finds a wrapper that actually has an impl nodelet, if the first
   * doesn't.
   */
  public Node getImplNodeletRightwards() {
    return getImplNodeletRightwards(null);
  }

  /**
   * Same as {@link #getImplNodeletRightwards()} but with early exit
   * @param toExcl Stop here if reached
   */
  public Node getImplNodeletRightwards(ContentNode toExcl) {
    // TODO(danilatos): This implementation will skip over an html-only node
    // in the midst of other impl nodelets. This might not be desirable in
    // some contexts...
    assert isContentAttached();
    ContentNode node = this;
    Node nodelet = null;
    ContentView renderedContent = getRenderedContentView();
    while (node != toExcl) {
      nodelet = node.getImplNodelet();
      if (nodelet != null) {
        break;
      }
      node = renderedContent.getNextSibling(node);
    }
    return nodelet;
  }

  /**
   * Same as {@link #getImplNodeletRightwards()}, but starts from the next
   * sibling of this ContentNode
   */
  public Node getNextImplNodeletRightwards() {
    return getNextImplNodeletRightwards(null);
  }

  /**
   * Same as {@link #getNextImplNodeletRightwards()} but with early exit
   * @param toExcl Stop here if reached
   */
  public Node getNextImplNodeletRightwards(ContentNode toExcl) {
    ContentNode next = getNextSibling();
    return next == null ? null : next.getImplNodeletRightwards(toExcl);
  }

  public Node normaliseImpl() {
    return getImplNodelet();
  }

  void setImplNodelet(Node nodelet) {
    implNodelet = nodelet;
  }

  void breakBackRef(boolean recurse) {
  }

  /**
   * TODO(danilatos): Use something other than this method to determine this
   * @return whether a node is persistent.
   */
  public boolean isPersistent() {
    return getIndexingContainer() != null;
  }

  /**
   * @see ReadableDocument#getParentElement(Object)
   */
  public ContentElement getParentElement() {
    return parent;
  }

  /**
   * @see ReadableDocument#getNextSibling(Object)
   */
  public ContentNode getNextSibling() {
    return next;
  }

  /**
   * @see ReadableDocument#getPreviousSibling(Object)
   */
  public ContentNode getPreviousSibling() {
    return prev;
  }

  /**
   * @see ReadableDocument#getFirstChild(Object)
   */
  public ContentNode getFirstChild() {
    return null;
  }

  /**
   * @see ReadableDocument#getLastChild(Object)
   */
  public ContentNode getLastChild() {
    return null;
  }

  /** package private setter, used by ContentElement */
  void setNext(ContentNode next) {
    this.next = next;
  }

  /** package private setter, used by ContentElement */
  void setPrev(ContentNode prev) {
    this.prev = prev;
  }

  /** package private setter, used by ContentElement */
  void setParent(ContentElement parent) {
    this.parent = parent;
  }

  /** package private getter, used by ContentRawDocument */
  OffsetList.Container<ContentNode> getIndexingContainer() {
    return indexingContainer;
  }

  /** package private setter, used by ContentRawDocument */
  void setIndexingContainer(OffsetList.Container<ContentNode> container) {
    indexingContainer = container;
  }

  /**
   * Package private, used by ContentElement
   * Removes from the wrapper structure
   * Does not affect the underlying dom node
   * Does not affect its own pointers
   * Does not affect its relationship with its children, if any
   */
  final void removeFromShadowTree() {
    if (prev == null) {
      if (parent != null) {
        parent.setFirstChild(next);
      }
    } else {
      prev.next = next;
    }
    if (next == null) {
      if (parent != null) {
        parent.setLastChild(prev);
      }
    } else {
      next.prev = prev;
    }
  }

  /**
   * Sets its prev, next and parent pointers to null
   * Does not affect underlying dom node
   * Does not affect neighbours
   * Does not affect relationship with children, if any
   */
  final void clearNodeLinks() {
    prev = next = parent = null;
  }

  /**
   * @see ReadableDocument#getNodeType(Object)
   */
  public abstract short getNodeType();

  /**
   * @return true if this node is an element
   */
  public abstract boolean isElement();

  /**
   * @return true if this node is a text node
   */
  public abstract boolean isTextNode();

  /**
   * @return the node as a text node if it is one, null otherwise
   */
  public abstract ContentTextNode asText();

  /**
   * @return the node as an element if it is one, null otherwise
   */
  public abstract ContentElement asElement();

  /**
   * @return true if this node is in the rendered view
   */
  public boolean isRendered() {
    // This logic might need updating at some point.
    // Currently, if a node is lacking an impl nodelet, then we treat it
    // as unrendered. Text nodes are an exception, because they often
    // lack an impl nodelet because of typing extraction & zipping.
    return getImplNodelet() != null || (isTextNode() && getParentElement().isRendered());
  }

  /** Package private low level functionality */
  final ExtendedClientDocumentContext getExtendedContext() {
    return context;
  }

  /**
   * @return the document context this node is a part of
   */
  public final ClientDocumentContext getContext() {
    return context;
  }

  /**
   * Exposed for subclasses.
   */
  public final HtmlView getFilteredHtmlView() {
    return context.rendering().getFilteredHtmlView();
  }

  /**
   * Exposed for subclasses.
   */
  public ContentView getRenderedContentView() {
    return context.rendering().getRenderedContentView();
  }

  /**
   * Exposed for subclasses.
   */
  public final CMutableDocument getMutableDoc() {
    return context.document();
  }

  /**
   * Exposed for subclasses
   *
   * DO NOT expose getAggressiveSelectionHelper() !
   * It could cause all kinds of problems with interleaved application of ops.
   */
  public final SelectionHelper getSelectionHelper() {
    return context.editing().getSelectionHelper();
  }

  // Package private
  final Repairer getRepairer() {
    return context.rendering().getRepairer();
  }

  // Package private
  final TypingExtractor getTypingExtractor() {
    return context.editing().getTypingExtractor();
  }

  /**
   * Exposed for subclasses
   */
  public final LocationMapper<ContentNode> getLocationMapper() {
    return context.locationMapper();
  }

  /**
   * Exposed for subclasses
   */
  public final boolean inEditMode() {
    return context.isEditing();
  }

  /**
   * Exposed for subclasses
   */
  public final String getEditorUniqueString() {
    return context.getDocumentId();
  }

  /**
   * Exposed for subclasses
   */
  public final SuggestionsManager getSuggestionsManager() {
    return context.editing().getSuggestionsManager();
  }

  /**
   * Exposed for subclasses
   */
  public final ContentElement getElementByName(String name) {
    return context.getElementByName(name);
  }

  /**
   * Check if the HTML for this element is "OK" (where OK loosely means
   * "doesn't need fixing").
   * @return true if html impl is correct & attached
   */
  public abstract boolean isConsistent();

  /**
   * Throw away and redo the html implementation (drastic repair mechanism)
   */
  public abstract void revertImplementation();

  /**
   * {@inheritDoc}
   */
  @Override
  public String toString() {
    String name = getClass().getName();
    name = name.substring(name.lastIndexOf('.') + 1);
    String nodeletString = "destroyed";
    try {
      // Note(user): this toString can fail for text nodes that IE's editor has deleted
      nodeletString = getImplNodelet() == null ? "null" :
          new Pretty<Node>().print(context.rendering().getFullHtmlView(), getImplNodelet());
    } catch (Throwable t) {
    }
    String contentString = new Pretty<ContentNode>().print(
        FullContentView.INSTANCE, this);
    return name + ": " + contentString + " / " + nodeletString;
  }

  //////// MUTATING NODE

  /**
   * {@inheritDoc}
   */
  public void onAddedToParent(ContentElement previousParent) {}

  /**
   * {@inheritDoc}
   */
  public void onRemovedFromParent(ContentElement newParent) {}

  /**
   * {@inheritDoc}
   */
  public void onChildAdded(ContentNode child) {}

  /**
   * {@inheritDoc}
   */
  public void onChildRemoved(ContentNode child) {}

  /**
   * {@inheritDoc}
   */
  public void onAttributeModified(String name, String oldValue, String newValue) {}

  /**
   * {@inheritDoc}
   */
  public void onDescendantsMutated() {}

  /**
   * {@inheritDoc}
   */
  public void onEmptied() {}

  void rethrowOrNoteErrorOnMutation(RuntimeException e) {
    try {
      assert false;
    } catch (AssertionError ae) {
      // assertions turned on - re-throw unconditionally
      throw e;
    }
    if (LogLevel.showErrors()) {
      throw e;
    } else {
      noteErrorOnMutationEvent(e);
    }
  }

  /**
   * This must be called after this node is added to a parent.
   * Calls onAddedToParent, onChildAdded on all appropriate nodes.
   */
  protected final void notifyAddedToParent(ContentElement oldParent,
      boolean notifyMutatedUpwards) {
    try {
      // TODO(danilatos, lars): Order of these? does it matter?
      onAddedToParent(oldParent);
      ContentElement parent = getParentElement();
      parent.onChildAdded(this);
    } catch (RuntimeException e) {
      rethrowOrNoteErrorOnMutation(e);
    }
    if (notifyMutatedUpwards) {
      parent.notifyChildrenMutated();
    }
  }

  /**
   * This must be called after this node is removed from a parent. Calls
   * onRemovingFromParent, onRemovingChild
   *
   * Does NOT call onDescendantsMutated
   *
   * @param oldParent the parent this node is being removed from
   * @param newParent the parent this node is being moved to, if any. null if it
   *        is being removed from the DOM altogether
   */
  protected final void notifyRemovedFromParent(ContentNode oldParent, ContentElement newParent) {
    try {
      onRemovedFromParent(newParent);
      oldParent.onChildRemoved(this);
    } catch (RuntimeException e) {
      rethrowOrNoteErrorOnMutation(e);
    }
  }

  /**
   * Gracefully handle any errors when changing the underlying HTML dom.
   * This should always be used and exception guards should be placed around
   * code that mutates the HTML, wherever exceptions could cause document
   * corruption.
   * @param e The exception thrown
   */
  void noteErrorWithImplMutation(Exception e) {
    // TODO(danilatos, mtsui): Better handling, see why we are throwing
    // exceptions in the first place and what sorts of exceptions.
    logger.error().log(e + " Scheduling revert.");
    ScheduleCommand.addCommand(new Task() {
      public void execute() {
        getRepairer().revert(Point.inElement(getParentElement(), ContentNode.this), null);
      }
    });
  }

  /**
   * Gracefully handle any errors thrown by external code in a mutation handler.
   * This should always be used and exception guards should be placed around
   * code that calls the notifyXXX methods, wherever exceptions could cause document
   * corruption.
   * @param e The exception thrown
   */
  void noteErrorOnMutationEvent(Exception e) {
    // TODO(danilatos): Better handling
    logger.error().log(
          "noteErrorOnMutationEvent: " + e);
    // For debug builds, fail here rather than trying to recover.
    assert false : "noteErrorOnMutationEvent: " + e;
  }

  ///// IMPL helpers

  /**
   * Non static versions. Separation exists purely because the exception guarding
   * requires the "this" context, but we'd like to enforce a static
   * context for the meat of the implementation.
   */
  void implInsertBefore(ContentElement parent,
      ContentNode from, ContentNode to, ContentNode refChild, Element oldContainerNodelet) {
    try {
      staticImplInsertBefore(parent, from, to, refChild, oldContainerNodelet);
    } catch (RuntimeException e) {
      e.printStackTrace();
      // Safe to swallow the exception, the impl mutation code does not
      // transitively affect external state.
      noteErrorWithImplMutation(e);
    }
  }

  /**
   * Do not use these directly, they are used by the non-static equivalents
   *
   * Parameters correspond to parameters of
   * @see RawDocument#insertBefore(Object, Object, Object, Object)
   */
  private static void staticImplInsertBefore(ContentElement parent,
      ContentNode from, ContentNode toExcl, ContentNode refChild, Element oldContainerNodelet) {
    Preconditions.checkArgument(toExcl == null
        || toExcl.getParentElement() == from.getParentElement(),
        "invalid toExcl");

    Element containerNodelet = parent.getContainerNodelet();
    if (containerNodelet != null) {
      Node implRef = null;
      // Don't use getImplNodeletRightwards(), it's too clever
      for (ContentNode node = refChild; node != null; node = node.getNextSibling()) {
        if (node.getImplNodelet() != null) {
          Preconditions.checkState(node.getImplNodelet().hasParentElement(),
              "implNodelet not attached");
          implRef = node.getImplNodelet();
          break;
        }
      }

      if (implRef != null) {
        assert implRef.getParentElement() == containerNodelet;
        // Be robust if assertions are off
        containerNodelet = implRef.getParentElement();
        if (containerNodelet == null) {
          return;
        }
      }

      for (ContentNode node = from; node != toExcl; node = node.getNextSibling()) {
        if (node.isTextNode()) {
          ((ContentTextNode) node).normaliseImpl();
        }
        Node nodelet = node.getImplNodelet();
        if (nodelet != null) {
          containerNodelet.insertBefore(nodelet, implRef);
        }
      }
    } else {
      if (oldContainerNodelet != null) {
        for (ContentNode node = from; node != toExcl; node = node.getNextSibling()) {
          Node nodelet = node.getImplNodelet();
          if (nodelet != null) {
            nodelet.removeFromParent();
          }
        }
      }
    }
  }

  /** {@inheritDoc} */
  public boolean isComparable() {
    // TODO(danilatos): Is there a more robust measure, whilst remaining efficient?
    return isContentAttached();
  }

  /**
   * Comparison is based on position in the tree.
   *
   * TODO(danilatos): Use our new indexing scheme to compare instead??
   *
   * WARNING(danilatos): This is a dynamic property! Be careful when you use it.
   * TODO(danilatos): Investigate if it's better to not implement the comparator
   * interface to prevent accidental inappropriate use, but just have this
   * method implemented for when needed directly.
   * {@link #isComparable()} will return false when this node cannot be compared
   * to other nodes.
   *
   * {@inheritDoc}
   */
  public int compareTo(ContentNode other) {
    // TODO(danilatos): Room for some optimisation in this method.
    // Could probably do most (not all) cases with some kind of text range
    // comparison.
    // http://developer.mozilla.org/en/docs/DOM:range.compareBoundaryPoints

    if (!isComparable() || !other.isComparable()) {
      throw new IllegalArgumentException("Cannot compare unattached nodes");
    }

    // Map of elements in the ancestor path -> child of said parent in ancestor path
    Map<ContentNode, ContentNode> ancestors =
        new HashMap<ContentNode, ContentNode>();

    ContentNode minePrev = null, theirsPrev = null;
    ContentNode mine = this, theirs = other;

    // Check if the same
    if (mine == theirs || mine.equals(theirs)) {
      return 0;
    }

    // Go up one level if text nodes, to avoid placing them as keys in the map
    if (mine instanceof ContentTextNode) {
      minePrev = mine;
      mine = mine.getParentElement();
    }

    if (theirs instanceof ContentTextNode) {
      theirsPrev = theirs;
      theirs = theirs.getParentElement();
    }

    // Populate my ancestor chain
    while (mine != null) {
      ancestors.put(mine, minePrev);
      minePrev = mine;
      mine = mine.getParentElement();
    }

    // Find nearest common ancestor
    ContentNode nca = theirs;
    while (!ancestors.containsKey(nca)) {
      theirsPrev = nca;
      assert nca != null : "Incomparable nodes!";
      nca = nca.getParentElement();
    }

    minePrev = ancestors.get(nca);
    if (minePrev == null) {
      return -1;
    }
    if (theirsPrev == null) {
      return 1;
    }
    // We assume that they are not equal, if we're up to here.
    for (ContentNode search = minePrev; search != null; search = search.getPreviousSibling()) {
      if (search.equals(theirsPrev)) {
        return 1;
      }
    }
    return -1;
  }

  /**
   * TODO(danilatos): A more robust way to see if the node still "exists".
   * w.r.t. the content representation.
   * We also need to clean these up when they are removed...
   *
   * @return true if the node is attached to our content tree
   */
  public boolean isContentAttached() {
    ContentElement e = isTextNode() ? getParentElement() : (ContentElement) this;
    ContentElement root = getMutableDoc().getDocumentElement();
    while (e != root) {
      if (e == null) {
        return false;
      }
      e = e.getParentElement();
    }
    return true;
  }

  /**
   * @return true if the node is still attached w.r.t. the html implementation
   */
  public boolean isImplAttached() {
    // TODO(danilatos): Implement this as doing a filtered search up the filtered html tree
    Node nodelet = getImplNodelet();
    return nodelet != null && nodelet.hasParentElement();
  }

  /**
   * Assert that node is healthy
   */
  public void debugAssertHealthy() {
    // Assert that implNodelet points back to this
//    Assert.assertEquals("Backref should be to wrapping ContentNode",
//        this, ContentNode.getContentNode(implNodelet));
  }

  /**
   * @param other
   * @return true if this node is equal to or is an ancestor of other
   */
  boolean isOrIsAncestorOf(ContentNode other) {
    while (other != null) {
      if (this == other) {
        return true;
      }
      other = other.getParentElement();
    }
    return false;
  }
}
TOP

Related Classes of org.waveprotocol.wave.client.editor.content.ContentNode

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.