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}