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