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