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