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.smack; 019 020import java.util.Collection; 021import java.util.Collections; 022import java.util.HashMap; 023import java.util.Map; 024import java.util.Set; 025import java.util.UUID; 026import java.util.WeakHashMap; 027import java.util.concurrent.CopyOnWriteArraySet; 028 029import org.jivesoftware.smack.SmackException.NotConnectedException; 030import org.jivesoftware.smack.filter.AndFilter; 031import org.jivesoftware.smack.filter.FromMatchesFilter; 032import org.jivesoftware.smack.filter.PacketFilter; 033import org.jivesoftware.smack.filter.ThreadFilter; 034import org.jivesoftware.smack.packet.Message; 035import org.jivesoftware.smack.packet.Message.Type; 036import org.jivesoftware.smack.packet.Packet; 037import org.jivesoftware.smack.util.StringUtils; 038 039/** 040 * The chat manager keeps track of references to all current chats. It will not hold any references 041 * in memory on its own so it is necessary to keep a reference to the chat object itself. To be 042 * made aware of new chats, register a listener by calling {@link #addChatListener(ChatManagerListener)}. 043 * 044 * @author Alexander Wenckus 045 */ 046public class ChatManager extends Manager{ 047 private static final Map<XMPPConnection, ChatManager> INSTANCES = new WeakHashMap<XMPPConnection, ChatManager>(); 048 049 /** 050 * Sets the default behaviour for allowing 'normal' messages to be used in chats. As some clients don't set 051 * the message type to chat, the type normal has to be accepted to allow chats with these clients. 052 */ 053 private static boolean defaultIsNormalInclude = true; 054 055 /** 056 * Sets the default behaviour for how to match chats when there is NO thread id in the incoming message. 057 */ 058 private static MatchMode defaultMatchMode = MatchMode.BARE_JID; 059 060 /** 061 * Returns the ChatManager instance associated with a given XMPPConnection. 062 * 063 * @param connection the connection used to look for the proper ServiceDiscoveryManager. 064 * @return the ChatManager associated with a given XMPPConnection. 065 */ 066 public static synchronized ChatManager getInstanceFor(XMPPConnection connection) { 067 ChatManager manager = INSTANCES.get(connection); 068 if (manager == null) 069 manager = new ChatManager(connection); 070 return manager; 071 } 072 073 /** 074 * Defines the different modes under which a match will be attempted with an existing chat when 075 * the incoming message does not have a thread id. 076 */ 077 public enum MatchMode { 078 /** 079 * Will not attempt to match, always creates a new chat. 080 */ 081 NONE, 082 /** 083 * Will match on the JID in the from field of the message. 084 */ 085 SUPPLIED_JID, 086 /** 087 * Will attempt to match on the JID in the from field, and then attempt the base JID if no match was found. 088 * This is the most lenient matching. 089 */ 090 BARE_JID; 091 } 092 093 /** 094 * Determines whether incoming messages of type normal can create chats. 095 */ 096 private boolean normalIncluded = defaultIsNormalInclude; 097 098 /** 099 * Determines how incoming message with no thread will be matched to existing chats. 100 */ 101 private MatchMode matchMode = defaultMatchMode; 102 103 /** 104 * Maps thread ID to chat. 105 */ 106 private Map<String, Chat> threadChats = Collections.synchronizedMap(new HashMap<String, Chat>()); 107 108 /** 109 * Maps jids to chats 110 */ 111 private Map<String, Chat> jidChats = Collections.synchronizedMap(new HashMap<String, Chat>()); 112 113 /** 114 * Maps base jids to chats 115 */ 116 private Map<String, Chat> baseJidChats = Collections.synchronizedMap(new HashMap<String, Chat>()); 117 118 private Set<ChatManagerListener> chatManagerListeners 119 = new CopyOnWriteArraySet<ChatManagerListener>(); 120 121 private Map<PacketInterceptor, PacketFilter> interceptors 122 = new WeakHashMap<PacketInterceptor, PacketFilter>(); 123 124 private ChatManager(XMPPConnection connection) { 125 super(connection); 126 127 PacketFilter filter = new PacketFilter() { 128 public boolean accept(Packet packet) { 129 if (!(packet instanceof Message)) { 130 return false; 131 } 132 Message.Type messageType = ((Message) packet).getType(); 133 return (messageType == Type.chat) || (normalIncluded ? messageType == Type.normal : false); 134 } 135 }; 136 137 // Add a listener for all message packets so that we can deliver 138 // messages to the best Chat instance available. 139 connection.addPacketListener(new PacketListener() { 140 public void processPacket(Packet packet) { 141 Message message = (Message) packet; 142 Chat chat; 143 if (message.getThread() == null) { 144 chat = getUserChat(message.getFrom()); 145 } 146 else { 147 chat = getThreadChat(message.getThread()); 148 } 149 150 if(chat == null) { 151 chat = createChat(message); 152 } 153 // The chat could not be created, abort here 154 if (chat == null) 155 return; 156 deliverMessage(chat, message); 157 } 158 }, filter); 159 INSTANCES.put(connection, this); 160 } 161 162 /** 163 * Determines whether incoming messages of type <i>normal</i> will be used for creating new chats or matching 164 * a message to existing ones. 165 * 166 * @return true if normal is allowed, false otherwise. 167 */ 168 public boolean isNormalIncluded() { 169 return normalIncluded; 170 } 171 172 /** 173 * Sets whether to allow incoming messages of type <i>normal</i> to be used for creating new chats or matching 174 * a message to an existing one. 175 * 176 * @param normalIncluded true to allow normal, false otherwise. 177 */ 178 public void setNormalIncluded(boolean normalIncluded) { 179 this.normalIncluded = normalIncluded; 180 } 181 182 /** 183 * Gets the current mode for matching messages with <b>NO</b> thread id to existing chats. 184 * 185 * @return The current mode. 186 */ 187 public MatchMode getMatchMode() { 188 return matchMode; 189 } 190 191 /** 192 * Sets the mode for matching messages with <b>NO</b> thread id to existing chats. 193 * 194 * @param matchMode The mode to set. 195 */ 196 public void setMatchMode(MatchMode matchMode) { 197 this.matchMode = matchMode; 198 } 199 200 /** 201 * Creates a new chat and returns it. 202 * 203 * @param userJID the user this chat is with. 204 * @param listener the listener which will listen for new messages from this chat. 205 * @return the created chat. 206 */ 207 public Chat createChat(String userJID, MessageListener listener) { 208 return createChat(userJID, null, listener); 209 } 210 211 /** 212 * Creates a new chat using the specified thread ID, then returns it. 213 * 214 * @param userJID the jid of the user this chat is with 215 * @param thread the thread of the created chat. 216 * @param listener the listener to add to the chat 217 * @return the created chat. 218 */ 219 public Chat createChat(String userJID, String thread, MessageListener listener) { 220 if (thread == null) { 221 thread = nextID(); 222 } 223 Chat chat = threadChats.get(thread); 224 if(chat != null) { 225 throw new IllegalArgumentException("ThreadID is already used"); 226 } 227 chat = createChat(userJID, thread, true); 228 chat.addMessageListener(listener); 229 return chat; 230 } 231 232 private Chat createChat(String userJID, String threadID, boolean createdLocally) { 233 Chat chat = new Chat(this, userJID, threadID); 234 threadChats.put(threadID, chat); 235 jidChats.put(userJID, chat); 236 baseJidChats.put(StringUtils.parseBareAddress(userJID), chat); 237 238 for(ChatManagerListener listener : chatManagerListeners) { 239 listener.chatCreated(chat, createdLocally); 240 } 241 242 return chat; 243 } 244 245 void closeChat(Chat chat) { 246 threadChats.remove(chat.getThreadID()); 247 String userJID = chat.getParticipant(); 248 jidChats.remove(userJID); 249 baseJidChats.remove(StringUtils.parseBareAddress(userJID)); 250 } 251 252 /** 253 * Creates a new {@link Chat} based on the message. May returns null if no chat could be 254 * created, e.g. because the message comes without from. 255 * 256 * @param message 257 * @return a Chat or null if none can be created 258 */ 259 private Chat createChat(Message message) { 260 String userJID = message.getFrom(); 261 // According to RFC6120 8.1.2.1 4. messages without a 'from' attribute are valid, but they 262 // are of no use in this case for ChatManager 263 if (userJID == null) { 264 return null; 265 } 266 String threadID = message.getThread(); 267 if(threadID == null) { 268 threadID = nextID(); 269 } 270 271 return createChat(userJID, threadID, false); 272 } 273 274 /** 275 * Try to get a matching chat for the given user JID, based on the {@link MatchMode}. 276 * <li>NONE - return null 277 * <li>SUPPLIED_JID - match the jid in the from field of the message exactly. 278 * <li>BARE_JID - if not match for from field, try the bare jid. 279 * 280 * @param userJID jid in the from field of message. 281 * @return Matching chat, or null if no match found. 282 */ 283 private Chat getUserChat(String userJID) { 284 if (matchMode == MatchMode.NONE) { 285 return null; 286 } 287 // According to RFC6120 8.1.2.1 4. messages without a 'from' attribute are valid, but they 288 // are of no use in this case for ChatManager 289 if (userJID == null) { 290 return null; 291 } 292 Chat match = jidChats.get(userJID); 293 294 if (match == null && (matchMode == MatchMode.BARE_JID)) { 295 match = baseJidChats.get(StringUtils.parseBareAddress(userJID)); 296 } 297 return match; 298 } 299 300 public Chat getThreadChat(String thread) { 301 return threadChats.get(thread); 302 } 303 304 /** 305 * Register a new listener with the ChatManager to recieve events related to chats. 306 * 307 * @param listener the listener. 308 */ 309 public void addChatListener(ChatManagerListener listener) { 310 chatManagerListeners.add(listener); 311 } 312 313 /** 314 * Removes a listener, it will no longer be notified of new events related to chats. 315 * 316 * @param listener the listener that is being removed 317 */ 318 public void removeChatListener(ChatManagerListener listener) { 319 chatManagerListeners.remove(listener); 320 } 321 322 /** 323 * Returns an unmodifiable collection of all chat listeners currently registered with this 324 * manager. 325 * 326 * @return an unmodifiable collection of all chat listeners currently registered with this 327 * manager. 328 */ 329 public Collection<ChatManagerListener> getChatListeners() { 330 return Collections.unmodifiableCollection(chatManagerListeners); 331 } 332 333 private void deliverMessage(Chat chat, Message message) { 334 // Here we will run any interceptors 335 chat.deliver(message); 336 } 337 338 void sendMessage(Chat chat, Message message) throws NotConnectedException { 339 for(Map.Entry<PacketInterceptor, PacketFilter> interceptor : interceptors.entrySet()) { 340 PacketFilter filter = interceptor.getValue(); 341 if(filter != null && filter.accept(message)) { 342 interceptor.getKey().interceptPacket(message); 343 } 344 } 345 // Ensure that messages being sent have a proper FROM value 346 if (message.getFrom() == null) { 347 message.setFrom(connection().getUser()); 348 } 349 connection().sendPacket(message); 350 } 351 352 PacketCollector createPacketCollector(Chat chat) { 353 return connection().createPacketCollector(new AndFilter(new ThreadFilter(chat.getThreadID()), 354 FromMatchesFilter.create(chat.getParticipant()))); 355 } 356 357 /** 358 * Adds an interceptor which intercepts any messages sent through chats. 359 * 360 * @param packetInterceptor the interceptor. 361 */ 362 public void addOutgoingMessageInterceptor(PacketInterceptor packetInterceptor) { 363 addOutgoingMessageInterceptor(packetInterceptor, null); 364 } 365 366 public void addOutgoingMessageInterceptor(PacketInterceptor packetInterceptor, PacketFilter filter) { 367 if (packetInterceptor != null) { 368 interceptors.put(packetInterceptor, filter); 369 } 370 } 371 372 /** 373 * Returns a unique id. 374 * 375 * @return the next id. 376 */ 377 private static String nextID() { 378 return UUID.randomUUID().toString(); 379 } 380 381 public static void setDefaultMatchMode(MatchMode mode) { 382 defaultMatchMode = mode; 383 } 384 385 public static void setDefaultIsNormalIncluded(boolean allowNormal) { 386 defaultIsNormalInclude = allowNormal; 387 } 388}