/** * 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.adt.docbased; import org.waveprotocol.wave.model.adt.ObservableMonotonicValue; import org.waveprotocol.wave.model.document.ObservableMutableDocument; import org.waveprotocol.wave.model.document.util.DocHelper; import org.waveprotocol.wave.model.document.util.DocumentEventRouter; import org.waveprotocol.wave.model.util.CopyOnWriteSet; import org.waveprotocol.wave.model.util.ElementListener; import org.waveprotocol.wave.model.util.Preconditions; import org.waveprotocol.wave.model.util.Serializer; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; /** * Provides a monotonically increasing value, implemented using a region of a * concurrent document. * */ public class DocumentBasedMonotonicValue<E, C extends Comparable<C>> implements ObservableMonotonicValue<C>, ElementListener<E> { /** Backing document service. */ private final DocumentEventRouter<? super E, E, ?> router; /** Serializer for converting between attribute values and entry values. */ private final Serializer<C> serializer; /** Element containing value entries. */ private final E container; /** Name to use for entry elements. */ private final String entryTagName; /** Name to use for the value attribute. */ private final String valueAttrName; /** Element holding the current value. */ private E value; private final Set<E> obsoleteEntries = new HashSet<E>(); /** Listeners. */ private final CopyOnWriteSet<Listener<? super C>> listeners = CopyOnWriteSet.create(); /** * Creates a monotonic map. * * @param router router for the document holding the map state * @param entryContainer element in which entry elements should be created * @param serializer converter between strings and values * @param entryTagName name to use for entry elements * @param valueAttrName name to use for value attributes */ private DocumentBasedMonotonicValue(DocumentEventRouter<? super E, E, ?> router, E entryContainer, Serializer<C> serializer, String entryTagName, String valueAttrName) { this.router = router; this.container = entryContainer; this.serializer = serializer; this.entryTagName = entryTagName; this.valueAttrName = valueAttrName; } /** * Creates a monotonic map. * * @see #DocumentBasedMonotonicValue(DocumentEventRouter, Object, Serializer, String, String) */ public static <E, C extends Comparable<C>> DocumentBasedMonotonicValue<E, C> create( DocumentEventRouter<? super E, E, ?> router, E entryContainer, Serializer<C> serializer, String entryTagName, String valueAttrName) { DocumentBasedMonotonicValue<E, C> value = new DocumentBasedMonotonicValue<E, C>( router, entryContainer, serializer, entryTagName, valueAttrName); router.addChildListener(entryContainer, value); value.load(); return value; } private ObservableMutableDocument<? super E, E, ?> getDocument() { return router.getDocument(); } /** * Loads from the document state, aggressively removing obsolete values. */ private void load() { ObservableMutableDocument<? super E, E, ?> document = getDocument(); E value = DocHelper.getFirstChildElement(document, container); E nextValue; while (value != null) { nextValue = DocHelper.getNextSiblingElement(document, value); onElementAdded(value); value = nextValue; } } /** * {@inheritDoc} */ @Override public C get() { return (value != null) ? valueOf(value) : null; } /** * {@inheritDoc} */ @Override public void set(C value) { Preconditions.checkNotNull(value, "value must not be null"); C currentValue = get(); if (currentValue == null || currentValue.compareTo(value) < 0) { createEntry(value); cleanup(); } } /** * Deletes the cached entry element for a particular key, if one exists. */ private void invalidateCacheEntry() { invalidateEntry(value); value = null; } /** * Deletes an entry from the document. * * @param entry */ private void invalidateEntry(E entry) { if (entry != null) { obsoleteEntries.add(entry); } } /** * Creates an entry element for the specified map entry. * * @param value */ private void createEntry(C value) { Map<String, String> attrs = new HashMap<String, String>(); attrs.put(valueAttrName, serializer.toString(value)); E entry = getDocument().createChildElement(container, entryTagName, attrs); } private void cleanup() { if (!obsoleteEntries.isEmpty()) { ObservableMutableDocument<? super E, E, ?> document = getDocument(); Collection<E> toDelete = new ArrayList<E>(obsoleteEntries); for (E e : toDelete) { document.deleteNode(e); } // Deletion callbacks should cleanup obsoleteEntries collection one by one. assert obsoleteEntries.isEmpty(); } } /** * Gets the value of an entry. * * @param entry * @return the value of an entry. */ private C valueOf(E entry) { return serializer.fromString(getDocument().getAttribute(entry, valueAttrName)); } // // Document mutation callbacks. // /** * Clears the cache reference to an entry, if the cache refers to it, in * response to an entry element being removed. */ @Override public void onElementRemoved(E entry) { if (!entryTagName.equals(getDocument().getTagName(entry))) { return; } // It is possible, in transient states, that there are multiple entries in the document for the // same key. Therefore, we can not blindly remove the entry from the cache based on key alone. if (value == entry) { C oldValue = get(); value = null; triggerOnEntryChanged(oldValue, null); } else { obsoleteEntries.remove(entry); } } /** * Updates the entry cache, if necessary, in response to an entry element * being added. * * This method also aggressively deletes any entries that are not greater * than the cached entry. */ @Override public void onElementAdded(E entry) { ObservableMutableDocument<? super E, E, ?> document = getDocument(); assert container.equals(document.getParentElement(entry)); if (!entryTagName.equals(document.getTagName(entry))) { return; } C newValue = valueOf(entry); C oldValue = get(); // If the new value should end up in the cache, delete the old one (if applicable) and update // the entry cache. // Otherwise, the new value is aggressively deleted. if (oldValue == null || oldValue.compareTo(newValue) < 0) { invalidateCacheEntry(); // This should clean up the old entry in onEntryRemoved. value = entry; triggerOnEntryChanged(oldValue, newValue); } else { invalidateEntry(entry); } } /** * Broadcasts a state-change event to registered listeners. * * @param oldValue old value * @param newValue new value */ private void triggerOnEntryChanged(C oldValue, C newValue) { for (Listener<? super C> l : listeners) { l.onSet(oldValue, newValue); } } /** * {@inheritDoc} */ @Override public void addListener(Listener<? super C> l) { listeners.add(l); } /** * {@inheritDoc} */ @Override public void removeListener(Listener<? super C> l) { listeners.remove(l); } }