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