/** * 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.indexed; import org.waveprotocol.wave.model.document.operation.AttributesUpdate; import org.waveprotocol.wave.model.util.ValueUtils; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.Stack; /** * Document event algebraic type. * * WARNING(danilatos): .equals() equality may result in distinct events * being treated as equal in some circumstances, such as two bits of deleted * content that lived adjacently and had identical xml. Two possible ways to * resolve this is to give them a sequence number, or just use object identity * for equality. Either would be inconsistent with the nice behaviour of * .equals() for the inserting methods. I'm not sure what the best answer is, * but for now it means DON'T put the events in a HashSet or similar. * * @author danilatos@google.com (Daniel Danilatos) * * @param <N> * @param <E> * @param <T> */ public abstract class DocumentEvent<N, E extends N, T extends N> { public enum Type { ATTRIBUTES, TEXT_INSERTED, CONTENT_INSERTED, TEXT_DELETED, CONTENT_DELETED, ANNOTATION_CHANGED, } private final Type type; private DocumentEvent(Type type) { this.type = type; } public Type getType() { return type; } /** * Describes element attribute modifications * * Redundant changes (where an attribute is set to its same value) are omitted */ public static final class AttributesModified<N, E extends N, T extends N> extends DocumentEvent<N, E, T> { private final E element; private final Map<String, String> oldValues; private final Map<String, String> newValues; public AttributesModified(E element, Map<String, String> oldValues, Map<String, String> newValues) { super(Type.ATTRIBUTES); this.element = element; HashMap<String, String> oldV = new HashMap<String, String>(oldValues); HashMap<String, String> newV = new HashMap<String, String>(newValues); List<String> keysToRemove = new ArrayList<String>(); for (Map.Entry<String, String> entry : oldV.entrySet()) { String key = entry.getKey(); if (!newV.containsKey(key)) { newV.put(key, null); } else if (ValueUtils.equal(newV.get(key), entry.getValue())) { newV.remove(key); keysToRemove.add(key); } } for (String key : keysToRemove) { oldV.remove(key); } for (Map.Entry<String, String> entry : newV.entrySet()) { if (!oldV.containsKey(entry.getKey())) { oldV.put(entry.getKey(), null); } } this.oldValues = Collections.unmodifiableMap(oldV); this.newValues = Collections.unmodifiableMap(newV); } public AttributesModified(E element, AttributesUpdate update) { super(Type.ATTRIBUTES); this.element = element; HashMap<String, String> oldV = new HashMap<String, String>(); HashMap<String, String> newV = new HashMap<String, String>(); for (int i = 0; i < update.changeSize(); i++) { String oldValue = update.getOldValue(i); String newValue = update.getNewValue(i); if (ValueUtils.notEqual(newValue, oldValue)) { oldV.put(update.getChangeKey(i), oldValue); newV.put(update.getChangeKey(i), newValue); } } this.oldValues = Collections.unmodifiableMap(oldV); this.newValues = Collections.unmodifiableMap(newV); } public E getElement() { return element; } /** * Returns a map of names to their previous values. * * The map contains only attributes that changed, and for newly added attributes, * the "old" value will be presented as null. * * @return map of names to old values */ // TODO(danilatos): Change getOldValues() and getNewValues() to an attributes update map? public Map<String, String> getOldValues() { return oldValues; } /** * Returns a map of names to their current values. * * The map contains only attributes that changed, and for removed attributes, * the new "value" will be presented as null. * * TODO(danilatos): Remove this method? It is convenient, but redundant with * respect to {@link #getOldValues()} and {@link #getElement()}. * Alternatively, the new values could be lazily computed. * * @return map of names to old values */ public Map<String, String> getNewValues() { return newValues; } /** * Returns the set of attribute names whose values have changed. * * This is equivalent to the key set of either {@link #getNewValues()} * or {@link #getOldValues()} (they are the same). * * TODO(danilatos): Remove this method? It is redundant but possibly convenient. * * @return set of attribute names whose values have changed */ public Set<String> getChangedAttributes() { return oldValues.keySet(); } // eclipse generated, clean up later @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((element == null) ? 0 : element.hashCode()); result = prime * result + ((newValues == null) ? 0 : newValues.hashCode()); result = prime * result + ((oldValues == null) ? 0 : oldValues.hashCode()); return result; } @SuppressWarnings("unchecked") @Override public boolean equals(Object obj) { if (this == obj) return true; if (!(obj instanceof AttributesModified)) return false; AttributesModified other = (AttributesModified) obj; if (element == null) { if (other.element != null) return false; } else if (!element.equals(other.element)) return false; if (newValues == null) { if (other.newValues != null) return false; } else if (!newValues.equals(other.newValues)) return false; if (oldValues == null) { if (other.oldValues != null) return false; } else if (!oldValues.equals(other.oldValues)) return false; return true; } @Override public String toString() { return "A:" + oldValues + "->" + newValues; } } /** * Event describing top-level text insertion (no structural content) */ public static final class TextInserted<N, E extends N, T extends N> extends DocumentEvent<N, E, T> { public final int location; public final String insertedText; public TextInserted(int location, String insertedText) { super(Type.TEXT_INSERTED); this.location = location; this.insertedText = insertedText; } public int getLocation() { return location; } public String getInsertedText() { return insertedText; } // eclipse generated, clean up later @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((insertedText == null) ? 0 : insertedText.hashCode()); result = prime * result + location; return result; } @SuppressWarnings("unchecked") @Override public boolean equals(Object obj) { if (this == obj) return true; if (!(obj instanceof TextInserted)) return false; TextInserted other = (TextInserted) obj; if (insertedText == null) { if (other.insertedText != null) return false; } else if (!insertedText.equals(other.insertedText)) return false; if (location != other.location) return false; return true; } @Override public String toString() { return "TI:" + insertedText + "@" + location; } } /** * Event describing structural content being inserted. May contain text, * and any such nested text will not be reported in a TextInserted event. */ public static final class ContentInserted<N, E extends N, T extends N> extends DocumentEvent<N, E, T> { private final E element; public ContentInserted(E element) { super(Type.CONTENT_INSERTED); this.element = element; } public E getSubtreeElement() { return element; } // eclipse generated, clean up later @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((element == null) ? 0 : element.hashCode()); return result; } @SuppressWarnings("unchecked") @Override public boolean equals(Object obj) { if (this == obj) return true; if (!(obj instanceof ContentInserted)) return false; ContentInserted other = (ContentInserted) obj; if (element == null) { if (other.element != null) return false; } else if (!element.equals(other.element)) return false; return true; } @Override public String toString() { return "CI:" + element; } } /** * Event describing top-level text deletion (no structural content) */ public static final class TextDeleted<N, E extends N, T extends N> extends DocumentEvent<N, E, T> { public final int location; public final String deletedText; public TextDeleted(int location, String deletedText) { super(Type.TEXT_DELETED); this.location = location; this.deletedText = deletedText; } public int getLocation() { return location; } public String getDeletedText() { return deletedText; } // eclipse generated, clean up later @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((deletedText == null) ? 0 : deletedText.hashCode()); result = prime * result + location; return result; } @SuppressWarnings("unchecked") @Override public boolean equals(Object obj) { if (this == obj) return true; if (!(obj instanceof TextDeleted)) return false; TextDeleted other = (TextDeleted) obj; if (deletedText == null) { if (other.deletedText != null) return false; } else if (!deletedText.equals(other.deletedText)) return false; if (location != other.location) return false; return true; } @Override public String toString() { return "TD:" + deletedText + "@" + location; } } /** * Event describing structural content being deleted. May deleted text, * and any such nested text will not be reported in a TextDeleted event. */ public static final class ContentDeleted<N, E extends N, T extends N> extends DocumentEvent<N, E, T> { public static enum TokenType { TEXT, ELEMENT_START, ELEMENT_END } /** * Representation of a single part of the deleted content. Each removed * element start tag, text string, and element end tag is represented by a * single Token. */ public static final class Token { private final TokenType type; private final String tagName; private final Map<String, String> attributes; private final String text; private Token(TokenType type, String tagName, Map<String, String> attributes, String text) { this.type = type; this.tagName = tagName; this.attributes = attributes; this.text = text; } static Token textToken(String text) { return new Token(TokenType.TEXT, null, null, text); } static Token elementStartToken(String tagName, Map<String, String> attributes) { return new Token(TokenType.ELEMENT_START, tagName, attributes, null); } static Token elementEndToken(String tagName) { return new Token(TokenType.ELEMENT_END, tagName, null, null); } public TokenType getType() { return type; } public String getTagName() { return tagName; } public Map<String, String> getAttributes() { return attributes; } public String getText() { return text; } @Override public String toString() { if (text != null) { return text; } else if (attributes != null) { StringBuilder b = new StringBuilder(); b.append("<" + tagName); for (String key : attributes.keySet()) { b.append(" " + key + "=" + attributes.get(key)); } b.append(">"); return b.toString(); } return "</" + tagName + ">"; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((attributes == null) ? 0 : attributes.hashCode()); result = prime * result + ((tagName == null) ? 0 : tagName.hashCode()); result = prime * result + ((text == null) ? 0 : text.hashCode()); result = prime * result + ((type == null) ? 0 : type.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (!(obj instanceof Token)) return false; Token other = (Token) obj; if (attributes == null) { if (other.attributes != null) return false; } else if (!attributes.equals(other.attributes)) return false; if (tagName == null) { if (other.tagName != null) return false; } else if (!tagName.equals(other.tagName)) return false; if (text == null) { if (other.text != null) return false; } else if (!text.equals(other.text)) return false; if (type == null) { if (other.type != null) return false; } else if (!type.equals(other.type)) return false; return true; } } public final int location; private final int size; private final List<Token> tokens; private final E root; public static final class Builder<N, E extends N, T extends N> { private final int start; private int size; private final List<Token> tokens; private final Stack<String> tagNames; private final E root; public Builder(int start, E root) { this.start = start; this.size = 0; this.tokens = new ArrayList<Token>(); this.tagNames = new Stack<String>(); this.root = root; } public ContentDeleted<N, E, T> build() { return new ContentDeleted<N, E, T>(start, size, tokens, root); } public void addText(String text) { tokens.add(Token.textToken(text)); this.size += text.length(); } public void addElementStart(String tagName, Map<String, String> elements) { tokens.add(Token.elementStartToken(tagName, elements)); tagNames.add(tagName); this.size++; } public void addElementEnd() { tokens.add(Token.elementEndToken(tagNames.pop())); this.size++; } } ContentDeleted(int location, int size, List<Token> tokens, E root) { super(Type.CONTENT_DELETED); this.location = location; this.size = size; assert tokens != null; this.tokens = tokens; this.root = root; } /** Location in the NEW document where the deletion occurred. */ public int getLocation() { return location; } /** * The topmost element that was deleted. */ public E getRoot() { return root; } public int getItemSize() { return size; } public Iterable<Token> getDeletedTokens() { return tokens; } // eclipse generated, clean up later @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + location; result = prime * result + size; result = prime * result + tokens.hashCode(); return result; } @SuppressWarnings("unchecked") @Override public boolean equals(Object obj) { if (this == obj) return true; if (!(obj instanceof ContentDeleted)) return false; ContentDeleted other = (ContentDeleted) obj; if (location != other.location) return false; if (size != other.size) return false; if (!tokens.equals(other.tokens)) return false; return true; } @Override public String toString() { String content = "CD:" + "@" + location + "-" + size + " ["; for (Token token : tokens) { content += token.toString(); } content += "]"; return content; } } /** * Event describing an annotation change. */ public static final class AnnotationChanged<N, E extends N, T extends N> extends DocumentEvent<N, E, T> { /** Start of the changed range */ public final int start; /** End of the changed range (one past the last affected item) */ public final int end; /** Key with changed value */ public final String key; /** New value for the key over the range */ public final String newValue; public AnnotationChanged(int start, int end, String key, String newValue) { super(Type.ANNOTATION_CHANGED); this.start = start; this.end = end; this.key = key; this.newValue = newValue; } @Override public String toString() { return "AC:@(" + start + "," + end + "):[" + key + ":" + newValue + "]"; } } }