MultiUserChatServerImpl.java 38.4 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;

14
import java.util.*;
Derek DeMoro's avatar
Derek DeMoro committed
15 16 17
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.LinkedBlockingQueue;
18 19 20
import java.text.DateFormat;
import java.text.SimpleDateFormat;

Derek DeMoro's avatar
Derek DeMoro committed
21 22
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
23
import org.jivesoftware.messenger.*;
Derek DeMoro's avatar
Derek DeMoro committed
24 25
import org.jivesoftware.messenger.auth.UnauthorizedException;
import org.jivesoftware.messenger.container.BasicModule;
Matt Tucker's avatar
Matt Tucker committed
26 27 28 29 30 31 32 33
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;
Derek DeMoro's avatar
Derek DeMoro committed
34 35 36 37 38 39
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
40
import org.jivesoftware.messenger.user.UserNotFoundException;
Derek DeMoro's avatar
Derek DeMoro committed
41 42
import org.jivesoftware.util.LocaleUtils;
import org.jivesoftware.util.Log;
43
import org.jivesoftware.util.JiveGlobals;
44
import org.xmpp.packet.*;
45
import org.xmpp.component.ComponentManager;
Matt Tucker's avatar
Matt Tucker committed
46 47

/**
48
 * Implements the chat server as a cached memory resident chat server. The server is also
Matt Tucker's avatar
Matt Tucker committed
49 50 51
 * 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 
52
 * separate process<p>
53
 *
54 55 56 57 58 59
 * Temporary rooms are held in memory as long as they have occupants. They will be destroyed after
 * the last occupant left the room. On the other hand, persistent rooms are always present in memory
 * even after the last occupant left the room. In order to keep memory clean of peristent rooms that
 * have been forgotten or abandonded this class includes a clean up process. The clean up process
 * will remove from memory rooms that haven't had occupants for a while. Moreover, forgotten or
 * abandoned rooms won't be loaded into memory when the Multi-User Chat service starts up.
Matt Tucker's avatar
Matt Tucker committed
60 61 62 63
 *
 * @author Gaston Dombiak
 */
public class MultiUserChatServerImpl extends BasicModule implements MultiUserChatServer,
64
        ServerItemsProvider, DiscoInfoProvider, DiscoItemsProvider, RoutableChannelHandler {
Matt Tucker's avatar
Matt Tucker committed
65

66 67 68 69
    private static final DateFormat dateFormatter = new SimpleDateFormat("yyyyMMdd'T'HH:mm:ss");
    static {
        dateFormatter.setTimeZone(TimeZone.getTimeZone("GMT+0"));
    }
Matt Tucker's avatar
Matt Tucker committed
70 71 72
    /**
     * The time to elapse between clearing of idle chat users.
     */
73
    private int user_timeout = 300000;
74 75 76
    /**
     * The number of milliseconds a user must be idle before he/she gets kicked from all the rooms.
     */
77
    private int user_idle = -1;
78 79 80 81
    /**
     * Task that kicks idle users from the rooms.
     */
    private UserTimeoutTask userTimeoutTask;
Matt Tucker's avatar
Matt Tucker committed
82 83 84
    /**
     * The time to elapse between logging the room conversations.
     */
85
    private int log_timeout = 300000;
Matt Tucker's avatar
Matt Tucker committed
86 87 88
    /**
     * The number of messages to log on each run of the logging process.
     */
89 90 91 92 93
    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
94
    /**
95
     * the chat service's hostname
Matt Tucker's avatar
Matt Tucker committed
96
     */
97
    private String chatServiceName = null;
Matt Tucker's avatar
Matt Tucker committed
98 99 100 101

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

Matt Tucker's avatar
Matt Tucker committed
104 105 106
    /**
     * chat users managed by this manager, table: key user jid (XMPPAddress); value ChatUser
     */
107
    private Map<JID, MUCUser> users = new ConcurrentHashMap<JID, MUCUser>();
Matt Tucker's avatar
Matt Tucker committed
108 109
    private HistoryStrategy historyStrategy;

110
    private RoutingTable routingTable = null;
Matt Tucker's avatar
Matt Tucker committed
111 112 113
    /**
     * The packet router for the server.
     */
114
    private PacketRouter router = null;
Matt Tucker's avatar
Matt Tucker committed
115 116 117
    /**
     * The handler of packets with namespace jabber:iq:register for the server.
     */
118
    private IQMUCRegisterHandler registerHandler = null;
Matt Tucker's avatar
Matt Tucker committed
119 120 121 122 123 124 125 126 127
    /**
     * 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.
     */
128
    private Timer timer = new Timer("MUC cleanup");
Matt Tucker's avatar
Matt Tucker committed
129

130 131 132 133 134 135 136 137
    /**
     * Flag that indicates if the service should provide information about locked rooms when
     * handling service discovery requests.
     * Note: Setting this flag in false is not compliant with the spec. A user may try to join a
     * locked room thinking that the room doesn't exist because the user didn't discover it before.
     */
    private boolean allowToDiscoverLockedRooms = true;

138 139 140 141 142 143 144
    /**
     * 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
145 146 147 148
    /**
     * Bare jids of users that are allowed to create MUC rooms. An empty list means that anyone can 
     * create a room. 
     */
149
    private Collection<String> allowedToCreate = new CopyOnWriteArrayList<String>();
Matt Tucker's avatar
Matt Tucker committed
150 151 152 153 154

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

    /**
     * Queue that holds the messages to log for the rooms that need to log their conversations.
     */
160
    private Queue<ConversationLogEntry> logQueue = new LinkedBlockingQueue<ConversationLogEntry>();
Matt Tucker's avatar
Matt Tucker committed
161

162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177
    /**
     * Max number of hours that a persistent room may be empty before the service removes the
     * room from memory. Unloaded rooms will exist in the database and may be loaded by a user
     * request. Default time limit is: 7 days.
     */
    private long emptyLimit = 7 * 24;
    /**
     * Task that removes rooms from memory that have been without activity for a period of time. A
     * room is considered without activity when no occupants are present in the room for a while.
     */
    private CleanupTask cleanupTask;
    /**
     * The time to elapse between each rooms cleanup. Default frequency is 60 minutes.
     */
    private final long cleanup_frequency = 60 * 60 * 1000;

Matt Tucker's avatar
Matt Tucker committed
178 179 180 181 182 183 184 185
    /**
     * Create a new group chat server.
     */
    public MultiUserChatServerImpl() {
        super("Basic multi user chat server");
        historyStrategy = new HistoryStrategy(null);
    }

186 187 188 189
    public String getDescription() {
        return null;
    }

190 191 192 193 194
    public void process(Packet packet) throws UnauthorizedException, PacketException {
        // TODO Remove this method when moving MUC as a component and removing module code
        processPacket(packet);
    }

195
    public void processPacket(Packet packet) {
196 197 198
        // The MUC service will receive all the packets whose domain matches the domain of the MUC
        // service. This means that, for instance, a disco request should be responded by the
        // service itself instead of relying on the server to handle the request.
199
        try {
200 201 202 203 204 205 206
            // Check if the packet is a disco request or a packet with namespace iq:register
            if (packet instanceof IQ) {
                if (process((IQ)packet)) {
                    return;
                }
            }
            // The packet is a normal packet that should possibly be sent to the room
207 208 209 210 211 212 213 214
            MUCUser user = getChatUser(packet.getFrom());
            user.process(packet);
        }
        catch (Exception e) {
            Log.error(LocaleUtils.getLocalizedString("admin.error"), e);
        }
    }

215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259
    /**
     * Returns true if the IQ packet was processed. This method should only process disco packets
     * as well as jabber:iq:register packets sent to the MUC service.
     *
     * @param iq the IQ packet to process.
     * @return true if the IQ packet was processed.
     */
    private boolean process(IQ iq) {
        Element childElement = iq.getChildElement();
        String namespace = null;
        if (childElement != null) {
            namespace = childElement.getNamespaceURI();
        }
        if ("jabber:iq:register".equals(namespace)) {
            IQ reply = registerHandler.handleIQ(iq);
            router.route(reply);
        }
        else if ("http://jabber.org/protocol/disco#info".equals(namespace)) {
            try {
                // TODO MUC should have an IQDiscoInfoHandler of its own when MUC becomes
                // a component
                IQ reply = XMPPServer.getInstance().getIQDiscoInfoHandler().handleIQ(iq);
                router.route(reply);
            }
            catch (UnauthorizedException e) {
                // Do nothing. This error should never happen
            }
        }
        else if ("http://jabber.org/protocol/disco#items".equals(namespace)) {
            try {
                // TODO MUC should have an IQDiscoItemsHandler of its own when MUC becomes
                // a component
                IQ reply = XMPPServer.getInstance().getIQDiscoItemsHandler().handleIQ(iq);
                router.route(reply);
            }
            catch (UnauthorizedException e) {
                // Do nothing. This error should never happen
            }
        }
        else {
            return false;
        }
        return true;
    }

260 261 262 263 264 265 266 267 268 269 270 271
    public void initialize(JID jid, ComponentManager componentManager) {

    }

    public void shutdown() {

    }

    public String getServiceDomain() {
        return chatServiceName + "." + XMPPServer.getInstance().getServerInfo().getName();
    }

272 273 274 275
    public JID getAddress() {
        return new JID(null, getServiceDomain(), null);
    }

Matt Tucker's avatar
Matt Tucker committed
276 277 278 279 280 281 282 283
    /**
     * 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() {
284
            checkForTimedOutUsers();
Matt Tucker's avatar
Matt Tucker committed
285 286 287
        }
    }

288
    private void checkForTimedOutUsers() {
289
        // Do nothing if this feature is disabled (i.e USER_IDLE equals -1)
290
        if (user_idle == -1) {
291 292
            return;
        }
293
        final long deadline = System.currentTimeMillis() - user_idle;
294
        for (MUCUser user : users.values()) {
Matt Tucker's avatar
Matt Tucker committed
295 296
            try {
                if (user.getLastPacketTime() < deadline) {
297 298 299
                    // Kick the user from all the rooms that he/she had previuosly joined
                    Iterator<MUCRole> roles = user.getRoles();
                    MUCRole role;
300 301
                    MUCRoom room;
                    Presence kickedPresence;
302 303
                    while (roles.hasNext()) {
                        role = roles.next();
304
                        room = role.getChatRoom();
305
                        try {
306
                            kickedPresence =
307
                                    room.kickOccupant(user.getAddress(), null, null);
308 309
                            // Send the updated presence to the room occupants
                            room.send(kickedPresence);
310 311 312 313 314
                        }
                        catch (NotAllowedException e) {
                            // Do nothing since we cannot kick owners or admins
                        }
                    }
Matt Tucker's avatar
Matt Tucker committed
315 316
                }
            }
317
            catch (Throwable e) {
Matt Tucker's avatar
Matt Tucker committed
318 319 320 321 322 323 324 325 326 327
                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() {
328 329 330 331 332
            try {
                logConversation();
            }
            catch (Throwable e) {
                Log.error(LocaleUtils.getLocalizedString("admin.error"), e);
Matt Tucker's avatar
Matt Tucker committed
333 334 335 336 337 338 339
            }
        }
    }

    private void logConversation() {
        ConversationLogEntry entry = null;
        boolean success = false;
340
        for (int index = 0; index <= log_batch_size && !logQueue.isEmpty(); index++) {
341
            entry = logQueue.poll();
Matt Tucker's avatar
Matt Tucker committed
342 343 344 345 346 347 348 349 350
            if (entry != null) {
                success = MUCPersistenceManager.saveConversationLogEntry(entry);
                if (!success) {
                    logQueue.add(entry);
                }
            }
        }
    }

351 352 353 354 355 356 357 358 359 360 361 362 363 364
    /**
     * 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);
            }
        }
    }

365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387
    /**
     * Removes from memory rooms that have been without activity for a period of time. A room is
     * considered without activity when no occupants are present in the room for a while.
     */
    private class CleanupTask extends TimerTask {
        public void run() {
            try {
                cleanupRooms();
            }
            catch (Throwable e) {
                Log.error(LocaleUtils.getLocalizedString("admin.error"), e);
            }
        }
    }

    private void cleanupRooms() {
        for (MUCRoom room : rooms.values()) {
            if (room.getEmptyDate() != null && room.getEmptyDate().before(getCleanupDate())) {
                removeChatRoom(room.getName());
            }
        }
    }

388
    public MUCRoom getChatRoom(String roomName, JID userjid) throws UnauthorizedException {
Matt Tucker's avatar
Matt Tucker committed
389
        MUCRoom room = null;
390
        synchronized (roomName.intern()) {
391
            room = rooms.get(roomName.toLowerCase());
Matt Tucker's avatar
Matt Tucker committed
392 393
            if (room == null) {
                room = new MUCRoomImpl(this, roomName, router);
394 395 396
                // 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
397 398
                    // persistent but was added to the DB after the server was started up or the
                    // room may be an old room that was not present in memory)
399
                    MUCPersistenceManager.loadFromDB((MUCRoomImpl) room);
400 401 402
                }
                catch (IllegalArgumentException e) {
                    // The room does not exist so check for creation permissions
Matt Tucker's avatar
Matt Tucker committed
403
                    // Room creation is always allowed for sysadmin
404
                    if (isRoomCreationRestricted() &&
405
                            !sysadmins.contains(userjid.toBareJID())) {
Matt Tucker's avatar
Matt Tucker committed
406
                        // The room creation is only allowed for certain JIDs
407
                        if (!allowedToCreate.contains(userjid.toBareJID())) {
Matt Tucker's avatar
Matt Tucker committed
408 409 410 411 412
                            // The user is not in the list of allowed JIDs to create a room so raise
                            // an exception
                            throw new UnauthorizedException();
                        }
                    }
413
                    room.addFirstOwner(userjid.toBareJID());
Matt Tucker's avatar
Matt Tucker committed
414 415 416 417 418 419 420 421
                }
                rooms.put(roomName.toLowerCase(), room);
            }
        }
        return room;
    }

    public MUCRoom getChatRoom(String roomName) {
422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444
        MUCRoom room = rooms.get(roomName.toLowerCase());
        if (room == null) {
            // Check if the room exists in the database and was not present in memory
            synchronized (roomName.intern()) {
                room = rooms.get(roomName.toLowerCase());
                if (room == null) {
                    room = new MUCRoomImpl(this, roomName, router);
                    // 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 or the
                        // room may be an old room that was not present in memory)
                        MUCPersistenceManager.loadFromDB((MUCRoomImpl) room);
                        rooms.put(roomName.toLowerCase(), room);
                    }
                    catch (IllegalArgumentException e) {
                        // The room does not exist so do nothing
                        room = null;
                    }
                }
            }
        }
        return room;
445 446 447
    }

    public List<MUCRoom> getChatRooms() {
448
        return new ArrayList<MUCRoom>(rooms.values());
Matt Tucker's avatar
Matt Tucker committed
449 450
    }

451 452 453 454
    public boolean hasChatRoom(String roomName) {
        return getChatRoom(roomName) != null;
    }

455
    public void removeChatRoom(String roomName) {
456 457
        final MUCRoom room = rooms.remove(roomName.toLowerCase());
        if (room != null) {
Matt Tucker's avatar
Matt Tucker committed
458 459 460 461 462
            final long chatLength = room.getChatLength();
            totalChatTime += chatLength;
        }
    }

463 464
    public String getServiceName() {
        return chatServiceName;
Matt Tucker's avatar
Matt Tucker committed
465 466 467 468 469 470
    }

    public HistoryStrategy getHistoryStrategy() {
        return historyStrategy;
    }

471
    public void removeUser(JID jabberID) {
472 473 474 475 476 477 478 479 480 481
        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
482 483 484 485 486
                }
            }
        }
    }

487
    public MUCUser getChatUser(JID userjid) throws UserNotFoundException {
Matt Tucker's avatar
Matt Tucker committed
488 489 490 491
        if (router == null) {
            throw new IllegalStateException("Not initialized");
        }
        MUCUser user = null;
492
        synchronized (userjid.toString().intern()) {
493
            user = users.get(userjid);
Matt Tucker's avatar
Matt Tucker committed
494 495 496 497 498 499 500 501 502
            if (user == null) {
                user = new MUCUserImpl(this, router, userjid);
                users.put(userjid, user);
            }
        }
        return user;
    }

    public void serverBroadcast(String msg) throws UnauthorizedException {
503 504
        for (MUCRoom room : rooms.values()) {
            room.serverBroadcast(msg);
Matt Tucker's avatar
Matt Tucker committed
505 506 507
        }
    }

508 509
    public void setServiceName(String name) {
        JiveGlobals.setProperty("xmpp.muc.service", name);
Matt Tucker's avatar
Matt Tucker committed
510 511
    }

512 513 514 515 516 517 518 519 520
    /**
     * Returns the limit date after which rooms whithout activity will be removed from memory.
     *
     * @return the limit date after which rooms whithout activity will be removed from memory.
     */
    private Date getCleanupDate() {
        return new Date(System.currentTimeMillis() - (emptyLimit * 3600000));
    }

521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586
    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;
    }

587
    public Collection<String> getUsersAllowedToCreate() {
Matt Tucker's avatar
Matt Tucker committed
588 589 590
        return allowedToCreate;
    }

591
    public Collection<String> getSysadmins() {
Matt Tucker's avatar
Matt Tucker committed
592 593 594 595 596
        return sysadmins;
    }

    public void addSysadmin(String userJID) {
        sysadmins.add(userJID.trim().toLowerCase());
597 598 599 600
        // 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
601 602 603 604
    }

    public void removeSysadmin(String userJID) {
        sysadmins.remove(userJID.trim().toLowerCase());
605 606 607 608
        // 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
609 610
    }

611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635
    /**
     * Returns the flag that indicates if the service should provide information about locked rooms
     * when handling service discovery requests.
     *
     * @return true if the service should provide information about locked rooms.
     */
    public boolean isAllowToDiscoverLockedRooms() {
        return allowToDiscoverLockedRooms;
    }

    /**
     * Sets the flag that indicates if the service should provide information about locked rooms
     * when handling service discovery requests.
     * Note: Setting this flag in false is not compliant with the spec. A user may try to join a
     * locked room thinking that the room doesn't exist because the user didn't discover it before.
     *
     * @param allowToDiscoverLockedRooms if the service should provide information about locked
     *        rooms.
     */
    public void setAllowToDiscoverLockedRooms(boolean allowToDiscoverLockedRooms) {
        this.allowToDiscoverLockedRooms = allowToDiscoverLockedRooms;
        JiveGlobals.setProperty("xmpp.muc.discover.locked",
                Boolean.toString(allowToDiscoverLockedRooms));
    }

636 637 638 639 640 641 642 643 644
    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
645 646 647 648
    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());
649 650 651 652
        // 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
653 654 655 656 657 658
    }

    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());
659 660 661 662 663 664
        // Update the config.
        String[] jids = new String[allowedToCreate.size()];
        jids = (String[])allowedToCreate.toArray(jids);
        JiveGlobals.setProperty("xmpp.muc.create.jid", fromArray(jids));
    }

665 666
    public void initialize(XMPPServer server) {
        super.initialize(server);
667

668
        chatServiceName = JiveGlobals.getProperty("xmpp.muc.service");
669 670 671
        // 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
672
        String property = JiveGlobals.getProperty("xmpp.muc.sysadmin.jid");
673 674 675 676 677 678
        String[] jids;
        if (property != null) {
            jids = property.split(",");
            for (int i = 0; i < jids.length; i++) {
                sysadmins.add(jids[i].trim().toLowerCase());
            }
679
        }
680 681
        allowToDiscoverLockedRooms =
                Boolean.parseBoolean(JiveGlobals.getProperty("xmpp.muc.discover.locked", "true"));
682 683
        roomCreationRestricted =
                Boolean.parseBoolean(JiveGlobals.getProperty("xmpp.muc.create.anyone", "false"));
684
        // Load the list of JIDs that are allowed to create a MUC room
685
        property = JiveGlobals.getProperty("xmpp.muc.create.jid");
686 687 688 689 690
        if (property != null) {
            jids = property.split(",");
            for (int i = 0; i < jids.length; i++) {
                allowedToCreate.add(jids[i].trim().toLowerCase());
            }
691 692 693 694
        }
        String value = JiveGlobals.getProperty("xmpp.muc.tasks.user.timeout");
        if (value != null) {
            try {
695
                user_timeout = Integer.parseInt(value);
Matt Tucker's avatar
Matt Tucker committed
696
            }
697 698
            catch (NumberFormatException e) {
                Log.error("Wrong number format of property xmpp.muc.tasks.user.timeout", e);
Matt Tucker's avatar
Matt Tucker committed
699
            }
700
        }
701 702 703
        value = JiveGlobals.getProperty("xmpp.muc.tasks.user.idle");
        if (value != null) {
            try {
704
                user_idle = Integer.parseInt(value);
705 706 707 708 709
            }
            catch (NumberFormatException e) {
                Log.error("Wrong number format of property xmpp.muc.tasks.user.idle", e);
            }
        }
710 711 712
        value = JiveGlobals.getProperty("xmpp.muc.tasks.log.timeout");
        if (value != null) {
            try {
713
                log_timeout = Integer.parseInt(value);
Matt Tucker's avatar
Matt Tucker committed
714
            }
715 716
            catch (NumberFormatException e) {
                Log.error("Wrong number format of property xmpp.muc.tasks.log.timeout", e);
Matt Tucker's avatar
Matt Tucker committed
717
            }
718 719 720 721
        }
        value = JiveGlobals.getProperty("xmpp.muc.tasks.log.batchsize");
        if (value != null) {
            try {
722
                log_batch_size = Integer.parseInt(value);
723 724 725
            }
            catch (NumberFormatException e) {
                Log.error("Wrong number format of property xmpp.muc.tasks.log.batchsize", e);
Matt Tucker's avatar
Matt Tucker committed
726 727
            }
        }
728 729
        if (chatServiceName == null) {
            chatServiceName = "conference";
730
        }
Matt Tucker's avatar
Matt Tucker committed
731 732
        // Run through the users every 5 minutes after a 5 minutes server startup delay (default
        // values)
733 734
        userTimeoutTask = new UserTimeoutTask();
        timer.schedule(userTimeoutTask, user_timeout, user_timeout);
Matt Tucker's avatar
Matt Tucker committed
735 736
        // Log the room conversations every 5 minutes after a 5 minutes server startup delay
        // (default values)
737 738
        logConversationTask = new LogConversationTask();
        timer.schedule(logConversationTask, log_timeout, log_timeout);
739 740 741
        // Remove unused rooms from memory
        cleanupTask = new CleanupTask();
        timer.schedule(cleanupTask, cleanup_frequency, cleanup_frequency);
742

743
        routingTable = server.getRoutingTable();
744
        router = server.getPacketRouter();
745 746
        // Configure the handler of iq:register packets
        registerHandler = new IQMUCRegisterHandler(this);
Matt Tucker's avatar
Matt Tucker committed
747 748 749 750
    }

    public void start() {
        super.start();
751 752 753 754 755 756
        // Add the route to this service
        routingTable.addRoute(getAddress(), this);
        ArrayList params = new ArrayList();
        params.clear();
        params.add(getServiceDomain());
        Log.info(LocaleUtils.getLocalizedString("startup.starting.muc", params));
757
        // Load all the persistent rooms to memory
758 759
        for (MUCRoom room : MUCPersistenceManager.loadRoomsFromDB(this, this.getCleanupDate(),
                router)) {
760 761
            rooms.put(room.getName().toLowerCase(), room);
        }
Matt Tucker's avatar
Matt Tucker committed
762 763 764 765
    }

    public void stop() {
        super.stop();
766
        // Remove the route to this service
767
        routingTable.removeRoute(getAddress());
Matt Tucker's avatar
Matt Tucker committed
768
        timer.cancel();
769
        logAllConversation();
Matt Tucker's avatar
Matt Tucker committed
770 771 772 773 774 775
    }

    public long getTotalChatTime() {
        return totalChatTime;
    }

776
    public void logConversation(MUCRoom room, Message message, JID sender) {
Matt Tucker's avatar
Matt Tucker committed
777 778 779 780 781 782 783 784
        logQueue.add(new ConversationLogEntry(new Date(), room, message, sender));
    }

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

        items.add(new DiscoServerItem() {
            public String getJID() {
785
                return getServiceDomain();
Matt Tucker's avatar
Matt Tucker committed
786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810
            }

            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();
    }

811
    public Iterator getIdentities(String name, String node, JID senderJID) {
Matt Tucker's avatar
Matt Tucker committed
812 813 814 815 816 817 818 819 820 821 822 823 824 825
        // 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);
826
            if (room != null && canDiscoverRoom(room)) {
Matt Tucker's avatar
Matt Tucker committed
827 828
                Element identity = DocumentHelper.createElement("identity");
                identity.addAttribute("category", "conference");
829
                identity.addAttribute("name", room.getNaturalLanguageName());
Matt Tucker's avatar
Matt Tucker committed
830 831 832 833 834 835 836 837 838
                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) {
839
                String reservedNick = room.getReservedNickname(senderJID.toBareJID());
Matt Tucker's avatar
Matt Tucker committed
840 841 842 843 844 845 846 847 848 849 850 851 852
                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();
    }

853
    public Iterator getFeatures(String name, String node, JID senderJID) {
Matt Tucker's avatar
Matt Tucker committed
854 855 856 857 858 859 860 861 862 863 864
        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);
865
            if (room != null && canDiscoverRoom(room)) {
Matt Tucker's avatar
Matt Tucker committed
866 867 868
                features.add("http://jabber.org/protocol/muc");
                // Always add public since only public rooms can be discovered
                features.add("muc_public");
869
                if (room.isMembersOnly()) {
Matt Tucker's avatar
Matt Tucker committed
870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903
                    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
904
    public XDataFormImpl getExtendedInfo(String name, String node, JID senderJID) {
Matt Tucker's avatar
Matt Tucker committed
905 906 907 908
        if (name != null && node == null) {
            // Answer the extended info of a given room
            // TODO lock the room while gathering this info???
            MUCRoom room = getChatRoom(name);
909
            if (room != null && canDiscoverRoom(room)) {
Matt Tucker's avatar
Matt Tucker committed
910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936
                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);*/

937 938 939 940 941
                field = new XFormFieldImpl("x-muc#roominfo_creationdate");
                field.setLabel(LocaleUtils.getLocalizedString("muc.extended.info.creationdate"));
                field.addValue(dateFormatter.format(room.getCreationDate()));
                dataForm.addField(field);

Matt Tucker's avatar
Matt Tucker committed
942 943 944 945 946 947
                return dataForm;
            }
        }
        return null;
    }

948
    public boolean hasInfo(String name, String node, JID senderJID)
Matt Tucker's avatar
Matt Tucker committed
949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964
            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;
    }

965
    public Iterator<Element> getItems(String name, String node, JID senderJID)
Matt Tucker's avatar
Matt Tucker committed
966
            throws UnauthorizedException {
967
        List<Element> answer = new ArrayList<Element>();
Matt Tucker's avatar
Matt Tucker committed
968
        if (name == null && node == null) {
969
            Element item;
Matt Tucker's avatar
Matt Tucker committed
970
            // Answer all the public rooms as items
971
            for (MUCRoom room : rooms.values()) {
972
                if (canDiscoverRoom(room)) {
973
                    item = DocumentHelper.createElement("item");
974
                    item.addAttribute("jid", room.getRole().getRoleAddress().toString());
975
                    item.addAttribute("name", room.getNaturalLanguageName());
976 977 978

                    answer.add(item);
                }
Matt Tucker's avatar
Matt Tucker committed
979 980 981 982 983
            }
        }
        else if (name != null && node == null) {
            // Answer the room occupants as items if that info is publicly available
            MUCRoom room = getChatRoom(name);
984
            if (room != null && canDiscoverRoom(room)) {
Matt Tucker's avatar
Matt Tucker committed
985
                Element item;
986
                for (MUCRole role : room.getOccupants()) {
987
                    // TODO Should we filter occupants that are invisible (presence is not broadcasted)?
Matt Tucker's avatar
Matt Tucker committed
988
                    item = DocumentHelper.createElement("item");
989
                    item.addAttribute("jid", role.getRoleAddress().toString());
Matt Tucker's avatar
Matt Tucker committed
990 991 992 993 994 995 996

                    answer.add(item);
                }
            }
        }
        return answer.iterator();
    }
997

998 999 1000 1001 1002 1003 1004 1005
    private boolean canDiscoverRoom(MUCRoom room) {
        // Check if locked rooms may be discovered
        if (!allowToDiscoverLockedRooms && room.isLocked()) {
            return false;
        }
        return room.isPublicRoom();
    }

1006 1007 1008 1009 1010 1011 1012
    /**
     * 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) {
1013
        StringBuilder buf = new StringBuilder();
1014 1015 1016 1017 1018 1019 1020 1021 1022
        for (int i=0; i<array.length; i++) {
            buf.append(array[i]);
            if (i != array.length-1) {
                buf.append(",");
            }
        }
        return buf.toString();
    }
}