AgentRoster.java

  1. /**
  2.  *
  3.  * Copyright 2003-2007 Jive Software.
  4.  *
  5.  * Licensed under the Apache License, Version 2.0 (the "License");
  6.  * you may not use this file except in compliance with the License.
  7.  * You may obtain a copy of the License at
  8.  *
  9.  *     http://www.apache.org/licenses/LICENSE-2.0
  10.  *
  11.  * Unless required by applicable law or agreed to in writing, software
  12.  * distributed under the License is distributed on an "AS IS" BASIS,
  13.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14.  * See the License for the specific language governing permissions and
  15.  * limitations under the License.
  16.  */

  17. package org.jivesoftware.smackx.workgroup.agent;

  18. import org.jivesoftware.smackx.workgroup.packet.AgentStatus;
  19. import org.jivesoftware.smackx.workgroup.packet.AgentStatusRequest;
  20. import org.jivesoftware.smack.StanzaListener;
  21. import org.jivesoftware.smack.SmackException.NotConnectedException;
  22. import org.jivesoftware.smack.XMPPConnection;
  23. import org.jivesoftware.smack.filter.StanzaFilter;
  24. import org.jivesoftware.smack.filter.StanzaTypeFilter;
  25. import org.jivesoftware.smack.packet.Stanza;
  26. import org.jivesoftware.smack.packet.Presence;
  27. import org.jxmpp.jid.FullJid;
  28. import org.jxmpp.jid.Jid;
  29. import org.jxmpp.jid.parts.Resourcepart;
  30. import org.jxmpp.util.XmppStringUtils;

  31. import java.util.ArrayList;
  32. import java.util.Collections;
  33. import java.util.HashMap;
  34. import java.util.HashSet;
  35. import java.util.Iterator;
  36. import java.util.List;
  37. import java.util.Map;
  38. import java.util.Set;
  39. import java.util.logging.Logger;

  40. /**
  41.  * Manges information about the agents in a workgroup and their presence.
  42.  *
  43.  * @author Matt Tucker
  44.  * @see AgentSession#getAgentRoster()
  45.  */
  46. public class AgentRoster {
  47.     private static final Logger LOGGER = Logger.getLogger(AgentRoster.class.getName());
  48.     private static final int EVENT_AGENT_ADDED = 0;
  49.     private static final int EVENT_AGENT_REMOVED = 1;
  50.     private static final int EVENT_PRESENCE_CHANGED = 2;

  51.     private XMPPConnection connection;
  52.     private Jid workgroupJID;
  53.     private List<String> entries;
  54.     private List<AgentRosterListener> listeners;
  55.     private final Map<Jid, Map<Resourcepart, Presence>> presenceMap = new HashMap<>();
  56.     // The roster is marked as initialized when at least a single roster packet
  57.     // has been recieved and processed.
  58.     boolean rosterInitialized = false;

  59.     /**
  60.      * Constructs a new AgentRoster.
  61.      *
  62.      * @param connection an XMPP connection.
  63.      * @throws NotConnectedException
  64.      * @throws InterruptedException
  65.      */
  66.     AgentRoster(XMPPConnection connection, Jid workgroupJID) throws NotConnectedException, InterruptedException {
  67.         this.connection = connection;
  68.         this.workgroupJID = workgroupJID;
  69.         entries = new ArrayList<String>();
  70.         listeners = new ArrayList<AgentRosterListener>();
  71.         // Listen for any roster packets.
  72.         StanzaFilter rosterFilter = new StanzaTypeFilter(AgentStatusRequest.class);
  73.         connection.addAsyncStanzaListener(new AgentStatusListener(), rosterFilter);
  74.         // Listen for any presence packets.
  75.         connection.addAsyncStanzaListener(new PresencePacketListener(),
  76.                 new StanzaTypeFilter(Presence.class));

  77.         // Send request for roster.
  78.         AgentStatusRequest request = new AgentStatusRequest();
  79.         request.setTo(workgroupJID);
  80.         connection.sendStanza(request);
  81.     }

  82.     /**
  83.      * Reloads the entire roster from the server. This is an asynchronous operation,
  84.      * which means the method will return immediately, and the roster will be
  85.      * reloaded at a later point when the server responds to the reload request.
  86.      * @throws NotConnectedException
  87.      * @throws InterruptedException
  88.      */
  89.     public void reload() throws NotConnectedException, InterruptedException {
  90.         AgentStatusRequest request = new AgentStatusRequest();
  91.         request.setTo(workgroupJID);
  92.         connection.sendStanza(request);
  93.     }

  94.     /**
  95.      * Adds a listener to this roster. The listener will be fired anytime one or more
  96.      * changes to the roster are pushed from the server.
  97.      *
  98.      * @param listener an agent roster listener.
  99.      */
  100.     public void addListener(AgentRosterListener listener) {
  101.         synchronized (listeners) {
  102.             if (!listeners.contains(listener)) {
  103.                 listeners.add(listener);

  104.                 // Fire events for the existing entries and presences in the roster
  105.                 for (Iterator<String> it = getAgents().iterator(); it.hasNext();) {
  106.                     String jid = it.next();
  107.                     // Check again in case the agent is no longer in the roster (highly unlikely
  108.                     // but possible)
  109.                     if (entries.contains(jid)) {
  110.                         // Fire the agent added event
  111.                         listener.agentAdded(jid);
  112.                         Map<Resourcepart, Presence> userPresences = presenceMap.get(jid);
  113.                         if (userPresences != null) {
  114.                             Iterator<Presence> presences = userPresences.values().iterator();
  115.                             while (presences.hasNext()) {
  116.                                 // Fire the presence changed event
  117.                                 listener.presenceChanged(presences.next());
  118.                             }
  119.                         }
  120.                     }
  121.                 }
  122.             }
  123.         }
  124.     }

  125.     /**
  126.      * Removes a listener from this roster. The listener will be fired anytime one or more
  127.      * changes to the roster are pushed from the server.
  128.      *
  129.      * @param listener a roster listener.
  130.      */
  131.     public void removeListener(AgentRosterListener listener) {
  132.         synchronized (listeners) {
  133.             listeners.remove(listener);
  134.         }
  135.     }

  136.     /**
  137.      * Returns a count of all agents in the workgroup.
  138.      *
  139.      * @return the number of agents in the workgroup.
  140.      */
  141.     public int getAgentCount() {
  142.         return entries.size();
  143.     }

  144.     /**
  145.      * Returns all agents (String JID values) in the workgroup.
  146.      *
  147.      * @return all entries in the roster.
  148.      */
  149.     public Set<String> getAgents() {
  150.         Set<String> agents = new HashSet<String>();
  151.         synchronized (entries) {
  152.             for (Iterator<String> i = entries.iterator(); i.hasNext();) {
  153.                 agents.add(i.next());
  154.             }
  155.         }
  156.         return Collections.unmodifiableSet(agents);
  157.     }

  158.     /**
  159.      * Returns true if the specified XMPP address is an agent in the workgroup.
  160.      *
  161.      * @param jid the XMPP address of the agent (eg "jsmith@example.com"). The
  162.      *            address can be in any valid format (e.g. "domain/resource", "user@domain"
  163.      *            or "user@domain/resource").
  164.      * @return true if the XMPP address is an agent in the workgroup.
  165.      */
  166.     public boolean contains(Jid jid) {
  167.         if (jid == null) {
  168.             return false;
  169.         }
  170.         synchronized (entries) {
  171.             for (Iterator<String> i = entries.iterator(); i.hasNext();) {
  172.                 String entry = i.next();
  173.                 if (entry.equals(jid)) {
  174.                     return true;
  175.                 }
  176.             }
  177.         }
  178.         return false;
  179.     }

  180.     /**
  181.      * Returns the presence info for a particular agent, or <tt>null</tt> if the agent
  182.      * is unavailable (offline) or if no presence information is available.<p>
  183.      *
  184.      * @param user a fully qualified xmpp JID. The address could be in any valid format (e.g.
  185.      *             "domain/resource", "user@domain" or "user@domain/resource").
  186.      * @return the agent's current presence, or <tt>null</tt> if the agent is unavailable
  187.      *         or if no presence information is available..
  188.      */
  189.     public Presence getPresence(Jid user) {
  190.         Jid key = getPresenceMapKey(user);
  191.         Map<Resourcepart, Presence> userPresences = presenceMap.get(key);
  192.         if (userPresences == null) {
  193.             Presence presence = new Presence(Presence.Type.unavailable);
  194.             presence.setFrom(user);
  195.             return presence;
  196.         }
  197.         else {
  198.             // Find the resource with the highest priority
  199.             // Might be changed to use the resource with the highest availability instead.
  200.             Iterator<Resourcepart> it = userPresences.keySet().iterator();
  201.             Presence p;
  202.             Presence presence = null;

  203.             while (it.hasNext()) {
  204.                 p = (Presence)userPresences.get(it.next());
  205.                 if (presence == null){
  206.                     presence = p;
  207.                 }
  208.                 else {
  209.                     if (p.getPriority() > presence.getPriority()) {
  210.                         presence = p;
  211.                     }
  212.                 }
  213.             }
  214.             if (presence == null) {
  215.                 presence = new Presence(Presence.Type.unavailable);
  216.                 presence.setFrom(user);
  217.                 return presence;
  218.             }
  219.             else {
  220.                 return presence;
  221.             }
  222.         }
  223.     }

  224.     /**
  225.      * Returns the key to use in the presenceMap for a fully qualified xmpp ID. The roster
  226.      * can contain any valid address format such us "domain/resource", "user@domain" or
  227.      * "user@domain/resource". If the roster contains an entry associated with the fully qualified
  228.      * xmpp ID then use the fully qualified xmpp ID as the key in presenceMap, otherwise use the
  229.      * bare address. Note: When the key in presenceMap is a fully qualified xmpp ID, the
  230.      * userPresences is useless since it will always contain one entry for the user.
  231.      *
  232.      * @param user the fully qualified xmpp ID, e.g. jdoe@example.com/Work.
  233.      * @return the key to use in the presenceMap for the fully qualified xmpp ID.
  234.      */
  235.     private Jid getPresenceMapKey(Jid user) {
  236.         Jid key = user;
  237.         if (!contains(user)) {
  238.             key = user.asBareJidIfPossible();
  239.         }
  240.         return key;
  241.     }

  242.     /**
  243.      * Fires event to listeners.
  244.      */
  245.     private void fireEvent(int eventType, Object eventObject) {
  246.         AgentRosterListener[] listeners = null;
  247.         synchronized (this.listeners) {
  248.             listeners = new AgentRosterListener[this.listeners.size()];
  249.             this.listeners.toArray(listeners);
  250.         }
  251.         for (int i = 0; i < listeners.length; i++) {
  252.             switch (eventType) {
  253.                 case EVENT_AGENT_ADDED:
  254.                     listeners[i].agentAdded((String)eventObject);
  255.                     break;
  256.                 case EVENT_AGENT_REMOVED:
  257.                     listeners[i].agentRemoved((String)eventObject);
  258.                     break;
  259.                 case EVENT_PRESENCE_CHANGED:
  260.                     listeners[i].presenceChanged((Presence)eventObject);
  261.                     break;
  262.             }
  263.         }
  264.     }

  265.     /**
  266.      * Listens for all presence packets and processes them.
  267.      */
  268.     private class PresencePacketListener implements StanzaListener {
  269.         public void processPacket(Stanza packet) {
  270.             Presence presence = (Presence)packet;
  271.             FullJid from = presence.getFrom().asFullJidIfPossible();
  272.             if (from == null) {
  273.                 // TODO Check if we need to ignore these presences or this is a server bug?
  274.                 LOGGER.warning("Presence with non full JID from: " + presence.toXML());
  275.                 return;
  276.             }
  277.             Jid key = getPresenceMapKey(from);

  278.             // If an "available" packet, add it to the presence map. Each presence map will hold
  279.             // for a particular user a map with the presence packets saved for each resource.
  280.             if (presence.getType() == Presence.Type.available) {
  281.                 // Ignore the presence packet unless it has an agent status extension.
  282.                 AgentStatus agentStatus = (AgentStatus)presence.getExtension(
  283.                         AgentStatus.ELEMENT_NAME, AgentStatus.NAMESPACE);
  284.                 if (agentStatus == null) {
  285.                     return;
  286.                 }
  287.                 // Ensure that this presence is coming from an Agent of the same workgroup
  288.                 // of this Agent
  289.                 else if (!workgroupJID.equals(agentStatus.getWorkgroupJID())) {
  290.                     return;
  291.                 }
  292.                 Map<Resourcepart, Presence> userPresences;
  293.                 // Get the user presence map
  294.                 if (presenceMap.get(key) == null) {
  295.                     userPresences = new HashMap<>();
  296.                     presenceMap.put(key, userPresences);
  297.                 }
  298.                 else {
  299.                     userPresences = presenceMap.get(key);
  300.                 }
  301.                 // Add the new presence, using the resources as a key.
  302.                 synchronized (userPresences) {
  303.                     userPresences.put(from.getResourcepart(), presence);
  304.                 }
  305.                 // Fire an event.
  306.                 synchronized (entries) {
  307.                     for (Iterator<String> i = entries.iterator(); i.hasNext();) {
  308.                         String entry = i.next();
  309.                         if (entry.equals(key.asBareJidIfPossible())) {
  310.                             fireEvent(EVENT_PRESENCE_CHANGED, packet);
  311.                         }
  312.                     }
  313.                 }
  314.             }
  315.             // If an "unavailable" packet, remove any entries in the presence map.
  316.             else if (presence.getType() == Presence.Type.unavailable) {
  317.                 if (presenceMap.get(key) != null) {
  318.                     Map<Resourcepart, Presence> userPresences = presenceMap.get(key);
  319.                     synchronized (userPresences) {
  320.                         userPresences.remove(from.getResourcepart());
  321.                     }
  322.                     if (userPresences.isEmpty()) {
  323.                         presenceMap.remove(key);
  324.                     }
  325.                 }
  326.                 // Fire an event.
  327.                 synchronized (entries) {
  328.                     for (Iterator<String> i = entries.iterator(); i.hasNext();) {
  329.                         String entry = (String)i.next();
  330.                         if (entry.equals(key.asBareJidIfPossible())) {
  331.                             fireEvent(EVENT_PRESENCE_CHANGED, packet);
  332.                         }
  333.                     }
  334.                 }
  335.             }
  336.         }
  337.     }

  338.     /**
  339.      * Listens for all roster packets and processes them.
  340.      */
  341.     private class AgentStatusListener implements StanzaListener {

  342.         public void processPacket(Stanza packet) {
  343.             if (packet instanceof AgentStatusRequest) {
  344.                 AgentStatusRequest statusRequest = (AgentStatusRequest)packet;
  345.                 for (Iterator<AgentStatusRequest.Item> i = statusRequest.getAgents().iterator(); i.hasNext();) {
  346.                     AgentStatusRequest.Item item = i.next();
  347.                     String agentJID = item.getJID();
  348.                     if ("remove".equals(item.getType())) {

  349.                         // Removing the user from the roster, so remove any presence information
  350.                         // about them.
  351.                         String key = XmppStringUtils.parseLocalpart(XmppStringUtils.parseLocalpart(agentJID) + "@" +
  352.                                 XmppStringUtils.parseDomain(agentJID));
  353.                         presenceMap.remove(key);
  354.                         // Fire event for roster listeners.
  355.                         fireEvent(EVENT_AGENT_REMOVED, agentJID);
  356.                     }
  357.                     else {
  358.                         entries.add(agentJID);
  359.                         // Fire event for roster listeners.
  360.                         fireEvent(EVENT_AGENT_ADDED, agentJID);
  361.                     }
  362.                 }

  363.                 // Mark the roster as initialized.
  364.                 rosterInitialized = true;
  365.             }
  366.         }
  367.     }
  368. }