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