/**
* 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.operation;
import org.waveprotocol.wave.model.document.indexed.IndexedDocument;
import org.waveprotocol.wave.model.document.indexed.NodeType;
import org.waveprotocol.wave.model.document.operation.automaton.DocOpAutomaton.OperationIllFormed;
import org.waveprotocol.wave.model.document.operation.automaton.DocOpAutomaton.OperationInvalid;
import org.waveprotocol.wave.model.document.operation.automaton.DocOpAutomaton.SchemaViolation;
import org.waveprotocol.wave.model.document.operation.automaton.DocOpAutomaton.ValidationResult;
import org.waveprotocol.wave.model.document.operation.automaton.DocOpAutomaton.ViolationCollector;
import org.waveprotocol.wave.model.document.operation.automaton.DocumentSchema;
import org.waveprotocol.wave.model.document.util.DocHelper;
import org.waveprotocol.wave.model.document.util.Point;
import org.waveprotocol.wave.model.util.Preconditions;
import java.util.ArrayList;
import java.util.HashSet;
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 mutation 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 any further mutation components is not clearly specified.
*
* The checkFinish() method determines whether ending an operation is acceptable,
* or whether any opening components are missing the corresponding closing
* component.
*
* The checkXXX methods accept a ViolationsAccu 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 ViolationsAccu 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 mutation components in that operation. DocumentOperationValidator does
* this.
*
* To generate a random operation, the automaton needs to be driven based on
* a random document mutation component generator.
* RandomDocumentMutationGenerator does this.
*
* @author ohler@google.com (Christian Ohler)
*/
// TODO(ohler/danilatos): Incorporate initial required elements schema checks
public final class NindoAutomaton<N, E extends N, T extends N> {
/**
* An object containing information about one individual reason why an
* operation is not valid, e.g. "skip 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;
}
protected 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;
}
}
// http://www.w3.org/TR/xml/#NT-NameStartChar
private static boolean isXmlNameStartChar(char c) {
// NameStartChar ::= ":" | [A-Z] | "_" | [a-z]
return c == ':' || ('A' <= c && c <= 'Z') || c == '_' | ('a' <= c && c <= 'z')
// | [#xC0-#xD6] | [#xD8-#xF6] | [#xF8-#x2FF]
|| (0xC0 <= c && c <= 0xD6) || (0xD8 <= c && c <= 0xF6) || (0xF8 <= c && c <= 0x2FF)
// | [#x370-#x37D] | [#x37F-#x1FFF]
|| (0x370 <= c && c <= 0x37D) || (0x37F <= c && c <= 0x1FFF)
// | [#x200C-#x200D] | [#x2070-#x218F]
|| (0x200C <= c && c <= 0x200D) || (0x2070 <= c && c <= 0x218F)
// | [#x2C00-#x2FEF] | [#x3001-#xD7FF]
|| (0x2C00 <= c && c <= 0x2FEF) || (0x3001 <= c && c <= 0xD7FF)
// | [#xF900-#xFDCF] | [#xFDF0-#xFFFD]
|| (0xF900 <= c && c <= 0xFDCF) || (0xFDF0 <= c && c <= 0xFFFD)
// | [#x10000-#xEFFFF]
// Ha, ha.
|| (0x10000 <= c && c <= 0xEFFFF);
}
private static boolean isXmlNameChar(char c) {
// NameChar ::= NameStartChar | "-" | "." | [0-9]
return isXmlNameStartChar(c) || c == '-' || c == '.' || ('0' <= c && c <= '9')
// | #xB7 | [#x0300-#x036F] | [#x203F-#x2040]
|| c == 0xB7 || (0x0300 <= c && c <= 0x036F) || (0x203F <= c && c <= 0x2040);
}
private static boolean isXmlName(String s) {
// Name ::= NameStartChar (NameChar)*
assert s != null;
if (s.length() == 0) {
return false;
}
if (!isXmlNameStartChar(s.charAt(0))) {
return false;
}
for (int i = 1; i < s.length(); i++) {
if (!isXmlNameChar(s.charAt(i))) {
return false;
}
}
return true;
}
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 mismatchedElementStart(ViolationCollector v) {
return addViolation(v, illFormedOperation("elementStart with no elementEnd"));
}
private ValidationResult mismatchedDeleteElementStart(ViolationCollector v) {
return addViolation(v, illFormedOperation("deleteElementStart with no deleteElementEnd"));
}
private ValidationResult mismatchedElementEnd(ViolationCollector v) {
return addViolation(v, illFormedOperation("elementEnd with no elementStart"));
}
private ValidationResult mismatchedDeleteElementEnd(ViolationCollector v) {
return addViolation(v, illFormedOperation("deleteElementEnd with no deleteElementStart"));
}
private ValidationResult mismatchedStartAnnotation(ViolationCollector v, String key) {
return addViolation(v, illFormedOperation("startAnnotation of key " + key
+ " with no endAnnotation"));
}
private ValidationResult mismatchedEndAnnotation(ViolationCollector v, String key) {
return addViolation(v, illFormedOperation("endAnnotation of key " + key
+ " with no startAnnotation"));
}
private ValidationResult skipDistanceNotPositive(ViolationCollector v) {
return addViolation(v, illFormedOperation("skip distance not positive"));
}
private ValidationResult skipInsideInsertOrDelete(ViolationCollector v) {
return addViolation(v, illFormedOperation("skip inside insert or delete"));
}
private ValidationResult attributeChangeInsideInsertOrDelete(ViolationCollector v) {
return addViolation(v, illFormedOperation("attribute change inside insert or delete"));
}
private ValidationResult skipPastEnd(ViolationCollector v) {
return addViolation(v, invalidOperation("skip past end of document"));
}
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 elementTypeNotXmlName(ViolationCollector v, String name) {
return addViolation(v, illFormedOperation("element type is not an XML name: \""
+ name + "\""));
}
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 attributeKeyNotXmlName(ViolationCollector v, String name) {
return addViolation(v, illFormedOperation("attribute key is not an XML name: \""
+ name + "\""));
}
private ValidationResult nullAttributeValue(ViolationCollector v) {
return addViolation(v, illFormedOperation("attribute value is null"));
}
private ValidationResult nullAnnotationKey(ViolationCollector v) {
return addViolation(v, illFormedOperation("annotation key is null"));
}
private ValidationResult textNotAllowedInElement(ViolationCollector v, String tag) {
return addViolation(v, schemaViolation("element type " + tag
+ " does not allow text content"));
}
private ValidationResult tooLong(ViolationCollector v) {
return addViolation(v, invalidOperation("intermediate or final document too long"));
}
private ValidationResult deleteLengthNotPositive(ViolationCollector v) {
return addViolation(v, illFormedOperation("delete length not positive"));
}
private ValidationResult cannotDeleteSoManyCharacters(ViolationCollector v,
int attempted, int available) {
return addViolation(v, invalidOperation("cannot delete " + attempted + " characters,"
+ " only " + available + " available"));
}
private ValidationResult invalidAttribute(ViolationCollector v, String type, String attr) {
return addViolation(v, schemaViolation("type " + type + " does not permit attribute " + attr));
}
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 typeInvalidRoot(ViolationCollector v) {
return addViolation(v, schemaViolation("type not permitted as root element"));
}
private ValidationResult invalidChild(ViolationCollector v, String parentTag, String childTag) {
return addViolation(v, schemaViolation("element type " + parentTag
+ " does not permit subelement type " + childTag));
}
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 final DocumentSchema schemaConstraints;
private enum DocSymbol { CHARACTER, OPEN, CLOSE, END }
private abstract static class StackEntry {
abstract ValidationResult notClosed(NindoAutomaton<?, ?, ?> a, ViolationCollector v);
InsertElement asInsertElement() { return null; }
DeleteElement asDeleteElement() { return null; }
}
private static class InsertElement extends StackEntry {
final String tag;
InsertElement(String tag) {
this.tag = tag;
}
static InsertElement getInstance(String tag) {
return new InsertElement(tag);
}
@Override
ValidationResult notClosed(NindoAutomaton<?, ?, ?> a, ViolationCollector v) {
return a.mismatchedElementStart(v);
}
@Override
InsertElement asInsertElement() { return this; }
}
private static class DeleteElement extends StackEntry {
static DeleteElement instance = new DeleteElement();
static DeleteElement getInstance() { return instance; }
@Override
ValidationResult notClosed(NindoAutomaton<?, ?, ?> a, ViolationCollector v) {
return a.mismatchedDeleteElementStart(v);
}
@Override
DeleteElement asDeleteElement() { return this; }
}
/**
* If effectivePos is at an element start, return that element.
* Else return null.
*/
private E elementStartingHere() {
Preconditions.checkPositionIndex(effectivePos, doc.size());
if (!(effectivePos + 1 < doc.size())) {
// effectivePos + 1 is the smallest possible index of the corresponding
// elementEnd; if that's beyond the end of the document, effectivePosition
// can't be an elementStart.
return null;
}
// Criterion: the enclosing element of effectivePos is the
// parent of the enclosing element of effectivePos + 1
// (if the enclosing element of effectivePos + 1 exists) (this
// also covers the corner case of the enclosing element of
// effectivePos being null).
E elementHere =
// can't create point for location 0
effectivePos == 0 ? doc.getDocumentElement() :
Point.enclosingElement(doc, doc.locate(effectivePos));
E elementNext = Point.enclosingElement(doc, doc.locate(effectivePos + 1));
if (elementNext == null) {
return null;
}
if (elementHere != doc.getParentElement(elementNext)) {
return null;
} else {
return elementNext;
}
}
/**
* If effectivePos is at an element end, return that element.
* Else return null.
*/
private E elementEndingNext() {
Preconditions.checkPositionIndex(effectivePos, doc.size());
if (effectivePos == 0) {
return null;
}
if (effectivePos == doc.size()) {
return null;
}
if (effectivePos == doc.size() - 1) {
// Root element ends here.
E root = doc.getDocumentElement();
assert root != null;
return root;
}
Point<N> point = doc.locate(effectivePos);
// Criterion: the enclosing element of effectivePos + 1 is the
// parent of the enclosing element of effectivePos
// (if the enclosing element of effectivePos exists) (this
// also covers the corner case of the enclosing element of
// effectivePos being null).
E elementHere = Point.enclosingElement(doc, doc.locate(effectivePos));
E elementNext = Point.enclosingElement(doc, doc.locate(effectivePos + 1));
if (elementHere == null) {
return null;
}
if (doc.getParentElement(elementHere) != elementNext) {
return null;
} else {
return elementHere;
}
}
// depth = 0 means enclosing element, depth = 1 means its parent, etc.
private static <N, E extends N, T extends N> E nthEnclosingElement(IndexedDocument<N, E, T> doc,
int pos, int depth) {
assert depth >= 0;
E e = Point.enclosingElement(doc, doc.locate(pos));
for (int i = 0; i < depth; i++) {
assert e != null;
e = doc.getParentElement(e);
}
return e;
}
private static <N, E extends N, T extends N> int remainingCharactersInElement(
IndexedDocument<N, E, T> doc, int pos) {
if (pos >= doc.size()) {
return 0;
}
Point<N> deletionPoint = doc.locate(pos);
if (!deletionPoint.isInTextNode()) {
return 0;
}
int offsetWithinNode = deletionPoint.getTextOffset();
N container = deletionPoint.getContainer();
assert doc.getNodeType(container) == NodeType.TEXT_NODE;
int nodeLength = DocHelper.getItemSize(doc, container);
int remainingChars = nodeLength - offsetWithinNode;
assert remainingChars >= 0;
while (true) {
N next = doc.getNextSibling(container);
if (next == null || doc.getNodeType(next) != NodeType.TEXT_NODE) {
break;
}
remainingChars += DocHelper.getItemSize(doc, next);
container = next;
}
return remainingChars;
}
private boolean tagAllowsText(String tag) {
switch (schemaConstraints.permittedCharacters(tag)) {
case ANY:
return true;
case BLIP_TEXT:
return true;
case NONE:
return false;
default:
throw new AssertionError("unexpected return value from permittedCharacters");
}
}
private boolean elementAllowedAsRoot(String tag) {
return schemaConstraints.permitsChild(null, tag);
}
private boolean elementAllowsAttribute(String tag, String attributeName) {
return schemaConstraints.permitsAttribute(tag, attributeName);
}
private boolean elementAllowsAttribute(String tag, String attributeName, String attributeValue) {
return schemaConstraints.permitsAttribute(tag, attributeName, attributeValue);
}
private boolean elementAllowsChild(String parentType, String childType) {
return schemaConstraints.permitsChild(parentType, childType);
}
// current state
private final IndexedDocument<N, E, T> doc;
private int effectivePos = 0;
private int resultingLength;
// first item is bottom of stack, last is top
private final ArrayList<StackEntry> stack = new ArrayList<StackEntry>();
private final Set<String> openAnnotationKeys = new HashSet<String>();
// more state to track for debugging
private int resultingPos = 0;
/**
* Creates an automaton that corresponds to the set of all possible operations
* on the given document under the given schema constraints.
*/
public NindoAutomaton(DocumentSchema schemaConstraints,
IndexedDocument<N, E, T> doc) {
this.schemaConstraints = schemaConstraints;
this.doc = doc;
this.resultingLength = doc.size();
}
// current state primitive readers
private int resultingLength() {
return resultingLength;
}
// note that effectivePos() is not, in general, <= resultingLength(). The
// values are basically unrelated.
private int effectivePos() {
return effectivePos;
}
private DocSymbol effectiveDocSymbol() {
if (effectivePos >= doc.size()) {
return DocSymbol.END;
}
{
E e = elementStartingHere();
if (e != null) {
return DocSymbol.OPEN;
}
}
{
E e = elementEndingNext();
if (e != null) {
return DocSymbol.CLOSE;
}
}
return DocSymbol.CHARACTER;
}
// only defined for open and close
private String effectiveDocSymbolTag() {
switch (effectiveDocSymbol()) {
case OPEN: {
String tag = doc.getTagName(elementStartingHere());
assert tag != null;
return tag;
}
case CLOSE: {
String tag = doc.getTagName(elementEndingNext());
assert tag != null;
return tag;
}
default:
throw new IllegalStateException("not at tag");
}
}
private boolean stackIsEmpty() {
return stack.isEmpty();
}
// undefined if stack is empty
private StackEntry topOfStack() {
assert !stack.isEmpty();
return stack.get(stack.size() - 1);
}
// null if outside root; must not be called if inserting
private E effectiveEnclosingElement() {
// need to take stack into account
// stack may be deleting, which does not affect level (semantics
// actually don't matter in this case, since no call sites call us during
// deletions)
// stack may be joining, which does not affect level (nested join needs
// to know tag that it is joining)
// stack may be splitting, which means we go down
// stack may be adding elements, which means we add levels
// if top of stack is insert element, that tells us the
// tag already
if (effectivePos == 0 || effectivePos >= doc.size()) {
return null;
}
if (stackIsEmpty()) {
return nthEnclosingElement(doc, effectivePos, 0);
} else {
if (topOfStack().asInsertElement() != null) {
assert false;
}
if (topOfStack().asDeleteElement() != null) {
{
boolean foundDeleteElement = false;
// TODO(ohler): Simplify this logic now that we no longer have anti elements.
// Even better, merge/consolidate with DocOpAutomaton
// The stack, when looking at it from bottom to top, must consist of a
// sequence of deleteAntiElements followed by a sequence deleteElements.
for (StackEntry e : stack) {
if (e.asDeleteElement() != null) {
foundDeleteElement = true;
} else {
assert false;
}
}
}
E e = nthEnclosingElement(doc, effectivePos, 0);
assert e != null; // cannot delete root
return e;
}
throw new RuntimeException("unexpected top of stack: " + topOfStack());
}
}
private String effectiveEnclosingElementTag() {
if (!stackIsEmpty() && topOfStack().asInsertElement() != null) {
return topOfStack().asInsertElement().tag;
} else {
E e = effectiveEnclosingElement();
if (e == null) {
return null;
} else {
return doc.getTagName(e);
}
}
}
// only defined if effective enclosing element is not null.
// null if effective enclosing element is root.
private String effectiveEnclosingElementParentTag() {
if (!stackIsEmpty() && topOfStack().asInsertElement() != null) {
if (stack.size() > 1) {
StackEntry s = stack.get(stack.size() - 2);
assert s != null;
assert s.asInsertElement() != null;
return s.asInsertElement().tag;
} else {
return topOfStack().asInsertElement().tag;
}
} else {
E e = effectiveEnclosingElement();
assert e != null;
E p = doc.getParentElement(e);
if (p == null) {
return null;
} else {
return doc.getTagName(p);
}
}
}
private boolean isAnnotationOpen(String key) {
return openAnnotationKeys.contains(key);
}
// Note that this is inclusive.
//
// NOTE(ohler): Some annotation-related indexing tricks may require
// storing 4 times the index in an int. Dividing by 5 just for some
// headroom.
//
// TODO(ohler): make sure the size limit for intermediate document states
// is compatible with composition.
private static final int MAX_DOC_LENGTH = Integer.MAX_VALUE / 5;
/**
* If an inserting mutation component is permitted as the next mutation
* component, returns the maximum number of items that can be added to the
* document in that component without exceeding any document size limits.
* Otherwise, the return value is undefined.
*/
// Need to be careful with potential overflows here in case we ever
// increase maxLength to Integer.MAX_VALUE.
public int maxLengthIncrease() {
int result = MAX_DOC_LENGTH - resultingLength;
assert result >= 0;
return result;
}
/**
* If a skip mutation component is permitted as the next mutation component,
* returns the maximum skip distance.
* Otherwise, the return value is undefined.
*/
public int maxSkipDistance() {
if (effectivePos >= doc.size()) {
return 0;
} else {
return doc.size() - effectivePos;
}
}
private boolean canIncreaseLength(int delta) {
assert delta >= 0;
return delta <= maxLengthIncrease();
}
private boolean canSkip(int distance) {
assert distance >= 0;
assert doc.size() <= MAX_DOC_LENGTH;
return distance <= maxSkipDistance();
}
/**
* If a deleteCharacters mutation component is permitted as the next mutation
* component, returns the maximum number of characters that it can delete.
* Otherwise, the return value is undefined.
*/
public int maxCharactersToDelete() {
return remainingCharactersInElement(doc, effectivePos);
}
private boolean topOfStackIsDeletion() {
return !stackIsEmpty() && topOfStack().asDeleteElement() != null;
}
private boolean topOfStackIsInsertion() {
return !stackIsEmpty() && topOfStack().asInsertElement() != null;
}
private boolean topOfStackIsInsertElement() {
return !stackIsEmpty() && (topOfStack().asInsertElement() != null);
}
private boolean topOfStackIsDeleteElement() {
return !stackIsEmpty() && (topOfStack().asDeleteElement() != null);
}
// 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 increaseLength(int delta) {
resultingLength += delta;
}
private void decreaseLength(int delta){
resultingLength -= delta;
}
private void pushOntoStack(StackEntry e) {
stack.add(e);
}
private void popStack() {
assert !stack.isEmpty();
stack.remove(stack.size() - 1);
}
private void setAnnotationOpen(String key) {
openAnnotationKeys.add(key);
}
private void setAnnotationClosed(String key) {
openAnnotationKeys.remove(key);
}
// check/do methods
/**
* Checks if a skip transition with the given parameters would be valid.
*/
public ValidationResult checkSkip(int distance, ViolationCollector v) {
if (distance <= 0) { return skipDistanceNotPositive(v); }
if (!stackIsEmpty()) { return skipInsideInsertOrDelete(v); }
if (!canSkip(distance)) { return skipPastEnd(v); }
return valid();
}
/**
* Performs a skip transition with the given parameters.
*/
public void doSkip(int distance) {
assert checkSkip(distance, null) != ValidationResult.ILL_FORMED;
advance(distance);
resultingPos += distance;
}
/**
* Checks if a characters transition with the given parameters would be valid.
*/
public ValidationResult checkCharacters(String characters, ViolationCollector v) {
// TODO(danilatos/ohler): Check schema and surrogates
if (characters == null) { return nullCharacters(v); }
if (characters.length() == 0) { return emptyCharacters(v); }
if (topOfStackIsDeletion()) { return insertInsideDelete(v); }
String enclosingTag = effectiveEnclosingElementTag();
if (!tagAllowsText(enclosingTag)) { return textNotAllowedInElement(v, enclosingTag); }
if (!canIncreaseLength(characters.length())) { return tooLong(v); }
return valid();
}
/**
* Performs a characters transition with the given parameters.
*/
public void doCharacters(String characters) {
assert checkCharacters(characters, null) != ValidationResult.ILL_FORMED;
increaseLength(characters.length());
resultingPos += characters.length();
}
/**
* Checks if a deleteCharacters transition with the given parameters would be valid.
*/
public ValidationResult checkDeleteCharacters(int count, ViolationCollector v) {
if (count <= 0) { return deleteLengthNotPositive(v); }
if (topOfStackIsInsertion()) { return deleteInsideInsert(v); }
int available = maxCharactersToDelete();
if (count > available) { return cannotDeleteSoManyCharacters(v, count, available); }
return valid();
}
/**
* Performs a deleteCharacters transition with the given parameters.
*/
public void doDeleteCharacters(int count) {
assert checkDeleteCharacters(count, null) != ValidationResult.ILL_FORMED;
advance(count);
decreaseLength(count);
}
private ValidationResult validateAttributes(String tag, Map<String, String> attr,
ViolationCollector v, boolean allowRemovals) {
if (attr == null) { return nullAttributes(v); }
for (Map.Entry<String, String> e : attr.entrySet()) {
String key = e.getKey();
String value = e.getValue();
if (key == null) { return nullAttributeKey(v); }
if (!isXmlName(key)) { return attributeKeyNotXmlName(v, key); }
if (value == null) {
if (!allowRemovals) { return nullAttributeValue(v); }
if (!elementAllowsAttribute(tag, key)) { return invalidAttribute(v, tag, key); }
} else {
if (!elementAllowsAttribute(tag, key, value)) {
return invalidAttribute(v, tag, key, value);
}
}
}
return ValidationResult.VALID;
}
/**
* Checks if an elementStart with the given parameters would be valid.
*/
public ValidationResult checkElementStart(String tag, Map<String, String> attr,
ViolationCollector v) {
if (tag == null) { return nullTag(v); }
if (!isXmlName(tag)) { return elementTypeNotXmlName(v, tag); }
{
ValidationResult attrViolation = validateAttributes(tag, attr, v, false);
if (attrViolation != ValidationResult.VALID) { return attrViolation; }
}
if (topOfStackIsDeletion()) { return insertInsideDelete(v); }
if (!canIncreaseLength(2)) { return tooLong(v); }
if (effectiveDocSymbol() == DocSymbol.END
&& effectiveEnclosingElementTag() == null
&& stackIsEmpty()
&& resultingLength() == 0) {
if (elementAllowedAsRoot(tag)) {
return valid();
} else {
return typeInvalidRoot(v);
}
}
String parentTag = effectiveEnclosingElementTag();
if (parentTag == null) {
if (!elementAllowedAsRoot(tag)) { return typeInvalidRoot(v); }
} else {
if (!elementAllowsChild(parentTag, tag)) { return invalidChild(v, parentTag, tag); }
}
return valid();
}
/**
* Performs an elementStart transition with the given parameters.
*/
public void doElementStart(String tag, Map<String, String> attr) {
assert checkElementStart(tag, attr, null) != ValidationResult.ILL_FORMED;
pushOntoStack(InsertElement.getInstance(tag));
increaseLength(2);
resultingPos += 1;
}
/**
* Checks if an elementEnd transition with the given parameters would be valid.
*/
public ValidationResult checkElementEnd(ViolationCollector v) {
if (!topOfStackIsInsertElement()) { return mismatchedElementEnd(v); }
return valid();
}
/**
* Performs an elementEnd transition with the given parameters.
*/
public void doElementEnd() {
assert checkElementEnd(null) != ValidationResult.ILL_FORMED;
popStack();
// size increase happens in element start
resultingPos += 1;
}
/**
* Checks if a deleteElementStart with the given parameters would be valid.
*/
public ValidationResult checkDeleteElementStart(ViolationCollector v) {
if (topOfStackIsInsertion()) { return deleteInsideInsert(v); }
if (effectiveDocSymbol() != DocSymbol.OPEN) { return noElementStartToDelete(v); }
return valid();
}
/**
* Performs a deleteElementStart transition with the given parameters.
*/
public void doDeleteElementStart() {
assert checkDeleteElementStart(null) != ValidationResult.ILL_FORMED;
pushOntoStack(DeleteElement.getInstance());
advance(1);
// size decrease happens in delete element end
}
/**
* Checks if a deleteElementEnd with the given parameters would be valid.
*/
public ValidationResult checkDeleteElementEnd(ViolationCollector v) {
if (!topOfStackIsDeleteElement()) { return mismatchedDeleteElementEnd(v); }
if (effectiveDocSymbol() != DocSymbol.CLOSE) { return noElementEndToDelete(v); }
return valid();
}
/**
* Performs a deleteElementEnd transition with the given parameters.
*/
public void doDeleteElementEnd() {
assert checkDeleteElementEnd(null) != ValidationResult.ILL_FORMED;
popStack();
decreaseLength(2);
advance(1);
}
private ValidationResult checkChangeAttributes(Map<String, String> attr, ViolationCollector v,
boolean allowNullValues) {
if (!stackIsEmpty()) { return attributeChangeInsideInsertOrDelete(v); }
if (effectiveDocSymbol() != DocSymbol.OPEN) { return noElementStartToChangeAttributes(v); }
String actualTag = effectiveDocSymbolTag();
assert actualTag != null;
return validateAttributes(actualTag, attr, v, allowNullValues);
}
/**
* Checks if a setAttributes with the given parameters would be valid.
*/
public ValidationResult checkSetAttributes(Map<String, String> attr, ViolationCollector v) {
return checkChangeAttributes(attr, v, false);
}
/**
* Performs a setAttributes transition with the given parameters.
*/
public void doSetAttributes(Map<String, String> attr) {
assert checkSetAttributes(attr, null) != ValidationResult.ILL_FORMED;
advance(1);
resultingPos += 1;
}
/**
* Checks if an updateAttributes with the given parameters would be valid.
*/
public ValidationResult checkUpdateAttributes(Map<String, String> attr, ViolationCollector v) {
return checkChangeAttributes(attr, v, true);
}
/**
* Performs an updateAttributes transition with the given parameters.
*/
public void doUpdateAttributes(Map<String, String> attr) {
assert checkUpdateAttributes(attr, null) != ValidationResult.ILL_FORMED;
advance(1);
resultingPos += 1;
}
private ValidationResult validateAnnotationKey(String key, ViolationCollector v) {
if (key == null) { return nullAnnotationKey(v); }
return ValidationResult.VALID;
}
/**
* Checks if a startAnnotation with the given parameters would be valid.
*/
public ValidationResult checkStartAnnotation(String key, String value, ViolationCollector v) {
{
ValidationResult r = validateAnnotationKey(key, v);
if (r != ValidationResult.VALID) { return r; }
}
return valid();
}
/**
* Performs a startAnnotation transition with the given parameters.
*/
public void doStartAnnotation(String key, String value) {
assert checkStartAnnotation(key, value, null) != ValidationResult.ILL_FORMED;
setAnnotationOpen(key);
}
/**
* Checks if an endAnnotation with the given parameters would be valid.
*/
public ValidationResult checkEndAnnotation(String key, ViolationCollector v) {
{
ValidationResult r = validateAnnotationKey(key, v);
if (r != ValidationResult.VALID) { return r; }
}
if (!isAnnotationOpen(key)) { return mismatchedEndAnnotation(v, key); }
return valid();
}
/**
* Performs an endAnnotation transition with the given parameters.
*/
public void doEndAnnotation(String key) {
assert checkEndAnnotation(key, null) != ValidationResult.ILL_FORMED;
setAnnotationClosed(key);
}
/**
* Checks whether the automaton is in an accepting state, i.e., whether the
* operation would be valid if no further mutation components follow.
*/
public ValidationResult checkFinish(ViolationCollector v) {
for (StackEntry e : stack) {
return e.notClosed(this, v);
}
for (String key : openAnnotationKeys) {
return mismatchedStartAnnotation(v, key);
}
return ValidationResult.VALID;
}
/**
* Notifies the automaton that no further mutation components follow.
*/
// This doesn't actually do anything important. It's here for symmetry only.
public void doFinish() {
assert checkFinish(null) != ValidationResult.ILL_FORMED;
}
}