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.EntityBareJid;
042import org.jxmpp.jid.EntityFullJid;
043import org.jxmpp.jid.Jid;
044import org.jxmpp.jid.impl.JidCreate;
045import org.jxmpp.jid.parts.Resourcepart;
046import org.jxmpp.stringprep.XmppStringprepException;
047
048/**
049 * Manges information about the agents in a workgroup and their presence.
050 *
051 * @author Matt Tucker
052 * @see AgentSession#getAgentRoster()
053 */
054public class AgentRoster {
055    private static final Logger LOGGER = Logger.getLogger(AgentRoster.class.getName());
056    private static final int EVENT_AGENT_ADDED = 0;
057    private static final int EVENT_AGENT_REMOVED = 1;
058    private static final int EVENT_PRESENCE_CHANGED = 2;
059
060    private final XMPPConnection connection;
061    private final EntityBareJid workgroupJID;
062    private final List<EntityBareJid> entries = new ArrayList<>();
063    private final List<AgentRosterListener> listeners = new ArrayList<>();
064    private final Map<Jid, Map<Resourcepart, Presence>> presenceMap = new HashMap<>();
065    // The roster is marked as initialized when at least a single roster packet
066    // has been recieved and processed.
067    boolean rosterInitialized = false;
068
069    /**
070     * Constructs a new AgentRoster.
071     *
072     * @param connection an XMPP connection.
073     * @throws NotConnectedException
074     * @throws InterruptedException
075     */
076    AgentRoster(XMPPConnection connection, EntityBareJid workgroupJID) throws NotConnectedException, InterruptedException {
077        this.connection = connection;
078        this.workgroupJID = workgroupJID;
079        // Listen for any roster packets.
080        StanzaFilter rosterFilter = new StanzaTypeFilter(AgentStatusRequest.class);
081        connection.addAsyncStanzaListener(new AgentStatusListener(), rosterFilter);
082        // Listen for any presence packets.
083        connection.addAsyncStanzaListener(new PresencePacketListener(),
084                new StanzaTypeFilter(Presence.class));
085
086        // Send request for roster.
087        AgentStatusRequest request = new AgentStatusRequest();
088        request.setTo(workgroupJID);
089        connection.sendStanza(request);
090    }
091
092    /**
093     * Reloads the entire roster from the server. This is an asynchronous operation,
094     * which means the method will return immediately, and the roster will be
095     * reloaded at a later point when the server responds to the reload request.
096     * @throws NotConnectedException
097     * @throws InterruptedException
098     */
099    public void reload() throws NotConnectedException, InterruptedException {
100        AgentStatusRequest request = new AgentStatusRequest();
101        request.setTo(workgroupJID);
102        connection.sendStanza(request);
103    }
104
105    /**
106     * Adds a listener to this roster. The listener will be fired anytime one or more
107     * changes to the roster are pushed from the server.
108     *
109     * @param listener an agent roster listener.
110     */
111    public void addListener(AgentRosterListener listener) {
112        synchronized (listeners) {
113            if (!listeners.contains(listener)) {
114                listeners.add(listener);
115
116                // Fire events for the existing entries and presences in the roster
117                for (EntityBareJid jid : getAgents()) {
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<EntityBareJid> getAgents() {
171        Set<EntityBareJid> agents = new HashSet<>();
172        synchronized (entries) {
173            for (Iterator<EntityBareJid> 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    public boolean contains(Jid jid) {
189        if (jid == null) {
190            return false;
191        }
192        synchronized (entries) {
193            for (Iterator<EntityBareJid> i = entries.iterator(); i.hasNext();) {
194                EntityBareJid entry = i.next();
195                if (entry.equals(jid)) {
196                    return true;
197                }
198            }
199        }
200        return false;
201    }
202
203    /**
204     * Returns the presence info for a particular agent, or <tt>null</tt> if the agent
205     * is unavailable (offline) or if no presence information is available.<p>
206     *
207     * @param user a fully qualified xmpp JID. The address could be in any valid format (e.g.
208     *             "domain/resource", "user@domain" or "user@domain/resource").
209     * @return the agent's current presence, or <tt>null</tt> if the agent is unavailable
210     *         or if no presence information is available..
211     */
212    public Presence getPresence(Jid user) {
213        Jid key = getPresenceMapKey(user);
214        Map<Resourcepart, Presence> userPresences = presenceMap.get(key);
215        if (userPresences == null) {
216            Presence presence = new Presence(Presence.Type.unavailable);
217            presence.setFrom(user);
218            return presence;
219        }
220        else {
221            // Find the resource with the highest priority
222            // Might be changed to use the resource with the highest availability instead.
223            Iterator<Resourcepart> it = userPresences.keySet().iterator();
224            Presence p;
225            Presence presence = null;
226
227            while (it.hasNext()) {
228                p = userPresences.get(it.next());
229                if (presence == null) {
230                    presence = p;
231                }
232                else {
233                    if (p.getPriority() > presence.getPriority()) {
234                        presence = p;
235                    }
236                }
237            }
238            if (presence == null) {
239                presence = new Presence(Presence.Type.unavailable);
240                presence.setFrom(user);
241                return presence;
242            }
243            else {
244                return presence;
245            }
246        }
247    }
248
249    /**
250     * Returns the key to use in the presenceMap for a fully qualified xmpp ID. The roster
251     * can contain any valid address format such us "domain/resource", "user@domain" or
252     * "user@domain/resource". If the roster contains an entry associated with the fully qualified
253     * xmpp ID then use the fully qualified xmpp ID as the key in presenceMap, otherwise use the
254     * bare address. Note: When the key in presenceMap is a fully qualified xmpp ID, the
255     * userPresences is useless since it will always contain one entry for the user.
256     *
257     * @param user the fully qualified xmpp ID, e.g. jdoe@example.com/Work.
258     * @return the key to use in the presenceMap for the fully qualified xmpp ID.
259     */
260    private Jid getPresenceMapKey(Jid user) {
261        Jid key = user;
262        if (!contains(user)) {
263            key = user.asEntityBareJidIfPossible();
264        }
265        return key;
266    }
267
268    /**
269     * Fires event to listeners.
270     */
271    private void fireEvent(int eventType, Object eventObject) {
272        AgentRosterListener[] listeners;
273        synchronized (this.listeners) {
274            listeners = new AgentRosterListener[this.listeners.size()];
275            this.listeners.toArray(listeners);
276        }
277        for (int i = 0; i < listeners.length; i++) {
278            switch (eventType) {
279                case EVENT_AGENT_ADDED:
280                    listeners[i].agentAdded((EntityBareJid) eventObject);
281                    break;
282                case EVENT_AGENT_REMOVED:
283                    listeners[i].agentRemoved((EntityBareJid) eventObject);
284                    break;
285                case EVENT_PRESENCE_CHANGED:
286                    listeners[i].presenceChanged((Presence) eventObject);
287                    break;
288            }
289        }
290    }
291
292    /**
293     * Listens for all presence packets and processes them.
294     */
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(null));
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 = 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<EntityBareJid> i = entries.iterator(); i.hasNext();) {
337                        EntityBareJid 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 (EntityBareJid entry : entries) {
358                        if (entry.equals(key.asEntityBareJidIfPossible())) {
359                            fireEvent(EVENT_PRESENCE_CHANGED, packet);
360                        }
361                    }
362                }
363            }
364        }
365    }
366
367    /**
368     * Listens for all roster packets and processes them.
369     */
370    private class AgentStatusListener implements StanzaListener {
371
372        @Override
373        public void processStanza(Stanza packet) {
374            if (packet instanceof AgentStatusRequest) {
375                AgentStatusRequest statusRequest = (AgentStatusRequest) packet;
376                for (Iterator<AgentStatusRequest.Item> i = statusRequest.getAgents().iterator(); i.hasNext();) {
377                    AgentStatusRequest.Item item = i.next();
378                    EntityBareJid agentJID = item.getJID();
379                    if ("remove".equals(item.getType())) {
380
381                        // Removing the user from the roster, so remove any presence information
382                        // about them.
383                        presenceMap.remove(agentJID.asBareJid());
384                        // Fire event for roster listeners.
385                        fireEvent(EVENT_AGENT_REMOVED, agentJID);
386                    }
387                    else {
388                        entries.add(agentJID);
389                        // Fire event for roster listeners.
390                        fireEvent(EVENT_AGENT_ADDED, agentJID);
391                    }
392                }
393
394                // Mark the roster as initialized.
395                rosterInitialized = true;
396            }
397        }
398    }
399}