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