/** * 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.operation; import org.waveprotocol.wave.model.document.indexed.IndexedDocument; import org.waveprotocol.wave.model.util.Preconditions; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; /** * A non-invertible document operation * */ public final class Nindo { public interface NindoCursor { void begin(); void finish(); void characters(String s); void elementStart(String type, Attributes attrs); void elementEnd(); void startAnnotation(String key, String value); void endAnnotation(String key); // skip to end is not required void skip(int n); void deleteCharacters(int n); void deleteElementStart(); void deleteElementEnd(); void replaceAttributes(Attributes attrs); void updateAttributes(Map<String, String> attrUpdate); } /** * A builder for Nindo objects. */ public static class Builder implements NindoCursor { private final List<MutationComponent> mutationList = new ArrayList<MutationComponent>(); public void skip(int skipSize) { assert skipSize > 0; mutationList.add(new Skip(skipSize)); } public void characters(String characters) { if (!characters.isEmpty()) { mutationList.add(new Characters(characters)); } } public void elementStart(String tagName, Attributes attributes) { assert attributes != null; mutationList.add(new ElementStart(tagName, attributes)); } public void elementEnd() { mutationList.add(ElementEnd.INSTANCE); } public void deleteCharacters(int deletionSize) { assert deletionSize > 0; mutationList.add(new DeleteCharacters(deletionSize)); } public void deleteElementStart() { mutationList.add(DeleteElementStart.INSTANCE); } public void deleteElementEnd() { mutationList.add(DeleteElementEnd.INSTANCE); } public void replaceAttributes(Attributes attributes) { mutationList.add(new ReplaceAttributes(attributes)); } public void updateAttributes(Map<String, String> attributes) { mutationList.add(new UpdateAttributes(attributes)); } public void startAnnotation(String key, String value) { mutationList.add(new StartAnnotation(key, value)); } public void endAnnotation(String key) { mutationList.add(new EndAnnotation(key)); } /** * Obtains a DocumentMutation from the builder. * * This method may do some error checking on the mutation and throw an * exception if it is ill-formed. * * @return The built DocumentMutation. */ public Nindo build() { return new Nindo(mutationList); } private Nindo buildWithoutTrailingSkip() { int size = mutationList.size(); if (size > 0 && mutationList.get(size - 1) instanceof Skip) { mutationList.remove(size - 1); } return build(); } boolean hasBeenUsed = false; @Override public void begin() { Preconditions.checkState(!hasBeenUsed, "Cannot reuse a builder"); hasBeenUsed = true; } @Override public void finish() { // Do nothing. build() actually builds. } } /** * A builder for applying annotations for a single key to a document in a linear order. * * This builder will generate the minimal mutation sequence to apply the requested * annotations. */ public static class AnnotationBuilder<N, E extends N, T extends N> { /** The builder we will use to construct the annotation */ private final Builder b = new Builder(); /** The document we will be applying the ops to */ private final IndexedDocument<N,E,T> doc; /** The key that we are applying annotations for */ private final String key; /** Accumulated skips that are yet to be applied */ private int skipAccum; /** Current position in the range */ private int currentPos; /** The limit of the range that we are applying to */ private final int rangeEnd; /** Whether we have actually applied anything yet */ private boolean dirty = false; /** * Construct a new annotation builder. * * @param doc indexed doc to apply the annotations to * @param rangeStart document location to begin applying annotations from * @param rangeEnd document location to apply annotations up to * @param key key to apply annotations for */ public AnnotationBuilder(IndexedDocument<N,E,T> doc, int rangeStart, int rangeEnd, String key) { this.doc = doc; this.skipAccum = rangeStart; this.currentPos = rangeStart; this.rangeEnd = rangeEnd; this.key = key; } /** * Sets the annotation to the given value up to the given location. * * @param value value to set for the annotation * @param end absolute location to set up to */ public void setUpTo(String value, int end) { Preconditions.checkPositionIndexes(currentPos, end, rangeEnd); while (currentPos < end) { String currentValue = doc.getAnnotation(currentPos, key); int nextChange = doc.firstAnnotationChange(currentPos, end, key, currentValue); if (nextChange == -1) { nextChange = end; } int size = nextChange - currentPos; if (size > 0) { boolean applyAnnotation = false; if (value == null) { // We want to apply a nullify annotation if the currentValue isn't // already null if (currentValue != null) { applyAnnotation = true; } } else { // We want to apply the value if the currentValue is different if ((currentValue == null) || (!currentValue.equals(value))) { applyAnnotation = true; } } if (applyAnnotation) { if (skipAccum > 0) { b.skip(skipAccum); skipAccum = 0; } b.startAnnotation(key, value); b.skip(size); b.endAnnotation(key); dirty = true; } else { skipAccum += size; } } currentPos = nextChange; } assert (currentPos == end); } /** * Clears the annotation up to the given point. * * Same effect as <code>setUpTo(null, end)</code>. * * @param end location to nullify up to */ public void clearUpTo(int end) { setUpTo(null, end); } /** @return the current position we have applied up to */ public int getCurrentPos() { return currentPos; } /** @return true if we have build an annotation to apply */ public boolean getDirty() { return dirty; } /** @return the built nindo */ public Nindo build() { return b.build(); } } /** * A component of the nindo. */ private static abstract class MutationComponent { abstract void apply(NindoCursor document); } /** * A "skip" mutation component. */ private static class Skip extends MutationComponent { final int skipSize; Skip(int skipSize) { this.skipSize = skipSize; } @Override void apply(NindoCursor document) { document.skip(skipSize); } } /** * A "characters" mutation component. */ private static class Characters extends MutationComponent { final String characters; Characters(String characters) { this.characters = characters; } @Override void apply(NindoCursor document) { document.characters(characters); } } /** * An "elementStart" mutation component. */ private static class ElementStart extends MutationComponent { final String tagName; final Attributes attributes; ElementStart(String tagName, Attributes attributes) { this.tagName = tagName; this.attributes = attributes; } @Override void apply(NindoCursor document) { document.elementStart(tagName, attributes); } } /** * An "elementEnd" mutation component. */ private static class ElementEnd extends MutationComponent { static final ElementEnd INSTANCE = new ElementEnd(); // Defining this private constructor doesn't achieve any extra privacy, but // is a hint that this class is intended to have a singleton instance. private ElementEnd() {} @Override void apply(NindoCursor document) { document.elementEnd(); } } /** * A "deleteCharacters" mutation component. */ private static class DeleteCharacters extends MutationComponent { final int deletionSize; DeleteCharacters(int deletionSize) { this.deletionSize = deletionSize; } @Override void apply(NindoCursor document) { document.deleteCharacters(deletionSize); } } /** * A "deleteElementEnd" mutation component. */ private static class DeleteElementStart extends MutationComponent { static final DeleteElementStart INSTANCE = new DeleteElementStart(); // Defining this private constructor doesn't achieve any extra privacy, but // is a hint that this class is intended to have a singleton instance. private DeleteElementStart() {} @Override void apply(NindoCursor document) { document.deleteElementStart(); } } /** * A "deleteElementEnd" mutation component. */ private static class DeleteElementEnd extends MutationComponent { static final DeleteElementEnd INSTANCE = new DeleteElementEnd(); // Defining this private constructor doesn't achieve any extra privacy, but // is a hint that this class is intended to have a singleton instance. private DeleteElementEnd() {} @Override void apply(NindoCursor document) { document.deleteElementEnd(); } } /** * A "setAttributes" mutation component. */ private static class ReplaceAttributes extends MutationComponent { final Attributes attributes; ReplaceAttributes(Attributes attributes) { this.attributes = attributes; } @Override void apply(NindoCursor document) { document.replaceAttributes(attributes); } } /** * An "updateAttributes" mutation component. */ private static class UpdateAttributes extends MutationComponent { final Map<String, String> attributesUpdate; UpdateAttributes(Map<String, String> update) { // TODO(danilatos): This is unsafe, and not immutable. this.attributesUpdate = update; } @Override void apply(NindoCursor document) { document.updateAttributes(attributesUpdate); } } /** * A "startAnnotation" mutation component. */ private static class StartAnnotation extends MutationComponent { final String key; final String value; StartAnnotation(String key, String value) { Preconditions.checkNotNull(key, "Null annotation key"); this.key = key; this.value = value; } @Override void apply(NindoCursor document) { document.startAnnotation(key, value); } } /** * A "endAnnotation" mutation component. */ private static class EndAnnotation extends MutationComponent { final String key; EndAnnotation(String key) { Preconditions.checkNotNull(key, "Null annotation key"); this.key = key; } @Override void apply(NindoCursor document) { document.endAnnotation(key); } } private final List<MutationComponent> mutationList; private Nindo(List<MutationComponent> mutationList) { this.mutationList = new ArrayList<MutationComponent>(mutationList); } /** * Creates a new nindo builder. */ public static Builder builder() { return new Builder(); } /** * Addes a skip of the given size to the start of the nindo if distance is * positive, reduces initial skips if the distance is negative. * * @return a new nindo */ public static Nindo shift(final int distance, Nindo nindo) { int remaining = Math.max(0, -distance); Builder b = builder(); if (distance > 0) { b.skip(distance); } for (MutationComponent c : nindo.mutationList) { if (remaining > 0) { if (c instanceof Skip) { int skipSize = ((Skip) c).skipSize; int diff = Math.min(remaining, skipSize); remaining -= diff; if (diff < skipSize) { b.skip(skipSize - diff); } } else { Preconditions.illegalArgument("Not enough initial skips in nindo to take off"); } } else { c.apply(b); } } return b.build(); } /** * Creates a new annotation builder. * * @param doc indexed doc to apply the annotations to * @param rangeStart document location to begin applying annotations from * @param rangeEnd document location to apply annotations up to * @param key key to apply annotations for */ public static <N, E extends N, T extends N> AnnotationBuilder<N, E, T> annotationBuilder( IndexedDocument<N,E,T> doc, int rangeStart, int rangeEnd, String key) { return new AnnotationBuilder<N, E, T>(doc, rangeStart, rangeEnd, key); } /** * Creates a document mutation that inserts the given characters at the given * location. * * @param location The location at which to insert characters. * @param characters The characters to insert. * @return The document mutation. */ public static Nindo insertCharacters(int location, String characters) { assert !characters.isEmpty(); List<MutationComponent> mutationList = new ArrayList<MutationComponent>(2); mutationList.add(new Skip(location)); mutationList.add(new Characters(characters)); return new Nindo(mutationList); } /** * Creates a document mutation that inserts an element at the given location. * * @param location The location at which to insert the element. * @param tagName The tag name of the element. * @param attributes The attributes of the element. * @return The document mutation. */ public static Nindo insertElement(int location, String tagName, Attributes attributes) { List<MutationComponent> mutationList = new ArrayList<MutationComponent>(4); if (location > 0) { mutationList.add(new Skip(location)); } mutationList.add(new ElementStart(tagName, attributes)); mutationList.add(ElementEnd.INSTANCE); return new Nindo(mutationList); } /** * Creates a document mutation that deletes the characters denoted by the * given range. * * @param start The start of the range of the characters to delete. * @param end The end of the range of the characters to delete. * @return The document mutation. */ public static Nindo deleteCharacters(int start, int end) { List<MutationComponent> mutationList = new ArrayList<MutationComponent>(2); mutationList.add(new Skip(start)); mutationList.add(new DeleteCharacters(end - start)); return new Nindo(mutationList); } /** * Creates a document mutation that deletes an empty element at a given * location. * * @param elementLocation The location of the empty element to delete. * @return The document mutation. */ public static Nindo deleteElement(int elementLocation) { List<MutationComponent> mutationList = new ArrayList<MutationComponent>(2); // It may be impossible to delete the root element, but let's check // that elsewhere. if (elementLocation > 0) { mutationList.add(new Skip(elementLocation)); } mutationList.add(DeleteElementStart.INSTANCE); mutationList.add(DeleteElementEnd.INSTANCE); return new Nindo(mutationList); } /** * Creates a document mutation that sets all the attributes of an element. * * @param location The location of the element whose attributes are to be set. * @param attributes The attributes that the element should have. * @return The document mutation. */ public static Nindo replaceAttributes(int location, Attributes attributes) { List<MutationComponent> mutationList = new ArrayList<MutationComponent>(2); if (location > 0) { mutationList.add(new Skip(location)); } mutationList.add(new ReplaceAttributes(attributes)); return new Nindo(mutationList); } /** * Creates a document mutation that sets an attribute of an element. * * @param location The location of the element whose attribute is to be set. * @param name The name of the attribute to set. * @param value The value to which to set the attribute. * @return The document mutation. */ public static Nindo setAttribute(int location, String name, String value) { List<MutationComponent> mutationList = new ArrayList<MutationComponent>(2); if (location > 0) { mutationList.add(new Skip(location)); } mutationList.add(new UpdateAttributes(Collections.singletonMap(name, value))); return new Nindo(mutationList); } /** * Creates a document mutation that removes an attribute of an element. * * @param location The location of the element whose attribute is to be * removed. * @param name The name of the attribute to remove. * @return The document mutation. */ public static Nindo removeAttribute(int location, String name) { List<MutationComponent> mutationList = new ArrayList<MutationComponent>(2); if (location > 0) { mutationList.add(new Skip(location)); } mutationList.add(new UpdateAttributes(Collections.singletonMap(name, (String)null))); return new Nindo(mutationList); } /** * Creates a document mutation that sets an annotation over a range. * * @param start The location of the start of the range on which the annotation * is to be set. * @param end The location of the end of the range on which the annotation is * to be set. * @param key The annotation key. * @param value The annotation value. * @return The document mutation. */ public static Nindo setAnnotation(int start, int end, String key, String value) { List<MutationComponent> mutationList = new ArrayList<MutationComponent>(0); if (start != end) { Preconditions.checkPositionIndexes(start, end, Integer.MAX_VALUE); if (start > 0) { mutationList.add(new Skip(start)); } mutationList.add(new StartAnnotation(key, value)); mutationList.add(new Skip(end - start)); mutationList.add(new EndAnnotation(key)); } return new Nindo(mutationList); } static boolean printing = false; /** * Applies this document mutation to the given document. * * @param document The document on which to apply the document mutation. */ public void apply(NindoCursor document) { document.begin(); if (!printing) { printing = true; //System.out.println(this); printing = false; } int i = 0; for (MutationComponent mutationComponent : mutationList) { i++; if (!printing) { //System.out.println(i + "/" + mutationList.size() + " - " + mutationComponent); } mutationComponent.apply(document); } document.finish(); } // /** // * @return the number of components in this mutation. // */ // public int getComponentSize() { // return mutationList.size(); // } @Override public String toString() { final StringBuilder builder = new StringBuilder(); apply(new NindoCursor() { public void begin() { builder.append("{"); } public void finish() { builder.append("}"); } public void skip(int skipSize) { builder.append("__" + skipSize + "; "); } public void characters(String characters) { builder.append("++" + toLiteral(characters) + "; "); } public void elementStart(String tagName, Attributes attributes) { builder.append("<< " + tagName + " " + attributes + "; "); } public void elementEnd() { builder.append(">>; "); } public void deleteCharacters(int deletionSize) { builder.append("-- " + deletionSize + "; "); } public void deleteElementStart() { builder.append("x<; "); } public void deleteElementEnd() { builder.append("x>; "); } public void replaceAttributes(Attributes attributes) { builder.append("s@ " + attributes + "; "); } public void updateAttributes(Map<String, String> attributes) { builder.append("u@ " + attributes + "; "); } public void startAnnotation(String key, String value) { builder.append("(( " + key + "=" + (value == null ? "null" : toLiteral(value)) + "; "); } public void endAnnotation(String key) { builder.append(")) " + key + "; "); } private String toLiteral(String string) { return "\"" + string.replace("\\", "\\\\").replace("\"", "\\\"") + "\""; } @Override public String toString() { return builder.toString(); } }); return builder.toString(); } public static Nindo fromDocOp(final DocOp docOp, boolean removeTrailingSkip) { final Builder b = new Builder(); docOp.apply(new DocOpCursor() { @Override public void deleteCharacters(String chars) { b.deleteCharacters(chars.length()); } @Override public void deleteElementEnd() { b.deleteElementEnd(); } @Override public void deleteElementStart(String type, Attributes attrs) { b.deleteElementStart(); } @Override public void replaceAttributes(Attributes oldAttrs, Attributes newAttrs) { b.replaceAttributes(newAttrs); } @Override public void retain(int itemCount) { b.skip(itemCount); } @Override public void updateAttributes(AttributesUpdate attrUpdate) { Map<String, String> updates = new HashMap<String, String>(); for (int i = 0; i < attrUpdate.changeSize(); i++) { updates.put(attrUpdate.getChangeKey(i), attrUpdate.getNewValue(i)); } b.updateAttributes(updates); } @Override public void annotationBoundary(AnnotationBoundaryMap map) { for (int i = 0; i < map.endSize(); i++) { b.endAnnotation(map.getEndKey(i)); } for (int i = 0; i < map.changeSize(); i++) { b.startAnnotation(map.getChangeKey(i), map.getNewValue(i)); } } @Override public void characters(String chars) { b.characters(chars); } @Override public void elementEnd() { b.elementEnd(); } @Override public void elementStart(String type, Attributes attrs) { b.elementStart(type, attrs); } }); return removeTrailingSkip ? b.buildWithoutTrailingSkip() : b.build(); } }