/*
* 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.io.File;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.document.DateTools;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.search.Filter;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Searcher;
import org.dom4j.DocumentException;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.jivesoftware.database.DbConnectionManager;
import org.jivesoftware.openfire.fastpath.providers.ChatNotes;
import org.jivesoftware.openfire.fastpath.util.TaskEngine;
import org.jivesoftware.util.ClassUtils;
import org.jivesoftware.util.JiveGlobals;
import org.jivesoftware.util.StringUtils;
import org.jivesoftware.xmpp.workgroup.AgentSession;
import org.jivesoftware.xmpp.workgroup.Workgroup;
import org.jivesoftware.xmpp.workgroup.event.WorkgroupEventDispatcher;
import org.jivesoftware.xmpp.workgroup.event.WorkgroupEventListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xmpp.packet.JID;
/**
* Manages the transcript search feature by defining properties of the search indexer. Each
* workgroup will use an instance of this class. Each instance can be configured according to
* the needs of each workgroup or may just use the global configuration. Read the properties
* section below to learn the variables that can be configured globaly and per workgroup.<p>
* <p/>
* Indexing can either be done real-time by calling updateIndex(boolean) or rebuildIndex(). Out of
* the box Live Assistant runs the indexer in timed update mode with a queue that holds the
* generated transcripts since the last update. Once the queue has been filled full an update will
* be forced even before the time interval has not been completed. It is possible to configure the
* size of the queue or even disable it and only update the index based on a timed update.<p>
* <p/>
* The automated updating mode can be adjusted by setting how often batch indexing is done. You
* can adjust this interval to suit your needs. Frequent updates mean that transcripts will be
* searchable more quickly. Less frequent updates use fewer system resources.<p>
* <p/>
* The following global properties are used by this class. Global properties will apply to all the
* workgroups unless the workgroup has overriden the property.
* <ul>
* <li><tt>workgroup.search.frequency.execution</tt> -- number of minutes to wait until the next
* update process is performed. Default is <tt>5</tt> minutes.</li>
* <li><tt>workgroup.search.pending.transcripts</tt> -- maximum number of transcripts that can be
* generated since the last update process was executed before forcing the update process to
* be executed. A value of -1 disables this feature. Default is <tt>5</tt> transcripts.</li>
* <li><tt>workgroup.search.frequency.optimization</tt> -- number of hours to wait until the next
* optimization. Default is <tt>24</tt> hours.</li>
* <li><tt>workgroup.search.analyzer.className</tt> -- name of the Lucene analyzer class to be
* used for indexing. If none was defined then {@link StandardAnalyzer} will be used.</li>
* <li><tt>workgroup.search.analyzer.stopWordList</tt> -- String[] of words to use in the global
* analyzer. If none was defined then the default stop words defined in Lucene will be used.
* </li>
* <li><tt>workgroup.search.maxdays</tt> -- maximum number of days a transcript could be old in
* order to be included when rebuilding the index. Default is <tt>365</tt> days.</li>
* </ul>
* <p/>
* The following workgroup properties are used by this class. Each workgroup has the option to
* override the corresponding defined global property.
* <ul>
* <li><tt>search.analyzer.className</tt> -- name of the Lucene analyzer class to be
* used for indexing. If none was defined then the value defined in
* <tt>workgroup.search.analyzer.className</tt> will be used instead.</li>
* <li><tt>search.analyzer.stopWordList</tt> -- String[] of words to use in the analyzer defined
* for the workgroup. If none was defined then the default stop words defined in Lucene will
* be used.</li>
* <li><tt>search.maxdays</tt> -- maximum number of days a transcript could be old in
* order to be included when rebuilding the index. If none was defined then the value defined
* in <tt>workgroup.search.maxdays</tt> will be used.</li>
* </ul>
*
* @author Gaston Dombiak
*/
public class ChatSearchManager implements WorkgroupEventListener {
private static final Logger Log = LoggerFactory.getLogger(ChatSearchManager.class);
private static final String CHATS_SINCE_DATE =
"SELECT sessionID,transcript,startTime FROM fpSession WHERE workgroupID=? AND " +
"startTime>? AND transcript IS NOT NULL ORDER BY startTime";
private static final String AGENTS_IN_SESSION =
"SELECT agentJID FROM fpAgentSession WHERE sessionID=?";
private static final String LOAD_DATES =
"SELECT lastUpdated,lastOptimization FROM fpSearchIndex WHERE workgroupID=?";
private static final String INSERT_DATES =
"INSERT INTO fpSearchIndex(workgroupID, lastUpdated, lastOptimization) VALUES(?,?,?)";
private static final String UPDATE_DATES =
"UPDATE fpSearchIndex SET lastUpdated=?,lastOptimization=? WHERE workgroupID=?";
private static final String DELETE_DATES =
"DELETE FROM fpSearchIndex WHERE workgroupID=?";
private static Map<String, ChatSearchManager> instances = new ConcurrentHashMap<String, ChatSearchManager>();
/**
* Holds the path to the parent folder of the folders that will store the workgroup
* index files.
*/
private static String parentFolder = JiveGlobals.getHomeDirectory() + File.separator + "index";
private static final long ONE_HOUR = 60 * 60 * 1000;
/**
* Hold the workgroup whose chats are being indexed by this instance. Each workgroup will
* have a ChatSearchManager since each ChatSearchManager may use a different Analyzer according
* to the workgroup needs.
*/
private Workgroup workgroup;
private Analyzer indexerAnalyzer;
private String searchDirectory;
private Searcher searcher = null;
private IndexReader searcherReader = null;
ReadWriteLock searcherLock = new ReentrantReadWriteLock();
/**
* Holds the date of the last chat that was added to the index. This information is used for
* getting the new chats since this date that should be added to the index.
*/
private Date lastUpdated;
/**
* Keeps the last time when the index was optimized. The index is optimized once a day.
*/
private Date lastOptimization;
/**
* Keeps the last date when the updating process was executed. Every time
* {@link #updateIndex(boolean)} or {@link #rebuildIndex()} are invoked this variable will
* be updated.
*/
private Date lastExecution;
/**
* Keeps the number of transcripts that have been generated since the last update process
* was executed.
*/
private AtomicInteger pendingTranscripts = new AtomicInteger(0);
/**
* Caches the filters for performance. The cached filters will be cleared when the index is
* modified.
*/
private ConcurrentHashMap<String, Filter> cachedFilters = new ConcurrentHashMap<String, Filter>();
static {
// Check if we need to create the parent folder
File dir = new File(parentFolder);
if (!dir.exists() || !dir.isDirectory()) {
dir.mkdir();
}
}
/**
* Returns the ChatSearchManager that should be used for a given {@link Workgroup}. The index
* Analyzer that the returned ChatSearchManager will use could be determined by the workgroup
* property <tt>search.analyzer.className</tt>. If the workgroup property has not been defined
* then the global Analyzer will be used.<p>
* <p/>
* The class of the global Analyzer can be specified setting the
* <tt>workgroup.search.analyzer.className</tt> property. If this property does not exist
* then a {@link StandardAnalyzer} will be used as the global Analyzer..
*
* @param workgroup the workgroup to index.
* @return the ChatSearchManager that should be used for a given workgroup.
*/
public static ChatSearchManager getInstanceFor(Workgroup workgroup) {
String workgroupName = workgroup.getJID().getNode();
ChatSearchManager answer = instances.get(workgroupName);
if (answer == null) {
synchronized (workgroupName.intern()) {
answer = instances.get(workgroupName);
if (answer == null) {
answer = new ChatSearchManager(workgroup);
instances.put(workgroupName, answer);
}
}
}
return answer;
}
/**
* Returns the Lucene analyzer class that is be used for indexing. The analyzer class
* name is stored as the Jive Property <tt>workgroup.search.analyzer.className</tt>.
*
* @return the name of the analyzer class that is used for indexing.
*/
public static String getAnalyzerClass() {
String analyzerClass = JiveGlobals.getProperty("workgroup.search.analyzer.className");
if (analyzerClass == null) {
return StandardAnalyzer.class.getName();
}
else {
return analyzerClass;
}
}
/**
* Sets the Lucene analyzer class that is used for indexing. Anytime the analyzer class
* is changed, the search index must be rebuilt for searching to work reliably. The analyzer
* class name is stored as the Jive Property <tt>workgroup.search.analyzer.className</tt>.
*
* @param className the name of the analyzer class will be used for indexing.
*/
public static void setAnalyzerClass(String className) {
if (className == null) {
throw new NullPointerException("Argument is null.");
}
// If the setting hasn't changed, do nothing.
if (className.equals(getAnalyzerClass())) {
return;
}
JiveGlobals.setProperty("workgroup.search.analyzer.className", className);
}
/**
* Notification message saying that the workgroup service is being shutdown. Release all
* the instances so the GC can claim all the workgroup objects.
*/
public static void shutdown() {
for (ChatSearchManager manager : instances.values()) {
manager.stop();
}
instances.clear();
}
private void stop() {
WorkgroupEventDispatcher.removeListener(this);
}
/**
* Returns the number of minutes to wait until the next update process is performed. The update
* process may be executed before the specified frequency if a given number of transcripts
* have been generated since the last execution. The maximum number of transcripts that can
* be generated before triggering the update process is specified by
* {@link #getMaxPendingTranscripts()}.
*/
private static int getExecutionFrequency() {
return JiveGlobals.getIntProperty("workgroup.search.frequency.execution", 5);
}
/**
* Returns the maximum number of transcripts that can be generated since the last update
* process was executed before forcing the update process to be executed. If the returned
* value is <= 0 then this functionality will be ignored.<p>
* <p/>
* In summary, the update process runs periodically but it may be force to be executed
* if a certain number of transcripts have been generated since the last update execution.
*
* @return the maximum number of transcripts that can be generated since the last update
* process was executed.
*/
private static int getMaxPendingTranscripts() {
return JiveGlobals.getIntProperty("workgroup.search.pending.transcripts", 5);
}
/**
* Returns the number of hours to wait until the next optimization. Optimizing the index makes
* the searches faster and reduces the number of files too.
*/
private static int getOptimizationFrequency() {
return JiveGlobals.getIntProperty("workgroup.search.frequency.optimization", 24);
}
ChatSearchManager(Workgroup workgroup) {
this.workgroup = workgroup;
searchDirectory = parentFolder + File.separator + workgroup.getJID().getNode();
loadAnalyzer();
loadLastUpdated();
WorkgroupEventDispatcher.addListener(this);
}
/**
* Load the search analyzer. A custom analyzer class will be used if it is defined.
*/
private void loadAnalyzer() {
Analyzer analyzer = null;
String analyzerClass = null;
String words = null;
// First check if the workgroup should use a special Analyzer
analyzerClass = workgroup.getProperties().getProperty("search.analyzer.className");
if (analyzerClass != null) {
words = workgroup.getProperties().getProperty("search.analyzer.stopWordList");
}
else {
// Use the global analyzer
analyzerClass = getAnalyzerClass();
words = JiveGlobals.getProperty("workgroup.search.analyzer.stopWordList");
}
// get stop word list is there was one
List<String> stopWords = new ArrayList<String>();
if (words != null) {
StringTokenizer st = new StringTokenizer(words, ",");
while (st.hasMoreTokens()) {
stopWords.add(st.nextToken().trim());
}
}
try {
analyzer = getAnalyzerInstance(analyzerClass, stopWords);
}
catch (Exception e) {
Log.error("Error loading custom " +
"search analyzer: " + analyzerClass, e);
}
// If the analyzer is null, use the standard analyzer.
if (analyzer == null && stopWords.size() > 0) {
analyzer = new StandardAnalyzer(stopWords.toArray(new String[stopWords.size()]));
}
else if (analyzer == null) {
analyzer = new StandardAnalyzer();
}
indexerAnalyzer = analyzer;
}
private Analyzer getAnalyzerInstance(String analyzerClass, List<String> stopWords) throws Exception {
Analyzer analyzer = null;
// Load the class.
Class c = null;
try {
c = ClassUtils.forName(analyzerClass);
}
catch (ClassNotFoundException e) {
c = getClass().getClassLoader().loadClass(analyzerClass);
}
// Create an instance of the custom analyzer.
if (stopWords.size() > 0) {
Class[] params = new Class[]{String[].class};
try {
Constructor constructor = c.getConstructor(params);
Object[] initargs = {(String[])stopWords.toArray(new String[stopWords.size()])};
analyzer = (Analyzer)constructor.newInstance(initargs);
}
catch (NoSuchMethodException e) {
// no String[] parameter to the constructor
analyzer = (Analyzer)c.newInstance();
}
}
else {
analyzer = (Analyzer)c.newInstance();
}
return analyzer;
}
private void loadLastUpdated() {
Connection con = null;
PreparedStatement pstmt = null;
ResultSet result = null;
try {
con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement(LOAD_DATES);
pstmt.setLong(1, workgroup.getID());
result = pstmt.executeQuery();
while (result.next()) {
lastUpdated = new Date(Long.parseLong(result.getString(1)));
lastOptimization = new Date(Long.parseLong(result.getString(2)));
lastExecution = lastUpdated;
}
}
catch (Exception ex) {
Log.error(ex.getMessage(), ex);
}
finally {
try {
if (pstmt != null) {
pstmt.close();
}
}
catch (Exception e) {
Log.error(e.getMessage(), e);
}
try {
if (result != null) {
result.close();
}
}
catch (SQLException e) {
Log.error(e.getMessage(), e);
}
try {
if (con != null) {
con.close();
}
}
catch (Exception e) {
Log.error(e.getMessage(), e);
}
}
}
/**
* Deletes the existing index and creates it again indexing the chats that took place
* since a given date. The lower limit date is calculated as the max number of days since a
* chat took place. There is a global property that holds the max number of days as well as
* a workgroup property that may redefine the default global value.
*
* @throws IOException if the directory cannot be read/written to, or there is a problem
* adding a document to the index.
*/
public synchronized void rebuildIndex() throws IOException {
// Calculate the max number of days based on the defined properties
int numDays = Integer.parseInt(JiveGlobals.getProperty("workgroup.search.maxdays", "365"));
String workgroupDays = workgroup.getProperties().getProperty("search.maxdays");
if (workgroupDays != null) {
numDays = Integer.parseInt(workgroupDays);
}
Calendar since = Calendar.getInstance();
since.add(Calendar.DATE, numDays * -1);
// Get the chats that took place since the specified date and add them to the index
rebuildIndex(since.getTime());
}
/**
* Updates the index file with new chats that took place since the last added chat to the
* index. If the index file is missing or a chat was never added to the index file then
* {@link #rebuildIndex} will be used instead.
*
* @param forceUpdate true if the index should be updated despite of the execution frequency.
* @throws IOException if the directory cannot be read/written to, or it does not exist, or
* there is a problem adding a document to the index.
*/
public synchronized void updateIndex(boolean forceUpdate) throws IOException {
// Check that the index files exist
File dir = new File(searchDirectory);
boolean create = !dir.exists() || !dir.isDirectory();
if (lastUpdated == null || create) {
// Recreate the index since it was never created or the index files disappeared
rebuildIndex();
}
else {
if (forceUpdate || (System.currentTimeMillis() - lastExecution.getTime()) / 60000 > getExecutionFrequency()) {
List<ChatInformation> chatsInformation = getChatsInformation(lastUpdated);
if (!chatsInformation.isEmpty()) {
// Reset the number of transcripts pending to be added to the index
pendingTranscripts.set(0);
Date lastDate = null;
IndexWriter writer = getWriter(false);
for (ChatInformation chat : chatsInformation) {
addTranscriptToIndex(chat, writer);
lastDate = chat.getCreationDate();
}
// Check if we need to optimize the index. The index is optimized once a day
if ((System.currentTimeMillis() - lastOptimization.getTime()) / ONE_HOUR >
getOptimizationFrequency()) {
writer.optimize();
// Update the optimized date
lastOptimization = new Date();
}
writer.close();
closeSearcherReader();
// Reset the filters cache
cachedFilters.clear();
// Update the last updated date
lastUpdated = lastDate;
// Save the last updated and optimized dates to the database
saveDates();
}
// Update the last time the update process was executed
lastExecution = new Date();
}
}
}
public void delete() {
try {
searcherLock.writeLock().lock();
try {
closeSearcherReader();
}
catch (IOException e) {
// Ignore.
}
// Delete index files
String[] files = new File(searchDirectory).list();
for (int i = 0; i < files.length; i++) {
File file = new File(searchDirectory, files[i]);
file.delete();
}
new File(searchDirectory).delete();
// Delete dates from the database
deleteDates();
// Remove this instance from the list of instances
instances.remove(workgroup.getJID().getNode());
// Remove this instance as a listener of the workgroup events
WorkgroupEventDispatcher.removeListener(this);
}
finally {
searcherLock.writeLock().unlock();
}
}
/**
* Returns a Lucene Searcher that can be used to execute queries. Lucene
* can handle index reading even while updates occur. However, in order
* for index changes to be reflected in search results, the reader must
* be re-opened whenever the modificationDate changes.<p>
* <p/>
* The location of the index is the "index" subdirectory in [jiveHome].
*
* @return a Searcher that can be used to execute queries.
*/
public Searcher getSearcher() throws IOException {
synchronized (indexerAnalyzer) {
if (searcherReader == null) {
if (searchDirectory != null && IndexReader.indexExists(searchDirectory)) {
searcherReader = IndexReader.open(searchDirectory);
searcher = new IndexSearcher(searcherReader);
}
else {
// Log warnings.
if (searchDirectory == null) {
Log.warn("Search " +
"directory not set, you must rebuild the index.");
}
else if (!IndexReader.indexExists(searchDirectory)) {
Log.warn("Search " +
"directory " + searchDirectory + " does not appear to " +
"be a valid search index. You must rebuild the index.");
}
return null;
}
}
}
return searcher;
}
Analyzer getAnalyzer() {
return indexerAnalyzer;
}
void putFilter(String key, Filter filter) {
cachedFilters.put(key, filter);
}
Filter getFilter(String key) {
return cachedFilters.get(key);
}
/**
* Closes the reader used by the searcher to indicate that a change to the index was made.
* A new searcher will be opened the next time one is requested.
*
* @throws IOException if an error occurs while closing the reader.
*/
private void closeSearcherReader() throws IOException {
if (searcherReader != null) {
try {
searcherLock.writeLock().lock();
searcherReader.close();
}
finally {
searcherReader = null;
searcherLock.writeLock().unlock();
}
}
}
/**
* Returns information about the chats that took place since a given date. The result is
* sorted from oldest chats to newest chats.
*
* @param since the date to use as the lower limit.
* @return information about the chats that took place since a given date.
*/
private List<ChatInformation> getChatsInformation(Date since) {
List<ChatInformation> chats = new ArrayList<ChatInformation>();
Connection con = null;
PreparedStatement pstmt = null;
ResultSet result = null;
try {
con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement(CHATS_SINCE_DATE);
pstmt.setLong(1, workgroup.getID());
pstmt.setString(2, StringUtils.dateToMillis(since));
result = pstmt.executeQuery();
while (result.next()) {
String sessionID = result.getString(1);
String transcript = result.getString(2);
String startTime = result.getString(3);
ChatNotes chatNotes = new ChatNotes();
String notes = chatNotes.getNotes(sessionID);
// Create a ChatInformation with the retrieved information
ChatInformation chatInfo = new ChatInformation(sessionID, transcript, startTime, notes);
if (chatInfo.getTranscript() != null) {
chats.add(chatInfo);
}
}
result.close();
// For each ChatInformation add the agents involved in the chat
for (ChatInformation chatInfo : chats) {
pstmt.close();
pstmt = con.prepareStatement(AGENTS_IN_SESSION);
pstmt.setString(1, chatInfo.getSessionID());
result = pstmt.executeQuery();
while (result.next()) {
chatInfo.getAgentJIDs().add(result.getString(1));
}
result.close();
}
}
catch (Exception ex) {
Log.error(ex.getMessage(), ex);
// Reset the answer if an error happened
chats = new ArrayList<ChatInformation>();
}
finally {
try {
if (pstmt != null) {
pstmt.close();
}
}
catch (Exception e) {
Log.error(e.getMessage(), e);
}
try {
if (result != null) {
result.close();
}
}
catch (SQLException e) {
Log.error(e.getMessage(), e);
}
try {
if (con != null) {
con.close();
}
}
catch (Exception e) {
Log.error(e.getMessage(), e);
}
try {
if (result != null) {
result.close();
}
}
catch (Exception e) {
Log.error(e.getMessage(), e);
}
}
// Return the chats order by startTime
return chats;
}
/**
* Retrieves information about each transcript that took place since the specified date and
* adds it to the index.<p>
* <p/>
* Note: In order to cope with large volumes of data we don't want to load
* all the information into memory. Therefore, for each retrieved row we create a
* ChatInformation instance and add it to the index.
*
* @param since the date to use as the lower limit.
* @throws IOException if rebuilding the index fails.
*/
private void rebuildIndex(Date since) throws IOException {
Date lastDate = null;
IndexWriter writer = getWriter(true);
Connection con = null;
PreparedStatement pstmt = null;
ResultSet result = null;
try {
// TODO Review logic for JDBC drivers that load all the answer into memory
con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement(CHATS_SINCE_DATE);
pstmt.setLong(1, workgroup.getID());
pstmt.setString(2, StringUtils.dateToMillis(since));
result = pstmt.executeQuery();
while (result.next()) {
String sessionID = result.getString(1);
String transcript = result.getString(2);
String startTime = result.getString(3);
String chatNotes = new ChatNotes().getNotes(sessionID);
ChatInformation chatInfo = new ChatInformation(sessionID, transcript, startTime, chatNotes);
if (chatInfo.getTranscript() != null) {
addAgentHistoryToChatInformation(chatInfo);
// Add the ChatInformation to the index
addTranscriptToIndex(chatInfo, writer);
lastDate = chatInfo.getCreationDate();
}
}
}
catch (Exception ex) {
Log.error(ex.getMessage(), ex);
// Reset the lastDate if an error happened
lastDate = null;
}
finally {
try {
if (result != null) {
result.close();
}
}
catch (SQLException e) {
Log.error(e.getMessage(), e);
}
DbConnectionManager.closeConnection(pstmt, con);
}
writer.optimize();
writer.close();
if (lastDate != null) {
closeSearcherReader();
// Reset the filters cache
cachedFilters.clear();
// Update the last updated and optimized dates
lastOptimization = new Date();
lastUpdated = lastDate;
lastExecution = new Date();
pendingTranscripts.set(0);
// Save the last updated and optimized dates to the database
saveDates();
}
}
private void addAgentHistoryToChatInformation(ChatInformation chatInfo) {
Connection con = null;
PreparedStatement pstmt = null;
ResultSet result = null;
try {
// Add the agents involved in the chat to the ChatInformation
con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement(AGENTS_IN_SESSION);
pstmt.setString(1, chatInfo.getSessionID());
result = pstmt.executeQuery();
while (result.next()) {
chatInfo.getAgentJIDs().add(result.getString(1));
}
}
catch (SQLException e) {
Log.error(e.getMessage(), e);
}
finally {
if (result != null) {
try {
result.close();
}
catch (SQLException e) {
Log.error(e.getMessage(), e);
}
}
DbConnectionManager.closeConnection(pstmt, con);
}
}
/**
* Update the dates of this indexer in the database
*/
private void saveDates() {
Connection con = null;
PreparedStatement pstmt = null;
try {
con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement(UPDATE_DATES);
pstmt.setString(1, StringUtils.dateToMillis(lastUpdated));
pstmt.setString(2, StringUtils.dateToMillis(lastOptimization));
pstmt.setLong(3, workgroup.getID());
boolean updated = pstmt.executeUpdate() > 0;
// If the row was not updated (because it doesn't exist) then insert a new row
if (!updated) {
pstmt.close();
pstmt = con.prepareStatement(INSERT_DATES);
pstmt.setLong(1, workgroup.getID());
pstmt.setString(2, StringUtils.dateToMillis(lastUpdated));
pstmt.setString(3, StringUtils.dateToMillis(lastOptimization));
pstmt.executeUpdate();
}
}
catch (Exception ex) {
Log.error(ex.getMessage(), ex);
}
finally {
try {
if (pstmt != null) {
pstmt.close();
}
}
catch (Exception e) {
Log.error(e.getMessage(), e);
}
try {
if (con != null) {
con.close();
}
}
catch (Exception e) {
Log.error(e.getMessage(), e);
}
}
}
/**
* Update the dates of this indexer in the database
*/
private void deleteDates() {
Connection con = null;
PreparedStatement pstmt = null;
try {
con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement(DELETE_DATES);
pstmt.setLong(1, workgroup.getID());
pstmt.executeUpdate();
}
catch (Exception ex) {
Log.error(ex.getMessage(), ex);
}
finally {
try {
if (pstmt != null) {
pstmt.close();
}
}
catch (Exception e) {
Log.error(e.getMessage(), e);
}
try {
if (con != null) {
con.close();
}
}
catch (Exception e) {
Log.error(e.getMessage(), e);
}
}
}
private void addTranscriptToIndex(ChatInformation chat, IndexWriter writer) throws IOException {
// Flag that indicates if the transcript includes one or more messages. If no message was
// found then nothing will be added to the index
boolean hasMessages = false;
Document document = new Document();
for (Iterator<Element> elements = chat.getTranscript().elementIterator(); elements.hasNext();) {
Element element = elements.next();
// Only add Messages to the index (Presences are discarded)
if ("message".equals(element.getName())) {
// TODO Index XHTML bodies?
String body = element.elementTextTrim("body");
String from = element.attributeValue("from");
String to = element.attributeValue("to");
String fromNickname = new JID(from).getResource();
String toNickname = new JID(to).getResource();
final StringBuilder builder = new StringBuilder();
builder.append(body);
builder.append(" ");
builder.append(fromNickname);
builder.append(" ");
builder.append(toNickname);
if (body != null) {
if (chat.getNotes() != null) {
builder.append(" ");
builder.append(chat.getNotes());
}
if (chat.getAgentJIDs() != null) {
for (String jid : chat.getAgentJIDs()) {
builder.append(" ");
builder.append(jid);
}
}
document.add(new Field("body", builder.toString(), Field.Store.NO,
Field.Index.TOKENIZED));
// Indicate that a message was found
hasMessages = true;
}
}
}
if (hasMessages) {
// Add the sessionID that indentifies the chat session to the document
document.add (new Field("sessionID", String.valueOf(chat.getSessionID()),
Field.Store.YES, Field.Index.UN_TOKENIZED));
// Add the JID of the agents involved in the chat to the document
for (String agentJID : chat.getAgentJIDs()) {
document.add(new Field("agentJID", agentJID,
Field.Store.YES, Field.Index.UN_TOKENIZED));
}
// Add the date when the chat started to the document
long date = chat.getCreationDate().getTime();
document.add(new Field("creationDate", DateTools.timeToString(date,
DateTools.Resolution.DAY), Field.Store.YES, Field.Index.UN_TOKENIZED));
writer.addDocument(document);
}
}
/**
* Returns a Lucene IndexWriter. The create param indicates whether an
* existing index should be used if it's found there.
*/
private IndexWriter getWriter(boolean create) throws IOException {
IndexWriter writer = new IndexWriter(searchDirectory, indexerAnalyzer, create);
return writer;
}
// ###############################################################################
// WorkgroupEventListener implemented methods
// ###############################################################################
public void workgroupCreated(Workgroup workgroup) {
//Do nothing
}
public void workgroupDeleting(Workgroup workgroup) {
//Do nothing
}
public void workgroupDeleted(Workgroup workgroup) {
// Do nothing if the notification is related to other workgroup
if (this.workgroup != workgroup) {
return;
}
delete();
}
public void workgroupOpened(Workgroup workgroup) {
//Do nothing
}
public void workgroupClosed(Workgroup workgroup) {
//Do nothing
}
public void agentJoined(Workgroup workgroup, AgentSession agentSession) {
//Do nothing
}
public void agentDeparted(Workgroup workgroup, AgentSession agentSession) {
//Do nothing
}
public void chatSupportStarted(Workgroup workgroup, String sessionID) {
//Do nothing
}
public void chatSupportFinished(Workgroup workgroup, String sessionID) {
// Do nothing if the notification is related to other workgroup
if (this.workgroup != workgroup) {
return;
}
// Update the number of generated transcripts since the last update process was executed
// If the maximum number of pending transcripts has been reached then force an update of
// the index
if (getMaxPendingTranscripts() > 0 &&
pendingTranscripts.incrementAndGet() == getMaxPendingTranscripts())
{
// Update in another thread
TaskEngine.getInstance().submit(new Runnable() {
public void run() {
try {
updateIndex(true);
}
catch (IOException e) {
Log.error(e.getMessage(), e);
}
}
});
}
}
public void agentJoinedChatSupport(Workgroup workgroup, String sessionID,
AgentSession agentSession) {
//Do nothing
}
public void agentLeftChatSupport(Workgroup workgroup, String sessionID,
AgentSession agentSession) {
//Do nothing
}
/**
* Class that holds information about a chat. Having this class avoids having to pass
* all the chat information as parameters across the methods.
*/
class ChatInformation {
private String sessionID;
private Date creationDate;
private Element transcript;
private List<String> agentJIDs;
private String notes;
public ChatInformation(String sessionID, String transcriptXML, String startTime, String notes) {
this.sessionID = sessionID;
try {
this.transcript = DocumentHelper.parseText(transcriptXML).getRootElement();
}
catch (DocumentException e) {
Log.error("Error retrieving chat information of session: " + sessionID, e);
Log.debug("Error retrieving chat information of session: " + sessionID +
" and transcript: " + transcriptXML, e);
}
this.creationDate = new Date(Long.parseLong(startTime));
agentJIDs = new ArrayList<String>();
this.notes = notes;
}
public String getSessionID() {
return sessionID;
}
public Date getCreationDate() {
return creationDate;
}
public Element getTranscript() {
return transcript;
}
public List<String> getAgentJIDs() {
return agentJIDs;
}
public String getNotes() {
return notes;
}
}
}