/**
* 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 com.google.common.annotations.VisibleForTesting;
import org.waveprotocol.wave.model.document.Doc;
import org.waveprotocol.wave.model.document.Document;
import org.waveprotocol.wave.model.document.ObservableDocument;
import org.waveprotocol.wave.model.document.operation.DocInitialization;
import org.waveprotocol.wave.model.document.operation.Nindo;
import org.waveprotocol.wave.model.document.util.Point;
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.WaveletId;
import org.waveprotocol.wave.model.id.WaveletIdSerializer;
import org.waveprotocol.wave.model.util.CollectionUtils;
import org.waveprotocol.wave.model.util.CopyOnWriteSet;
import org.waveprotocol.wave.model.util.Preconditions;
import org.waveprotocol.wave.model.util.StringMap;
import org.waveprotocol.wave.model.util.ValueUtils;
import org.waveprotocol.wave.model.wave.Blip;
import org.waveprotocol.wave.model.wave.ObservableWavelet;
import org.waveprotocol.wave.model.wave.ParticipantId;
import org.waveprotocol.wave.model.wave.SourcesEvents;
import org.waveprotocol.wave.model.wave.Wavelet;
import org.waveprotocol.wave.model.wave.WaveletListener;
import org.waveprotocol.wave.model.wave.opbased.WaveletListenerImpl;
import java.util.Set;
/**
* A {@link Conversation} implemented in terms of a {@link Wavelet}.
*
* @author anorth@google.com (Alex North)
*/
public final class WaveletBasedConversation implements ObservableConversation {
/**
* Provides wavelet based collaborators with construction of and access to the
* underlying wavelet and events.
*/
final class ComponentHelper {
/** Gets the conversation for this helper. */
WaveletBasedConversation getConversation() {
return WaveletBasedConversation.this;
}
/** Creates a new thread id. */
String createThreadId() {
// TODO(user): stop using the blip id when wave panel and rusty doesn't
// rely on it.
return idGenerator.peekBlipId();
//return idGenerator.newUniqueToken();
}
/**
* Creates and initialises a blip object in the wavelet.
*
* @param content initial content for the new blip, or {@code null} for
* default content
*/
Blip createBlip(DocInitialization content) {
Blip blip = wavelet.createBlip(idGenerator.newBlipId());
if (content != null) {
blip.getContent().hackConsume(Nindo.fromDocOp(content, false));
} else {
Document doc = blip.getContent();
doc.insertXml(Point.<Doc.N> end(doc.getDocumentElement()),
Blips.INITIAL_CONTENT);
}
return blip;
}
Blip getBlip(String blipId) {
return wavelet.getBlip(blipId);
}
/**
* Gets the source of wavelet-level events.
*/
SourcesEvents<WaveletListener> getWaveletEventSource() {
return wavelet;
}
}
/**
* Listens to threads in this conversation and forwards events to
* conversation listeners.
*/
private final class ThreadListenerAggregator implements WaveletBasedConversationThread.Listener {
private final WaveletBasedConversationThread thread;
ThreadListenerAggregator(WaveletBasedConversationThread thread) {
this.thread = thread;
}
@Override
public void onBlipAdded(WaveletBasedConversationBlip blip) {
observe(blip);
triggerOnBlipAdded(blip);
}
@Override
public void onDeleted() {
thread.removeListener(this);
triggerOnThreadDeleted(thread);
threads.remove(thread.getId());
}
}
/**
* Listens to blips in this conversation and forwards events to
* conversation listeners.
*/
private final class BlipListenerAggregator implements WaveletBasedConversationBlip.Listener {
private final WaveletBasedConversationBlip blip;
BlipListenerAggregator(WaveletBasedConversationBlip blip) {
this.blip = blip;
}
@Override
public void onReplyAdded(WaveletBasedConversationThread reply) {
observe(reply);
triggerOnThreadAdded(reply);
}
@Override
public void onInlineReplyAdded(WaveletBasedConversationThread reply, int location) {
observe(reply);
triggerOnInlineThreadAdded(reply, location);
}
@Override
public void onDeleted() {
blip.removeListener(this);
triggerOnBlipDeleted(blip);
blips.remove(blip.getId());
}
@Override
public void onContributorAdded(ParticipantId contributor) {
triggerOnBlipContributorAdded(blip, contributor);
}
@Override
public void onContributorRemoved(ParticipantId contributor) {
triggerOnBlipContributorRemoved(blip, contributor);
}
@Override
public void onSumbitted() {
triggerOnBlipSubmitted(blip);
}
@Override
public void onTimestampChanged(long oldTimestamp, long newTimestamp) {
triggerOnBlipTimestampChanged(blip, oldTimestamp, newTimestamp);
}
}
/** Forwards wavelet events to the conversation listeners. */
private final WaveletListener waveletListener = new WaveletListenerImpl() {
@Override
public void onParticipantAdded(ObservableWavelet wavelet, ParticipantId participant) {
triggerOnParticipantAdded(participant);
}
@Override
public void onParticipantRemoved(ObservableWavelet wavelet, ParticipantId participant) {
triggerOnParticipantRemoved(participant);
}
};
/** Forwards manifest events to conversation listeners. */
private final ObservableManifest.Listener manifestListener = new ObservableManifest.Listener() {
@Override
public void onAnchorChanged(AnchorData oldAnchor, AnchorData newAnchor) {
triggerOnAnchorChanged(oldAnchor, newAnchor);
}
};
/** Wave containing this conversation. */
private final WaveBasedConversationView wave;
/** Wavelet backing this conversation. */
private final ObservableWavelet wavelet;
/** Generator for component ids. */
private final IdGenerator idGenerator;
/** Value holding anchor information. */
private final ObservableManifest manifest;
/** The root conversation thread. */
private final WaveletBasedConversationThread rootThread;
/** Conversation listeners. */
private final CopyOnWriteSet<Listener> listeners = CopyOnWriteSet.create();
/** Anchor listeners. */
private final CopyOnWriteSet<AnchorListener> anchorListeners = CopyOnWriteSet.create();
/** Blips in this conversation. */
private final StringMap<WaveletBasedConversationBlip> blips = CollectionUtils.createStringMap();
/** Threads in this conversation. */
private final StringMap<ObservableConversationThread> threads = CollectionUtils.createStringMap();
/** Whether this conversation is still active. */
private boolean isUsable = true;
/**
* Checks whether a wavelet has conversation structure.
*/
public static boolean waveletHasConversation(Wavelet wavelet) {
return DocumentBasedManifest.documentHasManifest(getManifestDocument(wavelet));
}
/**
* Builds empty conversation structure on a wavelet.
*
* @throws IllegalStateException if the wavelet already has conversation
* structure
*/
public static void makeWaveletConversational(Wavelet wavelet) {
DocumentBasedManifest.initialiseDocumentManifest(getManifestDocument(wavelet));
}
/**
* Computes the conversation id for a wavelet.
*/
public static String idFor(WaveletId wavelet) {
return WaveletIdSerializer.INSTANCE.toString(wavelet);
}
/**
* Computes a wavelet id for a conversation.
*/
public static WaveletId widFor(String conversation) {
return WaveletIdSerializer.INSTANCE.fromString(conversation);
}
/**
* Builds a conversation model.
*
* @param view view containing this conversation
* @param wavelet wavelet on which to build the conversation
* @param manifest manifest describing the conversation structure
* @param idGenerator generator for new identifiers
* @return a new conversation model
*/
static WaveletBasedConversation create(WaveBasedConversationView view,
ObservableWavelet wavelet, ObservableManifest manifest, IdGenerator idGenerator) {
WaveletBasedConversation conversation =
new WaveletBasedConversation(view, wavelet, manifest, idGenerator);
wavelet.addListener(conversation.waveletListener);
manifest.addListener(conversation.manifestListener);
conversation.observe(conversation.rootThread);
return conversation;
}
/**
* Gets the document on which the conversation manifest is constructed.
*/
@VisibleForTesting
static ObservableDocument getManifestDocument(Wavelet wavelet) {
return wavelet.getDocument(IdConstants.MANIFEST_DOCUMENT_ID);
}
/**
* Constructs a new conversation backed by a wavelet.
*/
WaveletBasedConversation(WaveBasedConversationView wave, ObservableWavelet wavelet,
ObservableManifest manifest, IdGenerator idGenerator) {
Preconditions.checkNotNull(wavelet, "Null wavelet");
Preconditions.checkNotNull(manifest, "Null conversation manifest");
this.wave = wave;
this.wavelet = wavelet;
this.manifest = manifest;
this.idGenerator = idGenerator;
try {
this.rootThread = WaveletBasedConversationThread.create(manifest.getRootThread(), null,
new ComponentHelper());
} catch (RuntimeException e) {
throw new IllegalArgumentException("Failed to create conversation on wavelet "
+ wavelet.getWaveId() + " " + wavelet.getId(), e);
}
}
@Override
public boolean hasAnchor() {
// True if the manifest specifies an anchor to a wavelet in view.
return isValidAnchor(getAnchorWaveletId(), getAnchorBlipId());
}
@Override
public void delete() {
getRootThread().deleteBlips();
DocumentBasedManifest.delete(getManifestDocument(wavelet));
Preconditions.checkState(!isUsable,
"Conversation still usable after delete");
}
@Override
public Anchor getAnchor() {
return maybeMakeAnchor(getAnchorWaveletId(), getAnchorBlipId());
}
@Override
public void setAnchor(Anchor newAnchor) {
checkIsUsable();
if (newAnchor != null) {
Preconditions.checkArgument(newAnchor.getConversation().getClass() == getClass(),
"Anchor must not refer to a different conversation class");
Preconditions.checkArgument(newAnchor.getConversation() != this,
"Anchor must not refer to a different anchored conversation");
WaveletBasedConversation conv = (WaveletBasedConversation) newAnchor.getConversation();
String blipId = newAnchor.getBlip().getId();
manifest.setAnchor(new AnchorData(idFor(conv.getWaveletId()), blipId));
} else {
manifest.setAnchor(new AnchorData(null, null));
}
}
@Override
public Anchor createAnchor(ConversationBlip blip) {
checkIsUsable();
return new Anchor(this, blip);
}
@Override
public WaveletBasedConversationThread getRootThread() {
return rootThread;
}
@Override
public WaveletBasedConversationBlip getBlip(String id) {
return blips.get(id);
}
@Override
public WaveletBasedConversationThread getThread(String threadId) {
return (WaveletBasedConversationThread) threads.get(threadId);
}
@Override
public ObservableDocument getDataDocument(String name) {
if (IdUtil.isBlipId(name)) {
Preconditions.illegalArgument("Cannot fetch blip document " + name + " as a data document");
} else if (IdConstants.MANIFEST_DOCUMENT_ID.equals(name)) {
Preconditions.illegalArgument("Cannot fetch conversation manifest as a data document");
}
return wavelet.getDocument(name);
}
@Override
public Set<ParticipantId> getParticipantIds() {
return wavelet.getParticipantIds();
}
@Override
public void addParticipant(ParticipantId participant) {
checkIsUsable();
wavelet.addParticipant(participant);
}
@Override
public void removeParticipant(ParticipantId participant) {
checkIsUsable();
wavelet.removeParticipant(participant);
}
@Override
public String getId() {
return idFor(getWaveletId());
}
@Override
public void addListener(Listener listener) {
listeners.add(listener);
}
@Override
public void removeListener(Listener listener) {
listeners.remove(listener);
}
@Override
public void addListener(AnchorListener listener) {
anchorListeners.add(listener);
}
@Override
public void removeListener(AnchorListener listener) {
anchorListeners.remove(listener);
}
@Override
public String toString() {
return "WaveletBasedConversation(" + wavelet.getWaveId() + wavelet.getId() + ")";
}
/**
* Gets the wavelet backing this conversation.
*/
public ObservableWavelet getWavelet() {
return wavelet;
}
/**
* Destroys this conversation and detaches listeners. The conversation may
* still be inspected but may not be mutated and will not generate any further
* events.
*/
void destroy() {
checkIsUsable();
wavelet.removeListener(waveletListener);
manifest.removeListener(manifestListener);
listeners.clear();
anchorListeners.clear();
rootThread.destroy();
isUsable = false;
}
/**
* Gets the manifest describing this conversation.
*/
@VisibleForTesting
ObservableManifest getManifest() {
return manifest;
}
private WaveletId getWaveletId() {
return wavelet.getId();
}
private WaveletId getAnchorWaveletId() {
// may be null
String anchorWaveletId = manifest.getAnchor().getConversationId();
return widFor(anchorWaveletId);
}
private String getAnchorBlipId() {
return manifest.getAnchor().getBlipId();
}
/**
* Checks whether a wavelet id and blip id specify a valid anchor.
*/
private boolean isValidAnchor(WaveletId waveletId, String blipId) {
// True if the fields are non-null, the anchoring wavelet is in view, and
// the conversation has the blip.
boolean isValid = false;
if ((waveletId != null) && (blipId != null)) {
WaveletBasedConversation conversation = wave.getConversation(waveletId);
if (conversation != null) {
isValid = (conversation.getBlip(blipId) != null);
}
}
return isValid;
}
/**
* Deserialises a wavelet id if it's not null, else returns null;
*/
private WaveletId maybeMakeWaveletId(String idString) {
return widFor(idString);
}
/**
* Builds an anchor if the wave has a specific wavelet and blip, else returns
* null.
*/
private Anchor maybeMakeAnchor(WaveletId waveletId, String blipId) {
Anchor anchor = null;
if (isValidAnchor(waveletId, blipId)) {
WaveletBasedConversation anchorConversation = wave.getConversation(waveletId);
ConversationBlip anchorBlip = anchorConversation.getBlip(blipId);
anchor = new Anchor(anchorConversation, anchorBlip);
}
return anchor;
}
/**
* Throws {@link IllegalStateException} if this conversation has been
* destroyed.
*/
public void checkIsUsable() {
Preconditions.checkState(isUsable, "Cannot use destroyed conversation");
}
private void observe(WaveletBasedConversationBlip blip) {
blips.put(blip.getId(), blip);
blip.addListener(new BlipListenerAggregator(blip));
for (WaveletBasedConversationThread thread : blip.getReplyThreads()) {
observe(thread);
}
}
private void observe(WaveletBasedConversationThread thread) {
threads.put(thread.getId(), thread);
thread.addListener(new ThreadListenerAggregator(thread));
for (WaveletBasedConversationBlip blip : thread.getBlips()) {
observe(blip);
}
}
private void triggerOnParticipantAdded(ParticipantId participant) {
for (Listener l : listeners) {
l.onParticipantAdded(participant);
}
}
private void triggerOnParticipantRemoved(ParticipantId participant) {
for (Listener l : listeners) {
l.onParticipantRemoved(participant);
}
}
private void triggerOnAnchorChanged(AnchorData oldMAnchor, AnchorData newMAnchor) {
WaveletId oldWaveletId = maybeMakeWaveletId(oldMAnchor.getConversationId());
WaveletId newWaveletId = maybeMakeWaveletId(newMAnchor.getConversationId());
Anchor oldAnchor = maybeMakeAnchor(oldWaveletId, oldMAnchor.getBlipId());
Anchor newAnchor = maybeMakeAnchor(newWaveletId, newMAnchor.getBlipId());
triggerOnAnchorChanged(oldAnchor, newAnchor);
}
private void triggerOnAnchorChanged(Anchor oldAnchor, Anchor newAnchor) {
if (ValueUtils.notEqual(oldAnchor, newAnchor)) {
for (AnchorListener l : anchorListeners) {
l.onAnchorChanged(oldAnchor, newAnchor);
}
}
}
private void triggerOnBlipAdded(ObservableConversationBlip blip) {
for (Listener l : listeners) {
l.onBlipAdded(blip);
}
}
private void triggerOnBlipDeleted(ObservableConversationBlip blip) {
for (Listener l : listeners) {
l.onBlipDeleted(blip);
}
}
private void triggerOnThreadAdded(ObservableConversationThread thread) {
for (Listener l : listeners) {
l.onThreadAdded(thread);
}
}
private void triggerOnInlineThreadAdded(ObservableConversationThread thread, int location) {
for (Listener l : listeners) {
l.onInlineThreadAdded(thread, location);
}
}
private void triggerOnThreadDeleted(ObservableConversationThread thread) {
for (Listener l : listeners) {
l.onThreadDeleted(thread);
}
}
private void triggerOnBlipContributorAdded(ObservableConversationBlip blip,
ParticipantId contributor) {
for (Listener l : listeners) {
l.onBlipContributorAdded(blip, contributor);
}
}
private void triggerOnBlipContributorRemoved(ObservableConversationBlip blip,
ParticipantId contributor) {
for (Listener l : listeners) {
l.onBlipContributorRemoved(blip, contributor);
}
}
private void triggerOnBlipSubmitted(ObservableConversationBlip blip) {
for (Listener l : listeners) {
l.onBlipSumbitted(blip);
}
}
private void triggerOnBlipTimestampChanged(ObservableConversationBlip blip, long oldTimestamp,
long newTimestamp) {
for (Listener l : listeners) {
l.onBlipTimestampChanged(blip, oldTimestamp, newTimestamp);
}
}
}