September 2007
Motivation
User presence is the domain normally represented by instant messaging systems, and while libraries supporting presence have existed for Java for a while, presence isn't normally represented in J2EE.
The application domain for user presence is large. The monitoring subsystem of a J2EE application requires presence information to effectively route reports and alerts to monitoring operators. VoIP applications such as call centers can forward calls to available users and voice mail can be automatically activated for users whose presence is marked as 'extended-away' or 'off line'. Workflow-based systems that assign tasks to users as part of a workflow execution (such as those used in banking operations or hosting provisioning operations) cannot be reasonably efficient without having at their disposal the presence state of their operators pool. Finally, help-desk applications require a real-time view of the available assistants.
Introduction to XMPP
The standard protocol for presence is XMPP (RFCs 3920 & 3921), better known as Jabber. XMPP provides far more than a presence protocol. It is an extensible messaging protocol that can be used in:
- one-way reliable messaging (<message/> tag)
- asynchronous request-reply communications (<iq/> tag)
- 1 to N multicasts (using conference rooms)
- the exchange of presence information (<presence/>)
The XML format of XMPP messages makes it readily accessible in all programming environments. The extensible layout of messages allows for the easy addition of new services on top of a strong messaging platform.
Presence functionality is also included in the Session Initiation Protocol (RFC-3856). The above pattern can be applied to SIP presence notifications as well. XMPP has been chosen over SIP for presence because of the maturity of its server and API pure Java implementations and its extensible XML message format.
OpenFire
There exist several mature XMPP server implementations. In this document, we'll use OpenFire (known in the past as Wildfire or Jive Server). OpenFire is open-source and implemented by JiveSoftware Inc. who is also the developer of Smack, the Java XMPP library that we'll use.
OpenFire has an extensible architecture that allows writing plugins that tightly integrate with the core server to offer services based on XMPP. An OpenFire plugin is exposed to all events happening on the server (user, roster & presence modifications), it can intercept any XMPP packets exchanged between XMPP users and can use the persistence API of OpenFire to store its settings.
Presence
Presence events are sent by the XMPP server when a registered user:
- Logs in / Logs out.
- Changes her status to “available”, “away”, etc.
- Changes her custom status message.
XMPP specifies that presence information of a registered XMPP user is only available to authorized members of this user's roster (RFC 3921). That is, in order to register to user's A presence change events, one has to create another XMPP user B and add user A to B's roster. At that point, user A will receive a request to allow access to its presence from user B. In an XMPP GUI client this request usually manifests as a popup window notifying the user that she has been added to another user's roster. Clearly this is inconvenient as it requires a user to be logged in when we register to presence events from it. Fortunately OpenFire provides a plugin that offers a solution to this problem. The 'Subscription' plugin can be configured to automatically accept or reject presence subscription requests from a predefined set of users or from all users.
Presence Notifications
In this document, we consider 2 alternatives for receiving presence notifications:
- Create a special XMPP user on the server and add this server to the roster of all XMPP users
- Write an OpenFire plugin that registers to all presence events on the server and forwards them.
Presence Notifications as Roster Events
The approach of creating a user and adding her to the roster of all other users has considerable obvious disadvantages:
- It needs a special XMPP account that serves no other purpose.
- Pollutes the roster of all XMPP users with an entry that does not correspond to a person.
- It does not scale well as the number of users increases
- Introduces additional maintenance effort when users are created on the XMPP server
Despite the disadvantages, the approach is simple in conception and has an equally simple implementation.
OpenFire also provides a “Presence” plugin that exposes presence information through HTTP GET. This currently has 2 modes of operation: provide presence information to users subscribed to the roster or to all users. However it is trivial to allow calls only to subscribed users and a set of superusers. The disadvantage of this solution is the lack of presence change notifications.
Presence Notifications as JMS Messages
An alternative to roster events and strongly recommended in a J2EE environment is the translation of roster presence events to JMS messages. The JMS destination can be a queue (for a single subscriber) or a topic if multicasting of the roster event is required.

In the presence of a JMS server, this approach is much preferred. It provides one-to-one and one-to-many delivery semantics, reliable delivery and persistent storage of messages. The processing of the received JMS message integrates well with CMT & JTA transactions. Last, the use of Message-Driven Beans (MDBs) eliminates the need for polling and allows to react immediately to the received message.
Effectively, in this scenario JMS becomes the adapter of an external event source into an event source that is natural for J2EE systems. In the following we will describe a solution for integrating XMPP presence with a J2EE server by translating the presence events to JMS messages on the XMPP server. The JMS server of choice is JORAM. JORAM is JMS 1.1 certified JMS server developed by the ObjectWeb consortium. However any other JMS server such as ActiveMQ can be used.
OpenFire Plugin
The missing element for converting XMPP presence events to JMS messages is the OpenFire plugin mechanism. An OpenFire plugin is a jar package of classes & resources that lives on the OpenFire VM and is loaded by the server as part of its initialization. The plugin optionally provides a JSP page that integrates with OpenFire's GUI and allows configuring its settings. Furthermore a plugin can be undeployed or reloaded manually.
A plugin lives in the same VM as OpenFire and has access to all its core structures. It can register directly to all presence events on the server and thus no special user or roster subscription is required. This solution requires some knowledge of the OpenFire internals and the implementation of OpenFire plugins. Fortunately JiveSoftware provides tutorial for plugin development. Furthermore all plugins shipped with OpenFire are open-source and can be copied to replicate the directory structure and jump start on the creation of the new plugin.
Development
Existing OpenFire plugins are located in the source tree of the core OpenFire server. There's a single Ant-based building system which relies on this monolithic structure to build plugins. Plugins employ OpenFire Ant building system to build. In addition plugins require access to various classes of OpenFire.
We download & extract the full source package for OpenFire. Plugins are located in src/plugins. The plugin must adheres to a strict directory structure shown below:
myplugin/
|- plugin.xml <- Plugin definition file
|- readme.html <- Optional readme file for plugin, which will be displayed to end users
|- changelog.html <- Optional changelog file for plugin, which will be displayed to end users
|- logo_small.gif <- Optional small (16x16) icon associated with the plugin (can also be a .png file)
|- logo_large.gif <- Optional large (32x32) icon associated with the plugin (can also be a .png file)
|- classes/ <- Resources your plugin needs (i.e., a properties file)
|- database/ <- Optional database schema files that your plugin needs
|- i18n/ <- Optional i18n files to allow for internationalization of plugins.
|- lib/ <- Libraries (JAR files) your plugin needs
|- src/ <- Source code & Admin Console resources
|- java <- Source codde
|- web <- Resources for Admin Console integration, if any
|- images/
|- WEB-INF/
|- web.xml <- Generated web.xml containing compiled JSP entries
|- web-custom.xml <- Optional user-defined web.xml for custom servlets
The plugin.xml contains the definition of the plugin. It includes the full name of the plugin entry class, version & author information, a sort description as well as the minimal version of OpenFire supported.
<plugin>
<class>org.jivesoftware.wildfire.plugin.JmsPlugin</class>
<name>JMS Presence Gateway</name>
<description>Copy presence events to a JMS endpoint</description>
<author>John Georgiadis</author>
<version>1.0</version>
<date></date>
<url></url>
<minServerVersion>3.3.1</minServerVersion>
</plugin>
The plugin.xml also contains information about JSP pages that the plugin injects into the OpenFire administration web application. This is described later in the document when we discuss the configuration options.
The entry class for the plugin must extend the interface
public class JmsPlugin implements org.jivesoftware.openfire.container.Plugin
and it must implement the following methods for initializing and cleaning up the resources used by the plugin
void initializePlugin(PluginManager manager, File pluginDirectory);
void destroyPlugin();
The initialization code
- Sets the JNDI context object used for looking up the topic/queue connection factory
private void setupCtx(String host, int port) throws NamingException {
ctxProperties = new Properties();
ctxProperties.put("java.naming.factory.initial",
"org.objectweb.carol.jndi.spi.MultiOrbInitialContextFactory");
ctxProperties.put("java.naming.provider.url",
"rmi://"+host+":"+port);
ctxProperties.put("java.naming.factory.url.pkgs",
"org.objectweb.jonas.naming");
ctx = new InitialContext(ctxProperties);
}
- Registers a SessionEventListener & a PresenceEventListener
private void registerSessionListener() {
if (sessionListener != null)
return;
sessionListener = new SessionListener();
SessionEventDispatcher.addListener(sessionListener);
}
private void registerPresenceListener() {
if (presenceListener != null)
return;
presenceListener = new PresenceListener();
PresenceEventDispatcher.addListener(presenceListener);
}
Prior to version 3.3.1, when a connection to the server was abruptly closed, an unavailable presence event was sent. Since 3.3.1 an unavailable presence packet is routed to its destination (e.g. other registered roster XMPP users) but no event is sent to presence modification listeners in the server (see JIRA issue JM-1043). In order to cover for this case, a SessionEventListener is employed to capture the session removal event that follows the connection close.
The SessionEventListener interface is:
public interface SessionEventListener {
public void sessionCreated(Session session);
public void sessionDestroyed(Session session);
public void anonymousSessionCreated(Session session);
public void anonymousSessionDestroyed(Session session);
}
We are only interested in sessionDestroyed() call which we translate to an unavailable presence packet:
private void doSessionDestroyed(Session session)
throws UserNotFoundException, JMSException, NamingException {
Presence presence;
if (!(session instanceof ClientSession)) {
Log.debug("Session not a client session");
return;
}
presence = new Presence();
presence.setType(Presence.Type.unavailable);
presence.setFrom(session.getAddress());
Log.debug("Session for user "+((ClientSession)session).getUsername()+
" has become unavailable");
sendPacket(presence);
}
The PresenceEventListener interface is:
public interface PresenceEventListener {
public void availableSession(ClientSession session, Presence presence);
public void unavailableSession(ClientSession session, Presence presence);
public void presencePriorityChanged(ClientSession session, Presence presence);
public void presenceChanged(ClientSession session, Presence presence);
}
We need to provide an implementation for the following methods only:
- availableSession(); it is called when a new session is created for an XMPP user
- unavailableSession(); it is called when an unavailable presence packet is received. The user is still connected to the server and may resume activity.
- presenceChanged(); it is called when an available session has changed its presence. This is the case when the “show” field changes (e.g. away or dnd) or the presence “status” message changes.
In all 3 cases the implementation is the same. We forward the received presence packet to the JMS destination:
private void doUnavailableSession(ClientSession session,
Presence presence) throws UserNotFoundException, JMSException,
NamingException {
Log.debug("Session for user "+session.getUsername()+" has become"+
" unavailable");
sendPacket(presence);
}
private void doPresenceChanged(ClientSession session, Presence presence)
throws UserNotFoundException, JMSException, NamingException {
Log.debug("Presence for user "+session.getUsername()+" has changed"+
" to "+presence.getType());
sendPacket(presence);
}
private void doAvailableSession(ClientSession session, Presence presence)
throws UserNotFoundException, JMSException, NamingException {
Log.debug("Session for user "+session.getUsername()+" has become"+
" available");
sendPacket(presence);
}
Plugin Configuration
The configuration of the JMS endpoint takes advantage of OpenFire's GUI extensions for plugins and the support for persistent plugin configuration.
The plugin has 3 configurable properties and a get/set pair of methods for each:
- JNDI host. The default value is “localhost”
- JNDI port. The default value is 1099
- Queue (or topic) name
Each property has a unique name in OpenFire. E.g. the JNDI host property name is “plugin.jms.jndi.host”. Properties are stored and retrieved with the JiveGlobals utility class. E.g. for the JNDI host, the default value is “localhost and the get/set pair of calls are:
private static final String PROPERTY_JNDI_HOST = "plugin.jms.jndi.host";
private static final String JNDI_HOST = "localhost";
public String getJndiHost() {
return JiveGlobals.getProperty(PROPERTY_JNDI_HOST, JNDI_HOST);
}
public void setJndiPort(String port) {
JiveGlobals.setProperty(PROPERTY_JNDI_PORT, port);
}
To configure the above properties from the OpenFire GUI, a JSP page is created and injected into the OpenFire web application. The name & location of the JSP page can be specified in plugin.xml:
<adminconsole>
<tab id="tab-server">
<sidebar id="sidebar-server-settings">
<item id="jms-plugin-properties"
name="JMS Presence Gateway"
url="jms-plugin-properties.jsp"
description="Edit JMS Presence Gateway plugin properties"/>
</sidebar>
</tab>
</adminconsole>
The JSP page defines a simple input form with 3 entries. It takes a reference to the JmsPlugin and calls the getters & setters to read and update the plugin configuration properties. The page replicates the layout of the JSP page of the Presence plugin. It also adds a synchronization button that delivers the presence state of all registered XMPP users to JMS.
In the implementation of presence synchronization a list of all users is retrieved:
Collection<User> users = XMPPServer.getInstance().getUserManager().getUsers()
Then the code queries presence information for each user and forwards it to JMS. An empty presence object signifies that the user is unavailable:
for (User user:users) {
presence = presenceMan.getPresence(user);
if (presence == null)
presence = getUnavailablePresence(server.createJID(user.getUsername(), null));
sendPacket(presence);
}
Classpath
The plugin jar file is bundled with a set of jar libraries that live in the lib/ subdirectory. In this case we need J2EE javax.jms.* classes and the JORAM JMS provider classes. In addition JORAM uses Monolog for logging and the corresponding jar files are also included.
The class loader used by OpenFire to load a plugin is a URLClassLoader which holds all the entries in the plugin's lib/ directory. The thread context class loader is a JiveClassLoader which holds only the OpenFire jar entries. When the JNDI context is created to lookup the JMS queue factory, it makes a call to the thread context class loader to resolve the JNDI context factory. Therefore we need to replace the thread context class loader before the JNDI lookups:
ClassLoader loader;
loader = Thread.currentThread().getContextClassLoader();
Thread.currentThread().setContextClassLoader(getClass().getClassLoader());
try {
...
} finally {
Thread.currentThread().setContextClassLoader(loader);
}
JNDI lookups are performed in all the entry points of the plugin. The entry points of the plugin class are the initialization/removal methods, the listener methods & the sync() call. Thus all these methods are wrapped by the statements above.
JMS Dispatch
The JMS dispatch implementation in the OpenFire plugin is a straightforward JMS sender. We get a reference to a non-managed queue connection factory, open a connection to the JMS server, create a session and a sender to the queue. Then we send a JMS javax.jms.ObjectMessage that holds the XML version of the XMPP presence packet.
private void sendMessage(QueueConnectionFactory connFactory,
Queue queue, Serializable msg) throws JMSException {
QueueConnection conn;
QueueSession session;
QueueSender sender;
conn = connFactory.createQueueConnection();
try {
session = conn.createQueueSession(false,
QueueSession.AUTO_ACKNOWLEDGE);
try {
sender = (QueueSender) session.createSender(queue);
sender.setDeliveryMode(DeliveryMode.PERSISTENT);
sender.send(session.createObjectMessage(msg));
} finally {
session.close();
}
} finally {
conn.close();
}
}
private void sendMessage(String connFactory, String queue,
Serializable msg) throws NamingException, JMSException {
QueueConnectionFactory connFactoryObj;
Queue queueObj;
connFactoryObj = (QueueConnectionFactory)ctx.lookup(connFactory);
queueObj = (Queue) ctx.lookup(queue);
sendMessage(connFactoryObj, queueObj, msg);
}
private void sendPacket(Packet packet)
throws JMSException, NamingException {
if (packet == null)
return;
if (getQueue() == null || getQueue().length() == 0) {
Log.warn("Queue JNDI name not set. Packet not sent");
return;
}
if (Log.isInfoEnabled())
Log.info("Sending packet "+packet.toXML()+" to queue "+getQueue());
sendMessage(QUEUE_CONNECTION_FACTORY, getQueue(), packet.toXML());
}
In a demanding environment, the connection factory would probably be cached to remove the JNDI lookup for every presence packet. It would also be wrapped by a dynamic proxy that would refresh the underlying connection factory remote reference if that becomes obsolete (e.g. due to network failures or JMS server restarts).
JMS Reception
The receiving side is a JMS queue (or topic) subscriber, e.g. a message-driven bean (MDB). The incoming JMS message contains a XMPP presence packet in its XML form. This needs to be parsed to reconstruct the org.jivesoftware.smack.packet.Packet object. For that we can borrow code from smack source file org/jivesoftware/smack/PacketReader.java. The result is PacketParser class (included in the source code) which can parse message, iq & presence XMPP packets.
A sample MDB that receives Presence packets from a JMS queue is shown below:
public class XmppMdBean implements MessageDrivenBean, MessageListener {
private void handle(Presence packet) {
...
}
private void handle(Packet packet) {
if (packet == null)
return;
if ((packet instanceof Presence))
handle((Presence)packet);
}
private void handle(String xmlPacket) throws Exception {
Packet packet;
packet = new PacketParser().parse(xmlPacket);
if (packet != null)
handle(packet);
}
public void setMessageDrivenContext(MessageDrivenContext ctx) {
}
public void ejbRemove() {
}
public void ejbCreate() {
}
public void onMessage(Message msg) {
ObjectMessage objMsg;
try {
objMsg = (ObjectMessage)msg;
handle((String)objMsg.getObject());
} catch (Throwable ex) {
}
}
}
References
http://www.igniterealtime.org, OpenFire XMPP server & Smack Java XMPP API.
http://joram.objectweb.org, JORAM JMS server.
http://www.igniterealtime.org/builds/openfire/docs/latest/documentation/plugin-dev-guide.html, OpenFire plugin developer guide.
http://www.xmpp.org/rfcs, XMPP RFCs.
http://www.igniterealtime.org/issues/secure/Dashboard.jspa, JIRA issue tracking site for OpenFire and related products.
PRINTER FRIENDLY VERSION
|