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