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