/*
* Copyright (C) 2004-2006 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.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Set;
import java.util.TimeZone;
import java.util.concurrent.CopyOnWriteArraySet;
import org.dom4j.Element;
import org.jivesoftware.database.DbConnectionManager;
import org.jivesoftware.openfire.group.Group;
import org.jivesoftware.openfire.group.GroupManager;
import org.jivesoftware.openfire.group.GroupNotFoundException;
import org.jivesoftware.util.FastDateFormat;
import org.jivesoftware.util.NotFoundException;
import org.jivesoftware.xmpp.workgroup.dispatcher.Dispatcher;
import org.jivesoftware.xmpp.workgroup.dispatcher.RoundRobinDispatcher;
import org.jivesoftware.xmpp.workgroup.request.Request;
import org.jivesoftware.xmpp.workgroup.request.UserRequest;
import org.jivesoftware.xmpp.workgroup.spi.JiveLiveProperties;
import org.jivesoftware.xmpp.workgroup.utils.ModelUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xmpp.packet.JID;
import org.xmpp.packet.Presence;
/**
* Maintains a queue of requests waiting to be routed to agents. The workgroup
* adds and removes requests according to the protocol and the routing engine
* will process requests in the queue.
*
* @author Derek DeMoro
*/
public class RequestQueue {
private static final Logger Log = LoggerFactory.getLogger(RequestQueue.class);
private static final String LOAD_QUEUE =
"SELECT name, description, priority, maxchats, minchats, overflow, backupQueue FROM " +
"fpQueue WHERE queueID=?";
private static final String UPDATE_QUEUE =
"UPDATE fpQueue SET name=?, description=?, priority=?, maxchats=?, minchats=?, " +
"overflow=?, backupQueue=? WHERE queueID=?";
private static final String DELETE_QUEUE =
"DELETE FROM fpQueueAgent WHERE objectType=? AND objectID=? AND queueID=?";
private static final String LOAD_AGENTS =
"SELECT objectID, administrator FROM fpQueueAgent WHERE queueID=? AND objectType=?";
private static final String ADD_QUEUE_AGENT =
"INSERT INTO fpQueueAgent (objectType, objectID, queueID, administrator) " +
"VALUES (?,?,?,0)";
private static final String LOAD_QUEUE_GROUPS =
"SELECT groupName FROM fpQueueGroup WHERE queueID=?";
private static final String ADD_QUEUE_GROUP =
"INSERT INTO fpQueueGroup (queueID, groupName) VALUES (?,?)";
private static final String DELETE_QUEUE_GROUP =
"DELETE FROM fpQueueGroup WHERE queueID=? AND groupName=?";
/**
* A map of all requests in the workgroup keyed by the request's JID.
* Lets the server route request packets to the correct request.
*/
private LinkedList<UserRequest> requests = new LinkedList<UserRequest>();
/**
* A listof active agent sessions that belong to this queue.
*/
private AgentSessionList activeAgents = new AgentSessionList();
/**
* The current average time a request spends in this queue.
*/
private int averageTime;
/**
* The workgroup this queue belongs to.
*/
private Workgroup workgroup;
/**
* The default floor for maximum number of chats that an agent
* should have in this group.
*/
private int minChats;
/**
* The default ceiling for maximum number of chats that an agent
* should have in this group.
*/
private int maxChats;
/**
* The name of the queue (and the queue's resource identifier).
*/
private String name;
/**
* The description of the queue for the admin UI.
*/
private String description;
/**
* The routing priority of this request queue (not currently used).
*/
private int priority;
/**
* The agent groups that belong to this request queue.
*/
private Set<String> groups = new CopyOnWriteArraySet<String>();
/**
* The agents that belong directly to this request queue.
*/
private Set<Agent> agents = new CopyOnWriteArraySet<Agent>();
/**
* The workgoup entity properties for the queue.
*/
private JiveLiveProperties properties;
/**
* Dispatcher for the queue.
*/
private RoundRobinDispatcher dispatcher;
/**
* The overflow type of this queue.
*/
private OverflowType overflowType;
/**
* The backup queue if overflow is OVERFLOW_BACKUP.
*/
private long backupQueueID = 0;
/**
* Indicates if the queue's presence should be unavailable.
*/
private boolean presenceAvailable = true;
/**
* The total number of accepted requests *
*/
private int totalChatCount;
/**
* The total number of requests *
*/
private int totalRequestCount;
/**
* The total number of dropped requests *
*/
private int totalDroppedRequests;
/**
* The creation Date *
*/
private Date creationDate;
/**
* The queue id
*/
private long id;
/**
* The RequestQueue XMPPAdderess
*/
private JID address;
private AgentManager agentManager;
/**
* Creates a request queue for the given workgroup given a
* certain request queue ID.
*
* @param workgroup the workgroup this queue belongs to.
* @param id the queueID of the queue.
*/
public RequestQueue(Workgroup workgroup, long id) {
this.id = id;
this.workgroup = workgroup;
// Load up the Queue
loadQueue();
// Load all Groups
loadGroups();
// Load all Agents
loadAgents();
dispatcher = new RoundRobinDispatcher(this);
creationDate = new Date();
agentManager = workgroup.getAgentManager();
}
public Dispatcher getDispatcher() {
return dispatcher;
}
public Workgroup getWorkgroup() {
return workgroup;
}
// ############################################################################
// The request queue
// ############################################################################
public int getRequestCount() {
return requests.size();
}
public Collection<UserRequest> getRequests() {
return new ArrayList<UserRequest>(requests);
}
public UserRequest getRequest(JID requestAddress) {
UserRequest returnRequest = null;
for (UserRequest request : getRequests()) {
if (requestAddress.equals(request.getUserJID())) {
returnRequest = request;
break;
}
}
return returnRequest;
}
public void clearQueue() {
for (Request request : getRequests()) {
request.cancel(Request.CancelType.AGENT_NOT_FOUND);
}
requests.clear();
}
public void removeRequest(UserRequest request) {
if (request == null) {
throw new IllegalArgumentException();
}
totalRequestCount++;
if (request.getOffer() == null || !request.getOffer().isCancelled()) {
int waitTime = (int)(System.currentTimeMillis()
- request.getCreationTime().getTime()) / 1000;
if (averageTime == 0) {
averageTime = waitTime;
}
averageTime = (averageTime + waitTime) / 2;
totalChatCount++;
}
else {
final int timeout = (int)request.getOffer().getTimeout() / 1000;
int waitTime = (int)(System.currentTimeMillis()
- request.getCreationTime().getTime()) / 1000;
if (waitTime > timeout) {
// This was never left and timed-out
totalDroppedRequests++;
}
}
int index = requests.indexOf(request);
requests.remove(request);
if (requests.size() > 0 && index < requests.size()) {
sendRequestStatus(getRequests());
}
activeAgents.broadcastQueueStatus(this);
request.setRequestQueue(null);
}
public void addRequest(UserRequest request) {
if (request == null) {
throw new IllegalArgumentException();
}
request.setRequestQueue(this);
requests.add(request);
activeAgents.broadcastQueueStatus(this);
request.updateQueueStatus(false);
}
public UserRequest getFirst() {
return requests.getFirst();
}
public int getPosition(UserRequest request) {
return requests.indexOf(request);
}
public void sendStatus(JID recipient) {
try {
Presence queueStatus = getStatusPresence();
queueStatus.setTo(recipient);
workgroup.send(queueStatus);
}
catch (Exception e) {
Log.error(e.getMessage(), e);
}
}
public void sendDetailedStatus(JID recipient) {
try {
// queue details
Presence queueStatus = getDetailedStatusPresence();
queueStatus.setTo(recipient);
workgroup.send(queueStatus);
}
catch (Exception e) {
Log.error(e.getMessage(), e);
}
}
public Presence getStatusPresence() {
Presence queueStatus = new Presence();
queueStatus.setFrom(address);
// Add Notify Queue
Element status = queueStatus.addChildElement("notify-queue", "http://jabber.org/protocol/workgroup");
Element countElement = status.addElement("count");
countElement.setText(Integer.toString(getRequestCount()));
if (getRequestCount() > 0) {
Element oldestElement = status.addElement("oldest");
oldestElement.setText(UTC_FORMAT.format(getFirst().getCreationTime()));
}
Element timeElement = status.addElement("time");
timeElement.setText(Integer.toString(getAverageTime()));
Element statusElement = status.addElement("status");
if (workgroup.getStatus() == Workgroup.Status.OPEN && presenceAvailable) {
statusElement.setText("open");
}
else {
queueStatus.setType(Presence.Type.unavailable);
// TODO: actually active should be a full-blown workgroup state since queues
// may be empty but still active
if (getRequestCount() > 0) {
statusElement.setText("active");
}
else {
if (workgroup.getStatus() == Workgroup.Status.READY) {
statusElement.setText("ready");
}
else {
statusElement.setText("closed");
}
}
}
return queueStatus;
}
public Presence getDetailedStatusPresence() {
Presence queueStatus = new Presence();
queueStatus.setFrom(address);
if (workgroup.getStatus() == Workgroup.Status.OPEN && presenceAvailable) {
queueStatus.setType(null);
}
else {
queueStatus.setType(Presence.Type.unavailable);
}
Element details = queueStatus.addChildElement("notify-queue-details", "http://jabber.org/protocol/workgroup");
int i = 0;
for (UserRequest request : getRequests()) {
Element user = details.addElement("user", "http://jabber.org/protocol/workgroup");
try {
user.addAttribute("jid", request.getUserJID().toString());
// Add Sub-Elements
Element position = user.addElement("position");
position.setText(Integer.toString(i));
Element time = user.addElement("time");
time.setText(Integer.toString(request.getTimeStatus()));
Element joinTime = user.addElement("join-time");
joinTime.setText(UTC_FORMAT.format(request.getCreationTime()));
i++;
}
catch (Exception e) {
// Since we are not locking the list of requests while doing this operation (for
// performance reasons) it is possible that the request got accepted or cancelled
// thus generating a NPE
// Remove the request that generated the exception
details.remove(user);
// Log an error if the request still belongs to this queue
if (this.equals(request.getRequestQueue())) {
Log.error(e.getMessage(), e);
}
}
}
return queueStatus;
}
/**
* Returns true if this queue is opened and may be eligible for receiving new requests. The
* queue will be opened if there are agents connected to it.
*
* @return true if this queue is opened and may be eligible for receiving new requests.
*/
public boolean isOpened() {
return !activeAgents.isEmpty();
}
// #################################################################
// Standard formatting according to locale and Jabber specs
// #################################################################
private static final FastDateFormat UTC_FORMAT = FastDateFormat.getInstance("yyyyMMdd'T'HH:mm:ss", TimeZone.getTimeZone("UTC"));
static {
//UTC_FORMAT.setTimeZone(TimeZone.getTimeZone("GMT+0"));
}
// ###########################################################################
// Misc queue runtime properties
// ###########################################################################
public AgentSessionList getAgentSessionList() {
return activeAgents;
}
public int getAverageTime() {
return averageTime;
}
private void sendRequestStatus(Collection<UserRequest> requests) {
for (UserRequest request : requests) {
request.updateQueueStatus(false);
}
}
// ###########################################################################
// Agent Groups
// ###########################################################################
public int getGroupCount() {
return groups.size();
}
public Collection<Group> getGroups() {
return Collections.unmodifiableCollection(getGroupObjects());
}
private Collection<Group> getGroupObjects() {
final GroupManager groupManager = GroupManager.getInstance();
Set<Group> objects = new HashSet<Group>(groups.size());
for (String group : groups) {
try {
objects.add(groupManager.getGroup(group));
} catch (GroupNotFoundException e) {
Log.error("Error retrieving group: " + group, e);
}
}
return objects;
}
public boolean isMember(Agent agent) {
if (agents.contains(agent)) {
return true;
}
for (Group group : getGroupObjects()) {
if (group.isUser(agent.getAgentJID())) {
return true;
}
}
return false;
}
public boolean hasGroup(Group group) {
return groups.contains(group.getName());
}
public void addGroup(Group group) {
if (!groups.contains(group.getName())) {
boolean added = insertGroup(group.getName());
if (added) {
groups.add(group.getName());
WorkgroupManager workgroupManager = WorkgroupManager.getInstance();
AgentManager agentManager = workgroupManager.getAgentManager();
for (Agent agent : agentManager.getAgents(group)) {
agent.sendAgentAddedToAllAgents(this);
}
}
}
}
public void removeGroup(Group group) {
deleteGroup(group.getName());
if (groups.remove(group.getName())) {
for (Agent agent : agentManager.getAgents(group)) {
agent.sendAgentRemovedToAllAgents(this);
// Remove agent if necessary.
agentManager.removeAgentIfNecessary(agent);
}
}
}
// ##########################################################################
// Request Queue as agent group methods
// ##########################################################################
public int getMemberCount() {
int count = getMembers().size();
for (Group group : getGroups()) {
count += group.getMembers().size();
}
return count;
}
/**
* Adds an individual agent to the RequestQueue.
* @param agent the agent to add.
*/
public void addMember(Agent agent) {
if (!agents.contains(agent)) {
boolean added = addAgentToDb(agent.getID(), Boolean.FALSE);
if (added) {
agents.add(agent);
// Ask the new agent to notify the other agents of the queue of the new addition
agent.sendAgentAddedToAllAgents(this);
}
}
}
/**
* Removes an agent from the RequestQueue.
* @param agent the agent to remove.
*/
public void removeMember(Agent agent) {
deleteObject(agent.getID(), Boolean.FALSE);
agents.remove(agent);
// Remove agent if necessary
agentManager.removeAgentIfNecessary(agent);
// Ask the deleted agent to notify the other agents of the queue of the deletion
agent.sendAgentRemovedToAllAgents(this);
}
/**
* Returns members belong to this RequestQueue. Note, members does not include
* users belonging to Groups.
*
* @return a collection of queue members.
*/
public Collection<Agent> getMembers() {
final Set<Agent> agentList = new HashSet<Agent>(agents);
return Collections.unmodifiableCollection(agentList);
}
// #########################################################################
// Persistent accessor methods calls
// #########################################################################
public String getName() {
return name;
}
public void setName(String newName) {
// Handle empty string.
if (!ModelUtil.hasLength(newName)) {
return;
}
presenceAvailable = false;
try {
activeAgents.broadcastQueueStatus(this);
}
finally {
presenceAvailable = true;
}
this.name = newName;
JID workgroupJID = workgroup.getJID();
address = new JID(workgroupJID.getNode(), workgroupJID.getDomain(), this.name);
updateQueue();
activeAgents.broadcastQueueStatus(this);
}
public void setDescription(String description) {
this.description = description;
updateQueue();
}
public Date getCreationDate() {
return creationDate;
}
public void setCreationDate(Date creationDate) {
}
public Date getModificationDate() {
return null;
}
public void setModificationDate(Date modificationDate) {
}
public String getDescription() {
return description;
}
public DbProperties getProperties() {
if (properties == null) {
properties = new JiveLiveProperties("jlaQueueProp", id);
}
return properties;
}
public RequestQueue.OverflowType getOverflowType() {
return overflowType;
}
public void setOverflowType(RequestQueue.OverflowType type) {
if (type != null) {
overflowType = type;
updateQueue();
}
}
public RequestQueue getBackupQueue() {
RequestQueue queue = null;
if (backupQueueID > 0) {
try {
queue = workgroup.getRequestQueue(backupQueueID);
}
catch (NotFoundException e) {
Log.error(
"Backup queue with ID " + backupQueueID + " not found", e);
queue = null;
}
}
return queue;
}
public void setBackupQueue(RequestQueue queue) {
backupQueueID = queue.getID();
updateQueue();
}
private static final int AGENT_TYPE = 0;
private static final int GROUP_TYPE = 1;
private void loadQueue() {
Connection con = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement(LOAD_QUEUE);
pstmt.setLong(1, id);
rs = pstmt.executeQuery();
rs.next();
name = rs.getString(1);
address = new JID(workgroup.getJID().getNode(), workgroup.getJID().getDomain(), name);
description = rs.getString(2);
priority = rs.getInt(3);
maxChats = rs.getInt(4);
minChats = rs.getInt(5);
switch (rs.getInt(6)) {
case 1:
overflowType = OverflowType.OVERFLOW_RANDOM;
break;
case 2:
overflowType = OverflowType.OVERFLOW_BACKUP;
break;
default:
overflowType = OverflowType.OVERFLOW_NONE;
}
backupQueueID = rs.getLong(7);
}
catch (SQLException e) {
Log.error(e.getMessage(), e);
}
finally {
DbConnectionManager.closeConnection(rs, pstmt, con);
}
}
private void updateQueue() {
Connection con = null;
PreparedStatement pstmt = null;
try {
con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement(UPDATE_QUEUE);
pstmt.setString(1, name);
pstmt.setString(2, description);
pstmt.setInt(3, priority);
pstmt.setInt(4, maxChats);
pstmt.setInt(5, minChats);
pstmt.setInt(6, overflowType.ordinal());
pstmt.setLong(7, backupQueueID);
pstmt.setLong(8, id);
pstmt.executeUpdate();
}
catch (SQLException e) {
Log.error(e.getMessage(), e);
}
finally {
DbConnectionManager.closeConnection(pstmt, con);
}
}
private boolean deleteObject(long objectID, Object data) {
Connection con = null;
PreparedStatement pstmt = null;
try {
con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement(DELETE_QUEUE);
if ((Boolean)data) {
pstmt.setInt(1, GROUP_TYPE);
}
else {
pstmt.setInt(1, AGENT_TYPE);
}
pstmt.setLong(2, objectID);
pstmt.setLong(3, id);
pstmt.executeUpdate();
return true;
}
catch (SQLException e) {
Log.error(e.getMessage(), e);
}
finally {
DbConnectionManager.closeConnection(pstmt, con);
}
return false;
}
private boolean addAgentToDb(long objectID, Object data) {
Connection con = null;
PreparedStatement pstmt = null;
try {
con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement(ADD_QUEUE_AGENT);
if ((Boolean)data) {
pstmt.setInt(1, GROUP_TYPE);
}
else {
pstmt.setInt(1, AGENT_TYPE);
}
pstmt.setLong(2, objectID);
pstmt.setLong(3, id);
pstmt.executeUpdate();
return true;
}
catch (SQLException e) {
Log.error(e.getMessage(), e);
}
finally {
DbConnectionManager.closeConnection(pstmt, con);
}
return false;
}
private boolean insertGroup(String groupName) {
Connection con = null;
PreparedStatement pstmt = null;
try {
con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement(ADD_QUEUE_GROUP);
pstmt.setLong(1, id);
pstmt.setString(2, groupName);
pstmt.executeUpdate();
return true;
}
catch (SQLException e) {
Log.error(e.getMessage(), e);
}
finally {
DbConnectionManager.closeConnection(pstmt, con);
}
return false;
}
private boolean deleteGroup(String groupName) {
Connection con = null;
PreparedStatement pstmt = null;
try {
con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement(DELETE_QUEUE_GROUP);
pstmt.setLong(1, id);
pstmt.setString(2, groupName);
pstmt.executeUpdate();
return true;
}
catch (SQLException e) {
Log.error(e.getMessage(), e);
}
finally {
DbConnectionManager.closeConnection(pstmt, con);
}
return false;
}
private void loadGroups() {
Connection con = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement(LOAD_QUEUE_GROUPS);
pstmt.setLong(1, id);
rs = pstmt.executeQuery();
while (rs.next()) {
groups.add(rs.getString(1));
}
}
catch (Exception e) {
Log.error(e.getMessage(), e);
}
finally {
DbConnectionManager.closeConnection(rs, pstmt, con);
}
}
private void loadAgents() {
Connection con = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement(LOAD_AGENTS);
pstmt.setLong(1, id);
pstmt.setInt(2, AGENT_TYPE);
rs = pstmt.executeQuery();
AgentManager agentManager = workgroup.getAgentManager();
while (rs.next()) {
try {
Agent agent = agentManager.getAgent(rs.getLong(1));
agents.add(agent);
}
catch (AgentNotFoundException e) {
Log.error(e.getMessage(), e);
}
}
}
catch (SQLException e) {
Log.error(e.getMessage(), e);
}
finally {
DbConnectionManager.closeConnection(rs, pstmt, con);
}
}
// Stats Implementation
public int getTotalChatCount() {
return totalChatCount;
}
public int getTotalRequestCount() {
return totalRequestCount;
}
public int getDroppedRequestCount() {
return totalDroppedRequests;
}
public JID getAddress() {
if (address == null) {
throw new IllegalStateException();
}
return address;
}
public long getID() {
return id;
}
public String getUsername() {
return address.getNode().toLowerCase();
}
public void shutdown() {
dispatcher.shutdown();
}
/**
* Defines the overflow types available for queues.
*
* @author Iain Shigeoka
*/
public static enum OverflowType {
/**
* Requests are not overflowed to other queues.
*/
OVERFLOW_NONE,
/**
* Requests that aren't handled are overflowed to a random available queue.
*/
OVERFLOW_RANDOM,
/**
* Requests that aren't handled are overflowed to the queue's backup queue.
*/
OVERFLOW_BACKUP
}
}