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.WeakHashMap;
024import java.util.concurrent.ConcurrentHashMap;
025import java.util.concurrent.CopyOnWriteArraySet;
026import java.util.logging.Logger;
027
028import org.jivesoftware.smack.Manager;
029import org.jivesoftware.smack.MessageListener;
030import org.jivesoftware.smack.SmackException.NotConnectedException;
031import org.jivesoftware.smack.StanzaCollector;
032import org.jivesoftware.smack.StanzaListener;
033import org.jivesoftware.smack.XMPPConnection;
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.Stanza;
043import org.jivesoftware.smack.util.StringUtils;
044
045import org.jxmpp.jid.EntityBareJid;
046import org.jxmpp.jid.EntityJid;
047import org.jxmpp.jid.Jid;
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<>();
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() == Message.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 final Map<String, Chat> threadChats = new ConcurrentHashMap<>();
131
132    /**
133     * Maps jids to chats
134     */
135    private final Map<Jid, Chat> jidChats = new ConcurrentHashMap<>();
136
137    /**
138     * Maps base jids to chats
139     */
140    private final Map<EntityBareJid, Chat> baseJidChats = new ConcurrentHashMap<>();
141
142    private final Set<ChatManagerListener> chatManagerListeners = new CopyOnWriteArraySet<>();
143
144    private final Map<MessageListener, StanzaFilter> interceptors = new WeakHashMap<>();
145
146    private ChatManager(XMPPConnection connection) {
147        super(connection);
148
149        // Add a listener for all message packets so that we can deliver
150        // messages to the best Chat instance available.
151        connection.addSyncStanzaListener(new StanzaListener() {
152            @Override
153            public void processStanza(Stanza packet) {
154                Message message = (Message) packet;
155                Chat chat;
156                if (message.getThread() == null) {
157                    chat = getUserChat(message.getFrom());
158                }
159                else {
160                    chat = getThreadChat(message.getThread());
161                }
162
163                if (chat == null) {
164                    chat = createChat(message);
165                }
166                // The chat could not be created, abort here
167                if (chat == null)
168                    return;
169
170                // TODO: Use AsyncButOrdered (with Chat as Key?)
171                deliverMessage(chat, message);
172            }
173        }, packetFilter);
174        INSTANCES.put(connection, this);
175    }
176
177    /**
178     * Determines whether incoming messages of type <i>normal</i> will be used for creating new chats or matching
179     * a message to existing ones.
180     *
181     * @return true if normal is allowed, false otherwise.
182     */
183    public boolean isNormalIncluded() {
184        return normalIncluded;
185    }
186
187    /**
188     * Sets whether to allow incoming messages of type <i>normal</i> to be used for creating new chats or matching
189     * a message to an existing one.
190     *
191     * @param normalIncluded true to allow normal, false otherwise.
192     */
193    public void setNormalIncluded(boolean normalIncluded) {
194        this.normalIncluded = normalIncluded;
195    }
196
197    /**
198     * Gets the current mode for matching messages with <b>NO</b> thread id to existing chats.
199     *
200     * @return The current mode.
201     */
202    public MatchMode getMatchMode() {
203        return matchMode;
204    }
205
206    /**
207     * Sets the mode for matching messages with <b>NO</b> thread id to existing chats.
208     *
209     * @param matchMode The mode to set.
210     */
211    public void setMatchMode(MatchMode matchMode) {
212        this.matchMode = matchMode;
213    }
214
215    /**
216     * Creates a new chat and returns it.
217     *
218     * @param userJID the user this chat is with.
219     * @return the created chat.
220     */
221    public Chat createChat(EntityJid userJID) {
222        return createChat(userJID, null);
223    }
224
225    /**
226     * Creates a new chat and returns it.
227     *
228     * @param userJID the user this chat is with.
229     * @param listener the optional listener which will listen for new messages from this chat.
230     * @return the created chat.
231     */
232    public Chat createChat(EntityJid userJID, ChatMessageListener listener) {
233        return createChat(userJID, null, listener);
234    }
235
236    /**
237     * Creates a new chat using the specified thread ID, then returns it.
238     *
239     * @param userJID the jid of the user this chat is with
240     * @param thread the thread of the created chat.
241     * @param listener the optional listener to add to the chat
242     * @return the created chat.
243     */
244    public Chat createChat(EntityJid userJID, String thread, ChatMessageListener listener) {
245        if (thread == null) {
246            thread = nextID();
247        }
248        Chat chat = threadChats.get(thread);
249        if (chat != null) {
250            throw new IllegalArgumentException("ThreadID is already used");
251        }
252        chat = createChat(userJID, thread, true);
253        chat.addMessageListener(listener);
254        return chat;
255    }
256
257    private Chat createChat(EntityJid userJID, String threadID, boolean createdLocally) {
258        Chat chat = new Chat(this, userJID, threadID);
259        threadChats.put(threadID, chat);
260        jidChats.put(userJID, chat);
261        baseJidChats.put(userJID.asEntityBareJid(), chat);
262
263        for (ChatManagerListener listener : chatManagerListeners) {
264            listener.chatCreated(chat, createdLocally);
265        }
266
267        return chat;
268    }
269
270    void closeChat(Chat chat) {
271        threadChats.remove(chat.getThreadID());
272        EntityJid userJID = chat.getParticipant();
273        jidChats.remove(userJID);
274        baseJidChats.remove(userJID.asEntityBareJid());
275    }
276
277    /**
278     * Creates a new {@link Chat} based on the message. May returns null if no chat could be
279     * created, e.g. because the message comes without from.
280     *
281     * @param message TODO javadoc me please
282     * @return a Chat or null if none can be created
283     */
284    private Chat createChat(Message message) {
285        Jid from = message.getFrom();
286        // According to RFC6120 8.1.2.1 4. messages without a 'from' attribute are valid, but they
287        // are of no use in this case for ChatManager
288        if (from == null) {
289            return null;
290        }
291
292        EntityJid userJID = from.asEntityJidIfPossible();
293        if (userJID == null) {
294            LOGGER.warning("Message from JID without localpart: '" + message.toXML() + "'");
295            return null;
296        }
297        String threadID = message.getThread();
298        if (threadID == null) {
299            threadID = nextID();
300        }
301
302        return createChat(userJID, threadID, false);
303    }
304
305    /**
306     * Try to get a matching chat for the given user JID, based on the {@link MatchMode}.
307     * <li>NONE - return null
308     * <li>SUPPLIED_JID - match the jid in the from field of the message exactly.
309     * <li>BARE_JID - if not match for from field, try the bare jid.
310     *
311     * @param userJID jid in the from field of message.
312     * @return Matching chat, or null if no match found.
313     */
314    private Chat getUserChat(Jid userJID) {
315        if (matchMode == MatchMode.NONE) {
316            return null;
317        }
318        // According to RFC6120 8.1.2.1 4. messages without a 'from' attribute are valid, but they
319        // are of no use in this case for ChatManager
320        if (userJID == null) {
321            return null;
322        }
323        Chat match = jidChats.get(userJID);
324
325        if (match == null && (matchMode == MatchMode.BARE_JID)) {
326            EntityBareJid entityBareJid = userJID.asEntityBareJidIfPossible();
327            if (entityBareJid != null) {
328                match = baseJidChats.get(entityBareJid);
329            }
330        }
331        return match;
332    }
333
334    public Chat getThreadChat(String thread) {
335        return threadChats.get(thread);
336    }
337
338    /**
339     * Register a new listener with the ChatManager to receive events related to chats.
340     *
341     * @param listener the listener.
342     */
343    public void addChatListener(ChatManagerListener listener) {
344        chatManagerListeners.add(listener);
345    }
346
347    /**
348     * Removes a listener, it will no longer be notified of new events related to chats.
349     *
350     * @param listener the listener that is being removed
351     */
352    public void removeChatListener(ChatManagerListener listener) {
353        chatManagerListeners.remove(listener);
354    }
355
356    /**
357     * Returns an unmodifiable set of all chat listeners currently registered with this
358     * manager.
359     *
360     * @return an unmodifiable collection of all chat listeners currently registered with this
361     * manager.
362     */
363    public Set<ChatManagerListener> getChatListeners() {
364        return Collections.unmodifiableSet(chatManagerListeners);
365    }
366
367    private static void deliverMessage(Chat chat, Message message) {
368        // Here we will run any interceptors
369        chat.deliver(message);
370    }
371
372    void sendMessage(Chat chat, Message message) throws NotConnectedException, InterruptedException {
373        for (Map.Entry<MessageListener, StanzaFilter> interceptor : interceptors.entrySet()) {
374            StanzaFilter filter = interceptor.getValue();
375            if (filter != null && filter.accept(message)) {
376                interceptor.getKey().processMessage(message);
377            }
378        }
379        connection().sendStanza(message);
380    }
381
382    StanzaCollector createStanzaCollector(Chat chat) {
383        return connection().createStanzaCollector(new AndFilter(new ThreadFilter(chat.getThreadID()),
384                        FromMatchesFilter.create(chat.getParticipant())));
385    }
386
387    /**
388     * Adds an interceptor which intercepts any messages sent through chats.
389     *
390     * @param messageInterceptor the interceptor.
391     */
392    public void addOutgoingMessageInterceptor(MessageListener messageInterceptor) {
393        addOutgoingMessageInterceptor(messageInterceptor, null);
394    }
395
396    public void addOutgoingMessageInterceptor(MessageListener messageInterceptor, StanzaFilter filter) {
397        if (messageInterceptor == null) {
398            return;
399        }
400        interceptors.put(messageInterceptor, filter);
401    }
402
403    /**
404     * Returns a unique id.
405     *
406     * @return the next id.
407     */
408    private static String nextID() {
409        return StringUtils.secureUniqueRandomString();
410    }
411
412    public static void setDefaultMatchMode(MatchMode mode) {
413        defaultMatchMode = mode;
414    }
415
416    public static void setDefaultIsNormalIncluded(boolean allowNormal) {
417        defaultIsNormalInclude = allowNormal;
418    }
419}