Package org.waveprotocol.wave.model.document.indexed

Source Code of org.waveprotocol.wave.model.document.indexed.IndexedDocumentImpl$InvertibleCursor

/**
* 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.document.indexed;

import org.waveprotocol.wave.model.document.AnnotationCursor;
import org.waveprotocol.wave.model.document.AnnotationInterval;
import org.waveprotocol.wave.model.document.RangedAnnotation;
import org.waveprotocol.wave.model.document.indexed.RawAnnotationSet.AnnotationEndEvent;
import org.waveprotocol.wave.model.document.indexed.RawAnnotationSet.AnnotationEvent;
import org.waveprotocol.wave.model.document.indexed.RawAnnotationSet.AnnotationStartEvent;
import org.waveprotocol.wave.model.document.operation.AnnotationBoundaryMap;
import org.waveprotocol.wave.model.document.operation.Attributes;
import org.waveprotocol.wave.model.document.operation.AttributesUpdate;
import org.waveprotocol.wave.model.document.operation.Automatons;
import org.waveprotocol.wave.model.document.operation.DocOp;
import org.waveprotocol.wave.model.document.operation.DocInitialization;
import org.waveprotocol.wave.model.document.operation.DocOpCursor;
import org.waveprotocol.wave.model.document.operation.Nindo;
import org.waveprotocol.wave.model.document.operation.NindoValidator;
import org.waveprotocol.wave.model.document.operation.Nindo.NindoCursor;
import org.waveprotocol.wave.model.document.operation.algorithm.AnnotationsNormalizer;
import org.waveprotocol.wave.model.document.operation.algorithm.Composer;
import org.waveprotocol.wave.model.document.operation.automaton.AutomatonDocument;
import org.waveprotocol.wave.model.document.operation.automaton.DocumentSchema;
import org.waveprotocol.wave.model.document.operation.automaton.DocOpAutomaton.ViolationCollector;
import org.waveprotocol.wave.model.document.operation.impl.AttributesImpl;
import org.waveprotocol.wave.model.document.operation.impl.AttributesUpdateImpl;
import org.waveprotocol.wave.model.document.operation.impl.DocOpBuffer;
import org.waveprotocol.wave.model.document.operation.impl.DocOpBuilder;
import org.waveprotocol.wave.model.document.operation.impl.DocOpUtil;
import org.waveprotocol.wave.model.document.operation.impl.DocOpValidator;
import org.waveprotocol.wave.model.document.operation.impl.UncheckedDocOpBuffer;
import org.waveprotocol.wave.model.document.raw.RawDocument;
import org.waveprotocol.wave.model.document.util.AnnotationIntervalImpl;
import org.waveprotocol.wave.model.document.util.Annotations;
import org.waveprotocol.wave.model.document.util.DocOpScrub;
import org.waveprotocol.wave.model.document.util.EmptyDocument;
import org.waveprotocol.wave.model.document.util.Point;
import org.waveprotocol.wave.model.document.util.RangedAnnotationImpl;
import org.waveprotocol.wave.model.document.util.XmlStringBuilder;
import org.waveprotocol.wave.model.operation.OpCursorException;
import org.waveprotocol.wave.model.operation.OperationException;
import org.waveprotocol.wave.model.operation.OperationRuntimeException;
import org.waveprotocol.wave.model.util.Box;
import org.waveprotocol.wave.model.util.CollectionUtils;
import org.waveprotocol.wave.model.util.EvaluableOffsetList;
import org.waveprotocol.wave.model.util.OffsetList;
import org.waveprotocol.wave.model.util.Preconditions;
import org.waveprotocol.wave.model.util.ReadableStringMap;
import org.waveprotocol.wave.model.util.ReadableStringSet;
import org.waveprotocol.wave.model.util.StringMap;
import org.waveprotocol.wave.model.util.StringSet;
import org.waveprotocol.wave.model.util.ValueUtils;
import org.waveprotocol.wave.model.util.ReadableStringMap.ProcV;
import org.waveprotocol.wave.model.util.ReadableStringSet.Proc;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

/**
* An implementation of IndexedDocument with an associative operator for
* hashing.
*
* TODO(user): This doesn't yet do proper error checking. We need to
* implement proper error checking.
*
* TODO(user): Write more tests for this class.
*
* TODO(user): Optimise skip for small skips.
*
* @author danilatos@google.com (Daniel Danilatos)
* @author alexmah@google.com (Alexandre Mah)
*
* @param <N> The type of DOM nodes.
* @param <E> The type of DOM Element nodes.
* @param <T> The type of DOM Text nodes.
* @param <V> The type of result that the document evaluates to.
*/
public class IndexedDocumentImpl<N, E extends N, T extends N, V>
    implements IndexedDocument<N, E, T>, Validator {

  /**
   * Whether to perform validation on consumed ops and nindos
   *
   * This should be on, except if
   * - profiling shows that it is a bottleneck AND we have no bugs
   * - a test case wants to explicitly validate separately to do better error reporting
   */
  public static boolean performValidation = true;

  /**
   * A node-finding action that returns a point representing the location at
   * which the action is performed.
   *
   * The "natural" bias is to prefer text node point to parent-nodeAfter points,
   * and to prefer text node ends to text node beginnings. This results in things
   * working more smoothly with typing and annotations.
   *
   * TODO(danilatos): Separate this concern out?
   */
  private final OffsetList.LocationAction<N, Point<N>> pointFinder
      = new OffsetList.LocationAction<N, Point<N>>() {

    public Point<N> performAction(OffsetList.Container<N> container, int offset) {
      N domNode = container.getValue();
      Point<N> maybeAdjust;
      if (domNode == null) {
        if (container == offsetList.sentinel()) {
          return Point.<N>end(substrate.getDocumentElement());
        }
        E element = getParentOf(container);
        maybeAdjust = maybeTextNodeEnd(getLastChild(element));
        return maybeAdjust != null ? maybeAdjust : Point.<N>end(element);
      } else if (substrate.asElement(domNode) != null) {
        assert offset == 0;
        maybeAdjust = maybeTextNodeEnd(getPreviousSibling(domNode));
        return maybeAdjust != null ? maybeAdjust : Point.before(substrate, domNode);
      } else {
        if (offset == 0) {
          maybeAdjust = maybeTextNodeEnd(getPreviousSibling(domNode));
          return maybeAdjust != null ? maybeAdjust : Point.inText(domNode, offset);
        } else {
          return Point.inText(domNode, offset);
        }
      }
    }

    private Point<N> maybeTextNodeEnd(N node) {
      T textNode = asText(node);
      return textNode == null ? null : Point.<N>inText(textNode, getLength(textNode));
    }

  };

  /**
   * An action that, when performed at a location in this document, will set the
   * currentContainer and currentOffset fields of this object to correspond to
   * the location.
   */
  private final OffsetList.LocationAction<N,Void> locationUpdater =
      new OffsetList.LocationAction<N, Void>() {

    public Void performAction(OffsetList.Container<N> container, int offset) {
      currentContainer = container;
      currentOffset = offset;
      currentParent = getParentOf(container);
      return null;
    }

  };

  /**
   * For handling the DOM structure of this document.
   */
  private final RawDocument<N, E, T> substrate;

  /**
   * An offset list for tracking the offsets of parts of the document.
   */
  private final EvaluableOffsetList<N, V> offsetList;

  /**
   * The current location of the pointer in the document.
   */
  private int currentLocation;

  /**
   * The container where the current location resides.
   */
  private OffsetList.Container<N> currentContainer;

  /**
   * The offset of the current location in the current container.
   */
  private int currentOffset;

  /**
   * The parent of the current DOM node. This may become inaccurate when the
   * deletion of an element is in progress.
   */
  private E currentParent;

  /**
   * The current depth of structural modifications.
   */
  private int deletionDepth;

  /**
   * Annotation set datastructure to delegate to
   */
  private final RawAnnotationSet<Object> annotations;

  /**
   * Schema constraints
   */
  private final DocumentSchema schemaConstraints;

  /**
   * Automaton interface for validity checking
   */
  private final AutomatonDocument autoDoc = Automatons.fromReadable(this);

  /**
   * @param substrate raw dom document to use
   * @param rawAnnotations raw annotations to use
   */
  public IndexedDocumentImpl(RawDocument<N, E, T> substrate,
      RawAnnotationSet<Object> rawAnnotations, DocumentSchema constraints) {
    Preconditions.checkNotNull(constraints,
        "Null schema not allowed, use DocumentSchema.NO_SCHEMA_CONSTRAINTS");
    this.schemaConstraints = constraints;

    annotations = rawAnnotations != null
        ? rawAnnotations : new StubModifiableAnnotations<Object>();

    this.substrate = substrate;
    offsetList = new EvaluableOffsetList<N, V>(null);

    assert size() == 0;

    indexChildren(substrate.getDocumentElement());

    resetLocation();

    if (offsetList.size() > 0) {
      annotations.begin();
      annotations.insert(offsetList.size());
      annotations.finish();
    }

    assert substrate.getFirstChild(substrate.getDocumentElement()) != null || size() == 0;
  }


  /**
   * Indexes an element and its contents.
   *
   * @param element The element to index.
   */
  private void indexElement(E element) {
    OffsetList.Container<N> sentinel = offsetList.sentinel();
    insertBefore(sentinel, element, 1);
    indexChildren(element);
    sentinel.insertBefore(null, 1);
  }

  private void indexChildren(E element) {
    OffsetList.Container<N> sentinel = offsetList.sentinel();
    for (N child = substrate.getFirstChild(element); child != null;
        child = substrate.getNextSibling(child)) {
      E childElement = substrate.asElement(child);
      if (childElement != null) {
        indexElement(childElement);
      } else {
        T childText = substrate.asText(child);
        insertBefore(sentinel, childText, substrate.getLength(childText));
      }
    }
  }

  /**
   * {@inheritDoc}
   */
  public Point<N> locate(int location) {
    Preconditions.checkPositionIndex(location, offsetList.size());
    return offsetList.performActionAt(location, pointFinder);
  }

  /**
   * {@inheritDoc}
   */
  public int getLocation(N node) {
    Preconditions.checkNotNull(node, "Cannot get the location of a null node");
    OffsetList.Container<N> indexingContainer = substrate.getIndexingContainer(node);
    if (indexingContainer == null) {
      throw new IllegalArgumentException("getLocation: node has no indexing container - " + node);
    }
    if (indexingContainer.getNextContainer() == null) {
      throw new IllegalArgumentException("getLocation: node probably removed from DOM - " + node);
    }
    return indexingContainer.offset();
  }

  /**
   * {@inheritDoc}
   */
  public int getLocation(Point<N> point) {
    Preconditions.checkNotNull(point, "Cannot get the location of a null point");
    Point.checkPoint(this, point, "IndexedDocumentImpl#getLocation");
    if (point.isInTextNode()) {
      return getLocation(point.getContainer()) + point.getTextOffset();
    } else {
      N nodeAfter = point.getNodeAfter();
      if (nodeAfter == null) {
        return getLastLocationIn(point.getContainer());
      } else {
        return getLocation(nodeAfter);
      }
    }
  }

  /**
   * Gets the last location inside an element node.
   *
   * @param node An element node.
   * @return The last location inside an element node.
   */
  private int getLastLocationIn(N node) {
    N lastChild = substrate.getLastChild(node);
    if (lastChild != null) {
      if (substrate.asElement(lastChild) != null) {
        return getLastLocationIn(lastChild) + 1;
      } else {
        OffsetList.Container<N> indexingContainer = substrate.getIndexingContainer(lastChild);
        return indexingContainer.offset() + indexingContainer.size();
      }
    } else if (node == getDocumentElement()) {
      return size();
    } else {
      return substrate.getIndexingContainer(node).offset() + 1;
    }
  }

  private void moveToCurrentLocation() {
    offsetList.performActionAt(currentLocation, locationUpdater);
  }

  private final InvertibleCursor invertibleCursor = new InvertibleCursor();

  private boolean inconsistent = false;

  @Override
  public void consume(DocOp op) throws OperationException {
    consume(op, performValidation);
  }

  public void consume(DocOp op, boolean validate) throws OperationException {
    checkConsistent();

    if (validate) {
      maybeThrowOperationExceptionFor(op);
    }

    beginChange();

    try {
      op.apply(invertibleCursor);
    } catch (OpCursorException e) {
      throw new OperationException(e.getMessage(), e);
    }

    endChange();
  }

  public void maybeThrowOperationExceptionFor(DocOp op) throws OperationException {
    if (!DocOpValidator.validate(null, schemaConstraints, autoDoc, op).isValid()) {
      // Validate again to collect diagnostics (more expensive)
      ViolationCollector vc = new ViolationCollector();
      DocOpValidator.validate(vc, schemaConstraints, autoDoc, op);

      throw new OperationException(vc);
    }
  }

  private final NonInvertibleCursor nindoCursor = new NonInvertibleCursor();

  public DocOp consumeAndReturnInvertible(Nindo op) throws OperationException {
    return consumeAndReturnInvertible(op, performValidation);
  }

  public DocOp consumeAndReturnInvertible(Nindo op, boolean validate)
      throws OperationException {
    checkConsistent();

    if (validate) {
      maybeThrowOperationExceptionFor(op);
    }

    nindoCursor.begin2();

    try {
      op.apply(nindoCursor);
    } catch (OpCursorException e) {
      throw new OperationException(e.getMessage(), e);
    }

    return nindoCursor.finish2();
  }

  @Override
  public void maybeThrowOperationExceptionFor(Nindo op) throws OperationException {
    ViolationCollector vc = NindoValidator.validate(this, op, schemaConstraints);
    if (!vc.isValid()) {
      // TODO(danilatos): reconcile the two validation methods
      throw new OperationException(vc);
    }
  }

  private void checkConsistent() {
    Preconditions.checkState(!inconsistent, "The document is not in a consistent state");
  }

  private void beginChange() {

    beforeBegin();

    inconsistent = true;

    resetLocation();
    annotations.begin();
  }

  private void endChange() throws OperationException {
    if (currentLocation != size()) {
      throw new OperationException("Operation size does not match document size " +
          "[operation size:" + currentLocation + "] [doc size:" + size() + "]");
    }

    annotations.finish();
    checkSizeConsistency("finish");
    inconsistent = false;

    afterFinish();
  }

  private class InvertibleCursor implements DocOpCursor {

    @Override
    public void updateAttributes(AttributesUpdate attrUpdate) {
      annotations.skip(1);

      E node = substrate.asElement(currentContainer.getValue());
      for (int i = 0; i < attrUpdate.changeSize(); i++) {
        String name = attrUpdate.getChangeKey(i);
        String newValue = attrUpdate.getNewValue(i);
        if (newValue != null) {
          substrate.setAttribute(node, name, newValue);
        } else {
          substrate.removeAttribute(node, name);
        }
      }
      ++currentLocation;
      currentContainer = currentContainer.getNextContainer();
      currentParent = node;

      onModifyAttributes(currentParent, attrUpdate);
    }

    @Override
    public void deleteCharacters(String chars) {
      assert chars.length() > 0;
      annotations.delete(chars.length());
      doDeleteCharacters(chars.length());

      onDeleteCharacters(currentLocation, chars);
    }

    @Override
    public void replaceAttributes(Attributes oldAttrs, Attributes newAttrs) {
      annotations.skip(1);

      E node = substrate.asElement(currentContainer.getValue());
//      Map<String, String> oldAttributes = substrate.getAttributes(node);
      // Iterate over oldAttributes here, not attributeMap, since we are modifying
      // the map underlying the latter.
      for (Map.Entry<String, String> attribute : oldAttrs.entrySet()) {
        String key = attribute.getKey();
        if (!newAttrs.containsKey(key)) {
          substrate.removeAttribute(node, key);
        }
      }
      for (Map.Entry<String, String> attribute : newAttrs.entrySet()) {
        if (attribute.getValue() == null) {
          throw new OpCursorException("Null attribute value in setAttributes");
        }
        substrate.setAttribute(node, attribute.getKey(), attribute.getValue());
      }
      ++currentLocation;
      currentContainer = currentContainer.getNextContainer();
      currentParent = node;

      onModifyAttributes(currentParent, oldAttrs, newAttrs);
    }

    @Override
    public void retain(int itemCount) {
      assert itemCount > 0;
      checkRetain(itemCount);

      annotations.skip(itemCount);

      currentLocation += itemCount;
      moveToCurrentLocation();
    }

    @Override
    public void annotationBoundary(AnnotationBoundaryMap map) {
      for (int i = 0; i < map.endSize(); i++) {
        doEndAnnotation(map.getEndKey(i));
      }
      for (int i = 0; i < map.changeSize(); i++) {
        doStartAnnotation(map.getChangeKey(i), map.getNewValue(i));
      }
    }

    @Override
    public void characters(String characters) {
      doCharacters(characters);
    }

    @Override
    public void elementEnd() {
      doElementEnd();
    }

    @Override
    public void elementStart(String tagName, Attributes attributes) {
      doElementStart(tagName, attributes);
    }

    @Override
    public void deleteElementStart(String type, Attributes attrs) {

      E nodeToDelete = substrate.asElement(currentContainer.getValue());
      if (nodeToDelete == null) {
        throw new OpCursorException("No element to delete at the current location.");
      }
      if (deletionDepth == 0) {
        substrate.removeChild(currentParent, nodeToDelete);
      }

      annotations.delete(1);
      deleteCurrentContainer();

      ++deletionDepth;

      onDeleteElementStart(currentLocation, nodeToDelete);
    }

    public void deleteElementEnd() {
      onDeleteElementEnd();

      annotations.delete(1);
      deleteCurrentContainer();

      --deletionDepth;
    }
  }

  public class NonInvertibleCursor implements NindoCursor {
    private AnnotationsNormalizer<DocOp> builder;
    int sizeDiffSoFar;

    StringMap<String> requestedValues = CollectionUtils.createStringMap();
    StringSet requestedKeys = CollectionUtils.createStringSet();

    StringMap<String> newValues = CollectionUtils.createStringMap();

    StringSet endKeys = CollectionUtils.createStringSet();

    StringMap<String> deletionValues = CollectionUtils.createStringMap();

    boolean didSomethingOtherThanDeletionSinceAnnotationBoundary = false;

    private void begin2() {
      beginChange();
      builder = new AnnotationsNormalizer<DocOp>(
          performValidation ? new DocOpBuffer() : new UncheckedDocOpBuffer());
      sizeDiffSoFar = 0;
      deletionValues.clear();
    }

    private DocOp finish2() throws OperationException {
      endChange();
      return builder.finish();
    }

    @Override
    public void begin() {
    }

    @Override
    public void finish() {
      int remaining = size() - currentLocation;
      closeEndKeys();
      if (remaining > 0) {
        builder.retain(remaining);
      }
      currentLocation = size();
    }

    public void startAnnotation(String key, String value) {

      if (endKeys.contains(key)) {
        assert !didSomethingOtherThanDeletionSinceAnnotationBoundary
            : "Key: " + key + " endKeys: " + endKeys.toString();

        endKeys.remove(key);
      }

      doStartAnnotation(key, value);

      requestedValues.put(key, value);
      requestedKeys.add(key);

      newValues.put(key, value);

      didSomethingOtherThanDeletionSinceAnnotationBoundary = false;
      //System.out.println("  ---> " + requestedValues + ",   " + newValues + ",   " + endKeys);
    }

    public void endAnnotation(String key) {
      requestedValues.remove(key);
      requestedKeys.remove(key);

      newValues.remove(key);

      if (deletionValues.containsKey(key)) {
        endKeys.add(key);
      }

      doEndAnnotation(key);
      didSomethingOtherThanDeletionSinceAnnotationBoundary = false;
      //System.out.println("  ---> " + requestedValues + ",   " + newValues + ",   " + endKeys);
    }

    public void skip(int itemCount) {
      assert itemCount > 0;
      didSomethingOtherThanDeletionSinceAnnotationBoundary = true;
      checkRetain(itemCount);

      moveAndUpdateAnnotations(itemCount);

      moveToCurrentLocation();
    }

    private void moveAndUpdateAnnotations(int itemCount) {
      // TODO(danilatos): Some redundant updates most likely,
      // because the annotation tree will report its change
      // events. We still need beginUpdate for the
      // annotations which DONT change, to avoid losing
      // them in the operation. Would only matter for transform.
      beginUpdate();

      annotations.skip(itemCount);

      final int finalLocation = currentLocation + itemCount;

      if (!requestedValues.isEmpty()) {
        // HACK(danilatos): Skip doesn't return anything, so do it a slow
        // and annoying way for now.

        final List<AnnotationEvent> events = new ArrayList<AnnotationEvent>();
        final Box<ReadableStringMap<String>> annotations = Box.create();
        int currentLocationBackup = currentLocation;
        final StringSet open = CollectionUtils.createStringSet();
        for (AnnotationInterval<String> i :
            annotationIntervals(currentLocation, finalLocation, requestedKeys)) {
          currentLocation = i.start();
          annotations.boxed = i.annotations();
          requestedValues.each(new ProcV<String>() {
            public void apply(String key, String value) {
              String oldVal = annotations.boxed.get(key, null);
              if (ValueUtils.notEqual(value, oldVal)) {
                events.add(new AnnotationStartEvent(currentLocation, key, oldVal));
                open.add(key);
              } else if (open.contains(key)) {
                events.add(new AnnotationEndEvent(currentLocation, key));
                //assert open.contains(key);
                open.remove(key);
              }
            }
          });
        }
        open.each(new Proc() {
          @Override
          public void apply(String key) {
            events.add(new AnnotationEndEvent(finalLocation, key));
          }
        });
        currentLocation = currentLocationBackup;

        for (AnnotationEvent ev : events) {
          // Uncomment this when not using the above hack
          // int eventLocation = ev.index + sizeDiffSoFar;
          int eventLocation = ev.index;
          if (eventLocation > currentLocation) {

            builder.retain(eventLocation - currentLocation);
            currentLocation = eventLocation;
          }

          if (ev.getEndKey() != null) {
            maybeRenewAnnotation(ev.getEndKey());
            builder.endAnnotation(ev.getEndKey());
          } else {
            String changeKey = ev.getChangeKey();
            builder.startAnnotation(changeKey,
                ev.getChangeOldValue(), requestedValues.get(changeKey, null));
          }
        }
      }

      if (currentLocation < finalLocation) {
        builder.retain(finalLocation - currentLocation);
        currentLocation = finalLocation;
      }

      assert currentLocation == finalLocation;
    }

    private void beginInsert() {
      //System.out.print("INSERT " + requestedValues + ",   " + newValues + ",   " + endKeys);
      //System.out.print(",   " + deletionValues);
      newValues.each(new ProcV<String>() {
        @Override
        public void apply(String key, String value) {
          builder.startAnnotation(key, annotations.getInherited(key), value);
        }
      });
      newValues.clear();

      deletionValues.clear();
      deletionValues.putAll(requestedValues);

      closeEndKeys();
      //System.out.print(" ---> " + requestedValues + ",   " + newValues + ",   " + endKeys);
      //System.out.println(",   " + deletionValues);
    }


    private void beginUpdate() {
      //System.out.print("UPDATE/SKIP " + requestedValues + ",   " + newValues + ",   " + endKeys);
      //System.out.print(",   " + deletionValues);
      requestedValues.each(new ProcV<String>() {
        @Override
        public void apply(String key, String value) {
          String current = (String) annotations.getAnnotation(currentLocation, key);
          builder.startAnnotation(key, current, value);
        }
      });
      newValues.clear();

      deletionValues.clear();
      deletionValues.putAll(requestedValues);

      closeEndKeys();
      //System.out.print(" ---> " + requestedValues + ",   " + newValues + ",   " + endKeys);
      //System.out.println(",   " + deletionValues);
    }

    private void closeEndKeys() {
      endKeys.each(new Proc() {
        @Override
        public void apply(String key) {
          builder.endAnnotation(key);
        }
      });
      endKeys.clear();
    }


    public void elementStart(String tagName, Attributes attributes) {
      didSomethingOtherThanDeletionSinceAnnotationBoundary = true;
      beginInsert();

      doElementStart(tagName, attributes);

      builder.elementStart(tagName, attributes);
      sizeDiffSoFar++;
    }

    public void characters(String characters) {
      didSomethingOtherThanDeletionSinceAnnotationBoundary = true;
      beginInsert();
      doCharacters(characters);
      builder.characters(characters);
      sizeDiffSoFar += characters.length();
    }

    public void elementEnd() {
      didSomethingOtherThanDeletionSinceAnnotationBoundary = true;
      beginInsert();
      doElementEnd();
      builder.elementEnd();
      sizeDiffSoFar++;
    }

    public void updateAttributes(Map<String, String> attributes) {
      didSomethingOtherThanDeletionSinceAnnotationBoundary = true;

      beginUpdate();

      String[] triples = new String[attributes.size() * 3];

      E node = substrate.asElement(currentContainer.getValue());
      Map<String, String> oldAttributes = substrate.getAttributes(node);
      int i = 0;
      for (Map.Entry<String, String> attribute : attributes.entrySet()) {
        String name = attribute.getKey();
        String newValue = attribute.getValue();
        triples[i] = name;
        triples[i + 1] = oldAttributes.get(name);
        triples[i + 2] = newValue;
        if (newValue != null) {
          substrate.setAttribute(node, name, newValue);
        } else {
          substrate.removeAttribute(node, name);
        }
        i += 3;
      }

      currentLocation++;
      currentContainer = currentContainer.getNextContainer();
      currentParent = node;
      annotations.skip(1);

      AttributesUpdateImpl attrUpdate = new AttributesUpdateImpl(triples);
      builder.updateAttributes(attrUpdate);
      onModifyAttributes(currentParent, attrUpdate);
    }

    public void replaceAttributes(Attributes newAttrs) {
      didSomethingOtherThanDeletionSinceAnnotationBoundary = true;

      beginUpdate();

      E node = substrate.asElement(currentContainer.getValue());
      Attributes oldAttributes = new AttributesImpl(substrate.getAttributes(node));
      // Iterate over oldAttributes here, not attributeMap, since we are modifying
      // the map underlying the latter.
      for (Map.Entry<String, String> attribute : oldAttributes.entrySet()) {
        String key = attribute.getKey();
        if (!newAttrs.containsKey(key)) {
          substrate.removeAttribute(node, attribute.getKey());
        }
      }
      for (Map.Entry<String, String> attribute : newAttrs.entrySet()) {
        if (attribute.getValue() == null) {
          throw new OpCursorException("Null attribute value in setAttributes");
        }
        substrate.setAttribute(node, attribute.getKey(), attribute.getValue());
      }

      currentLocation++;
      currentContainer = currentContainer.getNextContainer();
      currentParent = node;
      annotations.skip(1);

      builder.replaceAttributes(oldAttributes, newAttrs);
      onModifyAttributes(currentParent, oldAttributes, newAttrs);
    }

    public void deleteElementStart() {
      E nodeToDelete = substrate.asElement(currentContainer.getValue());
      if (nodeToDelete == null) {
        throw new OpCursorException("No element to delete at the current location.");
      }
      String tagName = substrate.getTagName(nodeToDelete);
      Attributes attributes = new AttributesImpl(substrate.getAttributes(nodeToDelete));
      if (deletionDepth == 0) {
        substrate.removeChild(currentParent, nodeToDelete);
      }

      doSingleDelete(tagName, attributes);

      deleteCurrentContainer();

      deletionDepth++;
      sizeDiffSoFar--;

      onDeleteElementStart(currentLocation, nodeToDelete);
    }

    public void deleteElementEnd() {
      onDeleteElementEnd();

      doSingleDelete(null, null);

      deleteCurrentContainer();

      deletionDepth--;
      sizeDiffSoFar--;
    }

    private void doSingleDelete(String tagName, Attributes attrs) {
      List<AnnotationEvent> events = deleteAnnotations(1);

      boolean moved = false;
      int check = 0;

      for (AnnotationEvent ev : events) {
        int eventLocation = ev.index;
        if (eventLocation > currentLocation && !moved) {
          moved = true;
          buildDelete(tagName, attrs);
        }

        assert eventLocation == currentLocation + (moved ? 1 : 0)
            : currentLocation + " " + moved + " " + ev + " in " + events ;

        if (ev.getEndKey() != null) {
          assert moved;
          maybeRenewAnnotation(ev.getEndKey());
          builder.endAnnotation(ev.getEndKey());
          check--;
        } else {
          assert !moved;
          String changeKey = ev.getChangeKey();
          builder.startAnnotation(changeKey,
              ev.getChangeOldValue(), getLeftNeighbourAnnotation(changeKey));
          check++;
        }

      }
      assert check == 0;

      if (!moved) {
        buildDelete(tagName, attrs);
      }
    }

    private void buildDelete(String tagName, Attributes attrs) {
      if (tagName != null) {
        builder.deleteElementStart(tagName, attrs);
      } else {
        builder.deleteElementEnd();
      }
    }

    public void deleteCharacters(int deletionSize) {
      List<AnnotationEvent> events = deleteAnnotations(deletionSize);

      String oldChars = doDeleteCharacters(deletionSize);

      int finalLocation = currentLocation;

      int index = 0;
      int newIndex = -1;
      StringMap<String> check = CollectionUtils.createStringMap();

      for (AnnotationEvent ev : events) {
        int eventLocation = ev.index;
        newIndex = eventLocation - currentLocation;
        if (newIndex > index) {
          builder.deleteCharacters(oldChars.substring(index, newIndex));
          index = newIndex;
        }

        if (ev.getEndKey() != null) {
          maybeRenewAnnotation(ev.getEndKey());
          builder.endAnnotation(ev.getEndKey());

          assert check.containsKey(ev.getEndKey()) : "key: "  + ev.getEndKey() + ", " + check;
          check.remove(ev.getEndKey());
        } else {
          String changeKey = ev.getChangeKey();
          builder.startAnnotation(changeKey,
              ev.getChangeOldValue(), getLeftNeighbourAnnotation(changeKey));

          check.put(ev.getChangeKey(), ev.getChangeOldValue());
        }
      }

      assert check.isEmpty();

      if (index < deletionSize) {
        builder.deleteCharacters(oldChars.substring(index, deletionSize));
      }

      sizeDiffSoFar -= deletionSize;

      onDeleteCharacters(currentLocation, oldChars);
    }

    /**
     * @return annotation events for the deletion
     */
    // HACK(danilatos): This code is inefficient and does not belong in indexed document.
    private List<AnnotationEvent> deleteAnnotations(int size) {
      final List<AnnotationEvent> events = new ArrayList<AnnotationEvent>();

      final StringSet open = CollectionUtils.createStringSet();
      final StringMap<String> deletionInherit = CollectionUtils.createStringMap();
      deletionValues.each(new ReadableStringMap.ProcV<String>() {
        @Override
        public void apply(String key, String value) {
          deletionInherit.put(key, value);
        }
      });

      final int start = currentLocation;
      final int end = currentLocation + size;

      for (AnnotationInterval<String> i : annotationIntervals(start, end, null)) {
        assert i.end() > start;
        assert i.start() < end;
        final int realStart = Math.max(start, i.start());
        deletionInherit.each(new ReadableStringMap.ProcV<String>() {
          @Override
          public void apply(String key, String value) {
            events.add(
                new AnnotationStartEvent(realStart, key, getAnnotation(realStart, key)));
            open.add(key);
          }
        });
        i.annotations().each(new ReadableStringMap.ProcV<String>() {
          @Override
          public void apply(String key, String value) {
            if (!deletionInherit.containsKey(key)) {
              events.add(
                  new AnnotationStartEvent(realStart, key, value));
              open.add(key);
            }
          }
        });
      }

      // Produce endAnnotation calls for every annotation at the end.
      open.each(new StringSet.Proc() {
        @Override
        public void apply(String key) {
          events.add(new AnnotationEndEvent(end, key));
        }
      });

      annotations.delete(size);
      return events;
    }

    private void maybeRenewAnnotation(String key) {
      if (!newValues.containsKey(key) && requestedValues.containsKey(key)) {
        newValues.put(key, requestedValues.get(key));
      }
    }

    private String getLeftNeighbourAnnotation(String key) {
      if (deletionValues.containsKey(key)) {
        return deletionValues.get(key);
      }
      return currentLocation != 0
          ? (String) annotations.getAnnotation(currentLocation - 1, key) : null;
    }
  }

  private void checkRetain(int count) {
    if (currentLocation + count > size()) {
      throw new OpCursorException("Retain past end of document [location:" +
          (currentLocation + count) + "] [doc size:" + size() + "]");
    }
  }

  private void doElementStart(String tagName, Attributes attributes) {
    splitCurrent();
    E newElement = substrate.createElement(tagName, attributes,
        currentParent, currentContainer.getValue());
    insertBefore(currentContainer, newElement, 1);

    currentContainer.insertBefore(null, 1);
    annotations.insert(1);

    currentContainer = currentContainer.getPreviousContainer();
    currentParent = newElement;
    ++currentLocation;

    onElementStart(newElement);
  }

  private void doCharacters(String characters) {
    annotations.insert(characters.length());

    OffsetList.Container<N> previousNode = null;
    T previousTextNode = null;

    if (currentOffset == 0) {
      previousNode = currentContainer.getPreviousContainer();
      previousTextNode = substrate.asText(previousNode.getValue());
    }

    // Prefer appending data to previous text node
    if (previousTextNode != null) {
      substrate.appendData(previousTextNode, characters);
      previousNode.increaseSize(characters.length());
    } else {
      N domNode = currentContainer.getValue();
      T textNode = substrate.asText(domNode);

      if (textNode != null) {
        substrate.insertData(textNode, currentOffset, characters);
        currentContainer.increaseSize(characters.length());
        currentOffset += characters.length();
      } else {
        T newTextNode = substrate.createTextNode(characters, currentParent,
            currentContainer.getValue());
        insertBefore(currentContainer, newTextNode, characters.length());
      }
    }

    onCharacters(currentLocation, characters);

    currentLocation += characters.length();
  }

  private String doDeleteCharacters(int deletionSize) {
   StringBuilder textBuilder = new StringBuilder();
    if (deletionDepth <= 0) {
      T textNode = substrate.asText(currentContainer.getValue());
      int currentLength = substrate.getLength(textNode);
      if (currentOffset + deletionSize < currentLength) {
        textBuilder.append(
            substrate.getData(textNode).substring(currentOffset, currentOffset + deletionSize));
        substrate.deleteData(textNode, currentOffset, deletionSize);
        currentContainer.increaseSize(-deletionSize);
      } else {
        if (currentOffset > 0) {
          int amountToDelete = currentLength - currentOffset;
          textBuilder.append(substrate.getData(textNode).substring(currentOffset));
          substrate.deleteData(textNode, currentOffset, amountToDelete);
          currentContainer.increaseSize(-amountToDelete);
          deletionSize -= amountToDelete;
          currentContainer = currentContainer.getNextContainer();
          currentOffset = 0;
        }
        while (deletionSize > 0) {
          textNode = substrate.asText(currentContainer.getValue());
          currentLength = substrate.getLength(textNode);
          if (currentLength <= deletionSize) {
            textBuilder.append(substrate.getData(textNode));
            substrate.removeChild(currentParent, textNode);
            deleteCurrentContainer();
            deletionSize -= currentLength;
          } else {
            textBuilder.append(substrate.getData(textNode).substring(0, deletionSize));
            substrate.deleteData(textNode, 0, deletionSize);
            currentContainer.increaseSize(-deletionSize);
            break;
          }
        }
      }
    } else {
      while (deletionSize > 0) {
        T textNode = substrate.asText(currentContainer.getValue());
        assert textNode != null;
        if (deletionSize < substrate.getLength(textNode)) {
          // TODO(user): See if we can get rid of this cruft.
          textBuilder.append(substrate.getData(textNode).substring(0, deletionSize));
          substrate.deleteData(textNode, 0, deletionSize);
          currentContainer.increaseSize(-deletionSize);
          break;
        } else {
          textBuilder.append(substrate.getData(textNode));
          deletionSize -= currentContainer.size();
          deleteCurrentContainer();
        }
      }
    }

    return textBuilder.toString();
  }

  private void doElementEnd() {
    onElementEnd();
    annotations.insert(1);

    ++currentLocation;
    currentContainer = currentContainer.getNextContainer();
    currentParent = substrate.getParentElement(currentParent);
  }


  private void doStartAnnotation(String key, String value) {
    if (Annotations.isLocal(key)) {
      throw new IllegalArgumentException("Cannot access local annotations");
    }
    annotations.startAnnotation(key, value);
  }

  private void doEndAnnotation(String key) {
    if (Annotations.isLocal(key)) {
      throw new IllegalArgumentException("Cannot access local annotations");
    }
    annotations.endAnnotation(key);
  }

  private void resetLocation() {
    currentLocation = 0;
    currentOffset = 0;
    currentParent = substrate.getDocumentElement();
    currentContainer = offsetList.firstContainer();
  }

  /**
   * Deletes the current container.
   */
  private void deleteCurrentContainer() {
    OffsetList.Container<N> nextContainer = currentContainer.getNextContainer();
    currentContainer.remove();
    currentContainer = nextContainer;
  }

  /**
   * Gets the parent of the DOM node associated with the given container.
   *
   * TODO(user): Consider some efficiency improvements.
   *
   * @param container The container.
   * @return The parent of the DOM node associated with the given container.
   */
  private E getParentOf(OffsetList.Container<N> container) {
    N domNode = container.getValue();
    if (domNode != null) {
      return substrate.getParentElement(domNode);
    }
    int counter = 0;
    container = container.getPreviousContainer();
    while (container.getValue() == null) {
      ++counter;
      container = container.getPreviousContainer();
    }
    domNode = container.getValue();
    E parent = substrate.asElement(domNode);
    if (parent == null) {
      parent = substrate.getParentElement(domNode);
    }
    for (int i = 0; i < counter; ++i) {
      parent = substrate.getParentElement(parent);
    }
    return parent;
  }

  /**
   * Splits the current container at the current offset.
   */
  private void splitCurrent() {
    if (currentOffset != 0) {
      T domNode = substrate.asText(currentContainer.getValue());
      T newDomNode = substrate.splitText(domNode, currentOffset);
      assert newDomNode != null;
      currentContainer = currentContainer.split(currentOffset, newDomNode);
      substrate.setIndexingContainer(newDomNode, currentContainer);
      currentOffset = 0;
    }
  }

  /**
   * Inserts a DOM node into the indexing structure.
   *
   * @param container The container before which to insert.
   * @param domNode The DOM node to insert.
   * @param size The size of the inserted node.
   */
  private void insertBefore(OffsetList.Container<N> container, N domNode, int size) {
    substrate.setIndexingContainer(domNode, container.insertBefore(domNode, size));
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public String toString() {
    return "IndexedDI@" + Integer.toHexString(System.identityHashCode(this))
        + "[" + toDebugString() + "]";
  }


  @Override
  public String toDebugString() {
    try {
      return DocOpUtil.toXmlString(DocOpScrub.maybeScrub(toInitialization()));
    } catch (RuntimeException e) {
      if (!DocOpScrub.shouldScrubByDefault()) {
        try {
          return "#<NO ANNOTATIONS>: " + XmlStringBuilder.innerXml(this) + "# (" + e + ")";
        } catch (RuntimeException e2) {
          return "#<!SUPER BROKEN># (" + e + ")";
        }
      } else {
        return "#<!BROKEN># (" + e + ")";
      }
    }
  }

  /**
   * {@inheritDoc}
   */
  public String getData(T textNode) {
    return substrate.getData(textNode);
  }

  /**
   * {@inheritDoc}
   */
  public N getFirstChild(N node) {
    return substrate.getFirstChild(node);
  }

  /**
   * {@inheritDoc}
   */
  public N getLastChild(N node) {
    return substrate.getLastChild(node);
  }

  /**
   * {@inheritDoc}
   */
  public int getLength(T textNode) {
    return substrate.getLength(textNode);
  }

  /**
   * {@inheritDoc}
   */
  public N getNextSibling(N node) {
    return substrate.getNextSibling(node);
  }

  /**
   * {@inheritDoc}
   */
  public short getNodeType(N node) {
    return substrate.getNodeType(node);
  }

  /**
   * {@inheritDoc}
   */
  public E getParentElement(N node) {
    return substrate.getParentElement(node);
  }

  /**
   * {@inheritDoc}
   */
  public N getPreviousSibling(N node) {
    return substrate.getPreviousSibling(node);
  }

  /**
   * {@inheritDoc}
   */
  public E getDocumentElement() {
    return substrate.getDocumentElement();
  }

  /**
   * {@inheritDoc}
   */
  public E asElement(N node) {
    return substrate.asElement(node);
  }

  /**
   * {@inheritDoc}
   */
  public T asText(N node) {
    return substrate.asText(node);
  }

  /**
   * {@inheritDoc}
   */
  public boolean isSameNode(N node, N other) {
    return substrate.isSameNode(node, other);
  }

  /**
   * {@inheritDoc}
   */
  public Map<String,String> getAttributes(E element) {
    return substrate.getAttributes(element);
  }

  /**
   * {@inheritDoc}
   */
  public String getTagName(E element) {
    return substrate.getTagName(element);
  }

  /**
   * {@inheritDoc}
   */
  public String getAttribute(E element, String name) {
    return substrate.getAttribute(element, name);
  }

  @Override
  public int size() {
    checkSizeConsistency("size");
    return offsetList.size();
  }

  private void checkSizeConsistency(String msgPrefix) {
    // TODO(danilatos/ohler): Make this an assert once we are more confident
    if (annotations != null && offsetList != null && offsetList.size() != annotations.size()) {
      throw new RuntimeException(msgPrefix +
          ": Document and annotations have inconsistent size: " +
          offsetList.size() + " vs " + annotations.size() + ", respectively");
    }
  }

  /**
   * {@inheritDoc}
   */
  public String getAnnotation(int start, String key) {
    if (Annotations.isLocal(key)) {
      throw new IllegalArgumentException("Cannot access local annotations");
    }
    return (String) annotations.getAnnotation(start, key);
  }

  /**
   * {@inheritDoc}
   */
  public int firstAnnotationChange(int start, int end, String key, String fromValue) {
    if (Annotations.isLocal(key)) {
      throw new IllegalArgumentException("Cannot access local annotations");
    }
    return annotations.firstAnnotationChange(start, end, key, fromValue);
  }

  /**
   * {@inheritDoc}
   */
  public int lastAnnotationChange(int start, int end, String key, String fromValue) {
    if (Annotations.isLocal(key)) {
      throw new IllegalArgumentException("Cannot access local annotations");
    }
    return annotations.lastAnnotationChange(start, end, key, fromValue);
  }

  void checkValidPersistentKeys(ReadableStringSet keys) {
    keys.each(new Proc() {
      @Override
      public void apply(String key) {
        Annotations.checkPersistentKey(key);
      }
    });
  }

  /**
   * {@inheritDoc}
   */
  public T splitText(T textNode, int offset) {
    if (inconsistent) {
      throw new IllegalStateException("Cannot splitText() during a modification");
    }

    if (offset == 0) {
      return textNode;
    } else if (offset >= substrate.getLength(textNode)) {
      return null;
    }

    currentLocation = getLocation(textNode) + offset;
    offsetList.performActionAt(currentLocation, locationUpdater);
    splitCurrent();
    return asText(currentContainer.getValue());
  }

  /**
   * {@inheritDoc}
   */
  public T mergeText(T secondSibling) {
    if (inconsistent) {
      throw new IllegalStateException("Cannot mergeText() during a modification");
    }

    currentLocation = getLocation(secondSibling);
    offsetList.performActionAt(currentLocation, locationUpdater);
    T mergedNode = substrate.mergeText(secondSibling);

    if (mergedNode != null) {
      OffsetList.Container<N> previous = currentContainer.getPreviousContainer();
      currentContainer.increaseSize(previous.size());
      previous.remove();
      substrate.setIndexingContainer(mergedNode, currentContainer);
    }

    return mergedNode;
  }

  @Override
  public DocInitialization asOperation() {

    if (size() == 0) {
      return EmptyDocument.EMPTY_DOCUMENT;
    }

    DocOp domOp = serializeDom();

    DocOp annotationsOp = serializeAnnotations();

    try {
      final DocOp bothOps;
      if (performValidation) {
        bothOps = Composer.compose(domOp, annotationsOp);
      } else {
        bothOps = Composer.composeUnchecked(domOp, annotationsOp);
      }
      DocInitialization initialisation = DocOpUtil.asInitialization(bothOps);
      assert DocOpValidator.validate(null, schemaConstraints, initialisation).isValid();

      return initialisation;
    } catch (OperationException e) {
      throw new OperationRuntimeException("Bug either in indexed document or the composer", e);
    }
  }

  @Override
  public DocInitialization toInitialization() {
    return asOperation();
  }

  private DocOp serializeDom() {
    DocOpBuilder b = new DocOpBuilder();
    int depth = 0;
    for (N node : offsetList) {
      if (node != null) {
        T textNode = substrate.asText(node);
        if (textNode != null) {
          b.characters(substrate.getData(textNode));
        } else {
          E elementNode = substrate.asElement(node);
          b.elementStart(substrate.getTagName(elementNode),
              new AttributesImpl(substrate.getAttributes(elementNode)));
          depth++;
        }
      } else {
        // To avoid the sentinel
        depth--;
        if (depth >= 0) {
          b.elementEnd();
        }
      }
    }

    DocOp domOp = b.buildUnchecked();
    assert DocOpValidator.isWellFormed(null, domOp);
    return domOp;
  }

  private DocOp serializeAnnotations() {
    final AnnotationsNormalizer<DocOp> b =
        new AnnotationsNormalizer<DocOp>(new UncheckedDocOpBuffer());

    AnnotationInterval<Object> last = null;
    for (AnnotationInterval<Object> i : annotations.annotationIntervals(0, size(), knownKeys())) {
      i.diffFromLeft().each(new ProcV<Object>() {
        @Override
        public void apply(String key, Object value) {
          assert value == null || value instanceof String;
          if (value != null) {
            b.startAnnotation(key, null, (String) value);
          } else {
            b.endAnnotation(key);
          }
        }
      });
      b.retain(i.length());
      last = i;
    }
    if (size() > 0) {
      last.annotations().each(new ProcV<Object>() {
          @Override
          public void apply(String key, Object value) {
  //          assert value != null;
            b.endAnnotation(key);
          }
        });
    }
    DocOp annotationsOp = b.finish();
    assert DocOpValidator.isWellFormed(null, annotationsOp);
    return annotationsOp;
  }

  @Override
  public StringSet knownKeys() {
    final StringSet knownKeys = CollectionUtils.createStringSet();
    annotations.knownKeysLive().each(new Proc() {
      @Override
      public void apply(String key) {
        if (!Annotations.isLocal(key)) {
          knownKeys.add(key);
        }
      }
    });
    return knownKeys;
  }

  /**
   * Evaluate the document using the associative operator.
   *
   * @return The result of the evaluation
   */
  protected V evaluate() {
    return offsetList.evaluate();
  }

  /**
   * @return the "currentParent" element to which children are being added
   */
  private E getCurrentParent() {
    return currentParent;
  }

  /**
   * @return the current int location
   */
  private int getCurrentLocation() {
    return currentLocation;
  }

  /**
   * @return the "currentNode" we are in or before
   */
  protected N getCurrentNode() {
    return currentContainer.getValue();
  }

  @Override
  public void forEachAnnotationAt(int location,
      final ReadableStringMap.ProcV<String> callback) {
    annotations.forEachAnnotationAt(location, new ReadableStringMap.ProcV<Object>() {
      @Override
      public void apply(String key, Object value) {
        if (!Annotations.isLocal(key)) {
          assert value == null || value instanceof String;
          callback.apply(key, (String) value);
        }
      }
    });
  }

  /**
   * {@inheritDoc}
   */
  public AnnotationCursor annotationCursor(int start, int end, ReadableStringSet keys) {
    if (keys == null) {
      keys = knownKeys();
    } else {
      checkValidPersistentKeys(keys);
    }
    return annotations.annotationCursor(start, end, keys);
  }

  @Override
  public Iterable<AnnotationInterval<String>> annotationIntervals(int start, int end,
      ReadableStringSet keys) {

    if (keys == null) {
      keys = knownKeys();
    } else {
      checkValidPersistentKeys(keys);
    }

    final Iterable<AnnotationInterval<Object>> iterable =
        annotations.annotationIntervals(start, end, keys);
    return new Iterable<AnnotationInterval<String>>() {
      @Override
      public Iterator<AnnotationInterval<String>> iterator() {
        final Iterator<AnnotationInterval<Object>> iterator = iterable.iterator();
        return new Iterator<AnnotationInterval<String>>() {
          @Override
          public boolean hasNext() {
            return iterator.hasNext();
          }

          // SuppressWarnings because of conversion from RSMap<Object> to <String>, already checked
          // keys are persistent keys using checkValidPersistentKeys(), and this class has
          // an invariant that such keys only have string values. (Local annotation set views
          // disallow setting any values for non-local keys).
          @SuppressWarnings("unchecked")
          @Override
          public AnnotationInterval<String> next() {
            AnnotationInterval<Object> rawInterval = iterator.next();
            int start = rawInterval.start();
            int end = rawInterval.end();
            ReadableStringMap annotations = rawInterval.annotations();
            ReadableStringMap diffFromLeft = rawInterval.diffFromLeft();
            return new AnnotationIntervalImpl<String>(start, end, annotations, diffFromLeft);
          }

          @Override
          public void remove() {
            iterator.remove();
          }
        };
      }
    };
  }

  @Override
  public Iterable<RangedAnnotation<String>> rangedAnnotations(int start, int end,
      ReadableStringSet keys) {
    if (keys == null) {
      keys = knownKeys();
    } else {
      checkValidPersistentKeys(keys);
    }
    final Iterable<RangedAnnotation<Object>> iterable =
        annotations.rangedAnnotations(start, end, keys);
    return new Iterable<RangedAnnotation<String>>() {
      @Override
      public Iterator<RangedAnnotation<String>> iterator() {
        final Iterator<RangedAnnotation<Object>> iterator = iterable.iterator();
        return new Iterator<RangedAnnotation<String>>() {
          @Override
          public boolean hasNext() {
            return iterator.hasNext();
          }

          @Override
          public RangedAnnotation<String> next() {
            RangedAnnotation<Object> rawRange = iterator.next();
            String key = rawRange.key();
            // Safe cast because already checked with checkValidPersistentKeys();
            String value = (String) rawRange.value();
            int start = rawRange.start();
            int end = rawRange.end();
            return new RangedAnnotationImpl<String>(key, value, start, end);
          }

          @Override
          public void remove() {
            iterator.remove();
          }
        };
      }
    };
  }

  @Override
  public String toXmlString() {
    return DocOpUtil.toXmlString(asOperation());
  }

  protected void beforeBegin() {
  }

  protected void afterFinish() {
  }

  protected void onElementStart(E element) {
  }

  protected void onElementEnd() {
  }

  protected void onDeleteElementStart(int location, E element) {
  }

  protected void onDeleteElementEnd() {
  }

  protected void onModifyAttributes(E element, Attributes oldAttributes, Attributes newAttributes) {
  }

  protected void onModifyAttributes(E element, AttributesUpdate update) {
  }

  protected void onCharacters(int location, String characters) {
  }

  protected void onDeleteCharacters(int location, String characters) {
  }

  public DocumentSchema getSchema() {
    return schemaConstraints;
  }
}
TOP

Related Classes of org.waveprotocol.wave.model.document.indexed.IndexedDocumentImpl$InvertibleCursor

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.
script>