Home | History | Annotate | Download | only in agent
      1 /**
      2  * $Revision$
      3  * $Date$
      4  *
      5  * Copyright 2003-2007 Jive Software.
      6  *
      7  * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
      8  * you may not use this file except in compliance with the License.
      9  * You may obtain a copy of the License at
     10  *
     11  *     http://www.apache.org/licenses/LICENSE-2.0
     12  *
     13  * Unless required by applicable law or agreed to in writing, software
     14  * distributed under the License is distributed on an "AS IS" BASIS,
     15  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     16  * See the License for the specific language governing permissions and
     17  * limitations under the License.
     18  */
     19 
     20 package org.jivesoftware.smackx.workgroup.agent;
     21 
     22 import org.jivesoftware.smackx.workgroup.MetaData;
     23 import org.jivesoftware.smackx.workgroup.QueueUser;
     24 import org.jivesoftware.smackx.workgroup.WorkgroupInvitation;
     25 import org.jivesoftware.smackx.workgroup.WorkgroupInvitationListener;
     26 import org.jivesoftware.smackx.workgroup.ext.history.AgentChatHistory;
     27 import org.jivesoftware.smackx.workgroup.ext.history.ChatMetadata;
     28 import org.jivesoftware.smackx.workgroup.ext.macros.MacroGroup;
     29 import org.jivesoftware.smackx.workgroup.ext.macros.Macros;
     30 import org.jivesoftware.smackx.workgroup.ext.notes.ChatNotes;
     31 import org.jivesoftware.smackx.workgroup.packet.*;
     32 import org.jivesoftware.smackx.workgroup.settings.GenericSettings;
     33 import org.jivesoftware.smackx.workgroup.settings.SearchSettings;
     34 import org.jivesoftware.smack.*;
     35 import org.jivesoftware.smack.filter.*;
     36 import org.jivesoftware.smack.packet.*;
     37 import org.jivesoftware.smack.util.StringUtils;
     38 import org.jivesoftware.smackx.Form;
     39 import org.jivesoftware.smackx.ReportedData;
     40 import org.jivesoftware.smackx.packet.MUCUser;
     41 
     42 import java.util.*;
     43 
     44 /**
     45  * This class embodies the agent's active presence within a given workgroup. The application
     46  * should have N instances of this class, where N is the number of workgroups to which the
     47  * owning agent of the application belongs. This class provides all functionality that a
     48  * session within a given workgroup is expected to have from an agent's perspective -- setting
     49  * the status, tracking the status of queues to which the agent belongs within the workgroup, and
     50  * dequeuing customers.
     51  *
     52  * @author Matt Tucker
     53  * @author Derek DeMoro
     54  */
     55 public class AgentSession {
     56 
     57     private Connection connection;
     58 
     59     private String workgroupJID;
     60 
     61     private boolean online = false;
     62     private Presence.Mode presenceMode;
     63     private int maxChats;
     64     private final Map<String, List<String>> metaData;
     65 
     66     private Map<String, WorkgroupQueue> queues;
     67 
     68     private final List<OfferListener> offerListeners;
     69     private final List<WorkgroupInvitationListener> invitationListeners;
     70     private final List<QueueUsersListener> queueUsersListeners;
     71 
     72     private AgentRoster agentRoster = null;
     73     private TranscriptManager transcriptManager;
     74     private TranscriptSearchManager transcriptSearchManager;
     75     private Agent agent;
     76     private PacketListener packetListener;
     77 
     78     /**
     79      * Constructs a new agent session instance. Note, the {@link #setOnline(boolean)}
     80      * method must be called with an argument of <tt>true</tt> to mark the agent
     81      * as available to accept chat requests.
     82      *
     83      * @param connection   a connection instance which must have already gone through
     84      *                     authentication.
     85      * @param workgroupJID the fully qualified JID of the workgroup.
     86      */
     87     public AgentSession(String workgroupJID, Connection connection) {
     88         // Login must have been done before passing in connection.
     89         if (!connection.isAuthenticated()) {
     90             throw new IllegalStateException("Must login to server before creating workgroup.");
     91         }
     92 
     93         this.workgroupJID = workgroupJID;
     94         this.connection = connection;
     95         this.transcriptManager = new TranscriptManager(connection);
     96         this.transcriptSearchManager = new TranscriptSearchManager(connection);
     97 
     98         this.maxChats = -1;
     99 
    100         this.metaData = new HashMap<String, List<String>>();
    101 
    102         this.queues = new HashMap<String, WorkgroupQueue>();
    103 
    104         offerListeners = new ArrayList<OfferListener>();
    105         invitationListeners = new ArrayList<WorkgroupInvitationListener>();
    106         queueUsersListeners = new ArrayList<QueueUsersListener>();
    107 
    108         // Create a filter to listen for packets we're interested in.
    109         OrFilter filter = new OrFilter();
    110         filter.addFilter(new PacketTypeFilter(OfferRequestProvider.OfferRequestPacket.class));
    111         filter.addFilter(new PacketTypeFilter(OfferRevokeProvider.OfferRevokePacket.class));
    112         filter.addFilter(new PacketTypeFilter(Presence.class));
    113         filter.addFilter(new PacketTypeFilter(Message.class));
    114 
    115         packetListener = new PacketListener() {
    116             public void processPacket(Packet packet) {
    117                 try {
    118                     handlePacket(packet);
    119                 }
    120                 catch (Exception e) {
    121                     e.printStackTrace();
    122                 }
    123             }
    124         };
    125         connection.addPacketListener(packetListener, filter);
    126         // Create the agent associated to this session
    127         agent = new Agent(connection, workgroupJID);
    128     }
    129 
    130     /**
    131      * Close the agent session. The underlying connection will remain opened but the
    132      * packet listeners that were added by this agent session will be removed.
    133      */
    134     public void close() {
    135         connection.removePacketListener(packetListener);
    136     }
    137 
    138     /**
    139      * Returns the agent roster for the workgroup, which contains
    140      *
    141      * @return the AgentRoster
    142      */
    143     public AgentRoster getAgentRoster() {
    144         if (agentRoster == null) {
    145             agentRoster = new AgentRoster(connection, workgroupJID);
    146         }
    147 
    148         // This might be the first time the user has asked for the roster. If so, we
    149         // want to wait up to 2 seconds for the server to send back the list of agents.
    150         // This behavior shields API users from having to worry about the fact that the
    151         // operation is asynchronous, although they'll still have to listen for changes
    152         // to the roster.
    153         int elapsed = 0;
    154         while (!agentRoster.rosterInitialized && elapsed <= 2000) {
    155             try {
    156                 Thread.sleep(500);
    157             }
    158             catch (Exception e) {
    159                 // Ignore
    160             }
    161             elapsed += 500;
    162         }
    163         return agentRoster;
    164     }
    165 
    166     /**
    167      * Returns the agent's current presence mode.
    168      *
    169      * @return the agent's current presence mode.
    170      */
    171     public Presence.Mode getPresenceMode() {
    172         return presenceMode;
    173     }
    174 
    175     /**
    176      * Returns the maximum number of chats the agent can participate in.
    177      *
    178      * @return the maximum number of chats the agent can participate in.
    179      */
    180     public int getMaxChats() {
    181         return maxChats;
    182     }
    183 
    184     /**
    185      * Returns true if the agent is online with the workgroup.
    186      *
    187      * @return true if the agent is online with the workgroup.
    188      */
    189     public boolean isOnline() {
    190         return online;
    191     }
    192 
    193     /**
    194      * Allows the addition of a new key-value pair to the agent's meta data, if the value is
    195      * new data, the revised meta data will be rebroadcast in an agent's presence broadcast.
    196      *
    197      * @param key the meta data key
    198      * @param val the non-null meta data value
    199      * @throws XMPPException if an exception occurs.
    200      */
    201     public void setMetaData(String key, String val) throws XMPPException {
    202         synchronized (this.metaData) {
    203             List<String> oldVals = metaData.get(key);
    204 
    205             if ((oldVals == null) || (!oldVals.get(0).equals(val))) {
    206                 oldVals.set(0, val);
    207 
    208                 setStatus(presenceMode, maxChats);
    209             }
    210         }
    211     }
    212 
    213     /**
    214      * Allows the removal of data from the agent's meta data, if the key represents existing data,
    215      * the revised meta data will be rebroadcast in an agent's presence broadcast.
    216      *
    217      * @param key the meta data key.
    218      * @throws XMPPException if an exception occurs.
    219      */
    220     public void removeMetaData(String key) throws XMPPException {
    221         synchronized (this.metaData) {
    222             List<String> oldVal = metaData.remove(key);
    223 
    224             if (oldVal != null) {
    225                 setStatus(presenceMode, maxChats);
    226             }
    227         }
    228     }
    229 
    230     /**
    231      * Allows the retrieval of meta data for a specified key.
    232      *
    233      * @param key the meta data key
    234      * @return the meta data value associated with the key or <tt>null</tt> if the meta-data
    235      *         doesn't exist..
    236      */
    237     public List<String> getMetaData(String key) {
    238         return metaData.get(key);
    239     }
    240 
    241     /**
    242      * Sets whether the agent is online with the workgroup. If the user tries to go online with
    243      * the workgroup but is not allowed to be an agent, an XMPPError with error code 401 will
    244      * be thrown.
    245      *
    246      * @param online true to set the agent as online with the workgroup.
    247      * @throws XMPPException if an error occurs setting the online status.
    248      */
    249     public void setOnline(boolean online) throws XMPPException {
    250         // If the online status hasn't changed, do nothing.
    251         if (this.online == online) {
    252             return;
    253         }
    254 
    255         Presence presence;
    256 
    257         // If the user is going online...
    258         if (online) {
    259             presence = new Presence(Presence.Type.available);
    260             presence.setTo(workgroupJID);
    261             presence.addExtension(new DefaultPacketExtension(AgentStatus.ELEMENT_NAME,
    262                     AgentStatus.NAMESPACE));
    263 
    264             PacketCollector collector = this.connection.createPacketCollector(new AndFilter(new PacketTypeFilter(Presence.class), new FromContainsFilter(workgroupJID)));
    265 
    266             connection.sendPacket(presence);
    267 
    268             presence = (Presence)collector.nextResult(5000);
    269             collector.cancel();
    270             if (!presence.isAvailable()) {
    271                 throw new XMPPException("No response from server on status set.");
    272             }
    273 
    274             if (presence.getError() != null) {
    275                 throw new XMPPException(presence.getError());
    276             }
    277 
    278             // We can safely update this iv since we didn't get any error
    279             this.online = online;
    280         }
    281         // Otherwise the user is going offline...
    282         else {
    283             // Update this iv now since we don't care at this point of any error
    284             this.online = online;
    285 
    286             presence = new Presence(Presence.Type.unavailable);
    287             presence.setTo(workgroupJID);
    288             presence.addExtension(new DefaultPacketExtension(AgentStatus.ELEMENT_NAME,
    289                     AgentStatus.NAMESPACE));
    290             connection.sendPacket(presence);
    291         }
    292     }
    293 
    294     /**
    295      * Sets the agent's current status with the workgroup. The presence mode affects
    296      * how offers are routed to the agent. The possible presence modes with their
    297      * meanings are as follows:<ul>
    298      * <p/>
    299      * <li>Presence.Mode.AVAILABLE -- (Default) the agent is available for more chats
    300      * (equivalent to Presence.Mode.CHAT).
    301      * <li>Presence.Mode.DO_NOT_DISTURB -- the agent is busy and should not be disturbed.
    302      * However, special case, or extreme urgency chats may still be offered to the agent.
    303      * <li>Presence.Mode.AWAY -- the agent is not available and should not
    304      * have a chat routed to them (equivalent to Presence.Mode.EXTENDED_AWAY).</ul>
    305      * <p/>
    306      * The max chats value is the maximum number of chats the agent is willing to have
    307      * routed to them at once. Some servers may be configured to only accept max chat
    308      * values in a certain range; for example, between two and five. In that case, the
    309      * maxChats value the agent sends may be adjusted by the server to a value within that
    310      * range.
    311      *
    312      * @param presenceMode the presence mode of the agent.
    313      * @param maxChats     the maximum number of chats the agent is willing to accept.
    314      * @throws XMPPException         if an error occurs setting the agent status.
    315      * @throws IllegalStateException if the agent is not online with the workgroup.
    316      */
    317     public void setStatus(Presence.Mode presenceMode, int maxChats) throws XMPPException {
    318         setStatus(presenceMode, maxChats, null);
    319     }
    320 
    321     /**
    322      * Sets the agent's current status with the workgroup. The presence mode affects how offers
    323      * are routed to the agent. The possible presence modes with their meanings are as follows:<ul>
    324      * <p/>
    325      * <li>Presence.Mode.AVAILABLE -- (Default) the agent is available for more chats
    326      * (equivalent to Presence.Mode.CHAT).
    327      * <li>Presence.Mode.DO_NOT_DISTURB -- the agent is busy and should not be disturbed.
    328      * However, special case, or extreme urgency chats may still be offered to the agent.
    329      * <li>Presence.Mode.AWAY -- the agent is not available and should not
    330      * have a chat routed to them (equivalent to Presence.Mode.EXTENDED_AWAY).</ul>
    331      * <p/>
    332      * The max chats value is the maximum number of chats the agent is willing to have routed to
    333      * them at once. Some servers may be configured to only accept max chat values in a certain
    334      * range; for example, between two and five. In that case, the maxChats value the agent sends
    335      * may be adjusted by the server to a value within that range.
    336      *
    337      * @param presenceMode the presence mode of the agent.
    338      * @param maxChats     the maximum number of chats the agent is willing to accept.
    339      * @param status       sets the status message of the presence update.
    340      * @throws XMPPException         if an error occurs setting the agent status.
    341      * @throws IllegalStateException if the agent is not online with the workgroup.
    342      */
    343     public void setStatus(Presence.Mode presenceMode, int maxChats, String status)
    344             throws XMPPException {
    345         if (!online) {
    346             throw new IllegalStateException("Cannot set status when the agent is not online.");
    347         }
    348 
    349         if (presenceMode == null) {
    350             presenceMode = Presence.Mode.available;
    351         }
    352         this.presenceMode = presenceMode;
    353         this.maxChats = maxChats;
    354 
    355         Presence presence = new Presence(Presence.Type.available);
    356         presence.setMode(presenceMode);
    357         presence.setTo(this.getWorkgroupJID());
    358 
    359         if (status != null) {
    360             presence.setStatus(status);
    361         }
    362         // Send information about max chats and current chats as a packet extension.
    363         DefaultPacketExtension agentStatus = new DefaultPacketExtension(AgentStatus.ELEMENT_NAME,
    364                 AgentStatus.NAMESPACE);
    365         agentStatus.setValue("max-chats", "" + maxChats);
    366         presence.addExtension(agentStatus);
    367         presence.addExtension(new MetaData(this.metaData));
    368 
    369         PacketCollector collector = this.connection.createPacketCollector(new AndFilter(new PacketTypeFilter(Presence.class), new FromContainsFilter(workgroupJID)));
    370 
    371         this.connection.sendPacket(presence);
    372 
    373         presence = (Presence)collector.nextResult(5000);
    374         collector.cancel();
    375         if (!presence.isAvailable()) {
    376             throw new XMPPException("No response from server on status set.");
    377         }
    378 
    379         if (presence.getError() != null) {
    380             throw new XMPPException(presence.getError());
    381         }
    382     }
    383 
    384     /**
    385      * Sets the agent's current status with the workgroup. The presence mode affects how offers
    386      * are routed to the agent. The possible presence modes with their meanings are as follows:<ul>
    387      * <p/>
    388      * <li>Presence.Mode.AVAILABLE -- (Default) the agent is available for more chats
    389      * (equivalent to Presence.Mode.CHAT).
    390      * <li>Presence.Mode.DO_NOT_DISTURB -- the agent is busy and should not be disturbed.
    391      * However, special case, or extreme urgency chats may still be offered to the agent.
    392      * <li>Presence.Mode.AWAY -- the agent is not available and should not
    393      * have a chat routed to them (equivalent to Presence.Mode.EXTENDED_AWAY).</ul>
    394      *
    395      * @param presenceMode the presence mode of the agent.
    396      * @param status       sets the status message of the presence update.
    397      * @throws XMPPException         if an error occurs setting the agent status.
    398      * @throws IllegalStateException if the agent is not online with the workgroup.
    399      */
    400     public void setStatus(Presence.Mode presenceMode, String status) throws XMPPException {
    401         if (!online) {
    402             throw new IllegalStateException("Cannot set status when the agent is not online.");
    403         }
    404 
    405         if (presenceMode == null) {
    406             presenceMode = Presence.Mode.available;
    407         }
    408         this.presenceMode = presenceMode;
    409 
    410         Presence presence = new Presence(Presence.Type.available);
    411         presence.setMode(presenceMode);
    412         presence.setTo(this.getWorkgroupJID());
    413 
    414         if (status != null) {
    415             presence.setStatus(status);
    416         }
    417         presence.addExtension(new MetaData(this.metaData));
    418 
    419         PacketCollector collector = this.connection.createPacketCollector(new AndFilter(new PacketTypeFilter(Presence.class),
    420                 new FromContainsFilter(workgroupJID)));
    421 
    422         this.connection.sendPacket(presence);
    423 
    424         presence = (Presence)collector.nextResult(5000);
    425         collector.cancel();
    426         if (!presence.isAvailable()) {
    427             throw new XMPPException("No response from server on status set.");
    428         }
    429 
    430         if (presence.getError() != null) {
    431             throw new XMPPException(presence.getError());
    432         }
    433     }
    434 
    435     /**
    436      * Removes a user from the workgroup queue. This is an administrative action that the
    437      * <p/>
    438      * The agent is not guaranteed of having privileges to perform this action; an exception
    439      * denying the request may be thrown.
    440      *
    441      * @param userID the ID of the user to remove.
    442      * @throws XMPPException if an exception occurs.
    443      */
    444     public void dequeueUser(String userID) throws XMPPException {
    445         // todo: this method simply won't work right now.
    446         DepartQueuePacket departPacket = new DepartQueuePacket(this.workgroupJID);
    447 
    448         // PENDING
    449         this.connection.sendPacket(departPacket);
    450     }
    451 
    452     /**
    453      * Returns the transcripts of a given user. The answer will contain the complete history of
    454      * conversations that a user had.
    455      *
    456      * @param userID the id of the user to get his conversations.
    457      * @return the transcripts of a given user.
    458      * @throws XMPPException if an error occurs while getting the information.
    459      */
    460     public Transcripts getTranscripts(String userID) throws XMPPException {
    461         return transcriptManager.getTranscripts(workgroupJID, userID);
    462     }
    463 
    464     /**
    465      * Returns the full conversation transcript of a given session.
    466      *
    467      * @param sessionID the id of the session to get the full transcript.
    468      * @return the full conversation transcript of a given session.
    469      * @throws XMPPException if an error occurs while getting the information.
    470      */
    471     public Transcript getTranscript(String sessionID) throws XMPPException {
    472         return transcriptManager.getTranscript(workgroupJID, sessionID);
    473     }
    474 
    475     /**
    476      * Returns the Form to use for searching transcripts. It is unlikely that the server
    477      * will change the form (without a restart) so it is safe to keep the returned form
    478      * for future submissions.
    479      *
    480      * @return the Form to use for searching transcripts.
    481      * @throws XMPPException if an error occurs while sending the request to the server.
    482      */
    483     public Form getTranscriptSearchForm() throws XMPPException {
    484         return transcriptSearchManager.getSearchForm(StringUtils.parseServer(workgroupJID));
    485     }
    486 
    487     /**
    488      * Submits the completed form and returns the result of the transcript search. The result
    489      * will include all the data returned from the server so be careful with the amount of
    490      * data that the search may return.
    491      *
    492      * @param completedForm the filled out search form.
    493      * @return the result of the transcript search.
    494      * @throws XMPPException if an error occurs while submiting the search to the server.
    495      */
    496     public ReportedData searchTranscripts(Form completedForm) throws XMPPException {
    497         return transcriptSearchManager.submitSearch(StringUtils.parseServer(workgroupJID),
    498                 completedForm);
    499     }
    500 
    501     /**
    502      * Asks the workgroup for information about the occupants of the specified room. The returned
    503      * information will include the real JID of the occupants, the nickname of the user in the
    504      * room as well as the date when the user joined the room.
    505      *
    506      * @param roomID the room to get information about its occupants.
    507      * @return information about the occupants of the specified room.
    508      * @throws XMPPException if an error occurs while getting information from the server.
    509      */
    510     public OccupantsInfo getOccupantsInfo(String roomID) throws XMPPException {
    511         OccupantsInfo request = new OccupantsInfo(roomID);
    512         request.setType(IQ.Type.GET);
    513         request.setTo(workgroupJID);
    514 
    515         PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(request.getPacketID()));
    516         connection.sendPacket(request);
    517 
    518         OccupantsInfo response = (OccupantsInfo)collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
    519 
    520         // Cancel the collector.
    521         collector.cancel();
    522         if (response == null) {
    523             throw new XMPPException("No response from server.");
    524         }
    525         if (response.getError() != null) {
    526             throw new XMPPException(response.getError());
    527         }
    528         return response;
    529     }
    530 
    531     /**
    532      * @return the fully-qualified name of the workgroup for which this session exists
    533      */
    534     public String getWorkgroupJID() {
    535         return workgroupJID;
    536     }
    537 
    538     /**
    539      * Returns the Agent associated to this session.
    540      *
    541      * @return the Agent associated to this session.
    542      */
    543     public Agent getAgent() {
    544         return agent;
    545     }
    546 
    547     /**
    548      * @param queueName the name of the queue
    549      * @return an instance of WorkgroupQueue for the argument queue name, or null if none exists
    550      */
    551     public WorkgroupQueue getQueue(String queueName) {
    552         return queues.get(queueName);
    553     }
    554 
    555     public Iterator<WorkgroupQueue> getQueues() {
    556         return Collections.unmodifiableMap((new HashMap<String, WorkgroupQueue>(queues))).values().iterator();
    557     }
    558 
    559     public void addQueueUsersListener(QueueUsersListener listener) {
    560         synchronized (queueUsersListeners) {
    561             if (!queueUsersListeners.contains(listener)) {
    562                 queueUsersListeners.add(listener);
    563             }
    564         }
    565     }
    566 
    567     public void removeQueueUsersListener(QueueUsersListener listener) {
    568         synchronized (queueUsersListeners) {
    569             queueUsersListeners.remove(listener);
    570         }
    571     }
    572 
    573     /**
    574      * Adds an offer listener.
    575      *
    576      * @param offerListener the offer listener.
    577      */
    578     public void addOfferListener(OfferListener offerListener) {
    579         synchronized (offerListeners) {
    580             if (!offerListeners.contains(offerListener)) {
    581                 offerListeners.add(offerListener);
    582             }
    583         }
    584     }
    585 
    586     /**
    587      * Removes an offer listener.
    588      *
    589      * @param offerListener the offer listener.
    590      */
    591     public void removeOfferListener(OfferListener offerListener) {
    592         synchronized (offerListeners) {
    593             offerListeners.remove(offerListener);
    594         }
    595     }
    596 
    597     /**
    598      * Adds an invitation listener.
    599      *
    600      * @param invitationListener the invitation listener.
    601      */
    602     public void addInvitationListener(WorkgroupInvitationListener invitationListener) {
    603         synchronized (invitationListeners) {
    604             if (!invitationListeners.contains(invitationListener)) {
    605                 invitationListeners.add(invitationListener);
    606             }
    607         }
    608     }
    609 
    610     /**
    611      * Removes an invitation listener.
    612      *
    613      * @param invitationListener the invitation listener.
    614      */
    615     public void removeInvitationListener(WorkgroupInvitationListener invitationListener) {
    616         synchronized (invitationListeners) {
    617             invitationListeners.remove(invitationListener);
    618         }
    619     }
    620 
    621     private void fireOfferRequestEvent(OfferRequestProvider.OfferRequestPacket requestPacket) {
    622         Offer offer = new Offer(this.connection, this, requestPacket.getUserID(),
    623                 requestPacket.getUserJID(), this.getWorkgroupJID(),
    624                 new Date((new Date()).getTime() + (requestPacket.getTimeout() * 1000)),
    625                 requestPacket.getSessionID(), requestPacket.getMetaData(), requestPacket.getContent());
    626 
    627         synchronized (offerListeners) {
    628             for (OfferListener listener : offerListeners) {
    629                 listener.offerReceived(offer);
    630             }
    631         }
    632     }
    633 
    634     private void fireOfferRevokeEvent(OfferRevokeProvider.OfferRevokePacket orp) {
    635         RevokedOffer revokedOffer = new RevokedOffer(orp.getUserJID(), orp.getUserID(),
    636                 this.getWorkgroupJID(), orp.getSessionID(), orp.getReason(), new Date());
    637 
    638         synchronized (offerListeners) {
    639             for (OfferListener listener : offerListeners) {
    640                 listener.offerRevoked(revokedOffer);
    641             }
    642         }
    643     }
    644 
    645     private void fireInvitationEvent(String groupChatJID, String sessionID, String body,
    646                                      String from, Map<String, List<String>> metaData) {
    647         WorkgroupInvitation invitation = new WorkgroupInvitation(connection.getUser(), groupChatJID,
    648                 workgroupJID, sessionID, body, from, metaData);
    649 
    650         synchronized (invitationListeners) {
    651             for (WorkgroupInvitationListener listener : invitationListeners) {
    652                 listener.invitationReceived(invitation);
    653             }
    654         }
    655     }
    656 
    657     private void fireQueueUsersEvent(WorkgroupQueue queue, WorkgroupQueue.Status status,
    658                                      int averageWaitTime, Date oldestEntry, Set<QueueUser> users) {
    659         synchronized (queueUsersListeners) {
    660             for (QueueUsersListener listener : queueUsersListeners) {
    661                 if (status != null) {
    662                     listener.statusUpdated(queue, status);
    663                 }
    664                 if (averageWaitTime != -1) {
    665                     listener.averageWaitTimeUpdated(queue, averageWaitTime);
    666                 }
    667                 if (oldestEntry != null) {
    668                     listener.oldestEntryUpdated(queue, oldestEntry);
    669                 }
    670                 if (users != null) {
    671                     listener.usersUpdated(queue, users);
    672                 }
    673             }
    674         }
    675     }
    676 
    677     // PacketListener Implementation.
    678 
    679     private void handlePacket(Packet packet) {
    680         if (packet instanceof OfferRequestProvider.OfferRequestPacket) {
    681             // Acknowledge the IQ set.
    682             IQ reply = new IQ() {
    683                 public String getChildElementXML() {
    684                     return null;
    685                 }
    686             };
    687             reply.setPacketID(packet.getPacketID());
    688             reply.setTo(packet.getFrom());
    689             reply.setType(IQ.Type.RESULT);
    690             connection.sendPacket(reply);
    691 
    692             fireOfferRequestEvent((OfferRequestProvider.OfferRequestPacket)packet);
    693         }
    694         else if (packet instanceof Presence) {
    695             Presence presence = (Presence)packet;
    696 
    697             // The workgroup can send us a number of different presence packets. We
    698             // check for different packet extensions to see what type of presence
    699             // packet it is.
    700 
    701             String queueName = StringUtils.parseResource(presence.getFrom());
    702             WorkgroupQueue queue = queues.get(queueName);
    703             // If there isn't already an entry for the queue, create a new one.
    704             if (queue == null) {
    705                 queue = new WorkgroupQueue(queueName);
    706                 queues.put(queueName, queue);
    707             }
    708 
    709             // QueueOverview packet extensions contain basic information about a queue.
    710             QueueOverview queueOverview = (QueueOverview)presence.getExtension(QueueOverview.ELEMENT_NAME, QueueOverview.NAMESPACE);
    711             if (queueOverview != null) {
    712                 if (queueOverview.getStatus() == null) {
    713                     queue.setStatus(WorkgroupQueue.Status.CLOSED);
    714                 }
    715                 else {
    716                     queue.setStatus(queueOverview.getStatus());
    717                 }
    718                 queue.setAverageWaitTime(queueOverview.getAverageWaitTime());
    719                 queue.setOldestEntry(queueOverview.getOldestEntry());
    720                 // Fire event.
    721                 fireQueueUsersEvent(queue, queueOverview.getStatus(),
    722                         queueOverview.getAverageWaitTime(), queueOverview.getOldestEntry(),
    723                         null);
    724                 return;
    725             }
    726 
    727             // QueueDetails packet extensions contain information about the users in
    728             // a queue.
    729             QueueDetails queueDetails = (QueueDetails)packet.getExtension(QueueDetails.ELEMENT_NAME, QueueDetails.NAMESPACE);
    730             if (queueDetails != null) {
    731                 queue.setUsers(queueDetails.getUsers());
    732                 // Fire event.
    733                 fireQueueUsersEvent(queue, null, -1, null, queueDetails.getUsers());
    734                 return;
    735             }
    736 
    737             // Notify agent packets gives an overview of agent activity in a queue.
    738             DefaultPacketExtension notifyAgents = (DefaultPacketExtension)presence.getExtension("notify-agents", "http://jabber.org/protocol/workgroup");
    739             if (notifyAgents != null) {
    740                 int currentChats = Integer.parseInt(notifyAgents.getValue("current-chats"));
    741                 int maxChats = Integer.parseInt(notifyAgents.getValue("max-chats"));
    742                 queue.setCurrentChats(currentChats);
    743                 queue.setMaxChats(maxChats);
    744                 // Fire event.
    745                 // TODO: might need another event for current chats and max chats of queue
    746                 return;
    747             }
    748         }
    749         else if (packet instanceof Message) {
    750             Message message = (Message)packet;
    751 
    752             // Check if a room invitation was sent and if the sender is the workgroup
    753             MUCUser mucUser = (MUCUser)message.getExtension("x",
    754                     "http://jabber.org/protocol/muc#user");
    755             MUCUser.Invite invite = mucUser != null ? mucUser.getInvite() : null;
    756             if (invite != null && workgroupJID.equals(invite.getFrom())) {
    757                 String sessionID = null;
    758                 Map<String, List<String>> metaData = null;
    759 
    760                 SessionID sessionIDExt = (SessionID)message.getExtension(SessionID.ELEMENT_NAME,
    761                         SessionID.NAMESPACE);
    762                 if (sessionIDExt != null) {
    763                     sessionID = sessionIDExt.getSessionID();
    764                 }
    765 
    766                 MetaData metaDataExt = (MetaData)message.getExtension(MetaData.ELEMENT_NAME,
    767                         MetaData.NAMESPACE);
    768                 if (metaDataExt != null) {
    769                     metaData = metaDataExt.getMetaData();
    770                 }
    771 
    772                 this.fireInvitationEvent(message.getFrom(), sessionID, message.getBody(),
    773                         message.getFrom(), metaData);
    774             }
    775         }
    776         else if (packet instanceof OfferRevokeProvider.OfferRevokePacket) {
    777             // Acknowledge the IQ set.
    778             IQ reply = new IQ() {
    779                 public String getChildElementXML() {
    780                     return null;
    781                 }
    782             };
    783             reply.setPacketID(packet.getPacketID());
    784             reply.setType(IQ.Type.RESULT);
    785             connection.sendPacket(reply);
    786 
    787             fireOfferRevokeEvent((OfferRevokeProvider.OfferRevokePacket)packet);
    788         }
    789     }
    790 
    791     /**
    792      * Creates a ChatNote that will be mapped to the given chat session.
    793      *
    794      * @param sessionID the session id of a Chat Session.
    795      * @param note      the chat note to add.
    796      * @throws XMPPException is thrown if an error occurs while adding the note.
    797      */
    798     public void setNote(String sessionID, String note) throws XMPPException {
    799         note = ChatNotes.replace(note, "\n", "\\n");
    800         note = StringUtils.escapeForXML(note);
    801 
    802         ChatNotes notes = new ChatNotes();
    803         notes.setType(IQ.Type.SET);
    804         notes.setTo(workgroupJID);
    805         notes.setSessionID(sessionID);
    806         notes.setNotes(note);
    807         PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(notes.getPacketID()));
    808         // Send the request
    809         connection.sendPacket(notes);
    810 
    811         IQ response = (IQ)collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
    812 
    813         // Cancel the collector.
    814         collector.cancel();
    815         if (response == null) {
    816             throw new XMPPException("No response from server on status set.");
    817         }
    818         if (response.getError() != null) {
    819             throw new XMPPException(response.getError());
    820         }
    821     }
    822 
    823     /**
    824      * Retrieves the ChatNote associated with a given chat session.
    825      *
    826      * @param sessionID the sessionID of the chat session.
    827      * @return the <code>ChatNote</code> associated with a given chat session.
    828      * @throws XMPPException if an error occurs while retrieving the ChatNote.
    829      */
    830     public ChatNotes getNote(String sessionID) throws XMPPException {
    831         ChatNotes request = new ChatNotes();
    832         request.setType(IQ.Type.GET);
    833         request.setTo(workgroupJID);
    834         request.setSessionID(sessionID);
    835 
    836         PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(request.getPacketID()));
    837         connection.sendPacket(request);
    838 
    839         ChatNotes response = (ChatNotes)collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
    840 
    841         // Cancel the collector.
    842         collector.cancel();
    843         if (response == null) {
    844             throw new XMPPException("No response from server.");
    845         }
    846         if (response.getError() != null) {
    847             throw new XMPPException(response.getError());
    848         }
    849         return response;
    850 
    851     }
    852 
    853     /**
    854      * Retrieves the AgentChatHistory associated with a particular agent jid.
    855      *
    856      * @param jid the jid of the agent.
    857      * @param maxSessions the max number of sessions to retrieve.
    858      * @param startDate the starting date of sessions to retrieve.
    859      * @return the chat history associated with a given jid.
    860      * @throws XMPPException if an error occurs while retrieving the AgentChatHistory.
    861      */
    862     public AgentChatHistory getAgentHistory(String jid, int maxSessions, Date startDate) throws XMPPException {
    863         AgentChatHistory request;
    864         if (startDate != null) {
    865             request = new AgentChatHistory(jid, maxSessions, startDate);
    866         }
    867         else {
    868             request = new AgentChatHistory(jid, maxSessions);
    869         }
    870 
    871         request.setType(IQ.Type.GET);
    872         request.setTo(workgroupJID);
    873 
    874         PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(request.getPacketID()));
    875         connection.sendPacket(request);
    876 
    877         AgentChatHistory response = (AgentChatHistory)collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
    878 
    879         // Cancel the collector.
    880         collector.cancel();
    881         if (response == null) {
    882             throw new XMPPException("No response from server.");
    883         }
    884         if (response.getError() != null) {
    885             throw new XMPPException(response.getError());
    886         }
    887         return response;
    888     }
    889 
    890     /**
    891      * Asks the workgroup for it's Search Settings.
    892      *
    893      * @return SearchSettings the search settings for this workgroup.
    894      * @throws XMPPException if an error occurs while getting information from the server.
    895      */
    896     public SearchSettings getSearchSettings() throws XMPPException {
    897         SearchSettings request = new SearchSettings();
    898         request.setType(IQ.Type.GET);
    899         request.setTo(workgroupJID);
    900 
    901         PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(request.getPacketID()));
    902         connection.sendPacket(request);
    903 
    904 
    905         SearchSettings response = (SearchSettings)collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
    906 
    907         // Cancel the collector.
    908         collector.cancel();
    909         if (response == null) {
    910             throw new XMPPException("No response from server.");
    911         }
    912         if (response.getError() != null) {
    913             throw new XMPPException(response.getError());
    914         }
    915         return response;
    916     }
    917 
    918     /**
    919      * Asks the workgroup for it's Global Macros.
    920      *
    921      * @param global true to retrieve global macros, otherwise false for personal macros.
    922      * @return MacroGroup the root macro group.
    923      * @throws XMPPException if an error occurs while getting information from the server.
    924      */
    925     public MacroGroup getMacros(boolean global) throws XMPPException {
    926         Macros request = new Macros();
    927         request.setType(IQ.Type.GET);
    928         request.setTo(workgroupJID);
    929         request.setPersonal(!global);
    930 
    931         PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(request.getPacketID()));
    932         connection.sendPacket(request);
    933 
    934 
    935         Macros response = (Macros)collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
    936 
    937         // Cancel the collector.
    938         collector.cancel();
    939         if (response == null) {
    940             throw new XMPPException("No response from server.");
    941         }
    942         if (response.getError() != null) {
    943             throw new XMPPException(response.getError());
    944         }
    945         return response.getRootGroup();
    946     }
    947 
    948     /**
    949      * Persists the Personal Macro for an agent.
    950      *
    951      * @param group the macro group to save.
    952      * @throws XMPPException if an error occurs while getting information from the server.
    953      */
    954     public void saveMacros(MacroGroup group) throws XMPPException {
    955         Macros request = new Macros();
    956         request.setType(IQ.Type.SET);
    957         request.setTo(workgroupJID);
    958         request.setPersonal(true);
    959         request.setPersonalMacroGroup(group);
    960 
    961         PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(request.getPacketID()));
    962         connection.sendPacket(request);
    963 
    964 
    965         IQ response = (IQ)collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
    966 
    967         // Cancel the collector.
    968         collector.cancel();
    969         if (response == null) {
    970             throw new XMPPException("No response from server on status set.");
    971         }
    972         if (response.getError() != null) {
    973             throw new XMPPException(response.getError());
    974         }
    975     }
    976 
    977     /**
    978      * Query for metadata associated with a session id.
    979      *
    980      * @param sessionID the sessionID to query for.
    981      * @return Map a map of all metadata associated with the sessionID.
    982      * @throws XMPPException if an error occurs while getting information from the server.
    983      */
    984     public Map<String, List<String>> getChatMetadata(String sessionID) throws XMPPException {
    985         ChatMetadata request = new ChatMetadata();
    986         request.setType(IQ.Type.GET);
    987         request.setTo(workgroupJID);
    988         request.setSessionID(sessionID);
    989 
    990         PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(request.getPacketID()));
    991         connection.sendPacket(request);
    992 
    993 
    994         ChatMetadata response = (ChatMetadata)collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
    995 
    996         // Cancel the collector.
    997         collector.cancel();
    998         if (response == null) {
    999             throw new XMPPException("No response from server.");
   1000         }
   1001         if (response.getError() != null) {
   1002             throw new XMPPException(response.getError());
   1003         }
   1004         return response.getMetadata();
   1005     }
   1006 
   1007     /**
   1008      * Invites a user or agent to an existing session support. The provided invitee's JID can be of
   1009      * a user, an agent, a queue or a workgroup. In the case of a queue or a workgroup the workgroup service
   1010      * will decide the best agent to receive the invitation.<p>
   1011      *
   1012      * This method will return either when the service returned an ACK of the request or if an error occured
   1013      * while requesting the invitation. After sending the ACK the service will send the invitation to the target
   1014      * entity. When dealing with agents the common sequence of offer-response will be followed. However, when
   1015      * sending an invitation to a user a standard MUC invitation will be sent.<p>
   1016      *
   1017      * The agent or user that accepted the offer <b>MUST</b> join the room. Failing to do so will make
   1018      * the invitation to fail. The inviter will eventually receive a message error indicating that the invitee
   1019      * accepted the offer but failed to join the room.
   1020      *
   1021      * Different situations may lead to a failed invitation. Possible cases are: 1) all agents rejected the
   1022      * offer and ther are no agents available, 2) the agent that accepted the offer failed to join the room or
   1023      * 2) the user that received the MUC invitation never replied or joined the room. In any of these cases
   1024      * (or other failing cases) the inviter will get an error message with the failed notification.
   1025      *
   1026      * @param type type of entity that will get the invitation.
   1027      * @param invitee JID of entity that will get the invitation.
   1028      * @param sessionID ID of the support session that the invitee is being invited.
   1029      * @param reason the reason of the invitation.
   1030      * @throws XMPPException if the sender of the invitation is not an agent or the service failed to process
   1031      *         the request.
   1032      */
   1033     public void sendRoomInvitation(RoomInvitation.Type type, String invitee, String sessionID, String reason)
   1034             throws XMPPException {
   1035         final RoomInvitation invitation = new RoomInvitation(type, invitee, sessionID, reason);
   1036         IQ iq = new IQ() {
   1037 
   1038             public String getChildElementXML() {
   1039                 return invitation.toXML();
   1040             }
   1041         };
   1042         iq.setType(IQ.Type.SET);
   1043         iq.setTo(workgroupJID);
   1044         iq.setFrom(connection.getUser());
   1045 
   1046         PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(iq.getPacketID()));
   1047         connection.sendPacket(iq);
   1048 
   1049         IQ response = (IQ)collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
   1050 
   1051         // Cancel the collector.
   1052         collector.cancel();
   1053         if (response == null) {
   1054             throw new XMPPException("No response from server.");
   1055         }
   1056         if (response.getError() != null) {
   1057             throw new XMPPException(response.getError());
   1058         }
   1059     }
   1060 
   1061     /**
   1062      * Transfer an existing session support to another user or agent. The provided invitee's JID can be of
   1063      * a user, an agent, a queue or a workgroup. In the case of a queue or a workgroup the workgroup service
   1064      * will decide the best agent to receive the invitation.<p>
   1065      *
   1066      * This method will return either when the service returned an ACK of the request or if an error occured
   1067      * while requesting the transfer. After sending the ACK the service will send the invitation to the target
   1068      * entity. When dealing with agents the common sequence of offer-response will be followed. However, when
   1069      * sending an invitation to a user a standard MUC invitation will be sent.<p>
   1070      *
   1071      * Once the invitee joins the support room the workgroup service will kick the inviter from the room.<p>
   1072      *
   1073      * Different situations may lead to a failed transfers. Possible cases are: 1) all agents rejected the
   1074      * offer and there are no agents available, 2) the agent that accepted the offer failed to join the room
   1075      * or 2) the user that received the MUC invitation never replied or joined the room. In any of these cases
   1076      * (or other failing cases) the inviter will get an error message with the failed notification.
   1077      *
   1078      * @param type type of entity that will get the invitation.
   1079      * @param invitee JID of entity that will get the invitation.
   1080      * @param sessionID ID of the support session that the invitee is being invited.
   1081      * @param reason the reason of the invitation.
   1082      * @throws XMPPException if the sender of the invitation is not an agent or the service failed to process
   1083      *         the request.
   1084      */
   1085     public void sendRoomTransfer(RoomTransfer.Type type, String invitee, String sessionID, String reason)
   1086             throws XMPPException {
   1087         final RoomTransfer transfer = new RoomTransfer(type, invitee, sessionID, reason);
   1088         IQ iq = new IQ() {
   1089 
   1090             public String getChildElementXML() {
   1091                 return transfer.toXML();
   1092             }
   1093         };
   1094         iq.setType(IQ.Type.SET);
   1095         iq.setTo(workgroupJID);
   1096         iq.setFrom(connection.getUser());
   1097 
   1098         PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(iq.getPacketID()));
   1099         connection.sendPacket(iq);
   1100 
   1101         IQ response = (IQ)collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
   1102 
   1103         // Cancel the collector.
   1104         collector.cancel();
   1105         if (response == null) {
   1106             throw new XMPPException("No response from server.");
   1107         }
   1108         if (response.getError() != null) {
   1109             throw new XMPPException(response.getError());
   1110         }
   1111     }
   1112 
   1113     /**
   1114      * Returns the generic metadata of the workgroup the agent belongs to.
   1115      *
   1116      * @param con   the Connection to use.
   1117      * @param query an optional query object used to tell the server what metadata to retrieve. This can be null.
   1118      * @throws XMPPException if an error occurs while sending the request to the server.
   1119      * @return the settings for the workgroup.
   1120      */
   1121     public GenericSettings getGenericSettings(Connection con, String query) throws XMPPException {
   1122         GenericSettings setting = new GenericSettings();
   1123         setting.setType(IQ.Type.GET);
   1124         setting.setTo(workgroupJID);
   1125 
   1126         PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(setting.getPacketID()));
   1127         connection.sendPacket(setting);
   1128 
   1129         GenericSettings response = (GenericSettings)collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
   1130 
   1131         // Cancel the collector.
   1132         collector.cancel();
   1133         if (response == null) {
   1134             throw new XMPPException("No response from server on status set.");
   1135         }
   1136         if (response.getError() != null) {
   1137             throw new XMPPException(response.getError());
   1138         }
   1139         return response;
   1140     }
   1141 
   1142     public boolean hasMonitorPrivileges(Connection con) throws XMPPException {
   1143         MonitorPacket request = new MonitorPacket();
   1144         request.setType(IQ.Type.GET);
   1145         request.setTo(workgroupJID);
   1146 
   1147         PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(request.getPacketID()));
   1148         connection.sendPacket(request);
   1149 
   1150         MonitorPacket response = (MonitorPacket)collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
   1151 
   1152         // Cancel the collector.
   1153         collector.cancel();
   1154         if (response == null) {
   1155             throw new XMPPException("No response from server on status set.");
   1156         }
   1157         if (response.getError() != null) {
   1158             throw new XMPPException(response.getError());
   1159         }
   1160         return response.isMonitor();
   1161 
   1162     }
   1163 
   1164     public void makeRoomOwner(Connection con, String sessionID) throws XMPPException {
   1165         MonitorPacket request = new MonitorPacket();
   1166         request.setType(IQ.Type.SET);
   1167         request.setTo(workgroupJID);
   1168         request.setSessionID(sessionID);
   1169 
   1170 
   1171         PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(request.getPacketID()));
   1172         connection.sendPacket(request);
   1173 
   1174         Packet response = collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
   1175 
   1176         // Cancel the collector.
   1177         collector.cancel();
   1178         if (response == null) {
   1179             throw new XMPPException("No response from server on status set.");
   1180         }
   1181         if (response.getError() != null) {
   1182             throw new XMPPException(response.getError());
   1183         }
   1184     }
   1185 }