AgentSession.java

  1. /**
  2.  *
  3.  * Copyright 2003-2007 Jive Software.
  4.  *
  5.  * Licensed under the Apache License, Version 2.0 (the "License");
  6.  * you may not use this file except in compliance with the License.
  7.  * You may obtain a copy of the License at
  8.  *
  9.  *     http://www.apache.org/licenses/LICENSE-2.0
  10.  *
  11.  * Unless required by applicable law or agreed to in writing, software
  12.  * distributed under the License is distributed on an "AS IS" BASIS,
  13.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14.  * See the License for the specific language governing permissions and
  15.  * limitations under the License.
  16.  */

  17. package org.jivesoftware.smackx.workgroup.agent;


  18. import java.util.ArrayList;
  19. import java.util.Collections;
  20. import java.util.Date;
  21. import java.util.HashMap;
  22. import java.util.Iterator;
  23. import java.util.List;
  24. import java.util.Map;
  25. import java.util.Set;
  26. import java.util.logging.Level;
  27. import java.util.logging.Logger;

  28. import org.jivesoftware.smack.PacketCollector;
  29. import org.jivesoftware.smack.StanzaListener;
  30. import org.jivesoftware.smack.SmackException;
  31. import org.jivesoftware.smack.SmackException.NoResponseException;
  32. import org.jivesoftware.smack.SmackException.NotConnectedException;
  33. import org.jivesoftware.smack.XMPPConnection;
  34. import org.jivesoftware.smack.XMPPException;
  35. import org.jivesoftware.smack.XMPPException.XMPPErrorException;
  36. import org.jivesoftware.smack.filter.AndFilter;
  37. import org.jivesoftware.smack.filter.FromMatchesFilter;
  38. import org.jivesoftware.smack.filter.OrFilter;
  39. import org.jivesoftware.smack.filter.StanzaTypeFilter;
  40. import org.jivesoftware.smack.packet.DefaultExtensionElement;
  41. import org.jivesoftware.smack.packet.IQ;
  42. import org.jivesoftware.smack.packet.Message;
  43. import org.jivesoftware.smack.packet.Stanza;
  44. import org.jivesoftware.smack.packet.Presence;
  45. import org.jivesoftware.smackx.muc.packet.MUCUser;
  46. import org.jivesoftware.smackx.search.ReportedData;
  47. import org.jivesoftware.smackx.workgroup.MetaData;
  48. import org.jivesoftware.smackx.workgroup.QueueUser;
  49. import org.jivesoftware.smackx.workgroup.WorkgroupInvitation;
  50. import org.jivesoftware.smackx.workgroup.WorkgroupInvitationListener;
  51. import org.jivesoftware.smackx.workgroup.ext.history.AgentChatHistory;
  52. import org.jivesoftware.smackx.workgroup.ext.history.ChatMetadata;
  53. import org.jivesoftware.smackx.workgroup.ext.macros.MacroGroup;
  54. import org.jivesoftware.smackx.workgroup.ext.macros.Macros;
  55. import org.jivesoftware.smackx.workgroup.ext.notes.ChatNotes;
  56. import org.jivesoftware.smackx.workgroup.packet.AgentStatus;
  57. import org.jivesoftware.smackx.workgroup.packet.DepartQueuePacket;
  58. import org.jivesoftware.smackx.workgroup.packet.MonitorPacket;
  59. import org.jivesoftware.smackx.workgroup.packet.OccupantsInfo;
  60. import org.jivesoftware.smackx.workgroup.packet.OfferRequestProvider;
  61. import org.jivesoftware.smackx.workgroup.packet.OfferRevokeProvider;
  62. import org.jivesoftware.smackx.workgroup.packet.QueueDetails;
  63. import org.jivesoftware.smackx.workgroup.packet.QueueOverview;
  64. import org.jivesoftware.smackx.workgroup.packet.RoomInvitation;
  65. import org.jivesoftware.smackx.workgroup.packet.RoomTransfer;
  66. import org.jivesoftware.smackx.workgroup.packet.SessionID;
  67. import org.jivesoftware.smackx.workgroup.packet.Transcript;
  68. import org.jivesoftware.smackx.workgroup.packet.Transcripts;
  69. import org.jivesoftware.smackx.workgroup.settings.GenericSettings;
  70. import org.jivesoftware.smackx.workgroup.settings.SearchSettings;
  71. import org.jivesoftware.smackx.xdata.Form;
  72. import org.jxmpp.jid.Jid;
  73. import org.jxmpp.jid.parts.Resourcepart;

  74. /**
  75.  * This class embodies the agent's active presence within a given workgroup. The application
  76.  * should have N instances of this class, where N is the number of workgroups to which the
  77.  * owning agent of the application belongs. This class provides all functionality that a
  78.  * session within a given workgroup is expected to have from an agent's perspective -- setting
  79.  * the status, tracking the status of queues to which the agent belongs within the workgroup, and
  80.  * dequeuing customers.
  81.  *
  82.  * @author Matt Tucker
  83.  * @author Derek DeMoro
  84.  */
  85. public class AgentSession {
  86.     private static final Logger LOGGER = Logger.getLogger(AgentSession.class.getName());
  87.    
  88.     private XMPPConnection connection;

  89.     private Jid workgroupJID;

  90.     private boolean online = false;
  91.     private Presence.Mode presenceMode;
  92.     private int maxChats;
  93.     private final Map<String, List<String>> metaData;

  94.     private final Map<Resourcepart, WorkgroupQueue> queues = new HashMap<>();

  95.     private final List<OfferListener> offerListeners;
  96.     private final List<WorkgroupInvitationListener> invitationListeners;
  97.     private final List<QueueUsersListener> queueUsersListeners;

  98.     private AgentRoster agentRoster = null;
  99.     private TranscriptManager transcriptManager;
  100.     private TranscriptSearchManager transcriptSearchManager;
  101.     private Agent agent;
  102.     private StanzaListener packetListener;

  103.     /**
  104.      * Constructs a new agent session instance. Note, the {@link #setOnline(boolean)}
  105.      * method must be called with an argument of <tt>true</tt> to mark the agent
  106.      * as available to accept chat requests.
  107.      *
  108.      * @param connection   a connection instance which must have already gone through
  109.      *                     authentication.
  110.      * @param workgroupJID the fully qualified JID of the workgroup.
  111.      */
  112.     public AgentSession(Jid workgroupJID, XMPPConnection connection) {
  113.         // Login must have been done before passing in connection.
  114.         if (!connection.isAuthenticated()) {
  115.             throw new IllegalStateException("Must login to server before creating workgroup.");
  116.         }

  117.         this.workgroupJID = workgroupJID;
  118.         this.connection = connection;
  119.         this.transcriptManager = new TranscriptManager(connection);
  120.         this.transcriptSearchManager = new TranscriptSearchManager(connection);

  121.         this.maxChats = -1;

  122.         this.metaData = new HashMap<String, List<String>>();

  123.         offerListeners = new ArrayList<OfferListener>();
  124.         invitationListeners = new ArrayList<WorkgroupInvitationListener>();
  125.         queueUsersListeners = new ArrayList<QueueUsersListener>();

  126.         // Create a filter to listen for packets we're interested in.
  127.         OrFilter filter = new OrFilter(
  128.                         new StanzaTypeFilter(OfferRequestProvider.OfferRequestPacket.class),
  129.                         new StanzaTypeFilter(OfferRevokeProvider.OfferRevokePacket.class),
  130.                         new StanzaTypeFilter(Presence.class),
  131.                         new StanzaTypeFilter(Message.class));

  132.         packetListener = new StanzaListener() {
  133.             public void processPacket(Stanza packet) {
  134.                 try {
  135.                     handlePacket(packet);
  136.                 }
  137.                 catch (Exception e) {
  138.                     LOGGER.log(Level.SEVERE, "Error processing packet", e);
  139.                 }
  140.             }
  141.         };
  142.         connection.addAsyncStanzaListener(packetListener, filter);
  143.         // Create the agent associated to this session
  144.         agent = new Agent(connection, workgroupJID);
  145.     }

  146.     /**
  147.      * Close the agent session. The underlying connection will remain opened but the
  148.      * packet listeners that were added by this agent session will be removed.
  149.      */
  150.     public void close() {
  151.         connection.removeAsyncStanzaListener(packetListener);
  152.     }

  153.     /**
  154.      * Returns the agent roster for the workgroup, which contains
  155.      *
  156.      * @return the AgentRoster
  157.      * @throws NotConnectedException
  158.      * @throws InterruptedException
  159.      */
  160.     public AgentRoster getAgentRoster() throws NotConnectedException, InterruptedException {
  161.         if (agentRoster == null) {
  162.             agentRoster = new AgentRoster(connection, workgroupJID);
  163.         }

  164.         // This might be the first time the user has asked for the roster. If so, we
  165.         // want to wait up to 2 seconds for the server to send back the list of agents.
  166.         // This behavior shields API users from having to worry about the fact that the
  167.         // operation is asynchronous, although they'll still have to listen for changes
  168.         // to the roster.
  169.         int elapsed = 0;
  170.         while (!agentRoster.rosterInitialized && elapsed <= 2000) {
  171.             try {
  172.                 Thread.sleep(500);
  173.             }
  174.             catch (Exception e) {
  175.                 // Ignore
  176.             }
  177.             elapsed += 500;
  178.         }
  179.         return agentRoster;
  180.     }

  181.     /**
  182.      * Returns the agent's current presence mode.
  183.      *
  184.      * @return the agent's current presence mode.
  185.      */
  186.     public Presence.Mode getPresenceMode() {
  187.         return presenceMode;
  188.     }

  189.     /**
  190.      * Returns the maximum number of chats the agent can participate in.
  191.      *
  192.      * @return the maximum number of chats the agent can participate in.
  193.      */
  194.     public int getMaxChats() {
  195.         return maxChats;
  196.     }

  197.     /**
  198.      * Returns true if the agent is online with the workgroup.
  199.      *
  200.      * @return true if the agent is online with the workgroup.
  201.      */
  202.     public boolean isOnline() {
  203.         return online;
  204.     }

  205.     /**
  206.      * Allows the addition of a new key-value pair to the agent's meta data, if the value is
  207.      * new data, the revised meta data will be rebroadcast in an agent's presence broadcast.
  208.      *
  209.      * @param key the meta data key
  210.      * @param val the non-null meta data value
  211.      * @throws XMPPException if an exception occurs.
  212.      * @throws SmackException
  213.      * @throws InterruptedException
  214.      */
  215.     public void setMetaData(String key, String val) throws XMPPException, SmackException, InterruptedException {
  216.         synchronized (this.metaData) {
  217.             List<String> oldVals = metaData.get(key);

  218.             if ((oldVals == null) || (!oldVals.get(0).equals(val))) {
  219.                 oldVals.set(0, val);

  220.                 setStatus(presenceMode, maxChats);
  221.             }
  222.         }
  223.     }

  224.     /**
  225.      * Allows the removal of data from the agent's meta data, if the key represents existing data,
  226.      * the revised meta data will be rebroadcast in an agent's presence broadcast.
  227.      *
  228.      * @param key the meta data key.
  229.      * @throws XMPPException if an exception occurs.
  230.      * @throws SmackException
  231.      * @throws InterruptedException
  232.      */
  233.     public void removeMetaData(String key) throws XMPPException, SmackException, InterruptedException {
  234.         synchronized (this.metaData) {
  235.             List<String> oldVal = metaData.remove(key);

  236.             if (oldVal != null) {
  237.                 setStatus(presenceMode, maxChats);
  238.             }
  239.         }
  240.     }

  241.     /**
  242.      * Allows the retrieval of meta data for a specified key.
  243.      *
  244.      * @param key the meta data key
  245.      * @return the meta data value associated with the key or <tt>null</tt> if the meta-data
  246.      *         doesn't exist..
  247.      */
  248.     public List<String> getMetaData(String key) {
  249.         return metaData.get(key);
  250.     }

  251.     /**
  252.      * Sets whether the agent is online with the workgroup. If the user tries to go online with
  253.      * the workgroup but is not allowed to be an agent, an XMPPError with error code 401 will
  254.      * be thrown.
  255.      *
  256.      * @param online true to set the agent as online with the workgroup.
  257.      * @throws XMPPException if an error occurs setting the online status.
  258.      * @throws SmackException             assertEquals(SmackException.Type.NO_RESPONSE_FROM_SERVER, e.getType());
  259.             return;
  260.      * @throws InterruptedException
  261.      */
  262.     public void setOnline(boolean online) throws XMPPException, SmackException, InterruptedException {
  263.         // If the online status hasn't changed, do nothing.
  264.         if (this.online == online) {
  265.             return;
  266.         }

  267.         Presence presence;

  268.         // If the user is going online...
  269.         if (online) {
  270.             presence = new Presence(Presence.Type.available);
  271.             presence.setTo(workgroupJID);
  272.             presence.addExtension(new DefaultExtensionElement(AgentStatus.ELEMENT_NAME,
  273.                     AgentStatus.NAMESPACE));

  274.             PacketCollector collector = this.connection.createPacketCollectorAndSend(new AndFilter(
  275.                             new StanzaTypeFilter(Presence.class), FromMatchesFilter.create(workgroupJID)), presence);

  276.             presence = (Presence)collector.nextResultOrThrow();

  277.             // We can safely update this iv since we didn't get any error
  278.             this.online = online;
  279.         }
  280.         // Otherwise the user is going offline...
  281.         else {
  282.             // Update this iv now since we don't care at this point of any error
  283.             this.online = online;

  284.             presence = new Presence(Presence.Type.unavailable);
  285.             presence.setTo(workgroupJID);
  286.             presence.addExtension(new DefaultExtensionElement(AgentStatus.ELEMENT_NAME,
  287.                     AgentStatus.NAMESPACE));
  288.             connection.sendStanza(presence);
  289.         }
  290.     }

  291.     /**
  292.      * Sets the agent's current status with the workgroup. The presence mode affects
  293.      * how offers are routed to the agent. The possible presence modes with their
  294.      * meanings are as follows:<ul>
  295.      * <p/>
  296.      * <li>Presence.Mode.AVAILABLE -- (Default) the agent is available for more chats
  297.      * (equivalent to Presence.Mode.CHAT).
  298.      * <li>Presence.Mode.DO_NOT_DISTURB -- the agent is busy and should not be disturbed.
  299.      * However, special case, or extreme urgency chats may still be offered to the agent.
  300.      * <li>Presence.Mode.AWAY -- the agent is not available and should not
  301.      * have a chat routed to them (equivalent to Presence.Mode.EXTENDED_AWAY).</ul>
  302.      * <p/>
  303.      * The max chats value is the maximum number of chats the agent is willing to have
  304.      * routed to them at once. Some servers may be configured to only accept max chat
  305.      * values in a certain range; for example, between two and five. In that case, the
  306.      * maxChats value the agent sends may be adjusted by the server to a value within that
  307.      * range.
  308.      *
  309.      * @param presenceMode the presence mode of the agent.
  310.      * @param maxChats     the maximum number of chats the agent is willing to accept.
  311.      * @throws XMPPException         if an error occurs setting the agent status.
  312.      * @throws SmackException
  313.      * @throws InterruptedException
  314.      * @throws IllegalStateException if the agent is not online with the workgroup.
  315.      */
  316.     public void setStatus(Presence.Mode presenceMode, int maxChats) throws XMPPException, SmackException, InterruptedException {
  317.         setStatus(presenceMode, maxChats, null);
  318.     }

  319.     /**
  320.      * Sets the agent's current status with the workgroup. The presence mode affects how offers
  321.      * are routed to the agent. The possible presence modes with their meanings are as follows:<ul>
  322.      * <p/>
  323.      * <li>Presence.Mode.AVAILABLE -- (Default) the agent is available for more chats
  324.      * (equivalent to Presence.Mode.CHAT).
  325.      * <li>Presence.Mode.DO_NOT_DISTURB -- the agent is busy and should not be disturbed.
  326.      * However, special case, or extreme urgency chats may still be offered to the agent.
  327.      * <li>Presence.Mode.AWAY -- the agent is not available and should not
  328.      * have a chat routed to them (equivalent to Presence.Mode.EXTENDED_AWAY).</ul>
  329.      * <p/>
  330.      * The max chats value is the maximum number of chats the agent is willing to have routed to
  331.      * them at once. Some servers may be configured to only accept max chat values in a certain
  332.      * range; for example, between two and five. In that case, the maxChats value the agent sends
  333.      * may be adjusted by the server to a value within that range.
  334.      *
  335.      * @param presenceMode the presence mode of the agent.
  336.      * @param maxChats     the maximum number of chats the agent is willing to accept.
  337.      * @param status       sets the status message of the presence update.
  338.      * @throws XMPPErrorException
  339.      * @throws NoResponseException
  340.      * @throws NotConnectedException
  341.      * @throws InterruptedException
  342.      * @throws IllegalStateException if the agent is not online with the workgroup.
  343.      */
  344.     public void setStatus(Presence.Mode presenceMode, int maxChats, String status)
  345.                     throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
  346.         if (!online) {
  347.             throw new IllegalStateException("Cannot set status when the agent is not online.");
  348.         }

  349.         if (presenceMode == null) {
  350.             presenceMode = Presence.Mode.available;
  351.         }
  352.         this.presenceMode = presenceMode;
  353.         this.maxChats = maxChats;

  354.         Presence presence = new Presence(Presence.Type.available);
  355.         presence.setMode(presenceMode);
  356.         presence.setTo(this.getWorkgroupJID());

  357.         if (status != null) {
  358.             presence.setStatus(status);
  359.         }
  360.         // Send information about max chats and current chats as a packet extension.
  361.         DefaultExtensionElement agentStatus = new DefaultExtensionElement(AgentStatus.ELEMENT_NAME,
  362.                         AgentStatus.NAMESPACE);
  363.         agentStatus.setValue("max-chats", "" + maxChats);
  364.         presence.addExtension(agentStatus);
  365.         presence.addExtension(new MetaData(this.metaData));

  366.         PacketCollector collector = this.connection.createPacketCollectorAndSend(new AndFilter(
  367.                         new StanzaTypeFilter(Presence.class),
  368.                         FromMatchesFilter.create(workgroupJID)), presence);

  369.         collector.nextResultOrThrow();
  370.     }

  371.     /**
  372.      * Sets the agent's current status with the workgroup. The presence mode affects how offers
  373.      * are routed to the agent. The possible presence modes with their meanings are as follows:<ul>
  374.      * <p/>
  375.      * <li>Presence.Mode.AVAILABLE -- (Default) the agent is available for more chats
  376.      * (equivalent to Presence.Mode.CHAT).
  377.      * <li>Presence.Mode.DO_NOT_DISTURB -- the agent is busy and should not be disturbed.
  378.      * However, special case, or extreme urgency chats may still be offered to the agent.
  379.      * <li>Presence.Mode.AWAY -- the agent is not available and should not
  380.      * have a chat routed to them (equivalent to Presence.Mode.EXTENDED_AWAY).</ul>
  381.      *
  382.      * @param presenceMode the presence mode of the agent.
  383.      * @param status       sets the status message of the presence update.
  384.      * @throws XMPPErrorException
  385.      * @throws NoResponseException
  386.      * @throws NotConnectedException
  387.      * @throws InterruptedException
  388.      * @throws IllegalStateException if the agent is not online with the workgroup.
  389.      */
  390.     public void setStatus(Presence.Mode presenceMode, String status) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
  391.         if (!online) {
  392.             throw new IllegalStateException("Cannot set status when the agent is not online.");
  393.         }

  394.         if (presenceMode == null) {
  395.             presenceMode = Presence.Mode.available;
  396.         }
  397.         this.presenceMode = presenceMode;

  398.         Presence presence = new Presence(Presence.Type.available);
  399.         presence.setMode(presenceMode);
  400.         presence.setTo(this.getWorkgroupJID());

  401.         if (status != null) {
  402.             presence.setStatus(status);
  403.         }
  404.         presence.addExtension(new MetaData(this.metaData));

  405.         PacketCollector collector = this.connection.createPacketCollectorAndSend(new AndFilter(new StanzaTypeFilter(Presence.class),
  406.                 FromMatchesFilter.create(workgroupJID)), presence);

  407.         collector.nextResultOrThrow();
  408.     }

  409.     /**
  410.      * Removes a user from the workgroup queue. This is an administrative action that the
  411.      * <p/>
  412.      * The agent is not guaranteed of having privileges to perform this action; an exception
  413.      * denying the request may be thrown.
  414.      *
  415.      * @param userID the ID of the user to remove.
  416.      * @throws XMPPException if an exception occurs.
  417.      * @throws NotConnectedException
  418.      * @throws InterruptedException
  419.      */
  420.     public void dequeueUser(String userID) throws XMPPException, NotConnectedException, InterruptedException {
  421.         // todo: this method simply won't work right now.
  422.         DepartQueuePacket departPacket = new DepartQueuePacket(this.workgroupJID);

  423.         // PENDING
  424.         this.connection.sendStanza(departPacket);
  425.     }

  426.     /**
  427.      * Returns the transcripts of a given user. The answer will contain the complete history of
  428.      * conversations that a user had.
  429.      *
  430.      * @param userID the id of the user to get his conversations.
  431.      * @return the transcripts of a given user.
  432.      * @throws XMPPException if an error occurs while getting the information.
  433.      * @throws SmackException
  434.      * @throws InterruptedException
  435.      */
  436.     public Transcripts getTranscripts(Jid userID) throws XMPPException, SmackException, InterruptedException {
  437.         return transcriptManager.getTranscripts(workgroupJID, userID);
  438.     }

  439.     /**
  440.      * Returns the full conversation transcript of a given session.
  441.      *
  442.      * @param sessionID the id of the session to get the full transcript.
  443.      * @return the full conversation transcript of a given session.
  444.      * @throws XMPPException if an error occurs while getting the information.
  445.      * @throws SmackException
  446.      * @throws InterruptedException
  447.      */
  448.     public Transcript getTranscript(String sessionID) throws XMPPException, SmackException, InterruptedException {
  449.         return transcriptManager.getTranscript(workgroupJID, sessionID);
  450.     }

  451.     /**
  452.      * Returns the Form to use for searching transcripts. It is unlikely that the server
  453.      * will change the form (without a restart) so it is safe to keep the returned form
  454.      * for future submissions.
  455.      *
  456.      * @return the Form to use for searching transcripts.
  457.      * @throws XMPPException if an error occurs while sending the request to the server.
  458.      * @throws SmackException
  459.      * @throws InterruptedException
  460.      */
  461.     public Form getTranscriptSearchForm() throws XMPPException, SmackException, InterruptedException {
  462.         return transcriptSearchManager.getSearchForm(workgroupJID.asDomainBareJid());
  463.     }

  464.     /**
  465.      * Submits the completed form and returns the result of the transcript search. The result
  466.      * will include all the data returned from the server so be careful with the amount of
  467.      * data that the search may return.
  468.      *
  469.      * @param completedForm the filled out search form.
  470.      * @return the result of the transcript search.
  471.      * @throws SmackException
  472.      * @throws XMPPException
  473.      * @throws InterruptedException
  474.      */
  475.     public ReportedData searchTranscripts(Form completedForm) throws XMPPException, SmackException, InterruptedException {
  476.         return transcriptSearchManager.submitSearch(workgroupJID.asDomainBareJid(),
  477.                 completedForm);
  478.     }

  479.     /**
  480.      * Asks the workgroup for information about the occupants of the specified room. The returned
  481.      * information will include the real JID of the occupants, the nickname of the user in the
  482.      * room as well as the date when the user joined the room.
  483.      *
  484.      * @param roomID the room to get information about its occupants.
  485.      * @return information about the occupants of the specified room.
  486.      * @throws XMPPErrorException
  487.      * @throws NoResponseException
  488.      * @throws NotConnectedException
  489.      * @throws InterruptedException
  490.      */
  491.     public OccupantsInfo getOccupantsInfo(String roomID) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException  {
  492.         OccupantsInfo request = new OccupantsInfo(roomID);
  493.         request.setType(IQ.Type.get);
  494.         request.setTo(workgroupJID);

  495.         OccupantsInfo response = (OccupantsInfo) connection.createPacketCollectorAndSend(request).nextResultOrThrow();
  496.         return response;
  497.     }

  498.     /**
  499.      * @return the fully-qualified name of the workgroup for which this session exists
  500.      */
  501.     public Jid getWorkgroupJID() {
  502.         return workgroupJID;
  503.     }

  504.     /**
  505.      * Returns the Agent associated to this session.
  506.      *
  507.      * @return the Agent associated to this session.
  508.      */
  509.     public Agent getAgent() {
  510.         return agent;
  511.     }

  512.     /**
  513.      * @param queueName the name of the queue
  514.      * @return an instance of WorkgroupQueue for the argument queue name, or null if none exists
  515.      */
  516.     public WorkgroupQueue getQueue(String queueName) {
  517.         return queues.get(queueName);
  518.     }

  519.     public Iterator<WorkgroupQueue> getQueues() {
  520.         return Collections.unmodifiableMap((new HashMap<>(queues))).values().iterator();
  521.     }

  522.     public void addQueueUsersListener(QueueUsersListener listener) {
  523.         synchronized (queueUsersListeners) {
  524.             if (!queueUsersListeners.contains(listener)) {
  525.                 queueUsersListeners.add(listener);
  526.             }
  527.         }
  528.     }

  529.     public void removeQueueUsersListener(QueueUsersListener listener) {
  530.         synchronized (queueUsersListeners) {
  531.             queueUsersListeners.remove(listener);
  532.         }
  533.     }

  534.     /**
  535.      * Adds an offer listener.
  536.      *
  537.      * @param offerListener the offer listener.
  538.      */
  539.     public void addOfferListener(OfferListener offerListener) {
  540.         synchronized (offerListeners) {
  541.             if (!offerListeners.contains(offerListener)) {
  542.                 offerListeners.add(offerListener);
  543.             }
  544.         }
  545.     }

  546.     /**
  547.      * Removes an offer listener.
  548.      *
  549.      * @param offerListener the offer listener.
  550.      */
  551.     public void removeOfferListener(OfferListener offerListener) {
  552.         synchronized (offerListeners) {
  553.             offerListeners.remove(offerListener);
  554.         }
  555.     }

  556.     /**
  557.      * Adds an invitation listener.
  558.      *
  559.      * @param invitationListener the invitation listener.
  560.      */
  561.     public void addInvitationListener(WorkgroupInvitationListener invitationListener) {
  562.         synchronized (invitationListeners) {
  563.             if (!invitationListeners.contains(invitationListener)) {
  564.                 invitationListeners.add(invitationListener);
  565.             }
  566.         }
  567.     }

  568.     /**
  569.      * Removes an invitation listener.
  570.      *
  571.      * @param invitationListener the invitation listener.
  572.      */
  573.     public void removeInvitationListener(WorkgroupInvitationListener invitationListener) {
  574.         synchronized (invitationListeners) {
  575.             invitationListeners.remove(invitationListener);
  576.         }
  577.     }

  578.     private void fireOfferRequestEvent(OfferRequestProvider.OfferRequestPacket requestPacket) {
  579.         Offer offer = new Offer(this.connection, this, requestPacket.getUserID(),
  580.                 requestPacket.getUserJID(), this.getWorkgroupJID(),
  581.                 new Date((new Date()).getTime() + (requestPacket.getTimeout() * 1000)),
  582.                 requestPacket.getSessionID(), requestPacket.getMetaData(), requestPacket.getContent());

  583.         synchronized (offerListeners) {
  584.             for (OfferListener listener : offerListeners) {
  585.                 listener.offerReceived(offer);
  586.             }
  587.         }
  588.     }

  589.     private void fireOfferRevokeEvent(OfferRevokeProvider.OfferRevokePacket orp) {
  590.         RevokedOffer revokedOffer = new RevokedOffer(orp.getUserJID(), orp.getUserID(),
  591.                 this.getWorkgroupJID(), orp.getSessionID(), orp.getReason(), new Date());

  592.         synchronized (offerListeners) {
  593.             for (OfferListener listener : offerListeners) {
  594.                 listener.offerRevoked(revokedOffer);
  595.             }
  596.         }
  597.     }

  598.     private void fireInvitationEvent(Jid groupChatJID, String sessionID, String body,
  599.                                      Jid from, Map<String, List<String>> metaData) {
  600.         WorkgroupInvitation invitation = new WorkgroupInvitation(connection.getUser(), groupChatJID,
  601.                 workgroupJID, sessionID, body, from, metaData);

  602.         synchronized (invitationListeners) {
  603.             for (WorkgroupInvitationListener listener : invitationListeners) {
  604.                 listener.invitationReceived(invitation);
  605.             }
  606.         }
  607.     }

  608.     private void fireQueueUsersEvent(WorkgroupQueue queue, WorkgroupQueue.Status status,
  609.                                      int averageWaitTime, Date oldestEntry, Set<QueueUser> users) {
  610.         synchronized (queueUsersListeners) {
  611.             for (QueueUsersListener listener : queueUsersListeners) {
  612.                 if (status != null) {
  613.                     listener.statusUpdated(queue, status);
  614.                 }
  615.                 if (averageWaitTime != -1) {
  616.                     listener.averageWaitTimeUpdated(queue, averageWaitTime);
  617.                 }
  618.                 if (oldestEntry != null) {
  619.                     listener.oldestEntryUpdated(queue, oldestEntry);
  620.                 }
  621.                 if (users != null) {
  622.                     listener.usersUpdated(queue, users);
  623.                 }
  624.             }
  625.         }
  626.     }

  627.     // PacketListener Implementation.

  628.     private void handlePacket(Stanza packet) throws NotConnectedException, InterruptedException {
  629.         if (packet instanceof OfferRequestProvider.OfferRequestPacket) {
  630.             // Acknowledge the IQ set.
  631.             IQ reply = IQ.createResultIQ((IQ) packet);
  632.             connection.sendStanza(reply);

  633.             fireOfferRequestEvent((OfferRequestProvider.OfferRequestPacket)packet);
  634.         }
  635.         else if (packet instanceof Presence) {
  636.             Presence presence = (Presence)packet;

  637.             // The workgroup can send us a number of different presence packets. We
  638.             // check for different packet extensions to see what type of presence
  639.             // packet it is.

  640.             Resourcepart queueName = presence.getFrom().getResourceOrNull();
  641.             WorkgroupQueue queue = queues.get(queueName);
  642.             // If there isn't already an entry for the queue, create a new one.
  643.             if (queue == null) {
  644.                 queue = new WorkgroupQueue(queueName);
  645.                 queues.put(queueName, queue);
  646.             }

  647.             // QueueOverview packet extensions contain basic information about a queue.
  648.             QueueOverview queueOverview = (QueueOverview)presence.getExtension(QueueOverview.ELEMENT_NAME, QueueOverview.NAMESPACE);
  649.             if (queueOverview != null) {
  650.                 if (queueOverview.getStatus() == null) {
  651.                     queue.setStatus(WorkgroupQueue.Status.CLOSED);
  652.                 }
  653.                 else {
  654.                     queue.setStatus(queueOverview.getStatus());
  655.                 }
  656.                 queue.setAverageWaitTime(queueOverview.getAverageWaitTime());
  657.                 queue.setOldestEntry(queueOverview.getOldestEntry());
  658.                 // Fire event.
  659.                 fireQueueUsersEvent(queue, queueOverview.getStatus(),
  660.                         queueOverview.getAverageWaitTime(), queueOverview.getOldestEntry(),
  661.                         null);
  662.                 return;
  663.             }

  664.             // QueueDetails packet extensions contain information about the users in
  665.             // a queue.
  666.             QueueDetails queueDetails = (QueueDetails)packet.getExtension(QueueDetails.ELEMENT_NAME, QueueDetails.NAMESPACE);
  667.             if (queueDetails != null) {
  668.                 queue.setUsers(queueDetails.getUsers());
  669.                 // Fire event.
  670.                 fireQueueUsersEvent(queue, null, -1, null, queueDetails.getUsers());
  671.                 return;
  672.             }

  673.             // Notify agent packets gives an overview of agent activity in a queue.
  674.             DefaultExtensionElement notifyAgents = (DefaultExtensionElement)presence.getExtension("notify-agents", "http://jabber.org/protocol/workgroup");
  675.             if (notifyAgents != null) {
  676.                 int currentChats = Integer.parseInt(notifyAgents.getValue("current-chats"));
  677.                 int maxChats = Integer.parseInt(notifyAgents.getValue("max-chats"));
  678.                 queue.setCurrentChats(currentChats);
  679.                 queue.setMaxChats(maxChats);
  680.                 // Fire event.
  681.                 // TODO: might need another event for current chats and max chats of queue
  682.                 return;
  683.             }
  684.         }
  685.         else if (packet instanceof Message) {
  686.             Message message = (Message)packet;

  687.             // Check if a room invitation was sent and if the sender is the workgroup
  688.             MUCUser mucUser = (MUCUser)message.getExtension("x",
  689.                     "http://jabber.org/protocol/muc#user");
  690.             MUCUser.Invite invite = mucUser != null ? mucUser.getInvite() : null;
  691.             if (invite != null && workgroupJID.equals(invite.getFrom())) {
  692.                 String sessionID = null;
  693.                 Map<String, List<String>> metaData = null;

  694.                 SessionID sessionIDExt = (SessionID)message.getExtension(SessionID.ELEMENT_NAME,
  695.                         SessionID.NAMESPACE);
  696.                 if (sessionIDExt != null) {
  697.                     sessionID = sessionIDExt.getSessionID();
  698.                 }

  699.                 MetaData metaDataExt = (MetaData)message.getExtension(MetaData.ELEMENT_NAME,
  700.                         MetaData.NAMESPACE);
  701.                 if (metaDataExt != null) {
  702.                     metaData = metaDataExt.getMetaData();
  703.                 }

  704.                 this.fireInvitationEvent(message.getFrom(), sessionID, message.getBody(),
  705.                         message.getFrom(), metaData);
  706.             }
  707.         }
  708.         else if (packet instanceof OfferRevokeProvider.OfferRevokePacket) {
  709.             // Acknowledge the IQ set.
  710.             IQ reply = IQ.createResultIQ((OfferRevokeProvider.OfferRevokePacket) packet);
  711.             connection.sendStanza(reply);

  712.             fireOfferRevokeEvent((OfferRevokeProvider.OfferRevokePacket)packet);
  713.         }
  714.     }

  715.     /**
  716.      * Creates a ChatNote that will be mapped to the given chat session.
  717.      *
  718.      * @param sessionID the session id of a Chat Session.
  719.      * @param note      the chat note to add.
  720.      * @throws XMPPErrorException
  721.      * @throws NoResponseException
  722.      * @throws NotConnectedException
  723.      * @throws InterruptedException
  724.      */
  725.     public void setNote(String sessionID, String note) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException  {
  726.         ChatNotes notes = new ChatNotes();
  727.         notes.setType(IQ.Type.set);
  728.         notes.setTo(workgroupJID);
  729.         notes.setSessionID(sessionID);
  730.         notes.setNotes(note);
  731.         connection.createPacketCollectorAndSend(notes).nextResultOrThrow();
  732.     }

  733.     /**
  734.      * Retrieves the ChatNote associated with a given chat session.
  735.      *
  736.      * @param sessionID the sessionID of the chat session.
  737.      * @return the <code>ChatNote</code> associated with a given chat session.
  738.      * @throws XMPPErrorException if an error occurs while retrieving the ChatNote.
  739.      * @throws NoResponseException
  740.      * @throws NotConnectedException
  741.      * @throws InterruptedException
  742.      */
  743.     public ChatNotes getNote(String sessionID) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
  744.         ChatNotes request = new ChatNotes();
  745.         request.setType(IQ.Type.get);
  746.         request.setTo(workgroupJID);
  747.         request.setSessionID(sessionID);

  748.         ChatNotes response = (ChatNotes) connection.createPacketCollectorAndSend(request).nextResultOrThrow();
  749.         return response;
  750.     }

  751.     /**
  752.      * Retrieves the AgentChatHistory associated with a particular agent jid.
  753.      *
  754.      * @param jid the jid of the agent.
  755.      * @param maxSessions the max number of sessions to retrieve.
  756.      * @return the chat history associated with a given jid.
  757.      * @throws XMPPException if an error occurs while retrieving the AgentChatHistory.
  758.      * @throws NotConnectedException
  759.      * @throws InterruptedException
  760.      */
  761.     public AgentChatHistory getAgentHistory(String jid, int maxSessions, Date startDate) throws XMPPException, NotConnectedException, InterruptedException {
  762.         AgentChatHistory request;
  763.         if (startDate != null) {
  764.             request = new AgentChatHistory(jid, maxSessions, startDate);
  765.         }
  766.         else {
  767.             request = new AgentChatHistory(jid, maxSessions);
  768.         }

  769.         request.setType(IQ.Type.get);
  770.         request.setTo(workgroupJID);

  771.         AgentChatHistory response = connection.createPacketCollectorAndSend(
  772.                         request).nextResult();

  773.         return response;
  774.     }

  775.     /**
  776.      * Asks the workgroup for it's Search Settings.
  777.      *
  778.      * @return SearchSettings the search settings for this workgroup.
  779.      * @throws XMPPErrorException
  780.      * @throws NoResponseException
  781.      * @throws NotConnectedException
  782.      * @throws InterruptedException
  783.      */
  784.     public SearchSettings getSearchSettings() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
  785.         SearchSettings request = new SearchSettings();
  786.         request.setType(IQ.Type.get);
  787.         request.setTo(workgroupJID);

  788.         SearchSettings response = (SearchSettings) connection.createPacketCollectorAndSend(request).nextResultOrThrow();
  789.         return response;
  790.     }

  791.     /**
  792.      * Asks the workgroup for it's Global Macros.
  793.      *
  794.      * @param global true to retrieve global macros, otherwise false for personal macros.
  795.      * @return MacroGroup the root macro group.
  796.      * @throws XMPPErrorException if an error occurs while getting information from the server.
  797.      * @throws NoResponseException
  798.      * @throws NotConnectedException
  799.      * @throws InterruptedException
  800.      */
  801.     public MacroGroup getMacros(boolean global) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
  802.         Macros request = new Macros();
  803.         request.setType(IQ.Type.get);
  804.         request.setTo(workgroupJID);
  805.         request.setPersonal(!global);

  806.         Macros response = (Macros) connection.createPacketCollectorAndSend(request).nextResultOrThrow();
  807.         return response.getRootGroup();
  808.     }

  809.     /**
  810.      * Persists the Personal Macro for an agent.
  811.      *
  812.      * @param group the macro group to save.
  813.      * @throws XMPPErrorException
  814.      * @throws NoResponseException
  815.      * @throws NotConnectedException
  816.      * @throws InterruptedException
  817.      */
  818.     public void saveMacros(MacroGroup group) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
  819.         Macros request = new Macros();
  820.         request.setType(IQ.Type.set);
  821.         request.setTo(workgroupJID);
  822.         request.setPersonal(true);
  823.         request.setPersonalMacroGroup(group);

  824.         connection.createPacketCollectorAndSend(request).nextResultOrThrow();
  825.     }

  826.     /**
  827.      * Query for metadata associated with a session id.
  828.      *
  829.      * @param sessionID the sessionID to query for.
  830.      * @return Map a map of all metadata associated with the sessionID.
  831.      * @throws XMPPException if an error occurs while getting information from the server.
  832.      * @throws NotConnectedException
  833.      * @throws InterruptedException
  834.      */
  835.     public Map<String, List<String>> getChatMetadata(String sessionID) throws XMPPException, NotConnectedException, InterruptedException {
  836.         ChatMetadata request = new ChatMetadata();
  837.         request.setType(IQ.Type.get);
  838.         request.setTo(workgroupJID);
  839.         request.setSessionID(sessionID);

  840.         ChatMetadata response = connection.createPacketCollectorAndSend(request).nextResult();

  841.         return response.getMetadata();
  842.     }

  843.     /**
  844.      * Invites a user or agent to an existing session support. The provided invitee's JID can be of
  845.      * a user, an agent, a queue or a workgroup. In the case of a queue or a workgroup the workgroup service
  846.      * will decide the best agent to receive the invitation.<p>
  847.      *
  848.      * This method will return either when the service returned an ACK of the request or if an error occured
  849.      * while requesting the invitation. After sending the ACK the service will send the invitation to the target
  850.      * entity. When dealing with agents the common sequence of offer-response will be followed. However, when
  851.      * sending an invitation to a user a standard MUC invitation will be sent.<p>
  852.      *
  853.      * The agent or user that accepted the offer <b>MUST</b> join the room. Failing to do so will make
  854.      * the invitation to fail. The inviter will eventually receive a message error indicating that the invitee
  855.      * accepted the offer but failed to join the room.
  856.      *
  857.      * Different situations may lead to a failed invitation. Possible cases are: 1) all agents rejected the
  858.      * offer and ther are no agents available, 2) the agent that accepted the offer failed to join the room or
  859.      * 2) the user that received the MUC invitation never replied or joined the room. In any of these cases
  860.      * (or other failing cases) the inviter will get an error message with the failed notification.
  861.      *
  862.      * @param type type of entity that will get the invitation.
  863.      * @param invitee JID of entity that will get the invitation.
  864.      * @param sessionID ID of the support session that the invitee is being invited.
  865.      * @param reason the reason of the invitation.
  866.      * @throws XMPPErrorException if the sender of the invitation is not an agent or the service failed to process
  867.      *         the request.
  868.      * @throws NoResponseException
  869.      * @throws NotConnectedException
  870.      * @throws InterruptedException
  871.      */
  872.     public void sendRoomInvitation(RoomInvitation.Type type, String invitee, String sessionID, String reason) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException
  873.             {
  874.         final RoomInvitation invitation = new RoomInvitation(type, invitee, sessionID, reason);
  875.         IQ iq = new RoomInvitation.RoomInvitationIQ(invitation);
  876.         iq.setType(IQ.Type.set);
  877.         iq.setTo(workgroupJID);
  878.         iq.setFrom(connection.getUser());

  879.         connection.createPacketCollectorAndSend(iq).nextResultOrThrow();
  880.     }

  881.     /**
  882.      * Transfer an existing session support to another user or agent. The provided invitee's JID can be of
  883.      * a user, an agent, a queue or a workgroup. In the case of a queue or a workgroup the workgroup service
  884.      * will decide the best agent to receive the invitation.<p>
  885.      *
  886.      * This method will return either when the service returned an ACK of the request or if an error occured
  887.      * while requesting the transfer. After sending the ACK the service will send the invitation to the target
  888.      * entity. When dealing with agents the common sequence of offer-response will be followed. However, when
  889.      * sending an invitation to a user a standard MUC invitation will be sent.<p>
  890.      *
  891.      * Once the invitee joins the support room the workgroup service will kick the inviter from the room.<p>
  892.      *
  893.      * Different situations may lead to a failed transfers. Possible cases are: 1) all agents rejected the
  894.      * offer and there are no agents available, 2) the agent that accepted the offer failed to join the room
  895.      * or 2) the user that received the MUC invitation never replied or joined the room. In any of these cases
  896.      * (or other failing cases) the inviter will get an error message with the failed notification.
  897.      *
  898.      * @param type type of entity that will get the invitation.
  899.      * @param invitee JID of entity that will get the invitation.
  900.      * @param sessionID ID of the support session that the invitee is being invited.
  901.      * @param reason the reason of the invitation.
  902.      * @throws XMPPErrorException if the sender of the invitation is not an agent or the service failed to process
  903.      *         the request.
  904.      * @throws NoResponseException
  905.      * @throws NotConnectedException
  906.      * @throws InterruptedException
  907.      */
  908.     public void sendRoomTransfer(RoomTransfer.Type type, String invitee, String sessionID, String reason) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException
  909.             {
  910.         final RoomTransfer transfer = new RoomTransfer(type, invitee, sessionID, reason);
  911.         IQ iq = new RoomTransfer.RoomTransferIQ(transfer);
  912.         iq.setType(IQ.Type.set);
  913.         iq.setTo(workgroupJID);
  914.         iq.setFrom(connection.getUser());

  915.         connection.createPacketCollectorAndSend(iq).nextResultOrThrow();
  916.     }

  917.     /**
  918.      * Returns the generic metadata of the workgroup the agent belongs to.
  919.      *
  920.      * @param con   the XMPPConnection to use.
  921.      * @param query an optional query object used to tell the server what metadata to retrieve. This can be null.
  922.      * @return the settings for the workgroup.
  923.      * @throws XMPPErrorException if an error occurs while sending the request to the server.
  924.      * @throws NoResponseException
  925.      * @throws NotConnectedException
  926.      * @throws InterruptedException
  927.      */
  928.     public GenericSettings getGenericSettings(XMPPConnection con, String query) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
  929.         GenericSettings setting = new GenericSettings();
  930.         setting.setType(IQ.Type.get);
  931.         setting.setTo(workgroupJID);

  932.         GenericSettings response = (GenericSettings) connection.createPacketCollectorAndSend(
  933.                         setting).nextResultOrThrow();
  934.         return response;
  935.     }

  936.     public boolean hasMonitorPrivileges(XMPPConnection con) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException  {
  937.         MonitorPacket request = new MonitorPacket();
  938.         request.setType(IQ.Type.get);
  939.         request.setTo(workgroupJID);

  940.         MonitorPacket response = (MonitorPacket) connection.createPacketCollectorAndSend(request).nextResultOrThrow();
  941.         return response.isMonitor();
  942.     }

  943.     public void makeRoomOwner(XMPPConnection con, String sessionID) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException  {
  944.         MonitorPacket request = new MonitorPacket();
  945.         request.setType(IQ.Type.set);
  946.         request.setTo(workgroupJID);
  947.         request.setSessionID(sessionID);

  948.         connection.createPacketCollectorAndSend(request).nextResultOrThrow();
  949.     }
  950. }