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