/** * 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.ElementList; import org.waveprotocol.wave.model.adt.ObservableElementList; 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.document.util.Point; import org.waveprotocol.wave.model.util.CopyOnWriteSet; import org.waveprotocol.wave.model.util.ElementListener; import org.waveprotocol.wave.model.util.Preconditions; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; /** * An element list backed by elements in a document. * * Users must specify the type of the * abstract list element and the tag name to be used when creating list items. * An @link {@link Initializer} interprets abstract initial state into document * element attributes. A {@link Factory} adapts document elements to create * abstract list elements. * * <p> * Use delegation to implement a document based element * list. Create your list that implements the {@link ElementList} interface; at * the construction time, create an instance of this class, then use delegation * for all methods of the {@link ElementList} interface. * </p> * * <p> * If you are using an observable element list, the order of events, when adding * a new element, is as follows: * </p> * <ul> * <li> * A new element is created and inserted into the document (no calls, yet) * </li> * <li> * The factory supplied at the construction time has the * {@link Factory#adapt(DocumentEventRouter, Object)} method called. * </li> * <li> * The element created by the factory is recorded in in-memory data * structure. * </li> * <li> * The {@link ObservableElementList.Listener#onValueAdded(Object)} * method is called * </li> * </ul> * <p> * In particular this means you can safely call {@link #get(int)} method in * the observable list listener. * </p> * * <p> * A simple example of use: * <pre> * class MyElement { * class Initialiser { * public final String value; * * Initialiser(String value) { * this.value = value; * } * } * * private String value; * * MyElement(String value) { * this.value = value; * } * } * * DBEL.Factory<E, MyElement> factory = new DBEL.Factory<E, MyElement>() { * MyElement adapt(ObservableMutableDocument<? super E, E, ?> doc, E container) { * return new MyElement(doc.getElementAttribute(container, "myattribute")); * } * } * * DBEL.Initialiser initialiser = new DBEL.initialiser<MyElement.Initialiser>() { * public Map<String, String> makeInitialState(MyElement.Initialiser initialState) { * if (initialState != null) { * return Collections.<String, String> singletonMap("myattribute", initialState.value); * } * return Collections.<String, String> emptyMap(); * } * } * * ElementList<MyElement> list = DocumentBasedElementList.create(doc, container, "mytag", * factory, initialiser); * * MyElement newElement = list.add("newvalue"); * </pre> * </p> * * * @param <E> type of the document element * @param <T> type of the abstract element hosted in this list * @param <I> type of the initialisation data for new elements */ public final class DocumentBasedElementList<E, T, I> implements ObservableElementList<T, I>, ElementListener<E> { /** Maps list values to document elements that host them. */ private final Map<T, E> valueToElement; /** Maps document elements to list values. */ private final Map<E, T> elementToValue; /** Router for the document supporting this list */ private final DocumentEventRouter<? super E, E, ?> router; /** The parent that holds the children. */ private final E parent; /** In-memory mirror of the document. */ protected final List<T> orderedValues; /** The tag associated with children. */ private final String childTag; /** The factory for abstract children. */ private final Factory<E, ? extends T, I> factory; /** Listeners. */ private final CopyOnWriteSet<ObservableElementList.Listener<? super T>> listeners; /** * Creates a new element list supported by the given document and hosted in the given * element. * * @param router router for document supporting this list * @param parent the element in which this list is effectively stored * @param childTag the tag name for new elements * @param factory factory for adapting document elements to abstract elements */ private DocumentBasedElementList(DocumentEventRouter<? super E, E, ?> router, E parent, String childTag, Factory<E, ? extends T, I> factory) { this.orderedValues = new ArrayList<T>(); this.valueToElement = new HashMap<T, E>(); this.elementToValue = new HashMap<E, T>(); this.listeners = CopyOnWriteSet.create(); this.parent = parent; this.childTag = childTag; this.factory = factory; this.router = router; } private ObservableMutableDocument<? super E, E, ?> getDocument() { return router.getDocument(); } /** * Creates a new document-based element list. The list may store other * elements and even elements which itself are lists. * * @param <E> The type of the element node * @param <T> The type of the child node * @param <I> type of the initialisation data for new elements * @param router Router for the document supporting this list * @param parent The parent element holding list elements * @param childTag The tag with which list elements are created * @param factory The factory that for each element creates a child object * @return A new, document based element list */ public static <E, T, I> DocumentBasedElementList<E, T, I> create( DocumentEventRouter<? super E, E, ?> router, E parent, String childTag, Factory<E, ? extends T, I> factory) { DocumentBasedElementList<E, T, I> list = new DocumentBasedElementList<E, T, I>( router, parent, childTag, factory); list.dispatchAndLoad(); return list; } @Override public T add(I initialState) { Map<String, String> attributes = Initializer.Helper.buildAttributes(initialState, factory); E element = getDocument().createChildElement(parent, childTag, attributes); // As we are using an observable document, that is registered to post us events, we expect // that onElementAdded is called once the child is created. This method in turn creates a // child object for the given element and store it in the elementToValue map. We use this to // return the child for the given element. return elementToValue.get(element); } @Override public T get(int index) { return orderedValues.get(index); } @Override public int indexOf(T child) { return orderedValues.indexOf(child); } @Override public T add(int index, I initialState) { int last = orderedValues.size(); Preconditions.checkPositionIndex(index, last); if (index == last) { return add(initialState); } T childAfter = orderedValues.get(index); E nodeAfter = valueToElement.get(childAfter); Map<String, String> attributes = Initializer.Helper.buildAttributes(initialState, factory); E fresh = createBefore(getDocument(), nodeAfter, attributes); return elementToValue.get(fresh); } private <N, F extends N> F createBefore(ObservableMutableDocument<N, F, ?> doc, F element, Map<String, String> attributes) { Point<N> where = Point.before(doc, element); return doc.createElement(where, childTag, attributes); } @Override public Iterable<T> getValues() { return orderedValues; } @Override public boolean remove(T child) { E element = valueToElement.remove(child); if (element == null) { return false; } getDocument().deleteNode(element); return true; } @Override public void clear() { List<T> copy = new ArrayList<T>(orderedValues); for (T child : copy) { remove(child); } assert orderedValues.isEmpty(); } @Override public int size() { return orderedValues.size(); } @Override public void onElementAdded(E element) { assert getDocument().getParentElement(element).equals(parent) : "Received event for unrelated element"; if (childTag.equals(getDocument().getTagName(element))) { T child = factory.adapt(router, element); T sibling = getPreviousKnownValue(element); orderedValues.add(sibling == null ? 0 : orderedValues.indexOf(sibling) + 1, child); elementToValue.put(element, child); valueToElement.put(child, element); fireElementAdded(child); } } /** * Attempts to find the first known sibling value of the given element. This method looks back, * in hope that if we get notifications late, we get notifications about elements inserted * early first. * * @param added The freshly inserted element. * @return Either an existing list value, or null, if one cannot be found. */ private T getPreviousKnownValue(E added) { E prev = DocHelper.getPreviousSiblingElement(getDocument(), added); while (prev != null) { T value = elementToValue.get(prev); if (value != null) { return value; } prev = DocHelper.getPreviousSiblingElement(getDocument(), prev); } return null; } @Override public void onElementRemoved(E element) { T child = elementToValue.remove(element); if (child != null) { orderedValues.remove(child); fireElementRemoved(child); } } /** * Hooks up events for the observable document and loads this list from the document. */ private void dispatchAndLoad() { router.addChildListener(parent, this); load(); } /** * Loads this list from the document. */ private void load() { E entry = DocHelper.getFirstChildElement(getDocument(), parent); while (entry != null) { onElementAdded(entry); entry = DocHelper.getNextSiblingElement(getDocument(), entry); } } @Override public void addListener(ObservableElementList.Listener<T> listener) { listeners.add(listener); } @Override public void removeListener(ObservableElementList.Listener<T> listener) { listeners.remove(listener); } private void fireElementAdded(T child) { for (ObservableElementList.Listener<? super T> l : listeners) { l.onValueAdded(child); } } private void fireElementRemoved(T child) { for (ObservableElementList.Listener<? super T> l : listeners) { l.onValueRemoved(child); } } }