/** * Copyright 2008 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.indexed; import org.waveprotocol.wave.model.document.AnnotationCursor; import org.waveprotocol.wave.model.document.AnnotationInterval; import org.waveprotocol.wave.model.document.RangedAnnotation; import org.waveprotocol.wave.model.document.indexed.RawAnnotationSet.AnnotationEndEvent; import org.waveprotocol.wave.model.document.indexed.RawAnnotationSet.AnnotationEvent; import org.waveprotocol.wave.model.document.indexed.RawAnnotationSet.AnnotationStartEvent; import org.waveprotocol.wave.model.document.operation.AnnotationBoundaryMap; import org.waveprotocol.wave.model.document.operation.Attributes; import org.waveprotocol.wave.model.document.operation.AttributesUpdate; import org.waveprotocol.wave.model.document.operation.Automatons; import org.waveprotocol.wave.model.document.operation.DocOp; import org.waveprotocol.wave.model.document.operation.DocInitialization; import org.waveprotocol.wave.model.document.operation.DocOpCursor; import org.waveprotocol.wave.model.document.operation.Nindo; import org.waveprotocol.wave.model.document.operation.NindoValidator; import org.waveprotocol.wave.model.document.operation.Nindo.NindoCursor; import org.waveprotocol.wave.model.document.operation.algorithm.AnnotationsNormalizer; import org.waveprotocol.wave.model.document.operation.algorithm.Composer; import org.waveprotocol.wave.model.document.operation.automaton.AutomatonDocument; import org.waveprotocol.wave.model.document.operation.automaton.DocumentSchema; import org.waveprotocol.wave.model.document.operation.automaton.DocOpAutomaton.ViolationCollector; import org.waveprotocol.wave.model.document.operation.impl.AttributesImpl; import org.waveprotocol.wave.model.document.operation.impl.AttributesUpdateImpl; import org.waveprotocol.wave.model.document.operation.impl.DocOpBuffer; import org.waveprotocol.wave.model.document.operation.impl.DocOpBuilder; import org.waveprotocol.wave.model.document.operation.impl.DocOpUtil; import org.waveprotocol.wave.model.document.operation.impl.DocOpValidator; import org.waveprotocol.wave.model.document.operation.impl.UncheckedDocOpBuffer; import org.waveprotocol.wave.model.document.raw.RawDocument; import org.waveprotocol.wave.model.document.util.AnnotationIntervalImpl; import org.waveprotocol.wave.model.document.util.Annotations; import org.waveprotocol.wave.model.document.util.DocOpScrub; import org.waveprotocol.wave.model.document.util.EmptyDocument; import org.waveprotocol.wave.model.document.util.Point; import org.waveprotocol.wave.model.document.util.RangedAnnotationImpl; import org.waveprotocol.wave.model.document.util.XmlStringBuilder; import org.waveprotocol.wave.model.operation.OpCursorException; import org.waveprotocol.wave.model.operation.OperationException; import org.waveprotocol.wave.model.operation.OperationRuntimeException; import org.waveprotocol.wave.model.util.Box; import org.waveprotocol.wave.model.util.CollectionUtils; import org.waveprotocol.wave.model.util.EvaluableOffsetList; import org.waveprotocol.wave.model.util.OffsetList; import org.waveprotocol.wave.model.util.Preconditions; import org.waveprotocol.wave.model.util.ReadableStringMap; import org.waveprotocol.wave.model.util.ReadableStringSet; import org.waveprotocol.wave.model.util.StringMap; import org.waveprotocol.wave.model.util.StringSet; import org.waveprotocol.wave.model.util.ValueUtils; import org.waveprotocol.wave.model.util.ReadableStringMap.ProcV; import org.waveprotocol.wave.model.util.ReadableStringSet.Proc; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Map; /** * An implementation of IndexedDocument with an associative operator for * hashing. * * TODO(user): This doesn't yet do proper error checking. We need to * implement proper error checking. * * TODO(user): Write more tests for this class. * * TODO(user): Optimise skip for small skips. * * @author danilatos@google.com (Daniel Danilatos) * @author alexmah@google.com (Alexandre Mah) * * @param <N> The type of DOM nodes. * @param <E> The type of DOM Element nodes. * @param <T> The type of DOM Text nodes. * @param <V> The type of result that the document evaluates to. */ public class IndexedDocumentImpl<N, E extends N, T extends N, V> implements IndexedDocument<N, E, T>, Validator { /** * Whether to perform validation on consumed ops and nindos * * This should be on, except if * - profiling shows that it is a bottleneck AND we have no bugs * - a test case wants to explicitly validate separately to do better error reporting */ public static boolean performValidation = true; /** * A node-finding action that returns a point representing the location at * which the action is performed. * * The "natural" bias is to prefer text node point to parent-nodeAfter points, * and to prefer text node ends to text node beginnings. This results in things * working more smoothly with typing and annotations. * * TODO(danilatos): Separate this concern out? */ private final OffsetList.LocationAction<N, Point<N>> pointFinder = new OffsetList.LocationAction<N, Point<N>>() { public Point<N> performAction(OffsetList.Container<N> container, int offset) { N domNode = container.getValue(); Point<N> maybeAdjust; if (domNode == null) { if (container == offsetList.sentinel()) { return Point.<N>end(substrate.getDocumentElement()); } E element = getParentOf(container); maybeAdjust = maybeTextNodeEnd(getLastChild(element)); return maybeAdjust != null ? maybeAdjust : Point.<N>end(element); } else if (substrate.asElement(domNode) != null) { assert offset == 0; maybeAdjust = maybeTextNodeEnd(getPreviousSibling(domNode)); return maybeAdjust != null ? maybeAdjust : Point.before(substrate, domNode); } else { if (offset == 0) { maybeAdjust = maybeTextNodeEnd(getPreviousSibling(domNode)); return maybeAdjust != null ? maybeAdjust : Point.inText(domNode, offset); } else { return Point.inText(domNode, offset); } } } private Point<N> maybeTextNodeEnd(N node) { T textNode = asText(node); return textNode == null ? null : Point.<N>inText(textNode, getLength(textNode)); } }; /** * An action that, when performed at a location in this document, will set the * currentContainer and currentOffset fields of this object to correspond to * the location. */ private final OffsetList.LocationAction<N,Void> locationUpdater = new OffsetList.LocationAction<N, Void>() { public Void performAction(OffsetList.Container<N> container, int offset) { currentContainer = container; currentOffset = offset; currentParent = getParentOf(container); return null; } }; /** * For handling the DOM structure of this document. */ private final RawDocument<N, E, T> substrate; /** * An offset list for tracking the offsets of parts of the document. */ private final EvaluableOffsetList<N, V> offsetList; /** * The current location of the pointer in the document. */ private int currentLocation; /** * The container where the current location resides. */ private OffsetList.Container<N> currentContainer; /** * The offset of the current location in the current container. */ private int currentOffset; /** * The parent of the current DOM node. This may become inaccurate when the * deletion of an element is in progress. */ private E currentParent; /** * The current depth of structural modifications. */ private int deletionDepth; /** * Annotation set datastructure to delegate to */ private final RawAnnotationSet<Object> annotations; /** * Schema constraints */ private final DocumentSchema schemaConstraints; /** * Automaton interface for validity checking */ private final AutomatonDocument autoDoc = Automatons.fromReadable(this); /** * @param substrate raw dom document to use * @param rawAnnotations raw annotations to use */ public IndexedDocumentImpl(RawDocument<N, E, T> substrate, RawAnnotationSet<Object> rawAnnotations, DocumentSchema constraints) { Preconditions.checkNotNull(constraints, "Null schema not allowed, use DocumentSchema.NO_SCHEMA_CONSTRAINTS"); this.schemaConstraints = constraints; annotations = rawAnnotations != null ? rawAnnotations : new StubModifiableAnnotations<Object>(); this.substrate = substrate; offsetList = new EvaluableOffsetList<N, V>(null); assert size() == 0; indexChildren(substrate.getDocumentElement()); resetLocation(); if (offsetList.size() > 0) { annotations.begin(); annotations.insert(offsetList.size()); annotations.finish(); } assert substrate.getFirstChild(substrate.getDocumentElement()) != null || size() == 0; } /** * Indexes an element and its contents. * * @param element The element to index. */ private void indexElement(E element) { OffsetList.Container<N> sentinel = offsetList.sentinel(); insertBefore(sentinel, element, 1); indexChildren(element); sentinel.insertBefore(null, 1); } private void indexChildren(E element) { OffsetList.Container<N> sentinel = offsetList.sentinel(); for (N child = substrate.getFirstChild(element); child != null; child = substrate.getNextSibling(child)) { E childElement = substrate.asElement(child); if (childElement != null) { indexElement(childElement); } else { T childText = substrate.asText(child); insertBefore(sentinel, childText, substrate.getLength(childText)); } } } /** * {@inheritDoc} */ public Point<N> locate(int location) { Preconditions.checkPositionIndex(location, offsetList.size()); return offsetList.performActionAt(location, pointFinder); } /** * {@inheritDoc} */ public int getLocation(N node) { Preconditions.checkNotNull(node, "Cannot get the location of a null node"); OffsetList.Container<N> indexingContainer = substrate.getIndexingContainer(node); if (indexingContainer == null) { throw new IllegalArgumentException("getLocation: node has no indexing container - " + node); } if (indexingContainer.getNextContainer() == null) { throw new IllegalArgumentException("getLocation: node probably removed from DOM - " + node); } return indexingContainer.offset(); } /** * {@inheritDoc} */ public int getLocation(Point<N> point) { Preconditions.checkNotNull(point, "Cannot get the location of a null point"); Point.checkPoint(this, point, "IndexedDocumentImpl#getLocation"); if (point.isInTextNode()) { return getLocation(point.getContainer()) + point.getTextOffset(); } else { N nodeAfter = point.getNodeAfter(); if (nodeAfter == null) { return getLastLocationIn(point.getContainer()); } else { return getLocation(nodeAfter); } } } /** * Gets the last location inside an element node. * * @param node An element node. * @return The last location inside an element node. */ private int getLastLocationIn(N node) { N lastChild = substrate.getLastChild(node); if (lastChild != null) { if (substrate.asElement(lastChild) != null) { return getLastLocationIn(lastChild) + 1; } else { OffsetList.Container<N> indexingContainer = substrate.getIndexingContainer(lastChild); return indexingContainer.offset() + indexingContainer.size(); } } else if (node == getDocumentElement()) { return size(); } else { return substrate.getIndexingContainer(node).offset() + 1; } } private void moveToCurrentLocation() { offsetList.performActionAt(currentLocation, locationUpdater); } private final InvertibleCursor invertibleCursor = new InvertibleCursor(); private boolean inconsistent = false; @Override public void consume(DocOp op) throws OperationException { consume(op, performValidation); } public void consume(DocOp op, boolean validate) throws OperationException { checkConsistent(); if (validate) { maybeThrowOperationExceptionFor(op); } beginChange(); try { op.apply(invertibleCursor); } catch (OpCursorException e) { throw new OperationException(e.getMessage(), e); } endChange(); } public void maybeThrowOperationExceptionFor(DocOp op) throws OperationException { if (!DocOpValidator.validate(null, schemaConstraints, autoDoc, op).isValid()) { // Validate again to collect diagnostics (more expensive) ViolationCollector vc = new ViolationCollector(); DocOpValidator.validate(vc, schemaConstraints, autoDoc, op); throw new OperationException(vc); } } private final NonInvertibleCursor nindoCursor = new NonInvertibleCursor(); public DocOp consumeAndReturnInvertible(Nindo op) throws OperationException { return consumeAndReturnInvertible(op, performValidation); } public DocOp consumeAndReturnInvertible(Nindo op, boolean validate) throws OperationException { checkConsistent(); if (validate) { maybeThrowOperationExceptionFor(op); } nindoCursor.begin2(); try { op.apply(nindoCursor); } catch (OpCursorException e) { throw new OperationException(e.getMessage(), e); } return nindoCursor.finish2(); } @Override public void maybeThrowOperationExceptionFor(Nindo op) throws OperationException { ViolationCollector vc = NindoValidator.validate(this, op, schemaConstraints); if (!vc.isValid()) { // TODO(danilatos): reconcile the two validation methods throw new OperationException(vc); } } private void checkConsistent() { Preconditions.checkState(!inconsistent, "The document is not in a consistent state"); } private void beginChange() { beforeBegin(); inconsistent = true; resetLocation(); annotations.begin(); } private void endChange() throws OperationException { if (currentLocation != size()) { throw new OperationException("Operation size does not match document size " + "[operation size:" + currentLocation + "] [doc size:" + size() + "]"); } annotations.finish(); checkSizeConsistency("finish"); inconsistent = false; afterFinish(); } private class InvertibleCursor implements DocOpCursor { @Override public void updateAttributes(AttributesUpdate attrUpdate) { annotations.skip(1); E node = substrate.asElement(currentContainer.getValue()); for (int i = 0; i < attrUpdate.changeSize(); i++) { String name = attrUpdate.getChangeKey(i); String newValue = attrUpdate.getNewValue(i); if (newValue != null) { substrate.setAttribute(node, name, newValue); } else { substrate.removeAttribute(node, name); } } ++currentLocation; currentContainer = currentContainer.getNextContainer(); currentParent = node; onModifyAttributes(currentParent, attrUpdate); } @Override public void deleteCharacters(String chars) { assert chars.length() > 0; annotations.delete(chars.length()); doDeleteCharacters(chars.length()); onDeleteCharacters(currentLocation, chars); } @Override public void replaceAttributes(Attributes oldAttrs, Attributes newAttrs) { annotations.skip(1); E node = substrate.asElement(currentContainer.getValue()); // Map<String, String> oldAttributes = substrate.getAttributes(node); // Iterate over oldAttributes here, not attributeMap, since we are modifying // the map underlying the latter. for (Map.Entry<String, String> attribute : oldAttrs.entrySet()) { String key = attribute.getKey(); if (!newAttrs.containsKey(key)) { substrate.removeAttribute(node, key); } } for (Map.Entry<String, String> attribute : newAttrs.entrySet()) { if (attribute.getValue() == null) { throw new OpCursorException("Null attribute value in setAttributes"); } substrate.setAttribute(node, attribute.getKey(), attribute.getValue()); } ++currentLocation; currentContainer = currentContainer.getNextContainer(); currentParent = node; onModifyAttributes(currentParent, oldAttrs, newAttrs); } @Override public void retain(int itemCount) { assert itemCount > 0; checkRetain(itemCount); annotations.skip(itemCount); currentLocation += itemCount; moveToCurrentLocation(); } @Override public void annotationBoundary(AnnotationBoundaryMap map) { for (int i = 0; i < map.endSize(); i++) { doEndAnnotation(map.getEndKey(i)); } for (int i = 0; i < map.changeSize(); i++) { doStartAnnotation(map.getChangeKey(i), map.getNewValue(i)); } } @Override public void characters(String characters) { doCharacters(characters); } @Override public void elementEnd() { doElementEnd(); } @Override public void elementStart(String tagName, Attributes attributes) { doElementStart(tagName, attributes); } @Override public void deleteElementStart(String type, Attributes attrs) { E nodeToDelete = substrate.asElement(currentContainer.getValue()); if (nodeToDelete == null) { throw new OpCursorException("No element to delete at the current location."); } if (deletionDepth == 0) { substrate.removeChild(currentParent, nodeToDelete); } annotations.delete(1); deleteCurrentContainer(); ++deletionDepth; onDeleteElementStart(currentLocation, nodeToDelete); } public void deleteElementEnd() { onDeleteElementEnd(); annotations.delete(1); deleteCurrentContainer(); --deletionDepth; } } public class NonInvertibleCursor implements NindoCursor { private AnnotationsNormalizer<DocOp> builder; int sizeDiffSoFar; StringMap<String> requestedValues = CollectionUtils.createStringMap(); StringSet requestedKeys = CollectionUtils.createStringSet(); StringMap<String> newValues = CollectionUtils.createStringMap(); StringSet endKeys = CollectionUtils.createStringSet(); StringMap<String> deletionValues = CollectionUtils.createStringMap(); boolean didSomethingOtherThanDeletionSinceAnnotationBoundary = false; private void begin2() { beginChange(); builder = new AnnotationsNormalizer<DocOp>( performValidation ? new DocOpBuffer() : new UncheckedDocOpBuffer()); sizeDiffSoFar = 0; deletionValues.clear(); } private DocOp finish2() throws OperationException { endChange(); return builder.finish(); } @Override public void begin() { } @Override public void finish() { int remaining = size() - currentLocation; closeEndKeys(); if (remaining > 0) { builder.retain(remaining); } currentLocation = size(); } public void startAnnotation(String key, String value) { if (endKeys.contains(key)) { assert !didSomethingOtherThanDeletionSinceAnnotationBoundary : "Key: " + key + " endKeys: " + endKeys.toString(); endKeys.remove(key); } doStartAnnotation(key, value); requestedValues.put(key, value); requestedKeys.add(key); newValues.put(key, value); didSomethingOtherThanDeletionSinceAnnotationBoundary = false; //System.out.println(" ---> " + requestedValues + ", " + newValues + ", " + endKeys); } public void endAnnotation(String key) { requestedValues.remove(key); requestedKeys.remove(key); newValues.remove(key); if (deletionValues.containsKey(key)) { endKeys.add(key); } doEndAnnotation(key); didSomethingOtherThanDeletionSinceAnnotationBoundary = false; //System.out.println(" ---> " + requestedValues + ", " + newValues + ", " + endKeys); } public void skip(int itemCount) { assert itemCount > 0; didSomethingOtherThanDeletionSinceAnnotationBoundary = true; checkRetain(itemCount); moveAndUpdateAnnotations(itemCount); moveToCurrentLocation(); } private void moveAndUpdateAnnotations(int itemCount) { // TODO(danilatos): Some redundant updates most likely, // because the annotation tree will report its change // events. We still need beginUpdate for the // annotations which DONT change, to avoid losing // them in the operation. Would only matter for transform. beginUpdate(); annotations.skip(itemCount); final int finalLocation = currentLocation + itemCount; if (!requestedValues.isEmpty()) { // HACK(danilatos): Skip doesn't return anything, so do it a slow // and annoying way for now. final List<AnnotationEvent> events = new ArrayList<AnnotationEvent>(); final Box<ReadableStringMap<String>> annotations = Box.create(); int currentLocationBackup = currentLocation; final StringSet open = CollectionUtils.createStringSet(); for (AnnotationInterval<String> i : annotationIntervals(currentLocation, finalLocation, requestedKeys)) { currentLocation = i.start(); annotations.boxed = i.annotations(); requestedValues.each(new ProcV<String>() { public void apply(String key, String value) { String oldVal = annotations.boxed.get(key, null); if (ValueUtils.notEqual(value, oldVal)) { events.add(new AnnotationStartEvent(currentLocation, key, oldVal)); open.add(key); } else if (open.contains(key)) { events.add(new AnnotationEndEvent(currentLocation, key)); //assert open.contains(key); open.remove(key); } } }); } open.each(new Proc() { @Override public void apply(String key) { events.add(new AnnotationEndEvent(finalLocation, key)); } }); currentLocation = currentLocationBackup; for (AnnotationEvent ev : events) { // Uncomment this when not using the above hack // int eventLocation = ev.index + sizeDiffSoFar; int eventLocation = ev.index; if (eventLocation > currentLocation) { builder.retain(eventLocation - currentLocation); currentLocation = eventLocation; } if (ev.getEndKey() != null) { maybeRenewAnnotation(ev.getEndKey()); builder.endAnnotation(ev.getEndKey()); } else { String changeKey = ev.getChangeKey(); builder.startAnnotation(changeKey, ev.getChangeOldValue(), requestedValues.get(changeKey, null)); } } } if (currentLocation < finalLocation) { builder.retain(finalLocation - currentLocation); currentLocation = finalLocation; } assert currentLocation == finalLocation; } private void beginInsert() { //System.out.print("INSERT " + requestedValues + ", " + newValues + ", " + endKeys); //System.out.print(", " + deletionValues); newValues.each(new ProcV<String>() { @Override public void apply(String key, String value) { builder.startAnnotation(key, annotations.getInherited(key), value); } }); newValues.clear(); deletionValues.clear(); deletionValues.putAll(requestedValues); closeEndKeys(); //System.out.print(" ---> " + requestedValues + ", " + newValues + ", " + endKeys); //System.out.println(", " + deletionValues); } private void beginUpdate() { //System.out.print("UPDATE/SKIP " + requestedValues + ", " + newValues + ", " + endKeys); //System.out.print(", " + deletionValues); requestedValues.each(new ProcV<String>() { @Override public void apply(String key, String value) { String current = (String) annotations.getAnnotation(currentLocation, key); builder.startAnnotation(key, current, value); } }); newValues.clear(); deletionValues.clear(); deletionValues.putAll(requestedValues); closeEndKeys(); //System.out.print(" ---> " + requestedValues + ", " + newValues + ", " + endKeys); //System.out.println(", " + deletionValues); } private void closeEndKeys() { endKeys.each(new Proc() { @Override public void apply(String key) { builder.endAnnotation(key); } }); endKeys.clear(); } public void elementStart(String tagName, Attributes attributes) { didSomethingOtherThanDeletionSinceAnnotationBoundary = true; beginInsert(); doElementStart(tagName, attributes); builder.elementStart(tagName, attributes); sizeDiffSoFar++; } public void characters(String characters) { didSomethingOtherThanDeletionSinceAnnotationBoundary = true; beginInsert(); doCharacters(characters); builder.characters(characters); sizeDiffSoFar += characters.length(); } public void elementEnd() { didSomethingOtherThanDeletionSinceAnnotationBoundary = true; beginInsert(); doElementEnd(); builder.elementEnd(); sizeDiffSoFar++; } public void updateAttributes(Map<String, String> attributes) { didSomethingOtherThanDeletionSinceAnnotationBoundary = true; beginUpdate(); String[] triples = new String[attributes.size() * 3]; E node = substrate.asElement(currentContainer.getValue()); Map<String, String> oldAttributes = substrate.getAttributes(node); int i = 0; for (Map.Entry<String, String> attribute : attributes.entrySet()) { String name = attribute.getKey(); String newValue = attribute.getValue(); triples[i] = name; triples[i + 1] = oldAttributes.get(name); triples[i + 2] = newValue; if (newValue != null) { substrate.setAttribute(node, name, newValue); } else { substrate.removeAttribute(node, name); } i += 3; } currentLocation++; currentContainer = currentContainer.getNextContainer(); currentParent = node; annotations.skip(1); AttributesUpdateImpl attrUpdate = new AttributesUpdateImpl(triples); builder.updateAttributes(attrUpdate); onModifyAttributes(currentParent, attrUpdate); } public void replaceAttributes(Attributes newAttrs) { didSomethingOtherThanDeletionSinceAnnotationBoundary = true; beginUpdate(); E node = substrate.asElement(currentContainer.getValue()); Attributes oldAttributes = new AttributesImpl(substrate.getAttributes(node)); // Iterate over oldAttributes here, not attributeMap, since we are modifying // the map underlying the latter. for (Map.Entry<String, String> attribute : oldAttributes.entrySet()) { String key = attribute.getKey(); if (!newAttrs.containsKey(key)) { substrate.removeAttribute(node, attribute.getKey()); } } for (Map.Entry<String, String> attribute : newAttrs.entrySet()) { if (attribute.getValue() == null) { throw new OpCursorException("Null attribute value in setAttributes"); } substrate.setAttribute(node, attribute.getKey(), attribute.getValue()); } currentLocation++; currentContainer = currentContainer.getNextContainer(); currentParent = node; annotations.skip(1); builder.replaceAttributes(oldAttributes, newAttrs); onModifyAttributes(currentParent, oldAttributes, newAttrs); } public void deleteElementStart() { E nodeToDelete = substrate.asElement(currentContainer.getValue()); if (nodeToDelete == null) { throw new OpCursorException("No element to delete at the current location."); } String tagName = substrate.getTagName(nodeToDelete); Attributes attributes = new AttributesImpl(substrate.getAttributes(nodeToDelete)); if (deletionDepth == 0) { substrate.removeChild(currentParent, nodeToDelete); } doSingleDelete(tagName, attributes); deleteCurrentContainer(); deletionDepth++; sizeDiffSoFar--; onDeleteElementStart(currentLocation, nodeToDelete); } public void deleteElementEnd() { onDeleteElementEnd(); doSingleDelete(null, null); deleteCurrentContainer(); deletionDepth--; sizeDiffSoFar--; } private void doSingleDelete(String tagName, Attributes attrs) { List<AnnotationEvent> events = deleteAnnotations(1); boolean moved = false; int check = 0; for (AnnotationEvent ev : events) { int eventLocation = ev.index; if (eventLocation > currentLocation && !moved) { moved = true; buildDelete(tagName, attrs); } assert eventLocation == currentLocation + (moved ? 1 : 0) : currentLocation + " " + moved + " " + ev + " in " + events ; if (ev.getEndKey() != null) { assert moved; maybeRenewAnnotation(ev.getEndKey()); builder.endAnnotation(ev.getEndKey()); check--; } else { assert !moved; String changeKey = ev.getChangeKey(); builder.startAnnotation(changeKey, ev.getChangeOldValue(), getLeftNeighbourAnnotation(changeKey)); check++; } } assert check == 0; if (!moved) { buildDelete(tagName, attrs); } } private void buildDelete(String tagName, Attributes attrs) { if (tagName != null) { builder.deleteElementStart(tagName, attrs); } else { builder.deleteElementEnd(); } } public void deleteCharacters(int deletionSize) { List<AnnotationEvent> events = deleteAnnotations(deletionSize); String oldChars = doDeleteCharacters(deletionSize); int finalLocation = currentLocation; int index = 0; int newIndex = -1; StringMap<String> check = CollectionUtils.createStringMap(); for (AnnotationEvent ev : events) { int eventLocation = ev.index; newIndex = eventLocation - currentLocation; if (newIndex > index) { builder.deleteCharacters(oldChars.substring(index, newIndex)); index = newIndex; } if (ev.getEndKey() != null) { maybeRenewAnnotation(ev.getEndKey()); builder.endAnnotation(ev.getEndKey()); assert check.containsKey(ev.getEndKey()) : "key: " + ev.getEndKey() + ", " + check; check.remove(ev.getEndKey()); } else { String changeKey = ev.getChangeKey(); builder.startAnnotation(changeKey, ev.getChangeOldValue(), getLeftNeighbourAnnotation(changeKey)); check.put(ev.getChangeKey(), ev.getChangeOldValue()); } } assert check.isEmpty(); if (index < deletionSize) { builder.deleteCharacters(oldChars.substring(index, deletionSize)); } sizeDiffSoFar -= deletionSize; onDeleteCharacters(currentLocation, oldChars); } /** * @return annotation events for the deletion */ // HACK(danilatos): This code is inefficient and does not belong in indexed document. private List<AnnotationEvent> deleteAnnotations(int size) { final List<AnnotationEvent> events = new ArrayList<AnnotationEvent>(); final StringSet open = CollectionUtils.createStringSet(); final StringMap<String> deletionInherit = CollectionUtils.createStringMap(); deletionValues.each(new ReadableStringMap.ProcV<String>() { @Override public void apply(String key, String value) { deletionInherit.put(key, value); } }); final int start = currentLocation; final int end = currentLocation + size; for (AnnotationInterval<String> i : annotationIntervals(start, end, null)) { assert i.end() > start; assert i.start() < end; final int realStart = Math.max(start, i.start()); deletionInherit.each(new ReadableStringMap.ProcV<String>() { @Override public void apply(String key, String value) { events.add( new AnnotationStartEvent(realStart, key, getAnnotation(realStart, key))); open.add(key); } }); i.annotations().each(new ReadableStringMap.ProcV<String>() { @Override public void apply(String key, String value) { if (!deletionInherit.containsKey(key)) { events.add( new AnnotationStartEvent(realStart, key, value)); open.add(key); } } }); } // Produce endAnnotation calls for every annotation at the end. open.each(new StringSet.Proc() { @Override public void apply(String key) { events.add(new AnnotationEndEvent(end, key)); } }); annotations.delete(size); return events; } private void maybeRenewAnnotation(String key) { if (!newValues.containsKey(key) && requestedValues.containsKey(key)) { newValues.put(key, requestedValues.get(key)); } } private String getLeftNeighbourAnnotation(String key) { if (deletionValues.containsKey(key)) { return deletionValues.get(key); } return currentLocation != 0 ? (String) annotations.getAnnotation(currentLocation - 1, key) : null; } } private void checkRetain(int count) { if (currentLocation + count > size()) { throw new OpCursorException("Retain past end of document [location:" + (currentLocation + count) + "] [doc size:" + size() + "]"); } } private void doElementStart(String tagName, Attributes attributes) { splitCurrent(); E newElement = substrate.createElement(tagName, attributes, currentParent, currentContainer.getValue()); insertBefore(currentContainer, newElement, 1); currentContainer.insertBefore(null, 1); annotations.insert(1); currentContainer = currentContainer.getPreviousContainer(); currentParent = newElement; ++currentLocation; onElementStart(newElement); } private void doCharacters(String characters) { annotations.insert(characters.length()); OffsetList.Container<N> previousNode = null; T previousTextNode = null; if (currentOffset == 0) { previousNode = currentContainer.getPreviousContainer(); previousTextNode = substrate.asText(previousNode.getValue()); } // Prefer appending data to previous text node if (previousTextNode != null) { substrate.appendData(previousTextNode, characters); previousNode.increaseSize(characters.length()); } else { N domNode = currentContainer.getValue(); T textNode = substrate.asText(domNode); if (textNode != null) { substrate.insertData(textNode, currentOffset, characters); currentContainer.increaseSize(characters.length()); currentOffset += characters.length(); } else { T newTextNode = substrate.createTextNode(characters, currentParent, currentContainer.getValue()); insertBefore(currentContainer, newTextNode, characters.length()); } } onCharacters(currentLocation, characters); currentLocation += characters.length(); } private String doDeleteCharacters(int deletionSize) { StringBuilder textBuilder = new StringBuilder(); if (deletionDepth <= 0) { T textNode = substrate.asText(currentContainer.getValue()); int currentLength = substrate.getLength(textNode); if (currentOffset + deletionSize < currentLength) { textBuilder.append( substrate.getData(textNode).substring(currentOffset, currentOffset + deletionSize)); substrate.deleteData(textNode, currentOffset, deletionSize); currentContainer.increaseSize(-deletionSize); } else { if (currentOffset > 0) { int amountToDelete = currentLength - currentOffset; textBuilder.append(substrate.getData(textNode).substring(currentOffset)); substrate.deleteData(textNode, currentOffset, amountToDelete); currentContainer.increaseSize(-amountToDelete); deletionSize -= amountToDelete; currentContainer = currentContainer.getNextContainer(); currentOffset = 0; } while (deletionSize > 0) { textNode = substrate.asText(currentContainer.getValue()); currentLength = substrate.getLength(textNode); if (currentLength <= deletionSize) { textBuilder.append(substrate.getData(textNode)); substrate.removeChild(currentParent, textNode); deleteCurrentContainer(); deletionSize -= currentLength; } else { textBuilder.append(substrate.getData(textNode).substring(0, deletionSize)); substrate.deleteData(textNode, 0, deletionSize); currentContainer.increaseSize(-deletionSize); break; } } } } else { while (deletionSize > 0) { T textNode = substrate.asText(currentContainer.getValue()); assert textNode != null; if (deletionSize < substrate.getLength(textNode)) { // TODO(user): See if we can get rid of this cruft. textBuilder.append(substrate.getData(textNode).substring(0, deletionSize)); substrate.deleteData(textNode, 0, deletionSize); currentContainer.increaseSize(-deletionSize); break; } else { textBuilder.append(substrate.getData(textNode)); deletionSize -= currentContainer.size(); deleteCurrentContainer(); } } } return textBuilder.toString(); } private void doElementEnd() { onElementEnd(); annotations.insert(1); ++currentLocation; currentContainer = currentContainer.getNextContainer(); currentParent = substrate.getParentElement(currentParent); } private void doStartAnnotation(String key, String value) { if (Annotations.isLocal(key)) { throw new IllegalArgumentException("Cannot access local annotations"); } annotations.startAnnotation(key, value); } private void doEndAnnotation(String key) { if (Annotations.isLocal(key)) { throw new IllegalArgumentException("Cannot access local annotations"); } annotations.endAnnotation(key); } private void resetLocation() { currentLocation = 0; currentOffset = 0; currentParent = substrate.getDocumentElement(); currentContainer = offsetList.firstContainer(); } /** * Deletes the current container. */ private void deleteCurrentContainer() { OffsetList.Container<N> nextContainer = currentContainer.getNextContainer(); currentContainer.remove(); currentContainer = nextContainer; } /** * Gets the parent of the DOM node associated with the given container. * * TODO(user): Consider some efficiency improvements. * * @param container The container. * @return The parent of the DOM node associated with the given container. */ private E getParentOf(OffsetList.Container<N> container) { N domNode = container.getValue(); if (domNode != null) { return substrate.getParentElement(domNode); } int counter = 0; container = container.getPreviousContainer(); while (container.getValue() == null) { ++counter; container = container.getPreviousContainer(); } domNode = container.getValue(); E parent = substrate.asElement(domNode); if (parent == null) { parent = substrate.getParentElement(domNode); } for (int i = 0; i < counter; ++i) { parent = substrate.getParentElement(parent); } return parent; } /** * Splits the current container at the current offset. */ private void splitCurrent() { if (currentOffset != 0) { T domNode = substrate.asText(currentContainer.getValue()); T newDomNode = substrate.splitText(domNode, currentOffset); assert newDomNode != null; currentContainer = currentContainer.split(currentOffset, newDomNode); substrate.setIndexingContainer(newDomNode, currentContainer); currentOffset = 0; } } /** * Inserts a DOM node into the indexing structure. * * @param container The container before which to insert. * @param domNode The DOM node to insert. * @param size The size of the inserted node. */ private void insertBefore(OffsetList.Container<N> container, N domNode, int size) { substrate.setIndexingContainer(domNode, container.insertBefore(domNode, size)); } /** * {@inheritDoc} */ @Override public String toString() { return "IndexedDI@" + Integer.toHexString(System.identityHashCode(this)) + "[" + toDebugString() + "]"; } @Override public String toDebugString() { try { return DocOpUtil.toXmlString(DocOpScrub.maybeScrub(toInitialization())); } catch (RuntimeException e) { if (!DocOpScrub.shouldScrubByDefault()) { try { return "#<NO ANNOTATIONS>: " + XmlStringBuilder.innerXml(this) + "# (" + e + ")"; } catch (RuntimeException e2) { return "#<!SUPER BROKEN># (" + e + ")"; } } else { return "#<!BROKEN># (" + e + ")"; } } } /** * {@inheritDoc} */ public String getData(T textNode) { return substrate.getData(textNode); } /** * {@inheritDoc} */ public N getFirstChild(N node) { return substrate.getFirstChild(node); } /** * {@inheritDoc} */ public N getLastChild(N node) { return substrate.getLastChild(node); } /** * {@inheritDoc} */ public int getLength(T textNode) { return substrate.getLength(textNode); } /** * {@inheritDoc} */ public N getNextSibling(N node) { return substrate.getNextSibling(node); } /** * {@inheritDoc} */ public short getNodeType(N node) { return substrate.getNodeType(node); } /** * {@inheritDoc} */ public E getParentElement(N node) { return substrate.getParentElement(node); } /** * {@inheritDoc} */ public N getPreviousSibling(N node) { return substrate.getPreviousSibling(node); } /** * {@inheritDoc} */ public E getDocumentElement() { return substrate.getDocumentElement(); } /** * {@inheritDoc} */ public E asElement(N node) { return substrate.asElement(node); } /** * {@inheritDoc} */ public T asText(N node) { return substrate.asText(node); } /** * {@inheritDoc} */ public boolean isSameNode(N node, N other) { return substrate.isSameNode(node, other); } /** * {@inheritDoc} */ public Map<String,String> getAttributes(E element) { return substrate.getAttributes(element); } /** * {@inheritDoc} */ public String getTagName(E element) { return substrate.getTagName(element); } /** * {@inheritDoc} */ public String getAttribute(E element, String name) { return substrate.getAttribute(element, name); } @Override public int size() { checkSizeConsistency("size"); return offsetList.size(); } private void checkSizeConsistency(String msgPrefix) { // TODO(danilatos/ohler): Make this an assert once we are more confident if (annotations != null && offsetList != null && offsetList.size() != annotations.size()) { throw new RuntimeException(msgPrefix + ": Document and annotations have inconsistent size: " + offsetList.size() + " vs " + annotations.size() + ", respectively"); } } /** * {@inheritDoc} */ public String getAnnotation(int start, String key) { if (Annotations.isLocal(key)) { throw new IllegalArgumentException("Cannot access local annotations"); } return (String) annotations.getAnnotation(start, key); } /** * {@inheritDoc} */ public int firstAnnotationChange(int start, int end, String key, String fromValue) { if (Annotations.isLocal(key)) { throw new IllegalArgumentException("Cannot access local annotations"); } return annotations.firstAnnotationChange(start, end, key, fromValue); } /** * {@inheritDoc} */ public int lastAnnotationChange(int start, int end, String key, String fromValue) { if (Annotations.isLocal(key)) { throw new IllegalArgumentException("Cannot access local annotations"); } return annotations.lastAnnotationChange(start, end, key, fromValue); } void checkValidPersistentKeys(ReadableStringSet keys) { keys.each(new Proc() { @Override public void apply(String key) { Annotations.checkPersistentKey(key); } }); } /** * {@inheritDoc} */ public T splitText(T textNode, int offset) { if (inconsistent) { throw new IllegalStateException("Cannot splitText() during a modification"); } if (offset == 0) { return textNode; } else if (offset >= substrate.getLength(textNode)) { return null; } currentLocation = getLocation(textNode) + offset; offsetList.performActionAt(currentLocation, locationUpdater); splitCurrent(); return asText(currentContainer.getValue()); } /** * {@inheritDoc} */ public T mergeText(T secondSibling) { if (inconsistent) { throw new IllegalStateException("Cannot mergeText() during a modification"); } currentLocation = getLocation(secondSibling); offsetList.performActionAt(currentLocation, locationUpdater); T mergedNode = substrate.mergeText(secondSibling); if (mergedNode != null) { OffsetList.Container<N> previous = currentContainer.getPreviousContainer(); currentContainer.increaseSize(previous.size()); previous.remove(); substrate.setIndexingContainer(mergedNode, currentContainer); } return mergedNode; } @Override public DocInitialization asOperation() { if (size() == 0) { return EmptyDocument.EMPTY_DOCUMENT; } DocOp domOp = serializeDom(); DocOp annotationsOp = serializeAnnotations(); try { final DocOp bothOps; if (performValidation) { bothOps = Composer.compose(domOp, annotationsOp); } else { bothOps = Composer.composeUnchecked(domOp, annotationsOp); } DocInitialization initialisation = DocOpUtil.asInitialization(bothOps); assert DocOpValidator.validate(null, schemaConstraints, initialisation).isValid(); return initialisation; } catch (OperationException e) { throw new OperationRuntimeException("Bug either in indexed document or the composer", e); } } @Override public DocInitialization toInitialization() { return asOperation(); } private DocOp serializeDom() { DocOpBuilder b = new DocOpBuilder(); int depth = 0; for (N node : offsetList) { if (node != null) { T textNode = substrate.asText(node); if (textNode != null) { b.characters(substrate.getData(textNode)); } else { E elementNode = substrate.asElement(node); b.elementStart(substrate.getTagName(elementNode), new AttributesImpl(substrate.getAttributes(elementNode))); depth++; } } else { // To avoid the sentinel depth--; if (depth >= 0) { b.elementEnd(); } } } DocOp domOp = b.buildUnchecked(); assert DocOpValidator.isWellFormed(null, domOp); return domOp; } private DocOp serializeAnnotations() { final AnnotationsNormalizer<DocOp> b = new AnnotationsNormalizer<DocOp>(new UncheckedDocOpBuffer()); AnnotationInterval<Object> last = null; for (AnnotationInterval<Object> i : annotations.annotationIntervals(0, size(), knownKeys())) { i.diffFromLeft().each(new ProcV<Object>() { @Override public void apply(String key, Object value) { assert value == null || value instanceof String; if (value != null) { b.startAnnotation(key, null, (String) value); } else { b.endAnnotation(key); } } }); b.retain(i.length()); last = i; } if (size() > 0) { last.annotations().each(new ProcV<Object>() { @Override public void apply(String key, Object value) { // assert value != null; b.endAnnotation(key); } }); } DocOp annotationsOp = b.finish(); assert DocOpValidator.isWellFormed(null, annotationsOp); return annotationsOp; } @Override public StringSet knownKeys() { final StringSet knownKeys = CollectionUtils.createStringSet(); annotations.knownKeysLive().each(new Proc() { @Override public void apply(String key) { if (!Annotations.isLocal(key)) { knownKeys.add(key); } } }); return knownKeys; } /** * Evaluate the document using the associative operator. * * @return The result of the evaluation */ protected V evaluate() { return offsetList.evaluate(); } /** * @return the "currentParent" element to which children are being added */ private E getCurrentParent() { return currentParent; } /** * @return the current int location */ private int getCurrentLocation() { return currentLocation; } /** * @return the "currentNode" we are in or before */ protected N getCurrentNode() { return currentContainer.getValue(); } @Override public void forEachAnnotationAt(int location, final ReadableStringMap.ProcV<String> callback) { annotations.forEachAnnotationAt(location, new ReadableStringMap.ProcV<Object>() { @Override public void apply(String key, Object value) { if (!Annotations.isLocal(key)) { assert value == null || value instanceof String; callback.apply(key, (String) value); } } }); } /** * {@inheritDoc} */ public AnnotationCursor annotationCursor(int start, int end, ReadableStringSet keys) { if (keys == null) { keys = knownKeys(); } else { checkValidPersistentKeys(keys); } return annotations.annotationCursor(start, end, keys); } @Override public Iterable<AnnotationInterval<String>> annotationIntervals(int start, int end, ReadableStringSet keys) { if (keys == null) { keys = knownKeys(); } else { checkValidPersistentKeys(keys); } final Iterable<AnnotationInterval<Object>> iterable = annotations.annotationIntervals(start, end, keys); return new Iterable<AnnotationInterval<String>>() { @Override public Iterator<AnnotationInterval<String>> iterator() { final Iterator<AnnotationInterval<Object>> iterator = iterable.iterator(); return new Iterator<AnnotationInterval<String>>() { @Override public boolean hasNext() { return iterator.hasNext(); } // SuppressWarnings because of conversion from RSMap<Object> to <String>, already checked // keys are persistent keys using checkValidPersistentKeys(), and this class has // an invariant that such keys only have string values. (Local annotation set views // disallow setting any values for non-local keys). @SuppressWarnings("unchecked") @Override public AnnotationInterval<String> next() { AnnotationInterval<Object> rawInterval = iterator.next(); int start = rawInterval.start(); int end = rawInterval.end(); ReadableStringMap annotations = rawInterval.annotations(); ReadableStringMap diffFromLeft = rawInterval.diffFromLeft(); return new AnnotationIntervalImpl<String>(start, end, annotations, diffFromLeft); } @Override public void remove() { iterator.remove(); } }; } }; } @Override public Iterable<RangedAnnotation<String>> rangedAnnotations(int start, int end, ReadableStringSet keys) { if (keys == null) { keys = knownKeys(); } else { checkValidPersistentKeys(keys); } final Iterable<RangedAnnotation<Object>> iterable = annotations.rangedAnnotations(start, end, keys); return new Iterable<RangedAnnotation<String>>() { @Override public Iterator<RangedAnnotation<String>> iterator() { final Iterator<RangedAnnotation<Object>> iterator = iterable.iterator(); return new Iterator<RangedAnnotation<String>>() { @Override public boolean hasNext() { return iterator.hasNext(); } @Override public RangedAnnotation<String> next() { RangedAnnotation<Object> rawRange = iterator.next(); String key = rawRange.key(); // Safe cast because already checked with checkValidPersistentKeys(); String value = (String) rawRange.value(); int start = rawRange.start(); int end = rawRange.end(); return new RangedAnnotationImpl<String>(key, value, start, end); } @Override public void remove() { iterator.remove(); } }; } }; } @Override public String toXmlString() { return DocOpUtil.toXmlString(asOperation()); } protected void beforeBegin() { } protected void afterFinish() { } protected void onElementStart(E element) { } protected void onElementEnd() { } protected void onDeleteElementStart(int location, E element) { } protected void onDeleteElementEnd() { } protected void onModifyAttributes(E element, Attributes oldAttributes, Attributes newAttributes) { } protected void onModifyAttributes(E element, AttributesUpdate update) { } protected void onCharacters(int location, String characters) { } protected void onDeleteCharacters(int location, String characters) { } public DocumentSchema getSchema() { return schemaConstraints; } }