update to reflect API changes
[gae-samples.git] / search / java / src / com / google / appengine / demos / search / TextSearchServlet.java
blob2c23df05e5179ddb72b5cf9895f6c6c1828cfe9c
1 // Copyright 2011 Google Inc. All Rights Reserved.
3 package com.google.appengine.demos.search;
5 import com.google.appengine.api.search.Document;
6 import com.google.appengine.api.search.Field;
7 import com.google.appengine.api.search.Index;
8 import com.google.appengine.api.search.IndexSpec;
9 import com.google.appengine.api.search.OperationResult;
10 import com.google.appengine.api.search.Query;
11 import com.google.appengine.api.search.QueryOptions;
12 import com.google.appengine.api.search.Results;
13 import com.google.appengine.api.search.DeleteException;
14 import com.google.appengine.api.search.ScoredDocument;
15 import com.google.appengine.api.search.SearchServiceFactory;
16 import com.google.appengine.api.search.StatusCode;
17 import com.google.appengine.api.users.User;
18 import com.google.appengine.api.users.UserService;
19 import com.google.appengine.api.users.UserServiceFactory;
21 import java.io.IOException;
22 import java.util.ArrayList;
23 import java.util.Arrays;
24 import java.util.Date;
25 import java.util.Iterator;
26 import java.util.List;
27 import java.util.StringTokenizer;
28 import java.util.logging.Level;
29 import java.util.logging.Logger;
31 import javax.servlet.ServletException;
32 import javax.servlet.http.HttpServlet;
33 import javax.servlet.http.HttpServletRequest;
34 import javax.servlet.http.HttpServletResponse;
36 /**
37 * A demo servlet showing basic text search capabilities. This servlet
38 * has a single index shared between all users. It illustrates how to
39 * add, search for and remove documents from the shared index.
42 public class TextSearchServlet extends HttpServlet {
44 private static final long serialVersionUID = 1L;
46 private static final String VOID_REMOVE =
47 "Remove failed due to a null doc ID";
49 private static final String VOID_ADD =
50 "Document not added due to empty content";
52 /**
53 * The index used by this application. Since we only have one index
54 * we create one instance only. We build an index with the default
55 * consistency, which is Consistency.PER_DOCUMENT. These types of
56 * indexes are most suitable for streams and feeds, and can cope with
57 * a high rate of updates.
59 private static final Index INDEX = SearchServiceFactory.getSearchService()
60 .getIndex(IndexSpec.newBuilder().setName("shared_index"));
62 enum Action {
63 ADD, REMOVE, DEFAULT;
66 private static final Logger LOG = Logger.getLogger(
67 TextSearchServlet.class.getName());
69 @Override
70 public void doGet(HttpServletRequest req, HttpServletResponse resp)
71 throws IOException, ServletException {
72 User currentUser = setupUser(req);
73 String outcome = null;
74 switch (getAction(req)) {
75 case ADD:
76 outcome = add(req, currentUser);
77 break;
78 case REMOVE:
79 outcome = remove(req);
80 break;
81 // On DEFAULT we fall through and just execute search below.
83 String searchOutcome = search(req);
84 if (outcome == null) {
85 outcome = searchOutcome;
87 req.setAttribute("outcome", outcome);
88 req.getRequestDispatcher("index.jsp").forward(req, resp);
91 private User setupUser(HttpServletRequest req) {
92 UserService userService = UserServiceFactory.getUserService();
93 User currentUser = userService.getCurrentUser();
94 if (currentUser != null) {
95 req.setAttribute("authAction", "sign out");
96 req.setAttribute("authUrl",
97 userService.createLogoutURL(req.getRequestURI()));
98 } else {
99 currentUser = new User("nobody@example.com", "example.com");
100 req.setAttribute("authAction", "sign in");
101 req.setAttribute("authUrl",
102 userService.createLoginURL(req.getRequestURI()));
104 req.setAttribute("nickname", currentUser.getNickname());
105 return currentUser;
109 * Indexes a document built from the current request on behalf of the
110 * specified user. Each document has three fields in it. The content
111 * field stores used entered text. The email, and domain are extracted
112 * from the current user.
114 private String add(HttpServletRequest req, User currentUser) {
115 String content = req.getParameter("doc");
116 if (content == null || content.isEmpty()) {
117 LOG.warning(VOID_ADD);
118 return VOID_ADD;
120 String ratingStr = req.getParameter("rating");
121 int rating = 1;
122 if (ratingStr != null) {
123 rating = Integer.parseInt(ratingStr);
125 Document.Builder docBuilder = Document.newBuilder()
126 .addField(Field.newBuilder().setName("content").setText(content))
127 .addField(Field.newBuilder().setName("email")
128 .setText(currentUser.getEmail()))
129 .addField(Field.newBuilder().setName("domain")
130 .setText(currentUser.getAuthDomain()))
131 .addField(Field.newBuilder().setName("published").setDate(
132 Field.date(new Date())))
133 .addField(Field.newBuilder().setName("rating")
134 .setNumber(rating));
135 String tagStr = req.getParameter("tags");
136 if (tagStr != null) {
137 StringTokenizer tokenizer = new StringTokenizer(tagStr, ",");
138 while (tokenizer.hasMoreTokens()) {
139 docBuilder.addField(Field.newBuilder().setName("tag")
140 .setAtom(tokenizer.nextToken()));
143 Document doc = docBuilder.build();
144 LOG.info("Adding document:\n" + doc.toString());
145 try {
146 INDEX.put(doc);
147 return "Document added";
148 } catch (RuntimeException e) {
149 LOG.log(Level.SEVERE, "Failed to add " + doc, e);
150 return "Document not added due to an error " + e.getMessage();
154 private String getOnlyField(Document doc, String fieldName, String defaultValue) {
155 if (doc.getFieldCount(fieldName) == 1) {
156 return doc.getOnlyField(fieldName).getText();
158 LOG.severe("Field " + fieldName + " present " + doc.getFieldCount(fieldName));
159 return defaultValue;
163 * Searches the index for matching documents. If the query is not specified
164 * in the request, we search for any documents.
166 private String search(HttpServletRequest req) {
167 String queryStr = req.getParameter("query");
168 if (queryStr == null) {
169 queryStr = "";
171 String limitStr = req.getParameter("limit");
172 int limit = 10;
173 if (limitStr != null) {
174 try {
175 limit = Integer.parseInt(limitStr);
176 } catch (NumberFormatException e) {
177 LOG.severe("Failed to parse " + limitStr);
180 List<Document> found = new ArrayList<Document>();
181 String outcome = null;
182 try {
183 // Rather than just using a query we build a search request.
184 // This allows us to specify other attributes, such as the
185 // number of documents to be returned by search.
186 Query query = Query.newBuilder()
187 .setOptions(QueryOptions.newBuilder()
188 .setLimit(limit).
189 // for deployed apps, uncomment the line below to demo snippeting.
190 // This will not work on the dev_appserver.
191 // setFieldsToSnippet("content").
192 build())
193 .build(queryStr);
194 LOG.info("Sending query " + query);
195 Results<ScoredDocument> results = INDEX.search(query);
196 for (ScoredDocument scoredDoc : results) {
197 User author = new User(
198 getOnlyField(scoredDoc, "email", "user"),
199 getOnlyField(scoredDoc, "domain", "example.com"));
200 // Rather than presenting the original document to the
201 // user, we build a derived one that holds author's nickname.
202 List<Field> expressions = scoredDoc.getExpressions();
203 String content = null;
204 if (expressions != null) {
205 for (Field field : expressions) {
206 if ("content".equals(field.getName())) {
207 content = field.getHTML();
208 break;
212 if (content == null) {
213 content = getOnlyField(scoredDoc, "content", "");
215 Document derived = Document.newBuilder()
216 .setId(scoredDoc.getId())
217 .addField(Field.newBuilder().setName("content").setText(content))
218 .addField(Field.newBuilder().setName("nickname").setText(
219 author.getNickname()))
220 .addField(Field.newBuilder().setName("published").setDate(
221 scoredDoc.getOnlyField("published").getDate()))
222 .build();
223 found.add(derived);
225 } catch (RuntimeException e) {
226 LOG.log(Level.SEVERE, "Search with query '" + queryStr + "' failed", e);
227 outcome = "Search failed due to an error: " + e.getMessage();
229 req.setAttribute("found", found);
230 return outcome;
234 * Removes documents with IDs specified in the given request. In the demo
235 * application we do not perform any authorization checks, thus no user
236 * information is necessary.
238 private String remove(HttpServletRequest req) {
239 String[] docIds = req.getParameterValues("docid");
240 if (docIds == null) {
241 LOG.warning(VOID_REMOVE);
242 return VOID_REMOVE;
244 List<String> docIdList = Arrays.asList(docIds);
245 try {
246 INDEX.delete(docIdList);
247 return "Documents " + docIdList + " removed";
248 } catch (DeleteException e) {
249 List<String> failedIds = findFailedIds(docIdList, e.getResults());
250 LOG.log(Level.SEVERE, "Failed to remove documents " + failedIds, e);
251 return "Remove failed for " + failedIds;
256 * A convenience method that correlates document status to the document ID.
258 private List<String> findFailedIds(List<String> docIdList,
259 List<OperationResult> results) {
260 List<String> failedIds = new ArrayList<String>();
261 Iterator<OperationResult> opIter = results.iterator();
262 Iterator<String> idIter = docIdList.iterator();
263 while (opIter.hasNext() && idIter.hasNext()) {
264 OperationResult result = opIter.next();
265 String docId = idIter.next();
266 if (!StatusCode.OK.equals(result.getCode())) {
267 failedIds.add(docId);
270 return failedIds;
274 * Extracts the type of action stored in the request. We have only three
275 * types of actions: ADD, REMOVE and DEFAULT. The DEFAULT is included
276 * to indicate action other than ADD or REMOVE. We do not have a special
277 * acton for search, as we always execute search. This way we show documents
278 * that match terms entered in the search box, regardless of the operation.
280 * @param HTTP request received by the servlet
281 * @return the requested user action, as inferred from the request
283 private Action getAction(HttpServletRequest req) {
284 if (req.getParameter("index") != null) {
285 return Action.ADD;
287 if (req.getParameter("delete") != null) {
288 return Action.REMOVE;
290 return Action.DEFAULT;