/** * 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.AbstractObservableBasicSet; import org.waveprotocol.wave.model.document.ObservableMutableDocument; import org.waveprotocol.wave.model.document.operation.Attributes; import org.waveprotocol.wave.model.document.operation.impl.AttributesImpl; import org.waveprotocol.wave.model.document.util.DocHelper; import org.waveprotocol.wave.model.document.util.DocumentEventRouter; import org.waveprotocol.wave.model.util.CollectionUtils; import org.waveprotocol.wave.model.util.ElementListener; import org.waveprotocol.wave.model.util.Preconditions; import org.waveprotocol.wave.model.util.Serializer; import java.util.Map; import java.util.Set; /** * Implementation of a basic set, using elements in a region of a concurrent * document. * * Does not support null values. * */ public final class DocumentBasedBasicSet<E, T> extends AbstractObservableBasicSet<T> { /** Backing document service. */ private final DocumentEventRouter<? super E, E, ?> router; /** Serializer for converting between attribute values and entry values. */ private final Serializer<T> serializer; /** Element containing set 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; /** Maps from value held, to the element that holds it. */ private final Map<T, E> valueElements = CollectionUtils.newHashMap(); /** Child elements of the container we wish would go away. */ private final Set<E> obsoleteElements = CollectionUtils.newHashSet(); /** * Creates a basic set. The returned object must be init()ialized before it is * valid. * * @param router router for the document holding the set 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 DocumentBasedBasicSet(DocumentEventRouter<? super E, E, ?> router, E entryContainer, Serializer<T> serializer, String entryTagName, String valueAttrName) { this.router = router; this.container = entryContainer; this.serializer = serializer; this.entryTagName = entryTagName; this.valueAttrName = valueAttrName; } /** * Creates a document-based basic set. * * @see #DocumentBasedBasicSet(DocumentEventRouter, Object, Serializer, * String, String) */ public static <E, C extends Comparable<C>> DocumentBasedBasicSet<E, C> create( DocumentEventRouter<? super E, E, ?> router, E entryContainer, Serializer<C> serializer, String entryTagName, String valueAttrName) { return new DocumentBasedBasicSet<E, C>( router, entryContainer, serializer, entryTagName, valueAttrName).init(); } private ObservableMutableDocument<? super E, E, ?> getDocument() { return router.getDocument(); } /** * Initializes a new object. Call after construction is complete. */ private DocumentBasedBasicSet<E, T> init() { // Plumb events through router.addChildListener(container, new ElementListener<E>() { @Override public void onElementAdded(E element) { handleElementAdded(element); } @Override public void onElementRemoved(E element) { handleElementRemoved(element); } }); // Call handleElementAdded() to notify this class of existing data ObservableMutableDocument<? super E, E, ?> document = getDocument(); E curr = DocHelper.getFirstChildElement(document, container); E next; while (curr != null) { next = DocHelper.getNextSiblingElement(document, curr); handleElementAdded(curr); curr = next; } return this; } // // Methods from BasicSet interface. State is read only from the valueElements // and obsoleteElements members, and state is changed only by manipulating the // underlying document state. // @Override public Iterable<T> getValues() { return CollectionUtils.newArrayList(valueElements.keySet()); } @Override public boolean contains(T value) { return valueElements.containsKey(value); } @Override public void add(T value) { Preconditions.checkNotNull(value, "value must not be null"); // Add an element to represent the value if (!valueElements.containsKey(value)) { Attributes attrs = new AttributesImpl(valueAttrName, serializer.toString(value)); getDocument().createChildElement(container, entryTagName, attrs); deleteObsoleteElements(); } } @Override public void remove(T value) { if (valueElements.containsKey(value)) { deleteObsoleteElements(); getDocument().deleteNode(valueElements.get(value)); } } @Override public void clear() { if (!valueElements.isEmpty()) { deleteObsoleteElements(); // Remove every container element that corresponds to a value ObservableMutableDocument<? super E, E, ?> document = getDocument(); while (!valueElements.isEmpty()) { document.deleteNode(valueElements.values().iterator().next()); } } } // // Methods that respond to changes in the underlying document. These methods // update valueElements and obsoleteElements, and trigger any listeners to // this BasicSet. // /** * Handles a new element being added to the container. If it represents a new * value, that value is recorded. If it represents a value already in this * container, either the old element or the new element is marked obsolete. * The element marked obsolete is the one that appears later in the document. */ private void handleElementAdded(E newElement) { ObservableMutableDocument<? super E, E, ?> document = getDocument(); assert container.equals(document.getParentElement(newElement)); if (!entryTagName.equals(document.getTagName(newElement))) { return; } T value = valueOf(newElement); E oldEntry = valueElements.get(value); if (oldEntry == null) { // Entry is for a new value - add it to the element map and fire an event // to collection listeners valueElements.put(value, newElement); fireOnValueAdded(value); } else if (document.getLocation(oldEntry) < document.getLocation(newElement)) { // newEntry is not needed, so mark it obsolete obsoleteElements.add(newElement); } else { // oldEntry is no needed, so mark it obsoleted and use the new one instead obsoleteElements.add(oldEntry); valueElements.put(value, newElement); } } /** * Handles an element being removed from the container. */ private void handleElementRemoved(E deletedElement) { // To start with though, we do a quick check to see if deletedElement has // the same tag name as value elements. if (!entryTagName.equals(getDocument().getTagName(deletedElement))) { // Exit, because deletedElement definitely isn't part of this container. return; } if (obsoleteElements.remove(deletedElement)) { // Element was obsolete, so ignore the removal event return; } T value = valueOf(deletedElement); E existingElement = valueElements.get(value); if (existingElement != deletedElement) { // deleted element was not part of the backing store for this map, so // ignore it } else { valueElements.remove(value); fireOnValueRemoved(value); } } // // Helper methods. // /** * Deletes those elements that represent doubled-up values. This condition is * detected in the handleElementAdded() method, but the actual deletion is * only performed after some kind of write was made to the document via a call * to {@link #add(Object)}, {@link #remove(Object)} or {@link #clear()}. * * <p> * Delaying document mutation ensures that agents and clients that are only * listening to a document do not send mutation operations, preventing issues * of writing to read-only wavelets and minimizing the number of concurrent * writers sending ops to a wave server. * * <p> * When removing values from the container, obsolete elements ought to be * deleted <i>before</i> removing valid elements. This ensures that other * agents do not see an intermediate state where an obsolete element is * present, but a valid one is not. * * <p> * When adding values to the container, obsolete elements ought to be deleted * <i>after</i> adding valid elements. This is to maintain symmetry with * removal, thereby pleasing all right-thinking engineers. */ private void deleteObsoleteElements() { ObservableMutableDocument<? super E, E, ?> document = getDocument(); while (!obsoleteElements.isEmpty()) { // Delete the obsolete element from the document. The element removed // event callback will remove the item from the obsoleteElements set. document.deleteNode(obsoleteElements.iterator().next()); } } /** * Gets the value of an entry. * * @param element entry to evaluate * @return the value embedded in the element. */ private T valueOf(E element) { return serializer.fromString(getDocument().getAttribute(element, valueAttrName)); } }