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