| /** |
| * 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.server.rpc; |
| |
| import com.google.common.annotations.VisibleForTesting; |
| import com.google.inject.Inject; |
| import com.google.inject.Singleton; |
| import com.google.inject.name.Named; |
| import com.google.protobuf.Message; |
| import com.google.protobuf.MessageLite; |
| import com.google.wave.api.SearchResult; |
| import com.google.wave.api.SearchResult.Digest; |
| import com.google.wave.api.data.converter.EventDataConverterManager; |
| |
| import org.waveprotocol.box.search.SearchProto.SearchRequest; |
| import org.waveprotocol.box.search.SearchProto.SearchResponse; |
| import org.waveprotocol.box.search.SearchProto.SearchResponse.Builder; |
| import org.waveprotocol.box.server.authentication.SessionManager; |
| import org.waveprotocol.box.server.robots.OperationServiceRegistry; |
| import org.waveprotocol.box.server.robots.util.ConversationUtil; |
| import org.waveprotocol.box.server.rpc.ProtoSerializer.SerializationException; |
| import org.waveprotocol.box.server.waveserver.WaveletProvider; |
| import org.waveprotocol.box.stat.Timed; |
| import org.waveprotocol.box.webclient.search.SearchService; |
| import org.waveprotocol.wave.model.wave.ParticipantId; |
| import org.waveprotocol.wave.util.logging.Log; |
| |
| import java.io.IOException; |
| import java.util.List; |
| |
| import javax.servlet.http.HttpServletRequest; |
| import javax.servlet.http.HttpServletResponse; |
| |
| /** |
| * A servlet to provide search functionality by using Data API. Typically will |
| * be hosted on /search. |
| * |
| * Valid request format is: GET /search/?query=in:inbox&index=0&numResults=50. |
| * The format of the returned information is the protobuf-JSON format used by |
| * the websocket interface. |
| * |
| * @author vega113@gmail.com (Yuri Z.) |
| */ |
| @SuppressWarnings("serial") |
| @Singleton |
| public class SearchServlet extends AbstractSearchServlet { |
| |
| private static final Log LOG = Log.get(SearchServlet.class); |
| |
| private final ProtoSerializer serializer; |
| |
| /** |
| * Constructs SearchResponse which is a protobuf generated class from the |
| * output of Data API search service. SearchResponse contains the same |
| * information as searchResult. |
| * |
| * @param searchResult the search results with digests. |
| * @return SearchResponse |
| */ |
| public static SearchResponse serializeSearchResult(SearchResult searchResult, int total) { |
| Builder searchBuilder = SearchResponse.newBuilder(); |
| searchBuilder.setQuery(searchResult.getQuery()).setTotalResults(total); |
| for (SearchResult.Digest searchResultDigest : searchResult.getDigests()) { |
| SearchResponse.Digest digest = serializeDigest(searchResultDigest); |
| searchBuilder.addDigests(digest); |
| } |
| SearchResponse searchResponse = searchBuilder.build(); |
| return searchResponse; |
| } |
| |
| /** |
| * Copies data from {@link Digest} into {@link SearchResponse.Digest}. |
| */ |
| private static SearchResponse.Digest serializeDigest(Digest searchResultDigest) { |
| SearchResponse.Digest.Builder digestBuilder = SearchResponse.Digest.newBuilder(); |
| digestBuilder.setBlipCount(searchResultDigest.getBlipCount()); |
| digestBuilder.setLastModified(searchResultDigest.getLastModified()); |
| digestBuilder.setSnippet(searchResultDigest.getSnippet()); |
| digestBuilder.setTitle(searchResultDigest.getTitle()); |
| digestBuilder.setUnreadCount(searchResultDigest.getUnreadCount()); |
| digestBuilder.setWaveId(searchResultDigest.getWaveId()); |
| List<String> participants = searchResultDigest.getParticipants(); |
| if (participants.isEmpty()) { |
| // This shouldn't be possible. |
| digestBuilder.setAuthor("nobody@example.com"); |
| } else { |
| digestBuilder.setAuthor(participants.get(0)); |
| for (int i = 1; i < participants.size(); i++) { |
| digestBuilder.addParticipants(participants.get(i)); |
| } |
| } |
| SearchResponse.Digest digest = digestBuilder.build(); |
| return digest; |
| } |
| |
| @Inject |
| public SearchServlet(SessionManager sessionManager, EventDataConverterManager converterManager, |
| @Named("DataApiRegistry") OperationServiceRegistry operationRegistry, |
| WaveletProvider waveletProvider, ConversationUtil conversationUtil, ProtoSerializer serializer) { |
| super(conversationUtil, converterManager, waveletProvider, sessionManager, operationRegistry); |
| this.serializer = serializer; |
| } |
| |
| /** |
| * Creates HTTP response to the search query. Main entrypoint for this class. |
| */ |
| @Timed |
| @Override |
| @VisibleForTesting |
| protected void doGet(HttpServletRequest req, HttpServletResponse response) throws IOException { |
| ParticipantId user = sessionManager.getLoggedInUser(req.getSession(false)); |
| if (user == null) { |
| response.setStatus(HttpServletResponse.SC_FORBIDDEN); |
| return; |
| } |
| SearchRequest searchRequest = parseSearchRequest(req, response); |
| SearchResult searchResult = performSearch(searchRequest, user); |
| |
| int totalGuess = computeTotalResultsNumberGuess(searchRequest, searchResult); |
| LOG.fine("Results: " + searchResult.getNumResults() + ", total: " + totalGuess); |
| SearchResponse searchResponse = serializeSearchResult(searchResult, totalGuess); |
| serializeObjectToServlet(searchResponse, response); |
| } |
| |
| private int computeTotalResultsNumberGuess(SearchRequest searchRequest, SearchResult searchResult) { |
| // The Data API does not return the total size of the search result, even |
| // though the searcher knows it. The only approximate knowledge that can be |
| // gleaned from the Data API is whether there are more search results beyond |
| // those returned. If the searcher returns as many (or more) results as |
| // requested, then assume that more results exist, but the total is unknown. |
| // Otherwise, the total has been reached. |
| int totalGuess; |
| if (searchResult.getNumResults() >= searchRequest.getNumResults()) { |
| totalGuess = SearchService.UNKNOWN_SIZE; |
| } else { |
| totalGuess = searchRequest.getIndex() + searchResult.getNumResults(); |
| } |
| return totalGuess; |
| } |
| |
| /** |
| * Writes the json with search results to Response. |
| */ |
| private <P extends Message> void serializeObjectToServlet(P message, HttpServletResponse resp) |
| throws IOException { |
| if (message == null) { |
| resp.sendError(HttpServletResponse.SC_FORBIDDEN); |
| } else { |
| resp.setStatus(HttpServletResponse.SC_OK); |
| resp.setContentType("application/json; charset=utf8"); |
| // This is to make sure the fetched data is fresh - since the w3c spec |
| // is rarely respected. |
| resp.setHeader("Cache-Control", "no-store"); |
| try { |
| resp.getWriter().append(serializer.toJson(message).toString()); |
| } catch (SerializationException e) { |
| throw new IOException(e); |
| } |
| } |
| } |
| } |