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