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