/* * 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; } } }