Package org.waveprotocol.wave.model.document.operation.automaton

Source Code of org.waveprotocol.wave.model.document.operation.automaton.DocOpAutomaton$InsertStart

/**
* Copyright 2009 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*      http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.waveprotocol.wave.model.document.operation.automaton;

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.automaton.DocumentSchema.PermittedCharacters;
import org.waveprotocol.wave.model.document.operation.impl.AnnotationMap;
import org.waveprotocol.wave.model.document.operation.impl.AnnotationMapImpl;
import org.waveprotocol.wave.model.document.operation.impl.AnnotationsUpdateImpl;
import org.waveprotocol.wave.model.util.Preconditions;
import org.waveprotocol.wave.model.util.Utf16Util;

import java.io.PrintStream;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
* A state machine that can be used to accept or generate valid or invalid document
* operations.
*
* The basic usage model is as follows: An automaton is parameterized by a
* document and a set of constraints based on an XML schema, and will
* accept/generate all valid operations for that document and those constraints.
*
* Every possible operation component (such as "elementStart(...)") corresponds
* to a potential transition of the automaton.  The checkXXX methods
* (such as checkElementStart(...)) determine whether a given transition exists
* and is valid, or whether it is invalid, or ill-formed.  The doXXX methods
* will perform the transition.  Ill-formed transitions must not be performed.
* Invalid transitions are permitted, but after performing an invalid transition,
* the validity of the operation components that follow is not well-defined.
*
* The checkFinish() method determines whether ending an operation is acceptable
* (or whether any opening components are missing the corresponding closing
* component or similar).
*
* The checkXXX methods accept a ViolationCollector object where they will record
* details about any violations.  If a proposed transition is invalid for more
* than one reason, the checkXXX method may detect only one (or any subset) of
* the reasons and record only those violations.  The ViolationCollector parameter
* may also be null, in which case details about the violations will not be
* recorded.
*
* To validate an operation, the automaton needs to be driven according to
* the operation components in that operation.  DocOpValidator does
* this.
*
* To generate a random operation, the automaton needs to be driven based on
* a random document operation component generator.  RandomDocOpGenerator does
* this.
*/
// TODO: size limits
public final class DocOpAutomaton {

  /**
   * Set this to true when debugging the random generator.
   */
  // Perhaps we should merge each checkXXX() and doXXX() method pair
  // into one XXX method that does the check but then unconditionally
  // performs the transition (or destroys the automaton if the step
  // was ill-formed) and returns the check result.  This would make
  // the random generator less efficient (because it would have to
  // clone all the time), but simplify all other uses.
  private static final boolean EXPENSIVE_ASSERTIONS = false;

  /**
   * The overall result of validating an operation.
   */
  public enum ValidationResult {
    // These need to be ordered most severe to least severe due to the way
    // we implement mergeWith().

    /**
     * The operation is meaningless. One ore more components of this operation
     * have fields with illegal values and/or the sequence of components does
     * not have proper nesting, or is in some way illegal. The result of
     * applying such an operation to any document is undefined.
     */
    ILL_FORMED,

    /**
     * The operation is well-formed, but it does not match the document state it
     * was being checked against. It would be meaningless to attempt to apply
     * the operation to the state being checked against.
     */
    INVALID_DOCUMENT,

    /**
     * The operation is well formed, and if applied to the document being
     * checked against, would have a well defined and well formed result.
     * However, applying it would mean the resulting document would not conform
     * to the document schema being checked against.
     */
    INVALID_SCHEMA,

    /**
     * The operation is valid in every way with respect to the document and
     * schema being checked against.
     */
    VALID;

    /** @see #ILL_FORMED */
    public boolean isIllFormed() {
      return this == ILL_FORMED;
    }
    /** @see #INVALID_DOCUMENT */
    public boolean isInvalidDocument() {
      return this == INVALID_DOCUMENT;
    }
    /** @see #INVALID_SCHEMA */
    public boolean isInvalidSchema() {
      return this == INVALID_SCHEMA;
    }
    /** @see #VALID */
    public boolean isValid() {
      return this == VALID;
    }

    public ValidationResult mergeWith(ValidationResult other) {
      Preconditions.checkNotNull(other, "Null ValidationResult");
      return ValidationResult.values()[Math.min(this.ordinal(), other.ordinal())];
    }
  }

  /**
   * An object containing information about one individual reason why an
   * operation is not valid, e.g. "retain past end" or "deletion inside insertion".
   */
  public abstract static class Violation {
    private final String description;
    private final int originalDocumentPos;
    private final int resultingDocumentPos;
    Violation(String description, int originalPos, int resultingPos) {
      this.description = description;
      this.originalDocumentPos = originalPos;
      this.resultingDocumentPos = resultingPos;
    }
    public abstract ValidationResult validationResult();
    /**
     * @return a developer-readable description of the violation
     */
    public String description() {
      return description + " at original document position " + originalDocumentPos
          + " / resulting document position " + resultingDocumentPos;
    }
  }

  /**
   * An object containing information about the way in which an operation is
   * ill-formed.
   */
  public static final class OperationIllFormed extends Violation {
    public OperationIllFormed(String description, int originalPos, int resultingPos) {
      super(description, originalPos, resultingPos);
    }
    @Override
    public ValidationResult validationResult() { return ValidationResult.ILL_FORMED; }
  }

  /**
   * An object containing information about how an operation is invalid
   * for a reason that does not depend on XML schema constraints.
   */
  public static final class OperationInvalid extends Violation {
    public OperationInvalid(String description, int originalPos, int resultingPos) {
      super(description, originalPos, resultingPos);
    }
    @Override
    public ValidationResult validationResult() { return ValidationResult.INVALID_DOCUMENT; }
  }

  /**
   * An object containing information about how an operation violates XML
   * schema constraints.
   */
  public static final class SchemaViolation extends Violation {
    public SchemaViolation(String description, int originalPos, int resultingPos) {
      super(description, originalPos, resultingPos);
    }
    @Override
    public ValidationResult validationResult() { return ValidationResult.INVALID_SCHEMA; }
  }

  /**
   * A class to hold a set of violations.  The checkXXX methods take this
   * as an input parameter and add violations to it if there are any.
   */
  // I'm not particularly proud of the design of this class.
  public static final class ViolationCollector {
    private final List<OperationIllFormed> operationIllFormed = new ArrayList<OperationIllFormed>();
    private final List<OperationInvalid> operationInvalid = new ArrayList<OperationInvalid>();
    private final List<SchemaViolation> schemaViolations = new ArrayList<SchemaViolation>();
    public void add(OperationIllFormed v) {
      operationIllFormed.add(v);
    }
    public void add(OperationInvalid v) {
      operationInvalid.add(v);
    }
    public void add(SchemaViolation v) {
      schemaViolations.add(v);
    }
    /** True iff at least one violation of the well-formedness constraints was detected. */
    public boolean isIllFormed() {
      return getValidationResult().isIllFormed();
    }
    /**
     * True iff the most severe validation constraint detected was
     * {@link ValidationResult#INVALID_DOCUMENT}
     */
    public boolean isInvalidDocument() {
      return getValidationResult().isInvalidDocument();
    }
    /**
     * True iff the most severe validation constraint detected was
     * {@link ValidationResult#INVALID_SCHEMA}
     */
    public boolean isInvalidSchema() {
      return getValidationResult().isInvalidSchema();
    }
    /** True iff there were no violations. */
    public boolean isValid() {
      return getValidationResult().isValid();
    }
    /** The merged (most severe) validation result */
    public ValidationResult getValidationResult() {
      if (!operationIllFormed.isEmpty()) {
        return ValidationResult.ILL_FORMED;
      } else if (!operationInvalid.isEmpty()) {
        return ValidationResult.INVALID_DOCUMENT;
      } else if (!schemaViolations.isEmpty()) {
        return ValidationResult.INVALID_SCHEMA;
      } else {
        return ValidationResult.VALID;
      }
    }

    /** Returns a description of a single violation, or null if there are none. */
    public String firstDescription() {
      for (OperationIllFormed v : operationIllFormed) {
        return "ill-formed: " + v.description();
      }
      for (OperationInvalid v : operationInvalid) {
        return "invalid operation: " + v.description();
      }
      for (SchemaViolation v : schemaViolations) {
        return "schema violation: " + v.description();
      }
      return null;
    }

    /** Prints descriptions of violations that have been detected. */
    public void printDescriptions(PrintStream out) {
      printDescriptions(out, "");
    }

    /**
     * Prints descriptions of violations that have been detected, prefixing
     * each line of output with the given prefix.
     */
    public void printDescriptions(PrintStream out, String prefix) {
      if (isValid()) {
        out.println(prefix + "no violations");
        return;
      }
      for (OperationIllFormed v : operationIllFormed) {
        out.println(prefix + "ill-formed: " + v.description());
      }
      for (OperationInvalid v : operationInvalid) {
        out.println(prefix + "invalid operation: " + v.description());
      }
      for (SchemaViolation v : schemaViolations) {
        out.println(prefix + "schema violation: " + v.description());
      }
    }

    private int size() {
      return operationIllFormed.size() + operationInvalid.size() + schemaViolations.size();
    }

    @Override
    public String toString() {
      if (size() == 0) {
        return "ViolationCollector[0]";
      }
      StringBuilder b = new StringBuilder();
      b.append("ViolationCollector[" +
          size() + ": " + firstDescription() + "]");
      return b.toString();
    }
  }


  private ValidationResult addViolation(ViolationCollector a, OperationIllFormed v) {
    if (a != null) {
      a.add(v);
    }
    return v.validationResult();
  }

  private ValidationResult addViolation(ViolationCollector a, OperationInvalid v) {
    if (a != null) {
      a.add(v);
    }
    return v.validationResult();
  }

  private ValidationResult addViolation(ViolationCollector a, SchemaViolation v) {
    if (a != null) {
      a.add(v);
    }
    return v.validationResult();
  }

  private OperationIllFormed illFormedOperation(String description) {
    return new OperationIllFormed(description, effectivePos, resultingPos);
  }

  private OperationInvalid invalidOperation(String description) {
    return new OperationInvalid(description, effectivePos, resultingPos);
  }

  private SchemaViolation schemaViolation(String description) {
    return new SchemaViolation(description, effectivePos, resultingPos);
  }

  private ValidationResult valid() {
    return ValidationResult.VALID;
  }

  private ValidationResult mismatchedInsertStart(ViolationCollector v) {
    return addViolation(v, illFormedOperation("elementStart with no matching elementEnd"));
  }

  private ValidationResult mismatchedDeleteStart(ViolationCollector v) {
    return addViolation(v, illFormedOperation(
        "deleteElementStart with no matching deleteElementEnd"));
  }

  private ValidationResult mismatchedInsertEnd(ViolationCollector v) {
    return addViolation(v, illFormedOperation("elementEnd with no matching elementStart"));
  }

  private ValidationResult mismatchedDeleteEnd(ViolationCollector v) {
    return addViolation(v, illFormedOperation(
        "deleteElementEnd with no matching deleteElementStart"));
  }

  private ValidationResult mismatchedStartAnnotation(ViolationCollector v, String key) {
    return addViolation(v, illFormedOperation("annotation of key " + key
        + " starts but never ends"));
  }

  private ValidationResult mismatchedEndAnnotation(ViolationCollector v, String key) {
    return addViolation(v, illFormedOperation("annotation of key " + key
        + " ends without having started"));
  }

  private ValidationResult retainItemCountNotPositive(ViolationCollector v) {
    return addViolation(v, illFormedOperation("retain item count not positive"));
  }

  private ValidationResult retainInsideInsertOrDelete(ViolationCollector v) {
    return addViolation(v, illFormedOperation("retain inside insert or delete"));
  }

  private ValidationResult attributeChangeInsideInsertOrDelete(ViolationCollector v) {
    return addViolation(v, illFormedOperation("attribute change inside insert or delete"));
  }

  private ValidationResult retainPastEnd(ViolationCollector v, int expectedLength,
      int retainItemCount) {
    return addViolation(v, invalidOperation("retain past end of document, document length "
        + expectedLength + ", retain item count " + retainItemCount));
  }

  private ValidationResult missingRetainToEnd(ViolationCollector v,
      int expectedLength, int actualLength) {
    return addViolation(v, invalidOperation("operation shorter than document, document length "
        + expectedLength + ", length of input of operation " + actualLength));
  }

  private ValidationResult nullCharacters(ViolationCollector v) {
    return addViolation(v, illFormedOperation("characters is null"));
  }

  private ValidationResult emptyCharacters(ViolationCollector v) {
    return addViolation(v, illFormedOperation("characters is empty"));
  }

  private ValidationResult insertInsideDelete(ViolationCollector v) {
    return addViolation(v, illFormedOperation("insertion inside deletion"));
  }

  private ValidationResult deleteInsideInsert(ViolationCollector v) {
    return addViolation(v, illFormedOperation("deletion inside insertion"));
  }

  private ValidationResult nullTag(ViolationCollector v) {
    return addViolation(v, illFormedOperation("element type is null"));
  }

  private ValidationResult nullAttributes(ViolationCollector v) {
    return addViolation(v, illFormedOperation("attributes is null"));
  }

  private ValidationResult nullAttributeKey(ViolationCollector v) {
    return addViolation(v, illFormedOperation("attribute key is null"));
  }

  private ValidationResult nullAttributeValue(ViolationCollector v) {
    return addViolation(v, illFormedOperation("attribute value is null"));
  }

  private ValidationResult nullAttributesUpdate(ViolationCollector v) {
    return addViolation(v, illFormedOperation("attributes update is null"));
  }

  private ValidationResult attributeKeysNotStrictlyMonotonic(ViolationCollector v,
      String key1, String key2) {
    return addViolation(v, illFormedOperation("attribute keys not strictly monotonic: "
        + key1 + " >= " + key2));
  }

  private ValidationResult annotationKeysNotStrictlyMonotonic(ViolationCollector v,
      String key1, String key2) {
    return addViolation(v, illFormedOperation("annotation keys not strictly monotonic: "
        + key1 + " >= " + key2));
  }

  private ValidationResult nullAnnotationKey(ViolationCollector v) {
    return addViolation(v, illFormedOperation("annotation key is null"));
  }

  private ValidationResult invalidCharacterInAnnotationKey(ViolationCollector v, String key) {
    return addViolation(v, illFormedOperation("invalid character in annotation key: " + key));
  }

  private ValidationResult annotationKeyNotValidUtf16(ViolationCollector v) {
    return addViolation(v, illFormedOperation("annotation key is not valid UTF-16"));
  }

  private ValidationResult annotationValueNotValidUtf16(ViolationCollector v) {
    return addViolation(v, illFormedOperation("annotation value is not valid UTF-16"));
  }

  private ValidationResult charactersContainsSurrogate(ViolationCollector v) {
    return addViolation(v, illFormedOperation("characters component contains surrogate"));
  }

  private ValidationResult deleteCharactersContainsSurrogate(ViolationCollector v) {
    return addViolation(v, illFormedOperation("delete characters component contains surrogate"));
  }

  private ValidationResult charactersInvalidUnicode(ViolationCollector v) {
    return addViolation(v, illFormedOperation("characters component contains invalid unicode"));
  }

  private ValidationResult deleteCharactersInvalidUnicode(ViolationCollector v) {
    return addViolation(v, illFormedOperation("delete characters component contains invalid unicode"));
  }

  private ValidationResult attributeNameNotXmlName(ViolationCollector v, String name) {
    return addViolation(v, illFormedOperation("attribute name is not an XML Name: \""
        + name + "\""));
  }

  private ValidationResult attributeValueNotValidUtf16(ViolationCollector v) {
    return addViolation(v, illFormedOperation("attribute value is not valid UTF-16"));
  }

  private ValidationResult elementTypeNotXmlName(ViolationCollector v, String name) {
    return addViolation(v, illFormedOperation("element type is not an XML Name: \""
        + name + "\""));
  }

  private ValidationResult duplicateAnnotationKey(ViolationCollector v, String key) {
    return addViolation(v, illFormedOperation("annotation boundary contains duplicate key "
        + key));
  }

  private ValidationResult adjacentAnnotationBoundaries(ViolationCollector v) {
    return addViolation(v, illFormedOperation("adjacent annotation boundaries"));
  }

  private ValidationResult textNotAllowedInElement(ViolationCollector v, String tag) {
    return addViolation(v, schemaViolation("element type " + tag
        + " does not allow text content"));
  }

  private ValidationResult onlyBlipTextAllowedInElement(ViolationCollector v, String tag) {
    return addViolation(v, schemaViolation("element type " + tag
        + " only allows blip text content, not arbitrary characters"));
  }

  private ValidationResult cannotDeleteSoManyCharacters(ViolationCollector v,
      int available, String chars) {
    int attempted = chars.length();
    return addViolation(v, invalidOperation("cannot delete " + attempted + " characters,"
        + " only " + available + " available"));
  }

  private ValidationResult invalidAttribute(ViolationCollector v, String type, String attr,
      String value) {
    return addViolation(v, schemaViolation("type " + type + " does not permit attribute "
        + attr + " with value " + value));
  }

  private ValidationResult invalidChild(ViolationCollector v, String parentTag, String childTag) {
    if (parentTag == null) {
      return addViolation(v, schemaViolation("element type " + childTag
          + " not permitted at top level"));
    } else {
      return addViolation(v, schemaViolation("element type " + parentTag
          + " does not permit subelement type " + childTag));
    }
  }

  private ValidationResult differentElementTypeRequired(ViolationCollector v, String expectedType,
      String actualType) {
    return addViolation(v, schemaViolation("element of type " + expectedType
        + " required, not " + actualType));
  }

  private ValidationResult childElementRequired(ViolationCollector v, String expectedType) {
    return addViolation(v, schemaViolation("child element required, expected type "
        + expectedType));
  }

  private ValidationResult attemptToDeleteRequiredChild(ViolationCollector v) {
    return addViolation(v, schemaViolation("attempt to delete required child"));
  }

  private ValidationResult attemptToInsertBeforeRequiredChild(ViolationCollector v) {
    return addViolation(v, schemaViolation("attempt to insert before required child"));
  }

  private ValidationResult noElementStartToDelete(ViolationCollector v) {
    return addViolation(v, invalidOperation("no element start to delete here"));
  }

  private ValidationResult noElementEndToDelete(ViolationCollector v) {
    return addViolation(v, invalidOperation("no element end to delete here"));
  }

  private ValidationResult noElementStartToChangeAttributes(ViolationCollector v) {
    return addViolation(v, invalidOperation("no element start to change attributes here"));
  }

  private ValidationResult oldAnnotationsDifferFromDocument(ViolationCollector v,
      String key, String oldValue, String valueInDoc) {
    return addViolation(v, invalidOperation("old annotations differ from document: "
        + "purported old value for key " + key + " is " + oldValue
        + ", actual value in document is " + valueInDoc));
  }

  private ValidationResult newAnnotationsIncorrectForDeletion(ViolationCollector v) {
    return addViolation(v, invalidOperation("new annotation value incorrect for deletion"));
  }

  private ValidationResult oldTagDifferFromDocument(ViolationCollector v) {
    return addViolation(v, invalidOperation("old element type differs from document"));
  }

  private ValidationResult oldAttributesDifferFromDocument(ViolationCollector v) {
    return addViolation(v, invalidOperation("old attributes differ from document"));
  }

  private ValidationResult missingAnnotationForDeletion(ViolationCollector v, String key,
      String valueInDoc, String requiredValue) {
    return addViolation(v, invalidOperation("deletion does not reset value for key "
        + key + " from " + valueInDoc + " to " + requiredValue));
  }

  private ValidationResult oldCharacterDiffersFromDocument(ViolationCollector v,
      char expected, char actual) {
    return addViolation(v, invalidOperation("attempt to delete character " + actual
        + " when the actual character is " + expected));
  }

  private enum DocSymbol { CHARACTER, OPEN, CLOSE, END }

  private static class InsertStart {
    final String tag;

    InsertStart(String tag) {
      this.tag = tag;
    }

    static InsertStart getInstance(String tag) {
      assert tag != null;
      return new InsertStart(tag);
    }

    ValidationResult notClosed(DocOpAutomaton a, ViolationCollector v) {
      return a.mismatchedInsertStart(v);
    }
  }

  private String elementStartingHere() {
    Preconditions.checkPositionIndex(effectivePos, doc.length());
    return doc.elementStartingAt(effectivePos);
  }

  private String elementEndingNext() {
    Preconditions.checkPositionIndex(effectivePos, doc.length());
    return doc.elementEndingAt(effectivePos);
  }

  // tag==null means text allowed at top level
  private PermittedCharacters permittedCharacters(String type) {
    return constraints.permittedCharacters(type);
  }

  private boolean elementAllowsAttribute(String type, String attributeName, String attributeValue) {
    return constraints.permitsAttribute(type, attributeName, attributeValue);
  }

  // parentType==null means childType allowed at top level
  private boolean elementAllowsChild(String parentType, String childType) {
    return constraints.permitsChild(parentType, childType);
  }

  // returns either null or the type of the first required child
  private String requiredFirstChild(String parentType) {
    List<String> list = constraints.getRequiredInitialChildren(parentType);
    if (list.isEmpty()) {
      return null;
    } else if (list.size() > 1) {
      throw new UnsupportedOperationException("Schema requires multiple initial children");
    } else {
      return list.get(0);
    }
  }


  private final AutomatonDocument doc;
  private final DocumentSchema constraints;


  // current state

  private int effectivePos = 0;
  // first item is bottom of stack, last is top

  private final ArrayList<InsertStart> insertionStack;
  private String nextRequiredElement = null;
  private int deletionStackDepth = 0;
  private AnnotationsUpdateImpl annotationsUpdate = new AnnotationsUpdateImpl();
  private boolean afterAnnotationBoundary = false;
  // This can become null if the operation is invalid.
  private AnnotationMap targetAnnotationsForDeletion = EMPTY_ANNOTATIONS;


  // more state to track just to be able to produce better diagnostic messages

  private int resultingPos = 0;


  public static final AutomatonDocument EMPTY_DOCUMENT = new AutomatonDocument() {
    @Override
    public AnnotationMap annotationsAt(int pos) {
      return AnnotationMapImpl.EMPTY_MAP;
    }

    @Override
    public Attributes attributesAt(int pos) {
      return null;
    }

    @Override
    public int charAt(int pos) {
      return -1;
    }

    @Override
    public String elementEndingAt(int pos) {
      return null;
    }

    @Override
    public String elementStartingAt(int pos) {
      return null;
    }

    @Override
    public int length() {
      return 0;
    }

    @Override
    public String nthEnclosingElementTag(int insertionPoint, int depth) {
      return null;
    }

    @Override
    public int remainingCharactersInElement(int insertionPoint) {
      return 0;
    }

    @Override
    public String getAnnotation(int pos, String key) {
      return null;
    }

    @Override
    public int firstAnnotationChange(int start, int end, String key, String fromValue) {
      Preconditions.checkPositionIndexes(start, end, 0);
      // if (fromValue != null && end > start): can't happen since end == start == 0
      return -1;
    }
  };

  /**
   * Creates an automaton that corresponds to the set of all possible operations
   * on the given document under the given schema constraints.
   */
  public DocOpAutomaton(AutomatonDocument doc, DocumentSchema constraints) {
    this.doc = doc;
    this.constraints = constraints;
    this.nextRequiredElement = requiredFirstChild(null);
    this.insertionStack = new ArrayList<InsertStart>();
  }


  /**
   * Copy Constructor
   */
  public DocOpAutomaton(DocOpAutomaton other) {
    this(other, other.constraints);
  }

  /**
   * Copy Constructor 2
   */
  public DocOpAutomaton(DocOpAutomaton other, DocumentSchema constraints) {
    this.afterAnnotationBoundary = other.afterAnnotationBoundary;
    this.annotationsUpdate = other.annotationsUpdate;
    this.constraints = constraints;
    this.deletionStackDepth = other.deletionStackDepth;
    this.doc = other.doc;
    this.effectivePos = other.effectivePos;
    this.insertionStack = new ArrayList<InsertStart>(other.insertionStack);
    this.nextRequiredElement = other.nextRequiredElement;
    this.resultingPos = other.resultingPos;
    this.targetAnnotationsForDeletion = other.targetAnnotationsForDeletion;
  }

  // current state primitive readers

  private DocSymbol effectiveDocSymbol() {
    if (effectivePos >= doc.length()) {
      return DocSymbol.END;
    }
    {
      String s = elementStartingHere();
      if (s != null) {
        return DocSymbol.OPEN;
      }
    }
    {
      String s = elementEndingNext();
      if (s != null) {
        return DocSymbol.CLOSE;
      }
    }
    return DocSymbol.CHARACTER;
  }

  // only defined for open and close
  private String effectiveDocSymbolTag() {
    switch (effectiveDocSymbol()) {
      case OPEN: {
        String tag = elementStartingHere();
        assert tag != null;
        return tag;
      }
      case CLOSE: {
        String tag = elementEndingNext();
        assert tag != null;
        return tag;
      }
      default:
        throw new IllegalStateException("not at element start or end");
    }
  }

  // only defined for open
  private Attributes effectiveDocSymbolAttributes() {
    switch (effectiveDocSymbol()) {
      case OPEN: {
        Attributes attributes = doc.attributesAt(effectivePos);
        assert attributes != null;
        return attributes;
      }
      default:
        throw new IllegalStateException("not at element start");
    }
  }

  private boolean insertionStackIsEmpty() {
    return insertionStack.isEmpty();
  }

  private boolean deletionStackIsEmpty() {
    return deletionStackDepth == 0;
  }

  // null if at top level
  private String effectiveEnclosingElementTag() {
    // This procedure will find the element at depth == 0.
    int depth = 0;
    for (int i = insertionStack.size() - 1; i >= 0; i--) {
      InsertStart e = insertionStack.get(i);
      if (depth == 0) { return e.tag; }
      depth--;
    }
    if (effectivePos > doc.length()) { return null; }
    return doc.nthEnclosingElementTag(effectivePos, depth);
  }

  /**
   * Returns the maximum permitted retain item count, assuming that a retain
   * component is valid.
   */
  public int maxRetainItemCount() {
    if (effectivePos >= doc.length()) {
      return 0;
    } else {
      return doc.length() - effectivePos;
    }
  }

  public String currentElementStartTag() {
    return doc.elementStartingAt(effectivePos);
  }

  public Attributes currentElementStartAttributes() {
    return doc.attributesAt(effectivePos);
  }

  public AnnotationMap currentAnnotations() {
    if (effectivePos >= doc.length()) {
      return EMPTY_ANNOTATIONS;
    } else {
      return doc.annotationsAt(effectivePos);
    }
  }

  public int nextChar(int offset) {
    Preconditions.checkArgument(offset >= 0, "Offset must be positive");
    if (offset >= doc.length() - effectivePos) { return -1; }
    return doc.charAt(effectivePos + offset);
  }

  /**
   * Non-negative.  0 means neutral.  Larger values mean more complexity.
   */
  public int insertionStackComplexityMeasure() {
    return insertionStack.size();
  }

  /**
   * Non-negative.  0 means neutral.  Larger values mean more complexity.
   */
  public int deletionStackComplexityMeasure() {
    return deletionStackDepth;
  }

  public Set<String> openAnnotations() {
    HashSet<String> r = new HashSet<String>();
    for (int i = 0; i < annotationsUpdate.changeSize(); i++) {
      r.add(annotationsUpdate.getChangeKey(i));
    }
    return r;
  }

  private boolean canRetain(int itemCount) {
    assert itemCount >= 0;
    return itemCount <= maxRetainItemCount();
  }

  /**
   * If a deleteCharacters operation component is permitted as the next
   * component, returns the maximum number of characters that it can delete.
   * Otherwise, the return value is undefined.
   */
  public int maxCharactersToDelete() {
    if (effectivePos >= doc.length()) {
      return 0;
    }
    return doc.remainingCharactersInElement(effectivePos);
  }


  // current state manipulators

  private void advance(int distance) {
    // we're not asserting canIncreaseLength() or similar here, since
    // the driver may be generating an invalid op deliberately.
    assert distance >= 0;
    effectivePos += distance;
  }

  private void insertionStackPush(InsertStart e) {
    insertionStack.add(e);
  }

  private void deletionStackPush() {
    deletionStackDepth++;
  }

  private void insertionStackPop() {
    assert !insertionStack.isEmpty();
    insertionStack.remove(insertionStack.size() - 1);
  }

  private void deletionStackPop() {
    assert !deletionStackIsEmpty();
    deletionStackDepth--;
  }


  private static boolean equal(Object a, Object b) {
    return a == null ? b == null : a.equals(b);
  }


  // check/do methods

  private ValidationResult checkAnnotationsForRetain(ViolationCollector v, int itemCount) {
    for (int i = 0; i < annotationsUpdate.changeSize(); i++) {
      String key = annotationsUpdate.getChangeKey(i);
      String oldValue = annotationsUpdate.getOldValue(i);
      int firstChange = doc.firstAnnotationChange(effectivePos, effectivePos + itemCount,
          key, oldValue);
      if (firstChange != -1) {
        return oldAnnotationsDifferFromDocument(v, key, oldValue,
            doc.getAnnotation(firstChange, key));
      }
    }
    return valid();
  }

  public ValidationResult checkRetain(int itemCount, ViolationCollector v) {
    // well-formedness
    if (itemCount <= 0) { return retainItemCountNotPositive(v); }
    if (!insertionStackIsEmpty()) { return retainInsideInsertOrDelete(v); }
    if (!deletionStackIsEmpty()) { return retainInsideInsertOrDelete(v); }
    // validity
    if (!canRetain(itemCount)) { return retainPastEnd(v, doc.length(), itemCount); }
    return checkAnnotationsForRetain(v, itemCount);
  }

  public void doRetain(int itemCount) {
    if (EXPENSIVE_ASSERTIONS) {
      assert checkRetain(itemCount, null) != ValidationResult.ILL_FORMED;
    }
    advance(itemCount);
    updateDeletionTargetAnnotations();
    resultingPos += itemCount;
    afterAnnotationBoundary = false;
  }


  private ValidationResult validateAnnotationKey(String key, ViolationCollector v) {
    if (key == null) { return nullAnnotationKey(v); }
    if (key.contains("?") || key.contains("@")) { return invalidCharacterInAnnotationKey(v, key); }
    if (!Utf16Util.isValidUtf16(key)) { return annotationKeyNotValidUtf16(v); }
    return ValidationResult.VALID;
  }

  private ValidationResult validateAnnotationValue(String value, ViolationCollector v) {
    if (value == null) { return ValidationResult.VALID; }
    if (!Utf16Util.isValidUtf16(value)) { return annotationValueNotValidUtf16(v); }
    return ValidationResult.VALID;
  }


  public ValidationResult checkAnnotationBoundary(AnnotationBoundaryMap map,
      ViolationCollector v) {
    // well-formedness
    if (afterAnnotationBoundary) { return adjacentAnnotationBoundaries(v); }
    HashSet<String> endKeys = new HashSet<String>();
    for (int i = 0; i < map.endSize(); i++) {
      String key = map.getEndKey(i);
      {
        ValidationResult r = validateAnnotationKey(key, v);
        if (!r.isValid()) { return r; }
      }
      if (i > 0 && map.getEndKey(i - 1).compareTo(key) >= 0) {
        return annotationKeysNotStrictlyMonotonic(v, map.getEndKey(i - 1), key);
      }
      if (!annotationsUpdate.containsKey(key)) { return mismatchedEndAnnotation(v, key); }
      endKeys.add(key);
    }
    for (int i = 0; i < map.changeSize(); i++) {
      String key = map.getChangeKey(i);
      {
        ValidationResult r = validateAnnotationKey(key, v);
        if (!r.isValid()) { return r; }
      }
      {
        ValidationResult r = validateAnnotationValue(map.getOldValue(i), v);
        if (!r.isValid()) { return r; }
      }
      {
        ValidationResult r = validateAnnotationValue(map.getNewValue(i), v);
        if (!r.isValid()) { return r; }
      }
      if (i > 0 && map.getChangeKey(i - 1).compareTo(key) >= 0) {
        return annotationKeysNotStrictlyMonotonic(v, map.getChangeKey(i - 1), key);
      }
      if (endKeys.contains(key)) { return duplicateAnnotationKey(v, key); }
    }
    return valid();
  }

  public void doAnnotationBoundary(AnnotationBoundaryMap map) {
    if (EXPENSIVE_ASSERTIONS) {
      assert !checkAnnotationBoundary(map, null).isIllFormed();
    }
    annotationsUpdate = annotationsUpdate.composeWith(map);
    afterAnnotationBoundary = true;
  }


  private static final AnnotationMap EMPTY_ANNOTATIONS = AnnotationMapImpl.EMPTY_MAP;

  public AnnotationMap inheritedAnnotations() {
    if (effectivePos == 0 || effectivePos > doc.length()) {
      return EMPTY_ANNOTATIONS;
    } else {
      int posToInheritFrom = effectivePos - 1;
      return doc.annotationsAt(posToInheritFrom);
    }
  }

  private void updateDeletionTargetAnnotations() {
    if (effectivePos > doc.length()) {
      targetAnnotationsForDeletion = null;
    } else {
      targetAnnotationsForDeletion =
          inheritedAnnotations().updateWithNoCompatibilityCheck(annotationsUpdate);
    }
  }

  private ValidationResult checkAnnotationsForInsertion(ViolationCollector v) {
    if (effectivePos > doc.length()) {
      // Invalid operation, nothing to check.
      return valid();
    }
    int posToInheritFrom = effectivePos - 1;
    for (int i = 0; i < annotationsUpdate.changeSize(); i++) {
      String key = annotationsUpdate.getChangeKey(i);
      String oldValue = annotationsUpdate.getOldValue(i);
      String defaultFromDocument = posToInheritFrom == -1 ? null :
        doc.getAnnotation(posToInheritFrom, key);
      if (!equal(oldValue, defaultFromDocument)) {
        return oldAnnotationsDifferFromDocument(v, key, oldValue, defaultFromDocument);
      }
    }
    return valid();
  }

  private ValidationResult checkForInsertionBeforeRequiredChild(ViolationCollector v) {
    if (effectivePos < doc.length() && insertionStackIsEmpty()) {
      String parentType = doc.nthEnclosingElementTag(effectivePos, 0);
      String requiredFirstChild = requiredFirstChild(parentType);
      boolean isFirstChild = effectivePos == 0 || doc.elementStartingAt(effectivePos - 1) != null;
      if (isFirstChild && requiredFirstChild != null) {
        return attemptToInsertBeforeRequiredChild(v);
      }
    }
    return ValidationResult.VALID;
  }

  public ValidationResult checkCharacters(String chars, ViolationCollector v) {
    // well-formedness
    if (chars == null) { return nullCharacters(v); }
    if (chars.isEmpty()) { return emptyCharacters(v); }
    if (Utf16Util.firstSurrogate(chars) != -1) { return charactersContainsSurrogate(v); }
    if (!Utf16Util.isValidUtf16(chars)) { return charactersInvalidUnicode(v); }
    if (!deletionStackIsEmpty()) { return insertInsideDelete(v); }
    // validity
    {
      ValidationResult r = checkAnnotationsForInsertion(v);
      if (!r.isValid()) { return r; }
    }
    // schema
    if (nextRequiredElement != null) {
      return childElementRequired(v, nextRequiredElement);
    }
    {
      ValidationResult r = checkForInsertionBeforeRequiredChild(v);
      if (!r.isValid()) { return r; }
    }
    String enclosingTag = effectiveEnclosingElementTag();
    switch (permittedCharacters(enclosingTag)) {
      case NONE:
        return textNotAllowedInElement(v, enclosingTag);
      case BLIP_TEXT:
        if (!Utf16Util.isGoodUtf16ForBlip(chars)) {
          return onlyBlipTextAllowedInElement(v, enclosingTag);
        }
        break;
      case ANY:
        break;
      default:
        throw new AssertionError("unexpected return value from permittedCharacters()");
    }
    return valid();
  }

  public void doCharacters(String characters) {
    if (EXPENSIVE_ASSERTIONS) {
      assert checkCharacters(characters, null) != ValidationResult.ILL_FORMED;
    }
    updateDeletionTargetAnnotations();
    resultingPos += characters.length();
    afterAnnotationBoundary = false;
  }


  private ValidationResult checkAnnotationsForDeletion(ViolationCollector v, int itemCount) {
    if (targetAnnotationsForDeletion == null) {
      // Invalid operation, nothing to check.
      return valid();
    }

    // Check that all annotations contained in the update have correct old and
    // new values.
    for (int i = 0; i < annotationsUpdate.changeSize(); i++) {
      String key = annotationsUpdate.getChangeKey(i);
      String oldValue = annotationsUpdate.getOldValue(i);
      int firstChange = doc.firstAnnotationChange(effectivePos, effectivePos + itemCount,
          key, oldValue);
      if (firstChange != -1) {
        return oldAnnotationsDifferFromDocument(v, key, oldValue,
            doc.getAnnotation(firstChange, key));
      }
      String newValue = annotationsUpdate.getNewValue(i);
      if (!equal(newValue, targetAnnotationsForDeletion.get(key))) {
        return newAnnotationsIncorrectForDeletion(v);
      }
    }
    // TODO: Find a way to speed this up.
    for (int offset = 0; offset < itemCount; offset++) {
      int pos = effectivePos + offset;
      Map<String, String> annotationsHere = doc.annotationsAt(pos);
      // Check that the update contains all values that need to be set; the set of
      // keys to check is the union of keys at the current position and at the
      // position that it would inherit from.
      for (String key : annotationsHere.keySet()) {
        String valueInDoc = annotationsHere.get(key);
        String requiredValue = targetAnnotationsForDeletion.get(key);
        if (!equal(valueInDoc, requiredValue)) {
          if (!annotationsUpdate.containsKey(key)) {
            return missingAnnotationForDeletion(v, key, valueInDoc, requiredValue);
          }
        }
      }
      for (String key : targetAnnotationsForDeletion.keySet()) {
        String valueInDoc = annotationsHere.get(key);
        String requiredValue = targetAnnotationsForDeletion.get(key);
        if (!equal(valueInDoc, requiredValue)) {
          if (!annotationsUpdate.containsKey(key)) {
            return missingAnnotationForDeletion(v, key, valueInDoc, requiredValue);
          }
        }
      }
    }
    return valid();
  }


  private ValidationResult checkAttributesWellFormed(Attributes attr, ViolationCollector v) {
    if (attr == null) { return nullAttributes(v); }
    String previousKey = null;
    for (Map.Entry<String, String> e : attr.entrySet()) {
      if (e.getKey() == null) { return nullAttributeKey(v); }
      if (!Utf16Util.isXmlName(e.getKey())) { return attributeNameNotXmlName(v, e.getKey()); }
      if (e.getValue() == null) { return nullAttributeValue(v); }
      if (!Utf16Util.isValidUtf16(e.getValue())) { return attributeValueNotValidUtf16(v); }
      if (previousKey != null && previousKey.compareTo(e.getKey()) >= 0) {
        return attributeKeysNotStrictlyMonotonic(v, previousKey, e.getKey());
      }
      previousKey = e.getKey();
    }
    return ValidationResult.VALID;
  }

  private ValidationResult checkAttributesUpdateWellFormed(AttributesUpdate u,
      ViolationCollector v) {
    if (u == null) { return nullAttributesUpdate(v); }
    String previousKey = null;
    for (int i = 0; i < u.changeSize(); i++) {
      String key = u.getChangeKey(i);
      if (key == null) { return nullAttributeKey(v); }
      if (!Utf16Util.isXmlName(key)) { return attributeNameNotXmlName(v, key); }
      if (previousKey != null && previousKey.compareTo(key) >= 0) {
        return attributeKeysNotStrictlyMonotonic(v, previousKey, key);
      }
      if (u.getOldValue(i) != null && !Utf16Util.isValidUtf16(u.getOldValue(i))) {
        return attributeValueNotValidUtf16(v);
      }
      if (u.getNewValue(i) != null && !Utf16Util.isValidUtf16(u.getNewValue(i))) {
        return attributeValueNotValidUtf16(v);
      }
      previousKey = key;
    }
    return ValidationResult.VALID;
  }

  private ValidationResult validateAttributes(String tag, Attributes attr, ViolationCollector v) {
    for (Map.Entry<String, String> e : attr.entrySet()) {
      String key = e.getKey();
      String value = e.getValue();
      if (!elementAllowsAttribute(tag, key, value)) {
        return invalidAttribute(v, tag, key, value);
      }
    }
    return ValidationResult.VALID;
  }


  public ValidationResult checkElementStart(String type, Attributes attr, ViolationCollector v) {
    // well-formedness
    if (type == null) { return nullTag(v); }
    if (!Utf16Util.isXmlName(type)) { return elementTypeNotXmlName(v, type); }
    {
      ValidationResult r = checkAttributesWellFormed(attr, v);
      if (r != ValidationResult.VALID) { return r; }
    }
    if (!deletionStackIsEmpty()) { return insertInsideDelete(v); }

    // validity
    {
      ValidationResult r = checkAnnotationsForInsertion(v);
      if (!r.isValid()) { return r; }
    }

    // schema
    {
      ValidationResult r = validateAttributes(type, attr, v);
      if (r != ValidationResult.VALID) { return r; }
    }
    String parentTag = effectiveEnclosingElementTag();
    if (!elementAllowsChild(parentTag, type)) { return invalidChild(v, parentTag, type); }
    {
      ValidationResult r = checkForInsertionBeforeRequiredChild(v);
      if (!r.isValid()) { return r; }
    }
    if (nextRequiredElement != null && !nextRequiredElement.equals(type)) {
      return differentElementTypeRequired(v, nextRequiredElement, type);
    }
    return valid();
  }

  public void doElementStart(String type, Attributes attr) {
    if (EXPENSIVE_ASSERTIONS) {
      assert !checkElementStart(type, attr, null).isIllFormed();
    }
    updateDeletionTargetAnnotations();
    insertionStackPush(InsertStart.getInstance(type));
    nextRequiredElement = requiredFirstChild(type);
    resultingPos += 1;
    afterAnnotationBoundary = false;
   }


  public ValidationResult checkElementEnd(ViolationCollector v) {
    // well-formedness
    if (!deletionStackIsEmpty()) { return insertInsideDelete(v); }
    if (insertionStackIsEmpty()) { return mismatchedInsertEnd(v); }
    // validity
    {
      ValidationResult r = checkAnnotationsForInsertion(v);
      if (!r.isValid()) { return r; }
    }
    // schema
    if (nextRequiredElement != null) {
      return childElementRequired(v, nextRequiredElement);
    }
    return valid();
  }

  public void doElementEnd() {
    if (EXPENSIVE_ASSERTIONS) {
      assert !checkElementEnd(null).isIllFormed();
    }
    updateDeletionTargetAnnotations();
    insertionStackPop();
    resultingPos += 1;
    afterAnnotationBoundary = false;
  }


  private boolean attributesEqual(Attributes a, Attributes b) {
    if (a.size() != b.size()) { return false; }
    for (Map.Entry<String, String> ae : a.entrySet()) {
      if (!equal(ae.getValue(), b.get(ae.getKey()))) { return false; }
    }
    return true;
  }


  public ValidationResult checkDeleteCharacters(String chars, ViolationCollector v) {
    // well-formedness
    if (chars == null) { return nullCharacters(v); }
    if (chars.isEmpty()) { return emptyCharacters(v); }
    if (Utf16Util.firstSurrogate(chars) != -1) { return deleteCharactersContainsSurrogate(v); }
    if (!Utf16Util.isValidUtf16(chars)) { return deleteCharactersInvalidUnicode(v); }
    if (!insertionStackIsEmpty()) { return deleteInsideInsert(v); }
    // validity
    int docLength = doc.length();
    for (int offset = 0; offset < chars.length(); offset++) {
      if (effectivePos + offset >= docLength) {
        return cannotDeleteSoManyCharacters(v, offset, chars);
      }
      int charHereIfAny = doc.charAt(effectivePos + offset);
      if (charHereIfAny == -1) {
        return cannotDeleteSoManyCharacters(v, offset, chars);
      }
      char charHere = (char) charHereIfAny;
      if (charHere != chars.charAt(offset)) {
        return oldCharacterDiffersFromDocument(v, charHere, chars.charAt(offset));
      }
    }
    return checkAnnotationsForDeletion(v, chars.length());
  }

  public void doDeleteCharacters(String chars) {
    if (EXPENSIVE_ASSERTIONS) {
      assert !checkDeleteCharacters(chars, null).isIllFormed();
    }
    advance(chars.length());
    afterAnnotationBoundary = false;
  }


  public ValidationResult checkDeleteElementStart(String type, Attributes attr,
      ViolationCollector v) {
    // well-formedness
    if (type == null) { return nullTag(v); }
    if (!Utf16Util.isXmlName(type)) { return elementTypeNotXmlName(v, type); }
    {
      ValidationResult r = checkAttributesWellFormed(attr, v);
      if (r != ValidationResult.VALID) { return r; }
    }
    if (!insertionStackIsEmpty()) { return deleteInsideInsert(v); }
    // validity
    if (effectiveDocSymbol() != DocSymbol.OPEN) { return noElementStartToDelete(v); }
    if (!effectiveDocSymbolTag().equals(type)) { return oldTagDifferFromDocument(v); }
    if (!attributesEqual(attr, effectiveDocSymbolAttributes())) {
      return oldAttributesDifferFromDocument(v);
    }
    {
      ValidationResult r = checkAnnotationsForDeletion(v, 1);
      if (!r.isValid()) { return r; }
    }
    // schema
    if (deletionStackDepth == 0) {
      if (effectivePos < doc.length()) {
        String parentType = doc.nthEnclosingElementTag(effectivePos, 0);
        String requiredFirstChild = requiredFirstChild(parentType);
        boolean isFirstChild = effectivePos == 0 || doc.elementStartingAt(effectivePos - 1) != null;
        if (isFirstChild && requiredFirstChild != null) {
          return attemptToDeleteRequiredChild(v);
        }
      }
    }
    return valid();
  }

  public void doDeleteElementStart(String tag, Attributes attr) {
    if (EXPENSIVE_ASSERTIONS) {
      assert !checkDeleteElementStart(tag, attr, null).isIllFormed();
    }
    deletionStackPush();
    advance(1);
    afterAnnotationBoundary = false;
  }


  public ValidationResult checkDeleteElementEnd(ViolationCollector v) {
    // well-formedness
    if (!insertionStackIsEmpty()) { return deleteInsideInsert(v); }
    if (deletionStackIsEmpty()) { return mismatchedDeleteEnd(v); }
    // validity
    if (effectiveDocSymbol() != DocSymbol.CLOSE) { return noElementEndToDelete(v); }
    {
      ValidationResult r = checkAnnotationsForDeletion(v, 1);
      if (!r.isValid()) { return r; }
    }
    return valid();
  }

  public void doDeleteElementEnd() {
    if (EXPENSIVE_ASSERTIONS) {
      assert checkDeleteElementEnd(null) != ValidationResult.ILL_FORMED;
    }
    deletionStackPop();
    advance(1);
    afterAnnotationBoundary = false;
  }


  public ValidationResult checkUpdateAttributes(AttributesUpdate u, ViolationCollector v) {
    // well-formedness
    {
      ValidationResult r = checkAttributesUpdateWellFormed(u, v);
      if (!r.isValid()) { return r; }
    }
    if (!deletionStackIsEmpty()) { return attributeChangeInsideInsertOrDelete(v); }
    if (!insertionStackIsEmpty()) { return attributeChangeInsideInsertOrDelete(v); }

    // validity
    if (effectiveDocSymbol() != DocSymbol.OPEN) { return noElementStartToChangeAttributes(v); }
    String type = effectiveDocSymbolTag();
    assert type != null;
    Attributes oldAttrs = effectiveDocSymbolAttributes();
    for (int i = 0; i < u.changeSize(); i++) {
      String key = u.getChangeKey(i);
      String oldValue = u.getOldValue(i);
      if (!equal(oldValue, oldAttrs.get(key))) { return oldAttributesDifferFromDocument(v); }
    }
    {
      ValidationResult r = checkAnnotationsForRetain(v, 1);
      if (!r.isValid()) { return r; }
    }

    // schema
    for (int i = 0; i < u.changeSize(); i++) {
      String key = u.getChangeKey(i);
      String value = u.getNewValue(i);
      if (value != null) {
        if (!elementAllowsAttribute(type, key, value)) {
          return invalidAttribute(v, type, key, value);
        }
      }
    }
    return ValidationResult.VALID;
  }

  public void doUpdateAttributes(AttributesUpdate u) {
    if (EXPENSIVE_ASSERTIONS) {
      assert !checkUpdateAttributes(u, null).isIllFormed();
    }
    advance(1);
    updateDeletionTargetAnnotations();
    resultingPos += 1;
    afterAnnotationBoundary = false;
  }


  public ValidationResult checkReplaceAttributes(Attributes oldAttrs, Attributes newAttrs,
      ViolationCollector v) {
    // well-formedness
    {
      ValidationResult r = checkAttributesWellFormed(oldAttrs, v);
      if (!r.isValid()) { return r; }
    }
    {
      ValidationResult r = checkAttributesWellFormed(newAttrs, v);
      if (!r.isValid()) { return r; }
    }
    if (!deletionStackIsEmpty()) { return attributeChangeInsideInsertOrDelete(v); }
    if (!insertionStackIsEmpty()) { return attributeChangeInsideInsertOrDelete(v); }

    // validity
    if (effectiveDocSymbol() != DocSymbol.OPEN) { return noElementStartToChangeAttributes(v); }
    String type = effectiveDocSymbolTag();
    assert type != null;
    Attributes actualOldAttrs = effectiveDocSymbolAttributes();
    if (!attributesEqual(actualOldAttrs, oldAttrs)) { return oldAttributesDifferFromDocument(v); }
    {
      ValidationResult r = checkAnnotationsForRetain(v, 1);
      if (!r.isValid()) { return r; }
    }

    // schema
    {
      ValidationResult r = validateAttributes(type, newAttrs, v);
      if (!r.isValid()) { return r; }
    }
    return valid();
  }

  public void doReplaceAttributes(Attributes oldAttrs, Attributes newAttrs) {
    if (EXPENSIVE_ASSERTIONS) {
      assert !checkReplaceAttributes(oldAttrs, newAttrs, null).isIllFormed();
    }
    advance(1);
    updateDeletionTargetAnnotations();
    resultingPos += 1;
    afterAnnotationBoundary = false;
  }


  /**
   * Checks whether the automaton is in an accepting state, i.e., whether the
   * operation would be valid if no further operation components follow.
   */
  public ValidationResult checkFinish(ViolationCollector v) {
    // well-formedness
    if (!insertionStackIsEmpty()) {
      for (InsertStart e : insertionStack) {
        return e.notClosed(this, v);
      }
    }
    if (!deletionStackIsEmpty()) {
      return mismatchedDeleteStart(v);
    }
    if (annotationsUpdate.changeSize() > 0) {
      return mismatchedStartAnnotation(v, annotationsUpdate.getChangeKey(0));
    }

    // validity
    if (effectivePos != doc.length()) {
      return missingRetainToEnd(v, doc.length(), effectivePos);
    }
    return ValidationResult.VALID;
  }

}
TOP

Related Classes of org.waveprotocol.wave.model.document.operation.automaton.DocOpAutomaton$InsertStart

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.
ew');