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