diff --git a/source/org/jivesoftware/smackx/workgroup/Invitation.java b/source/org/jivesoftware/smackx/workgroup/Invitation.java
new file mode 100644
index 000000000..b5af5801e
--- /dev/null
+++ b/source/org/jivesoftware/smackx/workgroup/Invitation.java
@@ -0,0 +1,125 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright (C) 1999-2003 Jive Software. All rights reserved.
+ *
+ * This software is the proprietary information of Jive Software.
+ * Use is subject to license terms.
+ */
+
+package org.jivesoftware.smackx.workgroup;
+
+import java.util.Map;
+
+/**
+ * An immutable class wrapping up the basic information which comprises a group chat invitation.
+ *
+ * @author loki der quaeler
+ */
+public class Invitation {
+
+ protected String uniqueID;
+
+ protected String sessionID;
+
+ protected String groupChatName;
+ protected String issuingWorkgroupName;
+ protected String messageBody;
+ protected String invitationSender;
+ protected Map metaData;
+
+ /**
+ * This calls the 5-argument constructor with a null MetaData argument value
+ *
+ * @param jid the jid string with which the issuing AgentSession or Workgroup instance
+ * was created
+ * @param group the jid of the room to which the person is invited
+ * @param workgroup the jid of the workgroup issuing the invitation
+ * @param sessID the session id associated with the pending chat
+ * @param msgBody the body of the message which contained the invitation
+ * @param from the user jid who issued the invitation, if known, null otherwise
+ */
+ public Invitation (String jid, String group, String workgroup,
+ String sessID, String msgBody, String from) {
+ this(jid, group, workgroup, sessID, msgBody, from, null);
+ }
+
+ /**
+ * @param jid the jid string with which the issuing AgentSession or Workgroup instance
+ * was created
+ * @param group the jid of the room to which the person is invited
+ * @param workgroup the jid of the workgroup issuing the invitation
+ * @param sessID the session id associated with the pending chat
+ * @param msgBody the body of the message which contained the invitation
+ * @param from the user jid who issued the invitation, if known, null otherwise
+ * @param metaData the metadata sent with the invitation
+ */
+ public Invitation (String jid, String group, String workgroup, String sessID, String msgBody,
+ String from, Map metaData) {
+ super();
+
+ this.uniqueID = jid;
+ this.sessionID = sessID;
+ this.groupChatName = group;
+ this.issuingWorkgroupName = workgroup;
+ this.messageBody = msgBody;
+ this.invitationSender = from;
+ this.metaData = metaData;
+ }
+
+ /**
+ * @return the jid string with which the issuing AgentSession or Workgroup instance
+ * was created.
+ */
+ public String getUniqueID () {
+ return this.uniqueID;
+ }
+
+ /**
+ * @return the session id associated with the pending chat; working backwards temporally,
+ * this session id should match the session id to the corresponding offer request
+ * which resulted in this invitation.
+ */
+ public String getSessionID () {
+ return this.sessionID;
+ }
+
+ /**
+ * @return the jid of the room to which the person is invited.
+ */
+ public String getGroupChatName () {
+ return this.groupChatName;
+ }
+
+ /**
+ * @return the name of the workgroup from which the invitation was issued.
+ */
+ public String getWorkgroupName () {
+ return this.issuingWorkgroupName;
+ }
+
+ /**
+ * @return the contents of the body-block of the message that housed this invitation.
+ */
+ public String getMessageBody () {
+ return this.messageBody;
+ }
+
+ /**
+ * @return the user who issued the invitation, or null if it wasn't known.
+ */
+ public String getInvitationSender () {
+ return this.invitationSender;
+ }
+
+ /**
+ * @return the meta data associated with the invitation, or null if this instance was
+ * constructed with none
+ */
+ public Map getMetaData () {
+ return this.metaData;
+ }
+
+}
diff --git a/source/org/jivesoftware/smackx/workgroup/InvitationListener.java b/source/org/jivesoftware/smackx/workgroup/InvitationListener.java
new file mode 100644
index 000000000..ab9d53336
--- /dev/null
+++ b/source/org/jivesoftware/smackx/workgroup/InvitationListener.java
@@ -0,0 +1,30 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright (C) 1999-2003 Jive Software. All rights reserved.
+ *
+ * This software is the proprietary information of Jive Software.
+ * Use is subject to license terms.
+ */
+package org.jivesoftware.smackx.workgroup;
+
+/**
+ * An interface which all classes interested in hearing about group chat invitations should
+ * implement.
+ *
+ * @author loki der quaeler
+ */
+public interface InvitationListener {
+
+ /**
+ * The implementing class instance will be notified via this method when an invitation
+ * to join a group chat has been received from the server.
+ *
+ * @param invitation an Invitation instance embodying the information pertaining to the
+ * invitation
+ */
+ public void invitationReceived(Invitation invitation);
+
+}
diff --git a/source/org/jivesoftware/smackx/workgroup/MetaData.java b/source/org/jivesoftware/smackx/workgroup/MetaData.java
new file mode 100644
index 000000000..b0ec5350e
--- /dev/null
+++ b/source/org/jivesoftware/smackx/workgroup/MetaData.java
@@ -0,0 +1,59 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright (C) 1999-2003 Jive Software. All rights reserved.
+ *
+ * This software is the proprietary information of Jive Software.
+ * Use is subject to license terms.
+ */
+
+package org.jivesoftware.smackx.workgroup;
+
+import java.util.Map;
+
+import org.jivesoftware.smackx.workgroup.util.MetaDataUtils;
+
+import org.jivesoftware.smack.packet.PacketExtension;
+
+/**
+ * MetaData packet extension.
+ */
+public class MetaData implements PacketExtension {
+
+ /**
+ * Element name of the packet extension.
+ */
+ public static final String ELEMENT_NAME = "metadata";
+
+ /**
+ * Namespace of the packet extension.
+ */
+ public static final String NAMESPACE = "http://www.jivesoftware.com/workgroup/metadata";
+
+ private Map metaData;
+
+ public MetaData(Map metaData) {
+ this.metaData = metaData;
+ }
+
+ /**
+ * @return the Map of metadata contained by this instance
+ */
+ public Map getMetaData() {
+ return metaData;
+ }
+
+ public String getElementName() {
+ return ELEMENT_NAME;
+ }
+
+ public String getNamespace() {
+ return NAMESPACE;
+ }
+
+ public String toXML() {
+ return MetaDataUtils.serializeMetaData(this.getMetaData());
+ }
+}
\ No newline at end of file
diff --git a/source/org/jivesoftware/smackx/workgroup/QueueUser.java b/source/org/jivesoftware/smackx/workgroup/QueueUser.java
new file mode 100644
index 000000000..ffd4a4e99
--- /dev/null
+++ b/source/org/jivesoftware/smackx/workgroup/QueueUser.java
@@ -0,0 +1,77 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright (C) 1999-2003 Jive Software. All rights reserved.
+ *
+ * This software is the proprietary information of Jive Software.
+ * Use is subject to license terms.
+ */
+
+package org.jivesoftware.smackx.workgroup;
+
+import java.util.Date;
+
+/**
+ * An immutable class which wraps up customer-in-queue data return from the server; depending on
+ * the type of information dispatched from the server, not all information will be available in
+ * any given instance.
+ *
+ * @author loki der quaeler
+ */
+public class QueueUser {
+
+ private String userID;
+
+ private int queuePosition;
+ private int estimatedTime;
+ private Date joinDate;
+
+ /**
+ * @param uid the user jid of the customer in the queue
+ * @param position the position customer sits in the queue
+ * @param time the estimate of how much longer the customer will be in the queue in seconds
+ * @param joinedAt the timestamp of when the customer entered the queue
+ */
+ public QueueUser (String uid, int position, int time, Date joinedAt) {
+ super();
+
+ this.userID = uid;
+ this.queuePosition = position;
+ this.estimatedTime = time;
+ this.joinDate = joinedAt;
+ }
+
+ /**
+ * @return the user jid of the customer in the queue
+ */
+ public String getUserID () {
+ return this.userID;
+ }
+
+ /**
+ * @return the position in the queue at which the customer sits, or -1 if the update which
+ * this instance embodies is only a time update instead
+ */
+ public int getQueuePosition () {
+ return this.queuePosition;
+ }
+
+ /**
+ * @return the estimated time remaining of the customer in the queue in seconds, or -1 if
+ * if the update which this instance embodies is only a position update instead
+ */
+ public int getEstimatedRemainingTime () {
+ return this.estimatedTime;
+ }
+
+ /**
+ * @return the timestamp of when this customer entered the queue, or null if the server did not
+ * provide this information
+ */
+ public Date getQueueJoinTimestamp () {
+ return this.joinDate;
+ }
+
+}
diff --git a/source/org/jivesoftware/smackx/workgroup/agent/Agent.java b/source/org/jivesoftware/smackx/workgroup/agent/Agent.java
new file mode 100644
index 000000000..39beb3e75
--- /dev/null
+++ b/source/org/jivesoftware/smackx/workgroup/agent/Agent.java
@@ -0,0 +1,60 @@
+package org.jivesoftware.smackx.workgroup.agent;
+
+import org.jivesoftware.smack.packet.Presence;
+
+/**
+ * An Agent represents the agent role in a Workgroup Queue.
+ */
+public class Agent {
+
+ private String user;
+ private int maxChats = -1;
+ private int currentChats = -1;
+ private Presence presence = null;
+
+ /**
+ * Creates an Agent
+ * @param user - the current agents JID
+ * @param currentChats - the number of chats the agent is in.
+ * @param maxChats - the maximum number of chats the agent is allowed.
+ * @param presence - the agents presence
+ */
+ public Agent( String user, int currentChats, int maxChats, Presence presence ) {
+ this.user = user;
+ this.currentChats = currentChats;
+ this.maxChats = maxChats;
+ this.presence = presence;
+ }
+
+ /**
+ * Return the agents JID
+ * @return - the agents JID.
+ */
+ public String getUser() {
+ return user;
+ }
+
+ /**
+ * Return the maximum number of chats for this agent.
+ * @return - maximum number of chats allowed.
+ */
+ public int getMaxChats() {
+ return maxChats;
+ }
+
+ /**
+ * Return the current chat count.
+ * @return - the current chat count.
+ */
+ public int getCurrentChats() {
+ return currentChats;
+ }
+
+ /**
+ * Return the agents Presence
+ * @return - the agents presence.
+ */
+ public Presence getPresence() {
+ return presence;
+ }
+}
diff --git a/source/org/jivesoftware/smackx/workgroup/agent/AgentSession.java b/source/org/jivesoftware/smackx/workgroup/agent/AgentSession.java
new file mode 100644
index 000000000..6ce26a5b8
--- /dev/null
+++ b/source/org/jivesoftware/smackx/workgroup/agent/AgentSession.java
@@ -0,0 +1,644 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright (C) 1999-2003 Jive Software. All rights reserved.
+ *
+ * This software is the proprietary information of Jive Software.
+ * Use is subject to license terms.
+ */
+
+package org.jivesoftware.smackx.workgroup.agent;
+
+import java.util.*;
+
+import org.jivesoftware.smack.*;
+import org.jivesoftware.smack.filter.*;
+import org.jivesoftware.smack.packet.*;
+import org.jivesoftware.smack.util.StringUtils;
+import org.jivesoftware.smackx.GroupChatInvitation;
+
+import org.jivesoftware.smackx.workgroup.*;
+import org.jivesoftware.smackx.workgroup.packet.*;
+
+/**
+ * This class embodies the agent's active presence within a given workgroup. The application
+ * should have N instances of this class, where N is the number of workgroups to which the
+ * owning agent of the application belongs. This class provides all functionality that a
+ * session within a given workgroup is expected to have from an agent's perspective -- setting
+ * the status, tracking the status of queues to which the agent belongs within the workgroup, and
+ * dequeuing customers.
+ *
+ * @author Matt Tucker
+ * @author loki der quaeler
+ */
+public class AgentSession {
+
+ private XMPPConnection connection;
+
+ private String workgroupName;
+
+ private boolean online = false;
+ private Presence.Mode presenceMode;
+ private int currentChats;
+ private int maxChats;
+ private Map metaData;
+
+ private Map queues;
+
+ private List offerListeners;
+ private List invitationListeners;
+ private List queueUsersListeners;
+ private List queueAgentsListeners;
+
+ /**
+ * Creates a new agent session instance.
+ *
+ * @param connection a connection instance which must have already gone through authentication.
+ * @param workgroupName the fully qualified name of the workgroup.
+ */
+ public AgentSession(String workgroupName, XMPPConnection connection) {
+ // Login must have been done before passing in connection.
+ if (!connection.isAuthenticated()) {
+ throw new IllegalStateException("Must login to server before creating workgroup.");
+ }
+
+ this.workgroupName = workgroupName;
+ this.connection = connection;
+
+ this.maxChats = -1;
+
+ this.metaData = new HashMap();
+
+ this.queues = new HashMap();
+
+ offerListeners = new ArrayList();
+ invitationListeners = new ArrayList();
+ queueUsersListeners = new ArrayList();
+ queueAgentsListeners = new ArrayList();
+
+ // Create a filter to listen for packets we're interested in.
+ OrFilter filter = new OrFilter();
+ filter.addFilter(new PacketTypeFilter(OfferRequestProvider.OfferRequestPacket.class));
+ filter.addFilter(new PacketTypeFilter(OfferRevokeProvider.OfferRevokePacket.class));
+ filter.addFilter(new PacketTypeFilter(Presence.class));
+ filter.addFilter(new PacketTypeFilter(Message.class));
+
+ // only interested in packets from the workgroup or from operators also running the
+ // operator client -- for example peer-to-peer invites wouldn't come from
+ // the workgroup, but would come from the operator client
+ //OrFilter froms = new OrFilter(new FromContainsFilter(this.workgroupName),
+ // new FromContainsFilter(clientResource));
+
+ connection.addPacketListener(new PacketListener() {
+ public void processPacket(Packet packet) {
+ handlePacket(packet);
+ }
+ }, filter);
+ }
+
+ /**
+ * Returns the agent's current presence mode.
+ *
+ * @return the agent's current presence mode.
+ */
+ public Presence.Mode getPresenceMode() {
+ return presenceMode;
+ }
+
+ /**
+ * Returns the current number of chats the agent is in.
+ *
+ * @return the current number of chats the agent is in.
+ */
+ public int getCurrentChats() {
+ return currentChats;
+ }
+
+ /**
+ * Returns the maximum number of chats the agent can participate in.
+ *
+ * @return the maximum number of chats the agent can participate in.
+ */
+ public int getMaxChats() {
+ return maxChats;
+ }
+
+ /**
+ * Returns true if the agent is online with the workgroup.
+ *
+ * @return true if the agent is online with the workgroup.
+ */
+ public boolean isOnline() {
+ return online;
+ }
+
+ /**
+ * Allows the addition of a new key-value pair to the agent's meta data, if the value is
+ * new data, the revised meta data will be rebroadcast in an agent's presence broadcast.
+ *
+ * @param key the meta data key
+ * @param val the non-null meta data value
+ */
+ public void setMetaData(String key, String val) throws XMPPException {
+ synchronized (this.metaData) {
+ String oldVal = (String)this.metaData.get(key);
+
+ if ((oldVal == null) || (! oldVal.equals(val))) {
+ metaData.put(key, val);
+
+ setStatus(presenceMode, currentChats, maxChats);
+ }
+ }
+ }
+
+ /**
+ * Allows the removal of data from the agent's meta data, if the key represents existing data,
+ * the revised meta data will be rebroadcast in an agent's presence broadcast.
+ *
+ * @param key the meta data key
+ */
+ public void removeMetaData(String key)
+ throws XMPPException {
+ synchronized (this.metaData) {
+ String oldVal = (String)metaData.remove(key);
+
+ if (oldVal != null) {
+ setStatus(presenceMode, currentChats, maxChats);
+ }
+ }
+ }
+
+ /**
+ * Allows the retrieval of meta data for a specified key.
+ *
+ * @param key the meta data key
+ * @return the meta data value associated with the key or null if the meta-data
+ * doesn't exist..
+ */
+ public String getMetaData(String key) {
+ return (String)metaData.get(key);
+ }
+
+ /**
+ * Sets whether the agent is online with the workgroup. If the user tries to go online with
+ * the workgroup but is not allowed to be an agent, an XMPPError with error code 401 will
+ * be thrown.
+ *
+ * @param online true to set the agent as online with the workgroup.
+ * @throws XMPPException if an error occurs setting the online status.
+ */
+ public void setOnline( boolean online ) throws XMPPException {
+ // If the online status hasn't changed, do nothing.
+ if (this.online == online) {
+ return;
+ }
+ this.online = online;
+
+ Presence presence = null;
+
+ // If the user is going online...
+ if (online) {
+ presence = new Presence(Presence.Type.AVAILABLE);
+ presence.setTo(workgroupName);
+
+ PacketCollector collector = this.connection.createPacketCollector(new AndFilter(
+ new PacketTypeFilter(Presence.class), new FromContainsFilter(workgroupName)));
+
+ connection.sendPacket(presence);
+
+ presence = (Presence)collector.nextResult(5000);
+ collector.cancel();
+ if (presence == null) {
+ throw new XMPPException("No response from server on status set.");
+ }
+
+ if (presence.getError() != null) {
+ throw new XMPPException(presence.getError());
+ }
+ }
+ // Otherwise the user is going offline...
+ else {
+ presence = new Presence(Presence.Type.UNAVAILABLE);
+ presence.setTo(workgroupName);
+ connection.sendPacket(presence);
+ }
+ }
+
+ /**
+ * Sets the agent's current status with the workgroup. The presence mode affects how offers
+ * are routed to the agent. The possible presence modes with their meanings are as follows:
+ * + * The max chats value is the maximum number of chats the agent is willing to have routed to + * them at once. Some servers may be configured to only accept max chat values in a certain + * range; for example, between two and five. In that case, the maxChats value the agent sends + * may be adjusted by the server to a value within that range. + * + * @param presenceMode the presence mode of the agent. + * @param currentChats the current number of chats the agent is in. + * @param maxChats the maximum number of chats the agent is willing to accept. + * @throws XMPPException if an error occurs setting the agent status. + * @throws IllegalStateException if the agent is not online with the workgroup. + */ + public void setStatus(Presence.Mode presenceMode, int currentChats, int maxChats ) + throws XMPPException + { + setStatus( presenceMode, currentChats, maxChats, null ); + } + + + /** + * Sets the agent's current status with the workgroup. The presence mode affects how offers + * are routed to the agent. The possible presence modes with their meanings are as follows:
+ *
+ * The max chats value is the maximum number of chats the agent is willing to have routed to
+ * them at once. Some servers may be configured to only accept max chat values in a certain
+ * range; for example, between two and five. In that case, the maxChats value the agent sends
+ * may be adjusted by the server to a value within that range.
+ *
+ * @param presenceMode the presence mode of the agent.
+ * @param currentChats the current number of chats the agent is in.
+ * @param maxChats the maximum number of chats the agent is willing to accept.
+ * @param status sets the status message of the presence update.
+ * @throws XMPPException if an error occurs setting the agent status.
+ * @throws IllegalStateException if the agent is not online with the workgroup.
+ */
+ public void setStatus(Presence.Mode presenceMode, int currentChats, int maxChats, String status )
+ throws XMPPException
+ {
+ if (!online) {
+ throw new IllegalStateException("Cannot set status when the agent is not online.");
+ }
+
+ if (presenceMode == null) {
+ presenceMode = Presence.Mode.AVAILABLE;
+ }
+ this.presenceMode = presenceMode;
+ this.currentChats = currentChats;
+ this.maxChats = maxChats;
+
+ Presence presence = new Presence(Presence.Type.AVAILABLE);
+ presence.setMode(presenceMode);
+ presence.setTo(this.getWorkgroupName());
+
+ if( status != null ) {
+ presence.setStatus( status );
+ }
+ // Send information about max chats and current chats as a packet extension.
+ DefaultPacketExtension agentStatus = new DefaultPacketExtension(AgentStatus.ELEMENT_NAME,
+ AgentStatus.NAMESPACE);
+ agentStatus.setValue("current-chats", ""+currentChats);
+ agentStatus.setValue("max-chats", ""+maxChats);
+ presence.addExtension(agentStatus);
+ presence.addExtension(new MetaData(this.metaData));
+
+ PacketCollector collector = this.connection.createPacketCollector(new AndFilter(
+ new PacketTypeFilter(Presence.class), new FromContainsFilter(workgroupName)));
+
+ this.connection.sendPacket(presence);
+
+ presence = (Presence)collector.nextResult(5000);
+ collector.cancel();
+ if (presence == null) {
+ throw new XMPPException("No response from server on status set.");
+ }
+
+ if (presence.getError() != null) {
+ throw new XMPPException(presence.getError());
+ }
+ }
+
+ /**
+ * Removes a user from the workgroup queue. This is an administrative action that the
+ *
+ * The agent is not guaranteed of having privileges to perform this action; an exception
+ * denying the request may be thrown.
+ */
+ public void dequeueUser(String userID) throws XMPPException {
+ // todo: this method simply won't work right now.
+ DepartQueuePacket departPacket = new DepartQueuePacket(this.workgroupName);
+
+ // PENDING
+ this.connection.sendPacket(departPacket);
+ }
+
+ /**
+ * @return the fully-qualified name of the workgroup for which this session exists
+ */
+ public String getWorkgroupName() {
+ return workgroupName;
+ }
+
+ /**
+ * @param queueName the name of the queue
+ * @return an instance of WorkgroupQueue for the argument queue name, or null if none exists
+ */
+ public WorkgroupQueue getQueue(String queueName) {
+ return (WorkgroupQueue)queues.get(queueName);
+ }
+
+ public Iterator getQueues() {
+ return Collections.unmodifiableMap((new HashMap(queues))).values().iterator();
+ }
+
+ public void addQueueUsersListener(QueueUsersListener listener) {
+ synchronized(queueUsersListeners) {
+ if (!queueUsersListeners.contains(listener)) {
+ queueUsersListeners.add(listener);
+ }
+ }
+ }
+
+ public void removeQueueUsersListener(QueueUsersListener listener) {
+ synchronized(queueUsersListeners) {
+ queueUsersListeners.remove(listener);
+ }
+ }
+
+ public void addQueueAgentsListener(QueueAgentsListener listener) {
+ synchronized(queueAgentsListeners) {
+ if (!queueAgentsListeners.contains(listener)) {
+ queueAgentsListeners.add(listener);
+ }
+ }
+ }
+
+ public void removeQueueAgentsListener(QueueAgentsListener listener) {
+ synchronized(queueAgentsListeners) {
+ queueAgentsListeners.remove(listener);
+ }
+ }
+
+ /**
+ * Adds an offer listener.
+ *
+ * @param offerListener the offer listener.
+ */
+ public void addOfferListener(OfferListener offerListener) {
+ synchronized(offerListeners) {
+ if (!offerListeners.contains(offerListener)) {
+ offerListeners.add(offerListener);
+ }
+ }
+ }
+
+ /**
+ * Removes an offer listener.
+ *
+ * @param offerListener the offer listener.
+ */
+ public void removeOfferListener(OfferListener offerListener) {
+ synchronized(offerListeners) {
+ offerListeners.remove(offerListener);
+ }
+ }
+
+ /**
+ * Adds an invitation listener.
+ *
+ * @param invitationListener the invitation listener.
+ */
+ public void addInvitationListener(InvitationListener invitationListener) {
+ synchronized(invitationListeners) {
+ if (!invitationListeners.contains(invitationListener)) {
+ invitationListeners.add(invitationListener);
+ }
+ }
+ }
+
+ /**
+ * Removes an invitation listener.
+ *
+ * @param invitationListener the invitation listener.
+ */
+ public void removeInvitationListener(InvitationListener invitationListener) {
+ synchronized(invitationListeners) {
+ offerListeners.remove(invitationListener);
+ }
+ }
+
+ private void fireOfferRequestEvent(OfferRequestProvider.OfferRequestPacket requestPacket) {
+ Offer offer = new Offer(this.connection, this, requestPacket.getUserID(),
+ this.getWorkgroupName(),
+ new Date((new Date()).getTime()
+ + (requestPacket.getTimeout() * 1000)),
+ requestPacket.getSessionID(), requestPacket.getMetaData());
+
+ synchronized (offerListeners) {
+ for (Iterator i=offerListeners.iterator(); i.hasNext(); ) {
+ OfferListener listener = (OfferListener)i.next();
+ listener.offerReceived(offer);
+ }
+ }
+ }
+
+ private void fireOfferRevokeEvent(OfferRevokeProvider.OfferRevokePacket orp) {
+ RevokedOffer revokedOffer = new RevokedOffer(orp.getUserID(), this.getWorkgroupName(),
+ orp.getSessionID(), orp.getReason(),
+ new Date());
+
+ synchronized (offerListeners) {
+ for (Iterator i=offerListeners.iterator(); i.hasNext(); ) {
+ OfferListener listener = (OfferListener)i.next();
+ listener.offerRevoked(revokedOffer);
+ }
+ }
+ }
+
+ private void fireInvitationEvent(String groupChatJID, String sessionID, String body,
+ String from, Map metaData)
+ {
+ Invitation invitation = new Invitation(connection.getUser(), groupChatJID,
+ workgroupName, sessionID, body, from, metaData);
+
+ synchronized(invitationListeners) {
+ for (Iterator i=invitationListeners.iterator(); i.hasNext(); ) {
+ InvitationListener listener = (InvitationListener)i.next();
+ listener.invitationReceived(invitation);
+ }
+ }
+ }
+
+ private void fireQueueUsersEvent(WorkgroupQueue queue, WorkgroupQueue.Status status,
+ int averageWaitTime, Date oldestEntry, Set users)
+ {
+ synchronized(queueUsersListeners) {
+ for (Iterator i=queueUsersListeners.iterator(); i.hasNext(); ) {
+ QueueUsersListener listener = (QueueUsersListener)i.next();
+ if (status != null) {
+ listener.statusUpdated(queue, status);
+ }
+ if (averageWaitTime != -1) {
+ listener.averageWaitTimeUpdated(queue, averageWaitTime);
+ }
+ if (oldestEntry != null) {
+ listener.oldestEntryUpdated(queue, oldestEntry);
+ }
+ if (users != null) {
+ listener.usersUpdated(queue, users);
+ }
+ }
+ }
+ }
+
+ private void fireQueueAgentsEvent(WorkgroupQueue queue, int currentChats,
+ int maxChats, Set agents)
+ {
+ synchronized(queueAgentsListeners) {
+ for (Iterator i=queueAgentsListeners.iterator(); i.hasNext(); ) {
+ QueueAgentsListener listener = (QueueAgentsListener)i.next();
+ if (currentChats != -1) {
+ listener.currentChatsUpdated(queue, currentChats);
+ }
+ if (maxChats != -1) {
+ listener.maxChatsUpdated(queue, maxChats);
+ }
+ if (agents != null) {
+ listener.agentsUpdated(queue, agents);
+ }
+ }
+ }
+ }
+
+ // PacketListener Implementation.
+
+ private void handlePacket(Packet packet) {
+ if (packet instanceof OfferRequestProvider.OfferRequestPacket) {
+ fireOfferRequestEvent((OfferRequestProvider.OfferRequestPacket)packet);
+ }
+ else if (packet instanceof Presence) {
+ Presence presence = (Presence)packet;
+
+ // The workgroup can send us a number of different presence packets. We
+ // check for different packet extensions to see what type of presence
+ // packet it is.
+
+ String queueName = StringUtils.parseResource(presence.getFrom());
+ WorkgroupQueue queue = (WorkgroupQueue)queues.get(queueName);
+ // If there isn't already an entry for the queue, create a new one.
+ if (queue == null) {
+ queue = new WorkgroupQueue(queueName);
+ queues.put(queueName, queue);
+ }
+
+ // QueueOverview packet extensions contain basic information about a queue.
+ QueueOverview queueOverview = (QueueOverview)presence.getExtension(
+ QueueOverview.ELEMENT_NAME, QueueOverview.NAMESPACE);
+ if (queueOverview != null) {
+ if (queueOverview.getStatus() == null) {
+ queue.setStatus(WorkgroupQueue.Status.CLOSED);
+ }
+ else {
+ queue.setStatus(queueOverview.getStatus());
+ }
+ queue.setAverageWaitTime(queueOverview.getAverageWaitTime());
+ queue.setOldestEntry(queueOverview.getOldestEntry());
+ // Fire event.
+ fireQueueUsersEvent(queue, queueOverview.getStatus(),
+ queueOverview.getAverageWaitTime(), queueOverview.getOldestEntry(),
+ null);
+ return;
+ }
+
+ // QueueDetails packet extensions contain information about the users in
+ // a queue.
+ QueueDetails queueDetails = (QueueDetails)packet.getExtension(
+ QueueDetails.ELEMENT_NAME, QueueDetails.NAMESPACE);
+ if (queueDetails != null) {
+ queue.setUsers(queueDetails.getUsers());
+ // Fire event.
+ fireQueueUsersEvent(queue, null, -1, null, queueDetails.getUsers());
+ return;
+ }
+
+ // Notify agent packets gives an overview of agent activity in a queue.
+ DefaultPacketExtension notifyAgents = (DefaultPacketExtension)presence.getExtension(
+ "notify-agents", "xmpp:workgroup");
+ if (notifyAgents != null) {
+ int currentChats = Integer.parseInt(notifyAgents.getValue("current-chats"));
+ int maxChats = Integer.parseInt(notifyAgents.getValue("max-chats"));
+ queue.setCurrentChats(currentChats);
+ queue.setMaxChats(maxChats);
+ // Fire event.
+ fireQueueAgentsEvent(queue, currentChats, maxChats, null);
+ return;
+ }
+
+ // Agent status
+ AgentStatus agentStatus = (AgentStatus)presence.getExtension(AgentStatus.ELEMENT_NAME,
+ AgentStatus.NAMESPACE);
+ if (agentStatus != null) {
+ Set agents = agentStatus.getAgents();
+ // Look for information about the agent that created this session and
+ // update local status fields accordingly.
+ for (Iterator i=agents.iterator(); i.hasNext(); ) {
+ Agent agent = (Agent)i.next();
+ if (agent.getUser().equals(StringUtils.parseBareAddress(
+ connection.getUser())))
+ {
+ maxChats = agent.getMaxChats();
+ currentChats = agent.getCurrentChats();
+ }
+ }
+ // Set the list of agents for the queue.
+ queue.setAgents(agents);
+ // Fire event.
+ fireQueueAgentsEvent(queue, -1, -1, agentStatus.getAgents());
+ return;
+ }
+ }
+ else if (packet instanceof Message) {
+ Message message = (Message)packet;
+
+ GroupChatInvitation invitation = (GroupChatInvitation)message.getExtension(
+ GroupChatInvitation.ELEMENT_NAME, GroupChatInvitation.NAMESPACE);
+
+ if (invitation != null) {
+ String roomAddress = invitation.getRoomAddress();
+ String sessionID = null;
+ Map metaData = null;
+
+ SessionID sessionIDExt = (SessionID)message.getExtension(SessionID.ELEMENT_NAME,
+ SessionID.NAMESPACE);
+ if (sessionIDExt != null) {
+ sessionID = sessionIDExt.getSessionID();
+ }
+
+ MetaData metaDataExt = (MetaData)message.getExtension(MetaData.ELEMENT_NAME,
+ MetaData.NAMESPACE);
+ if (metaDataExt != null) {
+ metaData = metaDataExt.getMetaData();
+ }
+
+ this.fireInvitationEvent(roomAddress, sessionID, message.getBody(),
+ message.getFrom(), metaData);
+ }
+ }
+ else if (packet instanceof OfferRevokeProvider.OfferRevokePacket) {
+ fireOfferRevokeEvent((OfferRevokeProvider.OfferRevokePacket)packet);
+ }
+ }
+}
\ No newline at end of file
diff --git a/source/org/jivesoftware/smackx/workgroup/agent/Offer.java b/source/org/jivesoftware/smackx/workgroup/agent/Offer.java
new file mode 100644
index 000000000..d0c220579
--- /dev/null
+++ b/source/org/jivesoftware/smackx/workgroup/agent/Offer.java
@@ -0,0 +1,163 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright (C) 1999-2003 Jive Software. All rights reserved.
+ *
+ * This software is the proprietary information of Jive Software.
+ * Use is subject to license terms.
+ */
+
+package org.jivesoftware.smackx.workgroup.agent;
+
+import java.util.Date;
+import java.util.Map;
+
+import org.jivesoftware.smack.XMPPConnection;
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.packet.Packet;
+
+/**
+ * A class embodying the semantic agent chat offer; specific instances allow the acceptance or
+ * rejecting of the offer.
+ *
+ * @author Matt Tucker
+ * @author loki der quaeler
+ * @author Derek DeMoro
+ */
+public class Offer {
+ private XMPPConnection connection;
+ private AgentSession session;
+
+ private String sessionID;
+ private String userID;
+ private String workgroupName;
+ private Date expiresDate;
+ private Map metaData;
+
+ /**
+ * Creates a new offer.
+ *
+ * @param conn the XMPP connection with which the issuing session was created.
+ * @param agentSession the agent session instance through which this offer was issued.
+ * @param userID the XMPP address of the user from which the offer originates.
+ * @param workgroupName the fully qualified name of the workgroup.
+ * @param expiresDate the date at which this offer expires.
+ * @param sessionID the session id associated with the offer.
+ * @param metaData the metadata associated with the offer.
+ */
+ public Offer( XMPPConnection conn, AgentSession agentSession, String userID,
+ String workgroupName, Date expiresDate,
+ String sessionID, Map metaData ) {
+ this.connection = conn;
+ this.session = agentSession;
+ this.userID = userID;
+ this.workgroupName = workgroupName;
+ this.expiresDate = expiresDate;
+ this.sessionID = sessionID;
+ this.metaData = metaData;
+ }
+
+ /**
+ * Accepts the offer.
+ */
+ public void accept() {
+ Packet acceptPacket = new AcceptPacket( this.session.getWorkgroupName() );
+ connection.sendPacket( acceptPacket );
+ // TODO: listen for a reply.
+ }
+
+ /**
+ * Rejects the offer.
+ */
+ public void reject() {
+ RejectPacket rejectPacket = new RejectPacket( this.session.getWorkgroupName() );
+ connection.sendPacket( rejectPacket );
+ // TODO: listen for a reply.
+ }
+
+ /**
+ * Returns the XMPP address of the user from which the offer originates
+ * (eg jsmith@example.com/WebClient). For example, if the user jsmith initiates
+ * a support request by joining the workgroup queue, then this user ID will be
+ * jsmith's address.
+ *
+ * @return the XMPP address of the user from which the offer originates.
+ */
+ public String getUserID() {
+ return userID;
+ }
+
+ /**
+ * The fully qualified name of the workgroup (eg support@example.com).
+ *
+ * @return the name of the workgroup.
+ */
+ public String getWorkgroupName() {
+ return this.workgroupName;
+ }
+
+ /**
+ * The date when the offer will expire. The agent must {@link #accept()}
+ * the offer before the expiration date or the offer will lapse and be
+ * routed to another agent. Alternatively, the agent can {@link #reject()}
+ * the offer at any time if they don't wish to accept it..
+ *
+ * @return the date at which this offer expires.
+ */
+ public Date getExpiresDate() {
+ return this.expiresDate;
+ }
+
+ /**
+ * The session ID associated with the offer.
+ *
+ * @return the session id associated with the offer.
+ */
+ public String getSessionID() {
+ return this.sessionID;
+ }
+
+ /**
+ * The meta-data associated with the offer.
+ *
+ * @return the offer meta-data.
+ */
+ public Map getMetaData() {
+ return this.metaData;
+ }
+
+ /**
+ * Packet for rejecting offers.
+ */
+ private class RejectPacket extends IQ {
+
+ RejectPacket( String workgroup ) {
+ this.setTo( workgroup );
+ this.setType( IQ.Type.SET );
+ }
+
+ public String getChildElementXML() {
+ return "
+ *
+ * @author Matt Tucker
+ * @author loki der quaeler
+ * @see org.jivesoftware.smackx.workgroup.agent.AgentSession
+ */
+public interface OfferListener {
+
+ /**
+ * The implementing class instance will be notified via this when the AgentSession has received
+ * an offer for a chat. The instance will then have the ability to accept, reject, or ignore
+ * the request (resulting in a revocation-by-timeout).
+ *
+ * @param request the Offer instance embodying the details of the offer
+ */
+ public void offerReceived (Offer request);
+
+ /**
+ * The implementing class instance will be notified via this when the AgentSessino has received
+ * a revocation of a previously extended offer.
+ *
+ * @param revokedOffer the RevokedOffer instance embodying the details of the revoked offer
+ */
+ public void offerRevoked (RevokedOffer revokedOffer);
+
+}
diff --git a/source/org/jivesoftware/smackx/workgroup/agent/QueueAgentsListener.java b/source/org/jivesoftware/smackx/workgroup/agent/QueueAgentsListener.java
new file mode 100644
index 000000000..a0ee4e14e
--- /dev/null
+++ b/source/org/jivesoftware/smackx/workgroup/agent/QueueAgentsListener.java
@@ -0,0 +1,30 @@
+package org.jivesoftware.smackx.workgroup.agent;
+
+import java.util.Set;
+
+public interface QueueAgentsListener {
+
+ /**
+ * The current number of chats the agents are handling was updated.
+ *
+ * @param queue the workgroup queue.
+ * @param currentChats the current number of chats the agents are handling.
+ */
+ public void currentChatsUpdated(WorkgroupQueue queue, int currentChats);
+
+ /**
+ * The maximum number of chats the agents can handle was updated.
+ *
+ * @param queue the workgroup queue.
+ * @param maxChats the maximum number of chats the agents can handle.
+ */
+ public void maxChatsUpdated(WorkgroupQueue queue, int maxChats);
+
+ /**
+ * The list of available agents servicing the queue was updated.
+ *
+ * @param queue the workgroup queue.
+ * @param agents the available agents servicing the queue.
+ */
+ public void agentsUpdated(WorkgroupQueue queue, Set agents);
+}
\ No newline at end of file
diff --git a/source/org/jivesoftware/smackx/workgroup/agent/QueueUsersListener.java b/source/org/jivesoftware/smackx/workgroup/agent/QueueUsersListener.java
new file mode 100644
index 000000000..ce1a53fb7
--- /dev/null
+++ b/source/org/jivesoftware/smackx/workgroup/agent/QueueUsersListener.java
@@ -0,0 +1,39 @@
+package org.jivesoftware.smackx.workgroup.agent;
+
+import java.util.Date;
+import java.util.Set;
+
+public interface QueueUsersListener {
+
+ /**
+ * The status of the queue was updated.
+ *
+ * @param queue the workgroup queue.
+ * @param status the status of queue.
+ */
+ public void statusUpdated(WorkgroupQueue queue, WorkgroupQueue.Status status);
+
+ /**
+ * The average wait time of the queue was updated.
+ *
+ * @param queue the workgroup queue.
+ * @param averageWaitTime the average wait time of the queue.
+ */
+ public void averageWaitTimeUpdated(WorkgroupQueue queue, int averageWaitTime);
+
+ /**
+ * The date of oldest entry waiting in the queue was updated.
+ *
+ * @param queue the workgroup queue.
+ * @param oldestEntry the date of the oldest entry waiting in the queue.
+ */
+ public void oldestEntryUpdated(WorkgroupQueue queue, Date oldestEntry);
+
+ /**
+ * The list of users waiting in the queue was updated.
+ *
+ * @param queue the workgroup queue.
+ * @param users the list of users waiting in the queue.
+ */
+ public void usersUpdated(WorkgroupQueue queue, Set users);
+}
\ No newline at end of file
diff --git a/source/org/jivesoftware/smackx/workgroup/agent/RevokedOffer.java b/source/org/jivesoftware/smackx/workgroup/agent/RevokedOffer.java
new file mode 100644
index 000000000..d6b6023fc
--- /dev/null
+++ b/source/org/jivesoftware/smackx/workgroup/agent/RevokedOffer.java
@@ -0,0 +1,81 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright (C) 1999-2003 Jive Software. All rights reserved.
+ *
+ * This software is the proprietary information of Jive Software.
+ * Use is subject to license terms.
+ */
+
+package org.jivesoftware.smackx.workgroup.agent;
+
+import java.util.Date;
+
+/**
+ * An immutable simple class to embody the information concerning a revoked offer, this is namely
+ * the reason, the workgroup, the userJID, and the timestamp which the message was received.
+ *
+ * @author loki der quaeler
+ */
+public class RevokedOffer {
+
+ protected String userID;
+ protected String workgroupName;
+ protected String sessionID;
+ protected String reason;
+ protected Date timestamp;
+
+ /**
+ * @param uid the jid of the user for which this revocation was issued
+ * @param wg the fully qualified name of the workgroup
+ * @param sid the session id attributed to this chain of packets
+ * @param cause the server issued message as to why this revocation was issued
+ * @param ts the timestamp at which the revocation was issued
+ */
+ public RevokedOffer (String uid, String wg, String sid, String cause, Date ts) {
+ super();
+
+ this.userID = uid;
+ this.workgroupName = wg;
+ this.sessionID = sid;
+ this.reason = cause;
+ this.timestamp = ts;
+ }
+
+ /**
+ * @return the jid of the user for which this revocation was issued
+ */
+ public String getUserID () {
+ return this.userID;
+ }
+
+ /**
+ * @return the fully qualified name of the workgroup
+ */
+ public String getWorkgroupName () {
+ return this.workgroupName;
+ }
+
+ /**
+ * @return the session id which will associate all packets for the pending chat
+ */
+ public String getSessionID() {
+ return this.sessionID;
+ }
+
+ /**
+ * @return the server issued message as to why this revocation was issued
+ */
+ public String getReason () {
+ return this.reason;
+ }
+
+ /**
+ * @return the timestamp at which the revocation was issued
+ */
+ public Date getTimestamp () {
+ return this.timestamp;
+ }
+}
\ No newline at end of file
diff --git a/source/org/jivesoftware/smackx/workgroup/agent/WorkgroupQueue.java b/source/org/jivesoftware/smackx/workgroup/agent/WorkgroupQueue.java
new file mode 100644
index 000000000..ec068da14
--- /dev/null
+++ b/source/org/jivesoftware/smackx/workgroup/agent/WorkgroupQueue.java
@@ -0,0 +1,239 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright (C) 1999-2003 Jive Software. All rights reserved.
+ *
+ * This software is the proprietary information of Jive Software.
+ * Use is subject to license terms.
+ */
+
+package org.jivesoftware.smackx.workgroup.agent;
+
+import java.util.*;
+
+/**
+ * A queue in a workgroup, which is a pool of agents that are routed a specific type of
+ * chat request.
+ */
+public class WorkgroupQueue {
+
+ private String name;
+ private Status status = Status.CLOSED;
+
+ private int averageWaitTime = -1;
+ private Date oldestEntry = null;
+ private Set users = Collections.EMPTY_SET;
+
+ private Set agents = Collections.EMPTY_SET;
+ private int maxChats = 0;
+ private int currentChats = 0;
+
+ /**
+ * Creates a new workgroup queue instance.
+ *
+ * @param name the name of the queue.
+ */
+ WorkgroupQueue(String name) {
+ this.name = name;
+ }
+
+ /**
+ * Returns the name of the queue.
+ *
+ * @return the name of the queue.
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Returns the status of the queue.
+ *
+ * @return the status of the queue.
+ */
+ public Status getStatus() {
+ return status;
+ }
+
+ void setStatus(Status status) {
+ this.status = status;
+ }
+
+ /**
+ * Returns the number of users waiting in the queue waiting to be routed to
+ * an agent.
+ *
+ * @return the number of users waiting in the queue.
+ */
+ public int getUserCount() {
+ if (users == null) {
+ return 0;
+ }
+ return users.size();
+ }
+
+ /**
+ * Returns an Iterator for the users in the queue waiting to be routed to
+ * an agent (QueueUser instances).
+ *
+ * @return an Iterator for the users waiting in the queue.
+ */
+ public Iterator getUsers() {
+ if (users == null) {
+ return Collections.EMPTY_SET.iterator();
+ }
+ return Collections.unmodifiableSet(users).iterator();
+ }
+
+ void setUsers(Set users) {
+ this.users = users;
+ }
+
+ /**
+ * Returns the average amount of time users wait in the queue before being
+ * routed to an agent. If average wait time info isn't available, -1 will
+ * be returned.
+ *
+ * @return the average wait time
+ */
+ public int getAverageWaitTime() {
+ return averageWaitTime;
+ }
+
+ void setAverageWaitTime(int averageTime) {
+ this.averageWaitTime = averageTime;
+ }
+
+ /**
+ * Returns the date of the oldest request waiting in the queue. If there
+ * are no requests waiting to be routed, this method will return null.
+ *
+ * @return the date of the oldest request in the queue.
+ */
+ public Date getOldestEntry() {
+ return oldestEntry;
+ }
+
+ void setOldestEntry(Date oldestEntry) {
+ this.oldestEntry = oldestEntry;
+ }
+
+ /**
+ * Returns the count of the currently available agents in the queue.
+ *
+ * @return the number of active agents in the queue.
+ */
+ public int getAgentCount() {
+ synchronized (agents) {
+ return agents.size();
+ }
+ }
+
+ /**
+ * Returns an Iterator the currently active agents (Agent instances).
+ *
+ * @return an Iterator for the active agents.
+ */
+ public Iterator getAgents() {
+ return Collections.unmodifiableSet(agents).iterator();
+ }
+
+ void setAgents(Set agents) {
+ this.agents = agents;
+ }
+
+ /**
+ * Returns the maximum number of simultaneous chats the queue can handle.
+ *
+ * @return the max number of chats the queue can handle.
+ */
+ public int getMaxChats() {
+ return maxChats;
+ }
+
+ void setMaxChats(int maxChats) {
+ this.maxChats = maxChats;
+ }
+
+ /**
+ * Returns the current number of active chat sessions in the queue.
+ *
+ * @return the current number of active chat sessions in the queue.
+ */
+ public int getCurrentChats() {
+ return currentChats;
+ }
+
+ void setCurrentChats(int currentChats) {
+ this.currentChats = currentChats;
+ }
+
+ /**
+ * A class to represent the status of the workgroup. The possible values are:
+ *
+ *
+ * + * This class only provides a user's perspective into a workgroup and is not intended + * for use by agents. + * + * @author Matt Tucker + * @author loki der quaeler + */ +public class Workgroup { + + private String workgroupName; + private XMPPConnection connection; + private boolean inQueue; + private List invitationListeners; + private List queueListeners; + + private int queuePosition = -1; + private int queueRemainingTime = -1; + + /** + * Creates a new workgroup instance using the specified workgroup name + * (eg support@example.com) and XMPP connection. The connection must have + * undergone a successful login before being used to construct an instance of + * this class. + * + * @param workgroupName the fully qualified name of the workgroup. + * @param connection an XMPP connection which must have already undergone a + * successful login. + */ + public Workgroup(String workgroupName, XMPPConnection connection) { + // Login must have been done before passing in connection. + if (!connection.isAuthenticated()) { + throw new IllegalStateException("Must login to server before creating workgroup."); + } + + this.workgroupName = workgroupName; + this.connection = connection; + inQueue = false; + invitationListeners = new ArrayList(); + queueListeners = new ArrayList(); + + // Register as a queue listener for internal usage by this instance. + addQueueListener(new QueueListener() { + public void joinedQueue() { + inQueue = true; + } + + public void departedQueue() { + inQueue = false; + queuePosition = -1; + queueRemainingTime = -1; + } + + public void queuePositionUpdated(int currentPosition) { + queuePosition = currentPosition; + } + + public void queueWaitTimeUpdated(int secondsRemaining) { + queueRemainingTime = secondsRemaining; + } + }); + + // Register an invitation listener for internal usage by this instance. + addInvitationListener(new InvitationListener() { + public void invitationReceived(Invitation invitation) { + inQueue = false; + queuePosition = -1; + queueRemainingTime = -1; + } + }); + + // Register a packet listener for all queue events. + PacketFilter orFilter = new OrFilter(new PacketTypeFilter(Message.class), + new PacketTypeFilter(QueueUpdate.class)); + + PacketFilter filter = new AndFilter(new FromContainsFilter(this.workgroupName), orFilter); + + connection.addPacketListener(new PacketListener() { + public void processPacket(Packet packet) { + handlePacket(packet); + } + }, filter); + } + + /** + * Returns the name of this workgroup (eg support@example.com). + * + * @return the name of the workgroup. + */ + public String getWorkgroupName() { + return workgroupName; + } + + /** + * Returns true if the user is currently waiting in the workgroup queue. + * + * @return true if currently waiting in the queue. + */ + public boolean isInQueue() { + return inQueue; + } + + /** + * Returns the user's current position in the workgroup queue. A value of 0 means + * the user is next in line to be routed; therefore, if the queue position + * is being displayed to the end user it is usually a good idea to add 1 to + * the value this method returns before display. If the user is not currently + * waiting in the workgorup, or no queue position information is available, -1 + * will be returned. + * + * @return the user's current position in the workgorup queue, or -1 if the + * position isn't available or if the user isn't in the queue. + */ + public int getQueuePosition() { + return queuePosition; + } + + /** + * Returns the estimated time (in seconds) that the user has to left wait in + * the workgroup queue before being routed. If the user is not currently waiting + * int he workgroup, or no queue time information is available, -1 will be + * returned. + * + * @return the estimated time remaining (in seconds) that the user has to + * wait in the workgropu queue, or -1 if time information isn't available + * or if the user isn't int the queue. + */ + public int getQueueRemainingTime() { + return queueRemainingTime; + } + + /** + * Joins the workgroup queue to wait to be routed to an agent. After joining + * the queue, queue status events will be sent to indicate the user's position and + * estimated time left in the queue. Once joining the queue, there are three ways + * the user will leave the queue:
+ * + * Some servers may be configured to require certain meta-data in + * order to join the queue. In that case, the {@link #joinQueue(Map)} method + * should be used instead of this method so that meta-data may be passed in. + * + * @throws XMPPException if an error occured joining the queue. An error may indicate + * that a connection failure occured or that the server explicitly rejected the + * request to join the queue. + */ + public void joinQueue() throws XMPPException { + joinQueue(null); + } + + /** + * Joins the workgroup queue to wait to be routed to an agent. After joining + * the queue, queue status events will be sent to indicate the user's position and + * estimated time left in the queue. Once joining the queue, there are three ways + * the user will leave the queue:
+ * + * Arbitrary meta-data can be passed in with the queue join request in order to assist + * the server in routing the user to an agent and to provide information about the + * user to the agent. Some servers may be configured to require certain meta-data in + * order to join the queue.
+ * + * The server may reject the join queue request, which will cause an XMPPException to + * be thrown. The error codes for specific cases are as follows:
+ *
+ * Normally, the user would not manually leave the queue. However, they may wish to
+ * under certain circumstances -- for example, if they no longer wish to be routed
+ * to an agent because they've been waiting too long.
+ *
+ * @throws XMPPException if an error occured trying to send the depart queue
+ * request to the server.
+ */
+ public void departQueue() throws XMPPException {
+ // If not in the queue ignore the depart request.
+ if (!inQueue) {
+ return;
+ }
+
+ DepartQueuePacket departPacket = new DepartQueuePacket(this.workgroupName);
+ PacketCollector collector = this.connection.createPacketCollector(
+ new PacketIDFilter(departPacket.getPacketID()));
+
+ connection.sendPacket(departPacket);
+
+ IQ response = (IQ)collector.nextResult(5000);
+ collector.cancel();
+ if (response == null) {
+ throw new XMPPException("No response from the server.");
+ }
+ if (response.getError() != null) {
+ throw new XMPPException(response.getError());
+ }
+
+ // Notify listeners that we're no longer in the queue.
+ fireQueueDepartedEvent();
+ }
+
+ /**
+ * Adds a queue listener that will be notified of queue events for the user
+ * that created this Workgroup instance.
+ *
+ * @param queueListener the queue listener.
+ */
+ public void addQueueListener(QueueListener queueListener) {
+ synchronized(queueListeners) {
+ if (!queueListeners.contains(queueListener)) {
+ queueListeners.add(queueListener);
+ }
+ }
+ }
+
+ /**
+ * Removes a queue listener.
+ *
+ * @param queueListener the queue listener.
+ */
+ public void removeQueueListener(QueueListener queueListener) {
+ synchronized(queueListeners) {
+ queueListeners.remove(queueListener);
+ }
+ }
+
+ /**
+ * Adds an invitation listener that will be notified of groupchat invitations
+ * from the workgroup for the the user that created this Workgroup instance.
+ *
+ * @param invitationListener the invitation listener.
+ */
+ public void addInvitationListener(InvitationListener invitationListener) {
+ synchronized(invitationListeners) {
+ if (!invitationListeners.contains(invitationListener)) {
+ invitationListeners.add(invitationListener);
+ }
+ }
+ }
+
+ /**
+ * Removes an invitation listener.
+ *
+ * @param invitationListener the invitation listener.
+ */
+ public void removeQueueListener(InvitationListener invitationListener) {
+ synchronized(invitationListeners) {
+ invitationListeners.remove(invitationListener);
+ }
+ }
+
+ private void fireInvitationEvent(Invitation invitation) {
+ synchronized (invitationListeners) {
+ for (Iterator i=invitationListeners.iterator(); i.hasNext(); ) {
+ InvitationListener listener = (InvitationListener)i.next();
+ listener.invitationReceived(invitation);
+ }
+ }
+ }
+
+ private void fireQueueJoinedEvent() {
+ synchronized (queueListeners) {
+ for (Iterator i=queueListeners.iterator(); i.hasNext(); ) {
+ QueueListener listener = (QueueListener)i.next();
+ listener.joinedQueue();
+ }
+ }
+ }
+
+ private void fireQueueDepartedEvent() {
+ synchronized (queueListeners) {
+ for (Iterator i=queueListeners.iterator(); i.hasNext(); ) {
+ QueueListener listener = (QueueListener)i.next();
+ listener.departedQueue();
+ }
+ }
+ }
+
+ private void fireQueuePositionEvent(int currentPosition) {
+ synchronized (queueListeners) {
+ for (Iterator i=queueListeners.iterator(); i.hasNext(); ) {
+ QueueListener listener = (QueueListener)i.next();
+ listener.queuePositionUpdated(currentPosition);
+ }
+ }
+ }
+
+ private void fireQueueTimeEvent(int secondsRemaining) {
+ synchronized (queueListeners) {
+ for (Iterator i=queueListeners.iterator(); i.hasNext(); ) {
+ QueueListener listener = (QueueListener)i.next();
+ listener.queueWaitTimeUpdated(secondsRemaining);
+ }
+ }
+ }
+
+ // PacketListener Implementation.
+
+ private void handlePacket(Packet packet) {
+ if (packet instanceof Message) {
+ Message msg = (Message)packet;
+ // Check to see if the user left the queue.
+ PacketExtension pe = msg.getExtension("depart-queue", "xmpp:workgroup");
+
+ if (pe != null) {
+ fireQueueDepartedEvent();
+ }
+ else {
+ // Check to see if the user has been invited to a chat.
+ GroupChatInvitation invitation = (GroupChatInvitation)msg.getExtension(
+ "x", "jabber:x:conference");
+
+ if (invitation != null) {
+ String roomAddress = invitation.getRoomAddress();
+ String sessionID = null;
+ Map metaData = null;
+
+ pe = msg.getExtension(SessionID.ELEMENT_NAME,
+ SessionID.NAMESPACE);
+ if (pe != null) {
+ sessionID = ((SessionID)pe).getSessionID();
+ }
+
+ pe = msg.getExtension(MetaData.ELEMENT_NAME,
+ MetaData.NAMESPACE);
+ if (pe != null) {
+ metaData = ((MetaData)pe).getMetaData();
+ }
+
+ Invitation inv = new Invitation(connection.getUser(), roomAddress,
+ workgroupName, sessionID, msg.getBody(),
+ msg.getFrom(), metaData);
+
+ fireInvitationEvent(inv);
+ }
+ }
+ }
+ // Check to see if it's a queue update notification.
+ else if (packet instanceof QueueUpdate) {
+ QueueUpdate queueUpdate = (QueueUpdate)packet;
+ if (queueUpdate.getPosition() != -1) {
+ fireQueuePositionEvent(queueUpdate.getPosition());
+ }
+ if (queueUpdate.getRemaingTime() != -1) {
+ fireQueueTimeEvent(queueUpdate.getRemaingTime());
+ }
+ }
+ }
+
+ /**
+ * IQ packet to request joining the workgroup queue.
+ */
+ private class JoinQueuePacket extends IQ {
+
+ private Map metaData;
+
+ public JoinQueuePacket(String workgroup, Map metaData) {
+ this.metaData = metaData;
+
+ setTo(workgroup);
+ setType(IQ.Type.SET);
+ }
+
+ public String getChildElementXML() {
+ StringBuffer buf = new StringBuffer();
+
+ buf.append("
+ *
+ * Also, this is intended to be used to message methods that either return void, or have
+ * a return which the developer using this class is uninterested in receiving.
+ *
+ * @author loki der quaeler
+ */
+public class ListenerEventDispatcher
+ implements Runnable {
+
+ protected transient ArrayList triplets;
+
+ protected transient boolean hasFinishedDispatching;
+ protected transient boolean isRunning;
+
+ public ListenerEventDispatcher () {
+ super();
+
+ this.triplets = new ArrayList();
+
+ this.hasFinishedDispatching = false;
+ this.isRunning = false;
+ }
+
+ /**
+ * Add a listener triplet - the instance of the listener to be messaged, the Method on which
+ * the listener should be messaged, and the Object array of arguments to be supplied to the
+ * Method. No attempts are made to determine whether this triplet was already added.
+ *
+ * Messages are dispatched in the order in which they're added via this method; so if triplet
+ * X is added after triplet Z, then triplet Z will undergo messaging prior to triplet X.
+ *
+ * This method should not be called once the owning Thread instance has been started; if it
+ * is called, the triplet will not be added to the messaging queue.
+ *
+ * @param listenerInstance the instance of the listener to receive the associated notification
+ * @param listenerMethod the Method instance representing the method through which notification
+ * will occur
+ * @param methodArguments the arguments supplied to the notification method
+ */
+ public void addListenerTriplet (Object listenerInstance, Method listenerMethod,
+ Object[] methodArguments) {
+ if (! this.isRunning) {
+ this.triplets.add(new TripletContainer(listenerInstance, listenerMethod,
+ methodArguments));
+ }
+ }
+
+ /**
+ * @return whether this instance has finished dispatching its messages
+ */
+ public boolean hasFinished () {
+ return this.hasFinishedDispatching;
+ }
+
+ /**
+ *
+ * Runnable implementation
+ *
+ */
+ public void run () {
+ ListIterator li = null;
+
+ this.isRunning = true;
+
+ li = this.triplets.listIterator();
+ while (li.hasNext()) {
+ TripletContainer tc = (TripletContainer)li.next();
+
+ try {
+ tc.getListenerMethod().invoke(tc.getListenerInstance(), tc.getMethodArguments());
+ } catch (Exception e) {
+ System.err.println("Exception dispatching an event: " + e);
+
+ e.printStackTrace();
+ }
+ }
+
+ this.hasFinishedDispatching = true;
+ }
+
+
+ protected class TripletContainer {
+
+ protected Object listenerInstance;
+ protected Method listenerMethod;
+ protected Object[] methodArguments;
+
+ protected TripletContainer (Object inst, Method meth, Object[] args) {
+ super();
+
+ this.listenerInstance = inst;
+ this.listenerMethod = meth;
+ this.methodArguments = args;
+ }
+
+ protected Object getListenerInstance () {
+ return this.listenerInstance;
+ }
+
+ protected Method getListenerMethod () {
+ return this.listenerMethod;
+ }
+
+ protected Object[] getMethodArguments () {
+ return this.methodArguments;
+ }
+
+ }
+
+}
diff --git a/source/org/jivesoftware/smackx/workgroup/util/MetaDataUtils.java b/source/org/jivesoftware/smackx/workgroup/util/MetaDataUtils.java
new file mode 100644
index 000000000..521ae9dfc
--- /dev/null
+++ b/source/org/jivesoftware/smackx/workgroup/util/MetaDataUtils.java
@@ -0,0 +1,90 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright (C) 1999-2003 Jive Software. All rights reserved.
+ *
+ * This software is the proprietary information of Jive Software.
+ * Use is subject to license terms.
+ */
+
+package org.jivesoftware.smackx.workgroup.util;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.util.Map;
+import java.util.Iterator;
+import java.util.Hashtable;
+import java.util.Collections;
+import java.io.IOException;
+
+import org.jivesoftware.smackx.workgroup.MetaData;
+
+/**
+ * Utility class for meta-data parsing and writing.
+ *
+ * @author MattTucker
+ */
+public class MetaDataUtils {
+
+ /**
+ * Parses any available meta-data and returns it as a Map of String name/value pairs. The
+ * parser must be positioned at an opening meta-data tag, or the an empty map will be returned.
+ *
+ * @param parser the XML parser positioned at an opening meta-data tag.
+ * @return the meta-data.
+ * @throws XmlPullParserException if an error occurs while parsing the XML.
+ * @throws IOException if an error occurs while parsing the XML.
+ */
+ public static Map parseMetaData(XmlPullParser parser) throws XmlPullParserException, IOException {
+ int eventType = parser.getEventType();
+
+ // If correctly positioned on an opening meta-data tag, parse meta-data.
+ if ((eventType == XmlPullParser.START_TAG)
+ && parser.getName().equals(MetaData.ELEMENT_NAME)
+ && parser.getNamespace().equals(MetaData.NAMESPACE)) {
+ Map metaData = new Hashtable();
+
+ eventType = parser.nextTag();
+
+ // Keep parsing until we've gotten to end of meta-data.
+ while ((eventType != XmlPullParser.END_TAG)
+ || (! parser.getName().equals(MetaData.ELEMENT_NAME))) {
+ String name = parser.getAttributeValue(0);
+ String value = parser.nextText();
+
+ metaData.put(name, value);
+
+ eventType = parser.nextTag();
+ }
+
+ return metaData;
+ }
+
+ return Collections.EMPTY_MAP;
+ }
+
+ /**
+ * Serializes a Map of String name/value pairs into the meta-data XML format.
+ *
+ * @param metaData the Map of meta-data.
+ * @return the meta-data values in XML form.
+ */
+ public static String serializeMetaData(Map metaData) {
+ StringBuffer buf = new StringBuffer();
+ if (metaData != null && metaData.size() > 0) {
+ buf.append("