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