/*
* Copyright (C) 2004-2008 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.xmpp.workgroup;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.TimeZone;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import org.dom4j.Element;
import org.jivesoftware.openfire.fastpath.util.TaskEngine;
import org.jivesoftware.util.FastDateFormat;
import org.jivesoftware.util.LocaleUtils;
import org.jivesoftware.xmpp.workgroup.interceptor.InterceptorManager;
import org.jivesoftware.xmpp.workgroup.interceptor.OfferInterceptorManager;
import org.jivesoftware.xmpp.workgroup.interceptor.PacketRejectedException;
import org.jivesoftware.xmpp.workgroup.request.UserRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xmpp.packet.IQ;
import org.xmpp.packet.JID;
import org.xmpp.packet.Packet;
import org.xmpp.packet.Presence;
/**
* <p>A 'live' agent session.</p>
* <p>Agent sessions are only created and maintaned for available agents
* (although they may be show-dnd).</p>
*
* @author Derek DeMoro
*/
public class AgentSession {
private static final Logger Log = LoggerFactory.getLogger(AgentSession.class);
private static final FastDateFormat UTC_FORMAT = FastDateFormat.getInstance("yyyyMMdd'T'HH:mm:ss", TimeZone.getTimeZone("GMT+0"));
private Presence presence;
private Collection<Workgroup> workgroups = new ConcurrentLinkedQueue<Workgroup>();
private Offer offer;
/**
* Flag that indicates if the agent requested to get information of agents of all the workgroups
* where the agent has joined.
*/
private boolean requestedAgentInfo = false;
private Map<Workgroup, Queue<ChatInfo>> chatInfos = new ConcurrentHashMap<Workgroup, Queue<ChatInfo>>();
/**
* By default maxChats has a value of -1 which means that #getMaxChats will return
* the max number of chats per agent defined in the workgroup. The agent may overwrite
* the default value but the new value will not be persisted for future sessions.
*/
private int maxChats;
private Agent agent;
private JID address;
private long id;
private Date lastChatTime;
/**
* @param address the XMPPAddress to create an <code>AgentSession</code>
* @param agent the <code>Agent</code>
*/
public AgentSession(JID address, Agent agent) {
this.id = -1;
this.agent = agent;
this.address = address;
maxChats = -1;
presence = new Presence();
}
public Presence getPresence() {
return presence;
}
/**
* Updates the presence of the AgentSession with the new received presence. The max number of
* chats and number of current chats will be updated if that information was included in the presence.
* If no information was provided then default values of queues, workgroups or general settings will
* be used instead.
*
* @param packet the new presence sent by the agent.
*/
public void updatePresence(Presence packet) {
// Create a copy of the received Presence to use as the presence of the AgentSession
Presence sessionPresence = packet.createCopy();
// Remove the "agent-status" element from the new AgentSession's presence
Element child = sessionPresence.getChildElement("agent-status", "http://jabber.org/protocol/workgroup");
sessionPresence.getElement().remove(child);
// Set the new presence to the AgentSession
presence = sessionPresence;
// Set the new maximum number of chats and the number of current chats to the
// AgentSession based on the values sent within the presence (if any)
Element elem = packet.getChildElement("agent-status", "http://jabber.org/protocol/workgroup");
if (elem != null) {
Iterator<Element> metaIter = elem.elementIterator();
while (metaIter.hasNext()) {
Element agentStatusElement = metaIter.next();
if ("max-chats".equals(agentStatusElement.getName())) {
String maxChats = agentStatusElement.getText();
if (maxChats == null || maxChats.trim().length() == 0) {
setMaxChats(-1);
}
else {
setMaxChats(Integer.parseInt(maxChats));
}
}
}
}
}
public void join(Workgroup workgroup) {
boolean added = false;
boolean alreadyJoined = workgroups.contains(workgroup);
if(!alreadyJoined){
added = workgroups.add(workgroup);
}
for (RequestQueue requestQueue : workgroup.getRequestQueues()) {
if (requestQueue.isMember(getAgent())) {
if (added) {
requestQueue.getAgentSessionList().addAgentSession(this);
}
requestQueue.sendStatus(getJID());
requestQueue.sendDetailedStatus(getJID());
}
}
updateStatus(workgroup);
if (added) {
workgroup.agentJoined(this);
// Initialize the list that will hold the chats in the workgroup
chatInfos.put(workgroup, new ConcurrentLinkedQueue<ChatInfo>());
}
}
public void depart(Workgroup workgroup) {
boolean removed = workgroups.remove(workgroup);
if (removed) {
for (RequestQueue requestQueue : workgroup.getRequestQueues()) {
requestQueue.getAgentSessionList().removeAgentSession(this);
}
if (workgroups.isEmpty()) {
getAgent().closeSession(getJID());
}
updateStatus(workgroup);
workgroup.agentDeparted(this);
}
}
public int getCurrentChats(Workgroup group) {
return getChats(group).size();
}
private void updateStatus(Workgroup workgroup) {
if (getMaxChats(workgroup) > getCurrentChats(workgroup) && offer != null) {
offer.removeRejector(this);
}
sendStatusToAllAgents(workgroup);
// When the agent becomes unavailable he will stop receiving presence updates from other
// agents so the agent will need to request information from agents once again and discard
// the old presences since they are outdated
if (!presence.isAvailable()) {
requestedAgentInfo = false;
}
}
/**
* Sends a packet to this agent with all the agents that belong to this agent's workgroup. This
* packet will be sent only when the agent has requested it.<p>
* <p/>
* Once the list of agents has been sent the agent will start to receive updates when new
* agents are added or removed from the workgroup as well as when agents become available
* or unavailable.
*
* @param packet request made by agent.
* @param workgroup the workgroup whose agents will be sent to the requester.
*/
public void sendAgentsInWorkgroup(IQ packet, final Workgroup workgroup) {
IQ statusPacket = IQ.createResultIQ(packet);
Element agentStatusRequest = statusPacket.setChildElement("agent-status-request",
"http://jabber.org/protocol/workgroup");
for (Agent agentInWorkgroup : workgroup.getAgents()) {
if (agentInWorkgroup == agent) {
continue;
}
// Add the information of the agent
agentStatusRequest.add(agentInWorkgroup.getAgentInfo());
}
// Send the response for this queue
WorkgroupManager.getInstance().send(statusPacket);
// Upate the flag to indicate that the agent has requested information about the other
// agents. This implies that from now on this agent will start to receive presence
// updates from the other workgroup agents when they become unavailable or new agents
// join the workgroup
requestedAgentInfo = true;
// Send the presence of the available agents to this agent
// Note: Execute this process in another thread since we want to release this thread as
// soon as possible so other requests may be read and processed
TaskEngine.getInstance().submit(new Runnable() {
public void run() {
try {
sendStatusOfAllAgents(workgroup);
}
catch (Exception e) {
Log.error("Error sending status of all agents", e);
}
}
});
}
/**
* Sends information of the agent to the agent that requested it.
*
* @param packet the original packet that made the request to obtain the agent's info.
*/
public void sendAgentInfo(IQ packet) {
IQ statusPacket = IQ.createResultIQ(packet);
Element agentInfo = statusPacket.setChildElement("agent-info",
"http://jivesoftware.com/protocol/workgroup");
agentInfo.addElement("jid").setText(getAgent().getAgentJID().toBareJID());
agentInfo.addElement("name").setText(getAgent().getNickname());
// Send the response
WorkgroupManager.getInstance().send(statusPacket);
}
/**
* Sends the presence of each available agent in the workgroup to this agent.
*
* @param workgroup the workgroup whose agents' presences will be sent to this agent.
*/
private void sendStatusOfAllAgents(Workgroup workgroup) {
for (AgentSession agentSession : workgroup.getAgentSessions()) {
if (!agentSession.getJID().equals(address)) {
Presence statusPacket = agentSession.getPresence().createCopy();
statusPacket.setFrom(agentSession.getJID());
statusPacket.setTo(address);
// Add the agent-status element
agentSession.getAgentStatus(statusPacket,workgroup);
workgroup.send(statusPacket);
}
}
}
/**
* Sends this agent's status to all other agents in the Workgroup.
*
* @param workgroup the workgroup whose agent will be notified
*/
private void sendStatusToAllAgents(Workgroup workgroup) {
for (AgentSession agentSession : workgroup.getAgentSessions()) {
// Only send presences to Agents that are available and had requested to
// receive other agents' information
if (agentSession.hasRequestedAgentInfo() && !agentSession.getJID().equals(address)) {
Presence statusPacket = presence.createCopy();
statusPacket.setFrom(address);
statusPacket.setTo(agentSession.getJID());
// Add the agent-status element
getAgentStatus(statusPacket, workgroup);
workgroup.send(statusPacket);
}
}
}
private void getAgentStatus(Presence statusPacket, Workgroup workgroup) {
Element agentStatus = statusPacket.getElement().addElement("agent-status",
"http://jabber.org/protocol/workgroup");
// Add the workgroup JID as an attribute to the agent status element
agentStatus.addAttribute("jid", workgroup.getJID().toBareJID());
// Add max-chats element to the agent status element
Element maxChats = agentStatus.addElement("max-chats");
maxChats.setText(Integer.toString(getMaxChats(workgroup)));
// Add information about the current chats to the agent status element
Element currentChats = agentStatus.addElement("current-chats",
"http://jivesoftware.com/protocol/workgroup");
for (ChatInfo chatInfo : getChats(workgroup)) {
Element chatElement = currentChats.addElement("chat");
chatElement.addAttribute("sessionID", chatInfo.getSessionID());
chatElement.addAttribute("userID", chatInfo.getUserID());
chatElement.addAttribute("startTime", UTC_FORMAT.format(chatInfo.getDate()));
// Check for question
if (chatInfo.getQuestion() != null) {
chatElement.addAttribute("question", chatInfo.getQuestion());
}
// Check for username
if (chatInfo.getUsername() != null) {
chatElement.addAttribute("username", chatInfo.getUsername());
}
// Check for email
if (chatInfo.getEmail() != null) {
chatElement.addAttribute("email", chatInfo.getEmail());
}
}
}
@Override
public String toString() {
return "AI-" + Integer.toHexString(hashCode()) +
" JID " + address.toString() +
" CC " + Integer.toString(chatInfos.size()) +
" MC " + Integer.toString(maxChats);
}
/**
* Send an offer
*
* @param offer the <code>Offer</code> to send.
* @param offerPacket the packet to send to the agent with the offer.
* @return true if the packet was sent to the agent.
*/
public boolean sendOffer(Offer offer, IQ offerPacket) {
synchronized (this) {
if (this.offer != null) {
return false;
}
this.offer = offer;
}
try {
offer.addPendingSession(this);
InterceptorManager interceptorManager = OfferInterceptorManager.getInstance();
try {
Workgroup workgroup = offer.getRequest().getWorkgroup();
interceptorManager.invokeInterceptors(workgroup.getJID().toBareJID(),
offerPacket, false, false);
// Send the Offer to the agent
WorkgroupManager.getInstance().send(offerPacket);
interceptorManager.invokeInterceptors(workgroup.getJID().toBareJID(),
offerPacket, false, true);
}
catch (PacketRejectedException e) {
Log.warn("Offer was not sent " +
"due to interceptor REJECTION: " + offerPacket.toXML(), e);
}
return true;
}
catch (Exception e) {
Log.error(LocaleUtils.getLocalizedString("admin.error"), e);
return false;
}
}
public void sendRevoke(Offer offer, IQ agentRevoke) {
if (this.offer == null || !this.offer.equals(offer)) {
return;
}
try {
WorkgroupManager.getInstance().send(agentRevoke);
// Clear the offer associated with this agent session
removeOffer(offer);
}
catch (Exception e) {
Log.error(e.getMessage(), e);
}
}
public Agent getAgent() {
return agent;
}
/**
* Returns the <code>Workgroup</code> where this session is working.
*
* @return the <code>Workgroup</code> where this session is working.
*/
public Collection<Workgroup> getWorkgroups() {
return workgroups;
}
public int getMaxChats(Workgroup workgroup) {
int max = maxChats;
// Get the upper and lower limits
int upper = workgroup.getMaxChats();
int lower = workgroup.getMinChats();
// Ensure that max chats is in the limits
if (max == -1) {
max = upper;
}
if (max < lower) {
max = lower;
}
if (max > upper) {
max = upper;
}
return max;
}
private void setMaxChats(int max) {
maxChats = max;
}
// Sessions NEVER set min chats
public void setMinChats(int min) {
}
/**
* Returns a collection with the JID of the users that this agent is having a chat with.
*
* @param workgroup workgroup to get is chats.
* @return a collection with the JID of the users that this agent is having a chat with.
*/
public Collection<JID> getUsersJID(Workgroup workgroup) {
Collection<ChatInfo> chats = getChats(workgroup);
Collection<JID> jids = new ArrayList<JID>(chats.size());
for (ChatInfo info : chats) {
jids.add(info.getUserJID());
}
return jids;
}
@Override
public boolean equals(Object o) {
boolean match = false;
if (o instanceof AgentSession) {
match = ((AgentSession)o).getJID().equals(address);
}
return match;
}
public JID getJID() {
return address;
}
public long getID() {
return id;
}
public String getUsername() {
return address.getNode();
}
/**
* Returns true if the agent has requested to receive other agents' information. Until the
* agent requests to receive other agents' information he won't receive individual presence
* updates of other agents.
*
* @return true if the agent has requested to receive other agents' information.
*/
boolean hasRequestedAgentInfo() {
return requestedAgentInfo;
}
/**
* Returns true if the agent's presence is available and his status is nor DND (do not disturb)
* neither XA (extended away). These are the possible statuses and their meanings:<ul>
* <li>chat - Indicates the agent is available to chat (is idle and ready to handle more
* conversations).</li>
* <li>away - The agent is busy (possibly with other chats). The agent may still be able to
* handle other chats but an offer rejection is likely..</li>
* <li>xa - The agent is physically away from their terminal and should not have a chat routed
* to them.</li>
* <li>dnd - The agent is busy and should not be disturbed. However, special case, or extreme
* urgency chats may still be offered to the agent although offer rejection or offer timeouts
* are highly likely.</li>
* </ul>
*
* @return true if the agent's presence is available and his status is nor DND neither XA.
*/
public boolean isAvailableToChat() {
return (presence.getType() == null && presence.getShow() != Presence.Show.dnd &&
presence.getShow() != Presence.Show.xa && presence.getShow() != Presence.Show.away);
}
/**
* Adds information of a new chat that this agent is having with a user.
*
* @param workgroup workgroup where the chat has started.
* @param sessionID the id of the session that identifies the chat.
* @param request the initial request made by the user.
* @param date the date when the agent joined the chat.
*/
public void addChatInfo(Workgroup workgroup, String sessionID, UserRequest request, Date date) {
Queue<ChatInfo> queue = chatInfos.get(workgroup);
// Check if the agent has started a chat in a workgroup that he never joined (e.g. transfers)
if (queue == null) {
synchronized (workgroup) {
queue = chatInfos.get(workgroup);
if (queue == null) {
queue = new ConcurrentLinkedQueue<ChatInfo>();
chatInfos.put(workgroup, queue);
}
}
}
queue.add(new ChatInfo(sessionID, request, date));
// Update all agents with a new agent-status packet with the current-chats updated.
sendStatusToAllAgents(workgroup);
}
/**
* Removes information about a chat since the agent left the conversation.
*
* @param workgroup workgroup where the chat existed.
* @param sessionID the id of the session that identifies the chat.
*/
public void removeChatInfo(Workgroup workgroup, String sessionID) {
Queue<ChatInfo> chats = chatInfos.get(workgroup);
for (ChatInfo chatInfo : chats) {
if (sessionID.equals(chatInfo.getSessionID())) {
// Update last chat ended date
lastChatTime = new Date();
chats.remove(chatInfo);
// Update all agents with a new agent-status packet with the current-chats updated.
sendStatusToAllAgents(workgroup);
break;
}
}
}
/**
* Returns a list with the actual chats info that the agent is having at the moment. The
* returned collection is a snapshot of the chats so it will not be updated if a chat finished
* or a new one has started.
*
* @param workgroup workgroup to get its chats.
* @return a list with the actual chats info that the agent is having at the moment.
*/
public Collection<ChatInfo> getChatsInfo(Workgroup workgroup) {
return Collections.unmodifiableCollection(getChats(workgroup));
}
private Collection<ChatInfo> getChats(Workgroup workgroup) {
Queue<ChatInfo> chats = chatInfos.get(workgroup);
if (chats != null) {
return chats;
}
return Collections.emptyList();
}
/**
* This agent is not longer related to this offer. The agent may have been selected to answer
* the user's request or the offer has been assigned to another agent or the request was
* cancelled.
*
* @param offer the offer that is not longer related to this agent.
*/
public void removeOffer(Offer offer) {
if (offer.equals(this.offer)) {
this.offer = null;
}
else {
Log.debug("Offer not removed. " +
"To remove: " +
offer +
" existing " +
this.offer);
}
}
/**
* Returns true if the agent has received an offer and the server is still waiting for an
* answer.
*
* @return true if the agent has received an offer and the server is still waiting for an
* answer.
*/
public boolean isWaitingOfferAnswer() {
return offer != null;
}
/**
* Represents information about a Chat where this Agent is participating.
*
* @author Gaston Dombiak
*/
public static class ChatInfo implements Comparable<ChatInfo> {
private String sessionID;
private String userID;
private JID userJID;
private Date date;
private Workgroup workgroup;
// Add extra metadata
private String email;
private String username;
private String question;
public ChatInfo(String sessionID, UserRequest request, Date date) {
this.sessionID = sessionID;
this.userID = request.getUserID();
this.userJID = request.getUserJID();
this.workgroup = request.getWorkgroup();
this.date = date;
Map<String, List<String>> metadata = request.getMetaData();
if (metadata.containsKey("email")) {
email = listToString(metadata.get("email"));
}
if (metadata.containsKey("username")) {
username = listToString(metadata.get("username"));
}
if (metadata.containsKey("question")) {
question = listToString(metadata.get("question"));
}
}
/**
* Returns the sessionID associated to this chat. Each chat will have a unique sessionID
* that could be used for retrieving the whole transcript of the conversation.
*
* @return the sessionID associated to this chat.
*/
public String getSessionID() {
return sessionID;
}
/**
* Returns the user unique identification of the user that made the initial request and
* for which this chat was generated. If the user joined using an anonymous connection
* then the userID will be the value of the ID attribute of the USER element. Otherwise,
* the userID will be the bare JID of the user that made the request.
*
* @return the user unique identification of the user that made the initial request.
*/
public String getUserID() {
return userID;
}
/**
* Returns the JID of the user that made the initial request and for which this chat
* was generated.
*
* @return the JID of the user that made the initial request and for which this chat
* was generated.
*/
public JID getUserJID() {
return userJID;
}
/**
* Returns the date when this agent joined the chat.
*
* @return the date when this agent joined the chat.
*/
public Date getDate() {
return date;
}
/**
* Returns the email address of the user the agent is chatting with.
*
* @return the email address of the user the agent is chatting with.
*/
public String getEmail() {
return email;
}
/**
* Return the username of the user the agent is chatting with.
*
* @return the username of the user the agent is chatting with.
*/
public String getUsername() {
return username;
}
/**
* Return the question the user asked, if any.
*
* @return the question the user asked, if any.
*/
public String getQuestion() {
return question;
}
/**
* Returns the packets sent to the room together with the date when the packet was sent.
* The returned map will include both Presences and Messages.
*
* @return the packets sent to the room together with the date when the packet was sent.
*/
public Map<Packet, java.util.Date> getPackets() {
return workgroup.getTranscript(getSessionID());
}
public int compareTo(ChatInfo otherInfo) {
return date.compareTo(otherInfo.getDate());
}
}
/**
* Returns a list as a comma delimited string.
*
* @param list the list of strings.
* @return a comma delimited list of strings.
*/
private static String listToString(List<String> list) {
StringBuilder builder = new StringBuilder();
for (int i = 0; i < list.size(); i++) {
String entry = list.get(i);
builder.append(entry);
if (i != (list.size() - 1)) {
builder.append(",");
}
}
return builder.toString();
}
/**
* Return the time the last chat ended.
*
* @return the time.
*/
public Date getTimeLastChatEnded() {
return lastChatTime;
}
}