001: /*
002: * $Header: /cvsroot/mvnforum/mvnforum/src/com/mvnforum/search/post/PostSearchQuery.java,v 1.20 2008/01/15 11:17:57 minhnn Exp $
003: * $Author: minhnn $
004: * $Revision: 1.20 $
005: * $Date: 2008/01/15 11:17:57 $
006: *
007: * ====================================================================
008: *
009: * Copyright (C) 2002-2007 by MyVietnam.net
010: *
011: * All copyright notices regarding mvnForum MUST remain
012: * intact in the scripts and in the outputted HTML.
013: * The "powered by" text/logo with a link back to
014: * http://www.mvnForum.com and http://www.MyVietnam.net in
015: * the footer of the pages MUST remain visible when the pages
016: * are viewed on the internet or intranet.
017: *
018: * This program is free software; you can redistribute it and/or modify
019: * it under the terms of the GNU General Public License as published by
020: * the Free Software Foundation; either version 2 of the License, or
021: * any later version.
022: *
023: * This program is distributed in the hope that it will be useful,
024: * but WITHOUT ANY WARRANTY; without even the implied warranty of
025: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
026: * GNU General Public License for more details.
027: *
028: * You should have received a copy of the GNU General Public License
029: * along with this program; if not, write to the Free Software
030: * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
031: *
032: * Support can be obtained from support forums at:
033: * http://www.mvnForum.com/mvnforum/index
034: *
035: * Correspondence and Marketing Questions can be sent to:
036: * info at MyVietnam net
037: *
038: * @author: Minh Nguyen
039: * @author: Dejan Krsmanovic dejan_krsmanovic@yahoo.com
040: */
041: package com.mvnforum.search.post;
042:
043: import java.io.IOException;
044: import java.sql.Timestamp;
045: import java.util.*;
046:
047: import net.myvietnam.mvncore.exception.DatabaseException;
048: import net.myvietnam.mvncore.exception.ObjectNotFoundException;
049:
050: import org.apache.commons.logging.Log;
051: import org.apache.commons.logging.LogFactory;
052: import org.apache.lucene.analysis.Analyzer;
053: import org.apache.lucene.document.DateTools;
054: import org.apache.lucene.document.Document;
055: import org.apache.lucene.document.DateTools.Resolution;
056: import org.apache.lucene.index.Term;
057: import org.apache.lucene.queryParser.ParseException;
058: import org.apache.lucene.queryParser.QueryParser;
059: import org.apache.lucene.search.*;
060: import org.apache.lucene.store.Directory;
061:
062: import com.mvnforum.auth.MVNForumPermission;
063: import com.mvnforum.db.*;
064: import com.mvnforum.search.CombineFilter;
065: import com.mvnforum.search.IntegerFilter;
066: import com.mvnforum.service.MvnForumServiceFactory;
067: import com.mvnforum.service.SearchService;
068:
069: /**
070: * This class is used for specifying query that should be searched for. Query
071: * can contain keywords and further it can be filtered by specifying member
072: * name of the author, forumID as well as date interval for searching.
073: *
074: * searchString contains one or more keywords. Each keyword can use wildcards.
075: * ? for single character and * for multiple characters.
076: * For specifying boolean operators AND and OR operators can be used.....
077: *
078: * For all available options consult Lucene documentation http://jakarta.apache.org/lucene
079: *
080: * @author Dejan Krsmanovic dejan_krsmanovic@yahoo.com
081: */
082: public class PostSearchQuery {
083:
084: private static Log log = LogFactory.getLog(PostSearchQuery.class);
085:
086: // constant for search by time
087: public static final int SEARCH_ANY_DATE = 0;
088:
089: public static final int SEARCH_NEWER = 1;
090: public static final int SEARCH_OLDER = 2;
091:
092: // constant for search by the post title/body
093: public static final int SEARCH_ONLY_TITLE = 1;
094: public static final int SEARCH_ONLY_BODY = 2;
095:
096: // constant for sorting option by time
097: public static final int SEARCH_SORT_DEFAULT = 0;
098: public static final int SEARCH_SORT_TIME_DESC = 1;
099: public static final int SEARCH_SORT_TIME_ASC = 2;
100:
101: private int memberID = -1;
102: private int forumId = 0;
103: private boolean withAttachment = false;// currently does not use this variable, use minAttachmentCount
104:
105: // search with ignore attachment (attachmentCount is negative)
106: private int minAttachmentCount = -1;// or 0 should also be OK
107:
108: private String searchString = null;
109:
110: private Timestamp fromDate = null;
111: private Timestamp toDate = null;
112:
113: private int hitCount = 0;
114: private Collection searchResult = null;
115:
116: // 1|2 = 3;
117: private int scopeInPost = SEARCH_ONLY_TITLE | SEARCH_ONLY_BODY;
118:
119: private int sort = SEARCH_SORT_DEFAULT;
120:
121: public PostSearchQuery() {
122: }
123:
124: /**
125: * Set name of the author that should be searched for
126: * @param memberId
127: */
128: public void setMemberId(int memberId) {
129: this .memberID = memberId;
130: }
131:
132: /**
133: * Id of forum where post belongs. Set to -1 if all forums should be searched
134: * @param forumId
135: */
136: public void setForumId(int forumId) {
137: this .forumId = forumId;
138: }
139:
140: /**
141: * Set string that should be searched for.
142: * @param searchString
143: */
144: public void setSearchString(String searchString) {
145: this .searchString = searchString;
146: }
147:
148: public void setScopeInPost(int scopeInPost) {
149: this .scopeInPost = scopeInPost;
150: }
151:
152: public void setFromDate(Timestamp fromDate) {
153: this .fromDate = fromDate;
154: }
155:
156: public void setToDate(Timestamp toDate) {
157: this .toDate = toDate;
158: }
159:
160: public void setMinAttachmentCount(int count) {
161: this .minAttachmentCount = count;
162: }
163:
164: // Note that with IndexSearcher, the directory is closed automatically
165: protected IndexSearcher getSearcher(Directory directory)
166: throws IOException {
167: try {
168: IndexSearcher searcher = new IndexSearcher(directory);
169: return searcher;
170: } catch (IOException ex) {
171: // we throw new IOException because the original exception
172: // contain sensitive directory information
173: log
174: .error(
175: "Cannot access the lucene search index for query. Please check if you have configed mvnForumHome properly. You can also go to Admin Zone to rebuild the Lucene index files.",
176: ex);
177: //@todo : localize me
178: throw new IOException(
179: "Cannot access the lucene search index. Please report this error to web site Administrator (check mvnForumHome or rebuild Lucene index).");
180: }
181: }
182:
183: public void searchDocuments(int offset, int rowsToReturn,
184: MVNForumPermission permission) throws IOException,
185: DatabaseException, ObjectNotFoundException {
186:
187: // Now check if at least one of these input is present: key, member, attachment
188: if ((searchString == null || searchString.equals(""))
189: && (memberID == -1) && (minAttachmentCount < 1)) {
190: // should throw an Exception here, if not, later call getPostResult() will return null
191: return;
192: }
193: //Build the query
194: BooleanQuery query = new BooleanQuery();
195: try {
196: Query topicBodyQuery = getTopicBodyQuery();
197: if (topicBodyQuery != null) {
198: query.add(topicBodyQuery, BooleanClause.Occur.MUST);
199: log.debug("topicBodyQuery = " + topicBodyQuery);
200: }
201:
202: Query withAttachmentQuery = getWithAttachmentQuery();
203: if (withAttachment) {
204: query
205: .add(withAttachmentQuery,
206: BooleanClause.Occur.MUST);
207: log.debug("withAttachmentQuery = "
208: + withAttachmentQuery);
209: }
210:
211: Query categoryForumQuery = getCategoryForumQuery(permission);
212: if (categoryForumQuery != null) {
213: query.add(categoryForumQuery, BooleanClause.Occur.MUST);
214: log.debug("categoryForumQuery = " + categoryForumQuery);
215: }
216:
217: Query memberQuery = getMemberQuery();
218: if (memberQuery != null) {
219: query.add(memberQuery, BooleanClause.Occur.MUST);
220: log.debug("memberQuery = " + memberQuery);
221: }
222: } catch (ParseException pe) {
223: log.error("Cannot parse the search query", pe);
224: }
225: log.debug("booleanQuery = " + query);
226:
227: RangeFilter dateFilter = null;
228: //Add date filter if some of dates provided
229: if (fromDate != null && toDate != null) {
230: dateFilter = new RangeFilter(PostIndexer.FIELD_POST_DATE,
231: DateTools.dateToString(fromDate,
232: Resolution.MILLISECOND), DateTools
233: .dateToString(toDate,
234: Resolution.MILLISECOND), true, true);
235: } else if (fromDate != null) {
236: dateFilter = RangeFilter.More(PostIndexer.FIELD_POST_DATE,
237: DateTools.dateToString(fromDate,
238: Resolution.MILLISECOND));
239: } else if (toDate != null) {
240: dateFilter = RangeFilter.Less(PostIndexer.FIELD_POST_DATE,
241: DateTools.dateToString(toDate,
242: Resolution.MILLISECOND));
243: }
244:
245: IntegerFilter attachCountFilter = null;
246:
247: if (minAttachmentCount > 0) {
248: attachCountFilter = IntegerFilter.greaterThan(
249: PostIndexer.FIELD_ATTACHMENT_COUNT,
250: minAttachmentCount);
251: }
252:
253: Filter filter = null;
254:
255: if (dateFilter != null) {
256: if (attachCountFilter != null) {
257: filter = new CombineFilter(dateFilter,
258: attachCountFilter);
259: } else {
260: filter = dateFilter;
261: }
262: } else {
263: filter = attachCountFilter;
264: }
265:
266: //Now search the documents
267: Directory directory = null;
268: IndexSearcher searcher = null;
269: try {
270: SearchService service = MvnForumServiceFactory
271: .getMvnForumService().getSearchService();
272: directory = service.getSearchPostIndexDir();
273: searcher = getSearcher(directory);
274:
275: //If filter set then use it
276: Hits postHits = null;
277: if (filter != null) {
278: postHits = searcher.search(query, filter,
279: getQuerySort());
280: } else {
281: postHits = searcher.search(query, getQuerySort());
282: }
283:
284: hitCount = postHits.length();
285: searchResult = getPosts(postHits, offset, rowsToReturn);
286: } catch (IOException ex) {
287: throw ex;
288: } finally {
289: // NOTE that we don't close directory because searcher.close() already do that
290: if (searcher != null) {
291: try {
292: searcher.close();
293: } catch (IOException ex) {
294: log.debug("Error closing Lucene IndexSearcher", ex);
295: }
296: }
297: }
298: }
299:
300: public int getHitCount() {
301: return hitCount;
302: }
303:
304: public Collection getPostResult() {
305: if (searchResult == null) {
306: //create an empty list, in case result is null
307: searchResult = new ArrayList();
308: }
309: return searchResult;
310: }
311:
312: private Collection getPosts(Hits postHits, int offset,
313: int rowsToReturn) throws IOException,
314: ObjectNotFoundException, DatabaseException {
315:
316: if (offset < 0)
317: throw new IllegalArgumentException(
318: "The offset < 0 is not allowed.");
319: if (rowsToReturn <= 0)
320: throw new IllegalArgumentException(
321: "The rowsToReturn <= 0 is not allowed.");
322:
323: //int hitCount = getHitCount();
324: Collection retValue = new ArrayList(hitCount);
325:
326: for (int i = offset; (i < offset + rowsToReturn)
327: && (i < hitCount); i++) {
328: Document postDocument = postHits.doc(i);
329: int postID = Integer.parseInt(postDocument
330: .get(PostIndexer.FIELD_POST_ID));
331: PostBean postBean = PostCache.getInstance().getPost(postID);
332: retValue.add(postBean);
333: }
334: return retValue;
335: }
336:
337: private Query getTopicBodyQuery() throws ParseException {
338: if (searchString == null || searchString.equals("")) {
339: return null;
340: }
341: Analyzer analyzer = PostIndexer.getAnalyzer();
342: BooleanQuery topicBodyQuery = new BooleanQuery();
343:
344: //add topic query
345: Query topicQuery = new QueryParser(
346: PostIndexer.FIELD_POST_TOPIC, analyzer)
347: .parse(searchString);
348: topicBodyQuery.add(topicQuery, BooleanClause.Occur.SHOULD);
349:
350: //add body query
351: Query bodyQuery = new QueryParser(PostIndexer.FIELD_POST_BODY,
352: analyzer).parse(searchString);
353: if (scopeInPost == SEARCH_ONLY_TITLE) {
354: return topicQuery;
355: } else if (scopeInPost == SEARCH_ONLY_BODY) {
356: return bodyQuery;
357: }
358: topicBodyQuery.add(bodyQuery, BooleanClause.Occur.SHOULD);
359:
360: return topicBodyQuery;
361: }
362:
363: private Query getMemberQuery() {
364: Query memberQuery = null;
365: if (memberID > 0) {
366: Term memberTerm = new Term(PostIndexer.FIELD_MEMBER_ID,
367: String.valueOf(memberID));
368: memberQuery = new TermQuery(memberTerm);
369: }
370: return memberQuery;
371: }
372:
373: private Query getWithAttachmentQuery() {
374: Query withAttachmentQuery = null;
375: if (withAttachment) {
376: Term withAttachmentTerm = new Term(
377: PostIndexer.FIELD_WITH_ATTACHMENT, String
378: .valueOf(withAttachment));
379: withAttachmentQuery = new TermQuery(withAttachmentTerm);
380: }
381: return withAttachmentQuery;
382: }
383:
384: private Query getCategoryForumQuery(MVNForumPermission permission)
385: throws DatabaseException {
386: BooleanQuery categoryForumQuery = new BooleanQuery();
387: if (forumId == 0) {
388: // search all forum
389: Collection forumBeans = ForumCache.getInstance().getBeans();
390: for (Iterator iter = forumBeans.iterator(); iter.hasNext();) {
391: ForumBean forumBean = (ForumBean) iter.next();
392: int currentForumID = forumBean.getForumID();
393: if ((forumBean.getForumStatus() != ForumBean.FORUM_STATUS_DISABLED)
394: && permission.canReadPost(currentForumID)) {
395:
396: Term forumTerm = new Term(
397: PostIndexer.FIELD_FORUM_ID, String
398: .valueOf(currentForumID));
399: Query forumQuery = new TermQuery(forumTerm);
400: categoryForumQuery.add(forumQuery,
401: BooleanClause.Occur.SHOULD);
402: }
403: }
404: } else if (forumId > 0) {
405: // search in forum
406: Term forumTerm = new Term(PostIndexer.FIELD_FORUM_ID,
407: String.valueOf(forumId));
408: Query forumQuery = new TermQuery(forumTerm);
409: categoryForumQuery
410: .add(forumQuery, BooleanClause.Occur.MUST);
411: } else if (forumId < 0) {
412: // search in category
413: int categoryID = -forumId;//category is the negative value of forumID in this case
414: Collection forumBeans = ForumCache.getInstance().getBeans();
415: for (Iterator iter = forumBeans.iterator(); iter.hasNext();) {
416: ForumBean forumBean = (ForumBean) iter.next();
417: if (forumBean.getCategoryID() == categoryID) {
418: int currentForumID = forumBean.getForumID();
419: if ((forumBean.getForumStatus() != ForumBean.FORUM_STATUS_DISABLED)
420: && permission.canReadPost(currentForumID)) {
421:
422: Term forumTerm = new Term(
423: PostIndexer.FIELD_FORUM_ID, String
424: .valueOf(currentForumID));
425: Query forumQuery = new TermQuery(forumTerm);
426: categoryForumQuery.add(forumQuery,
427: BooleanClause.Occur.SHOULD);
428: }
429: }
430: }
431: }
432: return categoryForumQuery;
433: }
434:
435: public void setSort(int sort) {
436: if ((sort != SEARCH_SORT_DEFAULT)
437: && (sort != SEARCH_SORT_TIME_ASC)
438: && (sort != SEARCH_SORT_TIME_DESC)) {
439: throw new IllegalArgumentException(
440: "Does not support sort = " + sort);
441: }
442: this .sort = sort;
443: }
444:
445: public int getSort() {
446: return sort;
447: }
448:
449: private Sort getQuerySort() {
450: Sort sortObj = null;
451: switch (sort) {
452: case SEARCH_SORT_TIME_ASC:
453: sortObj = new Sort(new SortField(
454: PostIndexer.FIELD_POST_DATE, SortField.STRING,
455: false));
456: break;
457: case SEARCH_SORT_TIME_DESC:
458: sortObj = new Sort(
459: new SortField(PostIndexer.FIELD_POST_DATE,
460: SortField.STRING, true));
461: break;
462: default:
463: sortObj = new Sort();
464: break;
465: }
466: return sortObj;
467: }
468:
469: }
|