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}