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