| /** |
| * $Revision$ |
| * $Date$ |
| * |
| * Copyright 2003-2007 Jive Software. |
| * |
| * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package org.jivesoftware.smackx.workgroup.agent; |
| |
| import org.jivesoftware.smackx.workgroup.MetaData; |
| import org.jivesoftware.smackx.workgroup.QueueUser; |
| import org.jivesoftware.smackx.workgroup.WorkgroupInvitation; |
| import org.jivesoftware.smackx.workgroup.WorkgroupInvitationListener; |
| import org.jivesoftware.smackx.workgroup.ext.history.AgentChatHistory; |
| import org.jivesoftware.smackx.workgroup.ext.history.ChatMetadata; |
| import org.jivesoftware.smackx.workgroup.ext.macros.MacroGroup; |
| import org.jivesoftware.smackx.workgroup.ext.macros.Macros; |
| import org.jivesoftware.smackx.workgroup.ext.notes.ChatNotes; |
| import org.jivesoftware.smackx.workgroup.packet.*; |
| import org.jivesoftware.smackx.workgroup.settings.GenericSettings; |
| import org.jivesoftware.smackx.workgroup.settings.SearchSettings; |
| import org.jivesoftware.smack.*; |
| import org.jivesoftware.smack.filter.*; |
| import org.jivesoftware.smack.packet.*; |
| import org.jivesoftware.smack.util.StringUtils; |
| import org.jivesoftware.smackx.Form; |
| import org.jivesoftware.smackx.ReportedData; |
| import org.jivesoftware.smackx.packet.MUCUser; |
| |
| import java.util.*; |
| |
| /** |
| * This class embodies the agent's active presence within a given workgroup. The application |
| * should have N instances of this class, where N is the number of workgroups to which the |
| * owning agent of the application belongs. This class provides all functionality that a |
| * session within a given workgroup is expected to have from an agent's perspective -- setting |
| * the status, tracking the status of queues to which the agent belongs within the workgroup, and |
| * dequeuing customers. |
| * |
| * @author Matt Tucker |
| * @author Derek DeMoro |
| */ |
| public class AgentSession { |
| |
| private Connection connection; |
| |
| private String workgroupJID; |
| |
| private boolean online = false; |
| private Presence.Mode presenceMode; |
| private int maxChats; |
| private final Map<String, List<String>> metaData; |
| |
| private Map<String, WorkgroupQueue> queues; |
| |
| private final List<OfferListener> offerListeners; |
| private final List<WorkgroupInvitationListener> invitationListeners; |
| private final List<QueueUsersListener> queueUsersListeners; |
| |
| private AgentRoster agentRoster = null; |
| private TranscriptManager transcriptManager; |
| private TranscriptSearchManager transcriptSearchManager; |
| private Agent agent; |
| private PacketListener packetListener; |
| |
| /** |
| * Constructs a new agent session instance. Note, the {@link #setOnline(boolean)} |
| * method must be called with an argument of <tt>true</tt> to mark the agent |
| * as available to accept chat requests. |
| * |
| * @param connection a connection instance which must have already gone through |
| * authentication. |
| * @param workgroupJID the fully qualified JID of the workgroup. |
| */ |
| public AgentSession(String workgroupJID, Connection connection) { |
| // Login must have been done before passing in connection. |
| if (!connection.isAuthenticated()) { |
| throw new IllegalStateException("Must login to server before creating workgroup."); |
| } |
| |
| this.workgroupJID = workgroupJID; |
| this.connection = connection; |
| this.transcriptManager = new TranscriptManager(connection); |
| this.transcriptSearchManager = new TranscriptSearchManager(connection); |
| |
| this.maxChats = -1; |
| |
| this.metaData = new HashMap<String, List<String>>(); |
| |
| this.queues = new HashMap<String, WorkgroupQueue>(); |
| |
| offerListeners = new ArrayList<OfferListener>(); |
| invitationListeners = new ArrayList<WorkgroupInvitationListener>(); |
| queueUsersListeners = new ArrayList<QueueUsersListener>(); |
| |
| // Create a filter to listen for packets we're interested in. |
| OrFilter filter = new OrFilter(); |
| filter.addFilter(new PacketTypeFilter(OfferRequestProvider.OfferRequestPacket.class)); |
| filter.addFilter(new PacketTypeFilter(OfferRevokeProvider.OfferRevokePacket.class)); |
| filter.addFilter(new PacketTypeFilter(Presence.class)); |
| filter.addFilter(new PacketTypeFilter(Message.class)); |
| |
| packetListener = new PacketListener() { |
| public void processPacket(Packet packet) { |
| try { |
| handlePacket(packet); |
| } |
| catch (Exception e) { |
| e.printStackTrace(); |
| } |
| } |
| }; |
| connection.addPacketListener(packetListener, filter); |
| // Create the agent associated to this session |
| agent = new Agent(connection, workgroupJID); |
| } |
| |
| /** |
| * Close the agent session. The underlying connection will remain opened but the |
| * packet listeners that were added by this agent session will be removed. |
| */ |
| public void close() { |
| connection.removePacketListener(packetListener); |
| } |
| |
| /** |
| * Returns the agent roster for the workgroup, which contains |
| * |
| * @return the AgentRoster |
| */ |
| public AgentRoster getAgentRoster() { |
| if (agentRoster == null) { |
| agentRoster = new AgentRoster(connection, workgroupJID); |
| } |
| |
| // This might be the first time the user has asked for the roster. If so, we |
| // want to wait up to 2 seconds for the server to send back the list of agents. |
| // This behavior shields API users from having to worry about the fact that the |
| // operation is asynchronous, although they'll still have to listen for changes |
| // to the roster. |
| int elapsed = 0; |
| while (!agentRoster.rosterInitialized && elapsed <= 2000) { |
| try { |
| Thread.sleep(500); |
| } |
| catch (Exception e) { |
| // Ignore |
| } |
| elapsed += 500; |
| } |
| return agentRoster; |
| } |
| |
| /** |
| * Returns the agent's current presence mode. |
| * |
| * @return the agent's current presence mode. |
| */ |
| public Presence.Mode getPresenceMode() { |
| return presenceMode; |
| } |
| |
| /** |
| * Returns the maximum number of chats the agent can participate in. |
| * |
| * @return the maximum number of chats the agent can participate in. |
| */ |
| public int getMaxChats() { |
| return maxChats; |
| } |
| |
| /** |
| * Returns true if the agent is online with the workgroup. |
| * |
| * @return true if the agent is online with the workgroup. |
| */ |
| public boolean isOnline() { |
| return online; |
| } |
| |
| /** |
| * Allows the addition of a new key-value pair to the agent's meta data, if the value is |
| * new data, the revised meta data will be rebroadcast in an agent's presence broadcast. |
| * |
| * @param key the meta data key |
| * @param val the non-null meta data value |
| * @throws XMPPException if an exception occurs. |
| */ |
| public void setMetaData(String key, String val) throws XMPPException { |
| synchronized (this.metaData) { |
| List<String> oldVals = metaData.get(key); |
| |
| if ((oldVals == null) || (!oldVals.get(0).equals(val))) { |
| oldVals.set(0, val); |
| |
| setStatus(presenceMode, maxChats); |
| } |
| } |
| } |
| |
| /** |
| * Allows the removal of data from the agent's meta data, if the key represents existing data, |
| * the revised meta data will be rebroadcast in an agent's presence broadcast. |
| * |
| * @param key the meta data key. |
| * @throws XMPPException if an exception occurs. |
| */ |
| public void removeMetaData(String key) throws XMPPException { |
| synchronized (this.metaData) { |
| List<String> oldVal = metaData.remove(key); |
| |
| if (oldVal != null) { |
| setStatus(presenceMode, maxChats); |
| } |
| } |
| } |
| |
| /** |
| * Allows the retrieval of meta data for a specified key. |
| * |
| * @param key the meta data key |
| * @return the meta data value associated with the key or <tt>null</tt> if the meta-data |
| * doesn't exist.. |
| */ |
| public List<String> getMetaData(String key) { |
| return metaData.get(key); |
| } |
| |
| /** |
| * Sets whether the agent is online with the workgroup. If the user tries to go online with |
| * the workgroup but is not allowed to be an agent, an XMPPError with error code 401 will |
| * be thrown. |
| * |
| * @param online true to set the agent as online with the workgroup. |
| * @throws XMPPException if an error occurs setting the online status. |
| */ |
| public void setOnline(boolean online) throws XMPPException { |
| // If the online status hasn't changed, do nothing. |
| if (this.online == online) { |
| return; |
| } |
| |
| Presence presence; |
| |
| // If the user is going online... |
| if (online) { |
| presence = new Presence(Presence.Type.available); |
| presence.setTo(workgroupJID); |
| presence.addExtension(new DefaultPacketExtension(AgentStatus.ELEMENT_NAME, |
| AgentStatus.NAMESPACE)); |
| |
| PacketCollector collector = this.connection.createPacketCollector(new AndFilter(new PacketTypeFilter(Presence.class), new FromContainsFilter(workgroupJID))); |
| |
| connection.sendPacket(presence); |
| |
| presence = (Presence)collector.nextResult(5000); |
| collector.cancel(); |
| if (!presence.isAvailable()) { |
| throw new XMPPException("No response from server on status set."); |
| } |
| |
| if (presence.getError() != null) { |
| throw new XMPPException(presence.getError()); |
| } |
| |
| // We can safely update this iv since we didn't get any error |
| this.online = online; |
| } |
| // Otherwise the user is going offline... |
| else { |
| // Update this iv now since we don't care at this point of any error |
| this.online = online; |
| |
| presence = new Presence(Presence.Type.unavailable); |
| presence.setTo(workgroupJID); |
| presence.addExtension(new DefaultPacketExtension(AgentStatus.ELEMENT_NAME, |
| AgentStatus.NAMESPACE)); |
| connection.sendPacket(presence); |
| } |
| } |
| |
| /** |
| * Sets the agent's current status with the workgroup. The presence mode affects |
| * how offers are routed to the agent. The possible presence modes with their |
| * meanings are as follows:<ul> |
| * <p/> |
| * <li>Presence.Mode.AVAILABLE -- (Default) the agent is available for more chats |
| * (equivalent to Presence.Mode.CHAT). |
| * <li>Presence.Mode.DO_NOT_DISTURB -- the agent is busy and should not be disturbed. |
| * However, special case, or extreme urgency chats may still be offered to the agent. |
| * <li>Presence.Mode.AWAY -- the agent is not available and should not |
| * have a chat routed to them (equivalent to Presence.Mode.EXTENDED_AWAY).</ul> |
| * <p/> |
| * The max chats value is the maximum number of chats the agent is willing to have |
| * routed to them at once. Some servers may be configured to only accept max chat |
| * values in a certain range; for example, between two and five. In that case, the |
| * maxChats value the agent sends may be adjusted by the server to a value within that |
| * range. |
| * |
| * @param presenceMode the presence mode of the agent. |
| * @param maxChats the maximum number of chats the agent is willing to accept. |
| * @throws XMPPException if an error occurs setting the agent status. |
| * @throws IllegalStateException if the agent is not online with the workgroup. |
| */ |
| public void setStatus(Presence.Mode presenceMode, int maxChats) throws XMPPException { |
| setStatus(presenceMode, maxChats, null); |
| } |
| |
| /** |
| * Sets the agent's current status with the workgroup. The presence mode affects how offers |
| * are routed to the agent. The possible presence modes with their meanings are as follows:<ul> |
| * <p/> |
| * <li>Presence.Mode.AVAILABLE -- (Default) the agent is available for more chats |
| * (equivalent to Presence.Mode.CHAT). |
| * <li>Presence.Mode.DO_NOT_DISTURB -- the agent is busy and should not be disturbed. |
| * However, special case, or extreme urgency chats may still be offered to the agent. |
| * <li>Presence.Mode.AWAY -- the agent is not available and should not |
| * have a chat routed to them (equivalent to Presence.Mode.EXTENDED_AWAY).</ul> |
| * <p/> |
| * The max chats value is the maximum number of chats the agent is willing to have routed to |
| * them at once. Some servers may be configured to only accept max chat values in a certain |
| * range; for example, between two and five. In that case, the maxChats value the agent sends |
| * may be adjusted by the server to a value within that range. |
| * |
| * @param presenceMode the presence mode of the agent. |
| * @param maxChats the maximum number of chats the agent is willing to accept. |
| * @param status sets the status message of the presence update. |
| * @throws XMPPException if an error occurs setting the agent status. |
| * @throws IllegalStateException if the agent is not online with the workgroup. |
| */ |
| public void setStatus(Presence.Mode presenceMode, int maxChats, String status) |
| throws XMPPException { |
| if (!online) { |
| throw new IllegalStateException("Cannot set status when the agent is not online."); |
| } |
| |
| if (presenceMode == null) { |
| presenceMode = Presence.Mode.available; |
| } |
| this.presenceMode = presenceMode; |
| this.maxChats = maxChats; |
| |
| Presence presence = new Presence(Presence.Type.available); |
| presence.setMode(presenceMode); |
| presence.setTo(this.getWorkgroupJID()); |
| |
| if (status != null) { |
| presence.setStatus(status); |
| } |
| // Send information about max chats and current chats as a packet extension. |
| DefaultPacketExtension agentStatus = new DefaultPacketExtension(AgentStatus.ELEMENT_NAME, |
| AgentStatus.NAMESPACE); |
| agentStatus.setValue("max-chats", "" + maxChats); |
| presence.addExtension(agentStatus); |
| presence.addExtension(new MetaData(this.metaData)); |
| |
| PacketCollector collector = this.connection.createPacketCollector(new AndFilter(new PacketTypeFilter(Presence.class), new FromContainsFilter(workgroupJID))); |
| |
| this.connection.sendPacket(presence); |
| |
| presence = (Presence)collector.nextResult(5000); |
| collector.cancel(); |
| if (!presence.isAvailable()) { |
| throw new XMPPException("No response from server on status set."); |
| } |
| |
| if (presence.getError() != null) { |
| throw new XMPPException(presence.getError()); |
| } |
| } |
| |
| /** |
| * Sets the agent's current status with the workgroup. The presence mode affects how offers |
| * are routed to the agent. The possible presence modes with their meanings are as follows:<ul> |
| * <p/> |
| * <li>Presence.Mode.AVAILABLE -- (Default) the agent is available for more chats |
| * (equivalent to Presence.Mode.CHAT). |
| * <li>Presence.Mode.DO_NOT_DISTURB -- the agent is busy and should not be disturbed. |
| * However, special case, or extreme urgency chats may still be offered to the agent. |
| * <li>Presence.Mode.AWAY -- the agent is not available and should not |
| * have a chat routed to them (equivalent to Presence.Mode.EXTENDED_AWAY).</ul> |
| * |
| * @param presenceMode the presence mode of the agent. |
| * @param status sets the status message of the presence update. |
| * @throws XMPPException if an error occurs setting the agent status. |
| * @throws IllegalStateException if the agent is not online with the workgroup. |
| */ |
| public void setStatus(Presence.Mode presenceMode, String status) throws XMPPException { |
| if (!online) { |
| throw new IllegalStateException("Cannot set status when the agent is not online."); |
| } |
| |
| if (presenceMode == null) { |
| presenceMode = Presence.Mode.available; |
| } |
| this.presenceMode = presenceMode; |
| |
| Presence presence = new Presence(Presence.Type.available); |
| presence.setMode(presenceMode); |
| presence.setTo(this.getWorkgroupJID()); |
| |
| if (status != null) { |
| presence.setStatus(status); |
| } |
| presence.addExtension(new MetaData(this.metaData)); |
| |
| PacketCollector collector = this.connection.createPacketCollector(new AndFilter(new PacketTypeFilter(Presence.class), |
| new FromContainsFilter(workgroupJID))); |
| |
| this.connection.sendPacket(presence); |
| |
| presence = (Presence)collector.nextResult(5000); |
| collector.cancel(); |
| if (!presence.isAvailable()) { |
| throw new XMPPException("No response from server on status set."); |
| } |
| |
| if (presence.getError() != null) { |
| throw new XMPPException(presence.getError()); |
| } |
| } |
| |
| /** |
| * Removes a user from the workgroup queue. This is an administrative action that the |
| * <p/> |
| * The agent is not guaranteed of having privileges to perform this action; an exception |
| * denying the request may be thrown. |
| * |
| * @param userID the ID of the user to remove. |
| * @throws XMPPException if an exception occurs. |
| */ |
| public void dequeueUser(String userID) throws XMPPException { |
| // todo: this method simply won't work right now. |
| DepartQueuePacket departPacket = new DepartQueuePacket(this.workgroupJID); |
| |
| // PENDING |
| this.connection.sendPacket(departPacket); |
| } |
| |
| /** |
| * Returns the transcripts of a given user. The answer will contain the complete history of |
| * conversations that a user had. |
| * |
| * @param userID the id of the user to get his conversations. |
| * @return the transcripts of a given user. |
| * @throws XMPPException if an error occurs while getting the information. |
| */ |
| public Transcripts getTranscripts(String userID) throws XMPPException { |
| return transcriptManager.getTranscripts(workgroupJID, userID); |
| } |
| |
| /** |
| * Returns the full conversation transcript of a given session. |
| * |
| * @param sessionID the id of the session to get the full transcript. |
| * @return the full conversation transcript of a given session. |
| * @throws XMPPException if an error occurs while getting the information. |
| */ |
| public Transcript getTranscript(String sessionID) throws XMPPException { |
| return transcriptManager.getTranscript(workgroupJID, sessionID); |
| } |
| |
| /** |
| * Returns the Form to use for searching transcripts. It is unlikely that the server |
| * will change the form (without a restart) so it is safe to keep the returned form |
| * for future submissions. |
| * |
| * @return the Form to use for searching transcripts. |
| * @throws XMPPException if an error occurs while sending the request to the server. |
| */ |
| public Form getTranscriptSearchForm() throws XMPPException { |
| return transcriptSearchManager.getSearchForm(StringUtils.parseServer(workgroupJID)); |
| } |
| |
| /** |
| * Submits the completed form and returns the result of the transcript search. The result |
| * will include all the data returned from the server so be careful with the amount of |
| * data that the search may return. |
| * |
| * @param completedForm the filled out search form. |
| * @return the result of the transcript search. |
| * @throws XMPPException if an error occurs while submiting the search to the server. |
| */ |
| public ReportedData searchTranscripts(Form completedForm) throws XMPPException { |
| return transcriptSearchManager.submitSearch(StringUtils.parseServer(workgroupJID), |
| completedForm); |
| } |
| |
| /** |
| * Asks the workgroup for information about the occupants of the specified room. The returned |
| * information will include the real JID of the occupants, the nickname of the user in the |
| * room as well as the date when the user joined the room. |
| * |
| * @param roomID the room to get information about its occupants. |
| * @return information about the occupants of the specified room. |
| * @throws XMPPException if an error occurs while getting information from the server. |
| */ |
| public OccupantsInfo getOccupantsInfo(String roomID) throws XMPPException { |
| OccupantsInfo request = new OccupantsInfo(roomID); |
| request.setType(IQ.Type.GET); |
| request.setTo(workgroupJID); |
| |
| PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(request.getPacketID())); |
| connection.sendPacket(request); |
| |
| OccupantsInfo response = (OccupantsInfo)collector.nextResult(SmackConfiguration.getPacketReplyTimeout()); |
| |
| // Cancel the collector. |
| collector.cancel(); |
| if (response == null) { |
| throw new XMPPException("No response from server."); |
| } |
| if (response.getError() != null) { |
| throw new XMPPException(response.getError()); |
| } |
| return response; |
| } |
| |
| /** |
| * @return the fully-qualified name of the workgroup for which this session exists |
| */ |
| public String getWorkgroupJID() { |
| return workgroupJID; |
| } |
| |
| /** |
| * Returns the Agent associated to this session. |
| * |
| * @return the Agent associated to this session. |
| */ |
| public Agent getAgent() { |
| return agent; |
| } |
| |
| /** |
| * @param queueName the name of the queue |
| * @return an instance of WorkgroupQueue for the argument queue name, or null if none exists |
| */ |
| public WorkgroupQueue getQueue(String queueName) { |
| return queues.get(queueName); |
| } |
| |
| public Iterator<WorkgroupQueue> getQueues() { |
| return Collections.unmodifiableMap((new HashMap<String, WorkgroupQueue>(queues))).values().iterator(); |
| } |
| |
| public void addQueueUsersListener(QueueUsersListener listener) { |
| synchronized (queueUsersListeners) { |
| if (!queueUsersListeners.contains(listener)) { |
| queueUsersListeners.add(listener); |
| } |
| } |
| } |
| |
| public void removeQueueUsersListener(QueueUsersListener listener) { |
| synchronized (queueUsersListeners) { |
| queueUsersListeners.remove(listener); |
| } |
| } |
| |
| /** |
| * Adds an offer listener. |
| * |
| * @param offerListener the offer listener. |
| */ |
| public void addOfferListener(OfferListener offerListener) { |
| synchronized (offerListeners) { |
| if (!offerListeners.contains(offerListener)) { |
| offerListeners.add(offerListener); |
| } |
| } |
| } |
| |
| /** |
| * Removes an offer listener. |
| * |
| * @param offerListener the offer listener. |
| */ |
| public void removeOfferListener(OfferListener offerListener) { |
| synchronized (offerListeners) { |
| offerListeners.remove(offerListener); |
| } |
| } |
| |
| /** |
| * Adds an invitation listener. |
| * |
| * @param invitationListener the invitation listener. |
| */ |
| public void addInvitationListener(WorkgroupInvitationListener invitationListener) { |
| synchronized (invitationListeners) { |
| if (!invitationListeners.contains(invitationListener)) { |
| invitationListeners.add(invitationListener); |
| } |
| } |
| } |
| |
| /** |
| * Removes an invitation listener. |
| * |
| * @param invitationListener the invitation listener. |
| */ |
| public void removeInvitationListener(WorkgroupInvitationListener invitationListener) { |
| synchronized (invitationListeners) { |
| invitationListeners.remove(invitationListener); |
| } |
| } |
| |
| private void fireOfferRequestEvent(OfferRequestProvider.OfferRequestPacket requestPacket) { |
| Offer offer = new Offer(this.connection, this, requestPacket.getUserID(), |
| requestPacket.getUserJID(), this.getWorkgroupJID(), |
| new Date((new Date()).getTime() + (requestPacket.getTimeout() * 1000)), |
| requestPacket.getSessionID(), requestPacket.getMetaData(), requestPacket.getContent()); |
| |
| synchronized (offerListeners) { |
| for (OfferListener listener : offerListeners) { |
| listener.offerReceived(offer); |
| } |
| } |
| } |
| |
| private void fireOfferRevokeEvent(OfferRevokeProvider.OfferRevokePacket orp) { |
| RevokedOffer revokedOffer = new RevokedOffer(orp.getUserJID(), orp.getUserID(), |
| this.getWorkgroupJID(), orp.getSessionID(), orp.getReason(), new Date()); |
| |
| synchronized (offerListeners) { |
| for (OfferListener listener : offerListeners) { |
| listener.offerRevoked(revokedOffer); |
| } |
| } |
| } |
| |
| private void fireInvitationEvent(String groupChatJID, String sessionID, String body, |
| String from, Map<String, List<String>> metaData) { |
| WorkgroupInvitation invitation = new WorkgroupInvitation(connection.getUser(), groupChatJID, |
| workgroupJID, sessionID, body, from, metaData); |
| |
| synchronized (invitationListeners) { |
| for (WorkgroupInvitationListener listener : invitationListeners) { |
| listener.invitationReceived(invitation); |
| } |
| } |
| } |
| |
| private void fireQueueUsersEvent(WorkgroupQueue queue, WorkgroupQueue.Status status, |
| int averageWaitTime, Date oldestEntry, Set<QueueUser> users) { |
| synchronized (queueUsersListeners) { |
| for (QueueUsersListener listener : queueUsersListeners) { |
| if (status != null) { |
| listener.statusUpdated(queue, status); |
| } |
| if (averageWaitTime != -1) { |
| listener.averageWaitTimeUpdated(queue, averageWaitTime); |
| } |
| if (oldestEntry != null) { |
| listener.oldestEntryUpdated(queue, oldestEntry); |
| } |
| if (users != null) { |
| listener.usersUpdated(queue, users); |
| } |
| } |
| } |
| } |
| |
| // PacketListener Implementation. |
| |
| private void handlePacket(Packet packet) { |
| if (packet instanceof OfferRequestProvider.OfferRequestPacket) { |
| // Acknowledge the IQ set. |
| IQ reply = new IQ() { |
| public String getChildElementXML() { |
| return null; |
| } |
| }; |
| reply.setPacketID(packet.getPacketID()); |
| reply.setTo(packet.getFrom()); |
| reply.setType(IQ.Type.RESULT); |
| connection.sendPacket(reply); |
| |
| fireOfferRequestEvent((OfferRequestProvider.OfferRequestPacket)packet); |
| } |
| else if (packet instanceof Presence) { |
| Presence presence = (Presence)packet; |
| |
| // The workgroup can send us a number of different presence packets. We |
| // check for different packet extensions to see what type of presence |
| // packet it is. |
| |
| String queueName = StringUtils.parseResource(presence.getFrom()); |
| WorkgroupQueue queue = queues.get(queueName); |
| // If there isn't already an entry for the queue, create a new one. |
| if (queue == null) { |
| queue = new WorkgroupQueue(queueName); |
| queues.put(queueName, queue); |
| } |
| |
| // QueueOverview packet extensions contain basic information about a queue. |
| QueueOverview queueOverview = (QueueOverview)presence.getExtension(QueueOverview.ELEMENT_NAME, QueueOverview.NAMESPACE); |
| if (queueOverview != null) { |
| if (queueOverview.getStatus() == null) { |
| queue.setStatus(WorkgroupQueue.Status.CLOSED); |
| } |
| else { |
| queue.setStatus(queueOverview.getStatus()); |
| } |
| queue.setAverageWaitTime(queueOverview.getAverageWaitTime()); |
| queue.setOldestEntry(queueOverview.getOldestEntry()); |
| // Fire event. |
| fireQueueUsersEvent(queue, queueOverview.getStatus(), |
| queueOverview.getAverageWaitTime(), queueOverview.getOldestEntry(), |
| null); |
| return; |
| } |
| |
| // QueueDetails packet extensions contain information about the users in |
| // a queue. |
| QueueDetails queueDetails = (QueueDetails)packet.getExtension(QueueDetails.ELEMENT_NAME, QueueDetails.NAMESPACE); |
| if (queueDetails != null) { |
| queue.setUsers(queueDetails.getUsers()); |
| // Fire event. |
| fireQueueUsersEvent(queue, null, -1, null, queueDetails.getUsers()); |
| return; |
| } |
| |
| // Notify agent packets gives an overview of agent activity in a queue. |
| DefaultPacketExtension notifyAgents = (DefaultPacketExtension)presence.getExtension("notify-agents", "http://jabber.org/protocol/workgroup"); |
| if (notifyAgents != null) { |
| int currentChats = Integer.parseInt(notifyAgents.getValue("current-chats")); |
| int maxChats = Integer.parseInt(notifyAgents.getValue("max-chats")); |
| queue.setCurrentChats(currentChats); |
| queue.setMaxChats(maxChats); |
| // Fire event. |
| // TODO: might need another event for current chats and max chats of queue |
| return; |
| } |
| } |
| else if (packet instanceof Message) { |
| Message message = (Message)packet; |
| |
| // Check if a room invitation was sent and if the sender is the workgroup |
| MUCUser mucUser = (MUCUser)message.getExtension("x", |
| "http://jabber.org/protocol/muc#user"); |
| MUCUser.Invite invite = mucUser != null ? mucUser.getInvite() : null; |
| if (invite != null && workgroupJID.equals(invite.getFrom())) { |
| String sessionID = null; |
| Map<String, List<String>> metaData = null; |
| |
| SessionID sessionIDExt = (SessionID)message.getExtension(SessionID.ELEMENT_NAME, |
| SessionID.NAMESPACE); |
| if (sessionIDExt != null) { |
| sessionID = sessionIDExt.getSessionID(); |
| } |
| |
| MetaData metaDataExt = (MetaData)message.getExtension(MetaData.ELEMENT_NAME, |
| MetaData.NAMESPACE); |
| if (metaDataExt != null) { |
| metaData = metaDataExt.getMetaData(); |
| } |
| |
| this.fireInvitationEvent(message.getFrom(), sessionID, message.getBody(), |
| message.getFrom(), metaData); |
| } |
| } |
| else if (packet instanceof OfferRevokeProvider.OfferRevokePacket) { |
| // Acknowledge the IQ set. |
| IQ reply = new IQ() { |
| public String getChildElementXML() { |
| return null; |
| } |
| }; |
| reply.setPacketID(packet.getPacketID()); |
| reply.setType(IQ.Type.RESULT); |
| connection.sendPacket(reply); |
| |
| fireOfferRevokeEvent((OfferRevokeProvider.OfferRevokePacket)packet); |
| } |
| } |
| |
| /** |
| * Creates a ChatNote that will be mapped to the given chat session. |
| * |
| * @param sessionID the session id of a Chat Session. |
| * @param note the chat note to add. |
| * @throws XMPPException is thrown if an error occurs while adding the note. |
| */ |
| public void setNote(String sessionID, String note) throws XMPPException { |
| note = ChatNotes.replace(note, "\n", "\\n"); |
| note = StringUtils.escapeForXML(note); |
| |
| ChatNotes notes = new ChatNotes(); |
| notes.setType(IQ.Type.SET); |
| notes.setTo(workgroupJID); |
| notes.setSessionID(sessionID); |
| notes.setNotes(note); |
| PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(notes.getPacketID())); |
| // Send the request |
| connection.sendPacket(notes); |
| |
| IQ response = (IQ)collector.nextResult(SmackConfiguration.getPacketReplyTimeout()); |
| |
| // Cancel the collector. |
| collector.cancel(); |
| if (response == null) { |
| throw new XMPPException("No response from server on status set."); |
| } |
| if (response.getError() != null) { |
| throw new XMPPException(response.getError()); |
| } |
| } |
| |
| /** |
| * Retrieves the ChatNote associated with a given chat session. |
| * |
| * @param sessionID the sessionID of the chat session. |
| * @return the <code>ChatNote</code> associated with a given chat session. |
| * @throws XMPPException if an error occurs while retrieving the ChatNote. |
| */ |
| public ChatNotes getNote(String sessionID) throws XMPPException { |
| ChatNotes request = new ChatNotes(); |
| request.setType(IQ.Type.GET); |
| request.setTo(workgroupJID); |
| request.setSessionID(sessionID); |
| |
| PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(request.getPacketID())); |
| connection.sendPacket(request); |
| |
| ChatNotes response = (ChatNotes)collector.nextResult(SmackConfiguration.getPacketReplyTimeout()); |
| |
| // Cancel the collector. |
| collector.cancel(); |
| if (response == null) { |
| throw new XMPPException("No response from server."); |
| } |
| if (response.getError() != null) { |
| throw new XMPPException(response.getError()); |
| } |
| return response; |
| |
| } |
| |
| /** |
| * Retrieves the AgentChatHistory associated with a particular agent jid. |
| * |
| * @param jid the jid of the agent. |
| * @param maxSessions the max number of sessions to retrieve. |
| * @param startDate the starting date of sessions to retrieve. |
| * @return the chat history associated with a given jid. |
| * @throws XMPPException if an error occurs while retrieving the AgentChatHistory. |
| */ |
| public AgentChatHistory getAgentHistory(String jid, int maxSessions, Date startDate) throws XMPPException { |
| AgentChatHistory request; |
| if (startDate != null) { |
| request = new AgentChatHistory(jid, maxSessions, startDate); |
| } |
| else { |
| request = new AgentChatHistory(jid, maxSessions); |
| } |
| |
| request.setType(IQ.Type.GET); |
| request.setTo(workgroupJID); |
| |
| PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(request.getPacketID())); |
| connection.sendPacket(request); |
| |
| AgentChatHistory response = (AgentChatHistory)collector.nextResult(SmackConfiguration.getPacketReplyTimeout()); |
| |
| // Cancel the collector. |
| collector.cancel(); |
| if (response == null) { |
| throw new XMPPException("No response from server."); |
| } |
| if (response.getError() != null) { |
| throw new XMPPException(response.getError()); |
| } |
| return response; |
| } |
| |
| /** |
| * Asks the workgroup for it's Search Settings. |
| * |
| * @return SearchSettings the search settings for this workgroup. |
| * @throws XMPPException if an error occurs while getting information from the server. |
| */ |
| public SearchSettings getSearchSettings() throws XMPPException { |
| SearchSettings request = new SearchSettings(); |
| request.setType(IQ.Type.GET); |
| request.setTo(workgroupJID); |
| |
| PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(request.getPacketID())); |
| connection.sendPacket(request); |
| |
| |
| SearchSettings response = (SearchSettings)collector.nextResult(SmackConfiguration.getPacketReplyTimeout()); |
| |
| // Cancel the collector. |
| collector.cancel(); |
| if (response == null) { |
| throw new XMPPException("No response from server."); |
| } |
| if (response.getError() != null) { |
| throw new XMPPException(response.getError()); |
| } |
| return response; |
| } |
| |
| /** |
| * Asks the workgroup for it's Global Macros. |
| * |
| * @param global true to retrieve global macros, otherwise false for personal macros. |
| * @return MacroGroup the root macro group. |
| * @throws XMPPException if an error occurs while getting information from the server. |
| */ |
| public MacroGroup getMacros(boolean global) throws XMPPException { |
| Macros request = new Macros(); |
| request.setType(IQ.Type.GET); |
| request.setTo(workgroupJID); |
| request.setPersonal(!global); |
| |
| PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(request.getPacketID())); |
| connection.sendPacket(request); |
| |
| |
| Macros response = (Macros)collector.nextResult(SmackConfiguration.getPacketReplyTimeout()); |
| |
| // Cancel the collector. |
| collector.cancel(); |
| if (response == null) { |
| throw new XMPPException("No response from server."); |
| } |
| if (response.getError() != null) { |
| throw new XMPPException(response.getError()); |
| } |
| return response.getRootGroup(); |
| } |
| |
| /** |
| * Persists the Personal Macro for an agent. |
| * |
| * @param group the macro group to save. |
| * @throws XMPPException if an error occurs while getting information from the server. |
| */ |
| public void saveMacros(MacroGroup group) throws XMPPException { |
| Macros request = new Macros(); |
| request.setType(IQ.Type.SET); |
| request.setTo(workgroupJID); |
| request.setPersonal(true); |
| request.setPersonalMacroGroup(group); |
| |
| PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(request.getPacketID())); |
| connection.sendPacket(request); |
| |
| |
| IQ response = (IQ)collector.nextResult(SmackConfiguration.getPacketReplyTimeout()); |
| |
| // Cancel the collector. |
| collector.cancel(); |
| if (response == null) { |
| throw new XMPPException("No response from server on status set."); |
| } |
| if (response.getError() != null) { |
| throw new XMPPException(response.getError()); |
| } |
| } |
| |
| /** |
| * Query for metadata associated with a session id. |
| * |
| * @param sessionID the sessionID to query for. |
| * @return Map a map of all metadata associated with the sessionID. |
| * @throws XMPPException if an error occurs while getting information from the server. |
| */ |
| public Map<String, List<String>> getChatMetadata(String sessionID) throws XMPPException { |
| ChatMetadata request = new ChatMetadata(); |
| request.setType(IQ.Type.GET); |
| request.setTo(workgroupJID); |
| request.setSessionID(sessionID); |
| |
| PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(request.getPacketID())); |
| connection.sendPacket(request); |
| |
| |
| ChatMetadata response = (ChatMetadata)collector.nextResult(SmackConfiguration.getPacketReplyTimeout()); |
| |
| // Cancel the collector. |
| collector.cancel(); |
| if (response == null) { |
| throw new XMPPException("No response from server."); |
| } |
| if (response.getError() != null) { |
| throw new XMPPException(response.getError()); |
| } |
| return response.getMetadata(); |
| } |
| |
| /** |
| * Invites a user or agent to an existing session support. The provided invitee's JID can be of |
| * a user, an agent, a queue or a workgroup. In the case of a queue or a workgroup the workgroup service |
| * will decide the best agent to receive the invitation.<p> |
| * |
| * This method will return either when the service returned an ACK of the request or if an error occured |
| * while requesting the invitation. After sending the ACK the service will send the invitation to the target |
| * entity. When dealing with agents the common sequence of offer-response will be followed. However, when |
| * sending an invitation to a user a standard MUC invitation will be sent.<p> |
| * |
| * The agent or user that accepted the offer <b>MUST</b> join the room. Failing to do so will make |
| * the invitation to fail. The inviter will eventually receive a message error indicating that the invitee |
| * accepted the offer but failed to join the room. |
| * |
| * Different situations may lead to a failed invitation. Possible cases are: 1) all agents rejected the |
| * offer and ther are no agents available, 2) the agent that accepted the offer failed to join the room or |
| * 2) the user that received the MUC invitation never replied or joined the room. In any of these cases |
| * (or other failing cases) the inviter will get an error message with the failed notification. |
| * |
| * @param type type of entity that will get the invitation. |
| * @param invitee JID of entity that will get the invitation. |
| * @param sessionID ID of the support session that the invitee is being invited. |
| * @param reason the reason of the invitation. |
| * @throws XMPPException if the sender of the invitation is not an agent or the service failed to process |
| * the request. |
| */ |
| public void sendRoomInvitation(RoomInvitation.Type type, String invitee, String sessionID, String reason) |
| throws XMPPException { |
| final RoomInvitation invitation = new RoomInvitation(type, invitee, sessionID, reason); |
| IQ iq = new IQ() { |
| |
| public String getChildElementXML() { |
| return invitation.toXML(); |
| } |
| }; |
| iq.setType(IQ.Type.SET); |
| iq.setTo(workgroupJID); |
| iq.setFrom(connection.getUser()); |
| |
| PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(iq.getPacketID())); |
| connection.sendPacket(iq); |
| |
| IQ response = (IQ)collector.nextResult(SmackConfiguration.getPacketReplyTimeout()); |
| |
| // Cancel the collector. |
| collector.cancel(); |
| if (response == null) { |
| throw new XMPPException("No response from server."); |
| } |
| if (response.getError() != null) { |
| throw new XMPPException(response.getError()); |
| } |
| } |
| |
| /** |
| * Transfer an existing session support to another user or agent. The provided invitee's JID can be of |
| * a user, an agent, a queue or a workgroup. In the case of a queue or a workgroup the workgroup service |
| * will decide the best agent to receive the invitation.<p> |
| * |
| * This method will return either when the service returned an ACK of the request or if an error occured |
| * while requesting the transfer. After sending the ACK the service will send the invitation to the target |
| * entity. When dealing with agents the common sequence of offer-response will be followed. However, when |
| * sending an invitation to a user a standard MUC invitation will be sent.<p> |
| * |
| * Once the invitee joins the support room the workgroup service will kick the inviter from the room.<p> |
| * |
| * Different situations may lead to a failed transfers. Possible cases are: 1) all agents rejected the |
| * offer and there are no agents available, 2) the agent that accepted the offer failed to join the room |
| * or 2) the user that received the MUC invitation never replied or joined the room. In any of these cases |
| * (or other failing cases) the inviter will get an error message with the failed notification. |
| * |
| * @param type type of entity that will get the invitation. |
| * @param invitee JID of entity that will get the invitation. |
| * @param sessionID ID of the support session that the invitee is being invited. |
| * @param reason the reason of the invitation. |
| * @throws XMPPException if the sender of the invitation is not an agent or the service failed to process |
| * the request. |
| */ |
| public void sendRoomTransfer(RoomTransfer.Type type, String invitee, String sessionID, String reason) |
| throws XMPPException { |
| final RoomTransfer transfer = new RoomTransfer(type, invitee, sessionID, reason); |
| IQ iq = new IQ() { |
| |
| public String getChildElementXML() { |
| return transfer.toXML(); |
| } |
| }; |
| iq.setType(IQ.Type.SET); |
| iq.setTo(workgroupJID); |
| iq.setFrom(connection.getUser()); |
| |
| PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(iq.getPacketID())); |
| connection.sendPacket(iq); |
| |
| IQ response = (IQ)collector.nextResult(SmackConfiguration.getPacketReplyTimeout()); |
| |
| // Cancel the collector. |
| collector.cancel(); |
| if (response == null) { |
| throw new XMPPException("No response from server."); |
| } |
| if (response.getError() != null) { |
| throw new XMPPException(response.getError()); |
| } |
| } |
| |
| /** |
| * Returns the generic metadata of the workgroup the agent belongs to. |
| * |
| * @param con the Connection to use. |
| * @param query an optional query object used to tell the server what metadata to retrieve. This can be null. |
| * @throws XMPPException if an error occurs while sending the request to the server. |
| * @return the settings for the workgroup. |
| */ |
| public GenericSettings getGenericSettings(Connection con, String query) throws XMPPException { |
| GenericSettings setting = new GenericSettings(); |
| setting.setType(IQ.Type.GET); |
| setting.setTo(workgroupJID); |
| |
| PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(setting.getPacketID())); |
| connection.sendPacket(setting); |
| |
| GenericSettings response = (GenericSettings)collector.nextResult(SmackConfiguration.getPacketReplyTimeout()); |
| |
| // Cancel the collector. |
| collector.cancel(); |
| if (response == null) { |
| throw new XMPPException("No response from server on status set."); |
| } |
| if (response.getError() != null) { |
| throw new XMPPException(response.getError()); |
| } |
| return response; |
| } |
| |
| public boolean hasMonitorPrivileges(Connection con) throws XMPPException { |
| MonitorPacket request = new MonitorPacket(); |
| request.setType(IQ.Type.GET); |
| request.setTo(workgroupJID); |
| |
| PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(request.getPacketID())); |
| connection.sendPacket(request); |
| |
| MonitorPacket response = (MonitorPacket)collector.nextResult(SmackConfiguration.getPacketReplyTimeout()); |
| |
| // Cancel the collector. |
| collector.cancel(); |
| if (response == null) { |
| throw new XMPPException("No response from server on status set."); |
| } |
| if (response.getError() != null) { |
| throw new XMPPException(response.getError()); |
| } |
| return response.isMonitor(); |
| |
| } |
| |
| public void makeRoomOwner(Connection con, String sessionID) throws XMPPException { |
| MonitorPacket request = new MonitorPacket(); |
| request.setType(IQ.Type.SET); |
| request.setTo(workgroupJID); |
| request.setSessionID(sessionID); |
| |
| |
| PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(request.getPacketID())); |
| connection.sendPacket(request); |
| |
| Packet response = collector.nextResult(SmackConfiguration.getPacketReplyTimeout()); |
| |
| // Cancel the collector. |
| collector.cancel(); |
| if (response == null) { |
| throw new XMPPException("No response from server on status set."); |
| } |
| if (response.getError() != null) { |
| throw new XMPPException(response.getError()); |
| } |
| } |
| } |