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