MultiUserChatServerImpl.java 32.3 KB
Newer Older
Matt Tucker's avatar
Matt Tucker committed
1 2 3 4 5
/**
 * $RCSfile$
 * $Revision$
 * $Date$
 *
Matt Tucker's avatar
Matt Tucker committed
6
 * Copyright (C) 2004 Jive Software. All rights reserved.
Matt Tucker's avatar
Matt Tucker committed
7
 *
Matt Tucker's avatar
Matt Tucker committed
8 9
 * This software is published under the terms of the GNU Public License (GPL),
 * a copy of which is included in this distribution.
Matt Tucker's avatar
Matt Tucker committed
10
 */
Matt Tucker's avatar
Matt Tucker committed
11

Matt Tucker's avatar
Matt Tucker committed
12 13
package org.jivesoftware.messenger.muc.spi;

Derek DeMoro's avatar
Derek DeMoro committed
14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.LinkedBlockingQueue;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.jivesoftware.messenger.JiveGlobals;
import org.jivesoftware.messenger.PacketRouter;
import org.jivesoftware.messenger.RoutableChannelHandler;
import org.jivesoftware.messenger.RoutingTable;
import org.jivesoftware.messenger.XMPPServer;
import org.jivesoftware.messenger.auth.UnauthorizedException;
import org.jivesoftware.messenger.container.BasicModule;
Matt Tucker's avatar
Matt Tucker committed
35 36 37 38 39 40 41 42 43
import org.jivesoftware.messenger.disco.DiscoInfoProvider;
import org.jivesoftware.messenger.disco.DiscoItemsProvider;
import org.jivesoftware.messenger.disco.DiscoServerItem;
import org.jivesoftware.messenger.disco.ServerItemsProvider;
import org.jivesoftware.messenger.forms.DataForm;
import org.jivesoftware.messenger.forms.FormField;
import org.jivesoftware.messenger.forms.spi.XDataFormImpl;
import org.jivesoftware.messenger.forms.spi.XFormFieldImpl;
import org.jivesoftware.messenger.handler.IQRegisterHandler;
Derek DeMoro's avatar
Derek DeMoro committed
44 45 46 47 48 49
import org.jivesoftware.messenger.muc.HistoryStrategy;
import org.jivesoftware.messenger.muc.MUCRole;
import org.jivesoftware.messenger.muc.MUCRoom;
import org.jivesoftware.messenger.muc.MUCUser;
import org.jivesoftware.messenger.muc.MultiUserChatServer;
import org.jivesoftware.messenger.muc.NotAllowedException;
Matt Tucker's avatar
Matt Tucker committed
50
import org.jivesoftware.messenger.user.UserNotFoundException;
Derek DeMoro's avatar
Derek DeMoro committed
51 52
import org.jivesoftware.util.LocaleUtils;
import org.jivesoftware.util.Log;
53 54
import org.xmpp.packet.JID;
import org.xmpp.packet.Message;
Derek DeMoro's avatar
Derek DeMoro committed
55 56
import org.xmpp.packet.Packet;
import org.xmpp.packet.Presence;
Matt Tucker's avatar
Matt Tucker committed
57 58

/**
59
 * Implements the chat server as a cached memory resident chat server. The server is also
Matt Tucker's avatar
Matt Tucker committed
60 61 62
 * responsible for responding Multi-User Chat disco requests as well as removing inactive users from
 * the rooms after a period of time and to maintain a log of the conversation in the rooms that 
 * require to log their conversations. The conversations log is saved to the database using a 
63
 * separate process<p>
64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80
 *
 * Rooms in memory are held in the instance variable rooms. For optimization reasons, persistent
 * rooms that don't have occupants aren't kept in memory. But a client may need to discover all the
 * rooms (present in memory or not). So MultiUserChatServerImpl uses a cache of persistent room
 * surrogates. A room surrogate (lighter object) is created for each persistent room that is public,
 * persistent but is not in memory.  The cache starts up empty until a client requests the list of
 * rooms through service discovery. Once the disco request is received the cache is filled up with
 * persistent room surrogates.  The cache will keep all the surrogates in memory for an hour. If the
 * cache's entries weren't used in an hour, they will be removed from memory. Whenever a persistent
 * room is removed from memory (because all the occupants have left), the cache is cleared. But if
 * a persistent room is loaded from the database then the entry for that room in the cache is
 * removed. Note: Since the cache contains an entry for each room surrogate and the clearing
 * algorithm is based on the usage of each entry, it's possible that some entries are removed
 * while others don't thus generating that the provided list of discovered rooms won't be complete.
 * However, this possibility is low since the clients will most of the time ask for all the cache
 * entries and then ask for a particular entry. Anyway, if this possibility happens the cache will
 * be reset the next time that a persistent room is removed from memory.
Matt Tucker's avatar
Matt Tucker committed
81 82 83 84 85 86 87 88 89
 *
 * @author Gaston Dombiak
 */
public class MultiUserChatServerImpl extends BasicModule implements MultiUserChatServer,
        ServerItemsProvider, DiscoInfoProvider, DiscoItemsProvider, RoutableChannelHandler {

    /**
     * The time to elapse between clearing of idle chat users.
     */
90
    private int user_timeout = 300000;
91 92 93
    /**
     * The number of milliseconds a user must be idle before he/she gets kicked from all the rooms.
     */
94
    private int user_idle = -1;
95 96 97 98
    /**
     * Task that kicks idle users from the rooms.
     */
    private UserTimeoutTask userTimeoutTask;
Matt Tucker's avatar
Matt Tucker committed
99 100 101
    /**
     * The time to elapse between logging the room conversations.
     */
102
    private int log_timeout = 300000;
Matt Tucker's avatar
Matt Tucker committed
103 104 105
    /**
     * The number of messages to log on each run of the logging process.
     */
106 107 108 109 110
    private int log_batch_size = 50;
    /**
     * Task that flushes room conversation logs to the database.
     */
    private LogConversationTask logConversationTask;
Matt Tucker's avatar
Matt Tucker committed
111
    /**
112
     * the chat service's hostname
Matt Tucker's avatar
Matt Tucker committed
113
     */
114
    private String chatServiceName = null;
115
    private JID chatServiceAddress = null;
Matt Tucker's avatar
Matt Tucker committed
116 117 118 119

    /**
     * chatrooms managed by this manager, table: key room name (String); value ChatRoom
     */
120
    private Map<String,MUCRoom> rooms = new ConcurrentHashMap<String,MUCRoom>();
121

Matt Tucker's avatar
Matt Tucker committed
122 123 124
    /**
     * chat users managed by this manager, table: key user jid (XMPPAddress); value ChatUser
     */
125
    private Map<JID, MUCUser> users = new ConcurrentHashMap<JID, MUCUser>();
Matt Tucker's avatar
Matt Tucker committed
126 127 128 129 130 131
    private HistoryStrategy historyStrategy;

    private RoutingTable routingTable = null;
    /**
     * The packet router for the server.
     */
132
    private PacketRouter router = null;
Matt Tucker's avatar
Matt Tucker committed
133 134 135
    /**
     * The handler of packets with namespace jabber:iq:register for the server.
     */
136
    private IQRegisterHandler registerHandler = null;
Matt Tucker's avatar
Matt Tucker committed
137 138 139 140 141 142 143 144 145 146 147
    /**
     * The total time all agents took to chat *
     */
    public long totalChatTime;

    /**
     * Timer to monitor chatroom participants. If they've been idle for too long, probe for
     * presence.
     */
    private Timer timer = new Timer();

148 149 150 151 152 153 154
    /**
     * Returns the permission policy for creating rooms. A true value means that not anyone can
     * create a room, only the JIDs listed in <code>allowedToCreate</code> are allowed to create
     * rooms.
     */
    private boolean roomCreationRestricted = false;

Matt Tucker's avatar
Matt Tucker committed
155 156 157 158
    /**
     * Bare jids of users that are allowed to create MUC rooms. An empty list means that anyone can 
     * create a room. 
     */
159
    private Collection<String> allowedToCreate = new CopyOnWriteArrayList<String>();
Matt Tucker's avatar
Matt Tucker committed
160 161 162 163 164

    /**
     * Bare jids of users that are system administrators of the MUC service. A sysadmin has the same
     * permissions as a room owner. 
     */
165
    private Collection<String> sysadmins = new CopyOnWriteArrayList<String>();
Matt Tucker's avatar
Matt Tucker committed
166 167 168 169

    /**
     * Queue that holds the messages to log for the rooms that need to log their conversations.
     */
170
    private Queue<ConversationLogEntry> logQueue = new LinkedBlockingQueue<ConversationLogEntry>();
Matt Tucker's avatar
Matt Tucker committed
171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187

    /**
     * Create a new group chat server.
     */
    public MultiUserChatServerImpl() {
        super("Basic multi user chat server");
        historyStrategy = new HistoryStrategy(null);
    }

    /**
     * Probes the presence of any user who's last packet was sent more than 5 minute ago.
     */
    private class UserTimeoutTask extends TimerTask {
        /**
         * Remove any user that has been idle for longer than the user timeout time.
         */
        public void run() {
188
            checkForTimedOutUsers();
Matt Tucker's avatar
Matt Tucker committed
189 190 191
        }
    }

192
    private void checkForTimedOutUsers() {
193
        // Do nothing if this feature is disabled (i.e USER_IDLE equals -1)
194
        if (user_idle == -1) {
195 196
            return;
        }
197
        final long deadline = System.currentTimeMillis() - user_idle;
198
        for (MUCUser user : users.values()) {
Matt Tucker's avatar
Matt Tucker committed
199 200
            try {
                if (user.getLastPacketTime() < deadline) {
201 202 203
                    // Kick the user from all the rooms that he/she had previuosly joined
                    Iterator<MUCRole> roles = user.getRoles();
                    MUCRole role;
204 205
                    MUCRoom room;
                    Presence kickedPresence;
206 207
                    while (roles.hasNext()) {
                        role = roles.next();
208
                        room = role.getChatRoom();
209
                        try {
210
                            kickedPresence =
211
                                    room.kickOccupant(user.getAddress(), null, null);
212 213
                            // Send the updated presence to the room occupants
                            room.send(kickedPresence);
214 215 216 217 218
                        }
                        catch (NotAllowedException e) {
                            // Do nothing since we cannot kick owners or admins
                        }
                    }
Matt Tucker's avatar
Matt Tucker committed
219 220
                }
            }
221
            catch (Throwable e) {
Matt Tucker's avatar
Matt Tucker committed
222 223 224 225 226 227 228 229 230 231
                Log.error(LocaleUtils.getLocalizedString("admin.error"), e);
            }
        }
    }

    /**
     * Logs the conversation of the rooms that have this feature enabled.
     */
    private class LogConversationTask extends TimerTask {
        public void run() {
232 233 234 235 236
            try {
                logConversation();
            }
            catch (Throwable e) {
                Log.error(LocaleUtils.getLocalizedString("admin.error"), e);
Matt Tucker's avatar
Matt Tucker committed
237 238 239 240 241 242 243
            }
        }
    }

    private void logConversation() {
        ConversationLogEntry entry = null;
        boolean success = false;
244
        for (int index = 0; index <= log_batch_size && !logQueue.isEmpty(); index++) {
245
            entry = logQueue.poll();
Matt Tucker's avatar
Matt Tucker committed
246 247 248 249 250 251 252 253 254
            if (entry != null) {
                success = MUCPersistenceManager.saveConversationLogEntry(entry);
                if (!success) {
                    logQueue.add(entry);
                }
            }
        }
    }

255 256 257 258 259 260 261 262 263 264 265 266 267 268
    /**
     * Logs all the remaining conversation log entries to the database. Use this method to force
     * saving all the conversation log entries before the service becomes unavailable.
     */
    private void logAllConversation() {
        ConversationLogEntry entry = null;
        while (!logQueue.isEmpty()) {
            entry = logQueue.poll();
            if (entry != null) {
                MUCPersistenceManager.saveConversationLogEntry(entry);
            }
        }
    }

269
    public MUCRoom getChatRoom(String roomName, JID userjid) throws UnauthorizedException {
Matt Tucker's avatar
Matt Tucker committed
270
        MUCRoom room = null;
271
        synchronized (roomName.intern()) {
272
            room = rooms.get(roomName.toLowerCase());
Matt Tucker's avatar
Matt Tucker committed
273 274
            if (room == null) {
                room = new MUCRoomImpl(this, roomName, router);
275 276 277 278 279 280 281 282
                // If the room is persistent load the configuration values from the DB
                try {
                    // Try to load the room's configuration from the database (if the room is
                    // persistent but was added to the DB after the server was started up)
                    MUCPersistenceManager.loadFromDB(room);
                }
                catch (IllegalArgumentException e) {
                    // The room does not exist so check for creation permissions
Matt Tucker's avatar
Matt Tucker committed
283
                    // Room creation is always allowed for sysadmin
284
                    if (isRoomCreationRestricted() &&
285
                            !sysadmins.contains(userjid.toBareJID())) {
Matt Tucker's avatar
Matt Tucker committed
286
                        // The room creation is only allowed for certain JIDs
287
                        if (!allowedToCreate.contains(userjid.toBareJID())) {
Matt Tucker's avatar
Matt Tucker committed
288 289 290 291 292
                            // The user is not in the list of allowed JIDs to create a room so raise
                            // an exception
                            throw new UnauthorizedException();
                        }
                    }
293
                    room.addFirstOwner(userjid.toBareJID());
Matt Tucker's avatar
Matt Tucker committed
294 295 296 297 298 299 300 301
                }
                rooms.put(roomName.toLowerCase(), room);
            }
        }
        return room;
    }

    public MUCRoom getChatRoom(String roomName) {
302
        return rooms.get(roomName.toLowerCase());
303 304 305
    }

    public List<MUCRoom> getChatRooms() {
306
        return new ArrayList<MUCRoom>(rooms.values());
Matt Tucker's avatar
Matt Tucker committed
307 308
    }

309 310 311 312
    public boolean hasChatRoom(String roomName) {
        return getChatRoom(roomName) != null;
    }

313
    public void removeChatRoom(String roomName) {
314 315
        final MUCRoom room = rooms.remove(roomName.toLowerCase());
        if (room != null) {
Matt Tucker's avatar
Matt Tucker committed
316 317 318 319 320
            final long chatLength = room.getChatLength();
            totalChatTime += chatLength;
        }
    }

321 322
    public String getServiceName() {
        return chatServiceName;
Matt Tucker's avatar
Matt Tucker committed
323 324 325 326 327 328
    }

    public HistoryStrategy getHistoryStrategy() {
        return historyStrategy;
    }

329
    public void removeUser(JID jabberID) {
330 331 332 333 334 335 336 337 338 339
        MUCUser user = users.remove(jabberID);
        if (user != null) {
            Iterator<MUCRole> roles = user.getRoles();
            while (roles.hasNext()) {
                MUCRole role = roles.next();
                try {
                    role.getChatRoom().leaveRoom(role.getNickname());
                }
                catch (Exception e) {
                    Log.error(e);
Matt Tucker's avatar
Matt Tucker committed
340 341 342 343 344
                }
            }
        }
    }

345
    public MUCUser getChatUser(JID userjid) throws UserNotFoundException {
Matt Tucker's avatar
Matt Tucker committed
346 347 348 349
        if (router == null) {
            throw new IllegalStateException("Not initialized");
        }
        MUCUser user = null;
350
        synchronized (userjid.toString().intern()) {
351
            user = users.get(userjid);
Matt Tucker's avatar
Matt Tucker committed
352 353 354 355 356 357 358 359 360
            if (user == null) {
                user = new MUCUserImpl(this, router, userjid);
                users.put(userjid, user);
            }
        }
        return user;
    }

    public void serverBroadcast(String msg) throws UnauthorizedException {
361 362
        for (MUCRoom room : rooms.values()) {
            room.serverBroadcast(msg);
Matt Tucker's avatar
Matt Tucker committed
363 364 365
        }
    }

366 367
    public void setServiceName(String name) {
        JiveGlobals.setProperty("xmpp.muc.service", name);
Matt Tucker's avatar
Matt Tucker committed
368 369
    }

370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435
    public void setKickIdleUsersTimeout(int timeout) {
        if (this.user_timeout == timeout) {
            return;
        }
        // Cancel the existing task because the timeout has changed
        if (userTimeoutTask != null) {
            userTimeoutTask.cancel();
        }
        this.user_timeout = timeout;
        // Create a new task and schedule it with the new timeout
        userTimeoutTask = new UserTimeoutTask();
        timer.schedule(userTimeoutTask, user_timeout, user_timeout);
        // Set the new property value
        JiveGlobals.setProperty("xmpp.muc.tasks.user.timeout", Integer.toString(timeout));
    }

    public int getKickIdleUsersTimeout() {
        return user_timeout;
    }

    public void setUserIdleTime(int idleTime) {
        if (this.user_idle == idleTime) {
            return;
        }
        this.user_idle = idleTime;
        // Set the new property value
        JiveGlobals.setProperty("xmpp.muc.tasks.user.idle", Integer.toString(idleTime));
    }

    public int getUserIdleTime() {
        return user_idle;
    }

    public void setLogConversationsTimeout(int timeout) {
        if (this.log_timeout == timeout) {
            return;
        }
        // Cancel the existing task because the timeout has changed
        if (logConversationTask != null) {
            logConversationTask.cancel();
        }
        this.log_timeout = timeout;
        // Create a new task and schedule it with the new timeout
        logConversationTask = new LogConversationTask();
        timer.schedule(logConversationTask, log_timeout, log_timeout);
        // Set the new property value
        JiveGlobals.setProperty("xmpp.muc.tasks.log.timeout", Integer.toString(timeout));
    }

    public int getLogConversationsTimeout() {
        return log_timeout;
    }

    public void setLogConversationBatchSize(int size) {
        if (this.log_batch_size == size) {
            return;
        }
        this.log_batch_size = size;
        // Set the new property value
        JiveGlobals.setProperty("xmpp.muc.tasks.log.batchsize", Integer.toString(size));
    }

    public int getLogConversationBatchSize() {
        return log_batch_size;
    }

436
    public Collection<String> getUsersAllowedToCreate() {
Matt Tucker's avatar
Matt Tucker committed
437 438 439
        return allowedToCreate;
    }

440
    public Collection<String> getSysadmins() {
Matt Tucker's avatar
Matt Tucker committed
441 442 443 444 445
        return sysadmins;
    }

    public void addSysadmin(String userJID) {
        sysadmins.add(userJID.trim().toLowerCase());
446 447 448 449
        // Update the config.
        String[] jids = new String[sysadmins.size()];
        jids = (String[])sysadmins.toArray(jids);
        JiveGlobals.setProperty("xmpp.muc.sysadmin.jid", fromArray(jids));
Matt Tucker's avatar
Matt Tucker committed
450 451 452 453
    }

    public void removeSysadmin(String userJID) {
        sysadmins.remove(userJID.trim().toLowerCase());
454 455 456 457
        // Update the config.
        String[] jids = new String[sysadmins.size()];
        jids = (String[])sysadmins.toArray(jids);
        JiveGlobals.setProperty("xmpp.muc.sysadmin.jid", fromArray(jids));
Matt Tucker's avatar
Matt Tucker committed
458 459
    }

460 461 462 463 464 465 466 467 468
    public boolean isRoomCreationRestricted() {
        return roomCreationRestricted;
    }

    public void setRoomCreationRestricted(boolean roomCreationRestricted) {
        this.roomCreationRestricted = roomCreationRestricted;
        JiveGlobals.setProperty("xmpp.muc.create.anyone", Boolean.toString(roomCreationRestricted));
    }

Matt Tucker's avatar
Matt Tucker committed
469 470 471 472
    public void addUserAllowedToCreate(String userJID) {
        // Update the list of allowed JIDs to create MUC rooms. Since we are updating the instance
        // variable there is no need to restart the service
        allowedToCreate.add(userJID.trim().toLowerCase());
473 474 475 476
        // Update the config.
        String[] jids = new String[allowedToCreate.size()];
        jids = (String[])allowedToCreate.toArray(jids);
        JiveGlobals.setProperty("xmpp.muc.create.jid", fromArray(jids));
Matt Tucker's avatar
Matt Tucker committed
477 478 479 480 481 482
    }

    public void removeUserAllowedToCreate(String userJID) {
        // Update the list of allowed JIDs to create MUC rooms. Since we are updating the instance
        // variable there is no need to restart the service
        allowedToCreate.remove(userJID.trim().toLowerCase());
483 484 485 486 487 488
        // Update the config.
        String[] jids = new String[allowedToCreate.size()];
        jids = (String[])allowedToCreate.toArray(jids);
        JiveGlobals.setProperty("xmpp.muc.create.jid", fromArray(jids));
    }

489 490
    public void initialize(XMPPServer server) {
        super.initialize(server);
491

492
        chatServiceName = JiveGlobals.getProperty("xmpp.muc.service");
493 494 495
        // Trigger the strategy to load itself from the context
        historyStrategy.setContext("xmpp.muc.history");
        // Load the list of JIDs that are sysadmins of the MUC service
496
        String property = JiveGlobals.getProperty("xmpp.muc.sysadmin.jid");
497 498 499 500 501 502
        String[] jids;
        if (property != null) {
            jids = property.split(",");
            for (int i = 0; i < jids.length; i++) {
                sysadmins.add(jids[i].trim().toLowerCase());
            }
503
        }
504 505
        roomCreationRestricted =
                Boolean.parseBoolean(JiveGlobals.getProperty("xmpp.muc.create.anyone", "false"));
506
        // Load the list of JIDs that are allowed to create a MUC room
507
        property = JiveGlobals.getProperty("xmpp.muc.create.jid");
508 509 510 511 512
        if (property != null) {
            jids = property.split(",");
            for (int i = 0; i < jids.length; i++) {
                allowedToCreate.add(jids[i].trim().toLowerCase());
            }
513 514 515 516
        }
        String value = JiveGlobals.getProperty("xmpp.muc.tasks.user.timeout");
        if (value != null) {
            try {
517
                user_timeout = Integer.parseInt(value);
Matt Tucker's avatar
Matt Tucker committed
518
            }
519 520
            catch (NumberFormatException e) {
                Log.error("Wrong number format of property xmpp.muc.tasks.user.timeout", e);
Matt Tucker's avatar
Matt Tucker committed
521
            }
522
        }
523 524 525
        value = JiveGlobals.getProperty("xmpp.muc.tasks.user.idle");
        if (value != null) {
            try {
526
                user_idle = Integer.parseInt(value);
527 528 529 530 531
            }
            catch (NumberFormatException e) {
                Log.error("Wrong number format of property xmpp.muc.tasks.user.idle", e);
            }
        }
532 533 534
        value = JiveGlobals.getProperty("xmpp.muc.tasks.log.timeout");
        if (value != null) {
            try {
535
                log_timeout = Integer.parseInt(value);
Matt Tucker's avatar
Matt Tucker committed
536
            }
537 538
            catch (NumberFormatException e) {
                Log.error("Wrong number format of property xmpp.muc.tasks.log.timeout", e);
Matt Tucker's avatar
Matt Tucker committed
539
            }
540 541 542 543
        }
        value = JiveGlobals.getProperty("xmpp.muc.tasks.log.batchsize");
        if (value != null) {
            try {
544
                log_batch_size = Integer.parseInt(value);
545 546 547
            }
            catch (NumberFormatException e) {
                Log.error("Wrong number format of property xmpp.muc.tasks.log.batchsize", e);
Matt Tucker's avatar
Matt Tucker committed
548 549
            }
        }
550 551
        if (chatServiceName == null) {
            chatServiceName = "conference";
552 553
        }
        String serverName = null;
554
        serverName = server.getServerInfo().getName();
555
        if (serverName != null) {
556
            chatServiceName += "." + serverName;
Matt Tucker's avatar
Matt Tucker committed
557
        }
558
        chatServiceAddress = new JID(null, chatServiceName, null);
Matt Tucker's avatar
Matt Tucker committed
559 560
        // Run through the users every 5 minutes after a 5 minutes server startup delay (default
        // values)
561 562
        userTimeoutTask = new UserTimeoutTask();
        timer.schedule(userTimeoutTask, user_timeout, user_timeout);
Matt Tucker's avatar
Matt Tucker committed
563 564
        // Log the room conversations every 5 minutes after a 5 minutes server startup delay
        // (default values)
565 566
        logConversationTask = new LogConversationTask();
        timer.schedule(logConversationTask, log_timeout, log_timeout);
567 568 569 570 571 572 573 574 575 576 577 578 579

        routingTable = server.getRoutingTable();
        router = server.getPacketRouter();
        // TODO Remove the tracking for IQRegisterHandler when the component JEP gets implemented.
        registerHandler = server.getIQRegisterHandler();
        registerHandler.addDelegate(getServiceName(), new IQMUCRegisterHandler(this));

        // Add the route to this service
        routingTable.addRoute(chatServiceAddress, this);
        ArrayList params = new ArrayList();
        params.clear();
        params.add(chatServiceName);
        Log.info(LocaleUtils.getLocalizedString("startup.starting.muc", params));
Matt Tucker's avatar
Matt Tucker committed
580 581 582 583
    }

    public void start() {
        super.start();
584
        routingTable.addRoute(chatServiceAddress, this);
Matt Tucker's avatar
Matt Tucker committed
585 586
        ArrayList params = new ArrayList();
        params.clear();
587
        params.add(chatServiceName);
Matt Tucker's avatar
Matt Tucker committed
588
        Log.info(LocaleUtils.getLocalizedString("startup.starting.muc", params));
589 590 591 592
        // Load all the persistent rooms to memory
        for (MUCRoom room : MUCPersistenceManager.loadRoomsFromDB(this, router)) {
            rooms.put(room.getName().toLowerCase(), room);
        }
Matt Tucker's avatar
Matt Tucker committed
593 594 595 596
    }

    public void stop() {
        super.stop();
597
        routingTable.removeRoute(chatServiceAddress);
Matt Tucker's avatar
Matt Tucker committed
598
        timer.cancel();
599
        logAllConversation();
600
        if (registerHandler != null) {
601
            registerHandler.removeDelegate(getServiceName());
602
        }
Matt Tucker's avatar
Matt Tucker committed
603 604
    }

605
    public JID getAddress() {
606
        if (chatServiceAddress == null) {
Matt Tucker's avatar
Matt Tucker committed
607 608
            throw new IllegalStateException("Not initialized");
        }
609
        return chatServiceAddress;
Matt Tucker's avatar
Matt Tucker committed
610 611
    }

612
    public void process(Packet packet) {
Matt Tucker's avatar
Matt Tucker committed
613
        try {
614
            MUCUser user = getChatUser(packet.getFrom());
Matt Tucker's avatar
Matt Tucker committed
615 616 617 618 619 620 621 622 623 624 625
            user.process(packet);
        }
        catch (Exception e) {
            Log.error(LocaleUtils.getLocalizedString("admin.error"), e);
        }
    }

    public long getTotalChatTime() {
        return totalChatTime;
    }

626
    public void logConversation(MUCRoom room, Message message, JID sender) {
Matt Tucker's avatar
Matt Tucker committed
627 628 629 630 631 632 633 634
        logQueue.add(new ConversationLogEntry(new Date(), room, message, sender));
    }

    public Iterator getItems() {
        ArrayList items = new ArrayList();

        items.add(new DiscoServerItem() {
            public String getJID() {
635
                return chatServiceName;
Matt Tucker's avatar
Matt Tucker committed
636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660
            }

            public String getName() {
                return "Public Chatrooms";
            }

            public String getAction() {
                return null;
            }

            public String getNode() {
                return null;
            }

            public DiscoInfoProvider getDiscoInfoProvider() {
                return MultiUserChatServerImpl.this;
            }

            public DiscoItemsProvider getDiscoItemsProvider() {
                return MultiUserChatServerImpl.this;
            }
        });
        return items.iterator();
    }

661
    public Iterator getIdentities(String name, String node, JID senderJID) {
Matt Tucker's avatar
Matt Tucker committed
662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678
        // TODO Improve performance by not creating objects each time
        ArrayList identities = new ArrayList();
        if (name == null && node == null) {
            // Answer the identity of the MUC service
            Element identity = DocumentHelper.createElement("identity");
            identity.addAttribute("category", "conference");
            identity.addAttribute("name", "Public Chatrooms");
            identity.addAttribute("type", "text");

            identities.add(identity);
        }
        else if (name != null && node == null) {
            // Answer the identity of a given room
            MUCRoom room = getChatRoom(name);
            if (room != null && room.isPublicRoom()) {
                Element identity = DocumentHelper.createElement("identity");
                identity.addAttribute("category", "conference");
679
                identity.addAttribute("name", room.getNaturalLanguageName());
Matt Tucker's avatar
Matt Tucker committed
680 681 682 683 684 685 686 687 688
                identity.addAttribute("type", "text");

                identities.add(identity);
            }
        }
        else if (name != null && "x-roomuser-item".equals(node)) {
            // Answer reserved nickname for the sender of the disco request in the requested room
            MUCRoom room = getChatRoom(name);
            if (room != null) {
689
                String reservedNick = room.getReservedNickname(senderJID.toBareJID());
Matt Tucker's avatar
Matt Tucker committed
690 691 692 693 694 695 696 697 698 699 700 701 702
                if (reservedNick != null) {
                    Element identity = DocumentHelper.createElement("identity");
                    identity.addAttribute("category", "conference");
                    identity.addAttribute("name", reservedNick);
                    identity.addAttribute("type", "text");

                    identities.add(identity);
                }
            }
        }
        return identities.iterator();
    }

703
    public Iterator getFeatures(String name, String node, JID senderJID) {
Matt Tucker's avatar
Matt Tucker committed
704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753
        ArrayList features = new ArrayList();
        if (name == null && node == null) {
            // Answer the features of the MUC service
            features.add("http://jabber.org/protocol/muc");
            features.add("http://jabber.org/protocol/disco#info");
            features.add("http://jabber.org/protocol/disco#items");
        }
        else if (name != null && node == null) {
            // Answer the features of a given room
            // TODO lock the room while gathering this info???
            MUCRoom room = getChatRoom(name);
            if (room != null && room.isPublicRoom()) {
                features.add("http://jabber.org/protocol/muc");
                // Always add public since only public rooms can be discovered
                features.add("muc_public");
                if (room.isInvitationRequiredToEnter()) {
                    features.add("muc_membersonly");
                }
                else {
                    features.add("muc_open");
                }
                if (room.isModerated()) {
                    features.add("muc_moderated");
                }
                else {
                    features.add("muc_unmoderated");
                }
                if (room.canAnyoneDiscoverJID()) {
                    features.add("muc_nonanonymous");
                }
                else {
                    features.add("muc_semianonymous");
                }
                if (room.isPasswordProtected()) {
                    features.add("muc_passwordprotected");
                }
                else {
                    features.add("muc_unsecured");
                }
                if (room.isPersistent()) {
                    features.add("muc_persistent");
                }
                else {
                    features.add("muc_temporary");
                }
            }
        }
        return features.iterator();
    }

Derek DeMoro's avatar
Derek DeMoro committed
754
    public XDataFormImpl getExtendedInfo(String name, String node, JID senderJID) {
Matt Tucker's avatar
Matt Tucker committed
755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793
        if (name != null && node == null) {
            // Answer the extended info of a given room
            // TODO lock the room while gathering this info???
            // TODO Do not generate a form each time. Keep it as static or instance variable
            MUCRoom room = getChatRoom(name);
            if (room != null && room.isPublicRoom()) {
                XDataFormImpl dataForm = new XDataFormImpl(DataForm.TYPE_RESULT);

                XFormFieldImpl field = new XFormFieldImpl("FORM_TYPE");
                field.setType(FormField.TYPE_HIDDEN);
                field.addValue("http://jabber.org/protocol/muc#roominfo");
                dataForm.addField(field);

                field = new XFormFieldImpl("muc#roominfo_description");
                field.setLabel(LocaleUtils.getLocalizedString("muc.extended.info.desc"));
                field.addValue(room.getDescription());
                dataForm.addField(field);

                field = new XFormFieldImpl("muc#roominfo_subject");
                field.setLabel(LocaleUtils.getLocalizedString("muc.extended.info.subject"));
                field.addValue(room.getSubject());
                dataForm.addField(field);

                field = new XFormFieldImpl("muc#roominfo_occupants");
                field.setLabel(LocaleUtils.getLocalizedString("muc.extended.info.occupants"));
                field.addValue(Integer.toString(room.getOccupantsCount()));
                dataForm.addField(field);

                /*field = new XFormFieldImpl("muc#roominfo_lang");
                field.setLabel(LocaleUtils.getLocalizedString("muc.extended.info.language"));
                field.addValue(room.getLanguage());
                dataForm.addField(field);*/

                return dataForm;
            }
        }
        return null;
    }

794
    public boolean hasInfo(String name, String node, JID senderJID)
Matt Tucker's avatar
Matt Tucker committed
795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810
            throws UnauthorizedException {
        if (name == null && node == node) {
            // We always have info about the MUC service
            return true;
        }
        else if (name != null && node == null) {
            // We only have info if the room exists
            return hasChatRoom(name);
        }
        else if (name != null && "x-roomuser-item".equals(node)) {
            // We always have info about reserved names as long as the room exists
            return hasChatRoom(name);
        }
        return false;
    }

811
    public Iterator<Element> getItems(String name, String node, JID senderJID)
Matt Tucker's avatar
Matt Tucker committed
812
            throws UnauthorizedException {
813
        List<Element> answer = new ArrayList<Element>();
Matt Tucker's avatar
Matt Tucker committed
814
        if (name == null && node == null) {
815
            Element item;
Matt Tucker's avatar
Matt Tucker committed
816
            // Answer all the public rooms as items
817 818
            for (MUCRoom room : rooms.values()) {
                if (room.isPublicRoom()) {
819
                    item = DocumentHelper.createElement("item");
820
                    item.addAttribute("jid", room.getRole().getRoleAddress().toString());
821
                    item.addAttribute("name", room.getNaturalLanguageName());
822 823 824

                    answer.add(item);
                }
Matt Tucker's avatar
Matt Tucker committed
825 826 827 828 829 830 831
            }
        }
        else if (name != null && node == null) {
            // Answer the room occupants as items if that info is publicly available
            MUCRoom room = getChatRoom(name);
            if (room != null && room.isPublicRoom()) {
                Element item;
832
                for (MUCRole role : room.getOccupants()) {
833
                    // TODO Should we filter occupants that are invisible (presence is not broadcasted)?
Matt Tucker's avatar
Matt Tucker committed
834
                    item = DocumentHelper.createElement("item");
835
                    item.addAttribute("jid", role.getRoleAddress().toString());
Matt Tucker's avatar
Matt Tucker committed
836 837 838 839 840 841 842

                    answer.add(item);
                }
            }
        }
        return answer.iterator();
    }
843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860

    /**
     * Converts an array to a comma-delimitted String.
     *
     * @param array the array.
     * @return a comma delimtted String of the array values.
     */
    private static String fromArray(String [] array) {
        StringBuffer buf = new StringBuffer();
        for (int i=0; i<array.length; i++) {
            buf.append(array[i]);
            if (i != array.length-1) {
                buf.append(",");
            }
        }
        return buf.toString();
    }
}