Commit d8e59128 authored by Axel Brand's avatar Axel Brand Committed by daeva

GoJara Branch for Refactoring

git-svn-id: http://svn.igniterealtime.org/svn/repos/openfire/branches/plugins@13529 b35dd754-fafc-0310-a699-88a17e54d16e
parent c2596773
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html>
<head>
<title>GoJara 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>
GoJara Plugin Changelog
</h1>
<p><b>1.2.5</b> -- Feb 22, 2013</p>
<ul>
<li>Fixed bypassing the access control</li>
<li>Fixed InvalidArgumentException when invalid JID is in the packet</li>
</ul>
<p><b>1.2.4</b> -- Feb 21, 2013</p>
<ul>
<li>Fixed access control when server hostname and xmpp domain are not the same</li>
</ul>
<p><b>1.2.3</b> -- Feb 16, 2013</p>
<ul>
<li>Fixed: Included json.jar is now Java 5 compitabile [<a href="http://issues.igniterealtime.org/browse/OF-619">OF-619</a>]</li>
<li>No longer in alpha status</li>
</ul>
<p><b>1.2.2 Alpha</b> -- Feb 17, 2012</p>
<ul>
<li>Fixed: Remove remote contacts from roster when deleting gateway registration [<a href="http://issues.igniterealtime.org/browse/OF-570">OF-570</a>]</li>
</ul>
<p><b>1.2 Alpha</b> -- Feb 17, 2012</p>
<ul>
<li>Added feature: Auto response to jabber:iq:last</li>
<li>Added: Icon</li>
<li>Fixed: jQuery conflicts with OF's prototype.js</li>
</ul>
<p><b>1.1 Alpha</b> -- Jan 6, 2012</p>
<ul>
<li>Added feature: Limit gateways to specific user group</li>
<li>Capture packets to create statistics</li>
<li>Added live logging</li>
<li>Renamed to GoJara</li>
</ul>
<p><b>1.0 Alpha</b> -- Dec 12, 2011</p>
<ul>
<li>Initial release. </li>
</ul>
</body>
</html>
\ No newline at end of file
Name | Version | License
---------------------------------------------------------------------------------------------------------------------
json.jar | 50c3afb2166798c5de0896b90659d8a2b2f8fcec | http://www.json.org/license.html
Notes:
json.jar has been compiled using the code that is made available at https://github.com/douglascrockford/JSON-java
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<plugin>
<!-- Main plugin class -->
<class>org.jivesoftware.openfire.plugin.gojara.base.RemoteRosterPlugin</class>
<name>GoJara</name>
<description>ProtoXEP-xxxx: Remote Roster Management support</description>
<author>Holger Bergunde and Daniel Henninger</author>
<version>1.2.5</version>
<date>02/22/2013</date>
<databaseKey>gojara</databaseKey>
<databaseVersion>0</databaseVersion>
<minServerVersion>3.7.0</minServerVersion>
<!-- Admin console entries -->
<adminconsole>
<tab id="tab-server">
<sidebar id="sidebar-server-settings">
<item id="remoteRoster" name="${rr.summary.title}"
url="rr-main.jsp"
description="${rr.summary.title}" />
</sidebar>
</tab>
</adminconsole>
</plugin>
\ No newline at end of file
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html>
<head>
<title>GoJara 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>
GoJara Plugin Readme
</h1>
<h2>Overview</h2>
<p>
This plugin will implement the proto-XEP, Remote Roster, discussed
<a href="http://jkaluza.fedorapeople.org/remote-roster.html">here</a>.
Basically it's purpose is to allow an external component full
control over the part of a roster it's assigned. For example,
if the AIM transport is at aim.example.org, you can allow that transport
to make whatever modifications it needs without involving the end user.
This is especially useful so that a user does not get flooded with
subscription requests upon registration. This plugin also implements
a few minor extensions that Kraken used to implement for Spark, though
those are optional.
</p>
<h2>Installation</h2>
<p>Copy remoteRoster.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
remoteRoster.jar file over the existing file.</p>
<h2>Configuration</h2>
<p>The GoJara plugin can be configured under "Server"-"Server Settings"-"GoJara".</p>
</body>
</html>
\ No newline at end of file
CREATE TABLE ofGojaraStatistics (
logID bigint(20) NOT NULL AUTO_INCREMENT,
messageDate bigint(20) NOT NULL,
messageType tinytext NOT NULL,
fromJID text NOT NULL,
toJID text NOT NULL,
component text NOT NULL,
PRIMARY KEY (logID)
);
INSERT INTO ofVersion (name, version) VALUES ('gojara', 0);
CREATE TABLE ofGojaraStatistics (
logID Integer Identity NOT NULL,
messageDate BIGINT NOT NULL,
messageType VARCHAR(255) NOT NULL,
fromJID VARCHAR(255) NOT NULL,
toJID VARCHAR(255) NOT NULL,
component VARCHAR(255) NOT NULL,
PRIMARY KEY (logID)
);
INSERT INTO ofVersion (name, version) VALUES ('gojara', 0);
CREATE TABLE ofGojaraStatistics (
logID bigint(20) NOT NULL AUTO_INCREMENT,
messageDate bigint(20) NOT NULL,
messageType tinytext NOT NULL,
fromJID text NOT NULL,
toJID text NOT NULL,
component text NOT NULL,
PRIMARY KEY (logID)
);
INSERT INTO ofVersion (name, version) VALUES ('gojara', 0);
CREATE TABLE ofGojaraStatistics (
logID NUMBER(10) NOT NULL AUTO_INCREMENT,
messageDate NUMBER(10) NOT NULL,
messageType VARCHAR2(255) NOT NULL,
fromJID VARCHAR2(255) NOT NULL,
toJID VARCHAR2(255) NOT NULL,
component VARCHAR2(255) NOT NULL,
PRIMARY KEY (logID)
);
INSERT INTO ofVersion (name, version) VALUES ('gojara', 0);
CREATE TABLE ofGojaraStatistics (
logID bigserial NOT NULL,
messageDate bigint NOT NULL,
messageType varchar(255) NOT NULL,
fromJID varchar(255) NOT NULL,
toJID varchar(255) NOT NULL,
component varchar(255) NOT NULL,
PRIMARY KEY (logID)
);
INSERT INTO ofVersion (name, version) VALUES ('gojara', 0);
CREATE TABLE ofGojaraStatistics (
logID bigint(20) NOT NULL AUTO_INCREMENT,
messageDate bigint(20) NOT NULL,
messageType tinytext NOT NULL,
fromJID text NOT NULL,
toJID text NOT NULL,
component text NOT NULL,
PRIMARY KEY (logID)
);
INSERT INTO ofVersion (name, version) VALUES ('gojara', 0);
CREATE TABLE ofGojaraStatistics (
logID bigint(20) NOT NULL AUTO_INCREMENT,
messageDate bigint(20) NOT NULL,
messageType tinytext NOT NULL,
fromJID text NOT NULL,
toJID text NOT NULL,
component text NOT NULL,
PRIMARY KEY (logID)
);
INSERT INTO ofVersion (name, version) VALUES ('gojara', 0);
rr.summary.title=GoJara
\ No newline at end of file
package org.jivesoftware.openfire.plugin.gojara.base;
import java.io.File;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.Node;
import org.jivesoftware.openfire.SessionManager;
import org.jivesoftware.openfire.component.ComponentEventListener;
import org.jivesoftware.openfire.component.InternalComponentManager;
import org.jivesoftware.openfire.container.Plugin;
import org.jivesoftware.openfire.container.PluginManager;
import org.jivesoftware.openfire.plugin.gojara.messagefilter.handler.AbstractInterceptorHandler;
import org.jivesoftware.openfire.plugin.gojara.messagefilter.handler.GatewayInterceptorHandler;
import org.jivesoftware.openfire.plugin.gojara.utils.XpathHelper;
import org.jivesoftware.openfire.session.ComponentSession;
import org.jivesoftware.util.JiveGlobals;
import org.jivesoftware.util.PropertyEventDispatcher;
import org.jivesoftware.util.PropertyEventListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xmpp.packet.IQ;
import org.xmpp.packet.JID;
/**
* @author Holger Bergunde
*
* This class is the basic reprasentation for the GoJara plugin. It is
* the entry point for openfire to start or stop this plugin.
*
* GoJara has been developed to support XEP-xxx Remote Roster
* Management. Further information: <a
* href="http://jkaluza.fedorapeople.org/remote-roster.html">Here</a>
*
* RemoteRoster enables Spectrum IM support for Openfire. Currently only
* 2.3, 2.4 and 2.5 implemented. 2.1 and 2.2 of the protocol standard is
* not supported by Spectrum IM
*/
public class RemoteRosterPlugin implements Plugin {
private static final Logger Log = LoggerFactory.getLogger(RemoteRosterPlugin.class);
private static PluginManager pluginManager;
private final SessionManager _sessionManager = SessionManager.getInstance();
private Map<String, AbstractInterceptorHandler> _interceptors = new HashMap<String, AbstractInterceptorHandler>();
private Set<String> _waitingForIQResponse = new HashSet<String>();
private PropertyEventListener _settingsObserver;
public void initializePlugin(PluginManager manager, File pluginDirectory) {
Log.debug("Starting RemoteRoster Plugin");
pluginManager = manager;
manageExternalComponents();
listenToSettings();
}
/*
* Handles external components that connect to openfire. We check if the
* external component is maybe a gateway and interesting for us
*/
private void manageExternalComponents() {
InternalComponentManager compManager = InternalComponentManager.getInstance();
compManager.addListener(new ComponentEventListener() {
/*
* Check if the unregistered component contains to one of our
* package interceptors
*/
public void componentUnregistered(JID componentJID) {
ComponentSession session = _sessionManager.getComponentSession(componentJID.getDomain());
if (session != null && _interceptors.containsKey(session.getExternalComponent().getInitialSubdomain())) {
String initialSubdomain = session.getExternalComponent().getInitialSubdomain();
// Remove it from Map & ComponentManager
removeInterceptor(initialSubdomain);
}
}
/*
* If there is a new external Component, check if it is a gateway
* and add create a package interceptor if it is enabled
*/
public void componentRegistered(JID componentJID) {
_waitingForIQResponse.add(componentJID.getDomain());
}
public void componentInfoReceived(IQ iq) {
String from = iq.getFrom().getDomain();
// Waiting for this external component sending an IQ response to
// us?
if (_waitingForIQResponse.contains(from)) {
Element packet = iq.getChildElement();
Document doc = packet.getDocument();
List<Node> nodes = XpathHelper.findNodesInDocument(doc, "//disco:identity[@category='gateway']");
// Is this external component a gateway and there is no
// package interceptor for it?
if (nodes.size() > 0 && !_interceptors.containsKey(from)) {
updateInterceptors(from);
}
// We got the IQ, we can now remove it from the set, because
// we are not waiting any more
_waitingForIQResponse.remove(from);
}
}
});
}
/*
* Registers a listener for JiveGlobals. We might restart our service, if
* there were some changes for our gateways
*/
private void listenToSettings() {
_settingsObserver = new RemoteRosterPropertyListener() {
@Override
protected void changedProperty(String prop) {
updateInterceptors(prop);
}
};
PropertyEventDispatcher.addListener(_settingsObserver);
}
public void destroyPlugin() {
for (String key : _interceptors.keySet()) {
_interceptors.get(key).stop();
}
PropertyEventDispatcher.removeListener(_settingsObserver);
pluginManager = null;
}
private void updateInterceptors(String componentJID) {
boolean allowed = JiveGlobals.getBooleanProperty("plugin.remoteroster.jids." + componentJID, false);
if (allowed) {
if (!_interceptors.containsKey(componentJID)) {
createNewPackageIntercetor(componentJID);
}
} else {
if (_interceptors.containsKey(componentJID)) {
removeInterceptor(componentJID);
}
}
}
public String getName() {
return "gojara";
}
public static PluginManager getPluginManager() {
return pluginManager;
}
private void removeInterceptor(String initialSubdomain) {
AbstractInterceptorHandler interceptor = _interceptors.get(initialSubdomain);
if (interceptor != null) {
_interceptors.remove(initialSubdomain);
interceptor.stop();
}
}
/*
* We are handling all our gateways in a Map with subdomain, like
* icq.myserver.com, to ensure that there is only one interceptor for a
* specified gateway
*/
private void createNewPackageIntercetor(String initialSubdomain) {
AbstractInterceptorHandler interceptor = new GatewayInterceptorHandler(initialSubdomain);
_interceptors.put(initialSubdomain, interceptor);
interceptor.start();
}
}
package org.jivesoftware.openfire.plugin.gojara.base;
import java.util.Map;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.util.PropertyEventListener;
/**
* @author Holger Bergunde
*
* This class implements the @see PropertyEventListener It monitors the
* JiveGlobals to check if there are made changes.
*
*/
public abstract class RemoteRosterPropertyListener implements PropertyEventListener {
public void xmlPropertySet(String property, Map<String, Object> params) {
}
public void xmlPropertyDeleted(String property, Map<String, Object> params) {
}
public void propertySet(String property, Map<String, Object> params) {
if (property.contains("plugin.remoteroster.jids.")) {
changedProperty(property.replace("plugin.remoteroster.jids.", ""));
}
}
public void propertyDeleted(String property, Map<String, Object> params) {
String hostname = XMPPServer.getInstance().getServerInfo().getXMPPDomain();
property += "." + hostname;
changedProperty(property.replace("plugin.remoteroster.jids.", ""));
}
/**
*
* If there were changes we are interested in, this template method get
* called. The property string is truncated. Method gets triggered, if the
* property contains substring "plugin.remoteroster.jids."
*
* @param prop
* substring of changes property
*/
protected abstract void changedProperty(String prop);
}
package org.jivesoftware.openfire.plugin.gojara.database;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import org.apache.log4j.Logger;
import org.jivesoftware.database.DbConnectionManager;
import org.jivesoftware.util.JiveGlobals;
/**
* @author Holger Bergunde This class is used to store logs in the database. A
* log entry is representated by {@link LogEntry}
*/
public class DatabaseManager {
private static Logger Log = Logger.getLogger(DatabaseManager.class);
private static volatile DatabaseManager _myself;
private static final String COUNT_LOG_ENTRIES = "SELECT count(*) FROM ofGojaraStatistics";
private static final String COUNT_PACKAGES_ODLER = "SELECT count(*) FROM ofGojaraStatistics WHERE messageType like ? AND component = ? AND messageDate > ?";
private static final String GET_ALL_LOGS = "SELECT * FROM ofGojaraStatistics ORDER BY logID desc LIMIT 100";
// private static final String MOST_ACTIVE =
// "SELECT toJID, count(logID) AS counter FROM `ofGojaraStatistics` GROUP by toJID ORDER BY counter DESC";
private static final String ADD_NEW_LOG = "INSERT INTO ofGojaraStatistics(messageDate, messageType, fromJID, toJId, component) VALUES(?,?,?,?,?)";
private static final String CLEAN_OLD_DATA = "DELETE FROM ofGojaraStatistics WHERE messageDate < ?";
private static final String GET_LOGS_DATE_LIMIT_COMPONENT = "SELECT * FROM ofGojaraStatistics WHERE messageDate > ? AND component = ? ORDER BY messageDate DESC LIMIT ?";
private final int _dbCleanMinutes;
private DatabaseManager() {
/*
* Load time from globals if it is set. It represents the minutes the
* log entries stay in database until they will get deleted
*/
// TODO: Use PropertyEventListener to check if cleaner.minutes have
// changed
_dbCleanMinutes = JiveGlobals.getIntProperty("plugin.remoteroster.log.cleaner.minutes", 60);
startDatabaseCleanLoop();
}
private void startDatabaseCleanLoop() {
/*
* Database Cleaner thread and check for old log entries every 2 minute
*/
TimerTask task = new TimerTask() {
@Override
public void run() {
cleanOldLogEntries();
}
};
Timer timer = new Timer();
timer.schedule(task, 2 * 60 * 1000, 2 * 60 * 1000);
}
/**
* Singleton for Databasemanager, because we only need one.
*
* @return the Databasemanager
*/
public static DatabaseManager getInstance() {
if (_myself == null) {
synchronized (DatabaseManager.class) {
if (_myself == null)
_myself = new DatabaseManager();
}
}
return _myself;
}
/**
* Returns a list of LogEntry's ordered by date desc
*
* @param olderThan
* unix timestamp in ms
* @param limit
* num of rows max
* @param component
* the specified subdomain of the logged component
* @return Collection of {@link LogEntry}
*/
public Collection<LogEntry> getLogsByDateAndLimit(long olderThan, int limit, String component) {
List<LogEntry> result = new ArrayList<LogEntry>();
Connection con = null;
PreparedStatement pstmt = null;
try {
con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement(GET_LOGS_DATE_LIMIT_COMPONENT);
pstmt.setLong(1, olderThan);
pstmt.setString(2, component);
pstmt.setInt(3, limit);
ResultSet rs = pstmt.executeQuery();
while (rs.next()) {
String from = rs.getString(4);
String to = rs.getString(5);
String type = rs.getString(3);
long date = rs.getLong(2);
LogEntry res = new LogEntry(from, to, type, date);
result.add(res);
}
pstmt.close();
} catch (SQLException sqle) {
Log.error(sqle);
} finally {
DbConnectionManager.closeConnection(pstmt, con);
}
return result;
}
/*
* Cleans log entries older than 60 minutes if
* plugin.remoteroster.log.cleaner.minutes is not set
*/
private void cleanOldLogEntries() {
Connection con = null;
PreparedStatement pstmt = null;
try {
con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement(CLEAN_OLD_DATA);
pstmt.setLong(1, System.currentTimeMillis() - _dbCleanMinutes * 60 * 1000);
int rows = pstmt.executeUpdate();
Log.debug("Cleaned statistic database. Affected rows: " + rows);
pstmt.close();
} catch (SQLException sqle) {
Log.error(sqle);
} finally {
DbConnectionManager.closeConnection(pstmt, con);
}
}
/**
* Adds new log entry for specified component.
*
* @param component
* subdomain of the external component. e.g.
* icq.myjabberserver.com
* @param type
* string representation of the class. normaly it is like
* {@link org.xmpp.packet}
* @param from
* full qualified JID of user or component this packet was from
* @param to
* full qualified JID of user or component this packet was
* adressed to
*/
public void addNewLogEntry(String component, String type, String from, String to) {
Connection con = null;
PreparedStatement pstmt = null;
try {
con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement(ADD_NEW_LOG);
pstmt.setLong(1, System.currentTimeMillis());
pstmt.setString(2, type);
pstmt.setString(3, from);
pstmt.setString(4, to);
pstmt.setString(5, component);
pstmt.executeUpdate();
pstmt.close();
} catch (SQLException sqle) {
Log.error(sqle);
} finally {
DbConnectionManager.closeConnection(pstmt, con);
}
}
/**
* This method return the last 100 log entries. Every entry is one string
* and added to a ArrayList
*
* @return each log as string in a list
* */
public List<String> getAllLogs() {
Connection con = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
List<String> _result = new ArrayList<String>();
try {
con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement(GET_ALL_LOGS);
rs = pstmt.executeQuery();
while (rs.next()) {
String from = rs.getString(4);
String to = rs.getString(5);
String type = rs.getString(3);
String component = rs.getString(6);
Timestamp date = rs.getTimestamp(2);
String res = "From: " + from + " To: " + to + " Type: " + type + " Timestamp: " + date.toString()
+ "Component: " + component;
_result.add(res);
}
}
catch (SQLException sqle) {
Log.error(sqle);
} finally {
DbConnectionManager.closeConnection(rs, pstmt, con);
}
return _result;
}
/**
* Returns the size of the ofGoJaraStatistics table
*
* @return number rows in database as int or -1 if an error occurred
*/
public int getLogSize() {
Connection con = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement(COUNT_LOG_ENTRIES);
rs = pstmt.executeQuery();
rs.next();
return rs.getInt(1);
} catch (SQLException sqle) {
Log.error(sqle);
} finally {
DbConnectionManager.closeConnection(rs, pstmt, con);
}
return -1;
}
/**
* Counts the number of log entries in the databse
*
* @param subdomain
* subdomain of the component the packages were flown by
* @param packetClass
* the class the packet was instance of
* @return number of rows found in database or -1 if there was an error
*/
public int getPacketCount(String subdomain, @SuppressWarnings("rawtypes") Class packetClass) {
return getPacketCountOlderThan(subdomain, packetClass, _dbCleanMinutes);
}
/**
* Counts the number of log entries in the databse that are older than
* specified value
*
* @param component
* subdomain of the component the packages were flown by
* @param packetClass
* the class the packet was instance of
* @param minutes
* the log entry should not be older than (timestamp should be
* smaller than currentTime - minutes)
* @return number of rows found in database or -1 if there was an error
*/
public int getPacketCountOlderThan(String component, @SuppressWarnings("rawtypes") Class packetClass, int minutes) {
String classname = packetClass.getName();
Connection con = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement(COUNT_PACKAGES_ODLER);
pstmt.setString(1, "%" + classname + "");
pstmt.setString(2, component);
pstmt.setLong(3, System.currentTimeMillis() - minutes * 60 * 1000);
rs = pstmt.executeQuery();
rs.next();
return rs.getInt(1);
} catch (SQLException sqle) {
Log.error(sqle);
} finally {
DbConnectionManager.closeConnection(rs, pstmt, con);
}
return -1;
}
}
package org.jivesoftware.openfire.plugin.gojara.database;
/**
* This class represents a log entry for the GoJara plugin
*
* @author Holger Bergunde
*/
public class LogEntry {
private String _from;
private String _to;
private String _type;
private long _date;
/**
* Constructs a log entry
*
* @param from
* full qualified JID as String
* @param to
* full qualified JID as String
* @param type
* class name of packet as String
* @param date
* date of the packet in unixtimestamp miliseconds
*/
public LogEntry(String from, String to, String type, long date) {
_from = from;
_to = to;
_type = type;
_date = date;
}
/**
* Returns the sender of this packet represented by this log entry
*
* @return full qualified jid as string
*/
public String getFrom() {
return _from;
}
/**
* Returns the receiver of this packet represented by this log entry
*
* @return full qualified jid as string
*/
public String getTo() {
return _to;
}
/**
* Returns the packet type as class name
*
* @return class name as string
*/
public String getType() {
return _type;
}
/**
* Date of logentry
*
* @return date in unixtimestamp milliseconds
*/
public long getDate() {
return _date;
}
}
package org.jivesoftware.openfire.plugin.gojara.messagefilter.handler;
import java.util.HashSet;
import java.util.Set;
import org.apache.log4j.Logger;
import org.jivesoftware.openfire.interceptor.InterceptorManager;
import org.jivesoftware.openfire.interceptor.PacketInterceptor;
/**
*
* A gateway interceptor should extend this class. It supports the child class
* handling their interceptors.
*
* @author Holger Bergunde
*
*/
public abstract class AbstractInterceptorHandler {
private static Logger Log = Logger.getLogger(AbstractInterceptorHandler.class);
private String _subdomain;
private boolean _isRunning = false;
private Set<PacketInterceptor> _interceptors = new HashSet<PacketInterceptor>();
private InterceptorManager _iManager;
public AbstractInterceptorHandler(String subdomain) {
_subdomain = subdomain;
_iManager = InterceptorManager.getInstance();
}
/**
* Add a interceptor to the let it handled by the abstract implementation
* The handle must not be started. You have to stop() until you could add
* new interceptors.
*
* @param interceptor
* @return true if it successfully added, otherwise false
*/
protected boolean addInterceptor(PacketInterceptor interceptor) {
if (_isRunning) {
return false;
}
return _interceptors.add(interceptor);
}
/**
* Remove a interceptor from abstract implementation The handle must not be
* started. You have to stop() until you could remove interceptors.
*
* @param interceptor
* @return true if it successfully added, otherwise false
*/
protected boolean removeInterceptor(PacketInterceptor interceptor) {
if (_isRunning) {
return false;
}
return _interceptors.remove(interceptor);
}
/**
* Start handling the added interceptors.
* If it is started you could not remove or add interceptors
*/
public void start() {
Log.debug("Start handling message interceptors for gateway " + _subdomain);
_isRunning = true;
for (PacketInterceptor interceptor : _interceptors) {
_iManager.addInterceptor(interceptor);
}
}
/**
* Stop handling the added interceptors.
*/
public void stop() {
Log.debug("Stop handling message interceptors for gateway " + _subdomain);
if (!_isRunning)
return;
_isRunning = false;
for (PacketInterceptor interceptor : _interceptors) {
_iManager.removeInterceptor(interceptor);
}
}
}
package org.jivesoftware.openfire.plugin.gojara.messagefilter.handler;
import org.jivesoftware.openfire.plugin.gojara.messagefilter.interceptors.DiscoPackageInterceptorHandler;
import org.jivesoftware.openfire.plugin.gojara.messagefilter.interceptors.IQLastInterceptor;
import org.jivesoftware.openfire.plugin.gojara.messagefilter.interceptors.StatisticPackageInterceptor;
import org.jivesoftware.openfire.plugin.gojara.messagefilter.remoteroster.RemoteRosterInterceptor;
/**
*
* This is the main handler for our gateways. It initializes all needed
* interceptors with the component subdomain this handler is bind to You have to
* start and stop this handler manually.
*
* @author Holger Bergunde
*
*/
public class GatewayInterceptorHandler extends AbstractInterceptorHandler {
public GatewayInterceptorHandler(String subdomain) {
super(subdomain);
DiscoPackageInterceptorHandler discoInterceptor = new DiscoPackageInterceptorHandler(subdomain);
RemoteRosterInterceptor remoteRosterInterceptor = new RemoteRosterInterceptor(subdomain);
StatisticPackageInterceptor statisticInterceptor = new StatisticPackageInterceptor(subdomain);
IQLastInterceptor iqLastInterceptor = new IQLastInterceptor(subdomain);
addInterceptor(remoteRosterInterceptor);
addInterceptor(discoInterceptor);
addInterceptor(statisticInterceptor);
addInterceptor(iqLastInterceptor);
}
}
package org.jivesoftware.openfire.plugin.gojara.messagefilter.interceptors;
import java.util.List;
import org.dom4j.Element;
import org.dom4j.Node;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.interceptor.PacketInterceptor;
import org.jivesoftware.openfire.interceptor.PacketRejectedException;
import org.jivesoftware.openfire.plugin.gojara.permissions.PermissionManager;
import org.jivesoftware.openfire.plugin.gojara.utils.XpathHelper;
import org.jivesoftware.openfire.session.Session;
import org.xmpp.packet.IQ;
import org.xmpp.packet.Packet;
/**
*
* If the access to external components or gateways is limited to a special
* group in GoJara settings we have to filter the disco#infos from the server to
* the client. If the user is not on the list we are hiding the specified info
* and remove the item containing the gateways subdomain
*
* @author Holger Bergunde
*
*/
public class DiscoPackageInterceptorHandler implements PacketInterceptor {
private PermissionManager _permissions;
private String _subDomain;
private String _serverDomain;
public DiscoPackageInterceptorHandler(String subdomain) {
_permissions = new PermissionManager();
_subDomain = subdomain;
XMPPServer server = XMPPServer.getInstance();
_serverDomain = server.getServerInfo().getXMPPDomain();
}
public void interceptPacket(Packet packet, Session session, boolean incoming, boolean processed)
throws PacketRejectedException {
if (_permissions.isGatewayLimited(_subDomain)) {
if (packet instanceof IQ) {
IQ iqpacket = (IQ) packet;
Element root = iqpacket.getChildElement();
if (root == null)
return;
if (iqpacket.getFrom() == null || iqpacket.getFrom().toString().equals(_serverDomain)) {
String ns = root.getNamespaceURI();
if (ns.equals("http://jabber.org/protocol/disco#items") && iqpacket.getType().equals(IQ.Type.result)) {
if (!_permissions.allowedForUser(_subDomain, iqpacket.getTo())) {
List<Node> nodes = XpathHelper.findNodesInDocument(root.getDocument(), "//discoitems:item");
for (Node node : nodes) {
if (node.valueOf("@jid").equals(_subDomain)) {
root.remove(node);
}
}
}
}
}
}
}
}
}
package org.jivesoftware.openfire.plugin.gojara.messagefilter.interceptors;
import org.dom4j.Element;
import org.dom4j.tree.DefaultElement;
import org.jivesoftware.openfire.PacketRouter;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.interceptor.PacketInterceptor;
import org.jivesoftware.openfire.interceptor.PacketRejectedException;
import org.jivesoftware.openfire.session.Session;
import org.jivesoftware.util.JiveGlobals;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xmpp.packet.IQ;
import org.xmpp.packet.Packet;
/**
*
* Some clients try to check how long a contact is already offline. This feature
* is not supported by spectrum so it won't response to this IQ stanza. To
* prevent the client from waiting for a response we could answer with a
* service-unavailable message as described in XEP-12.
*
* @author Holger Bergunde
*
*/
public class IQLastInterceptor implements PacketInterceptor {
private String _subDomain;
protected static final Logger Log = LoggerFactory.getLogger(IQLastInterceptor.class);
public IQLastInterceptor(String subdomain) {
Log.debug("Createt IQLastInterceptor for " + subdomain);
_subDomain = subdomain;
}
public void interceptPacket(Packet packet, Session session, boolean incoming, boolean processed)
throws PacketRejectedException {
if (!iqLastInterceptorEnabled()) {
Log.debug("Auto repsonse to jabber:iq:last is deactivated. You could enabled it using the webinterface ");
return;
}
if (packet instanceof IQ && incoming && processed) {
IQ iqpacket = (IQ) packet;
if (iqpacket.getTo() == null || !iqpacket.getTo().toString().contains(_subDomain))
return;
Element root = iqpacket.getChildElement();
if (root == null)
return;
String ns = root.getNamespaceURI();
if (ns.equals("jabber:iq:last") && iqpacket.getType().equals(IQ.Type.get)) {
IQ answer = new IQ();
answer.setType(IQ.Type.error);
answer.setFrom(iqpacket.getTo());
answer.setTo(iqpacket.getFrom());
answer.setID(iqpacket.getID());
DefaultElement errorElement = new DefaultElement("error");
errorElement.addAttribute("type", "cancel");
errorElement.addAttribute("code", "503");
DefaultElement serviceElement = new DefaultElement("service-unavailable");
serviceElement.addNamespace("", "urn:ietf:params:xml:ns:xmpp-stanzas");
errorElement.add(serviceElement);
answer.setChildElement(errorElement);
Log.debug("Auto response to jabber:iq:last for " + _subDomain);
PacketRouter router = XMPPServer.getInstance().getPacketRouter();
router.route(answer);
}
}
}
private boolean iqLastInterceptorEnabled() {
return JiveGlobals.getBooleanProperty("plugin.remoteroster.iqLastFilter", false);
}
}
package org.jivesoftware.openfire.plugin.gojara.messagefilter.interceptors;
import org.jivesoftware.openfire.interceptor.PacketInterceptor;
import org.jivesoftware.openfire.interceptor.PacketRejectedException;
import org.jivesoftware.openfire.plugin.gojara.database.DatabaseManager;
import org.jivesoftware.openfire.session.Session;
import org.xmpp.packet.JID;
import org.xmpp.packet.Packet;
/**
*
* This class is only for logging messages between the gateway and the
* registered clients. It uses the database to save the packets from the past 60
* (configurable) minutes.
*
* @author Holger Bergunde
*
*/
public class StatisticPackageInterceptor implements PacketInterceptor {
private String _subdomain;
private DatabaseManager _db;
// private static final Logger Log = LoggerFactory.getLogger(StatisticPackageInterceptor.class);
public StatisticPackageInterceptor(String subdomain) {
_subdomain = subdomain;
_db = DatabaseManager.getInstance();
}
public void interceptPacket(Packet packet, Session session, boolean incoming, boolean processed)
throws PacketRejectedException {
try {
JID from = packet.getFrom();
JID to = packet.getTo();
if (from != null && to != null && processed && incoming) {
if (from.toString().contains(_subdomain) || to.toString().contains(_subdomain)) {
/*
* Spectrum sends a Ping to itself through the server to
* check if the server is alive. We ignore that for
* statistics
*/
if (to.toString().equals(from.toString()) && to.toString().equals(_subdomain))
return;
String type = packet.getClass().getName();
_db.addNewLogEntry(_subdomain, type, from.toString(), to.toString());
}
}
} catch (IllegalArgumentException e) {
// Log.warn("There was an illegal JID while writing gojara gateway statistics! "+e.getMessage());
// TODO: IF there are packages with an invalid from or to jid like
// to="@somehost.com" Tinder will throw an exception. We cannot
// prevent that, because we want to know if there is a from and to
// jid.
}
}
}
package org.jivesoftware.openfire.plugin.gojara.messagefilter.remoteroster;
import java.util.HashMap;
import java.util.Map;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.interceptor.PacketInterceptor;
import org.jivesoftware.openfire.interceptor.PacketRejectedException;
import org.jivesoftware.openfire.plugin.gojara.messagefilter.remoteroster.processors.AbstractRemoteRosterProcessor;
import org.jivesoftware.openfire.plugin.gojara.messagefilter.remoteroster.processors.ClientToComponentUpdateProcessor;
import org.jivesoftware.openfire.plugin.gojara.messagefilter.remoteroster.processors.DiscoIQRegisteredProcessor;
import org.jivesoftware.openfire.plugin.gojara.messagefilter.remoteroster.processors.NonPersistantRosterProcessor;
import org.jivesoftware.openfire.plugin.gojara.messagefilter.remoteroster.processors.ReceiveComponentUpdatesProcessor;
import org.jivesoftware.openfire.plugin.gojara.messagefilter.remoteroster.processors.SendRosterProcessor;
import org.jivesoftware.openfire.plugin.gojara.utils.XpathHelper;
import org.jivesoftware.openfire.roster.RosterManager;
import org.jivesoftware.openfire.session.Session;
import org.jivesoftware.util.JiveGlobals;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xmpp.packet.IQ;
import org.xmpp.packet.Packet;
import org.xmpp.packet.Presence;
/**
* This intercepter handles the main functionality described in the XEP-xxx
* Remote Roster Management standard. <a
* href="http://jkaluza.fedorapeople.org/remote-roster.html">XEP-xxx</a>
*
* It must be registered as an PacketInterceptor for each gateway. It will check
* the incoming packages for several preconditions and redirects the packages
* using a command pattern to individual packet handles.
*
* @author Holger Bergunde
*
*/
public class RemoteRosterInterceptor implements PacketInterceptor {
private static final Logger Log = LoggerFactory.getLogger(RemoteRosterInterceptor.class);
private String _mySubdomain;
private Map<String, AbstractRemoteRosterProcessor> _packetProcessor = new HashMap<String, AbstractRemoteRosterProcessor>();
public RemoteRosterInterceptor(String initialSubdomain) {
Log.debug("Starting Package Interceptor for " + initialSubdomain);
_mySubdomain = initialSubdomain;
XMPPServer server = XMPPServer.getInstance();
RosterManager rosterMananger = server.getRosterManager();
AbstractRemoteRosterProcessor sendroster = new SendRosterProcessor(rosterMananger, _mySubdomain);
AbstractRemoteRosterProcessor receiveChanges = new ReceiveComponentUpdatesProcessor(rosterMananger,
_mySubdomain);
AbstractRemoteRosterProcessor iqRegistered = new DiscoIQRegisteredProcessor(_mySubdomain);
AbstractRemoteRosterProcessor nonPersistant = new NonPersistantRosterProcessor(rosterMananger, _mySubdomain);
AbstractRemoteRosterProcessor updateToComponent = new ClientToComponentUpdateProcessor(_mySubdomain);
_packetProcessor.put("sendRoster", sendroster);
_packetProcessor.put("receiveChanges", receiveChanges);
_packetProcessor.put("sparkIQRegistered", iqRegistered);
_packetProcessor.put("handleNonPersistant", nonPersistant);
_packetProcessor.put("clientToComponentUpdate", updateToComponent);
}
public void interceptPacket(Packet packet, Session session, boolean incoming, boolean processed)
throws PacketRejectedException {
if (!processed && incoming) {
if (packet instanceof IQ) {
Log.debug("Incoming unprocessed package i might be interested in. I'm " + this.hashCode()+ " for subdomain "
+ this._mySubdomain + ". Package: \n" + packet.toString() + "\n");
IQ myPacket = (IQ) packet;
if (myPacket.getFrom() == null || myPacket.getTo() == null) {
/*
* If getTo() == null this is maybe a roster update from the
* Client to the Server, check if we should mirror this
* package to external component
*/
if (myPacket.getFrom() != null && myPacket.getType().equals(IQ.Type.set)
&& myPacket.getTo() == null) {
if (XpathHelper.findNodesInDocument(myPacket.getChildElement().getDocument(), "//roster:item")
.size() > 0) {
_packetProcessor.get("clientToComponentUpdate").process(myPacket);
}
}
return;
}
String from = myPacket.getFrom().toString();
String to = myPacket.getTo().toString();
if (from.equals(_mySubdomain)) {
if (myPacket.getType().equals(IQ.Type.get)
&& XpathHelper.findNodesInDocument(myPacket.getElement().getDocument(), "//roster:*").size() == 1) {
// This Package is a roster request by remote component
_packetProcessor.get("sendRoster").process(packet);
} else if (myPacket.getType().equals(IQ.Type.set)
&& XpathHelper.findNodesInDocument(myPacket.getElement().getDocument(), "//roster:item").size() >= 1) {
// Component sends roster update
_packetProcessor.get("receiveChanges").process(packet);
}
} else if (to.equals(_mySubdomain)
&& myPacket.getType().equals(IQ.Type.get)
&& myPacket.toString().contains("http://jabber.org/protocol/disco#info")) {
/*
* modify the disco#info for spark clients if enabled in
* admin panel
*/
_packetProcessor.get("sparkIQRegistered").process(packet);
}
} else if (!JiveGlobals.getBooleanProperty("plugin.remoteroster.persistent", false)) {
if (packet instanceof Presence && packet.getFrom().toString().equals(_mySubdomain)
&& !packet.getElement().getStringValue().equals("Connecting")){
System.out.println("Test for NonPersistant-Roster Cleanup!");
_packetProcessor.get("handleNonPersistant").process(packet);
}
}
}
}
}
// currently we dont need this, and it didnt seem to occur often anyway. Will test it later.
// could potentially save some traffic
//else if (packet instanceof Presence){
// String to = packet.getTo().toString();
// if (!to.equals(_mySubdomain) && to.contains(_mySubdomain)){
//
// Presence myPacket = ((Presence) packet);
// if (myPacket.getType().equals(Presence.Type.unavailable) || myPacket.getType().equals(Presence.Type.probe) ||
// myPacket.getType().equals(null)){
// System.out.println("this presence would be wasted");
// throw new PacketRejectedException();
// }
// }
\ No newline at end of file
package org.jivesoftware.openfire.plugin.gojara.messagefilter.remoteroster.processors;
import java.util.List;
import org.dom4j.Document;
import org.dom4j.Node;
import org.jivesoftware.openfire.PacketRouter;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.interceptor.PacketRejectedException;
import org.jivesoftware.openfire.plugin.gojara.messagefilter.remoteroster.RemoteRosterInterceptor;
import org.jivesoftware.openfire.plugin.gojara.utils.XpathHelper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xmpp.packet.Packet;
/**
* This packet and sub packet implements the command pattern. Every processor
* that extends this class have to implement the process function. The
* {@link RemoteRosterInterceptor} will register different implementations of
* this processor and redirect packages according to their functionality
*
* @author Holger Bergunde
*
*/
abstract public class AbstractRemoteRosterProcessor {
protected static final Logger Log = LoggerFactory.getLogger(AbstractRemoteRosterProcessor.class);
XMPPServer _server;
PacketRouter _router;
public AbstractRemoteRosterProcessor() {
_server = XMPPServer.getInstance();
}
/**
* Handles the passed packet. Might throw {@link PacketRejectedException} if
* the package should not be processed by openfire
*
* @param packet
* @throws PacketRejectedException
*/
abstract public void process(Packet packet) throws PacketRejectedException;
/**
* Use this method if you want to send your own packets through openfire
*
* @param packet
* packet to send
*/
protected void dispatchPacket(Packet packet) {
Log.debug("Sending package to PacketRouter: \n" + packet.toString() + "\n");
PacketRouter router = _server.getPacketRouter();
router.route(packet);
}
/**
* Redirects this method. Have a closer look to {@link XpathHelper}
* @param doc
* @param xpath
* @return
*/
protected List<Node> findNodesInDocument(Document doc, String xpath) {
return XpathHelper.findNodesInDocument(doc, xpath);
}
/**
* Redirects this method. Have a closer look to {@link XpathHelper}
* @param doc
* @param xpath
* @return
*/
protected String getUsernameFromJid(String jid) {
return XpathHelper.getUsernameFromJid(jid);
}
}
package org.jivesoftware.openfire.plugin.gojara.messagefilter.remoteroster.processors;
import java.util.Collection;
import org.jivesoftware.openfire.SharedGroupException;
import org.jivesoftware.openfire.interceptor.PacketRejectedException;
import org.jivesoftware.openfire.roster.Roster;
import org.jivesoftware.openfire.roster.RosterItem;
import org.jivesoftware.openfire.roster.RosterManager;
import org.jivesoftware.openfire.user.UserNotFoundException;
import org.xmpp.packet.IQ;
import org.xmpp.packet.Packet;
/**
*
* This class cleans up a roster from contacts if the user removes/deletes the
* gateway registration. After deleting a registration there should not be any
* gateway related contacts left
*
* @author holger.bergunde
*
*/
public class CleanUpRosterProcessor extends AbstractRemoteRosterProcessor {
private String _myDomain;
private RosterManager _rosterManager;
public CleanUpRosterProcessor(RosterManager rosterMananger, String mySubdomain) {
Log.debug("Created CleanUpRosterProcessor for " + mySubdomain);
_myDomain = mySubdomain;
_rosterManager = rosterMananger;
}
@Override
public void process(Packet packet) throws PacketRejectedException {
if (packet instanceof IQ) {
IQ iqPacket = (IQ) packet;
if (findNodesInDocument(iqPacket.getElement().getDocument(), "//register:remove").size() > 0) {
String username = getUsernameFromJid(packet.getFrom().toString());
Roster roster;
try {
roster = _rosterManager.getRoster(username);
Collection<RosterItem> items = roster.getRosterItems();
for (RosterItem item : items) {
String itemName = item.getJid().toString();
if (itemName.contains(_myDomain) && !itemName.equals(_myDomain)) {
Log.debug("Removing contact " + username + " from contact list.");
roster.deleteRosterItem(item.getJid(), false);
}
}
} catch (UserNotFoundException e) {
Log.debug("Could not found user while cleaning up the roster in GoJara for user " + username, e);
} catch (SharedGroupException e) {
// We should ignore this. External contacts cannot be in
// shared groups
}
}
}
}
}
\ No newline at end of file
package org.jivesoftware.openfire.plugin.gojara.messagefilter.remoteroster.processors;
import org.dom4j.Element;
import org.dom4j.Node;
import org.jivesoftware.openfire.interceptor.PacketRejectedException;
import org.jivesoftware.openfire.plugin.gojara.messagefilter.remoteroster.RemoteRosterInterceptor;
import org.xmpp.packet.IQ;
import org.xmpp.packet.Packet;
/**
* This class implements the XEP-xxx Remote Roster Management standard
* "2.4 Client sends user update". Part of command pattern used in
* {@link RemoteRosterInterceptor}
*
* Further information: <a
* href="http://jkaluza.fedorapeople.org/remote-roster.html#sect-id215516"
* >Here</a>
*
* @author Holger Bergunde
*
*/
public class ClientToComponentUpdateProcessor extends AbstractRemoteRosterProcessor {
private String _myDomain;
public ClientToComponentUpdateProcessor(String mySubdomain) {
Log.debug("Created ClientToComponentUpdateProcessor for " + mySubdomain);
_myDomain = mySubdomain;
}
@Override
public void process(Packet packet) throws PacketRejectedException {
Log.debug("Processing packet in ClientToComponentUpdateProcessor for " + _myDomain);
Element query = ((IQ) packet).getChildElement();
if (query != null && query.getNamespaceURI().equals("jabber:iq:roster")) {
if (findNodesInDocument(query.getDocument(), "//roster:item").size() > 0) {
for (Node n : findNodesInDocument(query.getDocument(), "//roster:item")) {
String jid = n.valueOf("@jid");
// TODO: We ignore remove iq packets for now. There might be
// conflicts
// when we remove our legacy network registration.
if (jid.contains("@" + _myDomain) && !n.valueOf("@subscription").equals("remove")) {
Log.debug("Mirroring packet from local network to legacy component " + _myDomain);
IQ forward = (IQ) packet.createCopy();
forward.setTo(_myDomain);
dispatchPacket(forward);
}
}
}
}
}
}
package org.jivesoftware.openfire.plugin.gojara.messagefilter.remoteroster.processors;
import java.util.Timer;
import java.util.TimerTask;
import org.dom4j.Attribute;
import org.dom4j.Element;
import org.dom4j.tree.DefaultAttribute;
import org.dom4j.tree.DefaultElement;
import org.jivesoftware.openfire.interceptor.InterceptorManager;
import org.jivesoftware.openfire.interceptor.PacketInterceptor;
import org.jivesoftware.openfire.interceptor.PacketRejectedException;
import org.jivesoftware.openfire.plugin.gojara.messagefilter.remoteroster.RemoteRosterInterceptor;
import org.jivesoftware.openfire.session.Session;
import org.jivesoftware.util.JiveGlobals;
import org.xmpp.packet.IQ;
import org.xmpp.packet.Packet;
/**
* This implements a special IQ package for Spark client. Spark checks the
* disco#info package send from external component for a <registered> tag.
* Because spectrum does not support this feature we have to modify the disco
* package from spectrum with the registered tag if the user is registered with
* this gateway. Part of command pattern used in {@link RemoteRosterInterceptor}
*
* @author Holger Bergunde
*
*/
public class DiscoIQRegisteredProcessor extends AbstractRemoteRosterProcessor {
private boolean _isRegistered = false;
private String _mySubdoman;
public DiscoIQRegisteredProcessor(String subdomain) {
Log.debug("Created DiscoIQResigteredProcessor for " + subdomain);
_mySubdoman = subdomain;
}
@Override
public void process(Packet packet) throws PacketRejectedException {
Log.debug("Processing packet in DiscoIQResigteredProcessor for " + _mySubdoman);
// Check if the jabber:iq:register is enabled in admin panel
boolean isFeatureEnabled = JiveGlobals.getBooleanProperty("plugin.remoteroster.sparkDiscoInfo", false);
if (!isFeatureEnabled) {
Log.debug("Spark extension is deactivated. Won't change the disco#info");
return;
}
String from = packet.getFrom().toString();
String to = packet.getTo().toString();
final InterceptorManager interceptorManager = InterceptorManager.getInstance();
final PacketInterceptor interceptor = new PacketInterceptor() {
public void interceptPacket(Packet packet, Session session, boolean incoming, boolean processed)
throws PacketRejectedException {
if (!processed && incoming) {
if (packet instanceof IQ) {
IQ iqPacket = (IQ) packet;
Element packetElement = iqPacket.getChildElement();
if (packetElement == null)
return;
String ns = iqPacket.getChildElement().getNamespace().getURI();
if (iqPacket.getType().equals(IQ.Type.result) && ns.equals("jabber:iq:register")
&& iqPacket.getFrom().toString().equals(_mySubdoman)) {
// Check if we are already registered
setRegistered(iqPacket.toString().contains("<registered/>"));
throw new PacketRejectedException();
} else if (iqPacket.getType().equals(IQ.Type.result)
&& ns.equals("http://jabber.org/protocol/disco#info")
&& iqPacket.getFrom().toString().equals(_mySubdoman)) {
/*
* This is the answer of the disco#info from spark
* to our component. add the jabber:iq:register
* feature if we are registered
*/
if (isRegistered()) {
Log.debug("Modifying disco#info packge to send registered iq feature to Spark user "
+ iqPacket.getTo().toString());
Attribute attribut = new DefaultAttribute("var", "jabber:iq:registered");
iqPacket.getChildElement().addElement("feature").add(attribut);
}
}
}
}
}
};
Log.debug("Creating my own listener for jabber:iq:register result to external component " + _mySubdoman);
interceptorManager.addInterceptor(interceptor);
IQ askComponent = new IQ();
askComponent.setTo(to);
askComponent.setFrom(from);
askComponent.setType(IQ.Type.get);
Element query = new DefaultElement("query");
query.addNamespace("", "jabber:iq:register");
askComponent.setChildElement(query);
// Remove the package intercepter in 1sec
TimerTask removeInterceptorTask = new TimerTask() {
@Override
public void run() {
Log.debug("Removing my created listener for jabber:iq:register. Component " + _mySubdoman);
interceptorManager.removeInterceptor(interceptor);
}
};
Timer timer = new Timer();
timer.schedule(removeInterceptorTask, 1000);
// Send the register query to component
dispatchPacket(askComponent);
}
private boolean isRegistered() {
return _isRegistered;
}
private void setRegistered(boolean bool) {
_isRegistered = bool;
}
}
package org.jivesoftware.openfire.plugin.gojara.messagefilter.remoteroster.processors;
import java.util.Collection;
import org.jivesoftware.openfire.interceptor.PacketRejectedException;
import org.jivesoftware.openfire.plugin.gojara.messagefilter.remoteroster.RemoteRosterInterceptor;
import org.jivesoftware.openfire.roster.Roster;
import org.jivesoftware.openfire.roster.RosterItem;
import org.jivesoftware.openfire.roster.RosterManager;
import org.xmpp.packet.Packet;
import org.xmpp.packet.Presence;
/**
* This class is a part of the command pattern used in
* {@link RemoteRosterInterceptor}. If the remote contacts should not be saved
* permanently in the users roster this command will clean up the users roster.
*
* @author Holger Bergunde
*
*/
public class NonPersistantRosterProcessor extends AbstractRemoteRosterProcessor {
private RosterManager _rosterManager;
private String _subDomain;
public NonPersistantRosterProcessor(RosterManager rostermananger, String subdomain) {
Log.debug("Created NonPersistantProcessor for " + subdomain);
_rosterManager = rostermananger;
_subDomain = subdomain;
}
@Override
public void process(Packet packet) throws PacketRejectedException {
Log.debug("Processing packet in NonPersistantRosterProcessor for " + _subDomain);
Presence myPacket = (Presence) packet;
String to = myPacket.getTo().toString();
String username = getUsernameFromJid(to);
if (myPacket.getType() != null && myPacket.getType().equals(Presence.Type.unavailable)) {
try {
Roster roster = _rosterManager.getRoster(username);
Collection<RosterItem> items = roster.getRosterItems();
for (RosterItem item : items) {
String itemName = item.getJid().toString();
if (itemName.contains(_subDomain) && !itemName.equals(_subDomain)) {
Log.debug("Removing contact " + item.getJid().toString() + " from contact list.");
roster.deleteRosterItem(item.getJid(), false);
}
}
} catch (Exception e) {
Log.debug("Execption occured when cleaning up the Roster.", e);
e.printStackTrace();
}
}
}
}
package org.jivesoftware.openfire.plugin.gojara.messagefilter.remoteroster.processors;
import java.util.ArrayList;
import java.util.List;
import org.dom4j.Node;
import org.jivesoftware.openfire.SharedGroupException;
import org.jivesoftware.openfire.interceptor.PacketRejectedException;
import org.jivesoftware.openfire.plugin.gojara.messagefilter.remoteroster.RemoteRosterInterceptor;
import org.jivesoftware.openfire.roster.Roster;
import org.jivesoftware.openfire.roster.RosterItem;
import org.jivesoftware.openfire.roster.RosterManager;
import org.jivesoftware.openfire.user.UserNotFoundException;
import org.jivesoftware.util.JiveGlobals;
import org.xmpp.packet.IQ;
import org.xmpp.packet.JID;
import org.xmpp.packet.Packet;
/**
*
* This class implements the XEP-xxx Remote Roster Management standard
* "2.5 Component sends roster update". Part of command pattern used in
* {@link RemoteRosterInterceptor}
*
* Further information: <a
* href="http://jkaluza.fedorapeople.org/remote-roster.html#sect-id215516"
* >Here</a>
*
* @author Holger Bergunde
*
*/
public class ReceiveComponentUpdatesProcessor extends AbstractRemoteRosterProcessor {
private RosterManager _rosterManager;
private String _mySubdomain;
public ReceiveComponentUpdatesProcessor(RosterManager rosterManager, String subdomain) {
_mySubdomain = subdomain;
Log.debug("Created ReceiveComponentUpdatesProcessor for " + _mySubdomain);
_rosterManager = rosterManager;
}
@Override
public void process(Packet packet) throws PacketRejectedException {
Log.debug("Processing packet in ClientToComponentUpdateProcessor for " + _mySubdomain);
IQ myPacket = (IQ) packet;
IQ response = IQ.createResultIQ(myPacket);
String to = myPacket.getTo().toString();
String username = getUsernameFromJid(to);
List<Node> nodes = findNodesInDocument(myPacket.getElement().getDocument(), "//roster:item");
for (Node n : nodes) {
Roster roster;
String jid = n.valueOf("@jid");
String name = n.valueOf("@name");
String subvalue = n.valueOf("@subscription");
if(subvalue.equals("both")){
try {
if (jid.equals(myPacket.getFrom().toString())) {
// Do not add the component itself to the contact list
break;
}
roster = _rosterManager.getRoster(username);
List<String> grouplist = new ArrayList<String>();
List<Node> groupnodes = findNodesInDocument(n.getDocument(), "//roster:group");
for (Node ne : groupnodes) {
String groupName = ne.getText();
grouplist.add(groupName);
}
boolean rosterPersisten = JiveGlobals.getBooleanProperty("plugin.remoteroster.persistent", true);
Log.debug("Adding/Updating User " + jid + " to roster " + to);
try {
RosterItem item = roster.getRosterItem(new JID(jid));
item.setGroups(grouplist);
roster.updateRosterItem(item);
//dont send iq-result if just updating user
break;
} catch (UserNotFoundException exc) {
//Then we should add him!
}
RosterItem item = roster.createRosterItem(new JID(jid), name, grouplist, false, rosterPersisten);
item.setSubStatus(RosterItem.SUB_BOTH);
roster.updateRosterItem(item);
} catch (Exception e) {
Log.debug("Could not add user to Roster although no entry should exist..." + username, e);
e.printStackTrace();
}
} else if (subvalue.equals("remove")){
try {
roster = _rosterManager.getRoster(username);
Log.debug("Removing contact " + username + " from contact list.");
//If the contact didnt exist in contact list it is likely the transport itself in which case
//we do not want to forward this msg to server...
RosterItem item = roster.deleteRosterItem(new JID(jid), false);
if (item == null) {
throw new PacketRejectedException();
}
} catch (UserNotFoundException e) {
Log.debug("Could not find user while cleaning up the roster in GoJara for user " + username, e);
response.setType(IQ.Type.error);
} catch (SharedGroupException e) {
// We should ignore this. External contacts cannot be in
// shared groups
}
}
dispatchPacket(response);
}
}
}
package org.jivesoftware.openfire.plugin.gojara.messagefilter.remoteroster.processors;
import java.util.Collection;
import org.dom4j.Element;
import org.dom4j.tree.DefaultAttribute;
import org.dom4j.tree.DefaultElement;
import org.jivesoftware.openfire.interceptor.PacketRejectedException;
import org.jivesoftware.openfire.plugin.gojara.messagefilter.remoteroster.RemoteRosterInterceptor;
import org.jivesoftware.openfire.roster.Roster;
import org.jivesoftware.openfire.roster.RosterItem;
import org.jivesoftware.openfire.roster.RosterManager;
import org.jivesoftware.openfire.user.UserNotFoundException;
import org.jivesoftware.util.JiveGlobals;
import org.xmpp.packet.IQ;
import org.xmpp.packet.Packet;
/**
* This class implements the XEP-xxx Remote Roster Management standard
* "2.3 Server or component requests user's roster". Part of command pattern
* used in {@link RemoteRosterInterceptor}
*
* Further information: <a
* href="http://jkaluza.fedorapeople.org/remote-roster.html#sect-id215516"
* >Here</a>
*
* @author Holger Bergunde
*
*/
public class SendRosterProcessor extends AbstractRemoteRosterProcessor {
private RosterManager _rosterManager;
private String _componentName;
public SendRosterProcessor(RosterManager rosterMananger, String componentName) {
Log.debug("Created SendRosterProcessor for " + componentName);
_rosterManager = rosterMananger;
_componentName = componentName;
}
@Override
public void process(Packet packet) throws PacketRejectedException {
Log.debug("Processing packet in SendRosterProcessor for " + _componentName);
IQ myPacket = (IQ) packet;
String user = myPacket.getTo().toString();
String username = getUsernameFromJid(user);
Roster roster;
if(JiveGlobals.getBooleanProperty("plugin.remoteroster.persistent", false)){
try {
roster = _rosterManager.getRoster(username);
Collection<RosterItem> items = roster.getRosterItems();
sendRosterToComponent(myPacket, items);
} catch (UserNotFoundException e) {
e.printStackTrace();
}
} else {
sendEmptyRoster(myPacket);
}
}
private void sendRosterToComponent(IQ requestPacket, Collection<RosterItem> items) {
Log.debug("Sending contacts from user " + requestPacket.getFrom().toString() + " to external Component");
IQ response = IQ.createResultIQ(requestPacket);
response.setTo(_componentName);
Element query = new DefaultElement("query");
for (RosterItem i : items) {
if (i.getJid().toString().contains(_componentName)) {
Log.debug("Roster exchange for external component " + _componentName + ". Sending user "
+ i.getJid().toString());
Element item = new DefaultElement("item", null);
item.add(new DefaultAttribute("jid", i.getJid().toString()));
item.add(new DefaultAttribute("name", i.getNickname()));
item.add(new DefaultAttribute("subscription", "both"));
for (String s : i.getGroups()) {
Element group = new DefaultElement("group");
group.setText(s);
item.add(group);
}
query.add(item);
}
}
query.addNamespace("", "jabber:iq:roster");
response.setChildElement(query);
dispatchPacket(response);
}
private void sendEmptyRoster(Packet requestPacket){
Log.debug("Sending nonpersistant-RemoteRosterResponse to external Component");
IQ iq = (IQ) requestPacket;
IQ response = IQ.createResultIQ(iq);
response.setTo(_componentName);
Element query = new DefaultElement("query");
query.addNamespace("", "jabber:iq:roster");
response.setChildElement(query);
dispatchPacket(response);
}
}
package org.jivesoftware.openfire.plugin.gojara.permissions;
import java.util.Collection;
import org.jivesoftware.openfire.group.Group;
import org.jivesoftware.openfire.group.GroupManager;
import org.jivesoftware.util.JiveGlobals;
import org.xmpp.packet.JID;
/**
*
* Gateways can be limited to a special user group. This manager helps you to
* check if the gateways is limited and if a user is in this group
*
* @author Holger Bergunde
*
*/
public class PermissionManager {
GroupManager _groupManager = GroupManager.getInstance();
public boolean isGatewayLimited(String subdomain) {
return getGroupForGateway(subdomain).length() > 0;
}
public boolean allowedForUser(String gateway, JID jid) {
String groupAllowedFor = getGroupForGateway(gateway);
if (groupAllowedFor != null) {
Collection<Group> groups = _groupManager.getGroups(jid);
for (Group gr : groups) {
if (gr.getName().equals(groupAllowedFor)) {
return true;
}
}
}
return false;
}
/**
* Returns the name of the group the usage is limited for the given gateway
*
* @param gateway
* the subdomain of the gateway
* @return name of the group, or "" if there is no group
*/
public String getGroupForGateway(String gateway) {
return JiveGlobals.getProperty("plugin.remoteroster.permissiongroup." + gateway, "");
}
/**
* Set the group name for the limitation
*
* @param gateway
* subdomain of the component
* @param group
* groupname that exists in openfire
*/
public void setGroupForGateway(String gateway, String group) {
JiveGlobals.setProperty("plugin.remoteroster.permissiongroup." + gateway, group);
}
/**
* Remove the limitaion from the specified gateway
*
* @param gateway
* subdomain of the component
*/
public void removeGatewayLimitation(String gateway) {
setGroupForGateway(gateway, "");
}
}
package org.jivesoftware.openfire.plugin.gojara.servlets;
import java.io.IOException;
import java.util.Collection;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.dom4j.Element;
import org.dom4j.tree.DefaultElement;
import org.jivesoftware.openfire.group.Group;
import org.jivesoftware.openfire.group.GroupManager;
/**
* Searching for groups via ajax from javascript. If there are groups matiching
* the search string it will send back to javascript using xml
*
* @author Holger Bergunde
*
*/
public class SearchGroupServlet extends HttpServlet {
/**
*
*/
private static final long serialVersionUID = 1L;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String param = req.getParameter("search");
Element root = new DefaultElement("result");
if (param != null && param.length() > 0) {
GroupManager manager = GroupManager.getInstance();
Collection<Group> groups = manager.getGroups();
for (Group gr : groups) {
if (gr.getName().startsWith(param)) {
root.addElement("item").addText(gr.getName());
}
}
}
resp.getOutputStream().write(root.asXML().getBytes());
resp.getOutputStream().close();
}
}
package org.jivesoftware.openfire.plugin.gojara.servlets;
import java.io.IOException;
import java.util.Collection;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.jivesoftware.openfire.plugin.gojara.database.DatabaseManager;
import org.jivesoftware.openfire.plugin.gojara.database.LogEntry;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
/**
* Servlet for live statistics using ajax. Sending last messages back for
* statistics using json notation.
*
* @author Holger Bergunde
*
*/
public class StatisticsServlet extends HttpServlet {
private static final long serialVersionUID = -6872070494892162304L;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
DatabaseManager db = DatabaseManager.getInstance();
String component = req.getParameter("component");
String fromString = req.getParameter("date");
/*
* { "packets": [ { "type": "IQ", "from": "holger" }, { "type":
* "Message", "from": "babett", "to": "holger", "date": "1243235344" } ]
* }
*/
int msgCnt = 0;
int iqCnt = 0;
int presenceCnt = 0;
int rosterCnt = 0;
JSONObject root = new JSONObject();
if (component != null && fromString != null) {
JSONArray packetArray = new JSONArray();
try {
root.put("packets", packetArray);
int limit = 40;
long from = Long.valueOf(fromString);
Collection<LogEntry> queryResult = db.getLogsByDateAndLimit(from, limit, component);
for (LogEntry entry : queryResult) {
JSONObject packet = new JSONObject();
packet.put("type", entry.getType()).put("to", entry.getTo()).put("from", entry.getFrom())
.put("date", entry.getDate());
packetArray.put(packet);
if (entry.getType().contains("IQ")) {
iqCnt++;
} else if (entry.getType().contains("Message")) {
msgCnt++;
} else if (entry.getType().contains("Roster")) {
rosterCnt++;
} else if (entry.getType().contains("Presence")) {
presenceCnt++;
}
}
JSONObject numbers = new JSONObject();
numbers.put("msg", msgCnt);
numbers.put("iq", iqCnt);
numbers.put("presence", presenceCnt);
numbers.put("roster", rosterCnt);
root.put("numbers", numbers);
} catch (JSONException e1) {
e1.printStackTrace();
}
}
resp.getOutputStream().write(root.toString().getBytes());
resp.getOutputStream().close();
}
}
package org.jivesoftware.openfire.plugin.gojara.utils;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.dom4j.Document;
import org.dom4j.DocumentHelper;
import org.dom4j.Node;
import org.dom4j.XPath;
public class XpathHelper {
/**
* Search the specified document for Nodes corresponding to the xpath Keep
* in mind that you have to use xmpp namespace for searching
* Predefined namespaces:
* jabber:iq:roster //roster:*
* jabber:iq:register //register:*
* http://jabber.org/protocol/disco#info //disco/*
* e.g
* '//roster:features'
*
* @param doc
* document
* @param xpath
* with roster namespace for searching in query nodes
* @return list of nodes found by xpath expression
*/
@SuppressWarnings("unchecked")
public static List<Node> findNodesInDocument(Document doc, String xpath)
{
Map<String, String> namespaceUris = new HashMap<String, String>();
namespaceUris.put("roster", "jabber:iq:roster");
namespaceUris.put("discoitems", "http://jabber.org/protocol/disco#items");
namespaceUris.put("register", "jabber:iq:register");
namespaceUris.put("disco", "http://jabber.org/protocol/disco#info");
XPath xPath = DocumentHelper.createXPath(xpath);
xPath.setNamespaceURIs(namespaceUris);
return xPath.selectNodes(doc);
}
/**
* Returns the username from the given jid. user.name@jabber.server.org
* returns "user.name"
*
* @param jid
* @return the extracted username as string
*/
public static String getUsernameFromJid(String jid)
{
int firstAtPos = jid.indexOf("@");
return firstAtPos != -1 ? jid.substring(0, firstAtPos) : jid;
}
}
<?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>stats</servlet-name>
<servlet-class>org.jivesoftware.openfire.plugin.gojara.servlets.StatisticsServlet</servlet-class>
</servlet>
<servlet>
<servlet-name>searchGroup</servlet-name>
<servlet-class>org.jivesoftware.openfire.plugin.gojara.servlets.SearchGroupServlet</servlet-class>
</servlet>
<!-- Servlet mappings -->
<servlet-mapping>
<servlet-name>stats</servlet-name>
<url-pattern>/stats</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>searchGroup</servlet-name>
<url-pattern>/groups</url-pattern>
</servlet-mapping>
</web-app>
@CHARSET "ISO-8859-1";
.header {
background-color: #FFF;
color: #363636;
padding: 3px;
margin-left: 20px;
font-weight: bold;
font-size: 14;
font-family: arial, helvetica, sans-serif;
}
.graph {
margin-top: 10px;
width: 100%;
height: 300px;
margin-bottom: 25px;
}
.div-main {
background: #F5F5F5 url(../../../images/jive-body-contentbox-bg.gif)
repeat-x scroll center top;
border: 1px solid #DCDCDC;
margin: 5px;
padding: 10px 10px;
float: left;
}
body table {
font-family: arial, helvetica, sans-serif;
font-size: 13;
}
#logTable {
background-color: #E3E3E3;
color: #363636;
width: 100%;
border-spacing: 0px;
border: 1px;
border-style: solid;
border-color: #575757;
border-spacing: 1px;
}
#tableLimit {
background-color: #E3E3E3;
border-width: 1px;
border-color: #363636;
border-style:solid;
width: 30px;
text-align: right;
float:right;
}
#logTable thead th {
border-bottom: 1px;
border-style: solid;
text-align: center;
border-color: #575757;
}
#logTable tfoot tr {
font-weight: bold;
border: 1px;
border-style: solid;
border-color: #575757;
}
\ No newline at end of file
@CHARSET "ISO-8859-1";
.suggest_link {
background-color: #FFFFFF;
padding: 2px 6px 2px 6px;
}
.suggest_link_over {
background-color: #3366CC;
padding: 2px 6px 2px 6px;
}
.grouptable {
border-spacing:0px;
}
#search_suggest {
position: absolute;
background-color: #FFFFFF;
text-align: left;
border: 1px solid #000000;
display: none;
}
.slider {
margin-left: 48px;
padding-top: 0px;
background-color: rgb(230, 230, 230);
border-color: #CCCCCC rgb(204, 204, 204);
color: white;
width: 550px;
border-style: solid;
border-width: 1px;
border-top: none;
display: none;
}
.sildeHeader {
background-color: #D4D2D2;
color: #000000;
padding-left: 5px;
padding-top: 2px;
padding-bottom: 2px;
border-bottom-color: gray;
border-bottom-width: 1px;
border-bottom-style: solid;
font-weight: bold;
}
.logtable {
border-left:6px;
width: 550px;
height: 100%;
border-spacing: 0px;
}
#loghead{
background-color:#DEDCDC;
font-weight: bold;
color:#404040;
}
#logodd {
background-color:#F0F0F0;
}
#logeven {
background-color:#E9E9E9;
}
#logfoot {
background-color:#DEDCDC;
}
div.graph {
width: 300px;
height: 100px;
float: right;
}
.gatewayHeader {
width: 600px;
background-color: #EEEEEE;
border-color: #CCCCCC rgb(204, 204, 204);
border-style: solid solid solid;
border-width: 1px 1px 1px;
}
.gatewayName {
padding-left: 10px;
}
.gatewayIcon {
border-left: 1px;
}
.gatewayCheckbox {
width: 20px;
}
.configTable {
width: 100%;
border-spacing:0px;
}
.configTable1Column {
width: 50%;
}
.gatewayIcons img {
float: right;
padding: 4px;
}
/*
#permissions {
margin-left: 1%;
padding-top: 0px;
background-color: rgb(230, 230, 230);
border-color: #CCCCCC rgb(204, 204, 204);
color: white;
width: 58%;
border-style: solid;
border-width: 1px;
border-top: none;
display: none;
} */
.permissionTableColumn {
width: 50%;
}
.ajaxloading {
padding-left: 4px;
padding-top: 3px;
}
.permissionTitle {
color: black;
size: +2;
}
dt {
width: 50px;
}
.hbg-bar {
padding-left: 5px;
background-color: #369;
color: #fff;
font-weight: bold;
}
.hbg-title {
text-align: center;
font-weight: bold;
}
\ No newline at end of file
if(!document.createElement("canvas").getContext){(function(){var z=Math;var K=z.round;var J=z.sin;var U=z.cos;var b=z.abs;var k=z.sqrt;var D=10;var F=D/2;function T(){return this.context_||(this.context_=new W(this))}var O=Array.prototype.slice;function G(i,j,m){var Z=O.call(arguments,2);return function(){return i.apply(j,Z.concat(O.call(arguments)))}}function AD(Z){return String(Z).replace(/&/g,"&amp;").replace(/"/g,"&quot;")}function r(i){if(!i.namespaces.g_vml_){i.namespaces.add("g_vml_","urn:schemas-microsoft-com:vml","#default#VML")}if(!i.namespaces.g_o_){i.namespaces.add("g_o_","urn:schemas-microsoft-com:office:office","#default#VML")}if(!i.styleSheets.ex_canvas_){var Z=i.createStyleSheet();Z.owningElement.id="ex_canvas_";Z.cssText="canvas{display:inline-block;overflow:hidden;text-align:left;width:300px;height:150px}"}}r(document);var E={init:function(Z){if(/MSIE/.test(navigator.userAgent)&&!window.opera){var i=Z||document;i.createElement("canvas");i.attachEvent("onreadystatechange",G(this.init_,this,i))}},init_:function(m){var j=m.getElementsByTagName("canvas");for(var Z=0;Z<j.length;Z++){this.initElement(j[Z])}},initElement:function(i){if(!i.getContext){i.getContext=T;r(i.ownerDocument);i.innerHTML="";i.attachEvent("onpropertychange",S);i.attachEvent("onresize",w);var Z=i.attributes;if(Z.width&&Z.width.specified){i.style.width=Z.width.nodeValue+"px"}else{i.width=i.clientWidth}if(Z.height&&Z.height.specified){i.style.height=Z.height.nodeValue+"px"}else{i.height=i.clientHeight}}return i}};function S(i){var Z=i.srcElement;switch(i.propertyName){case"width":Z.getContext().clearRect();Z.style.width=Z.attributes.width.nodeValue+"px";Z.firstChild.style.width=Z.clientWidth+"px";break;case"height":Z.getContext().clearRect();Z.style.height=Z.attributes.height.nodeValue+"px";Z.firstChild.style.height=Z.clientHeight+"px";break}}function w(i){var Z=i.srcElement;if(Z.firstChild){Z.firstChild.style.width=Z.clientWidth+"px";Z.firstChild.style.height=Z.clientHeight+"px"}}E.init();var I=[];for(var AC=0;AC<16;AC++){for(var AB=0;AB<16;AB++){I[AC*16+AB]=AC.toString(16)+AB.toString(16)}}function V(){return[[1,0,0],[0,1,0],[0,0,1]]}function d(m,j){var i=V();for(var Z=0;Z<3;Z++){for(var AF=0;AF<3;AF++){var p=0;for(var AE=0;AE<3;AE++){p+=m[Z][AE]*j[AE][AF]}i[Z][AF]=p}}return i}function Q(i,Z){Z.fillStyle=i.fillStyle;Z.lineCap=i.lineCap;Z.lineJoin=i.lineJoin;Z.lineWidth=i.lineWidth;Z.miterLimit=i.miterLimit;Z.shadowBlur=i.shadowBlur;Z.shadowColor=i.shadowColor;Z.shadowOffsetX=i.shadowOffsetX;Z.shadowOffsetY=i.shadowOffsetY;Z.strokeStyle=i.strokeStyle;Z.globalAlpha=i.globalAlpha;Z.font=i.font;Z.textAlign=i.textAlign;Z.textBaseline=i.textBaseline;Z.arcScaleX_=i.arcScaleX_;Z.arcScaleY_=i.arcScaleY_;Z.lineScale_=i.lineScale_}var B={aliceblue:"#F0F8FF",antiquewhite:"#FAEBD7",aquamarine:"#7FFFD4",azure:"#F0FFFF",beige:"#F5F5DC",bisque:"#FFE4C4",black:"#000000",blanchedalmond:"#FFEBCD",blueviolet:"#8A2BE2",brown:"#A52A2A",burlywood:"#DEB887",cadetblue:"#5F9EA0",chartreuse:"#7FFF00",chocolate:"#D2691E",coral:"#FF7F50",cornflowerblue:"#6495ED",cornsilk:"#FFF8DC",crimson:"#DC143C",cyan:"#00FFFF",darkblue:"#00008B",darkcyan:"#008B8B",darkgoldenrod:"#B8860B",darkgray:"#A9A9A9",darkgreen:"#006400",darkgrey:"#A9A9A9",darkkhaki:"#BDB76B",darkmagenta:"#8B008B",darkolivegreen:"#556B2F",darkorange:"#FF8C00",darkorchid:"#9932CC",darkred:"#8B0000",darksalmon:"#E9967A",darkseagreen:"#8FBC8F",darkslateblue:"#483D8B",darkslategray:"#2F4F4F",darkslategrey:"#2F4F4F",darkturquoise:"#00CED1",darkviolet:"#9400D3",deeppink:"#FF1493",deepskyblue:"#00BFFF",dimgray:"#696969",dimgrey:"#696969",dodgerblue:"#1E90FF",firebrick:"#B22222",floralwhite:"#FFFAF0",forestgreen:"#228B22",gainsboro:"#DCDCDC",ghostwhite:"#F8F8FF",gold:"#FFD700",goldenrod:"#DAA520",grey:"#808080",greenyellow:"#ADFF2F",honeydew:"#F0FFF0",hotpink:"#FF69B4",indianred:"#CD5C5C",indigo:"#4B0082",ivory:"#FFFFF0",khaki:"#F0E68C",lavender:"#E6E6FA",lavenderblush:"#FFF0F5",lawngreen:"#7CFC00",lemonchiffon:"#FFFACD",lightblue:"#ADD8E6",lightcoral:"#F08080",lightcyan:"#E0FFFF",lightgoldenrodyellow:"#FAFAD2",lightgreen:"#90EE90",lightgrey:"#D3D3D3",lightpink:"#FFB6C1",lightsalmon:"#FFA07A",lightseagreen:"#20B2AA",lightskyblue:"#87CEFA",lightslategray:"#778899",lightslategrey:"#778899",lightsteelblue:"#B0C4DE",lightyellow:"#FFFFE0",limegreen:"#32CD32",linen:"#FAF0E6",magenta:"#FF00FF",mediumaquamarine:"#66CDAA",mediumblue:"#0000CD",mediumorchid:"#BA55D3",mediumpurple:"#9370DB",mediumseagreen:"#3CB371",mediumslateblue:"#7B68EE",mediumspringgreen:"#00FA9A",mediumturquoise:"#48D1CC",mediumvioletred:"#C71585",midnightblue:"#191970",mintcream:"#F5FFFA",mistyrose:"#FFE4E1",moccasin:"#FFE4B5",navajowhite:"#FFDEAD",oldlace:"#FDF5E6",olivedrab:"#6B8E23",orange:"#FFA500",orangered:"#FF4500",orchid:"#DA70D6",palegoldenrod:"#EEE8AA",palegreen:"#98FB98",paleturquoise:"#AFEEEE",palevioletred:"#DB7093",papayawhip:"#FFEFD5",peachpuff:"#FFDAB9",peru:"#CD853F",pink:"#FFC0CB",plum:"#DDA0DD",powderblue:"#B0E0E6",rosybrown:"#BC8F8F",royalblue:"#4169E1",saddlebrown:"#8B4513",salmon:"#FA8072",sandybrown:"#F4A460",seagreen:"#2E8B57",seashell:"#FFF5EE",sienna:"#A0522D",skyblue:"#87CEEB",slateblue:"#6A5ACD",slategray:"#708090",slategrey:"#708090",snow:"#FFFAFA",springgreen:"#00FF7F",steelblue:"#4682B4",tan:"#D2B48C",thistle:"#D8BFD8",tomato:"#FF6347",turquoise:"#40E0D0",violet:"#EE82EE",wheat:"#F5DEB3",whitesmoke:"#F5F5F5",yellowgreen:"#9ACD32"};function g(i){var m=i.indexOf("(",3);var Z=i.indexOf(")",m+1);var j=i.substring(m+1,Z).split(",");if(j.length==4&&i.substr(3,1)=="a"){alpha=Number(j[3])}else{j[3]=1}return j}function C(Z){return parseFloat(Z)/100}function N(i,j,Z){return Math.min(Z,Math.max(j,i))}function c(AF){var j,i,Z;h=parseFloat(AF[0])/360%360;if(h<0){h++}s=N(C(AF[1]),0,1);l=N(C(AF[2]),0,1);if(s==0){j=i=Z=l}else{var m=l<0.5?l*(1+s):l+s-l*s;var AE=2*l-m;j=A(AE,m,h+1/3);i=A(AE,m,h);Z=A(AE,m,h-1/3)}return"#"+I[Math.floor(j*255)]+I[Math.floor(i*255)]+I[Math.floor(Z*255)]}function A(i,Z,j){if(j<0){j++}if(j>1){j--}if(6*j<1){return i+(Z-i)*6*j}else{if(2*j<1){return Z}else{if(3*j<2){return i+(Z-i)*(2/3-j)*6}else{return i}}}}function Y(Z){var AE,p=1;Z=String(Z);if(Z.charAt(0)=="#"){AE=Z}else{if(/^rgb/.test(Z)){var m=g(Z);var AE="#",AF;for(var j=0;j<3;j++){if(m[j].indexOf("%")!=-1){AF=Math.floor(C(m[j])*255)}else{AF=Number(m[j])}AE+=I[N(AF,0,255)]}p=m[3]}else{if(/^hsl/.test(Z)){var m=g(Z);AE=c(m);p=m[3]}else{AE=B[Z]||Z}}}return{color:AE,alpha:p}}var L={style:"normal",variant:"normal",weight:"normal",size:10,family:"sans-serif"};var f={};function X(Z){if(f[Z]){return f[Z]}var m=document.createElement("div");var j=m.style;try{j.font=Z}catch(i){}return f[Z]={style:j.fontStyle||L.style,variant:j.fontVariant||L.variant,weight:j.fontWeight||L.weight,size:j.fontSize||L.size,family:j.fontFamily||L.family}}function P(j,i){var Z={};for(var AF in j){Z[AF]=j[AF]}var AE=parseFloat(i.currentStyle.fontSize),m=parseFloat(j.size);if(typeof j.size=="number"){Z.size=j.size}else{if(j.size.indexOf("px")!=-1){Z.size=m}else{if(j.size.indexOf("em")!=-1){Z.size=AE*m}else{if(j.size.indexOf("%")!=-1){Z.size=(AE/100)*m}else{if(j.size.indexOf("pt")!=-1){Z.size=m/0.75}else{Z.size=AE}}}}}Z.size*=0.981;return Z}function AA(Z){return Z.style+" "+Z.variant+" "+Z.weight+" "+Z.size+"px "+Z.family}function t(Z){switch(Z){case"butt":return"flat";case"round":return"round";case"square":default:return"square"}}function W(i){this.m_=V();this.mStack_=[];this.aStack_=[];this.currentPath_=[];this.strokeStyle="#000";this.fillStyle="#000";this.lineWidth=1;this.lineJoin="miter";this.lineCap="butt";this.miterLimit=D*1;this.globalAlpha=1;this.font="10px sans-serif";this.textAlign="left";this.textBaseline="alphabetic";this.canvas=i;var Z=i.ownerDocument.createElement("div");Z.style.width=i.clientWidth+"px";Z.style.height=i.clientHeight+"px";Z.style.overflow="hidden";Z.style.position="absolute";i.appendChild(Z);this.element_=Z;this.arcScaleX_=1;this.arcScaleY_=1;this.lineScale_=1}var M=W.prototype;M.clearRect=function(){if(this.textMeasureEl_){this.textMeasureEl_.removeNode(true);this.textMeasureEl_=null}this.element_.innerHTML=""};M.beginPath=function(){this.currentPath_=[]};M.moveTo=function(i,Z){var j=this.getCoords_(i,Z);this.currentPath_.push({type:"moveTo",x:j.x,y:j.y});this.currentX_=j.x;this.currentY_=j.y};M.lineTo=function(i,Z){var j=this.getCoords_(i,Z);this.currentPath_.push({type:"lineTo",x:j.x,y:j.y});this.currentX_=j.x;this.currentY_=j.y};M.bezierCurveTo=function(j,i,AI,AH,AG,AE){var Z=this.getCoords_(AG,AE);var AF=this.getCoords_(j,i);var m=this.getCoords_(AI,AH);e(this,AF,m,Z)};function e(Z,m,j,i){Z.currentPath_.push({type:"bezierCurveTo",cp1x:m.x,cp1y:m.y,cp2x:j.x,cp2y:j.y,x:i.x,y:i.y});Z.currentX_=i.x;Z.currentY_=i.y}M.quadraticCurveTo=function(AG,j,i,Z){var AF=this.getCoords_(AG,j);var AE=this.getCoords_(i,Z);var AH={x:this.currentX_+2/3*(AF.x-this.currentX_),y:this.currentY_+2/3*(AF.y-this.currentY_)};var m={x:AH.x+(AE.x-this.currentX_)/3,y:AH.y+(AE.y-this.currentY_)/3};e(this,AH,m,AE)};M.arc=function(AJ,AH,AI,AE,i,j){AI*=D;var AN=j?"at":"wa";var AK=AJ+U(AE)*AI-F;var AM=AH+J(AE)*AI-F;var Z=AJ+U(i)*AI-F;var AL=AH+J(i)*AI-F;if(AK==Z&&!j){AK+=0.125}var m=this.getCoords_(AJ,AH);var AG=this.getCoords_(AK,AM);var AF=this.getCoords_(Z,AL);this.currentPath_.push({type:AN,x:m.x,y:m.y,radius:AI,xStart:AG.x,yStart:AG.y,xEnd:AF.x,yEnd:AF.y})};M.rect=function(j,i,Z,m){this.moveTo(j,i);this.lineTo(j+Z,i);this.lineTo(j+Z,i+m);this.lineTo(j,i+m);this.closePath()};M.strokeRect=function(j,i,Z,m){var p=this.currentPath_;this.beginPath();this.moveTo(j,i);this.lineTo(j+Z,i);this.lineTo(j+Z,i+m);this.lineTo(j,i+m);this.closePath();this.stroke();this.currentPath_=p};M.fillRect=function(j,i,Z,m){var p=this.currentPath_;this.beginPath();this.moveTo(j,i);this.lineTo(j+Z,i);this.lineTo(j+Z,i+m);this.lineTo(j,i+m);this.closePath();this.fill();this.currentPath_=p};M.createLinearGradient=function(i,m,Z,j){var p=new v("gradient");p.x0_=i;p.y0_=m;p.x1_=Z;p.y1_=j;return p};M.createRadialGradient=function(m,AE,j,i,p,Z){var AF=new v("gradientradial");AF.x0_=m;AF.y0_=AE;AF.r0_=j;AF.x1_=i;AF.y1_=p;AF.r1_=Z;return AF};M.drawImage=function(AO,j){var AH,AF,AJ,AV,AM,AK,AQ,AX;var AI=AO.runtimeStyle.width;var AN=AO.runtimeStyle.height;AO.runtimeStyle.width="auto";AO.runtimeStyle.height="auto";var AG=AO.width;var AT=AO.height;AO.runtimeStyle.width=AI;AO.runtimeStyle.height=AN;if(arguments.length==3){AH=arguments[1];AF=arguments[2];AM=AK=0;AQ=AJ=AG;AX=AV=AT}else{if(arguments.length==5){AH=arguments[1];AF=arguments[2];AJ=arguments[3];AV=arguments[4];AM=AK=0;AQ=AG;AX=AT}else{if(arguments.length==9){AM=arguments[1];AK=arguments[2];AQ=arguments[3];AX=arguments[4];AH=arguments[5];AF=arguments[6];AJ=arguments[7];AV=arguments[8]}else{throw Error("Invalid number of arguments")}}}var AW=this.getCoords_(AH,AF);var m=AQ/2;var i=AX/2;var AU=[];var Z=10;var AE=10;AU.push(" <g_vml_:group",' coordsize="',D*Z,",",D*AE,'"',' coordorigin="0,0"',' style="width:',Z,"px;height:",AE,"px;position:absolute;");if(this.m_[0][0]!=1||this.m_[0][1]||this.m_[1][1]!=1||this.m_[1][0]){var p=[];p.push("M11=",this.m_[0][0],",","M12=",this.m_[1][0],",","M21=",this.m_[0][1],",","M22=",this.m_[1][1],",","Dx=",K(AW.x/D),",","Dy=",K(AW.y/D),"");var AS=AW;var AR=this.getCoords_(AH+AJ,AF);var AP=this.getCoords_(AH,AF+AV);var AL=this.getCoords_(AH+AJ,AF+AV);AS.x=z.max(AS.x,AR.x,AP.x,AL.x);AS.y=z.max(AS.y,AR.y,AP.y,AL.y);AU.push("padding:0 ",K(AS.x/D),"px ",K(AS.y/D),"px 0;filter:progid:DXImageTransform.Microsoft.Matrix(",p.join(""),", sizingmethod='clip');")}else{AU.push("top:",K(AW.y/D),"px;left:",K(AW.x/D),"px;")}AU.push(' ">','<g_vml_:image src="',AO.src,'"',' style="width:',D*AJ,"px;"," height:",D*AV,'px"',' cropleft="',AM/AG,'"',' croptop="',AK/AT,'"',' cropright="',(AG-AM-AQ)/AG,'"',' cropbottom="',(AT-AK-AX)/AT,'"'," />","</g_vml_:group>");this.element_.insertAdjacentHTML("BeforeEnd",AU.join(""))};M.stroke=function(AM){var m=10;var AN=10;var AE=5000;var AG={x:null,y:null};var AL={x:null,y:null};for(var AH=0;AH<this.currentPath_.length;AH+=AE){var AK=[];var AF=false;AK.push("<g_vml_:shape",' filled="',!!AM,'"',' style="position:absolute;width:',m,"px;height:",AN,'px;"',' coordorigin="0,0"',' coordsize="',D*m,",",D*AN,'"',' stroked="',!AM,'"',' path="');var AO=false;for(var AI=AH;AI<Math.min(AH+AE,this.currentPath_.length);AI++){if(AI%AE==0&&AI>0){AK.push(" m ",K(this.currentPath_[AI-1].x),",",K(this.currentPath_[AI-1].y))}var Z=this.currentPath_[AI];var AJ;switch(Z.type){case"moveTo":AJ=Z;AK.push(" m ",K(Z.x),",",K(Z.y));break;case"lineTo":AK.push(" l ",K(Z.x),",",K(Z.y));break;case"close":AK.push(" x ");Z=null;break;case"bezierCurveTo":AK.push(" c ",K(Z.cp1x),",",K(Z.cp1y),",",K(Z.cp2x),",",K(Z.cp2y),",",K(Z.x),",",K(Z.y));break;case"at":case"wa":AK.push(" ",Z.type," ",K(Z.x-this.arcScaleX_*Z.radius),",",K(Z.y-this.arcScaleY_*Z.radius)," ",K(Z.x+this.arcScaleX_*Z.radius),",",K(Z.y+this.arcScaleY_*Z.radius)," ",K(Z.xStart),",",K(Z.yStart)," ",K(Z.xEnd),",",K(Z.yEnd));break}if(Z){if(AG.x==null||Z.x<AG.x){AG.x=Z.x}if(AL.x==null||Z.x>AL.x){AL.x=Z.x}if(AG.y==null||Z.y<AG.y){AG.y=Z.y}if(AL.y==null||Z.y>AL.y){AL.y=Z.y}}}AK.push(' ">');if(!AM){R(this,AK)}else{a(this,AK,AG,AL)}AK.push("</g_vml_:shape>");this.element_.insertAdjacentHTML("beforeEnd",AK.join(""))}};function R(j,AE){var i=Y(j.strokeStyle);var m=i.color;var p=i.alpha*j.globalAlpha;var Z=j.lineScale_*j.lineWidth;if(Z<1){p*=Z}AE.push("<g_vml_:stroke",' opacity="',p,'"',' joinstyle="',j.lineJoin,'"',' miterlimit="',j.miterLimit,'"',' endcap="',t(j.lineCap),'"',' weight="',Z,'px"',' color="',m,'" />')}function a(AO,AG,Ah,AP){var AH=AO.fillStyle;var AY=AO.arcScaleX_;var AX=AO.arcScaleY_;var Z=AP.x-Ah.x;var m=AP.y-Ah.y;if(AH instanceof v){var AL=0;var Ac={x:0,y:0};var AU=0;var AK=1;if(AH.type_=="gradient"){var AJ=AH.x0_/AY;var j=AH.y0_/AX;var AI=AH.x1_/AY;var Aj=AH.y1_/AX;var Ag=AO.getCoords_(AJ,j);var Af=AO.getCoords_(AI,Aj);var AE=Af.x-Ag.x;var p=Af.y-Ag.y;AL=Math.atan2(AE,p)*180/Math.PI;if(AL<0){AL+=360}if(AL<0.000001){AL=0}}else{var Ag=AO.getCoords_(AH.x0_,AH.y0_);Ac={x:(Ag.x-Ah.x)/Z,y:(Ag.y-Ah.y)/m};Z/=AY*D;m/=AX*D;var Aa=z.max(Z,m);AU=2*AH.r0_/Aa;AK=2*AH.r1_/Aa-AU}var AS=AH.colors_;AS.sort(function(Ak,i){return Ak.offset-i.offset});var AN=AS.length;var AR=AS[0].color;var AQ=AS[AN-1].color;var AW=AS[0].alpha*AO.globalAlpha;var AV=AS[AN-1].alpha*AO.globalAlpha;var Ab=[];for(var Ae=0;Ae<AN;Ae++){var AM=AS[Ae];Ab.push(AM.offset*AK+AU+" "+AM.color)}AG.push('<g_vml_:fill type="',AH.type_,'"',' method="none" focus="100%"',' color="',AR,'"',' color2="',AQ,'"',' colors="',Ab.join(","),'"',' opacity="',AV,'"',' g_o_:opacity2="',AW,'"',' angle="',AL,'"',' focusposition="',Ac.x,",",Ac.y,'" />')}else{if(AH instanceof u){if(Z&&m){var AF=-Ah.x;var AZ=-Ah.y;AG.push("<g_vml_:fill",' position="',AF/Z*AY*AY,",",AZ/m*AX*AX,'"',' type="tile"',' src="',AH.src_,'" />')}}else{var Ai=Y(AO.fillStyle);var AT=Ai.color;var Ad=Ai.alpha*AO.globalAlpha;AG.push('<g_vml_:fill color="',AT,'" opacity="',Ad,'" />')}}}M.fill=function(){this.stroke(true)};M.closePath=function(){this.currentPath_.push({type:"close"})};M.getCoords_=function(j,i){var Z=this.m_;return{x:D*(j*Z[0][0]+i*Z[1][0]+Z[2][0])-F,y:D*(j*Z[0][1]+i*Z[1][1]+Z[2][1])-F}};M.save=function(){var Z={};Q(this,Z);this.aStack_.push(Z);this.mStack_.push(this.m_);this.m_=d(V(),this.m_)};M.restore=function(){if(this.aStack_.length){Q(this.aStack_.pop(),this);this.m_=this.mStack_.pop()}};function H(Z){return isFinite(Z[0][0])&&isFinite(Z[0][1])&&isFinite(Z[1][0])&&isFinite(Z[1][1])&&isFinite(Z[2][0])&&isFinite(Z[2][1])}function y(i,Z,j){if(!H(Z)){return }i.m_=Z;if(j){var p=Z[0][0]*Z[1][1]-Z[0][1]*Z[1][0];i.lineScale_=k(b(p))}}M.translate=function(j,i){var Z=[[1,0,0],[0,1,0],[j,i,1]];y(this,d(Z,this.m_),false)};M.rotate=function(i){var m=U(i);var j=J(i);var Z=[[m,j,0],[-j,m,0],[0,0,1]];y(this,d(Z,this.m_),false)};M.scale=function(j,i){this.arcScaleX_*=j;this.arcScaleY_*=i;var Z=[[j,0,0],[0,i,0],[0,0,1]];y(this,d(Z,this.m_),true)};M.transform=function(p,m,AF,AE,i,Z){var j=[[p,m,0],[AF,AE,0],[i,Z,1]];y(this,d(j,this.m_),true)};M.setTransform=function(AE,p,AG,AF,j,i){var Z=[[AE,p,0],[AG,AF,0],[j,i,1]];y(this,Z,true)};M.drawText_=function(AK,AI,AH,AN,AG){var AM=this.m_,AQ=1000,i=0,AP=AQ,AF={x:0,y:0},AE=[];var Z=P(X(this.font),this.element_);var j=AA(Z);var AR=this.element_.currentStyle;var p=this.textAlign.toLowerCase();switch(p){case"left":case"center":case"right":break;case"end":p=AR.direction=="ltr"?"right":"left";break;case"start":p=AR.direction=="rtl"?"right":"left";break;default:p="left"}switch(this.textBaseline){case"hanging":case"top":AF.y=Z.size/1.75;break;case"middle":break;default:case null:case"alphabetic":case"ideographic":case"bottom":AF.y=-Z.size/2.25;break}switch(p){case"right":i=AQ;AP=0.05;break;case"center":i=AP=AQ/2;break}var AO=this.getCoords_(AI+AF.x,AH+AF.y);AE.push('<g_vml_:line from="',-i,' 0" to="',AP,' 0.05" ',' coordsize="100 100" coordorigin="0 0"',' filled="',!AG,'" stroked="',!!AG,'" style="position:absolute;width:1px;height:1px;">');if(AG){R(this,AE)}else{a(this,AE,{x:-i,y:0},{x:AP,y:Z.size})}var AL=AM[0][0].toFixed(3)+","+AM[1][0].toFixed(3)+","+AM[0][1].toFixed(3)+","+AM[1][1].toFixed(3)+",0,0";var AJ=K(AO.x/D)+","+K(AO.y/D);AE.push('<g_vml_:skew on="t" matrix="',AL,'" ',' offset="',AJ,'" origin="',i,' 0" />','<g_vml_:path textpathok="true" />','<g_vml_:textpath on="true" string="',AD(AK),'" style="v-text-align:',p,";font:",AD(j),'" /></g_vml_:line>');this.element_.insertAdjacentHTML("beforeEnd",AE.join(""))};M.fillText=function(j,Z,m,i){this.drawText_(j,Z,m,i,false)};M.strokeText=function(j,Z,m,i){this.drawText_(j,Z,m,i,true)};M.measureText=function(j){if(!this.textMeasureEl_){var Z='<span style="position:absolute;top:-20000px;left:0;padding:0;margin:0;border:none;white-space:pre;"></span>';this.element_.insertAdjacentHTML("beforeEnd",Z);this.textMeasureEl_=this.element_.lastChild}var i=this.element_.ownerDocument;this.textMeasureEl_.innerHTML="";this.textMeasureEl_.style.font=this.font;this.textMeasureEl_.appendChild(i.createTextNode(j));return{width:this.textMeasureEl_.offsetWidth}};M.clip=function(){};M.arcTo=function(){};M.createPattern=function(i,Z){return new u(i,Z)};function v(Z){this.type_=Z;this.x0_=0;this.y0_=0;this.r0_=0;this.x1_=0;this.y1_=0;this.r1_=0;this.colors_=[]}v.prototype.addColorStop=function(i,Z){Z=Y(Z);this.colors_.push({offset:i,color:Z.color,alpha:Z.alpha})};function u(i,Z){q(i);switch(Z){case"repeat":case null:case"":this.repetition_="repeat";break;case"repeat-x":case"repeat-y":case"no-repeat":this.repetition_=Z;break;default:n("SYNTAX_ERR")}this.src_=i.src;this.width_=i.width;this.height_=i.height}function n(Z){throw new o(Z)}function q(Z){if(!Z||Z.nodeType!=1||Z.tagName!="IMG"){n("TYPE_MISMATCH_ERR")}if(Z.readyState!="complete"){n("INVALID_STATE_ERR")}}function o(Z){this.code=this[Z];this.message=Z+": DOM Exception "+this.code}var x=o.prototype=new Error;x.INDEX_SIZE_ERR=1;x.DOMSTRING_SIZE_ERR=2;x.HIERARCHY_REQUEST_ERR=3;x.WRONG_DOCUMENT_ERR=4;x.INVALID_CHARACTER_ERR=5;x.NO_DATA_ALLOWED_ERR=6;x.NO_MODIFICATION_ALLOWED_ERR=7;x.NOT_FOUND_ERR=8;x.NOT_SUPPORTED_ERR=9;x.INUSE_ATTRIBUTE_ERR=10;x.INVALID_STATE_ERR=11;x.SYNTAX_ERR=12;x.INVALID_MODIFICATION_ERR=13;x.NAMESPACE_ERR=14;x.INVALID_ACCESS_ERR=15;x.VALIDATION_ERR=16;x.TYPE_MISMATCH_ERR=17;G_vmlCanvasManager=E;CanvasRenderingContext2D=W;CanvasGradient=v;CanvasPattern=u;DOMException=o})()};
\ No newline at end of file
var http = function() {
this.callback = function(a) {};
this.obj = window.XMLHttpRequest ? new XMLHttpRequest()
: new ActiveXObject("Microsoft.XMLHTTP");
this.load = function(url) {
this.obj.open('get', url);
this.obj.callback = this.callback;
this.obj.onreadystatechange = this.handle;
this.obj.send(null);
};
this.handle = function() {
if (this.readyState == 4)
this.callback(this);
};
};
This source diff could not be displayed because it is too large. You can view the blob instead.
/*
Flot plugin for rendering pie charts. The plugin assumes the data is
coming is as a single data value for each series, and each of those
values is a positive value or zero (negative numbers don't make
any sense and will cause strange effects). The data values do
NOT need to be passed in as percentage values because it
internally calculates the total and percentages.
* Created by Brian Medendorp, June 2009
* Updated November 2009 with contributions from: btburnett3, Anthony Aragues and Xavi Ivars
* Changes:
2009-10-22: lineJoin set to round
2009-10-23: IE full circle fix, donut
2009-11-11: Added basic hover from btburnett3 - does not work in IE, and center is off in Chrome and Opera
2009-11-17: Added IE hover capability submitted by Anthony Aragues
2009-11-18: Added bug fix submitted by Xavi Ivars (issues with arrays when other JS libraries are included as well)
Available options are:
series: {
pie: {
show: true/false
radius: 0-1 for percentage of fullsize, or a specified pixel length, or 'auto'
innerRadius: 0-1 for percentage of fullsize or a specified pixel length, for creating a donut effect
startAngle: 0-2 factor of PI used for starting angle (in radians) i.e 3/2 starts at the top, 0 and 2 have the same result
tilt: 0-1 for percentage to tilt the pie, where 1 is no tilt, and 0 is completely flat (nothing will show)
offset: {
top: integer value to move the pie up or down
left: integer value to move the pie left or right, or 'auto'
},
stroke: {
color: any hexidecimal color value (other formats may or may not work, so best to stick with something like '#FFF')
width: integer pixel width of the stroke
},
label: {
show: true/false, or 'auto'
formatter: a user-defined function that modifies the text/style of the label text
radius: 0-1 for percentage of fullsize, or a specified pixel length
background: {
color: any hexidecimal color value (other formats may or may not work, so best to stick with something like '#000')
opacity: 0-1
},
threshold: 0-1 for the percentage value at which to hide labels (if they're too small)
},
combine: {
threshold: 0-1 for the percentage value at which to combine slices (if they're too small)
color: any hexidecimal color value (other formats may or may not work, so best to stick with something like '#CCC'), if null, the plugin will automatically use the color of the first slice to be combined
label: any text value of what the combined slice should be labeled
}
highlight: {
opacity: 0-1
}
}
}
More detail and specific examples can be found in the included HTML file.
*/
(function ($)
{
function init(plot) // this is the "body" of the plugin
{
var canvas = null;
var target = null;
var maxRadius = null;
var centerLeft = null;
var centerTop = null;
var total = 0;
var redraw = true;
var redrawAttempts = 10;
var shrink = 0.95;
var legendWidth = 0;
var processed = false;
var raw = false;
// interactive variables
var highlights = [];
// add hook to determine if pie plugin in enabled, and then perform necessary operations
plot.hooks.processOptions.push(checkPieEnabled);
plot.hooks.bindEvents.push(bindEvents);
// check to see if the pie plugin is enabled
function checkPieEnabled(plot, options)
{
if (options.series.pie.show)
{
//disable grid
options.grid.show = false;
// set labels.show
if (options.series.pie.label.show=='auto')
if (options.legend.show)
options.series.pie.label.show = false;
else
options.series.pie.label.show = true;
// set radius
if (options.series.pie.radius=='auto')
if (options.series.pie.label.show)
options.series.pie.radius = 3/4;
else
options.series.pie.radius = 1;
// ensure sane tilt
if (options.series.pie.tilt>1)
options.series.pie.tilt=1;
if (options.series.pie.tilt<0)
options.series.pie.tilt=0;
// add processData hook to do transformations on the data
plot.hooks.processDatapoints.push(processDatapoints);
plot.hooks.drawOverlay.push(drawOverlay);
// add draw hook
plot.hooks.draw.push(draw);
}
}
// bind hoverable events
function bindEvents(plot, eventHolder)
{
var options = plot.getOptions();
if (options.series.pie.show && options.grid.hoverable)
eventHolder.unbind('mousemove').mousemove(onMouseMove);
if (options.series.pie.show && options.grid.clickable)
eventHolder.unbind('click').click(onClick);
}
// debugging function that prints out an object
function alertObject(obj)
{
var msg = '';
function traverse(obj, depth)
{
if (!depth)
depth = 0;
for (var i = 0; i < obj.length; ++i)
{
for (var j=0; j<depth; j++)
msg += '\t';
if( typeof obj[i] == "object")
{ // its an object
msg += ''+i+':\n';
traverse(obj[i], depth+1);
}
else
{ // its a value
msg += ''+i+': '+obj[i]+'\n';
}
}
}
traverse(obj);
alert(msg);
}
function calcTotal(data)
{
for (var i = 0; i < data.length; ++i)
{
var item = parseFloat(data[i].data[0][1]);
if (item)
total += item;
}
}
function processDatapoints(plot, series, data, datapoints)
{
if (!processed)
{
processed = true;
canvas = plot.getCanvas();
target = $(canvas).parent();
options = plot.getOptions();
plot.setData(combine(plot.getData()));
}
}
function setupPie()
{
legendWidth = target.children().filter('.legend').children().width();
// calculate maximum radius and center point
maxRadius = Math.min(canvas.width,(canvas.height/options.series.pie.tilt))/2;
centerTop = (canvas.height/2)+options.series.pie.offset.top;
centerLeft = (canvas.width/2);
if (options.series.pie.offset.left=='auto')
if (options.legend.position.match('w'))
centerLeft += legendWidth/2;
else
centerLeft -= legendWidth/2;
else
centerLeft += options.series.pie.offset.left;
if (centerLeft<maxRadius)
centerLeft = maxRadius;
else if (centerLeft>canvas.width-maxRadius)
centerLeft = canvas.width-maxRadius;
}
function fixData(data)
{
for (var i = 0; i < data.length; ++i)
{
if (typeof(data[i].data)=='number')
data[i].data = [[1,data[i].data]];
else if (typeof(data[i].data)=='undefined' || typeof(data[i].data[0])=='undefined')
{
if (typeof(data[i].data)!='undefined' && typeof(data[i].data.label)!='undefined')
data[i].label = data[i].data.label; // fix weirdness coming from flot
data[i].data = [[1,0]];
}
}
return data;
}
function combine(data)
{
data = fixData(data);
calcTotal(data);
var combined = 0;
var numCombined = 0;
var color = options.series.pie.combine.color;
var newdata = [];
for (var i = 0; i < data.length; ++i)
{
// make sure its a number
data[i].data[0][1] = parseFloat(data[i].data[0][1]);
if (!data[i].data[0][1])
data[i].data[0][1] = 0;
if (data[i].data[0][1]/total<=options.series.pie.combine.threshold)
{
combined += data[i].data[0][1];
numCombined++;
if (!color)
color = data[i].color;
}
else
{
newdata.push({
data: [[1,data[i].data[0][1]]],
color: data[i].color,
label: data[i].label,
angle: (data[i].data[0][1]*(Math.PI*2))/total,
percent: (data[i].data[0][1]/total*100)
});
}
}
if (numCombined>0)
newdata.push({
data: [[1,combined]],
color: color,
label: options.series.pie.combine.label,
angle: (combined*(Math.PI*2))/total,
percent: (combined/total*100)
});
return newdata;
}
function draw(plot, newCtx)
{
if (!target) return; // if no series were passed
ctx = newCtx;
setupPie();
var slices = plot.getData();
var attempts = 0;
while (redraw && attempts<redrawAttempts)
{
redraw = false;
if (attempts>0)
maxRadius *= shrink;
attempts += 1;
clear();
if (options.series.pie.tilt<=0.8)
drawShadow();
drawPie();
}
if (attempts >= redrawAttempts) {
clear();
target.prepend('<div class="error">Could not draw pie with labels contained inside canvas</div>');
}
if ( plot.setSeries && plot.insertLegend )
{
plot.setSeries(slices);
plot.insertLegend();
}
// we're actually done at this point, just defining internal functions at this point
function clear()
{
ctx.clearRect(0,0,canvas.width,canvas.height);
target.children().filter('.pieLabel, .pieLabelBackground').remove();
}
function drawShadow()
{
var shadowLeft = 5;
var shadowTop = 15;
var edge = 10;
var alpha = 0.02;
// set radius
if (options.series.pie.radius>1)
var radius = options.series.pie.radius;
else
var radius = maxRadius * options.series.pie.radius;
if (radius>=(canvas.width/2)-shadowLeft || radius*options.series.pie.tilt>=(canvas.height/2)-shadowTop || radius<=edge)
return; // shadow would be outside canvas, so don't draw it
ctx.save();
ctx.translate(shadowLeft,shadowTop);
ctx.globalAlpha = alpha;
ctx.fillStyle = '#000';
// center and rotate to starting position
ctx.translate(centerLeft,centerTop);
ctx.scale(1, options.series.pie.tilt);
//radius -= edge;
for (var i=1; i<=edge; i++)
{
ctx.beginPath();
ctx.arc(0,0,radius,0,Math.PI*2,false);
ctx.fill();
radius -= i;
}
ctx.restore();
}
function drawPie()
{
startAngle = Math.PI*options.series.pie.startAngle;
// set radius
if (options.series.pie.radius>1)
var radius = options.series.pie.radius;
else
var radius = maxRadius * options.series.pie.radius;
// center and rotate to starting position
ctx.save();
ctx.translate(centerLeft,centerTop);
ctx.scale(1, options.series.pie.tilt);
//ctx.rotate(startAngle); // start at top; -- This doesn't work properly in Opera
// draw slices
ctx.save();
var currentAngle = startAngle;
for (var i = 0; i < slices.length; ++i)
{
slices[i].startAngle = currentAngle;
drawSlice(slices[i].angle, slices[i].color, true);
}
ctx.restore();
// draw slice outlines
ctx.save();
ctx.lineWidth = options.series.pie.stroke.width;
currentAngle = startAngle;
for (var i = 0; i < slices.length; ++i)
drawSlice(slices[i].angle, options.series.pie.stroke.color, false);
ctx.restore();
// draw donut hole
drawDonutHole(ctx);
// draw labels
if (options.series.pie.label.show)
drawLabels();
// restore to original state
ctx.restore();
function drawSlice(angle, color, fill)
{
if (angle<=0)
return;
if (fill)
ctx.fillStyle = color;
else
{
ctx.strokeStyle = color;
ctx.lineJoin = 'round';
}
ctx.beginPath();
if (Math.abs(angle - Math.PI*2) > 0.000000001)
ctx.moveTo(0,0); // Center of the pie
else if ($.browser.msie)
angle -= 0.0001;
//ctx.arc(0,0,radius,0,angle,false); // This doesn't work properly in Opera
ctx.arc(0,0,radius,currentAngle,currentAngle+angle,false);
ctx.closePath();
//ctx.rotate(angle); // This doesn't work properly in Opera
currentAngle += angle;
if (fill)
ctx.fill();
else
ctx.stroke();
}
function drawLabels()
{
var currentAngle = startAngle;
// set radius
if (options.series.pie.label.radius>1)
var radius = options.series.pie.label.radius;
else
var radius = maxRadius * options.series.pie.label.radius;
for (var i = 0; i < slices.length; ++i)
{
if (slices[i].percent >= options.series.pie.label.threshold*100)
drawLabel(slices[i], currentAngle, i);
currentAngle += slices[i].angle;
}
function drawLabel(slice, startAngle, index)
{
if (slice.data[0][1]==0)
return;
// format label text
var lf = options.legend.labelFormatter, text, plf = options.series.pie.label.formatter;
if (lf)
text = lf(slice.label, slice);
else
text = slice.label;
if (plf)
text = plf(text, slice);
var halfAngle = ((startAngle+slice.angle) + startAngle)/2;
var x = centerLeft + Math.round(Math.cos(halfAngle) * radius);
var y = centerTop + Math.round(Math.sin(halfAngle) * radius) * options.series.pie.tilt;
var html = '<span class="pieLabel" id="pieLabel'+index+'" style="position:absolute;top:' + y + 'px;left:' + x + 'px;">' + text + "</span>";
target.append(html);
var label = target.children('#pieLabel'+index);
var labelTop = (y - label.height()/2);
var labelLeft = (x - label.width()/2);
label.css('top', labelTop);
label.css('left', labelLeft);
// check to make sure that the label is not outside the canvas
if (0-labelTop>0 || 0-labelLeft>0 || canvas.height-(labelTop+label.height())<0 || canvas.width-(labelLeft+label.width())<0)
redraw = true;
if (options.series.pie.label.background.opacity != 0) {
// put in the transparent background separately to avoid blended labels and label boxes
var c = options.series.pie.label.background.color;
if (c == null) {
c = slice.color;
}
var pos = 'top:'+labelTop+'px;left:'+labelLeft+'px;';
$('<div class="pieLabelBackground" style="position:absolute;width:' + label.width() + 'px;height:' + label.height() + 'px;' + pos +'background-color:' + c + ';"> </div>').insertBefore(label).css('opacity', options.series.pie.label.background.opacity);
}
} // end individual label function
} // end drawLabels function
} // end drawPie function
} // end draw function
// Placed here because it needs to be accessed from multiple locations
function drawDonutHole(layer)
{
// draw donut hole
if(options.series.pie.innerRadius > 0)
{
// subtract the center
layer.save();
innerRadius = options.series.pie.innerRadius > 1 ? options.series.pie.innerRadius : maxRadius * options.series.pie.innerRadius;
layer.globalCompositeOperation = 'destination-out'; // this does not work with excanvas, but it will fall back to using the stroke color
layer.beginPath();
layer.fillStyle = options.series.pie.stroke.color;
layer.arc(0,0,innerRadius,0,Math.PI*2,false);
layer.fill();
layer.closePath();
layer.restore();
// add inner stroke
layer.save();
layer.beginPath();
layer.strokeStyle = options.series.pie.stroke.color;
layer.arc(0,0,innerRadius,0,Math.PI*2,false);
layer.stroke();
layer.closePath();
layer.restore();
// TODO: add extra shadow inside hole (with a mask) if the pie is tilted.
}
}
//-- Additional Interactive related functions --
function isPointInPoly(poly, pt)
{
for(var c = false, i = -1, l = poly.length, j = l - 1; ++i < l; j = i)
((poly[i][1] <= pt[1] && pt[1] < poly[j][1]) || (poly[j][1] <= pt[1] && pt[1]< poly[i][1]))
&& (pt[0] < (poly[j][0] - poly[i][0]) * (pt[1] - poly[i][1]) / (poly[j][1] - poly[i][1]) + poly[i][0])
&& (c = !c);
return c;
}
function findNearbySlice(mouseX, mouseY)
{
var slices = plot.getData(),
options = plot.getOptions(),
radius = options.series.pie.radius > 1 ? options.series.pie.radius : maxRadius * options.series.pie.radius;
for (var i = 0; i < slices.length; ++i)
{
var s = slices[i];
if(s.pie.show)
{
ctx.save();
ctx.beginPath();
ctx.moveTo(0,0); // Center of the pie
//ctx.scale(1, options.series.pie.tilt); // this actually seems to break everything when here.
ctx.arc(0,0,radius,s.startAngle,s.startAngle+s.angle,false);
ctx.closePath();
x = mouseX-centerLeft;
y = mouseY-centerTop;
if(ctx.isPointInPath)
{
if (ctx.isPointInPath(mouseX-centerLeft, mouseY-centerTop))
{
//alert('found slice!');
ctx.restore();
return {datapoint: [s.percent, s.data], dataIndex: 0, series: s, seriesIndex: i};
}
}
else
{
// excanvas for IE doesn;t support isPointInPath, this is a workaround.
p1X = (radius * Math.cos(s.startAngle));
p1Y = (radius * Math.sin(s.startAngle));
p2X = (radius * Math.cos(s.startAngle+(s.angle/4)));
p2Y = (radius * Math.sin(s.startAngle+(s.angle/4)));
p3X = (radius * Math.cos(s.startAngle+(s.angle/2)));
p3Y = (radius * Math.sin(s.startAngle+(s.angle/2)));
p4X = (radius * Math.cos(s.startAngle+(s.angle/1.5)));
p4Y = (radius * Math.sin(s.startAngle+(s.angle/1.5)));
p5X = (radius * Math.cos(s.startAngle+s.angle));
p5Y = (radius * Math.sin(s.startAngle+s.angle));
arrPoly = [[0,0],[p1X,p1Y],[p2X,p2Y],[p3X,p3Y],[p4X,p4Y],[p5X,p5Y]];
arrPoint = [x,y];
// TODO: perhaps do some mathmatical trickery here with the Y-coordinate to compensate for pie tilt?
if(isPointInPoly(arrPoly, arrPoint))
{
ctx.restore();
return {datapoint: [s.percent, s.data], dataIndex: 0, series: s, seriesIndex: i};
}
}
ctx.restore();
}
}
return null;
}
function onMouseMove(e)
{
triggerClickHoverEvent('plothover', e);
}
function onClick(e)
{
triggerClickHoverEvent('plotclick', e);
}
// trigger click or hover event (they send the same parameters so we share their code)
function triggerClickHoverEvent(eventname, e)
{
var offset = plot.offset(),
canvasX = parseInt(e.pageX - offset.left),
canvasY = parseInt(e.pageY - offset.top),
item = findNearbySlice(canvasX, canvasY);
if (options.grid.autoHighlight)
{
// clear auto-highlights
for (var i = 0; i < highlights.length; ++i)
{
var h = highlights[i];
if (h.auto == eventname && !(item && h.series == item.series))
unhighlight(h.series);
}
}
// highlight the slice
if (item)
highlight(item.series, eventname);
// trigger any hover bind events
var pos = { pageX: e.pageX, pageY: e.pageY };
target.trigger(eventname, [ pos, item ]);
}
function highlight(s, auto)
{
if (typeof s == "number")
s = series[s];
var i = indexOfHighlight(s);
if (i == -1)
{
highlights.push({ series: s, auto: auto });
plot.triggerRedrawOverlay();
}
else if (!auto)
highlights[i].auto = false;
}
function unhighlight(s)
{
if (s == null)
{
highlights = [];
plot.triggerRedrawOverlay();
}
if (typeof s == "number")
s = series[s];
var i = indexOfHighlight(s);
if (i != -1)
{
highlights.splice(i, 1);
plot.triggerRedrawOverlay();
}
}
function indexOfHighlight(s)
{
for (var i = 0; i < highlights.length; ++i)
{
var h = highlights[i];
if (h.series == s)
return i;
}
return -1;
}
function drawOverlay(plot, octx)
{
//alert(options.series.pie.radius);
var options = plot.getOptions();
//alert(options.series.pie.radius);
var radius = options.series.pie.radius > 1 ? options.series.pie.radius : maxRadius * options.series.pie.radius;
octx.save();
octx.translate(centerLeft, centerTop);
octx.scale(1, options.series.pie.tilt);
for (i = 0; i < highlights.length; ++i)
drawHighlight(highlights[i].series);
drawDonutHole(octx);
octx.restore();
function drawHighlight(series)
{
if (series.angle < 0) return;
//octx.fillStyle = parseColor(options.series.pie.highlight.color).scale(null, null, null, options.series.pie.highlight.opacity).toString();
octx.fillStyle = "rgba(255, 255, 255, "+options.series.pie.highlight.opacity+")"; // this is temporary until we have access to parseColor
octx.beginPath();
if (Math.abs(series.angle - Math.PI*2) > 0.000000001)
octx.moveTo(0,0); // Center of the pie
octx.arc(0,0,radius,series.startAngle,series.startAngle+series.angle,false);
octx.closePath();
octx.fill();
}
}
} // end init (plugin body)
// define pie specific options and their default values
var options = {
series: {
pie: {
show: false,
radius: 'auto', // actual radius of the visible pie (based on full calculated radius if <=1, or hard pixel value)
innerRadius:0, /* for donut */
startAngle: 3/2,
tilt: 1,
offset: {
top: 0,
left: 'auto'
},
stroke: {
color: '#FFF',
width: 1
},
label: {
show: 'auto',
formatter: function(label, slice){
return '<div style="font-size:x-small;text-align:center;padding:2px;color:'+slice.color+';">'+label+'<br/>'+Math.round(slice.percent)+'%</div>';
}, // formatter function
radius: 1, // radius at which to place the labels (based on full calculated radius if <=1, or hard pixel value)
background: {
color: null,
opacity: 0
},
threshold: 0 // percentage at which to hide the label (i.e. the slice is too narrow)
},
combine: {
threshold: -1, // percentage at which to combine little slices into one larger slice
color: null, // color to give the new slice (auto-generated if null)
label: 'Other' // label to give the new slice
},
highlight: {
//color: '#FFF', // will add this functionality once parseColor is available
opacity: 0.5
}
}
}
};
$.plot.plugins.push({
init: init,
options: options,
name: "pie",
version: "1.0"
});
})(jQuery);
/*
* Horizontal Bar Graph for jQuery
* version 0.1a
*
* http://www.dumpsterdoggy.com/plugins/horiz-bar-graph
*
* Copyright (c) 2009 Chris Missal
* Dual licensed under the MIT (MIT-LICENSE.txt)
* and GPL (GPL-LICENSE.txt) licenses.
*/
(function($) {
$.fn.horizontalBarGraph = function(options) {
var opts = $.extend({}, $.fn.horizontalBarGraph.defaults, options);
this.children("dt,dd").each(function(i) {
var el = $(this);
if(el.is("dt")) {
el.css({display: "block", float: "left", clear: "left"}).addClass("hbg-label"); return;
} else {
(isTitleDD(el) && opts.hasTitles ? createTitle : createBar)(el, opts);
}
setBarHover(el, opts);
});
tryShowTitle(this);
if(opts.animated) {
createShowButton(opts, this).insertBefore(this);
}
if(opts.colors.length) {
setColors(this.children("dd"), opts);
}
if(opts.hoverColors.length) {
setHoverColors(this.children("dd"), opts);
}
scaleGraph(this);
return this;
};
function scaleGraph(graph) {
var maxWidth = 0;
graph.children("dt").each(function() {
maxWidth = Math.max($(this).width(), maxWidth);
}).css({width: maxWidth+"px"});
}
function setBarHover(bar, opts) {
bar.hover(function() {
bar.addClass("hbg-bar-hover");
}, function() {
bar.removeClass("hbg-bar-hover");
});
}
function createShowButton(opts, graph) {
var button = $("<span />").text(opts.button).addClass("hbg-show-button");
button.click(function() {
graph.children("dd").show('slow', function() { button.fadeOut('normal'); });
});
return button;
}
function createBar(e, opts) {
var val = e.text();
e.css({marginLeft: e.prev().is("dt") ? "5px" : "0px", width: Math.floor(val/opts.interval)+"px"});
e.html($("<span/>").html(val).addClass("hbg-value"));
applyOptions(e, opts);
}
function createTitle(e, opts) {
var title = e.text();
e.prev().attr("title", title);
e.remove();
}
function tryShowTitle(graph) {
var title = graph.attr("title");
if(title) {
$("<div/>").text(title).addClass("hbg-title").insertBefore(graph);
graph.css({overflow: "hidden"});
}
}
function setColors(bars, opts) {
var i = 0;
bars.each(function() {
var c = i++ % opts.colors.length;
$(this).css({backgroundColor: opts.colors[c]});
});
}
function setHoverColors(bars, opts) {
var i = 0;
bars.each(function(i) {
var bar = $(this);
var c = bar.css("background-color");
var hc = opts.hoverColors[i++ % opts.hoverColors.length];
bar.hover(function() {
$(this).css({backgroundColor: hc});
}, function() {
$(this).css({backgroundColor: c});
});
});
}
function applyOptions(e, opts) {
e.css({float: "left"}).addClass("hbg-bar");
if(opts.animated) { e.hide(); }
}
function isTitleDD(e) {
return (e.is(":even") && e.prev().is("dd"));
}
$.fn.horizontalBarGraph.defaults = {
interval: 1,
hasTitles: false,
animated: false,
button: 'Show Values',
colors: [],
hoverColors: []
};
})(jQuery);
This source diff could not be displayed because it is too large. You can view the blob instead.
/**
*
* jquery.sparkline.js
*
* v1.6
* (c) Splunk, Inc
* Contact: Gareth Watts (gareth@splunk.com)
* http://omnipotent.net/jquery.sparkline/
*
* Generates inline sparkline charts from data supplied either to the method
* or inline in HTML
*
* Compatible with Internet Explorer 6.0+ and modern browsers equipped with the canvas tag
* (Firefox 2.0+, Safari, Opera, etc)
*
* License: New BSD License
*
* Copyright (c) 2010, Splunk Inc.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* * Neither the name of Splunk Inc nor the names of its contributors may
* be used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
* SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
* OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
*
* Usage:
* $(selector).sparkline(values, options)
*
* If values is undefined or set to 'html' then the data values are read from the specified tag:
* <p>Sparkline: <span class="sparkline">1,4,6,6,8,5,3,5</span></p>
* $('.sparkline').sparkline();
* There must be no spaces in the enclosed data set
*
* Otherwise values must be an array of numbers or null values
* <p>Sparkline: <span id="sparkline1">This text replaced if the browser is compatible</span></p>
* $('#sparkline1').sparkline([1,4,6,6,8,5,3,5])
* $('#sparkline2').sparkline([1,4,6,null,null,5,3,5])
*
* Values can also be specified in an HTML comment, or as a values attribute:
* <p>Sparkline: <span class="sparkline"><!--1,4,6,6,8,5,3,5 --></span></p>
* <p>Sparkline: <span class="sparkline" values="1,4,6,6,8,5,3,5"></span></p>
* $('.sparkline').sparkline();
*
* For line charts, x values can also be specified:
* <p>Sparkline: <span class="sparkline">1:1,2.7:4,3.4:6,5:6,6:8,8.7:5,9:3,10:5</span></p>
* $('#sparkline1').sparkline([ [1,1], [2.7,4], [3.4,6], [5,6], [6,8], [8.7,5], [9,3], [10,5] ])
*
* By default, options should be passed in as teh second argument to the sparkline function:
* $('.sparkline').sparkline([1,2,3,4], {type: 'bar'})
*
* Options can also be set by passing them on the tag itself. This feature is disabled by default though
* as there's a slight performance overhead:
* $('.sparkline').sparkline([1,2,3,4], {enableTagOptions: true})
* <p>Sparkline: <span class="sparkline" sparkType="bar" sparkBarColor="red">loading</span></p>
* Prefix all options supplied as tag attribute with "spark" (configurable by setting tagOptionPrefix)
*
* Supported options:
* lineColor - Color of the line used for the chart
* fillColor - Color used to fill in the chart - Set to '' or false for a transparent chart
* width - Width of the chart - Defaults to 3 times the number of values in pixels
* height - Height of the chart - Defaults to the height of the containing element
* chartRangeMin - Specify the minimum value to use for the Y range of the chart - Defaults to the minimum value supplied
* chartRangeMax - Specify the maximum value to use for the Y range of the chart - Defaults to the maximum value supplied
* chartRangeClip - Clip out of range values to the max/min specified by chartRangeMin and chartRangeMax
* chartRangeMinX - Specify the minimum value to use for the X range of the chart - Defaults to the minimum value supplied
* chartRangeMaxX - Specify the maximum value to use for the X range of the chart - Defaults to the maximum value supplied
* composite - If true then don't erase any existing chart attached to the tag, but draw
* another chart over the top - Note that width and height are ignored if an
* existing chart is detected.
* tagValuesAttribute - Name of tag attribute to check for data values - Defaults to 'values'
* enableTagOptions - Whether to check tags for sparkline options
* tagOptionPrefix - Prefix used for options supplied as tag attributes - Defaults to 'spark'
*
* There are 7 types of sparkline, selected by supplying a "type" option of 'line' (default),
* 'bar', 'tristate', 'bullet', 'discrete', 'pie' or 'box'
* line - Line chart. Options:
* spotColor - Set to '' to not end each line in a circular spot
* minSpotColor - If set, color of spot at minimum value
* maxSpotColor - If set, color of spot at maximum value
* spotRadius - Radius in pixels
* lineWidth - Width of line in pixels
* normalRangeMin
* normalRangeMax - If set draws a filled horizontal bar between these two values marking the "normal"
* or expected range of values
* normalRangeColor - Color to use for the above bar
* drawNormalOnTop - Draw the normal range above the chart fill color if true
* defaultPixelsPerValue - Defaults to 3 pixels of width for each value in the chart
*
* bar - Bar chart. Options:
* barColor - Color of bars for postive values
* negBarColor - Color of bars for negative values
* zeroColor - Color of bars with zero values
* nullColor - Color of bars with null values - Defaults to omitting the bar entirely
* barWidth - Width of bars in pixels
* colorMap - Optional mappnig of values to colors to override the *BarColor values above
* can be an Array of values to control the color of individual bars
* barSpacing - Gap between bars in pixels
* zeroAxis - Centers the y-axis around zero if true
*
* tristate - Charts values of win (>0), lose (<0) or draw (=0)
* posBarColor - Color of win values
* negBarColor - Color of lose values
* zeroBarColor - Color of draw values
* barWidth - Width of bars in pixels
* barSpacing - Gap between bars in pixels
* colorMap - Optional mappnig of values to colors to override the *BarColor values above
* can be an Array of values to control the color of individual bars
*
* discrete - Options:
* lineHeight - Height of each line in pixels - Defaults to 30% of the graph height
* thesholdValue - Values less than this value will be drawn using thresholdColor instead of lineColor
* thresholdColor
*
* bullet - Values for bullet graphs msut be in the order: target, performance, range1, range2, range3, ...
* options:
* targetColor - The color of the vertical target marker
* targetWidth - The width of the target marker in pixels
* performanceColor - The color of the performance measure horizontal bar
* rangeColors - Colors to use for each qualitative range background color
*
* pie - Pie chart. Options:
* sliceColors - An array of colors to use for pie slices
* offset - Angle in degrees to offset the first slice - Try -90 or +90
*
* box - Box plot. Options:
* raw - Set to true to supply pre-computed plot points as values
* values should be: low_outlier, low_whisker, q1, median, q3, high_whisker, high_outlier
* When set to false you can supply any number of values and the box plot will
* be computed for you. Default is false.
* showOutliers - Set to true (default) to display outliers as circles
* outlierIRQ - Interquartile range used to determine outliers. Default 1.5
* boxLineColor - Outline color of the box
* boxFillColor - Fill color for the box
* whiskerColor - Line color used for whiskers
* outlierLineColor - Outline color of outlier circles
* outlierFillColor - Fill color of the outlier circles
* spotRadius - Radius of outlier circles
* medianColor - Line color of the median line
* target - Draw a target cross hair at the supplied value (default undefined)
*
*
*
* Examples:
* $('#sparkline1').sparkline(myvalues, { lineColor: '#f00', fillColor: false });
* $('.barsparks').sparkline('html', { type:'bar', height:'40px', barWidth:5 });
* $('#tristate').sparkline([1,1,-1,1,0,0,-1], { type:'tristate' }):
* $('#discrete').sparkline([1,3,4,5,5,3,4,5], { type:'discrete' });
* $('#bullet').sparkline([10,12,12,9,7], { type:'bullet' });
* $('#pie').sparkline([1,1,2], { type:'pie' });
*/
(function($) {
/*
* Default configuration settings
*/
var defaults = {
// Settings common to most/all chart types
common: {
type : 'line',
lineColor : '#00f',
fillColor : '#cdf',
defaultPixelsPerValue : 3,
width : 'auto',
height : 'auto',
composite : false,
tagValuesAttribute: 'values',
tagOptionsPrefix: 'spark',
enableTagOptions: false
},
// Defaults for line charts
line: {
spotColor : '#f80',
spotRadius : 1.5,
minSpotColor : '#f80',
maxSpotColor : '#f80',
lineWidth: 1,
normalRangeMin : undefined,
normalRangeMax : undefined,
normalRangeColor : '#ccc',
drawNormalOnTop: false,
chartRangeMin : undefined,
chartRangeMax : undefined,
chartRangeMinX : undefined,
chartRangeMaxX : undefined
},
// Defaults for bar charts
bar: {
barColor : '#00f',
negBarColor : '#f44',
zeroColor: undefined,
nullColor: undefined,
zeroAxis : undefined,
barWidth : 4,
barSpacing : 1,
chartRangeMax: undefined,
chartRangeMin: undefined,
chartRangeClip: false,
colorMap : undefined
},
// Defaults for tristate charts
tristate: {
barWidth : 4,
barSpacing : 1,
posBarColor: '#6f6',
negBarColor : '#f44',
zeroBarColor : '#999',
colorMap : {}
},
// Defaults for discrete charts
discrete: {
lineHeight: 'auto',
thresholdColor: undefined,
thresholdValue : 0,
chartRangeMax: undefined,
chartRangeMin: undefined,
chartRangeClip: false
},
// Defaults for bullet charts
bullet: {
targetColor : 'red',
targetWidth : 3, // width of the target bar in pixels
performanceColor : 'blue',
rangeColors : ['#D3DAFE', '#A8B6FF', '#7F94FF' ],
base : undefined // set this to a number to change the base start number
},
// Defaults for pie charts
pie: {
sliceColors : ['#f00', '#0f0', '#00f']
},
// Defaults for box plots
box: {
raw: false,
boxLineColor: 'black',
boxFillColor: '#cdf',
whiskerColor: 'black',
outlierLineColor: '#333',
outlierFillColor: 'white',
medianColor: 'red',
showOutliers: true,
outlierIQR: 1.5,
spotRadius: 1.5,
target: undefined,
targetColor: '#4a2',
chartRangeMax: undefined,
chartRangeMin: undefined
}
};
// Provide a cross-browser interface to a few simple drawing primitives
var VCanvas_base, VCanvas_canvas, VCanvas_vml;
$.fn.simpledraw = function(width, height, use_existing) {
if (use_existing && this[0].VCanvas) {
return this[0].VCanvas;
}
if (width === undefined) {
width=$(this).innerWidth();
}
if (height === undefined) {
height=$(this).innerHeight();
}
if ($.browser.hasCanvas) {
return new VCanvas_canvas(width, height, this);
} else if ($.browser.msie) {
return new VCanvas_vml(width, height, this);
} else {
return false;
}
};
var pending = [];
$.fn.sparkline = function(uservalues, userOptions) {
return this.each(function() {
var options = new $.fn.sparkline.options(this, userOptions);
var render = function() {
var values, width, height;
if (uservalues==='html' || uservalues===undefined) {
var vals = this.getAttribute(options.get('tagValuesAttribute'));
if (vals===undefined || vals===null) {
vals = $(this).html();
}
values = vals.replace(/(^\s*<!--)|(-->\s*$)|\s+/g, '').split(',');
} else {
values = uservalues;
}
width = options.get('width')=='auto' ? values.length*options.get('defaultPixelsPerValue') : options.get('width');
if (options.get('height') == 'auto') {
if (!options.get('composite') || !this.VCanvas) {
// must be a better way to get the line height
var tmp = document.createElement('span');
tmp.innerHTML = 'a';
$(this).html(tmp);
height = $(tmp).innerHeight();
$(tmp).remove();
}
} else {
height = options.get('height');
}
$.fn.sparkline[options.get('type')].call(this, values, options, width, height);
};
// jQuery 1.3.0 completely changed the meaning of :hidden :-/
if (($(this).html() && $(this).is(':hidden')) || ($.fn.jquery < "1.3.0" && $(this).parents().is(':hidden')) || !$(this).parents('body').length) {
pending.push([this, render]);
} else {
render.call(this);
}
});
};
$.fn.sparkline.defaults = defaults;
$.sparkline_display_visible = function() {
for (var i=pending.length-1; i>=0; i--) {
var el = pending[i][0];
if ($(el).is(':visible') && !$(el).parents().is(':hidden')) {
pending[i][1].call(el);
pending.splice(i, 1);
}
}
};
/**
* User option handler
*/
var UNSET_OPTION = {};
var normalizeValue = function(val) {
switch(val) {
case 'undefined':
val = undefined;
break;
case 'null':
val = null;
break;
case 'true':
val = true;
break;
case 'false':
val = false;
break;
default:
var nf = parseFloat(val);
if (val == nf) {
val = nf;
}
}
return val;
};
$.fn.sparkline.options = function(tag, userOptions) {
var extendedOptions;
this.userOptions = userOptions = userOptions || {};
this.tag = tag;
this.tagValCache = {};
var defaults = $.fn.sparkline.defaults;
var base = defaults.common;
this.tagOptionsPrefix = userOptions.enableTagOptions && (userOptions.tagOptionsPrefix || base.tagOptionsPrefix);
var tagOptionType = this.getTagSetting('type');
if (tagOptionType === UNSET_OPTION) {
extendedOptions = defaults[userOptions.type || base.type];
} else {
extendedOptions = defaults[tagOptionType];
}
this.mergedOptions = $.extend({}, base, extendedOptions, userOptions);
};
$.fn.sparkline.options.prototype.getTagSetting = function(key) {
var val, i, prefix = this.tagOptionsPrefix;
if (prefix === false || prefix === undefined) {
return UNSET_OPTION;
}
if (this.tagValCache.hasOwnProperty(key)) {
val = this.tagValCache.key;
} else {
val = this.tag.getAttribute(prefix + key);
if (val === undefined || val === null) {
val = UNSET_OPTION;
} else if (val.substr(0, 1) == '[') {
val = val.substr(1, val.length-2).split(',');
for(i=val.length; i--;) {
val[i] = normalizeValue(val[i].replace(/(^\s*)|(\s*$)/g, ''));
}
} else if (val.substr(0, 1) == '{') {
var pairs= val.substr(1, val.length-2).split(',');
val = {};
for(i=pairs.length; i--;) {
var keyval = pairs[i].split(':', 2);
val[keyval[0].replace(/(^\s*)|(\s*$)/g, '')] = normalizeValue(keyval[1].replace(/(^\s*)|(\s*$)/g, ''));
}
} else {
val = normalizeValue(val);
}
this.tagValCache.key = val;
}
return val;
};
$.fn.sparkline.options.prototype.get = function(key) {
var tagOption = this.getTagSetting(key);
if (tagOption !== UNSET_OPTION) {
return tagOption;
}
return this.mergedOptions[key];
};
/**
* Line charts
*/
$.fn.sparkline.line = function(values, options, width, height) {
var xvalues = [], yvalues = [], yminmax = [];
for (var i=0; i<values.length; i++) {
var val = values[i];
var isstr = typeof(values[i])=='string';
var isarray = typeof(values[i])=='object' && values[i] instanceof Array;
var sp = isstr && values[i].split(':');
if (isstr && sp.length == 2) { // x:y
xvalues.push(Number(sp[0]));
yvalues.push(Number(sp[1]));
yminmax.push(Number(sp[1]));
} else if (isarray) {
xvalues.push(val[0]);
yvalues.push(val[1]);
yminmax.push(val[1]);
} else {
xvalues.push(i);
if (values[i]===null || values[i]=='null') {
yvalues.push(null);
} else {
yvalues.push(Number(val));
yminmax.push(Number(val));
}
}
}
if (options.get('xvalues')) {
xvalues = options.get('xvalues');
}
var maxy = Math.max.apply(Math, yminmax);
var maxyval = maxy;
var miny = Math.min.apply(Math, yminmax);
var minyval = miny;
var maxx = Math.max.apply(Math, xvalues);
var minx = Math.min.apply(Math, xvalues);
var normalRangeMin = options.get('normalRangeMin');
var normalRangeMax = options.get('normalRangeMax');
if (normalRangeMin!==undefined) {
if (normalRangeMin<miny) {
miny = normalRangeMin;
}
if (normalRangeMax>maxy) {
maxy = normalRangeMax;
}
}
if (options.get('chartRangeMin')!==undefined && (options.get('chartRangeClip') || options.get('chartRangeMin')<miny)) {
miny = options.get('chartRangeMin');
}
if (options.get('chartRangeMax')!==undefined && (options.get('chartRangeClip') || options.get('chartRangeMax')>maxy)) {
maxy = options.get('chartRangeMax');
}
if (options.get('chartRangeMinX')!==undefined && (options.get('chartRangeClipX') || options.get('chartRangeMinX')<minx)) {
minx = options.get('chartRangeMinX');
}
if (options.get('chartRangeMaxX')!==undefined && (options.get('chartRangeClipX') || options.get('chartRangeMaxX')>maxx)) {
maxx = options.get('chartRangeMaxX');
}
var rangex = maxx-minx === 0 ? 1 : maxx-minx;
var rangey = maxy-miny === 0 ? 1 : maxy-miny;
var vl = yvalues.length-1;
if (vl<1) {
this.innerHTML = '';
return;
}
var target = $(this).simpledraw(width, height, options.get('composite'));
if (target) {
var canvas_width = target.pixel_width;
var canvas_height = target.pixel_height;
var canvas_top = 0;
var canvas_left = 0;
var spotRadius = options.get('spotRadius');
if (spotRadius && (canvas_width < (spotRadius*4) || canvas_height < (spotRadius*4))) {
spotRadius = 0;
}
if (spotRadius) {
// adjust the canvas size as required so that spots will fit
if (options.get('minSpotColor') || (options.get('spotColor') && yvalues[vl]==miny)) {
canvas_height -= Math.ceil(spotRadius);
}
if (options.get('maxSpotColor') || (options.get('spotColor') && yvalues[vl]==maxy)) {
canvas_height -= Math.ceil(spotRadius);
canvas_top += Math.ceil(spotRadius);
}
if (options.get('minSpotColor') || options.get('maxSpotColor') && (yvalues[0]==miny || yvalues[0]==maxy)) {
canvas_left += Math.ceil(spotRadius);
canvas_width -= Math.ceil(spotRadius);
}
if (options.get('spotColor') || (options.get('minSpotColor') || options.get('maxSpotColor') && (yvalues[vl]==miny||yvalues[vl]==maxy))) {
canvas_width -= Math.ceil(spotRadius);
}
}
canvas_height--;
var drawNormalRange = function() {
if (normalRangeMin!==undefined) {
var ytop = canvas_top+Math.round(canvas_height-(canvas_height*((normalRangeMax-miny)/rangey)));
var height = Math.round((canvas_height*(normalRangeMax-normalRangeMin))/rangey);
target.drawRect(canvas_left, ytop, canvas_width, height, undefined, options.get('normalRangeColor'));
}
};
if (!options.get('drawNormalOnTop')) {
drawNormalRange();
}
var path = [];
var paths = [path];
var x, y, vlen=yvalues.length;
for(i=0; i<vlen; i++) {
x=xvalues[i];
y=yvalues[i];
if (y===null) {
if (i) {
if (yvalues[i-1]!==null) {
path = [];
paths.push(path);
}
}
} else {
if (y < miny) {
y=miny;
}
if (y > maxy) {
y=maxy;
}
if (!path.length) {
// previous value was null
path.push([canvas_left+Math.round((x-minx)*(canvas_width/rangex)), canvas_top+canvas_height]);
}
path.push([canvas_left+Math.round((x-minx)*(canvas_width/rangex)), canvas_top+Math.round(canvas_height-(canvas_height*((y-miny)/rangey)))]);
}
}
var lineshapes = [];
var fillshapes = [];
var plen=paths.length;
for(i=0; i<plen; i++) {
path = paths[i];
if (!path.length) {
continue; // last value was null
}
if (options.get('fillColor')) {
path.push([path[path.length-1][0], canvas_top+canvas_height-1]);
fillshapes.push(path.slice(0));
path.pop();
}
// if there's only a single point in this path, then we want to display it as a vertical line
// which means we keep path[0] as is
if (path.length>2) {
// else we want the first value
path[0] = [ path[0][0], path[1][1] ];
}
lineshapes.push(path);
}
// draw the fill first, then optionally the normal range, then the line on top of that
plen = fillshapes.length;
for(i=0; i<plen; i++) {
target.drawShape(fillshapes[i], undefined, options.get('fillColor'));
}
if (options.get('drawNormalOnTop')) {
drawNormalRange();
}
plen = lineshapes.length;
for(i=0; i<plen; i++) {
target.drawShape(lineshapes[i], options.get('lineColor'), undefined, options.get('lineWidth'));
}
if (spotRadius && options.get('spotColor')) {
target.drawCircle(canvas_left+Math.round(xvalues[xvalues.length-1]*(canvas_width/rangex)), canvas_top+Math.round(canvas_height-(canvas_height*((yvalues[vl]-miny)/rangey))), spotRadius, undefined, options.get('spotColor'));
}
if (maxy!=minyval) {
if (spotRadius && options.get('minSpotColor')) {
x = xvalues[$.inArray(minyval, yvalues)];
target.drawCircle(canvas_left+Math.round((x-minx)*(canvas_width/rangex)), canvas_top+Math.round(canvas_height-(canvas_height*((minyval-miny)/rangey))), spotRadius, undefined, options.get('minSpotColor'));
}
if (spotRadius && options.get('maxSpotColor')) {
x = xvalues[$.inArray(maxyval, yvalues)];
target.drawCircle(canvas_left+Math.round((x-minx)*(canvas_width/rangex)), canvas_top+Math.round(canvas_height-(canvas_height*((maxyval-miny)/rangey))), spotRadius, undefined, options.get('maxSpotColor'));
}
}
} else {
// Remove the tag contents if sparklines aren't supported
this.innerHTML = '';
}
};
/**
* Bar charts
*/
$.fn.sparkline.bar = function(values, options, width, height) {
width = (values.length * options.get('barWidth')) + ((values.length-1) * options.get('barSpacing'));
var num_values = [];
for(var i=0, vlen=values.length; i<vlen; i++) {
if (values[i]=='null' || values[i]===null) {
values[i] = null;
} else {
values[i] = Number(values[i]);
num_values.push(Number(values[i]));
}
}
var max = Math.max.apply(Math, num_values),
min = Math.min.apply(Math, num_values);
if (options.get('chartRangeMin')!==undefined && (options.get('chartRangeClip') || options.get('chartRangeMin')<min)) {
min = options.get('chartRangeMin');
}
if (options.get('chartRangeMax')!==undefined && (options.get('chartRangeClip') || options.get('chartRangeMax')>max)) {
max = options.get('chartRangeMax');
}
var zeroAxis = options.get('zeroAxis');
if (zeroAxis === undefined) {
zeroAxis = min<0;
}
var range = max-min === 0 ? 1 : max-min;
var colorMapByIndex, colorMapByValue;
if ($.isArray(options.get('colorMap'))) {
colorMapByIndex = options.get('colorMap');
colorMapByValue = null;
} else {
colorMapByIndex = null;
colorMapByValue = options.get('colorMap');
}
var target = $(this).simpledraw(width, height, options.get('composite'));
if (target) {
var color,
canvas_height = target.pixel_height,
yzero = min<0 && zeroAxis ? canvas_height-Math.round(canvas_height * (Math.abs(min)/range))-1 : canvas_height-1;
for(i=values.length; i--;) {
var x = i*(options.get('barWidth')+options.get('barSpacing')),
y,
val = values[i];
if (val===null) {
if (options.get('nullColor')) {
color = options.get('nullColor');
val = (zeroAxis && min<0) ? 0 : min;
height = 1;
y = (zeroAxis && min<0) ? yzero : canvas_height - height;
} else {
continue;
}
} else {
if (val < min) {
val=min;
}
if (val > max) {
val=max;
}
color = (val < 0) ? options.get('negBarColor') : options.get('barColor');
if (zeroAxis && min<0) {
height = Math.round(canvas_height*((Math.abs(val)/range)))+1;
y = (val < 0) ? yzero : yzero-height;
} else {
height = Math.round(canvas_height*((val-min)/range))+1;
y = canvas_height-height;
}
if (val===0 && options.get('zeroColor')!==undefined) {
color = options.get('zeroColor');
}
if (colorMapByValue && colorMapByValue[val]) {
color = colorMapByValue[val];
} else if (colorMapByIndex && colorMapByIndex.length>i) {
color = colorMapByIndex[i];
}
if (color===null) {
continue;
}
}
target.drawRect(x, y, options.get('barWidth')-1, height-1, color, color);
}
} else {
// Remove the tag contents if sparklines aren't supported
this.innerHTML = '';
}
};
/**
* Tristate charts
*/
$.fn.sparkline.tristate = function(values, options, width, height) {
values = $.map(values, Number);
width = (values.length * options.get('barWidth')) + ((values.length-1) * options.get('barSpacing'));
var colorMapByIndex, colorMapByValue;
if ($.isArray(options.get('colorMap'))) {
colorMapByIndex = options.get('colorMap');
colorMapByValue = null;
} else {
colorMapByIndex = null;
colorMapByValue = options.get('colorMap');
}
var target = $(this).simpledraw(width, height, options.get('composite'));
if (target) {
var canvas_height = target.pixel_height,
half_height = Math.round(canvas_height/2);
for(var i=values.length; i--;) {
var x = i*(options.get('barWidth')+options.get('barSpacing')),
y, color;
if (values[i] < 0) {
y = half_height;
height = half_height-1;
color = options.get('negBarColor');
} else if (values[i] > 0) {
y = 0;
height = half_height-1;
color = options.get('posBarColor');
} else {
y = half_height-1;
height = 2;
color = options.get('zeroBarColor');
}
if (colorMapByValue && colorMapByValue[values[i]]) {
color = colorMapByValue[values[i]];
} else if (colorMapByIndex && colorMapByIndex.length>i) {
color = colorMapByIndex[i];
}
if (color===null) {
continue;
}
target.drawRect(x, y, options.get('barWidth')-1, height-1, color, color);
}
} else {
// Remove the tag contents if sparklines aren't supported
this.innerHTML = '';
}
};
/**
* Discrete charts
*/
$.fn.sparkline.discrete = function(values, options, width, height) {
values = $.map(values, Number);
width = options.get('width')=='auto' ? values.length*2 : width;
var interval = Math.floor(width / values.length);
var target = $(this).simpledraw(width, height, options.get('composite'));
if (target) {
var canvas_height = target.pixel_height,
line_height = options.get('lineHeight') == 'auto' ? Math.round(canvas_height * 0.3) : options.get('lineHeight'),
pheight = canvas_height - line_height,
min = Math.min.apply(Math, values),
max = Math.max.apply(Math, values);
if (options.get('chartRangeMin')!==undefined && (options.get('chartRangeClip') || options.get('chartRangeMin')<min)) {
min = options.get('chartRangeMin');
}
if (options.get('chartRangeMax')!==undefined && (options.get('chartRangeClip') || options.get('chartRangeMax')>max)) {
max = options.get('chartRangeMax');
}
var range = max-min;
for(var i=values.length; i--;) {
var val = values[i];
if (val < min) {
val=min;
}
if (val > max) {
val=max;
}
var x = (i*interval),
ytop = Math.round(pheight-pheight*((val-min)/range));
target.drawLine(x, ytop, x, ytop+line_height, (options.get('thresholdColor') && val < options.get('thresholdValue')) ? options.get('thresholdColor') : options.get('lineColor'));
}
} else {
// Remove the tag contents if sparklines aren't supported
this.innerHTML = '';
}
};
/**
* Bullet charts
*/
$.fn.sparkline.bullet = function(values, options, width, height) {
values = $.map(values, Number);
// target, performance, range1, range2, range3
width = options.get('width')=='auto' ? '4.0em' : width;
var target = $(this).simpledraw(width, height, options.get('composite'));
if (target && values.length>1) {
var canvas_width = target.pixel_width-Math.ceil(options.get('targetWidth')/2),
canvas_height = target.pixel_height,
min = Math.min.apply(Math, values),
max = Math.max.apply(Math, values);
if (options.get('base') === undefined) {
min = min < 0 ? min : 0;
} else {
min = options.get('base');
}
var range = max-min;
// draw range values
for(var i=2, vlen=values.length; i<vlen; i++) {
var rangeval = values[i],
rangewidth = Math.round(canvas_width*((rangeval-min)/range));
target.drawRect(0, 0, rangewidth-1, canvas_height-1, options.get('rangeColors')[i-2], options.get('rangeColors')[i-2]);
}
// draw the performance bar
var perfval = values[1],
perfwidth = Math.round(canvas_width*((perfval-min)/range));
target.drawRect(0, Math.round(canvas_height*0.3), perfwidth-1, Math.round(canvas_height*0.4)-1, options.get('performanceColor'), options.get('performanceColor'));
// draw the target line
var targetval = values[0],
x = Math.round(canvas_width*((targetval-min)/range)-(options.get('targetWidth')/2)),
targettop = Math.round(canvas_height*0.10),
targetheight = canvas_height-(targettop*2);
target.drawRect(x, targettop, options.get('targetWidth')-1, targetheight-1, options.get('targetColor'), options.get('targetColor'));
} else {
// Remove the tag contents if sparklines aren't supported
this.innerHTML = '';
}
};
/**
* Pie charts
*/
$.fn.sparkline.pie = function(values, options, width, height) {
values = $.map(values, Number);
width = options.get('width')=='auto' ? height : width;
var target = $(this).simpledraw(width, height, options.get('composite'));
if (target && values.length>1) {
var canvas_width = target.pixel_width,
canvas_height = target.pixel_height,
radius = Math.floor(Math.min(canvas_width, canvas_height)/2),
total = 0,
next = 0,
circle = 2*Math.PI;
for(var i=values.length; i--;) {
total += values[i];
}
if (options.get('offset')) {
next += (2*Math.PI)*(options.get('offset')/360);
}
var vlen = values.length;
for(i=0; i<vlen; i++) {
var start = next;
var end = next;
if (total > 0) { // avoid divide by zero
end = next + (circle*(values[i]/total));
}
target.drawPieSlice(radius, radius, radius, start, end, undefined, options.get('sliceColors')[i % options.get('sliceColors').length]);
next = end;
}
}
};
/**
* Box plots
*/
var quartile = function(values, q) {
if (q==2) {
var vl2 = Math.floor(values.length/2);
return values.length % 2 ? values[vl2] : (values[vl2]+values[vl2+1])/2;
} else {
var vl4 = Math.floor(values.length/4);
return values.length % 2 ? (values[vl4*q]+values[vl4*q+1])/2 : values[vl4*q];
}
};
$.fn.sparkline.box = function(values, options, width, height) {
values = $.map(values, Number);
width = options.get('width')=='auto' ? '4.0em' : width;
var minvalue = options.get('chartRangeMin')===undefined ? Math.min.apply(Math, values) : options.get('chartRangeMin'),
maxvalue = options.get('chartRangeMax')===undefined ? Math.max.apply(Math, values) : options.get('chartRangeMax'),
target = $(this).simpledraw(width, height, options.get('composite')),
vlen = values.length,
lwhisker, loutlier, q1, q2, q3, rwhisker, routlier;
if (target && values.length>1) {
var canvas_width = target.pixel_width,
canvas_height = target.pixel_height;
if (options.get('raw')) {
if (options.get('showOutliers') && values.length>5) {
loutlier=values[0]; lwhisker=values[1]; q1=values[2]; q2=values[3]; q3=values[4]; rwhisker=values[5]; routlier=values[6];
} else {
lwhisker=values[0]; q1=values[1]; q2=values[2]; q3=values[3]; rwhisker=values[4];
}
} else {
values.sort(function(a, b) { return a-b; });
q1 = quartile(values, 1);
q2 = quartile(values, 2);
q3 = quartile(values, 3);
var iqr = q3-q1;
if (options.get('showOutliers')) {
lwhisker=undefined; rwhisker=undefined;
for(var i=0; i<vlen; i++) {
if (lwhisker===undefined && values[i] > q1-(iqr*options.get('outlierIQR'))) {
lwhisker = values[i];
}
if (values[i] < q3+(iqr*options.get('outlierIQR'))) {
rwhisker = values[i];
}
}
loutlier = values[0];
routlier = values[vlen-1];
} else {
lwhisker = values[0];
rwhisker = values[vlen-1];
}
}
var unitsize = canvas_width / (maxvalue-minvalue+1),
canvas_left = 0;
if (options.get('showOutliers')) {
canvas_left = Math.ceil(options.get('spotRadius'));
canvas_width -= 2*Math.ceil(options.get('spotRadius'));
unitsize = canvas_width / (maxvalue-minvalue+1);
if (loutlier < lwhisker) {
target.drawCircle((loutlier-minvalue)*unitsize+canvas_left, canvas_height/2, options.get('spotRadius'), options.get('outlierLineColor'), options.get('outlierFillColor'));
}
if (routlier > rwhisker) {
target.drawCircle((routlier-minvalue)*unitsize+canvas_left, canvas_height/2, options.get('spotRadius'), options.get('outlierLineColor'), options.get('outlierFillColor'));
}
}
// box
target.drawRect(
Math.round((q1-minvalue)*unitsize+canvas_left),
Math.round(canvas_height*0.1),
Math.round((q3-q1)*unitsize),
Math.round(canvas_height*0.8),
options.get('boxLineColor'),
options.get('boxFillColor'));
// left whisker
target.drawLine(
Math.round((lwhisker-minvalue)*unitsize+canvas_left),
Math.round(canvas_height/2),
Math.round((q1-minvalue)*unitsize+canvas_left),
Math.round(canvas_height/2),
options.get('lineColor'));
target.drawLine(
Math.round((lwhisker-minvalue)*unitsize+canvas_left),
Math.round(canvas_height/4),
Math.round((lwhisker-minvalue)*unitsize+canvas_left),
Math.round(canvas_height-canvas_height/4),
options.get('whiskerColor'));
// right whisker
target.drawLine(Math.round((rwhisker-minvalue)*unitsize+canvas_left),
Math.round(canvas_height/2),
Math.round((q3-minvalue)*unitsize+canvas_left),
Math.round(canvas_height/2),
options.get('lineColor'));
target.drawLine(
Math.round((rwhisker-minvalue)*unitsize+canvas_left),
Math.round(canvas_height/4),
Math.round((rwhisker-minvalue)*unitsize+canvas_left),
Math.round(canvas_height-canvas_height/4),
options.get('whiskerColor'));
// median line
target.drawLine(
Math.round((q2-minvalue)*unitsize+canvas_left),
Math.round(canvas_height*0.1),
Math.round((q2-minvalue)*unitsize+canvas_left),
Math.round(canvas_height*0.9),
options.get('medianColor'));
if (options.get('target')) {
var size = Math.ceil(options.get('spotRadius'));
target.drawLine(
Math.round((options.get('target')-minvalue)*unitsize+canvas_left),
Math.round((canvas_height/2)-size),
Math.round((options.get('target')-minvalue)*unitsize+canvas_left),
Math.round((canvas_height/2)+size),
options.get('targetColor'));
target.drawLine(
Math.round((options.get('target')-minvalue)*unitsize+canvas_left-size),
Math.round(canvas_height/2),
Math.round((options.get('target')-minvalue)*unitsize+canvas_left+size),
Math.round(canvas_height/2),
options.get('targetColor'));
}
} else {
// Remove the tag contents if sparklines aren't supported
this.innerHTML = '';
}
};
// Setup a very simple "virtual canvas" to make drawing the few shapes we need easier
// This is accessible as $(foo).simpledraw()
if ($.browser.msie && !document.namespaces.v) {
document.namespaces.add('v', 'urn:schemas-microsoft-com:vml', '#default#VML');
}
if ($.browser.hasCanvas === undefined) {
var t = document.createElement('canvas');
$.browser.hasCanvas = t.getContext!==undefined;
}
VCanvas_base = function(width, height, target) {
};
VCanvas_base.prototype = {
init : function(width, height, target) {
this.width = width;
this.height = height;
this.target = target;
if (target[0]) {
target=target[0];
}
target.VCanvas = this;
},
drawShape : function(path, lineColor, fillColor, lineWidth) {
alert('drawShape not implemented');
},
drawLine : function(x1, y1, x2, y2, lineColor, lineWidth) {
return this.drawShape([ [x1,y1], [x2,y2] ], lineColor, lineWidth);
},
drawCircle : function(x, y, radius, lineColor, fillColor) {
alert('drawCircle not implemented');
},
drawPieSlice : function(x, y, radius, startAngle, endAngle, lineColor, fillColor) {
alert('drawPieSlice not implemented');
},
drawRect : function(x, y, width, height, lineColor, fillColor) {
alert('drawRect not implemented');
},
getElement : function() {
return this.canvas;
},
_insert : function(el, target) {
$(target).html(el);
}
};
VCanvas_canvas = function(width, height, target) {
return this.init(width, height, target);
};
VCanvas_canvas.prototype = $.extend(new VCanvas_base(), {
_super : VCanvas_base.prototype,
init : function(width, height, target) {
this._super.init(width, height, target);
this.canvas = document.createElement('canvas');
if (target[0]) {
target=target[0];
}
target.VCanvas = this;
$(this.canvas).css({ display:'inline-block', width:width, height:height, verticalAlign:'top' });
this._insert(this.canvas, target);
this.pixel_height = $(this.canvas).height();
this.pixel_width = $(this.canvas).width();
this.canvas.width = this.pixel_width;
this.canvas.height = this.pixel_height;
$(this.canvas).css({width: this.pixel_width, height: this.pixel_height});
},
_getContext : function(lineColor, fillColor, lineWidth) {
var context = this.canvas.getContext('2d');
if (lineColor !== undefined) {
context.strokeStyle = lineColor;
}
context.lineWidth = lineWidth===undefined ? 1 : lineWidth;
if (fillColor !== undefined) {
context.fillStyle = fillColor;
}
return context;
},
drawShape : function(path, lineColor, fillColor, lineWidth) {
var context = this._getContext(lineColor, fillColor, lineWidth);
context.beginPath();
context.moveTo(path[0][0]+0.5, path[0][1]+0.5);
for(var i=1, plen=path.length; i<plen; i++) {
context.lineTo(path[i][0]+0.5, path[i][1]+0.5); // the 0.5 offset gives us crisp pixel-width lines
}
if (lineColor !== undefined) {
context.stroke();
}
if (fillColor !== undefined) {
context.fill();
}
},
drawCircle : function(x, y, radius, lineColor, fillColor) {
var context = this._getContext(lineColor, fillColor);
context.beginPath();
context.arc(x, y, radius, 0, 2*Math.PI, false);
if (lineColor !== undefined) {
context.stroke();
}
if (fillColor !== undefined) {
context.fill();
}
},
drawPieSlice : function(x, y, radius, startAngle, endAngle, lineColor, fillColor) {
var context = this._getContext(lineColor, fillColor);
context.beginPath();
context.moveTo(x, y);
context.arc(x, y, radius, startAngle, endAngle, false);
context.lineTo(x, y);
context.closePath();
if (lineColor !== undefined) {
context.stroke();
}
if (fillColor) {
context.fill();
}
},
drawRect : function(x, y, width, height, lineColor, fillColor) {
return this.drawShape([ [x,y], [x+width, y], [x+width, y+height], [x, y+height], [x, y] ], lineColor, fillColor);
}
});
VCanvas_vml = function(width, height, target) {
return this.init(width, height, target);
};
VCanvas_vml.prototype = $.extend(new VCanvas_base(), {
_super : VCanvas_base.prototype,
init : function(width, height, target) {
this._super.init(width, height, target);
if (target[0]) {
target=target[0];
}
target.VCanvas = this;
this.canvas = document.createElement('span');
$(this.canvas).css({ display:'inline-block', position: 'relative', overflow:'hidden', width:width, height:height, margin:'0px', padding:'0px', verticalAlign: 'top'});
this._insert(this.canvas, target);
this.pixel_height = $(this.canvas).height();
this.pixel_width = $(this.canvas).width();
this.canvas.width = this.pixel_width;
this.canvas.height = this.pixel_height;
var groupel = '<v:group coordorigin="0 0" coordsize="'+this.pixel_width+' '+this.pixel_height+'"' +
' style="position:absolute;top:0;left:0;width:'+this.pixel_width+'px;height='+this.pixel_height+'px;"></v:group>';
this.canvas.insertAdjacentHTML('beforeEnd', groupel);
this.group = $(this.canvas).children()[0];
},
drawShape : function(path, lineColor, fillColor, lineWidth) {
var vpath = [];
for(var i=0, plen=path.length; i<plen; i++) {
vpath[i] = ''+(path[i][0])+','+(path[i][1]);
}
var initial = vpath.splice(0,1);
lineWidth = lineWidth === undefined ? 1 : lineWidth;
var stroke = lineColor === undefined ? ' stroked="false" ' : ' strokeWeight="'+lineWidth+'" strokeColor="'+lineColor+'" ';
var fill = fillColor === undefined ? ' filled="false"' : ' fillColor="'+fillColor+'" filled="true" ';
var closed = vpath[0] == vpath[vpath.length-1] ? 'x ' : '';
var vel = '<v:shape coordorigin="0 0" coordsize="'+this.pixel_width+' '+this.pixel_height+'" ' +
stroke +
fill +
' style="position:absolute;left:0px;top:0px;height:'+this.pixel_height+'px;width:'+this.pixel_width+'px;padding:0px;margin:0px;" ' +
' path="m '+initial+' l '+vpath.join(', ')+' '+closed+'e">' +
' </v:shape>';
this.group.insertAdjacentHTML('beforeEnd', vel);
},
drawCircle : function(x, y, radius, lineColor, fillColor) {
x -= radius+1;
y -= radius+1;
var stroke = lineColor === undefined ? ' stroked="false" ' : ' strokeWeight="1" strokeColor="'+lineColor+'" ';
var fill = fillColor === undefined ? ' filled="false"' : ' fillColor="'+fillColor+'" filled="true" ';
var vel = '<v:oval ' +
stroke +
fill +
' style="position:absolute;top:'+y+'px; left:'+x+'px; width:'+(radius*2)+'px; height:'+(radius*2)+'px"></v:oval>';
this.group.insertAdjacentHTML('beforeEnd', vel);
},
drawPieSlice : function(x, y, radius, startAngle, endAngle, lineColor, fillColor) {
if (startAngle == endAngle) {
return; // VML seems to have problem when start angle equals end angle.
}
if ((endAngle - startAngle) == (2*Math.PI)) {
startAngle = 0.0; // VML seems to have a problem when drawing a full circle that doesn't start 0
endAngle = (2*Math.PI);
}
var startx = x + Math.round(Math.cos(startAngle) * radius);
var starty = y + Math.round(Math.sin(startAngle) * radius);
var endx = x + Math.round(Math.cos(endAngle) * radius);
var endy = y + Math.round(Math.sin(endAngle) * radius);
// Prevent very small slices from being mistaken as a whole pie
if (startx==endx && starty==endy && (endAngle-startAngle) < Math.PI) {
return;
}
var vpath = [ x-radius, y-radius, x+radius, y+radius, startx, starty, endx, endy ];
var stroke = lineColor === undefined ? ' stroked="false" ' : ' strokeWeight="1" strokeColor="'+lineColor+'" ';
var fill = fillColor === undefined ? ' filled="false"' : ' fillColor="'+fillColor+'" filled="true" ';
var vel = '<v:shape coordorigin="0 0" coordsize="'+this.pixel_width+' '+this.pixel_height+'" ' +
stroke +
fill +
' style="position:absolute;left:0px;top:0px;height:'+this.pixel_height+'px;width:'+this.pixel_width+'px;padding:0px;margin:0px;" ' +
' path="m '+x+','+y+' wa '+vpath.join(', ')+' x e">' +
' </v:shape>';
this.group.insertAdjacentHTML('beforeEnd', vel);
},
drawRect : function(x, y, width, height, lineColor, fillColor) {
return this.drawShape( [ [x, y], [x, y+height], [x+width, y+height], [x+width, y], [x, y] ], lineColor, fillColor);
}
});
})(jQuery);
var dataIq = [];
var dataMsg = [];
var dataPres = [];
var dataRost = [];
var id = 0;
var lastTimestamp = 0;
var rowCounter = 0;
var limit = 50;
$(document).ready(function() {
drawGraph();
$('#tableLimit').val(limit);
window.setInterval("pollStats()", 1000);
$('#formLimit').submit(function(e) {
e.preventDefault();
console.log(e);
limit = $('#tableLimit').val();
});
})
function drawGraph() {
var options = {
lines : {
show : true
},
points : {
show : false
},
yaxis : {
min : 0
},
xaxis : {
show : false
},
grid : {
backgroundColor : {
colors : [ "#fff", "lightgray" ]
}
}
};
var placeholder = $(".graph");
if (dataIq.length > 50) {
dataIq = dataIq.slice(dataIq.length - 50, dataIq.length - 1);
dataMsg = dataMsg.slice(dataMsg.length - 50, dataMsg.length - 1);
dataPres = dataPres.slice(dataPres.length - 50, dataPres.length - 1);
dataRost = dataRost.slice(dataRost.length - 50, dataRost.length - 1);
}
$.plot($(".graph"), [ {
label : "IQ",
data : dataIq
}, {
label : "Messages",
data : dataMsg
}, {
label : "Roster",
data : dataRost
}, {
label : "Presence",
data : dataPres
} ], options);
}
function pollStats() {
var firstDate = $('#logSince').html();
if (lastTimestamp > 0) {
firstDate = lastTimestamp;
}
updateData(firstDate);
}
// Read a page's GET URL variables and return them as an associative array.
function getUrlVars() {
var vars = [], hash;
var hashes = window.location.href.slice(
window.location.href.indexOf('?') + 1).split('&');
for ( var i = 0; i < hashes.length; i++) {
hash = hashes[i].split('=');
vars.push(hash[0]);
vars[hash[0]] = hash[1];
}
return vars;
}
function updateData(lastDate) {
var callback = function(data, textStatus, jqXHR) {
var i = 0;
dataIq.push([ id, data.numbers.iq ]);
dataMsg.push([ id, data.numbers.msg ]);
dataPres.push([ id, data.numbers.presence ]);
dataRost.push([ id, data.numbers.roster ]);
id++;
while (data.packets[i] != null) {
var color = "#DEDEDE";
if (rowCounter % 2 == 0) {
color = "#EBEBEB";
}
if ($('.entry').length == 0) {
$('.tableBegin').html(
'<tr class="entry" style="background-color:' + color
+ ';"><td class="timeStamp">'
+ formatTimestamp(data.packets[i].date)
+ '</td><td>' + data.packets[i].type
+ '</td><td>' + data.packets[i].from
+ '</td><td>' + data.packets[i].to
+ '</td></tr>');
} else {
$('.entry').filter(":first").before(
'<tr class="entry" style="background-color:' + color
+ ';"><td class="timeStamp">'
+ formatTimestamp(data.packets[i].date)
+ '</td><td>' + data.packets[i].type
+ '</td><td>' + data.packets[i].from
+ '</td><td>' + data.packets[i].to
+ '</td></tr>');
}
lastTimestamp = data.packets[i].date > lastTimestamp ? data.packets[i].date
: lastTimestamp;
if (rowCounter > limit) {
console.log("entferne row!?");
$('.entry').filter(":last").remove();
}
console.log(rowCounter);
rowCounter++;
i++;
}
}
drawGraph();
var myDomain = getUrlVars()["component"];
$.ajax({
url : 'stats?component=' + myDomain + '&date=' + lastDate,
dataType : 'json',
data : '',
success : callback
});
}
function formatTimestamp(timeStamp) {
function fillZero(data) {
return data < 10 ? "0" + data : data;
}
var date = new Date(timeStamp);
var hours = fillZero(date.getHours());
var minutes = fillZero(date.getMinutes());
var seconds = fillZero(date.getSeconds());
var year = fillZero(date.getFullYear());
var month = fillZero(date.getMonth() + 1);
var day = fillZero(date.getDate());
var formattedTime = hours + ':' + minutes + ':' + seconds + " &nbsp; "
+ day + "." + month + "." + year;
return formattedTime;
}
This source diff could not be displayed because it is too large. You can view the blob instead.
jQuery.noConflict();
function searchSuggest(object) {
document.getElementById('search_suggest' + object).innerHTML = '';
var str = escape(document.getElementById('groupSearch' + object).value);
jQuery("#ajaxloading" + object).html("<img src=\"images/ajax-loader.gif\">");
var request = new http();
var f = function(obj) {
var ss = document.getElementById('search_suggest' + object);
var resp = obj.responseXML.documentElement.getElementsByTagName('item');
if (resp.length > 0) {
for ( var i = 0; i < resp.length; i++) {
var suggest = '<div onmouseover="javascript:suggestOver(this);" ';
suggest += 'onmouseout="javascript:suggestOut(this);" ';
suggest += 'onclick="javascript:setSearch(this.innerHTML,'
+ object + ');" ';
suggest += 'class="suggest_link">' + resp[i].textContent
+ '</div>';
ss.innerHTML += suggest;
}
jQuery("#search_suggest" + object).show("slow");
} else {
jQuery("#search_suggest" + object).hide("slow");
}
};
request.callback = f;
request.load('groups?search=' + str);
}
// Mouse over function
function suggestOver(div_value) {
div_value.className = 'suggest_link_over';
}
// Mouse out function
function suggestOut(div_value) {
div_value.className = 'suggest_link';
}
// Click function
function setSearch(value, object) {
document.getElementById("groupSearch" + object).value = value;
// document.getElementById('search_suggest').innerHTML = '';
jQuery("#search_suggest" + object).hide("slow");
checkIfExists(value, object);
}
function checkIfExists(value, object) {
var str = value;
var request = new http();
var f = function(obj) {
var ss = document.getElementById('search_suggest' + object);
var resp = obj.responseXML.documentElement.getElementsByTagName('item');
for ( var i = 0; i < resp.length; i++) {
var stri = String(str);
var respStr = String(resp[i].textContent);
if (stri == respStr) {
jQuery("#ajaxloading" + object).html(
"<img src=\"images/correct-16x16.png\">");
break;
}
}
};
request.callback = f;
request.load('groups?search=' + str);
}
jQuery(document).ready(function() {
jQuery(".browser-data").horizontalBarGraph({
interval : 1
});
var i = 0;
while (jQuery('#logiq' + i).html() != null) {
var iqs = jQuery('#logiq' + i).html();
var msg = jQuery('#logmsg' + i).html();
var roster = jQuery('#logroster' + i).html();
var presence = jQuery('#logpresence' + i).html();
var data = [ {
label : "IQ",
data : parseInt(iqs)
}, {
label : "Messages",
data : parseInt(msg)
}, {
label : "Roster",
data : parseInt(roster)
}, {
label : "Presence",
data : parseInt(presence)
} ];
jQuery.plot(jQuery("#pie" + i), data, {
series : {
pie : {
show : true,
radius : 1,
offset : {
left : 10
}
}
},
legend : {
show : true
},
grid : {
hoverable : true,
clickable : true
}
});
++i;
}
})
function slideToggle(value) {
jQuery(value).slideToggle();
}
<%@ page import="org.jivesoftware.openfire.plugin.gojara.permissions.PermissionManager"%>
<%@ page import="org.jivesoftware.openfire.plugin.gojara.database.DatabaseManager"%>
<%@ page import="org.dom4j.tree.DefaultElement"%>
<%@ page import="org.jivesoftware.openfire.group.GroupManager"%>
<%@ page import="org.jivesoftware.openfire.group.Group"%>
<%@ page import="org.jivesoftware.openfire.session.ComponentSession"%>
<%@ page import="java.util.Collection"%>
<%@ page import="org.jivesoftware.openfire.SessionManager"%>
<%@ page import="org.jivesoftware.util.JiveGlobals"%>
<%@ page import="java.util.HashMap"%>
<%@ page import="java.util.Map"%>
<%@ page import="java.util.List"%>
<%@ page import="java.util.Date"%>
<%@ page import="org.jivesoftware.util.ParamUtils"%>
<%@ taglib uri="http://java.sun.com/jstl/core_rt" prefix="c"%>
<%@ taglib uri="http://java.sun.com/jstl/fmt_rt" prefix="fmt"%>
<%
// webManager.init(request, response, session, application, out);
boolean componentSet = request.getParameter("component") != null;
String component = "";
if (componentSet) {
component = request.getParameter("component");
}
Date currentDate = new Date(System.currentTimeMillis());
String now = currentDate.getHours() + ":" + currentDate.getMinutes() + "." + currentDate.getSeconds()
+ " " + currentDate.getDate() + "." + currentDate.getMonth() + "." + currentDate.getYear();
%>
<html>
<head>
<title>Live logs <%=componentSet ? "for " + component : ""%></title>
<meta name="decorator" content="none" />
<link href="./css/liveStats.css" rel="stylesheet" type="text/css">
<script src="./js/http.js" type="text/javascript"></script>
<script src="./js/jquery.js" type="text/javascript"></script>
<script src="./js/liveStats.js" type="text/javascript"></script>
<script language="javascript" type="text/javascript" src="./js/jquery.flot.js"></script>
</head>
<body>
<div class="div-main">
<div class="header">
Live statistics for
<%=componentSet ? component : "NOT SET"%></div>
<div class="graph">Here should appear your stats</div>
<table id="logTable">
<thead>
<tr>
<th>Date</th>
<th>Type</th>
<th>From</th>
<th>To</th>
</tr>
</thead>
<tbody class="tableBegin">
</tbody>
<tfoot>
<tr>
<td colspan="2">Live logging since <%=now%>
</td>
<td colspan="2"><form id="limitForm">
<input type="text" id="tableLimit">
</form></td>
</tr>
</tfoot>
</table>
</div>
<span id="logSince" style="visibility: hidden;"><%=System.currentTimeMillis()%></span>
</body>
</html>
\ No newline at end of file
<%@ page import="org.jivesoftware.openfire.plugin.gojara.database.DatabaseManager"%>
<%@ page import="org.jivesoftware.openfire.plugin.gojara.permissions.PermissionManager"%>
<%@ page import="org.dom4j.tree.DefaultElement"%>
<%@ page import="org.jivesoftware.openfire.group.GroupManager"%>
<%@ page import="org.jivesoftware.openfire.group.Group"%>
<%@ page import="org.jivesoftware.openfire.session.ComponentSession"%>
<%@ page import="java.util.Collection"%>
<%@ page import="org.jivesoftware.openfire.SessionManager"%>
<%@ page import="org.jivesoftware.util.JiveGlobals"%>
<%@ page import="java.util.HashMap"%>
<%@ page import="java.util.Map"%>
<%@ page import="java.util.List"%>
<%@ page import="org.jivesoftware.util.ParamUtils"%>
<%@ taglib uri="http://java.sun.com/jstl/core_rt" prefix="c"%>
<%@ taglib uri="http://java.sun.com/jstl/fmt_rt" prefix="fmt"%>
<jsp:useBean id="webManager" class="org.jivesoftware.util.WebManager" />
<%
webManager.init(request, response, session, application, out);
boolean save = request.getParameter("save") != null;
boolean success = request.getParameter("success") != null;
//boolean persistentRoster = ParamUtils.getBooleanAttribute(request, "persistentEnabled");
boolean persistentRoster = true;
String sparkdiscoParam = request.getParameter("sparkDiscoInfo");
boolean sparkDiscoInfo = sparkdiscoParam == null ? false : sparkdiscoParam.equals("true");
String iqLastFilterPram = request.getParameter("iqLastFilter");
boolean iqLastFilter = iqLastFilterPram == null ? false : iqLastFilterPram.equals("true");
String[] componentsEnabled = request.getParameterValues("enabledComponents[]");
PermissionManager _pmanager = new PermissionManager();
DatabaseManager _db;
Map<String, String> errors = new HashMap<String, String>();
if (save) {
for (String property : JiveGlobals.getPropertyNames("plugin.remoteroster.jids")) {
JiveGlobals.deleteProperty(property);
}
if (componentsEnabled != null) {
for (int i = 0; i < componentsEnabled.length; i++) {
JiveGlobals.setProperty("plugin.remoteroster.jids." + componentsEnabled[i], "true");
String group = request.getParameter("input_group." + componentsEnabled[i]);
if (group != null) {
_pmanager.setGroupForGateway(componentsEnabled[i], group);
}
}
}
JiveGlobals.setProperty("plugin.remoteroster.persistent", (persistentRoster ? "true" : "false"));
JiveGlobals.setProperty("plugin.remoteroster.sparkDiscoInfo", (sparkDiscoInfo ? "true" : "false"));
JiveGlobals.setProperty("plugin.remoteroster.iqLastFilter", (iqLastFilter ? "true" : "false"));
response.sendRedirect("rr-main.jsp?success=true");
return;
}
// Get the session manager
SessionManager sessionManager = webManager.getSessionManager();
Collection<ComponentSession> sessions = sessionManager.getComponentSessions();
_db = DatabaseManager.getInstance();
%>
<html>
<head>
<title><fmt:message key="rr.summary.title" /></title>
<link href="./css/rr.css" rel="stylesheet" type="text/css">
<script src="./js/http.js" type="text/javascript"></script>
<script src="./js/jquery.js" type="text/javascript"></script>
<script src="./js/rr.js" type="text/javascript"></script>
<script src="./js/jquery.sparkline.js" type="text/javascript"></script>
<script src="./js/jquery.horiz-bar-graph.js" type="text/javascript"></script>
<!--[if lte IE 8]><script language="javascript" type="text/javascript" src="./js/excanvas.min.js"></script><![endif]-->
<script language="javascript" type="text/javascript" src="./js/jquery.flot.js"></script>
<script language="javascript" type="text/javascript" src="./js/jquery.flot.pie.js"></script>
<meta name="pageID" content="remoteRoster" />
<meta name="helpPage" content="" />
</head>
<body>
<p>Any components configured here will allow the external component associated with them full control over their
domain within any user's roster. Before enabling Remote Roster Management support for an external component, first
connect it like you would any external component. Once it has connected and registered with Openfire, it's JID should
show up below and you can enable Remote Roster support.</p>
<%
if (success) {
%>
<div class="jive-success">
<table cellpadding="0" cellspacing="0" border="0">
<tbody>
<tr>
<td class="jive-icon"><img src="images/success-16x16.gif" width="16" height="16" border="0" alt=""></td>
<td class="jive-icon-label">Settings saved!</td>
</tr>
</tbody>
</table>
</div>
<br>
<%
} else if (errors.size() > 0) {
%>
<div class="jive-error">
<table cellpadding="0" cellspacing="0" border="0">
<tbody>
<tr>
<td class="jive-icon"><img src="images/error-16x16.gif" width="16" height="16" border="0" alt=""></td>
<td class="jive-icon-label">Error saving settings!</td>
</tr>
</tbody>
</table>
</div>
<br>
<%
}
%>
<form action="rr-main.jsp?save" method="post">
<div class="jive-contentBoxHeader">Connected Gateway Components</div>
<div class="jive-contentBox">
<p>Select which components you want to enable remote roster on:</p>
<%
boolean gatewayFound = false;
int i = 0;
for (ComponentSession componentSession : sessions) {
if (!componentSession.getExternalComponent().getCategory().equals("gateway")) {
continue;
}
gatewayFound = true;
long incoming = componentSession.getNumClientPackets();
long outgoing = componentSession.getNumServerPackets();
long both = incoming + outgoing;
int incomingPercent = (int) (incoming * 100 / both);
int outgoingPercent = (int) (outgoing * 100 / both);
%>
<table class="gatewayHeader">
<tbody>
<tr>
<td class="gatewayCheckbox"><input type="checkbox" name="enabledComponents[]"
value="<%=componentSession.getExternalComponent().getInitialSubdomain()%>"
<%=JiveGlobals.getBooleanProperty("plugin.remoteroster.jids."
+ componentSession.getExternalComponent().getInitialSubdomain(), false) ? "checked=\"checked\""
: ""%> />
</td>
<td class="gatewayName"><%=componentSession.getExternalComponent().getName()%></td>
<td class="gatewayIcons"><img src="images/log-16x16.png" onclick="slideToggle('#logs<%=i%>')"><img
src="images/permissions-16x16.png" id="showPermissions" onclick="slideToggle('#permission<%=i%>')"><img
src="images/info-16x16.png" id="showConfig" onclick="slideToggle('#config<%=i%>')"></td>
</tr>
</tbody>
</table>
<div id="config<%=i%>" class="slider">
<div class="sildeHeader">Information</div>
<table class="configTable">
<tbody>
<tr id="logodd">
<td width="200px">Domain:</td>
<td><%=componentSession.getExternalComponent().getInitialSubdomain()%></td>
</tr>
<tr id="logeven">
<td>Status:</td>
<td>Online</td>
</tr>
<tr id="logodd">
<td>Packages Send/Received:</td>
<td><dl class="browser-data" title="">
<dt>Incoming</dt>
<dd><%=incomingPercent%></dd>
<dt>Outgoing</dt>
<dd><%=outgoingPercent%></dd>
</dl></td>
</tr>
</tbody>
</table>
</div>
<div id="permission<%=i%>" class="slider">
<div class="sildeHeader">Access control</div>
<table class="groupTable">
<tbody>
<tr id="loghead">
<td colspan="3">You can limit the access to the external component to an existing group</td>
</tr>
<tr>
<td class="permissionTableColumn">Groupname:</td>
<td><input class="groupInput" type="text" id="groupSearch<%=i%>"
name="input_group.<%=componentSession.getExternalComponent().getInitialSubdomain()%>" alt="Find Groups"
onkeyup="searchSuggest('<%=i%>');" autocomplete="off"
value="<%=_pmanager.getGroupForGateway(componentSession.getExternalComponent().getInitialSubdomain())%>">
<div id="search_suggest<%=i%>"></div></td>
<td style="vertical-align: top;">
<div class="ajaxloading" id="ajaxloading<%=i%>"></div>
</td>
</tr>
</tbody>
</table>
</div>
<div id="logs<%=i%>" class="slider">
<%
int iqs = _db.getPacketCount(componentSession.getExternalComponent().getInitialSubdomain(),
Class.forName("org.xmpp.packet.IQ"));
int msgs = _db.getPacketCount(componentSession.getExternalComponent().getInitialSubdomain(),
Class.forName("org.xmpp.packet.Message"));
int rosters = _db.getPacketCount(componentSession.getExternalComponent().getInitialSubdomain(),
Class.forName("org.xmpp.packet.Roster"));
int presences = _db.getPacketCount(componentSession.getExternalComponent().getInitialSubdomain(),
Class.forName("org.xmpp.packet.Presence"));
%>
<div class="sildeHeader">Logs & Statistics</div>
<table class="logtable">
<tfoot>
<tr id="logfoot">
<td colspan="2">Packages being logged for <%=JiveGlobals.getIntProperty("plugin.remoteroster.log.cleaner.minutes", 60)%>
minutes
</td>
<td><a style="float: right;"
onClick="window.open('liveStats.jsp?component=<%=componentSession.getExternalComponent().getInitialSubdomain()%>','mywindow','width=1200,height=700')">Show
realtime Log</a>
</tr>
</tfoot>
<tbody>
<tr id="loghead">
<td width="200px">Paket type</td>
<td width="100px">Number</td>
<td></td>
</tr>
<tr id="logodd">
<td>IQ</td>
<td id="logiq<%=i%>"><%=iqs%></td>
<td rowspan="5"><div id="pie<%=i%>" class="graph"></div></td>
</tr>
<tr id="logeven">
<td>Messages</td>
<td id="logmsg<%=i%>"><%=msgs%></td>
</tr>
<tr id="logodd">
<td>Roster</td>
<td id="logroster<%=i%>"><%=rosters%></td>
</tr>
<tr id="logeven">
<td>Presence</td>
<td id="logpresence<%=i%>"><%=presences%></td>
</tr>
<tr id="logodd">
<td><span style="font-weight: bold;">Total:</span></td>
<td><span style="font-weight: bold;"><%=iqs + msgs + rosters + presences%></span></td>
</tr>
</tbody>
</table>
</div>
<%
++i;
}
%>
<%
if (!gatewayFound) {
%>
<span style="font-weight: bold">No connected external gateway components found.</span>
<%
}
%>
</div>
<!-- DISABLED PERSISTENT ROSTER UNTIL SPECTRUM SUPPORTS IT
<div class="jive-contentBoxHeader">Options</div>
<div class="jive-contentBox">
<table cellpadding="3" cellspacing="0" border="0" width="100%">
<tbody>
<tr valign="top">
<td width="1%" nowrap class="c1">
Persistent Roster:
</td>
<td width="99%">
<table cellpadding="0" cellspacing="0" border="0">
<tbody>
<tr>
<td>
<input type="radio" name="persistentEnabled" value="true" checked id="PER01">
</td>
<td><label for="PER01">Enabled (remote rosters are saved into the user's stored roster)</label></td>
</tr>
<tr>
<td>
<input type="radio" name="persistentEnabled" value="false" id="PER02">
</td>
<td><label for="PER02">Disabled (remote rosters exist only in memory)</label></td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</div>
-->
<br /> <br />
<div class="jive-contentBoxHeader">Client specific options</div>
<div class="jive-contentBox">
<table cellpadding="3" cellspacing="0" border="0" width="100%">
<tbody>
<tr valign="top">
<td width="1%" nowrap class="c1">Spark:</td>
<td width="99%">
<table cellpadding="0" cellspacing="0" border="0">
<tbody>
<tr>
<td><input type="checkbox" name="sparkDiscoInfo" id="SDI" value="true"
<%=JiveGlobals.getBooleanProperty("plugin.remoteroster.sparkDiscoInfo", false) ? "checked=\"checked\""
: ""%> />
</td>
<td><label for="SDI"> Support jabber:iq:registered feature</label></td>
</tr>
<tr>
<td />
<td align="left" style="font-size: -3; color: grey">If you use Spark clients within your network, it
might be necessary to modify the service discovery packets between Spark and the external component. If you
check this RemoteRoster will add the feature "jabber:iq:registered" to the disco#info to indicate that the
Client is registered with the external component.</td>
</tr>
<tr>
<td><input type="checkbox" name="iqLastFilter" id="SDI2" value="true"
<%=JiveGlobals.getBooleanProperty("plugin.remoteroster.iqLastFilter", false) ? "checked=\"checked\""
: ""%> />
</td>
<td><label for="SDI">Reply to jabber:iq:last </label></td>
</tr>
<tr>
<td />
<td align="left" style="font-size: -3; color: grey">Some clients try to check how long a contact is already offline.
This feature is not supported by spectrum so it won't response to this IQ stanza. To prevent the client from waiting
for a response we could answer with a service-unavailable message as described in XEP-12.</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</div>
<input type="submit" name="save" value="Save Settings" />
</form>
</body>
</html>
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