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