/** * Copyright 2010 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.ObservableSingleton; import org.waveprotocol.wave.model.document.Doc; import org.waveprotocol.wave.model.document.ObservableDocument; import org.waveprotocol.wave.model.document.util.DocEventRouter; import org.waveprotocol.wave.model.document.util.DocHelper; import org.waveprotocol.wave.model.document.util.Point; import org.waveprotocol.wave.model.util.CopyOnWriteSet; import org.waveprotocol.wave.model.util.ElementListener; import org.waveprotocol.wave.model.util.ValueUtils; import java.util.Map; /** * Document-based implementation of a singleton. * * @author anorth@google.com (Alex North) * @param <V> type of the value * @param <I> type of a value initializer */ public final class DocumentBasedSingleton<V, I> implements ObservableSingleton<V, I>, ElementListener<Doc.E> { /** * Creates a singleton. * * @param router event router for the document holding the object * @param container container in which the singleton is stored * @param valueTagName tag name of a value instance */ public static <V, I> DocumentBasedSingleton<V, I> create(DocEventRouter router, Doc.E container, String valueTagName, Factory<Doc.E, V, I> valueFactory) { DocumentBasedSingleton<V, I> singleton = new DocumentBasedSingleton<V, I>(router, container, valueTagName, valueFactory); router.addChildListener(container, singleton); return singleton; } private final DocEventRouter router; private final Doc.E container; private final String valueTagName; private final Factory<Doc.E, V, I> valueFactory; private final CopyOnWriteSet<Listener<? super V>> listeners = CopyOnWriteSet.create(); /** The canonical value-providing document element. */ private Doc.E currentElement; /** The abstract value, initialized lazily. */ private V currentValue = null; private DocumentBasedSingleton(DocEventRouter router, Doc.E container, String valueTagName, Factory<Doc.E, V, I> valueFactory) { this.router = router; this.container = container; this.valueTagName = valueTagName; this.valueFactory = valueFactory; this.currentElement = findCanonicalElement(); } @Override public boolean hasValue() { return (currentElement != null); } @Override public V get() { maybeInitCurrentValue(); return currentValue; } @Override public V set(I initialState) { final Map<String, String> attributes = Initializer.Helper.buildAttributes(initialState, valueFactory); // Insert a new first-child element of the container. Point<Doc.N> insertionPoint = Point.inElement(container, getDocument().getFirstChild(container)); Doc.E element = getDocument().createElement(insertionPoint, valueTagName, attributes); // onElementAdded will create the value object. assert currentElement != null; assert currentValue != null; cleanup(); return currentValue; } @Override public void clear() { // Delete non-canonical elements first so no events are generated. cleanup(); // Then delete the canonical, which will remove currentValue. Doc.E element = findCanonicalElement(); if (element != null) { getDocument().deleteNode(element); } assert currentElement == null; assert currentValue == null; } @Override public void addListener(Listener<? super V> listener) { listeners.add(listener); } @Override public void removeListener(Listener<? super V> listener) { listeners.remove(listener); } @Override public void onElementAdded(Doc.E newElement) { // When a deletion and insertion are composed it's possible that // recalculation from the deletion event changes the current element // to the newly inserted canonical element before this event // is fired. Thus the check that the new element is not already // the current element. // Yet another failure of the ElementListener interface. if (newElement == findCanonicalElement() && newElement != currentElement) { changeCurrentValue(newElement); } } @Override public void onElementRemoved(Doc.E removedElement) { if (removedElement == currentElement) { changeCurrentValue(findCanonicalElement()); } } /** * Gets the document backing this singleton (for testing). */ DocEventRouter getEventRouter() { return router; } ObservableDocument getDocument() { return router.getDocument(); } /** * Finds the canonical value-providing document element, which is the first * element with the expected tag name. * * @return the canonical element, or null */ private Doc.E findCanonicalElement() { // NOTE(anorth): this is correct for planned transform semantics where // colliding insertions result in document order matching temporal order. // Current (June 2010) transformation results in the opposite ordering. return DocHelper.getElementWithTagName(getDocument(), valueTagName, container); } /** * Initializes the current value without firing events. */ private void maybeInitCurrentValue() { if (currentValue == null) { currentValue = (currentElement != null) ? valueFactory.adapt(getEventRouter(), currentElement) : null; } } /** * Sets the current value to be provided by a canonical element and fires * events. * * @param newCurrentElement new value-providing element, or null; */ private void changeCurrentValue(Doc.E newCurrentElement) { assert currentElement != newCurrentElement; maybeInitCurrentValue(); V oldValue = currentValue; currentElement = newCurrentElement; currentValue = null; maybeInitCurrentValue(); maybeTriggerOnValueChanged(oldValue, currentValue); } /** * Deletes all non-canonical elements from the document. */ private void cleanup() { Doc.E canonical = findCanonicalElement(); Doc.E toDelete = DocHelper.getLastElementWithTagName(getDocument(), valueTagName, container); while (toDelete != canonical) { getDocument().deleteNode(toDelete); toDelete = DocHelper.getLastElementWithTagName(getDocument(), valueTagName, container); } } private void maybeTriggerOnValueChanged(V oldValue, V newValue) { if (ValueUtils.notEqual(oldValue, newValue)) { for (Listener<? super V> l : listeners) { l.onValueChanged(oldValue, newValue); } } } }