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.PacketListener; 023import org.jivesoftware.smack.SmackException.NotConnectedException; 024import org.jivesoftware.smack.XMPPConnection; 025import org.jivesoftware.smack.filter.PacketFilter; 026import org.jivesoftware.smack.filter.PacketTypeFilter; 027import org.jivesoftware.smack.packet.Packet; 028import org.jivesoftware.smack.packet.Presence; 029import org.jivesoftware.smack.util.StringUtils; 030 031import java.util.ArrayList; 032import java.util.Collections; 033import java.util.HashMap; 034import java.util.HashSet; 035import java.util.Iterator; 036import java.util.List; 037import java.util.Locale; 038import java.util.Map; 039import java.util.Set; 040import java.util.logging.Logger; 041 042/** 043 * Manges information about the agents in a workgroup and their presence. 044 * 045 * @author Matt Tucker 046 * @see AgentSession#getAgentRoster() 047 */ 048public class AgentRoster { 049 private static final Logger LOGGER = Logger.getLogger(AgentRoster.class.getName()); 050 private static final int EVENT_AGENT_ADDED = 0; 051 private static final int EVENT_AGENT_REMOVED = 1; 052 private static final int EVENT_PRESENCE_CHANGED = 2; 053 054 private XMPPConnection connection; 055 private String workgroupJID; 056 private List<String> entries; 057 private List<AgentRosterListener> listeners; 058 private Map<String, Map<String, Presence>> presenceMap; 059 // The roster is marked as initialized when at least a single roster packet 060 // has been recieved and processed. 061 boolean rosterInitialized = false; 062 063 /** 064 * Constructs a new AgentRoster. 065 * 066 * @param connection an XMPP connection. 067 * @throws NotConnectedException 068 */ 069 AgentRoster(XMPPConnection connection, String workgroupJID) throws NotConnectedException { 070 this.connection = connection; 071 this.workgroupJID = workgroupJID; 072 entries = new ArrayList<String>(); 073 listeners = new ArrayList<AgentRosterListener>(); 074 presenceMap = new HashMap<String, Map<String, Presence>>(); 075 // Listen for any roster packets. 076 PacketFilter rosterFilter = new PacketTypeFilter(AgentStatusRequest.class); 077 connection.addPacketListener(new AgentStatusListener(), rosterFilter); 078 // Listen for any presence packets. 079 connection.addPacketListener(new PresencePacketListener(), 080 new PacketTypeFilter(Presence.class)); 081 082 // Send request for roster. 083 AgentStatusRequest request = new AgentStatusRequest(); 084 request.setTo(workgroupJID); 085 connection.sendPacket(request); 086 } 087 088 /** 089 * Reloads the entire roster from the server. This is an asynchronous operation, 090 * which means the method will return immediately, and the roster will be 091 * reloaded at a later point when the server responds to the reload request. 092 * @throws NotConnectedException 093 */ 094 public void reload() throws NotConnectedException { 095 AgentStatusRequest request = new AgentStatusRequest(); 096 request.setTo(workgroupJID); 097 connection.sendPacket(request); 098 } 099 100 /** 101 * Adds a listener to this roster. The listener will be fired anytime one or more 102 * changes to the roster are pushed from the server. 103 * 104 * @param listener an agent roster listener. 105 */ 106 public void addListener(AgentRosterListener listener) { 107 synchronized (listeners) { 108 if (!listeners.contains(listener)) { 109 listeners.add(listener); 110 111 // Fire events for the existing entries and presences in the roster 112 for (Iterator<String> it = getAgents().iterator(); it.hasNext();) { 113 String jid = it.next(); 114 // Check again in case the agent is no longer in the roster (highly unlikely 115 // but possible) 116 if (entries.contains(jid)) { 117 // Fire the agent added event 118 listener.agentAdded(jid); 119 Map<String,Presence> userPresences = presenceMap.get(jid); 120 if (userPresences != null) { 121 Iterator<Presence> presences = userPresences.values().iterator(); 122 while (presences.hasNext()) { 123 // Fire the presence changed event 124 listener.presenceChanged(presences.next()); 125 } 126 } 127 } 128 } 129 } 130 } 131 } 132 133 /** 134 * Removes a listener from this roster. The listener will be fired anytime one or more 135 * changes to the roster are pushed from the server. 136 * 137 * @param listener a roster listener. 138 */ 139 public void removeListener(AgentRosterListener listener) { 140 synchronized (listeners) { 141 listeners.remove(listener); 142 } 143 } 144 145 /** 146 * Returns a count of all agents in the workgroup. 147 * 148 * @return the number of agents in the workgroup. 149 */ 150 public int getAgentCount() { 151 return entries.size(); 152 } 153 154 /** 155 * Returns all agents (String JID values) in the workgroup. 156 * 157 * @return all entries in the roster. 158 */ 159 public Set<String> getAgents() { 160 Set<String> agents = new HashSet<String>(); 161 synchronized (entries) { 162 for (Iterator<String> i = entries.iterator(); i.hasNext();) { 163 agents.add(i.next()); 164 } 165 } 166 return Collections.unmodifiableSet(agents); 167 } 168 169 /** 170 * Returns true if the specified XMPP address is an agent in the workgroup. 171 * 172 * @param jid the XMPP address of the agent (eg "jsmith@example.com"). The 173 * address can be in any valid format (e.g. "domain/resource", "user@domain" 174 * or "user@domain/resource"). 175 * @return true if the XMPP address is an agent in the workgroup. 176 */ 177 public boolean contains(String jid) { 178 if (jid == null) { 179 return false; 180 } 181 synchronized (entries) { 182 for (Iterator<String> i = entries.iterator(); i.hasNext();) { 183 String entry = i.next(); 184 if (entry.toLowerCase(Locale.US).equals(jid.toLowerCase())) { 185 return true; 186 } 187 } 188 } 189 return false; 190 } 191 192 /** 193 * Returns the presence info for a particular agent, or <tt>null</tt> if the agent 194 * is unavailable (offline) or if no presence information is available.<p> 195 * 196 * @param user a fully qualified xmpp JID. The address could be in any valid format (e.g. 197 * "domain/resource", "user@domain" or "user@domain/resource"). 198 * @return the agent's current presence, or <tt>null</tt> if the agent is unavailable 199 * or if no presence information is available.. 200 */ 201 public Presence getPresence(String user) { 202 String key = getPresenceMapKey(user); 203 Map<String, Presence> userPresences = presenceMap.get(key); 204 if (userPresences == null) { 205 Presence presence = new Presence(Presence.Type.unavailable); 206 presence.setFrom(user); 207 return presence; 208 } 209 else { 210 // Find the resource with the highest priority 211 // Might be changed to use the resource with the highest availability instead. 212 Iterator<String> it = userPresences.keySet().iterator(); 213 Presence p; 214 Presence presence = null; 215 216 while (it.hasNext()) { 217 p = (Presence)userPresences.get(it.next()); 218 if (presence == null){ 219 presence = p; 220 } 221 else { 222 if (p.getPriority() > presence.getPriority()) { 223 presence = p; 224 } 225 } 226 } 227 if (presence == null) { 228 presence = new Presence(Presence.Type.unavailable); 229 presence.setFrom(user); 230 return presence; 231 } 232 else { 233 return presence; 234 } 235 } 236 } 237 238 /** 239 * Returns the key to use in the presenceMap for a fully qualified xmpp ID. The roster 240 * can contain any valid address format such us "domain/resource", "user@domain" or 241 * "user@domain/resource". If the roster contains an entry associated with the fully qualified 242 * xmpp ID then use the fully qualified xmpp ID as the key in presenceMap, otherwise use the 243 * bare address. Note: When the key in presenceMap is a fully qualified xmpp ID, the 244 * userPresences is useless since it will always contain one entry for the user. 245 * 246 * @param user the fully qualified xmpp ID, e.g. jdoe@example.com/Work. 247 * @return the key to use in the presenceMap for the fully qualified xmpp ID. 248 */ 249 private String getPresenceMapKey(String user) { 250 String key = user; 251 if (!contains(user)) { 252 key = StringUtils.parseBareAddress(user).toLowerCase(Locale.US); 253 } 254 return key; 255 } 256 257 /** 258 * Fires event to listeners. 259 */ 260 private void fireEvent(int eventType, Object eventObject) { 261 AgentRosterListener[] listeners = null; 262 synchronized (this.listeners) { 263 listeners = new AgentRosterListener[this.listeners.size()]; 264 this.listeners.toArray(listeners); 265 } 266 for (int i = 0; i < listeners.length; i++) { 267 switch (eventType) { 268 case EVENT_AGENT_ADDED: 269 listeners[i].agentAdded((String)eventObject); 270 break; 271 case EVENT_AGENT_REMOVED: 272 listeners[i].agentRemoved((String)eventObject); 273 break; 274 case EVENT_PRESENCE_CHANGED: 275 listeners[i].presenceChanged((Presence)eventObject); 276 break; 277 } 278 } 279 } 280 281 /** 282 * Listens for all presence packets and processes them. 283 */ 284 private class PresencePacketListener implements PacketListener { 285 public void processPacket(Packet packet) { 286 Presence presence = (Presence)packet; 287 String from = presence.getFrom(); 288 if (from == null) { 289 // TODO Check if we need to ignore these presences or this is a server bug? 290 LOGGER.warning("Presence with no FROM: " + presence.toXML()); 291 return; 292 } 293 String key = getPresenceMapKey(from); 294 295 // If an "available" packet, add it to the presence map. Each presence map will hold 296 // for a particular user a map with the presence packets saved for each resource. 297 if (presence.getType() == Presence.Type.available) { 298 // Ignore the presence packet unless it has an agent status extension. 299 AgentStatus agentStatus = (AgentStatus)presence.getExtension( 300 AgentStatus.ELEMENT_NAME, AgentStatus.NAMESPACE); 301 if (agentStatus == null) { 302 return; 303 } 304 // Ensure that this presence is coming from an Agent of the same workgroup 305 // of this Agent 306 else if (!workgroupJID.equals(agentStatus.getWorkgroupJID())) { 307 return; 308 } 309 Map<String, Presence> userPresences; 310 // Get the user presence map 311 if (presenceMap.get(key) == null) { 312 userPresences = new HashMap<String, Presence>(); 313 presenceMap.put(key, userPresences); 314 } 315 else { 316 userPresences = presenceMap.get(key); 317 } 318 // Add the new presence, using the resources as a key. 319 synchronized (userPresences) { 320 userPresences.put(StringUtils.parseResource(from), presence); 321 } 322 // Fire an event. 323 synchronized (entries) { 324 for (Iterator<String> i = entries.iterator(); i.hasNext();) { 325 String entry = i.next(); 326 if (entry.toLowerCase(Locale.US).equals(StringUtils.parseBareAddress(key).toLowerCase())) { 327 fireEvent(EVENT_PRESENCE_CHANGED, packet); 328 } 329 } 330 } 331 } 332 // If an "unavailable" packet, remove any entries in the presence map. 333 else if (presence.getType() == Presence.Type.unavailable) { 334 if (presenceMap.get(key) != null) { 335 Map<String,Presence> userPresences = presenceMap.get(key); 336 synchronized (userPresences) { 337 userPresences.remove(StringUtils.parseResource(from)); 338 } 339 if (userPresences.isEmpty()) { 340 presenceMap.remove(key); 341 } 342 } 343 // Fire an event. 344 synchronized (entries) { 345 for (Iterator<String> i = entries.iterator(); i.hasNext();) { 346 String entry = (String)i.next(); 347 if (entry.toLowerCase(Locale.US).equals(StringUtils.parseBareAddress(key).toLowerCase())) { 348 fireEvent(EVENT_PRESENCE_CHANGED, packet); 349 } 350 } 351 } 352 } 353 } 354 } 355 356 /** 357 * Listens for all roster packets and processes them. 358 */ 359 private class AgentStatusListener implements PacketListener { 360 361 public void processPacket(Packet packet) { 362 if (packet instanceof AgentStatusRequest) { 363 AgentStatusRequest statusRequest = (AgentStatusRequest)packet; 364 for (Iterator<AgentStatusRequest.Item> i = statusRequest.getAgents().iterator(); i.hasNext();) { 365 AgentStatusRequest.Item item = i.next(); 366 String agentJID = item.getJID(); 367 if ("remove".equals(item.getType())) { 368 369 // Removing the user from the roster, so remove any presence information 370 // about them. 371 String key = StringUtils.parseName(StringUtils.parseName(agentJID) + "@" + 372 StringUtils.parseServer(agentJID)); 373 presenceMap.remove(key); 374 // Fire event for roster listeners. 375 fireEvent(EVENT_AGENT_REMOVED, agentJID); 376 } 377 else { 378 entries.add(agentJID); 379 // Fire event for roster listeners. 380 fireEvent(EVENT_AGENT_ADDED, agentJID); 381 } 382 } 383 384 // Mark the roster as initialized. 385 rosterInitialized = true; 386 } 387 } 388 } 389}