1/**
2 * $RCSfile$
3 * $Revision: 2407 $
4 * $Date: 2004-11-02 15:37:00 -0800 (Tue, 02 Nov 2004) $
5 *
6 * Copyright 2003-2007 Jive Software.
7 *
8 * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
9 * you may not use this file except in compliance with the License.
10 * You may obtain a copy of the License at
11 *
12 *     http://www.apache.org/licenses/LICENSE-2.0
13 *
14 * Unless required by applicable law or agreed to in writing, software
15 * distributed under the License is distributed on an "AS IS" BASIS,
16 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 * See the License for the specific language governing permissions and
18 * limitations under the License.
19 */
20
21package org.jivesoftware.smack;
22
23import java.util.Collection;
24import java.util.Collections;
25import java.util.Map;
26import java.util.Set;
27import java.util.WeakHashMap;
28import java.util.concurrent.CopyOnWriteArraySet;
29
30import org.jivesoftware.smack.filter.AndFilter;
31import org.jivesoftware.smack.filter.FromContainsFilter;
32import org.jivesoftware.smack.filter.PacketFilter;
33import org.jivesoftware.smack.filter.ThreadFilter;
34import org.jivesoftware.smack.packet.Message;
35import org.jivesoftware.smack.packet.Packet;
36import org.jivesoftware.smack.util.StringUtils;
37import org.jivesoftware.smack.util.collections.ReferenceMap;
38
39import java.util.*;
40import java.util.concurrent.CopyOnWriteArraySet;
41
42/**
43 * The chat manager keeps track of references to all current chats. It will not hold any references
44 * in memory on its own so it is neccesary to keep a reference to the chat object itself. To be
45 * made aware of new chats, register a listener by calling {@link #addChatListener(ChatManagerListener)}.
46 *
47 * @author Alexander Wenckus
48 */
49public class ChatManager {
50
51    /**
52     * Returns the next unique id. Each id made up of a short alphanumeric
53     * prefix along with a unique numeric value.
54     *
55     * @return the next id.
56     */
57    private static synchronized String nextID() {
58        return prefix + Long.toString(id++);
59    }
60
61    /**
62     * A prefix helps to make sure that ID's are unique across mutliple instances.
63     */
64    private static String prefix = StringUtils.randomString(5);
65
66    /**
67     * Keeps track of the current increment, which is appended to the prefix to
68     * forum a unique ID.
69     */
70    private static long id = 0;
71
72    /**
73     * Maps thread ID to chat.
74     */
75    private Map<String, Chat> threadChats = Collections.synchronizedMap(new ReferenceMap<String, Chat>(ReferenceMap.HARD,
76            ReferenceMap.WEAK));
77
78    /**
79     * Maps jids to chats
80     */
81    private Map<String, Chat> jidChats = Collections.synchronizedMap(new ReferenceMap<String, Chat>(ReferenceMap.HARD,
82            ReferenceMap.WEAK));
83
84    /**
85     * Maps base jids to chats
86     */
87    private Map<String, Chat> baseJidChats = Collections.synchronizedMap(new ReferenceMap<String, Chat>(ReferenceMap.HARD,
88	    ReferenceMap.WEAK));
89
90    private Set<ChatManagerListener> chatManagerListeners
91            = new CopyOnWriteArraySet<ChatManagerListener>();
92
93    private Map<PacketInterceptor, PacketFilter> interceptors
94            = new WeakHashMap<PacketInterceptor, PacketFilter>();
95
96    private Connection connection;
97
98    ChatManager(Connection connection) {
99        this.connection = connection;
100
101        PacketFilter filter = new PacketFilter() {
102            public boolean accept(Packet packet) {
103                if (!(packet instanceof Message)) {
104                    return false;
105                }
106                Message.Type messageType = ((Message) packet).getType();
107                return messageType != Message.Type.groupchat &&
108                        messageType != Message.Type.headline;
109            }
110        };
111        // Add a listener for all message packets so that we can deliver errant
112        // messages to the best Chat instance available.
113        connection.addPacketListener(new PacketListener() {
114            public void processPacket(Packet packet) {
115                Message message = (Message) packet;
116                Chat chat;
117                if (message.getThread() == null) {
118                	chat = getUserChat(message.getFrom());
119                }
120                else {
121                    chat = getThreadChat(message.getThread());
122                    if (chat == null) {
123                        // Try to locate the chat based on the sender of the message
124                    	chat = getUserChat(message.getFrom());
125                    }
126                }
127
128                if(chat == null) {
129                    chat = createChat(message);
130                }
131                deliverMessage(chat, message);
132            }
133        }, filter);
134    }
135
136    /**
137     * Creates a new chat and returns it.
138     *
139     * @param userJID the user this chat is with.
140     * @param listener the listener which will listen for new messages from this chat.
141     * @return the created chat.
142     */
143    public Chat createChat(String userJID, MessageListener listener) {
144        String threadID;
145        do  {
146            threadID = nextID();
147        } while (threadChats.get(threadID) != null);
148
149        return createChat(userJID, threadID, listener);
150    }
151
152    /**
153     * Creates a new chat using the specified thread ID, then returns it.
154     *
155     * @param userJID the jid of the user this chat is with
156     * @param thread the thread of the created chat.
157     * @param listener the listener to add to the chat
158     * @return the created chat.
159     */
160    public Chat createChat(String userJID, String thread, MessageListener listener) {
161        if(thread == null) {
162            thread = nextID();
163        }
164        Chat chat = threadChats.get(thread);
165        if(chat != null) {
166            throw new IllegalArgumentException("ThreadID is already used");
167        }
168        chat = createChat(userJID, thread, true);
169        chat.addMessageListener(listener);
170        return chat;
171    }
172
173    private Chat createChat(String userJID, String threadID, boolean createdLocally) {
174        Chat chat = new Chat(this, userJID, threadID);
175        threadChats.put(threadID, chat);
176        jidChats.put(userJID, chat);
177        baseJidChats.put(StringUtils.parseBareAddress(userJID), chat);
178
179        for(ChatManagerListener listener : chatManagerListeners) {
180            listener.chatCreated(chat, createdLocally);
181        }
182
183        return chat;
184    }
185
186    private Chat createChat(Message message) {
187        String threadID = message.getThread();
188        if(threadID == null) {
189            threadID = nextID();
190        }
191        String userJID = message.getFrom();
192
193        return createChat(userJID, threadID, false);
194    }
195
196    /**
197     * Try to get a matching chat for the given user JID.  Try the full
198     * JID map first, the try to match on the base JID if no match is
199     * found.
200     *
201     * @param userJID
202     * @return
203     */
204    private Chat getUserChat(String userJID) {
205	Chat match = jidChats.get(userJID);
206
207	if (match == null) {
208	    match = baseJidChats.get(StringUtils.parseBareAddress(userJID));
209	}
210	return match;
211    }
212
213    public Chat getThreadChat(String thread) {
214        return threadChats.get(thread);
215    }
216
217    /**
218     * Register a new listener with the ChatManager to recieve events related to chats.
219     *
220     * @param listener the listener.
221     */
222    public void addChatListener(ChatManagerListener listener) {
223        chatManagerListeners.add(listener);
224    }
225
226    /**
227     * Removes a listener, it will no longer be notified of new events related to chats.
228     *
229     * @param listener the listener that is being removed
230     */
231    public void removeChatListener(ChatManagerListener listener) {
232        chatManagerListeners.remove(listener);
233    }
234
235    /**
236     * Returns an unmodifiable collection of all chat listeners currently registered with this
237     * manager.
238     *
239     * @return an unmodifiable collection of all chat listeners currently registered with this
240     * manager.
241     */
242    public Collection<ChatManagerListener> getChatListeners() {
243        return Collections.unmodifiableCollection(chatManagerListeners);
244    }
245
246    private void deliverMessage(Chat chat, Message message) {
247        // Here we will run any interceptors
248        chat.deliver(message);
249    }
250
251    void sendMessage(Chat chat, Message message) {
252        for(Map.Entry<PacketInterceptor, PacketFilter> interceptor : interceptors.entrySet()) {
253            PacketFilter filter = interceptor.getValue();
254            if(filter != null && filter.accept(message)) {
255                interceptor.getKey().interceptPacket(message);
256            }
257        }
258        // Ensure that messages being sent have a proper FROM value
259        if (message.getFrom() == null) {
260            message.setFrom(connection.getUser());
261        }
262        connection.sendPacket(message);
263    }
264
265    PacketCollector createPacketCollector(Chat chat) {
266        return connection.createPacketCollector(new AndFilter(new ThreadFilter(chat.getThreadID()),
267                new FromContainsFilter(chat.getParticipant())));
268    }
269
270    /**
271     * Adds an interceptor which intercepts any messages sent through chats.
272     *
273     * @param packetInterceptor the interceptor.
274     */
275    public void addOutgoingMessageInterceptor(PacketInterceptor packetInterceptor) {
276        addOutgoingMessageInterceptor(packetInterceptor, null);
277    }
278
279    public void addOutgoingMessageInterceptor(PacketInterceptor packetInterceptor, PacketFilter filter) {
280        if (packetInterceptor != null) {
281            interceptors.put(packetInterceptor, filter);
282        }
283    }
284}
285