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 current chats value indicates how many chats the agent is currently in. Because the agent + * is responsible for reporting the current chats value to the server, this value must + * be set every time it changes.

+ * + * 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 current chats value indicates how many chats the agent is currently in. Because the agent + * is responsible for reporting the current chats value to the server, this value must + * be set every time it changes.

+ * + * 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 ""; + } + } + + /** + * Packet for accepting an offer. + */ + private class AcceptPacket extends IQ { + + AcceptPacket( String workgroup ) { + this.setTo( workgroup ); + this.setType( IQ.Type.SET ); + } + + public String getChildElementXML() { + return ""; + } + } + +} \ No newline at end of file diff --git a/source/org/jivesoftware/smackx/workgroup/agent/OfferListener.java b/source/org/jivesoftware/smackx/workgroup/agent/OfferListener.java new file mode 100644 index 000000000..5d39f597a --- /dev/null +++ b/source/org/jivesoftware/smackx/workgroup/agent/OfferListener.java @@ -0,0 +1,41 @@ +/** + * $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; + +/** + * An interface which all classes interested in hearing about chat offers associated to a particular + * AgentSession instance should implement.
+ * + * @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: + * + *

+ */ + public static class Status { + + /** + * The queue is active and accepting new chat requests. + */ + public static final Status OPEN = new Status("open"); + + /** + * The queue is active but NOT accepting new chat requests. This state might + * occur when the workgroup has closed because regular support hours have closed, + * but there are still several requests left in the queue. + */ + public static final Status ACTIVE = new Status("active"); + + /** + * The queue is NOT active and NOT accepting new chat requests. + */ + public static final Status CLOSED = new Status("closed"); + + /** + * Converts a String into the corresponding status. Valid String values + * that can be converted to a status are: "open", "active", and "closed". + * + * @param type the String value to covert. + * @return the corresponding Type. + */ + public static Status fromString(String type) { + if (type == null) { + return null; + } + type = type.toLowerCase(); + if (OPEN.toString().equals(type)) { + return OPEN; + } + else if (ACTIVE.toString().equals(type)) { + return ACTIVE; + } + else if (CLOSED.toString().equals(type)) { + return CLOSED; + } + else { + return null; + } + } + + private String value; + + private Status(String value) { + this.value = value; + } + + public String toString() { + return value; + } + } +} \ No newline at end of file diff --git a/source/org/jivesoftware/smackx/workgroup/packet/AgentStatus.java b/source/org/jivesoftware/smackx/workgroup/packet/AgentStatus.java new file mode 100644 index 000000000..7bc606215 --- /dev/null +++ b/source/org/jivesoftware/smackx/workgroup/packet/AgentStatus.java @@ -0,0 +1,390 @@ +/** + * $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.packet; + +import java.util.*; +import java.beans.PropertyDescriptor; + +import org.jivesoftware.smack.packet.*; +import org.jivesoftware.smack.provider.PacketExtensionProvider; +import org.jivesoftware.smack.provider.ProviderManager; +import org.xmlpull.v1.XmlPullParser; +import org.jivesoftware.smackx.workgroup.agent.Agent; + + +/** + * An immutable container around the basic data known about a given agent, as well as a + * PacketExtension implementor which knows how to write the agent-status packet extension. + * The packet extension implementation doesn't transmit its presence information as, due to + * the protocol design, that is to be done by the presence packet which houses this extension. + * Similarly, the agent-status packet extension transmits nothing about the agent id, nor + * the metadata.
+ * + * PENDING: feels hacky to carry around the presence packet, but it's an adequate container + * for the time being.
+ * + * @author loki der quaeler + */ +public class AgentStatus implements PacketExtension { + + /** + * Element name of the packet extension. + */ + public static final String ELEMENT_NAME = "agent-status"; + + /** + * Namespace of the packet extension. + */ + public static final String NAMESPACE = "xmpp:workgroup"; + + private Set agents; + + AgentStatus() { + agents = new HashSet(); + } + + void addAgent(Agent agent) { + synchronized (agents) { + agents.add(agent); + } + } + + public int getAgentCount() { + synchronized (agents) { + return agents.size(); + } + } + + public Set getAgents() { + synchronized (agents) { + return Collections.unmodifiableSet(agents); + } + } + + public String getElementName () { + return ELEMENT_NAME; + } + + public String getNamespace () { + return NAMESPACE; + } + + public String toXML () { + StringBuffer buf = new StringBuffer(); + + buf.append("<").append(ELEMENT_NAME).append(" xmlns=\"").append(NAMESPACE).append("\">"); + + synchronized (agents) { + for (Iterator i=agents.iterator(); i.hasNext(); ) { + Agent agent = (Agent)i.next(); + buf.append(""); + + if (agent.getCurrentChats() != -1) { + buf.append(""); + buf.append(agent.getCurrentChats()); + buf.append(""); + } + + if (agent.getMaxChats() != -1) { + buf.append("").append(agent.getMaxChats()).append(""); + } + + if (agent.getPresence() != null) { + buf.append(agent.getPresence().toXML()); + // TODO: ensure that presence.toXML method is ok, then delete code below. + /*Presence presence = agent.getPresence(); + int priority = presence.getPriority(); + Presence.Mode mode = presence.getMode(); + String status = presence.getStatus(); + + buf.append(""); + if (status != null) { + buf.append("").append(status).append(""); + } + if (priority != -1) { + buf.append("").append(priority).append(""); + } + if (mode != null && mode != Presence.Mode.AVAILABLE) { + buf.append("").append(mode).append(""); + } + buf.append("");*/ + } + + buf.append(""); + } + } + + buf.append(" "); + + return buf.toString(); + } + + /** + * Packet extension provider for AgentStatus packets. + */ + public static class Provider implements PacketExtensionProvider { + + public PacketExtension parseExtension(XmlPullParser parser) throws Exception { + AgentStatus agentStatus = new AgentStatus(); + + int eventType = parser.getEventType(); + if (eventType != XmlPullParser.START_TAG) { + throw new IllegalStateException("Parser not in proper position, or bad XML."); + } + + eventType = parser.next(); + + while ((eventType == XmlPullParser.START_TAG) + && ("agent".equals(parser.getName()))) { + String jid = null; + int currentChats = -1; + int maxChats = -1; + Presence presence = null; + + jid = parser.getAttributeValue("", "jid"); + if (jid == null) { + // throw exception + } + + eventType = parser.next(); + String elementName = parser.getName(); + while ((eventType != XmlPullParser.END_TAG) || (!"agent".equals(elementName))) { + if ("current-chats".equals(elementName)) { + currentChats = Integer.parseInt(parser.nextText()); + parser.next(); + } + else if ("max-chats".equals(elementName)) { + maxChats = Integer.parseInt(parser.nextText()); + parser.next(); + } + else if ("presence".equals(elementName)) { + presence = parsePresence(parser); + parser.next(); + } + + eventType = parser.getEventType(); + elementName = parser.getName(); + + if (eventType != XmlPullParser.END_TAG) { + // throw exception + } + } + + Agent agent = new Agent(jid, currentChats, maxChats, presence); + agentStatus.addAgent(agent); + + eventType = parser.next(); + } + + if (eventType != XmlPullParser.END_TAG) { + // throw exception -- PENDING logic verify: useless case? + } + + return agentStatus; + } + + + // Note: all methods below are copied directly from the Smack PacketReader class + // and represent all methods that are needed for presence packet parsing. + // Unfortunately, there is no elegant way to pass of presence packet parsing to + // Smack core when the presence packet context is a non-standard one such as in + // the agent-status protocol. Future Smack changes may change this situation, + // which would allow us to delete the code copy. + + /** + * Parses a presence packet. + * + * @param parser the XML parser, positioned at the start of a presence packet. + * @return an Presence object. + * @throws Exception if an exception occurs while parsing the packet. + */ + private Presence parsePresence(XmlPullParser parser) throws Exception { + Presence.Type type = Presence.Type.fromString(parser.getAttributeValue("", "type")); + + Presence presence = new Presence(type); + presence.setTo(parser.getAttributeValue("", "to")); + presence.setFrom(parser.getAttributeValue("", "from")); + presence.setPacketID(parser.getAttributeValue("", "id")); + + // Parse sub-elements + boolean done = false; + while (!done) { + int eventType = parser.next(); + if (eventType == XmlPullParser.START_TAG) { + String elementName = parser.getName(); + String namespace = parser.getNamespace(); + if (elementName.equals("status")) { + presence.setStatus(parser.nextText()); + } + else if (elementName.equals("priority")) { + try { + int priority = Integer.parseInt(parser.nextText()); + presence.setPriority(priority); + } + catch (NumberFormatException nfe) { } + } + else if (elementName.equals("show")) { + presence.setMode(Presence.Mode.fromString(parser.nextText())); + } + else if (elementName.equals("error")) { + presence.setError(parseError(parser)); + } + // Otherwise, it must be a packet extension. + else { + presence.addExtension(parsePacketExtension(elementName, namespace, parser)); + } + } + else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals("presence")) { + done = true; + } + } + } + return presence; + } + + /** + * Parses a packet extension sub-packet. + * + * @param elementName the XML element name of the packet extension. + * @param namespace the XML namespace of the packet extension. + * @param parser the XML parser, positioned at the starting element of the extension. + * @return a PacketExtension. + * @throws Exception if a parsing error occurs. + */ + private PacketExtension parsePacketExtension(String elementName, String namespace, + XmlPullParser parser) throws Exception + { + // See if a provider is registered to handle the extension. + Object provider = ProviderManager.getExtensionProvider(elementName, namespace); + if (provider != null) { + if (provider instanceof PacketExtensionProvider) { + return ((PacketExtensionProvider)provider).parseExtension(parser); + } + else if (provider instanceof Class) { + return (PacketExtension)parseWithIntrospection( + elementName, (Class)provider, parser); + } + } + // No providers registered, so use a default extension. + DefaultPacketExtension extension = new DefaultPacketExtension(elementName, namespace); + boolean done = false; + while (!done) { + int eventType = parser.next(); + if (eventType == XmlPullParser.START_TAG) { + String name = parser.getName(); + // If an empty element, set the value with the empty string. + if (parser.isEmptyElementTag()) { + extension.setValue(name,""); + } + // Otherwise, get the the element text. + else { + eventType = parser.next(); + if (eventType == XmlPullParser.TEXT) { + String value = parser.getText(); + extension.setValue(name, value); + } + } + } + else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals(elementName)) { + done = true; + } + } + } + return extension; + } + + private Object parseWithIntrospection(String elementName, + Class objectClass, XmlPullParser parser) throws Exception + { + boolean done = false; + Object object = objectClass.newInstance(); + while (!done) { + int eventType = parser.next(); + if (eventType == XmlPullParser.START_TAG) { + String name = parser.getName(); + String stringValue = parser.nextText(); + PropertyDescriptor descriptor = new PropertyDescriptor(name, objectClass); + // Load the class type of the property. + Class propertyType = descriptor.getPropertyType(); + // Get the value of the property by converting it from a + // String to the correct object type. + Object value = decode(propertyType, stringValue); + // Set the value of the bean. + descriptor.getWriteMethod().invoke(object, new Object[] { value }); + } + else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals(elementName)) { + done = true; + } + } + } + return object; + } + + /** + * Decodes a String into an object of the specified type. If the object + * type is not supported, null will be returned. + * + * @param type the type of the property. + * @param value the encode String value to decode. + * @return the String value decoded into the specified type. + */ + private static Object decode(Class type, String value) throws Exception { + if (type.getName().equals("java.lang.String")) { + return value; + } + if (type.getName().equals("boolean")) { + return Boolean.valueOf(value); + } + if (type.getName().equals("int")) { + return Integer.valueOf(value); + } + if (type.getName().equals("long")) { + return Long.valueOf(value); + } + if (type.getName().equals("float")) { + return Float.valueOf(value); + } + if (type.getName().equals("double")) { + return Double.valueOf(value); + } + if (type.getName().equals("java.lang.Class")) { + return Class.forName(value); + } + return null; + } + + /** + * Parses error sub-packets. + * + * @param parser the XML parser. + * @return an error sub-packet. + * @throws Exception if an exception occurs while parsing the packet. + */ + private XMPPError parseError(XmlPullParser parser) throws Exception { + String errorCode = null; + for (int i=0; i + *
  • The user wants to leave the queue. In this case, an instance of this class + * should be created without passing in a user address. + *
  • An administrator or the server removes wants to remove a user from the queue. + * In that case, the address of the user to remove from the queue should be + * used to create an instance of this class. + * + * @author loki der quaeler + */ +public class DepartQueuePacket extends IQ { + + private String user; + + /** + * Creates a depart queue request packet to the specified workgroup. + * + * @param workgroup the workgroup to depart. + */ + public DepartQueuePacket(String workgroup) { + this(workgroup, null); + } + + /** + * Creates a depart queue request to the specified workgroup and for the + * specified user. + * + * @param workgroup the workgroup to depart. + * @param user the user to make depart from the queue. + */ + public DepartQueuePacket(String workgroup, String user) { + this.user = user; + + setTo(workgroup); + setType(IQ.Type.SET); + setFrom(user); + } + + public String getChildElementXML() { + StringBuffer buf = new StringBuffer("").append(this.user).append(""); + } + else { + buf.append("/>"); + } + + return buf.toString(); + } +} \ No newline at end of file diff --git a/source/org/jivesoftware/smackx/workgroup/packet/MetaDataProvider.java b/source/org/jivesoftware/smackx/workgroup/packet/MetaDataProvider.java new file mode 100644 index 000000000..5b1cc1c3d --- /dev/null +++ b/source/org/jivesoftware/smackx/workgroup/packet/MetaDataProvider.java @@ -0,0 +1,44 @@ +/** + * $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.packet; + +import java.util.Map; + +import org.jivesoftware.smackx.workgroup.MetaData; +import org.jivesoftware.smackx.workgroup.util.MetaDataUtils; + +import org.jivesoftware.smack.packet.PacketExtension; +import org.jivesoftware.smack.provider.PacketExtensionProvider; + +import org.xmlpull.v1.XmlPullParser; + + +/** + * This provider parses meta data if it's not contained already in a larger extension provider. + * + * @author loki der quaeler + */ +public class MetaDataProvider + implements PacketExtensionProvider { + + + /** + * PacketExtensionProvider implementation + */ + public PacketExtension parseExtension (XmlPullParser parser) + throws Exception { + Map metaData = MetaDataUtils.parseMetaData(parser); + + return new MetaData(metaData); + } + +} diff --git a/source/org/jivesoftware/smackx/workgroup/packet/OfferRequestProvider.java b/source/org/jivesoftware/smackx/workgroup/packet/OfferRequestProvider.java new file mode 100644 index 000000000..30627dff4 --- /dev/null +++ b/source/org/jivesoftware/smackx/workgroup/packet/OfferRequestProvider.java @@ -0,0 +1,154 @@ +/** + * $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.packet; + +import java.util.*; + +import org.jivesoftware.smackx.workgroup.*; +import org.jivesoftware.smackx.workgroup.util.MetaDataUtils; + +import org.jivesoftware.smack.packet.*; +import org.jivesoftware.smack.provider.*; + +import org.xmlpull.v1.XmlPullParser; + +/** + * An IQProvider for agent offer requests. + * + * @author loki der quaeler + */ +public class OfferRequestProvider implements IQProvider { + + public OfferRequestProvider () { + } + + public IQ parseIQ(XmlPullParser parser) throws Exception { + int eventType = parser.getEventType(); + String uid = null; + String sessionID = null; + int timeout = -1; + boolean done = false; + Map metaData = new HashMap(); + + if (eventType != XmlPullParser.START_TAG) { + // throw exception + } + + uid = parser.getAttributeValue("", "jid"); + if (uid == null) { + // throw exception + } + + parser.nextTag(); + while (!done) { + eventType = parser.getEventType(); + + if (eventType == XmlPullParser.START_TAG) { + String elemName = parser.getName(); + + if ("timeout".equals(elemName)) { + timeout = Integer.parseInt(parser.nextText()); + } + else if (MetaData.ELEMENT_NAME.equals(elemName)) { + metaData = MetaDataUtils.parseMetaData(parser); + } + else + if (SessionID.ELEMENT_NAME.equals(elemName)) { + sessionID = parser.getAttributeValue("", "session"); + + parser.nextTag(); + } + } + else if (eventType == XmlPullParser.END_TAG) { + if ("offer".equals(parser.getName())) { + done = true; + } + else { + parser.nextTag(); + } + } + else { + parser.nextTag(); + } + } + + OfferRequestPacket offerRequest = new OfferRequestPacket(uid, timeout, metaData, sessionID); + offerRequest.setType(IQ.Type.SET); + + return offerRequest; + } + + public static class OfferRequestPacket extends IQ { + + private int timeout; + private String userID; + private Map metaData; + private String sessionID; + + public OfferRequestPacket(String uid, int timeout, Map metaData, String sID) { + this.userID = uid; + this.timeout = timeout; + this.metaData = metaData; + this.sessionID = sID; + } + + public String getUserID() { + return userID; + } + + /** + * Returns the session id which will be associated with the customer for whom this offer + * is extended, or null if the offer did not contain one. + * + * @return the session id associated to the customer + */ + public String getSessionID() { + return sessionID; + } + + /** + * Returns the number of seconds the agent has to accept the offer before + * it times out. + * + * @return the offer timeout (in seconds). + */ + public int getTimeout() { + return this.timeout; + } + + public Map getMetaData() { + return this.metaData; + } + + public String getChildElementXML () { + StringBuffer buf = new StringBuffer(); + + buf.append(""); + buf.append("").append(timeout).append(""); + + if (sessionID != null) { + buf.append('<').append(SessionID.ELEMENT_NAME); + buf.append(" session=\""); + buf.append(getSessionID()).append("\" xmlns=\""); + buf.append(SessionID.NAMESPACE).append("\"/>"); + } + + if (metaData != null) { + buf.append(MetaDataUtils.serializeMetaData(metaData)); + } + + buf.append(""); + + return buf.toString(); + } + } +} diff --git a/source/org/jivesoftware/smackx/workgroup/packet/OfferRevokeProvider.java b/source/org/jivesoftware/smackx/workgroup/packet/OfferRevokeProvider.java new file mode 100644 index 000000000..6a09e305f --- /dev/null +++ b/source/org/jivesoftware/smackx/workgroup/packet/OfferRevokeProvider.java @@ -0,0 +1,86 @@ +/** + * $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.packet; + +import org.jivesoftware.smack.provider.IQProvider; +import org.jivesoftware.smack.packet.IQ; + +import org.xmlpull.v1.XmlPullParser; + +/** + * An IQProvider class which has savvy about the offer-revoke tag.
    + * + * @author loki der quaeler + */ +public class OfferRevokeProvider implements IQProvider { + + public IQ parseIQ (XmlPullParser parser) throws Exception { + // The parser will be positioned on the opening IQ tag, so get the JID attribute. + String uid = parser.getAttributeValue("", "jid"); + String reason = null; + String sessionID = null; + boolean done = false; + + while (!done) { + int eventType = parser.next(); + + if ((eventType == XmlPullParser.START_TAG) && parser.getName().equals("reason")) { + reason = parser.nextText(); + } + else if ((eventType == XmlPullParser.START_TAG) + && parser.getName().equals(SessionID.ELEMENT_NAME)) { + sessionID = parser.getAttributeValue("", "session"); + } + else if ((eventType == XmlPullParser.END_TAG) + && parser.getName().equals("offer-revoke")) { + done = true; + } + } + + return new OfferRevokePacket(uid, reason, sessionID); + } + + public class OfferRevokePacket extends IQ { + + protected String userID; + protected String sessionID; + protected String reason; + + public OfferRevokePacket (String uid, String cause, String sid) { + this.userID = uid; + this.reason = cause; + this.sessionID = sid; + } + + public String getUserID () { + return this.userID; + } + + public String getReason () { + return this.reason; + } + + public String getSessionID () { + return this.sessionID; + } + + public String getChildElementXML () { + StringBuffer buf = new StringBuffer(); + buf.append(""); + if (reason != null) { + buf.append("").append(reason).append(""); + } + buf.append(""); + return buf.toString(); + } + } +} diff --git a/source/org/jivesoftware/smackx/workgroup/packet/QueueDetails.java b/source/org/jivesoftware/smackx/workgroup/packet/QueueDetails.java new file mode 100644 index 000000000..7571f7269 --- /dev/null +++ b/source/org/jivesoftware/smackx/workgroup/packet/QueueDetails.java @@ -0,0 +1,179 @@ +package org.jivesoftware.smackx.workgroup.packet; + +import org.jivesoftware.smack.packet.PacketExtension; +import org.jivesoftware.smack.provider.PacketExtensionProvider; +import org.xmlpull.v1.XmlPullParser; + +import java.util.*; +import java.text.SimpleDateFormat; + +import org.jivesoftware.smackx.workgroup.QueueUser; + +/** + * Queue details packet extension, which contains details about the users + * currently in a queue. + */ +public class QueueDetails implements PacketExtension { + + /** + * Element name of the packet extension. + */ + public static final String ELEMENT_NAME = "notify-queue-details"; + + /** + * Namespace of the packet extension. + */ + public static final String NAMESPACE = "xmpp:workgroup"; + + private static final SimpleDateFormat DATE_FORMATTER = new SimpleDateFormat("yyyyMMdd'T'HH:mm:ss"); + + /** + * The list of users in the queue. + */ + private Set users; + + /** + * Creates a new QueueDetails packet + */ + private QueueDetails() { + users = new HashSet(); + } + + /** + * Returns the number of users currently in the queue that are waiting to + * be routed to an agent. + * + * @return the number of users in the queue. + */ + public int getUserCount() { + return users.size(); + } + + /** + * Returns the set of users in the queue that are waiting to + * be routed to an agent (as QueueUser objects). + * + * @return a Set for the users waiting in a queue. + */ + public Set getUsers() { + synchronized (users) { + return users; + } + } + + /** + * Adds a user to the packet. + * + * @param user the user. + */ + private void addUser(QueueUser user) { + synchronized (users) { + users.add(user); + } + } + + public String getElementName() { + return ELEMENT_NAME; + } + + public String getNamespace() { + return NAMESPACE; + } + + public String toXML() { + StringBuffer buf = new StringBuffer(); + buf.append("<").append(ELEMENT_NAME).append(" xmlns=\"").append(NAMESPACE).append("\">"); + + synchronized (users) { + for (Iterator i=users.iterator(); i.hasNext(); ) { + QueueUser user = (QueueUser)i.next(); + int position = user.getQueuePosition(); + int timeRemaining = user.getEstimatedRemainingTime(); + Date timestamp = user.getQueueJoinTimestamp(); + + buf.append(""); + + if (position != -1) { + buf.append("").append(position).append(""); + } + + if (timeRemaining != -1) { + buf.append(""); + } + + if (timestamp != null) { + buf.append(""); + buf.append(DATE_FORMATTER.format(timestamp)); + buf.append(""); + } + + buf.append(""); + } + } + buf.append(""); + return buf.toString(); + } + + /** + * Provider class for QueueDetails packet extensions. + */ + public static class Provider implements PacketExtensionProvider { + + public PacketExtension parseExtension(XmlPullParser parser) throws Exception { + QueueDetails queueDetails = new QueueDetails(); + + int eventType = parser.getEventType(); + while (eventType != XmlPullParser.END_TAG && + "notify-queue-details".equals(parser.getName())) + { + eventType = parser.next(); + while ((eventType == XmlPullParser.START_TAG) && "user".equals(parser.getName())) { + String uid = null; + int position = -1; + int time = -1; + Date joinTime = null; + + uid = parser.getAttributeValue("", "jid"); + + if (uid == null) { + // throw exception + } + + eventType = parser.next(); + while ((eventType != XmlPullParser.END_TAG) + || (! "user".equals(parser.getName()))) + { + if ("position".equals(parser.getName())) { + position = Integer.parseInt(parser.nextText()); + } + else if ("time".equals(parser.getName())) { + time = Integer.parseInt(parser.nextText()); + } + else if ("join-time".equals(parser.getName())) { + joinTime = DATE_FORMATTER.parse(parser.nextText()); + } + else if( parser.getName().equals( "waitTime" ) ) { + Date wait = DATE_FORMATTER.parse( parser.nextText() ); + System.out.println( wait ); + } + + + + eventType = parser.next(); + + if (eventType != XmlPullParser.END_TAG) { + // throw exception + } + } + + + + queueDetails.addUser(new QueueUser(uid, position, time, joinTime)); + + eventType = parser.next(); + } + } + return queueDetails; + } + } +} \ No newline at end of file diff --git a/source/org/jivesoftware/smackx/workgroup/packet/QueueOverview.java b/source/org/jivesoftware/smackx/workgroup/packet/QueueOverview.java new file mode 100644 index 000000000..83836f02f --- /dev/null +++ b/source/org/jivesoftware/smackx/workgroup/packet/QueueOverview.java @@ -0,0 +1,140 @@ +package org.jivesoftware.smackx.workgroup.packet; + +import org.jivesoftware.smack.packet.PacketExtension; +import org.jivesoftware.smack.provider.PacketExtensionProvider; +import org.xmlpull.v1.XmlPullParser; + +import java.util.Date; +import java.text.SimpleDateFormat; + +import org.jivesoftware.smackx.workgroup.agent.WorkgroupQueue; + +public class QueueOverview implements PacketExtension { + + /** + * Element name of the packet extension. + */ + public static String ELEMENT_NAME = "notify-queue"; + + /** + * Namespace of the packet extension. + */ + public static String NAMESPACE = "xmpp:workgroup"; + + private static final SimpleDateFormat DATE_FORMATTER = new SimpleDateFormat("yyyyMMdd'T'HH:mm:ss"); + + private int averageWaitTime; + private Date oldestEntry; + private int userCount; + private WorkgroupQueue.Status status; + + QueueOverview() { + this.averageWaitTime = -1; + this.oldestEntry = null; + this.userCount = -1; + this.status = null; + } + + void setAverageWaitTime(int averageWaitTime) { + this.averageWaitTime = averageWaitTime; + } + + public int getAverageWaitTime () { + return averageWaitTime; + } + + void setOldestEntry(Date oldestEntry) { + this.oldestEntry = oldestEntry; + } + + public Date getOldestEntry() { + return oldestEntry; + } + + void setUserCount(int userCount) { + this.userCount = userCount; + } + + public int getUserCount() { + return userCount; + } + + public WorkgroupQueue.Status getStatus() { + return status; + } + + void setStatus(WorkgroupQueue.Status status) { + this.status = status; + } + + public String getElementName () { + return ELEMENT_NAME; + } + + public String getNamespace () { + return NAMESPACE; + } + + public String toXML () { + StringBuffer buf = new StringBuffer(); + buf.append("<").append(ELEMENT_NAME).append(" xmlns=\"").append(NAMESPACE).append("\">"); + + if (userCount != -1) { + buf.append("").append(userCount).append(""); + } + if (oldestEntry != null) { + buf.append("").append(DATE_FORMATTER.format(oldestEntry)).append(""); + } + if (averageWaitTime != -1) { + buf.append(""); + } + if (status != null) { + buf.append("").append(status).append(""); + } + buf.append(""); + + return buf.toString(); + } + + public static class Provider implements PacketExtensionProvider { + + public PacketExtension parseExtension (XmlPullParser parser) throws Exception { + int eventType = parser.getEventType(); + QueueOverview queueOverview = new QueueOverview(); + + if (eventType != XmlPullParser.START_TAG) { + // throw exception + } + + eventType = parser.next(); + while ((eventType != XmlPullParser.END_TAG) + || (!ELEMENT_NAME.equals(parser.getName()))) + { + if ("count".equals(parser.getName())) { + queueOverview.setUserCount(Integer.parseInt(parser.nextText())); + } + else if ("time".equals(parser.getName())) { + queueOverview.setAverageWaitTime(Integer.parseInt(parser.nextText())); + } + else if ("oldest".equals(parser.getName())) { + queueOverview.setOldestEntry((DATE_FORMATTER.parse(parser.nextText()))); + } + else if ("status".equals(parser.getName())) { + queueOverview.setStatus(WorkgroupQueue.Status.fromString(parser.nextText())); + } + + eventType = parser.next(); + + if (eventType != XmlPullParser.END_TAG) { + // throw exception + } + } + + if (eventType != XmlPullParser.END_TAG) { + // throw exception + } + + return queueOverview; + } + } +} \ No newline at end of file diff --git a/source/org/jivesoftware/smackx/workgroup/packet/QueueUpdate.java b/source/org/jivesoftware/smackx/workgroup/packet/QueueUpdate.java new file mode 100644 index 000000000..f4da2c927 --- /dev/null +++ b/source/org/jivesoftware/smackx/workgroup/packet/QueueUpdate.java @@ -0,0 +1,93 @@ +package org.jivesoftware.smackx.workgroup.packet; + +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.provider.IQProvider; +import org.xmlpull.v1.XmlPullParser; + +/** + * An IQ packet that encapsulates both types of workgroup queue + * status notifications -- position updates, and estimated time + * left in the queue updates. + */ +public class QueueUpdate extends IQ { + + /** + * Element name of the packet extension. + */ + public static final String ELEMENT_NAME = "queue-status"; + + /** + * Namespace of the packet extension. + */ + public static final String NAMESPACE = "xmpp:workgroup"; + + private int position; + private int remainingTime; + + public QueueUpdate(int position, int remainingTime) { + this.position = position; + this.remainingTime = remainingTime; + } + + /** + * Returns the user's position in the workgroup queue, or -1 if the + * value isn't set on this packet. + * + * @return the position in the workgroup queue. + */ + public int getPosition() { + return this.position; + } + + /** + * Returns the user's estimated time left in the workgroup queue, or + * -1 if the value isn't set on this packet. + * + * @return the estimated time left in the workgroup queue. + */ + public int getRemaingTime() { + return remainingTime; + } + + public String getChildElementXML () { + StringBuffer buf = new StringBuffer(); + buf.append(""); + if (position != -1) { + buf.append("").append(position).append(""); + } + else if (remainingTime != -1) { + buf.append("").append(remainingTime).append(""); + } + buf.append(""); + return buf.toString(); + } + + public static class Provider implements IQProvider { + + public IQ parseIQ(XmlPullParser parser) throws Exception { + boolean done = false; + int position = -1; + int timeRemaining = -1; + while (!done) { + parser.next(); + String elementName = parser.getName(); + if (parser.getEventType() == XmlPullParser.START_TAG && "position".equals(elementName)) { + try { + position = Integer.parseInt(parser.nextText()); + } + catch (NumberFormatException nfe) { } + } + else if (parser.getEventType() == XmlPullParser.START_TAG && "time".equals(elementName)) { + try { + timeRemaining = Integer.parseInt(parser.nextText()); + } + catch (NumberFormatException nfe) { } + } + else if (parser.getEventType() == XmlPullParser.END_TAG && "queue-status".equals(elementName)) { + done = true; + } + } + return new QueueUpdate(position, timeRemaining); + } + } +} \ No newline at end of file diff --git a/source/org/jivesoftware/smackx/workgroup/packet/SessionID.java b/source/org/jivesoftware/smackx/workgroup/packet/SessionID.java new file mode 100644 index 000000000..ef712e05b --- /dev/null +++ b/source/org/jivesoftware/smackx/workgroup/packet/SessionID.java @@ -0,0 +1,58 @@ +package org.jivesoftware.smackx.workgroup.packet; + +import org.jivesoftware.smack.packet.PacketExtension; +import org.jivesoftware.smack.provider.PacketExtensionProvider; +import org.xmlpull.v1.XmlPullParser; + +public class SessionID implements PacketExtension { + + /** + * Element name of the packet extension. + */ + public static final String ELEMENT_NAME = "jive"; + + /** + * Namespace of the packet extension. + */ + public static final String NAMESPACE = "http://jivesoftware.com/protocol/workgroup"; + + private String sessionID; + + protected SessionID(String sessionID) { + this.sessionID = sessionID; + } + + public String getSessionID () { + return this.sessionID; + } + + public String getElementName() { + return ELEMENT_NAME; + } + + public String getNamespace() { + return NAMESPACE; + } + + public String toXML() { + StringBuffer buf = new StringBuffer(); + + buf.append("<").append(ELEMENT_NAME).append(" xmlns=\"").append(NAMESPACE).append("\" "); + buf.append("session=\"").append(this.getSessionID()); + buf.append("\"/>"); + + return buf.toString(); + } + + public static class Provider implements PacketExtensionProvider { + + public PacketExtension parseExtension(XmlPullParser parser) throws Exception { + String sessionID = parser.getAttributeValue("", "session"); + + // Advance to end of extension. + parser.next(); + + return new SessionID(sessionID); + } + } +} diff --git a/source/org/jivesoftware/smackx/workgroup/packet/WorkgroupInformation.java b/source/org/jivesoftware/smackx/workgroup/packet/WorkgroupInformation.java new file mode 100644 index 000000000..e855066e4 --- /dev/null +++ b/source/org/jivesoftware/smackx/workgroup/packet/WorkgroupInformation.java @@ -0,0 +1,76 @@ +package org.jivesoftware.smackx.workgroup.packet; + +import org.jivesoftware.smack.packet.PacketExtension; +import org.jivesoftware.smack.provider.PacketExtensionProvider; +import org.xmlpull.v1.XmlPullParser; + + +/** + * A packet extension that contains information about the user and agent in a + * workgroup chat. The packet extension is attached to group chat invitations. + */ +public class WorkgroupInformation implements PacketExtension { + + /** + * Element name of the packet extension. + */ + public static final String ELEMENT_NAME = "workgroup"; + + /** + * Namespace of the packet extension. + */ + public static final String NAMESPACE = "xmpp:workgroup"; + + private String userID; + private String agentID; + + protected WorkgroupInformation(String userID, String agentID) { + this.userID = userID; + this.agentID = agentID; + } + + public String getUserID() { + return userID; + } + + public String getAgentID() { + return agentID; + } + + public String getElementName() { + return ELEMENT_NAME; + } + + public String getNamespace() { + return NAMESPACE; + } + + public String toXML() { + StringBuffer buf = new StringBuffer(); + + buf.append('<').append(ELEMENT_NAME); + buf.append(" user=\"").append(userID).append("\""); + buf.append(" agent=\"").append(agentID); + buf.append("\" xmlns=\"").append(NAMESPACE).append("\" />"); + + return buf.toString(); + } + + public static class Provider implements PacketExtensionProvider { + + /** + * PacketExtensionProvider implementation + */ + public PacketExtension parseExtension (XmlPullParser parser) + throws Exception { + String user = parser.getAttributeValue("", "user"); + String agent = parser.getAttributeValue("", "agent"); + + // since this is a start and end tag, and we arrive on the start, this should guarantee + // we leave on the end + parser.next(); + + return new WorkgroupInformation(user, agent); + } + } +} \ No newline at end of file diff --git a/source/org/jivesoftware/smackx/workgroup/user/QueueListener.java b/source/org/jivesoftware/smackx/workgroup/user/QueueListener.java new file mode 100644 index 000000000..d17166ba5 --- /dev/null +++ b/source/org/jivesoftware/smackx/workgroup/user/QueueListener.java @@ -0,0 +1,47 @@ +/** + * $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.user; + +/** + * Listener interface for those that wish to be notified of workgroup queue events. + * + * @see Workgroup#addQueueListener(QueueListener) + * @author loki der quaeler + */ +public interface QueueListener { + + /** + * The user joined the workgroup queue. + */ + public void joinedQueue(); + + /** + * The user departed the workgroup queue. + */ + public void departedQueue(); + + /** + * The user's queue position has been updated to a new value. + * + * @param currentPosition the user's current position in the queue. + */ + public void queuePositionUpdated(int currentPosition); + + /** + * The user's estimated remaining wait time in the queue has been updated. + * + * @param secondsRemaining the estimated number of seconds remaining until the + * the user is routed to the agent. + */ + public void queueWaitTimeUpdated(int secondsRemaining); + +} diff --git a/source/org/jivesoftware/smackx/workgroup/user/Workgroup.java b/source/org/jivesoftware/smackx/workgroup/user/Workgroup.java new file mode 100644 index 000000000..2c392d045 --- /dev/null +++ b/source/org/jivesoftware/smackx/workgroup/user/Workgroup.java @@ -0,0 +1,456 @@ +/** + * $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.user; + +import java.util.*; + +import org.jivesoftware.smack.*; +import org.jivesoftware.smack.filter.*; +import org.jivesoftware.smack.packet.*; +import org.jivesoftware.smackx.GroupChatInvitation; + +import org.jivesoftware.smackx.workgroup.*; +import org.jivesoftware.smackx.workgroup.packet.*; +import org.jivesoftware.smackx.workgroup.util.MetaDataUtils; + +/** + * Provides workgroup services for users. Users can join the workgropu queue, depart the + * queue, find status information about their placement in the queue, and register to + * be notified when they are routed to an agent.

    + * + * 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:

    + * + * A user cannot request to join the queue again if already in the queue. Therefore, this + * method will do nothing if the user is already in 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:

    + * + * A user cannot request to join the queue again if already in the queue. Therefore, this + * method will do nothing if the user is already in 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:

    + * + * @param metaData the metaData for the join request. + * @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 (error code 503). The error code should be checked + * to determine the specific error. + */ + public void joinQueue(Map metaData) throws XMPPException { + // If already in the queue ignore the join request. + if (inQueue) { + return; + } + + JoinQueuePacket joinPacket = new JoinQueuePacket(workgroupName, metaData); + PacketCollector collector = connection.createPacketCollector( + new PacketIDFilter(joinPacket.getPacketID())); + + this.connection.sendPacket(joinPacket); + + IQ response = (IQ)collector.nextResult(10000); + + // Cancel the collector. + 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've joined the queue. + fireQueueJoinedEvent(); + } + + /** + * Departs the workgroup queue. If the user is not currently in the queue, this + * method will do nothing.

    + * + * 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(""); + buf.append(""); + + // Add any meta-data. + buf.append(MetaDataUtils.serializeMetaData(metaData)); + + buf.append(""); + + return buf.toString(); + } + } +} \ No newline at end of file diff --git a/source/org/jivesoftware/smackx/workgroup/util/ListenerEventDispatcher.java b/source/org/jivesoftware/smackx/workgroup/util/ListenerEventDispatcher.java new file mode 100644 index 000000000..6012d6cf1 --- /dev/null +++ b/source/org/jivesoftware/smackx/workgroup/util/ListenerEventDispatcher.java @@ -0,0 +1,131 @@ +/** + * $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 java.lang.reflect.Method; +import java.util.*; + +/** + * This class is a very flexible event dispatcher which implements Runnable so that it can + * dispatch easily from a newly created thread. The usage of this in code is more or less: + * create a new instance of this class, use addListenerTriplet to add as many listeners + * as desired to be messaged, create a new Thread using the instance of this class created + * as the argument to the constructor, start the new Thread instance.
    + * + * 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(""); + for (Iterator i=metaData.keySet().iterator(); i.hasNext(); ) { + Object key = i.next(); + String value = metaData.get(key).toString(); + buf.append(""); + buf.append(value); + buf.append(""); + } + buf.append(""); + } + return buf.toString(); + } +}