aboutsummaryrefslogtreecommitdiff
path: root/src/org/jivesoftware/smackx/workgroup/agent/AgentRoster.java
blob: 70c95ee2e823e74dfccaec9f4fa7b954e8b4888e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
/**
 * $Revision$
 * $Date$
 *
 * Copyright 2003-2007 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.smackx.workgroup.agent;

import org.jivesoftware.smackx.workgroup.packet.AgentStatus;
import org.jivesoftware.smackx.workgroup.packet.AgentStatusRequest;
import org.jivesoftware.smack.PacketListener;
import org.jivesoftware.smack.Connection;
import org.jivesoftware.smack.filter.PacketFilter;
import org.jivesoftware.smack.filter.PacketTypeFilter;
import org.jivesoftware.smack.packet.Packet;
import org.jivesoftware.smack.packet.Presence;
import org.jivesoftware.smack.util.StringUtils;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * Manges information about the agents in a workgroup and their presence.
 *
 * @author Matt Tucker
 * @see AgentSession#getAgentRoster()
 */
public class AgentRoster {

    private static final int EVENT_AGENT_ADDED = 0;
    private static final int EVENT_AGENT_REMOVED = 1;
    private static final int EVENT_PRESENCE_CHANGED = 2;

    private Connection connection;
    private String workgroupJID;
    private List<String> entries;
    private List<AgentRosterListener> listeners;
    private Map<String, Map<String, Presence>> presenceMap;
    // The roster is marked as initialized when at least a single roster packet
    // has been recieved and processed.
    boolean rosterInitialized = false;

    /**
     * Constructs a new AgentRoster.
     *
     * @param connection an XMPP connection.
     */
    AgentRoster(Connection connection, String workgroupJID) {
        this.connection = connection;
        this.workgroupJID = workgroupJID;
        entries = new ArrayList<String>();
        listeners = new ArrayList<AgentRosterListener>();
        presenceMap = new HashMap<String, Map<String, Presence>>();
        // Listen for any roster packets.
        PacketFilter rosterFilter = new PacketTypeFilter(AgentStatusRequest.class);
        connection.addPacketListener(new AgentStatusListener(), rosterFilter);
        // Listen for any presence packets.
        connection.addPacketListener(new PresencePacketListener(),
                new PacketTypeFilter(Presence.class));

        // Send request for roster.
        AgentStatusRequest request = new AgentStatusRequest();
        request.setTo(workgroupJID);
        connection.sendPacket(request);
    }

    /**
     * Reloads the entire roster from the server. This is an asynchronous operation,
     * which means the method will return immediately, and the roster will be
     * reloaded at a later point when the server responds to the reload request.
     */
    public void reload() {
        AgentStatusRequest request = new AgentStatusRequest();
        request.setTo(workgroupJID);
        connection.sendPacket(request);
    }

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

                // Fire events for the existing entries and presences in the roster
                for (Iterator<String> it = getAgents().iterator(); it.hasNext();) {
                    String jid = it.next();
                    // Check again in case the agent is no longer in the roster (highly unlikely
                    // but possible)
                    if (entries.contains(jid)) {
                        // Fire the agent added event
                        listener.agentAdded(jid);
                        Map<String,Presence> userPresences = presenceMap.get(jid);
                        if (userPresences != null) {
                            Iterator<Presence> presences = userPresences.values().iterator();
                            while (presences.hasNext()) {
                                // Fire the presence changed event
                                listener.presenceChanged(presences.next());
                            }
                        }
                    }
                }
            }
        }
    }

    /**
     * Removes a listener from this roster. The listener will be fired anytime one or more
     * changes to the roster are pushed from the server.
     *
     * @param listener a roster listener.
     */
    public void removeListener(AgentRosterListener listener) {
        synchronized (listeners) {
            listeners.remove(listener);
        }
    }

    /**
     * Returns a count of all agents in the workgroup.
     *
     * @return the number of agents in the workgroup.
     */
    public int getAgentCount() {
        return entries.size();
    }

    /**
     * Returns all agents (String JID values) in the workgroup.
     *
     * @return all entries in the roster.
     */
    public Set<String> getAgents() {
        Set<String> agents = new HashSet<String>();
        synchronized (entries) {
            for (Iterator<String> i = entries.iterator(); i.hasNext();) {
                agents.add(i.next());
            }
        }
        return Collections.unmodifiableSet(agents);
    }

    /**
     * Returns true if the specified XMPP address is an agent in the workgroup.
     *
     * @param jid the XMPP address of the agent (eg "jsmith@example.com"). The
     *            address can be in any valid format (e.g. "domain/resource", "user@domain"
     *            or "user@domain/resource").
     * @return true if the XMPP address is an agent in the workgroup.
     */
    public boolean contains(String jid) {
        if (jid == null) {
            return false;
        }
        synchronized (entries) {
            for (Iterator<String> i = entries.iterator(); i.hasNext();) {
                String entry = i.next();
                if (entry.toLowerCase().equals(jid.toLowerCase())) {
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * Returns the presence info for a particular agent, or <tt>null</tt> if the agent
     * is unavailable (offline) or if no presence information is available.<p>
     *
     * @param user a fully qualified xmpp JID. The address could be in any valid format (e.g.
     *             "domain/resource", "user@domain" or "user@domain/resource").
     * @return the agent's current presence, or <tt>null</tt> if the agent is unavailable
     *         or if no presence information is available..
     */
    public Presence getPresence(String user) {
        String key = getPresenceMapKey(user);
        Map<String, Presence> userPresences = presenceMap.get(key);
        if (userPresences == null) {
            Presence presence = new Presence(Presence.Type.unavailable);
            presence.setFrom(user);
            return presence;
        }
        else {
            // Find the resource with the highest priority
            // Might be changed to use the resource with the highest availability instead.
            Iterator<String> it = userPresences.keySet().iterator();
            Presence p;
            Presence presence = null;

            while (it.hasNext()) {
                p = (Presence)userPresences.get(it.next());
                if (presence == null){
                    presence = p;
                }
                else {
                    if (p.getPriority() > presence.getPriority()) {
                        presence = p;
                    }
                }
            }
            if (presence == null) {
                presence = new Presence(Presence.Type.unavailable);
                presence.setFrom(user);
                return presence;
            }
            else {
                return presence;
            }
        }
    }

    /**
     * Returns the key to use in the presenceMap for a fully qualified xmpp ID. The roster
     * can contain any valid address format such us "domain/resource", "user@domain" or
     * "user@domain/resource". If the roster contains an entry associated with the fully qualified
     * xmpp ID then use the fully qualified xmpp ID as the key in presenceMap, otherwise use the
     * bare address. Note: When the key in presenceMap is a fully qualified xmpp ID, the
     * userPresences is useless since it will always contain one entry for the user.
     *
     * @param user the fully qualified xmpp ID, e.g. jdoe@example.com/Work.
     * @return the key to use in the presenceMap for the fully qualified xmpp ID.
     */
    private String getPresenceMapKey(String user) {
        String key = user;
        if (!contains(user)) {
            key = StringUtils.parseBareAddress(user).toLowerCase();
        }
        return key;
    }

    /**
     * Fires event to listeners.
     */
    private void fireEvent(int eventType, Object eventObject) {
        AgentRosterListener[] listeners = null;
        synchronized (this.listeners) {
            listeners = new AgentRosterListener[this.listeners.size()];
            this.listeners.toArray(listeners);
        }
        for (int i = 0; i < listeners.length; i++) {
            switch (eventType) {
                case EVENT_AGENT_ADDED:
                    listeners[i].agentAdded((String)eventObject);
                    break;
                case EVENT_AGENT_REMOVED:
                    listeners[i].agentRemoved((String)eventObject);
                    break;
                case EVENT_PRESENCE_CHANGED:
                    listeners[i].presenceChanged((Presence)eventObject);
                    break;
            }
        }
    }

    /**
     * Listens for all presence packets and processes them.
     */
    private class PresencePacketListener implements PacketListener {
        public void processPacket(Packet packet) {
            Presence presence = (Presence)packet;
            String from = presence.getFrom();
            if (from == null) {
                // TODO Check if we need to ignore these presences or this is a server bug?
                System.out.println("Presence with no FROM: " + presence.toXML());
                return;
            }
            String key = getPresenceMapKey(from);

            // If an "available" packet, add it to the presence map. Each presence map will hold
            // for a particular user a map with the presence packets saved for each resource.
            if (presence.getType() == Presence.Type.available) {
                // Ignore the presence packet unless it has an agent status extension.
                AgentStatus agentStatus = (AgentStatus)presence.getExtension(
                        AgentStatus.ELEMENT_NAME, AgentStatus.NAMESPACE);
                if (agentStatus == null) {
                    return;
                }
                // Ensure that this presence is coming from an Agent of the same workgroup
                // of this Agent
                else if (!workgroupJID.equals(agentStatus.getWorkgroupJID())) {
                    return;
                }
                Map<String, Presence> userPresences;
                // Get the user presence map
                if (presenceMap.get(key) == null) {
                    userPresences = new HashMap<String, Presence>();
                    presenceMap.put(key, userPresences);
                }
                else {
                    userPresences = presenceMap.get(key);
                }
                // Add the new presence, using the resources as a key.
                synchronized (userPresences) {
                    userPresences.put(StringUtils.parseResource(from), presence);
                }
                // Fire an event.
                synchronized (entries) {
                    for (Iterator<String> i = entries.iterator(); i.hasNext();) {
                        String entry = i.next();
                        if (entry.toLowerCase().equals(StringUtils.parseBareAddress(key).toLowerCase())) {
                            fireEvent(EVENT_PRESENCE_CHANGED, packet);
                        }
                    }
                }
            }
            // If an "unavailable" packet, remove any entries in the presence map.
            else if (presence.getType() == Presence.Type.unavailable) {
                if (presenceMap.get(key) != null) {
                    Map<String,Presence> userPresences = presenceMap.get(key);
                    synchronized (userPresences) {
                        userPresences.remove(StringUtils.parseResource(from));
                    }
                    if (userPresences.isEmpty()) {
                        presenceMap.remove(key);
                    }
                }
                // Fire an event.
                synchronized (entries) {
                    for (Iterator<String> i = entries.iterator(); i.hasNext();) {
                        String entry = (String)i.next();
                        if (entry.toLowerCase().equals(StringUtils.parseBareAddress(key).toLowerCase())) {
                            fireEvent(EVENT_PRESENCE_CHANGED, packet);
                        }
                    }
                }
            }
        }
    }

    /**
     * Listens for all roster packets and processes them.
     */
    private class AgentStatusListener implements PacketListener {

        public void processPacket(Packet packet) {
            if (packet instanceof AgentStatusRequest) {
                AgentStatusRequest statusRequest = (AgentStatusRequest)packet;
                for (Iterator<AgentStatusRequest.Item> i = statusRequest.getAgents().iterator(); i.hasNext();) {
                    AgentStatusRequest.Item item = i.next();
                    String agentJID = item.getJID();
                    if ("remove".equals(item.getType())) {

                        // Removing the user from the roster, so remove any presence information
                        // about them.
                        String key = StringUtils.parseName(StringUtils.parseName(agentJID) + "@" +
                                StringUtils.parseServer(agentJID));
                        presenceMap.remove(key);
                        // Fire event for roster listeners.
                        fireEvent(EVENT_AGENT_REMOVED, agentJID);
                    }
                    else {
                        entries.add(agentJID);
                        // Fire event for roster listeners.
                        fireEvent(EVENT_AGENT_ADDED, agentJID);
                    }
                }

                // Mark the roster as initialized.
                rosterInitialized = true;
            }
        }
    }
}