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