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