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