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