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