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}