/** * 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.adt.ObservableSingleton; import org.waveprotocol.wave.model.adt.docbased.DocumentBasedSingleton; import org.waveprotocol.wave.model.document.ObservableDocument; import org.waveprotocol.wave.model.document.util.DefaultDocEventRouter; import org.waveprotocol.wave.model.document.util.DocEventRouter; import org.waveprotocol.wave.model.id.IdConstants; import org.waveprotocol.wave.model.id.IdGenerator; import org.waveprotocol.wave.model.id.IdUtil; import org.waveprotocol.wave.model.id.ModernIdSerialiser; import org.waveprotocol.wave.model.id.WaveId; import org.waveprotocol.wave.model.id.WaveletId; import org.waveprotocol.wave.model.util.CollectionUtils; import org.waveprotocol.wave.model.util.CopyOnWriteSet; import org.waveprotocol.wave.model.util.IdentityMap; import org.waveprotocol.wave.model.util.Preconditions; import org.waveprotocol.wave.model.wave.ObservableWavelet; import org.waveprotocol.wave.model.wave.WaveViewListener; import org.waveprotocol.wave.model.wave.Wavelet; import org.waveprotocol.wave.model.wave.opbased.ObservableWaveView; import java.util.Collection; import java.util.Collections; import java.util.Map; /** * A conversation view backed by a wave. * * @author anorth@google.com (Alex North) */ public class WaveBasedConversationView implements ObservableConversationView, WaveViewListener { /** * Wraps a wavelet to provide a conversation, or not, depending * on whether it has a manifest. */ private final class ConversationContainer implements ObservableSingleton.Listener<DocumentBasedManifest> { private final ObservableWavelet wavelet; private final ObservableSingleton<DocumentBasedManifest, Void> manifestContainer; private WaveletBasedConversation conversation; ConversationContainer(ObservableWavelet wavelet, ObservableSingleton<DocumentBasedManifest, Void> manifestContainer) { this.wavelet = wavelet; this.manifestContainer = manifestContainer; } ObservableWavelet getWavelet() { return wavelet; } WaveletBasedConversation getConversation() { if (conversation == null) { if (manifestContainer.hasValue()) { conversation = WaveletBasedConversation.create( WaveBasedConversationView.this, wavelet, manifestContainer.get(), idGenerator); } } return conversation; } @Override public void onValueChanged(DocumentBasedManifest oldValue, DocumentBasedManifest newValue) { if (oldValue != null) { assert conversation != null; conversations.remove(wavelet); conversation.destroy(); triggerOnConversationRemoved(conversation); conversation = null; } if (newValue != null) { assert conversation == null; conversation = WaveletBasedConversation.create(WaveBasedConversationView.this, wavelet, newValue, idGenerator); conversations.put(wavelet, conversation); triggerOnConversationAdded(conversation); } } } /** Id of this group of conversations. */ private final String id; /** Backing wave view. */ private final ObservableWaveView waveView; /** Generator for new ids. */ private final IdGenerator idGenerator; /** A container for every conversational wavelet in view. */ private final IdentityMap<Wavelet, ConversationContainer> containers = CollectionUtils.createIdentityMap(); /** Conversation adapters keyed by their wavelets. */ private final Map<Wavelet, WaveletBasedConversation> conversations = CollectionUtils.newHashMap(); private final CopyOnWriteSet<Listener> listeners = CopyOnWriteSet.create(); /** * Creates a conversation view on an wave view. */ public static WaveBasedConversationView create(ObservableWaveView waveView, IdGenerator idGenerator) { String id = idFor(waveView.getWaveId()); WaveBasedConversationView convView = new WaveBasedConversationView(id, waveView, idGenerator); waveView.addListener(convView); return convView; } /** * Computes the conversation view id for a wave. */ public static String idFor(WaveId wave) { return ModernIdSerialiser.INSTANCE.serialiseWaveId(wave); } private WaveBasedConversationView( String id, ObservableWaveView waveView, IdGenerator idGenerator) { this.id = id; this.waveView = waveView; this.idGenerator = idGenerator; for (ObservableWavelet wavelet : waveView.getWavelets()) { if (IdUtil.isConversationalId(wavelet.getId())) { createContainer(wavelet); } } } @Override public String getId() { return id; } @Override public Collection<WaveletBasedConversation> getConversations() { return Collections.unmodifiableCollection(conversations.values()); } @Override public WaveletBasedConversation getConversation(String conversationId) { return getConversation(WaveletBasedConversation.widFor(conversationId)); } @Override public WaveletBasedConversation getRoot() { ObservableWavelet rootWavelet = waveView.getRoot(); return (rootWavelet != null) ? conversations.get(rootWavelet) : null; } @Override public WaveletBasedConversation createRoot() { ObservableWavelet rootWavelet = waveView.createRoot(); return createNewConversation(rootWavelet); } @Override public WaveletBasedConversation createConversation() { ObservableWavelet wavelet; wavelet = waveView.createWavelet(); return createNewConversation(wavelet); } // WaveViewListener. @Override public void onWaveletAdded(ObservableWavelet wavelet) { if (IdUtil.isConversationalId(wavelet.getId())) { createContainer(wavelet); } } @Override public void onWaveletRemoved(ObservableWavelet wavelet) { containers.remove(wavelet); WaveletBasedConversation conversation = conversations.remove(wavelet); if (conversation != null) { triggerOnConversationRemoved(conversation); } } @Override public void addListener(Listener listener) { listeners.add(listener); } @Override public void removeListener(Listener listener) { listeners.remove(listener); } /** * Gets the wave view backing this conversation view. */ public ObservableWaveView getWaveView() { return waveView; } /** * Gets the conversation backed by a wavelet; null if the wavelet is not in * view or does not have conversation structure. */ public WaveletBasedConversation getConversation(WaveletId id) { if (!IdUtil.isConversationalId(id)) { Preconditions.illegalArgument("Wavelet id " + id + " is not conversational"); } ObservableWavelet wavelet = waveView.getWavelet(id); if (wavelet != null) { return conversations.get(wavelet); } return null; } /** * Creates and stores a conversation container. If the container has a * conversation then initializes that too. */ private ConversationContainer createContainer(ObservableWavelet wavelet) { ObservableDocument manifestDoc = wavelet.getDocument(IdConstants.MANIFEST_DOCUMENT_ID); DocEventRouter router = DefaultDocEventRouter.create(manifestDoc); ObservableSingleton<DocumentBasedManifest, Void> manifestContainer = DocumentBasedSingleton.create(router, manifestDoc.getDocumentElement(), DocumentBasedManifest.MANIFEST_TOP_TAG, DocumentBasedManifest.FACTORY); ConversationContainer container = new ConversationContainer(wavelet, manifestContainer); manifestContainer.addListener(container); containers.put(wavelet, container); WaveletBasedConversation conversation = container.getConversation(); if (conversation != null) { conversations.put(wavelet, conversation); triggerOnConversationAdded(conversation); } return container; } /** * Creates a new conversation model on a wavelet. * * @param wavelet wavelet with which to back the conversation * @return a conversation */ private WaveletBasedConversation createNewConversation(ObservableWavelet wavelet) { assert IdUtil.isConversationalId(wavelet.getId()); // Add conversation structure to the wavelet, then get the conversation // which may be constructed in the waiter. WaveletBasedConversation.makeWaveletConversational(wavelet); // The container will have created the conversation. assert conversations.containsKey(wavelet); return conversations.get(wavelet); } private void triggerOnConversationAdded(WaveletBasedConversation conversation) { for (Listener l : listeners) { l.onConversationAdded(conversation); } } private void triggerOnConversationRemoved(WaveletBasedConversation conversation) { for (Listener l : listeners) { l.onConversationRemoved(conversation); } } }