/** * 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.bootstrap; 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.DocInitialization; import org.waveprotocol.wave.model.document.operation.DocInitializationCursor; import org.waveprotocol.wave.model.document.operation.DocOp; import org.waveprotocol.wave.model.document.operation.DocOp.IsDocOp; import org.waveprotocol.wave.model.document.operation.DocOpCursor; import org.waveprotocol.wave.model.document.operation.ModifiableDocument; import org.waveprotocol.wave.model.document.operation.automaton.AutomatonDocument; 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.operation.impl.AnnotationBoundaryMapImpl; 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.AnnotationsUpdate; import org.waveprotocol.wave.model.document.operation.impl.AnnotationsUpdateImpl; import org.waveprotocol.wave.model.document.operation.impl.DocInitializationBuffer; import org.waveprotocol.wave.model.document.operation.impl.DocOpUtil; import org.waveprotocol.wave.model.document.operation.impl.DocOpValidator; 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.Preconditions; import java.util.ArrayList; import java.util.LinkedList; import java.util.List; import java.util.ListIterator; import java.util.NoSuchElementException; import java.util.TreeSet; /** * A document implementation that is easy to understand but not efficient. */ public class BootstrapDocument implements ModifiableDocument, AutomatonDocument, IsDocOp { private abstract class Item { AnnotationMap annotations; Item(AnnotationMap annotations) { this.annotations = annotations; } void applyItem(DocInitializationCursor c) { int size = knownAnnotationKeys.size(); String[] changeKeys = knownAnnotationKeys.toArray(new String[size]); assert changeKeys.length == size; String[] newValues = new String[size]; for (int i = 0; i < size; i++) { newValues[i] = annotations.get(changeKeys[i]); } c.annotationBoundary(new AnnotationBoundaryMapImpl( new String[0], changeKeys, new String[size], newValues)); applyData(c); } abstract void applyData(DocInitializationCursor c); AnnotationMap getAnnotations() { return annotations; } void updateAnnotations(AnnotationsUpdate annotationUpdate) { annotations = annotations.updateWith(annotationUpdate); } } private class CharacterItem extends Item { final char character; CharacterItem(char character, AnnotationMap annotations) { super(annotations); this.character = character; } @Override void applyData(DocInitializationCursor c) { c.characters("" + character); } @Override public String toString() { return "Character: " + character + " [" + annotations + "]"; } } private class ElementStartItem extends Item { final String tag; Attributes attrs; ElementStartItem(String tag, Attributes attrs, AnnotationMap annotations) { super(annotations); this.tag = tag; this.attrs = attrs; } @Override void applyData(DocInitializationCursor c) { c.elementStart(tag, attrs); } void replaceAttributes(Attributes newAttributes) { attrs = newAttributes; } void updateAttributes(AttributesUpdate update) { attrs = attrs.updateWith(update); } String getTagName() { return tag; } @Override public String toString() { return "ElementStart: " + tag + " " + attrs + " [" + annotations + "]"; } } private class ElementEndItem extends Item { ElementEndItem(AnnotationMap annotations) { super(annotations); } @Override void applyData(DocInitializationCursor c) { c.elementEnd(); } @Override public String toString() { return "ElementEnd: [" + annotations + "]"; } } private final DocumentSchema schemaConstraints; private final List<Item> items = new LinkedList<Item>(); // All annotation keys that we've ever encountered. private final TreeSet<String> knownAnnotationKeys = new TreeSet<String>(); private boolean inconsistent = false; public BootstrapDocument(DocumentSchema schemaConstraints) { this.schemaConstraints = schemaConstraints; } public BootstrapDocument() { this(DocumentSchema.NO_SCHEMA_CONSTRAINTS); } /** Copy constructor */ public BootstrapDocument(BootstrapDocument other) { this(other.schemaConstraints); try { consume(other.asOperation()); } catch (OperationException e) { throw new OperationRuntimeException("Invalid other document", e); } } @Override public DocInitialization asOperation() { checkConsistent(); DocInitializationBuffer b = new DocInitializationBuffer(); for (Item i : items) { i.applyItem(b); } if (!items.isEmpty()) { String[] endKeys = knownAnnotationKeys.toArray(new String[knownAnnotationKeys.size()]); b.annotationBoundary(new AnnotationBoundaryMapImpl( endKeys, new String[0], new String[0], new String[0])); } return b.finish(); } @Override public int length() { checkConsistent(); return items.size(); } private ListIterator<Item> readIterator; private final List<String> tagNames = new ArrayList<String>(); @Override public String elementStartingAt(int pos) { checkConsistent(); Item item = advance(pos); if (item instanceof ElementStartItem) { return ((ElementStartItem) item).getTagName(); } else { return null; } } @Override public Attributes attributesAt(int pos) { checkConsistent(); Item item = advance(pos); if (item instanceof ElementStartItem) { return ((ElementStartItem) item).attrs; } else { return null; } } @Override public String elementEndingAt(int pos) { checkConsistent(); Item item = advance(pos); if (item instanceof ElementEndItem) { return tagNames.get((tagNames.size() - 1)); } else { return null; } } @Override public int charAt(int pos) { checkConsistent(); Item item = advance(pos); if (item instanceof CharacterItem) { int c = ((CharacterItem) item).character; assert c != -1; return c; } else { return -1; } } @Override public String nthEnclosingElementTag(int insertionPoint, int depth) { checkConsistent(); advance(insertionPoint); if (depth >= tagNames.size()) { return null; } return tagNames.get(tagNames.size() - 1 - depth); } @Override public int remainingCharactersInElement(int insertionPoint) { checkConsistent(); advance(insertionPoint); int num = 0; try { while (readIterator.next() instanceof CharacterItem) { num++; } } catch (NoSuchElementException ex) { // reached document end. } for (int i = 0; i < num; i++) { readIterator.previous(); } return num; } @Override public AnnotationMap annotationsAt(int pos) { checkConsistent(); Preconditions.checkElementIndex(pos, items.size()); return advance(pos).getAnnotations(); } @Override public String getAnnotation(int pos, String key) { checkConsistent(); Preconditions.checkElementIndex(pos, items.size()); return advance(pos).getAnnotations().get(key); } private static boolean equal(Object a, Object b) { return a == null ? b == null : a.equals(b); } @Override public int firstAnnotationChange(int start, int end, String key, String fromValue) { Preconditions.checkPositionIndexes(start, end, items.size()); for (int pos = start; pos < end; pos++) { if (!equal(getAnnotation(pos, key), fromValue)) { return pos; } } return -1; } private Item currentItem() { if (!readIterator.hasNext()) { return null; } Item item = readIterator.next(); readIterator.previous(); return item; } // null if pos == items.size() private Item advance(int pos) { Preconditions.checkPositionIndex(pos, items.size()); if (readIterator == null || pos < readIterator.nextIndex()) { resetReadState(); } for (int i = readIterator.nextIndex(); i < pos; i++) { Item item = readIterator.next(); if (item instanceof ElementStartItem) { tagNames.add(((ElementStartItem) item).getTagName()); } else if (item instanceof ElementEndItem) { tagNames.remove(tagNames.size() - 1); } } return currentItem(); } private void resetReadState() { readIterator = items.listIterator(); tagNames.clear(); } AnnotationsUpdate annotationUpdates; @Override public void consume(DocOp m) throws OperationException { checkConsistent(); ViolationCollector v = new ViolationCollector(); DocOpValidator.validate(v, schemaConstraints, this, m); if (!v.isValid()) { throw new OperationException("Validation failed: " + v); } inconsistent = true; annotationUpdates = AnnotationsUpdateImpl.EMPTY_MAP; final ListIterator<Item> iterator = items.listIterator(); try { // In theory, the above call to the validator makes the error checking in // this DocOpCursor redundant. We check for errors anyway in case the // validator is incorrect. m.apply(new DocOpCursor() { Item current = null; AnnotationMap inherited = AnnotationMapImpl.EMPTY_MAP; private AnnotationMap insertionAnnotations() { return inherited.updateWith(annotationUpdates); } @Override public void annotationBoundary(AnnotationBoundaryMap map) { annotationUpdates = annotationUpdates.composeWith(map); for (int i = 0; i < map.changeSize(); i++) { knownAnnotationKeys.add(map.getChangeKey(i)); } } @Override public void characters(String s) { for (int i = 0; i < s.length(); i++) { iterator.add(new CharacterItem(s.charAt(i), insertionAnnotations())); } } @Override public void elementStart(String type, Attributes attrs) { iterator.add(new ElementStartItem(type, attrs, insertionAnnotations())); } @Override public void elementEnd() { iterator.add(new ElementEndItem(insertionAnnotations())); } @Override public void deleteCharacters(String s) { for (int i = 0; i < s.length(); i++) { CharacterItem item = nextCharacter(); if (s.charAt(i) != item.character) { throw new OpCursorException("Mismatched deleted characters: " + s.charAt(i) + " vs " + item.character); } inherited = item.getAnnotations(); iterator.remove(); } } @Override public void deleteElementEnd() { ElementEndItem item = nextElementEnd(); inherited = item.getAnnotations(); iterator.remove(); } @Override public void deleteElementStart(String tag, Attributes attrs) { ElementStartItem item = nextElementStart(); inherited = item.getAnnotations(); iterator.remove(); } @Override public void retain(int distance) { for (int i = 0; i < distance; i++) { inheritAndAnnotate(next()); } } @Override public void replaceAttributes(Attributes oldAttrs, Attributes newAttrs) { ElementStartItem item = nextElementStart(); item.replaceAttributes(newAttrs); inheritAndAnnotate(item); } @Override public void updateAttributes(AttributesUpdate attrUpdate) { ElementStartItem item = nextElementStart(); item.updateAttributes(attrUpdate); inheritAndAnnotate(item); } private void inheritAndAnnotate(Item item) { inherited = item.getAnnotations(); item.updateAnnotations(annotationUpdates); } Item next() { if (!iterator.hasNext()) { throw new OpCursorException("Action past end of document, of size: " + length()); } current = iterator.next(); return current; } ElementStartItem nextElementStart() { try { return (ElementStartItem) next(); } catch (ClassCastException e) { throw new OpCursorException("Not at an element start, at: " + current); } } ElementEndItem nextElementEnd() { try { return (ElementEndItem) next(); } catch (ClassCastException e) { throw new OpCursorException("Not at an element end, at: " + current); } } CharacterItem nextCharacter() { try { return (CharacterItem) next(); } catch (ClassCastException e) { throw new OpCursorException("Not at a character, at: " + current); } } }); if (iterator.hasNext()) { int remainingItems = 0; while (iterator.hasNext()) { remainingItems++; iterator.next(); } throw new OperationException("Missing retain to end of document (" + remainingItems + " items)"); } } catch (OpCursorException e) { throw new OperationException(e); } if (annotationUpdates.changeSize() != 0) { throw new OperationException("Unended annotations at end of operation: " + annotationUpdates); } resetReadState(); inconsistent = false; } private void checkConsistent() { if (inconsistent) { throw new IllegalStateException("The document is in an inconsistent state"); } } @Override public String toString() { return "BootstrapDocument: " + DocOpUtil.debugToXmlString(asOperation()); } }