Home | History | Annotate | Download | only in agent
      1 /**
      2  * $Revision$
      3  * $Date$
      4  *
      5  * Copyright 2003-2007 Jive Software.
      6  *
      7  * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
      8  * you may not use this file except in compliance with the License.
      9  * You may obtain a copy of the License at
     10  *
     11  *     http://www.apache.org/licenses/LICENSE-2.0
     12  *
     13  * Unless required by applicable law or agreed to in writing, software
     14  * distributed under the License is distributed on an "AS IS" BASIS,
     15  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     16  * See the License for the specific language governing permissions and
     17  * limitations under the License.
     18  */
     19 
     20 package org.jivesoftware.smackx.workgroup.agent;
     21 
     22 import org.jivesoftware.smackx.workgroup.packet.AgentStatus;
     23 import org.jivesoftware.smackx.workgroup.packet.AgentStatusRequest;
     24 import org.jivesoftware.smack.PacketListener;
     25 import org.jivesoftware.smack.Connection;
     26 import org.jivesoftware.smack.filter.PacketFilter;
     27 import org.jivesoftware.smack.filter.PacketTypeFilter;
     28 import org.jivesoftware.smack.packet.Packet;
     29 import org.jivesoftware.smack.packet.Presence;
     30 import org.jivesoftware.smack.util.StringUtils;
     31 
     32 import java.util.ArrayList;
     33 import java.util.Collections;
     34 import java.util.HashMap;
     35 import java.util.HashSet;
     36 import java.util.Iterator;
     37 import java.util.List;
     38 import java.util.Map;
     39 import java.util.Set;
     40 
     41 /**
     42  * Manges information about the agents in a workgroup and their presence.
     43  *
     44  * @author Matt Tucker
     45  * @see AgentSession#getAgentRoster()
     46  */
     47 public class AgentRoster {
     48 
     49     private static final int EVENT_AGENT_ADDED = 0;
     50     private static final int EVENT_AGENT_REMOVED = 1;
     51     private static final int EVENT_PRESENCE_CHANGED = 2;
     52 
     53     private Connection connection;
     54     private String workgroupJID;
     55     private List<String> entries;
     56     private List<AgentRosterListener> listeners;
     57     private Map<String, Map<String, Presence>> presenceMap;
     58     // The roster is marked as initialized when at least a single roster packet
     59     // has been recieved and processed.
     60     boolean rosterInitialized = false;
     61 
     62     /**
     63      * Constructs a new AgentRoster.
     64      *
     65      * @param connection an XMPP connection.
     66      */
     67     AgentRoster(Connection connection, String workgroupJID) {
     68         this.connection = connection;
     69         this.workgroupJID = workgroupJID;
     70         entries = new ArrayList<String>();
     71         listeners = new ArrayList<AgentRosterListener>();
     72         presenceMap = new HashMap<String, Map<String, Presence>>();
     73         // Listen for any roster packets.
     74         PacketFilter rosterFilter = new PacketTypeFilter(AgentStatusRequest.class);
     75         connection.addPacketListener(new AgentStatusListener(), rosterFilter);
     76         // Listen for any presence packets.
     77         connection.addPacketListener(new PresencePacketListener(),
     78                 new PacketTypeFilter(Presence.class));
     79 
     80         // Send request for roster.
     81         AgentStatusRequest request = new AgentStatusRequest();
     82         request.setTo(workgroupJID);
     83         connection.sendPacket(request);
     84     }
     85 
     86     /**
     87      * Reloads the entire roster from the server. This is an asynchronous operation,
     88      * which means the method will return immediately, and the roster will be
     89      * reloaded at a later point when the server responds to the reload request.
     90      */
     91     public void reload() {
     92         AgentStatusRequest request = new AgentStatusRequest();
     93         request.setTo(workgroupJID);
     94         connection.sendPacket(request);
     95     }
     96 
     97     /**
     98      * Adds a listener to this roster. The listener will be fired anytime one or more
     99      * changes to the roster are pushed from the server.
    100      *
    101      * @param listener an agent roster listener.
    102      */
    103     public void addListener(AgentRosterListener listener) {
    104         synchronized (listeners) {
    105             if (!listeners.contains(listener)) {
    106                 listeners.add(listener);
    107 
    108                 // Fire events for the existing entries and presences in the roster
    109                 for (Iterator<String> it = getAgents().iterator(); it.hasNext();) {
    110                     String jid = it.next();
    111                     // Check again in case the agent is no longer in the roster (highly unlikely
    112                     // but possible)
    113                     if (entries.contains(jid)) {
    114                         // Fire the agent added event
    115                         listener.agentAdded(jid);
    116                         Map<String,Presence> userPresences = presenceMap.get(jid);
    117                         if (userPresences != null) {
    118                             Iterator<Presence> presences = userPresences.values().iterator();
    119                             while (presences.hasNext()) {
    120                                 // Fire the presence changed event
    121                                 listener.presenceChanged(presences.next());
    122                             }
    123                         }
    124                     }
    125                 }
    126             }
    127         }
    128     }
    129 
    130     /**
    131      * Removes a listener from this roster. The listener will be fired anytime one or more
    132      * changes to the roster are pushed from the server.
    133      *
    134      * @param listener a roster listener.
    135      */
    136     public void removeListener(AgentRosterListener listener) {
    137         synchronized (listeners) {
    138             listeners.remove(listener);
    139         }
    140     }
    141 
    142     /**
    143      * Returns a count of all agents in the workgroup.
    144      *
    145      * @return the number of agents in the workgroup.
    146      */
    147     public int getAgentCount() {
    148         return entries.size();
    149     }
    150 
    151     /**
    152      * Returns all agents (String JID values) in the workgroup.
    153      *
    154      * @return all entries in the roster.
    155      */
    156     public Set<String> getAgents() {
    157         Set<String> agents = new HashSet<String>();
    158         synchronized (entries) {
    159             for (Iterator<String> i = entries.iterator(); i.hasNext();) {
    160                 agents.add(i.next());
    161             }
    162         }
    163         return Collections.unmodifiableSet(agents);
    164     }
    165 
    166     /**
    167      * Returns true if the specified XMPP address is an agent in the workgroup.
    168      *
    169      * @param jid the XMPP address of the agent (eg "jsmith (at) example.com"). The
    170      *            address can be in any valid format (e.g. "domain/resource", "user@domain"
    171      *            or "user@domain/resource").
    172      * @return true if the XMPP address is an agent in the workgroup.
    173      */
    174     public boolean contains(String jid) {
    175         if (jid == null) {
    176             return false;
    177         }
    178         synchronized (entries) {
    179             for (Iterator<String> i = entries.iterator(); i.hasNext();) {
    180                 String entry = i.next();
    181                 if (entry.toLowerCase().equals(jid.toLowerCase())) {
    182                     return true;
    183                 }
    184             }
    185         }
    186         return false;
    187     }
    188 
    189     /**
    190      * Returns the presence info for a particular agent, or <tt>null</tt> if the agent
    191      * is unavailable (offline) or if no presence information is available.<p>
    192      *
    193      * @param user a fully qualified xmpp JID. The address could be in any valid format (e.g.
    194      *             "domain/resource", "user@domain" or "user@domain/resource").
    195      * @return the agent's current presence, or <tt>null</tt> if the agent is unavailable
    196      *         or if no presence information is available..
    197      */
    198     public Presence getPresence(String user) {
    199         String key = getPresenceMapKey(user);
    200         Map<String, Presence> userPresences = presenceMap.get(key);
    201         if (userPresences == null) {
    202             Presence presence = new Presence(Presence.Type.unavailable);
    203             presence.setFrom(user);
    204             return presence;
    205         }
    206         else {
    207             // Find the resource with the highest priority
    208             // Might be changed to use the resource with the highest availability instead.
    209             Iterator<String> it = userPresences.keySet().iterator();
    210             Presence p;
    211             Presence presence = null;
    212 
    213             while (it.hasNext()) {
    214                 p = (Presence)userPresences.get(it.next());
    215                 if (presence == null){
    216                     presence = p;
    217                 }
    218                 else {
    219                     if (p.getPriority() > presence.getPriority()) {
    220                         presence = p;
    221                     }
    222                 }
    223             }
    224             if (presence == null) {
    225                 presence = new Presence(Presence.Type.unavailable);
    226                 presence.setFrom(user);
    227                 return presence;
    228             }
    229             else {
    230                 return presence;
    231             }
    232         }
    233     }
    234 
    235     /**
    236      * Returns the key to use in the presenceMap for a fully qualified xmpp ID. The roster
    237      * can contain any valid address format such us "domain/resource", "user@domain" or
    238      * "user@domain/resource". If the roster contains an entry associated with the fully qualified
    239      * xmpp ID then use the fully qualified xmpp ID as the key in presenceMap, otherwise use the
    240      * bare address. Note: When the key in presenceMap is a fully qualified xmpp ID, the
    241      * userPresences is useless since it will always contain one entry for the user.
    242      *
    243      * @param user the fully qualified xmpp ID, e.g. jdoe (at) example.com/Work.
    244      * @return the key to use in the presenceMap for the fully qualified xmpp ID.
    245      */
    246     private String getPresenceMapKey(String user) {
    247         String key = user;
    248         if (!contains(user)) {
    249             key = StringUtils.parseBareAddress(user).toLowerCase();
    250         }
    251         return key;
    252     }
    253 
    254     /**
    255      * Fires event to listeners.
    256      */
    257     private void fireEvent(int eventType, Object eventObject) {
    258         AgentRosterListener[] listeners = null;
    259         synchronized (this.listeners) {
    260             listeners = new AgentRosterListener[this.listeners.size()];
    261             this.listeners.toArray(listeners);
    262         }
    263         for (int i = 0; i < listeners.length; i++) {
    264             switch (eventType) {
    265                 case EVENT_AGENT_ADDED:
    266                     listeners[i].agentAdded((String)eventObject);
    267                     break;
    268                 case EVENT_AGENT_REMOVED:
    269                     listeners[i].agentRemoved((String)eventObject);
    270                     break;
    271                 case EVENT_PRESENCE_CHANGED:
    272                     listeners[i].presenceChanged((Presence)eventObject);
    273                     break;
    274             }
    275         }
    276     }
    277 
    278     /**
    279      * Listens for all presence packets and processes them.
    280      */
    281     private class PresencePacketListener implements PacketListener {
    282         public void processPacket(Packet packet) {
    283             Presence presence = (Presence)packet;
    284             String from = presence.getFrom();
    285             if (from == null) {
    286                 // TODO Check if we need to ignore these presences or this is a server bug?
    287                 System.out.println("Presence with no FROM: " + presence.toXML());
    288                 return;
    289             }
    290             String key = getPresenceMapKey(from);
    291 
    292             // If an "available" packet, add it to the presence map. Each presence map will hold
    293             // for a particular user a map with the presence packets saved for each resource.
    294             if (presence.getType() == Presence.Type.available) {
    295                 // Ignore the presence packet unless it has an agent status extension.
    296                 AgentStatus agentStatus = (AgentStatus)presence.getExtension(
    297                         AgentStatus.ELEMENT_NAME, AgentStatus.NAMESPACE);
    298                 if (agentStatus == null) {
    299                     return;
    300                 }
    301                 // Ensure that this presence is coming from an Agent of the same workgroup
    302                 // of this Agent
    303                 else if (!workgroupJID.equals(agentStatus.getWorkgroupJID())) {
    304                     return;
    305                 }
    306                 Map<String, Presence> userPresences;
    307                 // Get the user presence map
    308                 if (presenceMap.get(key) == null) {
    309                     userPresences = new HashMap<String, Presence>();
    310                     presenceMap.put(key, userPresences);
    311                 }
    312                 else {
    313                     userPresences = presenceMap.get(key);
    314                 }
    315                 // Add the new presence, using the resources as a key.
    316                 synchronized (userPresences) {
    317                     userPresences.put(StringUtils.parseResource(from), presence);
    318                 }
    319                 // Fire an event.
    320                 synchronized (entries) {
    321                     for (Iterator<String> i = entries.iterator(); i.hasNext();) {
    322                         String entry = i.next();
    323                         if (entry.toLowerCase().equals(StringUtils.parseBareAddress(key).toLowerCase())) {
    324                             fireEvent(EVENT_PRESENCE_CHANGED, packet);
    325                         }
    326                     }
    327                 }
    328             }
    329             // If an "unavailable" packet, remove any entries in the presence map.
    330             else if (presence.getType() == Presence.Type.unavailable) {
    331                 if (presenceMap.get(key) != null) {
    332                     Map<String,Presence> userPresences = presenceMap.get(key);
    333                     synchronized (userPresences) {
    334                         userPresences.remove(StringUtils.parseResource(from));
    335                     }
    336                     if (userPresences.isEmpty()) {
    337                         presenceMap.remove(key);
    338                     }
    339                 }
    340                 // Fire an event.
    341                 synchronized (entries) {
    342                     for (Iterator<String> i = entries.iterator(); i.hasNext();) {
    343                         String entry = (String)i.next();
    344                         if (entry.toLowerCase().equals(StringUtils.parseBareAddress(key).toLowerCase())) {
    345                             fireEvent(EVENT_PRESENCE_CHANGED, packet);
    346                         }
    347                     }
    348                 }
    349             }
    350         }
    351     }
    352 
    353     /**
    354      * Listens for all roster packets and processes them.
    355      */
    356     private class AgentStatusListener implements PacketListener {
    357 
    358         public void processPacket(Packet packet) {
    359             if (packet instanceof AgentStatusRequest) {
    360                 AgentStatusRequest statusRequest = (AgentStatusRequest)packet;
    361                 for (Iterator<AgentStatusRequest.Item> i = statusRequest.getAgents().iterator(); i.hasNext();) {
    362                     AgentStatusRequest.Item item = i.next();
    363                     String agentJID = item.getJID();
    364                     if ("remove".equals(item.getType())) {
    365 
    366                         // Removing the user from the roster, so remove any presence information
    367                         // about them.
    368                         String key = StringUtils.parseName(StringUtils.parseName(agentJID) + "@" +
    369                                 StringUtils.parseServer(agentJID));
    370                         presenceMap.remove(key);
    371                         // Fire event for roster listeners.
    372                         fireEvent(EVENT_AGENT_REMOVED, agentJID);
    373                     }
    374                     else {
    375                         entries.add(agentJID);
    376                         // Fire event for roster listeners.
    377                         fireEvent(EVENT_AGENT_ADDED, agentJID);
    378                     }
    379                 }
    380 
    381                 // Mark the roster as initialized.
    382                 rosterInitialized = true;
    383             }
    384         }
    385     }
    386 }