Commit 8a215713 authored by Leon Roy's avatar Leon Roy Committed by leonroy
parent d96e1190
/**
* $Revision$
* $Date$
*
* Copyright (C) 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.openfire.archive;
import java.io.File;
import java.io.FileWriter;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.Writer;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TimerTask;
import java.util.TreeSet;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
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.IndexModifier;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.Term;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;
import org.dom4j.DocumentFactory;
import org.dom4j.io.OutputFormat;
import org.dom4j.io.XMLWriter;
import org.jivesoftware.database.DbConnectionManager;
import org.jivesoftware.openfire.reporting.util.TaskEngine;
import org.jivesoftware.util.JiveConstants;
import org.jivesoftware.util.JiveGlobals;
import org.jivesoftware.util.XMLProperties;
import org.picocontainer.Startable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xmpp.packet.JID;
/**
* Indexes archived conversations. If conversation archiving is not enabled,
* this class does nothing. The search index is maintained in the <tt>monitoring/search</tt>
* directory of the Openfire home directory. It's automatically updated with the latest
* conversation content as long as conversation archiving is enabled. The index update
* interval is controllec by the Jive property "conversation.search.updateInterval" and
* the default value is 15 minutes.
*
* @see ArchiveSearcher
* @author Matt Tucker
*/
public class ArchiveIndexer implements Startable {
private static final Logger Log = LoggerFactory.getLogger(ArchiveIndexer.class);
private static final String ALL_CONVERSATIONS =
"SELECT conversationID, isExternal FROM ofConversation";
private static final String NEW_CONVERSATIONS =
"SELECT DISTINCT conversationID FROM ofMessageArchive WHERE sentDate > ?";
private static final String CONVERSATION_METADATA =
"SELECT isExternal FROM ofConversation WHERE conversationID=?";
private static final String CONVERSATION_MESSAGES =
"SELECT conversationID, sentDate, fromJID, toJID, body FROM ofMessageArchive " +
"WHERE conversationID IN ? ORDER BY conversationID";
private File searchDir;
private TaskEngine taskEngine;
private ConversationManager conversationManager;
private XMLProperties indexProperties;
private Directory directory;
private IndexSearcher searcher;
private Lock writerLock;
private boolean stopped = false;
private boolean rebuildInProgress = false;
private RebuildFuture rebuildFuture;
private long lastModified = 0;
private TimerTask indexUpdater;
/**
* Constructs a new archive indexer.
*
* @param conversationManager a ConversationManager instance.
* @param taskEngine a task engine instance.
*/
public ArchiveIndexer(ConversationManager conversationManager, TaskEngine taskEngine) {
this.conversationManager = conversationManager;
this.taskEngine = taskEngine;
}
public void start() {
searchDir = new File(JiveGlobals.getHomeDirectory() +
File.separator + "monitoring" + File.separator + "search");
if (!searchDir.exists()) {
searchDir.mkdirs();
}
boolean indexCreated = false;
try {
loadPropertiesFile(searchDir);
// If the index already exists, use it.
if (IndexReader.indexExists(searchDir)) {
directory = FSDirectory.getDirectory(searchDir, false);
}
// Otherwise, create a new index.
else {
directory = FSDirectory.getDirectory(searchDir, true);
indexCreated = true;
}
}
catch (IOException ioe) {
Log.error(ioe.getMessage(), ioe);
}
writerLock = new ReentrantLock(true);
// Force the directory unlocked if it's locked (due to non-clean app shut-down,
// for example).
try {
if (IndexReader.isLocked(directory)) {
Log.warn("Archiving search index was locked, probably due to non-clean " +
"application shutdown.");
IndexReader.unlock(directory);
}
}
catch (IOException ioe) {
Log.error(ioe.getMessage(), ioe);
}
String modified = indexProperties.getProperty("lastModified");
if (modified != null) {
try {
lastModified = Long.parseLong(modified);
}
catch (NumberFormatException nfe) {
// Ignore.
}
}
// If the index has never been updated, build it from scratch.
if (lastModified == 0 || indexCreated) {
taskEngine.submit(new Runnable() {
public void run() {
rebuildIndex();
}
});
}
indexUpdater = new TimerTask() {
@Override
public void run() {
updateIndex();
}
};
int updateInterval = JiveGlobals.getIntProperty("conversation.search.updateInterval", 15);
taskEngine.scheduleAtFixedRate(indexUpdater, JiveConstants.MINUTE * 5,
JiveConstants.MINUTE * updateInterval);
}
public void stop() {
stopped = true;
indexUpdater.cancel();
if (searcher != null) {
try {
searcher.close();
}
catch (Exception e) {
Log.error(e.getMessage(), e);
}
searcher = null;
}
try {
directory.close();
}
catch (Exception e) {
Log.error(e.getMessage(), e);
}
directory = null;
indexProperties = null;
conversationManager = null;
searchDir = null;
rebuildFuture = null;
}
/**
* Returns the total size of the search index (in bytes).
*
* @return the total size of the search index (in bytes).
*/
public long getIndexSize() {
File [] files = searchDir.listFiles(new FilenameFilter() {
public boolean accept(File dir, String name) {
// Ignore the index properties file since it's not part of the index.
return !name.equals("indexprops.xml");
}
});
if (files == null) {
// Search folder does not exist so size of index is 0
return 0;
}
long size = 0;
for (File file : files) {
size += file.length();
}
return size;
}
/**
* Updates the search index with all new conversation data since the last index update.
*/
public void updateIndex() {
// Immediately return if the service has been stopped.
if (stopped) {
return;
}
// Do nothing if archiving is disabled.
if (!conversationManager.isArchivingEnabled()) {
return;
}
// If we're currently rebuilding the index, return.
if (rebuildInProgress) {
return;
}
writerLock.lock();
IndexModifier writer = null;
try {
writer = new IndexModifier(directory, new StandardAnalyzer(), false);
List<Long> conversationIDs = new ArrayList<Long>();
Connection con = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement(NEW_CONVERSATIONS);
pstmt.setLong(1, lastModified);
rs = pstmt.executeQuery();
while (rs.next()) {
conversationIDs.add(rs.getLong(1));
}
}
catch (SQLException sqle) {
Log.error(sqle.getMessage(), sqle);
}
finally {
DbConnectionManager.closeConnection(rs, pstmt, con);
}
// Delete any conversations found -- they may have already been indexed, but
// updated since then.
for (long conversationID : conversationIDs) {
writer.deleteDocuments(new Term("conversationID", Long.toString(conversationID)));
}
// Load meta-data for each conversation.
Map<Long, Boolean> externalMetaData = new HashMap<Long, Boolean>();
for (long conversationID : conversationIDs) {
try {
con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement(CONVERSATION_METADATA);
pstmt.setLong(1, conversationID);
rs = pstmt.executeQuery();
while (rs.next()) {
externalMetaData.put(conversationID, rs.getInt(1) == 1);
}
}
catch (SQLException sqle) {
Log.error(sqle.getMessage(), sqle);
}
finally {
DbConnectionManager.closeConnection(rs, pstmt, con);
}
}
// Now index all the new conversations.
long newestDate = indexConversations(conversationIDs, externalMetaData, writer, false);
writer.optimize();
// Done indexing so store a last modified date.
if (newestDate != -1) {
lastModified = newestDate;
indexProperties.setProperty("lastModified", Long.toString(lastModified));
}
}
catch (IOException ioe) {
Log.error(ioe.getMessage(), ioe);
}
finally {
if (writer != null) {
try {
writer.close();
}
catch (Exception e) {
Log.error(e.getMessage(), e);
}
}
writerLock.unlock();
}
}
/**
* Rebuilds the search index with all archived conversation data. This method returns
* a Future that represents the status of the index rebuild process (also available
* via {@link #getIndexRebuildProgress()}). The integer value
* (values 0 through 100) represents the percentage of work done. If message archiving
* is disabled, this method will return <tt>null</tt>.
*
* @return a Future to indicate the status of rebuilding the index or <tt>null</tt> if
* rebuilding the index is not possible.
*/
public synchronized Future<Integer> rebuildIndex() {
// Immediately return if the service has been stopped.
if (stopped) {
return null;
}
// If a rebuild is already happening, return.
if (rebuildInProgress) {
return null;
}
rebuildInProgress = true;
// Do nothing if archiving is disabled.
if (!conversationManager.isArchivingEnabled()) {
return null;
}
// Create a future to track the index rebuild progress.
rebuildFuture = new RebuildFuture();
// Create a runnable that will perform the actual rebuild work.
Runnable rebuildTask = new Runnable() {
public void run() {
List<Long> conversationIDs = new ArrayList<Long>();
Map<Long, Boolean> externalMetaData = new HashMap<Long, Boolean>();
Connection con = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement(ALL_CONVERSATIONS);
rs = pstmt.executeQuery();
while (rs.next()) {
long conversationID = rs.getLong(1);
conversationIDs.add(conversationID);
externalMetaData.put(conversationID, rs.getInt(2) == 1);
}
}
catch (SQLException sqle) {
Log.error(sqle.getMessage(), sqle);
}
finally {
DbConnectionManager.closeConnection(rs, pstmt, con);
}
if (!conversationIDs.isEmpty()) {
// Index the conversations.
writerLock.lock();
IndexModifier writer = null;
try {
writer = new IndexModifier(directory, new StandardAnalyzer(), true);
long newestDate = indexConversations(conversationIDs, externalMetaData,
writer, true);
writer.optimize();
// Done indexing so store a last modified date.
if (newestDate != -1) {
lastModified = newestDate;
indexProperties.setProperty("lastModified", Long.toString(lastModified));
}
}
catch (IOException ioe) {
Log.error(ioe.getMessage(), ioe);
}
finally {
if (writer != null) {
try {
writer.close();
}
catch (Exception e) {
Log.error(e.getMessage(), e);
}
}
writerLock.unlock();
}
}
// Done rebuilding the index, so reset state.
rebuildFuture = null;
rebuildInProgress = false;
}
};
taskEngine.submit(rebuildTask);
return rebuildFuture;
}
/**
* Returns a Future representing the status of an index rebuild operation. This is the
* same Future returned by the {@link #rebuildIndex()} method; access is provided via
* this method as a convenience. If the index is not currently being rebuilt, this method
* will return <tt>null</tt>.
*
* @return a Future that represents the index rebuild status or <tt>null</tt> if the
* index is not being rebuilt.
*/
public Future<Integer> getIndexRebuildProgress() {
return rebuildFuture;
}
/**
* Indexes a set of conversations. Each conversation is stored as a single Lucene document
* by appending message bodies together. The date of the newest message indexed is
* returned, or -1 if no conversations are indexed.
*
* @param conversationIDs the ID's of the conversations to index.
* @param externalMetaData meta-data about whether each conversation involves a participant on
* an external server.
* @param writer an IndexModifier to add the documents to.
* @param indexRebuild true if this is an index rebuild operation.
* @return the date of the newest message archived.
*/
private long indexConversations(List<Long> conversationIDs, Map<Long, Boolean> externalMetaData,
IndexModifier writer, boolean indexRebuild) throws IOException
{
if (conversationIDs.isEmpty()) {
return -1;
}
// Keep track of how many conversations we index for index rebuild stats.
int indexedConversations = 0;
long newestDate = -1;
// Index 250 items at a time.
final int OP_SIZE = 250;
int n = ((conversationIDs.size() - 1) / OP_SIZE);
if (n == 0) {
n = 1;
}
for (int i = 0; i < n; i++) {
StringBuilder inSQL = new StringBuilder();
inSQL.append(" (");
int start = i * OP_SIZE;
int end = (start + OP_SIZE > conversationIDs.size()) ? conversationIDs.size() : start + OP_SIZE;
if (end > conversationIDs.size()) {
end = conversationIDs.size();
}
inSQL.append(conversationIDs.get(start));
for (int j = start + 1; j < end; j++) {
inSQL.append(", ").append(conversationIDs.get(j));
}
inSQL.append(")");
// Get the messages.
Connection con = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement(CONVERSATION_MESSAGES.replaceAll("\\?", inSQL.toString()));
rs = pstmt.executeQuery();
long conversationID = -1;
long date = -1;
Set<String> jids = null;
StringBuilder text = null;
// Loop through each message. Each conversation is a single document. So, as
// we find each conversation we save off the last chunk of content as a document.
while (rs.next()) {
long id = rs.getLong(1);
if (id != conversationID) {
if (conversationID != -1) {
// Index the previously defined doc.
boolean external = externalMetaData.get(conversationID);
indexDocument(writer, conversationID, external, date, jids, text.toString());
}
// Reset the variables to index the next conversation.
conversationID = id;
date = rs.getLong(2);
jids = new TreeSet<String>();
// Get the JID's. Each JID may be stored in full format. We convert
// to bare JID for indexing so that searching is possible.
jids.add(new JID(rs.getString(3)).toBareJID());
jids.add(new JID(rs.getString(4)).toBareJID());
text = new StringBuilder();
}
// Make sure that we record the earliest date of the conversation start
// for consistency.
long msgDate = rs.getLong(2);
if (msgDate < date) {
date = msgDate;
}
// See if this is the newest message found so far.
if (msgDate > newestDate) {
newestDate = msgDate;
}
// Add the body of the current message to the buffer.
text.append(DbConnectionManager.getLargeTextField(rs, 5)).append("\n");
}
// Finally, index the last document found.
if (conversationID != -1) {
// Index the previously defined doc.
boolean external = externalMetaData.get(conversationID);
indexDocument(writer, conversationID, external, date, jids, text.toString());
}
// If this is an index rebuild, we need to track the percentage done.
if (indexRebuild) {
indexedConversations++;
rebuildFuture.setPercentageDone(indexedConversations/conversationIDs.size());
}
}
catch (SQLException sqle) {
Log.error(sqle.getMessage(), sqle);
}
finally {
DbConnectionManager.closeConnection(rs, pstmt, con);
}
}
return newestDate;
}
/**
* Indexes a single conversation.
*
* @param writer the index modifier.
* @param conversationID the ID of the conversation to index.
* @param external true if the conversation has a participant from an external server.
* @param date the date the conversation was started.
* @param jids the JIDs of the users in the conversation.
* @param text the full text of the conversation.
* @throws IOException if an IOException occurs.
*/
private void indexDocument(IndexModifier writer, long conversationID, boolean external,
long date, Set<String> jids, String text) throws IOException
{
Document document = new Document();
document.add(new Field("conversationID", String.valueOf(conversationID),
Field.Store.YES, Field.Index.UN_TOKENIZED));
document.add(new Field("external", String.valueOf(external),
Field.Store.YES, Field.Index.UN_TOKENIZED));
document.add(new Field("date", DateTools.timeToString(date, DateTools.Resolution.DAY),
Field.Store.YES, Field.Index.UN_TOKENIZED));
for (String jid : jids) {
document.add(new Field("jid", jid, Field.Store.YES, Field.Index.TOKENIZED));
}
document.add(new Field("text", text, Field.Store.NO, Field.Index.TOKENIZED));
writer.addDocument(document);
}
/**
* Returns an IndexSearcher to search the archive index.
*
* @return an IndexSearcher.
* @throws IOException if an IOException occurs.
*/
synchronized IndexSearcher getSearcher() throws IOException {
// If the searcher hasn't been instantiated, create it.
if (searcher == null) {
searcher = new IndexSearcher(directory);
}
// See if the searcher needs to be closed due to the index being updated.
else if (!searcher.getIndexReader().isCurrent()) {
searcher.close();
searcher = new IndexSearcher(directory);
}
return searcher;
}
/**
* Loads a property manager for search properties if it isn't already
* loaded. If an XML file for the search properties isn't already
* created, it will attempt to make a file with default values.
*/
private void loadPropertiesFile(File searchDir) throws IOException {
File indexPropertiesFile = new File(searchDir, "indexprops.xml");
// Make sure the file actually exists. If it doesn't, a new file
// will be created.
// If it doesn't exists we have to create it.
if (!indexPropertiesFile.exists()) {
org.dom4j.Document doc = DocumentFactory.getInstance().createDocument(
DocumentFactory.getInstance().createElement("search"));
// Now, write out to the file.
Writer out = null;
try {
// Use JDOM's XMLOutputter to do the writing and formatting.
out = new FileWriter(indexPropertiesFile);
XMLWriter outputter = new XMLWriter(out, OutputFormat.createPrettyPrint());
outputter.write(doc);
outputter.flush();
}
catch (Exception e) {
Log.error(e.getMessage(), e);
}
finally {
try {
if (out != null) {
out.close();
}
}
catch (Exception e) {
// Ignore.
}
}
}
indexProperties = new XMLProperties(indexPropertiesFile);
}
/**
* A Future class to track the status of index rebuilding.
*/
private class RebuildFuture implements Future<Integer> {
private int percentageDone = 0;
public boolean cancel(boolean mayInterruptIfRunning) {
// Don't allow cancels.
return false;
}
public boolean isCancelled() {
return false;
}
public boolean isDone() {
return percentageDone == 100;
}
public Integer get() throws InterruptedException, ExecutionException {
return percentageDone;
}
public Integer get(long timeout, TimeUnit unit) throws InterruptedException,
ExecutionException, TimeoutException
{
return percentageDone;
}
/**
* Sets the percentage done.
*
* @param percentageDone the percentage done.
*/
public void setPercentageDone(int percentageDone) {
if (percentageDone < 0 || percentageDone > 100) {
throw new IllegalArgumentException("Invalid value: " + percentageDone);
}
this.percentageDone = percentageDone;
}
}
}
\ No newline at end of file
/**
* $Revision: 3034 $
* $Date: 2005-11-04 21:02:33 -0300 (Fri, 04 Nov 2005) $
*
* Copyright (C) 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.openfire.archive;
import org.jivesoftware.openfire.cluster.ClusterManager;
import org.jivesoftware.openfire.interceptor.InterceptorManager;
import org.jivesoftware.openfire.interceptor.PacketInterceptor;
import org.jivesoftware.openfire.interceptor.PacketRejectedException;
import org.jivesoftware.openfire.session.Session;
import org.picocontainer.Startable;
import org.xmpp.packet.JID;
import org.xmpp.packet.Message;
import org.xmpp.packet.Packet;
import java.util.Date;
/**
* Intercepts packets to track conversations. Only the following messages
* are processed:
* <ul>
* <li>Messages sent between local users.</li>
* <li>Messages sent between local user and remote entities (e.g. remote users).</li>
* <li>Messages sent between local users and users using legacy networks (i.e. transports).</li>
* </ul>
* Therefore, messages that are sent to Publish-Subscribe or any other internal service are ignored.
*
* @author Matt Tucker
*/
public class ArchiveInterceptor implements PacketInterceptor, Startable {
private ConversationManager conversationManager;
public ArchiveInterceptor(ConversationManager conversationManager) {
this.conversationManager = conversationManager;
}
public void interceptPacket(Packet packet, Session session, boolean incoming, boolean processed)
throws PacketRejectedException
{
// Ignore any packets that haven't already been processed by interceptors.
if (!processed) {
return;
}
if (packet instanceof Message) {
// Ignore any outgoing messages (we'll catch them when they're incoming).
if (!incoming) {
return;
}
Message message = (Message) packet;
// Ignore any messages that don't have a body so that we skip events.
// Note: XHTML messages should always include a body so we should be ok. It's
// possible that we may need special XHTML filtering in the future, however.
if (message.getBody() != null) {
// Only process messages that are between two users, group chat rooms, or gateways.
if (conversationManager.isConversation(message)) {
// Process this event in the senior cluster member or local JVM when not in a cluster
if (ClusterManager.isSeniorClusterMember()) {
conversationManager.processMessage(message.getFrom(), message.getTo(), message.getBody(), new Date());
}
else {
JID sender = message.getFrom();
JID receiver = message.getTo();
ConversationEventsQueue eventsQueue = conversationManager.getConversationEventsQueue();
eventsQueue.addChatEvent(conversationManager.getConversationKey(sender, receiver),
ConversationEvent.chatMessageReceived(sender, receiver,
conversationManager.isMessageArchivingEnabled() ? message.getBody() : null,
new Date()));
}
}
}
}
}
public void start() {
InterceptorManager.getInstance().addInterceptor(this);
}
public void stop() {
InterceptorManager.getInstance().removeInterceptor(this);
conversationManager = null;
}
}
/**
* $Revision$
* $Date$
*
* Copyright (C) 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.openfire.archive;
import org.xmpp.packet.JID;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
/**
* Defines a search query for use with an {@link ArchiveSearcher}. In general, there
* are two types of searches that users might perform:<ul>
*
* <li>Query string search: search conversations for specific keywords, optionally
* filtering the results by conversation participants or date range.
* <li>Meta-data search: find all conversations by certain users or within a certain
* date range. This search is typical for compliance purposes.
*
* @author Matt Tucker
*/
public class ArchiveSearch {
/**
* An integer value that represents NULL. The actual value is
* Integer.MAX_VALUE - 123 (an arbitrary number that has a very low
* probability of actually being selected by a user as a valid value).
*/
public static final int NULL_INT = Integer.MAX_VALUE - 123;
private String queryString;
private Collection<JID> participants = Collections.emptyList();
/**
* Start of conversation has to be bigger or equal to this value (if set)
*/
private Date dateRangeMin;
/**
* Start of conversation has to be smaller or equal to this value (if set)
*/
private Date dateRangeMax;
/**
* Specified timestamp has to be between start and last activity dates
*/
private Date includeTimestamp;
private JID room;
private int startIndex = 0;
private int numResults = NULL_INT;
private SortField sortField;
private SortOrder sortOrder;
private boolean externalWildcardMode;
/**
* Creates a new search on a query string.
*
* @param queryString the query string to use for the search.
* @return an ArchiveSearch instance to search using the specified query string.
*/
public static ArchiveSearch createKeywordSearch(String queryString) {
ArchiveSearch search = new ArchiveSearch();
search.setQueryString(queryString);
search.setSortField(SortField.relevance);
return search;
}
/**
* Constructs a new archive search, sorted on date descending.
*/
public ArchiveSearch() {
this.sortOrder = SortOrder.descending;
this.sortField = SortField.date;
}
/**
* Returns the query string used for the search or <tt>null</tt> if no query string
* has been set. The query String can contain the full
* <a href="http://lucene.apache.org/java/docs/queryparsersyntax.html">search syntax</a>
* supported by Lucene.
*
* @return the query string or <tt>null</tt> if no query string has been set.
*/
public String getQueryString() {
return queryString;
}
/**
* Sets the query string used for the search, which can be <tt>null</tt> to indicate that
* no query string should be used. The query String can contain the full
* <a href="http://lucene.apache.org/java/docs/queryparsersyntax.html">search syntax</a>
* supported by Lucene.
*
* @param queryString the query string or <tt>null</tt> if no query string should be used.
*/
public void setQueryString(String queryString) {
this.queryString = queryString;
}
/**
* Returns the participants that this search covers. If no participants are specified
* (via an empty collection), then this search will wildcard match against both users.
* If a single participant is specified, this search will wildcard match against the
* other participant. The wildcard matching mode is either external users only, or all
* users, depending on the value returned by {@link #isExternalWildcardMode()}.
*
* @return the participants that this search covers.
*/
public Collection<JID> getParticipants() {
return participants;
}
/**
* Sets the participants that this search covers. If no participants are specified
* then this search will wildcard match against both users. If a single participant
* is specified, this search will wildcard match against the other participant.
* The wildcard matching mode is either external users only, or all
* users, depending on the value returned by {@link #isExternalWildcardMode()}.
*
* @param participants the participants that this search covers.
*/
public void setParticipants(JID... participants) {
if (participants == null) {
this.participants = Collections.emptyList();
}
else {
if (participants.length > 2) {
throw new IllegalArgumentException("Not possible to search on more than " +
"two participants.");
}
// Enforce using the bare JID.
for (int i=0; i<participants.length; i++) {
participants[i] = new JID(participants[i].toBareJID());
}
this.participants = Arrays.asList(participants);
}
}
/**
* Returns the date that represents the lower boundary for conversations
* that will be returned by the search. If this value has not been set, the method
* will return <tt>null</tt>.
*
* @return a Date representing the lower bound for dates to search on or <tt>null</tt>
* if there is no lower bound.
*/
public Date getDateRangeMin() {
return dateRangeMin;
}
/**
* Sets the date that represents the lower boundary for conversations
* that will be returned by the search. A value of <tt>null</tt> indicates that
* there should be no lower boundary.
*
* @param dateRangeMin a Date representing the lower bound for dates to search on
* or <tt>null</tt> if there is no lower bound.
*/
public void setDateRangeMin(Date dateRangeMin) {
this.dateRangeMin = dateRangeMin;
}
/**
* Returns the date that represents the upper boundary for conversations
* that will be returned by the search. If this value has not been set, the method
* will return <tt>null</tt>.
*
* @return a Date representing the upper bound for dates to search on or <tt>null</tt>
* if there is no upper bound.
*/
public Date getDateRangeMax() {
return dateRangeMax;
}
/**
* Sets the date that represents the upper boundary for conversations
* that will be returned by the search. A value of <tt>null</tt> indicates that
* there should be no upper boundary.
*
* @param dateRangeMax a Date representing the upper bound for dates to search on
* or <tt>null</tt> if there is no upper bound.
*/
public void setDateRangeMax(Date dateRangeMax) {
this.dateRangeMax = dateRangeMax;
}
/**
* Returns the JID of the room for conversations that will be returned by the search. If
* this value has not been set, the method will return <tt>null</tt>.
*
* @return JID of the room or <tt>null</tt> if there is no room to filter on.
*/
public JID getRoom() {
return room;
}
/**
* Sets the JID of the room for conversations that will be returned by the search. If
* this value has not been set, the method will return <tt>null</tt>.
*
* @param room JID of the room or <tt>null</tt> if there is no room to filter on.
*/
public void setRoom(JID room) {
this.room = room;
}
/**
* Returns the timestamp to use for filtering conversations. This timestamp
* has to be between the time when the conversation started and ended.
*
* @return timestamp between the time when the conversation started and ended.
*/
public Date getIncludeTimestamp() {
return includeTimestamp;
}
/**
* Set the timestamp to use for filtering conversations. This timestamp
* has to be between the time when the conversation started and ended.
*
* @param includeTimestamp timestamp between the time when the conversation started and ended.
*/
public void setIncludeTimestamp(Date includeTimestamp) {
this.includeTimestamp = includeTimestamp;
}
/**
* Returns the sort order, which will be {@link SortOrder#ascending ascending} or
* {@link SortOrder#descending descending}.
*
* @return the sort order.
*/
public SortOrder getSortOrder() {
return this.sortOrder;
}
/**
* Sets the sort type, which will be {@link SortOrder#ascending ascending} or
* {@link SortOrder#descending descending}.
*
* @param sortOrder the order that results will be sorted in.
*/
public void setSortOrder(SortOrder sortOrder) {
this.sortOrder = sortOrder;
}
/**
* Returns the sort field, which will be {@link SortField#relevance relevance} or
* {@link SortField#relevance relevance}.
*
* @return the sort field.
*/
public SortField getSortField() {
return this.sortField;
}
/**
* Sets the sort field, which will be {@link SortField#relevance relevance} or
* {@link SortField#relevance relevance}.
*
* @param sortField the field that results will be sorted on.
*/
public void setSortField(SortField sortField) {
this.sortField = sortField;
}
/**
* Returns the max number of results that should be returned.
* The default value for is NULL_INT, which means there will be no limit
* on the number of results. This method can be used in combination with
* setStartIndex(int) to perform pagination of results.
*
* @return the max number of results to return.
* @see #setStartIndex(int)
*/
public int getNumResults() {
return numResults;
}
/**
* Sets the limit on the number of results to be returned.
*
* @param numResults the number of results to return.
*/
public void setNumResults(int numResults) {
if (numResults != NULL_INT && numResults < 0) {
throw new IllegalArgumentException("numResults cannot be less than 0.");
}
this.numResults = numResults;
}
/**
* Returns the index of the first result to return.
*
* @return the index of the first result which should be returned.
*/
public int getStartIndex() {
return startIndex;
}
/**
* Sets the index of the first result to return. For example, if the start
* index is set to 20, the Iterator returned will start at the 20th result
* in the query. This method can be used in combination with
* setNumResults(int) to perform pagination of results.
*
* @param startIndex the index of the first result to return.
*/
public void setStartIndex(int startIndex) {
if (startIndex < 0) {
throw new IllegalArgumentException("A start index less than 0 is not valid.");
}
this.startIndex = startIndex;
}
/**
* Returns true if the wildcard matching for participants is for users
* on external servers. Otherwise, wildcard matching will apply to any user
* (external or internal). For example, if a single participant "jsmith" is set
* and external wildcard mode is true, then the search will match any conversation
* between "jsmith" and any external users. If the external wildcard mode is false,
* then the search will match all conversations between "jsmith" and any other users.
*
* @return true if external wildcard mode is enabled.
*/
public boolean isExternalWildcardMode() {
return externalWildcardMode;
}
/**
* Sets whether wildcard matching for participants is for users on external
* servers. Otherwise, wildcard matching will apply to any user (external or
* internal). For example, if a single participant "jsmith" is set and external
* wildcard mode is true, then the search will match any conversation between
* "jsmith" and any external users. If the external wildcard mode is false, then
* the search will match all conversations between "jsmith" and any other users.
*
* @param mode true if external wildcard mode is enabled.
*/
public void setExternalWildcardMode(boolean mode) {
this.externalWildcardMode = mode;
}
/**
* The sort order of search results. The default sort order is descending. Note that
* if if the sort field is {@link SortField#relevance} (for a query string search),
* then the sort order is irrelevant. Relevance searches will always display the
* most relevant results first.
*/
public enum SortOrder {
/**
* Ascending sort (ie 3, 4, 5...).
*/
ascending,
/**
* Descending sort (ie 3, 2, 1...).
*/
descending
}
/**
* The field to sort results on.
*/
public enum SortField {
/**
* Sort results based on relevance. This sort type can only be used when
* searching with a query string. It <b>should</b> be the default sort field when
* doing a query string search.
*/
relevance,
/**
* Sort results based on date.
*/
date
}
}
/**
* $Revision$
* $Date$
*
* Copyright (C) 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.openfire.archive;
import java.io.IOException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.AbstractCollection;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.document.DateTools;
import org.apache.lucene.index.Term;
import org.apache.lucene.queryParser.ParseException;
import org.apache.lucene.queryParser.QueryParser;
import org.apache.lucene.search.BooleanClause;
import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.search.Filter;
import org.apache.lucene.search.Hit;
import org.apache.lucene.search.Hits;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.RangeFilter;
import org.apache.lucene.search.Sort;
import org.apache.lucene.search.TermQuery;
import org.jivesoftware.database.CachedPreparedStatement;
import org.jivesoftware.database.DbConnectionManager;
import org.picocontainer.Startable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xmpp.packet.JID;
/**
* Searches archived conversations. If conversation archiving is not enabled,
* this class does nothing. Searches may or may not include keyword searching. When
* keywords are used, the search is executed against the Lucene index. When keywords
* are not used, the search is database driven (e.g., "get all conversations between
* two users over the past year").
*
* @see ArchiveIndexer
* @author Matt Tucker
*/
public class ArchiveSearcher implements Startable {
private static final Logger Log = LoggerFactory.getLogger(ArchiveSearch.class);
private ConversationManager conversationManager;
private ArchiveIndexer archiveIndexer;
/**
* Constructs a new archive searcher.
*
* @param conversationManager a ConversationManager instance.
* @param archiveIndexer a ArchiveIndexer used to search through the search index.
*/
public ArchiveSearcher(ConversationManager conversationManager, ArchiveIndexer archiveIndexer) {
this.conversationManager = conversationManager;
this.archiveIndexer = archiveIndexer;
}
public void start() {
}
public void stop() {
conversationManager = null;
archiveIndexer = null;
}
/**
* Searches the archive using the specified search. The {@link ArchiveSearch} class
* is used to encapsulate all information about a search.
*
* @param search the search.
* @return a Collection of conversations that match the search query.
*/
public Collection<Conversation> search(ArchiveSearch search) {
// If the search has a query string it will be driven by Lucene. Otherwise
if (search.getQueryString() != null) {
return luceneSearch(search);
}
else {
return databaseSearch(search);
}
}
/**
* Searches the Lucene index for all archived conversations using the specified search.
*
* @param search the search.
* @return the collection of conversations that match the search.
*/
private Collection<Conversation> luceneSearch(ArchiveSearch search) {
try {
IndexSearcher searcher = archiveIndexer.getSearcher();
final StandardAnalyzer analyzer = new StandardAnalyzer();
// Create the query based on the search terms.
Query query = new QueryParser("text", analyzer).parse(search.getQueryString());
// See if the user wants to sort on something other than relevance. If so, we need
// to tell Lucene to do sorting. Default to a null sort so that it has no
// effect if sorting hasn't been selected.
Sort sort = null;
if (search.getSortField() != ArchiveSearch.SortField.relevance) {
if (search.getSortField() == ArchiveSearch.SortField.date) {
sort = new Sort("date", search.getSortOrder() == ArchiveSearch.SortOrder.descending);
}
}
// See if we need to filter on date. Default to a null filter so that it has
// no effect if date filtering hasn't been selected.
Filter filter = null;
if (search.getDateRangeMin() != null || search.getDateRangeMax() != null) {
String min = null;
if (search.getDateRangeMin() != null) {
min = DateTools.dateToString(search.getDateRangeMin(), DateTools.Resolution.DAY);
}
String max = null;
if (search.getDateRangeMax() != null) {
max = DateTools.dateToString(search.getDateRangeMax(), DateTools.Resolution.DAY);
}
// ENT-271: don't include upper or lower bound if these elements are null
filter = new RangeFilter("date", min, max, min != null, max != null );
}
// See if we need to match external conversations. This will only be true
// when less than two conversation participants are specified and external
// wildcard matching is enabled.
Collection<JID> participants = search.getParticipants();
if (search.getParticipants().size() < 2 && search.isExternalWildcardMode()) {
TermQuery externalQuery = new TermQuery(new Term("external", "true"));
// Add this query to the existing query.
BooleanQuery booleanQuery = new BooleanQuery();
booleanQuery.add(query, BooleanClause.Occur.MUST);
booleanQuery.add(externalQuery, BooleanClause.Occur.MUST);
query = booleanQuery;
}
// See if we need to restrict the search to certain users.
if (!participants.isEmpty()) {
if (participants.size() == 1) {
String jid = participants.iterator().next().toBareJID();
Query participantQuery = new QueryParser("jid", analyzer).parse(jid);
// Add this query to the existing query.
BooleanQuery booleanQuery = new BooleanQuery();
booleanQuery.add(query, BooleanClause.Occur.MUST);
booleanQuery.add(participantQuery, BooleanClause.Occur.MUST);
query = booleanQuery;
}
// Otherwise there are two participants.
else {
Iterator<JID> iter = participants.iterator();
String participant1 = iter.next().toBareJID();
String participant2 = iter.next().toBareJID();
BooleanQuery participantQuery = new BooleanQuery();
participantQuery.add(new QueryParser("jid", analyzer).parse(participant1),
BooleanClause.Occur.MUST);
participantQuery.add(new QueryParser("jid", analyzer).parse(participant2),
BooleanClause.Occur.MUST);
// Add this query to the existing query.
BooleanQuery booleanQuery = new BooleanQuery();
booleanQuery.add(query, BooleanClause.Occur.MUST);
booleanQuery.add(participantQuery, BooleanClause.Occur.MUST);
query = booleanQuery;
}
}
Hits hits = searcher.search(query, filter, sort);
int startIndex = search.getStartIndex();
int endIndex = startIndex + search.getNumResults() - 1;
// The end index can't be after the end of the results.
if (endIndex > hits.length() - 1) {
// endIndex = hits.length() - 1;
// TODO: We need to determine if this is necessary.
}
// If the start index is positioned after the end, return an empty list.
if (((endIndex - startIndex) + 1) <= 0) {
return Collections.emptyList();
}
// Otherwise return the results.
else {
return new LuceneQueryResults(hits, startIndex, endIndex);
}
}
catch (ParseException pe) {
Log.error(pe.getMessage(), pe);
return Collections.emptySet();
}
catch (IOException ioe) {
Log.error(ioe.getMessage(), ioe);
return Collections.emptySet();
}
}
/**
* Searches the database for all archived conversations using the specified search.
*
* @param search the search.
* @return the collection of conversations that match the search.
*/
private Collection<Conversation> databaseSearch(ArchiveSearch search) {
CachedPreparedStatement cachedPstmt = new CachedPreparedStatement();
// Build the SQL
StringBuilder query = new StringBuilder(160);
query.append("SELECT DISTINCT ofConversation.conversationID");
Collection<JID> participants = search.getParticipants();
boolean filterParticipants = !participants.isEmpty();
boolean filterDate = search.getDateRangeMin() != null || search.getDateRangeMax() != null;
boolean filterTimestamp = search.getIncludeTimestamp() != null;
boolean filterRoom = search.getRoom() != null;
// SELECT -- need to add value that we sort on. We always sort on date since that's
// the only valid current option for non-keyword searches.
query.append(", ofConversation.startDate");
// FROM -- values (in addition to jiveThread)
query.append(" FROM ofConversation");
if (filterParticipants) {
for (int i=0; i < participants.size(); i++) {
query.append(", ofConParticipant participant").append(i);
}
}
// WHERE BLOCK
boolean whereSet = false;
// See if we need to match against external conversations.
if (search.isExternalWildcardMode() && search.getParticipants().size() != 2) {
query.append(" WHERE isExternal=?");
cachedPstmt.addInt(1);
whereSet = true;
}
// Participants
if (filterParticipants) {
Iterator<JID> iter = participants.iterator();
for (int i=0; i < participants.size(); i++) {
if (!whereSet) {
query.append(" WHERE");
whereSet = true;
}
else {
query.append(" AND");
}
query.append(" ofConversation.conversationID=participant").append(i).append(".conversationID");
query.append(" AND ");
query.append("participant").append(i).append(".bareJID=?");
String partJID = iter.next().toString();
cachedPstmt.addString(partJID);
}
}
// Creation date range
if (filterDate) {
if (search.getDateRangeMin() != null) {
if (!whereSet) {
query.append(" WHERE");
whereSet = true;
}
else {
query.append(" AND");
}
query.append(" ofConversation.startDate >= ?");
cachedPstmt.addLong(search.getDateRangeMin().getTime());
}
if (search.getDateRangeMax() != null) {
if (!whereSet) {
query.append(" WHERE");
whereSet = true;
}
else {
query.append(" AND");
}
query.append(" ofConversation.startDate <= ?");
cachedPstmt.addLong(search.getDateRangeMax().getTime());
}
}
// Check if conversations have to happen at a given point in time
if (filterTimestamp) {
if (!whereSet) {
query.append(" WHERE");
whereSet = true;
}
else {
query.append(" AND");
}
query.append(" ofConversation.startDate <= ?");
cachedPstmt.addLong(search.getIncludeTimestamp().getTime());
query.append(" AND");
query.append(" ofConversation.lastActivity >= ?");
cachedPstmt.addLong(search.getIncludeTimestamp().getTime());
}
// Filter by room
if (filterRoom) {
if (!whereSet) {
query.append(" WHERE");
whereSet = true;
}
else {
query.append(" AND");
}
query.append(" ofConversation.room = ?");
cachedPstmt.addString(search.getRoom().toString());
}
// ORDER BY
query.append(" ORDER BY ofConversation.startDate");
if (search.getSortOrder() == ArchiveSearch.SortOrder.descending) {
query.append(" DESC");
}
else {
query.append(" ASC");
}
int startIndex = search.getStartIndex();
int numResults = search.getNumResults();
if (numResults != ArchiveSearch.NULL_INT) {
// MySQL optimization: use the LIMIT command to tell the database how many
// rows we need returned. The syntax is LIMIT [offset],[rows]
if (DbConnectionManager.getDatabaseType() == DbConnectionManager.DatabaseType.mysql) {
query.append(" LIMIT ").append(startIndex).append(",").append(numResults);
}
// PostgreSQL optimization: use the LIMIT command to tell the database how many
// rows we need returned. The syntax is LIMIT [rows] OFFSET [offset]
else if (DbConnectionManager.getDatabaseType() == DbConnectionManager.DatabaseType.postgresql) {
query.append(" LIMIT ").append(numResults).append(" OFFSET ").append(startIndex);
}
}
// Set the database query string.
cachedPstmt.setSQL(query.toString());
List<Long> conversationIDs = new ArrayList<Long>();
// Get all matching conversations from the database.
Connection con = null;
PreparedStatement pstmt = null;
try {
con = DbConnectionManager.getConnection();
pstmt = DbConnectionManager.createScrollablePreparedStatement(con, cachedPstmt.getSQL());
cachedPstmt.setParams(pstmt);
// Set the maximum number of rows to end at the end of this block.
// A MySQL optimization using the LIMIT command is part of the SQL.
// Therefore, we can skip this call on MySQL.
if (DbConnectionManager.getDatabaseType() != DbConnectionManager.DatabaseType.mysql
&& DbConnectionManager.getDatabaseType() != DbConnectionManager.DatabaseType.postgresql)
{
DbConnectionManager.setMaxRows(pstmt, startIndex+numResults);
}
ResultSet rs = pstmt.executeQuery();
// Position the cursor right before the first row that we're insterested in.
// A MySQL and Postgres optimization using the LIMIT command is part of the SQL.
// Therefore, we can skip this call on MySQL or Postgres.
if (DbConnectionManager.getDatabaseType() != DbConnectionManager.DatabaseType.mysql
&& DbConnectionManager.getDatabaseType() != DbConnectionManager.DatabaseType.postgresql)
{
DbConnectionManager.scrollResultSet(rs, startIndex);
}
// Keep reading results until the result set is exhausted or
// we come to the end of the block.
int count = 0;
while (rs.next() && count < numResults) {
conversationIDs.add(rs.getLong(1));
count++;
}
rs.close();
}
catch (SQLException sqle) {
Log.error(sqle.getMessage(), sqle);
}
finally {
DbConnectionManager.closeConnection(pstmt, con);
}
return new DatabaseQueryResults(conversationIDs);
}
/**
* Returns Hits from a database search against archived conversations as a Collection
* of Conversation objects.
*/
private class DatabaseQueryResults extends AbstractCollection<Conversation> {
private List<Long> conversationIDs;
/**
* Constructs a new query results object.
*
* @param conversationIDs the list of conversation IDs.
*/
public DatabaseQueryResults(List<Long> conversationIDs) {
this.conversationIDs = conversationIDs;
}
@Override
public Iterator<Conversation> iterator() {
final Iterator<Long> convIterator = conversationIDs.iterator();
return new Iterator<Conversation>() {
private Conversation nextElement = null;
public boolean hasNext() {
if (nextElement == null) {
nextElement = getNextElement();
if (nextElement == null) {
return false;
}
}
return true;
}
public Conversation next() {
Conversation element;
if (nextElement != null) {
element = nextElement;
nextElement = null;
}
else {
element = getNextElement();
if (element == null) {
throw new NoSuchElementException();
}
}
return element;
}
public void remove() {
throw new UnsupportedOperationException();
}
private Conversation getNextElement() {
if (!convIterator.hasNext()) {
return null;
}
while (convIterator.hasNext()) {
try {
long conversationID = convIterator.next();
return new Conversation(conversationManager, conversationID);
}
catch (Exception e) {
Log.error(e.getMessage(), e);
}
}
return null;
}
};
}
@Override
public int size() {
return conversationIDs.size();
}
}
/**
* Returns Hits from a Lucene search against archived conversations as a Collection
* of Conversation objects.
*/
private class LuceneQueryResults extends AbstractCollection<Conversation> {
private Hits hits;
private int index;
private int endIndex;
/**
* Constructs a new query results object.
*
* @param hits the search hits.
* @param startIndex the starting index that results should be returned from.
* @param endIndex the ending index that results should be returned to.
*/
public LuceneQueryResults(Hits hits, int startIndex, int endIndex) {
this.hits = hits;
this.index = startIndex;
this.endIndex = endIndex;
}
@Override
public Iterator<Conversation> iterator() {
final Iterator<Hit> hitsIterator = hits.iterator();
// Advance the iterator until we hit the index.
for (int i=0; i<index; i++) {
hitsIterator.next();
}
return new Iterator<Conversation>() {
private Conversation nextElement = null;
public boolean hasNext() {
if (nextElement == null) {
nextElement = getNextElement();
if (nextElement == null) {
return false;
}
}
return true;
}
public Conversation next() {
Conversation element;
if (nextElement != null) {
element = nextElement;
nextElement = null;
}
else {
element = getNextElement();
if (element == null) {
throw new NoSuchElementException();
}
}
return element;
}
public void remove() {
throw new UnsupportedOperationException();
}
private Conversation getNextElement() {
if (!hitsIterator.hasNext()) {
return null;
}
// If we've reached the end index, stop iterating.
else if (index >= endIndex) {
return null;
}
while (hitsIterator.hasNext()) {
try {
Hit hit = hitsIterator.next();
// Advance the index.
index++;
long conversationID = Long.parseLong(hit.get("conversationID"));
return new Conversation(conversationManager, conversationID);
}
catch (Exception e) {
Log.error(e.getMessage(), e);
}
}
return null;
}
};
}
@Override
public int size() {
return hits.length();
}
}
}
/**
* $Revision: 3034 $
* $Date: 2005-11-04 21:02:33 -0300 (Fri, 04 Nov 2005) $
*
* Copyright (C) 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.openfire.archive;
import org.xmpp.packet.JID;
import java.util.Date;
/**
* Represents an archived message.
*
* @author Matt Tucker
*/
public class ArchivedMessage {
private long conversationID;
private JID fromJID;
private JID toJID;
private Date sentDate;
private String body;
private boolean roomEvent;
/**
* Creates a new archived message.
*
* @param conversationID the ID of the conversation that the message is associated with.
* @param fromJID the JID of the user that sent the message.
* @param toJID the JID of the user that the message was sent to.
* @param sentDate the date the message was sent.
* @param body the body of the message
* @param roomEvent true if the message belongs to a room event. Eg. User joined room.
*/
public ArchivedMessage(long conversationID, JID fromJID, JID toJID, Date sentDate, String body, boolean roomEvent) {
this.conversationID = conversationID;
// Convert both JID's to bare JID's so that we don't store resource information.
this.fromJID = fromJID;
this.toJID = toJID;
this.sentDate = sentDate;
this.body = body;
this.roomEvent = roomEvent;
}
/**
* The conversation ID that the message is associated with.
*
* @return the conversation ID.
*/
public long getConversationID() {
return conversationID;
}
/**
* The JID of the user that sent the message.
*
* @return the sender JID.
*/
public JID getFromJID() {
return fromJID;
}
/**
* The JID of the user that received the message.
*
* @return the recipient JID.
*/
public JID getToJID() {
return toJID;
}
/**
* The date the message was sent.
*
* @return the date the message was sent.
*/
public Date getSentDate() {
return sentDate;
}
/**
* The body of the message.
*
* @return the body of the message.
*/
public String getBody() {
return body;
}
/**
* Returns true if the message belongs to a room event. Examples of room events are:
* user joined the room or user left the room.
*
* @return true if the message belongs to a room event.
*/
public boolean isRoomEvent() {
return roomEvent;
}
}
\ No newline at end of file
/**
* $Revision: 3034 $
* $Date: 2005-11-04 21:02:33 -0300 (Fri, 04 Nov 2005) $
*
* Copyright (C) 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.openfire.archive;
import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.jivesoftware.database.DbConnectionManager;
import org.jivesoftware.database.JiveID;
import org.jivesoftware.database.SequenceManager;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.muc.MUCRole;
import org.jivesoftware.openfire.muc.MUCRoom;
import org.jivesoftware.openfire.plugin.MonitoringPlugin;
import org.jivesoftware.openfire.user.UserNameManager;
import org.jivesoftware.openfire.user.UserNotFoundException;
import org.jivesoftware.util.JiveGlobals;
import org.jivesoftware.util.LocaleUtils;
import org.jivesoftware.util.NotFoundException;
import org.jivesoftware.util.cache.ExternalizableUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xmpp.packet.JID;
/**
* Represents an IM conversation between two people. A conversation encompasses a
* series of messages sent back and forth. It may cover a single topic or several.
* The start of a conversation occurs when the first message between two users is
* sent. It ends when either:
* <ul>
* <li>No messages are sent between the users for a certain period of time (default of 10
* minutes). The default value can be overridden by setting the Openfire property
* <tt>conversation.idleTime</tt>.</li>
* <li>The total conversation time reaches a maximum value (default of 60 minutes).
* The default value can be overridden by setting the Openfire property
* <tt>conversation.maxTime</tt>. When the max time has been reached and additional
* messages are sent between the users, a new conversation will simply be
* started.</li>
* </ul>
* <p/>
* Each conversation has a start time, date of the last message, and count of the
* messages in the conversation. Conversations are specially marked if one of the
* participants is on an external server. If archiving is enabled, the actual messages in
* the conversation can be retrieved.
*
* @author Matt Tucker
*/
@JiveID(50)
public class Conversation implements Externalizable {
private static final Logger Log = LoggerFactory.getLogger(Conversation.class);
private static final String INSERT_CONVERSATION =
"INSERT INTO ofConversation(conversationID, room, isExternal, startDate, " +
"lastActivity, messageCount) VALUES (?,?,?,?,?,0)";
private static final String INSERT_PARTICIPANT =
"INSERT INTO ofConParticipant(conversationID, joinedDate, bareJID, jidResource, nickname) " +
"VALUES (?,?,?,?,?)";
private static final String LOAD_CONVERSATION =
"SELECT room, isExternal, startDate, lastActivity, messageCount " +
"FROM ofConversation WHERE conversationID=?";
private static final String LOAD_PARTICIPANTS =
"SELECT bareJID, jidResource, nickname, joinedDate, leftDate FROM ofConParticipant " +
"WHERE conversationID=? ORDER BY joinedDate";
private static final String LOAD_MESSAGES =
"SELECT fromJID, toJID, sentDate, body FROM ofMessageArchive WHERE conversationID=? " +
"ORDER BY sentDate";
private transient ConversationManager conversationManager;
private long conversationID = -1;
private Map<String, UserParticipations> participants;
private boolean external;
private Date startDate;
private Date lastActivity;
private int messageCount;
/**
* Room where the group conversion is taking place. For one-to-one chats
* there is no room so this variable will be null.
*/
private JID room;
/**
* Do not use this constructor. It only exists for serialization purposes.
*/
public Conversation() {
}
/**
* Constructs a new one-to-one conversation.
*
* @param conversationManager the ConversationManager.
* @param users the two participants in the conversation.
* @param external true if the conversation includes a user on another server.
* @param startDate the starting date of the conversation.
*/
public Conversation(ConversationManager conversationManager, Collection<JID> users,
boolean external, Date startDate) {
if (users.size() != 2) {
throw new IllegalArgumentException("Illegal number of participants: " + users.size());
}
this.conversationManager = conversationManager;
this.participants = new HashMap<String, UserParticipations>(2);
// Ensure that we're use the full JID of each participant.
for (JID user : users) {
UserParticipations userParticipations = new UserParticipations(false);
userParticipations.addParticipation(new ConversationParticipation(startDate));
participants.put(user.toString(), userParticipations);
}
this.external = external;
this.startDate = startDate;
this.lastActivity = startDate;
// If archiving is enabled, insert the conversation into the database.
if (conversationManager.isMetadataArchivingEnabled()) {
try {
insertIntoDb();
}
catch (Exception e) {
Log.error(e.getMessage(), e);
}
}
}
/**
* Constructs a new group chat conversation that is taking place in a room.
*
* @param conversationManager the ConversationManager.
* @param room the JID of the room where the conversation is taking place.
* @param external true if the conversation includes a user on another server.
* @param startDate the starting date of the conversation.
*/
public Conversation(ConversationManager conversationManager, JID room, boolean external, Date startDate) {
this.conversationManager = conversationManager;
this.participants = new ConcurrentHashMap<String, UserParticipations>();
// Add list of existing room occupants as participants of this conversation
MUCRoom mucRoom = XMPPServer.getInstance().getMultiUserChatManager().getMultiUserChatService(room).getChatRoom(room.getNode());
if (mucRoom != null) {
for (MUCRole role : mucRoom.getOccupants()) {
UserParticipations userParticipations = new UserParticipations(true);
userParticipations.addParticipation(new ConversationParticipation(startDate, role.getNickname()));
participants.put(role.getUserAddress().toString(), userParticipations);
}
}
this.room = room;
this.external = external;
this.startDate = startDate;
this.lastActivity = startDate;
// If archiving is enabled, insert the conversation into the database.
if (conversationManager.isMetadataArchivingEnabled()) {
try {
insertIntoDb();
}
catch (Exception e) {
Log.error(e.getMessage(), e);
}
}
}
/**
* Loads a conversation from the database.
*
* @param conversationManager the conversation manager.
* @param conversationID the ID of the conversation.
* @throws NotFoundException if the conversation can't be loaded.
*/
public Conversation(ConversationManager conversationManager, long conversationID)
throws NotFoundException {
this.conversationManager = conversationManager;
this.conversationID = conversationID;
loadFromDb();
}
/**
* Returns the unique ID of the conversation. A unique ID is only meaningful when
* conversation archiving is enabled. Therefore, this method returns <tt>-1</tt> if
* archiving is not turned on.
*
* @return the unique ID of the conversation, or <tt>-1</tt> if conversation
* archiving is not enabled.
*/
public long getConversationID() {
return conversationID;
}
/**
* Returns the JID of the room where the group conversation took place. If the conversation
* was a one-to-one chat then a <tt>null</tt> value is returned.
*
* @return the JID of room or null if this was a one-to-one chat.
*/
public JID getRoom() {
return room;
}
/**
* Returns the conversation participants.
*
* @return the two conversation participants. Returned JIDs are full JIDs.
*/
public Collection<JID> getParticipants() {
List<JID> users = new ArrayList<JID>();
for (String key : participants.keySet()) {
users.add(new JID(key));
}
return users;
}
/**
* Returns the participations of the specified user (full JID) in this conversation. Each
* participation will hold the time when the user joined and left the conversation and the
* nickname if the room happened in a room.
*
* @param user the full JID of the user.
* @return the participations of the specified user (full JID) in this conversation.
*/
public Collection<ConversationParticipation> getParticipations(JID user) {
UserParticipations userParticipations = participants.get(user.toString());
if (userParticipations == null) {
return Collections.emptyList();
}
return userParticipations.getParticipations();
}
/**
* Returns true if one of the conversation participants is on an external server.
*
* @return true if one of the conversation participants is on an external server.
*/
public boolean isExternal() {
return external;
}
/**
* Returns the starting timestamp of the conversation.
*
* @return the start date.
*/
public Date getStartDate() {
return startDate;
}
/**
* Returns the timestamp the last message was receieved.
*
* @return the last activity.
*/
public Date getLastActivity() {
return lastActivity;
}
/**
* Returns the number of messages that make up the conversation.
*
* @return the message count.
*/
public int getMessageCount() {
return messageCount;
}
/**
* Returns the archived messages in the conversation. If message archiving is not
* enabled, this method will always return an empty collection. This method will only
* return messages that have already been batch-archived to the database; in other
* words, it does not provide a real-time view of new messages.
*
* @return the archived messages in the conversation.
*/
public List<ArchivedMessage> getMessages() {
if (room == null && !conversationManager.isMessageArchivingEnabled()) {
return Collections.emptyList();
}
else if (room != null && !conversationManager.isRoomArchivingEnabled()) {
return Collections.emptyList();
}
List<ArchivedMessage> messages = new ArrayList<ArchivedMessage>();
Connection con = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement(LOAD_MESSAGES);
pstmt.setLong(1, getConversationID());
rs = pstmt.executeQuery();
while (rs.next()) {
JID fromJID = new JID(rs.getString(1));
JID toJID = new JID(rs.getString(2));
Date date = new Date(rs.getLong(3));
String body = DbConnectionManager.getLargeTextField(rs, 4);
messages.add(new ArchivedMessage(conversationID, fromJID, toJID, date, body, false));
}
}
catch (SQLException sqle) {
Log.error(sqle.getMessage(), sqle);
}
finally {
DbConnectionManager.closeConnection(rs, pstmt, con);
}
// Add messages of users joining or leaving the group chat conversation
if (room != null) {
for (Map.Entry<String, UserParticipations> entry : participants.entrySet()) {
JID user = new JID(entry.getKey());
boolean anonymous = false;
String name;
try {
name = UserNameManager.getUserName(user);
}
catch (UserNotFoundException e) {
name = user.toBareJID();
anonymous = true;
}
for (ConversationParticipation participation : entry.getValue().getParticipations()) {
if (participation.getJoined() == null) {
Log.warn("Found muc participant with no join date in conversation: " + conversationID);
continue;
}
JID jid = new JID(room + "/" + participation.getNickname());
String joinBody;
String leftBody;
if (anonymous) {
joinBody = LocaleUtils.getLocalizedString("muc.conversation.joined.anonymous", "monitoring",
Arrays.asList(participation.getNickname()));
leftBody = LocaleUtils.getLocalizedString("muc.conversation.left.anonymous", "monitoring",
Arrays.asList(participation.getNickname()));
}
else {
joinBody = LocaleUtils.getLocalizedString("muc.conversation.joined", "monitoring",
Arrays.asList(participation.getNickname(), name));
leftBody = LocaleUtils.getLocalizedString("muc.conversation.left", "monitoring",
Arrays.asList(participation.getNickname(), name));
}
messages.add(
new ArchivedMessage(conversationID, user, jid, participation.getJoined(), joinBody, true));
if (participation.getLeft() != null) {
messages.add(new ArchivedMessage(conversationID, user, jid, participation.getLeft(), leftBody,
true));
}
}
}
// Sort messages by sent date
Collections.sort(messages, new Comparator<ArchivedMessage>() {
public int compare(ArchivedMessage o1, ArchivedMessage o2) {
return o1.getSentDate().compareTo(o2.getSentDate());
}
});
}
return messages;
}
@Override
public String toString() {
StringBuilder buf = new StringBuilder();
buf.append("Conversation [").append(conversationID).append("]");
if (room != null) {
buf.append(" in room").append(room);
}
buf.append(" between ").append(participants);
buf.append(". started ").append(JiveGlobals.formatDateTime(startDate));
buf.append(", last active ").append(JiveGlobals.formatDateTime(lastActivity));
buf.append(". Total messages: ").append(messageCount);
return buf.toString();
}
/**
* Called when a new message for the conversation is received. Each time a new
* message is received, the last activity date will be updated and the message
* count incremented.
*
* @param entity JID of the entity that sent the message.
* @param date the date the message was sent.
*/
synchronized void messageReceived(JID entity, Date date) {
lastActivity = date;
messageCount++;
}
synchronized void participantJoined(JID user, String nickname, long timestamp) {
// Add the sender of the message as a participant of this conversation. If the sender
// was already a participant then he/she will appear just once. Rooms are never considered
// as participants
UserParticipations userParticipations = participants.get(user.toString());
if (userParticipations == null) {
userParticipations = new UserParticipations(true);
participants.put(user.toString(), userParticipations);
}
else {
// Get last known participation and check that the user has finished it
ConversationParticipation lastParticipation = userParticipations.getRecentParticipation();
if (lastParticipation != null && lastParticipation.getLeft() == null) {
Log.warn("Found user that never left a previous conversation: " + user);
lastParticipation.participationEnded(new Date(timestamp));
// Queue storeage of updated participation information
conversationManager.queueParticipantLeft(this, user, lastParticipation);
}
}
ConversationParticipation newParticipation = new ConversationParticipation(new Date(timestamp), nickname);
// Add element to the beginning of the list
userParticipations.addParticipation(newParticipation);
// If archiving is enabled, insert the conversation into the database (if not persistent yet).
if (conversationManager.isMetadataArchivingEnabled()) {
try {
if (conversationID == -1) {
// Save new conversation to the database
insertIntoDb();
}
else {
// Store new participation information
insertIntoDb(user, nickname, timestamp);
}
}
catch (Exception e) {
Log.error(e.getMessage(), e);
}
}
}
synchronized void participantLeft(JID user, long timestamp) {
// Get the list of participations of the specified user
UserParticipations userParticipations = participants.get(user.toString());
if (userParticipations == null) {
Log.warn("Found user that left a conversation but never started it: " + user);
}
else {
// Get last known participation and check that the user has not finished it
ConversationParticipation currentParticipation = userParticipations.getRecentParticipation();
if (currentParticipation == null || currentParticipation.getLeft() != null) {
Log.warn("Found user that left a conversation but never started it: " + user);
}
else {
currentParticipation.participationEnded(new Date(timestamp));
// Queue storeage of updated participation information
conversationManager.queueParticipantLeft(this, user, currentParticipation);
}
}
}
/**
* Inserts a new conversation into the database.
*
* @throws SQLException if an error occurs inserting the conversation.
*/
private void insertIntoDb() throws SQLException {
this.conversationID = SequenceManager.nextID(this);
Connection con = null;
boolean abortTransaction = false;
try {
con = DbConnectionManager.getTransactionConnection();
PreparedStatement pstmt = con.prepareStatement(INSERT_CONVERSATION);
pstmt.setLong(1, conversationID);
pstmt.setString(2, room == null ? null : room.toString());
pstmt.setInt(3, (external ? 1 : 0));
pstmt.setLong(4, startDate.getTime());
pstmt.setLong(5, lastActivity.getTime());
pstmt.executeUpdate();
pstmt.close();
pstmt = con.prepareStatement(INSERT_PARTICIPANT);
for (Map.Entry<String, UserParticipations> entry : participants.entrySet()) {
JID user = new JID(entry.getKey());
for (ConversationParticipation participation : entry.getValue().getParticipations()) {
pstmt.setLong(1, conversationID);
pstmt.setLong(2, participation.getJoined().getTime());
pstmt.setString(3, user.toBareJID());
pstmt.setString(4, user.getResource() == null ? " " : user.getResource());
pstmt.setString(5, participation.getNickname());
pstmt.executeUpdate();
}
}
pstmt.close();
}
catch (SQLException sqle) {
abortTransaction = true;
throw sqle;
}
finally {
DbConnectionManager.closeTransactionConnection(con, abortTransaction);
}
}
/**
* Adds a new conversation participant into the database.
*
* @param participant the full JID of the participant.
* @param nickname nickname of the user in the room.
* @param joined timestamp when user joined the conversation.
* @throws SQLException if an error occurs inserting the conversation.
*/
private void insertIntoDb(JID participant, String nickname, long joined) throws SQLException {
Connection con = null;
try {
con = DbConnectionManager.getConnection();
PreparedStatement pstmt = con.prepareStatement(INSERT_PARTICIPANT);
pstmt.setLong(1, conversationID);
pstmt.setLong(2, joined);
pstmt.setString(3, participant.toBareJID());
pstmt.setString(4, participant.getResource());
pstmt.setString(5, nickname);
pstmt.executeUpdate();
pstmt.close();
}
catch (SQLException sqle) {
throw sqle;
}
finally {
DbConnectionManager.closeConnection(con);
}
}
private void loadFromDb() throws NotFoundException {
Connection con = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement(LOAD_CONVERSATION);
pstmt.setLong(1, conversationID);
rs = pstmt.executeQuery();
if (!rs.next()) {
throw new NotFoundException("Conversation not found: " + conversationID);
}
this.room = rs.getString(1) == null ? null : new JID(rs.getString(1));
this.external = rs.getInt(2) == 1;
this.startDate = new Date(rs.getLong(3));
this.lastActivity = new Date(rs.getLong(4));
this.messageCount = rs.getInt(5);
rs.close();
pstmt.close();
this.participants = new ConcurrentHashMap<String, UserParticipations>();
pstmt = con.prepareStatement(LOAD_PARTICIPANTS);
pstmt.setLong(1, conversationID);
rs = pstmt.executeQuery();
while (rs.next()) {
// Rebuild full JID of participant
String baredJID = rs.getString(1);
String resource = rs.getString(2);
JID fullJID = new JID("".equals(resource) ? baredJID : baredJID + "/" + resource);
// Rebuild joined and left time
ConversationParticipation participation =
new ConversationParticipation(new Date(rs.getLong(4)), rs.getString(3));
if (rs.getLong(5) > 0) {
participation.participationEnded(new Date(rs.getLong(5)));
}
// Store participation data
UserParticipations userParticipations = participants.get(fullJID.toString());
if (userParticipations == null) {
userParticipations = new UserParticipations(room != null);
participants.put(fullJID.toString(), userParticipations);
}
userParticipations.addParticipation(participation);
}
}
catch (SQLException sqle) {
Log.error(sqle.getMessage(), sqle);
}
finally {
DbConnectionManager.closeConnection(rs, pstmt, con);
}
}
/**
* Notification message inficating that conversation has finished so remaining participants
* should be marked that they left the conversation.
*
* @param nowDate the date when the conversation was finished
*/
void conversationEnded(Date nowDate) {
for (Map.Entry<String, UserParticipations> entry : participants.entrySet()) {
ConversationParticipation currentParticipation = entry.getValue().getRecentParticipation();
if (currentParticipation.getLeft() == null) {
currentParticipation.participationEnded(nowDate);
// Queue storage of updated participation information
conversationManager.queueParticipantLeft(this, new JID(entry.getKey()), currentParticipation);
}
}
}
public void writeExternal(ObjectOutput out) throws IOException {
ExternalizableUtil.getInstance().writeLong(out, conversationID);
ExternalizableUtil.getInstance().writeExternalizableMap(out, participants);
ExternalizableUtil.getInstance().writeBoolean(out, external);
ExternalizableUtil.getInstance().writeLong(out, startDate.getTime());
ExternalizableUtil.getInstance().writeLong(out, lastActivity.getTime());
ExternalizableUtil.getInstance().writeInt(out, messageCount);
ExternalizableUtil.getInstance().writeBoolean(out, room != null);
if (room != null) {
ExternalizableUtil.getInstance().writeSerializable(out, room);
}
}
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
MonitoringPlugin plugin = (MonitoringPlugin) XMPPServer.getInstance().getPluginManager().getPlugin(
"monitoring");
conversationManager = (ConversationManager) plugin.getModule(ConversationManager.class);
this.participants = new ConcurrentHashMap<String, UserParticipations>();
conversationID = ExternalizableUtil.getInstance().readLong(in);
ExternalizableUtil.getInstance().readExternalizableMap(in, participants, getClass().getClassLoader());
external = ExternalizableUtil.getInstance().readBoolean(in);
startDate = new Date(ExternalizableUtil.getInstance().readLong(in));
lastActivity = new Date(ExternalizableUtil.getInstance().readLong(in));
messageCount = ExternalizableUtil.getInstance().readInt(in);
if (ExternalizableUtil.getInstance().readBoolean(in)) {
room = (JID) ExternalizableUtil.getInstance().readSerializable(in);
}
}
}
\ No newline at end of file
/**
* $Revision: $
* $Date: $
*
* Copyright (C) 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.openfire.archive;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.muc.MUCRoom;
import org.jivesoftware.util.cache.ExternalizableUtil;
import org.xmpp.packet.JID;
import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.util.Date;
/**
* Conversation events are only used when running in a cluster as a way to send to the senior cluster
* member information about a conversation that is taking place in this cluster node.
*
* @author Gaston Dombiak
*/
public class ConversationEvent implements Externalizable {
private Type type;
private Date date;
private String body;
private JID sender;
private JID receiver;
private JID roomJID;
private JID user;
private String nickname;
/**
* Do not use this constructor. It only exists for serialization purposes.
*/
public ConversationEvent() {
}
public void run(ConversationManager conversationManager) {
if (Type.chatMessageReceived == type) {
conversationManager.processMessage(sender, receiver, body, date);
}
else if (Type.roomDestroyed == type) {
conversationManager.roomConversationEnded(roomJID, date);
}
else if (Type.occupantJoined == type) {
conversationManager.joinedGroupConversation(roomJID, user, nickname, date);
}
else if (Type.occupantLeft == type) {
conversationManager.leftGroupConversation(roomJID, user, date);
// If there are no more occupants then consider the group conversarion over
MUCRoom mucRoom = XMPPServer.getInstance().getMultiUserChatManager().getMultiUserChatService(roomJID).getChatRoom(roomJID.getNode());
if (mucRoom != null && mucRoom.getOccupantsCount() == 0) {
conversationManager.roomConversationEnded(roomJID, date);
}
}
else if (Type.nicknameChanged == type) {
conversationManager.leftGroupConversation(roomJID, user, date);
conversationManager.joinedGroupConversation(roomJID, user, nickname, new Date(date.getTime() + 1));
}
else if (Type.roomMessageReceived == type) {
conversationManager.processRoomMessage(roomJID, user, nickname, body, date);
}
}
public void writeExternal(ObjectOutput out) throws IOException {
ExternalizableUtil.getInstance().writeInt(out, type.ordinal());
ExternalizableUtil.getInstance().writeLong(out, date.getTime());
ExternalizableUtil.getInstance().writeBoolean(out, sender != null);
if (sender != null) {
ExternalizableUtil.getInstance().writeSerializable(out, sender);
}
ExternalizableUtil.getInstance().writeBoolean(out, receiver != null);
if (receiver != null) {
ExternalizableUtil.getInstance().writeSerializable(out, receiver);
}
ExternalizableUtil.getInstance().writeBoolean(out, body != null);
if (body != null) {
ExternalizableUtil.getInstance().writeSafeUTF(out, body);
}
ExternalizableUtil.getInstance().writeBoolean(out, roomJID != null);
if (roomJID != null) {
ExternalizableUtil.getInstance().writeSerializable(out, roomJID);
}
ExternalizableUtil.getInstance().writeBoolean(out, user != null);
if (user != null) {
ExternalizableUtil.getInstance().writeSerializable(out, user);
}
ExternalizableUtil.getInstance().writeBoolean(out, nickname != null);
if (nickname != null) {
ExternalizableUtil.getInstance().writeSafeUTF(out, nickname);
}
}
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
type = Type.values()[ExternalizableUtil.getInstance().readInt(in)];
date = new Date(ExternalizableUtil.getInstance().readLong(in));
if (ExternalizableUtil.getInstance().readBoolean(in)) {
sender = (JID) ExternalizableUtil.getInstance().readSerializable(in);
}
if (ExternalizableUtil.getInstance().readBoolean(in)) {
receiver = (JID) ExternalizableUtil.getInstance().readSerializable(in);
}
if (ExternalizableUtil.getInstance().readBoolean(in)) {
body = ExternalizableUtil.getInstance().readSafeUTF(in);
}
if (ExternalizableUtil.getInstance().readBoolean(in)) {
roomJID = (JID) ExternalizableUtil.getInstance().readSerializable(in);
}
if (ExternalizableUtil.getInstance().readBoolean(in)) {
user = (JID) ExternalizableUtil.getInstance().readSerializable(in);
}
if (ExternalizableUtil.getInstance().readBoolean(in)) {
nickname = ExternalizableUtil.getInstance().readSafeUTF(in);
}
}
public static ConversationEvent chatMessageReceived(JID sender, JID receiver, String body, Date date) {
ConversationEvent event = new ConversationEvent();
event.type = Type.chatMessageReceived;
event.sender = sender;
event.receiver = receiver;
event.body = body;
event.date = date;
return event;
}
public static ConversationEvent roomDestroyed(JID roomJID, Date date) {
ConversationEvent event = new ConversationEvent();
event.type = Type.roomDestroyed;
event.roomJID = roomJID;
event.date = date;
return event;
}
public static ConversationEvent occupantJoined(JID roomJID, JID user, String nickname, Date date) {
ConversationEvent event = new ConversationEvent();
event.type = Type.occupantJoined;
event.roomJID = roomJID;
event.user = user;
event.nickname = nickname;
event.date = date;
return event;
}
public static ConversationEvent occupantLeft(JID roomJID, JID user, Date date) {
ConversationEvent event = new ConversationEvent();
event.type = Type.occupantLeft;
event.roomJID = roomJID;
event.user = user;
event.date = date;
return event;
}
public static ConversationEvent nicknameChanged(JID roomJID, JID user, String newNickname, Date date) {
ConversationEvent event = new ConversationEvent();
event.type = Type.nicknameChanged;
event.roomJID = roomJID;
event.user = user;
event.nickname = newNickname;
event.date = date;
return event;
}
public static ConversationEvent roomMessageReceived(JID roomJID, JID user, String nickname, String body,
Date date) {
ConversationEvent event = new ConversationEvent();
event.type = Type.roomMessageReceived;
event.roomJID = roomJID;
event.user = user;
event.nickname = nickname;
event.body = body;
event.date = date;
return event;
}
private static enum Type {
/**
* Event triggered when a room was destroyed.
*/
roomDestroyed,
/**
* Event triggered when a new occupant joins a room.
*/
occupantJoined,
/**
* Event triggered when an occupant left a room.
*/
occupantLeft,
/**
* Event triggered when an occupant changed his nickname in a room.
*/
nicknameChanged,
/**
* Event triggered when a room occupant sent a message to a room.
*/
roomMessageReceived,
/**
* Event triggered when a user sent a message to another user.
*/
chatMessageReceived
}
}
/**
* $Revision: $
* $Date: $
*
* Copyright (C) 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.openfire.archive;
import org.jivesoftware.openfire.archive.cluster.SendConversationEventsTask;
import org.jivesoftware.openfire.reporting.util.TaskEngine;
import org.jivesoftware.openfire.cluster.ClusterManager;
import org.jivesoftware.util.JiveConstants;
import org.jivesoftware.util.cache.CacheFactory;
import java.util.*;
/**
* Queue conversation events generated by this JVM and send them to the senior cluster
* member every 3 seconds. This is an optimization to reduce traffic between the cluster
* nodes specialy when under heavy conversations load.
*
* @author Gaston Dombiak
*/
public class ConversationEventsQueue {
private ConversationManager conversationManager;
/**
* Chat events that are pending to be sent to the senior cluster member.
* Key: Conversation Key; Value: List of conversation events.
*/
private final Map<String, List<ConversationEvent>> chatEvents = new HashMap<String, List<ConversationEvent>>();
/**
* Group chat events that are pending to be sent to the senior cluster member.
* Key: Conversation Key; Value: List of conversation events.
*/
private final Map<String, List<ConversationEvent>> roomEvents = new HashMap<String, List<ConversationEvent>>();
public ConversationEventsQueue(ConversationManager conversationManager, TaskEngine taskEngine) {
this.conversationManager = conversationManager;
// Schedule a task to do conversation archiving.
TimerTask sendTask = new TimerTask() {
@Override
public void run() {
// Move queued events to a temp place
List<ConversationEvent> eventsToSend = new ArrayList<ConversationEvent>();
synchronized (chatEvents) {
for (List<ConversationEvent> list : chatEvents.values()) {
// Just send the first and last event if we are not archiving messages
if (!ConversationEventsQueue.this.conversationManager.isMessageArchivingEnabled() &&
list.size() > 2) {
eventsToSend.add(list.get(0));
eventsToSend.add(list.get(list.size() - 1));
}
else {
// Send all events
eventsToSend.addAll(list);
}
}
// We can empty the queue now
chatEvents.clear();
}
synchronized (roomEvents) {
for (List<ConversationEvent> list : roomEvents.values()) {
eventsToSend.addAll(list);
}
// We can empty the queue now
roomEvents.clear();
}
// Send the queued events (from the temp place) to the senior cluster member
CacheFactory.doClusterTask(new SendConversationEventsTask(eventsToSend),
ClusterManager.getSeniorClusterMember().toByteArray());
}
};
taskEngine.scheduleAtFixedRate(sendTask, JiveConstants.SECOND * 3, JiveConstants.SECOND * 3);
}
/**
* Queues the one-to-one chat event to be later sent to the senior cluster member.
*
* @param conversationKey unique key that identifies the conversation.
* @param event conversation event.
*/
public void addChatEvent(String conversationKey, ConversationEvent event) {
synchronized (chatEvents) {
List<ConversationEvent> events = chatEvents.get(conversationKey);
if (events == null) {
events = new ArrayList<ConversationEvent>();
chatEvents.put(conversationKey, events);
}
events.add(event);
}
}
/**
* Queues the group chat event to be later sent to the senior cluster member.
*
* @param conversationKey unique key that identifies the conversation.
* @param event conversation event.
*/
public void addGroupChatEvent(String conversationKey, ConversationEvent event) {
synchronized (roomEvents) {
List<ConversationEvent> events = roomEvents.get(conversationKey);
if (events == null) {
events = new ArrayList<ConversationEvent>();
roomEvents.put(conversationKey, events);
}
events.add(event);
}
}
}
/**
* $Revision$
* $Date$
*
* Copyright (C) 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.openfire.archive;
import org.jivesoftware.util.StringUtils;
/**
*
*/
public class ConversationInfo {
private long conversationID;
private String participant1;
private String participant2;
/**
* For group converstion we need to send a string array with the occupants' JIDs.
*/
private String[] allParticipants;
private String date;
private String lastActivity;
private String body;
private int messageCount;
private long duration;
public long getConversationID() {
return conversationID;
}
public void setConversationID(long conversationID) {
this.conversationID = conversationID;
}
public String getParticipant1() {
return participant1;
}
public void setParticipant1(String participant1) {
this.participant1 = participant1;
}
public String getParticipant2() {
return participant2;
}
public void setParticipant2(String participant2) {
this.participant2 = participant2;
}
public String[] getAllParticipants() {
return allParticipants;
}
public void setAllParticipants(String[] allParticipants) {
this.allParticipants = allParticipants;
}
public String getDate() {
return date;
}
public void setDate(String date) {
this.date = date;
}
public String getBody() {
return body;
}
public void setBody(String body) {
this.body = body;
}
public int getMessageCount() {
return messageCount;
}
public void setMessageCount(int messageCount) {
this.messageCount = messageCount;
}
public String getDuration() {
return StringUtils.getTimeFromLong(duration);
}
public void setDuration(long duration) {
this.duration = duration;
}
public String getLastActivity() {
return lastActivity;
}
public void setLastActivity(String lastActivity) {
this.lastActivity = lastActivity;
}
}
/**
* $Revision$
* $Date$
*
* Copyright (C) 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.openfire.archive;
import java.util.Date;
/**
* Listens for conversations being created, finished, and updated. Note that listeners
* are notified using application threads so any long running processing tasks that result
* from notifications should be scheduled for separate threads.
*
* @see ConversationManager#addConversationListener(ConversationListener)
* @author Matt Tucker
*/
public interface ConversationListener {
/**
* A conversation was created.
*
* @param conversation the conversation.
*/
public void conversationCreated(Conversation conversation);
/**
* A conversation was updated, which means that a new message was sent between
* the participants.
*
* @param conversation the conversation.
* @param date the date the conversation was updated.
*/
public void conversationUpdated(Conversation conversation, Date date);
/**
* A conversation ended due to inactivity or because the maximum conversation time
* was hit.
*
* @param conversation the conversation.
*/
public void conversationEnded(Conversation conversation);
}
\ No newline at end of file
/**
* $Revision: $
* $Date: $
*
* Copyright (C) 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.openfire.archive;
import org.dom4j.Element;
import org.jivesoftware.database.DbConnectionManager;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.XMPPServerInfo;
import org.jivesoftware.openfire.archive.cluster.GetConversationCountTask;
import org.jivesoftware.openfire.archive.cluster.GetConversationTask;
import org.jivesoftware.openfire.archive.cluster.GetConversationsTask;
import org.jivesoftware.openfire.cluster.ClusterManager;
import org.jivesoftware.openfire.component.ComponentEventListener;
import org.jivesoftware.openfire.component.InternalComponentManager;
import org.jivesoftware.openfire.reporting.util.TaskEngine;
import org.jivesoftware.openfire.stats.Statistic;
import org.jivesoftware.openfire.stats.StatisticsManager;
import org.jivesoftware.util.*;
import org.jivesoftware.util.cache.CacheFactory;
import org.picocontainer.Startable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xmpp.packet.IQ;
import org.xmpp.packet.JID;
import org.xmpp.packet.Message;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CopyOnWriteArraySet;
/**
* Manages all conversations in the system. Optionally, conversations (messages plus
* meta-data) can be archived to the database. Archiving of conversation data is
* enabled by default, but can be disabled by setting "conversation.metadataArchiving" to
* <tt>false</tt>. Archiving of messages in a conversation is disabled by default, but
* can be enabled by setting "conversation.messageArchiving" to <tt>true</tt>.<p>
*
* When running in a cluster only the senior cluster member will keep track of the active
* conversations. Other cluster nodes will forward conversation events that occurred in the
* local node to the senior cluster member. If the senior cluster member goes down then
* current conversations will be terminated and if users keep sending messages between them
* then new conversations will be created.
*
* @author Matt Tucker
*/
public class ConversationManager implements Startable, ComponentEventListener {
private static final Logger Log = LoggerFactory.getLogger(ConversationManager.class);
private static final String UPDATE_CONVERSATION =
"UPDATE ofConversation SET lastActivity=?, messageCount=? WHERE conversationID=?";
private static final String UPDATE_PARTICIPANT =
"UPDATE ofConParticipant SET leftDate=? WHERE conversationID=? AND bareJID=? AND jidResource=? AND joinedDate=?";
private static final String INSERT_MESSAGE =
"INSERT INTO ofMessageArchive(conversationID, fromJID, toJID, sentDate, body) " +
"VALUES (?,?,?,?,?)";
private static final String CONVERSATION_COUNT =
"SELECT COUNT(*) FROM ofConversation";
private static final String MESSAGE_COUNT =
"SELECT COUNT(*) FROM ofMessageArchive";
private static final int DEFAULT_IDLE_TIME = 10;
private static final int DEFAULT_MAX_TIME = 60;
public static final String CONVERSATIONS_KEY = "conversations";
private ConversationEventsQueue conversationEventsQueue;
private TaskEngine taskEngine;
private Map<String, Conversation> conversations = new ConcurrentHashMap<String, Conversation>();
private boolean metadataArchivingEnabled;
/**
* Flag that indicates if messages of one-to-one chats should be archived.
*/
private boolean messageArchivingEnabled;
/**
* Flag that indicates if messages of group chats (in MUC rooms) should be archived.
*/
private boolean roomArchivingEnabled;
/**
* List of room names to archive. When list is empty then all rooms are archived (if
* roomArchivingEnabled is enabled).
*/
private Collection<String> roomsArchived;
private long idleTime;
private long maxTime;
private PropertyEventListener propertyListener;
private Queue<Conversation> conversationQueue;
private Queue<ArchivedMessage> messageQueue;
/**
* Queue of participants that joined or left a conversation. This queue is processed by the
* ArchivingTask.
*/
private Queue<RoomParticipant> participantQueue;
private boolean archivingRunning = false;
private TimerTask archiveTask;
private TimerTask cleanupTask;
private Collection<ConversationListener> conversationListeners;
/**
* Keeps the address of those components that provide the gateway service.
*/
private List<String> gateways;
private XMPPServerInfo serverInfo;
public ConversationManager(TaskEngine taskEngine) {
this.taskEngine = taskEngine;
this.gateways = new CopyOnWriteArrayList<String>();
this.serverInfo = XMPPServer.getInstance().getServerInfo();
this.conversationEventsQueue = new ConversationEventsQueue(this, taskEngine);
}
public void start() {
metadataArchivingEnabled = JiveGlobals.getBooleanProperty("conversation.metadataArchiving", true);
messageArchivingEnabled = JiveGlobals.getBooleanProperty("conversation.messageArchiving", false);
if (messageArchivingEnabled && !metadataArchivingEnabled) {
Log.warn("Metadata archiving must be enabled when message archiving is enabled. Overriding setting.");
metadataArchivingEnabled = true;
}
roomArchivingEnabled = JiveGlobals.getBooleanProperty("conversation.roomArchiving", false);
roomsArchived = StringUtils.stringToCollection(JiveGlobals.getProperty("conversation.roomsArchived", ""));
if (roomArchivingEnabled && !metadataArchivingEnabled) {
Log.warn("Metadata archiving must be enabled when room archiving is enabled. Overriding setting.");
metadataArchivingEnabled = true;
}
idleTime = JiveGlobals.getIntProperty("conversation.idleTime", DEFAULT_IDLE_TIME) *
JiveConstants.MINUTE;
maxTime = JiveGlobals.getIntProperty("conversation.maxTime",
DEFAULT_MAX_TIME) * JiveConstants.MINUTE;
// Listen for any changes to the conversation properties.
propertyListener = new ConversationPropertyListener();
PropertyEventDispatcher.addListener(propertyListener);
conversationQueue = new ConcurrentLinkedQueue<Conversation>();
messageQueue = new ConcurrentLinkedQueue<ArchivedMessage>();
participantQueue = new ConcurrentLinkedQueue<RoomParticipant>();
conversationListeners = new CopyOnWriteArraySet<ConversationListener>();
// Schedule a task to do conversation archiving.
archiveTask = new TimerTask() {
@Override
public void run() {
new ArchivingTask().run();
}
};
taskEngine.scheduleAtFixedRate(archiveTask, JiveConstants.MINUTE, JiveConstants.MINUTE);
// Schedule a task to do conversation cleanup.
cleanupTask = new TimerTask() {
@Override
public void run() {
for (String key : conversations.keySet()) {
Conversation conversation = conversations.get(key);
long now = System.currentTimeMillis();
if ((now - conversation.getLastActivity().getTime() > idleTime) ||
(now - conversation.getStartDate().getTime() > maxTime)) {
removeConversation(key, conversation, new Date(now));
}
}
}
};
taskEngine.scheduleAtFixedRate(cleanupTask, JiveConstants.MINUTE * 5, JiveConstants.MINUTE * 5);
// Register a statistic.
Statistic conversationStat = new Statistic() {
public String getName() {
return LocaleUtils.getLocalizedString("stat.conversation.name", "monitoring");
}
public Type getStatType() {
return Type.count;
}
public String getDescription() {
return LocaleUtils.getLocalizedString("stat.conversation.desc", "monitoring");
}
public String getUnits() {
return LocaleUtils.getLocalizedString("stat.conversation.units", "monitoring");
}
public double sample() {
return getConversationCount();
}
public boolean isPartialSample() {
return false;
}
};
StatisticsManager.getInstance().addStatistic(CONVERSATIONS_KEY, conversationStat);
InternalComponentManager.getInstance().addListener(this);
}
public void stop() {
archiveTask.cancel();
archiveTask = null;
cleanupTask.cancel();
cleanupTask = null;
// Remove the statistics.
StatisticsManager.getInstance().removeStatistic(CONVERSATIONS_KEY);
PropertyEventDispatcher.removeListener(propertyListener);
propertyListener = null;
conversations.clear();
conversations = null;
// Archive anything remaining in the queue before quitting.
new ArchivingTask().run();
conversationQueue.clear();
conversationQueue = null;
messageQueue.clear();
messageQueue = null;
conversationListeners.clear();
conversationListeners = null;
serverInfo = null;
InternalComponentManager.getInstance().removeListener(this);
}
/**
* Returns true if metadata archiving is enabled. Conversation meta-data includes
* the participants, start date, last activity, and the count of messages sent.
* When archiving is enabled, all meta-data is written to the database.
*
* @return true if metadata archiving is enabled.
*/
public boolean isMetadataArchivingEnabled() {
return metadataArchivingEnabled;
}
/**
* Sets whether metadata archiving is enabled. Conversation meta-data includes
* the participants, start date, last activity, and the count of messages sent.
* When archiving is enabled, all meta-data is written to the database.
*
* @param enabled true if archiving should be enabled.
*/
public void setMetadataArchivingEnabled(boolean enabled) {
this.metadataArchivingEnabled = enabled;
JiveGlobals.setProperty("conversation.metadataArchiving", Boolean.toString(enabled));
}
/**
* Returns true if one-to-one chats or group chats messages are being archived.
*
* @return true if one-to-one chats or group chats messages are being archived.
*/
public boolean isArchivingEnabled() {
return isMessageArchivingEnabled() || isRoomArchivingEnabled();
}
/**
* Returns true if message archiving is enabled for one-to-one chats. When enabled, all messages
* in one-to-one conversations are stored in the database. Note: it's not possible for
* meta-data archiving to be disabled when message archiving is enabled; enabling
* message archiving automatically enables meta-data archiving.
*
* @return true if message archiving is enabled.
*/
public boolean isMessageArchivingEnabled() {
return messageArchivingEnabled;
}
/**
* Sets whether message archiving is enabled. When enabled, all messages
* in conversations are stored in the database. Note: it's not possible for
* meta-data archiving to be disabled when message archiving is enabled; enabling
* message archiving automatically enables meta-data archiving.
*
* @param enabled true if message should be enabled.
*/
public void setMessageArchivingEnabled(boolean enabled) {
this.messageArchivingEnabled = enabled;
JiveGlobals.setProperty("conversation.messageArchiving", Boolean.toString(enabled));
// Force metadata archiving enabled.
if (enabled) {
this.metadataArchivingEnabled = true;
}
}
/**
* Returns true if message archiving is enabled for group chats. When enabled, all messages
* in group conversations are stored in the database unless a list of rooms was specified
* in {@link #getRoomsArchived()} . Note: it's not possible for meta-data archiving to be
* disabled when room archiving is enabled; enabling room archiving automatically
* enables meta-data archiving.
*
* @return true if room archiving is enabled.
*/
public boolean isRoomArchivingEnabled() {
return roomArchivingEnabled;
}
/**
* Sets whether message archiving is enabled for group chats. When enabled, all messages
* in group conversations are stored in the database unless a list of rooms was specified
* in {@link #getRoomsArchived()} . Note: it's not possible for meta-data archiving to be
* disabled when room archiving is enabled; enabling room archiving automatically
* enables meta-data archiving.
*
* @param enabled if room archiving is enabled.
*/
public void setRoomArchivingEnabled(boolean enabled) {
this.roomArchivingEnabled = enabled;
JiveGlobals.setProperty("conversation.roomArchiving", Boolean.toString(enabled));
// Force metadata archiving enabled.
if (enabled) {
this.metadataArchivingEnabled = true;
}
}
/**
* Returns list of room names whose messages will be archived. When room archiving is enabled and
* this list is empty then messages of all local rooms will be archived. However, when name of
* rooms are defined in this list then only messages of those rooms will be archived.
*
* @return list of local room names whose messages will be archived.
*/
public Collection<String> getRoomsArchived() {
return roomsArchived;
}
/**
* Sets list of room names whose messages will be archived. When room archiving is enabled and
* this list is empty then messages of all local rooms will be archived. However, when name of
* rooms are defined in this list then only messages of those rooms will be archived.
*
* @param roomsArchived list of local room names whose messages will be archived.
*/
public void setRoomsArchived(Collection<String> roomsArchived) {
this.roomsArchived = roomsArchived;
JiveGlobals.setProperty("conversation.roomsArchived", StringUtils.collectionToString(roomsArchived));
}
/**
* Returns the number of minutes a conversation can be idle before it's ended.
*
* @return the conversation idle time.
*/
public int getIdleTime() {
return (int)(idleTime / JiveConstants.MINUTE);
}
/**
* Sets the number of minutes a conversation can be idle before it's ended.
*
* @param idleTime the max number of minutes a conversation can be idle before it's ended.
* @throws IllegalArgumentException if idleTime is less than 1.
*/
public void setIdleTime(int idleTime) {
if (idleTime < 1) {
throw new IllegalArgumentException("Idle time less than 1 is not valid: " + idleTime);
}
JiveGlobals.setProperty("conversation.idleTime", Integer.toString(idleTime));
this.idleTime = idleTime * JiveConstants.MINUTE;
}
/**
* Returns the maximum number of minutes a conversation can last before it's ended.
* Any additional messages between the participants in the chat will be associated
* with a new conversation.
*
* @return the maximum number of minutes a conversation can last.
*/
public int getMaxTime() {
return (int)(maxTime / JiveConstants.MINUTE);
}
/**
* Sets the maximum number of minutes a conversation can last before it's ended.
* Any additional messages between the participants in the chat will be associated
* with a new conversation.
*
* @param maxTime the maximum number of minutes a conversation can last.
* @throws IllegalArgumentException if maxTime is less than 1.
*/
public void setMaxTime(int maxTime) {
if (maxTime < 1) {
throw new IllegalArgumentException("Max time less than 1 is not valid: " + maxTime);
}
JiveGlobals.setProperty("conversation.maxTime", Integer.toString(maxTime));
this.maxTime = maxTime * JiveConstants.MINUTE;
}
public ConversationEventsQueue getConversationEventsQueue() {
return conversationEventsQueue;
}
/**
* Returns the count of active conversations.
*
* @return the count of active conversations.
*/
public int getConversationCount() {
if (ClusterManager.isSeniorClusterMember()) {
return conversations.size();
}
return (Integer) CacheFactory.doSynchronousClusterTask(new GetConversationCountTask(),
ClusterManager.getSeniorClusterMember().toByteArray());
}
/**
* Returns a conversation by ID.
*
* @param conversationID the ID of the conversation.
* @return the conversation.
* @throws NotFoundException if the conversation could not be found.
*/
public Conversation getConversation(long conversationID) throws NotFoundException {
if (ClusterManager.isSeniorClusterMember()) {
// Search through the currently active conversations.
for (Conversation conversation : conversations.values()) {
if (conversation.getConversationID() == conversationID) {
return conversation;
}
}
// Otherwise, it might be an archived conversation, so attempt to load it.
return new Conversation(this, conversationID);
}
else {
// Get this info from the senior cluster member when running in a cluster
Conversation conversation = (Conversation) CacheFactory.doSynchronousClusterTask(
new GetConversationTask(conversationID), ClusterManager.getSeniorClusterMember().toByteArray());
if (conversation == null) {
throw new NotFoundException("Conversation not found: " + conversationID);
}
return conversation;
}
}
/**
* Returns the set of active conversations.
*
* @return the active conversations.
*/
public Collection<Conversation> getConversations() {
if (ClusterManager.isSeniorClusterMember()) {
List<Conversation> conversationList = new ArrayList<Conversation>(conversations.values());
// Sort the conversations by creation date.
Collections.sort(conversationList, new Comparator<Conversation>() {
public int compare(Conversation c1, Conversation c2) {
return c1.getStartDate().compareTo(c2.getStartDate());
}
});
return conversationList;
}
else {
// Get this info from the senior cluster member when running in a cluster
return (Collection<Conversation>) CacheFactory.doSynchronousClusterTask(new GetConversationsTask(),
ClusterManager.getSeniorClusterMember().toByteArray());
}
}
/**
* Returns the total number of conversations that have been archived to the database.
* The archived conversation may only be the meta-data, or it might include messages
* as well if message archiving is turned on.
*
* @return the total number of archived conversations.
*/
public int getArchivedConversationCount() {
int conversationCount = 0;
Connection con = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement(CONVERSATION_COUNT);
rs = pstmt.executeQuery();
if (rs.next()) {
conversationCount = rs.getInt(1);
}
}
catch (SQLException sqle) {
Log.error(sqle.getMessage(), sqle);
}
finally {
DbConnectionManager.closeConnection(rs, pstmt, con);
}
return conversationCount;
}
/**
* Returns the total number of messages that have been archived to the database.
*
* @return the total number of archived messages.
*/
public int getArchivedMessageCount() {
int messageCount = 0;
Connection con = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement(MESSAGE_COUNT);
rs = pstmt.executeQuery();
if (rs.next()) {
messageCount = rs.getInt(1);
}
}
catch (SQLException sqle) {
Log.error(sqle.getMessage(), sqle);
}
finally {
DbConnectionManager.closeConnection(rs, pstmt, con);
}
return messageCount;
}
/**
* Adds a conversation listener, which will be notified of newly created conversations,
* conversations ending, and updates to conversations.
*
* @param listener the conversation listener.
*/
public void addConversationListener(ConversationListener listener) {
conversationListeners.add(listener);
}
/**
* Removes a conversation listener.
*
* @param listener the conversation listener.
*/
public void removeConversationListener(ConversationListener listener) {
conversationListeners.remove(listener);
}
/**
* Processes an incoming message of a one-to-one chat. The message will mapped to a
* conversation and then queued for storage if archiving is turned on.
*
* @param sender sender of the message.
* @param receiver receiver of the message.
* @param body body of the message.
* @param date date when the message was sent.
*/
void processMessage(JID sender, JID receiver, String body, Date date) {
String conversationKey = getConversationKey(sender, receiver);
synchronized (conversationKey.intern()) {
Conversation conversation = conversations.get(conversationKey);
// Create a new conversation if necessary.
if (conversation == null) {
Collection<JID> participants = new ArrayList<JID>(2);
participants.add(sender);
participants.add(receiver);
XMPPServer server = XMPPServer.getInstance();
// Check to see if this is an external conversation; i.e. one of the participants
// is on a different server. We can use XOR since we know that both JID's can't
// be external.
boolean external = isExternal(server, sender) ^ isExternal(server, receiver);
// Make sure that the user joined the conversation before a message was received
Date start = new Date(date.getTime() - 1);
conversation = new Conversation(this, participants, external, start);
conversations.put(conversationKey, conversation);
// Notify listeners of the newly created conversation.
for (ConversationListener listener : conversationListeners) {
listener.conversationCreated(conversation);
}
}
// Check to see if the current conversation exceeds either the max idle time
// or max conversation time.
else if ((date.getTime() - conversation.getLastActivity().getTime() > idleTime) ||
(date.getTime() - conversation.getStartDate().getTime() > maxTime)) {
removeConversation(conversationKey, conversation, conversation.getLastActivity());
Collection<JID> participants = new ArrayList<JID>(2);
participants.add(sender);
participants.add(receiver);
XMPPServer server = XMPPServer.getInstance();
// Check to see if this is an external conversation; i.e. one of the participants
// is on a different server. We can use XOR since we know that both JID's can't
// be external.
boolean external = isExternal(server, sender) ^ isExternal(server, receiver);
// Make sure that the user joined the conversation before a message was received
Date start = new Date(date.getTime() - 1);
conversation = new Conversation(this, participants, external, start);
conversations.put(conversationKey, conversation);
// Notify listeners of the newly created conversation.
for (ConversationListener listener : conversationListeners) {
listener.conversationCreated(conversation);
}
}
// Record the newly received message.
conversation.messageReceived(sender, date);
if (metadataArchivingEnabled) {
conversationQueue.add(conversation);
}
if (messageArchivingEnabled) {
messageQueue
.add(new ArchivedMessage(conversation.getConversationID(), sender, receiver, date, body, false));
}
// Notify listeners of the conversation update.
for (ConversationListener listener : conversationListeners) {
listener.conversationUpdated(conversation, date);
}
}
}
/**
* Processes an incoming message sent to a room. The message will mapped to a conversation and then
* queued for storage if archiving is turned on.
*
* @param roomJID the JID of the room where the group conversation is taking place.
* @param sender the JID of the entity that sent the message.
* @param nickname nickname of the user in the room when the message was sent.
* @param body the message sent to the room.
* @param date date when the message was sent.
*/
void processRoomMessage(JID roomJID, JID sender, String nickname, String body, Date date) {
String conversationKey = getRoomConversationKey(roomJID);
synchronized (conversationKey.intern()) {
Conversation conversation = conversations.get(conversationKey);
// Create a new conversation if necessary.
if (conversation == null) {
// Make sure that the user joined the conversation before a message was received
Date start = new Date(date.getTime() - 1);
conversation = new Conversation(this, roomJID, false, start);
conversations.put(conversationKey, conversation);
// Notify listeners of the newly created conversation.
for (ConversationListener listener : conversationListeners) {
listener.conversationCreated(conversation);
}
}
// Check to see if the current conversation exceeds either the max idle time
// or max conversation time.
else if ((date.getTime() - conversation.getLastActivity().getTime() > idleTime) ||
(date.getTime() - conversation.getStartDate().getTime() > maxTime)) {
removeConversation(conversationKey, conversation, conversation.getLastActivity());
// Make sure that the user joined the conversation before a message was received
Date start = new Date(date.getTime() - 1);
conversation = new Conversation(this, roomJID, false, start);
conversations.put(conversationKey, conversation);
// Notify listeners of the newly created conversation.
for (ConversationListener listener : conversationListeners) {
listener.conversationCreated(conversation);
}
}
// Record the newly received message.
conversation.messageReceived(sender, date);
if (metadataArchivingEnabled) {
conversationQueue.add(conversation);
}
if (roomArchivingEnabled && (roomsArchived.isEmpty() || roomsArchived.contains(roomJID.getNode()))) {
JID jid = new JID(roomJID + "/" + nickname);
messageQueue.add(new ArchivedMessage(conversation.getConversationID(), sender, jid, date, body, false));
}
// Notify listeners of the conversation update.
for (ConversationListener listener : conversationListeners) {
listener.conversationUpdated(conversation, date);
}
}
}
/**
* Notification message indicating that a user joined a groupchat conversation. If
* no groupchat conversation was taking place in the specified room then ignore this
* event.<p>
* <p/>
* Eventually, when a new conversation will start in the room and if this user is
* still in the room then the new conversation will detect this user and mark like
* if the user joined the converstion from the beginning.
*
* @param room the room where the user joined.
* @param user the user that joined the room.
* @param nickname nickname of the user in the room.
* @param date date when the user joined the group coversation.
*/
void joinedGroupConversation(JID room, JID user, String nickname, Date date) {
Conversation conversation = getRoomConversation(room);
if (conversation != null) {
conversation.participantJoined(user, nickname, date.getTime());
}
}
/**
* Notification message indicating that a user left a groupchat conversation. If
* no groupchat conversation was taking place in the specified room then ignore this
* event.
*
* @param room the room where the user left.
* @param user the user that left the room.
* @param date date when the user left the group coversation.
*/
void leftGroupConversation(JID room, JID user, Date date) {
Conversation conversation = getRoomConversation(room);
if (conversation != null) {
conversation.participantLeft(user, date.getTime());
}
}
void roomConversationEnded(JID room, Date date) {
Conversation conversation = getRoomConversation(room);
if (conversation != null) {
removeConversation(room.toString(), conversation, date);
}
}
private void removeConversation(String key, Conversation conversation, Date date) {
conversations.remove(key);
// Notify conversation that it has ended
conversation.conversationEnded(date);
// Notify listeners of the conversation ending.
for (ConversationListener listener : conversationListeners) {
listener.conversationEnded(conversation);
}
}
/**
* Returns the group conversation taking place in the specified room or <tt>null</tt> if none.
*
* @param room JID of the room.
* @return the group conversation taking place in the specified room or null if none.
*/
private Conversation getRoomConversation(JID room) {
String conversationKey = room.toString();
return conversations.get(conversationKey);
}
private boolean isExternal(XMPPServer server, JID jid) {
return !server.isLocal(jid) || gateways.contains(jid.getDomain());
}
/**
* Returns true if the specified message should be processed by the conversation manager.
* Only messages between two users, group chats, or gateways are processed.
*
* @param message the message to analyze.
* @return true if the specified message should be processed by the conversation manager.
*/
boolean isConversation(Message message) {
if (Message.Type.normal == message.getType() || Message.Type.chat == message.getType()) {
// TODO: how should conversations with components on other servers be handled?
return isConversationJID(message.getFrom()) && isConversationJID(message.getTo());
}
return false;
}
/**
* Returns true if the specified JID should be recorded in a conversation.
*
* @param jid the JID.
* @return true if the JID should be recorded in a conversation.
*/
private boolean isConversationJID(JID jid) {
// Ignore conversations when there is no jid
if (jid == null) {
return false;
}
XMPPServer server = XMPPServer.getInstance();
if (jid.getNode() == null) {
return false;
}
// Always accept local JIDs or JIDs related to gateways
// (this filters our components, MUC, pubsub, etc. except gateways).
if (server.isLocal(jid) || gateways.contains(jid.getDomain())) {
return true;
}
// If not a local JID, always record it.
if (!jid.getDomain().endsWith(serverInfo.getXMPPDomain())) {
return true;
}
// Otherwise return false.
return false;
}
/**
* Returns a unique key for a coversation between two JID's. The order of two JID parameters
* is irrelevant; the same key will be returned.
*
* @param jid1 the first JID.
* @param jid2 the second JID.
* @return a unique key.
*/
String getConversationKey(JID jid1, JID jid2) {
StringBuilder builder = new StringBuilder();
if (jid1.compareTo(jid2) < 0) {
builder.append(jid1.toBareJID()).append("_").append(jid2.toBareJID());
}
else {
builder.append(jid2.toBareJID()).append("_").append(jid1.toBareJID());
}
return builder.toString();
}
String getRoomConversationKey(JID roomJID) {
return roomJID.toString();
}
public void componentInfoReceived(IQ iq) {
//Check if the component is a gateway
boolean gatewayFound = false;
Element childElement = iq.getChildElement();
for (Iterator<Element> it = childElement.elementIterator("identity"); it.hasNext();) {
Element identity = it.next();
if ("gateway".equals(identity.attributeValue("category"))) {
gatewayFound = true;
}
}
// If component is a gateway then keep track of the component
if (gatewayFound) {
gateways.add(iq.getFrom().getDomain());
}
}
public void componentRegistered(JID componentJID) {
//Do nothing
}
public void componentUnregistered(JID componentJID) {
// Remove stored information about this component
gateways.remove(componentJID.getDomain());
}
void queueParticipantLeft(Conversation conversation, JID user, ConversationParticipation participation) {
RoomParticipant updatedParticipant = new RoomParticipant();
updatedParticipant.conversationID = conversation.getConversationID();
updatedParticipant.user = user;
updatedParticipant.joined = participation.getJoined();
updatedParticipant.left = participation.getLeft();
participantQueue.add(updatedParticipant);
}
/**
* A task that persists conversation meta-data and messages to the database.
*/
private class ArchivingTask implements Runnable {
public void run() {
synchronized (this) {
if (archivingRunning) {
return;
}
archivingRunning = true;
}
if (!messageQueue.isEmpty() || !conversationQueue.isEmpty() || !participantQueue.isEmpty()) {
Connection con = null;
PreparedStatement pstmt = null;
try {
con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement(INSERT_MESSAGE);
ArchivedMessage message;
int count = 0;
while ((message = messageQueue.poll()) != null) {
pstmt.setLong(1, message.getConversationID());
pstmt.setString(2, message.getFromJID().toString());
pstmt.setString(3, message.getToJID().toString());
pstmt.setLong(4, message.getSentDate().getTime());
DbConnectionManager.setLargeTextField(pstmt, 5, message.getBody());
if (DbConnectionManager.isBatchUpdatesSupported()) {
pstmt.addBatch();
}
else {
pstmt.execute();
}
// Only batch up to 500 items at a time.
if (count % 500 == 0 && DbConnectionManager.isBatchUpdatesSupported()) {
pstmt.executeBatch();
}
count++;
}
if (DbConnectionManager.isBatchUpdatesSupported()) {
pstmt.executeBatch();
}
pstmt = con.prepareStatement(UPDATE_CONVERSATION);
Conversation conversation;
count = 0;
while ((conversation = conversationQueue.poll()) != null) {
pstmt.setLong(1, conversation.getLastActivity().getTime());
pstmt.setInt(2, conversation.getMessageCount());
pstmt.setLong(3, conversation.getConversationID());
if (DbConnectionManager.isBatchUpdatesSupported()) {
pstmt.addBatch();
}
else {
pstmt.execute();
}
// Only batch up to 500 items at a time.
if (count % 500 == 0 && DbConnectionManager.isBatchUpdatesSupported()) {
pstmt.executeBatch();
}
count++;
}
if (DbConnectionManager.isBatchUpdatesSupported()) {
pstmt.executeBatch();
}
pstmt = con.prepareStatement(UPDATE_PARTICIPANT);
RoomParticipant particpiant;
count = 0;
while ((particpiant = participantQueue.poll()) != null) {
pstmt.setLong(1, particpiant.left.getTime());
pstmt.setLong(2, particpiant.conversationID);
pstmt.setString(3, particpiant.user.toBareJID());
pstmt.setString(4, particpiant.user.getResource() == null ? " " : particpiant.user.getResource());
pstmt.setLong(5, particpiant.joined.getTime());
if (DbConnectionManager.isBatchUpdatesSupported()) {
pstmt.addBatch();
}
else {
pstmt.execute();
}
// Only batch up to 500 items at a time.
if (count % 500 == 0 && DbConnectionManager.isBatchUpdatesSupported()) {
pstmt.executeBatch();
}
count++;
}
if (DbConnectionManager.isBatchUpdatesSupported()) {
pstmt.executeBatch();
}
}
catch (Exception e) {
Log.error(e.getMessage(), e);
}
finally {
DbConnectionManager.closeConnection(pstmt, con);
}
}
// Set archiving running back to false.
archivingRunning = false;
}
}
/**
* A PropertyEventListener that tracks updates to Jive properties that are related
* to conversation tracking and archiving.
*/
private class ConversationPropertyListener implements PropertyEventListener {
public void propertySet(String property, Map<String, Object> params) {
if (property.equals("conversation.metadataArchiving")) {
String value = (String)params.get("value");
metadataArchivingEnabled = Boolean.valueOf(value);
}
else if (property.equals("conversation.messageArchiving")) {
String value = (String)params.get("value");
messageArchivingEnabled = Boolean.valueOf(value);
// Force metadata archiving enabled on if message archiving on.
if (messageArchivingEnabled) {
metadataArchivingEnabled = true;
}
}
else if (property.equals("conversation.roomArchiving")) {
String value = (String)params.get("value");
roomArchivingEnabled = Boolean.valueOf(value);
// Force metadata archiving enabled on if message archiving on.
if (roomArchivingEnabled) {
metadataArchivingEnabled = true;
}
}
else if (property.equals("conversation.roomsArchived")) {
String value = (String)params.get("value");
roomsArchived = StringUtils.stringToCollection(value);
}
else if (property.equals("conversation.idleTime")) {
String value = (String)params.get("value");
try {
idleTime = Integer.parseInt(value) * JiveConstants.MINUTE;
}
catch (Exception e) {
Log.error(e.getMessage(), e);
idleTime = DEFAULT_IDLE_TIME * JiveConstants.MINUTE;
}
}
else if (property.equals("conversation.maxTime")) {
String value = (String)params.get("value");
try {
maxTime = Integer.parseInt(value) * JiveConstants.MINUTE;
}
catch (Exception e) {
Log.error(e.getMessage(), e);
maxTime = DEFAULT_MAX_TIME * JiveConstants.MINUTE;
}
}
}
public void propertyDeleted(String property, Map<String, Object> params) {
if (property.equals("conversation.metadataArchiving")) {
metadataArchivingEnabled = true;
}
else if (property.equals("conversation.messageArchiving")) {
messageArchivingEnabled = false;
}
else if (property.equals("conversation.roomArchiving")) {
roomArchivingEnabled = false;
}
else if (property.equals("conversation.roomsArchived")) {
roomsArchived = Collections.emptyList();
}
else if (property.equals("conversation.idleTime")) {
idleTime = DEFAULT_IDLE_TIME * JiveConstants.MINUTE;
}
else if (property.equals("conversation.maxTime")) {
maxTime = DEFAULT_MAX_TIME * JiveConstants.MINUTE;
}
}
public void xmlPropertySet(String property, Map<String, Object> params) {
// Ignore.
}
public void xmlPropertyDeleted(String property, Map<String, Object> params) {
// Ignore.
}
}
private static class RoomParticipant {
private long conversationID = -1;
private JID user;
private Date joined;
private Date left;
}
}
\ No newline at end of file
/**
* $Revision$
* $Date$
*
* Copyright (C) 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.openfire.archive;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.plugin.MonitoringPlugin;
import org.jivesoftware.util.NotFoundException;
import org.jivesoftware.util.ParamUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class ConversationPDFServlet extends HttpServlet {
private static final Logger Log = LoggerFactory.getLogger(ConversationPDFServlet.class);
@Override
public void init() throws ServletException {
}
@Override
public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
long conversationID = ParamUtils.getLongParameter(request, "conversationID", -1);
if (conversationID == -1) {
return;
}
MonitoringPlugin plugin = (MonitoringPlugin)XMPPServer.getInstance().getPluginManager().getPlugin(
"monitoring");
ConversationManager conversationManager = (ConversationManager)plugin.getModule(ConversationManager.class);
Conversation conversation;
if (conversationID > -1) {
try {
conversation = new Conversation(conversationManager, conversationID);
ByteArrayOutputStream stream = new ConversationUtils().getConversationPDF(conversation);
// setting some response headers
response.setHeader("Expires", "0");
response.setHeader("Cache-Control", "must-revalidate, post-check=0, pre-check=0");
response.setHeader("Pragma", "public");
// setting the content type
response.setContentType("application/pdf");
// the content length is needed for MSIE!!!
response.setContentLength(stream.size());
// write ByteArrayOutputStream to the ServletOutputStream
ServletOutputStream out = response.getOutputStream();
stream.writeTo(out);
out.flush();
}
catch (NotFoundException nfe) {
Log.error(nfe.getMessage(), nfe);
}
}
}
}
/**
* $Revision: $
* $Date: $
*
* Copyright (C) 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.openfire.archive;
import org.jivesoftware.util.cache.ExternalizableUtil;
import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.util.Date;
/**
* Participation of a user, connected from a specific resource, in a conversation. If
* a user joins and leaves the conversation many times then we will have many instances
* of this class.
*
* @author Gaston Dombiak
*/
public class ConversationParticipation implements Externalizable {
private Date joined = new Date();
private Date left;
private String nickname;
public ConversationParticipation() {
}
public ConversationParticipation(Date joined) {
this.joined = joined;
}
public ConversationParticipation(Date joined, String nickname) {
this.joined = joined;
this.nickname = nickname;
}
public void participationEnded(Date left) {
this.left = left;
}
/**
* Returns the date when the user joined the conversation.
*
* @return the date when the user joined the conversation.
*/
public Date getJoined() {
return joined;
}
/**
* Returns the date when the user left the conversation.
*
* @return the date when the user left the conversation.
*/
public Date getLeft() {
return left;
}
/**
* Returns the nickname of the user used in the group conversation or
* <tt>null</tt> if participation is in a one-to-one chat.
*
* @return the nickname of the user used in the group conversation.
*/
public String getNickname() {
return nickname;
}
public void writeExternal(ObjectOutput out) throws IOException {
ExternalizableUtil.getInstance().writeLong(out, joined.getTime());
ExternalizableUtil.getInstance().writeBoolean(out, nickname != null);
if (nickname != null) {
ExternalizableUtil.getInstance().writeSafeUTF(out, nickname);
}
ExternalizableUtil.getInstance().writeBoolean(out, left != null);
if (left != null) {
ExternalizableUtil.getInstance().writeLong(out, left.getTime());
}
}
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
joined = new Date(ExternalizableUtil.getInstance().readLong(in));
if (ExternalizableUtil.getInstance().readBoolean(in)) {
nickname = ExternalizableUtil.getInstance().readSafeUTF(in);
}
if (ExternalizableUtil.getInstance().readBoolean(in)) {
left = new Date(ExternalizableUtil.getInstance().readLong(in));
}
}
}
/**
* $Revision$
* $Date$
*
* Copyright (C) 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.openfire.archive;
import java.awt.Color;
import java.io.ByteArrayOutputStream;
import java.net.URL;
import java.util.Arrays;
import java.util.Collection;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Future;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.plugin.MonitoringPlugin;
import org.jivesoftware.openfire.user.UserManager;
import org.jivesoftware.util.JiveGlobals;
import org.jivesoftware.util.LocaleUtils;
import org.jivesoftware.util.NotFoundException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xmpp.packet.JID;
import com.lowagie.text.Chunk;
import com.lowagie.text.Document;
import com.lowagie.text.DocumentException;
import com.lowagie.text.Font;
import com.lowagie.text.FontFactory;
import com.lowagie.text.Image;
import com.lowagie.text.PageSize;
import com.lowagie.text.Paragraph;
import com.lowagie.text.pdf.PdfContentByte;
import com.lowagie.text.pdf.PdfPageEventHelper;
import com.lowagie.text.pdf.PdfWriter;
/**
* Utility class for asynchronous web calls for archiving tasks.
*
* @author Derek DeMoro
*/
public class ConversationUtils {
private static final Logger Log = LoggerFactory.getLogger(ConversationUtils.class);
/**
* Returns the status of the rebuilding of the messaging/metadata archives. This is done
* asynchronously.
*
* @return the status the rebuilding (0 - 100) where 100 is complete.
*/
public int getBuildProgress() {
// Get handle on the Monitoring plugin
MonitoringPlugin plugin =
(MonitoringPlugin)XMPPServer.getInstance().getPluginManager().getPlugin(
"monitoring");
ArchiveIndexer archiveIndexer = (ArchiveIndexer)plugin.getModule(ArchiveIndexer.class);
Future<Integer> future = archiveIndexer.getIndexRebuildProgress();
if (future != null) {
try {
return future.get();
}
catch (Exception e) {
Log.error(e.getMessage(), e);
}
}
return -1;
}
public ConversationInfo getConversationInfo(long conversationID, boolean formatParticipants) {
// Create ConversationInfo bean
ConversationInfo info = new ConversationInfo();
// Get handle on the Monitoring plugin
MonitoringPlugin plugin =
(MonitoringPlugin)XMPPServer.getInstance().getPluginManager().getPlugin(
"monitoring");
ConversationManager conversationmanager =
(ConversationManager)plugin.getModule(ConversationManager.class);
try {
Conversation conversation = conversationmanager.getConversation(conversationID);
info = toConversationInfo(conversation, formatParticipants);
}
catch (NotFoundException e) {
Log.error(e.getMessage(), e);
}
return info;
}
/**
* Retrieves all the existing conversations from the system.
*
* @return a Map of ConversationInfo objects.
*/
public Map<String, ConversationInfo> getConversations(boolean formatParticipants) {
Map<String, ConversationInfo> cons = new HashMap<String, ConversationInfo>();
MonitoringPlugin plugin = (MonitoringPlugin)XMPPServer.getInstance().getPluginManager()
.getPlugin("monitoring");
ConversationManager conversationManager =
(ConversationManager)plugin.getModule(ConversationManager.class);
Collection<Conversation> conversations = conversationManager.getConversations();
List<Conversation> lConversations =
Arrays.asList(conversations.toArray(new Conversation[conversations.size()]));
for (Iterator<Conversation> i = lConversations.iterator(); i.hasNext();) {
Conversation con = i.next();
ConversationInfo info = toConversationInfo(con, formatParticipants);
cons.put(Long.toString(con.getConversationID()), info);
}
return cons;
}
public ByteArrayOutputStream getConversationPDF(Conversation conversation) {
Font red = FontFactory
.getFont(FontFactory.HELVETICA, 12f, Font.BOLD, new Color(0xFF, 0x00, 0x00));
Font blue = FontFactory
.getFont(FontFactory.HELVETICA, 12f, Font.ITALIC, new Color(0x00, 0x00, 0xFF));
Font black = FontFactory.getFont(FontFactory.HELVETICA, 12f, Font.BOLD, Color.BLACK);
Map<String, Font> colorMap = new HashMap<String, Font>();
if (conversation != null) {
Collection<JID> set = conversation.getParticipants();
int count = 0;
for (JID jid : set) {
if (conversation.getRoom() == null) {
if (count == 0) {
colorMap.put(jid.toString(), blue);
}
else {
colorMap.put(jid.toString(), red);
}
count++;
}
else {
colorMap.put(jid.toString(), black);
}
}
}
return buildPDFContent(conversation, colorMap);
}
private ByteArrayOutputStream buildPDFContent(Conversation conversation,
Map<String, Font> colorMap) {
Font roomEvent = FontFactory
.getFont(FontFactory.HELVETICA, 12f, Font.ITALIC, new Color(0xFF, 0x00, 0xFF));
try {
Document document = new Document(PageSize.A4, 50, 50, 50, 50);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
PdfWriter writer = PdfWriter.getInstance(document, baos);
writer.setPageEvent(new PDFEventListener());
document.open();
Paragraph p = new Paragraph(
LocaleUtils.getLocalizedString("archive.search.pdf.title", "monitoring"),
FontFactory.getFont(FontFactory.HELVETICA,
18, Font.BOLD));
document.add(p);
document.add(Chunk.NEWLINE);
ConversationInfo coninfo = new ConversationUtils()
.getConversationInfo(conversation.getConversationID(), false);
String participantsDetail;
if (coninfo.getAllParticipants() == null) {
participantsDetail = coninfo.getParticipant1() + ", " + coninfo.getParticipant2();
}
else {
participantsDetail = String.valueOf(coninfo.getAllParticipants().length);
}
Paragraph chapterTitle = new Paragraph(
LocaleUtils
.getLocalizedString("archive.search.pdf.participants", "monitoring") +
" " + participantsDetail,
FontFactory.getFont(FontFactory.HELVETICA, 12,
Font.BOLD));
document.add(chapterTitle);
Paragraph startDate = new Paragraph(
LocaleUtils.getLocalizedString("archive.search.pdf.startdate", "monitoring") +
" " +
coninfo.getDate(),
FontFactory.getFont(FontFactory.HELVETICA, 12,
Font.BOLD));
document.add(startDate);
Paragraph duration = new Paragraph(
LocaleUtils.getLocalizedString("archive.search.pdf.duration", "monitoring") +
" " +
coninfo.getDuration(),
FontFactory.getFont(FontFactory.HELVETICA, 12,
Font.BOLD));
document.add(duration);
Paragraph messageCount = new Paragraph(
LocaleUtils
.getLocalizedString("archive.search.pdf.messagecount", "monitoring") +
" " +
conversation.getMessageCount(),
FontFactory.getFont(FontFactory.HELVETICA, 12,
Font.BOLD));
document.add(messageCount);
document.add(Chunk.NEWLINE);
Paragraph messageParagraph;
for (ArchivedMessage message : conversation.getMessages()) {
String time = JiveGlobals.formatTime(message.getSentDate());
String from = message.getFromJID().getNode();
if (conversation.getRoom() != null) {
from = message.getToJID().getResource();
}
String body = message.getBody();
String prefix;
if (!message.isRoomEvent()) {
prefix = "[" + time + "] " + from + ": ";
Font font = colorMap.get(message.getFromJID().toString());
if (font == null) {
font = colorMap.get(message.getFromJID().toBareJID());
}
if (font == null) {
font = FontFactory.getFont(FontFactory.HELVETICA, 12f, Font.BOLD, Color.BLACK);
}
messageParagraph = new Paragraph(new Chunk(prefix, font));
}
else {
prefix = "[" + time + "] ";
messageParagraph = new Paragraph(new Chunk(prefix, roomEvent));
}
messageParagraph.add(body);
messageParagraph.add(" ");
document.add(messageParagraph);
}
document.close();
return baos;
}
catch (DocumentException e) {
Log.error("error creating PDF document: " + e.getMessage(), e);
return null;
}
}
private ConversationInfo toConversationInfo(Conversation conversation,
boolean formatParticipants) {
final ConversationInfo info = new ConversationInfo();
// Set participants
Collection<JID> col = conversation.getParticipants();
if (conversation.getRoom() == null) {
JID user1 = (JID)col.toArray()[0];
info.setParticipant1(formatJID(formatParticipants, user1));
JID user2 = (JID)col.toArray()[1];
info.setParticipant2(formatJID(formatParticipants, user2));
}
else {
info.setConversationID(conversation.getConversationID());
JID[] occupants = col.toArray(new JID[col.size()]);
String[] jids = new String[col.size()];
for (int i = 0; i < occupants.length; i++) {
jids[i] = formatJID(formatParticipants, occupants[i]);
}
info.setAllParticipants(jids);
}
Map<String, String> cssLabels = new HashMap<String, String>();
int count = 0;
for (JID jid : col) {
if (!cssLabels.containsKey(jid.toString())) {
if (conversation.getRoom() == null) {
if (count % 2 == 0) {
cssLabels.put(jid.toBareJID(), "conversation-label2");
}
else {
cssLabels.put(jid.toBareJID(), "conversation-label1");
}
count++;
}
else {
cssLabels.put(jid.toString(), "conversation-label4");
}
}
}
// Set date
info.setDate(JiveGlobals.formatDateTime(conversation.getStartDate()));
info.setLastActivity(JiveGlobals.formatTime(conversation.getLastActivity()));
// Create body.
final StringBuilder builder = new StringBuilder();
builder.append("<table width=100%>");
for (ArchivedMessage message : conversation.getMessages()) {
String time = JiveGlobals.formatTime(message.getSentDate());
String from = message.getFromJID().getNode();
if (conversation.getRoom() != null) {
from = message.getToJID().getResource();
}
String cssLabel = cssLabels.get(message.getFromJID().toBareJID());
String body = message.getBody();
builder.append("<tr valign=top>");
if (!message.isRoomEvent()) {
builder.append("<td width=1% nowrap class=" + cssLabel + ">").append("[")
.append(time).append("]").append("</td>");
builder.append("<td width=1% class=" + cssLabel + ">").append(from).append(": ")
.append("</td>");
builder.append("<td class=conversation-body>").append(body).append("</td");
}
else {
builder.append("<td width=1% nowrap class=conversation-label3>").append("[")
.append(time).append("]").append("</td>");
builder.append("<td colspan=2 class=conversation-label3><i>").append(body)
.append("</i></td");
}
builder.append("</tr>");
}
if (conversation.getMessages().size() == 0) {
builder.append("<span class=small-description>" +
LocaleUtils.getLocalizedString("archive.search.results.archive_disabled",
"monitoring") +
"</a>");
}
info.setBody(builder.toString());
// Set message count
info.setMessageCount(conversation.getMessageCount());
long duration =
(conversation.getLastActivity().getTime() - conversation.getStartDate().getTime());
info.setDuration(duration);
return info;
}
private String formatJID(boolean html, JID jid) {
String formattedJID;
if (html) {
UserManager userManager = UserManager.getInstance();
if (XMPPServer.getInstance().isLocal(jid) &&
userManager.isRegisteredUser(jid.getNode())) {
formattedJID = "<a href='/user-properties.jsp?username=" +
jid.getNode() + "'>" + jid.toBareJID() + "</a>";
}
else {
formattedJID = jid.toBareJID();
}
}
else {
formattedJID = jid.toBareJID();
}
return formattedJID;
}
class PDFEventListener extends PdfPageEventHelper {
@Override
public void onEndPage(PdfWriter writer, Document document) {
PdfContentByte cb = writer.getDirectContent();
try {
cb.setColorStroke(new Color(156, 156, 156));
cb.setLineWidth(2);
cb.moveTo(document.leftMargin(), document.bottomMargin() - 5);
cb.lineTo(document.getPageSize().width() - document.rightMargin(),
document.bottomMargin() - 5);
cb.stroke();
ClassLoader classLoader = ConversationUtils.class.getClassLoader();
Enumeration<URL> providerEnum = classLoader.getResources("images/pdf_generatedbyof.gif");
while (providerEnum.hasMoreElements()) {
Image gif = Image.getInstance(providerEnum.nextElement());
cb.addImage(gif, 221, 0, 0, 28, (int)document.leftMargin(),
(int)document.bottomMargin() - 35);
}
}
catch (Exception e) {
Log.error("error drawing PDF footer: " + e.getMessage());
}
cb.saveState();
}
}
}
/**
* $Revision: $
* $Date: $
*
* Copyright (C) 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.openfire.archive;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.cluster.ClusterManager;
import org.jivesoftware.openfire.muc.MUCEventDispatcher;
import org.jivesoftware.openfire.muc.MUCEventListener;
import org.jivesoftware.openfire.muc.MUCRoom;
import org.picocontainer.Startable;
import org.xmpp.packet.JID;
import org.xmpp.packet.Message;
import java.util.Date;
/**
* Interceptor of MUC events of the local conferencing service. The interceptor is responsible
* for reacting to users joining and leaving rooms as well as messages being sent to rooms.
*
* @author Gaston Dombiak
*/
public class GroupConversationInterceptor implements MUCEventListener, Startable {
private ConversationManager conversationManager;
public GroupConversationInterceptor(ConversationManager conversationManager) {
this.conversationManager = conversationManager;
}
public void roomCreated(JID roomJID) {
//Do nothing
}
public void roomDestroyed(JID roomJID) {
// Process this event in the senior cluster member or local JVM when not in a cluster
if (ClusterManager.isSeniorClusterMember()) {
conversationManager.roomConversationEnded(roomJID, new Date());
}
else {
ConversationEventsQueue eventsQueue = conversationManager.getConversationEventsQueue();
eventsQueue.addGroupChatEvent(conversationManager.getRoomConversationKey(roomJID),
ConversationEvent.roomDestroyed(roomJID, new Date()));
}
}
public void occupantJoined(JID roomJID, JID user, String nickname) {
// Process this event in the senior cluster member or local JVM when not in a cluster
if (ClusterManager.isSeniorClusterMember()) {
conversationManager.joinedGroupConversation(roomJID, user, nickname, new Date());
}
else {
ConversationEventsQueue eventsQueue = conversationManager.getConversationEventsQueue();
eventsQueue.addGroupChatEvent(conversationManager.getRoomConversationKey(roomJID),
ConversationEvent.occupantJoined(roomJID, user, nickname, new Date()));
}
}
public void occupantLeft(JID roomJID, JID user) {
// Process this event in the senior cluster member or local JVM when not in a cluster
if (ClusterManager.isSeniorClusterMember()) {
conversationManager.leftGroupConversation(roomJID, user, new Date());
// If there are no more occupants then consider the group conversarion over
MUCRoom mucRoom = XMPPServer.getInstance().getMultiUserChatManager().getMultiUserChatService(roomJID).getChatRoom(roomJID.getNode());
if (mucRoom != null && mucRoom.getOccupantsCount() == 0) {
conversationManager.roomConversationEnded(roomJID, new Date());
}
}
else {
ConversationEventsQueue eventsQueue = conversationManager.getConversationEventsQueue();
eventsQueue.addGroupChatEvent(conversationManager.getRoomConversationKey(roomJID),
ConversationEvent.occupantLeft(roomJID, user, new Date()));
}
}
public void nicknameChanged(JID roomJID, JID user, String oldNickname, String newNickname) {
// Process this event in the senior cluster member or local JVM when not in a cluster
if (ClusterManager.isSeniorClusterMember()) {
occupantLeft(roomJID, user);
// Sleep 1 millisecond so that there is a delay between logging out and logging in
try {
Thread.sleep(1);
} catch (InterruptedException e) {
// Ignore
}
occupantJoined(roomJID, user, newNickname);
}
else {
ConversationEventsQueue eventsQueue = conversationManager.getConversationEventsQueue();
eventsQueue.addGroupChatEvent(conversationManager.getRoomConversationKey(roomJID),
ConversationEvent.nicknameChanged(roomJID, user, newNickname, new Date()));
}
}
public void messageReceived(JID roomJID, JID user, String nickname, Message message) {
// Process this event in the senior cluster member or local JVM when not in a cluster
if (ClusterManager.isSeniorClusterMember()) {
conversationManager.processRoomMessage(roomJID, user, nickname, message.getBody(), new Date());
}
else {
boolean withBody = conversationManager.isRoomArchivingEnabled() && (
conversationManager.getRoomsArchived().isEmpty() ||
conversationManager.getRoomsArchived().contains(roomJID.getNode()));
ConversationEventsQueue eventsQueue = conversationManager.getConversationEventsQueue();
eventsQueue.addGroupChatEvent(conversationManager.getRoomConversationKey(roomJID),
ConversationEvent.roomMessageReceived(roomJID, user, nickname, withBody ? message.getBody() : null, new Date()));
}
}
public void privateMessageRecieved(JID toJID, JID fromJID, Message message) {
if(message.getBody() != null) {
if (ClusterManager.isSeniorClusterMember()) {
conversationManager.processMessage(fromJID, toJID, message.getBody(), new Date());
}
else {
ConversationEventsQueue eventsQueue = conversationManager.getConversationEventsQueue();
eventsQueue.addChatEvent(conversationManager.getConversationKey(fromJID, toJID),
ConversationEvent.chatMessageReceived(toJID, fromJID,
conversationManager.isMessageArchivingEnabled() ? message.getBody() : null,
new Date()));
}
}
}
public void roomSubjectChanged(JID roomJID, JID user, String newSubject) {
// Do nothing
}
public void start() {
MUCEventDispatcher.addListener(this);
}
public void stop() {
MUCEventDispatcher.removeListener(this);
conversationManager = null;
}
}
/**
* $RCSfile $
* $Revision $
* $Date $
*
* Copyright (C) 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.openfire.archive;
import org.jivesoftware.util.cache.ExternalizableUtil;
import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* Created by IntelliJ IDEA.
* User: gato
* Date: Oct 9, 2007
* Time: 11:59:42 PM
* To change this template use File | Settings | File Templates.
*/
public class UserParticipations implements Externalizable {
/**
* Flag that indicates if the participations of the user were in a group chat conversation or a one-to-one
* chat.
*/
private boolean roomParticipation;
/**
* Participations of the same user in a groupchat or one-to-one chat. In a group chat conversation
* a user may leave the conversation and return later so for each time the user joined the room a new
* participation is going to be created. Moreover, each time the user changes his nickname in the room
* a new participation is created.
*/
private List<ConversationParticipation> participations;
public UserParticipations() {
}
public UserParticipations(boolean roomParticipation) {
this.roomParticipation = roomParticipation;
if (roomParticipation) {
participations = new ArrayList<ConversationParticipation>();
}
else {
participations = new CopyOnWriteArrayList<ConversationParticipation>();
}
}
public List<ConversationParticipation> getParticipations() {
return participations;
}
public ConversationParticipation getRecentParticipation() {
return participations.get(0);
}
public void addParticipation(ConversationParticipation participation) {
participations.add(0, participation);
}
public void writeExternal(ObjectOutput out) throws IOException {
ExternalizableUtil.getInstance().writeBoolean(out, roomParticipation);
ExternalizableUtil.getInstance().writeExternalizableCollection(out, participations);
}
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
roomParticipation = ExternalizableUtil.getInstance().readBoolean(in);
if (roomParticipation) {
participations = new ArrayList<ConversationParticipation>();
}
else {
participations = new CopyOnWriteArrayList<ConversationParticipation>();
}
ExternalizableUtil.getInstance().readExternalizableCollection(in, participations, getClass().getClassLoader());
}
}
/**
* $Revision: $
* $Date: $
*
* Copyright (C) 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.openfire.archive.cluster;
import org.jivesoftware.openfire.archive.ConversationManager;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.plugin.MonitoringPlugin;
import org.jivesoftware.util.cache.ClusterTask;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
/**
* Task that will return the number of current conversations taking place in the senior cluster member.
* All conversations in the cluster are kept in the senior cluster member.
*
* @author Gaston Dombiak
*/
public class GetConversationCountTask implements ClusterTask {
private int conversationCount;
public Object getResult() {
return conversationCount;
}
public void run() {
MonitoringPlugin plugin = (MonitoringPlugin) XMPPServer.getInstance().getPluginManager().getPlugin(
"monitoring");
ConversationManager conversationManager = (ConversationManager)plugin.getModule(ConversationManager.class);
conversationCount = conversationManager.getConversationCount();
}
public void writeExternal(ObjectOutput out) throws IOException {
// Do nothing
}
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
// Do nothing
}
}
/**
* $Revision: $
* $Date: $
*
* Copyright (C) 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.openfire.archive.cluster;
import org.jivesoftware.openfire.archive.Conversation;
import org.jivesoftware.openfire.archive.ConversationManager;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.plugin.MonitoringPlugin;
import org.jivesoftware.util.NotFoundException;
import org.jivesoftware.util.cache.ClusterTask;
import org.jivesoftware.util.cache.ExternalizableUtil;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
/**
* Task that returns the specified conversation or <tt>null</tt> if not found.
*
* @author Gaston Dombiak
*/
public class GetConversationTask implements ClusterTask {
private long conversationID;
private Conversation conversation;
public GetConversationTask() {
}
public GetConversationTask(long conversationID) {
this.conversationID = conversationID;
}
public Object getResult() {
return conversation;
}
public void run() {
MonitoringPlugin plugin = (MonitoringPlugin) XMPPServer.getInstance().getPluginManager().getPlugin(
"monitoring");
ConversationManager conversationManager = (ConversationManager)plugin.getModule(ConversationManager.class);
try {
conversation = conversationManager.getConversation(conversationID);
} catch (NotFoundException e) {
// Ignore. The requester of this task will throw this exception in his JVM
}
}
public void writeExternal(ObjectOutput out) throws IOException {
ExternalizableUtil.getInstance().writeLong(out, conversationID);
}
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
conversationID = ExternalizableUtil.getInstance().readLong(in);
}
}
/**
* $Revision: $
* $Date: $
*
* Copyright (C) 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.openfire.archive.cluster;
import org.jivesoftware.openfire.archive.Conversation;
import org.jivesoftware.openfire.archive.ConversationManager;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.plugin.MonitoringPlugin;
import org.jivesoftware.util.cache.ClusterTask;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.util.Collection;
/**
* Task that will return current conversations taking place in the senior cluster member.
* All conversations in the cluster are kept in the senior cluster member.
*
* @author Gaston Dombiak
*/
public class GetConversationsTask implements ClusterTask {
private Collection<Conversation> conversations;
public Object getResult() {
return conversations;
}
public void run() {
MonitoringPlugin plugin = (MonitoringPlugin) XMPPServer.getInstance().getPluginManager().getPlugin(
"monitoring");
ConversationManager conversationManager = (ConversationManager)plugin.getModule(ConversationManager.class);
conversations = conversationManager.getConversations();
}
public void writeExternal(ObjectOutput out) throws IOException {
// Do nothing
}
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
// Do nothing
}
}
/**
* $Revision: $
* $Date: $
*
* Copyright (C) 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.openfire.archive.cluster;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.util.ArrayList;
import java.util.List;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.archive.ConversationEvent;
import org.jivesoftware.openfire.archive.ConversationManager;
import org.jivesoftware.openfire.plugin.MonitoringPlugin;
import org.jivesoftware.util.cache.ClusterTask;
import org.jivesoftware.util.cache.ExternalizableUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Task that sends cnoversation events to the senior cluster member.
*
* @author Gaston Dombiak
*/
public class SendConversationEventsTask implements ClusterTask {
private static final Logger Log = LoggerFactory.getLogger(SendConversationEventsTask.class);
private List<ConversationEvent> events;
/**
* Do not use this constructor. It only exists for serialization purposes.
*/
public SendConversationEventsTask() {
}
public SendConversationEventsTask(List<ConversationEvent> events) {
this.events = events;
}
public Object getResult() {
return null;
}
public void run() {
MonitoringPlugin plugin = (MonitoringPlugin) XMPPServer.getInstance().getPluginManager().getPlugin(
"monitoring");
ConversationManager conversationManager = (ConversationManager)plugin.getModule(ConversationManager.class);
for (ConversationEvent event : events) {
try {
event.run(conversationManager);
} catch (Exception e) {
Log.error("Error while processing chat archiving event", e);
}
}
}
public void writeExternal(ObjectOutput out) throws IOException {
ExternalizableUtil.getInstance().writeExternalizableCollection(out, events);
}
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
events = new ArrayList<ConversationEvent>();
ExternalizableUtil.getInstance().readExternalizableCollection(in, events, getClass().getClassLoader());
}
}
/**
* $RCSfile$
* $Revision$
* $Date: $
*
* Copyright (C) 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.openfire.archive.commands;
import java.io.ByteArrayOutputStream;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import org.dom4j.Element;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.archive.ArchiveSearch;
import org.jivesoftware.openfire.archive.ArchiveSearcher;
import org.jivesoftware.openfire.archive.Conversation;
import org.jivesoftware.openfire.archive.ConversationManager;
import org.jivesoftware.openfire.archive.ConversationUtils;
import org.jivesoftware.openfire.commands.AdHocCommand;
import org.jivesoftware.openfire.commands.SessionData;
import org.jivesoftware.openfire.component.InternalComponentManager;
import org.jivesoftware.openfire.plugin.MonitoringPlugin;
import org.jivesoftware.util.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xmpp.component.ComponentManagerFactory;
import org.xmpp.forms.DataForm;
import org.xmpp.forms.FormField;
import org.xmpp.packet.JID;
/**
* Command that allows to retrieve PDF content of group chat transcripts.
*
* @author Gaston Dombiak
*
* TODO Use i18n
*/
public class GetGroupConversationTranscript extends AdHocCommand {
private static final Logger Log = LoggerFactory.getLogger(GetGroupConversationTranscript.class);
@Override
protected void addStageInformation(SessionData data, Element command) {
DataForm form = new DataForm(DataForm.Type.form);
form.setTitle("Requesting PDF of conversation transcript");
form.addInstruction("Fill out this form to request the conversation transcript in PDF format.");
FormField field = form.addField();
field.setType(FormField.Type.hidden);
field.setVariable("FORM_TYPE");
field.addValue("http://jabber.org/protocol/admin");
field = form.addField();
field.setType(FormField.Type.jid_single);
field.setLabel("JID of the user that participated in the chat");
field.setVariable("participant");
field.setRequired(true);
field = form.addField();
field.setType(FormField.Type.jid_single);
field.setLabel("JID of the room");
field.setVariable("room");
field.setRequired(true);
field = form.addField();
field.setType(FormField.Type.text_single);
field.setLabel("Time when the chat took place");
field.setVariable("time");
field.setRequired(true);
field = form.addField();
field.setType(FormField.Type.boolean_type);
field.setLabel("Include PDF");
field.setVariable("includePDF");
field.setRequired(true);
// Add the form to the command
command.add(form.getElement());
}
@Override
public void execute(SessionData data, Element command) {
Element note = command.addElement("note");
// Get handle on the Monitoring plugin
MonitoringPlugin plugin = (MonitoringPlugin) XMPPServer.getInstance().getPluginManager()
.getPlugin("monitoring");
ConversationManager conversationManager =
(ConversationManager) plugin.getModule(ConversationManager.class);
if (!conversationManager.isArchivingEnabled()) {
note.addAttribute("type", "error");
note.setText("Message archiving is not enabled.");
DataForm form = new DataForm(DataForm.Type.result);
FormField field = form.addField();
field.setType(FormField.Type.hidden);
field.setVariable("FORM_TYPE");
field.addValue("http://jabber.org/protocol/admin");
field = form.addField();
field.setLabel("Conversation Found?");
field.setVariable("found");
field.addValue(false);
// Add form to reply
command.add(form.getElement());
return;
}
try {
JID participant = new JID(data.getData().get("participant").get(0));
JID room = new JID(data.getData().get("room").get(0));
Date time = DataForm.parseDate(data.getData().get("time").get(0));
boolean includePDF = DataForm.parseBoolean(data.getData().get("includePDF").get(0));
// Get archive searcher module
ArchiveSearcher archiveSearcher = (ArchiveSearcher) plugin.getModule(ArchiveSearcher.class);
ArchiveSearch search = new ArchiveSearch();
search.setParticipants(participant);
search.setIncludeTimestamp(time);
search.setRoom(room);
Collection<Conversation> conversations = archiveSearcher.search(search);
DataForm form = new DataForm(DataForm.Type.result);
FormField field = form.addField();
field.setType(FormField.Type.hidden);
field.setVariable("FORM_TYPE");
field.addValue("http://jabber.org/protocol/admin");
field = form.addField();
field.setLabel("Conversation Found?");
field.setVariable("found");
field.addValue(!conversations.isEmpty());
if (includePDF) {
ByteArrayOutputStream stream = null;
if (!conversations.isEmpty()) {
stream = new ConversationUtils().getConversationPDF(conversations.iterator().next());
}
if (stream != null) {
field = form.addField();
field.setLabel("PDF");
field.setVariable("pdf");
field.addValue(StringUtils.encodeBase64(stream.toByteArray()));
}
}
// Add form to reply
command.add(form.getElement());
}
catch (Exception e) {
Log.error("Error occurred while running the command", e);
note.addAttribute("type", "error");
note.setText("Error while processing the command.");
}
}
@Override
public String getCode() {
return "http://jivesoftware.com/protocol/workgroup#get-group-conv-transcript";
}
@Override
public String getDefaultLabel() {
return "Get Group Conversation Transcript";
}
@Override
protected List<Action> getActions(SessionData data) {
return Arrays.asList(Action.complete);
}
@Override
protected Action getExecuteAction(SessionData data) {
return Action.complete;
}
@Override
public int getMaxStages(SessionData data) {
return 1;
}
/**
* Returns if the requester can access this command. Admins and components are allowed to
* execute this command.
*
* @param requester the JID of the entity requesting to execute this command.
* @return true if the requester can access this command.
*/
@Override
public boolean hasPermission(JID requester) {
InternalComponentManager componentManager =
(InternalComponentManager) ComponentManagerFactory.getComponentManager();
return super.hasPermission(requester) || componentManager.hasComponent(requester);
}
}
/**
* $Revision: 3034 $
* $Date: 2005-11-04 21:02:33 -0300 (Fri, 04 Nov 2005) $
*
* Copyright (C) 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.openfire.plugin;
import org.jivesoftware.openfire.archive.*;
import org.jivesoftware.openfire.container.Plugin;
import org.jivesoftware.openfire.container.PluginManager;
import org.jivesoftware.openfire.reporting.graph.GraphEngine;
import org.jivesoftware.openfire.reporting.stats.*;
import org.jivesoftware.openfire.reporting.util.TaskEngine;
import org.jivesoftware.util.JiveGlobals;
import org.jivesoftware.util.JiveProperties;
import org.picocontainer.MutablePicoContainer;
import org.picocontainer.defaults.DefaultPicoContainer;
import java.io.File;
import java.io.FileFilter;
/**
* Openfire Monitoring plugin.
*
* @author Matt Tucker
*/
public class MonitoringPlugin implements Plugin {
private MutablePicoContainer picoContainer;
private boolean shuttingDown = false;
public MonitoringPlugin() {
// Enable AWT headless mode so that stats will work in headless environments.
System.setProperty("java.awt.headless", "true");
picoContainer = new DefaultPicoContainer();
picoContainer.registerComponentInstance(TaskEngine.getInstance());
picoContainer.registerComponentInstance(JiveProperties.getInstance());
// Stats and Graphing classes
picoContainer.registerComponentImplementation(StatsEngine.class);
picoContainer.registerComponentImplementation(GraphEngine.class);
picoContainer.registerComponentImplementation(StatisticsModule.class);
picoContainer.registerComponentImplementation(StatsViewer.class,
getStatsViewerImplementation());
// Archive classes
picoContainer.registerComponentImplementation(ConversationManager.class);
picoContainer.registerComponentImplementation(ArchiveInterceptor.class);
picoContainer.registerComponentImplementation(GroupConversationInterceptor.class);
picoContainer.registerComponentImplementation(ArchiveSearcher.class);
picoContainer.registerComponentImplementation(ArchiveIndexer.class);
}
private Class<? extends StatsViewer> getStatsViewerImplementation() {
if (JiveGlobals.getBooleanProperty("stats.mock.viewer", false)) {
return MockStatsViewer.class;
}
else {
return DefaultStatsViewer.class;
}
}
/**
* Returns the instance of a module registered with the Monitoring plugin.
*
* @param clazz the module class.
* @return the instance of the module.
*/
public Object getModule(Class<?> clazz) {
return picoContainer.getComponentInstanceOfType(clazz);
}
public void initializePlugin(PluginManager manager, File pluginDirectory) {
System.out.println("Starting Monitoring Plugin");
// Check if we Enterprise is installed and stop loading this plugin if found
File pluginDir = new File(JiveGlobals.getHomeDirectory(), "plugins");
File[] jars = pluginDir.listFiles(new FileFilter() {
public boolean accept(File pathname) {
String fileName = pathname.getName().toLowerCase();
return (fileName.equalsIgnoreCase("enterprise.jar"));
}
});
if (jars.length > 0) {
// Do not load this plugin since Enterprise is still installed
System.out.println("Enterprise plugin found. Stopping Monitoring Plugin");
throw new IllegalStateException("This plugin cannot run next to the Enterprise plugin");
}
shuttingDown = false;
// Make sure that the monitoring folder exists under the home directory
File dir = new File(JiveGlobals.getHomeDirectory() +
File.separator + "monitoring");
if (!dir.exists()) {
dir.mkdirs();
}
picoContainer.start();
}
public void destroyPlugin() {
shuttingDown = true;
if (picoContainer != null) {
picoContainer.stop();
picoContainer.dispose();
picoContainer = null;
}
}
public boolean isShuttingDown() {
return shuttingDown;
}
}
/**
* $RCSfile $
* $Revision $
* $Date $
*
* Copyright (C) 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.openfire.reporting;
import java.io.IOException;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import uk.ltd.getahead.dwr.Configuration;
import uk.ltd.getahead.dwr.DWRServlet;
import uk.ltd.getahead.dwr.impl.DefaultInterfaceProcessor;
/**
* Use the EnterpriseDWR servlet to register your own DWR mappings to Enteprise.
*/
public class MonitoringDWR extends DWRServlet {
private static final Logger Log = LoggerFactory.getLogger(MonitoringDWR.class);
private Document document;
@Override
public void configure(ServletConfig servletConfig, Configuration configuration) throws ServletException {
try {
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = dbf.newDocumentBuilder();
document = builder.newDocument();
Element root = document.createElement("dwr");
document.appendChild(root);
Element allowElement = document.createElement("allow");
// Build stats bean
Element createElement = buildCreator("Stats", org.jivesoftware.openfire.reporting.stats.StatsAction.class.getName());
Element convertConversationElement = document.createElement("convert");
convertConversationElement.setAttribute("converter", "bean");
convertConversationElement.setAttribute("match", org.jivesoftware.openfire.archive.ConversationInfo.class.getName());
// Build conversation Element.
Element conversationElement = buildCreator("conversations", org.jivesoftware.openfire.archive.ConversationUtils.class.getName());
allowElement.appendChild(createElement);
allowElement.appendChild(convertConversationElement);
allowElement.appendChild(conversationElement);
root.appendChild(allowElement);
}
catch (ParserConfigurationException e) {
Log.error("error creating DWR configuration: " + e);
}
configuration.addConfig(document);
// Specify the path for the Stat.js file
Object bean = container.getBean("interface");
if (bean instanceof DefaultInterfaceProcessor) {
DefaultInterfaceProcessor processor = (DefaultInterfaceProcessor)bean;
processor.setOverridePath("/plugins/monitoring/dwr");
}
}
/**
* Builds a create element within the DWR servlet.
* @param javascriptID the javascript variable name to use.
* @param qualifiedClassName the fully qualified class name.
* @return the Element.
*/
private Element buildCreator(String javascriptID, String qualifiedClassName) {
Element element = document.createElement("create");
element.setAttribute("creator", "new");
element.setAttribute("javascript", javascriptID);
Element parameter = document.createElement("param");
parameter.setAttribute("name", "class");
parameter.setAttribute("value", qualifiedClassName);
element.appendChild(parameter);
return element;
}
@Override
protected void doPost(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse)
throws IOException, ServletException {
super.doPost(new MyServletRequestWrapper(httpServletRequest), httpServletResponse);
}
/**
* Custom HTTP request wrapper that overrides the path to use
*/
private static class MyServletRequestWrapper extends HttpServletRequestWrapper {
public MyServletRequestWrapper(HttpServletRequest httpServletRequest) {
super(httpServletRequest);
}
@Override
public String getPathInfo() {
String pathInfo = super.getPathInfo();
return pathInfo.replaceAll("/monitoring/dwr", "");
}
}
}
/**
* $RCSfile $
* $Revision $
* $Date $
*
* Copyright (C) 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.openfire.reporting.graph;
import org.jivesoftware.openfire.reporting.stats.StatsViewer;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.ChartFactory;
import org.jfree.chart.labels.StandardPieSectionLabelGenerator;
import org.jfree.chart.axis.*;
import org.jfree.chart.encoders.KeypointPNGEncoderAdapter;
import org.jfree.chart.plot.PlotOrientation;
import org.jfree.chart.plot.XYPlot;
import org.jfree.chart.plot.PiePlot;
import org.jfree.chart.renderer.xy.XYAreaRenderer;
import org.jfree.chart.renderer.xy.XYBarRenderer;
import org.jfree.chart.renderer.xy.XYItemRenderer;
import org.jfree.data.time.*;
import org.jfree.data.xy.XYDataset;
import org.jfree.data.xy.IntervalXYDataset;
import org.jfree.data.general.DefaultPieDataset;
import org.jfree.util.Rotation;
import org.jivesoftware.openfire.stats.Statistic;
import org.jivesoftware.util.JiveGlobals;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.*;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.text.NumberFormat;
/**
* Builds graphs off of statistics tracked in the <i>StatsEngine</i>.
*
* @author Alexander Wenckus
* @see StatsViewer
*/
public class GraphEngine {
private StatsViewer statsViewer;
private static final long YEAR = 31104000000L;
private static final long MONTH = 2592000000L;
private static final long WEEK = 604800000L;
private static final long DAY = 86400000L;
private TickUnits tickUnits;
private Locale oldLocale;
/**
* Default constructor used by the plugin container to construct the graph engine.
*
* @param statsViewer The viewer provides an mechanism to view the data being tracked by the <i>StatsEngine</i>.
*/
public GraphEngine(StatsViewer statsViewer) {
this.statsViewer = statsViewer;
}
/**
* Creates a graph in PNG format. The PNG graph is encoded by the KeypointPNGEncoderAdapter
* so that the resulting PNG is encoded with alpha transparency.
*
* @param key
* @param width
* @param height
* @param startTime
* @param endTime
* @param dataPoints
* @return
* @throws IOException
*/
public byte[] generateGraph(String key, int width, int height, String color, long startTime, long endTime,
int dataPoints) throws IOException
{
JFreeChart chart = generateChart(key, width, height, color, startTime, endTime,dataPoints);
KeypointPNGEncoderAdapter encoder = new KeypointPNGEncoderAdapter();
encoder.setEncodingAlpha(true);
return encoder.encode(chart.createBufferedImage(width, height, BufferedImage.BITMASK, null));
}
/**
* Creates a chart.
*
* @param key
* @param width
* @param height
* @param startTime
* @param endTime
* @param dataPoints
* @return
* @throws IOException
*/
public JFreeChart generateChart(String key, int width, int height, String color, long startTime, long endTime,
int dataPoints) throws IOException
{
Statistic[] def = statsViewer.getStatistic(key);
if (def == null) {
return null;
}
XYDataset data = populateData(key, def, startTime, endTime, dataPoints);
if (data == null) {
return null;
}
JFreeChart chart;
switch(def[0].getStatType()) {
case count:
chart = createTimeBarChart(null, color, def[0].getUnits(), data);
break;
default:
chart = createTimeAreaChart(null, color, def[0].getUnits(), data);
}
return chart;
}
/**
* Generates a Sparkline type graph. Sparkline graphs
* are "intense, simple, wordlike graphics" so named by Edward Tufte. The big
* difference between the graph produced by this method compared to the
* graph produced by the <code>generateGraph</code> method is that this one
* produces graphs with no x-axis and no y-axis and is usually smaller in size.
* @param key
* @param width
* @param height
* @param startTime
* @param endTime
* @param dataPoints
* @return
* @throws IOException
*/
public byte[] generateSparklinesGraph(String key, int width, int height, String color, long startTime,
long endTime, int dataPoints) throws IOException
{
Statistic[] def = statsViewer.getStatistic(key);
if (def == null) {
return null;
}
JFreeChart chart;
switch (def[0].getStatType()) {
case count:
chart = generateSparklineBarGraph(key, color, def, startTime, endTime, dataPoints);
break;
default:
chart = generateSparklineAreaChart(key, color, def, startTime, endTime, dataPoints);
}
KeypointPNGEncoderAdapter encoder = new KeypointPNGEncoderAdapter();
encoder.setEncodingAlpha(true);
return encoder.encode(chart.createBufferedImage(width, height, BufferedImage.BITMASK, null));
}
private XYDataset populateData(String key, Statistic[] def, long startTime, long endTime,
int dataPoints)
{
double[][] values = statsViewer.getData(key, startTime, endTime, dataPoints);
long timePeriod = endTime - startTime;
TimeSeries[] series = new TimeSeries[values.length];
TimeSeriesCollection dataSet = new TimeSeriesCollection();
for (int d = 0; d < values.length; d++) {
series[d] = new TimeSeries(def[d].getName(), getTimePeriodClass(timePeriod));
Statistic.Type type = def[d].getStatType();
long interval = timePeriod / values[d].length;
for (int i = 0; i < values[d].length; i++) {
series[d].addOrUpdate(
getTimePeriod(timePeriod, new Date(startTime + (i * interval)),
JiveGlobals.getTimeZone()), cleanData(type, values[d][i]));
}
dataSet.addSeries(series[d]);
}
return dataSet;
}
private Class<? extends RegularTimePeriod> getTimePeriodClass(long timePeriod) {
if (timePeriod > 86400000) {
return Day.class;
} else if (timePeriod > 3600000) {
return Hour.class;
} else {
return Minute.class;
}
}
private RegularTimePeriod getTimePeriod(long timePeriod, Date date, TimeZone zone) {
if (timePeriod > 86400000) {
return new Day(date, zone);
} else if (timePeriod > 3600000) {
return new Hour(date, zone);
} else {
return new Minute(date, zone);
}
}
/**
* Round up a defined value.
*
* @param type the type of Statistic.
* @param value the value.
* @return the rounded up value.
*/
private double cleanData(Statistic.Type type, double value) {
if(type == Statistic.Type.count) {
return Math.round(value);
}
return value;
}
/**
* Generates a generic Time Area Chart.
*
* @param title the title of the Chart.
* @param valueLabel the Y Axis label.
* @param data the data to populate with.
* @return the generated Chart.
*/
private JFreeChart createTimeAreaChart(String title, String color, String valueLabel, XYDataset data) {
PlotOrientation orientation = PlotOrientation.VERTICAL;
DateAxis xAxis = generateTimeAxis();
NumberAxis yAxis = new NumberAxis(valueLabel);
NumberFormat formatter = NumberFormat.getNumberInstance(JiveGlobals.getLocale());
formatter.setMaximumFractionDigits(2);
formatter.setMinimumFractionDigits(0);
yAxis.setNumberFormatOverride(formatter);
XYAreaRenderer renderer = new XYAreaRenderer(XYAreaRenderer.AREA);
renderer.setOutline(true);
return createChart(title, data, xAxis, yAxis, orientation, renderer,
GraphDefinition.getDefinition(color));
}
/**
* Generates a generic Time Bar Chart.
*
* @param title the title of the Chart.
* @param valueLabel the X Axis Label.
* @param data the data to populate with.
* @return the generated Chart.
*/
private JFreeChart createTimeBarChart(String title, String color, String valueLabel, XYDataset data) {
PlotOrientation orientation = PlotOrientation.VERTICAL;
DateAxis xAxis = generateTimeAxis();
NumberAxis yAxis = new NumberAxis(valueLabel);
NumberFormat formatter = NumberFormat.getNumberInstance(JiveGlobals.getLocale());
formatter.setMaximumFractionDigits(2);
formatter.setMinimumFractionDigits(0);
yAxis.setNumberFormatOverride(formatter);
yAxis.setAutoRangeIncludesZero(true);
return createChart(title, data, xAxis, yAxis, orientation, new XYBarRenderer(),
GraphDefinition.getDefinition(color));
}
/**
* Generates a Chart.
*
* @param title the title of the chart.
* @param data the data to use in the chart.
* @param xAxis the variables to use on the xAxis.
* @param yAxis the variables to use on the yAxis.
* @param orientation the orientation
* @param itemRenderer the type of renderer to use.
* @return the generated chart.
*/
private JFreeChart createChart(String title, XYDataset data, ValueAxis xAxis, ValueAxis yAxis,
PlotOrientation orientation, XYItemRenderer itemRenderer, GraphDefinition def)
{
int seriesCount = data.getSeriesCount();
for(int i = 0; i < seriesCount; i++) {
itemRenderer.setSeriesPaint(i, def.getInlineColor(i));
itemRenderer.setSeriesOutlinePaint(i, def.getOutlineColor(i));
}
XYPlot plot = new XYPlot(data, xAxis, yAxis, null);
plot.setOrientation(orientation);
plot.setRenderer(itemRenderer);
return createChart(title, plot);
}
private JFreeChart createChart(String title, XYPlot plot) {
JFreeChart chart = new JFreeChart(title, JFreeChart.DEFAULT_TITLE_FONT, plot, false);
chart.setBackgroundPaint(Color.white);
return chart;
}
/**
* Generates a simple Time Axis.
*
* @return the generated Time Axis.
*/
private DateAxis generateTimeAxis() {
DateAxis xAxis = new DateAxis("");
xAxis.setLowerMargin(0.05);
xAxis.setUpperMargin(0.02);
xAxis.setLabel(null);
xAxis.setTickLabelsVisible(true);
xAxis.setTickMarksVisible(true);
xAxis.setAxisLineVisible(true);
xAxis.setNegativeArrowVisible(false);
xAxis.setPositiveArrowVisible(false);
xAxis.setVisible(true);
xAxis.setTickMarkPosition(DateTickMarkPosition.MIDDLE);
Locale locale = JiveGlobals.getLocale();
// If the tick units have not yet been setup or the locale has changed
if(tickUnits == null || !locale.equals(oldLocale)) {
tickUnits = createTickUnits(locale, JiveGlobals.getTimeZone());
oldLocale = locale;
}
xAxis.setStandardTickUnits(tickUnits);
return xAxis;
}
private TickUnits createTickUnits(Locale locale, TimeZone zone) {
TickUnits units = new TickUnits();
// date formatters
DateFormat f1 = new SimpleDateFormat("HH:mm:ss.SSS", locale);
DateFormat f2 = new SimpleDateFormat("HH:mm:ss", locale);
DateFormat f3 = DateFormat.getTimeInstance(DateFormat.SHORT, locale);
DateFormat f4 = DateFormat.getDateTimeInstance(DateFormat.SHORT,
DateFormat.SHORT, locale);
DateFormat f5 = new SimpleDateFormat("d-MMM", locale);
DateFormat f6 = new SimpleDateFormat("MMM-yyyy", locale);
DateFormat f7 = new SimpleDateFormat("yyyy", locale);
// NOTE: timezone not needed on date formatters because dates have already been converted
// to the appropriate timezone by the respective RegularTimePeriod (Minute, Hour, Day, etc)
// see:
// http://www.jfree.org/jfreechart/api/gjdoc/org/jfree/data/time/Hour.html#Hour:Date:TimeZone
//
// If you do use a timezone on the formatters and the Jive TimeZone has been set to something
// other than the system timezone, time specific charts will show incorrect values.
/*
f1.setTimeZone(zone);
f2.setTimeZone(zone);
f3.setTimeZone(zone);
f4.setTimeZone(zone);
f5.setTimeZone(zone);
f6.setTimeZone(zone);
f7.setTimeZone(zone);
*/
// milliseconds
units.add(new DateTickUnit(DateTickUnit.MILLISECOND, 1, f1));
units.add(new DateTickUnit(DateTickUnit.MILLISECOND, 5, DateTickUnit.MILLISECOND, 1, f1));
units.add(new DateTickUnit(DateTickUnit.MILLISECOND, 10, DateTickUnit.MILLISECOND, 1, f1));
units.add(new DateTickUnit(DateTickUnit.MILLISECOND, 25, DateTickUnit.MILLISECOND, 5, f1));
units.add(new DateTickUnit(DateTickUnit.MILLISECOND, 50, DateTickUnit.MILLISECOND, 10, f1));
units.add(new DateTickUnit(DateTickUnit.MILLISECOND, 100, DateTickUnit.MILLISECOND, 10, f1));
units.add(new DateTickUnit( DateTickUnit.MILLISECOND, 250, DateTickUnit.MILLISECOND, 10, f1));
units.add(new DateTickUnit(DateTickUnit.MILLISECOND, 500, DateTickUnit.MILLISECOND, 50, f1));
// seconds
units.add(new DateTickUnit(DateTickUnit.SECOND, 1, DateTickUnit.MILLISECOND, 50, f2));
units.add(new DateTickUnit(DateTickUnit.SECOND, 5, DateTickUnit.SECOND, 1, f2));
units.add(new DateTickUnit(DateTickUnit.SECOND, 10, DateTickUnit.SECOND, 1, f2));
units.add(new DateTickUnit(DateTickUnit.SECOND, 30, DateTickUnit.SECOND, 5, f2));
// minutes
units.add(new DateTickUnit(DateTickUnit.MINUTE, 1, DateTickUnit.SECOND, 5, f3));
units.add( new DateTickUnit(DateTickUnit.MINUTE, 2, DateTickUnit.SECOND, 10, f3));
units.add(new DateTickUnit(DateTickUnit.MINUTE, 5, DateTickUnit.MINUTE, 1, f3));
units.add(new DateTickUnit(DateTickUnit.MINUTE, 10, DateTickUnit.MINUTE, 1, f3));
units.add(new DateTickUnit(DateTickUnit.MINUTE, 15, DateTickUnit.MINUTE, 5, f3));
units.add(new DateTickUnit(DateTickUnit.MINUTE, 20, DateTickUnit.MINUTE, 5, f3));
units.add(new DateTickUnit(DateTickUnit.MINUTE, 30, DateTickUnit.MINUTE, 5, f3));
// hours
units.add(new DateTickUnit(DateTickUnit.HOUR, 1, DateTickUnit.MINUTE, 5, f3));
units.add(new DateTickUnit(DateTickUnit.HOUR, 2, DateTickUnit.MINUTE, 10, f3));
units.add(new DateTickUnit(DateTickUnit.HOUR, 4, DateTickUnit.MINUTE, 30, f3) );
units.add(new DateTickUnit(DateTickUnit.HOUR, 6, DateTickUnit.HOUR, 1, f3));
units.add(new DateTickUnit(DateTickUnit.HOUR, 12, DateTickUnit.HOUR, 1, f4));
// days
units.add(new DateTickUnit(DateTickUnit.DAY, 1, DateTickUnit.HOUR, 1, f5));
units.add(new DateTickUnit(DateTickUnit.DAY, 2, DateTickUnit.HOUR, 1, f5));
units.add(new DateTickUnit(DateTickUnit.DAY, 7, DateTickUnit.DAY, 1, f5));
units.add(new DateTickUnit(DateTickUnit.DAY, 15, DateTickUnit.DAY, 1, f5));
// months
units.add(new DateTickUnit(DateTickUnit.MONTH, 1, DateTickUnit.DAY, 1, f6));
units.add(new DateTickUnit(DateTickUnit.MONTH, 2, DateTickUnit.DAY, 1, f6));
units.add(new DateTickUnit(DateTickUnit.MONTH, 3, DateTickUnit.MONTH, 1, f6));
units.add(new DateTickUnit(DateTickUnit.MONTH, 4, DateTickUnit.MONTH, 1, f6));
units.add(new DateTickUnit(DateTickUnit.MONTH, 6, DateTickUnit.MONTH, 1, f6));
// years
units.add(new DateTickUnit(DateTickUnit.YEAR, 1, DateTickUnit.MONTH, 1, f7));
units.add(new DateTickUnit(DateTickUnit.YEAR, 2, DateTickUnit.MONTH, 3, f7));
units.add(new DateTickUnit(DateTickUnit.YEAR, 5, DateTickUnit.YEAR, 1, f7));
units.add(new DateTickUnit(DateTickUnit.YEAR, 10, DateTickUnit.YEAR, 1, f7));
units.add(new DateTickUnit(DateTickUnit.YEAR, 25, DateTickUnit.YEAR, 5, f7));
units.add(new DateTickUnit(DateTickUnit.YEAR, 50, DateTickUnit.YEAR, 10, f7));
units.add(new DateTickUnit(DateTickUnit.YEAR, 100, DateTickUnit.YEAR, 20, f7));
return units;
}
/**
* Generates a SparkLine Time Area Chart.
* @param key
* @param stats
* @param startTime
* @param endTime
* @return chart
*/
private JFreeChart generateSparklineAreaChart(String key, String color, Statistic [] stats, long startTime, long endTime, int dataPoints) {
Color backgroundColor = getBackgroundColor();
XYDataset dataset = populateData(key, stats, startTime, endTime, dataPoints);
JFreeChart chart = ChartFactory.createXYAreaChart(
null, // chart title
null, // xaxis label
null, // yaxis label
dataset, // data
PlotOrientation.VERTICAL,
false, // include legend
false, // tooltips?
false // URLs?
);
chart.setBackgroundPaint(backgroundColor);
chart.setBorderVisible(false);
chart.setBorderPaint(null);
XYPlot plot = (XYPlot)chart.getPlot();
plot.setForegroundAlpha(1.0f);
plot.setDomainGridlinesVisible(false);
plot.setDomainCrosshairVisible(false);
plot.setRangeCrosshairVisible(false);
plot.setBackgroundPaint(backgroundColor);
plot.setRangeGridlinesVisible(false);
GraphDefinition graphDef = GraphDefinition.getDefinition(color);
Color plotColor = graphDef.getInlineColor(0);
plot.getRenderer().setSeriesPaint(0, plotColor);
plot.getRenderer().setBaseItemLabelsVisible(false);
plot.getRenderer().setBaseOutlinePaint(backgroundColor);
plot.setOutlineStroke(null);
plot.setDomainGridlinePaint(null);
NumberAxis xAxis = (NumberAxis)chart.getXYPlot().getDomainAxis();
xAxis.setLabel(null);
xAxis.setTickLabelsVisible(true);
xAxis.setTickMarksVisible(true);
xAxis.setAxisLineVisible(false);
xAxis.setNegativeArrowVisible(false);
xAxis.setPositiveArrowVisible(false);
xAxis.setVisible(false);
NumberAxis yAxis = (NumberAxis)chart.getXYPlot().getRangeAxis();
yAxis.setTickLabelsVisible(false);
yAxis.setTickMarksVisible(false);
yAxis.setAxisLineVisible(false);
yAxis.setNegativeArrowVisible(false);
yAxis.setPositiveArrowVisible(false);
yAxis.setVisible(false);
return chart;
}
/**
* Creates a Pie Chart based on map.
*
* @return the Pie Chart generated.
*/
public JFreeChart getPieChart(Map<String, Double> pieValues) {
DefaultPieDataset dataset = new DefaultPieDataset();
for (String key : pieValues.keySet()) {
dataset.setValue(key, pieValues.get(key));
}
JFreeChart chart = ChartFactory.createPieChart3D(
null, // chart title
dataset, // data
true, // include legend
true,
false
);
chart.setBackgroundPaint(Color.white);
chart.setBorderVisible(false);
chart.setBorderPaint(null);
PiePlot plot = (PiePlot)chart.getPlot();
plot.setSectionOutlinesVisible(false);
plot.setLabelFont(new Font("SansSerif", Font.BOLD, 12));
plot.setNoDataMessage("No data available");
plot.setCircular(true);
plot.setLabelGap(0.02);
plot.setOutlinePaint(null);
plot.setLabelLinksVisible(false);
plot.setLabelGenerator(null);
plot.setLegendLabelGenerator(new StandardPieSectionLabelGenerator("{0}"));
plot.setStartAngle(270);
plot.setDirection(Rotation.ANTICLOCKWISE);
plot.setForegroundAlpha(0.60f);
plot.setInteriorGap(0.33);
return chart;
}
/**
* Generates a Sparkline Bar Graph.
*
* @param def the key of the statistic object.
* @return the generated chart.
*/
public JFreeChart generateSparklineBarGraph(String key, String color, Statistic [] def, long startTime,
long endTime, int dataPoints)
{
Color backgroundColor = getBackgroundColor();
IntervalXYDataset dataset = (IntervalXYDataset) populateData(key, def, startTime, endTime, dataPoints);
JFreeChart chart = ChartFactory.createXYBarChart(
null, // chart title
null, // domain axis label
true,
null, // range axis label
dataset, // data
PlotOrientation.VERTICAL,
false, // include legend
false, // tooltips?
false // URLs?
);
chart.setBackgroundPaint(backgroundColor);
chart.setBorderVisible(false);
chart.setBorderPaint(null);
XYPlot plot = (XYPlot)chart.getPlot();
plot.setDomainGridlinesVisible(false);
plot.setDomainCrosshairVisible(false);
plot.setRangeCrosshairVisible(false);
plot.setBackgroundPaint(backgroundColor);
plot.setRangeGridlinesVisible(false);
GraphDefinition graphDef = GraphDefinition.getDefinition(color);
Color plotColor = graphDef.getInlineColor(0);
plot.getRenderer().setSeriesPaint(0, plotColor);
plot.getRenderer().setBaseItemLabelsVisible(false);
plot.getRenderer().setBaseOutlinePaint(backgroundColor);
plot.setOutlineStroke(null);
plot.setDomainGridlinePaint(null);
ValueAxis xAxis = chart.getXYPlot().getDomainAxis();
xAxis.setLabel(null);
xAxis.setTickLabelsVisible(true);
xAxis.setTickMarksVisible(true);
xAxis.setAxisLineVisible(false);
xAxis.setNegativeArrowVisible(false);
xAxis.setPositiveArrowVisible(false);
xAxis.setVisible(false);
ValueAxis yAxis = chart.getXYPlot().getRangeAxis();
yAxis.setTickLabelsVisible(false);
yAxis.setTickMarksVisible(false);
yAxis.setAxisLineVisible(false);
yAxis.setNegativeArrowVisible(false);
yAxis.setPositiveArrowVisible(false);
yAxis.setVisible(false);
return chart;
}
/**
* Takes a hexidecimel color value and returns its color equivelent.
*
* @param hexColor The hex color to be parsed
* @return The java color object
*/
private static Color getColor(String hexColor) {
return new Color(Integer.valueOf(hexColor.substring(0, 2), 16),
Integer.valueOf(hexColor.substring(2, 4), 16),
Integer.valueOf(hexColor.substring(4, 6), 16));
}
/**
* Returns a color that can be used as a background.
* @return Color
*/
private static Color getBackgroundColor() {
return new Color(255,255,255);
}
public static long[] parseTimePeriod(String timeperiod) {
if (null == timeperiod)
timeperiod = "last60minutes";
Date fromDate = null;
Date toDate = null;
long dataPoints = 60;
Calendar cal = Calendar.getInstance();
Date now = cal.getTime();
// Reset the day fields so we're at the beginning of the day.
cal.set(Calendar.HOUR, 0);
cal.set(Calendar.MINUTE, 0);
cal.set(Calendar.SECOND, 0);
cal.set(Calendar.MILLISECOND, 0);
// Compute "this week" by resetting the day of the week to the first day of the week
cal.set(Calendar.DAY_OF_WEEK, cal.getFirstDayOfWeek());
Date thisWeekStart = cal.getTime();
Date thisWeekEnd = now;
// Compute last week - start with the end boundary which is 1 millisecond before the start of this week
cal.add(Calendar.MILLISECOND, -1);
Date lastWeekEnd = cal.getTime();
// Add that millisecond back, subtract 7 days for the start boundary of "last week"
cal.add(Calendar.MILLISECOND, 1);
cal.add(Calendar.DAY_OF_YEAR, -7);
Date lastWeekStart = cal.getTime();
// Reset the time
cal.setTime(now);
cal.set(Calendar.HOUR, 0);
cal.set(Calendar.MINUTE, 0);
cal.set(Calendar.SECOND, 0);
cal.set(Calendar.MILLISECOND, 0);
// Reset to the 1st day of the month, make the the start boundary for "this month"
cal.set(Calendar.DAY_OF_MONTH, cal.getMinimum(Calendar.DAY_OF_MONTH));
Date thisMonthStart = cal.getTime();
Date thisMonthEnd = now;
// Compute last month
cal.add(Calendar.MILLISECOND, -1);
Date lastMonthEnd = cal.getTime();
cal.add(Calendar.MILLISECOND, 1);
cal.add(Calendar.MONTH, -1);
Date lastMonthStart = cal.getTime();
// Compute last 3 months
cal.setTime(now);
cal.add(Calendar.MONTH, -2);
cal.set(Calendar.HOUR, 0);
cal.set(Calendar.MINUTE, 0);
cal.set(Calendar.SECOND, 0);
cal.set(Calendar.MILLISECOND, 0);
Date last3MonthsStart = cal.getTime();
Date last3MonthsEnd = now;
// Compute last 7 days:
cal.setTime(now);
cal.add(Calendar.DAY_OF_YEAR, -6);
cal.set(Calendar.HOUR, 0);
cal.set(Calendar.MINUTE, 0);
cal.set(Calendar.SECOND, 0);
cal.set(Calendar.MILLISECOND, 0);
Date last7DaysStart = cal.getTime();
Date last7DaysEnd = now;
// Compute last 60 minutes;
cal.setTime(now);
cal.add(Calendar.MINUTE, -60);
Date last60MinutesStart = cal.getTime();
Date last60MinutesEnd = now;
// Compute last 24 hours;
cal.setTime(now);
cal.add(Calendar.HOUR, -23);
Date last24HoursStart = cal.getTime();
Date last24HoursEnd = now;
// Done, reset the cal internal date to now
cal.setTime(now);
if ("thisweek".equals(timeperiod)) {
fromDate = thisWeekStart;
toDate = thisWeekEnd;
dataPoints = 7;
} else if ("last7days".equals(timeperiod)) {
fromDate = last7DaysStart;
toDate = last7DaysEnd;
dataPoints = 7;
} else if ("lastweek".equals(timeperiod)) {
fromDate = lastWeekStart;
toDate = lastWeekEnd;
dataPoints = 7;
} else if ("thismonth".equals(timeperiod)) {
fromDate = thisMonthStart;
toDate = thisMonthEnd;
dataPoints = 30;
} else if ("lastmonth".equals(timeperiod)) {
fromDate = lastMonthStart;
toDate = lastMonthEnd;
dataPoints = 30;
} else if ("last3months".equals(timeperiod)) {
fromDate = last3MonthsStart;
toDate = last3MonthsEnd;
dataPoints = (long)Math.ceil((toDate.getTime() - fromDate.getTime()) / WEEK);
} else if ("last60minutes".equals(timeperiod)) {
fromDate = last60MinutesStart;
toDate = last60MinutesEnd;
dataPoints = 60;
} else if ("last24hours".equals(timeperiod)) {
fromDate = last24HoursStart;
toDate = last24HoursEnd;
dataPoints = 48;
} else {
String[] dates = timeperiod.split("to");
if (dates.length > 0) {
DateFormat formDateFormatter = new SimpleDateFormat("MM/dd/yy");
String fromDateParam = dates[0];
String toDateParam = dates[1];
if (fromDateParam != null) {
try {
fromDate = formDateFormatter.parse(fromDateParam);
}
catch (Exception e) {
// ignore formatting exception
}
}
if (toDateParam != null) {
try {
toDate = formDateFormatter.parse(toDateParam);
// Make this date be the end of the day (so it's the day *inclusive*, not *exclusive*)
Calendar adjusted = Calendar.getInstance();
adjusted.setTime(toDate);
adjusted.set(Calendar.HOUR_OF_DAY, 23);
adjusted.set(Calendar.MINUTE, 59);
adjusted.set(Calendar.SECOND, 59);
adjusted.set(Calendar.MILLISECOND, 999);
toDate = adjusted.getTime();
}
catch (Exception e) {
// ignore formatting exception
}
}
dataPoints = discoverDataPoints(fromDate, toDate);
}
}
// default to last 60 minutes
if (null == fromDate && null==toDate) {
return new long[] {last60MinutesStart.getTime(), last60MinutesEnd.getTime(), dataPoints};
} else if (null == fromDate) {
return new long[] {0, toDate.getTime(), dataPoints};
} else if (null == toDate) {
return new long[] {fromDate.getTime(), now.getTime(), dataPoints};
} else {
return new long[] {fromDate.getTime(), toDate.getTime(), dataPoints};
}
}
private static int discoverDataPoints(Date fromDate, Date toDate) {
long delta = toDate.getTime() - fromDate.getTime();
if(delta > YEAR) {
return (int)(delta / MONTH);
}
else if (delta > 2 * MONTH) {
return (int)(delta / WEEK);
}
else {
return (int)(delta / DAY);
}
}
public static class GraphDefinition {
public static final GraphDefinition standard_light;
public static final GraphDefinition standard_dark;
private Color[] inlineColors;
private Color[] outlineColors;
static {
standard_light = new GraphDefinition(
new Color[]{new Color(246, 171, 77), getColor("B1C3D9")},
new Color[]{new Color(217, 126, 12), getColor("17518C")}
);
standard_dark = new GraphDefinition(
new Color[]{new Color(116, 128, 141), getColor("74808D")},
new Color[]{new Color(116, 128, 141), getColor("74808D")}
);
}
public static GraphDefinition getDefinition(String colorscheme) {
GraphDefinition graphDef = GraphDefinition.standard_light;
if (colorscheme != null && colorscheme.equalsIgnoreCase("dark")) {
graphDef = GraphDefinition.standard_dark;
}
return graphDef;
}
public GraphDefinition(Color[] inlineColors, Color[] outlineColors) {
this.inlineColors = inlineColors;
this.outlineColors = outlineColors;
}
public Color getInlineColor(int index) {
return inlineColors[index];
}
public Color[] getInlineColors() {
return inlineColors;
}
public Color getOutlineColor(int index) {
return outlineColors[index];
}
public Color[] getOutlineColors() {
return outlineColors;
}
}
}
/**
* $RCSfile $
* $Revision $
* $Date $
*
* Copyright (C) 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.openfire.reporting.graph;
import org.jivesoftware.openfire.plugin.MonitoringPlugin;
import org.jivesoftware.openfire.reporting.stats.StatsViewer;
import com.lowagie.text.*;
import com.lowagie.text.Image;
import com.lowagie.text.pdf.*;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import java.io.IOException;
import java.io.ByteArrayOutputStream;
import java.util.*;
import java.util.List;
import java.awt.geom.Rectangle2D;
import java.awt.*;
import java.awt.Font;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.stats.Statistic;
import org.jivesoftware.util.Log;
import org.jivesoftware.util.ParamUtils;
import org.jivesoftware.util.JiveGlobals;
import org.jfree.chart.JFreeChart;
/**
*
*/
public class GraphServlet extends HttpServlet {
private GraphEngine graphEngine;
private StatsViewer statsViewer;
@Override
public void init() throws ServletException {
// load dependencies
MonitoringPlugin plugin =
(MonitoringPlugin) XMPPServer.getInstance().getPluginManager().getPlugin("monitoring");
this.graphEngine = (GraphEngine) plugin.getModule(GraphEngine.class);
this.statsViewer = (StatsViewer)plugin.getModule(StatsViewer.class);
}
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// retrieve parameters
String statisticKey = request.getParameter("stat");
String timePeriod = request.getParameter("timeperiod");
String graphcolor = request.getParameter("color");
boolean sparkLines = request.getParameter("sparkline") != null;
boolean pdfFormat = request.getParameter("pdf") != null;
long[] dateRange = GraphEngine.parseTimePeriod(timePeriod);
int width;
int height;
if (pdfFormat) {
// PDF A4 page = 595 wide - (50px * 2 margins) = 495
width = ParamUtils.getIntParameter(request, "width", 495);
height = ParamUtils.getIntParameter(request, "height", 252);
JFreeChart[] charts;
Statistic[] stats;
if (request.getParameter("pdf").equalsIgnoreCase("all")) {
String[] statKeys = statsViewer.getAllHighLevelStatKeys();
List<String> statList = Arrays.asList(statKeys);
Collections.sort(statList, new Comparator<String>() {
public int compare(String stat1, String stat2) {
String statName1 = statsViewer.getStatistic(stat1)[0].getName();
String statName2 = statsViewer.getStatistic(stat2)[0].getName();
return statName1.toLowerCase().compareTo(statName2.toLowerCase());
}
});
charts = new JFreeChart[statList.size()];
stats = new Statistic[statList.size()];
int index = 0;
for (String statName : statList) {
stats[index] = statsViewer.getStatistic(statName)[0];
charts[index] = graphEngine.generateChart(statName, width, height, graphcolor, dateRange[0], dateRange[1], (int)dateRange[2]);
index++;
}
} else {
charts = new JFreeChart[] {graphEngine.generateChart(statisticKey, width, height, graphcolor, dateRange[0], dateRange[1], (int)dateRange[2])};
stats = new Statistic[] {statsViewer.getStatistic(statisticKey)[0]};
}
writePDFContent(request, response, charts, stats, dateRange[0], dateRange[1], width, height);
} else {
byte[] chart;
if (sparkLines) {
width = ParamUtils.getIntParameter(request, "width", 200);
height = ParamUtils.getIntParameter(request, "height", 50);
chart = graphEngine.generateSparklinesGraph(statisticKey, width, height, graphcolor, dateRange[0], dateRange[1], (int)dateRange[2]);
}
else {
width = ParamUtils.getIntParameter(request, "width", 590);
height = ParamUtils.getIntParameter(request, "height", 300);
chart = graphEngine.generateGraph(statisticKey, width, height, graphcolor, dateRange[0], dateRange[1], (int)dateRange[2]);
}
writeImageContent(response, chart, "image/png");
}
}
private void writePDFContent(HttpServletRequest request, HttpServletResponse response, JFreeChart charts[], Statistic[] stats, long starttime, long endtime, int width, int height)
throws IOException
{
try {
Document document = new Document(PageSize.A4, 50, 50, 50, 50);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
PdfWriter writer = PdfWriter.getInstance(document, baos);
writer.setPageEvent(new PDFEventListener(request));
document.open();
int index = 0;
int chapIndex = 0;
for (Statistic stat : stats) {
String serverName = XMPPServer.getInstance().getServerInfo().getXMPPDomain();
String dateName = JiveGlobals.formatDate(new Date(starttime)) + " - " +
JiveGlobals.formatDate(new Date(endtime));
Paragraph paragraph = new Paragraph(serverName,
FontFactory.getFont(FontFactory.HELVETICA,
18, Font.BOLD));
document.add(paragraph);
paragraph = new Paragraph(dateName,
FontFactory.getFont(FontFactory.HELVETICA,
14, Font.PLAIN));
document.add(paragraph);
document.add(Chunk.NEWLINE);
document.add(Chunk.NEWLINE);
Paragraph chapterTitle = new Paragraph(++chapIndex + ". " + stat.getName(),
FontFactory.getFont(FontFactory.HELVETICA, 16,
Font.BOLD));
document.add(chapterTitle);
// total hack: no idea what tags people are going to use in the description
// possibly recommend that we only use a <p> tag?
String[] paragraphs = stat.getDescription().split("<p>");
for (String s : paragraphs) {
Paragraph p = new Paragraph(s);
document.add(p);
}
document.add(Chunk.NEWLINE);
PdfContentByte contentByte = writer.getDirectContent();
PdfTemplate template = contentByte.createTemplate(width, height);
Graphics2D graphs2D = template.createGraphics(width, height, new DefaultFontMapper());
Rectangle2D rectangle2D = new Rectangle2D.Double(0, 0, width, height);
charts[index++].draw(graphs2D, rectangle2D);
graphs2D.dispose();
float x = (document.getPageSize().width() / 2) - (width / 2);
contentByte.addTemplate(template, x, writer.getVerticalPosition(true) - height);
document.newPage();
}
document.close();
// setting some response headers
response.setHeader("Expires", "0");
response.setHeader("Cache-Control", "must-revalidate, post-check=0, pre-check=0");
response.setHeader("Pragma", "public");
// setting the content type
response.setContentType("application/pdf");
// the contentlength is needed for MSIE!!!
response.setContentLength(baos.size());
// write ByteArrayOutputStream to the ServletOutputStream
ServletOutputStream out = response.getOutputStream();
baos.writeTo(out);
out.flush();
} catch (DocumentException e) {
Log.error("error creating PDF document: " + e.getMessage());
}
}
private static void writeImageContent(HttpServletResponse response, byte[] imageData, String contentType)
throws IOException
{
ServletOutputStream os = response.getOutputStream();
response.setContentType(contentType);
os.write(imageData);
os.flush();
os.close();
}
class PDFEventListener extends PdfPageEventHelper {
private HttpServletRequest request;
public PDFEventListener(HttpServletRequest request) {
this.request = request;
}
@Override
public void onEndPage(PdfWriter writer, Document document) {
PdfContentByte cb = writer.getDirectContent();
try {
cb.setColorStroke(new Color(156,156,156));
cb.setLineWidth(2);
cb.moveTo(document.leftMargin(), document.bottomMargin() + 32);
cb.lineTo(document.getPageSize().width() - document.rightMargin(), document.bottomMargin() + 32);
cb.stroke();
Image gif = Image.getInstance("http://" + request.getServerName() +
":" + request.getServerPort() + "/plugins/monitoring/images/pdf_generatedbyof.gif");
cb.addImage(gif, 221, 0, 0, 28, (int)document.leftMargin(), (int)document.bottomMargin());
} catch (Exception e) {
Log.error("error drawing PDF footer: " + e.getMessage());
}
cb.saveState();
}
}
}
/**
* $RCSfile $
* $Revision $
* $Date $
*
* Copyright (C) 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.openfire.reporting.stats;
import org.jivesoftware.openfire.cluster.ClusterManager;
import org.jivesoftware.openfire.stats.Statistic;
import org.jrobin.core.RrdDb;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class DefaultStatsViewer implements StatsViewer {
private static final Logger Log = LoggerFactory.getLogger(DefaultStatsViewer.class);
private StatsEngine engine;
/**
* Default constructor used by the plugin container to create this class.
*
* @param engine The stats engine used to retrieve the stats.
*/
public DefaultStatsViewer(StatsEngine engine) {
this.engine = engine;
}
public String [] getAllHighLevelStatKeys() {
return engine.getAllHighLevelNames();
}
public Statistic[] getStatistic(String statKey) {
StatDefinition[] definitions = engine.getDefinition(statKey);
if(definitions == null) {
throw new IllegalArgumentException("Illegal stat key: " + statKey);
}
Statistic[] statistics = new Statistic[definitions.length];
int i = 0;
for(StatDefinition def : definitions) {
statistics[i++] = def.getStatistic();
}
return statistics;
}
public long getLastSampleTime(String key) {
return engine.getDefinition(key)[0].getLastSampleTime() * 1000;
}
public double[][] getData(String key, long startTime, long endTime) {
return engine.getDefinition(key)[0].getData(parseTime(startTime), parseTime(endTime));
}
/**
* Converts milliseconds to seconds.
*
* @param time the time to convert
* @return the converted time
*/
private long parseTime(long time) {
return time / 1000;
}
public double[][] getData(String key, long startTime, long endTime, int dataPoints) {
return engine.getDefinition(key)[0].getData(parseTime(startTime), parseTime(endTime), dataPoints);
}
public StatView getData(String key, TimePeriod timePeriod) {
StatDefinition def = engine.getDefinition(key)[0];
long endTime = def.getLastSampleTime();
long startTime = timePeriod.getStartTime(endTime);
double [][] data = def.getData(startTime, endTime, timePeriod.getDataPoints());
return new StatView(startTime, endTime, data);
}
public double[] getMax(String key, long startTime, long endTime) {
return engine.getDefinition(key)[0].getMax(parseTime(startTime), parseTime(endTime));
}
public double[] getMax(String key, long startTime, long endTime, int dataPoints) {
return engine.getDefinition(key)[0].getMax(parseTime(startTime), parseTime(endTime), dataPoints);
}
public double[] getMax(String key, TimePeriod timePeriod) {
StatDefinition def = engine.getDefinition(key)[0];
long lastTime = def.getLastSampleTime();
return def.getMax(timePeriod.getStartTime(lastTime), lastTime);
}
public double[] getMin(String key, long startTime, long endTime) {
return engine.getDefinition(key)[0].getMin(parseTime(startTime), parseTime(endTime));
}
public double[] getMin(String key, long startTime, long endTime, int dataPoints) {
return engine.getDefinition(key)[0].getMin(parseTime(startTime), parseTime(endTime), dataPoints);
}
public double[] getMin(String key, TimePeriod timePeriod) {
StatDefinition def = engine.getDefinition(key)[0];
long lastTime = def.getLastSampleTime();
return def.getMin(timePeriod.getStartTime(lastTime), lastTime);
}
public double[] getCurrentValue(String key) {
if (ClusterManager.isSeniorClusterMember()) {
return new double[] { engine.getDefinition(key)[0].getLastSample() };
}
else {
try {
if (RrdSqlBackend.exists(key)) {
RrdDb db = new RrdDb(key, true);
return new double[] { db.getLastDatasourceValues()[0] };
}
} catch (Exception e) {
Log.error("Error retrieving last sample value for: " + key, e);
}
return new double[] { 0 };
}
}
}
/**
* $Revision: $
* $Date: $
*
* Copyright (C) 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.openfire.reporting.stats;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.util.HashMap;
import java.util.Map;
import org.jivesoftware.openfire.stats.Statistic;
import org.jivesoftware.openfire.stats.StatisticsManager;
import org.jivesoftware.util.cache.ClusterTask;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Command that will be executed in each cluster node (except the invoker) to
* collect samples of statistics that keep track information that is local to
* the cluster node. Statistics that are able to gather the sample from all the
* cluster nodes are ignored by this command.
*
* @author Gaston Dombiak
*/
public class GetStatistics implements ClusterTask {
private static final Logger Log = LoggerFactory.getLogger(GetStatistics.class);
private Map<String, Double> samples;
public Object getResult() {
return samples;
}
public void run() {
samples = new HashMap<String, Double>();
for (Map.Entry<String, Statistic> statisticEntry : StatisticsManager.getInstance().getAllStatistics()) {
String key = statisticEntry.getKey();
Statistic statistic = statisticEntry.getValue();
// Only sample statistics that keep info of the cluster node and not the entire cluster
if (statistic.isPartialSample()) {
double statSample = sampleStat(key, statistic);
// Store sample result
samples.put(key, statSample);
}
}
}
public void writeExternal(ObjectOutput out) throws IOException {
// Ignore
}
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
// Ignore
}
/**
* Profiles the sampling to make sure that it does not take longer than half a second to
* complete, if it does, a warning is logged.
*
* @param statKey the key related to the statistic.
* @param statistic the statistic to be sampled.
* @return the sample.
*/
private double sampleStat(String statKey, Statistic statistic) {
long start = System.currentTimeMillis();
double sample = statistic.sample();
if (System.currentTimeMillis() - start >= 500) {
Log.warn("Stat " + statKey + " took longer than a second to sample.");
}
return sample;
}
}
/**
* $RCSfile $
* $Revision $
* $Date $
*
* Copyright (C) 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.openfire.reporting.stats;
import org.jivesoftware.openfire.stats.Statistic;
import java.util.Map;
import java.util.HashMap;
import java.util.Random;
/**
*
*/
public class MockStatsViewer implements StatsViewer {
private StatsEngine engine;
private Map<String, double[][]> dataCache = new HashMap<String, double[][]>();
Random random = new Random();
public MockStatsViewer(StatsEngine engine) {
this.engine = engine;
}
public String [] getAllHighLevelStatKeys() {
return engine.getAllHighLevelNames();
}
public Statistic[] getStatistic(String statKey) {
StatDefinition[] definitions = engine.getDefinition(statKey);
Statistic[] statistics = new Statistic[definitions.length];
int i = 0;
for (StatDefinition def : definitions) {
statistics[i++] = def.getStatistic();
}
return statistics;
}
public long getLastSampleTime(String key) {
return System.currentTimeMillis() / 1000;
}
public double[][] getData(String key, long startTime, long endTime) {
return getData(key, true);
}
public double[][] getData(String key, long startTime, long endTime, int dataPoints) {
return getData(key, true);
}
private double[][] getData(String key, boolean shouldUpdate) {
synchronized (("mock_" + key).intern()) {
double[][] data = dataCache.get(key);
if (data == null) {
Statistic[] stats = getStatistic(key);
data = new double[stats.length][];
for (int i = 0; i < data.length; i++) {
data[i] = new double[60];
for (int j = 0; j < data[i].length; j++) {
data[i][j] = random.nextInt(500);
}
}
dataCache.put(key, data);
}
else if(shouldUpdate) {
for (int i = 0; i < data.length; i++) {
double [] newData = new double[data[i].length];
System.arraycopy(data[i], 1, newData, 0, data[i].length - 1);
newData[newData.length - 1] = random.nextInt(500);
data[i] = newData;
}
}
return data;
}
}
public StatView getData(String key, TimePeriod timePeriod) {
long time = getLastSampleTime(key);
double[][] data = getData(key, timePeriod.getStartTime(time), time);
return new StatView(timePeriod.getStartTime(time), time, data);
}
public double[] getMax(String key, long startTime, long endTime) {
double [][] data = getData(key, false);
double[] toReturn = new double[data.length];
for(int i = 0; i < toReturn.length; i++) {
toReturn[i] = discoverMax(data[i]);
}
return toReturn;
}
public double[] getMax(String key, long startTime, long endTime, int dataPoints) {
return getMax(key, startTime, endTime);
}
public double[] getMax(String key, TimePeriod timePeriod) {
long time = getLastSampleTime(key);
return getMax(key, timePeriod.getStartTime(time), time);
}
private double discoverMax(double[] doubles) {
double max = 0;
for(double d : doubles) {
if(d > max) {
max = d;
}
}
return max;
}
public double[] getMin(String key, long startTime, long endTime) {
double [][] data = getData(key, false);
double[] toReturn = new double[data.length];
for(int i = 0; i < toReturn.length; i++) {
toReturn[i] = discoverMin(data[i]);
}
return toReturn;
}
public double[] getMin(String key, long startTime, long endTime, int dataPoints) {
return getMin(key, startTime, endTime);
}
public double[] getMin(String key, TimePeriod timePeriod) {
long time = getLastSampleTime(key);
return getMin(key, timePeriod.getStartTime(time), time);
}
private double discoverMin(double[] doubles) {
double min = doubles[0];
for(double d : doubles) {
if(d < min) {
min = d;
}
}
return min;
}
public double[] getCurrentValue(String key) {
double [][] data = getData(key, false);
double[] toReturn = new double[data.length];
for(int i = 0; i < toReturn.length; i++) {
toReturn[i] = data[i][data[i].length - 1];
}
return toReturn;
}
}
/**
* $RCSfile $
* $Revision $
* $Date $
*
* Copyright (C) 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.openfire.reporting.stats;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import org.jivesoftware.database.DbConnectionManager;
import org.jrobin.core.RrdBackend;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class RrdSqlBackend extends RrdBackend {
private static final Logger Log = LoggerFactory.getLogger(RrdSqlBackend.class);
// SQL prepared statements
static final String JDBC_SELECT = "SELECT bytes from ofRRDs where id = ?";
static final String JDBC_INSERT = "INSERT INTO ofRRDs (id, updatedDate, bytes) VALUES (?, ?, ?)";
static final String JDBC_UPDATE = "UPDATE ofRRDs SET bytes = ?, updatedDate=? WHERE id = ?";
static final String JDBC_DELETE = "DELETE FROM ofRRDs WHERE id = ?";
// this is the place where our RRD bytes will be stored
private byte[] buffer = null;
// When readOnly then the SQL DB is not updated
private boolean readOnly;
public static void importRRD(String id, File rrdFile) throws IOException {
// Read content from file
FileInputStream stream = null;
byte[] bytes = null;
try {
stream = new FileInputStream(rrdFile);
// Create the byte array to hold the data
bytes = new byte[(int) rrdFile.length()];
// Read in the bytes
int offset = 0;
int numRead;
while (offset < bytes.length && (numRead = stream.read(bytes, offset, bytes.length - offset)) >= 0) {
offset += numRead;
}
}
finally {
if (stream != null) {
stream.close();
}
}
// Save file content to the DB
Connection con = null;
PreparedStatement pstmt = null;
PreparedStatement insertStmt = null;
ResultSet rs = null;
try {
con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement(JDBC_SELECT);
pstmt.setString(1, id);
rs = pstmt.executeQuery();
if(rs.next()) {
// Do not import since there is already an RRD in the DB
}
else {
// RRD with the given id does not exist
// we'll insert a new row in the table using the supplied id
// but with no RRD bytes (null)
insertStmt = con.prepareStatement(JDBC_INSERT);
insertStmt.setString(1, id);
insertStmt.setLong(2, System.currentTimeMillis());
insertStmt.setBytes(3, bytes);
insertStmt.executeUpdate();
}
}
catch (Exception e) {
Log.error("Error while accessing information in database: " + e);
}
finally {
DbConnectionManager.closeStatement(insertStmt);
DbConnectionManager.closeConnection(rs, pstmt, con);
}
}
RrdSqlBackend(String id, boolean readOnly) throws IOException {
super(id);
this.readOnly = readOnly;
Connection con = null;
PreparedStatement pstmt = null;
PreparedStatement insertStmt = null;
ResultSet rs = null;
try {
con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement(JDBC_SELECT);
pstmt.setString(1, id);
rs = pstmt.executeQuery();
if(rs.next()) {
// RRD with the given id already exists
// bring RRD data to our buffer
buffer = rs.getBytes("bytes");
}
else {
// RRD with the given id does not exist
// we'll insert a new row in the table using the supplied id
// but with no RRD bytes (null)
insertStmt = con.prepareStatement(JDBC_INSERT);
insertStmt.setString(1, id);
insertStmt.setLong(2, System.currentTimeMillis());
insertStmt.setBytes(3, null);
insertStmt.executeUpdate();
}
}
catch (Exception e) {
Log.error("Error while accessing information in database: " + e);
}
finally {
DbConnectionManager.closeStatement(insertStmt);
DbConnectionManager.closeConnection(rs, pstmt, con);
}
}
// this method writes bytes supplied from the JRobin frontend
// to our memory buffer
@Override
protected void write(long offset, byte[] b) {
int pos = (int) offset;
for(int i = 0; i < b.length; i++) {
buffer[pos++] = b[i];
}
}
// this method reads bytes requested from the JRobin frontend
// and stores them in the supplied byte[] array
@Override
protected void read(long offset, byte[] b) {
int pos = (int) offset;
for(int i = 0; i < b.length; i++) {
b[i] = buffer[pos++];
}
}
// returns the RRD size (since all RRD bytes are
// in the buffer, it is equal to the buffer length
@Override
public long getLength() throws IOException {
return buffer.length;
}
// provides enough space in memory for the RRD
@Override
protected void setLength(long length) {
buffer = new byte[(int) length];
}
@Override
public void close() throws IOException {
super.close();
// Save data to the SQL DB only if not read-only
if (!readOnly) {
sync();
}
}
// sends bytes in memory to the database
protected void sync() throws IOException {
// RRD id is here
String id = super.getPath();
Connection con = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement(JDBC_UPDATE);
pstmt.setBytes(1, buffer);
pstmt.setLong(2, System.currentTimeMillis());
pstmt.setString(3, id);
pstmt.executeUpdate();
}
catch (Exception e) {
Log.error("Error while updating information in database: " + e);
}
finally {
DbConnectionManager.closeConnection(rs, pstmt, con);
}
}
// checks if RRD with the given id already exists in the database
// used from RrdSqlBackendFactory class
static boolean exists(String id) throws IOException {
Connection con = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement(JDBC_SELECT);
pstmt.setString(1, id);
rs = pstmt.executeQuery();
return rs.next();
}
catch (Exception e) {
Log.error("Error while accessing information in database: " + e);
}
finally {
DbConnectionManager.closeConnection(rs, pstmt, con);
}
return false;
}
}
\ No newline at end of file
/**
* $RCSfile $
* $Revision $
* $Date $
*
* Copyright (C) 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.openfire.reporting.stats;
import org.jrobin.core.RrdBackendFactory;
import org.jrobin.core.RrdBackend;
import java.io.IOException;
public class RrdSqlBackendFactory extends RrdBackendFactory {
// name of the factory
public static final String NAME = "SQL";
// creates bew RrdSqlBackend object for the given id (path)
// the second parameter is ignored
// for the reason of simplicity
@Override
protected RrdBackend open(String path, boolean readOnly)
throws IOException {
return new RrdSqlBackend(path, readOnly);
}
// checks if the RRD with the given id (path) already exists
// in the database
@Override
protected boolean exists(String path) throws IOException {
return RrdSqlBackend.exists(path);
}
// returns factory name
@Override
public String getFactoryName() {
return NAME;
}
}
\ No newline at end of file
/**
* $RCSfile $
* $Revision $
* $Date $
*
* Copyright (C) 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.openfire.reporting.stats;
import org.jivesoftware.openfire.stats.Statistic;
/**
* A class used by the StatsEngine to track all relevant meta information and data
* relating to a graph. It also provides a mechanism for other classes in the stats
* package to retrieve data relating to a stat.
*
* @author Alexander Wenckus
*/
abstract class StatDefinition {
private String dbPath;
private String datasourceName;
private Statistic stat;
public long lastSampleTime;
public double lastSample;
StatDefinition(String dbPath, String datasourceName, Statistic stat) {
this.dbPath = dbPath;
this.datasourceName = datasourceName;
this.stat = stat;
}
public String getDbPath() {
return dbPath;
}
public String getDatasourceName() {
return datasourceName;
}
public Statistic getStatistic() {
return stat;
}
public abstract double[][] getData(long startTime, long endTime);
public abstract double[][] getData(long startTime, long lastTime, int dataPoints);
public abstract long getLastSampleTime();
public abstract double getLastSample();
public abstract double[] getMax(long startTime, long endTime);
public abstract double[] getMin(long startTime, long endTime);
public abstract double[] getMin(long startTime, long endTime, int dataPoints);
public abstract double[] getMax(long l, long l1, int dataPoints);
}
/**
* $Revision$
* $Date$
*
* Copyright (C) 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.openfire.reporting.stats;
import org.jivesoftware.openfire.SessionManager;
import org.jivesoftware.openfire.interceptor.InterceptorManager;
import org.jivesoftware.openfire.interceptor.PacketInterceptor;
import org.jivesoftware.openfire.session.Session;
import org.jivesoftware.openfire.stats.Statistic;
import org.jivesoftware.openfire.stats.StatisticsManager;
import org.jivesoftware.openfire.stats.i18nStatistic;
import org.picocontainer.Startable;
import org.xmpp.packet.Packet;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Creates and manages Enteprise-specific statistics, specifically: <ul>
* <li>Incoming and outgoing packet traffic.
* <li>Server to server connections.
* <li>Active group chat rooms.
* <li>Active user sessions.
* </ul>
*
* @author Derek DeMoro
*/
public class StatisticsModule implements Startable {
public static final String MUC_ROOMS_KEY = "active_group_chats";
public static final String SERVER_2_SERVER_SESSIONS_KEY = "server_sessions";
public static final String SESSIONS_KEY = "sessions";
public static final String TRAFFIC_KEY = "packet_count";
private StatisticsManager statisticsManager;
private AtomicInteger packetCount = new AtomicInteger();
private PacketInterceptor packetInterceptor;
public void start() {
// Retrieve instance of StatisticsManager
statisticsManager = StatisticsManager.getInstance();
// Register a packet listener so that we can track packet traffic.
packetInterceptor = new PacketInterceptor() {
public void interceptPacket(Packet packet, Session session, boolean incoming,
boolean processed)
{
// Only track processed packets so that we don't count them twice.
if (processed) {
packetCount.incrementAndGet();
}
}
};
InterceptorManager.getInstance().addInterceptor(packetInterceptor);
// Register all statistics.
addServerToServerStatistic();
addActiveSessionsStatistic();
addPacketStatistic();
}
/**
* Remove all registered statistics.
*/
public void stop() {
// Remove Server to Server Statistic
statisticsManager.removeStatistic(SERVER_2_SERVER_SESSIONS_KEY);
// Remove Active Session Statistic
statisticsManager.removeStatistic(SESSIONS_KEY);
// Remove Packet Traffic Statistic
statisticsManager.removeStatistic(TRAFFIC_KEY);
statisticsManager = null;
// Remove the packet listener.
InterceptorManager.getInstance().removeInterceptor(packetInterceptor);
packetInterceptor = null;
packetCount = null;
}
/**
* Tracks the number of Server To Server connections taking place in the server at anyone time.
* This includes both incoming and outgoing connections.
*/
private void addServerToServerStatistic() {
// Register a statistic.
Statistic serverToServerStatistic = new i18nStatistic(SERVER_2_SERVER_SESSIONS_KEY, "monitoring",
Statistic.Type.count)
{
public double sample() {
return (SessionManager.getInstance().getIncomingServers().size() + SessionManager.
getInstance().getOutgoingServers().size());
}
public boolean isPartialSample() {
return false;
}
};
// Add to StatisticsManager
statisticsManager.addStatistic(SERVER_2_SERVER_SESSIONS_KEY, serverToServerStatistic);
}
/**
* Tracks the number of Active Sessions with the server at any point in time.
* Active Sessions are defined as one client connection.
*/
private void addActiveSessionsStatistic() {
// Register a statistic.
Statistic activeSessionStatistic = new i18nStatistic(SESSIONS_KEY, "monitoring", Statistic.Type.count) {
public double sample() {
return SessionManager.getInstance().getUserSessionsCount(false);
}
public boolean isPartialSample() {
return false;
}
};
statisticsManager.addStatistic(SESSIONS_KEY, activeSessionStatistic);
}
/**
* Tracks the total number of packets both incoming and outgoing in the server.
*/
private void addPacketStatistic() {
// Register a statistic.
Statistic packetTrafficStatistic = new i18nStatistic(TRAFFIC_KEY, "monitoring", Statistic.Type.rate) {
public double sample() {
return packetCount.getAndSet(0);
}
public boolean isPartialSample() {
return true;
}
};
statisticsManager.addStatistic(TRAFFIC_KEY, packetTrafficStatistic);
}
}
\ No newline at end of file
/**
* $RCSfile $
* $Revision $
* $Date $
*
* Copyright (C) 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.openfire.reporting.stats;
import org.jivesoftware.openfire.plugin.MonitoringPlugin;
import org.jivesoftware.openfire.archive.Conversation;
import org.jivesoftware.openfire.archive.ConversationManager;
import org.jivesoftware.openfire.reporting.graph.GraphEngine;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.stats.Statistic;
import org.jivesoftware.openfire.user.UserNameManager;
import org.jivesoftware.openfire.user.UserNotFoundException;
import org.jivesoftware.util.JiveGlobals;
import org.jivesoftware.util.LocaleUtils;
import org.jivesoftware.util.Log;
import org.jivesoftware.util.StringUtils;
import org.xmpp.packet.JID;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.text.DateFormat;
import java.text.NumberFormat;
import java.util.*;
/**
* Provides the server side callbacks for client side JavaScript functions for
* the stats dashboard page.
*
* @author Aaron Johnson
*/
public class StatsAction {
/**
* Retrieves a map containing the high / low and current count statistics
* for the 'sessions', 'conversations' and 'packet_count' statistics.
* @return map containing 3 maps (keys = 'sessions, 'conversations' and
* 'packet_count') each containing an array of int (low value, high value
* and current value).
*/
public Map<String, Map> getUpdatedStats(String timePeriod) {
Map<String, Map> results = new HashMap<String, Map>();
long[] startAndEnd = GraphEngine.parseTimePeriod(timePeriod);
String[] stats = new String[] {"sessions", "conversations", "packet_count",
"proxyTransferRate", "muc_rooms", "server_sessions", "server_bytes"};
for (String stat : stats) {
results.put(stat, getUpdatedStat(stat, startAndEnd));
}
return results;
}
/**
* Retrieve a a single stat update given a stat name and the name of a
* time period.
* @param statkey
* @param timePeriod
* @return map containing keys 'low', 'high' and 'count'.
*/
public Map getUpdatedStat(String statkey, String timePeriod) {
long[] startAndEnd = GraphEngine.parseTimePeriod(timePeriod);
return getUpdatedStat(statkey, startAndEnd);
}
private Map getUpdatedStat(String statkey, long[] timePeriod) {
MonitoringPlugin plugin = (MonitoringPlugin)XMPPServer.getInstance().getPluginManager().getPlugin("monitoring");
StatsViewer viewer = (StatsViewer)plugin.getModule(StatsViewer.class);
String[] lowHigh = getLowAndHigh(statkey, timePeriod);
Map stat = new HashMap();
stat.put("low", lowHigh[0]);
stat.put("high", lowHigh[1]);
stat.put("count", (int)viewer.getCurrentValue(statkey)[0]);
return stat;
}
/**
* Retrieves the last n conversations from the system that were created after
* the given conversationID.
*
* @param count the count of conversations to return.
* @param mostRecentConversationID the last conversationID that has been retrieved.
* @return a List of Map objects.
*/
public List<Map<String, Long>> getNLatestConversations(int count, long mostRecentConversationID) {
// TODO Fix plugin name 2 lines below and missing classes
List<Map<String, Long>> cons = new ArrayList<Map<String, Long>>();
MonitoringPlugin plugin = (MonitoringPlugin)XMPPServer.getInstance().getPluginManager().getPlugin("monitoring");
ConversationManager conversationManager = (ConversationManager)plugin.getModule(ConversationManager.class);
Collection<Conversation> conversations = conversationManager.getConversations();
List<Conversation> lConversations = Arrays.asList(conversations.toArray(new Conversation[conversations.size()]));
Collections.sort(lConversations, conversationComparator);
int counter = 0;
for (Iterator<Conversation> i = lConversations.iterator(); i.hasNext() && counter < count;) {
Conversation con = i.next();
if (mostRecentConversationID == con.getConversationID()) {
break;
} else {
Map mCon = new HashMap();
mCon.put("conversationid", con.getConversationID());
String users[];
int usersIdx = 0;
if (con.getRoom() == null) {
users = new String[con.getParticipants().size()];
for (JID jid : con.getParticipants()) {
String identifier = jid.toBareJID();
try {
identifier = UserNameManager.getUserName(jid, jid.toBareJID());
}
catch (UserNotFoundException e) {
// Ignore
}
users[usersIdx++] = StringUtils.abbreviate(identifier, 20);
}
}
else {
users = new String[2];
users[0] = LocaleUtils.getLocalizedString("dashboard.group_conversation", "monitoring");
try {
users[1] = "(<i>" + LocaleUtils.getLocalizedString("muc.room.summary.room") +
": <a href='../../muc-room-occupants.jsp?roomName=" +
URLEncoder.encode(con.getRoom().getNode(), "UTF-8") + "'>" + con.getRoom().getNode() +
"</a></i>)";
} catch (UnsupportedEncodingException e) {
Log.error(e.getMessage(), e);
}
}
mCon.put("users", users);
mCon.put("lastactivity", formatTimeLong(con.getLastActivity()));
mCon.put("messages", con.getMessageCount());
cons.add(0, mCon);
counter++;
}
}
return cons;
}
/**
* Given a statistic key and a start date, end date and number of datapoints, returns
* a String[] containing the low and high values (in that order) for the given time period.
*
* @param key the name of the statistic to return high and low values for.
* @param timePeriod start date, end date and number of data points.
* @return low and high values for the given time period / number of datapoints
*/
public static String[] getLowAndHigh(String key, long[] timePeriod) {
MonitoringPlugin plugin = (MonitoringPlugin)XMPPServer.getInstance().getPluginManager().getPlugin("monitoring");
StatsViewer viewer = (StatsViewer)plugin.getModule(StatsViewer.class);
Statistic.Type type = viewer.getStatistic(key)[0].getStatType();
double[] lows = viewer.getMin(key, timePeriod[0], timePeriod[1], (int)timePeriod[2]);
double[] highs = viewer.getMax(key, timePeriod[0], timePeriod[1], (int)timePeriod[2]);
String low;
NumberFormat format = NumberFormat.getNumberInstance();
format.setMaximumFractionDigits(0);
if(lows.length > 0) {
if(type == Statistic.Type.count) {
low = String.valueOf((int) lows[0]);
}
else {
double l = lows[0];
if(Double.isNaN(l)) {
l = 0;
}
low = format.format(l);
}
}
else {
low = String.valueOf(0);
}
String high;
if(highs.length > 0) {
if(type == Statistic.Type.count) {
high = String.valueOf((int) highs[0]);
}
else {
double h= highs[0];
if(Double.isNaN(h)) {
h = 0;
}
high = format.format(h);
}
}
else {
high = String.valueOf(0);
}
return new String[]{low, high};
}
private Comparator<Conversation> conversationComparator = new Comparator<Conversation>() {
public int compare(Conversation conv1, Conversation conv2) {
return conv2.getLastActivity().compareTo(conv1.getLastActivity());
}
};
/**
* Formats a given time using the <code>DateFormat.MEDIUM</code>. In the 'en' locale, this
* should result in a time formatted like this: 4:59:23 PM. The seconds are necessary when
* displaying time in the conversation scroller.
* @param time
* @return string a date formatted using DateFormat.MEDIUM
*/
public static String formatTimeLong(Date time) {
DateFormat formatter = DateFormat.getTimeInstance(DateFormat.MEDIUM, JiveGlobals.getLocale());
formatter.setTimeZone(JiveGlobals.getTimeZone());
return formatter.format(time);
}
}
/**
* $Revision: 3034 $
* $Date: 2005-11-04 21:02:33 -0300 (Fri, 04 Nov 2005) $
*
* Copyright (C) 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.openfire.reporting.stats;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TimerTask;
import org.jivesoftware.openfire.cluster.ClusterManager;
import org.jivesoftware.openfire.reporting.util.TaskEngine;
import org.jivesoftware.openfire.stats.Statistic;
import org.jivesoftware.openfire.stats.StatisticsManager;
import org.jivesoftware.util.JiveGlobals;
import org.jivesoftware.util.cache.CacheFactory;
import org.jrobin.core.ConsolFuns;
import org.jrobin.core.DsTypes;
import org.jrobin.core.FetchData;
import org.jrobin.core.RrdBackendFactory;
import org.jrobin.core.RrdDb;
import org.jrobin.core.RrdDef;
import org.jrobin.core.RrdException;
import org.jrobin.core.Sample;
import org.picocontainer.Startable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The stats workhorse. Handles the job of sampling the different statistics existing in
* the system and persiting them to the database. Also, it tracks through a <i>StatDefinition</i>
* for each stat all the meta information related to a stat.
*
* @author Alexander Wenckus
*/
public class StatsEngine implements Startable {
private static final Logger Log = LoggerFactory.getLogger(StatsEngine.class);
private static final int STAT_RESOULUTION = 60;
private final TaskEngine taskEngine;
private final StatisticsManager statsManager;
private final Map<String, StatDefinition> definitionMap = new HashMap<String, StatDefinition>();
private final Map<String, List<StatDefinition>> multiMap = new HashMap<String, List<StatDefinition>>();
private SampleTask samplingTask = new SampleTask();
/**
* The default constructor used by the plugin container.
*
* @param taskEngine Used to execute tasks.
*/
public StatsEngine(TaskEngine taskEngine) {
this.taskEngine = taskEngine;
statsManager = StatisticsManager.getInstance();
}
public void start() {
try {
// Set that RRD files will be stored in the database
RrdBackendFactory.registerAndSetAsDefaultFactory(new RrdSqlBackendFactory());
// After 10 milliseconds begin sampling in 60 second intervals. Note: We need to start
// asap so that the UI can access this info upon start up
taskEngine.scheduleAtFixedRate(samplingTask, 10, STAT_RESOULUTION * 1000L);
}
catch (RrdException e) {
Log.error("Error initializing RrdbPool.", e);
}
}
public void stop() {
// Clean-up sampling task
samplingTask.cancel();
}
private void checkDatabase(StatDefinition[] def) throws RrdException, IOException {
File directory = new File(getStatsDirectroy());
if (directory.exists()) {
// check if the rrd exists
File rrdFile = new File(getRrdFilePath(def[0].getDbPath()));
if (rrdFile.exists() && rrdFile.canRead()) {
try {
// Import existing RRD file into the DB
RrdSqlBackend.importRRD(def[0].getDbPath(), rrdFile);
// Delete the RRD file
rrdFile.delete();
} catch (IOException e) {
Log.error("Error importing rrd file: " + rrdFile, e);
}
}
}
// check if the rrd exists
if (!RrdSqlBackend.exists(def[0].getDbPath())) {
RrdDb db = null;
try {
RrdDef rrdDef = new RrdDef(def[0].getDbPath(), STAT_RESOULUTION);
for (StatDefinition stat : def) {
String dsType = determineDsType(stat.getStatistic().getStatType());
rrdDef.addDatasource(stat.getDatasourceName(), dsType, 5 * STAT_RESOULUTION, 0,
Double.NaN);
}
// Every minute for 1 hour.
rrdDef.addArchive(((DefaultStatDefinition) def[0]).
consolidationFunction, 0.5, 1, 60);
// Every half-hour for 1 day.
rrdDef.addArchive(ConsolFuns.CF_AVERAGE, 0.5, 30, 48);
// Every day for 5 years.
rrdDef.addArchive(ConsolFuns.CF_AVERAGE, 0.5, 1440, 1825);
// Every week for 5 years.
rrdDef.addArchive(ConsolFuns.CF_AVERAGE, 0.5, 10080, 260);
// Every month for 5 years.
rrdDef.addArchive(ConsolFuns.CF_AVERAGE, 0.5, 43200, 60);
db = new RrdDb(rrdDef);
}
finally {
if(db != null) {
db.close();
}
}
}
}
private String determineDsType(Statistic.Type statType) {
return DsTypes.DT_GAUGE;
}
/**
* Returns the path to the RRD file.
*
* @param datasourceName the name of the data source.
* @return the path to the RRD file.
*/
private String getRrdFilePath(String datasourceName) {
return getStatsDirectroy() + datasourceName + ".rrd";
}
/**
* Returns the directory in which all of the stat databases will be stored.
*
* @return Returns the directory in which all of the stat databases will be stored.
*/
private String getStatsDirectroy() {
return JiveGlobals.getHomeDirectory() + File.separator + "monitoring"
+ File.separator + "stats" + File.separator;
}
private StatDefinition createDefintion(String key) {
StatDefinition def = definitionMap.get(key);
if (def == null) {
Statistic statistic = statsManager.getStatistic(key);
String statGroup = statsManager.getMultistatGroup(key);
try {
def = new DefaultStatDefinition(statGroup != null ? statGroup : key, key, statistic);
// If the definition is a part of a group check to see all defiintions have been
// made for that group
StatDefinition[] definitions;
if (statGroup != null) {
definitions = checkAndCreateGroup(statGroup, def, true);
}
else {
definitions = new StatDefinition[]{def};
multiMap.put(key, Arrays.asList(definitions));
}
if (definitions != null) {
checkDatabase(definitions);
}
definitionMap.put(key, def);
}
catch (RrdException e) {
Log.error("Error creating database definition", e);
}
catch (IOException e) {
Log.error("Error creating database definition", e);
}
}
return def;
}
/**
* Checks to see that all StatDefinitions for a stat group have been created. If they have
* then an array of the StatDefinitions will be returned, if they haven't Null will be returned.
* <p>
* The purpose of this is to know when a database should be initialized, after all the StatDefinitions
* have been created.
*
* @param statGroup The statGroup being checked
* @param def The statdefinition that is being added to the statGroup
* @return Null if the statgroup is completely defined and an array of statdefinitions if it is.
*/
private StatDefinition[] checkAndCreateGroup(String statGroup, StatDefinition def,
boolean shouldCreate)
{
List<StatDefinition> statList = multiMap.get(statGroup);
if (shouldCreate && statList == null) {
statList = new ArrayList<StatDefinition>();
multiMap.put(statGroup, statList);
}
if (statList == null) {
return null;
}
if (shouldCreate) {
statList.add(def);
}
StatDefinition[] definitions;
if (statsManager.getStatGroup(statGroup).size() == statList.size()) {
definitions = statList.toArray(new StatDefinition[statList.size()]);
}
else {
definitions = null;
}
return definitions;
}
/**
* Returns the last minute that passed in seconds since the epoch.
*
* @return the last minute that passed in seconds since the epoch.
*/
private static long getLastMinute() {
Calendar calendar = Calendar.getInstance();
calendar.setTimeInMillis(System.currentTimeMillis());
calendar.set(Calendar.SECOND, 0);
return calendar.getTimeInMillis() / 1000;
}
/**
* Returns the definition or definitions related to a statkey. There can be multiple
* definitions if a stat is a multistat.
*
* @param statKey The key for which the definition is desired.
* @return Returns the definition or definitions related to a statkey. There can be multiple
* definitions if a stat is a multistat.
*/
StatDefinition[] getDefinition(String statKey) {
List<StatDefinition> defs = multiMap.get(statKey);
if (defs == null) {
StatDefinition def = definitionMap.get(statKey);
if (def != null) {
return new StatDefinition[] {def};
}
else {
return null;
}
}
else {
return defs.toArray(new StatDefinition[defs.size()]);
}
}
/**
* Returns any multistat group names and any stats that are not part of a multistat.
*
* @return Returns any multistat group names and any stats that are not part of a multistat.
*/
String [] getAllHighLevelNames() {
Set<String> keySet = multiMap.keySet();
return keySet.toArray(new String[keySet.size()]);
}
/**
* The task which samples statistics and persits them to the database.
*
* @author Alexander Wenckus
*/
private class SampleTask extends TimerTask {
private long lastSampleTime = 0;
@Override
public void run() {
if (!ClusterManager.isSeniorClusterMember()) {
// Create statistics definitions but do not sample them since we are not the senior cluster member
for (Map.Entry<String, Statistic> statisticEntry : statsManager.getAllStatistics()) {
String key = statisticEntry.getKey();
StatDefinition def = createDefintion(key);
// Check to see if this stat belongs to a multi-stat and if that multi-stat group
// has been completly defined
String group = statsManager.getMultistatGroup(key);
if (group != null) {
checkAndCreateGroup(group, def, false);
}
}
return;
}
long newTime = getLastMinute();
if (lastSampleTime != 0 && newTime <= lastSampleTime) {
Log.warn("Sample task not run because less then a second has passed since last " +
"sample.");
return;
}
lastSampleTime = newTime;
// Gather sample statistics from remote cluster nodes
Collection<Object> remoteSamples = CacheFactory.doSynchronousClusterTask(new GetStatistics(), false);
List<String> sampledStats = new ArrayList<String>();
for (Map.Entry<String, Statistic> statisticEntry : statsManager.getAllStatistics()) {
String key = statisticEntry.getKey();
StatDefinition def = createDefintion(key);
// Check to see if this stat belongs to a multi-stat and if that multi-stat group
// has been completly defined
String group = statsManager.getMultistatGroup(key);
StatDefinition [] definitions;
if (group != null) {
definitions = checkAndCreateGroup(group, def, false);
if (definitions == null || sampledStats.contains(def.getDatasourceName())) {
continue;
}
}
else {
definitions = new StatDefinition[]{def};
}
RrdDb db = null;
try {
newTime = getLastMinute();
if (def.lastSampleTime <= 0) {
for(StatDefinition definition : definitions) {
definition.lastSampleTime = newTime;
// It is possible that this plugin and thus the StatsEngine didn't
// start when Openfire started so we want to put the stats in a known
// state for proper sampling.
sampleStat(key, definition);
}
continue;
}
db = new RrdDb(def.getDbPath(), false);
// We want to double check the last sample time recorded in the db so as to
// prevent the log files from being inundated if more than one instance of
// Openfire is updating the same database. Also, if there is a task taking a
// long time to complete
if(newTime <= db.getLastArchiveUpdateTime()) {
Log.warn("Sample time of " + newTime + " for statistic " + key + " is " +
"invalid.");
}
Sample sample = db.createSample(newTime);
if (Log.isDebugEnabled()) {
Log.debug("Stat: " + db.getPath() + ". Last sample: " + db.getLastUpdateTime() +
". New sample: " + sample.getTime());
}
for (StatDefinition definition : definitions) {
// Get a statistic sample of this JVM
double statSample = sampleStat(key, definition);
// Add up samples of remote cluster nodes
for (Object nodeResult : remoteSamples) {
Map<String, Double> nodeSamples = (Map<String, Double>) nodeResult;
Double remoteSample = nodeSamples.get(key);
if (remoteSample != null) {
statSample += remoteSample;
}
}
// Update sample with values
sample.setValue(definition.getDatasourceName(), statSample);
sampledStats.add(definition.getDatasourceName());
definition.lastSampleTime = newTime;
definition.lastSample = statSample;
}
sample.update();
}
catch (IOException e) {
Log.error("Error sampling for statistic " + key, e);
}
catch (RrdException e) {
Log.error("Error sampling for statistic " + key, e);
}
finally {
if (db != null) {
try {
db.close();
}
catch (IOException e) {
Log.error("Error releasing db resource", e);
}
}
}
}
}
/**
* Profiles the sampling to make sure that it does not take longer than half a second to
* complete, if it does, a warning is logged.
*
* @param statKey the key related to the statistic.
* @param definition the statistic definition for the stat to be sampled.
* @return the sample.
*/
private double sampleStat(String statKey, StatDefinition definition) {
long start = System.currentTimeMillis();
double sample = definition.getStatistic().sample();
if (System.currentTimeMillis() - start >= 500) {
Log.warn("Stat " + statKey + " took longer than a second to sample.");
}
return sample;
}
}
/**
* Class to process all information retrieved from the stats databases. It also retains
* any meta information related to these databases.
*
* @author Alexander Wenckus
*/
private class DefaultStatDefinition extends StatDefinition {
private String consolidationFunction;
DefaultStatDefinition(String dbPath, String datasourceName, Statistic stat) {
super(dbPath, datasourceName, stat);
this.consolidationFunction = determineConsolidationFun(stat.getStatType());
}
private String determineConsolidationFun(Statistic.Type type) {
switch (type) {
case count:
return ConsolFuns.CF_LAST;
default:
return ConsolFuns.CF_AVERAGE;
}
}
@Override
public double[][] getData(long startTime, long endTime) {
return fetchData(consolidationFunction, startTime, endTime, -1);
}
@Override
public double[][] getData(long startTime, long endTime, int dataPoints) {
// Our greatest datapoints is 60 so if it is something less than that
// then we want an average.
return fetchData((dataPoints != 60 ? ConsolFuns.CF_AVERAGE : consolidationFunction),
startTime, endTime, dataPoints);
}
@Override
public long getLastSampleTime() {
return lastSampleTime;
}
@Override
public double getLastSample() {
return lastSample;
}
@Override
public double[] getMax(long startTime, long endTime) {
return getMax(startTime, endTime, 1);
}
private double discoverMax(double[] doubles) {
double max = 0;
for (double d : doubles) {
if (d > max) {
max = d;
}
}
return max;
}
private double[][] fetchData(String function, long startTime, long endTime, int dataPoints) {
RrdDb db = null;
try {
db = new RrdDb(getDbPath(), true);
FetchData data;
if (dataPoints > 0) {
data = db.createFetchRequest(function, startTime, endTime,
getResolution(startTime, endTime, dataPoints)).fetchData();
}
else {
data = db.createFetchRequest(function, startTime, endTime).fetchData();
}
return data.getValues();
}
catch (IOException e) {
Log.error("Error initializing Rrdb", e);
}
catch (RrdException e) {
Log.error("Error initializing Rrdb", e);
}
finally {
try {
if (db != null) {
db.close();
}
}
catch (IOException e) {
Log.error("Unable to release Rrdb resources",e);
}
}
return null;
}
private long getResolution(long startTime, long endTime, int dataPoints) {
return (endTime - startTime) / (dataPoints * 60);
}
@Override
public double[] getMin(long startTime, long endTime) {
return getMin(startTime, endTime, 1);
}
@Override
public double[] getMin(long startTime, long endTime, int dataPoints) {
double[][] fetchedData = fetchData(consolidationFunction, startTime,
endTime, dataPoints);
if (fetchedData != null) {
double[] toReturn = new double[fetchedData.length];
for (int i = 0; i < fetchedData.length; i++) {
toReturn[i] = discoverMin(fetchedData[i]);
}
return toReturn;
}
return new double[] { 0 };
}
@Override
public double[] getMax(long startTime, long endTime, int dataPoints) {
double[][] fetchedData = fetchData(consolidationFunction, startTime,
endTime, dataPoints);
if (fetchedData != null) {
double[] toReturn = new double[fetchedData.length];
for (int i = 0; i < fetchedData.length; i++) {
toReturn[i] = discoverMax(fetchedData[i]);
}
return toReturn;
}
return new double[] { 0 };
}
private double discoverMin(double[] doubles) {
double min = doubles[0];
for (double d : doubles) {
if (d < min) {
min = d;
}
}
return min;
}
}
}
\ No newline at end of file
/**
* $Revision $
* $Date $
*
* Copyright (C) 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.openfire.reporting.stats;
import org.jivesoftware.openfire.stats.Statistic;
/**
* Provides a view into the stats being tracked by the stats engine.
*
* @author Alexander Wenckus
*/
public interface StatsViewer {
/**
* Returns any multistat group keys and any keys that are not part of a multistat.
*
* @return any multistat group names and any stats that are not part of a multistat.
*/
String [] getAllHighLevelStatKeys();
/**
* Returns all statistic objects for a related key. For instance if the key is a multistat
* more than one Statistic will be returned. Statistic objects contain the meta information
* for a stat.
*
* @param statKey the key of the stat.
* @return all statistic objects for a related key.
* @throws IllegalArgumentException if the stat related to the statKey does not exist.
*/
Statistic[] getStatistic(String statKey);
/**
* Returns the last time this statistic was sampled.
*
* @param key the key for the statistic.
* @return the last time this statistic was sampled in milliseconds since the epoch.
*/
long getLastSampleTime(String key);
/**
* Retrieves the data for the related stat between the specified time period.
*
* @param key the key for the stat of which the data is being retrieved.
* @param startTime the lower bound of the time period in milliseconds since the epoch.
* @param endTime the upper bound of the time period in milliseconds since the epoch.
* @return an array of doubles representing the stat. If the stat is a multistat,
* more than one array is returned.
*/
double[][] getData(String key, long startTime, long endTime);
/**
* Retrieves the data for the related stat between the time period specified. The number
* of datapoints indicates how many doubles are to be returned for each stat, the
* statviewer will make a best effort ot conform to this, but if the backing datastore doesn't
* have a resolution for that particular amount of points then the closest match will be
* returned.
*
* @param key the key for the stat of which the data is being retrieved.
* @param startTime the lower bound of the time period.
* @param endTime the upper bound of the time period.
* @param dataPoints the number of desired datapoints
* @return an array of doubles representing the stat. If the stat is a multistat,
* more than one array is returned.
* @deprecated will be removed pending the completion of #getData(String, TimePeriod)
*/
@Deprecated
double[][] getData(String key, long startTime, long endTime, int dataPoints);
/**
* Retrieves the data for the related stat for the time period.
*
* @param key the key for the statistic.
* @param timePeriod the timeperiod for which the data is desired.
* @return an array of doubles representing the stat. If the stat is a multistat,
* more than one array is returned.
* @see #getData(String, long, long)
*/
StatView getData(String key, TimePeriod timePeriod);
/**
* Returns an array of doubles which is the max value between the time periods.
* If the provided key relates to a multistat, each array element relates to a max
* for that particular stat. If it is not a multistat, the array will be of length 1.
*
* @param key the multistat or stat key related to the stat.
* @param startTime the start time or lower range.
* @param endTime the end time or upper range
* @return an array of doubles which is the max value between the time periods.
*/
double[] getMax(String key, long startTime, long endTime);
double[] getMax(String key, long startTime, long endTime, int dataPoints);
/**
* Returns an array of doubles which is the max value for a time period.
* If the provided key relates to a multistat, each array element relates to a max
* for that particular stat. If it is not a multistat, the array will be of length 1.
*
* @param key the multistat or stat key related to the stat.
* @param timePeriod the time period over which the max should be returned.
* @return an array of doubles which is the max value for the time period.
*/
double[] getMax(String key, TimePeriod timePeriod);
/**
* Returns an array of doubles which is the minimum value between the time periods.
* If the provided key relates to a multistat, each array element relates to a minimum
* for that particular stat. If it is not a multistat, the array will be of length 1.
*
* @param key the multistat or stat key related to the stat.
* @param startTime the start time or lower range.
* @param endTime the end time or upper range
* @return an array of doubles which is the min value between the time periods.
*/
double[] getMin(String key, long startTime, long endTime);
double[] getMin(String key, long startTime, long endTime, int dataPoints);
/**
* Returns an array of doubles which is the minimum value for a time period.
* If the provided key relates to a multistat, each array element relates to a minimum
* for that particular stat. If it is not a multistat, the array will be of length 1.
*
* @param key the multistat or stat key related to the stat.
* @param timePeriod the time period over which the min should be returned.
* @return an array of doubles which is the min value for the time period.
*/
double[] getMin(String key, TimePeriod timePeriod);
/**
* Returns the last recorded value for a stat.
*
* @param key the key for the stat.
* @return the last value for a stat.
* @see #getLastSampleTime(String)
*/
double[] getCurrentValue(String key);
/**
* An enumeration for time period choices. A time period helps the stats viewer
* determine the period of time which data should be returned, it also provides a
* suggestion on the number of datapoints that should be provided.
*/
public enum TimePeriod {
last_hour(3600, 15),
last_day(43200, 15);
private long timePeriod;
private int dataPoints;
private TimePeriod(long timePeriod, int dataPoints) {
this.timePeriod = timePeriod;
this.dataPoints = dataPoints;
}
/**
* Takes an end time and returns a relative start time based off of the time period
* this method is being operated off of.
*
* @param endTime the end time, the time period is substracted from this to
* determine the start time.
* @return the determined start time.
*/
public long getStartTime(long endTime) {
return endTime - timePeriod;
}
/**
* A suggestion for the number of data points that should be returned.
*
* @return a suggestion for the number of data points that should be returned.
*/
public int getDataPoints() {
return dataPoints;
}
public long getTimePeriod() {
return timePeriod;
}
}
/**
* A snapshot of a stat in time.
*/
public final class StatView {
private long startTime;
private long endTime;
private double[][] data;
public StatView(long startTime, long endTime, double[][] data) {
this.startTime = startTime;
this.endTime = endTime;
this.data = data;
}
/**
* The starting time of the snap shot.
*
* @return the starting time of the snap shot.
*/
public long getStartTime() {
return startTime;
}
/**
* The end time of the snap shot.
*
* @return the end time of the snap shot.
*/
public long getEndTime() {
return endTime;
}
/**
* The data related to the snap shot.
*
* @return The data related to the snap shot.
*/
public double[][] getData() {
return data;
}
}
}
\ No newline at end of file
/**
* $Revision$
* $Date$
*
* Copyright (C) 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.openfire.reporting.util;
import org.picocontainer.Disposable;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Performs tasks using worker threads. It also allows tasks to be scheduled to be
* run at future dates. This class mimics relevant methods in both
* {@link ExecutorService} and {@link Timer}. Any {@link TimerTask} that's
* scheduled to be run in the future will automatically be run using the thread
* executor's thread pool. This means that the standard restriction that TimerTasks
* should run quickly does not apply.
*
* @author Matt Tucker
*/
public class TaskEngine implements Disposable {
private static TaskEngine instance = new TaskEngine();
/**
* Returns a task engine instance (singleton).
*
* @return a task engine.
*/
public static TaskEngine getInstance() {
return instance;
}
private Timer timer;
private ExecutorService executor;
private final Map<TimerTask, TimerTaskWrapper> wrappedTasks = new HashMap<TimerTask, TimerTaskWrapper>();
/**
* Constructs a new task engine.
*/
private TaskEngine() {
timer = new Timer("timer-monitoring", true);
executor = Executors.newCachedThreadPool(new ThreadFactory() {
final AtomicInteger threadNumber = new AtomicInteger(1);
public Thread newThread(Runnable runnable) {
// Use our own naming scheme for the threads.
Thread thread = new Thread(Thread.currentThread().getThreadGroup(), runnable,
"pool-monitoring" + threadNumber.getAndIncrement(), 0);
// Make workers daemon threads.
thread.setDaemon(true);
if (thread.getPriority() != Thread.NORM_PRIORITY) {
thread.setPriority(Thread.NORM_PRIORITY);
}
return thread;
}
});
}
/**
* Submits a Runnable task for execution and returns a Future
* representing that task.
*
* @param task the task to submit.
* @return a Future representing pending completion of the task,
* and whose <tt>get()</tt> method will return <tt>null</tt>
* upon completion.
* @throws java.util.concurrent.RejectedExecutionException if task cannot be scheduled
* for execution.
* @throws NullPointerException if task null.
*/
public Future<?> submit(Runnable task) {
return executor.submit(task);
}
/**
* Schedules the specified task for execution after the specified delay.
*
* @param task task to be scheduled.
* @param delay delay in milliseconds before task is to be executed.
* @throws IllegalArgumentException if <tt>delay</tt> is negative, or
* <tt>delay + System.currentTimeMillis()</tt> is negative.
* @throws IllegalStateException if task was already scheduled or
* cancelled, or timer was cancelled.
*/
public void schedule(TimerTask task, long delay) {
TimerTaskWrapper taskWrapper = new TimerTaskWrapper(task);
synchronized (wrappedTasks) {
wrappedTasks.put(task, taskWrapper);
}
timer.schedule(taskWrapper, delay);
}
/**
* Schedules the specified task for execution at the specified time. If
* the time is in the past, the task is scheduled for immediate execution.
*
* @param task task to be scheduled.
* @param time time at which task is to be executed.
* @throws IllegalArgumentException if <tt>time.getTime()</tt> is negative.
* @throws IllegalStateException if task was already scheduled or
* cancelled, timer was cancelled, or timer thread terminated.
*/
public void schedule(TimerTask task, Date time) {
TimerTaskWrapper taskWrapper = new TimerTaskWrapper(task);
synchronized (wrappedTasks) {
wrappedTasks.put(task, taskWrapper);
}
timer.schedule(taskWrapper, time);
}
/**
* Schedules the specified task for repeated <i>fixed-delay execution</i>,
* beginning after the specified delay. Subsequent executions take place
* at approximately regular intervals separated by the specified period.
*
* <p>In fixed-delay execution, each execution is scheduled relative to
* the actual execution time of the previous execution. If an execution
* is delayed for any reason (such as garbage collection or other
* background activity), subsequent executions will be delayed as well.
* In the long run, the frequency of execution will generally be slightly
* lower than the reciprocal of the specified period (assuming the system
* clock underlying <tt>Object.wait(long)</tt> is accurate).
*
* <p>Fixed-delay execution is appropriate for recurring activities
* that require "smoothness." In other words, it is appropriate for
* activities where it is more important to keep the frequency accurate
* in the short run than in the long run. This includes most animation
* tasks, such as blinking a cursor at regular intervals. It also includes
* tasks wherein regular activity is performed in response to human
* input, such as automatically repeating a character as long as a key
* is held down.
*
* @param task task to be scheduled.
* @param delay delay in milliseconds before task is to be executed.
* @param period time in milliseconds between successive task executions.
* @throws IllegalArgumentException if <tt>delay</tt> is negative, or
* <tt>delay + System.currentTimeMillis()</tt> is negative.
* @throws IllegalStateException if task was already scheduled or
* cancelled, timer was cancelled, or timer thread terminated.
*/
public void schedule(TimerTask task, long delay, long period) {
TimerTaskWrapper taskWrapper = new TimerTaskWrapper(task);
synchronized (wrappedTasks) {
wrappedTasks.put(task, taskWrapper);
}
timer.schedule(taskWrapper, delay, period);
}
/**
* Schedules the specified task for repeated <i>fixed-delay execution</i>,
* beginning at the specified time. Subsequent executions take place at
* approximately regular intervals, separated by the specified period.
*
* <p>In fixed-delay execution, each execution is scheduled relative to
* the actual execution time of the previous execution. If an execution
* is delayed for any reason (such as garbage collection or other
* background activity), subsequent executions will be delayed as well.
* In the long run, the frequency of execution will generally be slightly
* lower than the reciprocal of the specified period (assuming the system
* clock underlying <tt>Object.wait(long)</tt> is accurate).
*
* <p>Fixed-delay execution is appropriate for recurring activities
* that require "smoothness." In other words, it is appropriate for
* activities where it is more important to keep the frequency accurate
* in the short run than in the long run. This includes most animation
* tasks, such as blinking a cursor at regular intervals. It also includes
* tasks wherein regular activity is performed in response to human
* input, such as automatically repeating a character as long as a key
* is held down.
*
* @param task task to be scheduled.
* @param firstTime First time at which task is to be executed.
* @param period time in milliseconds between successive task executions.
* @throws IllegalArgumentException if <tt>time.getTime()</tt> is negative.
* @throws IllegalStateException if task was already scheduled or
* cancelled, timer was cancelled, or timer thread terminated.
*/
public void schedule(TimerTask task, Date firstTime, long period) {
TimerTaskWrapper taskWrapper = new TimerTaskWrapper(task);
synchronized (wrappedTasks) {
wrappedTasks.put(task, taskWrapper);
}
timer.schedule(taskWrapper, firstTime, period);
}
/**
* Schedules the specified task for repeated <i>fixed-rate execution</i>,
* beginning after the specified delay. Subsequent executions take place
* at approximately regular intervals, separated by the specified period.
*
* <p>In fixed-rate execution, each execution is scheduled relative to the
* scheduled execution time of the initial execution. If an execution is
* delayed for any reason (such as garbage collection or other background
* activity), two or more executions will occur in rapid succession to
* "catch up." In the long run, the frequency of execution will be
* exactly the reciprocal of the specified period (assuming the system
* clock underlying <tt>Object.wait(long)</tt> is accurate).
*
* <p>Fixed-rate execution is appropriate for recurring activities that
* are sensitive to <i>absolute</i> time, such as ringing a chime every
* hour on the hour, or running scheduled maintenance every day at a
* particular time. It is also appropriate for recurring activities
* where the total time to perform a fixed number of executions is
* important, such as a countdown timer that ticks once every second for
* ten seconds. Finally, fixed-rate execution is appropriate for
* scheduling multiple repeating timer tasks that must remain synchronized
* with respect to one another.
*
* @param task task to be scheduled.
* @param delay delay in milliseconds before task is to be executed.
* @param period time in milliseconds between successive task executions.
* @throws IllegalArgumentException if <tt>delay</tt> is negative, or
* <tt>delay + System.currentTimeMillis()</tt> is negative.
* @throws IllegalStateException if task was already scheduled or
* cancelled, timer was cancelled, or timer thread terminated.
*/
public void scheduleAtFixedRate(TimerTask task, long delay, long period) {
TimerTaskWrapper taskWrapper = new TimerTaskWrapper(task);
synchronized (wrappedTasks) {
wrappedTasks.put(task, taskWrapper);
}
timer.scheduleAtFixedRate(taskWrapper, delay, period);
}
/**
* Schedules the specified task for repeated <i>fixed-rate execution</i>,
* beginning at the specified time. Subsequent executions take place at
* approximately regular intervals, separated by the specified period.
*
* <p>In fixed-rate execution, each execution is scheduled relative to the
* scheduled execution time of the initial execution. If an execution is
* delayed for any reason (such as garbage collection or other background
* activity), two or more executions will occur in rapid succession to
* "catch up." In the long run, the frequency of execution will be
* exactly the reciprocal of the specified period (assuming the system
* clock underlying <tt>Object.wait(long)</tt> is accurate).
*
* <p>Fixed-rate execution is appropriate for recurring activities that
* are sensitive to <i>absolute</i> time, such as ringing a chime every
* hour on the hour, or running scheduled maintenance every day at a
* particular time. It is also appropriate for recurring activities
* where the total time to perform a fixed number of executions is
* important, such as a countdown timer that ticks once every second for
* ten seconds. Finally, fixed-rate execution is appropriate for
* scheduling multiple repeating timer tasks that must remain synchronized
* with respect to one another.
*
* @param task task to be scheduled.
* @param firstTime First time at which task is to be executed.
* @param period time in milliseconds between successive task executions.
* @throws IllegalArgumentException if <tt>time.getTime()</tt> is negative.
* @throws IllegalStateException if task was already scheduled or
* cancelled, timer was cancelled, or timer thread terminated.
*/
public void scheduleAtFixedRate(TimerTask task, Date firstTime, long period) {
TimerTaskWrapper taskWrapper = new TimerTaskWrapper(task);
synchronized (wrappedTasks) {
wrappedTasks.put(task, taskWrapper);
}
timer.scheduleAtFixedRate(taskWrapper, firstTime, period);
}
/**
* Cancels the execution of a scheduled task. {@link java.util.TimerTask#cancel()}
*
* @param task the scheduled task to cancel.
*/
public void cancelScheduledTask(TimerTask task) {
TaskEngine.TimerTaskWrapper taskWrapper;
synchronized (wrappedTasks) {
taskWrapper = wrappedTasks.remove(task);
}
if (taskWrapper != null) {
taskWrapper.cancel();
task.cancel();
}
}
public void dispose() {
executor.shutdownNow();
executor = null;
timer.cancel();
timer = null;
instance = null;
wrappedTasks.clear();
}
/**
* Wrapper class for a standard TimerTask. It simply executes the TimerTask
* using the executor's thread pool.
*/
private class TimerTaskWrapper extends TimerTask {
private TimerTask task;
public TimerTaskWrapper(TimerTask task) {
this.task = task;
}
@Override
public void run() {
executor.submit(task);
}
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment