/** * 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.conversation; import org.waveprotocol.wave.model.util.CollectionUtils; import org.waveprotocol.wave.model.util.CopyOnWriteSet; import org.waveprotocol.wave.model.wave.ObservableMap; import org.waveprotocol.wave.model.wave.SourcesEvents; import java.util.Collection; import java.util.Collections; import java.util.Map; /** * Managers anchoring of transient parasites to transient items via fixed * locations. * * A "location" is understood to mean a reference to an "item", where items are * transient: over time, they come into existence and go out of existence. At * each location, any number of "parasites" can be attached. Parasites can also * be attached to no location. Parasites too are transient: they come and go * over time. * * This class reveals all the parasites, as being either {@link #getAttached() * attached} to an item (via a hidden location), or {@link #getUnattached() * unattached} to any item (either because the parasites is not bound to a * location, or it is bound to a location that has no item). This view is kept * live. * * * <ul> * <li>the item to location mapping is a surjection (every item has exactly one * location, and every location has at moast one item);</li> * <li>the parasite to location mapping is a partial function (every parasite * maps to at most one location, every location maps to any number of * parasites); and</li> * <li>the two mappings share the same co-domain (the meaning of 'locations' is * the same)</li> * </ul> * * @param <L> location type * @param <I> item type * @param <P> parasite type */ public final class AnchorManager<L, I, P> implements SourcesEvents<AnchorManager.Listener<? super I, ? super P>> { interface Listener<I, P> { /** * Notifies this listener that some parasites have been attached. * * This event occurs: * <ul> * <li>when new parasites are added to a location that has an item;</li> * <li>when new parasites are added to a location that has no item; and</li> * <li>when an item is added in a location to which parasites had previously * been added.</li> * </ul> * * @param item item to which the parasites are attached, or {@code null} for * unanchored parasites * @param parasites newly attached parasites (non-empty) */ void onAttached(I item, Collection<? extends P> parasites); /** * Notifies this listener that some parasites have been detached. * * This event occurs: * <ul> * <li>when new parasites are removed from a location that has an item;</li> * <li>when new parasites are fromved from a location that has no item; and</li> * <li>when an item is removed from a location to which parasites are * attached.</li> * </ul> * * @param item item where the parasites were attached, or {@code null} for * unanchored parasites * @param parasites newly detached parasite (non-empty) */ void onDetached(I item, Collection<? extends P> parasites); } // // External helpers. // /** Maps location references to (transient) items. */ private final ObservableMap<L, ? extends I> locationResolver; // // Internal state. // /** Parasites attached to items (via locations). */ private final Map<I, Collection<P>> attached = CollectionUtils.newHashMap(); /** Parasites unattached (non-existent item or location). */ private final Map<L, Collection<P>> unattached = CollectionUtils.newHashMap(); /** Internal observer of location map. */ private final ObservableMap.Listener<L, I> itemObserver = new ObservableMap.Listener<L, I>() { @Override public void onEntryAdded(L location, I item) { AnchorManager.this.onEntryAdded(location, item); } @Override public void onEntryRemoved(L location, I item) { AnchorManager.this.onEntryRemoved(location, item); } }; /** Listeners. */ private final CopyOnWriteSet<Listener<? super I, ? super P>> listeners = CopyOnWriteSet.create(); /** * Creates an anchor manager. */ private AnchorManager(ObservableMap<L, ? extends I> locationResolver) { this.locationResolver = locationResolver; } /** * Creates an anchor manager. * * @param items map from location references to items in those locations. * @return a new anchor manager. */ public static <L, I, P> AnchorManager<L, I, P> create(ObservableMap<L, ? extends I> items) { AnchorManager<L, I, P> m = new AnchorManager<L, I, P>(items); m.init(); return m; } /** * Observes the location map. */ private void init() { locationResolver.addListener(itemObserver); } /** * Destroys this manager, releasing all resources it is using. */ public void destroy() { locationResolver.removeListener(itemObserver); } /** * Attaches a parasite at a location, notifying listeners of the * {@link Listener#onAttached(Object, Collection) attachment} event. * * @param location * @param parasite */ public void attachParasite(L location, P parasite) { // Does key point to something that exists yet? I item = locationResolver.get(location); if (item != null) { put(attached, item, parasite); } else { put(unattached, location, parasite); } triggerOnAttached(item, parasite); } /** * Detaches a parasite from a location, notifying listeners of the * {@link Listener#onDetached(Object, Collection) detachment} event. * * @param location * @param parasite */ public void detachParasite(L location, P parasite) { I item = locationResolver.get(location); if (item != null) { remove(attached, item, parasite); } else { remove(unattached, location, parasite); } triggerOnDetached(item, parasite); } // // Helper methods for maps from keys to lazy collections. // /** * Puts a value in a lazy-collection map. */ private static <K, P> void put(Map<K, Collection<P>> map, K key, P value) { Collection<P> keyValues = map.get(key); if (keyValues == null) { keyValues = CollectionUtils.newHashSet(); map.put(key, keyValues); } keyValues.add(value); } /** * Removes a value from a lazy-collection map. */ private static <K, P> void remove(Map<K, Collection<P>> map, K key, P value) { Collection<P> keyValues = map.get(key); keyValues.remove(value); if (keyValues.isEmpty()) { map.remove(key); } } // // Location map events. // private void onEntryAdded(L location, I item) { Collection<P> parasites = unattached.remove(location); if (parasites != null) { attached.put(item, parasites); triggerOnDetached(null, parasites); triggerOnAttached(item, parasites); } } private void onEntryRemoved(L location, I item) { Collection<P> parasites = attached.remove(item); if (parasites != null) { unattached.put(location, parasites); triggerOnDetached(item, parasites); triggerOnAttached(null, parasites); } } // // Anchoring state. // public Map<I, Collection<P>> getAttached() { return Collections.unmodifiableMap(attached); } public Collection<P> getUnattached() { Collection<P> allUnattached = CollectionUtils.newArrayList(); for (Collection<P> unattachedValues : unattached.values()) { allUnattached.addAll(unattachedValues); } return allUnattached; } // // Anchoring events. // @Override public void addListener(Listener<? super I, ? super P> listener) { listeners.add(listener); } @Override public void removeListener(Listener<? super I, ? super P> listener) { listeners.remove(listener); } private void triggerOnAttached(I item, P parasite) { triggerOnAttached(item, Collections.singleton(parasite)); } private void triggerOnDetached(I item, P parasite) { triggerOnDetached(item, Collections.singleton(parasite)); } private void triggerOnAttached(I item, Collection<P> parasites) { for (Listener<? super I, ? super P> listener : listeners) { listener.onAttached(item, parasites); } } private void triggerOnDetached(I item, Collection<P> parasites) { for (Listener<? super I, ? super P> listener : listeners) { listener.onDetached(item, parasites); } } }