Commit f8583a62 authored by Gaston Dombiak's avatar Gaston Dombiak Committed by gato

Port from 3.5.1.

git-svn-id: http://svn.igniterealtime.org/svn/repos/openfire/trunk@10310 b35dd754-fafc-0310-a699-88a17e54d16e
parent 03f3e222
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html>
<head>
<title>Monitoring Plugin Changelog</title>
<style type="text/css">
BODY {
font-size : 100%;
}
BODY, TD, TH {
font-family : tahoma, verdana, arial, helvetica, sans-serif;
font-size : 0.8em;
}
H2 {
font-size : 10pt;
font-weight : bold;
padding-left : 1em;
}
A:hover {
text-decoration : none;
}
H1 {
font-family : tahoma, arial, helvetica, sans-serif;
font-size : 1.4em;
font-weight: bold;
border-bottom : 1px #ccc solid;
padding-bottom : 2px;
}
TT {
font-family : courier new;
font-weight : bold;
color : #060;
}
PRE {
font-family : courier new;
font-size : 100%;
}
</style>
</head>
<body>
<h1>
Monitoring Plugin Changelog
</h1>
<p><b>1.1.0</b> -- July 31, 2008</p>
<ul>
<li>Compatible version with Openfire 3.6.0. </li>
</ul>
<p><b>1.0.0</b> -- April 24, 2008</p>
<ul>
<li>Initial release. </li>
</ul>
</body>
</html>
<?xml version="1.0" encoding="UTF-8"?>
<plugin>
<class>org.jivesoftware.openfire.plugin.MonitoringPlugin</class>
<name>Monitoring Service</name>
<description>Monitors conversations and statistics of the server.</description>
<author>Jive Software</author>
<version>1.1.0</version>
<date>7/31/2008</date>
<minServerVersion>3.5.1</minServerVersion>
<databaseKey>monitoring</databaseKey>
<databaseVersion>0</databaseVersion>
<adminconsole>
<tab id="tab-server">
<sidebar id="stats-dashboard" name="${admin.sidebar.statistics.name}" description="${admin.item.stats-dashboard.description}">
<item id="statistics" name="${admin.sidebar.statistics.name}"
url="stats-dashboard.jsp"
description="${admin.sidebar.statistics.description}"/>
<item id="stats-reporter" name="${admin.item.stats-reporter.name}"
url="stats-reporter.jsp"
description="${admin.item.stats-reporter.description}"/>
</sidebar>
<sidebar id="archiving" name="${admin.sidebar.archiving.name}" description="${admin.sidebar.archiving.description}">
<item id="archive-search" name="${admin.item.archive-search.name}"
url="archive-search.jsp"
description="${admin.item.archive-search.description}"/>
<item id="archiving-settings" name="${admin.item.archive-settings.name}"
url="archiving-settings.jsp"
description="${admin.item.archive-settings.description}"/>
<item id="active-conversations" name="${admin.item.active-conversations.name}"
url="conversations.jsp"
description="${admin.item.active-conversations.description}"/>
</sidebar>
</tab>
</adminconsole>
</plugin>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html>
<head>
<title>Monitoring Plugin Readme</title>
<style type="text/css">
BODY {
font-size : 100%;
}
BODY, TD, TH {
font-family : tahoma, verdana, arial, helvetica, sans-serif;
font-size : 0.8em;
}
H2 {
font-size : 10pt;
font-weight : bold;
}
A:hover {
text-decoration : none;
}
H1 {
font-family : tahoma, arial, helvetica, sans-serif;
font-size : 1.4em;
font-weight: bold;
border-bottom : 1px #ccc solid;
padding-bottom : 2px;
}
TT {
font-family : courier new;
font-weight : bold;
color : #060;
}
PRE {
font-family : courier new;
font-size : 100%;
}
#datatable TH {
color : #fff;
background-color : #2A448C;
text-align : left;
}
#datatable TD {
background-color : #FAF6EF;
}
#datatable .name {
background-color : #DCE2F5;
}
</style>
</head>
<body>
<h1>
Monitoring Plugin Readme
</h1>
<h2>Overview</h2>
<p>
The monitoring plugin adds support for chat archiving and server statistics to
Openfire. It provides support for generating various reports on the server
statistics, as well as managing, viewing, and reporting on chat logs.
</p>
<h2>Installation</h2>
<p>
Copy monitoring.jar into the plugins directory of your Openfire installation.
The plugin will then be automatically deployed. To upgrade to a new version,
copy the new monitoring.jar file over the existing file.
</p>
<h2>Upgrading from Enterprise</h2>
<p>
If you are upgrading from the Enterprise plugin, and wish to keep your old
statistics and logs, you will need to manually run some database scripts to
perform the migration. Note, if you don't care about your previous
statistics and chat logs, you don't have to worry about these steps.
</p>
<p>
First, you will need to shut down your Openfire server and remove the
enterprise plugin. To do this, perform the following steps:
<ol>
<li>Shut down your Openfire server</li>
<li>Remove the <b>enterprise.jar</b> file and the <b>enterprise</b> directory from the plugins directory in your Openfire install root</li>
<li>Install this plugin, <b>monitoring.jar</b> by copying it into the plugins directory.</li>
<li>At this point, you will need to start up Openfire and let it extract and install the <b>monitoring</b> plugin. You can watch for this to occur by looking under the Plugins tab in the Openfire admin console. Once it appears in the list, continue to the next step.</li>
<li>Shut the server back down again.</li>
<li>Go into your plugins/monitoring/database directory. There you will see
some scripts prefixed with <b>import_</b>. Log into your database, switch
to the Openfire's database as you configured during setup (you can find
this information in conf/openfire.xml if you don't remember it), and run
the script that matches the database you are using. Note that the embedded
database is hsqldb and you can use the script in bin/extra from the Openfire
install root (bin/extra/embedded-db-viewer.sh or
bin/extra/embedded-db-viewer.bat, depending on whether you are using Windows)
to access your embedded database.</li>
<li>Once the script has completed, you can start Openfire back up and all of your settings should be the way they were when you were running the Enterprise plugin.</li>
</ol>
</p>
<h2>Configuration</h2>
<p>
Chat archiving is enabled by default. However, only information about
who is communicating and at what time is stored unless chat transcript
archiving is enabled. To enable chat transcript archiving or group chat
archiving, you will need to log into the admin console and go to:<br />
Server --&gt; Archiving --&gt; Archiving Settings
</p>
</body>
</html>
TRUNCATE TABLE ofConversation;
INSERT INTO ofConversation
(conversationID, room, isExternal, startDate, lastActivity, messageCount)
SELECT conversationID, room, isExternal, startDate, lastActivity, messageCount
FROM entConversation;
TRUNCATE TABLE ofConParticipant;
INSERT INTO ofConParticipant
(conversationID, joinedDate, leftDate, bareJID, jidResource, nickname)
SELECT conversationID, joinedDate, leftDate, bareJID, jidResource, nickname
FROM entConParticipant;
TRUNCATE TABLE ofMessageArchive;
INSERT INTO ofMessageArchive
(conversationID, fromJID, toJID, sentDate, body)
SELECT conversationID, fromJID, toJID, sentDate, body
FROM entMessageArchive;
TRUNCATE TABLE ofRRDs;
INSERT INTO ofRRDs
(id, updatedDate, bytes)
SELECT id, updatedDate, bytes
FROM entRRDs;
TRUNCATE TABLE ofConversation;
INSERT INTO ofConversation
(conversationID, room, isExternal, startDate, lastActivity, messageCount)
SELECT conversationID, room, isExternal, startDate, lastActivity, messageCount
FROM entConversation;
TRUNCATE TABLE ofConParticipant;
INSERT INTO ofConParticipant
(conversationID, joinedDate, leftDate, bareJID, jidResource, nickname)
SELECT conversationID, joinedDate, leftDate, bareJID, jidResource, nickname
FROM entConParticipant;
TRUNCATE TABLE ofMessageArchive;
INSERT INTO ofMessageArchive
(conversationID, fromJID, toJID, sentDate, body)
SELECT conversationID, fromJID, toJID, sentDate, body
FROM entMessageArchive;
TRUNCATE TABLE ofRRDs;
INSERT INTO ofRRDs
(id, updatedDate, bytes)
SELECT id, updatedDate, bytes
FROM entRRDs;
TRUNCATE TABLE ofConversation;
INSERT INTO ofConversation
(conversationID, room, isExternal, startDate, lastActivity, messageCount)
SELECT conversationID, room, isExternal, startDate, lastActivity, messageCount
FROM entConversation;
TRUNCATE TABLE ofConParticipant;
INSERT INTO ofConParticipant
(conversationID, joinedDate, leftDate, bareJID, jidResource, nickname)
SELECT conversationID, joinedDate, leftDate, bareJID, jidResource, nickname
FROM entConParticipant;
TRUNCATE TABLE ofMessageArchive;
INSERT INTO ofMessageArchive
(conversationID, fromJID, toJID, sentDate, body)
SELECT conversationID, fromJID, toJID, sentDate, body
FROM entMessageArchive;
TRUNCATE TABLE ofRRDs;
INSERT INTO ofRRDs
(id, updatedDate, bytes)
SELECT id, updatedDate, bytes
FROM entRRDs;
TRUNCATE TABLE ofConversation;
INSERT INTO ofConversation
(conversationID, room, isExternal, startDate, lastActivity, messageCount)
SELECT conversationID, room, isExternal, startDate, lastActivity, messageCount
FROM entConversation;
TRUNCATE TABLE ofConParticipant;
INSERT INTO ofConParticipant
(conversationID, joinedDate, leftDate, bareJID, jidResource, nickname)
SELECT conversationID, joinedDate, leftDate, bareJID, jidResource, nickname
FROM entConParticipant;
TRUNCATE TABLE ofMessageArchive;
INSERT INTO ofMessageArchive
(conversationID, fromJID, toJID, sentDate, body)
SELECT conversationID, fromJID, toJID, sentDate, body
FROM entMessageArchive;
TRUNCATE TABLE ofRRDs;
INSERT INTO ofRRDs
(id, updatedDate, bytes)
SELECT id, updatedDate, bytes
FROM entRRDs;
commit;
TRUNCATE TABLE ofConversation;
INSERT INTO ofConversation
(conversationID, room, isExternal, startDate, lastActivity, messageCount)
SELECT conversationID, room, isExternal, startDate, lastActivity, messageCount
FROM entConversation;
TRUNCATE TABLE ofConParticipant;
INSERT INTO ofConParticipant
(conversationID, joinedDate, leftDate, bareJID, jidResource, nickname)
SELECT conversationID, joinedDate, leftDate, bareJID, jidResource, nickname
FROM entConParticipant;
TRUNCATE TABLE ofMessageArchive;
INSERT INTO ofMessageArchive
(conversationID, fromJID, toJID, sentDate, body)
SELECT conversationID, fromJID, toJID, sentDate, body
FROM entMessageArchive;
TRUNCATE TABLE ofRRDs;
INSERT INTO ofRRDs
(id, updatedDate, bytes)
SELECT id, updatedDate, bytes
FROM entRRDs;
TRUNCATE TABLE ofConversation;
INSERT INTO ofConversation
(conversationID, room, isExternal, startDate, lastActivity, messageCount)
SELECT conversationID, room, isExternal, startDate, lastActivity, messageCount
FROM entConversation;
TRUNCATE TABLE ofConParticipant;
INSERT INTO ofConParticipant
(conversationID, joinedDate, leftDate, bareJID, jidResource, nickname)
SELECT conversationID, joinedDate, leftDate, bareJID, jidResource, nickname
FROM entConParticipant;
TRUNCATE TABLE ofMessageArchive;
INSERT INTO ofMessageArchive
(conversationID, fromJID, toJID, sentDate, body)
SELECT conversationID, fromJID, toJID, sentDate, body
FROM entMessageArchive;
TRUNCATE TABLE ofRRDs;
INSERT INTO ofRRDs
(id, updatedDate, bytes)
SELECT id, updatedDate, bytes
FROM entRRDs;
-- $Revision$
-- $Date$
INSERT INTO ofVersion (name, version) VALUES ('monitoring', 0);
CREATE TABLE ofConversation (
conversationID INTEGER NOT NULL,
room VARCHAR(512),
isExternal INTEGER NOT NULL,
startDate BIGINT NOT NULL,
lastActivity BIGINT NOT NULL,
messageCount INTEGER NOT NULL,
CONSTRAINT ofConversation_pk PRIMARY KEY (conversationID)
);
CREATE INDEX ofConversation_ext_idx ON ofConversation (isExternal);
CREATE INDEX ofConversation_start_idx ON ofConversation (startDate);
CREATE INDEX ofConversation_last_idx ON ofConversation (lastActivity);
CREATE TABLE ofConParticipant (
conversationID INTEGER NOT NULL,
joinedDate BIGINT NOT NULL,
leftDate BIGINT,
bareJID VARCHAR(255) NOT NULL,
jidResource VARCHAR(255) NOT NULL,
nickname VARCHAR(255)
);
CREATE INDEX entConPar_con_idx ON ofConParticipant (conversationID, bareJID, jidResource, joinedDate);
CREATE INDEX entConPar_jid_idx ON ofConParticipant (bareJID);
CREATE TABLE ofMessageArchive (
conversationID INTEGER NOT NULL,
fromJID VARCHAR(1024) NOT NULL,
toJID VARCHAR(1024) NOT NULL,
sentDate BIGINT NOT NULL,
body LONG VARCHAR
);
CREATE INDEX ofMessageArchive_con_idx ON ofMessageArchive (conversationID);
CREATE TABLE ofRRDs (
id VARCHAR(100) NOT NULL,
updatedDate BIGINT NOT NULL,
bytes BLOB,
CONSTRAINT ofRRDs_pk PRIMARY KEY (id)
);
// $Revision$
// $Date$
INSERT INTO ofVersion (name, version) VALUES ('monitoring', 0);
CREATE TABLE ofConversation (
conversationID BIGINT NOT NULL,
room VARCHAR(1024) NULL,
isExternal INT NOT NULL,
startDate BIGINT NOT NULL,
lastActivity BIGINT NOT NULL,
messageCount INT NOT NULL,
CONSTRAINT ofConversation_pk PRIMARY KEY (conversationID)
);
CREATE INDEX ofConversation_ext_idx ON ofConversation (isExternal);
CREATE INDEX ofConversation_start_idx ON ofConversation (startDate);
CREATE INDEX ofConversation_last_idx ON ofConversation (lastActivity);
CREATE TABLE ofConParticipant (
conversationID BIGINT NOT NULL,
joinedDate BIGINT NOT NULL,
leftDate BIGINT NULL,
bareJID VARCHAR(255) NOT NULL,
jidResource VARCHAR(255) NOT NULL,
nickname VARCHAR(255) NULL
);
CREATE INDEX ofConParticipant_conv_idx ON ofConParticipant (conversationID, bareJID, jidResource, joinedDate);
CREATE INDEX ofConParticipant_jid_idx ON ofConParticipant (bareJID);
CREATE TABLE ofMessageArchive (
conversationID BIGINT NOT NULL,
fromJID VARCHAR(1024) NOT NULL,
toJID VARCHAR(1024) NOT NULL,
sentDate BIGINT NOT NULL,
body LONGVARCHAR
);
CREATE INDEX ofMessageArchive_con_idx ON ofMessageArchive (conversationID);
CREATE TABLE ofRRDs (
id VARCHAR(100) 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);
CREATE TABLE ofConversation (
conversationID BIGINT NOT NULL,
room VARCHAR(255) NULL,
isExternal TINYINT NOT NULL,
startDate BIGINT NOT NULL,
lastActivity BIGINT NOT NULL,
messageCount INT NOT NULL,
PRIMARY KEY (conversationID),
INDEX ofConversation_ext_idx (isExternal),
INDEX ofConversation_start_idx (startDate),
INDEX ofConversation_last_idx (lastActivity)
);
CREATE TABLE ofConParticipant (
conversationID BIGINT NOT NULL,
joinedDate BIGINT NOT NULL,
leftDate BIGINT NULL,
bareJID VARCHAR(200) NOT NULL,
jidResource VARCHAR(100) NOT NULL,
nickname VARCHAR(255) NULL,
INDEX ofConParticipant_conv_idx (conversationID, bareJID, jidResource, joinedDate),
INDEX ofConParticipant_jid_idx (bareJID)
);
CREATE TABLE ofMessageArchive (
conversationID BIGINT NOT NULL,
fromJID VARCHAR(255) NOT NULL,
toJID VARCHAR(255) NOT NULL,
sentDate BIGINT NOT NULL,
body TEXT,
INDEX entMsgArchive_con_idx (conversationID)
);
CREATE TABLE ofRRDs (
id VARCHAR(100) NOT NULL,
updatedDate BIGINT NOT NULL,
bytes MEDIUMBLOB NULL,
PRIMARY KEY (id)
);
-- $Revision$
-- $Date$
INSERT INTO ofVersion (name, version) VALUES ('monitoring', 0);
CREATE TABLE ofConversation (
conversationID INTEGER NOT NULL,
room VARCHAR2(1024) NULL,
isExternal NUMBER(2) NOT NULL,
startDate INTEGER NOT NULL,
lastActivity INTEGER NOT NULL,
messageCount INT NOT NULL,
CONSTRAINT ofConversation_pk PRIMARY KEY (conversationID)
);
CREATE INDEX ofConversation_ext_idx ON ofConversation (isExternal);
CREATE INDEX ofConversation_start_idx ON ofConversation (startDate);
CREATE INDEX ofConversation_last_idx ON ofConversation (lastActivity);
CREATE TABLE ofConParticipant (
conversationID INTEGER NOT NULL,
joinedDate INTEGER NOT NULL,
leftDate INTEGER NULL,
bareJID VARCHAR2(255) NOT NULL,
jidResource VARCHAR2(255) NOT NULL,
nickname VARCHAR2(255) NULL
);
CREATE INDEX ofConParticipant_conv_idx ON ofConParticipant (conversationID, bareJID, jidResource, joinedDate);
CREATE INDEX ofConParticipant_jid_idx ON ofConParticipant (bareJID);
CREATE TABLE ofMessageArchive (
conversationID INTEGER NOT NULL,
fromJID VARCHAR2(1024) NOT NULL,
toJID VARCHAR2(1024) NOT NULL,
sentDate INTEGER NOT NULL,
body LONG
);
CREATE INDEX ofMessageArchive_con_idx ON ofMessageArchive (conversationID);
CREATE TABLE ofRRDs (
id VARCHAR2(100) NOT NULL,
updatedDate INTEGER NOT NULL,
bytes BLOB NULL,
CONSTRAINT ofRRDs_pk PRIMARY KEY (id)
);
-- $Revision$
-- $Date$
INSERT INTO ofVersion (name, version) VALUES ('monitoring', 0);
CREATE TABLE ofConversation (
conversationID INTEGER NOT NULL,
room VARCHAR(1024) NULL,
isExternal SMALLINT NOT NULL,
startDate BIGINT NOT NULL,
lastActivity BIGINT NOT NULL,
messageCount INTEGER NOT NULL,
CONSTRAINT ofConversation_pk PRIMARY KEY (conversationID)
);
CREATE INDEX ofConversation_ext_idx ON ofConversation (isExternal);
CREATE INDEX ofConversation_start_idx ON ofConversation (startDate);
CREATE INDEX ofConversation_last_idx ON ofConversation (lastActivity);
CREATE TABLE ofConParticipant (
conversationID INTEGER NOT NULL,
joinedDate BIGINT NOT NULL,
leftDate BIGINT NULL,
bareJID VARCHAR(255) NOT NULL,
jidResource VARCHAR(255) NOT NULL,
nickname VARCHAR(255) NULL
);
CREATE INDEX ofConParticipant_conv_idx ON ofConParticipant (conversationID, bareJID, jidResource, joinedDate);
CREATE INDEX ofConParticipant_jid_idx ON ofConParticipant (bareJID);
CREATE TABLE ofMessageArchive (
conversationID INTEGER NOT NULL,
fromJID VARCHAR(1024) NOT NULL,
toJID VARCHAR(1024) NOT NULL,
sentDate BIGINT NOT NULL,
body TEXT
);
CREATE INDEX ofMessageArchive_con_idx ON ofMessageArchive (conversationID);
CREATE TABLE ofRRDs (
id VARCHAR(100) NOT NULL,
updatedDate BIGINT NOT NULL,
bytes bytea NULL,
CONSTRAINT ofRRDs_pk PRIMARY KEY (id)
);
/* $Revision$ */
/* $Date$ */
INSERT INTO ofVersion (name, version) VALUES ('monitoring', 0);
CREATE TABLE ofConversation (
conversationID BIGINT NOT NULL,
room NVARCHAR(1024) NULL,
isExternal TINYINT NOT NULL,
startDate BIGINT NOT NULL,
lastActivity BIGINT NOT NULL,
messageCount INT NOT NULL,
CONSTRAINT ofConversation_pk PRIMARY KEY (conversationID)
);
CREATE INDEX ofConversation_ext_idx ON ofConversation (isExternal);
CREATE INDEX ofConversation_start_idx ON ofConversation (startDate);
CREATE INDEX ofConversation_last_idx ON ofConversation (lastActivity);
CREATE TABLE ofConParticipant (
conversationID BIGINT NOT NULL,
joinedDate BIGINT NOT NULL,
leftDate BIGINT NULL,
bareJID NVARCHAR(255) NOT NULL,
jidResource NVARCHAR(255) NOT NULL,
nickname NVARCHAR(255) NULL
);
CREATE INDEX ofConParticipant_conv_idx ON ofConParticipant (conversationID, bareJID, jidResource, joinedDate);
CREATE INDEX ofConParticipant_jid_idx ON ofConParticipant (bareJID);
CREATE TABLE ofMessageArchive (
conversationID BIGINT NOT NULL,
fromJID NVARCHAR(1024) NOT NULL,
toJID NVARCHAR(1024) NOT NULL,
sentDate BIGINT NOT NULL,
body NTEXT
);
CREATE INDEX ofMessageArchive_con_idx ON ofMessageArchive (conversationID);
CREATE TABLE ofRRDs (
id NVARCHAR(100) NOT NULL,
updatedDate BIGINT NOT NULL,
bytes IMAGE NULL,
CONSTRAINT ofRRDs_pk PRIMARY KEY (id)
);
# $RCSfile$
# $Revision: 3148 $
# $Date: 2005-12-01 14:50:45 -0300 (Thu, 01 Dec 2005) $
##
## Monitoring Resource Bundle
##
## Additional locales can be specified by creating a new resource file in this
## directory using the following conventions:
##
## monitoring_i18n "_" language "_" country ".properties"
## monitoring_i18n "_" language ".properties"
##
## e.g.
## monitoring_i18n_en.propertis <- English resources
## monitoring_i18n_en_US.properties <- American US resources
## monitoring_i18n_de.properties <- German resources
## monitoring_i18n_ja.properties <- Japanese resources
##
## Please note that the two digit language code should be lower case, and the
## two digit country code should be in uppercase. Often, it is not necessary to
## specify the country code.
##
## A full list of language codes can be found at
## http://www-old.ics.uci.edu/pub/ietf/http/related/iso639.txt
## and a full list of country codes can be found at
## http://www.chemie.fu-berlin.de/diverse/doc/ISO_3166.html
##
## In property strings that are parameterized, single quotes can be used to
## quote the "{" (curly brace) if necessary. A real single quote is represented by ''.
##
## REVISION HISTORY (by Monitoring version):
##
## 1.0.0
## Initial Release
# Monitoring
login.title = Admin Console
error.exception = Exception:
admin.tab.monitoring.name=Monitoring
admin.tab.monitoring.description=Monitoring Server
admin.sidebar.statistics.name=Statistics
admin.sidebar.statistics.description=Statistics
admin.item.stats-dashboard.name=Dashboard
admin.item.stats-dashboard.description=View overview of server statistics.
admin.item.stats-reporter.name=All Reports
admin.item.stats-reporter.description=View detailed reports of server statistics.
admin.sidebar.archiving.name=Archiving
admin.sidebar.archiving.description=Archiving Settings
admin.item.archive-search.name=Search Archive
admin.item.archive-search.description=Click to search archive.
admin.item.archive-settings.name=Archiving Settings
admin.item.archive-settings.description=Click to configure archiving.
admin.item.active-conversations.name=Conversations
admin.item.active-conversations.description=Click to view active conversations.
archive.settings.title = Archive Settings
archive.settings.success = Archive Settings have been saved.
archive.settings.rebuild.success = Search Indexes are rebuilding.
archive.settings.message.metadata.title = Message and Metadata Settings
archive.settings.message.metadata.description = Enable or disable message and/or metadata archiving.
archive.settings.description = Use the form below to manage the archiving settings.
archive.settings.enable.metadata = Conversation State Archiving
archive.settings.enable.metadata.description = Record who talks to who, how long their conversations last, \
and the number of messages in each conversation. The actual message contents will not be recorded unless \
message archiving is enabled.
archive.settings.enable.message = Message Archiving
archive.settings.enable.message.description = Archive the full text of all messages sent between users. \
Message text will be searchable using keywords.
archive.settings.idle.time = Idle Time
archive.settings.idle.time.description = The number of minutes a conversation can be idle before it's ended.
archive.settings.max.time = Max Time
archive.settings.max.time.description = The maximum number of minutes a conversation can last before it's ended.
archive.settings.index.settings = Index Settings
archive.settings.index.settings.description = View and/or rebuild the current Search Index.
archive.settings.current.index = Current Search Index
archive.settings.current.index.description = The current size of the message index.
archive.settings.message.count = Archived Message Count
archive.settings.message.count.description = The total number of archived messages.
archive.settings.conversation.count = Archived Conversation Count
archive.settings.conversation.count.description = The total number of archived conversations.
archive.settings.update.settings = Update Settings
archive.settings.cancel = Cancel
archive.settings.rebuild = Rebuild Index
archive.settings.any = Any
archive.settings.one_to_one=Archive one-to-one chats
archive.settings.group_chats=Archive group chats
archive.settings.certain_rooms=Only archive conversations of the following room names (separated by comma)
archive.search.title = Search Archive
archive.search.participants = Participant(s):
archive.search.participants.tooltip = Enter or browse for the name of the participant to search for. \
You can also enter a second participant for more specific conversations.
archive.search.participants.any = Any
archive.search.participants.browse = Browse
archive.search.daterange = Date Range:
archive.search.daterange.tooltip = Enter specific date ranges to search between. You can specify a \
start date and/or end date.
archive.search.daterange.start = Start:
archive.search.daterange.end = End:
archive.search.daterange.any = Any
archive.search.daterange.format = Use mm/dd/yy
archive.search.daterange.error = The end time must be after the start time.
archive.search.keywords = Keywords:
archive.search.keywords.optional = (optional)
archive.search.keywords.disabled = Keyword searching is disabled. To enable keyword searching, you must \
enable message archiving in {0}Archiving Settings{1}.
archive.search.pdf.title = Conversation Transcript
archive.search.pdf.participants = Participants:
archive.search.pdf.startdate = Start Date:
archive.search.pdf.duration = Duration:
archive.search.pdf.messagecount = Message Count:
archive.search.submit = Search
archive.search.results = Search Results:
archive.search.results.description = Your search returned {0} results. Select a result on the left to view the chat conversation.
archive.search.results.xofy = of
archive.search.results.participants = Participants:
archive.search.results.messagecount = Message Count:
archive.search.results.date = Date:
archive.search.results.duration = Duration:
archive.search.results.none = No conversations were found using the specified search criteria. Please change your search criteria and try again.
archive.search.results.archive_disabled = The messages in this conversation were not archived.
archive.conversations = Active conversations:
archive.conversations.users = Users
archive.conversations.duration = Duration
archive.conversations.lastactivity = Last Activity
archive.conversations.messages = Messages
archive.converations.no_conversations = No active conversations.
stat.active_group_chats.name = Group Chat: Rooms
stat.active_group_chats.desc = The number of group chat rooms that have been active over time.
stat.active_group_chats.units = Group chat Rooms
stat.server_sessions.name = Server to Server Connections
stat.server_sessions.desc = Number of Server to Server Connections.
stat.server_sessions.units = S2S Connections
stat.sessions.name = Client Connections
stat.sessions.desc = Number of Clients Connected Directly to the Server.
stat.sessions.units = Client Connections
stat.packet_count.name = Packet Count
stat.packet_count.desc = Number of Packets Sent and Received by Server.
stat.packet_count.units = Packets per Minute
stat.conversation.name = Conversations
stat.conversation.desc = Conversations between users.
stat.conversation.units = Conversations
# Bookmarks
users = Users
groups = Groups
options = Options
cancel = Cancel
create = Create
# Dashboard
dashboard.title = Dashboard
dashboard.description = A snapshot of the current activity in the Server.
dashboard.directions = Click on the graphs below to see an enlargement.
dashboard.snapshot.enlarge = Enlarge Graph
dashboard.snapshot.shrink = Shrink Graph
dashboard.timespan = Timespan:
dashboard.timespan.lasthour = 1 Hour
dashboard.timespan.last24hours = 24 Hours
dashboard.timespan.last7days = 7 Days
dashboard.spotlights.high = High:
dashboard.spotlights.low = Low:
dashboard.spotlights.packetactivity = Packets Per Minute
dashboard.spotlights.activeconversations = Active Conversations
dashboard.spotlights.currentusers = Current Users
dashboard.quickstats = Quick Stats
dashboard.quickstats.high = High
dashboard.quickstats.low = Low
dashboard.currentconversations = Current Conversations
dashboard.currentconversations.details = view details
dashboard.currentconversations.users = Users
dashboard.currentconversations.lastactivity = Last Activity
dashboard.currentconversations.messagecount = Messages
dashboard.currentconversations.none = No active conversations.
dashboard.group_conversation=Group Conversation
# All Reports
allreports.title = All Reports
allreports.daterange = Date Range
allreports.daterange.preset = Preset
allreports.daterange.preset.last60minutes = Last 60 minutes
allreports.daterange.preset.last24hours = Last 24 hours
allreports.daterange.preset.thisweek = This week
allreports.daterange.preset.last7days = Last 7 days
allreports.daterange.preset.lastweek = Last week
allreports.daterange.preset.thismonth = This month
allreports.daterange.preset.lastmonth = Last month
allreports.daterange.preset.last3months = Last 3 months
allreports.daterange.specific = Specific
allreports.daterange.specific.startdate = Start:
allreports.daterange.specific.enddate = End:
allreports.daterange.startdate.error = Please enter a start date in the format mm/dd/yy.
allreports.daterange.enddate.error = Please enter an end date before tomorrow in the format mm/dd/yy.
allreports.selectreport = Select Report
allreports.download.allreports = Download All Reports
allreports.download.allreports.pdf = PDF
allreports.download.allreports.pdf.format = PDF Format
allreports.download.singlereport = Download this report:
allreports.download.singlereport.pdf = PDF
allreports.reportinformation = Report Information
archive.group_conversation={0}Group Conversation{1}
archive.search.group_conversation=Group Chat: {0}
archive.group_conversation.close=Close
archive.group_conversation.participants=Participants
archive.group_conversation.participants.title=Group Chat Participants
archive.group_conversation.participants.description=The table below shows the users that participated in the group \
conversation that took place in the room: {0}.
archive.group_conversation.participants.participant=Users
archive.group_conversation.participants.empty=No participants were found.
muc.conversation.joined.anonymous={0} (anonymous) has joined the room
muc.conversation.left.anonymous={0} (anonymous) has left the room
muc.conversation.joined={0} ({1}) has joined the room
muc.conversation.left={0} ({1}) has left the room
\ No newline at end of file
# $RCSfile$
# $Revision: 3148 $
# $Date: 2005-12-01 14:50:45 -0300 (Thu, 01 Dec 2005) $
##
## Monitoring Resource Bundle - Czech locale (cs_CZ)
##
## For a full changelog, refer to the English bundle, monitoring_i18n.properties.
##
## Updated for release: 1.0.0
# Monitoring
login.title = Administr\u00e1torsk\u00e1 konzole
error.exception = V\u00fdjimka:
admin.tab.monitoring.name=Monitoring
admin.tab.monitoring.description=Monitoring
admin.sidebar.statistics.name=Statistiky
admin.sidebar.statistics.description="Openfire statistiky
admin.item.stats-dashboard.name=Dashboard
admin.item.stats-dashboard.description=Zobrazen\u00ed p\u0159ehledu statistik serveru.
admin.item.stats-reporter.name=V\u0161echny z\u00e1znamy
admin.item.stats-reporter.description=Zobrazen\u00ed detailn\u00edch z\u00e1znam\u016f statistik serveru.
admin.sidebar.archiving.name=Archivace
admin.sidebar.archiving.description=Nastaven\u00ed archivace Openfire
admin.item.archive-search.name=Prohled\u00e1v\u00e1n\u00ed archivu
admin.item.archive-search.description=Klikn\u011bte pro prohled\u00e1v\u00e1n\u00ed archivu
admin.item.archive-settings.name=Nastaven\u00ed archivace
admin.item.archive-settings.description=Klikn\u011bte pro nastaven\u00ed archivace.
admin.item.active-conversations.name=Konverzace
admin.item.active-conversations.description=Klikn\u011bte pro zobrazen\u00ed aktivn\u00edch konverzac\u00ed.
archive.settings.title = Nastaven\u00ed archivace
archive.settings.success = Nastaven\u00ed archivace bylo ulo\u017eeno.
archive.settings.rebuild.success = Vyhled\u00e1vac\u00ed indexy jsou obnovov\u00e1ny.
archive.settings.message.metadata.title = Nastaven\u00ed zpr\u00e1v a metadat
archive.settings.message.metadata.description = Povol\u00ed nebo zak\u00e1\u017ee archivaci zpr\u00e1v a/nebo metadat.
archive.settings.description = Pou\u017eijte spodn\u00ed formul\u00e1\u0159 pro spr\u00e1vu nastaven\u00ed archivace.
archive.settings.enable.metadata = Archivace stavu konverzace
archive.settings.enable.metadata.description = Zaznamen\u00e1v\u00e1 kdo s k\u00fdm mluv\u00ed, jak dlouho trv\u00e1 jejich konverzace, \
a po\u010det zpr\u00e1v v ka\u017ed\u00e9 konverzaci. Skute\u010dn\u00fd obsah zpr\u00e1v nebude zaznamen\u00e1v\u00e1n pokud nebude \
povolena archivace zpr\u00e1v.
archive.settings.enable.message = Archivace zpr\u00e1v
archive.settings.enable.message.description = Archivuje cel\u00fd text v\u0161ech zpr\u00e1v zaslan\u00fdch mezi u\u017eivateli. \
Text zpr\u00e1v bude mo\u017en\u00e9 prohled\u00e1vat za pomoci kl\u00ed\u010dov\u00fdch slov.
archive.settings.idle.time = Doba ne\u010dinnosti
archive.settings.idle.time.description = Po\u010det minut b\u011bhem kter\u00fdch m\u016f\u017ee b\u00fdt konverzace ne\u010dinn\u00e1 ne\u017e bude ukon\u010dena.
archive.settings.max.time = Maxim\u00e1ln\u00ed \u010das
archive.settings.max.time.description = Maxim\u00e1ln\u00ed po\u010det minut trv\u00e1n\u00ed konverzace ne\u017e bude ukon\u010dena.
archive.settings.index.settings = Nastaven\u00ed index\u016f
archive.settings.index.settings.description = Prohl\u00ed\u017een\u00ed a/nebo obnova aktu\u00e1ln\u00edho vyhled\u00e1vac\u00edho indexu.
archive.settings.current.index = Aktu\u00e1ln\u00ed vyhled\u00e1vac\u00ed index
archive.settings.current.index.description = Aktu\u00e1ln\u00ed velikost indexu zpr\u00e1v.
archive.settings.message.count = Po\u010det archivovan\u00fdch zpr\u00e1v
archive.settings.message.count.description = Celkov\u00fd po\u010det archivovan\u00fdch zpr\u00e1v.
archive.settings.conversation.count = Po\u010det archivovan\u00fdch konverzac\u00ed
archive.settings.conversation.count.description = Celkov\u00fd po\u010det archivovan\u00fdch konverzac\u00ed.
archive.settings.update.settings = Aktualizovat nastaven\u00ed
archive.settings.cancel = Zru\u0161it
archive.settings.rebuild = Obnovit index
archive.settings.any = Jak\u00fdkoli
archive.settings.one_to_one=Archivovat chaty jeden na jednoho
archive.settings.group_chats=Archivovat skupinov\u00e9 chaty
archive.settings.certain_rooms=Archivovat pouze konverzace n\u00e1sleduj\u00edc\u00edch jmen m\u00edstnost\u00ed (odd\u011blen\u00e9 \u010d\u00e1rkou)
archive.search.title = Prohled\u00e1v\u00e1n\u00ed archivu
archive.search.participants = \u00da\u010dastn\u00edk(-ci):
archive.search.participants.tooltip = Zadejte nebo vyberte jm\u00e9ho hledan\u00e9ho \u00fa\u010dastn\u00edka. \
M\u016f\u017eete tak\u00e9 zadat druh\u00e9ho \u00fa\u010dastn\u00edka pro z\u00edsk\u00e1n\u00ed konkr\u00e9tn\u011bj\u0161\u00ed konverzace.
archive.search.participants.any = Kdokoli
archive.search.participants.browse = Proch\u00e1zet
archive.search.daterange = Rozsah datum\u016f:
archive.search.daterange.tooltip = Zadejte specifick\u00fd rozsah datum\u016f, mezi kter\u00fdmi hledat. M\u016f\u017eete zadat \
po\u010d\u00e1te\u010dn\u00ed a/nebo koncov\u00e9 datum.
archive.search.daterange.start = Za\u010d\u00e1tek:
archive.search.daterange.end = Konec:
archive.search.daterange.any = Kdykoli
archive.search.daterange.format = Pou\u017eijte mm/dd/yy
archive.search.daterange.error = Koncov\u00e9 datum mus\u00ed b\u00fdt po po\u010d\u00e1te\u010dn\u00edm \u010dase.
archive.search.keywords = Kl\u00ed\u010dov\u00e1 slova:
archive.search.keywords.optional = (voliteln\u011b)
archive.search.keywords.disabled = Hled\u00e1n\u00ed kl\u00ed\u010dov\u00fdch slov je zak\u00e1z\u00e1no. Pro povolen\u00ed vyhled\u00e1v\u00e1n\u00ed kl\u00ed\u010dov\u00fdch slov mus\u00edte \
povolit archivaci zpr\u00e1v v {0}Nastaven\u00ed archivace{1}.
archive.search.pdf.title = P\u0159epis konverzace
archive.search.pdf.participants = \u00da\u010dastn\u00edci:
archive.search.pdf.startdate = Datum za\u010d\u00e1tku:
archive.search.pdf.duration = Trv\u00e1n\u00ed:
archive.search.pdf.messagecount = Po\u010det zpr\u00e1v:
archive.search.submit = Hled\u00e1n\u00ed
archive.search.results = V\u00fdsledky hled\u00e1n\u00ed:
archive.search.results.description = Va\u0161e hled\u00e1n\u00ed vr\u00e1tilo {0} v\u00fdsledk\u016f. Vyberte vlevo v\u00fdsledek pro zobrazen\u00ed konverzace chatu.
archive.search.results.xofy = z
archive.search.results.participants = \u00da\u010dastn\u00edci:
archive.search.results.messagecount = Po\u010det zpr\u00e1v:
archive.search.results.date = Datum:
archive.search.results.duration = Trv\u00e1n\u00ed:
archive.search.results.none = Nebyly nalezeny \u017e\u00e1dn\u00e9 konverzace za pou\u017eit\u00ed zadan\u00fdch v\u00fdb\u011brov\u00fdch podm\u00ednek. Pros\u00edm zm\u011b\u0148te va\u0161e vyhled\u00e1vac\u00ed podm\u00ednky a zkuste to znovu.
archive.search.results.archive_disabled = Zpr\u00e1vy v t\u00e9to konverzaci nebyly archivov\u00e1ny.
archive.conversations = Aktivn\u00ed konverzace:
archive.conversations.users = U\u017eivatel\u00e9
archive.conversations.duration = Trv\u00e1n\u00ed
archive.conversations.lastactivity = Posledn\u00ed aktivita
archive.conversations.messages = Zpr\u00e1vy
archive.converations.no_conversations = \u017d\u00e1dn\u00e9 aktivn\u00ed konverzace.
stat.active_group_chats.name = Skupinov\u00fd chat: M\u00edstnosti
stat.active_group_chats.desc = Po\u010det m\u00edstnost\u00ed skupinov\u00e9ho chatu, kter\u00e9 byly v posledn\u00ed dob\u011b aktivn\u00ed.
stat.active_group_chats.units = M\u00edstnost\u00ed skupinov\u00e9ho chatu
stat.server_sessions.name = Meziserverov\u00e9 relace
stat.server_sessions.desc = Po\u010det spojen\u00ed server-server.
stat.server_sessions.units = Spojen\u00ed S2S
stat.sessions.name = Klientsk\u00e9 relace
stat.sessions.desc = Po\u010det klient\u016f p\u0159\u00edmo p\u0159ipojen\u00fdch k serveru.
stat.sessions.units = Klientsk\u00e9 relace
stat.packet_count.name = Po\u010det paket\u016f
stat.packet_count.desc = Po\u010det paket\u016f p\u0159ijat\u00fdch a odeslan\u00fdch serverem Openfire.
stat.packet_count.units = Pakety za minutu
stat.conversation.name = Konverzace
stat.conversation.desc = Konverzace mezi u\u017eivateli.
stat.conversation.units = Konverzace
# Bookmarks
users = U\u017eivatel\u00e9
groups = Skupiny
options = Volby
cancel = Zru\u0161it
create = Vytvo\u0159it
# Dashboard
dashboard.title = Openfire Dashboard
dashboard.description = Zobrazen\u00ed sou\u010dasn\u00e9 aktivity ve Openfire.
dashboard.directions = Klikn\u011bte na spodn\u00ed grafy pro zobrazen\u00ed zv\u011bt\u0161eniny.
dashboard.snapshot.enlarge = Zv\u011bt\u0161it graf
dashboard.snapshot.shrink = Zmen\u0161it graf
dashboard.timespan = \u010casov\u00e9 rozp\u011bt\u00ed:
dashboard.timespan.lasthour = 1 hodina
dashboard.timespan.last24hours = 24 hodin
dashboard.timespan.last7days = 7 dn\u00ed
dashboard.spotlights.high = Vysok\u00e9:
dashboard.spotlights.low = N\u00edzk\u00e9:
dashboard.spotlights.packetactivity = Paket\u016f za minutu
dashboard.spotlights.activeconversations = Aktivn\u00ed konverzace
dashboard.spotlights.currentusers = Sou\u010dasn\u00ed u\u017eivatel\u00e9
dashboard.quickstats = Rychl\u00e9 statistiky
dashboard.quickstats.high = Vysok\u00e9
dashboard.quickstats.low = N\u00edzk\u00e9
dashboard.currentconversations = Sou\u010dasn\u00e9 konverzace
dashboard.currentconversations.details = zobrazit detaily
dashboard.currentconversations.users = U\u017eivatel\u00e9
dashboard.currentconversations.lastactivity = Posledn\u00ed aktivita
dashboard.currentconversations.messagecount = Zpr\u00e1vy
dashboard.currentconversations.none = \u017d\u00e1dn\u00e9 aktivn\u00ed konverzace.
# All Reports
allreports.title = V\u0161echny zpr\u00e1vy
allreports.daterange = Rozsah datum\u016f
allreports.daterange.preset = P\u0159ednastaveno
allreports.daterange.preset.last60minutes = Posledn\u00edch 60 minut
allreports.daterange.preset.last24hours = Posledn\u00edch 24 hodin
allreports.daterange.preset.thisweek = Tento t\u00fdden
allreports.daterange.preset.last7days = Posledn\u00edch 7 dn\u00ed
allreports.daterange.preset.lastweek = Posledn\u00ed t\u00fdden
allreports.daterange.preset.thismonth = Tento m\u011bs\u00edc
allreports.daterange.preset.lastmonth = Posledn\u00edch m\u011bs\u00edc
allreports.daterange.preset.last3months = Posledn\u00ed 3 m\u011bs\u00edce
allreports.daterange.specific = Specifick\u00e9
allreports.daterange.specific.startdate = Po\u010d\u00e1tek:
allreports.daterange.specific.enddate = konec:
allreports.daterange.startdate.error = Pros\u00edm zadejte po\u010d\u00e1te\u010dn\u00ed datum ve form\u00e1tu mm/dd/yy.
allreports.daterange.enddate.error = Pros\u00edm zadejte datum p\u0159ed z\u00edt\u0159kem ve form\u00e1tu mm/dd/yy.
allreports.selectreport = Vybrat zpr\u00e1vy
allreports.download.allreports = St\u00e1hnout v\u0161echny zpr\u00e1vy
allreports.download.allreports.pdf = PDF
allreports.download.allreports.pdf.format = Form\u00e1t PDF
allreports.download.singlereport = St\u00e1hnout tuto zpr\u00e1vu:
allreports.download.singlereport.pdf = PDF
allreports.reportinformation = Informace o zpr\u00e1v\u011b
# $RCSfile$
# $Revision: $
# $Date: $
##
## Monitoring Resource Bundle - Spanish locale (es)
##
## For a full changelog, refer to the English bundle, monitoring_i18n.properties.
##
## Updated for release: 1.0.0
# Monitoring
login.title = Consola de Administraci\u00f3n
error.exception = Excepci\u00f3n:
admin.tab.monitoring.name=Monitoreo
admin.tab.monitoring.description=Monitoreo
admin.sidebar.statistics.name=Estad\u00edsticas
admin.sidebar.statistics.description=Estad\u00edsticas de Openfire
admin.item.stats-dashboard.name=Consola
admin.item.stats-dashboard.description=Ver vistazo general de estad\u00edsticas del servidor.
admin.item.stats-reporter.name=Todos los Reportes
admin.item.stats-reporter.description=Ver reportes detallados de estad\u00edsticas del servidor.
admin.sidebar.archiving.name=Almacenamiento
admin.sidebar.archiving.description=Seteos de Almacenamiento de Openfire
admin.item.archive-search.name=Buscar en Almacenamiento
admin.item.archive-search.description=Haga clic para buscar en almacenamiento.
admin.item.archive-settings.name=Seteos de Almacenamiento
admin.item.archive-settings.description=Haga clic para configurar almacenamiento.
admin.item.active-conversations.name=Conversaciones
admin.item.active-conversations.description=Haga clic para ver conversaciones activas.
archive.settings.title = Seteos de Almacenamiento
archive.settings.success = Seteos de Almacenamiento han sido salvados.
archive.settings.rebuild.success = Indices de b\u00fasqueda estan siendo recontruidos.
archive.settings.message.metadata.title = Seteos de Mensajes y Metadatos
archive.settings.message.metadata.description = Habilita o deshabilita almacenamiento de mensajes y/o metadatos.
archive.settings.description = Utilice el siguiente formulario para administrar los seteos de almacenamiento.
archive.settings.enable.metadata = Almacenamiento de Estados de Conversaciones
archive.settings.enable.metadata.description = Registra qui\u00e9n charla con qui\u00e9n, cu\u00e1nto dura la conversaci\u00f3n, \
y el n\u00famero de mensajes en cada conversaci\u00f3n. El contenido de los mensajes no ser\u00e1 guardado a menos que \
almacenamiento de mensajes sea habilitado.
archive.settings.enable.message = Almacenamiento de Mensajes
archive.settings.enable.message.description = Almacenar el texto completo de todos los mensajes enviados entre usuarios. \
Se podr\u00e1 buscar en los textos de los mensajes utilizando palabras claves.
archive.settings.idle.time = Tiempo Ocioso
archive.settings.idle.time.description = El n\u00famero de minutos una conversaci\u00f3n puede estar libre antes de ser finalizada.
archive.settings.max.time = Tiempo M\u00e1ximo
archive.settings.max.time.description = El m\u00e1ximo n\u00famero de minutos una conversacion puede durar antes de ser finalizada.
archive.settings.index.settings = Seteos de Indices
archive.settings.index.settings.description = Ver y/o reconstruir el \u00edndice de b\u00fasqueda actual.
archive.settings.current.index = Indice de B\u00fasqueda Actual
archive.settings.current.index.description = El tama\u00f1o actual del \u00edndice de mensajes.
archive.settings.message.count = Contador de Almacenamiento de Mensajes
archive.settings.message.count.description = El n\u00famero total de mensajes almacenados.
archive.settings.conversation.count = Contador de Almacenamiento de Conversaciones
archive.settings.conversation.count.description = El n\u00famero total de conversaciones almacenadas.
archive.settings.update.settings = Seteos de Actualizaci\u00f3n
archive.settings.cancel = Cancelar
archive.settings.rebuild = Reconstruir Indice
archive.settings.any = Cualquiera
archive.search.title = B\u00fasqueda en Almacenamiento
archive.search.participants = Participante(s):
archive.search.participants.tooltip = Ingrese o consulte el nombre de los participantes a buscar. \
Puede tambi\u00e9n ingresar un segundo participante para conversaciones m\u00e1s espec\u00edficas.
archive.search.participants.any = Cualquiera
archive.search.participants.browse = Consultar
archive.search.daterange = Rango de Fechas:
archive.search.daterange.tooltip = Ingrese un rango de fechas espec\u00edfico para buscar. Puede \
especificar una fecha de inicio y/o fecha de fin.
archive.search.daterange.start = Inicio:
archive.search.daterange.end = Fin:
archive.search.daterange.any = Cualquiera
archive.search.daterange.format = Formato mm/dd/yy
archive.search.daterange.error = La fecha de fin debe ser posterior a la de inicio.
archive.search.keywords = Palabras claves:
archive.search.keywords.optional = (opcional)
archive.search.keywords.disabled = B\u00fasqueda por palabras claves esta deshabilitado. Para habilitarla debe \
habilitar almacenamiento de mensajes en {0}Seteos de Almacenamiento{1}.
archive.search.pdf.title = Transcripci\u00f3n de Conversaci\u00f3n
archive.search.pdf.participants = Participantes:
archive.search.pdf.startdate = Fecha de inicio:
archive.search.pdf.duration = Duraci\u00f3n:
archive.search.pdf.messagecount = Cantidad de Mensajes:
archive.search.submit = Buscar
archive.search.results = Resultados de B\u00fasqueda:
archive.search.results.description = Su b\u00fasqueda retorn\u00f3 {0} resultados. Seleccione el resultado en la izquierda para visualizar la conversaci\u00f3n.
archive.search.results.xofy = de
archive.search.results.participants = Participantes:
archive.search.results.messagecount = Cantidad de Mensajes:
archive.search.results.date = Fecha:
archive.search.results.duration = Duraci\u00f3n:
archive.search.results.none = No se han encontrado conversaciones de acuerdo al criterio de b\u00fasqueda. Modifique su criterio de b\u00fasqueda e intente nuevamente.
archive.search.results.archive_disabled = Los mensajes en esta conversaci\u00f3n no fueron almacenados.
archive.conversations = Conversaciones Activas:
archive.conversations.users = Usuarios
archive.conversations.duration = Duraci\u00f3n
archive.conversations.lastactivity = \u00daltima Actividad
archive.conversations.messages = Mensajes
archive.converations.no_conversations = No hay conversaciones activas.
stat.active_group_chats.name = Conferencias: Salas
stat.active_group_chats.desc = N\u00famero de salas de conferencias que han estado activas en el tiempo.
stat.active_group_chats.units = Salas de Conferencias
stat.server_sessions.name = Conexiones entre servidores
stat.server_sessions.desc = N\u00famero de conexiones entre servidores.
stat.server_sessions.units = Conexiones entre servidores
stat.sessions.name = Conexiones de Clientes
stat.sessions.desc = N\u00famero de Clientes Conectados directamente al servidor.
stat.sessions.units = Conexiones de Clientes
stat.packet_count.name = Cantidad de Paquetes
stat.packet_count.desc = N\u00famero de Paquetes Enviados y Recibidos por Openfire.
stat.packet_count.units = Paquetes por Minuto
stat.conversation.name = Conversaciones
stat.conversation.desc = Conversaciones entre usuarios.
stat.conversation.units = Conversaciones
# Bookmarks
users = Usuarios
groups = Grupos
options = Opciones
cancel = Cancelar
create = Crear
# Dashboard
dashboard.title = Consola de Openfire
dashboard.description = Una foto de la actividad actual en Openfire.
dashboard.directions = Haga clic en los gr\u00e1ficos para obtener una ampliaci\u00f3n.
dashboard.snapshot.enlarge = Agrandar Gr\u00e1fico
dashboard.snapshot.shrink = Encoger Gr\u00e1fico
dashboard.timespan = Per\u00edodo:
dashboard.timespan.lasthour = 1 Hora
dashboard.timespan.last24hours = 24 Horas
dashboard.timespan.last7days = 7 D\u00edas
dashboard.spotlights.high = M\u00e1x:
dashboard.spotlights.low = M\u00edn:
dashboard.spotlights.packetactivity = Paquetes Por Minuto
dashboard.spotlights.activeconversations = Conversaciones Vigentes
dashboard.spotlights.currentusers = Usuarios Actuales
dashboard.quickstats = Estad\u00edsticas R\u00e1pidas
dashboard.quickstats.high = M\u00e1x
dashboard.quickstats.low = M\u00edn
dashboard.currentconversations = Conversaciones Vigentes
dashboard.currentconversations.details = ver detalles
dashboard.currentconversations.users = Usuarios
dashboard.currentconversations.lastactivity = \u00daltima Actividad
dashboard.currentconversations.messagecount = Mensajes
dashboard.currentconversations.none = No hay conversaciones vigentes.
# All Reports
allreports.title = Todos los Reportes
allreports.daterange = Rango de Fechas
allreports.daterange.preset = Programadas
allreports.daterange.preset.last60minutes = \u00daltimos 60 minutos
allreports.daterange.preset.last24hours = \u00daltimas 24 horas
allreports.daterange.preset.thisweek = Esta semana
allreports.daterange.preset.last7days = \u00daltimos 7 d\u00edas
allreports.daterange.preset.lastweek = Semana pasada
allreports.daterange.preset.thismonth = Este mes
allreports.daterange.preset.lastmonth = Mes pasado
allreports.daterange.preset.last3months = \u00daltimos 3 meses
allreports.daterange.specific = Espec\u00edfico
allreports.daterange.specific.startdate = Inicio:
allreports.daterange.specific.enddate = Fin:
allreports.daterange.startdate.error = Ingrese una fecha de inicio en el formato mm/dd/yy.
allreports.daterange.enddate.error = Ingrese una fecha de fin en el formato mm/dd/yy.
allreports.selectreport = Seleccione Reporte
allreports.download.allreports = Descargar Todos los Reportes
allreports.download.allreports.pdf = PDF
allreports.download.allreports.pdf.format = Formato PDF
allreports.download.singlereport = Descargar este reporte:
allreports.download.singlereport.pdf = PDF
allreports.reportinformation = Informaci\u00f3n del Reporte
muc.conversation.joined={0} ({1}) ha ingresado al cuarto
muc.conversation.joined.anonymous={0} (anonimo) ha ingresado al cuarto
muc.conversation.left={0} ({1}) se ha ido del cuarto
muc.conversation.left.anonymous={0} (anonimo) se ha ido del cuarto
dashboard.group_conversation=Conversacion de Grupo
archive.settings.group_chats=Archivar charlas de grupos
archive.search.group_conversation=Charla de Grupo: {0}
archive.group_conversation={0}Conversacion de Grupo{1}
archive.group_conversation.close=Cerrar
archive.group_conversation.participants=Participantes
archive.group_conversation.participants.empty=No se han encontrado participantes.
archive.group_conversation.participants.participant=Usuarios
archive.group_conversation.participants.title=Participantes de la charla de Grupo
admin.item.sparkweb.description=Haga clic para comenzar SparkWeb
admin.item.sparkweb.name=SparkWeb
admin.item.webclient=Cliente Web
archive.settings.certain_rooms=S\u00f3lo archivar conversaciones de los siguientes cuartos (separar con coma)
archive.settings.one_to_one=Archivar charlas uno-a-uno
\ No newline at end of file
# Java Resource Bundle
# Modified by Zaval JRC Editor (C) Zaval CE Group
# http://www.zaval.org/products/jrc-editor/
## Updated for release: 1.0.0
#
admin.item.active-conversations.description=Cliquez pour voir les conversations en cours.
admin.item.active-conversations.name=Conversations
admin.item.archive-search.description=Cliquez pour rechercher dans les archives.
admin.item.archive-search.name=Recherche Archive
admin.item.archive-settings.description=Cliquez pour param\u00e9trer l'archivage.
admin.item.archive-settings.name=Param\u00e8tres d'Archivage
admin.item.client-features.description=Cliquez pour activer ou d\u00e9sactiver les modules client.
admin.item.client-features.name=Dispositifs Client
admin.item.client-version.description=Cliquez pour configurer quelles applications client pourront se connecter a votre serveur.
admin.item.client-version.name=Clients Authoris\u00e9s
admin.item.groupchat-bookmarks.description=Cliquez pour administrer les signets "group chat" des utilisateurs
admin.item.groupchat-bookmarks.name=Signets Group Chat
admin.item.spark-version.description=Cliquez pour restreindre la version Spark correspondante a chaque plate-forme.
admin.item.spark-version.name=Version Spark
admin.item.stats-dashboard.description=Voir le r\u00e9sum\u00e9 des statistiques serveur.
admin.item.stats-dashboard.name=Tableau de Bord
admin.item.stats-reporter.description=Voir les rapports d\u00e9taill\u00e9s des statistiques serveur.
admin.item.stats-reporter.name=Rapports
admin.item.url-bookmarks.description=Cliquez pour administrer les signets URL des utilisateurs
admin.item.url-bookmarks.name=Signets URL
admin.sidebar.archiving.description=Param\u00e8tres d'Archivage Openfire Entreprise
admin.sidebar.archiving.name=Archivage
admin.sidebar.client.description=Administrer les Modules Client et les Deploiements Spark
admin.sidebar.client.name=Administration des Clients
admin.sidebar.statistics.description="Statistiques Openfire Entreprise
admin.sidebar.statistics.name=Statistiques
admin.tab.monitoring.description=Openfire Entreprise
admin.tab.monitoring.name=Entreprise
allreports.daterange=Dates
allreports.daterange.enddate.error=Veuillez s\u00e9l\u00e9ctionner une date avant demain dans le format mm/jj/aa
allreports.daterange.preset=Pr\u00e9s\u00e9lection
allreports.daterange.preset.last24hours=Dernieres 24 heures
allreports.daterange.preset.last3months=3 dernier mois
allreports.daterange.preset.last60minutes=60 derniere minutes
allreports.daterange.preset.last7days=7 dernier jours
allreports.daterange.preset.lastmonth=Mois Pr\u00e9c\u00e9dent
allreports.daterange.preset.lastweek=Semaine Pr\u00e9c\u00e9dente
allreports.daterange.preset.thismonth=Ce mois-ci
allreports.daterange.preset.thisweek=Cette Semaine
allreports.daterange.specific=Sp\u00e9cifique
allreports.daterange.specific.enddate=Fin:
allreports.daterange.specific.startdate=D\u00e9but:
allreports.daterange.startdate.error=Veuillez entrer une date de d\u00e9but au format mm/jj/aa
allreports.download.allreports=T\u00e9l\u00e9charger tous les Rapports
allreports.download.allreports.pdf= PDF
allreports.download.allreports.pdf.format=Format PDF
allreports.download.singlereport=T\u00e9l\u00e9charger ce rapport:
allreports.download.singlereport.pdf=PDF
allreports.reportinformation=Information sur le Rapport
allreports.selectreport=Choisissez un Rapport
# All Reports
allreports.title=Rapports
archive.converations.no_conversations=Aucune conversation en cours.
archive.conversations=Conversations en cours:
archive.conversations.duration=Dur\u00e9e
archive.conversations.lastactivity=Derni\u00e8re Activit\u00e9
archive.conversations.messages=Messages
archive.conversations.users=Utilisateurs
archive.search.daterange=Dates:
archive.search.daterange.any=N'importe
archive.search.daterange.end=Fin:
archive.search.daterange.error=L'heure de fin doit etre apr\u00e8s l'heure de d\u00e9part.
archive.search.daterange.format=Utilisez mm/jj/aa
archive.search.daterange.start=D\u00e9part:
archive.search.daterange.tooltip=Veuillez entrer les dates ente lequelles vous souhaitez rechercher. Vous pouvez choisir une date de d\u00e9part et/ou une date de fin.
archive.search.keywords=Mots-Cl\u00e9s:
archive.search.keywords.disabled=La recherche par Mot-Cl\u00e9s a \u00e9t\u00e9 d\u00e9sactiv\u00e9e. Pour l'activer, vous devez aussi activer l'archivage des messages dans l'onglet {0}Param\u00e8tres d'Archivage{1}.
archive.search.keywords.optional= (option)
archive.search.participants=Participant(s):
archive.search.participants.any=N'importe
archive.search.participants.browse=Parcourir
archive.search.participants.tooltip=Veuillez entrer ou s\u00e9l\u00e9ctionner le nom du participant que vous recherchez. Vous pouvez aussi entrer celui d'un autre participant afin d'affiner votre recherche sur des conversations en particulier.
archive.search.pdf.duration=Dur\u00e9e:
archive.search.pdf.messagecount=Nombre de Messages:
archive.search.pdf.participants=Participants:
archive.search.pdf.startdate=Date de D\u00e9part:
archive.search.pdf.title=Transcript de la Conversation:
archive.search.results=R\u00e9sultats de la Recherche:
archive.search.results.archive_disabled=Les messages de cette conversation n'ont pas \u00e9t\u00e9 archiv\u00e9s.
archive.search.results.date=Date:
archive.search.results.description=Votre recherche a donn\u00e9 {0} r\u00e9sultats. S\u00e9lectionnez un r\u00e9sultat \u00e0 gauche pour consulter la conversation.
archive.search.results.duration=Dur\u00e9e:
archive.search.results.messagecount=Nombre de Messages:
archive.search.results.none=Aucune conversations n'ont \u00e9t\u00e9 trouv\u00e9es avec les crit\u00e8res de recherche sp\u00e9cifi\u00e9s. Veuillez modifier vos crit\u00e8res et r\u00e9essayer.
archive.search.results.participants=Participants:
archive.search.results.xofy=de(s)
archive.search.submit=Rechercher
archive.search.title=Rechercher dans les Archives
archive.settings.any=N'importe
archive.settings.cancel=Annuler
archive.settings.conversation.count=Nombre de Conversations Archiv\u00e9es
archive.settings.conversation.count.description=Le total des conversations archiv\u00e9es.
archive.settings.current.index=Index de Recherche Courant
archive.settings.current.index.description=La taille actuelle des indexes de message.
archive.settings.description=Utilisez le formulaire ci-dessous pour administrer les param\u00e8tres d'archivage.
archive.settings.enable.message=Archivage des Messages
archive.settings.enable.message.description=Archiver le texte des tous les messages envoy\u00e9s entre les utilisateurs. Le texte des messages pourra \u00eatre recherch\u00e9 par mot-cl\u00e9.
archive.settings.enable.metadata=Archivage des Etats de Conversation
archive.settings.enable.metadata.description=Enregistrer qui discute avec qui, combin de temps dure leurs conversations et le nombre exact de messages dans chaque conversation. Le contenu des messages ne sera pas enregistr\u00e9 \u00e0 mois que l'archivage des messages soit activ\u00e9.
archive.settings.idle.time=Temps Idle
archive.settings.idle.time.description=Le nombre de minutes d'idle autoris\u00e9 durant une conversation avant qu'elle ne soit coup\u00e9e automatiquement.
archive.settings.index.settings=Param\u00e8tres d'Indexage
archive.settings.index.settings.description=Voir et/ou reconstruire les indexes de recherche.
archive.settings.max.time=Temps Max
archive.settings.max.time.description=Le nombre de minutes maximum d'une conversation avant qu'elle ne soit coup\u00e9e automatiquement.
archive.settings.message.count=Nombre de messages archiv\u00e9s
archive.settings.message.count.description=Le nombre total de messages archiv\u00e9s.
archive.settings.message.metadata.description=Activer ou D\u00e9sactiver l'archivage des messages et/ou metadata.
archive.settings.message.metadata.title=Param\u00e8tres de Message et Metadata
archive.settings.rebuild=R\u00e9-indexer
archive.settings.rebuild.success=Le re-indexage est en cours.
archive.settings.success=Les param\u00e8tres d'archivage ont \u00e9t\u00e9 enregistr\u00e9s.
archive.settings.title=Param\u00e8tres d'Archivage
archive.settings.update.settings=Param\u00e8tres de Mise \u00e0 jour
create=Cr\u00e9er
dashboard.currentconversations=Conversations en cours
dashboard.currentconversations.details=Voir les d\u00e9tails
dashboard.currentconversations.lastactivity=Derni\u00e8re activit\u00e9
dashboard.currentconversations.messagecount=Messages
dashboard.currentconversations.none=Aucune conversation en cours.
dashboard.currentconversations.users=Utilisateurs
dashboard.description=Un "snapshot" de l'activit\u00e9 sur Openfire
dashboard.directions=Cliquez sur les graphes ci-dessous pour les agrandir.
dashboard.quickstats=Stats
dashboard.quickstats.high=Haut
dashboard.quickstats.low=Bas
dashboard.snapshot.enlarge=Agrandir
dashboard.snapshot.shrink=R\u00e9duire
dashboard.spotlights.activeconversations=Conversqtions en cours
dashboard.spotlights.currentusers=Utilisateurs courant
dashboard.spotlights.high=Haut:
dashboard.spotlights.low=Bas:
dashboard.spotlights.packetactivity=Paquets par minute
dashboard.timespan=Periode:
dashboard.timespan.last24hours=24 heures
dashboard.timespan.last7days=7 jours
dashboard.timespan.lasthour=1 Heure
# Dashboard
dashboard.title=Tableau de Bord Openfire
error.exception=Exception:
groups=Groupes
# Monitoring
login.title=Console d'Administration
options=Options
stat.active_group_chats.desc=Le nombre de salons "Group chat" actifs.
stat.active_group_chats.name=Group Chat: Salons
stat.active_group_chats.units=Salons
stat.conversation.desc=Conversations entre utilisateurs
stat.conversation.name=Conversations
stat.conversation.units=Conversations
stat.packet_count.desc=Nombre de paquets envoy\u00e9s et re\u00e7us par Openfire.
stat.packet_count.name=Nombre de Paquets
stat.packet_count.units=Paquets par Minute
stat.sessions.desc=Nombre de clients connect\u00e9s directement au serveur.
stat.sessions.name=Connexions Client
stat.sessions.units=Connexions Client
stat.server_sessions.desc=No nombre de connexion Serveur \u00e0 Serveur
stat.server_sessions.name=Connexions Serveur \u00e0 Serveur
stat.server_sessions.units=Conexions S2S
# Bookmarks
users=Utilisateurs
# $RCSfile$
# $Revision: 3148 $
# $Date: 2005-12-01 14:50:45 -0300 (Thu, 01 Dec 2005) $
##
## Monitoring Resource Bundle
##
## Additional locales can be specified by creating a new resource file in this
## directory using the following conventions:
##
## monitoring_i18n "_" language "_" country ".properties"
## monitoring_i18n "_" language ".properties"
##
## e.g.
## monitoring_i18n_en.propertis <- English resources
## monitoring_i18n_en_US.properties <- American US resources
## monitoring_i18n_de.properties <- German resources
## monitoring_i18n_ja.properties <- Japanese resources
##
## Please note that the two digit language code should be lower case, and the
## two digit country code should be in uppercase. Often, it is not necessary to
## specify the country code.
##
## A full list of language codes can be found at
## http://www-old.ics.uci.edu/pub/ietf/http/related/iso639.txt
## and a full list of country codes can be found at
## http://www.chemie.fu-berlin.de/diverse/doc/ISO_3166.html
##
## In property strings that are parameterized, single quotes can be used to
## quote the "{" (curly brace) if necessary. A real single quote is represented by ''.
##
## REVISION HISTORY (by Monitoring version):
##
## Updated for release: 1.0.0
# Monitoring
login.title = Console Administrativo
error.exception = Exce\u00e7\u00e3o:
admin.tab.monitoring.name=Monitoring
admin.tab.monitoring.description=Monitoring
admin.sidebar.statistics.name=Estat\u00edsticas
admin.sidebar.statistics.description=Estat\u00edsticas da Openfire
admin.item.stats-dashboard.name=Painel
admin.item.stats-dashboard.description=Ver descri\u00e7\u00e3o das estat\u00edsticas do servidor.
admin.item.stats-reporter.name=Todos os Relat\u00f3rios
admin.item.stats-reporter.description=Ver relat\u00f3rios detalhados das estat\u00edsticas do servidor.
admin.sidebar.archiving.name=Arquivamento
admin.sidebar.archiving.description=Ajustes de Arquivamento do Openfire
admin.item.archive-search.name=Arquivo de busca
admin.item.archive-search.description=Clique para procurar aquivos.
admin.item.archive-settings.name=Ajustes de Arquivamento
admin.item.archive-settings.description=Clique para configurar arquivamento.
admin.item.active-conversations.name=Conversas
admin.item.active-conversations.description=Clique para ver conversas ativas.
archive.settings.title = Ajustes de Arquivamento
archive.settings.success = Ajustes de Arquivamento foram salvos.
archive.settings.rebuild.success = Refazendo \u00edndices de Busca.
archive.settings.message.metadata.title = Ajustes de Mensagens e Metadata
archive.settings.message.metadata.description = Habilitar ou desabilitar arquivamento de mensagem e/ou metadata.
archive.settings.description = Utilize o formul\u00e1rio abaixo para gerenciar ajustes de arquivamento.
archive.settings.enable.metadata = Arquivamento de Estado de Conversa
archive.settings.enable.metadata.description = Gravar quem fala com quem, tempo de conversa, e o n\u00famero de \
mensagens em cada conversa. O conte\u00fado da mensagem n\u00e3o ser\u00e1 gravado a menos que arquivamento de mensagem \
esteja habilitado.
archive.settings.enable.message = Arquivamento de Mensagem
archive.settings.enable.message.description = Arquivar o texto completo de todas as mensagens enviadas entre usu\u00e1rios. \
Texto da mensagem ser\u00e1 pesquis\u00e1vel utilizando palavras chaves
archive.settings.idle.time = Tempo Ocioso
archive.settings.idle.time.description = N\u00famero de minutos que a conversa pode estar ociosa antes de ser encerrada.
archive.settings.max.time = Tempo M\u00e1ximo
archive.settings.max.time.description = N\u00famero m\u00e1ximo de minutos que a conversa pode durar antes de ser encerrada.
archive.settings.index.settings = Ajustes de \u00edndice
archive.settings.index.settings.description = Ver e/ou refazer o \u00edndice de Buscas atual.
archive.settings.current.index = \u00edndice de Buscas Atual
archive.settings.current.index.description = O tamanho atual do \u00edndice de mensagens.
archive.settings.message.count = N\u00famero de Mensagens Arquivadas
archive.settings.message.count.description = O n\u00famero total de mensagens arquivadas.
archive.settings.conversation.count = N\u00famero de Conversas Arquivadas
archive.settings.conversation.count.description = O n\u00famero total de conversas arquivadas.
archive.settings.update.settings = Ajustes de Atualiza\u00e7\u00e3o
archive.settings.cancel = Cancelar
archive.settings.rebuild = Refazer \u00edndice
archive.settings.any = Qualquer
archive.search.title = Arquivo de Buscas
archive.search.participants = Participante(s):
archive.search.participants.tooltip = Insira ou pesquise o nome do participante a ser pesquisado.\
Voc\u00ea tamb\u00e9m pode inserir um segundo participante para conversas mais espec\u00edficas.
archive.search.participants.any = Qualquer
archive.search.participants.browse = Listar
archive.search.daterange = Intervalo de Datas:
archive.search.daterange.tooltip = Insira intervalo de datas espec\u00edfico para pesquisa. Voc\u00ea pode especificar uma data \
de in\u00edcio e/ou uma data de fim
archive.search.daterange.start = In\u00edcio:
archive.search.daterange.end = Final:
archive.search.daterange.any = Qualquer
archive.search.daterange.format = Utilize mm/dd/aa
archive.search.daterange.error = A hora final deve ser ap\u00f3s a hora inicial
archive.search.keywords = Palavras-chave:
archive.search.keywords.optional = (opcional)
archive.search.keywords.disabled = Pesquisa por palavras-chave est\u00e1 desabilitada. Para habilitar pesquisa por \
palavras-chave, voc\u00ea deve habilitar arquivamento de mensagem em {0}Ajustes de Arquivamento{1}.
archive.search.pdf.title = Transcri\u00e7\u00e3o da Conversa
archive.search.pdf.participants = Participantes:
archive.search.pdf.startdate = Data de In\u00edcio:
archive.search.pdf.duration = Dura\u00e7\u00e3o:
archive.search.pdf.messagecount = N\u00famero de Mensagens:
archive.search.submit = Busca
archive.search.results = Resultados da Pesquisa:
archive.search.results.description = Sua pesquisa retornou {0} resultados. Selecione o resultado à esquerda para \
visualizar a conversa.
archive.search.results.xofy = de\
of
archive.search.results.participants = Participantes:
archive.search.results.messagecount = N\u00famero de Mensagens:
archive.search.results.date = Data:
archive.search.results.duration = Dura\u00e7\u00e3o:
archive.search.results.none = Nenhuma conversa foi encontrada utilizando os crit\u00e9rios de pesquisa especificados. \
Por favor, modifique os crit\u00e9rios de pesquisa e tente novamente.
archive.search.results.archive_disabled = As mensagens nesta conversa n\u00e3o foram arquivadas.
archive.conversations = Conversas ativas:
archive.conversations.users = Usu\u00e1rios
archive.conversations.duration = Dura\u00e7\u00e3o
archive.conversations.lastactivity = \u00daltima Atividade
archive.conversations.messages = Mensagens
archive.converations.no_conversations = Nenhuma conversa ativa.
stat.active_group_chats.name = Bate-Papo em Grupo: Salas
stat.active_group_chats.desc = O n\u00famero de salas de bate-papo que estiveram ativas ao longo do tempo
stat.active_group_chats.units = Salas de Bate-Papo
stat.server_sessions.name = Conex\u00f5es de Servidor para Servidor
stat.server_sessions.desc = N\u00famero de Conex\u00f5es de Servidor para Servidor.
stat.server_sessions.units = Conex\u00f5es S2S
stat.sessions.name = Conex\u00f5es de Cliente
stat.sessions.desc = N\u00famero de Clientes Conectados Diretamente ao Servidor.
stat.sessions.units = Conex\u00f5es de Cliente
stat.packet_count.name = N\u00famero de Pacotes
stat.packet_count.desc = N\u00famero de Pacotes Enviados e Recebidos pelo Openfire.
stat.packet_count.units = Pacotes por Minuto
stat.conversation.name = Conversas
stat.conversation.desc = Conversas entre usu\u00e1rios.
stat.conversation.units = Conversas
# Bookmarks
users = Usu\u00e1rios
groups = Grupos
options = Op\u00e7\u00f5es
cancel = Cancelar
create = Criar
# Dashboard
dashboard.title = Painel do Openfire
dashboard.description = Um resumo da atividade atual do Openfire.
dashboard.directions = Clique nos gr\u00e1ficos para ampli\u00e1-los.
dashboard.snapshot.enlarge = Aumentar Gr\u00e1fico
dashboard.snapshot.shrink = Diminuir Gr\u00e1fico
dashboard.timespan = Intervalo:
dashboard.timespan.lasthour = 1 Hora
dashboard.timespan.last24hours = 24 Horas
dashboard.timespan.last7days = 7 Dias
dashboard.spotlights.high = Alto:
dashboard.spotlights.low = Baixo:
dashboard.spotlights.packetactivity = Pacotes por Minuto
dashboard.spotlights.activeconversations = Conversas Ativas
dashboard.spotlights.currentusers = Usu\u00e1rios Atuais
dashboard.quickstats = Status R\u00e1pidos
dashboard.quickstats.high = Alto
dashboard.quickstats.low = Baixo
dashboard.currentconversations = Conversas Atuais
dashboard.currentconversations.details = Ver detalhes
dashboard.currentconversations.users = Usu\u00e1rios
dashboard.currentconversations.lastactivity = \u00daltima Atividade
dashboard.currentconversations.messagecount = Mensagens
dashboard.currentconversations.none = Nenhuma conversa ativa.
# All Reports
allreports.title = Todos os Relat\u00f3rios
allreports.daterange = Intervalo de datas
allreports.daterange.preset = Pr\u00e9-ajustado
allreports.daterange.preset.last60minutes = \u00faltimos 60 minutos
allreports.daterange.preset.last24hours = \u00daltimas 24 horas
allreports.daterange.preset.thisweek = Esta Semana
allreports.daterange.preset.last7days = \u00daltimos 7 dias
allreports.daterange.preset.lastweek = \u00daltima semana
allreports.daterange.preset.thismonth = Este m\u00eas
allreports.daterange.preset.lastmonth = \u00daltimo m\u00eas
allreports.daterange.preset.last3months = \u00daltimos 3 meses
allreports.daterange.specific = Espec\u00edfico
allreports.daterange.specific.startdate = In\u00edcio:
allreports.daterange.specific.enddate = Fim:
allreports.daterange.startdate.error = Por favor, insira uma data de in\u00edcio no formato mm/dd/aa.
allreports.daterange.enddate.error = Por favor, insira uma data de fim antes de amanh\u00e3 no formato mm/dd/aa.
allreports.selectreport = Selecionar Relat\u00f3rio
allreports.download.allreports = Fazer Download de Todos Relat\u00f3rios
allreports.download.allreports.pdf = PDF
allreports.download.allreports.pdf.format = Formato PDF
allreports.download.singlereport = Fazer download deste relat\u00f3rio:
allreports.download.singlereport.pdf = PDF
allreports.reportinformation = Informa\u00e7\u00f5es do Relat\u00f3rios
global.pages=P\u00e1ginas
global.showing=Mostrando
permitted.client.website=
test = Test
\ No newline at end of file
# $RCSfile$
# $Revision: 3148 $
# $Date: 2005-12-01 14:50:45 -0300 (Thu, 01 Dec 2005) $
##
## Monitoring Resource Bundle
##
## Additional locales can be specified by creating a new resource file in this
## directory using the following conventions:
##
## monitoring_i18n "_" language "_" country ".properties"
## monitoring_i18n "_" language ".properties"
##
## e.g.
## monitoring_i18n_en.propertis <- English resources
## monitoring_i18n_en_US.properties <- American US resources
## monitoring_i18n_de.properties <- German resources
## monitoring_i18n_ja.properties <- Japanese resources
##
## Please note that the two digit language code should be lower case, and the
## two digit country code should be in uppercase. Often, it is not necessary to
## specify the country code.
##
## A full list of language codes can be found at
## http://www-old.ics.uci.edu/pub/ietf/http/related/iso639.txt
## and a full list of country codes can be found at
## http://www.chemie.fu-berlin.de/diverse/doc/ISO_3166.html
##
## In property strings that are parameterized, single quotes can be used to
## quote the "{" (curly brace) if necessary. A real single quote is represented by ''.
##
## REVISION HISTORY (by Monitoring version):
##
## Updated for release: 1.0.0
# Monitoring
login.title = \u7ba1\u7406\u63a7\u5236\u53f0
error.exception = \u5f02\u5e38:
admin.tab.monitoring.name=\u4f01\u4e1a\u7248
admin.tab.monitoring.description=Openfire\u4f01\u4e1a\u7248
admin.sidebar.statistics.name=\u7edf\u8ba1\u8868
admin.sidebar.statistics.description=Openfire\u4f01\u4e1a\u7248\u7edf\u8ba1\u8868
admin.item.stats-dashboard.name=\u4eea\u8868\u76d8
admin.item.stats-dashboard.description=\u6d4f\u89c8\u6574\u4e2a\u670d\u52a1\u5668\u7684\u7edf\u8ba1\u8868
admin.item.stats-reporter.name=\u6240\u6709\u7684\u62a5\u8868
admin.item.stats-reporter.description=\u67e5\u770b\u7edf\u8ba1\u8868\u7684\u62a5\u8868\u660e\u7ec6\u4fe1\u606f
admin.sidebar.archiving.name=\u6863\u6848\u6587\u4ef6
admin.sidebar.archiving.description=Openfire\u4f01\u4e1a\u7248\u5b58\u6863\u8bbe\u7f6e
admin.item.archive-search.name=\u641c\u7d22\u5b58\u6863\u6587\u4ef6
admin.item.archive-search.description=\u5355\u51fb\u641c\u7d22\u5b58\u6863\u6587\u4ef6
admin.item.archive-settings.name=\u5b58\u6863\u8bbe\u7f6e
admin.item.archive-settings.description=\u5355\u51fb\u8bbe\u7f6e\u5b58\u6863\u6587\u4ef6
admin.item.active-conversations.name=\u4f1a\u8bdd
admin.item.active-conversations.description=\u5355\u51fb\u67e5\u770b\u6d3b\u52a8\u7684\u5bf9\u8bdd
archive.settings.title = \u5b58\u6863\u8bbe\u7f6e
archive.settings.success = \u5b58\u6863\u8bbe\u7f6e\u4fdd\u5b58\u6210\u529f
archive.settings.rebuild.success = \u641c\u7d22\u7d22\u5f15\u91cd\u5efa
archive.settings.message.metadata.title = \u4fe1\u606f\u548c\u5143\u6570\u636e\u8bbe\u7f6e
archive.settings.message.metadata.description = \u542f\u7528\u6216\u7981\u7528\u4fe1\u606f\u548c\u5143\u6570\u636e\u5b58\u6863
archive.settings.description = \u4f7f\u7528\u4e0b\u9762\u7684\u8868\u5355\u7528\u4e8e\u7ba1\u7406\u5b58\u6863\u8bbe\u7f6e
archive.settings.enable.metadata = \u4f1a\u8bdd\u72b6\u6001\u5b58\u6863
archive.settings.enable.metadata.description = \u8bb0\u5f55\u67d0\u4e24\u4eba\u4e4b\u95f4\u7684\u4f1a\u8bdd\uff0c\u6bcf\u6b21\u4f1a\u8bdd\u7684\u4fe1\u606f\u6570\u548c\u4f1a\u8bdd\u6301\u7eed\u65f6\u95f4\u3002\u5f53\u4fe1\u606f\u5b58\u6863\u542f\u7528\u65f6\u5b9e\u9645\u7684\u4fe1\u606f\u5185\u5bb9\u5c06\u88ab\u8bb0\u5f55\u3002
archive.settings.enable.message = \u4fe1\u606f\u5b58\u6863
archive.settings.enable.message.description = \u5728\u7528\u6237\u4e4b\u95f4\u7684\u4f20\u9001\u7684\u6240\u6709\u4fe1\u606f\u5b58\u6863\u6587\u672c\u53ef\u4ee5\u4f7f\u7528\u5173\u952e\u8bcd\u65b9\u5f0f\u67e5\u627e\u3002
archive.settings.idle.time = \u7a7a\u95f2\u65f6\u95f4
archive.settings.idle.time.description = \u4f1a\u8bdd\u7ed3\u675f\u540e\u7a7a\u95f2\u7684\u65f6\u95f4
archive.settings.max.time = \u6700\u5927\u65f6\u95f4
archive.settings.max.time.description = \u4f1a\u8bdd\u7ed3\u675f\u524d\u53ef\u6301\u7eed\u7684\u6700\u5927\u65f6\u95f4
archive.settings.index.settings = \u7d22\u5f15\u8bbe\u7f6e
archive.settings.index.settings.description = \u67e5\u770b\u6216\u91cd\u5efa\u5f53\u524d\u7684\u641c\u7d22\u7d22\u5f15
archive.settings.current.index = \u5f53\u524d\u7684\u641c\u7d22\u7d22\u5f15
archive.settings.current.index.description = \u5f53\u524d\u7684\u4fe1\u606f\u7d22\u5f15\u5927\u5c0f
archive.settings.message.count = \u5b58\u6863\u4fe1\u606f\u6570\u91cf
archive.settings.message.count.description = \u5b58\u6863\u4fe1\u606f\u603b\u6570
archive.settings.conversation.count = \u5b58\u6863\u5bf9\u8bdd\u6570\u91cf
archive.settings.conversation.count.description = \u5b58\u6863\u5bf9\u8bdd\u603b\u6570
archive.settings.update.settings = \u66f4\u65b0\u8bbe\u7f6e
archive.settings.cancel = \u53d6\u6d88
archive.settings.rebuild = \u91cd\u5efa\u7d22\u5f15
archive.settings.any = \u4efb\u4f55\u4e00\u9879
archive.search.title = \u641c\u7d22\u5b58\u6863
archive.search.participants = \u53c2\u4e0e\u8005:
archive.search.participants.tooltip = \u8f93\u5165\u6216\u6d4f\u89c8\u53c2\u4e0e\u8005\u540d\u5b57\u7528\u4e8e\u641c\u7d22\u3002\u60a8\u4e5f\u53ef\u4ee5\u8f93\u5165\u7b2c\u4e8c\u4e2a\u53c2\u4e0e\u8005\u7528\u4e8e\u641c\u7d22\u66f4\u591a\u7684\u5bf9\u8bdd\u4fe1\u606f
archive.search.participants.any = \u4efb\u4f55\u4e00\u9879
archive.search.participants.browse = \u6d4f\u89c8
archive.search.daterange = \u65f6\u95f4\u8303\u56f4:
archive.search.daterange.tooltip = \u8f93\u5165\u4e00\u4e2a\u6307\u5b9a\u7684\u65e5\u671f\u7528\u4e8e\u641c\u7d22\u3002\u60a8\u53ef\u4ee5\u6307\u5b9a\u4e00\u4e2a\u5f00\u59cb\u65f6\u95f4\u6216\u7ed3\u675f\u65f6\u95f4\u3002
archive.search.daterange.start = \u5f00\u59cb:
archive.search.daterange.end = \u7ed3\u675f:
archive.search.daterange.any = \u4efb\u4f55\u4e00\u9879
archive.search.daterange.format = \u4f7f\u7528 mm/dd/yy
archive.search.daterange.error = \u7ed3\u675f\u65f6\u95f4\u5fc5\u987b\u5728\u5f00\u59cb\u65f6\u95f4\u4e4b\u540e\u3002
archive.search.keywords = \u5173\u952e\u5b57:
archive.search.keywords.optional = (\u53ef\u9009\u9879)
archive.search.keywords.disabled = \u5173\u952e\u5b57\u641c\u7d22\u7981\u7528\u3002\u8981\u5f00\u542f\u5173\u952e\u5b57\u641c\u7d22\uff0c\u4f60\u5fc5\u987b\u5f00\u542f\u5728{0}\u5b58\u6863\u8bbe\u7f6e\u4e2d\u7684\u4fe1\u606f\u5b58\u6863{1}
archive.search.pdf.title = \u5bf9\u8bdd\u590d\u5236\u6587\u672c
archive.search.pdf.participants = \u53c2\u4e0e\u8005:
archive.search.pdf.startdate = \u5f00\u59cb\u65f6\u95f4:
archive.search.pdf.duration = \u6301\u7eed\u65f6\u95f4:
archive.search.pdf.messagecount = \u4fe1\u606f\u5408\u8ba1:
archive.search.submit = \u641c\u7d22
archive.search.results = \u641c\u7d22\u7ed3\u679c:
archive.search.results.description = \u60a8\u7684\u641c\u7d22\u8fd4\u56de {0} \u7ed3\u679c. \u5728\u5de6\u8fb9\u9009\u62e9\u4e00\u4e2a\u7ed3\u679c\u67e5\u770b\u5bf9\u8bdd
archive.search.results.xofy = \u5bf9\u4e8e
archive.search.results.participants = \u53c2\u4e0e\u8005:
archive.search.results.messagecount = \u4fe1\u606f\u6570\u91cf:
archive.search.results.date = \u65e5\u671f:
archive.search.results.duration = \u6301\u7eed\u65f6\u95f4:
archive.search.results.none = \u6307\u5b9a\u7684\u641c\u7d22\u6807\u51c6\u4e2d\u6ca1\u6709\u627e\u5230\u76f8\u5173\u7684\u4f1a\u8bdd\u4fe1\u606f\u3002\u8bf7\u6539\u53d8\u60a8\u7684\u641c\u7d22\u6807\u51c6\u518d\u8bd5\u4e00\u6b21\u3002
archive.search.results.archive_disabled = \u672c\u6b21\u5bf9\u8bdd\u4fe1\u606f\u5e76\u6ca1\u6709\u5b58\u6863\u3002
archive.conversations = \u6d3b\u8dc3\u7684\u5bf9\u8bdd:
archive.conversations.users = \u7528\u6237
archive.conversations.duration = \u6301\u7eed\u65f6\u95f4
archive.conversations.lastactivity = \u6700\u8fd1\u7684\u6d3b\u52a8
archive.conversations.messages = \u4fe1\u606f
archive.converations.no_conversations = \u975e\u6d3b\u8dc3\u7684\u5bf9\u8bdd
stat.active_group_chats.name = \u5206\u7ec4\u5bf9\u8bdd: \u623f\u95f4
stat.active_group_chats.desc = \u6309\u65f6\u95f4\u6d3b\u8dc3\u7684\u5206\u7ec4\u5bf9\u8bdd\u623f\u95f4\u6570
stat.active_group_chats.units = \u5206\u7ec4\u5bf9\u8bdd\u623f\u95f4
stat.server_sessions.name = \u670d\u52a1\u5668\u4e0e\u670d\u52a1\u5668\u7684\u8fde\u63a5
stat.server_sessions.desc = \u670d\u52a1\u5668\u4e0e\u670d\u52a1\u5668\u7684\u8fde\u63a5\u6570
stat.server_sessions.units = S2S\u8fde\u63a5
stat.sessions.name = \u5ba2\u6237\u7aef\u8fde\u63a5
stat.sessions.desc = \u5ba2\u6237\u7aef\u76f4\u63a5\u8fde\u63a5\u5230\u670d\u52a1\u5668\u4e0a\u7684\u6570\u91cf
stat.sessions.units = \u5ba2\u6237\u7aef\u8fde\u63a5
stat.packet_count.name = \u6570\u636e\u5305\u6570\u91cf
stat.packet_count.desc = Openfire\u4f20\u9012\u548c\u63a5\u6536\u7684\u6570\u636e\u5305\u6570\u91cf
stat.packet_count.units = \u6bcf\u5206\u949f\u6570\u636e\u5305
stat.conversation.name = \u5bf9\u8bdd
stat.conversation.desc = \u7528\u6237\u95f4\u7684\u5bf9\u8bdd
stat.conversation.units = \u5bf9\u8bdd
# Bookmarks
users = \u7528\u6237
groups = \u7ec4
options = \u9009\u9879
cancel = \u53d6\u6d88
create = \u5efa\u7acb
# Dashboard
dashboard.title = Openfire\u4eea\u8868\u76d8
dashboard.description = \u5728Openfire\u4e2d\u5f53\u524d\u6d3b\u52a8\u7684\u5feb\u7167.
dashboard.directions = \u5355\u51fb\u4e0b\u9762\u7684\u56fe\u8868\u770b\u4e00\u4e2a\u653e\u5927\u7684\u56fe\u8c61
dashboard.snapshot.enlarge = \u653e\u5927\u56fe\u50cf
dashboard.snapshot.shrink = \u7f29\u5c0f\u56fe\u50cf
dashboard.timespan = \u65f6\u95f4\u95f4\u9694:
dashboard.timespan.lasthour = 1\u5c0f\u65f6
dashboard.timespan.last24hours = 24\u5c0f\u65f6
dashboard.timespan.last7days = 7\u5929
dashboard.spotlights.high = \u9ad8:
dashboard.spotlights.low = \u4f4e:
dashboard.spotlights.packetactivity = \u6bcf\u5206\u949f\u7684\u6570\u636e\u5305
dashboard.spotlights.activeconversations = \u6d3b\u52a8\u7684\u4f1a\u8bdd
dashboard.spotlights.currentusers = \u5f53\u524d\u7528\u6237
dashboard.quickstats = \u5feb\u901f\u72b6\u6001
dashboard.quickstats.high = \u9ad8
dashboard.quickstats.low = \u4f4e
dashboard.currentconversations = \u5f53\u524d\u7684\u4f1a\u8bdd
dashboard.currentconversations.details = \u67e5\u770b\u8be6\u7ec6\u8d44\u6599
dashboard.currentconversations.users = \u7528\u6237
dashboard.currentconversations.lastactivity = \u6700\u65b0\u7684\u6d3b\u52a8
dashboard.currentconversations.messagecount = \u4fe1\u606f
dashboard.currentconversations.none = \u975e\u6d3b\u52a8\u7684\u4f1a\u8bdd
# All Reports
allreports.title = \u6240\u6709\u7684\u62a5\u544a
allreports.daterange = \u65e5\u671f\u8303\u56f4
allreports.daterange.preset = \u4e8b\u5148\u8c03\u6574
allreports.daterange.preset.last60minutes = \u6700\u65b060\u5206\u949f
allreports.daterange.preset.last24hours = \u6700\u65b024\u5c0f\u65f6
allreports.daterange.preset.thisweek = \u672c\u5468
allreports.daterange.preset.last7days = \u6700\u8fd17\u5929
allreports.daterange.preset.lastweek = \u4e0a\u5468
allreports.daterange.preset.thismonth = \u672c\u6708
allreports.daterange.preset.lastmonth = \u4e0a\u4e2a\u6708
allreports.daterange.preset.last3months = \u6700\u8fd13\u4e2a\u6708
allreports.daterange.specific = \u7279\u522b\u6307\u5b9a
allreports.daterange.specific.startdate = \u5f00\u59cb:
allreports.daterange.specific.enddate = \u7ed3\u675f:
allreports.daterange.startdate.error = \u8bf7\u7528mm/dd/yy\u683c\u5f0f\u8f93\u5165\u4e00\u4e2a\u5f00\u59cb\u65e5\u671f
allreports.daterange.enddate.error = \u8bf7\u7528mm/dd/yy\u683c\u5f0f\u8f93\u5165\u4e00\u4e2a\u7ed3\u675f\u65e5\u671f
allreports.selectreport = \u9009\u62e9\u62a5\u544a
allreports.download.allreports = \u4e0b\u8f7d\u6240\u6709\u7684\u62a5\u544a
allreports.download.allreports.pdf = PDF
allreports.download.allreports.pdf.format = PDF\u683c\u5f0f
allreports.download.singlereport = \u4e0b\u8f7d\u8fd9\u4efd\u62a5\u544a:
allreports.download.singlereport.pdf = PDF
allreports.reportinformation = \u62a5\u544a\u4fe1\u606f
/**
* $Revision$
* $Date$
*
* Copyright (C) 2008 Jive Software. All rights reserved.
*
* This software is published under the terms of the GNU Public License (GPL),
* a copy of which is included in this distribution, or a commercial license
* agreement with Jive.
*/
package org.jivesoftware.openfire.archive;
import org.jivesoftware.openfire.reporting.util.TaskEngine;
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.util.JiveConstants;
import org.jivesoftware.util.JiveGlobals;
import org.jivesoftware.util.Log;
import org.jivesoftware.util.XMLProperties;
import org.picocontainer.Startable;
import org.xmpp.packet.JID;
import java.io.*;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.*;
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;
/**
* 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 String ALL_CONVERSATIONS =
"SELECT conversationID, isExternal FROM ofConversation";
private static final String NEW_CONVERSATIONS =
"SELECT DISTINCT conversationID FROM ofMessageArchive WHERE sentDate > ?";
private static final String CONVERSATION_METADATA =
"SELECT isExternal FROM ofConversation WHERE conversationID=?";
private static final String CONVERSATION_MESSAGES =
"SELECT conversationID, sentDate, fromJID, toJID, body FROM ofMessageArchive " +
"WHERE conversationID IN ? ORDER BY conversationID";
private File searchDir;
private TaskEngine taskEngine;
private ConversationManager conversationManager;
private XMLProperties indexProperties;
private Directory directory;
private IndexSearcher searcher;
private Lock writerLock;
private boolean stopped = false;
private boolean rebuildInProgress = false;
private RebuildFuture rebuildFuture;
private long lastModified = 0;
private TimerTask indexUpdater;
/**
* Constructs a new archive indexer.
*
* @param conversationManager a ConversationManager instance.
* @param taskEngine a task engine instance.
*/
public ArchiveIndexer(ConversationManager conversationManager, TaskEngine taskEngine) {
this.conversationManager = conversationManager;
this.taskEngine = taskEngine;
}
public void start() {
searchDir = new File(JiveGlobals.getHomeDirectory() +
File.separator + "monitoring" + File.separator + "search");
if (!searchDir.exists()) {
searchDir.mkdirs();
}
boolean indexCreated = false;
try {
loadPropertiesFile(searchDir);
// If the index already exists, use it.
if (IndexReader.indexExists(searchDir)) {
directory = FSDirectory.getDirectory(searchDir, false);
}
// Otherwise, create a new index.
else {
directory = FSDirectory.getDirectory(searchDir, true);
indexCreated = true;
}
}
catch (IOException ioe) {
Log.error(ioe);
}
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);
}
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() {
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);
}
searcher = null;
}
try {
directory.close();
}
catch (Exception e) {
Log.error(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);
}
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);
}
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);
}
finally {
if (writer != null) {
try {
writer.close();
}
catch (Exception e) {
Log.error(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);
}
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);
}
finally {
if (writer != null) {
try {
writer.close();
}
catch (Exception e) {
Log.error(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);
}
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);
}
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 {
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.
*
* This software is published under the terms of the GNU Public License (GPL),
* a copy of which is included in this distribution, or a commercial license
* agreement with Jive.
*/
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.
* This software is published under the terms of the GNU Public License (GPL),
* a copy of which is included in this distribution, or a commercial license
* agreement with Jive.
*/
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.
*
* This software is published under the terms of the GNU Public License (GPL),
* a copy of which is included in this distribution, or a commercial license
* agreement with Jive.
*/
package org.jivesoftware.openfire.archive;
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.*;
import org.jivesoftware.database.CachedPreparedStatement;
import org.jivesoftware.database.DbConnectionManager;
import org.jivesoftware.util.Log;
import org.picocontainer.Startable;
import org.xmpp.packet.JID;
import java.io.IOException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.*;
/**
* 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 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);
return Collections.emptySet();
}
catch (IOException ioe) {
Log.error(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);
}
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 {
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;
}
public Iterator iterator() {
final Iterator<Long> convIterator = conversationIDs.iterator();
return new Iterator() {
private Object nextElement = null;
public boolean hasNext() {
if (nextElement == null) {
nextElement = getNextElement();
if (nextElement == null) {
return false;
}
}
return true;
}
public Object next() {
Object 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 Object 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);
}
}
return null;
}
};
}
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 {
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;
}
public Iterator iterator() {
final Iterator hitsIterator = hits.iterator();
// Advance the iterator until we hit the index.
for (int i=0; i<index; i++) {
hitsIterator.next();
}
return new Iterator() {
private Object nextElement = null;
public boolean hasNext() {
if (nextElement == null) {
nextElement = getNextElement();
if (nextElement == null) {
return false;
}
}
return true;
}
public Object next() {
Object 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 Object 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 = (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);
}
}
return null;
}
};
}
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.
*
* This software is published under the terms of the GNU Public License (GPL),
* a copy of which is included in this distribution, or a commercial license
* agreement with Jive.
*/
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.
*
* This software is published under the terms of the GNU Public License (GPL),
* a copy of which is included in this distribution, or a commercial license
* agreement with Jive.
*/
package org.jivesoftware.openfire.archive;
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.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.NotFoundException;
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.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import org.jivesoftware.openfire.plugin.MonitoringPlugin;
/**
* Represents an IM conversation between two people. A conversation encompasses a
* series of messages sent back and forth. It may cover a single topic or several.
* The start of a conversation occurs when the first message between two users is
* sent. It ends when either:
* <ul>
* <li>No messages are sent between the users for a certain period of time (default of 10
* minutes). The default value can be overridden by setting the Openfire property
* <tt>conversation.idleTime</tt>.</li>
* <li>The total conversation time reaches a maximum value (default of 60 minutes).
* The default value can be overridden by setting the Openfire property
* <tt>conversation.maxTime</tt>. When the max time has been reached and additional
* messages are sent between the users, a new conversation will simply be
* started.</li>
* </ul>
* <p/>
* Each conversation has a start time, date of the last message, and count of the
* messages in the conversation. Conversations are specially marked if one of the
* participants is on an external server. If archiving is enabled, the actual messages in
* the conversation can be retrieved.
*
* @author Matt Tucker
*/
@JiveID(50)
public class Conversation implements Externalizable {
private static final 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);
}
}
}
/**
* 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);
}
}
}
/**
* 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);
}
finally {
DbConnectionManager.closeConnection(rs, pstmt, con);
}
// Add messages of users joining or leaving the group chat conversation
if (room != null) {
for (Map.Entry<String, UserParticipations> entry : participants.entrySet()) {
JID user = new JID(entry.getKey());
boolean anonymous = false;
String name;
try {
name = UserNameManager.getUserName(user);
}
catch (UserNotFoundException e) {
name = user.toBareJID();
anonymous = true;
}
for (ConversationParticipation participation : entry.getValue().getParticipations()) {
if (participation.getJoined() == null) {
Log.warn("Found muc participant with no join date in conversation: " + conversationID);
continue;
}
JID jid = new JID(room + "/" + participation.getNickname());
String joinBody;
String leftBody;
if (anonymous) {
joinBody = LocaleUtils.getLocalizedString("muc.conversation.joined.anonymous", "monitoring",
Arrays.asList(participation.getNickname()));
leftBody = LocaleUtils.getLocalizedString("muc.conversation.left.anonymous", "monitoring",
Arrays.asList(participation.getNickname()));
}
else {
joinBody = LocaleUtils.getLocalizedString("muc.conversation.joined", "monitoring",
Arrays.asList(participation.getNickname(), name));
leftBody = LocaleUtils.getLocalizedString("muc.conversation.left", "monitoring",
Arrays.asList(participation.getNickname(), name));
}
messages.add(
new ArchivedMessage(conversationID, user, jid, participation.getJoined(), joinBody, true));
if (participation.getLeft() != null) {
messages.add(new ArchivedMessage(conversationID, user, jid, participation.getLeft(), leftBody,
true));
}
}
}
// Sort messages by sent date
Collections.sort(messages, new Comparator<ArchivedMessage>() {
public int compare(ArchivedMessage o1, ArchivedMessage o2) {
return o1.getSentDate().compareTo(o2.getSentDate());
}
});
}
return messages;
}
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);
}
}
}
synchronized void participantLeft(JID user, long timestamp) {
// Get the list of participations of the specified user
UserParticipations userParticipations = participants.get(user.toString());
if (userParticipations == null) {
Log.warn("Found user that left a conversation but never started it: " + user);
}
else {
// Get last known participation and check that the user has not finished it
ConversationParticipation currentParticipation = userParticipations.getRecentParticipation();
if (currentParticipation == null || currentParticipation.getLeft() != null) {
Log.warn("Found user that left a conversation but never started it: " + user);
}
else {
currentParticipation.participationEnded(new Date(timestamp));
// Queue storeage of updated participation information
conversationManager.queueParticipantLeft(this, user, currentParticipation);
}
}
}
/**
* Inserts a new conversation into the database.
*
* @throws SQLException if an error occurs inserting the conversation.
*/
private void insertIntoDb() throws SQLException {
this.conversationID = SequenceManager.nextID(this);
Connection con = null;
boolean abortTransaction = false;
try {
con = DbConnectionManager.getTransactionConnection();
PreparedStatement pstmt = con.prepareStatement(INSERT_CONVERSATION);
pstmt.setLong(1, conversationID);
pstmt.setString(2, room == null ? null : room.toString());
pstmt.setInt(3, (external ? 1 : 0));
pstmt.setLong(4, startDate.getTime());
pstmt.setLong(5, lastActivity.getTime());
pstmt.executeUpdate();
pstmt.close();
pstmt = con.prepareStatement(INSERT_PARTICIPANT);
for (Map.Entry<String, UserParticipations> entry : participants.entrySet()) {
JID user = new JID(entry.getKey());
for (ConversationParticipation participation : entry.getValue().getParticipations()) {
pstmt.setLong(1, conversationID);
pstmt.setLong(2, participation.getJoined().getTime());
pstmt.setString(3, user.toBareJID());
pstmt.setString(4, user.getResource() == null ? " " : user.getResource());
pstmt.setString(5, participation.getNickname());
pstmt.executeUpdate();
}
}
pstmt.close();
}
catch (SQLException sqle) {
abortTransaction = true;
throw sqle;
}
finally {
DbConnectionManager.closeTransactionConnection(con, abortTransaction);
}
}
/**
* Adds a new conversation participant into the database.
*
* @param participant the full JID of the participant.
* @param nickname nickname of the user in the room.
* @param joined timestamp when user joined the conversation.
* @throws SQLException if an error occurs inserting the conversation.
*/
private void insertIntoDb(JID participant, String nickname, long joined) throws SQLException {
Connection con = null;
try {
con = DbConnectionManager.getConnection();
PreparedStatement pstmt = con.prepareStatement(INSERT_PARTICIPANT);
pstmt.setLong(1, conversationID);
pstmt.setLong(2, joined);
pstmt.setString(3, participant.toBareJID());
pstmt.setString(4, participant.getResource());
pstmt.setString(5, nickname);
pstmt.executeUpdate();
pstmt.close();
}
catch (SQLException sqle) {
throw sqle;
}
finally {
DbConnectionManager.closeConnection(con);
}
}
private void loadFromDb() throws NotFoundException {
Connection con = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement(LOAD_CONVERSATION);
pstmt.setLong(1, conversationID);
rs = pstmt.executeQuery();
if (!rs.next()) {
throw new NotFoundException("Conversation not found: " + conversationID);
}
this.room = rs.getString(1) == null ? null : new JID(rs.getString(1));
this.external = rs.getInt(2) == 1;
this.startDate = new Date(rs.getLong(3));
this.lastActivity = new Date(rs.getLong(4));
this.messageCount = rs.getInt(5);
rs.close();
pstmt.close();
this.participants = new ConcurrentHashMap<String, UserParticipations>();
pstmt = con.prepareStatement(LOAD_PARTICIPANTS);
pstmt.setLong(1, conversationID);
rs = pstmt.executeQuery();
while (rs.next()) {
// Rebuild full JID of participant
String baredJID = rs.getString(1);
String resource = rs.getString(2);
JID fullJID = new JID("".equals(resource) ? baredJID : baredJID + "/" + resource);
// Rebuild joined and left time
ConversationParticipation participation =
new ConversationParticipation(new Date(rs.getLong(4)), rs.getString(3));
if (rs.getLong(5) > 0) {
participation.participationEnded(new Date(rs.getLong(5)));
}
// Store participation data
UserParticipations userParticipations = participants.get(fullJID.toString());
if (userParticipations == null) {
userParticipations = new UserParticipations(room != null);
participants.put(fullJID.toString(), userParticipations);
}
userParticipations.addParticipation(participation);
}
}
catch (SQLException sqle) {
Log.error(sqle);
}
finally {
DbConnectionManager.closeConnection(rs, pstmt, con);
}
}
/**
* Notification message inficating that conversation has finished so remaining participants
* should be marked that they left the conversation.
*
* @param nowDate the date when the conversation was finished
*/
void conversationEnded(Date nowDate) {
for (Map.Entry<String, UserParticipations> entry : participants.entrySet()) {
ConversationParticipation currentParticipation = entry.getValue().getRecentParticipation();
if (currentParticipation.getLeft() == null) {
currentParticipation.participationEnded(nowDate);
// Queue storage of updated participation information
conversationManager.queueParticipantLeft(this, new JID(entry.getKey()), currentParticipation);
}
}
}
public void writeExternal(ObjectOutput out) throws IOException {
ExternalizableUtil.getInstance().writeLong(out, conversationID);
ExternalizableUtil.getInstance().writeExternalizableMap(out, participants);
ExternalizableUtil.getInstance().writeBoolean(out, external);
ExternalizableUtil.getInstance().writeLong(out, startDate.getTime());
ExternalizableUtil.getInstance().writeLong(out, lastActivity.getTime());
ExternalizableUtil.getInstance().writeInt(out, messageCount);
ExternalizableUtil.getInstance().writeBoolean(out, room != null);
if (room != null) {
ExternalizableUtil.getInstance().writeSerializable(out, room);
}
}
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
MonitoringPlugin plugin = (MonitoringPlugin) XMPPServer.getInstance().getPluginManager().getPlugin(
"monitoring");
conversationManager = (ConversationManager) plugin.getModule(ConversationManager.class);
this.participants = new ConcurrentHashMap<String, UserParticipations>();
conversationID = ExternalizableUtil.getInstance().readLong(in);
ExternalizableUtil.getInstance().readExternalizableMap(in, participants, getClass().getClassLoader());
external = ExternalizableUtil.getInstance().readBoolean(in);
startDate = new Date(ExternalizableUtil.getInstance().readLong(in));
lastActivity = new Date(ExternalizableUtil.getInstance().readLong(in));
messageCount = ExternalizableUtil.getInstance().readInt(in);
if (ExternalizableUtil.getInstance().readBoolean(in)) {
room = (JID) ExternalizableUtil.getInstance().readSerializable(in);
}
}
}
\ No newline at end of file
/**
* $Revision: $
* $Date: $
*
* Copyright (C) 2008 Jive Software. All rights reserved.
*
* This software is published under the terms of the GNU Public License (GPL),
* a copy of which is included in this distribution, or a commercial license
* agreement with Jive.
*/
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.
*
* This software is published under the terms of the GNU Public License (GPL),
* a copy of which is included in this distribution, or a commercial license
* agreement with Jive.
*/
package org.jivesoftware.openfire.archive;
import org.jivesoftware.openfire.archive.cluster.SendConversationEventsTask;
import org.jivesoftware.openfire.reporting.util.TaskEngine;
import org.jivesoftware.openfire.cluster.ClusterManager;
import org.jivesoftware.util.JiveConstants;
import org.jivesoftware.util.cache.CacheFactory;
import java.util.*;
/**
* Queue conversation events generated by this JVM and send them to the senior cluster
* member every 3 seconds. This is an optimization to reduce traffic between the cluster
* nodes specialy when under heavy conversations load.
*
* @author Gaston Dombiak
*/
public class ConversationEventsQueue {
private ConversationManager conversationManager;
/**
* Chat events that are pending to be sent to the senior cluster member.
* Key: Conversation Key; Value: List of conversation events.
*/
private final Map<String, List<ConversationEvent>> chatEvents = new HashMap<String, List<ConversationEvent>>();
/**
* Group chat events that are pending to be sent to the senior cluster member.
* Key: Conversation Key; Value: List of conversation events.
*/
private final Map<String, List<ConversationEvent>> roomEvents = new HashMap<String, List<ConversationEvent>>();
public ConversationEventsQueue(ConversationManager conversationManager, TaskEngine taskEngine) {
this.conversationManager = conversationManager;
// Schedule a task to do conversation archiving.
TimerTask sendTask = new TimerTask() {
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.
*
* This software is published under the terms of the GNU Public License (GPL),
* a copy of which is included in this distribution, or a commercial license
* agreement with Jive.
*/
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.
* This software is published under the terms of the GNU Public License (GPL),
* a copy of which is included in this distribution, or a commercial license
* agreement with Jive.
*/
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.
*
* This software is published under the terms of the GNU Public License (GPL),
* a copy of which is included in this distribution, or a commercial license
* agreement with Jive.
*/
package org.jivesoftware.openfire.archive;
import org.dom4j.Element;
import org.jivesoftware.database.DbConnectionManager;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.XMPPServerInfo;
import org.jivesoftware.openfire.archive.cluster.GetConversationCountTask;
import org.jivesoftware.openfire.archive.cluster.GetConversationTask;
import org.jivesoftware.openfire.archive.cluster.GetConversationsTask;
import org.jivesoftware.openfire.cluster.ClusterManager;
import org.jivesoftware.openfire.component.ComponentEventListener;
import org.jivesoftware.openfire.component.InternalComponentManager;
import org.jivesoftware.openfire.reporting.util.TaskEngine;
import org.jivesoftware.openfire.stats.Statistic;
import org.jivesoftware.openfire.stats.StatisticsManager;
import org.jivesoftware.util.*;
import org.jivesoftware.util.cache.CacheFactory;
import org.picocontainer.Startable;
import org.xmpp.packet.IQ;
import org.xmpp.packet.JID;
import org.xmpp.packet.Message;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CopyOnWriteArraySet;
/**
* Manages all conversations in the system. Optionally, conversations (messages plus
* meta-data) can be archived to the database. Archiving of conversation data is
* enabled by default, but can be disabled by setting "conversation.metadataArchiving" to
* <tt>false</tt>. Archiving of messages in a conversation is disabled by default, but
* can be enabled by setting "conversation.messageArchiving" to <tt>true</tt>.<p>
*
* When running in a cluster only the senior cluster member will keep track of the active
* conversations. Other cluster nodes will forward conversation events that occured 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 converstions will be created.
*
* @author Matt Tucker
*/
public class ConversationManager implements Startable, ComponentEventListener {
private static final String UPDATE_CONVERSATION =
"UPDATE ofConversation SET lastActivity=?, messageCount=? WHERE conversationID=?";
private static final String UPDATE_PARTICIPANT =
"UPDATE ofConParticipant SET leftDate=? WHERE conversationID=? AND bareJID=? AND jidResource=? AND joinedDate=?";
private static final String INSERT_MESSAGE =
"INSERT INTO ofMessageArchive(conversationID, fromJID, toJID, sentDate, body) " +
"VALUES (?,?,?,?,?)";
private static final String CONVERSATION_COUNT =
"SELECT COUNT(*) FROM ofConversation";
private static final String MESSAGE_COUNT =
"SELECT COUNT(*) FROM ofMessageArchive";
private static final int DEFAULT_IDLE_TIME = 10;
private static final int DEFAULT_MAX_TIME = 60;
public static final String CONVERSATIONS_KEY = "conversations";
private ConversationEventsQueue conversationEventsQueue;
private TaskEngine taskEngine;
private Map<String, Conversation> conversations = new ConcurrentHashMap<String, Conversation>();
private boolean metadataArchivingEnabled;
/**
* Flag that indicates if messages of one-to-one chats should be archived.
*/
private boolean messageArchivingEnabled;
/**
* Flag that indicates if messages of group chats (in MUC rooms) should be archived.
*/
private boolean roomArchivingEnabled;
/**
* List of room names to archive. When list is empty then all rooms are archived (if
* roomArchivingEnabled is enabled).
*/
private Collection<String> roomsArchived;
private long idleTime;
private long maxTime;
private PropertyEventListener propertyListener;
private Queue<Conversation> conversationQueue;
private Queue<ArchivedMessage> messageQueue;
/**
* Queue of participants that joined or left a conversation. This queue is processed by the
* ArchivingTask.
*/
private Queue<RoomParticipant> participantQueue;
private boolean archivingRunning = false;
private TimerTask archiveTask;
private TimerTask cleanupTask;
private Collection<ConversationListener> conversationListeners;
/**
* Keeps the address of those components that provide the gateway service.
*/
private List<String> gateways;
private XMPPServerInfo serverInfo;
public ConversationManager(TaskEngine taskEngine) {
this.taskEngine = taskEngine;
this.gateways = new CopyOnWriteArrayList<String>();
this.serverInfo = XMPPServer.getInstance().getServerInfo();
this.conversationEventsQueue = new ConversationEventsQueue(this, taskEngine);
}
public void start() {
metadataArchivingEnabled = JiveGlobals.getBooleanProperty("conversation.metadataArchiving", true);
messageArchivingEnabled = JiveGlobals.getBooleanProperty("conversation.messageArchiving", false);
if (messageArchivingEnabled && !metadataArchivingEnabled) {
Log.warn("Metadata archiving must be enabled when message archiving is enabled. Overriding setting.");
metadataArchivingEnabled = true;
}
roomArchivingEnabled = JiveGlobals.getBooleanProperty("conversation.roomArchiving", false);
roomsArchived = StringUtils.stringToCollection(JiveGlobals.getProperty("conversation.roomsArchived", ""));
if (roomArchivingEnabled && !metadataArchivingEnabled) {
Log.warn("Metadata archiving must be enabled when room archiving is enabled. Overriding setting.");
metadataArchivingEnabled = true;
}
idleTime = JiveGlobals.getIntProperty("conversation.idleTime", DEFAULT_IDLE_TIME) *
JiveConstants.MINUTE;
maxTime = JiveGlobals.getIntProperty("conversation.maxTime",
DEFAULT_MAX_TIME) * JiveConstants.MINUTE;
// Listen for any changes to the conversation properties.
propertyListener = new ConversationPropertyListener();
PropertyEventDispatcher.addListener(propertyListener);
conversationQueue = new ConcurrentLinkedQueue<Conversation>();
messageQueue = new ConcurrentLinkedQueue<ArchivedMessage>();
participantQueue = new ConcurrentLinkedQueue<RoomParticipant>();
conversationListeners = new CopyOnWriteArraySet<ConversationListener>();
// Schedule a task to do conversation archiving.
archiveTask = new TimerTask() {
public void run() {
new ArchivingTask().run();
}
};
taskEngine.scheduleAtFixedRate(archiveTask, JiveConstants.MINUTE, JiveConstants.MINUTE);
// Schedule a task to do conversation cleanup.
cleanupTask = new TimerTask() {
public void run() {
for (String key : conversations.keySet()) {
Conversation conversation = conversations.get(key);
long now = System.currentTimeMillis();
if ((now - conversation.getLastActivity().getTime() > idleTime) ||
(now - conversation.getStartDate().getTime() > maxTime)) {
removeConversation(key, conversation, new Date(now));
}
}
}
};
taskEngine.scheduleAtFixedRate(cleanupTask, JiveConstants.MINUTE * 5, JiveConstants.MINUTE * 5);
// Register a statistic.
Statistic conversationStat = new Statistic() {
public String getName() {
return LocaleUtils.getLocalizedString("stat.conversation.name", "monitoring");
}
public Type getStatType() {
return Type.count;
}
public String getDescription() {
return LocaleUtils.getLocalizedString("stat.conversation.desc", "monitoring");
}
public String getUnits() {
return LocaleUtils.getLocalizedString("stat.conversation.units", "monitoring");
}
public double sample() {
return getConversationCount();
}
public boolean isPartialSample() {
return false;
}
};
StatisticsManager.getInstance().addStatistic(CONVERSATIONS_KEY, conversationStat);
InternalComponentManager.getInstance().addListener(this);
}
public void stop() {
archiveTask.cancel();
archiveTask = null;
cleanupTask.cancel();
cleanupTask = null;
// Remove the statistics.
StatisticsManager.getInstance().removeStatistic(CONVERSATIONS_KEY);
PropertyEventDispatcher.removeListener(propertyListener);
propertyListener = null;
conversations.clear();
conversations = null;
// Archive anything remaining in the queue before quitting.
new ArchivingTask().run();
conversationQueue.clear();
conversationQueue = null;
messageQueue.clear();
messageQueue = null;
conversationListeners.clear();
conversationListeners = null;
serverInfo = null;
InternalComponentManager.getInstance().removeListener(this);
}
/**
* Returns true if metadata archiving is enabled. Conversation meta-data includes
* the participants, start date, last activity, and the count of messages sent.
* When archiving is enabled, all meta-data is written to the database.
*
* @return true if metadata archiving is enabled.
*/
public boolean isMetadataArchivingEnabled() {
return metadataArchivingEnabled;
}
/**
* Sets whether metadata archiving is enabled. Conversation meta-data includes
* the participants, start date, last activity, and the count of messages sent.
* When archiving is enabled, all meta-data is written to the database.
*
* @param enabled true if archiving should be enabled.
*/
public void setMetadataArchivingEnabled(boolean enabled) {
this.metadataArchivingEnabled = enabled;
JiveGlobals.setProperty("conversation.metadataArchiving", Boolean.toString(enabled));
}
/**
* Returns true if one-to-one chats or group chats messages are being archived.
*
* @return true if one-to-one chats or group chats messages are being archived.
*/
public boolean isArchivingEnabled() {
return isMessageArchivingEnabled() || isRoomArchivingEnabled();
}
/**
* Returns true if message archiving is enabled for one-to-one chats. When enabled, all messages
* in one-to-one conversations are stored in the database. Note: it's not possible for
* meta-data archiving to be disabled when message archiving is enabled; enabling
* message archiving automatically enables meta-data archiving.
*
* @return true if message archiving is enabled.
*/
public boolean isMessageArchivingEnabled() {
return messageArchivingEnabled;
}
/**
* Sets whether message archiving is enabled. When enabled, all messages
* in conversations are stored in the database. Note: it's not possible for
* meta-data archiving to be disabled when message archiving is enabled; enabling
* message archiving automatically enables meta-data archiving.
*
* @param enabled true if message should be enabled.
*/
public void setMessageArchivingEnabled(boolean enabled) {
this.messageArchivingEnabled = enabled;
JiveGlobals.setProperty("conversation.messageArchiving", Boolean.toString(enabled));
// Force metadata archiving enabled.
if (enabled) {
this.metadataArchivingEnabled = true;
}
}
/**
* Returns true if message archiving is enabled for group chats. When enabled, all messages
* in group conversations are stored in the database unless a list of rooms was specified
* in {@link #getRoomsArchived()} . Note: it's not possible for meta-data archiving to be
* disabled when room archiving is enabled; enabling room archiving automatically
* enables meta-data archiving.
*
* @return true if room archiving is enabled.
*/
public boolean isRoomArchivingEnabled() {
return roomArchivingEnabled;
}
/**
* Sets whether message archiving is enabled for group chats. When enabled, all messages
* in group conversations are stored in the database unless a list of rooms was specified
* in {@link #getRoomsArchived()} . Note: it's not possible for meta-data archiving to be
* disabled when room archiving is enabled; enabling room archiving automatically
* enables meta-data archiving.
*
* @param enabled if room archiving is enabled.
*/
public void setRoomArchivingEnabled(boolean enabled) {
this.roomArchivingEnabled = enabled;
JiveGlobals.setProperty("conversation.roomArchiving", Boolean.toString(enabled));
// Force metadata archiving enabled.
if (enabled) {
this.metadataArchivingEnabled = true;
}
}
/**
* Returns list of room names whose messages will be archived. When room archiving is enabled and
* this list is empty then messages of all local rooms will be archived. However, when name of
* rooms are defined in this list then only messages of those rooms will be archived.
*
* @return list of local room names whose messages will be archived.
*/
public Collection<String> getRoomsArchived() {
return roomsArchived;
}
/**
* Sets list of room names whose messages will be archived. When room archiving is enabled and
* this list is empty then messages of all local rooms will be archived. However, when name of
* rooms are defined in this list then only messages of those rooms will be archived.
*
* @param roomsArchived list of local room names whose messages will be archived.
*/
public void setRoomsArchived(Collection<String> roomsArchived) {
this.roomsArchived = roomsArchived;
JiveGlobals.setProperty("conversation.roomsArchived", StringUtils.collectionToString(roomsArchived));
}
/**
* Returns the number of minutes a conversation can be idle before it's ended.
*
* @return the conversation idle time.
*/
public int getIdleTime() {
return (int)(idleTime / JiveConstants.MINUTE);
}
/**
* Sets the number of minutes a conversation can be idle before it's ended.
*
* @param idleTime the max number of minutes a conversation can be idle before it's ended.
* @throws IllegalArgumentException if idleTime is less than 1.
*/
public void setIdleTime(int idleTime) {
if (idleTime < 1) {
throw new IllegalArgumentException("Idle time less than 1 is not valid: " + idleTime);
}
JiveGlobals.setProperty("conversation.idleTime", Integer.toString(idleTime));
this.idleTime = idleTime * JiveConstants.MINUTE;
}
/**
* Returns the maximum number of minutes a conversation can last before it's ended.
* Any additional messages between the participants in the chat will be associated
* with a new conversation.
*
* @return the maximum number of minutes a conversation can last.
*/
public int getMaxTime() {
return (int)(maxTime / JiveConstants.MINUTE);
}
/**
* Sets the maximum number of minutes a conversation can last before it's ended.
* Any additional messages between the participants in the chat will be associated
* with a new conversation.
*
* @param maxTime the maximum number of minutes a conversation can last.
* @throws IllegalArgumentException if maxTime is less than 1.
*/
public void setMaxTime(int maxTime) {
if (maxTime < 1) {
throw new IllegalArgumentException("Max time less than 1 is not valid: " + maxTime);
}
JiveGlobals.setProperty("conversation.maxTime", Integer.toString(maxTime));
this.maxTime = maxTime * JiveConstants.MINUTE;
}
public ConversationEventsQueue getConversationEventsQueue() {
return conversationEventsQueue;
}
/**
* Returns the count of active conversations.
*
* @return the count of active conversations.
*/
public int getConversationCount() {
if (ClusterManager.isSeniorClusterMember()) {
return conversations.size();
}
return (Integer) CacheFactory.doSynchronousClusterTask(new GetConversationCountTask(),
ClusterManager.getSeniorClusterMember().toByteArray());
}
/**
* Returns a conversation by ID.
*
* @param conversationID the ID of the conversation.
* @return the conversation.
* @throws NotFoundException if the conversation could not be found.
*/
public Conversation getConversation(long conversationID) throws NotFoundException {
if (ClusterManager.isSeniorClusterMember()) {
// Search through the currently active conversations.
for (Conversation conversation : conversations.values()) {
if (conversation.getConversationID() == conversationID) {
return conversation;
}
}
// Otherwise, it might be an archived conversation, so attempt to load it.
return new Conversation(this, conversationID);
}
else {
// Get this info from the senior cluster member when running in a cluster
Conversation conversation = (Conversation) CacheFactory.doSynchronousClusterTask(
new GetConversationTask(conversationID), ClusterManager.getSeniorClusterMember().toByteArray());
if (conversation == null) {
throw new NotFoundException("Conversation not found: " + conversationID);
}
return conversation;
}
}
/**
* Returns the set of active conversations.
*
* @return the active conversations.
*/
public Collection<Conversation> getConversations() {
if (ClusterManager.isSeniorClusterMember()) {
List<Conversation> conversationList = new ArrayList<Conversation>(conversations.values());
// Sort the conversations by creation date.
Collections.sort(conversationList, new Comparator<Conversation>() {
public int compare(Conversation c1, Conversation c2) {
return c1.getStartDate().compareTo(c2.getStartDate());
}
});
return conversationList;
}
else {
// Get this info from the senior cluster member when running in a cluster
return (Collection<Conversation>) CacheFactory.doSynchronousClusterTask(new GetConversationsTask(),
ClusterManager.getSeniorClusterMember().toByteArray());
}
}
/**
* Returns the total number of conversations that have been archived to the database.
* The archived conversation may only be the meta-data, or it might include messages
* as well if message archiving is turned on.
*
* @return the total number of archived conversations.
*/
public int getArchivedConversationCount() {
int conversationCount = 0;
Connection con = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement(CONVERSATION_COUNT);
rs = pstmt.executeQuery();
if (rs.next()) {
conversationCount = rs.getInt(1);
}
}
catch (SQLException sqle) {
Log.error(sqle);
}
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);
}
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 it = childElement.elementIterator("identity"); it.hasNext();) {
Element identity = (Element)it.next();
if ("gateway".equals(identity.attributeValue("category"))) {
gatewayFound = true;
}
}
// If component is a gateway then keep track of the component
if (gatewayFound) {
gateways.add(iq.getFrom().getDomain());
}
}
public void componentRegistered(JID componentJID) {
//Do nothing
}
public void componentUnregistered(JID componentJID) {
// Remove stored information about this component
gateways.remove(componentJID.getDomain());
}
void queueParticipantLeft(Conversation conversation, JID user, ConversationParticipation participation) {
RoomParticipant updatedParticipant = new RoomParticipant();
updatedParticipant.conversationID = conversation.getConversationID();
updatedParticipant.user = user;
updatedParticipant.joined = participation.getJoined();
updatedParticipant.left = participation.getLeft();
participantQueue.add(updatedParticipant);
}
/**
* A task that persists conversation meta-data and messages to the database.
*/
private class ArchivingTask implements Runnable {
public void run() {
synchronized (this) {
if (archivingRunning) {
return;
}
archivingRunning = true;
}
if (!messageQueue.isEmpty() || !conversationQueue.isEmpty() || !participantQueue.isEmpty()) {
Connection con = null;
PreparedStatement pstmt = null;
try {
con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement(INSERT_MESSAGE);
ArchivedMessage message;
int count = 0;
while ((message = messageQueue.poll()) != null) {
pstmt.setLong(1, message.getConversationID());
pstmt.setString(2, message.getFromJID().toString());
pstmt.setString(3, message.getToJID().toString());
pstmt.setLong(4, message.getSentDate().getTime());
DbConnectionManager.setLargeTextField(pstmt, 5, message.getBody());
if (DbConnectionManager.isBatchUpdatesSupported()) {
pstmt.addBatch();
}
else {
pstmt.execute();
}
// Only batch up to 500 items at a time.
if (count % 500 == 0 && DbConnectionManager.isBatchUpdatesSupported()) {
pstmt.executeBatch();
}
count++;
}
if (DbConnectionManager.isBatchUpdatesSupported()) {
pstmt.executeBatch();
}
pstmt = con.prepareStatement(UPDATE_CONVERSATION);
Conversation conversation;
count = 0;
while ((conversation = conversationQueue.poll()) != null) {
pstmt.setLong(1, conversation.getLastActivity().getTime());
pstmt.setInt(2, conversation.getMessageCount());
pstmt.setLong(3, conversation.getConversationID());
if (DbConnectionManager.isBatchUpdatesSupported()) {
pstmt.addBatch();
}
else {
pstmt.execute();
}
// Only batch up to 500 items at a time.
if (count % 500 == 0 && DbConnectionManager.isBatchUpdatesSupported()) {
pstmt.executeBatch();
}
count++;
}
if (DbConnectionManager.isBatchUpdatesSupported()) {
pstmt.executeBatch();
}
pstmt = con.prepareStatement(UPDATE_PARTICIPANT);
RoomParticipant particpiant;
count = 0;
while ((particpiant = participantQueue.poll()) != null) {
pstmt.setLong(1, particpiant.left.getTime());
pstmt.setLong(2, particpiant.conversationID);
pstmt.setString(3, particpiant.user.toBareJID());
pstmt.setString(4, particpiant.user.getResource() == null ? " " : particpiant.user.getResource());
pstmt.setLong(5, particpiant.joined.getTime());
if (DbConnectionManager.isBatchUpdatesSupported()) {
pstmt.addBatch();
}
else {
pstmt.execute();
}
// Only batch up to 500 items at a time.
if (count % 500 == 0 && DbConnectionManager.isBatchUpdatesSupported()) {
pstmt.executeBatch();
}
count++;
}
if (DbConnectionManager.isBatchUpdatesSupported()) {
pstmt.executeBatch();
}
}
catch (Exception e) {
Log.error(e);
}
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 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);
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);
maxTime = DEFAULT_MAX_TIME * JiveConstants.MINUTE;
}
}
}
public void propertyDeleted(String property, Map 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 params) {
// Ignore.
}
public void xmlPropertyDeleted(String property, Map 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.
*
* This software is published under the terms of the GNU Public License (GPL),
* a copy of which is included in this distribution, or a commercial license
* agreement with Jive.
*/
package org.jivesoftware.openfire.archive;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.plugin.MonitoringPlugin;
import org.jivesoftware.util.Log;
import org.jivesoftware.util.NotFoundException;
import org.jivesoftware.util.ParamUtils;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
/**
*
*/
public class ConversationPDFServlet extends HttpServlet {
public void init() throws ServletException {
}
public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
long conversationID = ParamUtils.getLongParameter(request, "conversationID", -1);
if (conversationID == -1) {
return;
}
MonitoringPlugin plugin = (MonitoringPlugin)XMPPServer.getInstance().getPluginManager().getPlugin(
"monitoring");
ConversationManager conversationManager = (ConversationManager)plugin.getModule(ConversationManager.class);
Conversation conversation;
if (conversationID > -1) {
try {
conversation = new Conversation(conversationManager, conversationID);
ByteArrayOutputStream stream = new ConversationUtils().getConversationPDF(conversation);
// setting some response headers
response.setHeader("Expires", "0");
response.setHeader("Cache-Control", "must-revalidate, post-check=0, pre-check=0");
response.setHeader("Pragma", "public");
// setting the content type
response.setContentType("application/pdf");
// the contentlength 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);
}
}
}
}
/**
* $Revision: $
* $Date: $
*
* Copyright (C) 2008 Jive Software. All rights reserved.
*
* This software is published under the terms of the GNU Public License (GPL),
* a copy of which is included in this distribution, or a commercial license
* agreement with Jive.
*/
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.
*
* This software is published under the terms of the GNU Public License (GPL),
* a copy of which is included in this distribution, or a commercial license
* agreement with Jive.
*/
package org.jivesoftware.openfire.archive;
import com.lowagie.text.*;
import com.lowagie.text.Font;
import com.lowagie.text.Image;
import com.lowagie.text.pdf.PdfContentByte;
import com.lowagie.text.pdf.PdfPageEventHelper;
import com.lowagie.text.pdf.PdfWriter;
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.Log;
import org.jivesoftware.util.NotFoundException;
import org.xmpp.component.ComponentManagerFactory;
import org.xmpp.packet.JID;
import java.awt.*;
import java.io.ByteArrayOutputStream;
import java.net.URL;
import java.util.*;
import java.util.List;
import java.util.concurrent.Future;
/**
* Utility class for asynchronous web calls for archiving tasks.
*
* @author Derek DeMoro
*/
public class ConversationUtils {
/**
* Returns the status of the rebuilding of the messaging/metadata archives. This is done
* asynchronously.
*
* @return the status the rebuilding (0 - 100) where 100 is complete.
*/
public int getBuildProgress() {
// Get handle on the Monitoring plugin
MonitoringPlugin plugin =
(MonitoringPlugin)XMPPServer.getInstance().getPluginManager().getPlugin(
"monitoring");
ArchiveIndexer archiveIndexer = (ArchiveIndexer)plugin.getModule(ArchiveIndexer.class);
Future<Integer> future = archiveIndexer.getIndexRebuildProgress();
if (future != null) {
try {
return future.get();
}
catch (Exception e) {
Log.error(e);
}
}
return -1;
}
public ConversationInfo getConversationInfo(long conversationID, boolean formatParticipants) {
// Create ConversationInfo bean
ConversationInfo info = new ConversationInfo();
// Get handle on the Monitoring plugin
MonitoringPlugin plugin =
(MonitoringPlugin)XMPPServer.getInstance().getPluginManager().getPlugin(
"monitoring");
ConversationManager conversationmanager =
(ConversationManager)plugin.getModule(ConversationManager.class);
try {
Conversation conversation = conversationmanager.getConversation(conversationID);
info = toConversationInfo(conversation, formatParticipants);
}
catch (NotFoundException e) {
ComponentManagerFactory.getComponentManager().getLog().error(e);
}
return info;
}
/**
* Retrieves all the existing conversations from the system.
*
* @return a Map of ConversationInfo objects.
*/
public Map<String, ConversationInfo> getConversations(boolean formatParticipants) {
Map<String, ConversationInfo> cons = new HashMap<String, ConversationInfo>();
MonitoringPlugin plugin = (MonitoringPlugin)XMPPServer.getInstance().getPluginManager()
.getPlugin("monitoring");
ConversationManager conversationManager =
(ConversationManager)plugin.getModule(ConversationManager.class);
Collection<Conversation> conversations = conversationManager.getConversations();
List<Conversation> lConversations =
Arrays.asList(conversations.toArray(new Conversation[conversations.size()]));
for (Iterator<Conversation> i = lConversations.iterator(); i.hasNext();) {
Conversation con = i.next();
ConversationInfo info = toConversationInfo(con, formatParticipants);
cons.put(Long.toString(con.getConversationID()), info);
}
return cons;
}
public ByteArrayOutputStream getConversationPDF(Conversation conversation) {
Font red = FontFactory
.getFont(FontFactory.HELVETICA, 12f, Font.BOLD, new Color(0xFF, 0x00, 0x00));
Font blue = FontFactory
.getFont(FontFactory.HELVETICA, 12f, Font.ITALIC, new Color(0x00, 0x00, 0xFF));
Font black = FontFactory.getFont(FontFactory.HELVETICA, 12f, Font.BOLD, Color.BLACK);
Map<String, Font> colorMap = new HashMap<String, Font>();
if (conversation != null) {
Collection<JID> set = conversation.getParticipants();
int count = 0;
for (JID jid : set) {
if (conversation.getRoom() == null) {
if (count == 0) {
colorMap.put(jid.toString(), blue);
}
else {
colorMap.put(jid.toString(), red);
}
count++;
}
else {
colorMap.put(jid.toString(), black);
}
}
}
return buildPDFContent(conversation, colorMap);
}
private ByteArrayOutputStream buildPDFContent(Conversation conversation,
Map<String, Font> colorMap) {
Font roomEvent = FontFactory
.getFont(FontFactory.HELVETICA, 12f, Font.ITALIC, new Color(0xFF, 0x00, 0xFF));
try {
Document document = new Document(PageSize.A4, 50, 50, 50, 50);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
PdfWriter writer = PdfWriter.getInstance(document, baos);
writer.setPageEvent(new PDFEventListener());
document.open();
Paragraph p = new Paragraph(
LocaleUtils.getLocalizedString("archive.search.pdf.title", "monitoring"),
FontFactory.getFont(FontFactory.HELVETICA,
18, Font.BOLD));
document.add(p);
document.add(Chunk.NEWLINE);
ConversationInfo coninfo = new ConversationUtils()
.getConversationInfo(conversation.getConversationID(), false);
String participantsDetail;
if (coninfo.getAllParticipants() == null) {
participantsDetail = coninfo.getParticipant1() + ", " + coninfo.getParticipant2();
}
else {
participantsDetail = String.valueOf(coninfo.getAllParticipants().length);
}
Paragraph chapterTitle = new Paragraph(
LocaleUtils
.getLocalizedString("archive.search.pdf.participants", "monitoring") +
" " + participantsDetail,
FontFactory.getFont(FontFactory.HELVETICA, 12,
Font.BOLD));
document.add(chapterTitle);
Paragraph startDate = new Paragraph(
LocaleUtils.getLocalizedString("archive.search.pdf.startdate", "monitoring") +
" " +
coninfo.getDate(),
FontFactory.getFont(FontFactory.HELVETICA, 12,
Font.BOLD));
document.add(startDate);
Paragraph duration = new Paragraph(
LocaleUtils.getLocalizedString("archive.search.pdf.duration", "monitoring") +
" " +
coninfo.getDuration(),
FontFactory.getFont(FontFactory.HELVETICA, 12,
Font.BOLD));
document.add(duration);
Paragraph messageCount = new Paragraph(
LocaleUtils
.getLocalizedString("archive.search.pdf.messagecount", "monitoring") +
" " +
conversation.getMessageCount(),
FontFactory.getFont(FontFactory.HELVETICA, 12,
Font.BOLD));
document.add(messageCount);
document.add(Chunk.NEWLINE);
Paragraph messageParagraph;
for (ArchivedMessage message : conversation.getMessages()) {
String time = JiveGlobals.formatTime(message.getSentDate());
String from = message.getFromJID().getNode();
if (conversation.getRoom() != null) {
from = message.getToJID().getResource();
}
String body = message.getBody();
String prefix;
if (!message.isRoomEvent()) {
prefix = "[" + time + "] " + from + ": ";
Font font = colorMap.get(message.getFromJID().toString());
if (font == null) {
font = colorMap.get(message.getFromJID().toBareJID());
}
if (font == null) {
font = FontFactory.getFont(FontFactory.HELVETICA, 12f, Font.BOLD, Color.BLACK);
}
messageParagraph = new Paragraph(new Chunk(prefix, font));
}
else {
prefix = "[" + time + "] ";
messageParagraph = new Paragraph(new Chunk(prefix, roomEvent));
}
messageParagraph.add(body);
messageParagraph.add(" ");
document.add(messageParagraph);
}
document.close();
return baos;
}
catch (DocumentException e) {
Log.error("error creating PDF document: " + e.getMessage(), e);
return null;
}
}
private ConversationInfo toConversationInfo(Conversation conversation,
boolean formatParticipants) {
final ConversationInfo info = new ConversationInfo();
// Set participants
Collection<JID> col = conversation.getParticipants();
if (conversation.getRoom() == null) {
JID user1 = (JID)col.toArray()[0];
info.setParticipant1(formatJID(formatParticipants, user1));
JID user2 = (JID)col.toArray()[1];
info.setParticipant2(formatJID(formatParticipants, user2));
}
else {
info.setConversationID(conversation.getConversationID());
JID[] occupants = col.toArray(new JID[col.size()]);
String[] jids = new String[col.size()];
for (int i = 0; i < occupants.length; i++) {
jids[i] = formatJID(formatParticipants, occupants[i]);
}
info.setAllParticipants(jids);
}
Map<String, String> cssLabels = new HashMap<String, String>();
int count = 0;
for (JID jid : col) {
if (!cssLabels.containsKey(jid.toString())) {
if (conversation.getRoom() == null) {
if (count % 2 == 0) {
cssLabels.put(jid.toBareJID(), "conversation-label2");
}
else {
cssLabels.put(jid.toBareJID(), "conversation-label1");
}
count++;
}
else {
cssLabels.put(jid.toString(), "conversation-label4");
}
}
}
// Set date
info.setDate(JiveGlobals.formatDateTime(conversation.getStartDate()));
info.setLastActivity(JiveGlobals.formatTime(conversation.getLastActivity()));
// Create body.
final StringBuilder builder = new StringBuilder();
builder.append("<table width=100%>");
for (ArchivedMessage message : conversation.getMessages()) {
String time = JiveGlobals.formatTime(message.getSentDate());
String from = message.getFromJID().getNode();
if (conversation.getRoom() != null) {
from = message.getToJID().getResource();
}
String cssLabel = cssLabels.get(message.getFromJID().toBareJID());
String body = message.getBody();
builder.append("<tr valign=top>");
if (!message.isRoomEvent()) {
builder.append("<td width=1% nowrap class=" + cssLabel + ">").append("[")
.append(time).append("]").append("</td>");
builder.append("<td width=1% class=" + cssLabel + ">").append(from).append(": ")
.append("</td>");
builder.append("<td class=conversation-body>").append(body).append("</td");
}
else {
builder.append("<td width=1% nowrap class=conversation-label3>").append("[")
.append(time).append("]").append("</td>");
builder.append("<td colspan=2 class=conversation-label3><i>").append(body)
.append("</i></td");
}
builder.append("</tr>");
}
if (conversation.getMessages().size() == 0) {
builder.append("<span class=small-description>" +
LocaleUtils.getLocalizedString("archive.search.results.archive_disabled",
"monitoring") +
"</a>");
}
info.setBody(builder.toString());
// Set message count
info.setMessageCount(conversation.getMessageCount());
long duration =
(conversation.getLastActivity().getTime() - conversation.getStartDate().getTime());
info.setDuration(duration);
return info;
}
private String formatJID(boolean html, JID jid) {
String formattedJID;
if (html) {
UserManager userManager = UserManager.getInstance();
if (XMPPServer.getInstance().isLocal(jid) &&
userManager.isRegisteredUser(jid.getNode())) {
formattedJID = "<a href='/user-properties.jsp?username=" +
jid.getNode() + "'>" + jid.toBareJID() + "</a>";
}
else {
formattedJID = jid.toBareJID();
}
}
else {
formattedJID = jid.toBareJID();
}
return formattedJID;
}
class PDFEventListener extends PdfPageEventHelper {
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.
*
* This software is published under the terms of the GNU Public License (GPL),
* a copy of which is included in this distribution, or a commercial license
* agreement with Jive.
*/
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 roomSubjectChanged(JID roomJID, JID user, String newSubject) {
// Do nothing
}
public void start() {
MUCEventDispatcher.addListener(this);
}
public void stop() {
MUCEventDispatcher.removeListener(this);
conversationManager = null;
}
}
/**
* $RCSfile $
* $Revision $
* $Date $
*
* Copyright (C) 2008 Jive Software. All rights reserved.
*
* This software is published under the terms of the GNU Public License (GPL),
* a copy of which is included in this distribution, or a commercial license
* agreement with Jive.
*/
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.
*
* This software is published under the terms of the GNU Public License (GPL),
* a copy of which is included in this distribution, or a commercial license
* agreement with Jive.
*/
package org.jivesoftware.openfire.archive.cluster;
import org.jivesoftware.openfire.archive.ConversationManager;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.plugin.MonitoringPlugin;
import org.jivesoftware.util.cache.ClusterTask;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
/**
* Task that will return the number of current conversations taking place in the senior cluster member.
* All conversations in the cluster are kept in the senior cluster member.
*
* @author Gaston Dombiak
*/
public class GetConversationCountTask implements ClusterTask {
private int conversationCount;
public Object getResult() {
return conversationCount;
}
public void run() {
MonitoringPlugin plugin = (MonitoringPlugin) XMPPServer.getInstance().getPluginManager().getPlugin(
"monitoring");
ConversationManager conversationManager = (ConversationManager)plugin.getModule(ConversationManager.class);
conversationCount = conversationManager.getConversationCount();
}
public void writeExternal(ObjectOutput out) throws IOException {
// Do nothing
}
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
// Do nothing
}
}
/**
* $Revision: $
* $Date: $
*
* Copyright (C) 2008 Jive Software. All rights reserved.
*
* This software is published under the terms of the GNU Public License (GPL),
* a copy of which is included in this distribution, or a commercial license
* agreement with Jive.
*/
package org.jivesoftware.openfire.archive.cluster;
import org.jivesoftware.openfire.archive.Conversation;
import org.jivesoftware.openfire.archive.ConversationManager;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.plugin.MonitoringPlugin;
import org.jivesoftware.util.NotFoundException;
import org.jivesoftware.util.cache.ClusterTask;
import org.jivesoftware.util.cache.ExternalizableUtil;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
/**
* Task that returns the specified conversation or <tt>null</tt> if not found.
*
* @author Gaston Dombiak
*/
public class GetConversationTask implements ClusterTask {
private long conversationID;
private Conversation conversation;
public GetConversationTask() {
}
public GetConversationTask(long conversationID) {
this.conversationID = conversationID;
}
public Object getResult() {
return conversation;
}
public void run() {
MonitoringPlugin plugin = (MonitoringPlugin) XMPPServer.getInstance().getPluginManager().getPlugin(
"monitoring");
ConversationManager conversationManager = (ConversationManager)plugin.getModule(ConversationManager.class);
try {
conversation = conversationManager.getConversation(conversationID);
} catch (NotFoundException e) {
// Ignore. The requester of this task will throw this exception in his JVM
}
}
public void writeExternal(ObjectOutput out) throws IOException {
ExternalizableUtil.getInstance().writeLong(out, conversationID);
}
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
conversationID = ExternalizableUtil.getInstance().readLong(in);
}
}
/**
* $Revision: $
* $Date: $
*
* Copyright (C) 2008 Jive Software. All rights reserved.
*
* This software is published under the terms of the GNU Public License (GPL),
* a copy of which is included in this distribution, or a commercial license
* agreement with Jive.
*/
package org.jivesoftware.openfire.archive.cluster;
import org.jivesoftware.openfire.archive.Conversation;
import org.jivesoftware.openfire.archive.ConversationManager;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.plugin.MonitoringPlugin;
import org.jivesoftware.util.cache.ClusterTask;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.util.Collection;
/**
* Task that will return current conversations taking place in the senior cluster member.
* All conversations in the cluster are kept in the senior cluster member.
*
* @author Gaston Dombiak
*/
public class GetConversationsTask implements ClusterTask {
private Collection<Conversation> conversations;
public Object getResult() {
return conversations;
}
public void run() {
MonitoringPlugin plugin = (MonitoringPlugin) XMPPServer.getInstance().getPluginManager().getPlugin(
"monitoring");
ConversationManager conversationManager = (ConversationManager)plugin.getModule(ConversationManager.class);
conversations = conversationManager.getConversations();
}
public void writeExternal(ObjectOutput out) throws IOException {
// Do nothing
}
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
// Do nothing
}
}
/**
* $Revision: $
* $Date: $
*
* Copyright (C) 2008 Jive Software. All rights reserved.
*
* This software is published under the terms of the GNU Public License (GPL),
* a copy of which is included in this distribution, or a commercial license
* agreement with Jive.
*/
package org.jivesoftware.openfire.archive.cluster;
import org.jivesoftware.openfire.archive.ConversationEvent;
import org.jivesoftware.openfire.archive.ConversationManager;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.plugin.MonitoringPlugin;
import org.jivesoftware.util.Log;
import org.jivesoftware.util.cache.ClusterTask;
import org.jivesoftware.util.cache.ExternalizableUtil;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.util.ArrayList;
import java.util.List;
/**
* Task that sends cnoversation events to the senior cluster member.
*
* @author Gaston Dombiak
*/
public class SendConversationEventsTask implements ClusterTask {
private List<ConversationEvent> events;
/**
* Do not use this constructor. It only exists for serialization purposes.
*/
public SendConversationEventsTask() {
}
public SendConversationEventsTask(List<ConversationEvent> events) {
this.events = events;
}
public Object getResult() {
return null;
}
public void run() {
MonitoringPlugin plugin = (MonitoringPlugin) XMPPServer.getInstance().getPluginManager().getPlugin(
"monitoring");
ConversationManager conversationManager = (ConversationManager)plugin.getModule(ConversationManager.class);
for (ConversationEvent event : events) {
try {
event.run(conversationManager);
} catch (Exception e) {
Log.error("Error while processing chat archiving event", e);
}
}
}
public void writeExternal(ObjectOutput out) throws IOException {
ExternalizableUtil.getInstance().writeExternalizableCollection(out, events);
}
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
events = new ArrayList<ConversationEvent>();
ExternalizableUtil.getInstance().readExternalizableCollection(in, events, getClass().getClassLoader());
}
}
/**
* $RCSfile$
* $Revision$
* $Date: $
*
* Copyright (C) 2008 Jive Software. All rights reserved.
*
* This software is published under the terms of the GNU Public License (GPL),
* a copy of which is included in this distribution, or a commercial license
* agreement with Jive.
*/
package org.jivesoftware.openfire.archive.commands;
import org.dom4j.Element;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.archive.*;
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.Log;
import org.jivesoftware.util.StringUtils;
import org.xmpp.component.ComponentManagerFactory;
import org.xmpp.forms.DataForm;
import org.xmpp.forms.FormField;
import org.xmpp.packet.JID;
import java.io.ByteArrayOutputStream;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.List;
/**
* Command that allows to retrieve PDF content of group chat transcripts.
*
* @author Gaston Dombiak
*
* TODO Use i18n
*/
public class GetGroupConversationTranscript extends AdHocCommand {
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());
}
public void execute(SessionData data, Element command) {
Element note = command.addElement("note");
// Get handle on the Monitoring plugin
MonitoringPlugin plugin = (MonitoringPlugin) XMPPServer.getInstance().getPluginManager()
.getPlugin("monitoring");
ConversationManager conversationManager =
(ConversationManager) plugin.getModule(ConversationManager.class);
if (!conversationManager.isArchivingEnabled()) {
note.addAttribute("type", "error");
note.setText("Message archiving is not enabled.");
DataForm form = new DataForm(DataForm.Type.result);
FormField field = form.addField();
field.setType(FormField.Type.hidden);
field.setVariable("FORM_TYPE");
field.addValue("http://jabber.org/protocol/admin");
field = form.addField();
field.setLabel("Conversation Found?");
field.setVariable("found");
field.addValue(false);
// Add form to reply
command.add(form.getElement());
return;
}
try {
JID participant = new JID(data.getData().get("participant").get(0));
JID room = new JID(data.getData().get("room").get(0));
Date time = DataForm.parseDate(data.getData().get("time").get(0));
boolean includePDF = DataForm.parseBoolean(data.getData().get("includePDF").get(0));
// Get archive searcher module
ArchiveSearcher archiveSearcher = (ArchiveSearcher) plugin.getModule(ArchiveSearcher.class);
ArchiveSearch search = new ArchiveSearch();
search.setParticipants(participant);
search.setIncludeTimestamp(time);
search.setRoom(room);
Collection<Conversation> conversations = archiveSearcher.search(search);
DataForm form = new DataForm(DataForm.Type.result);
FormField field = form.addField();
field.setType(FormField.Type.hidden);
field.setVariable("FORM_TYPE");
field.addValue("http://jabber.org/protocol/admin");
field = form.addField();
field.setLabel("Conversation Found?");
field.setVariable("found");
field.addValue(!conversations.isEmpty());
if (includePDF) {
ByteArrayOutputStream stream = null;
if (!conversations.isEmpty()) {
stream = new ConversationUtils().getConversationPDF(conversations.iterator().next());
}
if (stream != null) {
field = form.addField();
field.setLabel("PDF");
field.setVariable("pdf");
field.addValue(StringUtils.encodeBase64(stream.toByteArray()));
}
}
// Add form to reply
command.add(form.getElement());
}
catch (Exception e) {
Log.error("Error occurred while running the command", e);
note.addAttribute("type", "error");
note.setText("Error while processing the command.");
}
}
public String getCode() {
return "http://jivesoftware.com/protocol/workgroup#get-group-conv-transcript";
}
public String getDefaultLabel() {
return "Get Group Conversation Transcript";
}
protected List<Action> getActions(SessionData data) {
return Arrays.asList(Action.complete);
}
protected Action getExecuteAction(SessionData data) {
return Action.complete;
}
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.
*/
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.
*
* This software is published under the terms of the GNU Public License (GPL),
* a copy of which is included in this distribution, or a commercial license
* agreement with Jive.
*/
package org.jivesoftware.openfire.plugin;
import org.jivesoftware.openfire.archive.*;
import org.jivesoftware.openfire.container.Plugin;
import org.jivesoftware.openfire.container.PluginManager;
import org.jivesoftware.openfire.reporting.graph.GraphEngine;
import org.jivesoftware.openfire.reporting.stats.*;
import org.jivesoftware.openfire.reporting.util.TaskEngine;
import org.jivesoftware.util.JiveGlobals;
import org.jivesoftware.util.JiveProperties;
import org.picocontainer.MutablePicoContainer;
import org.picocontainer.defaults.DefaultPicoContainer;
import java.io.File;
import java.io.FileFilter;
/**
* Openfire Monitoring plugin.
*
* @author Matt Tucker
*/
public class MonitoringPlugin implements Plugin {
private MutablePicoContainer picoContainer;
private boolean shuttingDown = false;
public MonitoringPlugin() {
// Enable AWT headless mode so that stats will work in headless environments.
System.setProperty("java.awt.headless", "true");
picoContainer = new DefaultPicoContainer();
picoContainer.registerComponentInstance(TaskEngine.getInstance());
picoContainer.registerComponentInstance(JiveProperties.getInstance());
// Stats and Graphing classes
picoContainer.registerComponentImplementation(StatsEngine.class);
picoContainer.registerComponentImplementation(GraphEngine.class);
picoContainer.registerComponentImplementation(StatisticsModule.class);
picoContainer.registerComponentImplementation(StatsViewer.class,
getStatsViewerImplementation());
// Archive classes
picoContainer.registerComponentImplementation(ConversationManager.class);
picoContainer.registerComponentImplementation(ArchiveInterceptor.class);
picoContainer.registerComponentImplementation(GroupConversationInterceptor.class);
picoContainer.registerComponentImplementation(ArchiveSearcher.class);
picoContainer.registerComponentImplementation(ArchiveIndexer.class);
}
private Class<? extends StatsViewer> getStatsViewerImplementation() {
if (JiveGlobals.getBooleanProperty("stats.mock.viewer", false)) {
return MockStatsViewer.class;
}
else {
return DefaultStatsViewer.class;
}
}
/**
* Returns the instance of a module registered with the Monitoring plugin.
*
* @param clazz the module class.
* @return the instance of the module.
*/
public Object getModule(Class clazz) {
return picoContainer.getComponentInstanceOfType(clazz);
}
public void initializePlugin(PluginManager manager, File pluginDirectory) {
System.out.println("Starting Monitoring Plugin");
// Check if we Enterprise is installed and stop loading this plugin if found
File pluginDir = new File(JiveGlobals.getHomeDirectory(), "plugins");
File[] jars = pluginDir.listFiles(new FileFilter() {
public boolean accept(File pathname) {
String fileName = pathname.getName().toLowerCase();
return (fileName.equalsIgnoreCase("enterprise.jar"));
}
});
if (jars.length > 0) {
// Do not load this plugin since Enterprise is still installed
System.out.println("Enterprise plugin found. Stopping Monitoring Plugin");
throw new IllegalStateException("This plugin cannot run next to the Enterprise plugin");
}
shuttingDown = false;
// Make sure that the monitoring folder exists under the home directory
File dir = new File(JiveGlobals.getHomeDirectory() +
File.separator + "monitoring");
if (!dir.exists()) {
dir.mkdirs();
}
picoContainer.start();
}
public void destroyPlugin() {
shuttingDown = true;
if (picoContainer != null) {
picoContainer.stop();
picoContainer.dispose();
picoContainer = null;
}
}
public boolean isShuttingDown() {
return shuttingDown;
}
}
/**
* $RCSfile $
* $Revision $
* $Date $
*
* Copyright (C) 2008 Jive Software. All rights reserved.
*
* This software is published under the terms of the GNU Public License (GPL),
* a copy of which is included in this distribution, or a commercial license
* agreement with Jive.
*/
package org.jivesoftware.openfire.reporting;
import org.jivesoftware.util.Log;
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;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.io.IOException;
/**
* Use the EnterpriseDWR servlet to register your own DWR mappings to Enteprise.
*/
public class MonitoringDWR extends DWRServlet {
private Document document;
public void configure(ServletConfig servletConfig, Configuration configuration) throws ServletException {
try {
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = dbf.newDocumentBuilder();
document = builder.newDocument();
Element root = document.createElement("dwr");
document.appendChild(root);
Element allowElement = document.createElement("allow");
// Build stats bean
Element createElement = buildCreator("Stats", org.jivesoftware.openfire.reporting.stats.StatsAction.class.getName());
Element convertConversationElement = document.createElement("convert");
convertConversationElement.setAttribute("converter", "bean");
convertConversationElement.setAttribute("match", org.jivesoftware.openfire.archive.ConversationInfo.class.getName());
// Build conversation Element.
Element conversationElement = buildCreator("conversations", org.jivesoftware.openfire.archive.ConversationUtils.class.getName());
allowElement.appendChild(createElement);
allowElement.appendChild(convertConversationElement);
allowElement.appendChild(conversationElement);
root.appendChild(allowElement);
}
catch (ParserConfigurationException e) {
Log.error("error creating DWR configuration: " + e);
}
configuration.addConfig(document);
// Specify the path for the Stat.js file
Object bean = container.getBean("interface");
if (bean instanceof DefaultInterfaceProcessor) {
DefaultInterfaceProcessor processor = (DefaultInterfaceProcessor)bean;
processor.setOverridePath("/plugins/monitoring/dwr");
}
}
/**
* Builds a create element within the DWR servlet.
* @param javascriptID the javascript variable name to use.
* @param qualifiedClassName the fully qualified class name.
* @return the Element.
*/
private Element buildCreator(String javascriptID, String qualifiedClassName) {
Element element = document.createElement("create");
element.setAttribute("creator", "new");
element.setAttribute("javascript", javascriptID);
Element parameter = document.createElement("param");
parameter.setAttribute("name", "class");
parameter.setAttribute("value", qualifiedClassName);
element.appendChild(parameter);
return element;
}
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);
}
public String getPathInfo() {
String pathInfo = super.getPathInfo();
return pathInfo.replaceAll("/monitoring/dwr", "");
}
}
}
/**
* $RCSfile $
* $Revision $
* $Date $
*
* Copyright (C) 2008 Jive Software. All rights reserved.
*
* This software is published under the terms of the GNU Public License (GPL),
* a copy of which is included in this distribution, or a commercial license
* agreement with Jive.
*/
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.
*
* This software is published under the terms of the GNU Public License (GPL),
* a copy of which is included in this distribution, or a commercial license
* agreement with Jive.
*/
package org.jivesoftware.openfire.reporting.graph;
import org.jivesoftware.openfire.plugin.MonitoringPlugin;
import org.jivesoftware.openfire.reporting.stats.StatsViewer;
import com.lowagie.text.*;
import com.lowagie.text.Image;
import com.lowagie.text.pdf.*;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import java.io.IOException;
import java.io.ByteArrayOutputStream;
import java.util.*;
import java.util.List;
import java.awt.geom.Rectangle2D;
import java.awt.*;
import java.awt.Font;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.stats.Statistic;
import org.jivesoftware.util.Log;
import org.jivesoftware.util.ParamUtils;
import org.jivesoftware.util.JiveGlobals;
import org.jfree.chart.JFreeChart;
/**
*
*/
public class GraphServlet extends HttpServlet {
private GraphEngine graphEngine;
private StatsViewer statsViewer;
public void init() throws ServletException {
// load dependencies
MonitoringPlugin plugin =
(MonitoringPlugin) XMPPServer.getInstance().getPluginManager().getPlugin("monitoring");
this.graphEngine = (GraphEngine) plugin.getModule(GraphEngine.class);
this.statsViewer = (StatsViewer)plugin.getModule(StatsViewer.class);
}
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;
}
public void onEndPage(PdfWriter writer, Document document) {
PdfContentByte cb = writer.getDirectContent();
try {
cb.setColorStroke(new Color(156,156,156));
cb.setLineWidth(2);
cb.moveTo(document.leftMargin(), document.bottomMargin() + 32);
cb.lineTo(document.getPageSize().width() - document.rightMargin(), document.bottomMargin() + 32);
cb.stroke();
Image gif = Image.getInstance("http://" + request.getServerName() +
":" + request.getServerPort() + "/plugins/monitoring/images/pdf_generatedbyof.gif");
cb.addImage(gif, 221, 0, 0, 28, (int)document.leftMargin(), (int)document.bottomMargin());
} catch (Exception e) {
Log.error("error drawing PDF footer: " + e.getMessage());
}
cb.saveState();
}
}
}
/**
* $RCSfile $
* $Revision $
* $Date $
*
* Copyright (C) 2008 Jive Software. All rights reserved.
*
* This software is published under the terms of the GNU Public License (GPL),
* a copy of which is included in this distribution, or a commercial license
* agreement with Jive.
*/
package org.jivesoftware.openfire.reporting.stats;
import org.jivesoftware.openfire.cluster.ClusterManager;
import org.jivesoftware.openfire.stats.Statistic;
import org.jivesoftware.util.Log;
import org.jrobin.core.RrdDb;
public class DefaultStatsViewer implements StatsViewer {
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.
*
* This software is published under the terms of the GNU Public License (GPL),
* a copy of which is included in this distribution, or a commercial license
* agreement with Jive.
*/
package org.jivesoftware.openfire.reporting.stats;
import org.jivesoftware.openfire.stats.Statistic;
import org.jivesoftware.openfire.stats.StatisticsManager;
import org.jivesoftware.util.Log;
import org.jivesoftware.util.cache.ClusterTask;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.util.HashMap;
import java.util.Map;
/**
* 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 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.
*
* This software is published under the terms of the GNU Public License (GPL),
* a copy of which is included in this distribution, or a commercial license
* agreement with Jive.
*/
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.
*
* This software is published under the terms of the GNU Public License (GPL),
* a copy of which is included in this distribution, or a commercial license
* agreement with Jive.
*/
package org.jivesoftware.openfire.reporting.stats;
import org.jivesoftware.database.DbConnectionManager;
import org.jivesoftware.util.Log;
import org.jrobin.core.RrdBackend;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
public class RrdSqlBackend extends RrdBackend {
// 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
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
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
public long getLength() throws IOException {
return buffer.length;
}
// provides enough space in memory for the RRD
protected void setLength(long length) {
buffer = new byte[(int) length];
}
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.
*
* This software is published under the terms of the GNU Public License (GPL),
* a copy of which is included in this distribution, or a commercial license
* agreement with Jive.
*/
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
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
protected boolean exists(String path) throws IOException {
return RrdSqlBackend.exists(path);
}
// returns factory name
public String getFactoryName() {
return NAME;
}
}
\ No newline at end of file
/**
* $RCSfile $
* $Revision $
* $Date $
*
* Copyright (C) 2008 Jive Software. All rights reserved.
*
* This software is published under the terms of the GNU Public License (GPL),
* a copy of which is included in this distribution, or a commercial license
* agreement with Jive.
*/
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.
*
* This software is published under the terms of the GNU Public License (GPL),
* a copy of which is included in this distribution, or a commercial license
* agreement with Jive.
*/
package org.jivesoftware.openfire.reporting.stats;
import org.jivesoftware.openfire.SessionManager;
import org.jivesoftware.openfire.interceptor.InterceptorManager;
import org.jivesoftware.openfire.interceptor.PacketInterceptor;
import org.jivesoftware.openfire.session.Session;
import org.jivesoftware.openfire.stats.Statistic;
import org.jivesoftware.openfire.stats.StatisticsManager;
import org.jivesoftware.openfire.stats.i18nStatistic;
import org.picocontainer.Startable;
import org.xmpp.packet.Packet;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Creates and manages Enteprise-specific statistics, specifically: <ul>
* <li>Incoming and outgoing packet traffic.
* <li>Server to server connections.
* <li>Active group chat rooms.
* <li>Active user sessions.
* </ul>
*
* @author Derek DeMoro
*/
public class StatisticsModule implements Startable {
public static final String MUC_ROOMS_KEY = "active_group_chats";
public static final String SERVER_2_SERVER_SESSIONS_KEY = "server_sessions";
public static final String SESSIONS_KEY = "sessions";
public static final String TRAFFIC_KEY = "packet_count";
private StatisticsManager statisticsManager;
private AtomicInteger packetCount = new AtomicInteger();
private PacketInterceptor packetInterceptor;
public void start() {
// Retrieve instance of StatisticsManager
statisticsManager = StatisticsManager.getInstance();
// Register a packet listener so that we can track packet traffic.
packetInterceptor = new PacketInterceptor() {
public void interceptPacket(Packet packet, Session session, boolean incoming,
boolean processed)
{
// Only track processed packets so that we don't count them twice.
if (processed) {
packetCount.incrementAndGet();
}
}
};
InterceptorManager.getInstance().addInterceptor(packetInterceptor);
// Register all statistics.
addServerToServerStatistic();
addActiveSessionsStatistic();
addPacketStatistic();
}
/**
* Remove all registered statistics.
*/
public void stop() {
// Remove Server to Server Statistic
statisticsManager.removeStatistic(SERVER_2_SERVER_SESSIONS_KEY);
// Remove Active Session Statistic
statisticsManager.removeStatistic(SESSIONS_KEY);
// Remove Packet Traffic Statistic
statisticsManager.removeStatistic(TRAFFIC_KEY);
statisticsManager = null;
// Remove the packet listener.
InterceptorManager.getInstance().removeInterceptor(packetInterceptor);
packetInterceptor = null;
packetCount = null;
}
/**
* Tracks the number of Server To Server connections taking place in the server at anyone time.
* This includes both incoming and outgoing connections.
*/
private void addServerToServerStatistic() {
// Register a statistic.
Statistic serverToServerStatistic = new i18nStatistic(SERVER_2_SERVER_SESSIONS_KEY, "monitoring",
Statistic.Type.count)
{
public double sample() {
return (SessionManager.getInstance().getIncomingServers().size() + SessionManager.
getInstance().getOutgoingServers().size());
}
public boolean isPartialSample() {
return false;
}
};
// Add to StatisticsManager
statisticsManager.addStatistic(SERVER_2_SERVER_SESSIONS_KEY, serverToServerStatistic);
}
/**
* Tracks the number of Active Sessions with the server at any point in time.
* Active Sessions are defined as one client connection.
*/
private void addActiveSessionsStatistic() {
// Register a statistic.
Statistic activeSessionStatistic = new i18nStatistic(SESSIONS_KEY, "monitoring", Statistic.Type.count) {
public double sample() {
return SessionManager.getInstance().getUserSessionsCount(false);
}
public boolean isPartialSample() {
return false;
}
};
statisticsManager.addStatistic(SESSIONS_KEY, activeSessionStatistic);
}
/**
* Tracks the total number of packets both incoming and outgoing in the server.
*/
private void addPacketStatistic() {
// Register a statistic.
Statistic packetTrafficStatistic = new i18nStatistic(TRAFFIC_KEY, "monitoring", Statistic.Type.rate) {
public double sample() {
return packetCount.getAndSet(0);
}
public boolean isPartialSample() {
return true;
}
};
statisticsManager.addStatistic(TRAFFIC_KEY, packetTrafficStatistic);
}
}
\ No newline at end of file
/**
* $RCSfile $
* $Revision $
* $Date $
*
* Copyright (C) 2008 Jive Software. All rights reserved.
*
* This software is published under the terms of the GNU Public License (GPL),
* a copy of which is included in this distribution, or a commercial license
* agreement with Jive.
*/
package org.jivesoftware.openfire.reporting.stats;
import org.jivesoftware.openfire.plugin.MonitoringPlugin;
import org.jivesoftware.openfire.archive.Conversation;
import org.jivesoftware.openfire.archive.ConversationManager;
import org.jivesoftware.openfire.reporting.graph.GraphEngine;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.stats.Statistic;
import org.jivesoftware.openfire.user.UserNameManager;
import org.jivesoftware.openfire.user.UserNotFoundException;
import org.jivesoftware.util.JiveGlobals;
import org.jivesoftware.util.LocaleUtils;
import org.jivesoftware.util.Log;
import org.jivesoftware.util.StringUtils;
import org.xmpp.packet.JID;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.text.DateFormat;
import java.text.NumberFormat;
import java.util.*;
/**
* Provides the server side callbacks for client side JavaScript functions for
* the stats dashboard page.
*
* @author Aaron Johnson
*/
public class StatsAction {
/**
* Retrieves a map containing the high / low and current count statistics
* for the 'sessions', 'conversations' and 'packet_count' statistics.
* @return map containing 3 maps (keys = 'sessions, 'conversations' and
* 'packet_count') each containing an array of int (low value, high value
* and current value).
*/
public Map<String, Map> getUpdatedStats(String timePeriod) {
Map<String, Map> results = new HashMap<String, Map>();
long[] startAndEnd = GraphEngine.parseTimePeriod(timePeriod);
String[] stats = new String[] {"sessions", "conversations", "packet_count",
"proxyTransferRate", "muc_rooms", "server_sessions", "server_bytes"};
for (String stat : stats) {
results.put(stat, getUpdatedStat(stat, startAndEnd));
}
return results;
}
/**
* Retrieve a a single stat update given a stat name and the name of a
* time period.
* @param statkey
* @param timePeriod
* @return map containing keys 'low', 'high' and 'count'.
*/
public Map getUpdatedStat(String statkey, String timePeriod) {
long[] startAndEnd = GraphEngine.parseTimePeriod(timePeriod);
return getUpdatedStat(statkey, startAndEnd);
}
private Map getUpdatedStat(String statkey, long[] timePeriod) {
MonitoringPlugin plugin = (MonitoringPlugin)XMPPServer.getInstance().getPluginManager().getPlugin("monitoring");
StatsViewer viewer = (StatsViewer)plugin.getModule(StatsViewer.class);
String[] lowHigh = getLowAndHigh(statkey, timePeriod);
Map stat = new HashMap();
stat.put("low", lowHigh[0]);
stat.put("high", lowHigh[1]);
stat.put("count", (int)viewer.getCurrentValue(statkey)[0]);
return stat;
}
/**
* Retrieves the last n conversations from the system that were created after
* the given conversationID.
*
* @param count the count of conversations to return.
* @param mostRecentConversationID the last conversationID that has been retrieved.
* @return a List of Map objects.
*/
public List getNLatestConversations(int count, long mostRecentConversationID) {
// TODO Fix plugin name 2 lines below and missing classes
List<Map> cons = new ArrayList<Map>();
MonitoringPlugin plugin = (MonitoringPlugin)XMPPServer.getInstance().getPluginManager().getPlugin("monitoring");
ConversationManager conversationManager = (ConversationManager)plugin.getModule(ConversationManager.class);
Collection<Conversation> conversations = conversationManager.getConversations();
List<Conversation> lConversations = Arrays.asList(conversations.toArray(new Conversation[conversations.size()]));
Collections.sort(lConversations, conversationComparator);
int counter = 0;
for (Iterator<Conversation> i = lConversations.iterator(); i.hasNext() && counter < count;) {
Conversation con = i.next();
if (mostRecentConversationID == con.getConversationID()) {
break;
} else {
Map mCon = new HashMap();
mCon.put("conversationid", con.getConversationID());
String users[];
int usersIdx = 0;
if (con.getRoom() == null) {
users = new String[con.getParticipants().size()];
for (JID jid : con.getParticipants()) {
String identifier = jid.toBareJID();
try {
identifier = UserNameManager.getUserName(jid, jid.toBareJID());
}
catch (UserNotFoundException e) {
// Ignore
}
users[usersIdx++] = StringUtils.abbreviate(identifier, 20);
}
}
else {
users = new String[2];
users[0] = LocaleUtils.getLocalizedString("dashboard.group_conversation", "monitoring");
try {
users[1] = "(<i>" + LocaleUtils.getLocalizedString("muc.room.summary.room") +
": <a href='../../muc-room-occupants.jsp?roomName=" +
URLEncoder.encode(con.getRoom().getNode(), "UTF-8") + "'>" + con.getRoom().getNode() +
"</a></i>)";
} catch (UnsupportedEncodingException e) {
Log.error(e);
}
}
mCon.put("users", users);
mCon.put("lastactivity", formatTimeLong(con.getLastActivity()));
mCon.put("messages", con.getMessageCount());
cons.add(0, mCon);
counter++;
}
}
return cons;
}
/**
* Given a statistic key and a start date, end date and number of datapoints, returns
* a String[] containing the low and high values (in that order) for the given time period.
*
* @param key the name of the statistic to return high and low values for.
* @param timePeriod start date, end date and number of data points.
* @return low and high values for the given time period / number of datapoints
*/
public static String[] getLowAndHigh(String key, long[] timePeriod) {
MonitoringPlugin plugin = (MonitoringPlugin)XMPPServer.getInstance().getPluginManager().getPlugin("monitoring");
StatsViewer viewer = (StatsViewer)plugin.getModule(StatsViewer.class);
Statistic.Type type = viewer.getStatistic(key)[0].getStatType();
double[] lows = viewer.getMin(key, timePeriod[0], timePeriod[1], (int)timePeriod[2]);
double[] highs = viewer.getMax(key, timePeriod[0], timePeriod[1], (int)timePeriod[2]);
String low;
NumberFormat format = NumberFormat.getNumberInstance();
format.setMaximumFractionDigits(0);
if(lows.length > 0) {
if(type == Statistic.Type.count) {
low = String.valueOf((int) lows[0]);
}
else {
double l = lows[0];
if(Double.isNaN(l)) {
l = 0;
}
low = format.format(l);
}
}
else {
low = String.valueOf(0);
}
String high;
if(highs.length > 0) {
if(type == Statistic.Type.count) {
high = String.valueOf((int) highs[0]);
}
else {
double h= highs[0];
if(Double.isNaN(h)) {
h = 0;
}
high = format.format(h);
}
}
else {
high = String.valueOf(0);
}
return new String[]{low, high};
}
private Comparator<Conversation> conversationComparator = new Comparator<Conversation>() {
public int compare(Conversation conv1, Conversation conv2) {
return conv2.getLastActivity().compareTo(conv1.getLastActivity());
}
};
/**
* Formats a given time using the <code>DateFormat.MEDIUM</code>. In the 'en' locale, this
* should result in a time formatted like this: 4:59:23 PM. The seconds are necessary when
* displaying time in the conversation scroller.
* @param time
* @return string a date formatted using DateFormat.MEDIUM
*/
public static String formatTimeLong(Date time) {
DateFormat formatter = DateFormat.getTimeInstance(DateFormat.MEDIUM, JiveGlobals.getLocale());
formatter.setTimeZone(JiveGlobals.getTimeZone());
return formatter.format(time);
}
}
/**
* $Revision: 3034 $
* $Date: 2005-11-04 21:02:33 -0300 (Fri, 04 Nov 2005) $
*
* Copyright (C) 2008 Jive Software. All rights reserved.
*
* This software is published under the terms of the GNU Public License (GPL),
* a copy of which is included in this distribution, or a commercial license
* agreement with Jive.
*/
package org.jivesoftware.openfire.reporting.stats;
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.Log;
import org.jivesoftware.util.cache.CacheFactory;
import org.jrobin.core.*;
import org.picocontainer.Startable;
import java.io.File;
import java.io.IOException;
import java.util.*;
/**
* 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 int STAT_RESOULUTION = 60;
private final TaskEngine taskEngine;
private final StatisticsManager statsManager;
private final Map<String, StatDefinition> definitionMap = new HashMap<String, StatDefinition>();
private final Map<String, List<StatDefinition>> multiMap = new HashMap<String, List<StatDefinition>>();
private SampleTask samplingTask = new SampleTask();
/**
* The default constructor used by the plugin container.
*
* @param taskEngine Used to execute tasks.
*/
public StatsEngine(TaskEngine taskEngine) {
this.taskEngine = taskEngine;
statsManager = StatisticsManager.getInstance();
}
public void start() {
try {
// Set that RRD files will be stored in the database
RrdBackendFactory.registerAndSetAsDefaultFactory(new RrdSqlBackendFactory());
// After 10 milliseconds begin sampling in 60 second intervals. Note: We need to start
// asap so that the UI can access this info upon start up
taskEngine.scheduleAtFixedRate(samplingTask, 10, STAT_RESOULUTION * 1000L);
}
catch (RrdException e) {
Log.error("Error initializing RrdbPool.", e);
}
}
public void stop() {
// Clean-up sampling task
samplingTask.cancel();
}
private void checkDatabase(StatDefinition[] def) throws RrdException, IOException {
File directory = new File(getStatsDirectroy());
if (directory.exists()) {
// check if the rrd exists
File rrdFile = new File(getRrdFilePath(def[0].getDbPath()));
if (rrdFile.exists() && rrdFile.canRead()) {
try {
// Import existing RRD file into the DB
RrdSqlBackend.importRRD(def[0].getDbPath(), rrdFile);
// Delete the RRD file
rrdFile.delete();
} catch (IOException e) {
Log.error("Error importing rrd file: " + rrdFile, e);
}
}
}
// check if the rrd exists
if (!RrdSqlBackend.exists(def[0].getDbPath())) {
RrdDb db = null;
try {
RrdDef rrdDef = new RrdDef(def[0].getDbPath(), STAT_RESOULUTION);
for (StatDefinition stat : def) {
String dsType = determineDsType(stat.getStatistic().getStatType());
rrdDef.addDatasource(stat.getDatasourceName(), dsType, 5 * STAT_RESOULUTION, 0,
Double.NaN);
}
// Every minute for 1 hour.
rrdDef.addArchive(((DefaultStatDefinition) def[0]).
consolidationFunction, 0.5, 1, 60);
// Every half-hour for 1 day.
rrdDef.addArchive(ConsolFuns.CF_AVERAGE, 0.5, 30, 48);
// Every day for 5 years.
rrdDef.addArchive(ConsolFuns.CF_AVERAGE, 0.5, 1440, 1825);
// Every week for 5 years.
rrdDef.addArchive(ConsolFuns.CF_AVERAGE, 0.5, 10080, 260);
// Every month for 5 years.
rrdDef.addArchive(ConsolFuns.CF_AVERAGE, 0.5, 43200, 60);
db = new RrdDb(rrdDef);
}
finally {
if(db != null) {
db.close();
}
}
}
}
private String determineDsType(Statistic.Type statType) {
return DsTypes.DT_GAUGE;
}
/**
* Returns the path to the RRD file.
*
* @param datasourceName the name of the data source.
* @return the path to the RRD file.
*/
private String getRrdFilePath(String datasourceName) {
return getStatsDirectroy() + datasourceName + ".rrd";
}
/**
* Returns the directory in which all of the stat databases will be stored.
*
* @return Returns the directory in which all of the stat databases will be stored.
*/
private String getStatsDirectroy() {
return JiveGlobals.getHomeDirectory() + File.separator + "monitoring"
+ File.separator + "stats" + File.separator;
}
private StatDefinition createDefintion(String key) {
StatDefinition def = definitionMap.get(key);
if (def == null) {
Statistic statistic = statsManager.getStatistic(key);
String statGroup = statsManager.getMultistatGroup(key);
try {
def = new DefaultStatDefinition(statGroup != null ? statGroup : key, key, statistic);
// If the definition is a part of a group check to see all defiintions have been
// made for that group
StatDefinition[] definitions;
if (statGroup != null) {
definitions = checkAndCreateGroup(statGroup, def, true);
}
else {
definitions = new StatDefinition[]{def};
multiMap.put(key, Arrays.asList(definitions));
}
if (definitions != null) {
checkDatabase(definitions);
}
definitionMap.put(key, def);
}
catch (RrdException e) {
Log.error("Error creating database definition", e);
}
catch (IOException e) {
Log.error("Error creating database definition", e);
}
}
return def;
}
/**
* Checks to see that all StatDefinitions for a stat group have been created. If they have
* then an array of the StatDefinitions will be returned, if they haven't Null will be returned.
* <p>
* The purpose of this is to know when a database should be initialized, after all the StatDefinitions
* have been created.
*
* @param statGroup The statGroup being checked
* @param def The statdefinition that is being added to the statGroup
* @return Null if the statgroup is completely defined and an array of statdefinitions if it is.
*/
private StatDefinition[] checkAndCreateGroup(String statGroup, StatDefinition def,
boolean shouldCreate)
{
List<StatDefinition> statList = multiMap.get(statGroup);
if (shouldCreate && statList == null) {
statList = new ArrayList<StatDefinition>();
multiMap.put(statGroup, statList);
}
if (statList == null) {
return null;
}
if (shouldCreate) {
statList.add(def);
}
StatDefinition[] definitions;
if (statsManager.getStatGroup(statGroup).size() == statList.size()) {
definitions = statList.toArray(new StatDefinition[statList.size()]);
}
else {
definitions = null;
}
return definitions;
}
/**
* Returns the last minute that passed in seconds since the epoch.
*
* @return the last minute that passed in seconds since the epoch.
*/
private static long getLastMinute() {
Calendar calendar = Calendar.getInstance();
calendar.setTimeInMillis(System.currentTimeMillis());
calendar.set(Calendar.SECOND, 0);
return calendar.getTimeInMillis() / 1000;
}
/**
* Returns the definition or definitions related to a statkey. There can be multiple
* definitions if a stat is a multistat.
*
* @param statKey The key for which the definition is desired.
* @return Returns the definition or definitions related to a statkey. There can be multiple
* definitions if a stat is a multistat.
*/
StatDefinition[] getDefinition(String statKey) {
List<StatDefinition> defs = multiMap.get(statKey);
if (defs == null) {
StatDefinition def = definitionMap.get(statKey);
if (def != null) {
return new StatDefinition[] {def};
}
else {
return null;
}
}
else {
return defs.toArray(new StatDefinition[defs.size()]);
}
}
/**
* Returns any multistat group names and any stats that are not part of a multistat.
*
* @return Returns any multistat group names and any stats that are not part of a multistat.
*/
String [] getAllHighLevelNames() {
Set<String> keySet = multiMap.keySet();
return keySet.toArray(new String[keySet.size()]);
}
/**
* The task which samples statistics and persits them to the database.
*
* @author Alexander Wenckus
*/
private class SampleTask extends TimerTask {
private long lastSampleTime = 0;
public void run() {
if (!ClusterManager.isSeniorClusterMember()) {
// Do not create new samples of the statistics since we are not the senior cluster member
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;
}
}
public double[][] getData(long startTime, long endTime) {
return fetchData(consolidationFunction, startTime, endTime, -1);
}
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);
}
public long getLastSampleTime() {
return lastSampleTime;
}
public double getLastSample() {
return lastSample;
}
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);
}
public double[] getMin(long startTime, long endTime) {
return getMin(startTime, endTime, 1);
}
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 };
}
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.
*
* This software is published under the terms of the GNU Public License (GPL),
* a copy of which is included in this distribution, or a commercial license
* agreement with Jive.
*/
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)
*/
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.
*
* This software is published under the terms of the GNU Public License (GPL),
* a copy of which is included in this distribution, or a commercial license
* agreement with Jive.
*/
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;
}
public void run() {
executor.submit(task);
}
}
}
<?xml version='1.0' encoding='ISO-8859-1'?>
<!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" "http://java.sun.com/dtd/web-app_2_3.dtd">
<web-app>
<!-- Servlets -->
<servlet>
<servlet-name>GraphServlet</servlet-name>
<servlet-class>org.jivesoftware.openfire.reporting.graph.GraphServlet</servlet-class>
</servlet>
<servlet>
<servlet-name>dwr-invoker</servlet-name>
<display-name>DWR Servlet</display-name>
<description>Direct Web Remoter Servlet</description>
<servlet-class>org.jivesoftware.openfire.reporting.MonitoringDWR</servlet-class>
<init-param>
<param-name>logLevel</param-name>
<param-value>FATAL</param-value>
</init-param>
<init-param>
<param-name>skipDefaultConfig</param-name>
<param-value>true</param-value>
</init-param>
</servlet>
<servlet>
<servlet-name>ConversationServlet</servlet-name>
<servlet-class>org.jivesoftware.openfire.archive.ConversationPDFServlet</servlet-class>
</servlet>
<!-- Servlet mappings -->
<servlet-mapping>
<servlet-name>GraphServlet</servlet-name>
<url-pattern>/graph</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>ConversationServlet</servlet-name>
<url-pattern>/conversation</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>dwr-invoker</servlet-name>
<url-pattern>/dwr/*</url-pattern>
</servlet-mapping>
</web-app>
<%@ page import="org.jivesoftware.openfire.plugin.MonitoringPlugin" %>
<%@ page import="org.jivesoftware.openfire.archive.Conversation" %>
<%@ page import="org.jivesoftware.openfire.archive.ConversationManager" %>
<%@ page import="org.jivesoftware.openfire.archive.ConversationParticipation" %>
<%@ page import="org.jivesoftware.openfire.XMPPServer" %>
<%@ page import="org.jivesoftware.openfire.user.UserManager" %>
<%@ page import="org.jivesoftware.util.Log" %>
<%@ page import="org.jivesoftware.util.NotFoundException" %>
<%@ page import="org.jivesoftware.util.ParamUtils" %>
<%@ page import="org.xmpp.packet.JID" %>
<%@ page import="java.util.*" %>
<%@ taglib uri="http://java.sun.com/jstl/core_rt" prefix="c" %>
<%@ taglib uri="http://java.sun.com/jstl/fmt_rt" prefix="fmt" %>
<%
long conversationID = ParamUtils.getLongParameter(request, "conversationID", -1);
int start = ParamUtils.getIntParameter(request, "start", 0);
// Get handle on the Monitoring plugin
XMPPServer server = XMPPServer.getInstance();
UserManager userManager = server.getUserManager();
MonitoringPlugin plugin = (MonitoringPlugin) server.getPluginManager().getPlugin("monitoring");
ConversationManager conversationmanager = (ConversationManager) plugin.getModule(ConversationManager.class);
List<String[]> values = new ArrayList<String[]>();
JID room = null;
try {
Conversation conversation = conversationmanager.getConversation(conversationID);
List<JID> participants = new ArrayList<JID>(conversation.getParticipants());
for (JID user : participants) {
for (ConversationParticipation participation : conversation.getParticipations(user)) {
values.add(new String[]{participation.getNickname(), user.toString()});
}
}
Collections.sort(values, new Comparator<String[]>() {
public int compare(String[] o1, String[] o2) {
return o1[0].compareTo(o2[0]);
}
});
room = conversation.getRoom();
}
catch (NotFoundException e) {
Log.error("Conversation not found: " + conversationID, e);
}
// paginator vars
int range = 16;
int numPages = (int) Math.ceil((double) (values.size() / 2) / (double) (range / 2));
int curPage = (start / range) + 1;
%>
<html>
<head>
<meta name="decorator" content="none"/>
</head>
<body>
<script type="text/javascript" language="javascript" src="scripts/tooltips/domTT.js"></script>
<script type="text/javascript" language="javascript" src="scripts/tooltips/domLib.js"></script>
<style type="text/css">
#lightbox{
top: 20%;
margin-top: -20px;
}
.jive-testPanel {
display: block;
position: relative;
float: left;
margin: 0;
padding: 0;
border: 2px solid #666666;
background-color: #f8f7eb;
overflow: hidden;
z-index: 9997;
-moz-border-radius: 6px;
}
.jive-testPanel-content {
display: block;
float: left;
padding: 20px;
font-size: 8pt;
z-index: 9999;
}
.jive-testPanel-close a,
.jive-testPanel-close a:visited {
float: right;
color: #666;
padding: 2px 5px 2px 18px;
margin: 0;
font-size: 8pt;
background: transparent url(../../../images/setup_btn_closetestx.gif) no-repeat left;
background-position: 4;
border: 1px solid #ccc;
z-index: 9999;
}
.jive-testPanel-close a:hover {
background-color: #e9e8d9;
}
.jive-testPanel-content h2 {
font-size: 14pt;
color: #396b9c;
margin: 0 0 10px 0;
padding: 0;
}
.jive-testPanel-content h2 span {
font-size: 10pt;
color: #000;
}
.jive-testPanel-content h4 {
font-size: 12pt;
margin: 0 0 10px 0;
padding: 0;
}
.jive-testPanel-content h4.jive-testSuccess {
color: #1e7100;
}
.jive-testPanel-content h4.jive-testError {
color: #890000;
}
</style>
<!-- BEGIN connection settings test panel -->
<div class="jive-testPanel">
<div class="jive-testPanel-content">
<div align="right" class="jive-testPanel-close">
<a href="#" class="lbAction" rel="deactivate"><fmt:message key="archive.group_conversation.close" /></a>
</div>
<h2><fmt:message key="archive.group_conversation.participants.title"/></h2>
<p><fmt:message key="archive.group_conversation.participants.description">
<fmt:param value="<%= room != null ? "<b>"+room.getNode()+"</b>" : "" %>" />
</fmt:message>
</p>
<fmt:message key="archive.group_conversation.participants" />: <b><%= values.size() %></b>
<% if (numPages > 1) { %>
-- <fmt:message key="global.showing" /> <%= (start+1) %>-<%= (start+range) %>
<% } %>
<% if (numPages > 1) { %>
<p>
<fmt:message key="global.pages" />:
[
<% for (int i=0; i<numPages; i++) {
String sep = ((i+1)<numPages) ? " " : "";
boolean isCurrent = (i+1) == curPage;
%>
<a href="#" rel="deactivate" onclick="showOccupants('<%=conversationID%>', <%=(i*range)%>);return false;" class="<%= ((isCurrent) ? "jive-current" : "") %>"><%= (i+1) %></a><%= sep %>
<% } %>
]
<% } %>
<div class="jive-table">
<table cellpadding="0" cellspacing="0" border="0" width="100%">
<thead>
<tr>
<th colspan="2"><fmt:message key="archive.group_conversation.participants.participant" /></th>
</tr>
</thead>
<tbody>
<% if (!values.isEmpty()) {
int from = (curPage-1) * range;
int to = curPage * range;
// Check ranges
from = from > values.size() ? values.size() : from;
to = to > values.size() ? values.size() : to;
// Get subset of participants to display
values = values.subList(from, to);
for (Iterator<String[]> it = values.iterator(); it.hasNext();) {
String[] participation = it.next();
String nickname = participation[0];
JID participant = new JID(participation[1]);
%>
<tr>
<td><%=nickname%> <i>(<%= server.isLocal(participant) && userManager.isRegisteredUser(participant) ? "<a href='/user-properties.jsp?username=" + participant.getNode() + "'>" + participant.toBareJID() + "</a>" : participant.toBareJID() %>)</i></td>
<% if (it.hasNext()) {
participation = it.next();
nickname = participation[0];
participant = new JID(participation[1]);
%>
<td><%=nickname%> <i>(<%= server.isLocal(participant) && userManager.isRegisteredUser(participant) ? "<a href='/user-properties.jsp?username=" + participant.getNode() + "'>" + participant.toBareJID() + "</a>" : participant.toBareJID() %>)</i></td>
<% } else { %>
<td>&nbsp;</td>
<% } %>
</tr>
<% } } else { %>
<tr>
<td colspan="3"><fmt:message key="archive.group_conversation.participants.empty" /></td>
</tr>
<% } %>
</tbody>
</table>
</div>
</div>
</div>
</body>
</html>
<%@ page errorPage="/error.jsp" import="org.jivesoftware.openfire.plugin.MonitoringPlugin"%>
<%@ page import="org.jivesoftware.openfire.archive.ArchiveSearch" %>
<%@ page import="org.jivesoftware.openfire.archive.ArchiveSearcher" %>
<%@ page import="org.jivesoftware.openfire.archive.Conversation" %>
<%@ page import="org.jivesoftware.openfire.archive.ConversationManager" %>
<%@ page import="org.jivesoftware.openfire.XMPPServer" %>
<%@ page import="org.jivesoftware.openfire.user.UserManager" %>
<%@ page import="org.jivesoftware.openfire.user.UserNameManager" %>
<%@ page import="org.jivesoftware.openfire.user.UserNotFoundException" %>
<%@ page import="org.jivesoftware.util.*" %>
<%@ page import="org.xmpp.packet.JID" %>
<%@ page import="java.text.DateFormat"%>
<%@ page import="java.text.SimpleDateFormat" %>
<%@ page import="java.util.*" %>
<%@ taglib uri="http://java.sun.com/jstl/core_rt" prefix="c" %>
<%@ taglib uri="http://java.sun.com/jstl/fmt_rt" prefix="fmt" %>
<%
// Get handle on the Monitoring plugin
MonitoringPlugin plugin = (MonitoringPlugin) XMPPServer.getInstance().getPluginManager().getPlugin(
"monitoring");
ArchiveSearcher archiveSearcher = (ArchiveSearcher) plugin.getModule(
ArchiveSearcher.class);
ConversationManager conversationManager = (ConversationManager) plugin.getModule(
ConversationManager.class);
boolean submit = request.getParameter("submitForm") != null;
if (!submit) {
submit = request.getParameter("parseRange") != null;
}
String query = request.getParameter("keywords");
Collection<Conversation> conversations = null;
String participant1 = request.getParameter("participant1");
String participant2 = request.getParameter("participant2");
String startDate = request.getParameter("startDate");
String endDate = request.getParameter("endDate");
String anyText = LocaleUtils.getLocalizedString("archive.settings.any", "monitoring");
int start = 0;
int range = 15;
int numPages = 1;
int curPage = (start / range) + 1;
if (anyText.equals(participant1)) {
participant1 = null;
}
if (anyText.equals(participant2)) {
participant2 = null;
}
if (anyText.equals(startDate)) {
startDate = null;
}
if (anyText.equals(endDate)) {
endDate = null;
}
if (submit) {
UserManager userManager = UserManager.getInstance();
ArchiveSearch search = new ArchiveSearch();
JID participant1JID = null;
JID participant2JID = null;
String serverName = XMPPServer.getInstance().getServerInfo().getXMPPDomain();
if (participant1 != null && participant1.length() > 0) {
int position = participant1.lastIndexOf("@");
if (position > -1) {
String node = participant1.substring(0, position);
participant1JID = new JID(JID.escapeNode(node) + participant1.substring(position));
} else {
participant1JID = new JID(JID.escapeNode(participant1), serverName, null);
}
}
if (participant2 != null && participant2.length() > 0) {
int position = participant2.lastIndexOf("@");
if (position > -1) {
String node = participant2.substring(0, position);
participant2JID = new JID(JID.escapeNode(node) + participant2.substring(position));
} else {
participant2JID = new JID(JID.escapeNode(participant2), serverName, null);
}
}
if (startDate != null && startDate.length() > 0) {
DateFormat formatter = new SimpleDateFormat("MM/dd/yy");
try {
Date date = formatter.parse(startDate);
search.setDateRangeMin(date);
}
catch (Exception e) {
// TODO: mark as an error in the JSP instead of logging..
Log.error(e);
}
}
if (endDate != null && endDate.length() > 0) {
DateFormat formatter = new SimpleDateFormat("MM/dd/yy");
try {
Date date = formatter.parse(endDate);
// The user has chosen an end date and expects that any conversation
// that falls on that day will be included in the search results. For
// example, say the user choose 6/17/2006 as an end date. If a conversation
// occurs at 5:33 PM that day, it should be included in the results. In
// order to make this possible, we need to make the end date one millisecond
// before the next day starts.
date = new Date(date.getTime() + JiveConstants.DAY - 1);
search.setDateRangeMax(date);
}
catch (Exception e) {
// TODO: mark as an error in the JSP instead of logging..
Log.error(e);
}
}
if (query != null && query.length() > 0) {
search.setQueryString(query);
}
if (participant1JID != null && participant2JID != null) {
search.setParticipants(participant1JID, participant2JID);
} else if (participant1JID != null) {
search.setParticipants(participant1JID);
} else if (participant2JID != null) {
search.setParticipants(participant2JID);
}
start = ParamUtils.getIntParameter(request, "start", 0);
range = 15;
conversations = archiveSearcher.search(search);
numPages = (int) Math.ceil((double) conversations.size() / (double) range);
curPage = (start / range) + 1;
}
boolean isArchiveEnabled = conversationManager.isArchivingEnabled();
%>
<html>
<head>
<title><fmt:message key="archive.search.title" /></title>
<meta name="pageID" content="archive-search"/>
<script src="/js/prototype.js" type="text/javascript"></script>
<script src="/js/scriptaculous.js" type="text/javascript"></script>
<script src="dwr/engine.js" type="text/javascript"></script>
<script src="dwr/util.js" type="text/javascript"></script>
<script src="dwr/interface/conversations.js" type="text/javascript"></script>
<script type="text/javascript" language="javascript" src="scripts/tooltips/domLib.js"></script>
<script type="text/javascript" language="javascript" src="scripts/tooltips/domTT.js"></script>
<style type="text/css">@import url( /js/jscalendar/calendar-win2k-cold-1.css );</style>
<script type="text/javascript" src="/js/jscalendar/calendar.js"></script>
<script type="text/javascript" src="/js/jscalendar/i18n.jsp"></script>
<script type="text/javascript" src="/js/jscalendar/calendar-setup.js"></script>
<script type="text/javascript">
function hover(oRow) {
oRow.style.background = "#A6CAF0";
oRow.style.cursor = "pointer";
}
function noHover(oRow) {
oRow.style.background = "white";
}
function viewConversation(conversationID) {
window.frames['view'].location.href = "conversation-viewer.jsp?conversationID=" + conversationID;
}
function submitFormAgain(start, range){
document.f.start.value = start;
document.f.range.value = range;
document.f.parseRange.value = "true";
document.f.submit();
}
</script>
<style type="text/css">
.small-label {
font-size: 11px;
font-weight: bold;
font-family: Verdana, Arial, sans-serif;
}
.small-label-no-bold {
font-size: 11px;
font-family: Verdana, Arial, sans-serif;
}
.small-label-with-padding {
font-size: 12px;
font-weight: bold;
font-family: Verdana, Arial, sans-serif;
}
.small-text {
font-size: 11px;
font-family: Verdana, Arial, sans-serif;
line-height: 11px;
}
.very-small-label {
font-size: 10px;
font-weight: bold;
font-family: Verdana, Arial, sans-serif;
padding-right:5px;
}
.stat {
margin: 0px 0px 8px 0px;
border: 1px solid #cccccc;
-moz-border-radius: 3px;
}
.stat td table {
margin: 5px 10px 5px 10px;
}
.stat div.verticalrule {
display: block;
width: 1px;
height: 110px;
background-color: #cccccc;
overflow: hidden;
margin-left: 3px;
margin-right: 3px;
}
.conversation-body {
color: black;
font-size: 11px;
font-family: Verdana, Arial, sans-serif;
}
.conversation-label1 {
color: blue;
font-size: 10px;
font-family: Verdana, Arial, sans-serif;
}
.conversation-label2 {
color: red;
font-size: 10px;
font-family: Verdana, Arial, sans-serif;
}
.conversation-label3 {
color: orchid;
font-size: 10px;
font-family: Verdana, Arial, sans-serif;
}
.conversation-label4 {
color: black;
font-size: 10px;
font-family: Verdana, Arial, sans-serif;
}
.conversation-table {
font-family: Verdana, Arial, sans-serif;
font-size: 11px;
}
.conversation-table td {
font-size: 11px;
padding: 5px 5px 5px 5px;
}
.light-gray-border {
border-color: #bbb;
border-style: solid;
border-width: 1px 1px 1px 1px;
}
.light-gray-border-bottom {
border-color: #bbb;
border-style: solid;
border-width: 0px 0px 1px 0px;
}
.small-description {
font-size: 11px;
font-family: Verdana, Arial, sans-serif;
color: #666;
}
.description {
font-size: 12px;
font-family: Verdana, Arial, sans-serif;
color: #666;
}
.pagination {
border-color: #bbb;
border-style: solid;
border-width: 0px 0px 1px 0px;
font-size: 10px;
font-family: Verdana, Arial, sans-serif;
}
.content {
border-color: #bbb;
border-style: solid;
border-width: 0px 0px 1px 0px;
}
/* Default DOM Tooltip Style */
div.domTT {
border: 1px solid #bbb;
background-color: #FFFBE2;
font-family: Arial, Helvetica sans-serif;
font-size: 9px;
padding: 5px;
}
div.domTT .caption {
font-family: serif;
font-size: 12px;
font-weight: bold;
padding: 1px 2px;
color: #FFFFFF;
}
div.domTT .contents {
font-size: 12px;
font-family: sans-serif;
padding: 3px 2px;
}
.textfield {
font-size: 11px;
font-family: Verdana, Arial, sans-serif;
height: 20px;
background: #efefef;
}
.keyword-field {
font-size: 11px;
font-family: Verdana, Arial, sans-serif;
height: 20px;
}
#searchResults {
margin: 10px 0px 10px 0px;
}
#searchResults h3 {
font-size: 14px;
padding: 0px;
margin: 0px 0px 2px 0px;
color: #555555;
}
#searchResults p.resultDescription {
margin: 0px 0px 12px 0px;
}
</style>
<style type="text/css" title="setupStyle" media="screen">
@import "../../style/lightbox.css";
</style>
<script language="JavaScript" type="text/javascript" src="../../js/lightbox.js"></script>
<script type="text/javascript">
var selectedConversation;
function showConversation(conv) {
selectedConversation = conv;
conversations.getConversationInfo(showConv, conv, true);
}
function showConv(results) {
$('chat-viewer-empty').style.display = 'none';
$('chat-viewer').style.display = '';
if (results.allParticipants != null) {
$('con-participant1').innerHTML = results.allParticipants.length;
$('con-participant2').innerHTML = '(<a href="#" onclick="showOccupants(' + results.conversationID + ', 0);return false;">view</a>)';
}
else {
$('con-participant1').innerHTML = results.participant1 + ',';
$('con-participant2').innerHTML = results.participant2;
}
$('con-chatTime').innerHTML = results.date;
$('conversation-body').innerHTML = results.body;
$('con-noMessages').innerHTML = results.messageCount;
$('con-duration').innerHTML = results.duration;
<% if (conversationManager.isArchivingEnabled()) { %>
$('con-chat-link').innerHTML = '<a href="conversation?conversationID='+selectedConversation+'" class="very-small-label" style="text-decoration:none" target=_blank>View PDF</a>';
<% } else { %>
Element.hide('pdf-image');
<% } %>
}
function showOccupants(conversationID, start) {
var aref = document.getElementById('lbmessage');
aref.href = 'archive-conversation-participants.jsp?conversationID=' + conversationID + '&start=' + start;
var lbCont = document.getElementById('lbContent');
if (lbCont != null) {
document.getElementById('lightbox').removeChild(lbCont);
}
lb = new lightbox(aref);
lb.activate();
}
function grayOut(ele) {
if (ele.value == 'Any') {
ele.style.backgroundColor = "#FFFBE2";
}
else {
ele.style.backgroundColor = "#ffffff";
}
}
</script>
<script type="text/javascript" src="/js/behaviour.js"></script>
<script type="text/javascript">
// Add a nice little rollover effect to any row in a jive-table object. This will help
// visually link left and right columns.
var selectedElement;
var myrules = {
'.conversation-table TR' : function(el) {
var backgroundColor;
var selected = false;
el.onmouseover = function() {
if (selectedElement != null && selectedElement == this) {
return;
}
backgroundColor = this.style.backgroundColor;
this.style.backgroundColor = '#dedede';
this.style.cursor = 'pointer';
}
el.onmouseout = function() {
if (selectedElement != this) {
this.style.backgroundColor = backgroundColor;
}
}
el.onmousedown = function() {
this.style.backgroundColor = '#fffBc2';
if (selectedElement != null) {
selectedElement.style.backgroundColor = backgroundColor;
}
selectedElement = this;
}
}
};
var textfieldRules = {
'.textfield' : function(el) {
el.onblur = function() {
var va = el.value;
if (va.length == 0 || va == 'Any') {
this.style.backgroundColor = '#efefef';
el.value = "<%= anyText%>";
}
else {
this.style.backgroundColor = '#ffffff';
}
}
el.onfocus = function() {
var va = el.value;
if (va == 'Any') {
this.style.backgroundColor = '#ffffff';
el.value = "";
}
}
}
};
Behaviour.register(textfieldRules);
Behaviour.register(myrules);
</script>
<style type="text/css">
@import "style/style.css";
</style>
</head>
<body>
<a href="archive-conversation-participants.jsp?conversationID=" id="lbmessage" title="<fmt:message key="archive.group_conversation.participants" />" style="display:none;"></a>
<form action="archive-search.jsp" name="f">
<!-- Search Table -->
<div>
<table class="stat">
<tr valign="top">
<td>
<table>
<tr>
<td colspan="3">
<img src="images/icon_participants.gif" align="absmiddle" alt="" style="margin-right: 4px;"/>
<b><fmt:message key="archive.search.participants" /></b>
<a onmouseover="domTT_activate(this, event, 'content',
'<fmt:message key="archive.search.participants.tooltip"/>',
'trail', true, 'direction', 'northeast', 'width', '220');"><img src="images/icon_help_14x14.gif" alt="" vspace="2" align="texttop"/></a>
</td>
</tr>
<tr>
<td>
<input type="text" size="22" name="participant1" value="<%= participant1 != null ? participant1 :
LocaleUtils.getLocalizedString("archive.search.participants.any", "monitoring") %>" class="textfield"/>
</td>
</tr>
<tr>
<td>
<input type="text" size="22" name="participant2" value="<%= participant2 != null ? participant2 : anyText %>" class="textfield"/>
</td>
</tr>
</table>
</td>
<td width="0" height="100%" valign="middle">
<div class="verticalrule"></div>
</td>
<td>
<table>
<tr>
<td colspan="3">
<img src="images/icon_daterange.gif" align="absmiddle" alt="" style="margin: 0px 4px 0px 2px;"/>
<b><fmt:message key="archive.search.daterange" /></b>
<a onmouseover="domTT_activate(this, event, 'content',
'<fmt:message key="archive.search.daterange.tooltip"/>',
'trail', true, 'direction', 'northeast', 'width', '220');"><img src="images/icon_help_14x14.gif" vspace="2" align="texttop"/></a>
</td>
</tr>
<tr valign="top">
<td><fmt:message key="archive.search.daterange.start" /></td>
<td>
<input type="text" id="startDate" name="startDate" size="13"
value="<%= startDate != null ? startDate :
LocaleUtils.getLocalizedString("archive.search.daterange.any", "monitoring")%>" class="textfield"/><br/>
<span class="jive-description"><fmt:message key="archive.search.daterange.format" /></span>
</td>
<td>
<img src="images/icon_calendarpicker.gif" vspace="3" id="startDateTrigger">
</td>
</tr>
<tr valign="top">
<td><fmt:message key="archive.search.daterange.end" /></td>
<td>
<input type="text" id="endDate" name="endDate" size="13"
value="<%= endDate != null ? endDate :
LocaleUtils.getLocalizedString("archive.search.daterange.any", "monitoring") %>" class="textfield"/><br/>
<span class="jive-description"><fmt:message key="archive.search.daterange.format" /></span>
</td>
<td>
<img src="images/icon_calendarpicker.gif" vspace="3" id="endDateTrigger">
</td>
</tr>
</table>
</td>
<td>
<td width="0" height="100%" valign="middle">
<div class="verticalrule"></div>
</td>
</td>
<td>
<table>
<tr valign="top">
<td>
<img src="images/icon_keywords.gif" align="absmiddle" alt="" style="margin-right: 4px;"/>
<b><fmt:message key="archive.search.keywords" /></b> <fmt:message key="archive.search.keywords.optional" />
</td>
</tr>
<tr>
<td>
<% if(isArchiveEnabled){%>
<input type="text" name="keywords" size="35" class="keyword-field" value="<%= query != null ? query : ""%>"/>
<% } else { %>
<fmt:message key="archive.search.keywords.disabled">
<fmt:param value="<a href='archiving-settings.jsp'>" />
<fmt:param value="</a>" />
</fmt:message>
<% } %>
</td>
</tr>
</table>
</td>
</tr>
</table>
</div>
<input type="submit" name="submitForm" value="<fmt:message key="archive.search.submit" />" class="small-text"/>
<input type="hidden" name="start" />
<input type="hidden" name="range" />
<input type="hidden" name="parseRange" />
</form>
<%
// Code for the searches.
%>
<% if (conversations != null && conversations.size() > 0) { %>
<table id="searchResults" width="100%" style="<%= conversations == null ? "display:none;" : "" %>">
<tr>
<td colspan="2">
<h3><fmt:message key="archive.search.results" /> <%= conversations.size() %></h3>
<p class="resultDescription">
<fmt:message key="archive.search.results.description">
<fmt:param value="<%= conversations.size()%>" />
</fmt:message>
</p>
</td>
</tr>
<tr valign="top">
<td width="300">
<!-- Search Result Table -->
<table cellspacing="0" class="light-gray-border">
<tr class="light-gray-border-bottom">
<td class="light-gray-border-bottom">
<%
int endPoint = (start + range) > conversations.size() ? conversations.size() : (start + range);
%>
<span class="small-label-with-padding">
<%= start + 1%> - <%= endPoint %> <fmt:message key="archive.search.results.xofy" />
<%= conversations.size()%></span>
</td>
<td align="right" nowrap class="light-gray-border-bottom" style="padding-right:3px;">
<% if (numPages > 1) { %>
<p>
<% int num = 5 + curPage;
int s = curPage - 1;
if (s > 5) {
s -= 5;
}
if (s < 5) {
s = 0;
}
if (s > 2) {
%>
<a href="javascript:submitFormAgain('0', '<%= range%>');">1</a> ...
<%
}
int i = 0;
for (i = s; i < numPages && i < num; i++) {
String sep = ((i + 1) < numPages) ? " " : "";
boolean isCurrent = (i + 1) == curPage;
%>
<a href="javascript:submitFormAgain('<%= (i*range) %>', '<%= range %>');"
class="<%= ((isCurrent) ? "small-label" : "small-label-no-bold") %>"
><%= (i + 1) %></a><%= sep %>
<% } %>
<% if (i < numPages) { %>
... <a href="javascript:submitFormAgain('<%= ((numPages-1)*range) %>', '<%= range %>');"><%= numPages %></a>
<% } %>
</p>
<% } else { %>
&nbsp;
<% } %>
</td>
</tr>
<tr>
<td colspan="2" align="left">
<div style="HEIGHT:300px;width:285px;OVERFLOW:auto">
<table cellpadding="3" cellspacing="0" width="100%" class="conversation-table">
<%
int i = 1;
int end = start + range + 1;
for (Conversation conversation : conversations) {
if(i == end){
break;
}
else if(i < start){
i++;
continue;
}
Map<String, JID> participants = getParticipants(conversation);
String color = "#FFFFFF";
if (i % 2 == 0) {
color = "#F0F0F0";
}
%>
<tr id="<%= conversation.getConversationID()%>" valign="top" bgcolor="<%= color%>" onclick="showConversation('<%= conversation.getConversationID() %>'); return false;">
<td><b><%= i %>.</b></td>
<td width="98%">
<% if (conversation.getRoom() == null) { %>
<%
Iterator iter = participants.keySet().iterator();
while (iter.hasNext()) {
String name = (String)iter.next();
%>
<%= name%><br/>
<% } %>
<% } else { %>
<i><fmt:message key="archive.search.group_conversation">
<fmt:param value="<%= conversation.getRoom().getNode() %>" />
</fmt:message></i><br>
<fmt:message key="archive.search.results.participants" /> <%= conversation.getParticipants().size() %>
<% } %>
</td>
<td align="right" nowrap>
<%= getFormattedDate(conversation)%>
</td>
</tr>
<% i++;
} %>
</table>
</div>
</td>
</tr>
</table>
</td>
<td>
<!-- Conversation Viewer (empty) -->
<div id="chat-viewer-empty">
<table class="light-gray-border" width="100%" style="height: 323px;">
<tr>
<td align="center" valign="top" bgcolor="#fafafa">
<br>
<p>Select a conversation to the left to view details.</p></td>
</tr>
</table>
</div>
<!-- Conversation Viewer -->
<div id="chat-viewer" style="display:none;">
<table class="light-gray-border" cellspacing="0">
<tr valign="top">
<td width="99%" bgcolor="#f0f0f0" class="light-gray-border-bottom" style="padding: 3px 2px 4px 5px;">
<span class="small-label"><fmt:message key="archive.search.results.participants" /></span>&nbsp;
<span class="small-text" id="con-participant1"></span>&nbsp;
<span class="small-text" id="con-participant2"></span><br/>
<span class="small-label"><fmt:message key="archive.search.results.messagecount" /></span>&nbsp;
<span class="small-text" id="con-noMessages"></span><br/>
<span class="small-label"><fmt:message key="archive.search.results.date" /></span>&nbsp;
<span class="small-text" id="con-chatTime"></span><br/>
<span class="small-label"><fmt:message key="archive.search.results.duration" /></span>&nbsp;
<span class="small-text" id="con-duration"></span>
</td>
<td id="pdf-image" width="1%" bgcolor="#f0f0f0" nowrap align="right" class="light-gray-border-bottom" style="padding: 4px 3px 3px 0px;">
<img src="images/icon_pdf.gif" alt="" align="texttop" border="0" /> <span id="con-chat-link"></span>
</td>
</tr>
<tr>
<td colspan="2">
<div class="conversation" id="conversation-body" style="HEIGHT:241px;width:100%;OVERFLOW:auto">
</div>
</td>
</tr>
</table>
</div>
</td>
</tr>
</table>
<% } else if(submit) { %>
<span class="description">
<fmt:message key="archive.search.results.none" />
</span>
<% } %>
<script type="text/javascript">
grayOut(f.participant1);
grayOut(f.participant2);
grayOut(f.startDate);
grayOut(f.endDate);
function catcalc(cal) {
var endDateField = $('endDate');
var startDateField = $('startDate');
var endTime = new Date(endDateField.value);
var startTime = new Date(startDateField.value);
if(endTime.getTime() < startTime.getTime()){
alert("<fmt:message key="archive.search.daterange.error" />");
startDateField.value = "<fmt:message key="archive.search.daterange.any" />";
}
}
Calendar.setup(
{
inputField : "startDate", // ID of the input field
ifFormat : "%m/%d/%y", // the date format
button : "startDateTrigger", // ID of the button
onUpdate : catcalc
});
Calendar.setup(
{
inputField : "endDate", // ID of the input field
ifFormat : "%m/%d/%y", // the date format
button : "endDateTrigger", // ID of the button
onUpdate : catcalc
});
</script>
</body>
</html>
<%!
public TreeMap<String, JID> getParticipants(Conversation conv) {
final TreeMap<String, JID> participants = new TreeMap<String, JID>();
for (JID jid : conv.getParticipants()) {
try {
if (jid == null) {
continue;
}
String identifier = jid.toBareJID();
try {
identifier = UserNameManager.getUserName(jid, jid.toBareJID());
} catch (UserNotFoundException e) {
// Ignore
}
participants.put(identifier, jid);
}
catch (Exception e) {
Log.error(e);
}
}
return participants;
}
public String getFormattedDate(Conversation conv) {
return JiveGlobals.formatDate(conv.getStartDate());
}
%>
\ No newline at end of file
<%@ page errorPage="/error.jsp" import="org.jivesoftware.openfire.plugin.MonitoringPlugin"
%>
<%@ page import="org.jivesoftware.openfire.archive.ArchiveIndexer" %>
<%@ page import="org.jivesoftware.openfire.archive.ConversationManager, org.jivesoftware.util.ByteFormat, org.jivesoftware.util.ParamUtils" %>
<%@ page import="org.jivesoftware.openfire.XMPPServer" %>
<%@ page import="org.jivesoftware.util.StringUtils" %>
<%@ page import="java.util.HashMap" %>
<%@ page import="java.util.Map" %>
<%@ taglib uri="http://java.sun.com/jstl/core_rt" prefix="c" %>
<%@ taglib uri="http://java.sun.com/jstl/fmt_rt" prefix="fmt" %>
<%
// Get handle on the Monitoring plugin
MonitoringPlugin plugin = (MonitoringPlugin) XMPPServer.getInstance().getPluginManager().getPlugin("monitoring");
ConversationManager conversationManager = (ConversationManager) plugin.getModule(
ConversationManager.class);
ArchiveIndexer archiveIndexer = (ArchiveIndexer) plugin.getModule(ArchiveIndexer.class);
ByteFormat byteFormatter = new ByteFormat();
String indexSize = byteFormatter.format(archiveIndexer.getIndexSize());
%>
<html>
<head>
<title><fmt:message key="archive.settings.title"/></title>
<meta name="pageID" content="archiving-settings"/>
<link rel="stylesheet" type="text/css" href="style/global.css">
<script src="dwr/engine.js" type="text/javascript"></script>
<script src="dwr/util.js" type="text/javascript"></script>
<script src="dwr/interface/conversations.js" type="text/javascript"></script>
<link rel="stylesheet" type="text/css" href="style/style.css">
<script type="text/javascript">
// Calls a getBuildProgress
function getBuildProgress() {
conversations.getBuildProgress(showBuildProgress);
}
function showBuildProgress(progress) {
var rebuildElement = document.getElementById("rebuildElement");
if (progress != null && progress != -1){
// Update progress item.
rebuildElement.style.display = '';
var rebuildProgress = document.getElementById('rebuildProgress');
rebuildProgress.innerHTML = progress;
setTimeout("getBuildProgress()", 1000);
}
else {
var rebuildProgress = document.getElementById('rebuildProgress');
rebuildProgress.innerHTML = "100";
Effect.Fade('rebuildElement');
}
}
</script>
<style type="text/css">
.small-label {
font-size: 11px;
font-weight: bold;
font-family: verdana;
}
.small-text {
font-size: 11px;
font-family: verdana;
}
.stat {
border: 1px;
border-color: #ccc;
border-style: dotted;
}
.conversation-body {
color: black;
font-size: 11px;
font-family: verdana;
}
.conversation-label1 {
color: blue;
font-size: 11px;
font-family: verdana;
}
.conversation-label2 {
color: red;
font-size: 11px;
font-family: verdana;
}
.conversation-table {
font-family: verdana;
font-size: 12px;
}
.light-gray-border {
border-color: #bbb;
border-style: solid;
border-width: 1px 1px 1px 1px;
}
.light-gray-border-bottom {
border-color: #bbb;
border-style: solid;
border-width: 0px 0px 1px 0px;
}
.content {
border-color: #bbb;
border-style: solid;
border-width: 0px 0px 1px 0px;
}
/* Default DOM Tooltip Style */
div.domTT {
border: 1px solid #bbb;
background-color: #F9F5D5;
font-family: arial;
font-size: 9px;
padding: 5px;
}
div.domTT .caption {
font-family: serif;
font-size: 12px;
font-weight: bold;
padding: 1px 2px;
color: #FFFFFF;
}
div.domTT .contents {
font-size: 12px;
font-family: sans-serif;
padding: 3px 2px;
}
.textfield {
font-size: 11px;
font-family: verdana;
padding: 3px 2px;
background: #efefef;
}
.keyword-field {
font-size: 11px;
font-family: verdana;
padding: 3px 2px;
}
</style>
<style type="text/css">
@import "style/style.css";
</style>
</head>
<body>
<% // Get parameters
boolean update = request.getParameter("update") != null;
boolean messageArchiving = conversationManager.isMessageArchivingEnabled();
boolean roomArchiving = conversationManager.isRoomArchivingEnabled();
int idleTime = ParamUtils.getIntParameter(request, "idleTime", conversationManager.getIdleTime());
int maxTime = ParamUtils.getIntParameter(request, "maxTime", conversationManager.getMaxTime());
boolean rebuildIndex = request.getParameter("rebuild") != null;
if (request.getParameter("cancel") != null) {
response.sendRedirect("archiving-settings.jsp");
return;
}
if (rebuildIndex) {
archiveIndexer.rebuildIndex();
}
// Update the session kick policy if requested
Map errors = new HashMap();
String errorMessage = "";
if (update) {
// New settings for message archiving.
boolean metadataArchiving = request.getParameter("metadataArchiving") != null;
messageArchiving = request.getParameter("messageArchiving") != null;
roomArchiving = request.getParameter("roomArchiving") != null;
String roomsArchived = request.getParameter("roomsArchived");
// Validate params
if (idleTime < 1) {
errors.put("idleTime", "");
errorMessage = "Idle Time must be greater than 0.";
}
if (maxTime < 1) {
errors.put("maxTime", "");
errorMessage = "Max Time must be greater than 0.";
}
if (roomsArchived != null && roomsArchived.contains("@")) {
errors.put("roomsArchived", "");
errorMessage = "Only name of local rooms should be specified.";
}
// If no errors, continue:
if (errors.size() == 0) {
conversationManager.setMetadataArchivingEnabled(metadataArchiving);
conversationManager.setMessageArchivingEnabled(messageArchiving);
conversationManager.setRoomArchivingEnabled(roomArchiving);
conversationManager.setRoomsArchived(StringUtils.stringToCollection(roomsArchived));
conversationManager.setIdleTime(idleTime);
conversationManager.setMaxTime(maxTime);
%>
<div class="success">
<fmt:message key="archive.settings.success"/>
</div><br>
<%
}
}
%>
<%
if (rebuildIndex) {
%>
<div class="success">
<fmt:message key="archive.settings.rebuild.success"/>
</div><br/>
<script type="text/javascript">
getBuildProgress();
</script>
<% } %>
<% if (errors.size() > 0) { %>
<div class="error">
<%= errorMessage%>
</div>
<br/>
<% } %>
<p>
<fmt:message key="archive.settings.description"/>
</p>
<form action="archiving-settings.jsp" method="post">
<table class="settingsTable" cellpadding="3" cellspacing="0" border="0" width="90%">
<thead>
<tr>
<th colspan="3"><fmt:message key="archive.settings.message.metadata.title" /></th>
</tr>
</thead>
<tbody>
<tr>
<td colspan="3"><p><fmt:message key="archive.settings.message.metadata.description" /></p></td>
</tr>
<tr>
<td colspan="2" width="90%"><label class="jive-label" for="metadata"><fmt:message key="archive.settings.enable.metadata"/>:</label><br>
<fmt:message key="archive.settings.enable.metadata.description"/></td>
<td><input type="checkbox" id="metadata" name="metadataArchiving" <%= conversationManager.isMetadataArchivingEnabled() ? "checked" : "" %> /></td>
</tr>
<tr>
<td colspan="3"><label class="jive-label"><fmt:message key="archive.settings.enable.message"/>:</label><br>
<fmt:message key="archive.settings.enable.message.description"/><br>
<table width=70% align=right border="0" cellpadding="3" cellspacing="0">
<tr>
<td><fmt:message key="archive.settings.one_to_one"/></td>
<td><input type="checkbox" name="messageArchiving" <%= conversationManager.isMessageArchivingEnabled() ? "checked" : ""%> /></td>
</tr>
<tr>
<td><fmt:message key="archive.settings.group_chats"/></td>
<td><input type="checkbox" name="roomArchiving" <%= conversationManager.isRoomArchivingEnabled() ? "checked" : ""%> /></td>
</tr>
<tr>
<td><fmt:message key="archive.settings.certain_rooms"/></td>
<td><textarea name="roomsArchived" cols="30" rows="2" wrap="virtual"><%= StringUtils.collectionToString(conversationManager.getRoomsArchived()) %></textarea></td>
</tr>
</table>
</td>
</tr>
<tr>
<td><label class="jive-label"><fmt:message key="archive.settings.idle.time"/>:</label><br>
<fmt:message key="archive.settings.idle.time.description"/></td>
<td><input type="text" name="idleTime" size="10" maxlength="10" value="<%= conversationManager.getIdleTime()%>" /></td>
<td></td>
</tr>
<tr>
<td><label class="jive-label"><fmt:message key="archive.settings.max.time"/>:</label><br>
<fmt:message key="archive.settings.max.time.description"/><br><br></td>
<td><input type="text" name="maxTime" size="10" maxlength="10" value="<%= conversationManager.getMaxTime()%>" /></td>
<td></td>
</tr>
</tbody>
</table>
<input type="submit" name="update" value="<fmt:message key="archive.settings.update.settings" />">
<input type="submit" name="cancel" value="<fmt:message key="archive.settings.cancel" />">
<br>
<br>
<% if (messageArchiving || roomArchiving) { %>
<br>
<table class="settingsTable" cellpadding="3" cellspacing="0" border="0" width="90%">
<thead>
<tr>
<th colspan="3" width="100%"><fmt:message key="archive.settings.index.settings"/></th>
</tr>
</thead>
<tbody>
<tr>
<td colspan="3" width="100%"><p><fmt:message key="archive.settings.index.settings.description"/></p></td>
</tr>
<tr valign="top">
<td width="80%"><b><fmt:message key="archive.settings.current.index"/></b> - <fmt:message key="archive.settings.current.index.description"/></td>
<td><%= indexSize %></td>
<td></td>
</tr>
<tr valign="top">
<td><b><fmt:message key="archive.settings.message.count"/></b> - <fmt:message key="archive.settings.message.count.description"/></td>
<td><%= conversationManager.getArchivedMessageCount()%></td>
<td></td>
</tr>
<tr valign="top">
<td><b><fmt:message key="archive.settings.conversation.count"/></b> - <fmt:message key="archive.settings.conversation.count.description"/><br><br></td>
<td><%= conversationManager.getArchivedConversationCount()%></td>
<td></td>
</tr>
</tbody>
</table>
<input type="submit" name="rebuild" value="<fmt:message key="archive.settings.rebuild" />"/>
<span id="rebuildElement" style="display:none;" class="jive-description">Rebuilding is <span id="rebuildProgress"></span>% complete.</span>
<%} %>
</form>
</body>
</html>
<%@ page import="org.jivesoftware.openfire.plugin.MonitoringPlugin" %>
<%@ page import="org.jivesoftware.openfire.archive.ArchivedMessage" %>
<%@ page import="org.jivesoftware.openfire.archive.Conversation" %>
<%@ page import="org.jivesoftware.openfire.archive.ConversationManager" %>
<%@ page import="org.jivesoftware.openfire.XMPPServer" %>
<%@ page import="org.xmpp.packet.JID" %>
<%@ page import="java.util.HashMap" %>
<%@ page import="java.util.Map" %>
<%@ page import="org.jivesoftware.util.*"%><%@ page import="java.util.Collection"%>
<%!
Map<String, String> colorMap = new HashMap<String, String>();
%>
<%
// Get handle on the Monitoring plugin
MonitoringPlugin plugin = (MonitoringPlugin)XMPPServer.getInstance().getPluginManager().getPlugin(
"monitoring");
ConversationManager conversationManager = (ConversationManager)plugin.getModule(ConversationManager.class);
long conversationID = ParamUtils.getLongParameter(request, "conversationID", -1);
Conversation conversation = null;
if (conversationID > -1) {
try {
conversation = new Conversation(conversationManager, conversationID);
}
catch (NotFoundException nfe) {
Log.error(nfe);
}
}
Map<String, String> colorMap = new HashMap<String, String>();
if (conversation != null) {
Collection<JID> set = conversation.getParticipants();
int count = 0;
for (JID jid : set) {
if (count == 0) {
colorMap.put(jid.toBareJID(), "blue");
}
else {
colorMap.put(jid.toBareJID(), "red");
}
count++;
}
}
%>
<html>
<head>
<meta name="decorator" content="none"/>
<title>Conversation Viewer</title>
<style type="text/css">
@import "style/style.css";
</style>
</head>
<body>
<%
if (conversation != null) {
%>
<table width="100%">
<% for (ArchivedMessage message : conversation.getMessages()) { %>
<tr valign="top">
<td width="1%" nowrap class="jive-description" style="color:<%= getColor(message.getFromJID()) %>">
[<%= JiveGlobals.formatTime(message.getSentDate())%>] <%= message.getFromJID().getNode()%>:</td>
<td><span class="jive-description"><%= StringUtils.escapeHTMLTags(message.getBody())%></span></td>
</tr>
<%}%>
</table>
<% }
else { %>
No conversation could be found.
<% } %>
<%!
String getColor(JID jid){
String color = colorMap.get(jid.toBareJID());
if(color == null){
Log.debug("Unable to find "+jid.toBareJID()+" using "+colorMap);
}
return color;
}
%>
</body>
</html>
\ No newline at end of file
<%@ page import="java.util.*,
org.jivesoftware.openfire.XMPPServer"
%>
<%@ page import="org.jivesoftware.openfire.plugin.MonitoringPlugin"%>
<%@ page import="org.jivesoftware.openfire.archive.ConversationManager"%>
<%@ page import="org.jivesoftware.openfire.archive.Conversation"%>
<%@ page import="org.jivesoftware.util.JiveGlobals"%>
<%@ page import="org.xmpp.packet.JID"%>
<%@ page import="org.jivesoftware.openfire.user.UserManager"%>
<%@ page import="java.net.URLEncoder" %>
<%@ page import="org.jivesoftware.util.StringUtils" %>
<%@ taglib uri="http://java.sun.com/jstl/core_rt" prefix="c" %>
<%@ taglib uri="http://java.sun.com/jstl/fmt_rt" prefix="fmt" %>
<%
// Get handle on the Monitoring plugin
MonitoringPlugin plugin = (MonitoringPlugin)XMPPServer.getInstance().getPluginManager().getPlugin(
"monitoring");
ConversationManager conversationManager = (ConversationManager)plugin.getModule(
ConversationManager.class);
XMPPServer server = XMPPServer.getInstance();
UserManager userManager = UserManager.getInstance();
%>
<html>
<head>
<title>Conversations</title>
<meta name="pageID" content="active-conversations"/>
<script src="/js/prototype.js" type="text/javascript"></script>
<script src="/js/scriptaculous.js" type="text/javascript"></script>
<script src="/plugins/monitoring/dwr/engine.js" type="text/javascript" ></script>
<script src="/plugins/monitoring/dwr/util.js" type="text/javascript" ></script>
<script src="/plugins/monitoring/dwr/interface/conversations.js" type="text/javascript"></script>
</head>
<body>
<style type="text/css">
@import "style/style.css";
</style>
<script type="text/javascript">
DWREngine.setErrorHandler(handleError);
window.onerror = handleError;
function handleError() {
// swallow errors: probably caused by the server being down
}
var peConversations = new PeriodicalExecuter(conversationUpdater, 10);
function conversationUpdater() {
try {
conversations.getConversations(updateConversations, true);
} catch(err) {
// swallow errors
}
}
function updateConversations(data) {
conversationsTable = $('conversations');
rows = conversationsTable.getElementsByTagName("tr");
// loop over existing rows in the table
var rowsToDelete = new Array();
for (i = 0; i < rows.length; i++) {
// is this a conversation row?
if (rows[i].id == 'noconversations') {
rowsToDelete.push(i);
} else if (rows[i].id != '') {
// does the conversation exist in update we received?
convID = rows[i].id.replace('conversation-', '');
if (data[convID] != undefined) {
row = rows[i];
cells = row.getElementsByTagName('td');
conversation = data[convID];
if (cells[3].innerHTML != conversation.messageCount) {
users = conversation.participant1 + '<br />' + conversation.participant2;
cells[0].innerHTML = users;
cells[1].innerHTML = conversation.duration;
cells[2].innerHTML = conversation.lastActivity;
cells[3].innerHTML = conversation.messageCount;
new Effect.Highlight(row, {duration: 3.0});
}
// doesn't exist in update, delete from table
} else {
rowsToDelete.push(i);
}
}
}
for (i=0; i<rowsToDelete.length; i++) {
conversationsTable.deleteRow(rowsToDelete[i]);
}
// then add any new conversations from the update
counter = 0;
for (var c in data) {
counter++;
// does this conversation already exist?
if ($('conversation-' + c) == undefined) {
conversation = data[c];
users = conversation.participant1 + '<br />' + conversation.participant2;
var newTR = document.createElement("tr");
newTR.setAttribute('id', 'conversation-' + c)
conversationsTable.appendChild(newTR);
var TD = document.createElement("TD");
TD.innerHTML = users;
newTR.appendChild(TD);
TD = document.createElement("TD");
TD.innerHTML = conversation.duration;
newTR.appendChild(TD);
TD = document.createElement("TD");
TD.innerHTML = conversation.lastActivity;
newTR.appendChild(TD);
TD = document.createElement("TD");
TD.innerHTML = conversation.messageCount;
newTR.appendChild(TD);
}
}
// update activeConversations number
$('activeConversations').innerHTML = counter;
}
</script>
<!-- <a href="#" onclick="conversationUpdater(); return false;">click me</a> -->
<p>
<fmt:message key="archive.conversations" />
<span id="activeConversations"><%= conversationManager.getConversationCount() %></span
</p>
<%
Collection<Conversation> conversations = conversationManager.getConversations();
%>
<div class="jive-table">
<table cellpadding="0" cellspacing="0" border="0" width="100%" id="conversations">
<thead>
<tr>
<th nowrap><fmt:message key="archive.conversations.users" /></th>
<th nowrap><fmt:message key="archive.conversations.duration" /></th>
<th nowrap><fmt:message key="archive.conversations.lastactivity" /></th>
<th nowrap><fmt:message key="archive.conversations.messages" /></th>
</tr>
</thead>
<tbody>
<%
if (conversations.isEmpty()) {
%>
<tr id="noconversations">
<td colspan="4">
<fmt:message key="archive.converations.no_conversations" />
</td>
</tr>
<% } %>
<%
for (Conversation conversation : conversations) {
Collection<JID> participants = conversation.getParticipants();
%>
<tr id="conversation-<%= conversation.getConversationID()%>">
<td>
<% if (conversation.getRoom() == null) { %>
<% for (JID jid : participants) { %>
<% if (server.isLocal(jid) && userManager.isRegisteredUser(jid.getNode())) { %>
<a href="/user-properties.jsp?username=<%= jid.getNode() %>"><%= jid %></a><br />
<% } else { %>
<%= jid.toBareJID() %><br/>
<% } %>
<% } %>
<% } else { %>
<fmt:message key="archive.group_conversation">
<fmt:param value="<%= "<a href='../../muc-room-occupants.jsp?roomName=" + URLEncoder.encode(conversation.getRoom().getNode(), "UTF-8") + "'>" %>" />
<fmt:param value="<%= "</a>" %>" />
</fmt:message>
<% } %>
</td>
<%
long duration = conversation.getLastActivity().getTime() -
conversation.getStartDate().getTime();
%>
<td><%= StringUtils.getTimeFromLong(duration) %></td>
<td><%= JiveGlobals.formatTime(conversation.getLastActivity()) %></td>
<td><%= conversation.getMessageCount() %></td>
</tr>
<% } %>
</tbody>
</table>
</div>
</body>
</html>
\ No newline at end of file
<html>
<title><meta name="decorator" content="none"/></title>
<body>
<div id="viewer"></div>
</body>
</html>
\ No newline at end of file
/** $Id: domLib.js 2321 2006-06-12 06:45:41Z dallen $ */
// {{{ license
/*
* Copyright 2002-2005 Dan Allen, Mojavelinux.com (dan.allen@mojavelinux.com)
*
* 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.
*/
// }}}
// {{{ intro
/**
* Title: DOM Library Core
* Version: 0.70
*
* Summary:
* A set of commonly used functions that make it easier to create javascript
* applications that rely on the DOM.
*
* Updated: 2005/05/17
*
* Maintainer: Dan Allen <dan.allen@mojavelinux.com>
* Maintainer: Jason Rust <jrust@rustyparts.com>
*
* License: Apache 2.0
*/
// }}}
// {{{ global constants (DO NOT EDIT)
// -- Browser Detection --
var domLib_userAgent = navigator.userAgent.toLowerCase();
var domLib_isMac = navigator.appVersion.indexOf('Mac') != -1;
var domLib_isWin = domLib_userAgent.indexOf('windows') != -1;
// NOTE: could use window.opera for detecting Opera
var domLib_isOpera = domLib_userAgent.indexOf('opera') != -1;
var domLib_isOpera7up = domLib_userAgent.match(/opera.(7|8)/i);
var domLib_isSafari = domLib_userAgent.indexOf('safari') != -1;
var domLib_isKonq = domLib_userAgent.indexOf('konqueror') != -1;
// Both konqueror and safari use the khtml rendering engine
var domLib_isKHTML = (domLib_isKonq || domLib_isSafari || domLib_userAgent.indexOf('khtml') != -1);
var domLib_isIE = (!domLib_isKHTML && !domLib_isOpera && (domLib_userAgent.indexOf('msie 5') != -1 || domLib_userAgent.indexOf('msie 6') != -1 || domLib_userAgent.indexOf('msie 7') != -1));
var domLib_isIE5up = domLib_isIE;
var domLib_isIE50 = (domLib_isIE && domLib_userAgent.indexOf('msie 5.0') != -1);
var domLib_isIE55 = (domLib_isIE && domLib_userAgent.indexOf('msie 5.5') != -1);
var domLib_isIE5 = (domLib_isIE50 || domLib_isIE55);
// safari and konq may use string "khtml, like gecko", so check for destinctive /
var domLib_isGecko = domLib_userAgent.indexOf('gecko/') != -1;
var domLib_isMacIE = (domLib_isIE && domLib_isMac);
var domLib_isIE55up = domLib_isIE5up && !domLib_isIE50 && !domLib_isMacIE;
var domLib_isIE6up = domLib_isIE55up && !domLib_isIE55;
// -- Browser Abilities --
var domLib_standardsMode = (document.compatMode && document.compatMode == 'CSS1Compat');
var domLib_useLibrary = (domLib_isOpera7up || domLib_isKHTML || domLib_isIE5up || domLib_isGecko || domLib_isMacIE || document.defaultView);
// fixed in Konq3.2
var domLib_hasBrokenTimeout = (domLib_isMacIE || (domLib_isKonq && domLib_userAgent.match(/konqueror\/3.([2-9])/) == null));
var domLib_canFade = (domLib_isGecko || domLib_isIE || domLib_isSafari || domLib_isOpera);
var domLib_canDrawOverSelect = (domLib_isMac || domLib_isOpera || domLib_isGecko);
var domLib_canDrawOverFlash = (domLib_isMac || domLib_isWin);
// -- Event Variables --
var domLib_eventTarget = domLib_isIE ? 'srcElement' : 'currentTarget';
var domLib_eventButton = domLib_isIE ? 'button' : 'which';
var domLib_eventTo = domLib_isIE ? 'toElement' : 'relatedTarget';
var domLib_stylePointer = domLib_isIE ? 'hand' : 'pointer';
// NOTE: a bug exists in Opera that prevents maxWidth from being set to 'none', so we make it huge
var domLib_styleNoMaxWidth = domLib_isOpera ? '10000px' : 'none';
var domLib_hidePosition = '-1000px';
var domLib_scrollbarWidth = 14;
var domLib_autoId = 1;
var domLib_zIndex = 100;
// -- Detection --
var domLib_collisionElements;
var domLib_collisionsCached = false;
var domLib_timeoutStateId = 0;
var domLib_timeoutStates = new Hash();
// }}}
// {{{ DOM enhancements
if (!document.ELEMENT_NODE)
{
document.ELEMENT_NODE = 1;
document.ATTRIBUTE_NODE = 2;
document.TEXT_NODE = 3;
document.DOCUMENT_NODE = 9;
document.DOCUMENT_FRAGMENT_NODE = 11;
}
function domLib_clone(obj)
{
var copy = {};
for (var i in obj)
{
var value = obj[i];
try
{
if (value != null && typeof(value) == 'object' && value != window && !value.nodeType)
{
copy[i] = domLib_clone(value);
}
else
{
copy[i] = value;
}
}
catch(e)
{
copy[i] = value;
}
}
return copy;
}
// }}}
// {{{ class Hash()
function Hash()
{
this.length = 0;
this.numericLength = 0;
this.elementData = [];
for (var i = 0; i < arguments.length; i += 2)
{
if (typeof(arguments[i + 1]) != 'undefined')
{
this.elementData[arguments[i]] = arguments[i + 1];
this.length++;
if (arguments[i] == parseInt(arguments[i]))
{
this.numericLength++;
}
}
}
}
// using prototype as opposed to inner functions saves on memory
Hash.prototype.get = function(in_key)
{
if (typeof(this.elementData[in_key]) != 'undefined') {
return this.elementData[in_key];
}
return null;
}
Hash.prototype.set = function(in_key, in_value)
{
if (typeof(in_value) != 'undefined')
{
if (typeof(this.elementData[in_key]) == 'undefined')
{
this.length++;
if (in_key == parseInt(in_key))
{
this.numericLength++;
}
}
return this.elementData[in_key] = in_value;
}
return false;
}
Hash.prototype.remove = function(in_key)
{
var tmp_value;
if (typeof(this.elementData[in_key]) != 'undefined')
{
this.length--;
if (in_key == parseInt(in_key))
{
this.numericLength--;
}
tmp_value = this.elementData[in_key];
delete this.elementData[in_key];
}
return tmp_value;
}
Hash.prototype.size = function()
{
return this.length;
}
Hash.prototype.has = function(in_key)
{
return typeof(this.elementData[in_key]) != 'undefined';
}
Hash.prototype.find = function(in_obj)
{
for (var tmp_key in this.elementData)
{
if (this.elementData[tmp_key] == in_obj)
{
return tmp_key;
}
}
return null;
}
Hash.prototype.merge = function(in_hash)
{
for (var tmp_key in in_hash.elementData)
{
if (typeof(this.elementData[tmp_key]) == 'undefined')
{
this.length++;
if (tmp_key == parseInt(tmp_key))
{
this.numericLength++;
}
}
this.elementData[tmp_key] = in_hash.elementData[tmp_key];
}
}
Hash.prototype.compare = function(in_hash)
{
if (this.length != in_hash.length)
{
return false;
}
for (var tmp_key in this.elementData)
{
if (this.elementData[tmp_key] != in_hash.elementData[tmp_key])
{
return false;
}
}
return true;
}
// }}}
// {{{ domLib_isDescendantOf()
function domLib_isDescendantOf(in_object, in_ancestor, in_bannedTags)
{
if (in_object == null)
{
return false;
}
if (in_object == in_ancestor)
{
return true;
}
if (typeof(in_bannedTags) != 'undefined' &&
(',' + in_bannedTags.join(',') + ',').indexOf(',' + in_object.tagName + ',') != -1)
{
return false;
}
while (in_object != document.documentElement)
{
try
{
if ((tmp_object = in_object.offsetParent) && tmp_object == in_ancestor)
{
return true;
}
else if ((tmp_object = in_object.parentNode) == in_ancestor)
{
return true;
}
else
{
in_object = tmp_object;
}
}
// in case we get some wierd error, assume we left the building
catch(e)
{
return false;
}
}
return false;
}
// }}}
// {{{ domLib_detectCollisions()
/**
* For any given target element, determine if elements on the page
* are colliding with it that do not obey the rules of z-index.
*/
function domLib_detectCollisions(in_object, in_recover, in_useCache)
{
// the reason for the cache is that if the root menu is built before
// the page is done loading, then it might not find all the elements.
// so really the only time you don't use cache is when building the
// menu as part of the page load
if (!domLib_collisionsCached)
{
var tags = [];
if (!domLib_canDrawOverFlash)
{
tags[tags.length] = 'object';
}
if (!domLib_canDrawOverSelect)
{
tags[tags.length] = 'select';
}
domLib_collisionElements = domLib_getElementsByTagNames(tags, true);
domLib_collisionsCached = in_useCache;
}
// if we don't have a tip, then unhide selects
if (in_recover)
{
for (var cnt = 0; cnt < domLib_collisionElements.length; cnt++)
{
var thisElement = domLib_collisionElements[cnt];
if (!thisElement.hideList)
{
thisElement.hideList = new Hash();
}
thisElement.hideList.remove(in_object.id);
if (!thisElement.hideList.length)
{
domLib_collisionElements[cnt].style.visibility = 'visible';
if (domLib_isKonq)
{
domLib_collisionElements[cnt].style.display = '';
}
}
}
return;
}
else if (domLib_collisionElements.length == 0)
{
return;
}
// okay, we have a tip, so hunt and destroy
var objectOffsets = domLib_getOffsets(in_object);
for (var cnt = 0; cnt < domLib_collisionElements.length; cnt++)
{
var thisElement = domLib_collisionElements[cnt];
// if collision element is in active element, move on
// WARNING: is this too costly?
if (domLib_isDescendantOf(thisElement, in_object))
{
continue;
}
// konqueror only has trouble with multirow selects
if (domLib_isKonq &&
thisElement.tagName == 'SELECT' &&
(thisElement.size <= 1 && !thisElement.multiple))
{
continue;
}
if (!thisElement.hideList)
{
thisElement.hideList = new Hash();
}
var selectOffsets = domLib_getOffsets(thisElement);
var center2centerDistance = Math.sqrt(Math.pow(selectOffsets.get('leftCenter') - objectOffsets.get('leftCenter'), 2) + Math.pow(selectOffsets.get('topCenter') - objectOffsets.get('topCenter'), 2));
var radiusSum = selectOffsets.get('radius') + objectOffsets.get('radius');
// the encompassing circles are overlapping, get in for a closer look
if (center2centerDistance < radiusSum)
{
// tip is left of select
if ((objectOffsets.get('leftCenter') <= selectOffsets.get('leftCenter') && objectOffsets.get('right') < selectOffsets.get('left')) ||
// tip is right of select
(objectOffsets.get('leftCenter') > selectOffsets.get('leftCenter') && objectOffsets.get('left') > selectOffsets.get('right')) ||
// tip is above select
(objectOffsets.get('topCenter') <= selectOffsets.get('topCenter') && objectOffsets.get('bottom') < selectOffsets.get('top')) ||
// tip is below select
(objectOffsets.get('topCenter') > selectOffsets.get('topCenter') && objectOffsets.get('top') > selectOffsets.get('bottom')))
{
thisElement.hideList.remove(in_object.id);
if (!thisElement.hideList.length)
{
thisElement.style.visibility = 'visible';
if (domLib_isKonq)
{
thisElement.style.display = '';
}
}
}
else
{
thisElement.hideList.set(in_object.id, true);
thisElement.style.visibility = 'hidden';
if (domLib_isKonq)
{
thisElement.style.display = 'none';
}
}
}
}
}
// }}}
// {{{ domLib_getOffsets()
function domLib_getOffsets(in_object, in_preserveScroll)
{
if (typeof(in_preserveScroll) == 'undefined') {
in_preserveScroll = false;
}
var originalObject = in_object;
var originalWidth = in_object.offsetWidth;
var originalHeight = in_object.offsetHeight;
var offsetLeft = 0;
var offsetTop = 0;
while (in_object)
{
offsetLeft += in_object.offsetLeft;
offsetTop += in_object.offsetTop;
in_object = in_object.offsetParent;
// consider scroll offset of parent elements
if (in_object && !in_preserveScroll)
{
offsetLeft -= in_object.scrollLeft;
offsetTop -= in_object.scrollTop;
}
}
// MacIE misreports the offsets (even with margin: 0 in body{}), still not perfect
if (domLib_isMacIE) {
offsetLeft += 10;
offsetTop += 10;
}
return new Hash(
'left', offsetLeft,
'top', offsetTop,
'right', offsetLeft + originalWidth,
'bottom', offsetTop + originalHeight,
'leftCenter', offsetLeft + originalWidth/2,
'topCenter', offsetTop + originalHeight/2,
'radius', Math.max(originalWidth, originalHeight)
);
}
// }}}
// {{{ domLib_setTimeout()
function domLib_setTimeout(in_function, in_timeout, in_args)
{
if (typeof(in_args) == 'undefined')
{
in_args = [];
}
if (in_timeout == -1)
{
// timeout event is disabled
return 0;
}
else if (in_timeout == 0)
{
in_function(in_args);
return 0;
}
// must make a copy of the arguments so that we release the reference
var args = domLib_clone(in_args);
if (!domLib_hasBrokenTimeout)
{
return setTimeout(function() { in_function(args); }, in_timeout);
}
else
{
var id = domLib_timeoutStateId++;
var data = new Hash();
data.set('function', in_function);
data.set('args', args);
domLib_timeoutStates.set(id, data);
data.set('timeoutId', setTimeout('domLib_timeoutStates.get(' + id + ').get(\'function\')(domLib_timeoutStates.get(' + id + ').get(\'args\')); domLib_timeoutStates.remove(' + id + ');', in_timeout));
return id;
}
}
// }}}
// {{{ domLib_clearTimeout()
function domLib_clearTimeout(in_id)
{
if (!domLib_hasBrokenTimeout)
{
if (in_id > 0) {
clearTimeout(in_id);
}
}
else
{
if (domLib_timeoutStates.has(in_id))
{
clearTimeout(domLib_timeoutStates.get(in_id).get('timeoutId'))
domLib_timeoutStates.remove(in_id);
}
}
}
// }}}
// {{{ domLib_getEventPosition()
function domLib_getEventPosition(in_eventObj)
{
var eventPosition = new Hash('x', 0, 'y', 0, 'scrollX', 0, 'scrollY', 0);
// IE varies depending on standard compliance mode
if (domLib_isIE)
{
var doc = (domLib_standardsMode ? document.documentElement : document.body);
// NOTE: events may fire before the body has been loaded
if (doc)
{
eventPosition.set('x', in_eventObj.clientX + doc.scrollLeft);
eventPosition.set('y', in_eventObj.clientY + doc.scrollTop);
eventPosition.set('scrollX', doc.scrollLeft);
eventPosition.set('scrollY', doc.scrollTop);
}
}
else
{
eventPosition.set('x', in_eventObj.pageX);
eventPosition.set('y', in_eventObj.pageY);
eventPosition.set('scrollX', in_eventObj.pageX - in_eventObj.clientX);
eventPosition.set('scrollY', in_eventObj.pageY - in_eventObj.clientY);
}
return eventPosition;
}
// }}}
// {{{ domLib_cancelBubble()
function domLib_cancelBubble(in_event)
{
var eventObj = in_event ? in_event : window.event;
eventObj.cancelBubble = true;
}
// }}}
// {{{ domLib_getIFrameReference()
function domLib_getIFrameReference(in_frame)
{
if (domLib_isGecko || domLib_isIE)
{
return in_frame.frameElement;
}
else
{
// we could either do it this way or require an id on the frame
// equivalent to the name
var name = in_frame.name;
if (!name || !in_frame.parent)
{
return null;
}
var candidates = in_frame.parent.document.getElementsByTagName('iframe');
for (var i = 0; i < candidates.length; i++)
{
if (candidates[i].name == name)
{
return candidates[i];
}
}
return null;
}
}
// }}}
// {{{ domLib_getElementsByClass()
function domLib_getElementsByClass(in_class)
{
var elements = domLib_isIE5 ? document.all : document.getElementsByTagName('*');
var matches = [];
var cnt = 0;
for (var i = 0; i < elements.length; i++)
{
if ((" " + elements[i].className + " ").indexOf(" " + in_class + " ") != -1)
{
matches[cnt++] = elements[i];
}
}
return matches;
}
// }}}
// {{{ domLib_getElementsByTagNames()
function domLib_getElementsByTagNames(in_list, in_excludeHidden)
{
var elements = [];
for (var i = 0; i < in_list.length; i++)
{
var matches = document.getElementsByTagName(in_list[i]);
for (var j = 0; j < matches.length; j++)
{
// skip objects that have nested embeds, or else we get "flashing"
if (matches[j].tagName == 'OBJECT' && domLib_isGecko)
{
var kids = matches[j].childNodes;
var skip = false;
for (var k = 0; k < kids.length; k++)
{
if (kids[k].tagName == 'EMBED')
{
skip = true;
break;
}
}
if (skip) continue;
}
if (in_excludeHidden && domLib_getComputedStyle(matches[j], 'visibility') == 'hidden')
{
continue;
}
elements[elements.length] = matches[j];
}
}
return elements;
}
// }}}
// {{{ domLib_getComputedStyle()
function domLib_getComputedStyle(in_obj, in_property)
{
if (domLib_isIE)
{
var humpBackProp = in_property.replace(/-(.)/, function (a, b) { return b.toUpperCase(); });
return eval('in_obj.currentStyle.' + humpBackProp);
}
// getComputedStyle() is broken in konqueror, so let's go for the style object
else if (domLib_isKonq)
{
//var humpBackProp = in_property.replace(/-(.)/, function (a, b) { return b.toUpperCase(); });
return eval('in_obj.style.' + in_property);
}
else
{
return document.defaultView.getComputedStyle(in_obj, null).getPropertyValue(in_property);
}
}
// }}}
// {{{ makeTrue()
function makeTrue()
{
return true;
}
// }}}
// {{{ makeFalse()
function makeFalse()
{
return false;
}
// }}}
/** $Id: domTT.js 2324 2006-06-12 07:06:39Z dallen $ */
// {{{ license
/*
* Copyright 2002-2005 Dan Allen, Mojavelinux.com (dan.allen@mojavelinux.com)
*
* 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.
*/
// }}}
// {{{ intro
/**
* Title: DOM Tooltip Library
* Version: 0.7.3
*
* Summary:
* Allows developers to add custom tooltips to the webpages. Tooltips are
* generated using the domTT_activate() function and customized by setting
* a handful of options.
*
* Maintainer: Dan Allen <dan.allen@mojavelinux.com>
* Contributors:
* Josh Gross <josh@jportalhome.com>
* Jason Rust <jason@rustyparts.com>
*
* License: Apache 2.0
* However, if you use this library, you earn the position of official bug
* reporter :) Please post questions or problem reports to the newsgroup:
*
* http://groups-beta.google.com/group/dom-tooltip
*
* If you are doing this for commercial work, perhaps you could send me a few
* Starbucks Coffee gift dollars or PayPal bucks to encourage future
* developement (NOT REQUIRED). E-mail me for my snail mail address.
*
* Homepage: http://www.mojavelinux.com/projects/domtooltip/
*
* Newsgroup: http://groups-beta.google.com/group/dom-tooltip
*
* Freshmeat Project: http://freshmeat.net/projects/domtt/?topic_id=92
*
* Updated: 2005/07/16
*
* Supported Browsers:
* Mozilla (Gecko), IE 5.5+, IE on Mac, Safari, Konqueror, Opera 7
*
* Usage:
* Please see the HOWTO documentation.
**/
// }}}
// {{{ settings (editable)
// IE mouse events seem to be off by 2 pixels
var domTT_offsetX = (domLib_isIE ? -2 : 0);
var domTT_offsetY = (domLib_isIE ? 4 : 2);
var domTT_direction = 'southeast';
var domTT_mouseHeight = domLib_isIE ? 13 : 19;
var domTT_closeLink = 'X';
var domTT_closeAction = 'hide';
var domTT_activateDelay = 500;
var domTT_maxWidth = false;
var domTT_styleClass = 'domTT';
var domTT_fade = 'neither';
var domTT_lifetime = 0;
var domTT_grid = 0;
var domTT_trailDelay = 200;
var domTT_useGlobalMousePosition = true;
var domTT_postponeActivation = false;
var domTT_tooltipIdPrefix = '[domTT]';
var domTT_screenEdgeDetection = true;
var domTT_screenEdgePadding = 4;
var domTT_oneOnly = false;
var domTT_cloneNodes = false;
var domTT_detectCollisions = true;
var domTT_bannedTags = ['OPTION'];
var domTT_draggable = false;
if (typeof(domTT_dragEnabled) == 'undefined')
{
domTT_dragEnabled = false;
}
// }}}
// {{{ globals (DO NOT EDIT)
var domTT_predefined = new Hash();
// tooltips are keyed on both the tip id and the owner id,
// since events can originate on either object
var domTT_tooltips = new Hash();
var domTT_lastOpened = 0;
var domTT_documentLoaded = false;
var domTT_mousePosition = null;
// }}}
// {{{ document.onmousemove
if (domLib_useLibrary && domTT_useGlobalMousePosition)
{
document.onmousemove = function(in_event)
{
if (typeof(in_event) == 'undefined') { in_event = window.event; }
domTT_mousePosition = domLib_getEventPosition(in_event);
if (domTT_dragEnabled && domTT_dragMouseDown)
{
domTT_dragUpdate(in_event);
}
}
}
// }}}
// {{{ domTT_activate()
function domTT_activate(in_this, in_event)
{
if (!domLib_useLibrary || (domTT_postponeActivation && !domTT_documentLoaded)) { return false; }
// make sure in_event is set (for IE, some cases we have to use window.event)
if (typeof(in_event) == 'undefined') { in_event = window.event; }
// don't allow tooltips on banned tags (such as OPTION)
if (in_event != null) {
var target = in_event.srcElement ? in_event.srcElement : in_event.target;
if (target != null && (',' + domTT_bannedTags.join(',') + ',').indexOf(',' + target.tagName + ',') != -1)
{
return false;
}
}
var owner = document.body;
// we have an active event so get the owner
if (in_event != null && in_event.type.match(/key|mouse|click|contextmenu/i))
{
// make sure we have nothing higher than the body element
if (in_this.nodeType && in_this.nodeType != document.DOCUMENT_NODE)
{
owner = in_this;
}
}
// non active event (make sure we were passed a string id)
else
{
if (typeof(in_this) != 'object' && !(owner = domTT_tooltips.get(in_this)))
{
// NOTE: two steps to avoid "flashing" in gecko
var embryo = document.createElement('div');
owner = document.body.appendChild(embryo);
owner.style.display = 'none';
owner.id = in_this;
}
}
// make sure the owner has a unique id
if (!owner.id)
{
owner.id = '__autoId' + domLib_autoId++;
}
// see if we should only be opening one tip at a time
// NOTE: this is not "perfect" yet since it really steps on any other
// tip working on fade out or delayed close, but it get's the job done
if (domTT_oneOnly && domTT_lastOpened)
{
domTT_deactivate(domTT_lastOpened);
}
domTT_lastOpened = owner.id;
var tooltip = domTT_tooltips.get(owner.id);
if (tooltip)
{
if (tooltip.get('eventType') != in_event.type)
{
if (tooltip.get('type') == 'greasy')
{
tooltip.set('closeAction', 'destroy');
domTT_deactivate(owner.id);
}
else if (tooltip.get('status') != 'inactive')
{
return owner.id;
}
}
else
{
if (tooltip.get('status') == 'inactive')
{
tooltip.set('status', 'pending');
tooltip.set('activateTimeout', domLib_setTimeout(domTT_runShow, tooltip.get('delay'), [owner.id, in_event]));
return owner.id;
}
// either pending or active, let it be
else
{
return owner.id;
}
}
}
// setup the default options hash
var options = new Hash(
'caption', '',
'content', '',
'clearMouse', true,
'closeAction', domTT_closeAction,
'closeLink', domTT_closeLink,
'delay', domTT_activateDelay,
'direction', domTT_direction,
'draggable', domTT_draggable,
'fade', domTT_fade,
'fadeMax', 100,
'grid', domTT_grid,
'id', domTT_tooltipIdPrefix + owner.id,
'inframe', false,
'lifetime', domTT_lifetime,
'offsetX', domTT_offsetX,
'offsetY', domTT_offsetY,
'parent', document.body,
'position', 'absolute',
'styleClass', domTT_styleClass,
'type', 'greasy',
'trail', false,
'lazy', false
);
// load in the options from the function call
for (var i = 2; i < arguments.length; i += 2)
{
// load in predefined
if (arguments[i] == 'predefined')
{
var predefinedOptions = domTT_predefined.get(arguments[i + 1]);
for (var j in predefinedOptions.elementData)
{
options.set(j, predefinedOptions.get(j));
}
}
// set option
else
{
options.set(arguments[i], arguments[i + 1]);
}
}
options.set('eventType', in_event != null ? in_event.type : null);
// immediately set the status text if provided
if (options.has('statusText'))
{
try { window.status = options.get('statusText'); } catch(e) {}
}
// if we didn't give content...assume we just wanted to change the status and return
if (!options.has('content') || options.get('content') == '' || options.get('content') == null)
{
if (typeof(owner.onmouseout) != 'function')
{
owner.onmouseout = function(in_event) { domTT_mouseout(this, in_event); };
}
return owner.id;
}
options.set('owner', owner);
domTT_create(options);
// determine the show delay
options.set('delay', (in_event != null && in_event.type.match(/click|mousedown|contextmenu/i)) ? 0 : parseInt(options.get('delay')));
domTT_tooltips.set(owner.id, options);
domTT_tooltips.set(options.get('id'), options);
options.set('status', 'pending');
options.set('activateTimeout', domLib_setTimeout(domTT_runShow, options.get('delay'), [owner.id, in_event]));
return owner.id;
}
// }}}
// {{{ domTT_create()
function domTT_create(in_options)
{
var tipOwner = in_options.get('owner');
var parentObj = in_options.get('parent');
var parentDoc = parentObj.ownerDocument || parentObj.document;
// create the tooltip and hide it
// NOTE: two steps to avoid "flashing" in gecko
var embryo = parentDoc.createElement('div');
var tipObj = parentObj.appendChild(embryo);
tipObj.style.position = 'absolute';
tipObj.style.left = '0px';
tipObj.style.top = '0px';
tipObj.style.visibility = 'hidden';
tipObj.id = in_options.get('id');
tipObj.className = in_options.get('styleClass');
var contentBlock;
var tableLayout = false;
if (in_options.get('caption') || (in_options.get('type') == 'sticky' && in_options.get('caption') !== false))
{
tableLayout = true;
// layout the tip with a hidden formatting table
var tipLayoutTable = tipObj.appendChild(parentDoc.createElement('table'));
tipLayoutTable.style.borderCollapse = 'collapse';
if (domLib_isKHTML)
{
tipLayoutTable.cellSpacing = 0;
}
var tipLayoutTbody = tipLayoutTable.appendChild(parentDoc.createElement('tbody'));
var numCaptionCells = 0;
var captionRow = tipLayoutTbody.appendChild(parentDoc.createElement('tr'));
var captionCell = captionRow.appendChild(parentDoc.createElement('td'));
captionCell.style.padding = '0px';
var caption = captionCell.appendChild(parentDoc.createElement('div'));
caption.className = 'caption';
if (domLib_isIE50)
{
caption.style.height = '100%';
}
if (in_options.get('caption').nodeType)
{
caption.appendChild(domTT_cloneNodes ? in_options.get('caption').cloneNode(1) : in_options.get('caption'));
}
else
{
caption.innerHTML = in_options.get('caption');
}
if (in_options.get('type') == 'sticky')
{
var numCaptionCells = 2;
var closeLinkCell = captionRow.appendChild(parentDoc.createElement('td'));
closeLinkCell.style.padding = '0px';
var closeLink = closeLinkCell.appendChild(parentDoc.createElement('div'));
closeLink.className = 'caption';
if (domLib_isIE50)
{
closeLink.style.height = '100%';
}
closeLink.style.textAlign = 'right';
closeLink.style.cursor = domLib_stylePointer;
// merge the styles of the two cells
closeLink.style.borderLeftWidth = caption.style.borderRightWidth = '0px';
closeLink.style.paddingLeft = caption.style.paddingRight = '0px';
closeLink.style.marginLeft = caption.style.marginRight = '0px';
if (in_options.get('closeLink').nodeType)
{
closeLink.appendChild(in_options.get('closeLink').cloneNode(1));
}
else
{
closeLink.innerHTML = in_options.get('closeLink');
}
closeLink.onclick = function()
{
domTT_deactivate(tipOwner.id);
};
closeLink.onmousedown = function(in_event)
{
if (typeof(in_event) == 'undefined') { in_event = window.event; }
in_event.cancelBubble = true;
};
// MacIE has to have a newline at the end and must be made with createTextNode()
if (domLib_isMacIE)
{
closeLinkCell.appendChild(parentDoc.createTextNode("\n"));
}
}
// MacIE has to have a newline at the end and must be made with createTextNode()
if (domLib_isMacIE)
{
captionCell.appendChild(parentDoc.createTextNode("\n"));
}
var contentRow = tipLayoutTbody.appendChild(parentDoc.createElement('tr'));
var contentCell = contentRow.appendChild(parentDoc.createElement('td'));
contentCell.style.padding = '0px';
if (numCaptionCells)
{
if (domLib_isIE || domLib_isOpera)
{
contentCell.colSpan = numCaptionCells;
}
else
{
contentCell.setAttribute('colspan', numCaptionCells);
}
}
contentBlock = contentCell.appendChild(parentDoc.createElement('div'));
if (domLib_isIE50)
{
contentBlock.style.height = '100%';
}
}
else
{
contentBlock = tipObj.appendChild(parentDoc.createElement('div'));
}
contentBlock.className = 'contents';
var content = in_options.get('content');
// allow content has a function to return the actual content
if (typeof(content) == 'function') {
content = content(in_options.get('id'));
}
if (content != null && content.nodeType)
{
contentBlock.appendChild(domTT_cloneNodes ? content.cloneNode(1) : content);
}
else
{
contentBlock.innerHTML = content;
}
// adjust the width if specified
if (in_options.has('width'))
{
tipObj.style.width = parseInt(in_options.get('width')) + 'px';
}
// check if we are overridding the maxWidth
// if the browser supports maxWidth, the global setting will be ignored (assume stylesheet)
var maxWidth = domTT_maxWidth;
if (in_options.has('maxWidth'))
{
if ((maxWidth = in_options.get('maxWidth')) === false)
{
tipObj.style.maxWidth = domLib_styleNoMaxWidth;
}
else
{
maxWidth = parseInt(in_options.get('maxWidth'));
tipObj.style.maxWidth = maxWidth + 'px';
}
}
// HACK: fix lack of maxWidth in CSS for KHTML and IE
if (maxWidth !== false && (domLib_isIE || domLib_isKHTML) && tipObj.offsetWidth > maxWidth)
{
tipObj.style.width = maxWidth + 'px';
}
in_options.set('offsetWidth', tipObj.offsetWidth);
in_options.set('offsetHeight', tipObj.offsetHeight);
// konqueror miscalcuates the width of the containing div when using the layout table based on the
// border size of the containing div
if (domLib_isKonq && tableLayout && !tipObj.style.width)
{
var left = document.defaultView.getComputedStyle(tipObj, '').getPropertyValue('border-left-width');
var right = document.defaultView.getComputedStyle(tipObj, '').getPropertyValue('border-right-width');
left = left.substring(left.indexOf(':') + 2, left.indexOf(';'));
right = right.substring(right.indexOf(':') + 2, right.indexOf(';'));
var correction = 2 * ((left ? parseInt(left) : 0) + (right ? parseInt(right) : 0));
tipObj.style.width = (tipObj.offsetWidth - correction) + 'px';
}
// if a width is not set on an absolutely positioned object, both IE and Opera
// will attempt to wrap when it spills outside of body...we cannot have that
if (domLib_isIE || domLib_isOpera)
{
if (!tipObj.style.width)
{
// HACK: the correction here is for a border
tipObj.style.width = (tipObj.offsetWidth - 2) + 'px';
}
// HACK: the correction here is for a border
tipObj.style.height = (tipObj.offsetHeight - 2) + 'px';
}
// store placement offsets from event position
var offsetX, offsetY;
// tooltip floats
if (in_options.get('position') == 'absolute' && !(in_options.has('x') && in_options.has('y')))
{
// determine the offset relative to the pointer
switch (in_options.get('direction'))
{
case 'northeast':
offsetX = in_options.get('offsetX');
offsetY = 0 - tipObj.offsetHeight - in_options.get('offsetY');
break;
case 'northwest':
offsetX = 0 - tipObj.offsetWidth - in_options.get('offsetX');
offsetY = 0 - tipObj.offsetHeight - in_options.get('offsetY');
break;
case 'north':
offsetX = 0 - parseInt(tipObj.offsetWidth/2);
offsetY = 0 - tipObj.offsetHeight - in_options.get('offsetY');
break;
case 'southwest':
offsetX = 0 - tipObj.offsetWidth - in_options.get('offsetX');
offsetY = in_options.get('offsetY');
break;
case 'southeast':
offsetX = in_options.get('offsetX');
offsetY = in_options.get('offsetY');
break;
case 'south':
offsetX = 0 - parseInt(tipObj.offsetWidth/2);
offsetY = in_options.get('offsetY');
break;
}
// if we are in an iframe, get the offsets of the iframe in the parent document
if (in_options.get('inframe'))
{
var iframeObj = domLib_getIFrameReference(window);
if (iframeObj)
{
var frameOffsets = domLib_getOffsets(iframeObj);
offsetX += frameOffsets.get('left');
offsetY += frameOffsets.get('top');
}
}
}
// tooltip is fixed
else
{
offsetX = 0;
offsetY = 0;
in_options.set('trail', false);
}
// set the direction-specific offsetX/Y
in_options.set('offsetX', offsetX);
in_options.set('offsetY', offsetY);
if (in_options.get('clearMouse') && in_options.get('direction').indexOf('south') != -1)
{
in_options.set('mouseOffset', domTT_mouseHeight);
}
else
{
in_options.set('mouseOffset', 0);
}
if (domLib_canFade && typeof(Fadomatic) == 'function')
{
if (in_options.get('fade') != 'neither')
{
var fadeHandler = new Fadomatic(tipObj, 10, 0, 0, in_options.get('fadeMax'));
in_options.set('fadeHandler', fadeHandler);
}
}
else
{
in_options.set('fade', 'neither');
}
// setup mouse events
if (in_options.get('trail') && typeof(tipOwner.onmousemove) != 'function')
{
tipOwner.onmousemove = function(in_event) { domTT_mousemove(this, in_event); };
}
if (typeof(tipOwner.onmouseout) != 'function')
{
tipOwner.onmouseout = function(in_event) { domTT_mouseout(this, in_event); };
}
if (in_options.get('type') == 'sticky')
{
if (in_options.get('position') == 'absolute' && domTT_dragEnabled && in_options.get('draggable'))
{
if (domLib_isIE)
{
captionRow.onselectstart = function() { return false; };
}
// setup drag
captionRow.onmousedown = function(in_event) { domTT_dragStart(tipObj, in_event); };
captionRow.onmousemove = function(in_event) { domTT_dragUpdate(in_event); };
captionRow.onmouseup = function() { domTT_dragStop(); };
}
}
else if (in_options.get('type') == 'velcro')
{
/* can use once we have deactivateDelay
tipObj.onmouseover = function(in_event)
{
if (typeof(in_event) == 'undefined') { in_event = window.event; }
var tooltip = domTT_tooltips.get(tipObj.id);
if (in_options.get('lifetime')) {
domLib_clearTimeout(in_options.get('lifetimeTimeout');
}
};
*/
tipObj.onmouseout = function(in_event)
{
if (typeof(in_event) == 'undefined') { in_event = window.event; }
if (!domLib_isDescendantOf(in_event[domLib_eventTo], tipObj, domTT_bannedTags)) {
domTT_deactivate(tipOwner.id);
}
};
// NOTE: this might interfere with links in the tip
tipObj.onclick = function(in_event)
{
domTT_deactivate(tipOwner.id);
};
}
if (in_options.get('position') == 'relative')
{
tipObj.style.position = 'relative';
}
in_options.set('node', tipObj);
in_options.set('status', 'inactive');
}
// }}}
// {{{ domTT_show()
// in_id is either tip id or the owner id
function domTT_show(in_id, in_event)
{
// should always find one since this call would be cancelled if tip was killed
var tooltip = domTT_tooltips.get(in_id);
var status = tooltip.get('status');
var tipObj = tooltip.get('node');
if (tooltip.get('position') == 'absolute')
{
var mouseX, mouseY;
if (tooltip.has('x') && tooltip.has('y'))
{
mouseX = tooltip.get('x');
mouseY = tooltip.get('y');
}
else if (!domTT_useGlobalMousePosition || domTT_mousePosition == null || status == 'active' || tooltip.get('delay') == 0)
{
var eventPosition = domLib_getEventPosition(in_event);
var eventX = eventPosition.get('x');
var eventY = eventPosition.get('y');
if (tooltip.get('inframe'))
{
eventX -= eventPosition.get('scrollX');
eventY -= eventPosition.get('scrollY');
}
// only move tip along requested trail axis when updating position
if (status == 'active' && tooltip.get('trail') !== true)
{
var trail = tooltip.get('trail');
if (trail == 'x')
{
mouseX = eventX;
mouseY = tooltip.get('mouseY');
}
else if (trail == 'y')
{
mouseX = tooltip.get('mouseX');
mouseY = eventY;
}
}
else
{
mouseX = eventX;
mouseY = eventY;
}
}
else
{
mouseX = domTT_mousePosition.get('x');
mouseY = domTT_mousePosition.get('y');
if (tooltip.get('inframe'))
{
mouseX -= domTT_mousePosition.get('scrollX');
mouseY -= domTT_mousePosition.get('scrollY');
}
}
// we are using a grid for updates
if (tooltip.get('grid'))
{
// if this is not a mousemove event or it is a mousemove event on an active tip and
// the movement is bigger than the grid
if (in_event.type != 'mousemove' || (status == 'active' && (Math.abs(tooltip.get('lastX') - mouseX) > tooltip.get('grid') || Math.abs(tooltip.get('lastY') - mouseY) > tooltip.get('grid'))))
{
tooltip.set('lastX', mouseX);
tooltip.set('lastY', mouseY);
}
// did not satisfy the grid movement requirement
else
{
return false;
}
}
// mouseX and mouseY store the last acknowleged mouse position,
// good for trailing on one axis
tooltip.set('mouseX', mouseX);
tooltip.set('mouseY', mouseY);
var coordinates;
if (domTT_screenEdgeDetection)
{
coordinates = domTT_correctEdgeBleed(
tooltip.get('offsetWidth'),
tooltip.get('offsetHeight'),
mouseX,
mouseY,
tooltip.get('offsetX'),
tooltip.get('offsetY'),
tooltip.get('mouseOffset'),
tooltip.get('inframe') ? window.parent : window
);
}
else
{
coordinates = {
'x' : mouseX + tooltip.get('offsetX'),
'y' : mouseY + tooltip.get('offsetY') + tooltip.get('mouseOffset')
};
}
// update the position
tipObj.style.left = coordinates.x + 'px';
tipObj.style.top = coordinates.y + 'px';
// increase the tip zIndex so it goes over previously shown tips
tipObj.style.zIndex = domLib_zIndex++;
}
// if tip is not active, active it now and check for a fade in
if (status == 'pending')
{
// unhide the tooltip
tooltip.set('status', 'active');
tipObj.style.display = '';
tipObj.style.visibility = 'visible';
var fade = tooltip.get('fade');
if (fade != 'neither')
{
var fadeHandler = tooltip.get('fadeHandler');
if (fade == 'out' || fade == 'both')
{
fadeHandler.haltFade();
if (fade == 'out')
{
fadeHandler.halt();
}
}
if (fade == 'in' || fade == 'both')
{
fadeHandler.fadeIn();
}
}
if (tooltip.get('type') == 'greasy' && tooltip.get('lifetime') != 0)
{
tooltip.set('lifetimeTimeout', domLib_setTimeout(domTT_runDeactivate, tooltip.get('lifetime'), [tipObj.id]));
}
}
if (tooltip.get('position') == 'absolute' && domTT_detectCollisions)
{
// utilize original collision element cache
domLib_detectCollisions(tipObj, false, true);
}
}
// }}}
// {{{ domTT_close()
// in_handle can either be an child object of the tip, the tip id or the owner id
function domTT_close(in_handle)
{
var id;
if (typeof(in_handle) == 'object' && in_handle.nodeType)
{
var obj = in_handle;
while (!obj.id || !domTT_tooltips.get(obj.id))
{
obj = obj.parentNode;
if (obj.nodeType != document.ELEMENT_NODE) { return; }
}
id = obj.id;
}
else
{
id = in_handle;
}
domTT_deactivate(id);
}
// }}}
// {{{ domTT_closeAll()
// run through the tooltips and close them all
function domTT_closeAll()
{
// NOTE: this will iterate 2x # of tooltips
for (var id in domTT_tooltips.elementData) {
domTT_close(id);
}
}
// }}}
// {{{ domTT_deactivate()
// in_id is either the tip id or the owner id
function domTT_deactivate(in_id)
{
var tooltip = domTT_tooltips.get(in_id);
if (tooltip)
{
var status = tooltip.get('status');
if (status == 'pending')
{
// cancel the creation of this tip if it is still pending
domLib_clearTimeout(tooltip.get('activateTimeout'));
tooltip.set('status', 'inactive');
}
else if (status == 'active')
{
if (tooltip.get('lifetime'))
{
domLib_clearTimeout(tooltip.get('lifetimeTimeout'));
}
var tipObj = tooltip.get('node');
if (tooltip.get('closeAction') == 'hide')
{
var fade = tooltip.get('fade');
if (fade != 'neither')
{
var fadeHandler = tooltip.get('fadeHandler');
if (fade == 'out' || fade == 'both')
{
fadeHandler.fadeOut();
}
else
{
fadeHandler.hide();
}
}
else
{
tipObj.style.display = 'none';
}
}
else
{
tooltip.get('parent').removeChild(tipObj);
domTT_tooltips.remove(tooltip.get('owner').id);
domTT_tooltips.remove(tooltip.get('id'));
}
tooltip.set('status', 'inactive');
if (domTT_detectCollisions) {
// unhide all of the selects that are owned by this object
// utilize original collision element cache
domLib_detectCollisions(tipObj, true, true);
}
}
}
}
// }}}
// {{{ domTT_mouseout()
function domTT_mouseout(in_owner, in_event)
{
if (!domLib_useLibrary) { return false; }
if (typeof(in_event) == 'undefined') { in_event = window.event; }
var toChild = domLib_isDescendantOf(in_event[domLib_eventTo], in_owner, domTT_bannedTags);
var tooltip = domTT_tooltips.get(in_owner.id);
if (tooltip && (tooltip.get('type') == 'greasy' || tooltip.get('status') != 'active'))
{
// deactivate tip if exists and we moved away from the owner
if (!toChild)
{
domTT_deactivate(in_owner.id);
try { window.status = window.defaultStatus; } catch(e) {}
}
}
else if (!toChild)
{
try { window.status = window.defaultStatus; } catch(e) {}
}
}
// }}}
// {{{ domTT_mousemove()
function domTT_mousemove(in_owner, in_event)
{
if (!domLib_useLibrary) { return false; }
if (typeof(in_event) == 'undefined') { in_event = window.event; }
var tooltip = domTT_tooltips.get(in_owner.id);
if (tooltip && tooltip.get('trail') && tooltip.get('status') == 'active')
{
// see if we are trailing lazy
if (tooltip.get('lazy'))
{
domLib_setTimeout(domTT_runShow, domTT_trailDelay, [in_owner.id, in_event]);
}
else
{
domTT_show(in_owner.id, in_event);
}
}
}
// }}}
// {{{ domTT_addPredefined()
function domTT_addPredefined(in_id)
{
var options = new Hash();
for (var i = 1; i < arguments.length; i += 2)
{
options.set(arguments[i], arguments[i + 1]);
}
domTT_predefined.set(in_id, options);
}
// }}}
// {{{ domTT_correctEdgeBleed()
function domTT_correctEdgeBleed(in_width, in_height, in_x, in_y, in_offsetX, in_offsetY, in_mouseOffset, in_window)
{
var win, doc;
var bleedRight, bleedBottom;
var pageHeight, pageWidth, pageYOffset, pageXOffset;
var x = in_x + in_offsetX;
var y = in_y + in_offsetY + in_mouseOffset;
win = (typeof(in_window) == 'undefined' ? window : in_window);
// Gecko and IE swaps values of clientHeight, clientWidth properties when
// in standards compliance mode from documentElement to document.body
doc = ((domLib_standardsMode && (domLib_isIE || domLib_isGecko)) ? win.document.documentElement : win.document.body);
// for IE in compliance mode
if (domLib_isIE)
{
pageHeight = doc.clientHeight;
pageWidth = doc.clientWidth;
pageYOffset = doc.scrollTop;
pageXOffset = doc.scrollLeft;
}
else
{
pageHeight = doc.clientHeight;
pageWidth = doc.clientWidth;
if (domLib_isKHTML)
{
pageHeight = win.innerHeight;
}
pageYOffset = win.pageYOffset;
pageXOffset = win.pageXOffset;
}
// we are bleeding off the right, move tip over to stay on page
// logic: take x position, add width and subtract from effective page width
if ((bleedRight = (x - pageXOffset) + in_width - (pageWidth - domTT_screenEdgePadding)) > 0)
{
x -= bleedRight;
}
// we are bleeding to the left, move tip over to stay on page
// if tip doesn't fit, we will go back to bleeding off the right
// logic: take x position and check if less than edge padding
if ((x - pageXOffset) < domTT_screenEdgePadding)
{
x = domTT_screenEdgePadding + pageXOffset;
}
// if we are bleeding off the bottom, flip to north
// logic: take y position, add height and subtract from effective page height
if ((bleedBottom = (y - pageYOffset) + in_height - (pageHeight - domTT_screenEdgePadding)) > 0)
{
y = in_y - in_height - in_offsetY;
}
// if we are bleeding off the top, flip to south
// if tip doesn't fit, we will go back to bleeding off the bottom
// logic: take y position and check if less than edge padding
if ((y - pageYOffset) < domTT_screenEdgePadding)
{
y = in_y + domTT_mouseHeight + in_offsetY;
}
return {'x' : x, 'y' : y};
}
// }}}
// {{{ domTT_isActive()
// in_id is either the tip id or the owner id
function domTT_isActive(in_id)
{
var tooltip = domTT_tooltips.get(in_id);
if (!tooltip || tooltip.get('status') != 'active')
{
return false;
}
else
{
return true;
}
}
// }}}
// {{{ domTT_runXXX()
// All of these domMenu_runXXX() methods are used by the event handling sections to
// avoid the circular memory leaks caused by inner functions
function domTT_runDeactivate(args) { domTT_deactivate(args[0]); }
function domTT_runShow(args) { domTT_show(args[0], args[1]); }
// }}}
// {{{ domTT_replaceTitles()
function domTT_replaceTitles(in_decorator)
{
var elements = domLib_getElementsByClass('tooltip');
for (var i = 0; i < elements.length; i++)
{
if (elements[i].title)
{
var content;
if (typeof(in_decorator) == 'function')
{
content = in_decorator(elements[i]);
}
else
{
content = elements[i].title;
}
content = content.replace(new RegExp('\'', 'g'), '\\\'');
elements[i].onmouseover = new Function('in_event', "domTT_activate(this, in_event, 'content', '" + content + "')");
elements[i].title = '';
}
}
}
// }}}
// {{{ domTT_update()
// Allow authors to update the contents of existing tips using the DOM
// Unfortunately, the tip must already exist, or else no work is done.
// TODO: make getting at content or caption cleaner
function domTT_update(handle, content, type)
{
// type defaults to 'content', can also be 'caption'
if (typeof(type) == 'undefined')
{
type = 'content';
}
var tip = domTT_tooltips.get(handle);
if (!tip)
{
return;
}
var tipObj = tip.get('node');
var updateNode;
if (type == 'content')
{
// <div class="contents">...
updateNode = tipObj.firstChild;
if (updateNode.className != 'contents')
{
// <table><tbody><tr>...</tr><tr><td><div class="contents">...
updateNode = updateNode.firstChild.firstChild.nextSibling.firstChild.firstChild;
}
}
else
{
updateNode = tipObj.firstChild;
if (updateNode.className == 'contents')
{
// missing caption
return;
}
// <table><tbody><tr><td><div class="caption">...
updateNode = updateNode.firstChild.firstChild.firstChild.firstChild;
}
// TODO: allow for a DOM node as content
updateNode.innerHTML = content;
}
// }}}
/** $Id: domTT_drag.js 2315 2006-06-12 05:45:36Z dallen $ */
// {{{ license
/*
* Copyright 2002-2005 Dan Allen, Mojavelinux.com (dan.allen@mojavelinux.com)
*
* 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.
*/
// }}}
// {{{ globals (DO NOT EDIT)
var domTT_dragEnabled = true;
var domTT_currentDragTarget;
var domTT_dragMouseDown;
var domTT_dragOffsetLeft;
var domTT_dragOffsetTop;
// }}}
// {{{ domTT_dragStart()
function domTT_dragStart(in_this, in_event)
{
if (typeof(in_event) == 'undefined') { in_event = window.event; }
var eventButton = in_event[domLib_eventButton];
if (eventButton != 1 && !domLib_isKHTML)
{
return;
}
domTT_currentDragTarget = in_this;
in_this.style.cursor = 'move';
// upgrade our z-index
in_this.style.zIndex = ++domLib_zIndex;
var eventPosition = domLib_getEventPosition(in_event);
var targetPosition = domLib_getOffsets(in_this);
domTT_dragOffsetLeft = eventPosition.get('x') - targetPosition.get('left');
domTT_dragOffsetTop = eventPosition.get('y') - targetPosition.get('top');
domTT_dragMouseDown = true;
}
// }}}
// {{{ domTT_dragUpdate()
function domTT_dragUpdate(in_event)
{
if (domTT_dragMouseDown)
{
if (domLib_isGecko)
{
window.getSelection().removeAllRanges()
}
if (domTT_useGlobalMousePosition && domTT_mousePosition != null)
{
var eventPosition = domTT_mousePosition;
}
else
{
if (typeof(in_event) == 'undefined') { in_event = window.event; }
var eventPosition = domLib_getEventPosition(in_event);
}
domTT_currentDragTarget.style.left = (eventPosition.get('x') - domTT_dragOffsetLeft) + 'px';
domTT_currentDragTarget.style.top = (eventPosition.get('y') - domTT_dragOffsetTop) + 'px';
// update the collision detection
domLib_detectCollisions(domTT_currentDragTarget);
}
}
// }}}
// {{{ domTT_dragStop()
function domTT_dragStop()
{
if (domTT_dragMouseDown) {
domTT_dragMouseDown = false;
domTT_currentDragTarget.style.cursor = 'default';
domTT_currentDragTarget = null;
if (domLib_isGecko)
{
window.getSelection().removeAllRanges()
}
}
}
// }}}
/** $Id$ */
// Title: Fadomatic
// Version: 1.2
// Homepage: http://chimpen.com/fadomatic
// Author: Philip McCarthy <fadomatic@chimpen.com>
// Fade interval in milliseconds
// Make this larger if you experience performance issues
Fadomatic.INTERVAL_MILLIS = 50;
// Creates a fader
// element - The element to fade
// speed - The speed to fade at, from 0.0 to 100.0
// initialOpacity (optional, default 100) - element's starting opacity, 0 to 100
// minOpacity (optional, default 0) - element's minimum opacity, 0 to 100
// maxOpacity (optional, default 0) - element's minimum opacity, 0 to 100
function Fadomatic (element, rate, initialOpacity, minOpacity, maxOpacity) {
this._element = element;
this._intervalId = null;
this._rate = rate;
this._isFadeOut = true;
// Set initial opacity and bounds
// NB use 99 instead of 100 to avoid flicker at start of fade
this._minOpacity = 0;
this._maxOpacity = 99;
this._opacity = 99;
if (typeof minOpacity != 'undefined') {
if (minOpacity < 0) {
this._minOpacity = 0;
} else if (minOpacity > 99) {
this._minOpacity = 99;
} else {
this._minOpacity = minOpacity;
}
}
if (typeof maxOpacity != 'undefined') {
if (maxOpacity < 0) {
this._maxOpacity = 0;
} else if (maxOpacity > 99) {
this._maxOpacity = 99;
} else {
this._maxOpacity = maxOpacity;
}
if (this._maxOpacity < this._minOpacity) {
this._maxOpacity = this._minOpacity;
}
}
if (typeof initialOpacity != 'undefined') {
if (initialOpacity > this._maxOpacity) {
this._opacity = this._maxOpacity;
} else if (initialOpacity < this._minOpacity) {
this._opacity = this._minOpacity;
} else {
this._opacity = initialOpacity;
}
}
// See if we're using W3C opacity, MSIE filter, or just
// toggling visiblity
if(typeof element.style.opacity != 'undefined') {
this._updateOpacity = this._updateOpacityW3c;
} else if(typeof element.style.filter != 'undefined') {
// If there's not an alpha filter on the element already,
// add one
if (element.style.filter.indexOf("alpha") == -1) {
// Attempt to preserve existing filters
var existingFilters="";
if (element.style.filter) {
existingFilters = element.style.filter+" ";
}
element.style.filter = existingFilters+"alpha(opacity="+this._opacity+")";
}
this._updateOpacity = this._updateOpacityMSIE;
} else {
this._updateOpacity = this._updateVisibility;
}
this._updateOpacity();
}
// Initiates a fade out
Fadomatic.prototype.fadeOut = function () {
this._isFadeOut = true;
this._beginFade();
}
// Initiates a fade in
Fadomatic.prototype.fadeIn = function () {
this._isFadeOut = false;
this._beginFade();
}
// Makes the element completely opaque, stops any fade in progress
Fadomatic.prototype.show = function () {
this.haltFade();
this._opacity = this._maxOpacity;
this._updateOpacity();
}
// Makes the element completely transparent, stops any fade in progress
Fadomatic.prototype.hide = function () {
this.haltFade();
this._opacity = 0;
this._updateOpacity();
}
// Halts any fade in progress
Fadomatic.prototype.haltFade = function () {
clearInterval(this._intervalId);
}
// Resumes a fade where it was halted
Fadomatic.prototype.resumeFade = function () {
this._beginFade();
}
// Pseudo-private members
Fadomatic.prototype._beginFade = function () {
this.haltFade();
var objref = this;
this._intervalId = setInterval(function() { objref._tickFade(); },Fadomatic.INTERVAL_MILLIS);
}
Fadomatic.prototype._tickFade = function () {
if (this._isFadeOut) {
this._opacity -= this._rate;
if (this._opacity < this._minOpacity) {
this._opacity = this._minOpacity;
this.haltFade();
}
} else {
this._opacity += this._rate;
if (this._opacity > this._maxOpacity ) {
this._opacity = this._maxOpacity;
this.haltFade();
}
}
this._updateOpacity();
}
Fadomatic.prototype._updateVisibility = function () {
if (this._opacity > 0) {
this._element.style.visibility = 'visible';
} else {
this._element.style.visibility = 'hidden';
}
}
Fadomatic.prototype._updateOpacityW3c = function () {
this._element.style.opacity = this._opacity/100;
this._updateVisibility();
}
Fadomatic.prototype._updateOpacityMSIE = function () {
this._element.filters.alpha.opacity = this._opacity;
this._updateVisibility();
}
Fadomatic.prototype._updateOpacity = null;
<%@ page import="org.jivesoftware.openfire.archive.Conversation" %>
<%@ page import="org.jivesoftware.openfire.archive.ConversationManager" %>
<%@ page import="org.jivesoftware.openfire.reporting.graph.GraphEngine" %>
<%@ page import="org.jivesoftware.openfire.reporting.stats.StatisticsModule" %>
<%@ page import="org.jivesoftware.openfire.reporting.stats.StatsAction"%>
<%@ page import="org.jivesoftware.openfire.reporting.stats.StatsViewer"%>
<%@ page import="org.jivesoftware.openfire.user.UserNameManager"%>
<%@ page import="org.jivesoftware.openfire.user.UserNotFoundException"%>
<%@ page import="org.jivesoftware.util.CookieUtils"%>
<%@ page import="org.jivesoftware.util.LocaleUtils"%>
<%@ page import="org.jivesoftware.util.StringUtils"%>
<%@ page import="org.xmpp.packet.JID"%>
<%@ page import="javax.servlet.http.Cookie"%>
<%@ page import="java.net.URLEncoder"%>
<%@ page import="java.util.*"%>
<%@ page import="org.jivesoftware.openfire.XMPPServer" %>
<%@ page import="org.jivesoftware.openfire.plugin.MonitoringPlugin" %>
<%@ taglib uri="http://java.sun.com/jstl/core_rt" prefix="c" %>
<%@ taglib uri="http://java.sun.com/jstl/fmt_rt" prefix="fmt" %>
<%
String sessionKey = StatisticsModule.SESSIONS_KEY;
MonitoringPlugin plugin = (MonitoringPlugin) XMPPServer.getInstance().getPluginManager().getPlugin("monitoring");
ConversationManager conversationManager = (ConversationManager)plugin.getModule(ConversationManager.class);
StatsViewer viewer = (StatsViewer)plugin.getModule(StatsViewer.class);
String timePeriod = "last60minutes";
Cookie timePeriodCookie = CookieUtils.getCookie(request, COOKIE_TIMEPERIOD);
if (timePeriodCookie != null) {
String cookieValue = timePeriodCookie.getValue();
timePeriod = cookieValue;
}
%>
<html>
<head>
<title><fmt:message key="admin.sidebar.statistics.name" /></title>
<meta name="pageID" content="statistics"/>
<script src="/js/prototype.js" type="text/javascript"></script>
<script src="/js/scriptaculous.js" type="text/javascript"></script>
<script src="dwr/engine.js" type="text/javascript" ></script>
<script src="dwr/util.js" type="text/javascript" ></script>
<script src="dwr/interface/Stats.js" type="text/javascript"></script>
<style type="text/css">
.stats-description {
color : black;
font-size : 18px;
font-weight : bold;
}
.stats-current {
color : #555555;
font-size : 20px;
font-weight : bold;
}
.stat {
border : 1px;
border-color : #ccc;
border-style : solid;
background-color : #fffBe2;
-moz-border-radius: 5px;
}
.stat_selected {
border : 1px;
border-color : #f6ab4d;
border-style : solid;
background-color : #fffBc2;
-moz-border-radius: 5px;
}
.stat_enlarge_link {
display: block;
position: relative;
margin: 4px 0px 2px 6px;
padding-left: 18px;
background: url(images/reports_dash-expand-small.gif) no-repeat;
font-size: 11px;
}
.stat_shrink_link {
position: relative;
margin: 4px 0px 2px 6px;
padding-left: 18px;
background: url(images/reports_dash-contract-small.gif) no-repeat;
font-size: 11px;
}
.timeControl {
border : 1px;
border-color : #ccc;
border-style : solid;
background-color : white;
}
.wrapper {
border : 1px;
border-color : #ccc;
border-style : solid;
-moz-border-radius: 5px;
}
.quickstats {
border: 1px solid #cccccc;
border-bottom: none;
}
.quickstats thead th {
background-color: #eeeeee;
text-align: left;
padding: 3px;
border-bottom: 1px solid #cccccc;
}
.quickstats tbody td {
padding: 6px;
border-bottom: 1px solid #cccccc;
font-size: 11px;
}
.conversation {
border-bottom : 1px;
border-top : 0px;
border-right : 0px;
border-left : 0px;
border-color : #ccc;
border-style : solid;
}
.conversation table td {
font-size: 11px;
}
conv-users, conv-messages {
float: left;
display: block;
text-decoration: none;
}
</style>
<style type="text/css">
@import "style/style.css";
</style>
</head>
<body>
<script type="text/javascript">
PeriodicalExecuter.prototype.registerCallback = function() {
this.intervalID = setInterval(this.onTimerEvent.bind(this), this.frequency * 1000);
}
PeriodicalExecuter.prototype.stop = function() {
clearInterval(this.intervalID);
}
DWREngine.setErrorHandler(handleError);
window.onerror = handleError;
function handleError() {
// swallow errors: probably caused by the server being down
}
var peStats = new PeriodicalExecuter(statsUpdater, 30);
var currentTimePeriod = '<%= timePeriod %>';
function statsUpdater() {
try {
Stats.getUpdatedStats(currentTimePeriod, updateStats);
} catch(err) {
// swallow errors
}
}
function changeTimePeriod(period) {
if (currentTimePeriod != period) {
$(currentTimePeriod).className = '';
$(period).className = 'timeControl';
currentTimePeriod = period;
createCookie("<%= COOKIE_TIMEPERIOD %>",currentTimePeriod,1000);
Stats.getUpdatedStats(currentTimePeriod, updateStats);
}
}
function updateStats(stats) {
for (var stat in stats) {
updateTable(stat, stats[stat]);
if (stat == 'conversations' || stat == 'packet_count' || stat == 'sessions') {
updateGraph('sparklines-' + stat, 'stat=' + stat + '&sparkline=true');
} else {
updateGraph('sparklines-' + stat, 'stat=' + stat + '&sparkline=true&color=dark');
}
}
}
function updateTable(id, data) {
$(id + '.low').innerHTML = data.low;
$(id + '.high').innerHTML = data.high;
if ($(id + '.count') != undefined) {
$(id + '.count').innerHTML = data.count;
}
}
function updateGraph(graphid, graphkey) {
var d = new Date();
var t = d.getTime()
$(graphid).src = 'graph?' + graphkey + '&t=' + t + "&timeperiod=" + currentTimePeriod + "&format=png";
statParam = graphkey.split('&');
statName = statParam[0].split('=');
if (isSnapshotDetailVisible && currentSnapshot == statName[1]) {
viewElement = $('snapshot-detail-image');
viewElement.src = 'graph?stat=' + statName[1] + '&t=' + t + '&timeperiod=' + currentTimePeriod + '&width=700&height=250&format=png'
}
}
var lastConversationID = 0;
var getConversationsDelay = 10000;
var insertConversationsDelay = 2000;
var peGetConversations;
var peInsertConversations;
var conversations = new Array();
function startupConversations() {
conversationUpdater();
peGetConversations = new PeriodicalExecuter(conversationUpdater, getConversationsDelay/1000);
}
function conversationUpdater() {
Stats.getNLatestConversations(6, lastConversationID, updateConversations);
}
function updateConversations(data) {
// list of map objects with users, lastactivity, messages keys
if (data.length > 0) {
for (var i=0; i<data.length; i++) {
conversations[conversations.length] = data[i];
}
lastConversationID = conversations[conversations.length -1].conversationid;
}
// adjust insert frequency based on how many are in the queue
if (data.length > 0 && Math.round(getConversationsDelay/(data.length)) > 2000) {
insertConversationsDelay = Math.round(10000/(data.length));
} else {
insertConversationsDelay = 2000;
}
if (peInsertConversations) {
peInsertConversations.stop();
}
peInsertConversations = new PeriodicalExecuter(insertConversation, insertConversationsDelay/1000);
}
function insertConversation() {
if (conversations.length > 0) {
if ($('conversations-scroller-none') != undefined) {
Element.hide('conversations-scroller-none');
Element.show('conversations-scroller');
}
var conversation = conversations.shift();
convTableID = 'conversations-scroller';
var tbody = $(convTableID);
var rows = tbody.getElementsByTagName("div");
for (var i = rows.length-1; i > 0; i--) {
rows[i].innerHTML = rows[i-1].innerHTML;
}
newRow = document.createElement("div");
newRow.setAttribute("class", "conversation");
newRow.setAttribute('conversationid', conversation.conversationid);
users = conversation.users;
userString = '';
for (i=0; i<users.length; i++) {
userString += users[i] + "<br />";
}
newRowHTML =
'<table cellspacing="0" cellpadding="0" border="0">' +
'<tr>' +
'<td style="width:8px;"><img src="images/blank.gif" height="40" width="8" alt="" border="0" /></td>' +
'<td style="width:147px;">' +
userString +
'</td>' +
'<td align="center" style="width:85px;">' +
conversation.lastactivity +
'</td>' +
'<td><img src="images/blank.gif" width="6" alt="" border="0" /></td>' +
'<td align="center" style="width:77px;">' + conversation.messages + '</td>' +
'</tr>' +
'</table>';
newRow.innerHTML = newRowHTML;
if (!isIE()) {
rows[0].style.display = 'none';
rows[0].innerHTML = newRow.innerHTML;
new Effect.Appear(rows[0]);
} else {
rows[0].innerHTML = newRow.innerHTML;
}
}
}
function isIE() {
return navigator.appName.indexOf('Microsoft') != -1;
}
var isSnapshotDetailVisible = false;
var currentSnapshot = '';
function displaySnapshotDetail(snapshot) {
if (!isSnapshotDetailVisible) {
$('snapshot-detail-image').src = 'graph?stat=' + snapshot + '&t=' + t + '&timeperiod=' + currentTimePeriod + '&width=700&height=250&format=png';
Effect.SlideDown('snapshot-detail');
isSnapshotDetailVisible = true;
toggleSnapshotSelected(snapshot);
currentSnapshot = snapshot;
} else {
if ($('snapshot-detail-image').src.indexOf(snapshot) == -1) {
viewElement = $('snapshot-detail-image');
viewElement.style.display = "none";
viewElement.src = '/images/blank.gif';
var i = new Image();
i.onload = function() {
viewElement.src = i.src;
Effect.Appear('snapshot-detail-image');
}
var d = new Date();
var t = d.getTime()
i.src = 'graph?stat=' + snapshot + '&t=' + t + '&timeperiod=' + currentTimePeriod + '&width=700&height=250&format=png';
toggleSnapshotSelected(snapshot);
currentSnapshot = snapshot;
} else {
hideSnapshotDetail();
currentSnapshot = '';
$('table-sessions').className = "stat";
$('table-conversations').className = "stat";
$('table-packet_count').className = "stat";
}
}
}
function toggleSnapshotSelected(selected) {
$('table-' + selected).className = "stat_selected";
$(selected + '-enlarge').className = 'stat_shrink_link';
$(selected + '-enlarge').innerHTML = '<fmt:message key="dashboard.snapshot.enlarge" />';
if (currentSnapshot != '') {
$('table-' + currentSnapshot).className = "stat";
$(currentSnapshot + '-enlarge').className = 'stat_enlarge_link';
$(currentSnapshot + '-enlarge').innerHTML = '<fmt:message key="dashboard.snapshot.shrink" />';
}
}
function hideSnapshotDetail() {
if (isSnapshotDetailVisible) {
$(currentSnapshot + '-enlarge').className = 'stat_enlarge_link';
$(currentSnapshot + '-enlarge').innerHTML = '<fmt:message key="dashboard.snapshot.enlarge" />';
Effect.SlideUp('snapshot-detail');
currentSnapshot = '';
$('table-sessions').className = "stat";
$('table-conversations').className = "stat";
$('table-packet_count').className = "stat";
isSnapshotDetailVisible = false;
}
}
function createCookie(name,value,days) {
if (days) {
var date = new Date();
date.setTime(date.getTime()+(days*24*60*60*1000));
var expires = "; expires="+date.toGMTString();
} else {
var expires = "";
}
document.cookie = name+"="+value+expires+"; path=/";
}
</script>
<div id="instructions">
<table width="756" border="0">
<tr>
<td width="426">
<p><fmt:message key="dashboard.description" /><br /><fmt:message key="dashboard.directions" /></p>
</td>
<td width="330" align="right">
<table class="stat" width="315" cellspacing="0" cellpadding="0">
<tr>
<td colspan="6"><img src="images/blank.gif" height="9" width="1" alt="" /></td>
</tr>
<tr>
<td><img src="images/blank.gif" height="1" width="9" alt="" /></td>
<td><b><fmt:message key="dashboard.timespan" /></b></td>
<td>
<table
<% if (timePeriod.equalsIgnoreCase("last60minutes")) { %>
class="timeControl"
<% }%>
id="last60minutes" style="cursor: pointer;">
<tr onClick="changeTimePeriod('last60minutes'); return false;">
<td><img src="images/icon_clock-1hour.gif"
alt="<fmt:message key="dashboard.timespan.lasthour" />" border="0" /></td>
<td> <fmt:message key="dashboard.timespan.lasthour" /></td>
</tr>
</table>
</td>
<td>
<table
<% if (timePeriod.equalsIgnoreCase("last24hours")) { %>
class="timeControl"
<% }%>
id="last24hours" style="cursor: pointer;">
<tr onClick="changeTimePeriod('last24hours'); return false;">
<td><img src="images/icon_clock-24hour.gif"
alt="<fmt:message key="dashboard.timespan.last24hours" />" border="0" /></td>
<td> <fmt:message key="dashboard.timespan.last24hours" /></td>
</tr>
</table>
</td>
<td>
<table
<% if (timePeriod.equalsIgnoreCase("last7days")) { %>
class="timeControl"
<% }%>
id="last7days" style="cursor: pointer;">
<tr onClick="changeTimePeriod('last7days'); return false;">
<td><img src="images/icon_calendar-week.gif"
alt="<fmt:message key="dashboard.timespan.last7days" />" border="0" /></td>
<td> <fmt:message key="dashboard.timespan.last7days" /></td>
</tr>
</table>
</td>
<td><img src="images/blank.gif" height="1" width="9" alt="" /></td>
</tr>
<tr>
<td colspan="6"><img src="images/blank.gif" height="9" width="1" alt="" /></td>
</tr>
</table>
</td>
</tr>
</table>
<br />
</div>
<table class="wrapper">
<tr>
<td colspan="3">
<div id="snapshot-detail" style="display:none;">
<div>
<table cellpadding="0" cellspacing="0" border="0">
<tr>
<td colspan="2"><img border="0" width="700" height="25" src="images/blank.gif" alt=""/></td>
</tr>
<tr>
<td colspan="2">
<div style="display: block; width: 692px; text-align: right;">
<div class="stat_shrink_link" style="background: none;">
<a href="#" onclick="hideSnapshotDetail(); return false;">
<img src="images/reports_dash-contract-small.gif" alt="" border="0" hspace="2" align="texttop"><fmt:message key="dashboard.snapshot.shrink" />
</a>
</div>
</div>
</td>
</tr>
<tr>
<td><img border="0" width="1" height="250" src="images/blank.gif" alt=""/></td>
<td>
<a href="#" onclick="hideSnapshotDetail(); return false;">
<img border="0" width="700" height="250" src="images/blank.gif" alt="" id="snapshot-detail-image"/></a></td>
</tr>
</table>
</div>
</div>
<img src="images/blank.gif" height="14" width="1" alt="" /></td>
</tr>
<tr>
<td><img src="images/blank.gif" height="1" width="16" alt="" /></td>
<td>
<div id="snapshot">
<table width="705" cellpadding="0" cellspacing="0" border="0">
<tr>
<%
long[] startAndEnd = GraphEngine.parseTimePeriod(timePeriod);
String[] sessionsHighLow = StatsAction.getLowAndHigh("sessions", startAndEnd);
String[] conversationsHighLow = StatsAction.getLowAndHigh("conversations", startAndEnd);
String[] messageHighLow = StatsAction.getLowAndHigh("packet_count", startAndEnd);
String[] serversHighLow = StatsAction.getLowAndHigh("server_sessions", startAndEnd);
String[] mucHighLow = StatsAction.getLowAndHigh("muc_rooms", startAndEnd);
String[] fileTransferHighLow = StatsAction.getLowAndHigh("proxyTransferRate", startAndEnd);
String[] serverBytesHighLow = StatsAction.getLowAndHigh("server_bytes", startAndEnd);
%>
<td align="left">
<table class="stat" width="220" id="table-sessions">
<tr>
<td colspan="5"><img src="images/blank.gif" width="1" height="2" border="0" /></td>
</tr>
<tr>
<td colspan="5" align="center">
<span class="stats-description">
<fmt:message key="dashboard.spotlights.currentusers" />
</span>
</td>
</tr>
<tr>
<td width="13"><img src="images/blank.gif" width="13" height="1" border="0" /></td>
<td align="left" valign="middle" nowrap width="27%">
<fmt:message key="dashboard.spotlights.low" />
<span id="sessions.low"><%= sessionsHighLow[0]%></span>
</td>
<td align="center" width="27%">
<span class="stats-current" id="sessions.count">
<%= (int)viewer.getCurrentValue(StatisticsModule.SESSIONS_KEY)[0] %>
</span>
</td>
<td align="right" valign="middle" nowrap width="27%">
<fmt:message key="dashboard.spotlights.high" />
<span id="sessions.high"><%= sessionsHighLow[1]%></span>
</td>
<td width="13"><img src="images/blank.gif" width="13" height="1" border="0" /></td>
</tr>
<tr>
<td colspan="5" align="center">
<a href="#" onclick="displaySnapshotDetail('sessions'); return false;">
<img width="200" height="50" style="border: 1px solid #b4b4b4;"
src="graph?stat=<%=sessionKey%>&sparkline=true&format=png"
alt="<fmt:message key="dashboard.spotlights.currentusers" />"
id="sparklines-sessions"/><br>
<div align="left" id="sessions-enlarge" class="stat_enlarge_link"><fmt:message key="dashboard.snapshot.enlarge" /></div></a></td>
</tr>
</table>
</td>
<td align="center">
<table class="stat" width="220" id="table-conversations">
<tr>
<td colspan="5"><img src="images/blank.gif" width="1" height="2" border="0" /></td>
</tr>
<tr>
<td colspan="5" align="center">
<span class="stats-description">
<fmt:message key="dashboard.spotlights.activeconversations" />
</span>
</td>
</tr>
<tr>
<td align="center" width="13"><img src="images/blank.gif" width="13" height="1" border="0" /></td>
<td align="left" valign="middle" nowrap width="27%">
<fmt:message key="dashboard.spotlights.low" />
<span id="conversations.low"><%= conversationsHighLow[0]%></span>
</td>
<td align="center" width="27%">
<span class="stats-current" id="conversations.count">
<%= (int)viewer.getCurrentValue(ConversationManager.CONVERSATIONS_KEY)[0] %>
</span>
</td>
<td align="right" valign="middle" nowrap width="27%">
<fmt:message key="dashboard.spotlights.high" />
<span id="conversations.high"><%= conversationsHighLow[1]%></span>
</td>
<td align="center" width="13"><img src="images/blank.gif" width="13" height="1" border="0" /></td>
</tr>
<tr>
<td colspan="5" align="center"><a href="#"
onclick="displaySnapshotDetail('conversations'); return false;"><img
width="200" height="50" style="border: 1px solid #b4b4b4;"
src="graph?stat=conversations&sparkline=true&format=png"
alt="<fmt:message key="dashboard.spotlights.activeconversations" />"
id="sparklines-conversations"/><br>
<div align="left" id="conversations-enlarge" class="stat_enlarge_link"><fmt:message key="dashboard.snapshot.enlarge" /></div></a></td>
</tr>
</table>
</td>
<td align="right">
<table class="stat" width="220" id="table-packet_count">
<tr>
<td colspan="5"><img src="images/blank.gif" width="1" height="2" border="0" /></td>
</tr>
<tr>
<td colspan="5" align="center">
<span class="stats-description">
<fmt:message key="dashboard.spotlights.packetactivity" />
</span>
</td>
</tr>
<tr>
<td align="center" width="13"><img src="images/blank.gif" width="13" height="1" border="0" /></td>
<td align="left" valign="middle" nowrap width="27%">
<fmt:message key="dashboard.spotlights.low" />
<span id="packet_count.low"><%= messageHighLow[0]%></span>
</td>
<td align="center" width="27%">
<span class="stats-current" id="packet_count.count">
<%= (int)viewer.getCurrentValue(StatisticsModule.TRAFFIC_KEY)[0] %>
</span>
</td>
<td align="right" valign="middle" nowrap width="27%">
<fmt:message key="dashboard.spotlights.high" />
<span id="packet_count.high"><%= messageHighLow[1]%></span>
</td>
<td align="center" width="13"><img src="images/blank.gif" width="13" height="1" border="0" /></td>
</tr>
<tr>
<td colspan="5" align="center"><a href="#"
onclick="displaySnapshotDetail('packet_count'); return false;"><img
width="200" height="50" style="border: 1px solid #b4b4b4;"
src="graph?stat=packet_count&sparkline=true&format=png"
alt="<fmt:message key="dashboard.spotlights.packetactivity" />"
id="sparklines-packet_count"/><br>
<div align="left" id="packet_count-enlarge" class="stat_enlarge_link"><fmt:message key="dashboard.snapshot.enlarge" /></div></a></td>
</tr>
</table>
</td>
</tr>
</table>
</div>
<br/>
<!-- Handle SparkLines Stats -->
<table width="705" cellpadding="0" cellspacing="0" border="0">
<tr valign="top">
<td width="371">
<table cellpadding="0" cellspacing="0" border="0" width="371" class="quickstats">
<thead>
<tr>
<th colspan="2">
<fmt:message key="dashboard.quickstats" />
</th>
<th style="font-weight:normal; font-size: 11px;">
<fmt:message key="dashboard.quickstats.low" />
</th>
<th>
</th>
<th style="font-weight:normal; font-size: 11px; padding-right: 8px;">
<fmt:message key="dashboard.quickstats.high" />
</th>
</tr>
</thead>
<tbody>
<tr>
<td><b><%= viewer.getStatistic("server_sessions")[0].getName() %></b></td>
<td width="1%"><img id="sparklines-server_sessions"
src="graph?stat=server_sessions&sparkline=true&color=dark&format=png"
style="border: 1px solid #b4b4b4;" width="180" height="50" /></td>
<td id="server_sessions.low" align="center"><%= serversHighLow[0] %></td>
<td><img src="images/blank.gif" border="0" width="7" height="1" alt="" /></td>
<td id="server_sessions.high" align="center"><%= serversHighLow[1] %></td>
</tr>
<tr>
<td><b><%= viewer.getStatistic("muc_rooms")[0].getName() %></b></td>
<td><img id="sparklines-muc_rooms"
src="graph?stat=muc_rooms&sparkline=true&color=dark&format=png"
style="border: 1px solid #b4b4b4;" width="180" height="50" /></td>
<td id="muc_rooms.low" align="center"><%= mucHighLow[0] %></td>
<td><img src="images/blank.gif" border="0" width="7" height="1" alt="" /></td>
<td id="muc_rooms.high" align="center"><%= mucHighLow[1] %></td>
</tr>
<tr>
<td><b><%= viewer.getStatistic("proxyTransferRate")[0].getName() %></b></td>
<td width="1%"><img id="sparklines-proxyTransferRate"
src="graph?stat=proxyTransferRate&sparkline=true&color=dark&format=png"
style="border: 1px solid #b4b4b4;" width="180" height="50" /></td>
<td id="proxyTransferRate.low" align="center"><%= fileTransferHighLow[0] %></td>
<td><img src="images/blank.gif" border="0" width="7" height="1" alt="" /></td>
<td id="proxyTransferRate.high" align="center"><%= fileTransferHighLow[1] %></td>
</tr>
<tr>
<td><b><%= viewer.getStatistic("server_bytes")[0].getName() %></b><br />
</td>
<td width="1%"><img id="sparklines-server_bytes"
src="graph?stat=server_bytes&sparkline=true&color=dark&format=png"
style="border: 1px solid #b4b4b4;" width="180" height="50" /></td>
<td id="server_bytes.low" align="center"><%= serverBytesHighLow[0] %></td>
<td><img src="images/blank.gif" border="0" width="7" height="1" alt="" /></td>
<td id="server_bytes.high" align="center"><%= serverBytesHighLow[1] %></td>
</tr>
</tbody>
</table>
<br>
</td>
<td width="17"><img src="images/blank.gif" width="17" height="1" border="0" alt="" /></td>
<td width="317">
<table cellpadding="0" cellspacing="0" border="0" width="100%" class="jive-table" style="border: 1px solid #cccccc; border-bottom: none;">
<thead>
<tr>
<th>
<fmt:message key="dashboard.currentconversations" />
(<a href="conversations.jsp"><fmt:message
key="dashboard.currentconversations.details" /></a>)
</th>
</tr>
</thead>
<tr>
<td style="padding:0px 0px 0px 8px;background-color:#bbbbbb">
<table cellspacing="0" cellpadding="0" border="0">
<tr>
<td style="width:147px;color:white;font-size:8pt;">
<b><fmt:message key="dashboard.currentconversations.users" /></b>
</td>
<td align="center" style="width:85px;color:white;font-size:8pt;">
<b><fmt:message key="dashboard.currentconversations.lastactivity" /></b>
</td>
<td></td>
<td align="center" style="width:77px;color:white;font-size:8pt;">
<b><fmt:message key="dashboard.currentconversations.messagecount" /></b>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td style="padding:0px">
<%
// Get handle on the Monitoring plugin
Collection<Conversation> conversations = conversationManager.getConversations();
String displayStyle = "''";
if (conversations.isEmpty()) {
displayStyle = "none";
%>
<div id="conversations-scroller-none" style="padding: 10px;">
<fmt:message key="dashboard.currentconversations.none" />
</div>
<% } %>
<div id="conversations-scroller" style="display:<%= displayStyle %>">
<%
List<Conversation> lConversations = Arrays.asList(
conversations.toArray(new Conversation[conversations.size()]));
Collections.sort(lConversations, conversationComparator);
for (int i = 0; i < 6; i++) {
String participantNames = "";
String activityTime = "";
String messageCount = "";
if (lConversations.size() > i) {
Conversation conversation = lConversations.get(i);
if (conversation.getRoom() == null) {
Collection<JID> participants = conversation.getParticipants();
for (JID jid : participants) {
String identifier = jid.toBareJID();
try {
identifier = UserNameManager.getUserName(jid, jid.toBareJID());
} catch (UserNotFoundException e) {
// Ignore
}
participantNames +=
StringUtils.abbreviate(identifier, 20) +
"<br />";
}
} else {
// Display "group conversation" with a link to the room occupants
/*participantNames = LocaleUtils.getLocalizedString(
"archive.group_conversation", "monitoring", Arrays.asList(
"<a href='../../muc-room-occupants.jsp?roomName=" +
URLEncoder.encode(conversation.getRoom().getNode(),
"UTF-8") + "'>", "</a>"));*/
participantNames = LocaleUtils.getLocalizedString("dashboard.group_conversation", "monitoring");
participantNames += "<br/>";
participantNames += "(<i>" + LocaleUtils.getLocalizedString("muc.room.summary.room") + ": <a href='../../muc-room-occupants.jsp?roomName=" + URLEncoder.encode(conversation.getRoom().getNode(),"UTF-8") + "'>" + conversation.getRoom().getNode() + "</a></i>)";
}
activityTime =
StatsAction.formatTimeLong(conversation.getLastActivity());
messageCount = Integer.toString(conversation.getMessageCount());
}
%>
<div class="conversation"
<% if (i == 3) {%>style="opacity: 0.7;filter:alpha(opacity=10);" <%}%>
<% if (i == 4) {%>style="opacity: 0.4;filter:alpha(opacity=10);" <%}%>
<% if (i == 5) {%>style="opacity: 0.2;filter:alpha(opacity=10);border-bottom:0px;" <%}%>
>
<table cellspacing="0" cellpadding="0" border="0">
<tr>
<td style="width:8px;"><img src="images/blank.gif" height="38" width="8" alt="" border="0" /></td>
<td style="width:147px;">
<%= participantNames %>
</td>
<td align="center" style="width:85px;"><%= activityTime %></td>
<td><img src="images/blank.gif" width="6" alt="" border="0" /></td>
<td align="center" style="width:77px;"><%= messageCount %></td>
</tr>
</table>
</div>
<% } %>
</div>
</td>
</tr>
</table>
<br>
</td>
</tr>
<tr>
<td></td>
<td></td>
</tr>
</table>
</td>
<td><img src="images/blank.gif" height="1" width="16" alt="" /></td>
</tr>
</table>
<br>
<script type="text/javascript">
window.onload = startupConversations;
</script>
</body>
</html>
<%!
public static final String COOKIE_TIMEPERIOD = "openfire-dashboard-timeperiod";
/**
* Sorts conversations by last modified time
*/
final Comparator<Conversation> conversationComparator = new Comparator<Conversation>() {
public int compare(Conversation conv1, Conversation conv2) {
return conv2.getLastActivity().compareTo(conv1.getLastActivity());
}
};
%>
\ No newline at end of file
<%@ page import="org.jivesoftware.openfire.reporting.stats.StatsViewer" %>
<%@ page import="org.jivesoftware.util.CookieUtils" %>
<%@ page import="org.jivesoftware.util.JiveGlobals"%>
<%@ page import="org.jivesoftware.util.ParamUtils"%>
<%@ page import="org.jivesoftware.openfire.stats.Statistic"%>
<%@ page import="javax.servlet.http.Cookie"%>
<%@ page import="java.util.Arrays"%>
<%@ page import="java.util.Collections"%>
<%@ page import="java.util.Comparator"%>
<%@ page import="java.util.List"%>
<%@ page import="org.jivesoftware.openfire.plugin.MonitoringPlugin" %>
<%@ page import="org.jivesoftware.openfire.XMPPServer" %>
<%@ taglib uri="http://java.sun.com/jstl/core_rt" prefix="c" %>
<%@ taglib uri="http://java.sun.com/jstl/fmt_rt" prefix="fmt" %>
<%
List<String> statList = Arrays.asList(getStatsViewer().getAllHighLevelStatKeys());
Collections.sort(statList, statisticComporator);
String dateRangeType = ParamUtils.getParameter(request,"dateRangeType");
String dateRangePreset = ParamUtils.getParameter(request,"dateRangePreset");
String fromDateParam = ParamUtils.getParameter(request,"fromDate");
String toDateParam = ParamUtils.getParameter(request,"toDate");
String timePeriod = "last60minutes";
Cookie timePeriodCookie = CookieUtils.getCookie(request, COOKIE_TIMEPERIOD);
if (timePeriodCookie != null) {
String cookieValue = timePeriodCookie.getValue();
if (cookieValue.startsWith("this") || cookieValue.startsWith("last")) {
dateRangeType = "preset";
dateRangePreset = cookieValue;
}
else {
String[] dates = cookieValue.split("to");
dateRangeType = "specific";
if (dates.length == 2) {
fromDateParam = dates[0];
toDateParam = dates[1];
}
}
timePeriod = cookieValue;
}
// Set parameter defaults
if (dateRangeType == null) {
dateRangeType = "preset";
}
%>
<fmt:setLocale value="<%= JiveGlobals.getLocale().getLanguage() %>"/>
<html>
<head>
<title><fmt:message key="allreports.title" /></title>
<meta name="pageID" content="stats-reporter"/>
<script src="/js/prototype.js" type="text/javascript"></script>
<script src="/js/effects.js" type="text/javascript"></script>
<script src="/js/scriptaculous.js" type="text/javascript"></script>
<style type="text/css">
.dateerror { font-weight: bold; color:red;}
.dateerrorinput {background-color:red};
.datenormal { font-weight: normal; color:black;}
</style>
<style type="text/css">@import url( /js/jscalendar/calendar-win2k-cold-1.css );</style>
<script type="text/javascript" src="/js/jscalendar/calendar.js"></script>
<script type="text/javascript" src="/js/jscalendar/i18n.jsp"></script>
<script type="text/javascript" src="/js/jscalendar/calendar-setup.js"></script>
<script type="text/javascript">
var datesAreValid = true;
var stats = {};
<%
for (String statKey : statList) { %><% Statistic stat = getStatsViewer().getStatistic(statKey)[0]; %>
stats["<%= statKey %>"] = {"name":"<%= stat.getName() %>","description":"<%= stat.getDescription() %>"};
<% } %>
var currentStat = '<%= STAT_DEFAULT %>';
var currentTimePeriod = '<%= timePeriod %>';
function viewStat(stat) {
timePeriod = '';
timePeriodName = '';
if ($('drt01').checked == true) {
// get a preset value
drselect = $('dateRangePreset');
timePeriod = drselect[drselect.selectedIndex].value;
timePeriodName = drselect[drselect.selectedIndex].text;
datesAreValid = true;
} else {
// get a date range
validateStartAndEndDate();
if (datesAreValid) {
timePeriod = $('fromDate').value + 'to' + $('toDate').value;
timePeriodName = $('fromDate').value + ' to ' + $('toDate').value;
} else {
return;
}
}
if (datesAreValid && (stat != currentStat || timePeriod != currentTimePeriod)) {
var viewElement = $('viewer');
var pdfViewElement = $('pdfviewer');
var pdfViewAllElement = $('pdfviewerall');
viewElement.style.display = "none";
var i = new Image();
i.onload = function() {
viewElement.src = i.src;
pdfViewElement.href = i.src + "&pdf=true";
pdfViewAllElement.href = i.src + "&pdf=all";
$('graph-header').innerHTML = stats[stat].name + ': ' + timePeriodName;
$('graph-description').innerHTML = '<b>' + stats[stat].name + '</b><br /><br />' +
stats[stat].description;
Effect.Appear('viewer');
currentStat = stat;
currentTimePeriod = timePeriod;
createCookie("<%= COOKIE_TIMEPERIOD %>",currentTimePeriod,1000);
}
var d = new Date();
var t = d.getTime()
i.src = "graph?stat=" + stat + "&date=" + t + '&timeperiod=' + timePeriod + '&width=500&height=250';
}
}
function createCookie(name,value,days) {
if (days) {
var date = new Date();
date.setTime(date.getTime()+(days*24*60*60*1000));
var expires = "; expires="+date.toGMTString();
}
else {
var expires = "";
}
document.cookie = name+"="+value+expires+"; path=/";
}
function writeTimePeriod() {
if ($('drt01').checked == true) {
drselect = $('dateRangePreset');
document.write(drselect[drselect.selectedIndex].text);
}
else {
// get a date range
if ($('fromDate').value != '' && $('toDate').value != '') {
document.write($('fromDate').value + ' to ' + $('toDate').value);
}
}
}
function checkPreset() {
document.statsForm.dateRangeType[0].checked=true;
document.statsForm.dateRangePreset.disabled = false;
document.statsForm.fromDate.disabled = true;
document.statsForm.toDate.disabled = true;
viewStat(currentStat);
}
function checkSpecific() {
document.statsForm.dateRangeType[1].checked=true
document.statsForm.fromDate.disabled = false;
document.statsForm.toDate.disabled = false;
document.statsForm.dateRangePreset.disabled = true;
viewStat(currentStat);
}
function validateStartAndEndDate() {
if ($('fromDate').value != '' && $('toDate').value != '') {
fromDate = $('fromDate').value;
toDate = $('toDate').value;
if (!isValidDate(fromDate)) {
$('fromDateTitle').className = 'dateerror';
$('fromDate').className = 'dateerrorinput';
datesAreValid = false;
return;
}
if (!isValidDate(toDate)) {
$('toDateTitle').className = 'dateerror';
$('toDate').className = 'dateerrorinput';
datesAreValid = false;
return;
}
if (!isValidCombination(fromDate, toDate)) {
$('toDateTitle').className = 'dateerror';
$('fromDateTitle').className = 'dateerror';
$('toDate').className = 'dateerrorinput';
$('fromDate').className = 'dateerrorinput';
datesAreValid = false;
return;
}
datesAreValid = true;
$('toDate').className = '';
$('fromDate').className = '';
$('toDateTitle').className = 'datenormal';
$('fromDateTitle').className = 'datenormal';
return;
}
else {
datesAreValid = false;
return;
}
}
function isValidCombination(startdate, enddate) {
if (!getDate(startdate) || !getDate(enddate)) {
return false;
}
else {
return getDate(startdate) < getDate(enddate);
}
}
function getDate(datestring) {
dateSplit = datestring.split('/');
if (dateSplit.length < 3) {
return false;
}
var monthLength = new Array(31,28,31,30,31,30,31,31,30,31,30,31);
var day = parseInt(dateSplit[1]);
var month = parseInt(dateSplit[0]);
var year = parseInt(dateSplit[2]);
if (!day || !month || !year)
return false;
year = year + 2000;
if (year/4 == parseInt(year/4))
monthLength[1] = 29;
if (day > monthLength[month-1])
return false;
monthLength[1] = 28;
var now = new Date();
now = now.getTime(); //NN3
var dateToCheck = new Date();
dateToCheck.setYear(year);
dateToCheck.setMonth(month-1);
dateToCheck.setDate(day);
var checkDate = dateToCheck.getTime();
if (now < checkDate) {
return false;
}
else {
return checkDate;
}
}
function isValidDate(datestring) {
d = getDate(datestring);
if (!d) {
return false;
}
else {
var now = new Date();
now = now.getTime(); //NN3
if (now < d) {
return false;
} else {
return true;
}
}
}
</script>
<style type="text/css">
@import "style/style.css";
</style>
</head>
<body>
<table cellpadding="0" cellspacing="0" border="0" width="753">
<tr>
<td width="180" valign="top">
<table cellpadding="0" cellspacing="0" border="0" width="180" class="jive-table">
<thead>
<tr>
<th>
<fmt:message key="allreports.daterange" />
</th>
</tr>
</thead>
<tr style="border-bottom: none"><form action="" id="statsForm" name="statsForm">
<td colspan="2" valign="top" width="180">
<table>
<tr>
<td width="1%" valign="top">
<input type="radio" name="dateRangeType" value="preset" id="drt01"
onclick="checkPreset();"
<%= ("preset".equals(dateRangeType) ? "checked" : "") %>>
</td>
<td width="99%" valign="top">
<label for="drt01"><fmt:message key="allreports.daterange.preset" /></label></td>
</tr>
<tr>
<td colspan="2" align="right">
<select size="1" name="dateRangePreset" id="dateRangePreset" onchange="checkPreset();">
<option value="last60minutes"
<%= ("last60minutes".equals(dateRangePreset) ? "selected" : "") %>
><fmt:message key="allreports.daterange.preset.last60minutes" /> </option>
<option value="last24hours"
<%= ("last24hours".equals(dateRangePreset) ? "selected" : "") %>
><fmt:message key="allreports.daterange.preset.last24hours" /> </option>
<option value="thisweek"
<%= ("thisweek".equals(dateRangePreset) ? "selected" : "") %>
><fmt:message key="allreports.daterange.preset.thisweek" /> </option>
<option value="last7days"
<%= ("last7days".equals(dateRangePreset) ? "selected" : "") %>
><fmt:message key="allreports.daterange.preset.last7days" /> </option>
<option value="lastweek"
<%= ("lastweek".equals(dateRangePreset) ? "selected" : "") %>
><fmt:message key="allreports.daterange.preset.lastweek" /> </option>
<option value="thismonth"
<%= ("thismonth".equals(dateRangePreset) ? "selected" : "") %>
><fmt:message key="allreports.daterange.preset.thismonth" /> </option>
<option value="lastmonth"
<%= ("lastmonth".equals(dateRangePreset) ? "selected" : "") %>
><fmt:message key="allreports.daterange.preset.lastmonth" /> </option>
<option value="last3months"
<%= ("last3months".equals(dateRangePreset) ? "selected" : "") %>
><fmt:message key="allreports.daterange.preset.last3months" /> </option>
</select>
</td>
</tr>
<tr valign="top">
<td width="1%" valign="top">
<input type="radio" name="dateRangeType" value="specific" id="drt02"
onclick="checkSpecific();"
<%= ("specific".equals(dateRangeType) ? "checked" : "") %>>
</td>
<td width="99%"><label for="drt02"><fmt:message key="allreports.daterange.specific" /></label></td>
</tr>
<tr valign="top">
<td colspan="2" align="right">
<table border="0" cellspacing="0" cellpadding="0">
<tr>
<td align="right" id="fromDateTitle" class="datenormal">
<fmt:message key="allreports.daterange.specific.startdate" />
</td>
<td>
<input type="text" size="10" name="fromDate" id="fromDate" maxlength="10"
onclick="checkSpecific();"
onchange="viewStat(currentStat);"
<%= ("specific".equals(dateRangeType) ? "" : "disabled") %>
value="<%= (fromDateParam != null ? fromDateParam : "") %>"
>
</td>
<td>
&nbsp;<img src="images/icon_calendarpicker.gif" id="fromDateCal" />
</td>
</tr>
<tr>
<td align="right" id="toDateTitle" class="datenormal">
<fmt:message key="allreports.daterange.specific.enddate" />
</td>
<td>
<input type="text" size="10" name="toDate" id="toDate" maxlength="10"
onclick="checkSpecific();"
onchange="viewStat(currentStat);"
<%= ("specific".equals(dateRangeType) ? "" : "disabled") %>
value="<%= (toDateParam != null ? toDateParam : "") %>">
</td>
<td>
&nbsp;<img src="images/icon_calendarpicker.gif" id="toDateCal" />
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</form></tr>
</table>
<br />
<table cellpadding="0" cellspacing="0" border="0" width="180" class="jive-table"
style="border-bottom: 1px solid #bbb;">
<thead>
<tr>
<th>
<fmt:message key="allreports.selectreport" />
</th>
</tr>
</thead>
<tbody>
<% for (String stat : statList) { %>
<tr
id="statDetail<%= stat %>"
<% if (stat.equalsIgnoreCase(STAT_DEFAULT)) { %>
class="allreports_report_selected"
<% }
else { %>
class="allreports_report_default"
<% } %>
>
<td valign="top"
onclick="viewStat('<%= stat %>');toggleSelected('<%= stat %>'); return false;"
onmouseover="toggleMouseOver('<%= stat %>');"
onmouseout="toggleMouseOver('<%= stat %>');">
<%= getStatsViewer().getStatistic(stat)[0].getName()%>
</td>
</tr>
<% } %>
<script type="text/javascript">
var selectedStat = '<%= STAT_DEFAULT %>';
function toggleSelected(statname) {
$('statDetail' + selectedStat).className = 'allreports_report_default';
$('statDetail' + statname).className = 'allreports_report_selected';
selectedStat = statname;
}
function toggleMouseOver(statname) {
if (statname != selectedStat) {
if ($('statDetail' + statname).className == 'allreports_report_hover') {
$('statDetail' + statname).className = 'allreports_report_default';
}
else {
$('statDetail' + statname).className = 'allreports_report_hover';
}
}
}
</script>
</tbody>
</table>
<br />
<table cellpadding="0" cellspacing="0" border="0" width="180" class="jive-table">
<thead>
<tr>
<th style="border-bottom:none">
<table cellspacing='0' cellpadding='0' border='0' align="center" style="padding:0px;border-bottom:none">
<tr>
<td colspan="2" style="padding:0px;border-bottom:none; font-size: 12px;">
<b><fmt:message key="allreports.download.allreports" /></b>
</td>
<td style="padding:0px;border-bottom:none"><img
src="images/blank.gif" alt="" border="0" height="1" width="1" /></td>
</tr>
</table>
</th>
</tr>
<tr>
<th style="border-bottom : 1px #ccc solid;">
<table cellspacing='0' cellpadding='0' border='0' align="center" style="padding:0px;border-bottom:none">
<tr>
<td style="padding:0px;border-bottom:none; font-size: 12px;"><img
src="images/icon_pdf.gif"
alt="<fmt:message key="allreports.download.allreports.pdf.format" />" border="0" /></td>
<td style="padding:0px;border-bottom:none; font-size: 12px;">&nbsp;
<a target="_blank" href="graph?stat=<%= STAT_DEFAULT %>&timeperiod=<%= timePeriod %>&pdf=all"
id="pdfviewerall"><fmt:message key="allreports.download.allreports.pdf" /></a></td>
</tr>
</table>
</th>
</thead>
</table>
</td>
<td width="20">&nbsp;</td>
<td valign="top" width="553">
<table cellpadding="0" cellspacing="0" border="0" width="553" class="jive-table">
<thead>
<tr>
<th width="70%" id="graph-header">
<%= getStatsViewer().getStatistic(STAT_DEFAULT)[0].getName() %>:
<script type="text/javascript">writeTimePeriod();</script>
</th>
<th style="text-align:right; border-bottom : 1px #ccc solid; padding:0px;" nowrap>
<table cellspacing='0' cellpadding='0' border='0' align="right" style="padding:0px;border-bottom:none;">
<tr>
<td style="padding:0px;border-bottom:none;font-size: 11px;" nowrap>
<fmt:message key="allreports.download.singlereport" />&nbsp;&nbsp;</td>
<td style="padding:0px;border-bottom:none;"><img
src="images/icon_pdf.gif" alt="PDF Format" border="0" /></td>
<td style="padding:0px;border-bottom:none;font-size: 11px;">&nbsp;
<a target="_blank" href="graph?stat=<%= STAT_DEFAULT %>&timeperiod=<%= timePeriod %>&pdf=true"
id="pdfviewer"><fmt:message key="allreports.download.singlereport.pdf" /></a></td>
<td style="padding:0px;border-bottom:none;"><img
src="images/blank.gif" alt="" border="0" height="1" width="8" /></td>
</tr>
</table>
</th>
</tr>
</thead>
<tr>
<td colspan="2" style="padding:0px; border-bottom:0px"><img src="/images/blank.gif" alt="" border="0" height="1" width="500" /></td>
</tr>
<tr>
<td colspan="2">
<table class="noclass">
<tr>
<td><img src="/images/blank.gif" alt="" border="0" height="280" width="1" /></td>
<td><img id="viewer" src="graph?stat=<%= STAT_DEFAULT %>&timeperiod=<%= timePeriod %>&width=500&height=250" border="0" /></td>
</tr>
<!-- <tr>
<td colspan="2">
<div id="graph-description_NEW" style="padding: 10px 10px 20px 15px;">
<%= getStatsViewer().getStatistic(STAT_DEFAULT)[0].getDescription() %>
</div>
</td>
</tr> -->
</table>
</td>
</tr>
</table>
<br />
<table cellpadding="0" cellspacing="0" border="0" width="553" class="jive-table">
<thead>
<tr>
<th style="border-bottom : 1px #ccc solid">
<fmt:message key="allreports.reportinformation" />
</th>
</tr>
</thead>
<tr>
<td align="left" id="graph-description">
<b><%= getStatsViewer().getStatistic(STAT_DEFAULT)[0].getName() %></b><br /><br />
<%= getStatsViewer().getStatistic(STAT_DEFAULT)[0].getDescription() %>
</td>
</tr>
</table>
</td>
</tr>
</table>
<br>
<script type="text/javascript" >
Calendar.setup(
{
inputField : "fromDate", // ID of the input field
ifFormat : "%m/%d/%y", // the date format
button : "fromDateCal", // ID of the button
onUpdate : viewStat
});
Calendar.setup(
{
inputField : "toDate", // ID of the input field
ifFormat : "%m/%d/%y", // the date format
button : "toDateCal", // ID of the button
onUpdate : viewStat
});
</script>
</body>
</html>
<%!
private StatsViewer statsViewer;
public static final String STAT_DEFAULT = "sessions";
public static final String COOKIE_TIMEPERIOD = "openfire-reporting-timeperiod";
public StatsViewer getStatsViewer() {
if (statsViewer == null) {
MonitoringPlugin plugin = (MonitoringPlugin) XMPPServer.getInstance().getPluginManager().getPlugin("monitoring");
statsViewer = (StatsViewer) plugin.getModule(StatsViewer.class);
}
return statsViewer;
}
/**
* Sorts Statistics by Name.
*/
final Comparator<String> statisticComporator = new Comparator<String>() {
public int compare(String stat1, String stat2) {
String statName1 = getStatsViewer().getStatistic(stat1)[0].getName();
String statName2 = getStatsViewer().getStatistic(stat2)[0].getName();
return statName1.toLowerCase().compareTo(statName2.toLowerCase());
}
};
%>
\ No newline at end of file
/* alertbox styles (errors and warnings) */
/* global alertbox styles */
.alertbox {
display: block;
position: relative;
-moz-border-radius: 3px;
padding: 0px 0px 0px 0px;
margin: 0px 0px 0px 0px;
width: 735px;
height: auto;
font-size: .85em;
color: #000000;
overflow: hidden;
}
.alertbox h3 {
display: block;
font-size: 1.1em;
margin: 15px 0px 5px 0px;
padding: 0px;
}
.alertbox p {
font-size: .9em;
margin: 3px 0px 10px 0px;
}
.alertbox a {
color: #000000;
}
.alertbox a:hover {
text-decoration: underline;
}
.linkbtn {
display: block;
position: relative;
float: right;
z-index: 100;
top: 10px;
right: 10px;
}
.linkbtn a {
font-weight: bold;
text-decoration: none;
}
.linkbtn a:hover {
text-decoration: underline;
}
.licenseIcon {
display: block;
position: absolute;
width: 22px;
height: 22px;
margin: 12px 12px 20px 12px;
padding: 0px 0px 0px 0px;
}
.licenseIconsmall {
display: block;
position: relative;
float: left;
width: 16px;
height: 16px;
margin: 0px 5px 4px 0px;
padding: 0px 0px 0px 0px;
}
.licenseContent {
display: block;
position: relative;
clear: none;
top: 0px;
left: 50px;
width: 670px;
}
/* alertboxsmall styles */
.alertboxsmall {
display: block;
position: relative;
float: right;
-moz-border-radius-topleft: 3px;
-moz-border-radius-bottomleft: 3px;
padding: 0px 0px 0px 0px;
margin: -12px -18px 0px 0px;
width: 100px;
font-size: .85em;
color: #000000;
}
.alertboxsmall div.licenseContentsmall {
margin: 5px;
line-height: 1.2em;
}
.alertboxsmall div.licenseContentsmall a {
display: block;
font-weight: bold;
text-decoration: none;
color: #A85E00;
padding: 3px 0px 0px 0px;
}
.alertboxsmall div.licenseContentsmall a:hover {
text-decoration: underline;
}
/* error-specific alertbox styles */
.licenseError {
border: 1px solid #BB8888;
background: #EDB9B1 url(../images/certificateimg_error.gif) no-repeat bottom right;
}
.licenseError div.licenseIcon {
background: url(../images/icon_error.gif) no-repeat;
}
.licenseError h3 {
color: #8C0900;
}
.licenseError div.linkbtn a {
color: #8C0900;
}
/* warning-specific alertbox styles */
.licenseWarning {
border: 1px solid #D9B04C;
background: #FFE9B2 url(../images/certificateimg_warning.gif) no-repeat bottom right;
margin-bottom: 10px;
}
.licenseWarning div.licenseIcon {
background: url(../images/icon_warning.gif) no-repeat;
}
.licenseWarning h3 {
color: #CA7303;
}
.licenseWarning div.linkbtn a {
color: #A85E00;
}
.licenseWarningsmall div.licenseIconsmall {
background: url(../images/icon_warning-small.gif) no-repeat;
}
.licenseWarningsmall {
border: 1px solid #D9B04C;
background: #FFE9B2;
}
/* license page specific styles */
.licenseHeader {
display: block;
width: 732px;
overflow: hidden;
border: 1px solid #BBBBBB;
}
.licenseHeader strong {
display: block;
margin: 5px 10px 5px 10px;
}
.licenseBody {
display: block;
width: 732px;
overflow: hidden;
background-color: #FFFFFF;
border: 1px solid #BBBBBB;
border-top: none;
margin-bottom: 25px;
}
.licenseBody table {
width: 90%;
margin: 10px;
}
.licenseBody div.licenseContents {
width: 710px;
margin: 13px;
}
#enterLicenseLink {
display: block;
}
#enterLicenseLink a.licenseLink {
background: url(/images/add-16x16.gif) no-repeat;
padding: 0px 0px 0px 20px;
}
#enterLicense {
display: block;
}
#enterLicense a.cancelLink {
background: url(/images/forbidden-16x16.gif) no-repeat;
padding: 0px 0px 0px 20px;
}
.licenseBody form {
display: block;
width: 680px;
margin: 0px 0px 15px 0px;
padding: 0px;
text-align: right;
}
.licenseBody form textarea {
width: 680px;
height: 185px;
margin-bottom: 5px;
font-family: "Courier New", Courier, monospace;
font-size: 13px;
}
.licenseFormError {
width: 680px;
text-align:left;
color: #8C0900;
font-weight: bold;
padding: 10px 0px 10px 0px;
}
strong.erroritem {
color: #6E1F1F;
}
/* general page elements */
table.settingsTable {
display: block;
border: 1px solid #BBBBBB;
margin: 5px 0px 15px 0px;
}
table.settingsTable thead th {
border-bottom: 1px solid #BBBBBB;
padding: 3px 8px 3px 12px;
font-weight: bold;
text-align: left;
}
table.settingsTable tbody tr td {
padding: 5px 10px 5px 15px;
}
table.settingsTable tbody tr td p {
padding: 10px 0px 5px 0px;
}
table.settingsTable tr {
padding: 0px 0px 10px 0px;
}
/* Permit Client specific styles */
/* modified the fieldset from existing */
/* condensed border to one entry, pulled "width: 95%;" */
fieldset {
display: block;
position: relative;
-moz-border-radius: 3px;
border: 1px solid #CCCCCC;
padding: 2px 0px 0px 0px;
margin: 0px 0px 0px 0px;
}
fieldset legend {
color: #000000;
margin-left: 15px;
}
.clientscontent {
display: block;
position: relative;
margin: 0px 15px 0px 15px;
}
.permitclientbox {
display: block;
position: relative;
border: 1px solid #DCDCDC;
-moz-border-radius: 3px;
padding: 3px 2px 2px 2px;
margin: 10px 0px 10px 0px;
width: 600px;
height: auto;
color: #000000;
overflow: hidden;
background-color: #F4F4F4;
line-height: 1.6em;
}
.permitclientbox table tr td {
line-height: 1.8em;
}
.permitclientbox span {
font-size: .85em;
margin-left: 4px;
}
.horizontalrule {
display: block;
height: 1px;
background-color: #DCDCDC;
margin-top: 2px;
margin-bottom: 14px;
overflow: hidden;
clear: both;
}
.permitclientActive {
background-color: #FFFBE2;
}
.specifyclients {
display: block;
position: relative;
margin: 0px 15px 18px 15px;
width: auto;
padding: 0px;
}
/* Reports specific styles */
tr.allreports_report_default td {
padding: 3px 5px 3px 12px;
cursor: pointer;
background-color: #ffffff;
border: none;
border-top: 1px solid #ffffff;
border-bottom: 1px solid #ffffff;
font-size: 12px;
color: #D76C0D;
}
tr.allreports_report_selected td {
padding: 3px 5px 3px 12px;
cursor: pointer;
border: none;
border-top: 1px solid #bbbbbb;
border-bottom: 1px solid #bbbbbb;
background: #edeed7 url('../images/reports_selected-arrow.gif') no-repeat top left;
background-position: 4px 6px;
font-size: 12px;
color: #2f302b;
font-weight: bold;
}
tr.allreports_report_hover td {
padding: 3px 5px 3px 12px;
cursor: pointer;
background-color: #f4f4f4;
border: none;
border-top: 1px solid #bbbbbb;
border-bottom: 1px solid #bbbbbb;
font-size: 12px;
color: #D76C0D;
}
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