1 /** 2 * $Revision$ 3 * $Date$ 4 * 5 * Copyright 2003-2007 Jive Software. 6 * 7 * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); 8 * you may not use this file except in compliance with the License. 9 * You may obtain a copy of the License at 10 * 11 * http://www.apache.org/licenses/LICENSE-2.0 12 * 13 * Unless required by applicable law or agreed to in writing, software 14 * distributed under the License is distributed on an "AS IS" BASIS, 15 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 * See the License for the specific language governing permissions and 17 * limitations under the License. 18 */ 19 20 package org.jivesoftware.smackx.workgroup.agent; 21 22 import org.jivesoftware.smackx.workgroup.packet.AgentStatus; 23 import org.jivesoftware.smackx.workgroup.packet.AgentStatusRequest; 24 import org.jivesoftware.smack.PacketListener; 25 import org.jivesoftware.smack.Connection; 26 import org.jivesoftware.smack.filter.PacketFilter; 27 import org.jivesoftware.smack.filter.PacketTypeFilter; 28 import org.jivesoftware.smack.packet.Packet; 29 import org.jivesoftware.smack.packet.Presence; 30 import org.jivesoftware.smack.util.StringUtils; 31 32 import java.util.ArrayList; 33 import java.util.Collections; 34 import java.util.HashMap; 35 import java.util.HashSet; 36 import java.util.Iterator; 37 import java.util.List; 38 import java.util.Map; 39 import java.util.Set; 40 41 /** 42 * Manges information about the agents in a workgroup and their presence. 43 * 44 * @author Matt Tucker 45 * @see AgentSession#getAgentRoster() 46 */ 47 public class AgentRoster { 48 49 private static final int EVENT_AGENT_ADDED = 0; 50 private static final int EVENT_AGENT_REMOVED = 1; 51 private static final int EVENT_PRESENCE_CHANGED = 2; 52 53 private Connection connection; 54 private String workgroupJID; 55 private List<String> entries; 56 private List<AgentRosterListener> listeners; 57 private Map<String, Map<String, Presence>> presenceMap; 58 // The roster is marked as initialized when at least a single roster packet 59 // has been recieved and processed. 60 boolean rosterInitialized = false; 61 62 /** 63 * Constructs a new AgentRoster. 64 * 65 * @param connection an XMPP connection. 66 */ 67 AgentRoster(Connection connection, String workgroupJID) { 68 this.connection = connection; 69 this.workgroupJID = workgroupJID; 70 entries = new ArrayList<String>(); 71 listeners = new ArrayList<AgentRosterListener>(); 72 presenceMap = new HashMap<String, Map<String, Presence>>(); 73 // Listen for any roster packets. 74 PacketFilter rosterFilter = new PacketTypeFilter(AgentStatusRequest.class); 75 connection.addPacketListener(new AgentStatusListener(), rosterFilter); 76 // Listen for any presence packets. 77 connection.addPacketListener(new PresencePacketListener(), 78 new PacketTypeFilter(Presence.class)); 79 80 // Send request for roster. 81 AgentStatusRequest request = new AgentStatusRequest(); 82 request.setTo(workgroupJID); 83 connection.sendPacket(request); 84 } 85 86 /** 87 * Reloads the entire roster from the server. This is an asynchronous operation, 88 * which means the method will return immediately, and the roster will be 89 * reloaded at a later point when the server responds to the reload request. 90 */ 91 public void reload() { 92 AgentStatusRequest request = new AgentStatusRequest(); 93 request.setTo(workgroupJID); 94 connection.sendPacket(request); 95 } 96 97 /** 98 * Adds a listener to this roster. The listener will be fired anytime one or more 99 * changes to the roster are pushed from the server. 100 * 101 * @param listener an agent roster listener. 102 */ 103 public void addListener(AgentRosterListener listener) { 104 synchronized (listeners) { 105 if (!listeners.contains(listener)) { 106 listeners.add(listener); 107 108 // Fire events for the existing entries and presences in the roster 109 for (Iterator<String> it = getAgents().iterator(); it.hasNext();) { 110 String jid = it.next(); 111 // Check again in case the agent is no longer in the roster (highly unlikely 112 // but possible) 113 if (entries.contains(jid)) { 114 // Fire the agent added event 115 listener.agentAdded(jid); 116 Map<String,Presence> userPresences = presenceMap.get(jid); 117 if (userPresences != null) { 118 Iterator<Presence> presences = userPresences.values().iterator(); 119 while (presences.hasNext()) { 120 // Fire the presence changed event 121 listener.presenceChanged(presences.next()); 122 } 123 } 124 } 125 } 126 } 127 } 128 } 129 130 /** 131 * Removes a listener from this roster. The listener will be fired anytime one or more 132 * changes to the roster are pushed from the server. 133 * 134 * @param listener a roster listener. 135 */ 136 public void removeListener(AgentRosterListener listener) { 137 synchronized (listeners) { 138 listeners.remove(listener); 139 } 140 } 141 142 /** 143 * Returns a count of all agents in the workgroup. 144 * 145 * @return the number of agents in the workgroup. 146 */ 147 public int getAgentCount() { 148 return entries.size(); 149 } 150 151 /** 152 * Returns all agents (String JID values) in the workgroup. 153 * 154 * @return all entries in the roster. 155 */ 156 public Set<String> getAgents() { 157 Set<String> agents = new HashSet<String>(); 158 synchronized (entries) { 159 for (Iterator<String> i = entries.iterator(); i.hasNext();) { 160 agents.add(i.next()); 161 } 162 } 163 return Collections.unmodifiableSet(agents); 164 } 165 166 /** 167 * Returns true if the specified XMPP address is an agent in the workgroup. 168 * 169 * @param jid the XMPP address of the agent (eg "jsmith (at) example.com"). The 170 * address can be in any valid format (e.g. "domain/resource", "user@domain" 171 * or "user@domain/resource"). 172 * @return true if the XMPP address is an agent in the workgroup. 173 */ 174 public boolean contains(String jid) { 175 if (jid == null) { 176 return false; 177 } 178 synchronized (entries) { 179 for (Iterator<String> i = entries.iterator(); i.hasNext();) { 180 String entry = i.next(); 181 if (entry.toLowerCase().equals(jid.toLowerCase())) { 182 return true; 183 } 184 } 185 } 186 return false; 187 } 188 189 /** 190 * Returns the presence info for a particular agent, or <tt>null</tt> if the agent 191 * is unavailable (offline) or if no presence information is available.<p> 192 * 193 * @param user a fully qualified xmpp JID. The address could be in any valid format (e.g. 194 * "domain/resource", "user@domain" or "user@domain/resource"). 195 * @return the agent's current presence, or <tt>null</tt> if the agent is unavailable 196 * or if no presence information is available.. 197 */ 198 public Presence getPresence(String user) { 199 String key = getPresenceMapKey(user); 200 Map<String, Presence> userPresences = presenceMap.get(key); 201 if (userPresences == null) { 202 Presence presence = new Presence(Presence.Type.unavailable); 203 presence.setFrom(user); 204 return presence; 205 } 206 else { 207 // Find the resource with the highest priority 208 // Might be changed to use the resource with the highest availability instead. 209 Iterator<String> it = userPresences.keySet().iterator(); 210 Presence p; 211 Presence presence = null; 212 213 while (it.hasNext()) { 214 p = (Presence)userPresences.get(it.next()); 215 if (presence == null){ 216 presence = p; 217 } 218 else { 219 if (p.getPriority() > presence.getPriority()) { 220 presence = p; 221 } 222 } 223 } 224 if (presence == null) { 225 presence = new Presence(Presence.Type.unavailable); 226 presence.setFrom(user); 227 return presence; 228 } 229 else { 230 return presence; 231 } 232 } 233 } 234 235 /** 236 * Returns the key to use in the presenceMap for a fully qualified xmpp ID. The roster 237 * can contain any valid address format such us "domain/resource", "user@domain" or 238 * "user@domain/resource". If the roster contains an entry associated with the fully qualified 239 * xmpp ID then use the fully qualified xmpp ID as the key in presenceMap, otherwise use the 240 * bare address. Note: When the key in presenceMap is a fully qualified xmpp ID, the 241 * userPresences is useless since it will always contain one entry for the user. 242 * 243 * @param user the fully qualified xmpp ID, e.g. jdoe (at) example.com/Work. 244 * @return the key to use in the presenceMap for the fully qualified xmpp ID. 245 */ 246 private String getPresenceMapKey(String user) { 247 String key = user; 248 if (!contains(user)) { 249 key = StringUtils.parseBareAddress(user).toLowerCase(); 250 } 251 return key; 252 } 253 254 /** 255 * Fires event to listeners. 256 */ 257 private void fireEvent(int eventType, Object eventObject) { 258 AgentRosterListener[] listeners = null; 259 synchronized (this.listeners) { 260 listeners = new AgentRosterListener[this.listeners.size()]; 261 this.listeners.toArray(listeners); 262 } 263 for (int i = 0; i < listeners.length; i++) { 264 switch (eventType) { 265 case EVENT_AGENT_ADDED: 266 listeners[i].agentAdded((String)eventObject); 267 break; 268 case EVENT_AGENT_REMOVED: 269 listeners[i].agentRemoved((String)eventObject); 270 break; 271 case EVENT_PRESENCE_CHANGED: 272 listeners[i].presenceChanged((Presence)eventObject); 273 break; 274 } 275 } 276 } 277 278 /** 279 * Listens for all presence packets and processes them. 280 */ 281 private class PresencePacketListener implements PacketListener { 282 public void processPacket(Packet packet) { 283 Presence presence = (Presence)packet; 284 String from = presence.getFrom(); 285 if (from == null) { 286 // TODO Check if we need to ignore these presences or this is a server bug? 287 System.out.println("Presence with no FROM: " + presence.toXML()); 288 return; 289 } 290 String key = getPresenceMapKey(from); 291 292 // If an "available" packet, add it to the presence map. Each presence map will hold 293 // for a particular user a map with the presence packets saved for each resource. 294 if (presence.getType() == Presence.Type.available) { 295 // Ignore the presence packet unless it has an agent status extension. 296 AgentStatus agentStatus = (AgentStatus)presence.getExtension( 297 AgentStatus.ELEMENT_NAME, AgentStatus.NAMESPACE); 298 if (agentStatus == null) { 299 return; 300 } 301 // Ensure that this presence is coming from an Agent of the same workgroup 302 // of this Agent 303 else if (!workgroupJID.equals(agentStatus.getWorkgroupJID())) { 304 return; 305 } 306 Map<String, Presence> userPresences; 307 // Get the user presence map 308 if (presenceMap.get(key) == null) { 309 userPresences = new HashMap<String, Presence>(); 310 presenceMap.put(key, userPresences); 311 } 312 else { 313 userPresences = presenceMap.get(key); 314 } 315 // Add the new presence, using the resources as a key. 316 synchronized (userPresences) { 317 userPresences.put(StringUtils.parseResource(from), presence); 318 } 319 // Fire an event. 320 synchronized (entries) { 321 for (Iterator<String> i = entries.iterator(); i.hasNext();) { 322 String entry = i.next(); 323 if (entry.toLowerCase().equals(StringUtils.parseBareAddress(key).toLowerCase())) { 324 fireEvent(EVENT_PRESENCE_CHANGED, packet); 325 } 326 } 327 } 328 } 329 // If an "unavailable" packet, remove any entries in the presence map. 330 else if (presence.getType() == Presence.Type.unavailable) { 331 if (presenceMap.get(key) != null) { 332 Map<String,Presence> userPresences = presenceMap.get(key); 333 synchronized (userPresences) { 334 userPresences.remove(StringUtils.parseResource(from)); 335 } 336 if (userPresences.isEmpty()) { 337 presenceMap.remove(key); 338 } 339 } 340 // Fire an event. 341 synchronized (entries) { 342 for (Iterator<String> i = entries.iterator(); i.hasNext();) { 343 String entry = (String)i.next(); 344 if (entry.toLowerCase().equals(StringUtils.parseBareAddress(key).toLowerCase())) { 345 fireEvent(EVENT_PRESENCE_CHANGED, packet); 346 } 347 } 348 } 349 } 350 } 351 } 352 353 /** 354 * Listens for all roster packets and processes them. 355 */ 356 private class AgentStatusListener implements PacketListener { 357 358 public void processPacket(Packet packet) { 359 if (packet instanceof AgentStatusRequest) { 360 AgentStatusRequest statusRequest = (AgentStatusRequest)packet; 361 for (Iterator<AgentStatusRequest.Item> i = statusRequest.getAgents().iterator(); i.hasNext();) { 362 AgentStatusRequest.Item item = i.next(); 363 String agentJID = item.getJID(); 364 if ("remove".equals(item.getType())) { 365 366 // Removing the user from the roster, so remove any presence information 367 // about them. 368 String key = StringUtils.parseName(StringUtils.parseName(agentJID) + "@" + 369 StringUtils.parseServer(agentJID)); 370 presenceMap.remove(key); 371 // Fire event for roster listeners. 372 fireEvent(EVENT_AGENT_REMOVED, agentJID); 373 } 374 else { 375 entries.add(agentJID); 376 // Fire event for roster listeners. 377 fireEvent(EVENT_AGENT_ADDED, agentJID); 378 } 379 } 380 381 // Mark the roster as initialized. 382 rosterInitialized = true; 383 } 384 } 385 } 386 }