/**
* 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.operation.DocInitialization;
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.ReadableStringMap.ProcV;
import org.waveprotocol.wave.model.util.StringMap;
import org.waveprotocol.wave.model.wave.Blip;
import org.waveprotocol.wave.model.wave.SourcesEvents;
import java.util.Iterator;
/**
* A conversation thread backed by a region of a wavelet's manifest document.
*
* @author anorth@google.com (Alex North)
*/
final class WaveletBasedConversationThread implements ObservableConversationThread,
SourcesEvents<WaveletBasedConversationThread.Listener>, ObservableManifestThread.Listener {
/** Receives events on a conversation thread. */
interface Listener {
/**
* Notifies this listener that a blip has been added to this thread.
*
* @param blip the new blip
*/
void onBlipAdded(WaveletBasedConversationBlip blip);
/**
* Notifies this listener that the thread was removed from the conversation.
* No further methods may be called on the thread.
*/
void onDeleted();
}
/** Manifest entry for this thread. */
private final ObservableManifestThread manifestThread;
/** Blip to which this thread is a reply (null for root thread). */
private final WaveletBasedConversationBlip parentBlip;
/** Helper for wavelet access. */
private final WaveletBasedConversation.ComponentHelper helper;
/** Blips in this thread. */
private final StringMap<WaveletBasedConversationBlip> blips = CollectionUtils.createStringMap();
/** Whether this thread is safe to use. Set false when deleted. */
private boolean isUsable = true;
private final CopyOnWriteSet<Listener> listeners = CopyOnWriteSet.create();
/**
* Creates a new conversation thread.
*
* @param manifestThread data for the thread
* @param parentBlip blip to which this thread is a reply (null for root)
* @param helper provides conversation components
*/
static WaveletBasedConversationThread create(ObservableManifestThread manifestThread,
WaveletBasedConversationBlip parentBlip, WaveletBasedConversation.ComponentHelper helper) {
WaveletBasedConversationThread thread = new WaveletBasedConversationThread(manifestThread,
parentBlip, helper);
for (ObservableManifestBlip manifestBlip : manifestThread.getBlips()) {
Blip blip = helper.getBlip(manifestBlip.getId());
if (blip != null) {
thread.adaptBlip(manifestBlip, blip);
}
}
manifestThread.addListener(thread);
return thread;
}
private WaveletBasedConversationThread(ObservableManifestThread manifestThread,
WaveletBasedConversationBlip parentBlip, WaveletBasedConversation.ComponentHelper helper) {
Preconditions.checkNotNull(manifestThread,
"WaveletBasedConversationThread received null manifest thread");
this.manifestThread = manifestThread;
this.helper = helper;
this.parentBlip = parentBlip;
}
@Override
public WaveletBasedConversation getConversation() {
return helper.getConversation();
}
@Override
public WaveletBasedConversationBlip getParentBlip() {
return parentBlip;
}
@Override
public Iterable<WaveletBasedConversationBlip> getBlips() {
final Iterable<? extends ObservableManifestBlip> manifestBlips = manifestThread.getBlips();
return new Iterable<WaveletBasedConversationBlip>() {
@Override
public Iterator<WaveletBasedConversationBlip> iterator() {
return WrapperIterator.create(manifestBlips.iterator(), blips);
}
};
}
@Override
public WaveletBasedConversationBlip getFirstBlip() {
WaveletBasedConversationBlip result = null;
if (!blips.isEmpty()) {
ObservableManifestBlip manifestBlip = manifestThread.getBlip(0);
result = blips.get(manifestBlip.getId());
if (result == null) {
// Very uncommon case: the first blip in the manifest doesn't have a
// corresponding blip object. Fall back to iterating since iteration
// handles this correctly.
for (WaveletBasedConversationBlip firstBlip : getBlips()) {
result = firstBlip;
break;
}
}
}
return result;
}
@Override
public WaveletBasedConversationBlip appendBlip() {
checkIsUsable();
return appendBlipWithContent(null);
}
@Override
public WaveletBasedConversationBlip appendBlip(DocInitialization content) {
Preconditions.checkNotNull(content, "initialization is null");
checkIsUsable();
return appendBlipWithContent(content);
}
@Override
public WaveletBasedConversationBlip insertBlip(ConversationBlip successor) {
checkIsUsable();
if (!blips.containsKey(successor.getId())) {
Preconditions.illegalArgument(
"Can't insert blip before blip " + successor + " not from this thread");
}
WaveletBasedConversationBlip insertBefore = (WaveletBasedConversationBlip) successor;
int index = manifestThread.indexOf(insertBefore.getManifestBlip());
Blip blip = helper.createBlip(null);
String blipId = blip.getId();
manifestThread.insertBlip(index, blipId);
return blips.get(blipId);
}
@Override
public String getId() {
return (this == getConversation().getRootThread()) ? "" : manifestThread.getId();
}
@Override
public void delete() {
if (isRootThread()) {
deleteBlips();
} else {
parentBlip.deleteThread(this);
}
}
@Override
public void addListener(Listener listener) {
listeners.add(listener);
}
@Override
public void removeListener(Listener listener) {
listeners.remove(listener);
}
//
// ObservableManifestThread.Listener
// These methods update local data structures in response to changes in
// the underlying data, either synchronously in local methods or from
// remote changes. They don't make further changes to the data.
//
@Override
public void onBlipAdded(ObservableManifestBlip manifestBlip) {
Blip blip = helper.getBlip(manifestBlip.getId());
if (blip != null) {
// Note that this means the blip will be ignored if it doesn't exist in
// the wavelet when the manifest entry is added.
WaveletBasedConversationBlip convBlip = adaptBlip(manifestBlip, blip);
triggerOnBlipAdded(convBlip);
}
}
@Override
public void onBlipRemoved(ObservableManifestBlip blip) {
WaveletBasedConversationBlip blipToRemove = blips.get(blip.getId());
if (blipToRemove != null) {
forgetBlip(blipToRemove);
}
}
@Override
public String toString() {
return "WaveletBasedConversationThread(id = " + manifestThread.getId() + ", inline = "
+ manifestThread.isInline() + ")";
}
// Package-private methods for WaveletBasedConversationBlip.
ManifestThread getManifestThread() {
return manifestThread;
}
/**
* Deletes a blip from this thread, deleting that blip's replies.
*/
void deleteBlip(WaveletBasedConversationBlip blipToDelete, boolean shouldDeleteSelfIfEmpty) {
Preconditions.checkArgument(blips.containsKey(blipToDelete.getId()),
"Can't delete blip not from this thread");
blipToDelete.deleteThreads();
manifestThread.removeBlip(blipToDelete.getManifestBlip());
blipToDelete.clearContent();
if (shouldDeleteSelfIfEmpty) {
deleteSelfIfEmpty();
}
}
/**
* Deletes all blips from this thread.
*
* @see #deleteBlip(WaveletBasedConversationBlip, boolean)
*/
void deleteBlips() {
for (WaveletBasedConversationBlip replyBlip : CollectionUtils.valueList(blips)) {
deleteBlip(replyBlip, false);
}
}
void deleteSelfIfEmpty() {
if (blips.isEmpty() && !isRootThread()) {
parentBlip.deleteThread(this);
}
}
/**
* Invalidates this thread. It may no longer be accessed.
*/
void invalidate() {
checkIsUsable();
manifestThread.removeListener(this);
isUsable = false;
}
/**
* Recursively invalidates this thread and its blips.
*/
void destroy() {
blips.each(new ProcV<WaveletBasedConversationBlip>() {
@Override
public void apply(String key, WaveletBasedConversationBlip blip) {
blip.destroy();
}
});
invalidate();
listeners.clear();
}
/**
* Checks that this thread is safe to access.
*/
@VisibleForTesting
void checkIsUsable() {
if (!isUsable) {
Preconditions.illegalState("Deleted thread is not usable: " + this);
}
}
/**
* Checks whether this is the root thread.
*/
private boolean isRootThread() {
return parentBlip == null;
}
/**
* Appends a blip.
*
* @param content optional content
* @return new blip.
*/
private WaveletBasedConversationBlip appendBlipWithContent(DocInitialization content) {
Blip blip = helper.createBlip(content);
String blipId = blip.getId();
manifestThread.appendBlip(blipId);
return blips.get(blipId);
}
/**
* Creates a conversation blip backed by a manifest blip and inserts it in
* {@code blips}.
*/
private WaveletBasedConversationBlip adaptBlip(ObservableManifestBlip manifestBlip, Blip blip) {
WaveletBasedConversationBlip convBlip =
WaveletBasedConversationBlip.create(manifestBlip, blip, this, helper);
blips.put(manifestBlip.getId(), convBlip);
return convBlip;
}
/**
* Removes a blip from the internal list and triggers its deletion event.
*/
private void forgetBlip(WaveletBasedConversationBlip blipToRemove) {
String idToRemove = blipToRemove.getId();
assert blips.containsKey(idToRemove);
blips.remove(idToRemove);
blipToRemove.triggerOnDeleted();
}
private void triggerOnBlipAdded(WaveletBasedConversationBlip blip) {
for (Listener l : listeners) {
l.onBlipAdded(blip);
}
}
// Package-private for access from WaveletBasedConversationBlip
void triggerOnDeleted() {
invalidate();
for (Listener l : listeners) {
l.onDeleted();
}
}
}