001/**
002 *
003 * Copyright 2003-2007 Jive Software.
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License");
006 * you may not use this file except in compliance with the License.
007 * You may obtain a copy of the License at
008 *
009 *     http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017
018package org.jivesoftware.smackx.workgroup.agent;
019
020import org.jivesoftware.smackx.workgroup.packet.AgentStatus;
021import org.jivesoftware.smackx.workgroup.packet.AgentStatusRequest;
022import org.jivesoftware.smack.PacketListener;
023import org.jivesoftware.smack.SmackException.NotConnectedException;
024import org.jivesoftware.smack.XMPPConnection;
025import org.jivesoftware.smack.filter.PacketFilter;
026import org.jivesoftware.smack.filter.PacketTypeFilter;
027import org.jivesoftware.smack.packet.Packet;
028import org.jivesoftware.smack.packet.Presence;
029import org.jivesoftware.smack.util.StringUtils;
030
031import java.util.ArrayList;
032import java.util.Collections;
033import java.util.HashMap;
034import java.util.HashSet;
035import java.util.Iterator;
036import java.util.List;
037import java.util.Locale;
038import java.util.Map;
039import java.util.Set;
040import java.util.logging.Logger;
041
042/**
043 * Manges information about the agents in a workgroup and their presence.
044 *
045 * @author Matt Tucker
046 * @see AgentSession#getAgentRoster()
047 */
048public class AgentRoster {
049    private static final Logger LOGGER = Logger.getLogger(AgentRoster.class.getName());
050    private static final int EVENT_AGENT_ADDED = 0;
051    private static final int EVENT_AGENT_REMOVED = 1;
052    private static final int EVENT_PRESENCE_CHANGED = 2;
053
054    private XMPPConnection connection;
055    private String workgroupJID;
056    private List<String> entries;
057    private List<AgentRosterListener> listeners;
058    private Map<String, Map<String, Presence>> presenceMap;
059    // The roster is marked as initialized when at least a single roster packet
060    // has been recieved and processed.
061    boolean rosterInitialized = false;
062
063    /**
064     * Constructs a new AgentRoster.
065     *
066     * @param connection an XMPP connection.
067     * @throws NotConnectedException 
068     */
069    AgentRoster(XMPPConnection connection, String workgroupJID) throws NotConnectedException {
070        this.connection = connection;
071        this.workgroupJID = workgroupJID;
072        entries = new ArrayList<String>();
073        listeners = new ArrayList<AgentRosterListener>();
074        presenceMap = new HashMap<String, Map<String, Presence>>();
075        // Listen for any roster packets.
076        PacketFilter rosterFilter = new PacketTypeFilter(AgentStatusRequest.class);
077        connection.addPacketListener(new AgentStatusListener(), rosterFilter);
078        // Listen for any presence packets.
079        connection.addPacketListener(new PresencePacketListener(),
080                new PacketTypeFilter(Presence.class));
081
082        // Send request for roster.
083        AgentStatusRequest request = new AgentStatusRequest();
084        request.setTo(workgroupJID);
085        connection.sendPacket(request);
086    }
087
088    /**
089     * Reloads the entire roster from the server. This is an asynchronous operation,
090     * which means the method will return immediately, and the roster will be
091     * reloaded at a later point when the server responds to the reload request.
092     * @throws NotConnectedException 
093     */
094    public void reload() throws NotConnectedException {
095        AgentStatusRequest request = new AgentStatusRequest();
096        request.setTo(workgroupJID);
097        connection.sendPacket(request);
098    }
099
100    /**
101     * Adds a listener to this roster. The listener will be fired anytime one or more
102     * changes to the roster are pushed from the server.
103     *
104     * @param listener an agent roster listener.
105     */
106    public void addListener(AgentRosterListener listener) {
107        synchronized (listeners) {
108            if (!listeners.contains(listener)) {
109                listeners.add(listener);
110
111                // Fire events for the existing entries and presences in the roster
112                for (Iterator<String> it = getAgents().iterator(); it.hasNext();) {
113                    String jid = it.next();
114                    // Check again in case the agent is no longer in the roster (highly unlikely
115                    // but possible)
116                    if (entries.contains(jid)) {
117                        // Fire the agent added event
118                        listener.agentAdded(jid);
119                        Map<String,Presence> userPresences = presenceMap.get(jid);
120                        if (userPresences != null) {
121                            Iterator<Presence> presences = userPresences.values().iterator();
122                            while (presences.hasNext()) {
123                                // Fire the presence changed event
124                                listener.presenceChanged(presences.next());
125                            }
126                        }
127                    }
128                }
129            }
130        }
131    }
132
133    /**
134     * Removes a listener from this roster. The listener will be fired anytime one or more
135     * changes to the roster are pushed from the server.
136     *
137     * @param listener a roster listener.
138     */
139    public void removeListener(AgentRosterListener listener) {
140        synchronized (listeners) {
141            listeners.remove(listener);
142        }
143    }
144
145    /**
146     * Returns a count of all agents in the workgroup.
147     *
148     * @return the number of agents in the workgroup.
149     */
150    public int getAgentCount() {
151        return entries.size();
152    }
153
154    /**
155     * Returns all agents (String JID values) in the workgroup.
156     *
157     * @return all entries in the roster.
158     */
159    public Set<String> getAgents() {
160        Set<String> agents = new HashSet<String>();
161        synchronized (entries) {
162            for (Iterator<String> i = entries.iterator(); i.hasNext();) {
163                agents.add(i.next());
164            }
165        }
166        return Collections.unmodifiableSet(agents);
167    }
168
169    /**
170     * Returns true if the specified XMPP address is an agent in the workgroup.
171     *
172     * @param jid the XMPP address of the agent (eg "jsmith@example.com"). The
173     *            address can be in any valid format (e.g. "domain/resource", "user@domain"
174     *            or "user@domain/resource").
175     * @return true if the XMPP address is an agent in the workgroup.
176     */
177    public boolean contains(String jid) {
178        if (jid == null) {
179            return false;
180        }
181        synchronized (entries) {
182            for (Iterator<String> i = entries.iterator(); i.hasNext();) {
183                String entry = i.next();
184                if (entry.toLowerCase(Locale.US).equals(jid.toLowerCase())) {
185                    return true;
186                }
187            }
188        }
189        return false;
190    }
191
192    /**
193     * Returns the presence info for a particular agent, or <tt>null</tt> if the agent
194     * is unavailable (offline) or if no presence information is available.<p>
195     *
196     * @param user a fully qualified xmpp JID. The address could be in any valid format (e.g.
197     *             "domain/resource", "user@domain" or "user@domain/resource").
198     * @return the agent's current presence, or <tt>null</tt> if the agent is unavailable
199     *         or if no presence information is available..
200     */
201    public Presence getPresence(String user) {
202        String key = getPresenceMapKey(user);
203        Map<String, Presence> userPresences = presenceMap.get(key);
204        if (userPresences == null) {
205            Presence presence = new Presence(Presence.Type.unavailable);
206            presence.setFrom(user);
207            return presence;
208        }
209        else {
210            // Find the resource with the highest priority
211            // Might be changed to use the resource with the highest availability instead.
212            Iterator<String> it = userPresences.keySet().iterator();
213            Presence p;
214            Presence presence = null;
215
216            while (it.hasNext()) {
217                p = (Presence)userPresences.get(it.next());
218                if (presence == null){
219                    presence = p;
220                }
221                else {
222                    if (p.getPriority() > presence.getPriority()) {
223                        presence = p;
224                    }
225                }
226            }
227            if (presence == null) {
228                presence = new Presence(Presence.Type.unavailable);
229                presence.setFrom(user);
230                return presence;
231            }
232            else {
233                return presence;
234            }
235        }
236    }
237
238    /**
239     * Returns the key to use in the presenceMap for a fully qualified xmpp ID. The roster
240     * can contain any valid address format such us "domain/resource", "user@domain" or
241     * "user@domain/resource". If the roster contains an entry associated with the fully qualified
242     * xmpp ID then use the fully qualified xmpp ID as the key in presenceMap, otherwise use the
243     * bare address. Note: When the key in presenceMap is a fully qualified xmpp ID, the
244     * userPresences is useless since it will always contain one entry for the user.
245     *
246     * @param user the fully qualified xmpp ID, e.g. jdoe@example.com/Work.
247     * @return the key to use in the presenceMap for the fully qualified xmpp ID.
248     */
249    private String getPresenceMapKey(String user) {
250        String key = user;
251        if (!contains(user)) {
252            key = StringUtils.parseBareAddress(user).toLowerCase(Locale.US);
253        }
254        return key;
255    }
256
257    /**
258     * Fires event to listeners.
259     */
260    private void fireEvent(int eventType, Object eventObject) {
261        AgentRosterListener[] listeners = null;
262        synchronized (this.listeners) {
263            listeners = new AgentRosterListener[this.listeners.size()];
264            this.listeners.toArray(listeners);
265        }
266        for (int i = 0; i < listeners.length; i++) {
267            switch (eventType) {
268                case EVENT_AGENT_ADDED:
269                    listeners[i].agentAdded((String)eventObject);
270                    break;
271                case EVENT_AGENT_REMOVED:
272                    listeners[i].agentRemoved((String)eventObject);
273                    break;
274                case EVENT_PRESENCE_CHANGED:
275                    listeners[i].presenceChanged((Presence)eventObject);
276                    break;
277            }
278        }
279    }
280
281    /**
282     * Listens for all presence packets and processes them.
283     */
284    private class PresencePacketListener implements PacketListener {
285        public void processPacket(Packet packet) {
286            Presence presence = (Presence)packet;
287            String from = presence.getFrom();
288            if (from == null) {
289                // TODO Check if we need to ignore these presences or this is a server bug?
290                LOGGER.warning("Presence with no FROM: " + presence.toXML());
291                return;
292            }
293            String key = getPresenceMapKey(from);
294
295            // If an "available" packet, add it to the presence map. Each presence map will hold
296            // for a particular user a map with the presence packets saved for each resource.
297            if (presence.getType() == Presence.Type.available) {
298                // Ignore the presence packet unless it has an agent status extension.
299                AgentStatus agentStatus = (AgentStatus)presence.getExtension(
300                        AgentStatus.ELEMENT_NAME, AgentStatus.NAMESPACE);
301                if (agentStatus == null) {
302                    return;
303                }
304                // Ensure that this presence is coming from an Agent of the same workgroup
305                // of this Agent
306                else if (!workgroupJID.equals(agentStatus.getWorkgroupJID())) {
307                    return;
308                }
309                Map<String, Presence> userPresences;
310                // Get the user presence map
311                if (presenceMap.get(key) == null) {
312                    userPresences = new HashMap<String, Presence>();
313                    presenceMap.put(key, userPresences);
314                }
315                else {
316                    userPresences = presenceMap.get(key);
317                }
318                // Add the new presence, using the resources as a key.
319                synchronized (userPresences) {
320                    userPresences.put(StringUtils.parseResource(from), presence);
321                }
322                // Fire an event.
323                synchronized (entries) {
324                    for (Iterator<String> i = entries.iterator(); i.hasNext();) {
325                        String entry = i.next();
326                        if (entry.toLowerCase(Locale.US).equals(StringUtils.parseBareAddress(key).toLowerCase())) {
327                            fireEvent(EVENT_PRESENCE_CHANGED, packet);
328                        }
329                    }
330                }
331            }
332            // If an "unavailable" packet, remove any entries in the presence map.
333            else if (presence.getType() == Presence.Type.unavailable) {
334                if (presenceMap.get(key) != null) {
335                    Map<String,Presence> userPresences = presenceMap.get(key);
336                    synchronized (userPresences) {
337                        userPresences.remove(StringUtils.parseResource(from));
338                    }
339                    if (userPresences.isEmpty()) {
340                        presenceMap.remove(key);
341                    }
342                }
343                // Fire an event.
344                synchronized (entries) {
345                    for (Iterator<String> i = entries.iterator(); i.hasNext();) {
346                        String entry = (String)i.next();
347                        if (entry.toLowerCase(Locale.US).equals(StringUtils.parseBareAddress(key).toLowerCase())) {
348                            fireEvent(EVENT_PRESENCE_CHANGED, packet);
349                        }
350                    }
351                }
352            }
353        }
354    }
355
356    /**
357     * Listens for all roster packets and processes them.
358     */
359    private class AgentStatusListener implements PacketListener {
360
361        public void processPacket(Packet packet) {
362            if (packet instanceof AgentStatusRequest) {
363                AgentStatusRequest statusRequest = (AgentStatusRequest)packet;
364                for (Iterator<AgentStatusRequest.Item> i = statusRequest.getAgents().iterator(); i.hasNext();) {
365                    AgentStatusRequest.Item item = i.next();
366                    String agentJID = item.getJID();
367                    if ("remove".equals(item.getType())) {
368
369                        // Removing the user from the roster, so remove any presence information
370                        // about them.
371                        String key = StringUtils.parseName(StringUtils.parseName(agentJID) + "@" +
372                                StringUtils.parseServer(agentJID));
373                        presenceMap.remove(key);
374                        // Fire event for roster listeners.
375                        fireEvent(EVENT_AGENT_REMOVED, agentJID);
376                    }
377                    else {
378                        entries.add(agentJID);
379                        // Fire event for roster listeners.
380                        fireEvent(EVENT_AGENT_ADDED, agentJID);
381                    }
382                }
383
384                // Mark the roster as initialized.
385                rosterInitialized = true;
386            }
387        }
388    }
389}