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}