/* * Copyright (C) 2004-2008 Jive Software. All rights reserved. * * 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.jivesoftware.xmpp.workgroup.search; import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.List; import org.jivesoftware.util.JiveGlobals; import org.jivesoftware.util.StringUtils; import org.jivesoftware.xmpp.workgroup.Workgroup; import org.xmpp.packet.JID; /** * Encapsulates a search for transcripts in a workgroup. Use the public constructor * {@link #ChatSearch(Workgroup, Date, Date, JID, String)} to create a search where * the Workgroup parameter defines the workgroup where the search will be performed. * You can mix and match the constructor parameters depending on what kind of query * you'd like to perform.<p> * * The result is composed of {@link QueryResult} that hold basic information about * the transcript. You can use {@link QueryResult#getSessionID()} for locating * the original session that generated the chat transcript.<p> * * Below is a code sample that performs a search:<p> * * <pre> * ChatSearch search = new ChatSearch(workgroup, null, new Date(), null, "Jive is cool"); * for (QueryResult result : search.getResults()) { * Map<String, Object> fields = new LinkedHashMap<String, Object>(); * fields.put("sessionID", result.getSessionID()); * fields.put("startDate", result.getStartDate()); * fields.put("agentJIDs", result.getAgentJIDs()); * fields.put("relevance", Float.valueOf(result.getRelevance())); * searchResults.addItemFields(fields); * } * </pre><p> * * The following workgroup properties are used by this class: * <ul> * <li><tt>workgroup.search.maxResultsSize</tt> -- max number of results to include in the search * answer. Default is <tt>500</tt>.</li> * <li><tt>workgroup.search.wildcardIgnored</tt> -- Determine if wildcards should be ignored. * Default is <tt>false</tt>.</li> * <li><tt>workgroup.search.defaultOperator</tt> -- Boolean operator of the QueryParser. Default * is <tt>OR</tt>.</li> * </ul> * * @author Gaston Dombiak */ public class ChatSearch { // private static final int MAX_RESULTS_SIZE; /** * Indicates whether wildcards should be enabled or ignored in searches. */ private static boolean wildcardIgnored = false; private Date afterDate; private Date beforeDate; private Workgroup workgroup; private JID agentJID; private String queryString; /** * The results of the query. */ private transient List<QueryResult> results = new ArrayList<QueryResult>(); static { // // Load a custom max results size value from the Jive property file or // // default to 500. // int maxSize = 500; // String maxResultsSize = JiveGlobals.getProperty("workgroup.search.maxResultsSize"); // if (maxResultsSize != null) { // try { // maxSize = Integer.parseInt(maxResultsSize); // } // catch (NumberFormatException nfe) { // // Ignore. // } // } // MAX_RESULTS_SIZE = maxSize; // Determine if wildcards should be ignored wildcardIgnored = JiveGlobals.getBooleanProperty("workgroup.search.wildcardIgnored"); } public static boolean isWildcardIgnored() { return wildcardIgnored; } public static void setWildcardIgnored(boolean value) { wildcardIgnored = value; JiveGlobals.setProperty("workgroup.search.wildcardIgnored", "" + value); } /** * Creates a search on the specified workgroup. The only required parameters are * <tt>workgroup</tt> and <tt>queryString</tt>. * * @param workgroup Workgroup where the search will be performed. * @param from min date for the transcripts to include in the search. Optional parameter. * @param upto max date for the transcripts to include in the search. Optional parameter. * @param agentJID JID of the agent involved in the transcripts to include. Optional parameter. * @param queryString String that the transcripts should have. */ public ChatSearch(Workgroup workgroup, Date from, Date upto, JID agentJID, String queryString) { this.afterDate = from; this.beforeDate = upto; this.workgroup = workgroup; this.agentJID = agentJID; this.queryString = wildcardIgnored ? stripWildcards(queryString) : queryString; // we lowercase all queries because Wildcard, Prefix, and Fuzzy queries are case sensitive. // (http://www.jguru.com/faq/view.jsp?EID=538312) // however, we need to make sure that AND and OR terms are left uppercase // (Otherwise they do not work) if (!wildcardIgnored && containsWildcards(queryString)) { this.queryString = lowerCaseQueryString(this.queryString); } } public Date getAfterDate() { return afterDate; } public Date getBeforeDate() { return beforeDate; } public Workgroup getWorkgroup() { return workgroup; } public JID getAgentJID() { return agentJID; } public String getQueryString() { return queryString; } public List<QueryResult> getResults() { if (getQueryString() == null) { return Collections.emptyList(); } if (results.isEmpty()) { executeQuery(); } if (results.size() > 1) { //Collections.sort(results, new QueryResultComparator(this)); } return Collections.unmodifiableList(results); } public List<QueryResult> getResults(int startIndex, int numResults){ if (startIndex < 0 || numResults < 0) { throw new IllegalArgumentException("Parameter value can't be less than 0."); } if (getQueryString() == null) { return Collections.emptyList(); } if (results.isEmpty()) { executeQuery(); } int endIndex = startIndex + numResults - 1; if (endIndex > results.size() - 1) { endIndex = results.size() - 1; } if (((endIndex - startIndex) + 1) <= 0) { return Collections.emptyList(); } else { if (results.size() > 1) { //Collections.sort(results, new QueryResultComparator(this)); } return Collections.unmodifiableList(results.subList(startIndex, endIndex + 1)); } } /** * Execute the query and store the results in the results array. */ private void executeQuery() { // TODO: FIX THIS CODE! /*ChatSearchManager searchManager = ChatSearchManager.getInstanceFor(workgroup); try { // Acquire a read lock on the searcher lock so we know the query will be completed // correctly. searchManager.searcherLock.readLock().lock(); Searcher searcher = searchManager.getSearcher(); if (searcher == null) { // Searcher can be null if the index doesn't exist. results.clear(); return; } Query query = null; if (queryString == null || queryString.equals("")) { results.clear(); return; } else { String cleanQueryString = escapeBadCharacters(queryString); // Create the QueryParser instance QueryParser bodyQP = new QueryParser("body", searchManager.getAnalyzer()); // Override the default OR operator, if set if ("AND".equals(JiveGlobals.getProperty("workgroup.search.defaultOperator"))) { bodyQP.setOperator(QueryParser.DEFAULT_OPERATOR_AND); } query = bodyQP.parse(cleanQueryString); } MultiFilter multiFilter = new MultiFilter(2); int filterCount = 0; // Date filter if (beforeDate != null || afterDate != null) { if (beforeDate != null && afterDate != null) { String key = "bDate:" + beforeDate.getTime() + ":aDate:" + afterDate.getTime(); Filter filter = (Filter) searchManager.getFilter(key); if (filter == null) { filter = new CachingWrapperFilter(new DateFilter("creationDate", beforeDate, afterDate)); searchManager.putFilter(key, filter); } multiFilter.add(filter); filterCount++; } else if (beforeDate == null) { String key = "aDate:" + afterDate.getTime(); Filter filter = (Filter) searchManager.getFilter(key); if (filter == null) { filter = new CachingWrapperFilter(DateFilter.After("creationDate", afterDate)); searchManager.putFilter(key, filter ); } multiFilter.add(filter); filterCount++; } else { String key = "bDate:" + beforeDate.getTime(); Filter filter = (Filter) searchManager.getFilter(key); if (filter == null) { filter = new CachingWrapperFilter(DateFilter.Before("creationDate", beforeDate)); searchManager.putFilter(key, filter ); } multiFilter.add(filter); filterCount++; } } // Agent filter if (agentJID != null) { String bareJID = agentJID.toBareJID(); String key = "agentJID:" + bareJID; Filter filter = (Filter) searchManager.getFilter(key); if (filter == null) { filter = new CachingWrapperFilter(new FieldFilter("agentJID", "" + bareJID)); searchManager.putFilter(key, filter); } multiFilter.add(filter); filterCount++; } Hits hits; // Only apply filters if any are defined. if (filterCount > 0) { hits = searcher.search(query, multiFilter); } else { hits = searcher.search(query); } // Don't return more search results than the maximum number allowed. int numResults = hits.length() < MAX_RESULTS_SIZE ? hits.length() : MAX_RESULTS_SIZE; for (int i = 0; i < numResults; i++) { try { Document doc = hits.doc(i); String sessionID = doc.get("sessionID"); Date createDate = DateField.stringToDate(doc.get("creationDate")); String[] jids = doc.getValues("agentJID"); List<String> agentJIDs = jids == null ? Collections.EMPTY_LIST : Arrays.asList(jids); QueryResult result = new QueryResult(workgroup, sessionID, createDate, agentJIDs, hits.score(i)); results.add(result); } catch (NumberFormatException e) { Log.error(e.getMessage(), e); } } hits = null; } catch (ParseException e) { Log.error("Search failure - " + "lucene error parsing query: " + queryString, e); results.clear(); } catch(Exception e) { Log.error(e.getMessage(), e); results.clear(); } finally { searchManager.searcherLock.readLock().unlock(); } */ } private String stripWildcards(String string) { string = StringUtils.replace(string, "*", ""); string = StringUtils.replace(string, "?", ""); string = StringUtils.replace(string, "~", ""); return string; } private boolean containsWildcards(String string) { return string.indexOf('?') != -1 || string.indexOf('*') != -1 || string.indexOf('~') != -1; } // private String escapeBadCharacters(String queryString) { // return StringUtils.replace(queryString, ":", "\\:"); // } private String lowerCaseQueryString(String queryString) { char[] chars = queryString.toCharArray(); for (int i = 0; i < chars.length; i++) { char c = chars[i]; if (Character.isUpperCase(c)) { if (c == 'A') { // leave uppercase if standalone "AND" if (i > 0 && i + 3 < chars.length && !Character.isLetterOrDigit(chars[i-1]) && !Character.isLetterOrDigit(chars[i+3]) && (chars[i+1] == 'n' || chars[i+1] == 'N') && (chars[i+2] == 'd' || chars[i+2] == 'D')) { chars[i+1] = Character.toUpperCase(chars[i+1]); chars[i+2] = Character.toUpperCase(chars[i+2]); i += 2; continue; } } else if (c == 'O') { // leave uppercase if standalone "OR" if (i > 0 && i + 2 < chars.length && !Character.isLetterOrDigit(chars[i-1]) && !Character.isLetterOrDigit(chars[i+2]) && (chars[i+1] == 'r' || chars[i+1] == 'R')) { chars[i+1] = Character.toUpperCase(chars[i+1]); i++; continue; } } chars[i] = Character.toLowerCase(c); } } return new String(chars); } }