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