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