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