/* * 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 } }