/** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.waveprotocol.wave.model.document.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.OffsetPoint.Finder; import org.waveprotocol.wave.model.document.operation.DocOp; import org.waveprotocol.wave.model.document.util.AnnotationIntervalImpl; import org.waveprotocol.wave.model.document.util.GenericAnnotationCursor; import org.waveprotocol.wave.model.document.util.GenericAnnotationIntervalIterable; import org.waveprotocol.wave.model.operation.OpCursorException; 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.OffsetList.Container; import org.waveprotocol.wave.model.util.Pair; 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 java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.NoSuchElementException; /** * Simple implementation of an annotation set. * * The implementation is based on a single {@link OffsetList}, with each * container in the list representing a contiguous range of identical key-value * pairs. * * @author danilatos@google.com (Daniel Danilatos) */ public class SimpleAnnotationSet implements RawAnnotationSet<Object> { /** * "Immutable" map of values (can be abused, but please don't). */ private static final class Values { private static final Values EMPTY = new Values(); /** Cached hash value, used for fast equality comparison */ private final int hash; /** The values */ private final Map<String, Object> map; Values() { map = Collections.emptyMap(); hash = map.hashCode(); } Values(Values other, Changes changes) { map = new HashMap<String, Object>(other.map); for (Map.Entry<String, Pair<Integer, Object>> entry : changes.entrySet()) { if (entry.getValue() == null) { map.remove(entry.getKey()); } else { map.put(entry.getKey(), entry.getValue().getSecond()); } } hash = map.hashCode(); } Object get(String key) { return map.get(key); } @Override public boolean equals(Object obj) { if (obj == null) { return false; } else if (obj instanceof Values) { Values v = (Values) obj; if (v.hash != hash) { return false; } else { return map.equals(v.map); } } else { return false; } } @Override public int hashCode() { return hash; } @Override public String toString() { return map.toString(); } } /** Convenience type alias */ @SuppressWarnings("serial") private static class Changes extends HashMap<String, Pair<Integer, Object>> { } /** * Represents a notification for our listeners, which we bunch up until the * end of a mutation. */ private class Notification { int start; int end; String key; Object value; Notification(int start, int end, String key, Object value) { this.start = start; this.end = end; this.key = key; this.value = value; } void exe() { listener.onAnnotationChange(start, end, key, value); } } /** * Handy interface for traversing the list. Changes may be made to it in the * called methods, but only to the given container or previous containers. The * next and following containers may not be touched. */ private interface ContainerTraverser { /** Called when the whole container is being traversed */ void wholeContainer(OffsetList.Container<Values> container); /** Called when a portion of the container is being traversed */ void partialContainer(OffsetList.Container<Values> container, int startOffset, int endOffset); /** * Called after the traversal is finished, with the container that would * have been traversed next */ void finished(OffsetList.Container<Values> missedContainer); } /** * Handy finder instance. */ private final Finder<Values> finder = OffsetPoint.finder(); /** * Listener for changes. */ private final AnnotationSetListener<Object> listener; /** * List of annotation ranges. */ private final OffsetList<Values> ranges; /** * Current location during a mutation from the streaming interface. */ private int currentLocation; /** * The current key-value pairs being "painted" during a mutation as the * currentLocation progresses. */ private Changes currentChanges; /** * Values to the left of the current location, ignoring the effects of * annotation changes. */ private Values oldValues; /** * Notifications being batched to be sent to the listener at the end of a * modification */ private List<Notification> notifications; /** * @param listener listener for changes. */ public SimpleAnnotationSet(AnnotationSetListener<Object> listener) { this.listener = listener; ranges = new EvaluableOffsetList<Values, Void>(); } @Override public void begin() { reset(); notifications = new ArrayList<Notification>(); } @Override public void finish() { // For good measure reset(); for (Notification notification : notifications) { notification.exe(); } } private void reset() { currentChanges = new Changes(); oldValues = Values.EMPTY; currentLocation = 0; } @Override public void skip(int skipSize) { if (skipSize > size() - currentLocation) { throw new OpCursorException("attempt to skip beyond end of document (cursor at " + currentLocation + ", size is " + size() + ", distance is " + skipSize + ")"); } traverse(skipTraverser, skipSize); currentLocation += skipSize; } /** * Traverser used by skip, which "paints" the current key-value pairs into the * data structure, also handling merging and splitting of containers where * appropriate. */ private final ContainerTraverser paintingTraverser = new ContainerTraverser() { public void partialContainer(OffsetList.Container<Values> container, int startOffset, int endOffset) { Values newValue = applyChanges(container.getValue()); if (newValue.equals(container.getValue())) { // Don't split it up if the change has no effect to this container return; } if (startOffset > 0) { container = container.split(startOffset, container.getValue()); } int size = endOffset - startOffset; if (size < container.size()) { container.split(size, container.getValue()); container.setValue(newValue); } else { container.setValue(newValue); } } public void wholeContainer(OffsetList.Container<Values> container) { container.setValue(applyChanges(container.getValue())); maybeMergeWithPrevious(container); } public void finished(OffsetList.Container<Values> missedContainer) { maybeMergeWithPrevious(missedContainer); } }; private final ContainerTraverser skipTraverser = new ContainerTraverser() { public void partialContainer(Container<Values> container, int startOffset, int endOffset) { oldValues = container.getValue(); paintingTraverser.partialContainer(container, startOffset, endOffset); } public void wholeContainer(Container<Values> container) { oldValues = container.getValue(); paintingTraverser.wholeContainer(container); } public void finished(Container<Values> missedContainer) { paintingTraverser.finished(missedContainer); } }; @Override public void insert(int insertSize) { // Semantics: inserts always push an annotation boundary. OffsetPoint<Values> p = ranges.performActionAt(currentLocation, finder); OffsetList.Container<Values> current = p.getContainer(); if (p.getOffset() > 0) { current = current.split(p.getOffset(), current.getValue()); } current.insertBefore(oldValues, insertSize); traverse(paintingTraverser, insertSize); currentLocation += insertSize; } @Override public void delete(int deleteSize) { if (deleteSize > size() - currentLocation) { throw new OpCursorException("attempt to delete beyond end of document (cursor at " + currentLocation + ", size is " + size() + ", deleteSize is " + deleteSize + ")"); } traverse(deleteTraverser, deleteSize); } /** * Traverser for deleting containers or parts of containers across a range. */ private final ContainerTraverser deleteTraverser = new ContainerTraverser() { public void partialContainer(OffsetList.Container<Values> container, int startOffset, int endOffset) { oldValues = container.getValue(); container.increaseSize(startOffset - endOffset); } public void wholeContainer(OffsetList.Container<Values> container) { oldValues = container.getValue(); container.remove(); } public void finished(OffsetList.Container<Values> missedContainer) { maybeMergeWithPrevious(missedContainer); } }; @Override public void startAnnotation(String key, Object value) { maybeNoteChange(key); currentChanges.put(key, new Pair<Integer, Object>(currentLocation, value)); } @Override public void endAnnotation(String key) { assert currentChanges.containsKey(key); maybeNoteChange(key); currentChanges.remove(key); } /** * Check if a notification is relevant at this point for the given key, and if * so, note it for later. * * @param key */ private void maybeNoteChange(String key) { if (listener == null) { return; } if (currentChanges.containsKey(key)) { Pair<Integer, Object> info = currentChanges.get(key); notifications.add(new Notification(info.getFirst().intValue(), currentLocation, key, info .getSecond())); } } /** * Return a new value with the currentChanges applied to the given value */ private Values applyChanges(Values values) { return currentChanges.isEmpty() ? values : new Values(values, currentChanges); } /** * Merge the given container with the previous container, if they have equal * values */ private void maybeMergeWithPrevious(OffsetList.Container<Values> container) { OffsetList.Container<Values> previous = container.getPreviousContainer(); if (previous != ranges.sentinel() && previous.getValue().equals(container.getValue())) { container.increaseSize(previous.size()); previous.remove(); } } /** * @see ContainerTraverser */ private void traverse(ContainerTraverser traverser, int distance) { OffsetPoint<Values> p = ranges.performActionAt(currentLocation, finder); int offset = p.getOffset(); OffsetList.Container<Values> container = p.getContainer(); while (distance > 0) { int containerMoveSize = Math.min(distance, container.size() - offset); OffsetList.Container<Values> next = container.getNextContainer(); if (offset > 0) { traverser.partialContainer(container, offset, offset + containerMoveSize); } else if (containerMoveSize == container.size()) { traverser.wholeContainer(container); } else { traverser.partialContainer(container, 0, containerMoveSize); } offset = 0; distance -= containerMoveSize; container = next; } traverser.finished(container); } // Reader methods @Override public int size() { return ranges.size(); } @Override public Object getAnnotation(int location, String key) { Preconditions.checkElementIndex(location, size()); checkKeyNotNull(key); Values values = ranges.performActionAt(location, finder).getValue(); return values == null ? null : values.get(key); } @Override public int firstAnnotationChange(int start, int end, String key, Object fromValue) { Preconditions.checkPositionIndexes(start, end, size()); checkKeyNotNull(key); start = Math.max(0, start); end = Math.min(end, ranges.size()); OffsetPoint<Values> point = ranges.performActionAt(start, finder); OffsetList.Container<Values> container = point.getContainer(); int offset = point.getOffset(); int location = start; while (location < end) { if (!eq(getValue(container, key), fromValue)) { return location; } if (container == ranges.sentinel()) { break; } location += container.size() - offset; container = container.getNextContainer(); offset = 0; } return -1; } @Override public int lastAnnotationChange(int start, int end, String key, Object fromValue) { Preconditions.checkPositionIndexes(start, end, size()); checkKeyNotNull(key); start = Math.max(0, start); end = Math.min(end, ranges.size()); OffsetPoint<Values> point = ranges.performActionAt(end, finder); OffsetList.Container<Values> container = point.getContainer(); int offset = point.getOffset(); if (offset == 0) { container = container.getPreviousContainer(); offset = container == ranges.sentinel() ? 0 : container.size(); } int location = end; while (location > start) { if (!eq(getValue(container, key), fromValue)) { return location; } if (container == null) { break; } location -= offset; container = container.getPreviousContainer(); offset = container == ranges.sentinel() ? 0 : container.size(); } return -1; } @Override public AnnotationCursor annotationCursor(int start, int end, ReadableStringSet keys) { if (keys == null) { throw new RuntimeException("not implemented"); } return new GenericAnnotationCursor<Object>(this, start, end, keys); } private Object getValue(OffsetList.Container<Values> container, String key) { // If only we had the Maybe monad in Java... return (container == ranges.sentinel()) ? null : (container.getValue() == null) ? null : container.getValue().get(key); } private boolean eq(Object a, Object b) { return a == null ? b == null : a.equals(b); } protected void checkKeyNotNull(String key) { Preconditions.checkNotNull(key, "key must not be null"); } @Override public void forEachAnnotationAt(int location, ReadableStringMap.ProcV<Object> callback) { throw new RuntimeException("not implemented"); } private class RangesIterator implements Iterator<AnnotationInterval<Object>> { Container<Values> next = ranges.firstContainer(); @Override public boolean hasNext() { return next != ranges.sentinel(); } @Override public AnnotationInterval<Object> next() { if (!hasNext()) { throw new NoSuchElementException("no more intervals"); } StringMap<Object> annotations = CollectionUtils.createStringMap(); annotations.putAll(next.getValue().map); StringMap<Object> diffFromLeft = CollectionUtils.createStringMap(); for (Map.Entry<String, Object> e : next.getValue().map.entrySet()) { String key = e.getKey(); Object value = e.getValue(); if ((next.getPreviousContainer() == ranges.sentinel())) { if (value != null) { diffFromLeft.put(key, value); } } else { if (!eq(value, next.getPreviousContainer().getValue().get(key))) { diffFromLeft.put(key, value); } } } if (next.getPreviousContainer() != ranges.sentinel()) { // Find and add value=null entries that may be implicit. for (Map.Entry<String, Object> e : next.getPreviousContainer().getValue().map.entrySet()) { String key = e.getKey(); Object value = e.getValue(); if (value != null && !next.getValue().map.containsKey(key)) { diffFromLeft.put(key, null); annotations.put(key, null); } } } AnnotationInterval<Object> i = new AnnotationIntervalImpl<Object>(next.offset(), next.offset() + next.size(), annotations, diffFromLeft); next = next.getNextContainer(); return i; } @Override public void remove() { throw new UnsupportedOperationException("removing an annotation interval is not supported"); } } @Override public Iterable<AnnotationInterval<Object>> annotationIntervals(int start, int end, ReadableStringSet keys) { if (keys == null) { if (start > 0 || end < size()) { throw new RuntimeException("not implemented"); } return new Iterable<AnnotationInterval<Object>>() { @Override public Iterator<AnnotationInterval<Object>> iterator() { return new RangesIterator(); } }; } return new GenericAnnotationIntervalIterable<Object>(this, start, end, keys); } @Override public Iterable<RangedAnnotation<Object>> rangedAnnotations(int start, int end, ReadableStringSet keys) { throw new RuntimeException("not implemented"); } @Override public ReadableStringSet knownKeysLive() { throw new UnsupportedOperationException("knownKeysLive"); } @Override public ReadableStringSet knownKeys() { throw new UnsupportedOperationException("knownKeys"); } @Override public String getInherited(String key) { throw new UnsupportedOperationException("getInherited"); } }