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
20package org.jivesoftware.smackx.workgroup.agent;
21
22import org.jivesoftware.smackx.workgroup.packet.AgentStatus;
23import org.jivesoftware.smackx.workgroup.packet.AgentStatusRequest;
24import org.jivesoftware.smack.PacketListener;
25import org.jivesoftware.smack.Connection;
26import org.jivesoftware.smack.filter.PacketFilter;
27import org.jivesoftware.smack.filter.PacketTypeFilter;
28import org.jivesoftware.smack.packet.Packet;
29import org.jivesoftware.smack.packet.Presence;
30import org.jivesoftware.smack.util.StringUtils;
31
32import java.util.ArrayList;
33import java.util.Collections;
34import java.util.HashMap;
35import java.util.HashSet;
36import java.util.Iterator;
37import java.util.List;
38import java.util.Map;
39import 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 */
47public 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@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@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}