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