/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.box.webclient.search;
import org.waveprotocol.box.common.Snippets;
import org.waveprotocol.wave.client.state.BlipReadStateMonitor;
import org.waveprotocol.wave.model.conversation.Conversation;
import org.waveprotocol.wave.model.conversation.ConversationStructure;
import org.waveprotocol.wave.model.conversation.TitleHelper;
import org.waveprotocol.wave.model.document.WaveContext;
import org.waveprotocol.wave.model.id.IdUtil;
import org.waveprotocol.wave.model.id.WaveId;
import org.waveprotocol.wave.model.util.CollectionUtils;
import org.waveprotocol.wave.model.util.CopyOnWriteSet;
import org.waveprotocol.wave.model.version.HashedVersion;
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.WaveViewListener;
import org.waveprotocol.wave.model.wave.Wavelet;
import org.waveprotocol.wave.model.wave.WaveletListener;
import org.waveprotocol.wave.model.wave.data.ObservableWaveletData;
import java.util.List;
import java.util.Set;
/**
* Produces a digest from a wave.
*
* @author hearnden@google.com (David Hearnden)
*/
public final class WaveBasedDigest
implements Digest, BlipReadStateMonitor.Listener, WaveViewListener, WaveletListener {
private final static int PARTICIPANT_SNIPPET_SIZE = 2;
private final static double NO_TIME = 1;
/** The wave to digest. */
private final WaveContext wave;
/** Observers of this digest. */
// TODO(hearnden): make a single listener.
private final CopyOnWriteSet<Listener> listeners = CopyOnWriteSet.create();
// Lazily-constructed cached answers.
private List<ParticipantId> participantSnippet;
private ParticipantId author;
private double lastModified = NO_TIME;
String snippet = null;
WaveBasedDigest(WaveContext wave) {
this.wave = wave;
}
/**
* Creates a digest.
*/
public static WaveBasedDigest create(WaveContext wave) {
WaveBasedDigest digest = new WaveBasedDigest(wave);
digest.init();
return digest;
}
private void init() {
for (ObservableWavelet wavelet : wave.getWave().getWavelets()) {
wavelet.addListener(this);
}
wave.getWave().addListener(this);
wave.getBlipMonitor().addListener(this);
}
/**
* Releases listeners from observed resources.
*/
void destroy() {
wave.getBlipMonitor().removeListener(this);
wave.getWave().removeListener(this);
for (ObservableWavelet wavelet : wave.getWave().getWavelets()) {
wavelet.removeListener(this);
}
}
private void ensureParticipants() {
if (participantSnippet != null) {
return;
}
// Participant order is defined as follows:
//
// 1. Let the conversations be in their natural order, except with the main
// conversation promoted to first. Project out the participants of those
// conversations into a list.
// 2. The digest author is the first participant in that list.
// 3. The participant snippet is the next PARTICIPANT_SNIPPET_SIZE unique
// participants in that list.
Conversation main = ConversationStructure.getMainConversation(wave.getConversations());
List<Conversation> conversations = CollectionUtils.newLinkedList();
conversations.addAll(wave.getConversations().getConversations());
// Waves are not forced to have conversations in them, so it is legitimate
// for main to be null.
if (main != null) {
conversations.remove(main);
conversations.add(0, main);
}
// Collect the author and participants in the same list, then partition
// afterwards.
participantSnippet = CollectionUtils.newArrayList();
outer: for (Conversation conversation : conversations) {
for (ParticipantId participant : conversation.getParticipantIds()) {
if (participantSnippet.size() < PARTICIPANT_SNIPPET_SIZE + 1) {
participantSnippet.add(participant);
} else {
break outer;
}
}
}
if (!participantSnippet.isEmpty()) {
author = participantSnippet.get(0);
participantSnippet = participantSnippet.subList(1, participantSnippet.size());
}
}
private void invalidateParticipants() {
author = null;
participantSnippet = null;
}
private void ensureLmt() {
if (lastModified != NO_TIME) {
return;
}
for (Wavelet wavelet : wave.getWave().getWavelets()) {
if (!IdUtil.isConversationalId(wavelet.getId())) {
// Skip non conversational wavelets.
continue;
}
lastModified = Math.max(lastModified, wavelet.getLastModifiedTime());
}
}
@SuppressWarnings("unused")
private void invalidateLmt() {
lastModified = NO_TIME;
}
@Override
public WaveId getWaveId() {
return wave.getWave().getWaveId();
}
@Override
public ParticipantId getAuthor() {
ensureParticipants();
return author;
}
@Override
public List<ParticipantId> getParticipantsSnippet() {
ensureParticipants();
return participantSnippet;
}
@Override
public String getTitle() {
return TitleHelper.getTitle(wave);
}
@Override
public String getSnippet() {
updateSnippet(wave.getWave().getRoot());
return snippet;
}
@Override
public int getBlipCount() {
return wave.getBlipMonitor().getReadCount() + wave.getBlipMonitor().getUnreadCount();
}
@Override
public int getUnreadCount() {
return wave.getBlipMonitor().getUnreadCount();
}
@Override
public double getLastModifiedTime() {
ensureLmt();
return lastModified;
}
//
// Events sent by this digest.
//
/**
* Observes digest changes.
*/
interface Listener {
void onChanged();
}
public void addListener(Listener listener) {
listeners.add(listener);
}
public void removeListener(Listener listener) {
listeners.remove(listener);
}
private void fireOnChanged() {
for (Listener listener : listeners) {
listener.onChanged();
}
}
//
// Events of interest to this digest.
//
@Override
public void onReadStateChanged() {
fireOnChanged();
}
private void updateSnippet(ObservableWavelet wavelet) {
ObservableWaveletData waveletData = wavelet.getWaveletData();
if (waveletData != null){
Set<String> docsIds = waveletData.getDocumentIds();
if (!docsIds.contains("conversation")) {
return;
}
snippet = Snippets.renderSnippet(waveletData, Snippets.DIGEST_SNIPPET_LENGTH).trim();
String title = getTitle();
if (snippet.startsWith(title) && !title.isEmpty()) {
// Strip the title from the snippet if the snippet starts with the title.
snippet = snippet.substring(title.length());
}
}
}
@Override
public void onLastModifiedTimeChanged(ObservableWavelet wavelet, long oldTime, long newTime) {
if (newTime != oldTime) {
fireOnChanged();
}
}
@Override
public void onWaveletAdded(ObservableWavelet wavelet) {
wavelet.addListener(this);
}
@Override
public void onWaveletRemoved(ObservableWavelet wavelet) {
wavelet.removeListener(this);
}
@Override
public void onParticipantAdded(ObservableWavelet wavelet, ParticipantId participant) {
invalidateParticipants();
fireOnChanged();
}
@Override
public void onParticipantRemoved(ObservableWavelet wavelet, ParticipantId participant) {
invalidateParticipants();
fireOnChanged();
}
//
// Events not of interest.
//
@Override
public void onBlipAdded(ObservableWavelet wavelet, Blip blip) {
}
@Override
public void onBlipRemoved(ObservableWavelet wavelet, Blip blip) {
}
@Override
public void onBlipSubmitted(ObservableWavelet wavelet, Blip blip) {
}
@Override
public void onBlipContributorAdded(
ObservableWavelet wavelet, Blip blip, ParticipantId contributor) {
}
@Override
public void onBlipContributorRemoved(
ObservableWavelet wavelet, Blip blip, ParticipantId contributor) {
}
@Override
public void onBlipTimestampModified(
ObservableWavelet wavelet, Blip blip, long oldTime, long newTime) {
}
@Override
public void onBlipVersionModified(
ObservableWavelet wavelet, Blip blip, Long oldVersion, Long newVersion) {
}
@Override
@Deprecated
public void onRemoteBlipContentModified(ObservableWavelet wavelet, Blip blip) {
}
@Override
public void onHashedVersionChanged(
ObservableWavelet wavelet, HashedVersion oldHashedVersion, HashedVersion newHashedVersion) {
}
@Override
public void onVersionChanged(ObservableWavelet wavelet, long oldVersion, long newVersion) {
}
}