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}