Commit 19d1b41e authored by Leon Roy's avatar Leon Roy Committed by leonroy

Monitoring plugin 1.3.0 integrating Jive Monitoring Plugin and Stefan Reuter's...

Monitoring plugin 1.3.0 integrating Jive Monitoring Plugin and Stefan Reuter's Open Archive plugin to give both XEP-0136 support and group as well as individual chat archiving.

git-svn-id: http://svn.igniterealtime.org/svn/repos/openfire/trunk@13322 b35dd754-fafc-0310-a699-88a17e54d16e
parent 8a215713
......@@ -5,11 +5,11 @@
<name>Monitoring Service</name>
<description>Monitors conversations and statistics of the server.</description>
<author>Jive Software</author>
<version>1.2.0</version>
<date>12/1/2009</date>
<version>1.3.0</version>
<date>17/10/2012</date>
<minServerVersion>3.7.0</minServerVersion>
<databaseKey>monitoring</databaseKey>
<databaseVersion>0</databaseVersion>
<databaseVersion>1</databaseVersion>
<adminconsole>
<tab id="tab-server">
......
-- $Revision$
-- $Date$
INSERT INTO ofVersion (name, version) VALUES ('monitoring', 0);
INSERT INTO ofVersion (name, version) VALUES ('monitoring', 1);
CREATE TABLE ofConversation (
conversationID INTEGER NOT NULL,
......@@ -30,7 +30,9 @@ CREATE INDEX entConPar_jid_idx ON ofConParticipant (bareJID);
CREATE TABLE ofMessageArchive (
conversationID INTEGER NOT NULL,
fromJID VARCHAR(1024) NOT NULL,
fromJIDResource VARCHAR(255) NULL,
toJID VARCHAR(1024) NOT NULL,
toJIDResource VARCHAR(255) NULL,
sentDate BIGINT NOT NULL,
body LONG VARCHAR
);
......
// $Revision$
// $Date$
INSERT INTO ofVersion (name, version) VALUES ('monitoring', 0);
INSERT INTO ofVersion (name, version) VALUES ('monitoring', 1);
CREATE TABLE ofConversation (
conversationID BIGINT NOT NULL,
......@@ -30,7 +30,9 @@ CREATE INDEX ofConParticipant_jid_idx ON ofConParticipant (bareJID);
CREATE TABLE ofMessageArchive (
conversationID BIGINT NOT NULL,
fromJID VARCHAR(1024) NOT NULL,
fromJIDResource VARCHAR(255) NULL,
toJID VARCHAR(1024) NOT NULL,
toJIDResource VARCHAR(255) NULL,
sentDate BIGINT NOT NULL,
body LONGVARCHAR
);
......@@ -38,7 +40,7 @@ CREATE INDEX ofMessageArchive_con_idx ON ofMessageArchive (conversationID);
CREATE TABLE ofRRDs (
id VARCHAR(100) NOT NULL,
updatedDate BIGINT NOT NULL,
updatedDate BIGINT NOT NULL,
bytes VARBINARY NULL,
CONSTRAINT ofRRDs_pk PRIMARY KEY (id)
);
......
# $Revision$
# $Date$
INSERT INTO ofVersion (name, version) VALUES ('monitoring', 0);
INSERT INTO ofVersion (name, version) VALUES ('monitoring', 1);
CREATE TABLE ofConversation (
conversationID BIGINT NOT NULL,
......@@ -30,10 +30,12 @@ CREATE TABLE ofConParticipant (
CREATE TABLE ofMessageArchive (
conversationID BIGINT NOT NULL,
fromJID VARCHAR(255) NOT NULL,
fromJIDResource VARCHAR(100) NULL,
toJID VARCHAR(255) NOT NULL,
toJIDResource VARCHAR(100) NULL,
sentDate BIGINT NOT NULL,
body TEXT,
INDEX entMsgArchive_con_idx (conversationID)
INDEX gtmsMsgArchive_con_idx (conversationID)
);
CREATE TABLE ofRRDs (
......
-- $Revision$
-- $Date$
INSERT INTO ofVersion (name, version) VALUES ('monitoring', 0);
INSERT INTO ofVersion (name, version) VALUES ('monitoring', 1);
CREATE TABLE ofConversation (
conversationID INTEGER NOT NULL,
......@@ -30,7 +30,9 @@ CREATE INDEX ofConParticipant_jid_idx ON ofConParticipant (bareJID);
CREATE TABLE ofMessageArchive (
conversationID INTEGER NOT NULL,
fromJID VARCHAR2(1024) NOT NULL,
fromJIDResource VARCHAR2(255) NULL,
toJID VARCHAR2(1024) NOT NULL,
toJIDResource VARCHAR2(255) NULL,
sentDate INTEGER NOT NULL,
body LONG
);
......
-- $Revision$
-- $Date$
INSERT INTO ofVersion (name, version) VALUES ('monitoring', 0);
INSERT INTO ofVersion (name, version) VALUES ('monitoring', 1);
CREATE TABLE ofConversation (
conversationID INTEGER NOT NULL,
......@@ -30,7 +30,9 @@ CREATE INDEX ofConParticipant_jid_idx ON ofConParticipant (bareJID);
CREATE TABLE ofMessageArchive (
conversationID INTEGER NOT NULL,
fromJID VARCHAR(1024) NOT NULL,
fromJIDResource VARCHAR(1024) NULL,
toJID VARCHAR(1024) NOT NULL,
toJIDResource VARCHAR(1024) NULL,
sentDate BIGINT NOT NULL,
body TEXT
);
......
/* $Revision$ */
/* $Date$ */
INSERT INTO ofVersion (name, version) VALUES ('monitoring', 0);
INSERT INTO ofVersion (name, version) VALUES ('monitoring', 1);
CREATE TABLE ofConversation (
conversationID BIGINT NOT NULL,
......@@ -30,7 +30,9 @@ CREATE INDEX ofConParticipant_jid_idx ON ofConParticipant (bareJID);
CREATE TABLE ofMessageArchive (
conversationID BIGINT NOT NULL,
fromJID NVARCHAR(1024) NOT NULL,
fromJIDResource NVARCHAR(1024) NULL,
toJID NVARCHAR(1024) NOT NULL,
toJIDResource NVARCHAR(1024) NULL,
sentDate BIGINT NOT NULL,
body NTEXT
);
......
-- $Revision$
-- $Date$
ALTER TABLE ofMessageArchive ADD COLUMN fromJIDResource VARCHAR(255) NULL;
ALTER TABLE ofMessageArchive ADD COLUMN toJIDResource VARCHAR(255) NULL;
-- Update database version
UPDATE jiveVersion SET version = 1 WHERE name = 'monitoring';
\ No newline at end of file
-- $Revision$
-- $Date$
ALTER TABLE ofMessageArchive ADD COLUMN fromJIDResource VARCHAR(255) NULL;
ALTER TABLE ofMessageArchive ADD COLUMN toJIDResource VARCHAR(255) NULL;
-- Update database version
UPDATE jiveVersion SET version = 1 WHERE name = 'monitoring';
\ No newline at end of file
-- $Revision$
-- $Date$
ALTER TABLE ofMessageArchive ADD COLUMN fromJIDResource VARCHAR(255) NULL;
ALTER TABLE ofMessageArchive ADD COLUMN toJIDResource VARCHAR(255) NULL;
-- Update database version
UPDATE jiveVersion SET version = 1 WHERE name = 'monitoring';
\ No newline at end of file
-- $Revision$
-- $Date$
ALTER TABLE ofMessageArchive ADD fromJIDResource VARCHAR(255) NULL;
ALTER TABLE ofMessageArchive ADD toJIDResource VARCHAR(255) NULL;
-- Update database version
UPDATE jiveVersion SET version = 1 WHERE name = 'monitoring';
commit;
\ No newline at end of file
-- $Revision$
-- $Date$
ALTER TABLE ofMessageArchive ADD COLUMN fromJIDResource VARCHAR(255) NULL;
ALTER TABLE ofMessageArchive ADD COLUMN toJIDResource VARCHAR(255) NULL;
-- Update database version
UPDATE jiveVersion SET version = 1 WHERE name = 'monitoring';
\ No newline at end of file
-- $Revision$
-- $Date$
ALTER TABLE ofMessageArchive ADD fromJIDResource NVARCHAR(255) NULL;
ALTER TABLE ofMessageArchive ADD toJIDResource NVARCHAR(255) NULL;
-- Update database version
UPDATE jiveVersion SET version = 1 WHERE name = 'monitoring';
\ No newline at end of file
package com.reucon.openfire.plugin.archive;
import java.util.Date;
import org.jivesoftware.openfire.session.Session;
import org.xmpp.packet.JID;
import org.xmpp.packet.Message;
import com.reucon.openfire.plugin.archive.model.ArchivedMessage;
/**
* Factory to create model objects.
*/
public class ArchiveFactory {
private ArchiveFactory() {
}
public static ArchivedMessage createArchivedMessage(Session session,
Message message, ArchivedMessage.Direction direction, JID withJid) {
final ArchivedMessage archivedMessage;
archivedMessage = new ArchivedMessage(new Date(), direction, message
.getType().toString(), withJid);
archivedMessage.setSubject(message.getSubject());
archivedMessage.setBody(message.getBody());
return archivedMessage;
}
}
package com.reucon.openfire.plugin.archive;
import org.jivesoftware.openfire.session.Session;
import org.xmpp.packet.Message;
/**
* Adds messages to the archive.
*/
public interface ArchiveManager
{
/**
* Adds a message to the archive.
*
* @param session the session the message was received through.
* @param message the message to archive.
* @param incoming <code>true</code> if this a message received by the server, <code>false</code> if it
* is sent by the server.
*/
void archiveMessage(Session session, Message message, boolean incoming);
/**
* Sets the conversation timeout.<p>
* A new conversation is created if there no messages have been exchanged between two JIDs
* for the given timeout.
*
* @param conversationTimeout the conversation timeout to set in minutes.
*/
void setConversationTimeout(int conversationTimeout);
}
package com.reucon.openfire.plugin.archive;
/**
* Literals for configuration properties.
*/
public interface ArchiveProperties
{
// TODO: change the below to a separate property to allow archiving but disable/enable XEP-0136
String ENABLED = "conversation.metadataArchiving";
String INDEX_DIR = "archive.indexdir";
// Unnecessary since Open Archive Archive Manager no longer archives messages
String CONVERSATION_TIMEOUT = "conversation.idleTime";
}
package com.reucon.openfire.plugin.archive;
import com.reucon.openfire.plugin.archive.model.ArchivedMessage;
/**
* Consumes an ArchivedMessage.
*/
public interface ArchivedMessageConsumer
{
boolean consume(ArchivedMessage message);
}
package com.reucon.openfire.plugin.archive;
import com.reucon.openfire.plugin.archive.model.Conversation;
import java.util.Collection;
import java.util.Date;
/**
* Maintains an index for message retrieval.
*/
public interface IndexManager
{
/**
* Asynchronously indexes the given object.
* @param object the object to index.
* @return <code>true</code> if successfully queued for indexing, <code>false</code> otherwise.
*/
boolean indexObject(Object object);
/**
* Rebuilds the index.
*
* @return the number of messages indexed or -1 on error.
*/
int rebuildIndex();
Collection<String> searchParticipant(String token);
Collection<Conversation> findConversations(String[] participants, Date startDate, Date endDate, String keywords);
void destroy();
}
package com.reucon.openfire.plugin.archive;
import com.reucon.openfire.plugin.archive.model.ArchivedMessage;
import com.reucon.openfire.plugin.archive.model.Conversation;
import com.reucon.openfire.plugin.archive.model.Participant;
import com.reucon.openfire.plugin.archive.xep0059.XmppResultSet;
import java.util.Collection;
import java.util.Date;
import java.util.List;
/**
* Manages database persistence.
*/
public interface PersistenceManager
{
/**
* Creates a new archived message.
*
* @param message the message to create.
* @return <code>true</code> on success, <code>false</code> otherwise.
*/
boolean createMessage(ArchivedMessage message);
/**
* Selects all messages and passes each message to the given callback for processing.
*
* @param callback callback to process messages.
* @return number of messages processed.
*/
int processAllMessages(ArchivedMessageConsumer callback);
/**
* Creates a new conversation.
*
* @param conversation the conversation to create.
* @return <code>true</code> on success, <code>false</code> otherwise.
*/
boolean createConversation(Conversation conversation);
/**
* Updates the end time of a conversation. The conversation must be persisted.
*
* @param conversation conversation to update with id and endDate attributes not null.
* @return <code>true</code> on success, <code>false</code> otherwise.
*/
boolean updateConversationEnd(Conversation conversation);
/**
* Adds a new participant to a conversation.
*
* @param participant the participant to add.
* @param conversationId id of the conversation to add the participant to.
* @return <code>true</code> on success, <code>false</code> otherwise.
*/
boolean createParticipant(Participant participant, Long conversationId);
List<Conversation> findConversations(String[] participants, Date startDate, Date endDate);
/**
* Searches for conversations.
*
* @param startDate earliest start date of the conversation to find or <code>null</code> for any.
* @param endDate latest end date of the conversation to find or <code>null</code> for any.
* @param owner bare jid of the owner of the conversation to find or <code>null</code> for any.
* @param with bare jid of the communication partner or <code>null</code> for any. This is either
* the jid of another XMPP user or the jid of a group chat.
* @return the conversations that matched search critera without messages and participants.
*/
Collection<Conversation> findConversations(Date startDate, Date endDate, String owner, String with, XmppResultSet xmppResultSet);
Collection<Conversation> getActiveConversations(int conversationTimeout);
List<Conversation> getConversations(Collection<Long> conversationIds);
/**
* Returns the conversation with the given owner, with and start time including participants and messages.
*
* @param ownerJid bare jid of the conversation's owner.
* @param withJid bare jid of the communication partner.
* @param start exact start time
* @return the matching conversation or <code>null</code> if none matches.
*/
Conversation getConversation(String ownerJid, String withJid, Date start);
/**
* Returns the conversation with the given id including participants and messages.
*
* @param conversationId id of the conversation to retrieve.
* @return the matching conversation or <code>null</code> if none matches.
*/
Conversation getConversation(Long conversationId);
}
package com.reucon.openfire.plugin.archive.impl;
import com.reucon.openfire.plugin.archive.ArchiveFactory;
import com.reucon.openfire.plugin.archive.ArchiveManager;
import com.reucon.openfire.plugin.archive.IndexManager;
import com.reucon.openfire.plugin.archive.PersistenceManager;
import com.reucon.openfire.plugin.archive.model.ArchivedMessage;
import com.reucon.openfire.plugin.archive.model.Conversation;
import com.reucon.openfire.plugin.archive.model.Participant;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.session.Session;
import org.xmpp.packet.JID;
import org.xmpp.packet.Message;
import java.util.ArrayList;
import java.util.Collection;
/**
* Default implementation of ArchiveManager.
*/
public class ArchiveManagerImpl implements ArchiveManager
{
private final PersistenceManager persistenceManager;
private final IndexManager indexManager;
private final Collection<Conversation> activeConversations;
private int conversationTimeout;
public ArchiveManagerImpl(PersistenceManager persistenceManager, IndexManager indexManager,
int conversationTimeout)
{
this.persistenceManager = persistenceManager;
this.indexManager = indexManager;
this.conversationTimeout = conversationTimeout;
activeConversations = persistenceManager.getActiveConversations(conversationTimeout);
}
public void archiveMessage(Session session, Message message, boolean incoming)
{
final XMPPServer server = XMPPServer.getInstance();
final ArchivedMessage.Direction direction;
final ArchivedMessage archivedMessage;
final Conversation conversation;
final JID ownerJid;
final JID withJid;
// TODO support groupchat
if (message.getType() != Message.Type.chat && message.getType() != Message.Type.normal)
{
return;
}
if (server.isLocal(message.getFrom()) && incoming)
{
ownerJid = message.getFrom();
withJid = message.getTo();
// sent by the owner => to
direction = ArchivedMessage.Direction.to;
}
else if (server.isLocal(message.getTo()) && ! incoming)
{
ownerJid = message.getTo();
withJid = message.getFrom();
// received by the owner => from
direction = ArchivedMessage.Direction.from;
}
else
{
return;
}
archivedMessage = ArchiveFactory.createArchivedMessage(session, message, direction, withJid);
if (archivedMessage.isEmpty())
{
return;
}
conversation = determineConversation(ownerJid, withJid, message.getSubject(), message.getThread(), archivedMessage);
archivedMessage.setConversation(conversation);
persistenceManager.createMessage(archivedMessage);
if (indexManager != null)
{
indexManager.indexObject(archivedMessage);
}
}
public void setConversationTimeout(int conversationTimeout)
{
this.conversationTimeout = conversationTimeout;
}
private Conversation determineConversation(JID ownerJid, JID withJid, String subject, String thread, ArchivedMessage archivedMessage)
{
Conversation conversation = null;
Collection<Conversation> staleConversations;
staleConversations = new ArrayList<Conversation>();
synchronized (activeConversations)
{
for (Conversation c : activeConversations)
{
if (c.isStale(conversationTimeout))
{
staleConversations.add(c);
continue;
}
if (matches(ownerJid, withJid, thread, c))
{
conversation = c;
break;
}
}
activeConversations.removeAll(staleConversations);
if (conversation == null)
{
final Participant p1;
final Participant p2;
conversation = new Conversation(archivedMessage.getTime(),
ownerJid.toBareJID(), ownerJid.getResource(), withJid.toBareJID(), withJid.getResource(),
subject, thread);
persistenceManager.createConversation(conversation);
p1 = new Participant(archivedMessage.getTime(), ownerJid.toBareJID());
conversation.addParticipant(p1);
persistenceManager.createParticipant(p1, conversation.getId());
p2 = new Participant(archivedMessage.getTime(), withJid.toBareJID());
conversation.addParticipant(p2);
persistenceManager.createParticipant(p2, conversation.getId());
activeConversations.add(conversation);
}
else
{
conversation.setEnd(archivedMessage.getTime());
persistenceManager.updateConversationEnd(conversation);
}
}
return conversation;
}
private boolean matches(JID ownerJid, JID withJid, String thread, Conversation c)
{
if (! ownerJid.toBareJID().equals(c.getOwnerJid()))
{
return false;
}
if (! withJid.toBareJID().equals(c.getWithJid()))
{
return false;
}
/*
if (ownerJid.getResource() != null)
{
if (! ownerJid.getResource().equals(c.getOwnerResource()))
{
return false;
}
}
else
{
if (c.getOwnerResource() != null)
{
return false;
}
}
if (withJid.getResource() != null)
{
if (! withJid.getResource().equals(c.getWithResource()))
{
return false;
}
}
else
{
if (c.getWithResource() != null)
{
return false;
}
}
*/
if (thread != null)
{
if (! thread.equals(c.getThread()))
{
return false;
}
}
else
{
if (c.getThread() != null)
{
return false;
}
}
return true;
}
}
package com.reucon.openfire.plugin.archive.impl;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import org.jivesoftware.database.DbConnectionManager;
import org.jivesoftware.util.Log;
import org.xmpp.packet.JID;
import com.reucon.openfire.plugin.archive.ArchivedMessageConsumer;
import com.reucon.openfire.plugin.archive.PersistenceManager;
import com.reucon.openfire.plugin.archive.model.ArchivedMessage;
import com.reucon.openfire.plugin.archive.model.ArchivedMessage.Direction;
import com.reucon.openfire.plugin.archive.model.Conversation;
import com.reucon.openfire.plugin.archive.model.Participant;
import com.reucon.openfire.plugin.archive.xep0059.XmppResultSet;
/**
* Manages database persistence.
*/
public class JdbcPersistenceManager implements PersistenceManager {
public static final int DEFAULT_MAX = 1000;
public static final String SELECT_MESSAGES_BY_CONVERSATION = "SELECT DISTINCT "
+ "ofConversation.conversationID, "
+ "ofConversation.room, "
+ "ofConversation.isExternal, "
+ "ofConversation.startDate, "
+ "ofConversation.lastActivity, "
+ "ofConversation.messageCount, "
+ "ofConParticipant.joinedDate, "
+ "ofConParticipant.leftDate, "
+ "ofConParticipant.bareJID, "
+ "ofConParticipant.jidResource, "
+ "ofConParticipant.nickname, "
+ "ofMessageArchive.fromJID, "
+ "ofMessageArchive.toJID, "
+ "ofMessageArchive.sentDate, "
+ "ofMessageArchive.body "
+ "FROM ofConversation "
+ "INNER JOIN ofConParticipant ON ofConversation.conversationID = ofConParticipant.conversationID "
+ "INNER JOIN ofMessageArchive ON ofConParticipant.conversationID = ofMessageArchive.conversationID "
+ "WHERE ofConversation.conversationID = ? AND ofConParticipant.bareJID = ? ORDER BY ofMessageArchive.sentDate";
// public static final String SELECT_MESSAGES_BY_CONVERSATION =
// "SELECT messageId,time,direction,type,subject,body "
// + "FROM archiveMessages WHERE conversationId = ? ORDER BY time";
public static final String SELECT_CONVERSATIONS = "SELECT DISTINCT "
+ "ofConversation.conversationID, "
+ "ofConversation.room, "
+ "ofConversation.isExternal, "
+ "ofConversation.startDate, "
+ "ofConversation.lastActivity, "
+ "ofConversation.messageCount, "
+ "ofConParticipant.joinedDate, "
+ "ofConParticipant.leftDate, "
+ "ofConParticipant.bareJID, "
+ "ofConParticipant.jidResource, "
+ "ofConParticipant.nickname, "
+ "ofMessageArchive.fromJID, "
+ "ofMessageArchive.toJID, "
+ "ofMessageArchive.sentDate, "
+ "ofMessageArchive.body "
+ "FROM ofConversation "
+ "INNER JOIN ofConParticipant ON ofConversation.conversationID = ofConParticipant.conversationID "
+ "INNER JOIN ofMessageArchive ON ofConParticipant.conversationID = ofMessageArchive.conversationID";
// public static final String SELECT_CONVERSATIONS =
// "SELECT c.conversationId,c.startTime,c.endTime,c.ownerJid,c.ownerResource,c.withJid,c.withResource,"
// + " c.subject,c.thread " + "FROM archiveConversations AS c";
public static final String COUNT_CONVERSATIONS = "SELECT COUNT(DISTINCT ofConversation.conversationID) FROM ofConversation "
+ "INNER JOIN ofConParticipant ON ofConversation.conversationID = ofConParticipant.conversationID "
+ "INNER JOIN ofMessageArchive ON ofConParticipant.conversationID = ofMessageArchive.conversationID";
// public static final String COUNT_CONVERSATIONS =
// "SELECT count(*) FROM archiveConversations AS c";
public static final String CONVERSATION_ID = "ofConversation.conversationID";
// public static final String CONVERSATION_ID = "c.conversationId";
public static final String CONVERSATION_START_TIME = "ofConversation.startDate";
// public static final String CONVERSATION_START_TIME = "c.startTime";
public static final String CONVERSATION_END_TIME = "ofConversation.lastActivity";
// public static final String CONVERSATION_END_TIME = "c.endTime";
public static final String CONVERSATION_OWNER_JID = "ofConParticipant.bareJID";
// public static final String CONVERSATION_OWNER_JID = "c.ownerJid";
public static final String CONVERSATION_WITH_JID = "(ofMessageArchive.toJID = ? OR ofMessageArchive.fromJID = ?)";
// public static final String CONVERSATION_WITH_JID = "c.withJid";
public static final String SELECT_ACTIVE_CONVERSATIONS = "SELECT DISTINCT "
+ "ofConversation.conversationID, "
+ "ofConversation.room, "
+ "ofConversation.isExternal, "
+ "ofConversation.startDate, "
+ "ofConversation.lastActivity, "
+ "ofConversation.messageCount, "
+ "ofConParticipant.joinedDate, "
+ "ofConParticipant.leftDate, "
+ "ofConParticipant.bareJID, "
+ "ofConParticipant.jidResource, "
+ "ofConParticipant.nickname, "
+ "ofMessageArchive.fromJID, "
+ "ofMessageArchive.toJID, "
+ "ofMessageArchive.sentDate, "
+ "ofMessageArchive.body "
+ "FROM ofConversation "
+ "INNER JOIN ofConParticipant ON ofConversation.conversationID = ofConParticipant.conversationID "
+ "INNER JOIN ofMessageArchive ON ofConParticipant.conversationID = ofMessageArchive.conversationID "
+ "WHERE ofConversation.lastActivity > ?";
// public static final String SELECT_ACTIVE_CONVERSATIONS =
// "SELECT c.conversationId,c.startTime,c.endTime,c.ownerJid,c.ownerResource,withJid,c.withResource,"
// + " c.subject,c.thread "
// + "FROM archiveConversations AS c WHERE c.endTime > ?";
public static final String SELECT_PARTICIPANTS_BY_CONVERSATION = "SELECT DISTINCT "
+ "ofConversation.conversationID, "
+ "ofConversation.startDate, "
+ "ofConversation.lastActivity, "
+ "ofConParticipant.bareJID "
+ "FROM ofConversation "
+ "INNER JOIN ofConParticipant ON ofConversation.conversationID = ofConParticipant.conversationID "
+ "INNER JOIN ofMessageArchive ON ofConParticipant.conversationID = ofMessageArchive.conversationID "
+ "WHERE ofConversation.conversationID = ? ORDER BY ofConversation.startDate";
// public static final String SELECT_PARTICIPANTS_BY_CONVERSATION =
// "SELECT participantId,startTime,endTime,jid FROM archiveParticipants WHERE conversationId =? ORDER BY startTime";
public boolean createMessage(ArchivedMessage message) {
/* read only */
return false;
}
public int processAllMessages(ArchivedMessageConsumer callback) {
return 0;
}
public boolean createConversation(Conversation conversation) {
/* read only */
return false;
}
public boolean updateConversationEnd(Conversation conversation) {
/* read only */
return false;
}
public boolean createParticipant(Participant participant,
Long conversationId) {
return false;
}
public List<Conversation> findConversations(String[] participants,
Date startDate, Date endDate) {
final List<Conversation> conversations = new ArrayList<Conversation>();
return conversations;
}
public Collection<Conversation> findConversations(Date startDate,
Date endDate, String ownerJid, String withJid,
XmppResultSet xmppResultSet) {
final HashMap<Long, Conversation> conversations;
final StringBuilder querySB;
final StringBuilder whereSB;
final StringBuilder limitSB;
conversations = new HashMap<Long, Conversation>();
querySB = new StringBuilder(SELECT_CONVERSATIONS);
whereSB = new StringBuilder();
limitSB = new StringBuilder();
if (startDate != null) {
appendWhere(whereSB, CONVERSATION_START_TIME, " >= ?");
}
if (endDate != null) {
appendWhere(whereSB, CONVERSATION_END_TIME, " <= ?");
}
if (ownerJid != null) {
appendWhere(whereSB, CONVERSATION_OWNER_JID, " = ?");
}
if (withJid != null) {
appendWhere(whereSB, CONVERSATION_WITH_JID);
}
if (xmppResultSet != null) {
Integer firstIndex = null;
int max = xmppResultSet.getMax() != null ? xmppResultSet.getMax()
: DEFAULT_MAX;
xmppResultSet.setCount(countConversations(startDate, endDate,
ownerJid, withJid, whereSB.toString()));
if (xmppResultSet.getIndex() != null) {
firstIndex = xmppResultSet.getIndex();
} else if (xmppResultSet.getAfter() != null) {
firstIndex = countConversationsBefore(startDate, endDate,
ownerJid, withJid, xmppResultSet.getAfter(),
whereSB.toString());
firstIndex += 1;
} else if (xmppResultSet.getBefore() != null) {
firstIndex = countConversationsBefore(startDate, endDate,
ownerJid, withJid, xmppResultSet.getBefore(),
whereSB.toString());
firstIndex -= max;
if (firstIndex < 0) {
firstIndex = 0;
}
}
firstIndex = firstIndex != null ? firstIndex : 0;
limitSB.append(" LIMIT ").append(max);
limitSB.append(" OFFSET ").append(firstIndex);
xmppResultSet.setFirstIndex(firstIndex);
}
if (whereSB.length() != 0) {
querySB.append(" WHERE ").append(whereSB);
}
querySB.append(" ORDER BY ").append(CONVERSATION_ID);
querySB.append(limitSB);
Connection con = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement(querySB.toString());
bindConversationParameters(startDate, endDate, ownerJid, withJid,
pstmt);
rs = pstmt.executeQuery();
Log.debug("findConversations: SELECT_CONVERSATIONS: "
+ pstmt.toString());
while (rs.next()) {
Conversation conv = extractConversation(rs);
conversations.put(conv.getId(), conv);
}
} catch (SQLException sqle) {
Log.error("Error selecting conversations", sqle);
} finally {
DbConnectionManager.closeConnection(rs, pstmt, con);
}
if (xmppResultSet != null && conversations.size() > 0) {
ArrayList<Long> sortedConvKeys = new ArrayList<Long>(
conversations.keySet());
Collections.sort(sortedConvKeys);
xmppResultSet.setFirst(sortedConvKeys.get(0));
xmppResultSet
.setLast(sortedConvKeys.get(sortedConvKeys.size() - 1));
}
return conversations.values();
}
private void appendWhere(StringBuilder sb, String... fragments) {
if (sb.length() != 0) {
sb.append(" AND ");
}
for (String fragment : fragments) {
sb.append(fragment);
}
}
private int countConversations(Date startDate, Date endDate,
String ownerJid, String withJid, String whereClause) {
StringBuilder querySB;
querySB = new StringBuilder(COUNT_CONVERSATIONS);
if (whereClause != null && whereClause.length() != 0) {
querySB.append(" WHERE ").append(whereClause);
}
Connection con = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement(querySB.toString());
bindConversationParameters(startDate, endDate, ownerJid, withJid,
pstmt);
rs = pstmt.executeQuery();
if (rs.next()) {
return rs.getInt(1);
} else {
return 0;
}
} catch (SQLException sqle) {
Log.error("Error counting conversations", sqle);
return 0;
} finally {
DbConnectionManager.closeConnection(rs, pstmt, con);
}
}
private int countConversationsBefore(Date startDate, Date endDate,
String ownerJid, String withJid, Long before, String whereClause) {
StringBuilder querySB;
querySB = new StringBuilder(COUNT_CONVERSATIONS);
querySB.append(" WHERE ");
if (whereClause != null && whereClause.length() != 0) {
querySB.append(whereClause);
querySB.append(" AND ");
}
querySB.append(CONVERSATION_ID).append(" < ?");
Connection con = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
int parameterIndex;
con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement(querySB.toString());
parameterIndex = bindConversationParameters(startDate, endDate,
ownerJid, withJid, pstmt);
pstmt.setLong(parameterIndex, before);
rs = pstmt.executeQuery();
if (rs.next()) {
return rs.getInt(1);
} else {
return 0;
}
} catch (SQLException sqle) {
Log.error("Error counting conversations", sqle);
return 0;
} finally {
DbConnectionManager.closeConnection(rs, pstmt, con);
}
}
private int bindConversationParameters(Date startDate, Date endDate,
String ownerJid, String withJid, PreparedStatement pstmt)
throws SQLException {
int parameterIndex = 1;
if (startDate != null) {
pstmt.setLong(parameterIndex++, dateToMillis(startDate));
}
if (endDate != null) {
pstmt.setLong(parameterIndex++, dateToMillis(endDate));
}
if (ownerJid != null) {
pstmt.setString(parameterIndex++, ownerJid);
}
if (withJid != null) {
pstmt.setString(parameterIndex++, withJid);
pstmt.setString(parameterIndex++, withJid);
}
return parameterIndex;
}
public Collection<Conversation> getActiveConversations(
int conversationTimeout) {
final Collection<Conversation> conversations;
final long now = System.currentTimeMillis();
conversations = new ArrayList<Conversation>();
Connection con = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement(SELECT_ACTIVE_CONVERSATIONS);
pstmt.setLong(1, now - conversationTimeout * 60L * 1000L);
rs = pstmt.executeQuery();
while (rs.next()) {
conversations.add(extractConversation(rs));
}
} catch (SQLException sqle) {
Log.error("Error selecting conversations", sqle);
} finally {
DbConnectionManager.closeConnection(rs, pstmt, con);
}
return conversations;
}
public List<Conversation> getConversations(Collection<Long> conversationIds) {
final List<Conversation> conversations;
final StringBuilder querySB;
conversations = new ArrayList<Conversation>();
if (conversationIds.isEmpty()) {
return conversations;
}
querySB = new StringBuilder(SELECT_CONVERSATIONS);
querySB.append(" WHERE ");
querySB.append(CONVERSATION_ID);
querySB.append(" IN ( ");
for (int i = 0; i < conversationIds.size(); i++) {
if (i == 0) {
querySB.append("?");
} else {
querySB.append(",?");
}
}
querySB.append(" )");
querySB.append(" ORDER BY ").append(CONVERSATION_END_TIME);
Connection con = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement(querySB.toString());
int i = 0;
for (Long id : conversationIds) {
pstmt.setLong(++i, id);
}
rs = pstmt.executeQuery();
while (rs.next()) {
conversations.add(extractConversation(rs));
}
} catch (SQLException sqle) {
Log.error("Error selecting conversations", sqle);
} finally {
DbConnectionManager.closeConnection(rs, pstmt, con);
}
return conversations;
}
public Conversation getConversation(String ownerJid, String withJid,
Date start) {
return getConversation(null, ownerJid, withJid, start);
}
public Conversation getConversation(Long conversationId) {
return getConversation(conversationId, null, null, null);
}
private Conversation getConversation(Long conversationId, String ownerJid,
String withJid, Date start) {
Conversation conversation = null;
StringBuilder querySB;
Connection con = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
querySB = new StringBuilder(SELECT_CONVERSATIONS);
querySB.append(" WHERE ");
if (conversationId != null) {
querySB.append(CONVERSATION_ID).append(" = ? ");
} else {
querySB.append(CONVERSATION_OWNER_JID).append(" = ?");
if (withJid != null) {
querySB.append(" AND ");
querySB.append(CONVERSATION_WITH_JID);
}
if (start != null) {
querySB.append(" AND ");
querySB.append(CONVERSATION_START_TIME).append(" = ? ");
}
}
try {
con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement(querySB.toString());
int i = 1;
if (conversationId != null) {
pstmt.setLong(1, conversationId);
} else {
pstmt.setString(i++, ownerJid);
if (withJid != null) {
pstmt.setString(i++, withJid);
pstmt.setString(i++, withJid);
}
if (start != null) {
pstmt.setLong(i++, dateToMillis(start));
}
}
rs = pstmt.executeQuery();
Log.debug("getConversation: SELECT_CONVERSATIONS: "
+ pstmt.toString());
if (rs.next()) {
conversation = extractConversation(rs);
} else {
return null;
}
rs.close();
pstmt.close();
pstmt = con.prepareStatement(SELECT_PARTICIPANTS_BY_CONVERSATION);
pstmt.setLong(1, conversation.getId());
rs = pstmt.executeQuery();
Log.debug("getConversation: SELECT_PARTICIPANTS_BY_CONVERSATION: "
+ pstmt.toString());
while (rs.next()) {
for (Participant participant : extractParticipant(rs)) {
conversation.addParticipant(participant);
}
}
rs.close();
pstmt.close();
pstmt = con.prepareStatement(SELECT_MESSAGES_BY_CONVERSATION);
pstmt.setLong(1, conversation.getId());
pstmt.setString(2, conversation.getOwnerJid());
rs = pstmt.executeQuery();
Log.debug("getConversation: SELECT_MESSAGES_BY_CONVERSATION: "
+ pstmt.toString());
while (rs.next()) {
ArchivedMessage message;
message = extractMessage(rs);
message.setConversation(conversation);
conversation.addMessage(message);
}
} catch (SQLException sqle) {
Log.error("Error selecting conversation", sqle);
} finally {
DbConnectionManager.closeConnection(rs, pstmt, con);
}
return conversation;
}
private String getWithJidConversations(ResultSet rs) throws SQLException {
String bareJid = rs.getString("bareJID");
String fromJid = rs.getString("fromJID");
String toJid = rs.getString("toJID");
String room = rs.getString("room");
String result = null;
if (bareJid != null && fromJid != null && toJid != null) {
if (room != null && !room.equals("")) {
result = room;
} else if (fromJid.contains(bareJid)) {
result = toJid;
} else {
result = fromJid;
}
}
return result;
}
private Direction getDirection(ResultSet rs) throws SQLException {
Direction direction = null;
String bareJid = rs.getString("bareJID");
String fromJid = rs.getString("fromJID");
String toJid = rs.getString("toJID");
if (bareJid != null && fromJid != null && toJid != null) {
if (bareJid.equals(fromJid)) {
/*
* if message from me to withJid then it is to the withJid
* participant
*/
direction = Direction.to;
} else {
/*
* if message to me from withJid then it is from the withJid
* participant
*/
direction = Direction.from;
}
}
return direction;
}
private Conversation extractConversation(ResultSet rs) throws SQLException {
final Conversation conversation;
long id = rs.getLong("conversationID");
Date startDate = millisToDate(rs.getLong("startDate"));
String ownerJid = rs.getString("bareJID");
String ownerResource = null;
String withJid = getWithJidConversations(rs);
String withResource = null;
String subject = null;
String thread = String.valueOf(id);
conversation = new Conversation(startDate, ownerJid, ownerResource,
withJid, withResource, subject, thread);
conversation.setId(id);
return conversation;
}
private Collection<Participant> extractParticipant(ResultSet rs)
throws SQLException {
Collection<Participant> participants = new HashSet<Participant>();
Date startDate = millisToDate(rs.getLong("startDate"));
String participantJid = rs.getString("bareJID");
Date endDate = millisToDate(rs.getLong("lastActivity"));
if (participantJid != null) {
Participant participant = new Participant(startDate, participantJid);
participant.setEnd(endDate);
participants.add(participant);
}
// String withJid = getWithJid(rs);
// if (withJid != null) {
// Participant participant = new Participant(startDate, participantJid);
// participant.setEnd(endDate);
// participants.add(participant);
// }
return participants;
}
private ArchivedMessage extractMessage(ResultSet rs) throws SQLException {
final ArchivedMessage message;
Date time = millisToDate(rs.getLong("sentDate"));
Direction direction = getDirection(rs);
String type = null;
String subject = null;
String body = rs.getString("body");
String bareJid = rs.getString("bareJID");
JID withJid = null;
if (Direction.from == direction) {
withJid = new JID(rs.getString("fromJID"));
}
message = new ArchivedMessage(time, direction, null, withJid);
// message.setId(id);
// message.setSubject(subject);
message.setBody(body);
return message;
}
private Long dateToMillis(Date date) {
return date == null ? null : date.getTime();
}
private Date millisToDate(Long millis) {
return millis == null ? null : new Date(millis);
}
}
package com.reucon.openfire.plugin.archive.model;
import org.jivesoftware.database.JiveID;
import org.xmpp.packet.JID;
import java.util.Date;
/**
* An archived message.
*/
@JiveID(601)
public class ArchivedMessage {
public enum Direction {
/**
* A message sent by the owner.
*/
to,
/**
* A message received by the owner.
*/
from
}
private Long id;
private final Date time;
private final Direction direction;
private final String type;
private String subject;
private String body;
private Conversation conversation;
private JID withJid;
public ArchivedMessage(Date time, Direction direction, String type, JID withJid) {
this.time = time;
this.direction = direction;
this.type = type;
this.withJid = withJid;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Date getTime() {
return time;
}
public Direction getDirection() {
return direction;
}
public String getType() {
return type;
}
public String getSubject() {
return subject;
}
public void setSubject(String subject) {
this.subject = subject;
}
public String getBody() {
return body;
}
public void setBody(String body) {
this.body = body;
}
public Conversation getConversation() {
return conversation;
}
public void setConversation(Conversation conversation) {
this.conversation = conversation;
}
/**
* Checks if this message contains payload that should be archived.
*
* @return <code>true</code> if this message is empty, <code>false</code>
* otherwise.
*/
public boolean isEmpty() {
return subject == null && body == null;
}
public JID getWithJid() {
return withJid;
}
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("ArchivedMessage[id=").append(id).append(",");
sb.append("time=").append(time).append(",");
sb.append("direction=").append(direction).append("]");
return sb.toString();
}
}
package com.reucon.openfire.plugin.archive.model;
import org.jivesoftware.database.JiveID;
import java.util.*;
/**
* A conversation between two or more participants.
*/
@JiveID(602)
public class Conversation
{
private Long id;
private final Date start;
private Date end;
private final String ownerJid;
private final String ownerResource;
private final String withJid;
private final String withResource;
private String subject;
private final String thread;
private final List<Participant> participants;
private final List<ArchivedMessage> messages;
public Conversation(Date start, String ownerJid, String ownerResource, String withJid, String withResource,
String subject, String thread)
{
this(start, start, ownerJid, ownerResource, withJid, withResource, subject, thread);
}
public Conversation(Date start, Date end, String ownerJid, String ownerResource, String withJid, String withResource,
String subject, String thread)
{
this.start = start;
this.end = end;
this.ownerJid = ownerJid;
this.ownerResource = ownerResource;
this.withJid = withJid;
this.withResource = withResource;
this.subject = subject;
this.thread = thread;
participants = new ArrayList<Participant>();
messages = new ArrayList<ArchivedMessage>();
}
public Long getId()
{
return id;
}
public void setId(Long id)
{
this.id = id;
}
public Date getStart()
{
return start;
}
public Date getEnd()
{
return end;
}
public void setEnd(Date end)
{
this.end = end;
}
public String getOwnerJid()
{
return ownerJid;
}
public String getOwnerResource()
{
return ownerResource;
}
public String getWithJid()
{
return withJid;
}
public String getWithResource()
{
return withResource;
}
public String getSubject()
{
return subject;
}
public void setSubject(String subject)
{
this.subject = subject;
}
public String getThread()
{
return thread;
}
public Collection<Participant> getParticipants()
{
return Collections.unmodifiableCollection(participants);
}
public void addParticipant(Participant participant)
{
synchronized (participants)
{
participants.add(participant);
}
}
public List<ArchivedMessage> getMessages()
{
return Collections.unmodifiableList(messages);
}
public void addMessage(ArchivedMessage message)
{
synchronized (messages)
{
messages.add(message);
}
}
public boolean isStale(int conversationTimeout)
{
Long now = System.currentTimeMillis();
return end.getTime() + conversationTimeout * 60L * 1000L < now;
}
/**
* Checks if this conversation has an active participant with the given JID.
*
* @param jid JID of the participant
* @return <code>true</code> if this conversation has an active participant with the given JID,
* <code>false</code> otherwise.
*/
public boolean hasParticipant(String jid)
{
synchronized (participants)
{
for (Participant p : participants)
{
if (p.getJid().equals(jid))
{
return true;
}
}
}
return false;
}
/**
* Checks if this conversation is new and has not yet been persisted.
*
* @return <code>true</code> if this conversation is new and has not yet been persisted,
* <code>false</code> otherwise.
*/
public boolean isNew()
{
return id == null;
}
}
package com.reucon.openfire.plugin.archive.model;
import org.jivesoftware.database.JiveID;
import java.util.Date;
/**
*
*/
@JiveID(603)
public class Participant
{
private long id;
private final Date start;
private Date end;
private final String jid;
public Participant(Date start, String jid)
{
this.start = start;
this.jid = jid;
}
public long getId()
{
return id;
}
public void setId(long id)
{
this.id = id;
}
public Date getStart()
{
return start;
}
public Date getEnd()
{
return end;
}
public void setEnd(Date end)
{
this.end = end;
}
public String getJid()
{
return jid;
}
}
package com.reucon.openfire.plugin.archive.model;
import java.util.Map;
/**
* A user's archiving preferences according to XEP-0136.
*/
public class Preferences
{
public enum MethodUsage
{
forbid,
concide,
prefer
}
private String username;
private Map<String, MethodUsage> methods;
}
package com.reucon.openfire.plugin.archive.util;
public class EscapeUtil
{
public static String escapeHtml(String source)
{
int terminatorIndex;
if (source == null)
{
return null;
}
StringBuffer result = new StringBuffer(source.length() * 2);
for (int i = 0; i < source.length(); i++)
{
int ch = source.charAt(i);
// avoid escaping already escaped characters
if (ch == 38)
{
terminatorIndex = source.indexOf(";", i);
if (terminatorIndex > 0)
{
if (source.substring(i + 1, terminatorIndex).matches("#[0-9]+|lt|gt|amp|quote"))
{
result.append(source.substring(i, terminatorIndex + 1));
// Skip remaining chars up to (and including) ";"
i = terminatorIndex;
continue;
}
}
}
if (ch == 10)
{
result.append("<br/>");
}
else if (ch != 32 && (ch > 122 || ch < 48 || ch == 60 || ch == 62))
{
result.append("&#");
result.append(ch);
result.append(";");
}
else
{
result.append((char) ch);
}
}
return new String(result);
}
}
package com.reucon.openfire.plugin.archive.util;
import org.jivesoftware.util.JiveConstants;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.TimeZone;
/**
* Utility class to parse and format dates in UTC that adhere to the DateTime format specified
* in Jabber Date and Time Profiles.
*/
public class XmppDateUtil
{
private static final DateFormat dateFormat;
private static final DateFormat dateFormatWithoutMillis;
static
{
dateFormat = new SimpleDateFormat(JiveConstants.XMPP_DATETIME_FORMAT);
dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
dateFormatWithoutMillis = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
dateFormatWithoutMillis.setTimeZone(TimeZone.getTimeZone("UTC"));
}
private XmppDateUtil()
{
}
public static Date parseDate(String dateString)
{
Date date = null;
if (dateString == null)
{
return null;
}
synchronized(dateFormat)
{
try
{
date = dateFormat.parse(dateString);
}
catch (ParseException e)
{
// ignore
}
}
if (date != null)
{
return date;
}
synchronized(dateFormatWithoutMillis)
{
try
{
date = dateFormatWithoutMillis.parse(dateString);
}
catch (ParseException e)
{
// ignore
}
}
return date;
}
public static String formatDate(Date date)
{
if (date == null)
{
return null;
}
synchronized(dateFormat)
{
return dateFormat.format(date);
}
}
}
package com.reucon.openfire.plugin.archive.xep0059;
import org.dom4j.DocumentFactory;
import org.dom4j.Element;
/**
* A <a href="http://www.xmpp.org/extensions/xep-0059.html">XEP-0059</a> result set.
*/
public class XmppResultSet
{
public static String NAMESPACE = "http://jabber.org/protocol/rsm";
private Long after;
private Long before;
private Integer index;
private Integer max;
private Long first;
private Integer firstIndex;
private Long last;
private Integer count;
public XmppResultSet(Element setElement)
{
if (setElement.element("after") != null)
{
try
{
after = Long.parseLong(setElement.elementText("after"));
if (after < 0)
{
after = null;
}
}
catch (Exception e)
{
// swallow
}
}
if (setElement.element("before") != null)
{
try
{
before = Long.parseLong(setElement.elementText("before"));
if (before < 0)
{
before = null;
}
}
catch (Exception e)
{
// swallow
}
}
if (setElement.element("max") != null)
{
try
{
max = Integer.parseInt(setElement.elementText("max"));
if (max < 0)
{
max = null;
}
}
catch (Exception e)
{
// swallow
}
}
if (setElement.element("index") != null)
{
try
{
index = Integer.parseInt(setElement.elementText("index"));
if (index < 0)
{
index = null;
}
}
catch (Exception e)
{
// swallow
}
}
}
public Long getAfter()
{
return after;
}
public Long getBefore()
{
return before;
}
/**
* Returns the index of the first element to return.
*
* @return the index of the first element to return.
*/
public Integer getIndex()
{
return index;
}
/**
* Returns the maximum number of items to return.
*
* @return the maximum number of items to return.
*/
public Integer getMax()
{
return max;
}
/**
* Sets the id of the first element returned.
*
* @param first the id of the first element returned.
*/
public void setFirst(Long first)
{
this.first = first;
}
/**
* Sets the index of the first element returned.
*
* @param firstIndex the index of the first element returned.
*/
public void setFirstIndex(Integer firstIndex)
{
this.firstIndex = firstIndex;
}
/**
* Sets the id of the last element returned.
*
* @param last the id of the last element returned.
*/
public void setLast(Long last)
{
this.last = last;
}
/**
* Sets the number of elements returned.
*
* @param count the number of elements returned.
*/
public void setCount(Integer count)
{
this.count = count;
}
public Element createResultElement()
{
final Element set;
set = DocumentFactory.getInstance().createElement("set", NAMESPACE);
if (first != null)
{
final Element firstElement;
firstElement = set.addElement("first");
firstElement.setText(first.toString());
if (firstIndex != null)
{
firstElement.addAttribute("index", firstIndex.toString());
}
}
if (last != null)
{
set.addElement("last").setText(last.toString());
}
if (count != null)
{
set.addElement("count").setText(count.toString());
}
return set;
}
}
package com.reucon.openfire.plugin.archive.xep0136;
import org.jivesoftware.openfire.IQHandlerInfo;
import org.jivesoftware.openfire.handler.IQHandler;
import org.jivesoftware.openfire.plugin.MonitoringPlugin;
import org.xmpp.packet.IQ;
import org.xmpp.packet.Packet;
import org.xmpp.packet.PacketError;
import com.reucon.openfire.plugin.archive.IndexManager;
import com.reucon.openfire.plugin.archive.PersistenceManager;
/**
* Abstract base class for XEP-0136 IQ Handlers.
*/
public abstract class AbstractIQHandler extends IQHandler {
protected static final String NAMESPACE = "urn:xmpp:archive";
private final IQHandlerInfo info;
protected AbstractIQHandler(String moduleName, String elementName) {
super(moduleName);
this.info = new IQHandlerInfo(elementName, NAMESPACE);
}
public final IQHandlerInfo getInfo() {
return info;
}
protected PersistenceManager getPersistenceManager() {
return MonitoringPlugin.getInstance().getPersistenceManager();
}
protected IndexManager getIndexManager() {
return MonitoringPlugin.getInstance().getIndexManager();
}
protected IQ error(Packet packet, PacketError.Condition condition) {
IQ reply;
reply = new IQ(IQ.Type.error, packet.getID());
reply.setFrom(packet.getTo());
reply.setTo(packet.getFrom());
reply.setError(condition);
return reply;
}
}
package com.reucon.openfire.plugin.archive.xep0136;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import org.dom4j.Element;
import org.jivesoftware.openfire.auth.UnauthorizedException;
import org.jivesoftware.openfire.disco.ServerFeaturesProvider;
import org.xmpp.packet.IQ;
import org.xmpp.packet.JID;
import com.reucon.openfire.plugin.archive.model.Conversation;
import com.reucon.openfire.plugin.archive.util.XmppDateUtil;
import com.reucon.openfire.plugin.archive.xep0059.XmppResultSet;
/**
* Message Archiving List Handler.
*/
public class IQListHandler extends AbstractIQHandler implements
ServerFeaturesProvider {
private static final String NAMESPACE_MANAGE = "urn:xmpp:archive:manage";
public IQListHandler() {
super("Message Archiving List Handler", "list");
}
public IQ handleIQ(IQ packet) throws UnauthorizedException {
IQ reply = IQ.createResultIQ(packet);
ListRequest listRequest = new ListRequest(packet.getChildElement());
JID from = packet.getFrom();
Element listElement = reply.setChildElement("list", NAMESPACE);
Collection<Conversation> conversations = list(from, listRequest);
XmppResultSet resultSet = listRequest.getResultSet();
for (Conversation conversation : conversations) {
addChatElement(listElement, conversation);
}
if (resultSet != null) {
listElement.add(resultSet.createResultElement());
}
return reply;
}
private Collection<Conversation> list(JID from, ListRequest request) {
return getPersistenceManager().findConversations(request.getStart(),
request.getEnd(), from.toBareJID(), request.getWith(),
request.getResultSet());
}
private Element addChatElement(Element listElement,
Conversation conversation) {
Element chatElement = listElement.addElement("chat");
chatElement.addAttribute("with", conversation.getWithJid());
chatElement.addAttribute("start",
XmppDateUtil.formatDate(conversation.getStart()));
return chatElement;
}
public Iterator<String> getFeatures() {
ArrayList<String> features = new ArrayList<String>();
features.add(NAMESPACE_MANAGE);
return features.iterator();
}
}
package com.reucon.openfire.plugin.archive.xep0136;
import org.dom4j.Element;
import org.jivesoftware.openfire.auth.UnauthorizedException;
import org.jivesoftware.openfire.disco.ServerFeaturesProvider;
import org.xmpp.packet.IQ;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
/**
* Message Archiving Preferences Handler.
*/
public class IQPrefHandler extends AbstractIQHandler implements ServerFeaturesProvider
{
private static final String NAMESPACE_PREF = "urn:xmpp:archive:pref";
public IQPrefHandler()
{
super("Message Archiving Preferences Handler", "pref");
}
@SuppressWarnings("unchecked")
public IQ handleIQ(IQ packet) throws UnauthorizedException
{
IQ reply = IQ.createResultIQ(packet);
Element prefRequest = packet.getChildElement();
System.err.println("Received pref message from " + packet.getFrom());
if (prefRequest.element("default") != null)
{
Element defaultItem = prefRequest.element("default");
// User requests to set default modes
defaultItem.attribute("save"); // body, false, message, stream
defaultItem.attribute("otr");
defaultItem.attribute("expire");
}
for (Element item : (List<Element>) prefRequest.elements("item"))
{
// User requests to set modes for a contact
item.attribute("jid");
item.attribute("save"); // body, false, message, stream
item.attribute("otr");
item.attribute("expire");
}
for (Element method : (List<Element>) prefRequest.elements("method"))
{
// User requests to set archiving method preferences
method.attribute("type");
method.attribute("use");
}
return reply;
}
public Iterator<String> getFeatures()
{
ArrayList<String> features = new ArrayList<String>();
features.add(NAMESPACE_PREF);
return features.iterator();
}
}
package com.reucon.openfire.plugin.archive.xep0136;
import com.reucon.openfire.plugin.archive.model.ArchivedMessage;
import com.reucon.openfire.plugin.archive.model.Conversation;
import com.reucon.openfire.plugin.archive.util.XmppDateUtil;
import com.reucon.openfire.plugin.archive.xep0059.XmppResultSet;
import org.dom4j.Element;
import org.jivesoftware.openfire.auth.UnauthorizedException;
import org.xmpp.packet.IQ;
import org.xmpp.packet.JID;
import org.xmpp.packet.PacketError;
import java.util.List;
/**
* Message Archiving Retrieve Handler.
*/
public class IQRetrieveHandler extends AbstractIQHandler {
public IQRetrieveHandler() {
super("Message Archiving Retrieve Handler", "retrieve");
}
public IQ handleIQ(IQ packet) throws UnauthorizedException {
final IQ reply = IQ.createResultIQ(packet);
final RetrieveRequest retrieveRequest = new RetrieveRequest(
packet.getChildElement());
int fromIndex; // inclusive
int toIndex; // exclusive
int max;
final Conversation conversation = retrieve(packet.getFrom(),
retrieveRequest);
if (conversation == null) {
return error(packet, PacketError.Condition.item_not_found);
}
final Element chatElement = reply.setChildElement("chat", NAMESPACE);
chatElement.addAttribute("with", conversation.getWithJid());
chatElement.addAttribute("start",
XmppDateUtil.formatDate(conversation.getStart()));
max = conversation.getMessages().size();
fromIndex = 0;
toIndex = max > 0 ? max : 0;
final XmppResultSet resultSet = retrieveRequest.getResultSet();
if (resultSet != null) {
if (resultSet.getMax() != null && resultSet.getMax() <= max) {
max = resultSet.getMax();
toIndex = fromIndex + max;
}
if (resultSet.getIndex() != null) {
fromIndex = resultSet.getIndex();
toIndex = fromIndex + max;
} else if (resultSet.getAfter() != null) {
fromIndex = resultSet.getAfter().intValue() + 1;
toIndex = fromIndex + max;
} else if (resultSet.getBefore() != null) {
toIndex = resultSet.getBefore().intValue();
fromIndex = toIndex - max;
}
}
fromIndex = fromIndex < 0 ? 0 : fromIndex;
toIndex = toIndex > conversation.getMessages().size() ? conversation
.getMessages().size() : toIndex;
toIndex = toIndex < fromIndex ? fromIndex : toIndex;
final List<ArchivedMessage> messages = conversation.getMessages()
.subList(fromIndex, toIndex);
for (ArchivedMessage message : messages) {
addMessageElement(chatElement, conversation, message);
}
if (resultSet != null && messages.size() > 0) {
resultSet.setFirst((long) fromIndex);
resultSet.setFirstIndex(fromIndex);
resultSet.setLast((long) toIndex - 1);
resultSet.setCount(conversation.getMessages().size());
chatElement.add(resultSet.createResultElement());
}
return reply;
}
private Conversation retrieve(JID from, RetrieveRequest request) {
return getPersistenceManager().getConversation(from.toBareJID(),
request.getWith(), request.getStart());
}
private Element addMessageElement(Element parentElement,
Conversation conversation, ArchivedMessage message) {
final Element messageElement;
final long secs;
secs = (message.getTime().getTime() - conversation.getStart().getTime()) / 1000;
messageElement = parentElement.addElement(message.getDirection()
.toString());
messageElement.addAttribute("secs", Long.toString(secs));
if (message.getWithJid() != null) {
messageElement.addAttribute("jid", message.getWithJid().toBareJID());
}
messageElement.addElement("body").setText(message.getBody());
return messageElement;
}
}
package com.reucon.openfire.plugin.archive.xep0136;
import com.reucon.openfire.plugin.archive.util.XmppDateUtil;
import com.reucon.openfire.plugin.archive.xep0059.XmppResultSet;
import org.dom4j.Element;
import org.dom4j.QName;
import java.util.Date;
/**
* A request to retrieve a list of collections.
*/
public class ListRequest
{
private String with;
private Date start;
private Date end;
private XmppResultSet resultSet;
public ListRequest(Element listElement)
{
if (listElement.attribute("with") != null)
{
this.with = listElement.attributeValue("with");
}
if (listElement.attribute("start") != null)
{
this.start = XmppDateUtil.parseDate(listElement.attributeValue("start"));
}
if (listElement.attribute("end") != null)
{
this.end = XmppDateUtil.parseDate(listElement.attributeValue("end"));
}
Element setElement = listElement.element(QName.get("set", XmppResultSet.NAMESPACE));
if (setElement != null)
{
resultSet = new XmppResultSet(setElement);
}
}
public String getWith()
{
return with;
}
public Date getStart()
{
return start;
}
public Date getEnd()
{
return end;
}
public XmppResultSet getResultSet()
{
return resultSet;
}
}
package com.reucon.openfire.plugin.archive.xep0136;
import com.reucon.openfire.plugin.archive.util.XmppDateUtil;
import org.dom4j.Element;
import java.util.Date;
/**
* A request to remove one or more collections.
* <p>
* To request the removal of a single collection the client sends an empty &lt;remove/&gt; element.<br>
* The 'with' (full JID) and 'start' attributes MUST be included to uniquely identify the collection.
* <p>
* The client may remove several collections at once.<br/>
* The 'start' and 'end' elements MAY be specified to indicate a date range.<br/>
* The 'with' attribute MAY be a full JID, bare JID or domain.
* <p>
* If the value of the optional 'open' attribute is set to 'true' then only collections that are currently
* being recorded automatically by the server (see Automated Archiving) are removed.
*/
public class RemoveRequest
{
private final String with;
private final Date start;
private final Date end;
private final boolean open;
public RemoveRequest(Element listElement)
{
this.with = listElement.attributeValue("with");
this.start = XmppDateUtil.parseDate(listElement.attributeValue("start"));
this.end = XmppDateUtil.parseDate(listElement.attributeValue("end"));
this.open = "true".equals(listElement.attributeValue("open"));
}
/**
* The 'with' attribute MAY be a full JID, bare JID or domain.<br>
* If the 'with' attribute is omitted then collections with any JID are removed.
*
* @return the value of the with attribute, may be <code>null</code>.
*/
public String getWith()
{
return with;
}
/**
* If the start date is before all the collections in the archive then all collections prior
* to the end date are removed.
*
* @return the value of the start attribute, may be <code>null</code>.
*/
public Date getStart()
{
return start;
}
/**
* If the end date is in the future then then all collections after the start date are removed.
*
* @return the value of the end attribute, may be <code>null</code>.
*/
public Date getEnd()
{
return end;
}
/**
* If the value of the optional 'open' attribute is set to 'true' then only collections that
* are currently being recorded automatically by the server (see Automated Archiving) are removed.
*
* @return the value of the open attribute or <code>false</code> if not set.
*/
public boolean getOpen()
{
return open;
}
}
\ No newline at end of file
package com.reucon.openfire.plugin.archive.xep0136;
import com.reucon.openfire.plugin.archive.util.XmppDateUtil;
import com.reucon.openfire.plugin.archive.xep0059.XmppResultSet;
import org.dom4j.Element;
import org.dom4j.QName;
import java.util.Date;
/**
* A request to retrieve a collection.
*/
public class RetrieveRequest
{
private String with;
private Date start;
private XmppResultSet resultSet;
public RetrieveRequest(Element listElement)
{
this.with = listElement.attributeValue("with");
this.start = XmppDateUtil.parseDate(listElement.attributeValue("start"));
Element setElement = listElement.element(QName.get("set", XmppResultSet.NAMESPACE));
if (setElement != null)
{
resultSet = new XmppResultSet(setElement);
}
}
public String getWith()
{
return with;
}
public Date getStart()
{
return start;
}
public XmppResultSet getResultSet()
{
return resultSet;
}
}
package com.reucon.openfire.plugin.archive.xep0136;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import org.jivesoftware.openfire.IQRouter;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.auth.UnauthorizedException;
import org.jivesoftware.openfire.disco.IQDiscoInfoHandler;
import org.jivesoftware.openfire.disco.ServerFeaturesProvider;
import org.jivesoftware.openfire.handler.IQHandler;
import org.jivesoftware.openfire.plugin.MonitoringPlugin;
import org.jivesoftware.util.Log;
import org.xmpp.packet.IQ;
import org.xmpp.packet.PacketError;
/**
* Encapsulates support for <a
* href="http://www.xmpp.org/extensions/xep-0136.html">XEP-0136</a>.
*/
public class Xep0136Support {
private static final String NAMESPACE_AUTO = "urn:xmpp:archive:auto";
final XMPPServer server;
final Map<String, IQHandler> element2Handlers;
final IQHandler iqDispatcher;
final Collection<IQHandler> iqHandlers;
public Xep0136Support(XMPPServer server) {
this.server = server;
this.element2Handlers = Collections
.synchronizedMap(new HashMap<String, IQHandler>());
this.iqDispatcher = new AbstractIQHandler("XEP-0136 IQ Dispatcher",
null) {
public IQ handleIQ(IQ packet) throws UnauthorizedException {
if (!MonitoringPlugin.getInstance().isEnabled()) {
return error(packet,
PacketError.Condition.feature_not_implemented);
}
final IQHandler iqHandler = element2Handlers.get(packet
.getChildElement().getName());
if (iqHandler != null) {
return iqHandler.handleIQ(packet);
} else {
return error(packet,
PacketError.Condition.feature_not_implemented);
}
}
};
iqHandlers = new ArrayList<IQHandler>();
// support for #ns-pref
// iqHandlers.add(new IQPrefHandler());
// support for #ns-manage
iqHandlers.add(new IQListHandler());
iqHandlers.add(new IQRetrieveHandler());
// iqHandlers.add(new IQRemoveHandler());
}
public void start() {
for (IQHandler iqHandler : iqHandlers) {
try {
iqHandler.initialize(server);
iqHandler.start();
} catch (Exception e) {
Log.error("Unable to initialize and start "
+ iqHandler.getClass());
continue;
}
element2Handlers.put(iqHandler.getInfo().getName(), iqHandler);
if (iqHandler instanceof ServerFeaturesProvider) {
for (Iterator<String> i = ((ServerFeaturesProvider) iqHandler)
.getFeatures(); i.hasNext();) {
server.getIQDiscoInfoHandler().addServerFeature(i.next());
}
}
}
server.getIQDiscoInfoHandler().addServerFeature(NAMESPACE_AUTO);
server.getIQRouter().addHandler(iqDispatcher);
}
public void stop() {
IQRouter iqRouter = server.getIQRouter();
IQDiscoInfoHandler iqDiscoInfoHandler = server.getIQDiscoInfoHandler();
for (IQHandler iqHandler : iqHandlers) {
element2Handlers.remove(iqHandler.getInfo().getName());
try {
iqHandler.stop();
iqHandler.destroy();
} catch (Exception e) {
Log.warn("Unable to stop and destroy " + iqHandler.getClass());
}
if (iqHandler instanceof ServerFeaturesProvider) {
for (Iterator<String> i = ((ServerFeaturesProvider) iqHandler)
.getFeatures(); i.hasNext();) {
if (iqDiscoInfoHandler != null) {
iqDiscoInfoHandler.removeServerFeature(i.next());
}
}
}
}
if (iqRouter != null) {
iqRouter.removeHandler(iqDispatcher);
}
}
}
/**
* $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 + MonitoringConstants.NAME + 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(600)
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", MonitoringConstants.NAME,
Arrays.asList(participation.getNickname()));
leftBody = LocaleUtils.getLocalizedString("muc.conversation.left.anonymous", MonitoringConstants.NAME,
Arrays.asList(participation.getNickname()));
}
else {
joinBody = LocaleUtils.getLocalizedString("muc.conversation.joined", MonitoringConstants.NAME,
Arrays.asList(participation.getNickname(), name));
leftBody = LocaleUtils.getLocalizedString("muc.conversation.left", MonitoringConstants.NAME,
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() == null ? "" : 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(
MonitoringConstants.NAME);
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 java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TimerTask;
import org.jivesoftware.openfire.archive.cluster.SendConversationEventsTask;
import org.jivesoftware.openfire.cluster.ClusterManager;
import org.jivesoftware.openfire.reporting.util.TaskEngine;
import org.jivesoftware.util.JiveConstants;
import org.jivesoftware.util.cache.CacheFactory;
/**
* 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 java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.TimerTask;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CopyOnWriteArraySet;
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.JiveConstants;
import org.jivesoftware.util.JiveGlobals;
import org.jivesoftware.util.LocaleUtils;
import org.jivesoftware.util.NotFoundException;
import org.jivesoftware.util.PropertyEventDispatcher;
import org.jivesoftware.util.PropertyEventListener;
import org.jivesoftware.util.StringUtils;
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;
/**
* 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, fromJIDResource, toJID, toJIDResource, 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", MonitoringConstants.NAME);
}
public Type getStatType() {
return Type.count;
}
public String getDescription() {
return LocaleUtils.getLocalizedString("stat.conversation.desc", MonitoringConstants.NAME);
}
public String getUnits() {
return LocaleUtils.getLocalizedString("stat.conversation.units", MonitoringConstants.NAME);
}
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().toBareJID());
pstmt.setString(3, message.getFromJID().getResource());
pstmt.setString(4, message.getToJID().toBareJID());
pstmt.setString(5, message.getToJID().getResource());
pstmt.setLong(6, message.getSentDate().getTime());
DbConnectionManager.setLargeTextField(pstmt, 7, 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(
MonitoringConstants.NAME);
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(
MonitoringConstants.NAME);
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(
MonitoringConstants.NAME);
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(MonitoringConstants.NAME);
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", MonitoringConstants.NAME),
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", MonitoringConstants.NAME) +
" " + participantsDetail,
FontFactory.getFont(FontFactory.HELVETICA, 12,
Font.BOLD));
document.add(chapterTitle);
Paragraph startDate = new Paragraph(
LocaleUtils.getLocalizedString("archive.search.pdf.startdate", MonitoringConstants.NAME) +
" " +
coninfo.getDate(),
FontFactory.getFont(FontFactory.HELVETICA, 12,
Font.BOLD));
document.add(startDate);
Paragraph duration = new Paragraph(
LocaleUtils.getLocalizedString("archive.search.pdf.duration", MonitoringConstants.NAME) +
" " +
coninfo.getDuration(),
FontFactory.getFont(FontFactory.HELVETICA, 12,
Font.BOLD));
document.add(duration);
Paragraph messageCount = new Paragraph(
LocaleUtils
.getLocalizedString("archive.search.pdf.messagecount", MonitoringConstants.NAME) +
" " +
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",
MonitoringConstants.NAME) +
"</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;
}
}
package org.jivesoftware.openfire.archive;
public class MonitoringConstants {
public static final String NAME = "monitoring";
}
/**
* $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.archive.MonitoringConstants;
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(
MonitoringConstants.NAME);
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.archive.MonitoringConstants;
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(
MonitoringConstants.NAME);
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.archive.MonitoringConstants;
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(
MonitoringConstants.NAME);
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.archive.MonitoringConstants;
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(
MonitoringConstants.NAME);
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.archive.MonitoringConstants;
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(MonitoringConstants.NAME);
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 java.io.File;
import java.io.FileFilter;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.archive.ArchiveIndexer;
import org.jivesoftware.openfire.archive.ArchiveInterceptor;
import org.jivesoftware.openfire.archive.ArchiveSearcher;
import org.jivesoftware.openfire.archive.ConversationManager;
import org.jivesoftware.openfire.archive.GroupConversationInterceptor;
import org.jivesoftware.openfire.archive.MonitoringConstants;
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.DefaultStatsViewer;
import org.jivesoftware.openfire.reporting.stats.MockStatsViewer;
import org.jivesoftware.openfire.reporting.stats.StatisticsModule;
import org.jivesoftware.openfire.reporting.stats.StatsEngine;
import org.jivesoftware.openfire.reporting.stats.StatsViewer;
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 com.reucon.openfire.plugin.archive.ArchiveManager;
import com.reucon.openfire.plugin.archive.ArchiveProperties;
import com.reucon.openfire.plugin.archive.IndexManager;
import com.reucon.openfire.plugin.archive.PersistenceManager;
import com.reucon.openfire.plugin.archive.impl.ArchiveManagerImpl;
import com.reucon.openfire.plugin.archive.impl.JdbcPersistenceManager;
import com.reucon.openfire.plugin.archive.xep0136.Xep0136Support;
/**
* Openfire Monitoring plugin.
*
* @author Matt Tucker
*/
public class MonitoringPlugin implements Plugin {
private static final int DEFAULT_CONVERSATION_TIMEOUT = 30; // minutes
private MutablePicoContainer picoContainer;
private boolean shuttingDown = false;
private int conversationTimeout;
private static MonitoringPlugin instance;
private boolean enabled = true;
private PersistenceManager persistenceManager;
private ArchiveManager archiveManager;
private IndexManager indexManager;
private Xep0136Support xep0136Support;
public MonitoringPlugin() {
instance = this;
// 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;
}
}
public static MonitoringPlugin getInstance() {
return instance;
}
/* enabled property */
public boolean isEnabled() {
return this.enabled;
}
public ArchiveManager getArchiveManager() {
return archiveManager;
}
public IndexManager getIndexManager() {
return indexManager;
}
public PersistenceManager getPersistenceManager() {
return persistenceManager;
}
/**
* 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) {
/* Configuration */
conversationTimeout = JiveGlobals.getIntProperty(
ArchiveProperties.CONVERSATION_TIMEOUT,
DEFAULT_CONVERSATION_TIMEOUT);
enabled = JiveGlobals.getBooleanProperty(ArchiveProperties.ENABLED,
false);
persistenceManager = new JdbcPersistenceManager();
archiveManager = new ArchiveManagerImpl(persistenceManager,
indexManager, conversationTimeout);
xep0136Support = new Xep0136Support(XMPPServer.getInstance());
xep0136Support.start();
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
+ MonitoringConstants.NAME);
if (!dir.exists()) {
dir.mkdirs();
}
picoContainer.start();
xep0136Support = new Xep0136Support(XMPPServer.getInstance());
xep0136Support.start();
}
public void destroyPlugin() {
shuttingDown = true;
if (picoContainer != null) {
picoContainer.stop();
picoContainer.dispose();
picoContainer = null;
}
instance = 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.jivesoftware.openfire.archive.MonitoringConstants;
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/"+MonitoringConstants.NAME+"/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("/"+MonitoringConstants.NAME+"/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 java.awt.Color;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.geom.Rectangle2D;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
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.jfree.chart.JFreeChart;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.archive.MonitoringConstants;
import org.jivesoftware.openfire.plugin.MonitoringPlugin;
import org.jivesoftware.openfire.reporting.stats.StatsViewer;
import org.jivesoftware.openfire.stats.Statistic;
import org.jivesoftware.util.JiveGlobals;
import org.jivesoftware.util.Log;
import org.jivesoftware.util.ParamUtils;
import com.lowagie.text.Chunk;
import com.lowagie.text.Document;
import com.lowagie.text.DocumentException;
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.DefaultFontMapper;
import com.lowagie.text.pdf.PdfContentByte;
import com.lowagie.text.pdf.PdfPageEventHelper;
import com.lowagie.text.pdf.PdfTemplate;
import com.lowagie.text.pdf.PdfWriter;
/**
*
*/
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(MonitoringConstants.NAME);
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/"+MonitoringConstants.NAME+"/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 java.util.concurrent.atomic.AtomicInteger;
import org.jivesoftware.openfire.SessionManager;
import org.jivesoftware.openfire.archive.MonitoringConstants;
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;
/**
* 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, MonitoringConstants.NAME,
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, MonitoringConstants.NAME, 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, MonitoringConstants.NAME, 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 java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.text.DateFormat;
import java.text.NumberFormat;
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.Iterator;
import java.util.List;
import java.util.Map;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.archive.Conversation;
import org.jivesoftware.openfire.archive.ConversationManager;
import org.jivesoftware.openfire.archive.MonitoringConstants;
import org.jivesoftware.openfire.plugin.MonitoringPlugin;
import org.jivesoftware.openfire.reporting.graph.GraphEngine;
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;
/**
* 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(MonitoringConstants.NAME);
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(MonitoringConstants.NAME);
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", MonitoringConstants.NAME);
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(MonitoringConstants.NAME);
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.archive.MonitoringConstants;
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 + MonitoringConstants.NAME
+ 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