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}