Author: yurize
Date: Sat Nov 26 19:27:56 2011
New Revision: 1206549
URL: http://svn.apache.org/viewvc?rev=1206549&view=rev
Log:
Adds some files forgotten in previous commit
Added:
incubator/wave/trunk/src/org/waveprotocol/box/server/waveserver/WaveDigester.java
incubator/wave/trunk/test/org/waveprotocol/box/server/robots/operations/TestingWaveletData.java
incubator/wave/trunk/test/org/waveprotocol/box/server/waveserver/WaveDigesterTest.java
Added:
incubator/wave/trunk/src/org/waveprotocol/box/server/waveserver/WaveDigester.java
URL:
http://svn.apache.org/viewvc/incubator/wave/trunk/src/org/waveprotocol/box/server/waveserver/WaveDigester.java?rev=1206549&view=auto
==============================================================================
---
incubator/wave/trunk/src/org/waveprotocol/box/server/waveserver/WaveDigester.java
(added)
+++
incubator/wave/trunk/src/org/waveprotocol/box/server/waveserver/WaveDigester.java
Sat Nov 26 19:27:56 2011
@@ -0,0 +1,239 @@
+/**
+ * Copyright 2011 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.box.server.waveserver;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.inject.Inject;
+import com.google.wave.api.ApiIdSerializer;
+import com.google.wave.api.SearchResult;
+import com.google.wave.api.SearchResult.Digest;
+
+import org.waveprotocol.box.common.Snippets;
+import org.waveprotocol.box.server.robots.util.ConversationUtil;
+import org.waveprotocol.wave.model.conversation.BlipIterators;
+import org.waveprotocol.wave.model.conversation.ConversationBlip;
+import org.waveprotocol.wave.model.conversation.ObservableConversation;
+import org.waveprotocol.wave.model.conversation.ObservableConversationBlip;
+import org.waveprotocol.wave.model.conversation.ObservableConversationView;
+import org.waveprotocol.wave.model.conversation.TitleHelper;
+import org.waveprotocol.wave.model.conversation.WaveletBasedConversation;
+import org.waveprotocol.wave.model.document.Document;
+import org.waveprotocol.wave.model.id.IdUtil;
+import org.waveprotocol.wave.model.id.ModernIdSerialiser;
+import org.waveprotocol.wave.model.id.WaveletId;
+import org.waveprotocol.wave.model.supplement.PrimitiveSupplement;
+import org.waveprotocol.wave.model.supplement.PrimitiveSupplementImpl;
+import org.waveprotocol.wave.model.supplement.SupplementedWave;
+import org.waveprotocol.wave.model.supplement.SupplementedWaveImpl;
+import org.waveprotocol.wave.model.supplement.WaveletBasedSupplement;
+import
org.waveprotocol.wave.model.supplement.SupplementedWaveImpl.DefaultFollow;
+import org.waveprotocol.wave.model.util.CollectionUtils;
+import org.waveprotocol.wave.model.wave.ParticipantId;
+import org.waveprotocol.wave.model.wave.data.ObservableWaveletData;
+import org.waveprotocol.wave.model.wave.data.WaveViewData;
+import org.waveprotocol.wave.model.wave.data.WaveletData;
+import org.waveprotocol.wave.model.wave.opbased.OpBasedWavelet;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Generates digests for the search service.
+ *
+ * @author [email protected]
+ */
+public class WaveDigester {
+
+ private final ConversationUtil conversationUtil;
+ private static final int DIGEST_SNIPPET_LENGTH = 140;
+ private static final int PARTICIPANTS_SNIPPET_LENGTH = 5;
+ private static final String EMPTY_WAVELET_TITLE = "";
+
+ @Inject
+ public WaveDigester(ConversationUtil conversationUtil) {
+ this.conversationUtil = conversationUtil;
+ }
+
+ public SearchResult generateSearchResult(ParticipantId participant, String
query,
+ Collection<WaveViewData> results) {
+ // Generate exactly one digest per wave. This includes conversational and
+ // non-conversational waves. The position-based API for search prevents the
+ // luxury of extra filtering here. Filtering can only be done in the
+ // searchProvider. All waves returned by the search provider must be
+ // included in the search result.
+ SearchResult result = new SearchResult(query);
+ if (results == null) {
+ return result;
+ }
+ for (WaveViewData wave : results) {
+ // Note: the indexing infrastructure only supports single-conversation
+ // waves, and requires raw wavelet access for snippeting.
+ ObservableWaveletData root = null;
+ ObservableWaveletData other = null;
+ ObservableWaveletData udw = null;
+ for (ObservableWaveletData waveletData : wave.getWavelets()) {
+ WaveletId waveletId = waveletData.getWaveletId();
+ if (IdUtil.isConversationRootWaveletId(waveletId)) {
+ root = waveletData;
+ } else if (IdUtil.isConversationalId(waveletId)) {
+ other = waveletData;
+ } else if (IdUtil.isUserDataWavelet(waveletId)) {
+ // assume this is the user data wavelet for the right user.
+ udw = waveletData;
+ }
+ }
+
+ ObservableWaveletData convWavelet = root != null ? root : other;
+ SupplementedWave supplement = null;
+ ObservableConversationView conversations = null;
+ if (convWavelet != null) {
+ OpBasedWavelet wavelet = OpBasedWavelet.createReadOnly(convWavelet);
+ if (WaveletBasedConversation.waveletHasConversation(wavelet)) {
+ conversations = conversationUtil.buildConversation(wavelet);
+ supplement = buildSupplement(participant, conversations, udw);
+ }
+ }
+ if (conversations != null) {
+ // This is a conversational wave. Produce a conversational digest.
+ result.addDigest(generateDigest(conversations, supplement,
convWavelet));
+ } else {
+ // It is unknown how to present this wave.
+ result.addDigest(generateEmptyorUnknownDigest(wave));
+ }
+ }
+
+ assert result.getDigests().size() == results.size();
+ return result;
+ }
+
+ /**
+ * Produces a digest for a set of conversations. Never returns null.
+ *
+ * @param conversations the conversation.
+ * @param supplement the supplement that allows to easily perform various
+ * queries on user related state of the wavelet.
+ * @param rawWaveletData the waveletData from which the digest is generated.
+ * This wavelet is a copy.
+ * @return the server representation of the digest for the query.
+ */
+ Digest generateDigest(ObservableConversationView conversations,
SupplementedWave supplement,
+ WaveletData rawWaveletData) {
+ ObservableConversation rootConversation = conversations.getRoot();
+ ObservableConversationBlip firstBlip = null;
+ if (rootConversation != null && rootConversation.getRootThread() != null
+ && rootConversation.getRootThread().getFirstBlip() != null) {
+ firstBlip = rootConversation.getRootThread().getFirstBlip();
+ }
+ String title;
+ if (firstBlip != null) {
+ Document firstBlipContents = firstBlip.getContent();
+ title = TitleHelper.extractTitle(firstBlipContents).trim();
+ } else {
+ title = EMPTY_WAVELET_TITLE;
+ }
+
+ String snippet = Snippets.renderSnippet(rawWaveletData,
DIGEST_SNIPPET_LENGTH).trim();
+ if (snippet.startsWith(title) && !title.isEmpty()) {
+ // Strip the title from the snippet if the snippet starts with the title.
+ snippet = snippet.substring(title.length());
+ }
+ String waveId =
ApiIdSerializer.instance().serialiseWaveId(rawWaveletData.getWaveId());
+ List<String> participants = CollectionUtils.newArrayList();
+ for (ParticipantId p : rawWaveletData.getParticipants()) {
+ if (participants.size() < PARTICIPANTS_SNIPPET_LENGTH) {
+ participants.add(p.getAddress());
+ } else {
+ break;
+ }
+ }
+ int unreadCount = 0;
+ int blipCount = 0;
+ for (ConversationBlip blip : BlipIterators.breadthFirst(rootConversation))
{
+ if (supplement.isUnread(blip)) {
+ unreadCount++;
+ }
+ blipCount++;
+ }
+ return new Digest(title, snippet, waveId, participants,
rawWaveletData.getLastModifiedTime(),
+ rawWaveletData.getCreationTime(), unreadCount, blipCount);
+ }
+
+ /** @return a digest for an empty wave. */
+ private Digest emptyDigest(WaveViewData wave) {
+ String title =
ModernIdSerialiser.INSTANCE.serialiseWaveId(wave.getWaveId());
+ String id = ApiIdSerializer.instance().serialiseWaveId(wave.getWaveId());
+ return new Digest(title, "(empty)", id, Collections.<String> emptyList(),
-1L, -1L, 0, 0);
+ }
+
+ /** @return a digest for an unrecognised type of wave. */
+ private Digest unknownDigest(WaveViewData wave) {
+ String title =
ModernIdSerialiser.INSTANCE.serialiseWaveId(wave.getWaveId());
+ String id = ApiIdSerializer.instance().serialiseWaveId(wave.getWaveId());
+ long lmt = -1L;
+ long created = -1L;
+ int docs = 0;
+ List<String> participants = new ArrayList<String>();
+ for (WaveletData data : wave.getWavelets()) {
+ lmt = Math.max(lmt, data.getLastModifiedTime());
+ created = Math.max(lmt, data.getCreationTime());
+ docs += data.getDocumentIds().size();
+
+ for (ParticipantId p : data.getParticipants()) {
+ if (participants.size() < PARTICIPANTS_SNIPPET_LENGTH) {
+ participants.add(p.getAddress());
+ } else {
+ break;
+ }
+ }
+ }
+ return new Digest(title, "(unknown)", id, participants, lmt, created, 0,
docs);
+ }
+
+ /**
+ * Generates an empty digest in case the wave is empty, or an unknown digest
+ * otherwise.
+ *
+ * @param wave the wave.
+ * @return the generated digest.
+ */
+ Digest generateEmptyorUnknownDigest(WaveViewData wave) {
+ boolean empty = !wave.getWavelets().iterator().hasNext();
+ Digest digest = empty ? emptyDigest(wave) : unknownDigest(wave);
+ return digest;
+ }
+
+ /**
+ * Builds the supplement model from a wave. Never returns null.
+ *
+ * @param viewer the participant for which the supplement is constructed.
+ * @param conversations conversations in the wave
+ * @param udw the user data wavelet for the logged user.
+ * @return the wave supplement.
+ */
+ @VisibleForTesting
+ SupplementedWave buildSupplement(ParticipantId viewer,
ObservableConversationView conversations,
+ ObservableWaveletData udw) {
+ // Use mock state if there is no UDW.
+ PrimitiveSupplement udwState =
+ udw != null ?
WaveletBasedSupplement.create(OpBasedWavelet.createReadOnly(udw))
+ : new PrimitiveSupplementImpl();
+ return SupplementedWaveImpl.create(udwState, conversations, viewer,
DefaultFollow.ALWAYS);
+ }
+}
Added:
incubator/wave/trunk/test/org/waveprotocol/box/server/robots/operations/TestingWaveletData.java
URL:
http://svn.apache.org/viewvc/incubator/wave/trunk/test/org/waveprotocol/box/server/robots/operations/TestingWaveletData.java?rev=1206549&view=auto
==============================================================================
---
incubator/wave/trunk/test/org/waveprotocol/box/server/robots/operations/TestingWaveletData.java
(added)
+++
incubator/wave/trunk/test/org/waveprotocol/box/server/robots/operations/TestingWaveletData.java
Sat Nov 26 19:27:56 2011
@@ -0,0 +1,109 @@
+/**
+ * Copyright 2011 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.box.server.robots.operations;
+
+import com.google.common.collect.ImmutableList;
+
+import org.waveprotocol.box.server.util.WaveletDataUtil;
+import org.waveprotocol.wave.model.conversation.Conversation;
+import org.waveprotocol.wave.model.conversation.ConversationBlip;
+import org.waveprotocol.wave.model.conversation.ConversationView;
+import org.waveprotocol.wave.model.conversation.TitleHelper;
+import org.waveprotocol.wave.model.conversation.WaveBasedConversationView;
+import org.waveprotocol.wave.model.conversation.WaveletBasedConversation;
+import org.waveprotocol.wave.model.document.util.LineContainers;
+import org.waveprotocol.wave.model.document.util.XmlStringBuilder;
+import org.waveprotocol.wave.model.id.WaveId;
+import org.waveprotocol.wave.model.id.WaveletId;
+import org.waveprotocol.wave.model.operation.SilentOperationSink;
+import
org.waveprotocol.wave.model.operation.wave.BasicWaveletOperationContextFactory;
+import org.waveprotocol.wave.model.operation.wave.WaveletOperation;
+import org.waveprotocol.wave.model.testing.BasicFactories;
+import org.waveprotocol.wave.model.testing.FakeIdGenerator;
+import org.waveprotocol.wave.model.version.HashedVersion;
+import org.waveprotocol.wave.model.wave.ParticipantId;
+import org.waveprotocol.wave.model.wave.ParticipationHelper;
+import org.waveprotocol.wave.model.wave.ReadOnlyWaveView;
+import org.waveprotocol.wave.model.wave.data.ObservableWaveletData;
+import org.waveprotocol.wave.model.wave.data.WaveViewData;
+import org.waveprotocol.wave.model.wave.data.WaveletData;
+import org.waveprotocol.wave.model.wave.data.impl.WaveViewDataImpl;
+import org.waveprotocol.wave.model.wave.data.impl.WaveletDataImpl;
+import org.waveprotocol.wave.model.wave.opbased.OpBasedWavelet;
+
+import java.util.List;
+
+/**
+ * Builds a wavelet and provides direct access to the various layers of
+ * abstraction.
+ *
+ * @author [email protected] (Joseph Gentle)
+ */
+public class TestingWaveletData {
+ private final ObservableWaveletData waveletData;
+ private final ObservableWaveletData userWaveletData;
+ private final Conversation conversation;
+ private final WaveViewData waveViewData;
+
+ public TestingWaveletData(
+ WaveId waveId, WaveletId waveletId, ParticipantId author, boolean
isConversational) {
+ waveletData =
+ new WaveletDataImpl(waveletId, author, 1234567890, 0,
HashedVersion.unsigned(0), 0,
+ waveId,
BasicFactories.observablePluggableMutableDocumentFactory());
+ userWaveletData =
+ new WaveletDataImpl(WaveletId.of("example.com",
"[email protected]"), author,
+ 1234567890, 0, HashedVersion.unsigned(0), 0,
+ waveId, BasicFactories.observablePluggableMutableDocumentFactory());
+
+ OpBasedWavelet wavelet =
+ new OpBasedWavelet(waveId, waveletData, new
BasicWaveletOperationContextFactory(author),
+ ParticipationHelper.DEFAULT,
+ SilentOperationSink.Executor.<WaveletOperation,
WaveletData>build(waveletData),
+ SilentOperationSink.VOID);
+ ReadOnlyWaveView waveView = new ReadOnlyWaveView(waveId);
+ waveView.addWavelet(wavelet);
+
+ if (isConversational) {
+ ConversationView conversationView =
WaveBasedConversationView.create(waveView, FakeIdGenerator.create());
+ WaveletBasedConversation.makeWaveletConversational(wavelet);
+ conversation = conversationView.getRoot();
+
+ conversation.addParticipant(author);
+ } else {
+ conversation = null;
+ }
+
+ waveViewData = WaveViewDataImpl.create(waveId,
ImmutableList.of(waveletData, userWaveletData));
+ }
+
+ public void appendBlipWithText(String text) {
+ ConversationBlip blip = conversation.getRootThread().appendBlip();
+ LineContainers.appendToLastLine(blip.getContent(),
XmlStringBuilder.createText(text));
+ TitleHelper.maybeFindAndSetImplicitTitle(blip.getContent());
+ }
+
+ public List<ObservableWaveletData> copyWaveletData() {
+ // This data object already has an op-based owner on top. Must copy it.
+ return ImmutableList.of(WaveletDataUtil.copyWavelet(waveletData),
+ WaveletDataUtil.copyWavelet(userWaveletData));
+ }
+
+ public WaveViewData copyViewData() {
+ return WaveViewDataImpl.create(waveViewData.getWaveId(),copyWaveletData());
+ }
+}
Added:
incubator/wave/trunk/test/org/waveprotocol/box/server/waveserver/WaveDigesterTest.java
URL:
http://svn.apache.org/viewvc/incubator/wave/trunk/test/org/waveprotocol/box/server/waveserver/WaveDigesterTest.java?rev=1206549&view=auto
==============================================================================
---
incubator/wave/trunk/test/org/waveprotocol/box/server/waveserver/WaveDigesterTest.java
(added)
+++
incubator/wave/trunk/test/org/waveprotocol/box/server/waveserver/WaveDigesterTest.java
Sat Nov 26 19:27:56 2011
@@ -0,0 +1,101 @@
+/**
+ * Copyright 2011 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.box.server.waveserver;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static
org.waveprotocol.box.server.util.testing.TestingConstants.PARTICIPANT;
+import static
org.waveprotocol.box.server.util.testing.TestingConstants.WAVE_ID;
+
+import com.google.wave.api.SearchResult.Digest;
+
+import junit.framework.TestCase;
+
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.waveprotocol.box.server.robots.operations.TestingWaveletData;
+import org.waveprotocol.box.server.robots.util.ConversationUtil;
+import org.waveprotocol.wave.model.conversation.ConversationBlip;
+import org.waveprotocol.wave.model.conversation.ObservableConversationView;
+import org.waveprotocol.wave.model.id.IdGenerator;
+import org.waveprotocol.wave.model.id.WaveletId;
+import org.waveprotocol.wave.model.supplement.SupplementedWave;
+import org.waveprotocol.wave.model.wave.ObservableWavelet;
+import org.waveprotocol.wave.model.wave.data.ObservableWaveletData;
+import org.waveprotocol.wave.model.wave.opbased.OpBasedWavelet;
+
+/**
+ * Unit tests for {@link WaveDigester}.
+ *
+ * @author [email protected] (Yuri Zelikov)
+ */
+public class WaveDigesterTest extends TestCase {
+
+ private static final WaveletId CONVERSATION_WAVELET_ID =
WaveletId.of("example.com", "conv+root");
+
+ @Mock
+ private IdGenerator idGenerator;
+
+ private ConversationUtil conversationUtil;
+
+ private WaveDigester digester;
+
+ @Override
+ protected void setUp() {
+ MockitoAnnotations.initMocks(this);
+
+ conversationUtil = new ConversationUtil(idGenerator);
+ digester = new WaveDigester(conversationUtil);
+ }
+
+ public void testWaveletWithNoBlipsResultsInEmptyTitleAndNoBlips() {
+ TestingWaveletData data =
+ new TestingWaveletData(WAVE_ID, CONVERSATION_WAVELET_ID, PARTICIPANT,
true);
+ ObservableWaveletData observableWaveletData =
data.copyWaveletData().get(0);
+ ObservableWavelet wavelet =
OpBasedWavelet.createReadOnly(observableWaveletData);
+ ObservableConversationView conversation =
conversationUtil.buildConversation(wavelet);
+
+ SupplementedWave supplement = mock(SupplementedWave.class);
+ when(supplement.isUnread(any(ConversationBlip.class))).thenReturn(true);
+
+ Digest digest = digester.generateDigest(conversation, supplement,
observableWaveletData);
+
+ assertEquals("", digest.getTitle());
+ assertEquals(digest.getBlipCount(), 0);
+ }
+
+
+ public void testWaveletWithBlipsResultsInNonEmptyTitle() {
+ TestingWaveletData data =
+ new TestingWaveletData(WAVE_ID, CONVERSATION_WAVELET_ID, PARTICIPANT,
true);
+ String title = "title";
+ data.appendBlipWithText(title);
+ ObservableWaveletData observableWaveletData =
data.copyWaveletData().get(0);
+ ObservableWavelet wavelet =
OpBasedWavelet.createReadOnly(observableWaveletData);
+ ObservableConversationView conversation =
conversationUtil.buildConversation(wavelet);
+
+ SupplementedWave supplement = mock(SupplementedWave.class);
+ when(supplement.isUnread(any(ConversationBlip.class))).thenReturn(true);
+
+ Digest digest = digester.generateDigest(conversation, supplement,
observableWaveletData);
+
+ assertEquals(title, digest.getTitle());
+ assertEquals(digest.getBlipCount(), 1);
+ }
+}