/**
* Copyright 2010 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.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableMap;
import com.google.inject.Inject;
import com.google.wave.api.ApiIdSerializer;
import com.google.wave.api.InvalidRequestException;
import com.google.wave.api.JsonRpcConstant.ParamsProperty;
import com.google.wave.api.OperationRequest;
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.OperationContext;
import org.waveprotocol.box.server.robots.util.ConversationUtil;
import org.waveprotocol.box.server.robots.util.OperationUtil;
import org.waveprotocol.box.server.waveserver.SearchProvider;
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.SupplementedWaveImpl.DefaultFollow;
import org.waveprotocol.wave.model.supplement.WaveletBasedSupplement;
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;
import java.util.Map;
/**
* {@link OperationService} for the "search" operation.
*
* @author ljvderijk@google.com (Lennard de Rijk)
* @author josephg@gmail.com (Joseph Gentle)
*/
public class SearchService implements OperationService {
/**
* The number of search results to return if not defined in the request.
* Defined in the spec.
*/
private static final int DEFAULT_NUMBER_SEARCH_RESULTS = 10;
private static final int DIGEST_SNIPPET_LENGTH = 140;
private static final int PARTICIPANTS_SNIPPET_LENGTH = 5;
private static final String EMPTY_WAVELET_TITLE = "";
private final SearchProvider searchProvider;
private final ConversationUtil conversationUtil;
@Inject
public SearchService(SearchProvider searchProvider, ConversationUtil conversationUtil) {
this.searchProvider = searchProvider;
this.conversationUtil = conversationUtil;
}
@Override
public void execute(
OperationRequest operation, OperationContext context, ParticipantId participant)
throws InvalidRequestException {
String query = OperationUtil.getRequiredParameter(operation, ParamsProperty.QUERY);
int index = OperationUtil.getOptionalParameter(operation, ParamsProperty.INDEX, 0);
int numResults = OperationUtil.getOptionalParameter(
operation, ParamsProperty.NUM_RESULTS, DEFAULT_NUMBER_SEARCH_RESULTS);
SearchResult result = search(participant, query, index, numResults);
Map<ParamsProperty, Object> data =
ImmutableMap.<ParamsProperty, Object> of(ParamsProperty.SEARCH_RESULTS, result);
context.constructResponse(operation, data);
}
/**
* 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.
*/
@VisibleForTesting
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(),
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, 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;
int docs = 0;
List<String> participants = new ArrayList<String>();
for (WaveletData data : wave.getWavelets()) {
lmt = Math.max(lmt, data.getLastModifiedTime());
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, 0, docs);
}
// Note that this search implementation is only of prototype quality.
private SearchResult search(
ParticipantId participant, String query, int startAt, int numResults) {
Collection<WaveViewData> results =
searchProvider.search(participant, query, startAt, numResults);
// 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);
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;
}
/**
* 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);
}
}