com.adspore.splat.xep0060.PubSubService.java Source code

Java tutorial

Introduction

Here is the source code for com.adspore.splat.xep0060.PubSubService.java

Source

/**
 * $RCSfile: $
 * $Revision: $
 * $Date: $
 *
 * Copyright (C) 2005-2008 Jive Software. All rights reserved.
 *
 * 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.
 */

package com.adspore.splat.xep0060;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.*;

import org.dom4j.Element;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xmpp.forms.DataForm;
import org.xmpp.packet.IQ;
import org.xmpp.packet.JID;
import org.xmpp.packet.Message;
import org.xmpp.packet.Packet;

import com.adspore.contracts.IContext;
import com.adspore.contracts.IProperties;
import com.adspore.splat.SplatComponent;
import com.adspore.splat.SplatConstants;
import com.adspore.splat.xep0030.DiscoInfoProvider;
import com.adspore.splat.xep0030.DiscoItem;
import com.adspore.splat.xep0030.DiscoItemsProvider;
import com.adspore.splat.xep0060.models.AccessModel;
import com.adspore.splat.xep0060.models.PublisherModel;
import com.adspore.util.StringUtils;

public class PubSubService implements DiscoInfoProvider, DiscoItemsProvider {
    private static final Logger LOG = LoggerFactory.getLogger(PubSubService.class);

    protected final IContext mContext;
    protected final SplatComponent mComponent;

    protected CollectionNode mRootCollectionNode = null;
    protected Map<String, Node> mNodes = new HashMap<String, Node>();

    protected static final Set<Element> mServiceIdentities = new CopyOnWriteArraySet<Element>();
    protected static final Set<String> mServiceFeatures = new CopyOnWriteArraySet<String>();

    private ScheduledExecutorService mPublisherThreadPool; // =
    /**
     * 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.
     */
    protected boolean mNodeCreationRestricted = false;

    /**
     * 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.
     */
    protected boolean mMultipleSubscriptionsEnabled = true;

    /**
     * Bare jids of users that are allowed to create nodes. An empty list means that anyone can
     * create nodes.
     */
    protected Collection<String> allowedToCreate = new CopyOnWriteArrayList<String>();

    /**
     * Default configuration to use for newly created leaf nodes.
     */
    protected static DefaultNodeConfiguration mDefaultLeafConfig;

    /**
     * Default configuration to use for newly created collection nodes.
     */
    protected static DefaultNodeConfiguration mDefaultCollectionConfig;

    /**
     * Flag that indicates if the service is enabled.
     */
    protected boolean mServiceEnabled = true;

    static {
        // Create and save default configuration for leaf nodes;
        mDefaultLeafConfig = new DefaultNodeConfiguration(true);
        mDefaultLeafConfig.setAccessModel(AccessModel.open);
        mDefaultLeafConfig.setPublisherModel(PublisherModel.publishers);
        mDefaultLeafConfig.setDeliverPayloads(true);
        mDefaultLeafConfig.setLanguage("English");
        mDefaultLeafConfig.setMaxPayloadSize(5120);
        mDefaultLeafConfig.setNotifyConfigChanges(true);
        mDefaultLeafConfig.setNotifyDelete(true);
        mDefaultLeafConfig.setNotifyRetract(true);
        mDefaultLeafConfig.setPersistPublishedItems(false);
        mDefaultLeafConfig.setMaxPublishedItems(-1);
        mDefaultLeafConfig.setPresenceBasedDelivery(false);
        mDefaultLeafConfig.setSendItemSubscribe(true);
        mDefaultLeafConfig.setSubscriptionEnabled(true);
        mDefaultLeafConfig.setReplyPolicy(null);

        // Create and save default configuration for collection nodes;
        mDefaultCollectionConfig = new DefaultNodeConfiguration(false);
        mDefaultCollectionConfig.setAccessModel(AccessModel.open);
        mDefaultCollectionConfig.setPublisherModel(PublisherModel.publishers);
        mDefaultCollectionConfig.setDeliverPayloads(true);
        mDefaultCollectionConfig.setLanguage("English");
        mDefaultCollectionConfig.setNotifyConfigChanges(true);
        mDefaultCollectionConfig.setNotifyDelete(true);
        mDefaultCollectionConfig.setNotifyRetract(true);
        mDefaultCollectionConfig.setPresenceBasedDelivery(false);
        mDefaultCollectionConfig.setSubscriptionEnabled(true);
        mDefaultCollectionConfig.setReplyPolicy(null);
        mDefaultCollectionConfig.setAssociationPolicy(CollectionNode.LeafNodeAssociationPolicy.all);
        mDefaultCollectionConfig.setMaxLeafNodes(-1);

        //  TODO:  Figure out if a service can have more than one identity object
        //   <identity>
        //Element toAdd = DocumentHelper.createElement("identity");
        //toAdd.addAttribute("category", "pubsub");
        //toAdd.addAttribute("type", "service");
        //mServiceIdentities.add(toAdd);

        //   <feature>
        mServiceFeatures.add("http://jabber.org/protocol/pubsub");
        // Collection nodes are supported
        mServiceFeatures.add("http://jabber.org/protocol/pubsub#collections");
        // Configuration of node options is supported
        mServiceFeatures.add("http://jabber.org/protocol/pubsub#config-node");
        // Simultaneous creation and configuration of nodes is supported
        mServiceFeatures.add("http://jabber.org/protocol/pubsub#create-and-configure");
        // Creation of nodes is supported
        mServiceFeatures.add("http://jabber.org/protocol/pubsub#create-nodes");
        // Deletion of nodes is supported
        mServiceFeatures.add("http://jabber.org/protocol/pubsub#delete-nodes");
        // Retrieval of pending subscription approvals is supported
        mServiceFeatures.add("http://jabber.org/protocol/pubsub#get-pending");
        // Creation of instant nodes is supported
        mServiceFeatures.add("http://jabber.org/protocol/pubsub#instant-nodes");
        // Publishers may specify item identifiers
        mServiceFeatures.add("http://jabber.org/protocol/pubsub#item-ids");

        // TODO Time-based subscriptions are supported (clean up thread missing, rest is supported)
        //features.add("http://jabber.org/protocol/pubsub#leased-subscription");
        // Node meta-data is supported
        mServiceFeatures.add("http://jabber.org/protocol/pubsub#meta-data");
        // Node owners may modify affiliations
        mServiceFeatures.add("http://jabber.org/protocol/pubsub#modify-affiliations");
        // Node owners may manage subscriptions.
        mServiceFeatures.add("http://jabber.org/protocol/pubsub#manage-subscriptions");
        // A single entity may subscribe to a node multiple times
        mServiceFeatures.add("http://jabber.org/protocol/pubsub#multi-subscribe");
        // The outcast affiliation is supported
        mServiceFeatures.add("http://jabber.org/protocol/pubsub#outcast-affiliation");
        // Persistent items are supported
        mServiceFeatures.add("http://jabber.org/protocol/pubsub#persistent-items");
        // Presence-based delivery of event notifications is supported
        mServiceFeatures.add("http://jabber.org/protocol/pubsub#presence-notifications");
        // Publishing items is supported (note: not valid for collection nodes)
        mServiceFeatures.add("http://jabber.org/protocol/pubsub#publish");
        // The publisher affiliation is supported
        mServiceFeatures.add("http://jabber.org/protocol/pubsub#publisher-affiliation");
        // Purging of nodes is supported
        mServiceFeatures.add("http://jabber.org/protocol/pubsub#purge-nodes");
        // Item retraction is supported
        mServiceFeatures.add("http://jabber.org/protocol/pubsub#retract-items");
        // Retrieval of current affiliations is supported
        mServiceFeatures.add("http://jabber.org/protocol/pubsub#retrieve-affiliations");
        // Retrieval of default node configuration is supported.
        mServiceFeatures.add("http://jabber.org/protocol/pubsub#retrieve-default");
        // Item retrieval is supported
        mServiceFeatures.add("http://jabber.org/protocol/pubsub#retrieve-items");
        // Retrieval of current subscriptions is supported.
        mServiceFeatures.add("http://jabber.org/protocol/pubsub#retrieve-subscriptions");
        // Subscribing and unsubscribing are supported
        mServiceFeatures.add("http://jabber.org/protocol/pubsub#subscribe");
        // Configuration of subscription options is supported
        mServiceFeatures.add("http://jabber.org/protocol/pubsub#subscription-options");
        // Default access model for nodes created on the service
        String modelName = getDefaultNodeConfiguration(true).getAccessModel().getName();
        mServiceFeatures.add("http://jabber.org/protocol/pubsub#default_access_model_" + modelName);
    }

    /**
     * Prevent users from creating without proper dependency injection.
     */
    @SuppressWarnings("unused")
    private PubSubService() {
        mContext = null;
        mComponent = null;
    }

    /**
     * Creates a new PubSub module suitable for use with the SplatComponent
     * @param context
     * @param splatComponent
     */
    public PubSubService(IContext context, SplatComponent splatComponent) {
        mContext = context;
        mComponent = splatComponent;
    }

    /**
     * Performs post construction initialization of the PubSub Service, should be called after the component has connected
     * to the server, as it requires the component's JID and domain to be initialized.
     */
    public void initialize() {
        loadProperties();
        loadDefaultProviders();
        createDefaultNodes();
    }

    /**
     * PROPERTIES
     */
    private void loadProperties() {
        final IProperties props = mContext.getContextProperties();

        //  Setup the publishing thread pool, worker threads responsible for publishing node items to listeners.
        int publisherThreadCount = props.getIntProp(SplatConstants.SPLAT_COMPONENT_PUBSUB_THREAD_COUNT, 10);
        LOG.info("#initialize()... Publisher Threadpool size:" + publisherThreadCount);
        mPublisherThreadPool = new ScheduledThreadPoolExecutor(publisherThreadCount,
                new NamedThreadFactory("Splat:PubSub"));
        ;

        // Load the list of JIDs that are allowed to create nodes
        String creators = props.getStringProp(SplatConstants.SPLAT_COMPONENT_PUBSUB_CREATORS, "");
        String[] jids;
        if (creators != null) {
            jids = creators.split(",");
            for (String jid : jids) {
                final String jidToAdd = jid.trim().toLowerCase();
                allowedToCreate.add(jidToAdd);
                LOG.info("#initialize()... Added admin JID: {}", jidToAdd);
            }
        }

        mMultipleSubscriptionsEnabled = props
                .getBooleanProp(SplatConstants.SPLAT_COMPONENT_PUBSUB_MULTI_SUBSCRIPTION_ENABLED, true);
        LOG.info("#initialize()... Multiple subscriptions enabled: {}", mMultipleSubscriptionsEnabled);

        mNodeCreationRestricted = props
                .getBooleanProp(SplatConstants.SPLAT_COMPONENT_PUBSUB_NODE_CREATION_RESTRICTED, false);
        LOG.info("#initialize()... Node creation restricted: {}", mNodeCreationRestricted);
    }

    /**
     * PROVIDERS
     */
    private void loadDefaultProviders() {
        //   Set Handlers..
        mComponent.getDiscoInfoHandler().setProvider(mComponent.getJID().toBareJID(), this);
        mComponent.getDiscoItemsHandler().setProvider(mComponent.getJID().toBareJID(), this);
    }

    private void createDefaultNodes() {
        // Create root collection node
        final JID componentJID = mComponent.getJID();
        mRootCollectionNode = new CollectionNode(this, null, "", componentJID);

        // Add the creator as the node owner
        mRootCollectionNode.addOwner(componentJID);
    }

    public boolean canCreateNode(JID creator) {
        // Node creation is always allowed for sysadmin
        if (isNodeCreationRestricted() && !isServiceAdmin(creator)) {
            return false;
        }
        return true;
    }

    @Override
    public boolean supportsFeature(String featureNamespace) {
        return mServiceFeatures.contains(featureNamespace);
    }

    public boolean isServiceAdmin(JID user) {
        return mComponent.getSysadmins().contains(user.toBareJID()) || allowedToCreate.contains(user.toBareJID());
    }

    public boolean isInstantNodeSupported() {
        return true;
    }

    public boolean isCollectionNodesSupported() {
        return true;
    }

    public CollectionNode getRootCollectionNode() {
        return mRootCollectionNode;
    }

    public static DefaultNodeConfiguration getDefaultNodeConfiguration(boolean leafType) {
        if (leafType) {
            return mDefaultLeafConfig;
        }
        return mDefaultCollectionConfig;
    }

    public Collection<String> getShowPresences(JID subscriber) {
        return PubSubEngine.getShowPresences(this, subscriber);
    }

    public void presenceSubscriptionNotRequired(Node node, JID user) {
        PubSubEngine.presenceSubscriptionNotRequired(this, node, user);
    }

    public void presenceSubscriptionRequired(Node node, JID user) {
        PubSubEngine.presenceSubscriptionRequired(this, node, user);
    }

    public JID getAddress() {
        return mComponent.getJID();
    }

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

    public boolean isNodeCreationRestricted() {
        return mNodeCreationRestricted;
    }

    public boolean isMultipleSubscriptionsEnabled() {
        return mMultipleSubscriptionsEnabled;
    }

    public void setNodeCreationRestricted(boolean nodeCreationRestricted) {
        this.mNodeCreationRestricted = nodeCreationRestricted;
    }

    public void addUserAllowedToCreate(String userJID) {
        // Update the list of allowed JIDs to create nodes.
        allowedToCreate.add(userJID.trim().toLowerCase());
    }

    public void removeUserAllowedToCreate(String userJID) {
        // Update the list of allowed JIDs to create nodes.
        allowedToCreate.remove(userJID.trim().toLowerCase());
    }

    public Future<String> submitTask(Callable<String> task) {
        return mPublisherThreadPool.submit(task);
    }

    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);
            message.setID(node.getNodeID() + "__" + jid.toBareJID() + "__" + StringUtils.randomString(5));
            send(message);
        }
    }

    public void sendNotification(Node node, Message message, JID jid) {
        message.setFrom(getAddress());
        message.setTo(jid);
        message.setID(node.getNodeID() + "__" + jid.toBareJID() + "__" + StringUtils.randomString(5));
        send(message);
    }

    private boolean canDiscoverNode(Node pubNode) {
        return true;
    }

    /**
     * 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) {
                buf.append(",");
            }
        }
        return buf.toString();
    }

    public void send(Packet packet) {
        mComponent.send(packet);
    }

    /**
     * Indicates that this module has information about the requested node.
     * The node's full path is stored in the targetJID's resource, permitting nesting.
     */
    public boolean hasInfo(JID targetJID, JID senderJID) {
        if (null == targetJID.getResource() || targetJID.getResource().equals("")
                || mNodes.containsKey(targetJID.getResource())) {
            return true;
        } else {
            return false;
        }
    }

    /**
     * Indicates that this module has items for the requested node.
     */
    public boolean hasItems(JID targetJID, JID senderJID) {
        if (null != targetJID && null == targetJID.getNode()) {
            if (null == targetJID.getResource()) {
                return !mRootCollectionNode.getNodes().isEmpty();
            } else {
                return mNodes.containsKey(targetJID.getResource());
            }
        }
        return false;
    }

    /**
     * Returns Discovery Items based on the the targetJID and the sender JID's ability
     * to discover those items.
     */
    public Iterator<DiscoItem> getItems(JID targetJID, JID senderJID) {
        List<DiscoItem> result = new ArrayList<DiscoItem>();

        if (null == targetJID.getResource()) {

            Collection<Node> rootCollection = mRootCollectionNode.getNodes();
            Iterator<Node> itor = rootCollection.iterator();
            while (itor.hasNext()) {
                Node nestedNode = itor.next();
                if (canDiscoverNode(nestedNode)) {
                    result.add(
                            new DiscoItem(mComponent.getJID(), nestedNode.getName(), nestedNode.getNodeID(), null));
                }
            }
            LOG.debug("#getItems()...ROOT: {} items returned", result.size());
        }

        else if (null != targetJID.getResource()) {
            //   Specific Node within hierarchy
            if (mNodes.containsKey(targetJID.getResource())) {
                Node targetNode = mNodes.get(targetJID.getResource());
                if (targetNode.isCollectionNode()) {
                    Collection<Node> collection = targetNode.getNodes();
                    Iterator<Node> itor = collection.iterator();
                    while (itor.hasNext()) {
                        Node nestedNode = itor.next();
                        if (canDiscoverNode(nestedNode)) {
                            result.add(new DiscoItem(mComponent.getJID(), nestedNode.getName(),
                                    nestedNode.getNodeID(), null));
                        }
                    }
                    LOG.debug("#getItems()...Target is Collection: {}, {} items returned", targetJID.getResource(),
                            result.size());
                }

                else {
                    for (PublishedItem publishedItem : targetNode.getPublishedItems()) {
                        DiscoItem toAdd = new DiscoItem(mComponent.getJID(), publishedItem.getID(), null, null);
                        result.add(toAdd);
                    }
                    LOG.debug("#getItems()...Target is Leaf: {}, {} items returned", targetJID.getResource(),
                            result.size());
                }
            }
        }
        return result.iterator();
    }

    public Iterator<Element> getIdentities(JID targetJID, JID senderJID) {
        final ArrayList<Element> identities = new ArrayList<Element>();

        if (null == targetJID.getResource()) {
            return mServiceIdentities.iterator();
        }

        else if (mNodes.containsKey(targetJID.getResource())) {
            // Answer the identity of a given node
            Node pubNode = getNode(targetJID.getResource());
            if (canDiscoverNode(pubNode)) {
                identities.add(pubNode.getIdentity());

            }
        }
        return identities.iterator();
    }

    public Iterator<String> getFeatures(JID targetJID, JID senderJID) {
        final ArrayList<String> result = new ArrayList<String>();
        if (null == targetJID.getResource()) {
            return mServiceFeatures.iterator();
        } else if (mNodes.containsKey(targetJID.getResource())) {
            result.add("http://jabber.org/protocol/pubsub");
        }
        return result.iterator();
    }

    public DataForm getExtendedInfo(JID targetJID, JID senderJID) {
        if (null != targetJID.getResource()) {
            Node pubNode = getNode(targetJID.getResource());
            if (canDiscoverNode(pubNode)) {
                return pubNode.getMetadataForm();
            }
        }
        return null;
    }

    /**
     * Direct access to the PubSub node map, returns the Node with the specified name
     * if it exists within the map, null otherwise.
     * @param nodeID
     * @return
     */
    public Node getNode(String nodeID) {
        return mNodes.get(nodeID);
    }

    /**
     * Direct access to the PubSub node map, returns all the current nodes.
     * @return
     */
    public Collection<Node> getNodes() {
        return mNodes.values();
    }

    /**
     * Direct access to the PubSub node map, adds the specified node to the tree
     * @param node
     */
    public void addNode(Node node) {
        mNodes.put(node.getNodeID(), node);
    }

    /**
     * Adds the specified node into the PubSub tree, using the provided node's nodeId as the
     * path for placement.  Warning! This operation will reset the node's parentNode to a collection
     * node indicated in the path.  If you need to maintain symantic purity of the node path, use
     * the more direct 'addNode' operation and build the subpath elements.
     * @param node
     * @return
     */
    public boolean addNodePathed(Node node) {
        String[] pathElements = node.getNodeID().split("/");

        StringBuilder sb = new StringBuilder(); //   Used to rebuild the path, element by element

        CollectionNode currentNode = mRootCollectionNode;
        CollectionNode parentNode = null;

        for (int inx = 0; (inx < pathElements.length - 1); inx++) {
            sb.append(pathElements[inx]);
            parentNode = currentNode;

            try {
                currentNode = (CollectionNode) mNodes.get(sb.toString());
            } catch (ClassCastException e) {
                LOG.error(
                        "Namespace collision when adding pathed node!  Existing leaf prevents new collection node at: {}",
                        sb.toString());
                return false;
            }

            if (null == currentNode) {
                LOG.debug("Adding new collection node to build up path: {}", sb.toString());
                currentNode = new CollectionNode(this, parentNode, sb.toString(), getAddress());
                addNode(currentNode);
                if (null != parentNode) {
                    parentNode.addChildNode(currentNode);
                }
            }
            sb.append("/").toString();
        }
        mNodes.put(node.getNodeID(), node);
        currentNode.addChildNode(node);
        node.changeParent(currentNode);
        return true;
    }

    /**
     * Direct access to the PubSub node map, removes the specified Node.
     * @param nodeID
     */
    public void removeNode(String nodeID) {
        mNodes.remove(nodeID);
    }

    /**
     * Processes the IQ internally within the PubSub engine, just as if it had been
     * received from the incoming IQ handler within the AbstractComponent.  Permits
     * the caller to block on the result rather than sending the message out/back
     * to the component and have it handled on a separate thread.
     * @param toProcess
     * @return
     */
    public IQ sendLocal(IQ toProcess) {
        return PubSubEngine.process(this, toProcess);
    }

    /**
     *  Helper method for creating named threads rather than anonymous threads, permits better debugging.
     */
    private static class NamedThreadFactory implements ThreadFactory {
        private final String mName;

        public NamedThreadFactory(String name) {
            mName = name;
        }

        @Override
        public Thread newThread(Runnable r) {
            return new Thread(r, mName);
        }
    }

}