/*
* 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.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.TimeZone;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ConcurrentHashMap;
import org.dom4j.Element;
import org.jivesoftware.database.DbConnectionManager;
import org.jivesoftware.openfire.fastpath.util.TaskEngine;
import org.jivesoftware.util.FastDateFormat;
import org.jivesoftware.util.JiveConstants;
import org.jivesoftware.util.XMPPDateTimeFormat;
import org.jivesoftware.xmpp.workgroup.interceptor.AgentInterceptorManager;
import org.jivesoftware.xmpp.workgroup.interceptor.InterceptorManager;
import org.jivesoftware.xmpp.workgroup.interceptor.PacketRejectedException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xmpp.packet.JID;
import org.xmpp.packet.PacketError;
import org.xmpp.packet.Presence;
/**
* <p>The Workgroup's presence handler processes all incoming
* presence packets sent to the workgroup.</p>
* <p/>
* <p>Currently the workgroup recognizes:</p>
* <ul>
* <li>Presence from Agents</li>
* <ul>
* <li>available - Join the workgoup and create agent session</li>
* <li>unavailable - Depart the workgroup and delete agent session</li>
* </ul>
* </ul>
*
* @author Derek DeMoro
*/
public class WorkgroupPresence {
private static final Logger Log = LoggerFactory.getLogger(WorkgroupPresence.class);
private static final FastDateFormat UTC_FORMAT = FastDateFormat
.getInstance(XMPPDateTimeFormat.XMPP_DELAY_DATETIME_FORMAT, TimeZone.getTimeZone("UTC"));
private static final String LOAD_ROSTER =
"SELECT jid FROM fpWorkgroupRoster WHERE workgroupID=?";
private static final String CREATE_ROSTER_ITEM =
"INSERT INTO fpWorkgroupRoster (workgroupID, jid) VALUES (?, ?)";
private static final String DELETE_ROSTER_ITEM =
"DELETE FROM fpWorkgroupRoster WHERE workgroupID=? AND jid=?";
private static final String DELETE_ROSTER_ITEMS =
"DELETE FROM fpWorkgroupRoster WHERE workgroupID=?";
private Workgroup workgroup;
/**
* Holds the JID of users that sent an available presence to the workgroup as a way to be
* notified when the workgroup changes his presence status. This is like a temporary way
* to susbcribe to the workgroup presence. The temporary subscription will be removed
* when an unavailable presence is sent to the workgroup.
*/
private final Set<JID> listeners = Collections.newSetFromMap(new ConcurrentHashMap<JID, Boolean>());
/**
* Holds the bare JID address of all the users that subscribed to the presence of the
* workgroup. Users may want to track the presence of the workgroup to find out when agents
* are available. Users that send a presence subscribe packet will automatically get their
* requests accepted.
*/
private List<String> presenceSubscribers = new CopyOnWriteArrayList<String>();
public WorkgroupPresence(Workgroup workgroup) {
this.workgroup = workgroup;
// Load the bare JIDs of the users that are interested in the workgroup presence
loadRosterItems();
}
public void process(Presence packet) {
try {
JID sender = packet.getFrom();
// Handling Subscription
if (Presence.Type.subscribe == packet.getType()) {
// User wants to track the workgroup presence so accept the request
// Add sender to the workgroup roster
// Only create item if user was not already subscribed to workgroup's presence
if (!presenceSubscribers.contains(sender.toBareJID())) {
createRosterItem(sender);
}
// Reply that the subscription request was approved
Presence reply = new Presence();
reply.setTo(sender);
reply.setFrom(workgroup.getJID());
reply.setType(Presence.Type.subscribed);
workgroup.send(reply);
// Send the presence of the workgroup to the user that requested it
sendPresence(packet.getFrom());
}
else if (Presence.Type.unsubscribe == packet.getType()) {
// Remove sender from the workgroup roster
deleteRosterItem(sender);
// Send confirmation of unsubscription
Presence reply = new Presence();
reply.setTo(sender);
reply.setFrom(workgroup.getJID());
reply.setType(Presence.Type.unsubscribed);
workgroup.send(reply);
// Send unavailable presence of the workgroup
reply = new Presence();
reply.setTo(sender);
reply.setFrom(workgroup.getJID());
reply.setType(Presence.Type.unavailable);
workgroup.send(reply);
}
else if (Presence.Type.subscribed == packet.getType()) {
// ignore
}
else if (Presence.Type.unsubscribed == packet.getType()) {
// ignore
}
else if (Presence.Type.probe == packet.getType()) {
// Send the presence of the workgroup to the user that requested it
sendPresence(packet.getFrom());
}
else {
try {
agentToWorkgroup(packet);
}
catch (AgentNotFoundException e) {
Presence reply = new Presence();
reply.setError(new PacketError(PacketError.Condition.not_authorized));
reply.setTo(packet.getFrom());
reply.setFrom(workgroup.getJID());
workgroup.send(reply);
StringBuilder errorMessage = new StringBuilder();
errorMessage.append("Sender: ");
errorMessage.append(packet.getFrom().toString());
errorMessage.append(" Workgroup: ");
errorMessage.append(workgroup.getJID().toString());
Log.debug(errorMessage.toString(), e);
}
}
}
catch (Exception e) {
Log.error(e.getMessage(), e);
Presence reply = new Presence();
reply.setError(new PacketError(PacketError.Condition.internal_server_error));
reply.setTo(packet.getFrom());
reply.setFrom(workgroup.getJID());
workgroup.send(reply);
}
}
/**
* Sends the presence of the workgroup to the specified JID address.
*
* @param address the XMPP address that will receive the presence of the workgroup.
*/
public void sendPresence(JID address) {
Presence presence = new Presence();
presence.setTo(address);
presence.setFrom(workgroup.getJID());
Presence.Type type;
if (workgroup.isAvailable()) {
type = null;
// Add the a child element that will contain information about the workgroup
Element child = presence.addChildElement("workgroup",
"http://jivesoftware.com/protocol/workgroup");
// Add the last modification date of the workgroup
child.addElement("lastModified").setText(UTC_FORMAT.format(workgroup.getModificationDate()));
}
else {
type = Presence.Type.unavailable;
// Add the a child element that will contain information about the workgroup
Element child = presence.addChildElement("workgroup",
"http://jivesoftware.com/protocol/workgroup");
// Add the last modification date of the workgroup
child.addElement("lastModified").setText(UTC_FORMAT.format(workgroup.getModificationDate()));
}
presence.setType(type);
workgroup.send(presence);
}
/**
* Send the workgroup presence to the users that subscribed to the workgroup's presence.
*/
void broadcastWorkgroupPresence() {
TaskEngine.getInstance().submit(new Runnable() {
public void run() {
try {
// Send the workgroup presence to the workgroup subscribers
for (String bareJID : presenceSubscribers) {
sendPresence(new JID(bareJID));
}
// Send the workgroup presence to the users that temporary subscribed
// to the workgroup
for (JID tempSubscriber : listeners) {
sendPresence(tempSubscriber);
}
}
catch (Exception e) {
Log.error(
"Error broadcasting available presence", e);
}
}
});
}
/**
* The workgroup is being destroyed so remove all the accepted presence subscriptions.
*/
void workgroupDestroyed() {
TaskEngine.getInstance().submit(new Runnable() {
public void run() {
try {
for (String bareJID : presenceSubscribers) {
// Cancel the previously granted subscription request
Presence reply = new Presence();
reply.setTo(bareJID);
reply.setFrom(workgroup.getJID());
reply.setType(Presence.Type.unsubscribed);
workgroup.send(reply);
}
// Delete the subscribers from the database
deleteRosterItems();
}
catch (Exception e) {
Log.error(
"Error broadcasting available presence", e);
}
}
});
}
private void agentToWorkgroup(Presence packet) throws AgentNotFoundException {
String workgroupNode = workgroup.getJID().getNode();
String resource = packet.getFrom().getResource();
boolean usesAgentResource = workgroupNode.equalsIgnoreCase(resource);
final JID sender = packet.getFrom();
Agent agent = null;
// Check if the sender of the Presence is an Agent otherwise return an error
try {
agent = workgroup.getAgentManager().getAgent(sender);
}
catch (AgentNotFoundException notFound) {
if (usesAgentResource) {
throw notFound;
}
}
// Check if the presence includes the Workgroup extension
boolean includesExtension = packet
.getChildElement("agent-status", "http://jabber.org/protocol/workgroup") != null;
AgentManager agentManager = workgroup.getAgentManager();
if (agent != null && !agentManager.isInWorkgroup(agent, workgroup) && includesExtension) {
throw new AgentNotFoundException();
}
if (agent == null || !agentManager.isInWorkgroup(agent, workgroup)) {
if (packet.isAvailable()) {
if (!sender.equals(workgroup.getJID())) {
// Add the user to the list of temporary subscribers. Since the
// user sent this presence using a directed presence the server
// will send an unavailable presence if the user gets disconnected.
// When an unavailable presence is received the subscription is
// going to be removed.
listeners.add(packet.getFrom());
sendPresence(packet.getFrom());
}
}
else {
listeners.remove(packet.getFrom());
}
return;
}
InterceptorManager interceptorManager = AgentInterceptorManager.getInstance();
try {
interceptorManager.invokeInterceptors(workgroup.getJID().toBareJID(), packet, true, false);
// Try to update the presence of the AgentSession with the new presence
AgentSession agentSession = agent.getAgentSession();
if (agentSession == null) {
if (!includesExtension) {
// Ignote presence packet of agents that have not joined the workgroup
// and does not contain the proper extension
return;
}
// Add new agent session.
agentSession = agent.createSession(packet.getFrom());
if (agentSession == null) {
// User is not able to join since an existing session from another resource already exists
Presence reply = new Presence();
reply.setID(packet.getID());
reply.setTo(packet.getFrom());
reply.setFrom(packet.getTo());
reply.setError(PacketError.Condition.not_allowed);
workgroup.send(reply);
return;
}
}
// Update session's presence with latest presence
agentSession.updatePresence(packet);
if (agentSession.getPresence().getType() == null) {
agentSession.join(workgroup);
}
else {
agentSession.depart(workgroup);
}
interceptorManager.invokeInterceptors(workgroup.getJID().toBareJID(), packet, true, true);
}
catch (PacketRejectedException e) {
workgroup.rejectPacket(packet, e);
}
}
private void loadRosterItems() {
Connection con = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement(LOAD_ROSTER);
pstmt.setLong(1, workgroup.getID());
rs = pstmt.executeQuery();
List<String> jids = new ArrayList<String>();
while (rs.next()) {
jids.add(rs.getString(1));
}
presenceSubscribers.addAll(jids);
}
catch (SQLException e) {
Log.error(
"Error loading workgroup roster items ", e);
}
finally {
DbConnectionManager.closeConnection(rs, pstmt, con);
}
}
private void createRosterItem(JID sender) throws SQLException {
String bareJID = sender.toBareJID();
Connection con = null;
PreparedStatement pstmt = null;
try {
con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement(CREATE_ROSTER_ITEM);
pstmt.setLong(1, workgroup.getID());
pstmt.setString(2, bareJID);
pstmt.executeUpdate();
// Add the bareJID of the user to the list of users that are tracking the
// workgroup's presence
presenceSubscribers.add(bareJID);
}
finally {
DbConnectionManager.closeConnection(pstmt, con);
}
}
private void deleteRosterItem(JID sender) throws SQLException {
String bareJID = sender.toBareJID();
Connection con = null;
PreparedStatement pstmt = null;
try {
con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement(DELETE_ROSTER_ITEM);
pstmt.setLong(1, workgroup.getID());
pstmt.setString(2, bareJID);
pstmt.executeUpdate();
// Remove the bareJID of the user from the list of users that are tracking the
// workgroup's presence
presenceSubscribers.remove(bareJID);
}
finally {
DbConnectionManager.closeConnection(pstmt, con);
}
}
private void deleteRosterItems() throws SQLException {
Connection con = null;
PreparedStatement pstmt = null;
try {
con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement(DELETE_ROSTER_ITEMS);
pstmt.setLong(1, workgroup.getID());
pstmt.executeUpdate();
}
finally {
DbConnectionManager.closeConnection(pstmt, con);
}
}
// private int getNumberOfAgentsOnline() {
// int onlineAgents = 0;
// WorkgroupManager workgroupManager = WorkgroupManager.getInstance();
// for (Workgroup workgroup : workgroupManager.getWorkgroups()) {
// onlineAgents += workgroup.getAgentSessions().size();
// }
// return onlineAgents;
// }
}