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