PubSubModule.java 32 KB
Newer Older
Matt Tucker's avatar
Matt Tucker committed
1
/**
2
 * Copyright (C) 2005-2008 Jive Software. All rights reserved.
Matt Tucker's avatar
Matt Tucker committed
3
 *
4 5 6 7 8 9 10 11 12 13 14
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
Matt Tucker's avatar
Matt Tucker committed
15 16
 */

17
package org.jivesoftware.openfire.pubsub;
Matt Tucker's avatar
Matt Tucker committed
18

19 20 21 22 23 24 25 26
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;

Matt Tucker's avatar
Matt Tucker committed
27 28
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
29 30 31 32
import org.jivesoftware.openfire.PacketRouter;
import org.jivesoftware.openfire.RoutableChannelHandler;
import org.jivesoftware.openfire.RoutingTable;
import org.jivesoftware.openfire.XMPPServer;
33
import org.jivesoftware.openfire.commands.AdHocCommandManager;
34 35
import org.jivesoftware.openfire.component.InternalComponentManager;
import org.jivesoftware.openfire.container.BasicModule;
36 37 38 39
import org.jivesoftware.openfire.disco.DiscoInfoProvider;
import org.jivesoftware.openfire.disco.DiscoItem;
import org.jivesoftware.openfire.disco.DiscoItemsProvider;
import org.jivesoftware.openfire.disco.DiscoServerItem;
40 41
import org.jivesoftware.openfire.disco.IQDiscoInfoHandler;
import org.jivesoftware.openfire.disco.IQDiscoItemsHandler;
42
import org.jivesoftware.openfire.disco.ServerItemsProvider;
43 44
import org.jivesoftware.openfire.pubsub.models.AccessModel;
import org.jivesoftware.openfire.pubsub.models.PublisherModel;
45 46 47 48 49 50 51
import org.jivesoftware.util.JiveGlobals;
import org.jivesoftware.util.LocaleUtils;
import org.jivesoftware.util.PropertyEventDispatcher;
import org.jivesoftware.util.PropertyEventListener;
import org.jivesoftware.util.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
52
import org.xmpp.forms.DataForm;
53 54 55 56 57 58
import org.xmpp.packet.IQ;
import org.xmpp.packet.JID;
import org.xmpp.packet.Message;
import org.xmpp.packet.Packet;
import org.xmpp.packet.PacketError;
import org.xmpp.packet.Presence;
Matt Tucker's avatar
Matt Tucker committed
59 60 61 62 63 64 65 66

/**
 * Module that implements JEP-60: Publish-Subscribe. By default node collections and
 * instant nodes are supported.
 *
 * @author Matt Tucker
 */
public class PubSubModule extends BasicModule implements ServerItemsProvider, DiscoInfoProvider,
67
        DiscoItemsProvider, RoutableChannelHandler, PubSubService, PropertyEventListener {
Matt Tucker's avatar
Matt Tucker committed
68

69 70
	private static final Logger Log = LoggerFactory.getLogger(PubSubModule.class);

Matt Tucker's avatar
Matt Tucker committed
71 72 73 74 75 76 77 78 79 80 81 82 83
    /**
     * the chat service's hostname
     */
    private String serviceName = null;

    /**
     * Collection node that acts as the root node of the entire node hierarchy.
     */
    private CollectionNode rootCollectionNode = null;

    /**
     * Nodes managed by this manager, table: key nodeID (String); value Node
     */
84
    private Map<String, Node> nodes = new ConcurrentHashMap<>();
85 86 87 88 89 90 91 92 93
    
    /**
     * Keep a registry of the presence's show value of users that subscribed to a node of
     * the pubsub service and for which the node only delivers notifications for online users
     * or node subscriptions deliver events based on the user presence show value. Offline
     * users will not have an entry in the map. Note: Key-> bare JID and Value-> Map whose key
     * is full JID of connected resource and value is show value of the last received presence.
     */
    private Map<String, Map<String, String>> barePresences =
94
            new ConcurrentHashMap<>();
95 96 97 98 99 100
    
    /**
     * Manager that keeps the list of ad-hoc commands and processing command requests.
     */
    private AdHocCommandManager manager;
    
Matt Tucker's avatar
Matt Tucker committed
101 102 103 104 105 106 107
    /**
     * Returns the permission policy for creating nodes. A true value means that not anyone can
     * create a node, only the JIDs listed in <code>allowedToCreate</code> are allowed to create
     * nodes.
     */
    private boolean nodeCreationRestricted = false;

108 109 110 111 112 113 114
    /**
     * Flag that indicates if a user may have more than one subscription with the node. When multiple
     * subscriptions is enabled each subscription request, event notification and unsubscription request
     * should include a subid attribute.
     */
    private boolean multipleSubscriptionsEnabled = true;

Matt Tucker's avatar
Matt Tucker committed
115 116 117 118
    /**
     * Bare jids of users that are allowed to create nodes. An empty list means that anyone can
     * create nodes.
     */
119
    private Collection<String> allowedToCreate = new CopyOnWriteArrayList<>();
Matt Tucker's avatar
Matt Tucker committed
120 121 122 123 124

    /**
     * Bare jids of users that are system administrators of the PubSub service. A sysadmin
     * has the same permissions as a node owner.
     */
125
    private Collection<String> sysadmins = new CopyOnWriteArrayList<>();
Matt Tucker's avatar
Matt Tucker committed
126 127 128 129 130 131 132 133

    /**
     * The packet router for the server.
     */
    private PacketRouter router = null;

    private RoutingTable routingTable = null;

134 135 136 137 138 139 140 141 142
    /**
     * The disco info handler for this module
     */
    private IQDiscoInfoHandler iqDiscoInfoHandler = null;
   /**
    * The disco items handler for this module
    */ 
    private IQDiscoItemsHandler iqDiscoItemsHandler = null;

Matt Tucker's avatar
Matt Tucker committed
143 144 145 146 147 148 149 150 151 152 153 154 155 156
    /**
     * Default configuration to use for newly created leaf nodes.
     */
    private DefaultNodeConfiguration leafDefaultConfiguration;
    /**
     * Default configuration to use for newly created collection nodes.
     */
    private DefaultNodeConfiguration collectionDefaultConfiguration;

    /**
     * Private component that actually performs the pubsub work.
     */
    private PubSubEngine engine = null;

157 158 159 160 161
    /**
     * Flag that indicates if the service is enabled.
     */
    private boolean serviceEnabled = true;

Matt Tucker's avatar
Matt Tucker committed
162 163
    public PubSubModule() {
        super("Publish Subscribe Service");
164 165 166 167
        
        // Initialize the ad-hoc commands manager to use for this pubsub service
        manager = new AdHocCommandManager();
        manager.addCommand(new PendingSubscriptionsCommand(this));
Matt Tucker's avatar
Matt Tucker committed
168 169
    }

170
    @Override
Matt Tucker's avatar
Matt Tucker committed
171 172 173 174
    public void process(Packet packet) {
        try {
            // Check if the packet is a disco request or a packet with namespace iq:register
            if (packet instanceof IQ) {
175
                if (!engine.process(this, (IQ) packet)) {
Matt Tucker's avatar
Matt Tucker committed
176 177 178 179
                    process((IQ) packet);
                }
            }
            else if (packet instanceof Presence) {
180
                engine.process(this, (Presence) packet);
Matt Tucker's avatar
Matt Tucker committed
181 182
            }
            else {
183
                engine.process(this, (Message) packet);
Matt Tucker's avatar
Matt Tucker committed
184 185 186 187 188 189 190 191 192 193 194 195 196
            }
        }
        catch (Exception e) {
            Log.error(LocaleUtils.getLocalizedString("admin.error"), e);
            if (packet instanceof IQ) {
                // Send internal server error
                IQ reply = IQ.createResultIQ((IQ) packet);
                reply.setError(PacketError.Condition.internal_server_error);
                send(reply);
            }
        }
    }

197 198 199 200
    private void sendServiceUnavailablePacket(IQ iq) {
        engine.sendErrorPacket(iq, PacketError.Condition.service_unavailable, null);
    }

Matt Tucker's avatar
Matt Tucker committed
201 202 203 204 205 206 207 208 209 210 211 212
    private void process(IQ iq) {
        // Ignore IQs of type ERROR
        if (IQ.Type.error == iq.getType()) {
            return;
        }
        Element childElement = iq.getChildElement();
        String namespace = null;

        if (childElement != null) {
            namespace = childElement.getNamespaceURI();
        }
        if ("http://jabber.org/protocol/disco#info".equals(namespace)) {
213 214 215 216 217 218 219
            if (iqDiscoInfoHandler != null) {
                IQ reply = iqDiscoInfoHandler.handleIQ(iq);
                router.route(reply);
            } else {
                sendServiceUnavailablePacket(iq);
                return;
            }
Matt Tucker's avatar
Matt Tucker committed
220 221
        }
        else if ("http://jabber.org/protocol/disco#items".equals(namespace)) {
222 223 224 225 226 227 228 229
            if (iqDiscoItemsHandler != null) {
                IQ reply = iqDiscoItemsHandler.handleIQ(iq);
                router.route(reply);
            } else {
                sendServiceUnavailablePacket(iq);
                return;
            }
            
Matt Tucker's avatar
Matt Tucker committed
230 231
        }
        else {
Matt Tucker's avatar
Matt Tucker committed
232
            // Unknown namespace requested so return error to sender
233
            sendServiceUnavailablePacket(iq);
Matt Tucker's avatar
Matt Tucker committed
234 235 236
        }
    }

237
    @Override
Matt Tucker's avatar
Matt Tucker committed
238 239 240 241
    public String getServiceID() {
        return "pubsub";
    }

242
    @Override
Matt Tucker's avatar
Matt Tucker committed
243 244 245 246 247 248 249 250 251
    public boolean canCreateNode(JID creator) {
        // Node creation is always allowed for sysadmin
        if (isNodeCreationRestricted() && !isServiceAdmin(creator)) {
            // The user is not allowed to create nodes
            return false;
        }
        return true;
    }

252
    @Override
Matt Tucker's avatar
Matt Tucker committed
253
    public boolean isServiceAdmin(JID user) {
254
        return sysadmins.contains(user.toBareJID()) || allowedToCreate.contains(user.toBareJID()) ||
255
                InternalComponentManager.getInstance().hasComponent(user);
Matt Tucker's avatar
Matt Tucker committed
256 257
    }

258
    @Override
Matt Tucker's avatar
Matt Tucker committed
259 260 261 262
    public boolean isInstantNodeSupported() {
        return true;
    }

263
    @Override
Matt Tucker's avatar
Matt Tucker committed
264 265 266 267
    public boolean isCollectionNodesSupported() {
        return true;
    }

268
    @Override
Matt Tucker's avatar
Matt Tucker committed
269 270 271 272
    public CollectionNode getRootCollectionNode() {
        return rootCollectionNode;
    }

273
    @Override
Matt Tucker's avatar
Matt Tucker committed
274 275 276 277 278 279 280
    public DefaultNodeConfiguration getDefaultNodeConfiguration(boolean leafType) {
        if (leafType) {
            return leafDefaultConfiguration;
        }
        return collectionDefaultConfiguration;
    }

281
    @Override
282
    public Collection<String> getShowPresences(JID subscriber) {
283
        return PubSubEngine.getShowPresences(this, subscriber);
284 285
    }

286
    @Override
287
    public void presenceSubscriptionNotRequired(Node node, JID user) {
288
        PubSubEngine.presenceSubscriptionNotRequired(this, node, user);
289 290
    }

291
    @Override
292
    public void presenceSubscriptionRequired(Node node, JID user) {
293
        PubSubEngine.presenceSubscriptionRequired(this, node, user);
Matt Tucker's avatar
Matt Tucker committed
294 295 296 297 298 299 300
    }

    public String getServiceName() {
        return serviceName;
    }

    public String getServiceDomain() {
301
        return serviceName + "." + XMPPServer.getInstance().getServerInfo().getXMPPDomain();
Matt Tucker's avatar
Matt Tucker committed
302 303
    }

304
    @Override
Matt Tucker's avatar
Matt Tucker committed
305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321
    public JID getAddress() {
        // TODO Cache this JID for performance?
        return new JID(null, getServiceDomain(), null);
    }

    public Collection<String> getUsersAllowedToCreate() {
        return allowedToCreate;
    }

    public Collection<String> getSysadmins() {
        return sysadmins;
    }

    public void addSysadmin(String userJID) {
        sysadmins.add(userJID.trim().toLowerCase());
        // Update the config.
        String[] jids = new String[sysadmins.size()];
322
        jids = sysadmins.toArray(jids);
Matt Tucker's avatar
Matt Tucker committed
323 324 325 326 327 328 329
        JiveGlobals.setProperty("xmpp.pubsub.sysadmin.jid", fromArray(jids));
    }

    public void removeSysadmin(String userJID) {
        sysadmins.remove(userJID.trim().toLowerCase());
        // Update the config.
        String[] jids = new String[sysadmins.size()];
330
        jids = sysadmins.toArray(jids);
Matt Tucker's avatar
Matt Tucker committed
331 332 333 334 335 336 337
        JiveGlobals.setProperty("xmpp.pubsub.sysadmin.jid", fromArray(jids));
    }

    public boolean isNodeCreationRestricted() {
        return nodeCreationRestricted;
    }

338
    @Override
339 340 341 342
    public boolean isMultipleSubscriptionsEnabled() {
        return multipleSubscriptionsEnabled;
    }

Matt Tucker's avatar
Matt Tucker committed
343 344 345 346 347 348 349 350 351 352
    public void setNodeCreationRestricted(boolean nodeCreationRestricted) {
        this.nodeCreationRestricted = nodeCreationRestricted;
        JiveGlobals.setProperty("xmpp.pubsub.create.anyone", Boolean.toString(nodeCreationRestricted));
    }

    public void addUserAllowedToCreate(String userJID) {
        // Update the list of allowed JIDs to create nodes.
        allowedToCreate.add(userJID.trim().toLowerCase());
        // Update the config.
        String[] jids = new String[allowedToCreate.size()];
353
        jids = allowedToCreate.toArray(jids);
Matt Tucker's avatar
Matt Tucker committed
354 355 356 357 358 359 360 361
        JiveGlobals.setProperty("xmpp.pubsub.create.jid", fromArray(jids));
    }

    public void removeUserAllowedToCreate(String userJID) {
        // Update the list of allowed JIDs to create nodes.
        allowedToCreate.remove(userJID.trim().toLowerCase());
        // Update the config.
        String[] jids = new String[allowedToCreate.size()];
362
        jids = allowedToCreate.toArray(jids);
Matt Tucker's avatar
Matt Tucker committed
363 364 365
        JiveGlobals.setProperty("xmpp.pubsub.create.jid", fromArray(jids));
    }

366 367
    @Override
	public void initialize(XMPPServer server) {
Matt Tucker's avatar
Matt Tucker committed
368
        super.initialize(server);
369 370 371 372 373 374
        
        JiveGlobals.migrateProperty("xmpp.pubsub.enabled");
        JiveGlobals.migrateProperty("xmpp.pubsub.service");
        JiveGlobals.migrateProperty("xmpp.pubsub.root.nodeID");
        JiveGlobals.migrateProperty("xmpp.pubsub.root.creator");
        JiveGlobals.migrateProperty("xmpp.pubsub.multiple-subscriptions");
Matt Tucker's avatar
Matt Tucker committed
375

376 377 378
        // Listen to property events so that the template is always up to date
        PropertyEventDispatcher.addListener(this);

379 380
        setIQDiscoItemsHandler(XMPPServer.getInstance().getIQDiscoItemsHandler());
        setIQDiscoInfoHandler(XMPPServer.getInstance().getIQDiscoInfoHandler());
381
        serviceEnabled = JiveGlobals.getBooleanProperty("xmpp.pubsub.enabled", true);
Matt Tucker's avatar
Matt Tucker committed
382 383 384 385 386 387 388 389 390
        serviceName = JiveGlobals.getProperty("xmpp.pubsub.service");
        if (serviceName == null) {
            serviceName = "pubsub";
        }
        // Load the list of JIDs that are sysadmins of the PubSub service
        String property = JiveGlobals.getProperty("xmpp.pubsub.sysadmin.jid");
        String[] jids;
        if (property != null) {
            jids = property.split(",");
Gaston Dombiak's avatar
Gaston Dombiak committed
391 392
            for (String jid : jids) {
                sysadmins.add(jid.trim().toLowerCase());
Matt Tucker's avatar
Matt Tucker committed
393 394
            }
        }
395
        nodeCreationRestricted = JiveGlobals.getBooleanProperty("xmpp.pubsub.create.anyone", false);
Matt Tucker's avatar
Matt Tucker committed
396 397 398 399
        // Load the list of JIDs that are allowed to create nodes
        property = JiveGlobals.getProperty("xmpp.pubsub.create.jid");
        if (property != null) {
            jids = property.split(",");
Gaston Dombiak's avatar
Gaston Dombiak committed
400 401
            for (String jid : jids) {
                allowedToCreate.add(jid.trim().toLowerCase());
Matt Tucker's avatar
Matt Tucker committed
402 403 404
            }
        }

405 406
        multipleSubscriptionsEnabled = JiveGlobals.getBooleanProperty("xmpp.pubsub.multiple-subscriptions", true);

Matt Tucker's avatar
Matt Tucker committed
407 408 409
        routingTable = server.getRoutingTable();
        router = server.getPacketRouter();

410
        engine = new PubSubEngine(router);
Matt Tucker's avatar
Matt Tucker committed
411 412 413 414 415 416 417 418

        // Load default configuration for leaf nodes
        leafDefaultConfiguration = PubSubPersistenceManager.loadDefaultConfiguration(this, true);
        if (leafDefaultConfiguration == null) {
            // Create and save default configuration for leaf nodes;
            leafDefaultConfiguration = new DefaultNodeConfiguration(true);
            leafDefaultConfiguration.setAccessModel(AccessModel.open);
            leafDefaultConfiguration.setPublisherModel(PublisherModel.publishers);
Gaston Dombiak's avatar
Gaston Dombiak committed
419 420 421 422 423 424 425
            leafDefaultConfiguration.setDeliverPayloads(true);
            leafDefaultConfiguration.setLanguage("English");
            leafDefaultConfiguration.setMaxPayloadSize(5120);
            leafDefaultConfiguration.setNotifyConfigChanges(true);
            leafDefaultConfiguration.setNotifyDelete(true);
            leafDefaultConfiguration.setNotifyRetract(true);
            leafDefaultConfiguration.setPersistPublishedItems(false);
426
            leafDefaultConfiguration.setMaxPublishedItems(1);
Gaston Dombiak's avatar
Gaston Dombiak committed
427 428 429
            leafDefaultConfiguration.setPresenceBasedDelivery(false);
            leafDefaultConfiguration.setSendItemSubscribe(true);
            leafDefaultConfiguration.setSubscriptionEnabled(true);
Matt Tucker's avatar
Matt Tucker committed
430 431 432 433 434 435 436 437 438 439 440
            leafDefaultConfiguration.setReplyPolicy(null);
            PubSubPersistenceManager.createDefaultConfiguration(this, leafDefaultConfiguration);
        }
        // Load default configuration for collection nodes
        collectionDefaultConfiguration =
                PubSubPersistenceManager.loadDefaultConfiguration(this, false);
        if (collectionDefaultConfiguration == null ) {
            // Create and save default configuration for collection nodes;
            collectionDefaultConfiguration = new DefaultNodeConfiguration(false);
            collectionDefaultConfiguration.setAccessModel(AccessModel.open);
            collectionDefaultConfiguration.setPublisherModel(PublisherModel.publishers);
Gaston Dombiak's avatar
Gaston Dombiak committed
441 442 443 444 445 446 447 448 449
            collectionDefaultConfiguration.setDeliverPayloads(false);
            collectionDefaultConfiguration.setLanguage("English");
            collectionDefaultConfiguration.setNotifyConfigChanges(true);
            collectionDefaultConfiguration.setNotifyDelete(true);
            collectionDefaultConfiguration.setNotifyRetract(true);
            collectionDefaultConfiguration.setPresenceBasedDelivery(false);
            collectionDefaultConfiguration.setSubscriptionEnabled(true);
            collectionDefaultConfiguration.setReplyPolicy(null);
            collectionDefaultConfiguration
Matt Tucker's avatar
Matt Tucker committed
450
                    .setAssociationPolicy(CollectionNode.LeafNodeAssociationPolicy.all);
Gaston Dombiak's avatar
Gaston Dombiak committed
451
            collectionDefaultConfiguration.setMaxLeafNodes(-1);
Matt Tucker's avatar
Matt Tucker committed
452 453 454 455 456 457 458 459 460 461 462
            PubSubPersistenceManager
                    .createDefaultConfiguration(this, collectionDefaultConfiguration);
        }

        // Load nodes to memory
        PubSubPersistenceManager.loadNodes(this);
        // Ensure that we have a root collection node
        String rootNodeID = JiveGlobals.getProperty("xmpp.pubsub.root.nodeID", "");
        if (nodes.isEmpty()) {
            // Create root collection node
            String creator = JiveGlobals.getProperty("xmpp.pubsub.root.creator");
463 464
//            JID creatorJID = creator != null ? new JID(creator) : server.getAdmins().iterator().next();
            JID creatorJID = creator != null ? new JID(creator) : new JID(server.getServerInfo().getXMPPDomain());
Matt Tucker's avatar
Matt Tucker committed
465 466 467 468 469 470 471 472 473 474 475
            rootCollectionNode = new CollectionNode(this, null, rootNodeID, creatorJID);
            // Add the creator as the node owner
            rootCollectionNode.addOwner(creatorJID);
            // Save new root node
            rootCollectionNode.saveToDB();
        }
        else {
            rootCollectionNode = (CollectionNode) getNode(rootNodeID);
        }
    }

476 477
    @Override
	public void start() {
478 479 480 481
        // Check that the service is enabled
        if (!isServiceEnabled()) {
            return;
        }
Matt Tucker's avatar
Matt Tucker committed
482 483
        super.start();
        // Add the route to this service
Gaston Dombiak's avatar
Gaston Dombiak committed
484
        routingTable.addComponentRoute(getAddress(), this);
Matt Tucker's avatar
Matt Tucker committed
485
        // Start the pubsub engine
486
        engine.start(this);
487
        ArrayList<String> params = new ArrayList<>();
Matt Tucker's avatar
Matt Tucker committed
488 489 490 491 492
        params.clear();
        params.add(getServiceDomain());
        Log.info(LocaleUtils.getLocalizedString("startup.starting.pubsub", params));
    }

493 494
    @Override
	public void stop() {
Matt Tucker's avatar
Matt Tucker committed
495 496
        super.stop();
        // Remove the route to this service
Gaston Dombiak's avatar
Gaston Dombiak committed
497
        routingTable.removeComponentRoute(getAddress());
Matt Tucker's avatar
Matt Tucker committed
498 499
        // Stop the pubsub engine. This will gives us the chance to
        // save queued items to the database.
500
        engine.shutdown(this);
Matt Tucker's avatar
Matt Tucker committed
501 502
    }

503 504 505 506 507 508 509 510
    public void setIQDiscoItemsHandler(IQDiscoItemsHandler iqDiscoItemsHandler) {
        this.iqDiscoItemsHandler = iqDiscoItemsHandler;
    }
    
    public void setIQDiscoInfoHandler(IQDiscoInfoHandler iqDiscoInfoHandler) {
        this.iqDiscoInfoHandler = iqDiscoInfoHandler ;
    }

511 512 513 514 515 516 517
    private void enableService(boolean enabled) {
        if (serviceEnabled == enabled) {
            // Do nothing if the service status has not changed
            return;
        }
        if (!enabled) {
            // Disable disco information
518 519 520
            if (iqDiscoItemsHandler != null) {
                iqDiscoItemsHandler.removeServerItemsProvider(this);
            }
521 522 523 524 525 526 527 528
            // Stop the service/module
            stop();
        }
        serviceEnabled = enabled;
        if (enabled) {
            // Start the service/module
            start();
            // Enable disco information
529 530 531
            if (iqDiscoItemsHandler != null) {
                iqDiscoItemsHandler.addServerItemsProvider(this);
            }
532 533 534
        }
    }

535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551
    public void setServiceEnabled(boolean enabled) {
        // Enable/disable the service
        enableService(enabled);
        // Store the new setting
        JiveGlobals.setProperty("xmpp.pubsub.enabled", Boolean.toString(enabled));
    }

    /**
     * Returns true if the service is available. Use {@link #setServiceEnabled(boolean)} to
     * enable or disable the service.
     *
     * @return true if the MUC service is available.
     */
    public boolean isServiceEnabled() {
        return serviceEnabled;
    }

552 553
    public void markedAsSeniorClusterMember() {
        // Offer the service since we are the senior cluster member
554
		// enableService(true);
555 556
    }

557
    @Override
Matt Tucker's avatar
Matt Tucker committed
558
    public Iterator<DiscoServerItem> getItems() {
559 560 561 562
        // Check if the service is disabled. Info is not available when disabled.
        if (!isServiceEnabled()) {
            return null;
        }
563
        ArrayList<DiscoServerItem> items = new ArrayList<>();
564 565 566 567 568
		final DiscoServerItem item = new DiscoServerItem(new JID(
			getServiceDomain()), "Publish-Subscribe service", null, null, this,
			this);
		items.add(item);
		return items.iterator();
Matt Tucker's avatar
Matt Tucker committed
569 570
    }

571
    @Override
Matt Tucker's avatar
Matt Tucker committed
572
    public Iterator<Element> getIdentities(String name, String node, JID senderJID) {
573
        ArrayList<Element> identities = new ArrayList<>();
Matt Tucker's avatar
Matt Tucker committed
574 575 576 577 578
        if (name == null && node == null) {
            // Answer the identity of the PubSub service
            Element identity = DocumentHelper.createElement("identity");
            identity.addAttribute("category", "pubsub");
            identity.addAttribute("name", "Publish-Subscribe service");
Matt Tucker's avatar
Matt Tucker committed
579
            identity.addAttribute("type", "service");
Matt Tucker's avatar
Matt Tucker committed
580 581 582

            identities.add(identity);
        }
Matt Tucker's avatar
Matt Tucker committed
583
        else if (name == null) {
Matt Tucker's avatar
Matt Tucker committed
584 585
            // Answer the identity of a given node
            Node pubNode = getNode(node);
Matt Tucker's avatar
Matt Tucker committed
586
            if (canDiscoverNode(pubNode)) {
Matt Tucker's avatar
Matt Tucker committed
587 588 589 590 591 592 593 594 595 596
                Element identity = DocumentHelper.createElement("identity");
                identity.addAttribute("category", "pubsub");
                identity.addAttribute("type", pubNode.isCollectionNode() ? "collection" : "leaf");

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

597
    @Override
Matt Tucker's avatar
Matt Tucker committed
598
    public Iterator<String> getFeatures(String name, String node, JID senderJID) {
599
        ArrayList<String> features = new ArrayList<>();
Matt Tucker's avatar
Matt Tucker committed
600 601 602
        if (name == null && node == null) {
            // Answer the features of the PubSub service
            features.add("http://jabber.org/protocol/pubsub");
603 604 605
            // Default access model for nodes created on the service
            String modelName = getDefaultNodeConfiguration(true).getAccessModel().getName();
            features.add("http://jabber.org/protocol/pubsub#access-" + modelName);
Matt Tucker's avatar
Matt Tucker committed
606 607 608 609
            if (isCollectionNodesSupported()) {
                // Collection nodes are supported
                features.add("http://jabber.org/protocol/pubsub#collections");
            }
Matt Tucker's avatar
Matt Tucker committed
610 611
            // Configuration of node options is supported
            features.add("http://jabber.org/protocol/pubsub#config-node");
Matt Tucker's avatar
Matt Tucker committed
612 613
            // Simultaneous creation and configuration of nodes is supported
            features.add("http://jabber.org/protocol/pubsub#create-and-configure");
Matt Tucker's avatar
Matt Tucker committed
614 615 616 617
            // Creation of nodes is supported
            features.add("http://jabber.org/protocol/pubsub#create-nodes");
            // Deletion of nodes is supported
            features.add("http://jabber.org/protocol/pubsub#delete-nodes");
Matt Tucker's avatar
Matt Tucker committed
618 619
            // Retrieval of pending subscription approvals is supported
            features.add("http://jabber.org/protocol/pubsub#get-pending");
Matt Tucker's avatar
Matt Tucker committed
620 621 622 623
            if (isInstantNodeSupported()) {
                // Creation of instant nodes is supported
                features.add("http://jabber.org/protocol/pubsub#instant-nodes");
            }
Matt Tucker's avatar
Matt Tucker committed
624 625
            // Publishers may specify item identifiers
            features.add("http://jabber.org/protocol/pubsub#item-ids");
Matt Tucker's avatar
Matt Tucker committed
626
            // TODO Time-based subscriptions are supported (clean up thread missing, rest is supported)
Matt Tucker's avatar
Matt Tucker committed
627 628 629
            //features.add("http://jabber.org/protocol/pubsub#leased-subscription");
            // Node meta-data is supported
            features.add("http://jabber.org/protocol/pubsub#meta-data");
Matt Tucker's avatar
Matt Tucker committed
630 631
            // Node owners may modify affiliations
            features.add("http://jabber.org/protocol/pubsub#modify-affiliations");
632 633
            // Node owners may manage subscriptions.
            features.add("http://jabber.org/protocol/pubsub#manage-subscriptions");
Matt Tucker's avatar
Matt Tucker committed
634 635 636 637 638 639 640 641
            // A single entity may subscribe to a node multiple times
            features.add("http://jabber.org/protocol/pubsub#multi-subscribe");
            // The outcast affiliation is supported
            features.add("http://jabber.org/protocol/pubsub#outcast-affiliation");
            // Persistent items are supported
            features.add("http://jabber.org/protocol/pubsub#persistent-items");
            // Presence-based delivery of event notifications is supported
            features.add("http://jabber.org/protocol/pubsub#presence-notifications");
Matt Tucker's avatar
Matt Tucker committed
642 643
            // Publishing items is supported (note: not valid for collection nodes)
            features.add("http://jabber.org/protocol/pubsub#publish");
Matt Tucker's avatar
Matt Tucker committed
644 645 646 647 648 649 650 651
            // The publisher affiliation is supported
            features.add("http://jabber.org/protocol/pubsub#publisher-affiliation");
            // Purging of nodes is supported
            features.add("http://jabber.org/protocol/pubsub#purge-nodes");
            // Item retraction is supported
            features.add("http://jabber.org/protocol/pubsub#retract-items");
            // Retrieval of current affiliations is supported
            features.add("http://jabber.org/protocol/pubsub#retrieve-affiliations");
652 653
            // Retrieval of default node configuration is supported.
            features.add("http://jabber.org/protocol/pubsub#retrieve-default");
Matt Tucker's avatar
Matt Tucker committed
654 655
            // Item retrieval is supported
            features.add("http://jabber.org/protocol/pubsub#retrieve-items");
656 657
            // Retrieval of current subscriptions is supported.
            features.add("http://jabber.org/protocol/pubsub#retrieve-subscriptions");
Matt Tucker's avatar
Matt Tucker committed
658 659 660 661 662
            // Subscribing and unsubscribing are supported
            features.add("http://jabber.org/protocol/pubsub#subscribe");
            // Configuration of subscription options is supported
            features.add("http://jabber.org/protocol/pubsub#subscription-options");
        }
Matt Tucker's avatar
Matt Tucker committed
663
        else if (name == null) {
Matt Tucker's avatar
Matt Tucker committed
664 665
            // Answer the features of a given node
            Node pubNode = getNode(node);
Matt Tucker's avatar
Matt Tucker committed
666
            if (canDiscoverNode(pubNode)) {
Matt Tucker's avatar
Matt Tucker committed
667
                // Answer the features of the PubSub service
Matt Tucker's avatar
Matt Tucker committed
668 669 670 671 672 673
                features.add("http://jabber.org/protocol/pubsub");
            }
        }
        return features.iterator();
    }

674
    @Override
675
    public DataForm getExtendedInfo(String name, String node, JID senderJID) {
Matt Tucker's avatar
Matt Tucker committed
676 677 678
        if (name == null && node != null) {
            // Answer the extended info of a given node
            Node pubNode = getNode(node);
Matt Tucker's avatar
Matt Tucker committed
679
            if (canDiscoverNode(pubNode)) {
Matt Tucker's avatar
Matt Tucker committed
680
                // Get the metadata data form
681
                return pubNode.getMetadataForm();
Matt Tucker's avatar
Matt Tucker committed
682 683 684 685 686
            }
        }
        return null;
    }

687
    @Override
Matt Tucker's avatar
Matt Tucker committed
688
    public boolean hasInfo(String name, String node, JID senderJID) {
689 690 691 692
        // Check if the service is disabled. Info is not available when disabled.
        if (!isServiceEnabled()) {
            return false;
        }
693 694
        if (name == null && node == null) {
            // We always have info about the Pubsub service
Matt Tucker's avatar
Matt Tucker committed
695 696
            return true;
        }
Matt Tucker's avatar
Matt Tucker committed
697
        else if (name == null) {
Matt Tucker's avatar
Matt Tucker committed
698 699 700 701 702 703
            // We only have info if the node exists
            return hasNode(node);
        }
        return false;
    }

704
    @Override
705
    public Iterator<DiscoItem> getItems(String name, String node, JID senderJID) {
706 707 708 709
        // Check if the service is disabled. Info is not available when disabled.
        if (!isServiceEnabled()) {
            return null;
        }
710
        List<DiscoItem> answer = new ArrayList<>();
Matt Tucker's avatar
Matt Tucker committed
711
        String serviceDomain = getServiceDomain();
Matt Tucker's avatar
Matt Tucker committed
712
        if (name == null && node == null) {
Matt Tucker's avatar
Matt Tucker committed
713 714
            // Answer all first level nodes
            for (Node pubNode : rootCollectionNode.getNodes()) {
Matt Tucker's avatar
Matt Tucker committed
715
                if (canDiscoverNode(pubNode)) {
716 717 718
                	final DiscoItem item = new DiscoItem(
						new JID(serviceDomain), pubNode.getName(),
						pubNode.getNodeID(), null);
Matt Tucker's avatar
Matt Tucker committed
719 720 721 722
                    answer.add(item);
                }
            }
        }
Matt Tucker's avatar
Matt Tucker committed
723
        else if (name == null) {
Matt Tucker's avatar
Matt Tucker committed
724 725 726 727 728 729
            Node pubNode = getNode(node);
            if (pubNode != null && canDiscoverNode(pubNode)) {
                if (pubNode.isCollectionNode()) {
                    // Answer all nested nodes as items
                    for (Node nestedNode : pubNode.getNodes()) {
                        if (canDiscoverNode(nestedNode)) {
730 731
                        	final DiscoItem item = new DiscoItem(new JID(serviceDomain), nestedNode.getName(),
								nestedNode.getNodeID(), null);
Matt Tucker's avatar
Matt Tucker committed
732 733 734 735 736 737
                            answer.add(item);
                        }
                    }
                }
                else {
                    // This is a leaf node so answer the published items which exist on the service
738
                    for (PublishedItem publishedItem : pubNode.getPublishedItems()) {
739
                        answer.add(new DiscoItem(new JID(serviceDomain), publishedItem.getID(), null, null));
Matt Tucker's avatar
Matt Tucker committed
740 741 742
                    }
                }
            }
Matt Tucker's avatar
Matt Tucker committed
743 744 745 746
            else {
                // Answer null to indicate that specified item was not found
                return null;
            }
Matt Tucker's avatar
Matt Tucker committed
747 748 749 750
        }
        return answer.iterator();
    }

751
    @Override
Matt Tucker's avatar
Matt Tucker committed
752 753 754 755 756
    public void broadcast(Node node, Message message, Collection<JID> jids) {
        // TODO Possibly use a thread pool for sending packets (based on the jids size)
        message.setFrom(getAddress());
        for (JID jid : jids) {
            message.setTo(jid);
757
            message.setID(StringUtils.randomString(8));
Matt Tucker's avatar
Matt Tucker committed
758 759 760 761
            router.route(message);
        }
    }

762
    @Override
Matt Tucker's avatar
Matt Tucker committed
763 764 765 766
    public void send(Packet packet) {
        router.route(packet);
    }

767
    @Override
Matt Tucker's avatar
Matt Tucker committed
768 769 770
    public void sendNotification(Node node, Message message, JID jid) {
        message.setFrom(getAddress());
        message.setTo(jid);
771
        message.setID(StringUtils.randomString(8));
Matt Tucker's avatar
Matt Tucker committed
772 773 774
        router.route(message);
    }

775
    @Override
Matt Tucker's avatar
Matt Tucker committed
776 777 778 779
    public Node getNode(String nodeID) {
        return nodes.get(nodeID);
    }

780
    @Override
Matt Tucker's avatar
Matt Tucker committed
781 782 783 784 785 786 787 788
    public Collection<Node> getNodes() {
        return nodes.values();
    }

    private boolean hasNode(String nodeID) {
        return getNode(nodeID) != null;
    }

789
    @Override
Matt Tucker's avatar
Matt Tucker committed
790 791 792 793
    public void addNode(Node node) {
        nodes.put(node.getNodeID(), node);
    }

794
    @Override
Matt Tucker's avatar
Matt Tucker committed
795 796 797 798 799
    public void removeNode(String nodeID) {
        nodes.remove(nodeID);
    }

    private boolean canDiscoverNode(Node pubNode) {
Matt Tucker's avatar
Matt Tucker committed
800
        return true;
Matt Tucker's avatar
Matt Tucker committed
801 802 803 804 805 806 807 808 809 810 811 812 813
    }

    /**
     * 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) {
        StringBuilder buf = new StringBuilder();
        for (int i=0; i<array.length; i++) {
            buf.append(array[i]);
            if (i != array.length-1) {
814
                buf.append(',');
Matt Tucker's avatar
Matt Tucker committed
815 816 817 818
            }
        }
        return buf.toString();
    }
819

820
    @Override
821 822 823 824
    public Map<String, Map<String, String>> getBarePresences() {
        return barePresences;
    }

825
    @Override
826 827 828 829
    public AdHocCommandManager getManager() {
        return manager;
    }

830
    @Override
831 832 833 834 835 836 837 838
    public void propertySet(String property, Map<String, Object> params) {
        if (property.equals("xmpp.pubsub.enabled")) {
            boolean enabled = Boolean.parseBoolean((String)params.get("value"));
            // Enable/disable the service
            enableService(enabled);
        }
    }

839
    @Override
840 841 842 843 844 845 846
    public void propertyDeleted(String property, Map<String, Object> params) {
        if (property.equals("xmpp.pubsub.enabled")) {
            // Enable/disable the service
            enableService(true);
        }
    }

847
    @Override
848 849 850 851
    public void xmlPropertySet(String property, Map<String, Object> params) {
        // Do nothing
    }

852
    @Override
853 854 855
    public void xmlPropertyDeleted(String property, Map<String, Object> params) {
        // Do nothing
    }
Matt Tucker's avatar
Matt Tucker committed
856
}