com.magnet.mmx.server.plugin.mmxmgmt.handler.MMXChannelManager.java Source code

Java tutorial

Introduction

Here is the source code for com.magnet.mmx.server.plugin.mmxmgmt.handler.MMXChannelManager.java

Source

/*   Copyright (c) 2015 Magnet Systems, Inc.
 *
 *  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.magnet.mmx.server.plugin.mmxmgmt.handler;

import com.magnet.mmx.protocol.*;
import com.magnet.mmx.protocol.ChannelAction.ListType;
import com.magnet.mmx.protocol.ChannelAction.MMXPublishedItem;
import com.magnet.mmx.server.api.v1.protocol.ChannelCreateInfo;
import com.magnet.mmx.server.common.data.AppEntity;
import com.magnet.mmx.server.plugin.mmxmgmt.MMXException;
import com.magnet.mmx.server.plugin.mmxmgmt.db.*;
import com.magnet.mmx.server.plugin.mmxmgmt.pubsub.PubSubPersistenceManagerExt;
import com.magnet.mmx.server.plugin.mmxmgmt.pubsub.TopicQueryBuilder;
import com.magnet.mmx.server.plugin.mmxmgmt.search.PaginationInfo;
import com.magnet.mmx.server.plugin.mmxmgmt.topic.TopicNode;
import com.magnet.mmx.server.plugin.mmxmgmt.util.DBUtil;
import com.magnet.mmx.server.plugin.mmxmgmt.util.JIDUtil;
import com.magnet.mmx.server.plugin.mmxmgmt.util.MMXServerConstants;
import com.magnet.mmx.util.AppChannel;
import com.magnet.mmx.util.ChannelHelper;
import com.magnet.mmx.util.Utils;
import org.dom4j.Element;
import org.jivesoftware.database.DbConnectionManager;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.pubsub.*;
import org.jivesoftware.openfire.pubsub.cluster.RefreshNodeTask;
import org.jivesoftware.openfire.pubsub.models.AccessModel;
import org.jivesoftware.openfire.pubsub.models.PublisherModel;
import org.jivesoftware.util.LocaleUtils;
import org.jivesoftware.util.StringUtils;
import org.jivesoftware.util.XMPPDateTimeFormat;
import org.jivesoftware.util.cache.CacheFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xmpp.forms.DataForm;
import org.xmpp.packet.JID;
import org.xmpp.packet.Message;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.*;
import java.util.Map.Entry;

public class MMXChannelManager {

    private static final Logger LOGGER = LoggerFactory.getLogger(MMXAppManager.class);
    private static final boolean EXCLUDE_USER_CHANNELS = true;
    private static final int MAX_ENTRIES = 100;
    private final XMPPServer mServer = XMPPServer.getInstance();
    private final PubSubService mPubSubModule = mServer.getPubSubModule();
    private final Map<String, JID> mCachedServerUsers = Collections
            .synchronizedMap(new LinkedHashMap<String, JID>(MAX_ENTRIES + 1, 0.75f, true) {
                private static final long serialVersionUID = 1L;

                @Override
                protected boolean removeEldestEntry(Map.Entry<String, JID> eldest) {
                    return size() > MAX_ENTRIES;
                }
            });

    private static MMXChannelManager sInstance = null;

    public static final String MISSING_APP_ROOT_CHANNEL = "Root channel not found for selected app.";
    public static final String INVALID_CHANNEL_ID = "Supplied channel id is invalid.";
    public static final String INVALID_CHANNEL_NAME = "Supplied channel name is invalid.";
    public static final String DUPLICATE_CHANNEL_ID = "Channel with supplied id exists.";

    protected MMXChannelManager() {
    }

    public static MMXChannelManager getInstance() {
        if (sInstance == null) {
            sInstance = new MMXChannelManager();
        }
        return sInstance;
    }

    List<String> listChannels(String parentNode) {
        Collection<Node> nodes = mPubSubModule.getNodes();
        ArrayList<String> result = new ArrayList<String>();
        for (Node node : nodes) {
            result.add(node.getNodeID());
        }
        return result;
    }

    /**
     * Get a list of Leaf Channel nodes for the passed in appId
     * (collection nodes aren't returned)
     * @param appId
     * @return
     */
    public List<TopicNode> listChannelsForAppId(String appId) {
        ArrayList<TopicNode> result = new ArrayList<TopicNode>();
        CollectionNode rootNode = getRootAppChannel(appId);
        if (rootNode != null) {
            Collection<Node> nodes = rootNode.getNodes();
            for (Node node : nodes) {
                //For fixing: https://magneteng.atlassian.net/browse/MOB-833
                if (node.isCollectionNode()) {
                    continue;
                }
                String identifier = node.getNodeID();
                boolean isAppChannel = ChannelHelper.isAppChannel(identifier, appId);
                if (isAppChannel) {
                    TopicNode tn = TopicNode.build(appId, node);
                    result.add(tn);
                }
            }
        }
        return result;
    }

    /**
     * Get a list of Leaf Channel nodes for the passed in appId
     * (collection nodes aren't returned)
     * @param appId
     * @return
     */
    public List<com.magnet.mmx.server.api.v1.protocol.ChannelInfo> getChannelInfo(String appId) {
        ArrayList<com.magnet.mmx.server.api.v1.protocol.ChannelInfo> result = new ArrayList<com.magnet.mmx.server.api.v1.protocol.ChannelInfo>();
        CollectionNode rootNode = getRootAppChannel(appId);
        if (rootNode != null) {
            Collection<Node> nodes = rootNode.getNodes();
            for (Node node : nodes) {
                //For fixing: https://magneteng.atlassian.net/browse/MOB-833
                if (node.isCollectionNode()) {
                    continue;
                }
                String identifier = node.getNodeID();
                boolean isAppChannel = ChannelHelper.isAppChannel(identifier, appId);
                if (isAppChannel) {
                    com.magnet.mmx.server.api.v1.protocol.ChannelInfo info = getChannelInfoFromNode(appId, node);
                    result.add(info);
                }
            }
        }
        return result;
    }

    public com.magnet.mmx.server.api.v1.protocol.ChannelInfo getChannelInfoFromNode(String appId, Node node) {
        com.magnet.mmx.server.api.v1.protocol.ChannelInfo info = new com.magnet.mmx.server.api.v1.protocol.ChannelInfo();
        info.setDescription(node.getDescription());
        info.setChannelName(node.getName());
        if (node instanceof LeafNode) {
            LeafNode lnode = (LeafNode) node;
            info.setSubscriptionEnabled(lnode.isSubscriptionEnabled());
            info.setMaxItems(lnode.isPersistPublishedItems() ? lnode.getMaxPublishedItems() : 0);
        }
        info.setChannelName(ChannelHelper.parseNode(node.getNodeID()).getName());
        info.setPublisherType(node.getPublisherModel().getName());
        return info;
    }

    /**
     * Get a list of subscriptions for a specific channel
     * @param channelId
     * @return
     */
    public List<NodeSubscription> listSubscriptionsForChannel(String channelId) {
        String lowerCase = channelId.toLowerCase();
        LeafNode node = (LeafNode) mPubSubModule.getNode(lowerCase);
        Collection<NodeSubscription> rv = Collections.emptyList();
        if (node != null) {
            rv = node.getAllSubscriptions();
        }
        ArrayList<NodeSubscription> returnList = new ArrayList<NodeSubscription>(rv);
        return returnList;
    }

    public ChannelActionResult createChannel(AppEntity entity, ChannelCreateInfo channelInfo) {
        ChannelActionResult result = new ChannelActionResult();
        if (channelInfo.getChannelName() == null || channelInfo.getChannelName().isEmpty()) {
            result.setSuccess(false);
            result.setCode(ChannelFailureCode.INVALID_CHANNEL_NAME);
            result.setMessage("Channel id is either null or empty");
            return result;
        }
        if (channelInfo.getChannelName().length() > MMXServerConstants.MAX_TOPIC_NAME_LEN) {
            result.setSuccess(false);
            result.setCode(ChannelFailureCode.INVALID_CHANNEL_ID);
            result.setMessage("Supplied channel id exceeds the permitted maximum length of:"
                    + MMXServerConstants.MAX_TOPIC_NAME_LEN);
            return result;
        }
        String channelName = channelInfo.getChannelName();
        channelName = ChannelHelper.normalizePath(channelName);

        String appId = entity.getAppId();
        String channelId = ChannelHelper.makeChannel(appId,
                channelInfo.isPrivateChannel() ? entity.getServerUserId() : null, channelName);

        /**
         * Get the appRootNode
         */
        CollectionNode appRootNode = getRootAppChannel(appId);
        if (appRootNode == null) {
            result = new ChannelActionResult();
            result.setSuccess(false);
            result.setMessage(MISSING_APP_ROOT_CHANNEL);
            result.setCode(ChannelFailureCode.NOTFOUND);
            return result;
        }
        // create the requested channel
        Node newAppChannel = null;

        try {
            newAppChannel = createLeafNode(JIDUtil.makeNode(entity.getServerUserId(), appId), channelId,
                    appRootNode, channelInfo);
            result.setSuccess(true);
            result.setNode(TopicNode.build(appId, newAppChannel));
            // Add role mappings
            List<String> roles = channelInfo.getRoles();
            if (roles == null || roles.isEmpty()) {
                roles = Collections.singletonList(MMXServerConstants.TOPIC_ROLE_PUBLIC);
            }
            TopicRoleDAO roleDAO = getTopicRoleDAO();
            roleDAO.addTopicRoles("pubsub", newAppChannel.getNodeID(), roles);
        } catch (NotAcceptableException e) {
            result.setSuccess(false);
            result.setCode(ChannelFailureCode.UNKNOWN);
            result.setMessage(e.getMessage());
        } catch (ChannelExistsException e) {
            result.setSuccess(false);
            result.setCode(ChannelFailureCode.DUPLICATE);
            result.setMessage(DUPLICATE_CHANNEL_ID);
        }
        return result;
    }

    /**
     * Delete a channel identified by a channel id.
     * @param appId The app ID for error message.
     * @param channelId The node ID.
     * @return
     */
    public ChannelActionResult deleteChannel(String appId, String channelId) {

        ChannelActionResult result = new ChannelActionResult();

        Node gonner = mPubSubModule.getNode(channelId);

        if (gonner == null) {
            result.setSuccess(false);
            result.setCode(ChannelFailureCode.NOTFOUND);
            result.setMessage(INVALID_CHANNEL_ID);
        } else {
            LOGGER.trace("Deleting channel with id:" + channelId);
            gonner.delete();
            result.setSuccess(true);
            result.setNode(TopicNode.build(appId, gonner));
            result.setMessage(INVALID_CHANNEL_ID);
        }
        return result;
    }

    /**
     * Get the root collection node for an app
     *
     * @param appId
     * @return
     */
    public CollectionNode getRootAppChannel(String appId) {
        Node result = mPubSubModule.getNode(appId);
        if (result != null && result.isCollectionNode()) {
            return (CollectionNode) result;
        } else {
            return null;
        }
    }

    /**
     * Get a Node representing the channel with the specified channelId.
     * @param channelId
     * @return Node if one exists false other wise.
     */
    public Node getTopicNode(String channelId) {
        Node result = mPubSubModule.getNode(channelId);
        return result;
    }

    // creatorUsername is "userID%appID"
    CollectionNode createCollectionNode(String creatorUsername, String nodeId, CollectionNode parentNode) {
        CollectionNode parent = (parentNode == null) ? mPubSubModule.getRootCollectionNode() : parentNode;
        Node result = mPubSubModule.getNode(nodeId);
        if (result != null && !result.isCollectionNode()) {
            result.delete();
            result = null;
            // TODO cleanup existing published Items for this node
        }

        if (result == null) {
            ConfigureForm form = new ConfigureForm(DataForm.Type.submit);
            form.setSendItemSubscribe(true);
            form.setDeliverPayloads(true);
            form.setNotifyRetract(false);
            form.setNotifyDelete(false);
            form.setNotifyConfig(false);
            form.setNodeType(ConfigureForm.NodeType.collection);
            form.setPublishModel(ConfigureForm.PublishModel.open);
            LOGGER.trace("Collection config form: " + form);

            JID jid = new JID(creatorUsername, mServer.getServerInfo().getXMPPDomain(), null);
            CollectionNode node = new CollectionNode(mPubSubModule, parent, nodeId, jid);
            node.addOwner(jid);
            try {
                node.configure(form);
            } catch (NotAcceptableException e) {
                LOGGER.warn("NotAcceptableException", e);
            }
            node.saveToDB();
            CacheFactory.doClusterTask(new RefreshNodeTask(node));
            result = node;
        }
        return (CollectionNode) result;
    }

    /**
     * Create a channel using the channel request.
     *
     * @param createUsername
     * @param channelId
     * @param parentNode
     * @throws ChannelExistsException   -- if channel already exists
     * @throws NotAcceptableException -- if an exception is thrown by openfire during channel creation.
     */
    private LeafNode createLeafNode(String createUsername, String channelId, CollectionNode parentNode,
            ChannelCreateInfo channelInfo) throws ChannelExistsException, NotAcceptableException {

        String channelName = channelInfo.getChannelName();
        String channelDescription = channelInfo.getDescription();
        int maxItems = channelInfo.getMaxItems();

        Node result = mPubSubModule.getNode(channelId);
        if (result != null) {
            throw new ChannelExistsException("Channel with id: " + channelId + " exists");
        }
        LeafNode createdNode = null;
        if (result == null) {
            // This config form should use the same values from setOptions().
            ConfigureForm form = new ConfigureForm(DataForm.Type.submit);
            form.setAccessModel(ChannelHelper.isUserChannel(channelId) ? ConfigureForm.AccessModel.authorize
                    : ConfigureForm.AccessModel.open);
            form.setPersistentItems(maxItems != 0);
            form.setMaxItems(maxItems);
            form.setSendItemSubscribe(true);
            form.setMaxPayloadSize(Constants.MAX_PAYLOAD_SIZE);
            form.setDeliverPayloads(true);
            form.setNotifyRetract(false);
            form.setNotifyDelete(false);
            form.setNotifyConfig(false);
            form.setNodeType(ConfigureForm.NodeType.leaf);
            // TODO: default permission for channels created from console is subscribers
            TopicAction.PublisherType permission = channelInfo.getPublishPermission();
            if (permission == null) {
                permission = TopicAction.PublisherType.subscribers;
            }
            form.setPublishModel(ConfigureForm.convert(permission));
            form.setTitle(channelName);
            form.setSubscribe(channelInfo.isSubscriptionEnabled());
            if (channelDescription != null) {
                form.setDescription(channelDescription);
            }
            //      LOGGER.trace("Leaf config form: "+form);

            JID jid = new JID(createUsername, mServer.getServerInfo().getXMPPDomain(), null);
            LeafNode node = new LeafNode(mPubSubModule, parentNode, channelId, jid);
            node.addOwner(jid);
            try {
                node.configure(form);
            } catch (NotAcceptableException e) {
                LOGGER.warn("NotAcceptableException", e);
                throw e;
            }

            if (channelInfo.isSubscribeOnCreate()) {
                NodeSubscription subscription = subscribeToNode(node, jid, jid);
                // TODO: not returning the subscription ID yet.
            }

            node.saveToDB();
            CacheFactory.doClusterTask(new RefreshNodeTask(node));
            createdNode = node;
        }
        return createdNode;
    }

    String getAppIdPrefix(String appId) {
        return ChannelHelper.CHANNEL_DELIM + appId + ChannelHelper.CHANNEL_DELIM + "*"
                + ChannelHelper.CHANNEL_DELIM;
    }

    /**
     * Class that represents a result of a channel related action.
     */
    public static class ChannelActionResult {
        boolean success;
        private TopicNode node;
        private String message;
        private ChannelFailureCode code;

        public boolean isSuccess() {
            return success;
        }

        public void setSuccess(boolean success) {
            this.success = success;
        }

        public TopicNode getNode() {
            return node;
        }

        public void setNode(TopicNode node) {
            this.node = node;
        }

        public String getMessage() {
            return message;
        }

        public void setMessage(String message) {
            this.message = message;
        }

        public ChannelFailureCode getCode() {
            return code;
        }

        public void setCode(ChannelFailureCode code) {
            this.code = code;
        }
    }

    public static enum ChannelFailureCode {
        DUPLICATE, NOTFOUND, UNKNOWN, INVALID_CHANNEL_ID, INVALID_CHANNEL_NAME
    }

    public static class ChannelExistsException extends RuntimeException {
        public ChannelExistsException(String message, Throwable cause) {
            super(message, cause);
        }

        public ChannelExistsException(String message) {
            super(message);
        }
    }

    private void setOptions(String channelName, Node node, MMXTopicOptions options) {
        ConfigureForm form = new ConfigureForm(DataForm.Type.submit);
        form.setSendItemSubscribe(true);
        form.setMaxPayloadSize(Constants.MAX_PAYLOAD_SIZE);
        form.setDeliverPayloads(true);
        form.setNotifyRetract(false);
        form.setNotifyDelete(false);
        form.setNotifyConfig(false);
        form.setAccessModel(ChannelHelper.isUserChannel(node.getNodeID()) ? ConfigureForm.AccessModel.authorize
                : ConfigureForm.AccessModel.open);
        form.setNodeType(node.isCollectionNode() ? ConfigureForm.NodeType.collection : ConfigureForm.NodeType.leaf);
        if (options == null) {
            form.setPublishModel(ConfigureForm.PublishModel.open);
            form.setPersistentItems(true);
            form.setMaxItems(-1);
            form.setSubscribe(true);
            form.setTitle(channelName);
        } else {
            // Set the default values if not specified.
            options.fillWithDefaults();

            form.setPublishModel(ConfigureForm.convert(options.getPublisherType()));
            form.setPersistentItems(options.getMaxItems() != 0);
            form.setMaxItems(options.getMaxItems());
            form.setSubscribe(options.isSubscriptionEnabled());
            form.setTitle(channelName);
            form.setDescription(options.getDescription());
        }
        try {
            node.configure(form);
        } catch (NotAcceptableException e) {
            e.printStackTrace();
        }
    }

    // Add a list of owners to a node.  The owners should be unique.
    private void addOwnersToNode(Node node, JID[] owners) {
        for (JID owner : owners) {
            if (owner != null) {
                node.addOwner(owner);
            }
        }
    }

    private JID getServerUser(String appId) {
        // Use LRU cache for server user JID.
        JID jid = mCachedServerUsers.get(appId);
        if (jid == null) {
            AppDAO appDAO = DBUtil.getAppDAO();
            String userId = appDAO.getServerUserForApp(appId);
            if (userId != null) {
                jid = new JID(JIDUtil.makeNode(userId, appId), mServer.getServerInfo().getXMPPDomain(), null);
                mCachedServerUsers.put(appId, jid);
            }
        }
        return jid;
    }

    // Create the collection node and its ancestors recursively.
    private CollectionNode createCollectionNode(int prefix, String nodeId, JID creator, JID[] owners)
            throws IllegalArgumentException {
        //    LOGGER.trace("createCollectionNode: prefix="+prefix+", nodeId="+nodeId);
        if (nodeId == null) {
            return null;
        }
        // Check if the collection node exists and is valid.
        Node node;
        if ((node = mPubSubModule.getNode(nodeId)) != null) {
            if (!node.isCollectionNode()) {
                throw new IllegalArgumentException(
                        nodeId.substring(prefix) + " exists, but not a collection node.");
            }
            //      LOGGER.trace("return existing collection node="+nodeId);
            return (CollectionNode) node;
        }
        String parentNodeId = ChannelHelper.getParent(prefix, nodeId);
        CollectionNode parentNode;
        if (parentNodeId == null) {
            parentNode = getRootAppChannel(ChannelHelper.getRootNodeId(nodeId));
        } else {
            parentNode = createCollectionNode(prefix, parentNodeId, creator, owners);
        }

        synchronized (nodeId.intern()) {
            if ((node = mPubSubModule.getNode(nodeId)) == null) {
                LOGGER.trace("create collection node=" + nodeId + ", parent="
                        + ((parentNode == null) ? "" : parentNode.getNodeID()));
                node = new CollectionNode(mPubSubModule, parentNode, nodeId, creator);
                setOptions(null, node, null);
                addOwnersToNode(node, owners);
                node.saveToDB();

                CacheFactory.doClusterTask(new RefreshNodeTask(node));
            }
        }
        LOGGER.trace("return new collection node=" + nodeId);
        return (CollectionNode) node;
    }

    /**
     * Create an application-wide or personal channel and create all parent nodes
     * if needed.
     * @param from
     * @param appId
     * @param rqt
     * @return
     * @throws MMXException
     */
    public MMXStatus createChannel(JID from, String appId, ChannelAction.CreateRequest rqt) throws MMXException {
        String channel = rqt.getChannelName();
        try {
            // Note, ChannelHelper.checkPathAllowed(channel) should not be called here;
            // it is a restriction in the client SDK.
            channel = ChannelHelper.normalizePath(channel);
        } catch (IllegalArgumentException e) {
            throw new MMXException(e.getMessage(), StatusCode.BAD_REQUEST.getCode());
        }

        boolean isValid = ChannelHelper.validateApplicationChannelName(channel);
        if (!isValid) {
            throw new MMXException(StatusCode.INVALID_CHANNEL_NAME.getMessage(),
                    StatusCode.INVALID_CHANNEL_NAME.getCode());
        }
        String userId = JIDUtil.getUserId(from);
        JID owner = from.asBareJID();
        JID serverUser = getServerUser(appId);
        // Don't add server user if the creator is the server user already.
        JID[] owners = { owner, owner.equals(serverUser) ? null : serverUser };
        String channelId = ChannelHelper.makeChannel(appId, rqt.isPersonal() ? userId : null, channel);
        //    LOGGER.trace("createChannel realChannel="+realChannel+", channel="+channel);

        if (!mPubSubModule.canCreateNode(from)) {
            throw new MMXException(StatusCode.FORBIDDEN.getMessage(channel), StatusCode.FORBIDDEN.getCode());
        }
        if (mPubSubModule.getNode(channelId) != null) {
            throw new MMXException(StatusCode.CHANNEL_EXISTS.getMessage(channel),
                    StatusCode.CHANNEL_EXISTS.getCode());
        }
        if (!mPubSubModule.isInstantNodeSupported()) {
            throw new MMXException(StatusCode.NOT_ACCEPTABLE.getMessage(channel),
                    StatusCode.NOT_ACCEPTABLE.getCode());
        }
        int prefix = ChannelHelper.getPrefixLength(channelId);
        String parentId = ChannelHelper.getParent(prefix, channelId);
        CollectionNode parent;
        if (parentId == null) {
            parent = getRootAppChannel(appId);
        } else {
            try {
                // Recursively create the parent nodes if they don't exist.
                parent = createCollectionNode(prefix, parentId, from, owners);
            } catch (Throwable e) {
                throw new MMXException(e.getMessage(), StatusCode.BAD_REQUEST.getCode());
            }
        }
        if (parent != null && !rqt.isCollection()) {
            if (!parent.isAssociationAllowed(from)) {
                // Check if requester is allowed to add a new leaf node to the parent.
                throw new MMXException(StatusCode.FORBIDDEN.getMessage(channel), StatusCode.FORBIDDEN.getCode());
            }
            if (parent.isMaxLeafNodeReached()) {
                throw new MMXException("Max nodes exceeded in the parent of " + channel,
                        StatusCode.CONFLICT.getCode());
            }
        }

        synchronized (channelId.intern()) {
            if (mPubSubModule.getNode(channelId) != null) {
                throw new MMXException(StatusCode.CHANNEL_EXISTS.getMessage(channel),
                        StatusCode.CHANNEL_EXISTS.getCode());
            }
            //      LOGGER.trace("create node="+realChannel+", parent="+parent);
            Node node;
            if (rqt.isCollection()) {
                node = new CollectionNode(mPubSubModule, parent, channelId, from);
            } else {
                node = new LeafNode(mPubSubModule, parent, channelId, from);
            }
            // Add the creator as the owner.
            addOwnersToNode(node, owners);
            setOptions(channel, node, rqt.getOptions());

            // Do the auto-subscription for creator.
            if (rqt.getOptions() != null && rqt.getOptions().isSubscribeOnCreate()) {
                NodeSubscription subscription = subscribeToNode(node, owner, owner);
                // TODO: not returning the subscription ID yet.
            }

            node.saveToDB();
            CacheFactory.doClusterTask(new RefreshNodeTask(node));
            /**
             * Add the mapping for the roles.
             */
            TopicRoleDAO roleDAO = getTopicRoleDAO();
            List<String> roles = rqt.getRoles();
            if (roles == null) {
                roles = Collections.singletonList(MMXServerConstants.TOPIC_ROLE_PUBLIC);
            }
            roleDAO.addTopicRoles("pubsub", node.getNodeID(), roles);

        }
        //    LOGGER.trace("create node="+realChannel+" success");

        MMXStatus status = (new MMXStatus()).setCode(StatusCode.SUCCESS.getCode())
                .setMessage(StatusCode.SUCCESS.getMessage());
        return status;
    }

    private NodeSubscription subscribeToNode(Node node, JID owner, JID subscriber) {
        SubscribeForm optionsForm = new SubscribeForm(DataForm.Type.submit);
        // Receive notification of new items only.
        optionsForm.setSubscriptionType(NodeSubscription.Type.items);
        optionsForm.setSubscriptionDepth("all");
        // Don't set other options; it will change the subscription state to
        // pending because it will wait for the owner's approval.

        // Need a modified Node.java from mmx-openfire repo.
        NodeSubscription subscription = node.createSubscription(null, owner, subscriber, false, optionsForm);
        return subscription;
    }

    private int deleteNode(Node node, JID owner) throws MMXException {
        if (!node.getOwners().contains(owner)) {
            AppChannel channel = ChannelHelper.parseChannel(node.getNodeID());
            throw new MMXException(StatusCode.FORBIDDEN.getMessage(channel.getName()),
                    StatusCode.FORBIDDEN.getCode());
        }
        int count = 0;
        if (node.isCollectionNode()) {
            for (Node child : node.getNodes()) {
                if (child.isCollectionNode()) {
                    count += deleteNode(child, owner);
                } else {
                    if (!child.getOwners().contains(owner)) {
                        AppChannel channel = ChannelHelper.parseChannel(child.getNodeID());
                        throw new MMXException(StatusCode.FORBIDDEN.getMessage(channel.getName()),
                                StatusCode.FORBIDDEN.getCode());
                    }

                    //          LOGGER.trace("delete leaf node=" + node.getNodeID());
                    child.delete();
                    ++count;
                }
            }
        }
        //    LOGGER.trace("delete node="+node.getNodeID());
        node.delete();
        ++count;
        return count;
    }

    public MMXStatus deleteChannel(JID from, String appId, ChannelAction.DeleteRequest rqt) throws MMXException {
        String channel = ChannelHelper.normalizePath(rqt.getChannel());
        String userId = JIDUtil.getUserId(from);
        JID owner = from.asBareJID();
        String realChannel = ChannelHelper.makeChannel(appId, rqt.isPersonal() ? userId : null, channel);
        Node node = mPubSubModule.getNode(realChannel);
        if (node == null) {
            throw new MMXException(StatusCode.CHANNEL_NOT_FOUND.getMessage(channel),
                    StatusCode.CHANNEL_NOT_FOUND.getCode());
        }
        int count = deleteNode(node, owner);
        MMXStatus status = (new MMXStatus()).setCode(StatusCode.SUCCESS.getCode())
                .setMessage(count + " channel" + ((count == 1) ? " is" : "s are") + " deleted");
        return status;
    }

    public ChannelInfo getChannel(String appId, MMXChannelId channel) throws MMXException {
        return getChannel(null, appId, channel);
    }

    public ChannelInfo getChannel(JID from, String appId, MMXChannelId channel) throws MMXException {
        String realChannel = ChannelHelper.makeChannel(appId, channel.getEscUserId(),
                ChannelHelper.normalizePath(channel.getName()));
        Node node = mPubSubModule.getNode(realChannel);
        if (node == null) {
            throw new MMXException(StatusCode.CHANNEL_NOT_FOUND.getMessage(channel.getName()),
                    StatusCode.CHANNEL_NOT_FOUND.getCode());
        }
        //    // A user can get the channel info if the channel is a global channel, or the owner
        //    // of a user channel, or a subscriber to a user channel.
        //    if (channel.isUserChannel() && !node.getOwners().contains(from.asBareJID()) &&
        //        node.getSubscriptions(from.asBareJID()).size() > 0) {
        //      throw new MMXException(StatusCode.FORBIDDEN.getMessage(channel.getName()),
        //          StatusCode.FORBIDDEN.getCode());
        //    }
        return nodeToInfo(channel.getUserId(), channel.getName(), node);
    }

    public Node getChannelNode(String appId, MMXChannelId channel) throws MMXException {
        String realChannel = ChannelHelper.makeChannel(appId, channel.getEscUserId(),
                ChannelHelper.normalizePath(channel.getName()));
        Node node = mPubSubModule.getNode(realChannel);
        if (node == null) {
            throw new MMXException(StatusCode.CHANNEL_NOT_FOUND.getMessage(channel.getName()),
                    StatusCode.CHANNEL_NOT_FOUND.getCode());
        }
        return node;
    }

    public List<ChannelInfo> getChannels(JID from, String appId, List<MMXChannelId> channels) throws MMXException {
        List<ChannelInfo> infos = new ArrayList<ChannelInfo>(channels.size());
        for (MMXChannelId channel : channels) {
            try {
                infos.add(getChannel(from, appId, channel));
            } catch (Throwable e) {
                infos.add(null);
            }
        }
        return infos;
    }

    public MMXStatus retractAllFromChannel(JID from, String appId, ChannelAction.RetractAllRequest rqt)
            throws MMXException {
        String channel = ChannelHelper.normalizePath(rqt.getChannel());
        String userId = JIDUtil.getUserId(from);
        JID owner = from.asBareJID();
        String realChannel = ChannelHelper.makeChannel(appId, rqt.isPersonal() ? userId : null, channel);
        Node node = mPubSubModule.getNode(realChannel);
        if (node == null) {
            throw new MMXException(StatusCode.CHANNEL_NOT_FOUND.getMessage(channel),
                    StatusCode.CHANNEL_NOT_FOUND.getCode());
        }
        if (!node.getOwners().contains(owner)) {
            throw new MMXException(StatusCode.FORBIDDEN.getMessage(channel), StatusCode.FORBIDDEN.getCode());
        }
        LeafNode leafNode = (LeafNode) node;
        List<PublishedItem> pubItems = leafNode.getPublishedItems();
        leafNode.deleteItems(pubItems);

        int count = (pubItems == null) ? 0 : pubItems.size();
        MMXStatus status = (new MMXStatus()).setCode(StatusCode.SUCCESS.getCode())
                .setMessage(count + " item" + ((count == 1) ? " is" : "s are") + " retracted");
        return status;
    }

    public Map<String, Integer> retractFromChannel(JID from, String appId, ChannelAction.RetractRequest rqt)
            throws MMXException {
        String channel = ChannelHelper.normalizePath(rqt.getChannel());
        String realChannel = ChannelHelper.makeChannel(appId, rqt.getUserId(), channel);
        Node node = mPubSubModule.getNode(realChannel);
        if (node == null) {
            throw new MMXException(StatusCode.CHANNEL_NOT_FOUND.getMessage(channel),
                    StatusCode.CHANNEL_NOT_FOUND.getCode());
        }
        if (node.isCollectionNode()) {
            throw new MMXException("Cannot retract items from a collection channel",
                    StatusCode.NOT_IMPLEMENTED.getCode());
        }

        LeafNode leafNode = (LeafNode) node;
        List<String> itemIds = rqt.getItemIds();
        if (itemIds == null || itemIds.size() == 0) {
            throw new MMXException(StatusCode.BAD_REQUEST.getMessage("no item ID's"),
                    StatusCode.BAD_REQUEST.getCode());
        }
        if (!leafNode.isItemRequired()) {
            // Cannot delete items from a leaf node that doesn't handle itemIDs
            throw new MMXException("Items required in this channel", StatusCode.NOT_IMPLEMENTED.getCode());
        }

        List<PublishedItem> pubItems = new ArrayList<PublishedItem>(itemIds.size());
        Map<String, Integer> results = new HashMap<String, Integer>(itemIds.size());
        for (String itemId : itemIds) {
            if (itemId == null) {
                continue;
            }
            PublishedItem item = leafNode.getPublishedItem(itemId);
            if (item == null) {
                results.put(itemId, StatusCode.ITEM_NOT_FOUND.getCode());
            }
            if (!item.canDelete(from)) {
                results.put(itemId, StatusCode.FORBIDDEN.getCode());
            }
            pubItems.add(item);
            results.put(itemId, StatusCode.SUCCESS.getCode());
        }
        leafNode.deleteItems(pubItems);
        return results;
    }

    public ChannelAction.SubscribeResponse subscribeChannel(JID from, String appId,
            ChannelAction.SubscribeRequest rqt, List<String> userRoles) throws MMXException {
        String channel = ChannelHelper.normalizePath(rqt.getChannel());
        String realChannel = ChannelHelper.makeChannel(appId, rqt.getUserId(), channel);
        Node node = mPubSubModule.getNode(realChannel);
        if (node == null) {
            throw new MMXException(StatusCode.CHANNEL_NOT_FOUND.getMessage(channel),
                    StatusCode.CHANNEL_NOT_FOUND.getCode());
        }

        JID owner = from.asBareJID();
        // The subscriber can specify a different resource or without resource.
        JID subscriber = new JID(from.getNode(), from.getDomain(), rqt.getDevId());

        AccessModel accessModel = node.getAccessModel();
        if (!accessModel.canSubscribe(node, owner, subscriber)) {
            throw new MMXException(StatusCode.FORBIDDEN.getMessage(channel), StatusCode.FORBIDDEN.getCode());
        }

        // Check if the subscription owner is a user with outcast affiliation
        NodeAffiliate nodeAffiliate = node.getAffiliate(owner);
        if (nodeAffiliate != null && nodeAffiliate.getAffiliation() == NodeAffiliate.Affiliation.outcast) {
            throw new MMXException(StatusCode.FORBIDDEN.getMessage(channel), StatusCode.FORBIDDEN.getCode());
        }

        // Check that subscriptions to the node are enabled
        if (!node.isSubscriptionEnabled()) {
            throw new MMXException(StatusCode.FORBIDDEN.getMessage(channel), StatusCode.FORBIDDEN.getCode());
        }
        /*
         * ensure user has the necessary role for subscribing to the channel.
         */
        boolean isSubScriptionAllowed = isAllowed(node.getNodeID(), userRoles);
        if (!isSubScriptionAllowed) {
            LOGGER.info("Subscription to Channel:{} not allowed for user with roles:{}", node.getNodeID(),
                    userRoles);
            throw new MMXException(StatusCode.FORBIDDEN.getMessage(channel), StatusCode.FORBIDDEN.getCode());
        }
        // Check for duplicated subscription; return error or existing subscription.
        NodeSubscription subscription;
        if ((subscription = node.getSubscription(subscriber)) != null) {
            if (rqt.isErrorOnDup()) {
                throw new MMXException(StatusCode.SUBSCRIPTION_EXISTS.getMessage(channel),
                        StatusCode.SUBSCRIPTION_EXISTS.getCode());
            } else {
                ChannelAction.SubscribeResponse resp = new ChannelAction.SubscribeResponse(subscription.getID(),
                        StatusCode.SUCCESS.getCode(), StatusCode.SUCCESS.getMessage());
                return resp;
            }
        }

        subscription = subscribeToNode(node, owner, subscriber);

        ChannelAction.SubscribeResponse resp = new ChannelAction.SubscribeResponse(subscription.getID(),
                StatusCode.SUCCESS.getCode(), StatusCode.SUCCESS.getMessage());
        return resp;
    }

    public MMXStatus unsubscribeChannel(JID from, String appId, ChannelAction.UnsubscribeRequest rqt)
            throws MMXException {
        String channel = ChannelHelper.normalizePath(rqt.getChannel());
        String realChannel = ChannelHelper.makeChannel(appId, rqt.getUserId(), channel);
        Node node = mPubSubModule.getNode(realChannel);
        if (node == null) {
            throw new MMXException(StatusCode.CHANNEL_NOT_FOUND.getMessage(channel),
                    StatusCode.CHANNEL_NOT_FOUND.getCode());
        }
        int count = 0;
        JID owner = from.asBareJID();
        String subId = rqt.getSubId();
        for (NodeSubscription subscription : node.getSubscriptions(owner)) {
            if (subId == null || subscription.getID().equals(subId)) {
                ++count;
                node.cancelSubscription(subscription);
            }
        }
        if (count == 0) {
            throw new MMXException(StatusCode.GONE.getMessage(), StatusCode.GONE.getCode());
        }

        MMXStatus status = (new MMXStatus()).setCode(StatusCode.SUCCESS.getCode())
                .setMessage(count + " subscription" + ((count == 1) ? " is" : "s are") + " cancelled");
        return status;
    }

    public MMXStatus unsubscribeForDev(JID from, String appId, ChannelAction.UnsubscribeForDevRequest rqt)
            throws MMXException {
        String prefix = ChannelHelper.CHANNEL_DELIM + appId + ChannelHelper.CHANNEL_DELIM;
        int count = 0;
        JID owner = from.asBareJID();
        String devId = rqt.getDevId();
        for (Node node : mPubSubModule.getNodes()) {
            if (!node.getNodeID().startsWith(prefix)) {
                continue;
            }
            for (NodeSubscription subscription : node.getSubscriptions(owner)) {
                if (devId.equals(subscription.getJID().getResource())) {
                    ++count;
                    node.cancelSubscription(subscription);
                }
            }
        }
        MMXStatus status = (new MMXStatus()).setCode(StatusCode.SUCCESS.getCode())
                .setMessage(count + " subscription" + ((count == 1) ? " is" : "s are") + " cancelled");
        return status;
    }

    public ChannelAction.ListResponse listChannels(JID from, String appId, ChannelAction.ListRequest rqt,
            List<String> userRoles) throws MMXException {
        ChannelAction.ListResponse resp = new ChannelAction.ListResponse();
        Integer maxLimit = rqt.getLimit();
        boolean recursive = rqt.isRecursive();
        String start = rqt.getStart();
        ListType type = rqt.getType();
        int limit = (maxLimit == null || maxLimit == -1) ? Integer.MAX_VALUE : maxLimit;
        String realChannel;
        if (start == null || start.isEmpty()) {
            // Get the g
            // lobal root node.
            Node node = mPubSubModule.getNode(appId);
            if (node == null) {
                throw new MMXException(StatusCode.APP_NODE_NOT_FOUND.getMessage(appId),
                        StatusCode.APP_NODE_NOT_FOUND.getCode());
            }
            // Get its top level children nodes filtered by the search type.
            String userId = JIDUtil.getUserId(from);
            limit = getTopChildNodes(recursive, node, resp, limit, type, userId, userRoles);
        } else {
            // Filter by the global channels.
            if (type == ListType.global || type == ListType.both) {
                realChannel = ChannelHelper.makeChannel(appId, null, start);
                // Get the global channel node first.
                Node node = mPubSubModule.getNode(realChannel);
                if (node == null) {
                    throw new MMXException(StatusCode.CHANNEL_NOT_FOUND.getMessage(start),
                            StatusCode.CHANNEL_NOT_FOUND.getCode());
                }
                // Get its children nodes without search filter.
                limit = getChildNodes(recursive, node, resp, limit);
            }
            // Filter by the user channels.
            if (type == ListType.personal || type == ListType.both) {
                realChannel = ChannelHelper.makeChannel(appId, JIDUtil.getUserId(from), start);
                // Get the personal channel node first.
                Node node = mPubSubModule.getNode(realChannel);
                if (node == null) {
                    throw new MMXException(StatusCode.CHANNEL_NOT_FOUND.getMessage(start),
                            StatusCode.CHANNEL_NOT_FOUND.getCode());
                }
                // Get its children nodes without search filter.
                limit = getChildNodes(recursive, node, resp, limit);
            }
        }
        return resp;
    }

    public ChannelInfo nodeToChannelInfo(String userId, Node node) {
        return nodeToInfo(userId, node.getName(), node);
    }

    private ChannelInfo nodeToInfo(String userId, String channel, Node node) {
        ChannelInfo info = new ChannelInfo(userId, node.getName() != null ? node.getName() : channel,
                node.isCollectionNode()).setCreationDate(node.getCreationDate())
                        .setDescription(node.getDescription()).setModifiedDate(node.getModificationDate())
                        .setCreator(node.getCreator().toString())
                        .setSubscriptionEnabled(node.isSubscriptionEnabled());
        if (!node.isCollectionNode()) {
            LeafNode leafNode = (LeafNode) node;
            info.setMaxItems(leafNode.isPersistPublishedItems() ? leafNode.getMaxPublishedItems() : 0)
                    .setMaxPayloadSize(leafNode.getMaxPayloadSize())
                    .setPersistent(leafNode.isPersistPublishedItems())
                    .setPublishPermission(ConfigureForm.convert(leafNode.getPublisherModel()));
        } else {
            info.setMaxItems(0).setMaxPayloadSize(0).setPersistent(false).setPublishPermission(null);
        }
        return info;
    }

    // Get all top level children nodes which are either global channels or user
    // channels.  Filter out the nodes by the search type.
    private int getTopChildNodes(boolean recursive, Node root, ChannelAction.ListResponse resp, int limit,
            ListType type, String userId, List<String> roles) {
        boolean globalOnly = (type == ListType.global);
        if (roles == null || roles.isEmpty()) {
            roles = Collections.singletonList(MMXServerConstants.TOPIC_ROLE_PUBLIC);
        }
        for (Node child : root.getNodes()) {
            AppChannel channel = ChannelHelper.parseChannel(child.getNodeID());
            if (channel == null) {
                LOGGER.warn("Ignore malformed channel: " + child.getNodeID());
                continue;
            }
            // Brain teaser: the xor below is actually same as
            //    (type == ListType.global && !channel.isUserChannel()) ||
            //    (type == ListType.personal && channel.isUserChannel()))
            if ((type == ListType.both) || (globalOnly ^ channel.isUserChannel())) {
                if (channel.isUserChannel() && !channel.getEscUserId().equals(userId)) {
                    continue;
                }
                if (--limit < 0) {
                    return limit;
                }
                //resp.add(nodeToInfo(channel.getUserId(), channel.getName(), child));
                /**
                 * Add a check to see if the channel role mapping allows current user's roles
                 * to access this channel. The check should be done only for global channels
                 */
                if (!channel.isUserChannel()) {
                    boolean userHasRole = isAllowed(child.getNodeID(), roles);
                    if (userHasRole) {
                        resp.add(nodeToInfo(channel.getUserId(), channel.getName(), child));
                    }
                } else {
                    resp.add(nodeToInfo(channel.getUserId(), channel.getName(), child));
                }
                if (recursive && child.isCollectionNode()) {
                    limit = getChildNodes(recursive, child, resp, limit);
                }
            }
        }
        return limit;
    }

    // Get all children nodes recursively below the top level.
    // @return < 0 if exceeding the limit, >= 0 if within the limit.
    private int getChildNodes(boolean recursive, Node node, ChannelAction.ListResponse resp, int limit) {
        if (!node.isCollectionNode()) {
            AppChannel channel = ChannelHelper.parseChannel(node.getNodeID());
            if (channel == null) {
                LOGGER.warn("Ignore malformed channel: " + node.getNodeID());
            } else {
                if (--limit < 0) {
                    return limit;
                }
                resp.add(nodeToInfo(channel.getUserId(), channel.getName(), node));
            }
        } else {
            for (Node child : node.getNodes()) {
                AppChannel channel = ChannelHelper.parseChannel(child.getNodeID());
                if (channel == null) {
                    LOGGER.warn("Ignore malformed channel: " + child.getNodeID());
                    continue;
                }
                if (--limit < 0) {
                    return limit;
                }
                resp.add(nodeToInfo(channel.getUserId(), channel.getName(), child));
                if (recursive && child.isCollectionNode()) {
                    limit = getChildNodes(recursive, child, resp, limit);
                }
            }
        }
        return limit;
    }

    private String[] getChannels(String appId, List<MMXChannelId> list, int begin, int size) {
        if (list == null) {
            return null;
        }
        size = Math.min(size, list.size() - begin);
        String[] channels = new String[size];
        for (int i = 0, index = begin; --size >= 0; index++, i++) {
            MMXChannelId channelNode = list.get(index);
            channels[i] = ChannelHelper.makeChannel(appId, channelNode.getEscUserId(), channelNode.getName());
        }
        return channels;
    }

    private static class SQLHelper {
        public static String generateArgList(int numOfArgs) {
            if (numOfArgs == 0) {
                return "";
            }
            StringBuilder sb = new StringBuilder((numOfArgs - 1) * 2 + 1);
            while (--numOfArgs > 0) {
                sb.append("?,");
            }
            sb.append('?');
            return sb.toString();
        }

        public static void bindArgList(PreparedStatement pstmt, int index, String[] args) throws SQLException {
            for (String arg : args) {
                pstmt.setString(index++, arg);
            }
        }

        public static void bindArgList(PreparedStatement pstmt, int index, List<String> args) throws SQLException {
            for (String arg : args) {
                pstmt.setString(index++, arg);
            }
        }
    }

    public ChannelAction.SummaryResponse getSummary(JID from, String appId, ChannelAction.SummaryRequest rqt)
            throws MMXException {
        // Build a collection of channel ID's from the request; it contains channels
        // without any published items.
        HashSet<MMXChannelId> tpNoItems = new HashSet<MMXChannelId>(rqt.getChannelNodes().size());
        for (MMXChannelId channelId : rqt.getChannelNodes()) {
            tpNoItems.add(channelId);
        }
        ChannelAction.SummaryResponse resp = new ChannelAction.SummaryResponse(rqt.getChannelNodes().size());
        Connection con = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;
        try {
            con = DbConnectionManager.getConnection();
            int start = 0;
            int numOfChannels = rqt.getChannelNodes().size();
            StringBuilder dateRange = new StringBuilder();
            ArrayList<String> dates = new ArrayList<String>();
            if (rqt.getSince() != null) {
                dateRange.append("AND ofPubsubItem.creationDate >= ? ");
                dates.add(StringUtils.dateToMillis(rqt.getSince()));
            }
            if (rqt.getUntil() != null) {
                dateRange.append("AND ofPubsubItem.creationDate <= ? ");
                dates.add(StringUtils.dateToMillis(rqt.getUntil()));
            }
            do {
                // Limit to 128 channels per query because some DBMS cannot handle more
                // than 255 arguments in the IN clause.
                String[] channels = getChannels(appId, rqt.getChannelNodes(), start, 128);
                if (channels == null || channels.length == 0) {
                    break;
                }
                String argList = SQLHelper.generateArgList(channels.length);
                String sql = "SELECT ofPubsubNode.maxItems,count(*),max(ofPubsubItem.creationDate),ofPubsubNode.nodeId, ofPubsubNode.name "
                        + "FROM ofPubsubItem, ofPubsubNode " + "WHERE ofPubsubItem.serviceID=? AND "
                        + "      ofPubsubItem.nodeID = ofPubsubNode.nodeId AND " + "      ofPubsubItem.nodeID IN ("
                        + argList + ") " + dateRange + "GROUP BY ofPubsubItem.nodeID";
                pstmt = con.prepareStatement(sql);
                pstmt.setString(1, mPubSubModule.getServiceID());
                SQLHelper.bindArgList(pstmt, 2, channels);
                SQLHelper.bindArgList(pstmt, 2 + channels.length, dates);
                rs = pstmt.executeQuery();
                while (rs.next()) {
                    int maxItems = rs.getInt(1);
                    int count = rs.getInt(2);
                    Date creationDate = new Date(Long.parseLong(rs.getString(3).trim()));
                    String nodeId = rs.getString(4);
                    MMXChannelId channelId = ChannelHelper.parseNode(nodeId);
                    String channelName = rs.getString(5);
                    MMXChannelId channelIdWithOriginalName = new MMXChannelId(channelId.getUserId(), channelName);
                    resp.add(new ChannelSummary(channelIdWithOriginalName)
                            .setCount((maxItems < 0) ? count : Math.min(maxItems, count))
                            .setLastPubTime(creationDate));
                    // This channel has published items; remove it from the collection.
                    tpNoItems.remove(channelId);
                }
                start += channels.length;
            } while (start < numOfChannels);
            // Fill the response with the channels having no published items.
            Iterator<MMXChannelId> it = tpNoItems.iterator();
            while (it.hasNext()) {
                resp.add(new ChannelSummary(it.next()).setCount(0));
            }
            return resp;
        } catch (Exception sqle) {
            LOGGER.error(sqle.getMessage(), sqle);
            throw new MMXException(sqle.getMessage(), StatusCode.SERVER_ERROR.getCode());
        } finally {
            DbConnectionManager.closeConnection(rs, pstmt, con);
        }
    }

    public List<ChannelInfo> searchByTags(JID from, String appId, TagSearch rqt) throws MMXException {
        List<TopicEntity> entities;
        List<String> tags = rqt.getTags();
        TopicDAO channelDao = DBUtil.getTopicDAO();
        switch (rqt.getOperator()) {
        case AND:
            entities = channelDao.getTopicsForTagAND(tags, appId);
            break;
        case OR:
            entities = channelDao.getTopicsForTagOR(tags, appId);
            break;
        default:
            entities = new ArrayList<TopicEntity>(0);
            break;
        }
        // Convert the channel entity to channel info.
        List<ChannelInfo> res = new ArrayList<ChannelInfo>(entities.size());
        for (TopicEntity entity : entities) {
            MMXChannelId channel = ChannelHelper.parseNode(entity.getNodeId());
            ChannelInfo info = new ChannelInfo(channel.getUserId(), channel.getName(), !entity.isLeaf())
                    .setDescription(entity.getDescription()).setCreationDate(entity.getCreationDate())
                    .setMaxItems(entity.isPersistItems() ? entity.getMaxItems() : 0)
                    .setMaxPayloadSize(entity.getMaxPayloadSize()).setModifiedDate(entity.getModificationDate())
                    .setPublishPermission(ConfigureForm.convert(PublisherModel.valueOf(entity.getPublisherModel())))
                    .setPersistent(entity.isPersistItems()).setCreator(entity.getCreator())
                    .setSubscriptionEnabled(entity.isSubscriptionEnabled());
            res.add(info);
        }
        return res;
    }

    public ChannelAction.ChannelTags getTags(JID from, String appId, MMXChannelId rqt) throws MMXException {
        String channel = ChannelHelper.normalizePath(rqt.getName());
        String realChannel = ChannelHelper.makeChannel(appId, rqt.getEscUserId(), channel);
        Node node = mPubSubModule.getNode(realChannel);
        // No need to check for permission; just check for existing.
        if (node == null) {
            throw new MMXException(StatusCode.CHANNEL_NOT_FOUND.getMessage(channel),
                    StatusCode.CHANNEL_NOT_FOUND.getCode());
        }

        TagDAO tagDao = DBUtil.getTagDAO();
        String serviceId = node.getService().getServiceID();
        String nodeId = node.getNodeID();
        List<String> tags = new ArrayList<String>();
        try {
            tags = tagDao.getTagsForTopic(appId, serviceId, nodeId);
        } catch (Exception e) {
            LOGGER.error("getTags : caught exception for getting tags appId={}, serviceId={}, nodeId={}",
                    new Object[] { appId, serviceId, nodeId, e });

        }

        ChannelAction.ChannelTags channelTags = new ChannelAction.ChannelTags(rqt.getUserId(), rqt.getName(), tags,
                new Date());
        return channelTags;
    }

    public MMXStatus setTags(JID from, String appId, ChannelAction.ChannelTags rqt) throws MMXException {
        String channel = ChannelHelper.normalizePath(rqt.getChannelName());
        String realChannel = ChannelHelper.makeChannel(appId, rqt.getUserId(), channel);
        Node node = mPubSubModule.getNode(realChannel);
        // No need to check for permission; just check for existing.
        if (node == null) {
            LOGGER.trace("setTags : node not found appId={},  channel={}", appId, realChannel);
            throw new MMXException(StatusCode.CHANNEL_NOT_FOUND.getMessage(channel),
                    StatusCode.CHANNEL_NOT_FOUND.getCode());
        }
        List<String> tags = rqt.getTags();
        String serviceId = node.getService().getServiceID();
        String nodeId = node.getNodeID();

        TagDAO tagDao = DBUtil.getTagDAO();
        tagDao.deleteAllTagsForTopic(appId, serviceId, nodeId);

        if (!Utils.isNullOrEmpty(tags)) {
            for (String tag : tags) {
                try {
                    tagDao.createTopicTag(tag, appId, serviceId, nodeId);
                } catch (DbInteractionException e) {
                    return (new MMXStatus()).setCode(StatusCode.SERVER_ERROR.getCode()).setMessage(e.getMessage());
                }
            }
        }

        MMXStatus status = (new MMXStatus()).setCode(StatusCode.SUCCESS.getCode())
                .setMessage(StatusCode.SUCCESS.getMessage());
        return status;
    }

    public MMXStatus addTags(JID from, String appId, ChannelAction.ChannelTags rqt) throws MMXException {
        String channel = ChannelHelper.normalizePath(rqt.getChannelName());
        String realChannel = ChannelHelper.makeChannel(appId, rqt.getUserId(), channel);
        Node node = mPubSubModule.getNode(realChannel);
        // No need to check for permission; just check for existing.
        if (node == null) {
            throw new MMXException(StatusCode.CHANNEL_NOT_FOUND.getMessage(channel),
                    StatusCode.CHANNEL_NOT_FOUND.getCode());
        }
        List<String> tags = rqt.getTags();

        String serviceId = node.getService().getServiceID();
        String nodeId = node.getNodeID();

        if (!Utils.isNullOrEmpty(tags)) {
            TagDAO tagDao = DBUtil.getTagDAO();
            for (String tag : tags) {
                try {
                    LOGGER.trace("addTags : creating channel setting tag={}, appId={}, serviceId={}, nodeId={}",
                            new Object[] { tag, appId, serviceId, nodeId });

                    tagDao.createTopicTag(tag, appId, serviceId, nodeId);
                } catch (DbInteractionException e) {
                    LOGGER.error("addTags : caught exception setting tag={}, appId={}, serviceId={}, nodeId={}",
                            new Object[] { tag, appId, serviceId, nodeId, e });
                    return (new MMXStatus()).setCode(StatusCode.SERVER_ERROR.getCode()).setMessage(e.getMessage());
                }
            }
        } else {
            LOGGER.trace("addTags : received an empty tag list appId={}, serviceId={}, nodeId={}",
                    new Object[] { appId, serviceId, nodeId });
        }

        MMXStatus status = (new MMXStatus()).setCode(StatusCode.SUCCESS.getCode())
                .setMessage(StatusCode.SUCCESS.getMessage());
        return status;
    }

    public MMXStatus removeTags(JID from, String appId, ChannelAction.ChannelTags rqt) throws MMXException {
        String channel = ChannelHelper.normalizePath(rqt.getChannelName());
        String realChannel = ChannelHelper.makeChannel(appId, rqt.getUserId(), channel);
        Node node = mPubSubModule.getNode(realChannel);
        // No need to check for permission; just check for existing.
        if (node == null) {
            throw new MMXException(StatusCode.CHANNEL_NOT_FOUND.getMessage(channel),
                    StatusCode.CHANNEL_NOT_FOUND.getCode());
        }
        List<String> tags = rqt.getTags();
        String serviceId = node.getService().getServiceID();
        String nodeId = node.getNodeID();

        if (!Utils.isNullOrEmpty(tags)) {
            TagDAO tagDao = DBUtil.getTagDAO();
            tagDao.deleteTagsForTopic(tags, appId, serviceId, nodeId);
        }

        MMXStatus status = (new MMXStatus()).setCode(StatusCode.SUCCESS.getCode())
                .setMessage(StatusCode.SUCCESS.getMessage());
        return status;
    }

    public ChannelAction.ChannelQueryResponse searchChannel(JID from, String appId,
            TopicAction.TopicSearchRequest rqt, List<String> userRoles) throws MMXException {
        String userId = JIDUtil.getUserId(from);
        TopicQueryBuilder queryBuilder = new TopicQueryBuilder();
        int offset = rqt.getOffset();
        int size = rqt.getLimit();
        if (size > PubSubPersistenceManagerExt.MAX_ROWS_RETURN) {
            throw new MMXException(
                    StatusCode.VALUE_TOO_LARGE.getMessage(
                            "Max number of results cannot exceed " + PubSubPersistenceManagerExt.MAX_ROWS_RETURN),
                    StatusCode.VALUE_TOO_LARGE.getCode());
        } else if (size < 0) {
            size = PubSubPersistenceManagerExt.MAX_ROWS_RETURN;
        }
        PaginationInfo pgInfo = PaginationInfo.build(size, offset);
        QueryBuilderResult queryBuilderResult = queryBuilder.buildPaginationQuery(rqt, appId, pgInfo, userId,
                userRoles);
        SearchResult<TopicAction.TopicInfoWithSubscriptionCount> results = PubSubPersistenceManagerExt
                .getTopicWithPagination(new OpenFireDBConnectionProvider(), queryBuilderResult, pgInfo);

        List<ChannelInfo> channelList = new ArrayList<ChannelInfo>(results.getResults().size());

        for (TopicAction.TopicInfoWithSubscriptionCount ti : results.getResults()) {
            ChannelInfo info = new ChannelInfo(ti.getUserId(), ti.getName(), ti.isCollection())
                    .setDescription(ti.getDescription()).setCreationDate(ti.getCreationDate())
                    .setModifiedDate(ti.getModifiedDate()).setPublishPermission(ti.getPublisherType())
                    .setMaxPayloadSize(ti.getMaxPayloadSize()).setMaxItems(ti.isPersistent() ? ti.getMaxItems() : 0)
                    .setPersistent(ti.isPersistent()).setCreator(ti.getCreator())
                    .setSubscriptionEnabled(ti.isSubscriptionEnabled());
            channelList.add(info);
        }
        ChannelAction.ChannelQueryResponse resp = new ChannelAction.ChannelQueryResponse(results.getTotal(),
                channelList);
        return resp;
    }

    public MMXStatus processSendLastPublishedItems(JID from, String appId, SendLastPublishedItems rqt)
            throws MMXException {
        JID fromUser = from.asBareJID();
        Date since = rqt.getSince();
        if (since == null) {
            // Missing "since" here
            throw new MMXException(StatusCode.INVALID_DATE.getMessage(), StatusCode.INVALID_DATE.getCode());
        }

        // Don't query the DB directly because some items are cached in memory.
        // Besides, it is faster looping through all cached nodes in memory than
        // query the ofPubsubSubscription table because # of nodes should be many
        // less than # of subscriptions.

        String prefix = ChannelHelper.makePrefix(appId);

        // Find all collection nodes subscribed by the user.
        TreeMap<String, Node> colNodes = new TreeMap<String, Node>();
        for (Node node : mPubSubModule.getNodes()) {
            if (!node.getNodeID().startsWith(prefix)) {
                // Skip channels that do not belong to this app.
                continue;
            }
            if (node.isCollectionNode()) {
                Collection<NodeSubscription> subs = node.getSubscriptions(fromUser);
                if (subs != null && subs.size() > 0) {
                    colNodes.put(node.getNodeID(), node);
                    LOGGER.trace("Collection node=" + node.getNodeID() + " is subscribed");
                }
            }
        }

        int numSent = 0, numSubs = 0;
        int maxItems = rqt.getMaxItems();
        for (Node node : mPubSubModule.getNodes()) {
            if (!node.getNodeID().startsWith(prefix)) {
                // Skip channels that do not belong to this app.
                continue;
            }
            if (maxItems == 1) {
                // Check the leaf node if its last published item should be sent.
                PublishedItem item = node.getLastPublishedItem();
                //        if (item == null) {
                //          LOGGER.trace("No published items in subscribed node="+node.getNodeID());
                //        } else {
                //          LOGGER.trace("since="+since.getTime()+", creatDate="+item.getCreationDate().getTime()+
                //              ", node="+node.getNodeID()+", last pub item="+item.getID());
                //        }
                if (item == null || item.getCreationDate().getTime() < since.getTime()) {
                    // Skip all last published items older than the last delivery time.
                    continue;
                }
                Collection<NodeSubscription> subs = node.getSubscriptions(fromUser);
                if (subs == null || subs.size() == 0) {
                    // The leaf node has no subscriptions, check its ancestor for subscriptions.
                    Node ancestor = findAncestor(colNodes, node);
                    if (ancestor == null) {
                        continue;
                    }
                    subs = ancestor.getSubscriptions(fromUser);
                }
                // Either the leaf node or ancestor node has subscriptions with the
                // latest published item.
                numSubs += subs.size();
                for (NodeSubscription sub : subs) {
                    if (sendLastPublishedItem(item, sub, from)) {
                        ++numSent;
                        //            LOGGER.trace("Sent last published item="+item.getID()+
                        //                        ", sub ID="+sub.getID());
                    } else {
                        //            LOGGER.trace("cannot send last published item="+item.getID()+
                        //                        ", sub ID="+sub.getID());
                    }
                }
            } else {
                if (node.isCollectionNode()) {
                    continue;
                }
                Collection<NodeSubscription> subs = node.getSubscriptions(fromUser);
                if (subs == null || subs.size() == 0) {
                    // The leaf node is not subscribed, check its ancestor for subscriptions.
                    Node ancestor = findAncestor(colNodes, node);
                    if (ancestor == null) {
                        //            LOGGER.trace("No ancestor node is subscribed, skip "+node.getNodeID());
                        continue;
                    }
                    // Get the subscriptions from the ancestor.
                    subs = ancestor.getSubscriptions(fromUser);
                    if (subs.size() == 0) {
                        //            LOGGER.trace("No subscriptions in ancestor node "+ancestor.getNodeID());
                        continue;
                    }
                }

                //        LOGGER.trace("Fetch published items from "+node.getNodeID()+", since="+since);
                List<PublishedItem> items = PubSubPersistenceManagerExt.getPublishedItems((LeafNode) node, maxItems,
                        since);
                if (items == null || items.size() == 0) {
                    //          LOGGER.trace("No published items in "+node.getNodeID()+", since="+since);
                    continue;
                }

                // Either the leaf node or ancestor node has subscriptions with the
                // latest published item.
                numSubs += subs.size();
                for (NodeSubscription sub : subs) {
                    if (sendLastPublishedItems(items, sub, from)) {
                        numSent += items.size();
                        //            LOGGER.trace("Sent last published #items="+items.size()+
                        //                        ", sub ID="+sub.getID());
                    } else {
                        //            LOGGER.warn("cannot send last published items="+items.size()+
                        //                        ", sub ID="+sub.getID());
                    }
                }
            }
        }

        MMXStatus status = (new MMXStatus()).setCode(Constants.STATUS_CODE_200)
                .setMessage(numSubs + " subscriptions; " + numSent + " published items sent");
        return status;
    }

    private Node findAncestor(TreeMap<String, Node> map, Node node) {
        Entry<String, Node> entry = map.floorEntry(node.getNodeID());
        if ((entry != null) && node.getNodeID().startsWith(entry.getKey())) {
            return entry.getValue();
        } else {
            return null;
        }
    }

    private boolean sendLastPublishedItem(PublishedItem publishedItem, NodeSubscription subNode, JID to) {
        if (!subNode.canSendPublicationEvent(publishedItem.getNode(), publishedItem)) {
            return false;
        }
        Node node = subNode.getNode();
        Message notification = new Message();
        Element event = notification.getElement().addElement("event", "http://jabber.org/protocol/pubsub#event");
        Element items = event.addElement("items");
        items.addAttribute("node", node.getNodeID());
        Element item = items.addElement("item");
        if (publishedItem.getNode().isItemRequired()) {
            item.addAttribute("id", publishedItem.getID());
        }
        if (node.isPayloadDelivered() && publishedItem.getPayload() != null) {
            item.add(publishedItem.getPayload().createCopy());
        }
        // Add a message body (if required)
        if (subNode.isIncludingBody()) {
            notification.setBody(LocaleUtils.getLocalizedString("pubsub.notification.message.body"));
        }
        // Include date when published item was created
        notification.getElement().addElement("delay", "urn:xmpp:delay").addAttribute("stamp",
                XMPPDateTimeFormat.format(publishedItem.getCreationDate()));
        // Send the event notification to the subscriber
        node.getService().sendNotification(node, notification, to);
        //    node.getService().sendNotification(node, notification, subNode.getJID());
        return true;
    }

    private boolean sendLastPublishedItems(List<PublishedItem> publishedItems, NodeSubscription subNode, JID to) {
        PublishedItem pubItem = publishedItems.get(0);
        if (!subNode.canSendPublicationEvent(pubItem.getNode(), pubItem)) {
            return false;
        }
        Node node = subNode.getNode();
        Message notification = new Message();
        Element event = notification.getElement().addElement("event", "http://jabber.org/protocol/pubsub#event");
        Element items = event.addElement("items");
        items.addAttribute("node", node.getNodeID());
        for (PublishedItem publishedItem : publishedItems) {
            Element item = items.addElement("item");
            if (publishedItem.getNode().isItemRequired()) {
                item.addAttribute("id", publishedItem.getID());
            }
            if (node.isPayloadDelivered() && publishedItem.getPayload() != null) {
                item.add(publishedItem.getPayload().createCopy());
            }
        }
        // Add a message body (if required)
        if (subNode.isIncludingBody()) {
            notification.setBody(LocaleUtils.getLocalizedString("pubsub.notification.message.body"));
        }
        // Include date when published item was created
        notification.getElement().addElement("delay", "urn:xmpp:delay").addAttribute("stamp",
                XMPPDateTimeFormat.format(pubItem.getCreationDate()));
        // Send the event notification to the subscriber
        node.getService().sendNotification(node, notification, to);
        // node.getService().sendNotification(node, notification, subNode.getJID());
        return true;
    }

    /**
     * Create the <code>all</code> version channel of an OS.  All its parent channels
     * will be created if they do not exist.
     * @param creatorUserId The app server user ID.
     * @param appId The app ID.
     * @param osType The non-null OS type.
     * @param displayName The display name of the "all" versions.
     * @return
     */
    public MMXStatus createOSChannel(String creatorUserId, String appId, OSType osType, String displayName) {
        if (osType == null) {
            throw new NullPointerException("OS type is null");
        }
        String allChannel = ChannelHelper.makeOSChannel(osType, ChannelHelper.CHANNEL_LEAF_ALL);
        MMXTopicOptions options = new MMXTopicOptions().setMaxItems(-1);
        ChannelAction.CreateRequest rqt = new ChannelAction.CreateRequest(allChannel, false, options);
        try {
            // create the OS and all version channels
            JID jid = new JID(creatorUserId, mServer.getServerInfo().getXMPPDomain(), null);
            MMXStatus status = createChannel(jid, appId, rqt);
            return status;
        } catch (MMXException e) {
            e.printStackTrace();
            return new MMXStatus().setCode(e.getCode()).setMessage(e.getMessage());
        }
    }

    /**
     * Delete the OS channel and its children, or a specific OS type channel.
     * @param creatorUserId The app server user ID.
     * @param appId The app ID.
     * @param osType null for all OS, or a specific OS type.
     * @return
     */
    public MMXStatus deleteOSChannel(String creatorUserId, String appId, OSType osType) {
        String channel = ChannelHelper.makeOSChannel(osType, null);
        ChannelAction.DeleteRequest rqt = new ChannelAction.DeleteRequest(channel, false);
        try {
            // delete the OS channel and its children.
            JID jid = new JID(creatorUserId, mServer.getServerInfo().getXMPPDomain(), null);
            MMXStatus status = deleteChannel(jid, appId, rqt);
            return status;
        } catch (MMXException e) {
            e.printStackTrace();
            return new MMXStatus().setCode(e.getCode()).setMessage(e.getMessage());
        }
    }

    public ChannelAction.FetchResponse fetchItems(JID from, String appId, ChannelAction.FetchRequest rqt)
            throws MMXException {
        String channel = ChannelHelper.normalizePath(rqt.getChannel());
        String realChannel = ChannelHelper.makeChannel(appId, rqt.getUserId(), channel);
        ChannelAction.FetchOptions options = rqt.getOptions();
        String subId = null;
        Date since = null;
        Date until = null;
        boolean ascending = false;
        int maxItems = 0;
        int offset = 0;
        if (options != null) {
            subId = options.getSubId();
            since = options.getSince();
            until = options.getUntil();
            ascending = options.isAscending();
            maxItems = options.getMaxItems();
            offset = options.getOffset();
        }
        // If not defined, default to system property ("xmpp.pubsub.fetch.max")
        if (maxItems <= 0) {
            maxItems = -1;
        }
        // If not defined, default to the epoch.
        if (since == null) {
            since = new Date(0L);
        }
        // If not defined, default to current time.
        if (until == null) {
            until = new Date();
        }

        Node node = mPubSubModule.getNode(realChannel);
        if (node == null) {
            throw new MMXException(StatusCode.CHANNEL_NOT_FOUND.getMessage(channel),
                    StatusCode.CHANNEL_NOT_FOUND.getCode());
        }
        if (node.isCollectionNode()) {
            throw new MMXException("Cannot fetch items from a collection channel",
                    StatusCode.NOT_IMPLEMENTED.getCode());
        }
        // Check if sender and subscriber JIDs match or if a valid "trusted proxy" is being used
        // Assumed that the owner of the subscription is the bare JID of the subscription JID.
        JID owner = from.asBareJID();
        if (!node.getAccessModel().canAccessItems(node, owner, from)) {
            throw new MMXException(StatusCode.FORBIDDEN.getMessage(channel), StatusCode.FORBIDDEN.getCode());
        }
        // Check that the requester is not an outcast
        NodeAffiliate affiliate = node.getAffiliate(owner);
        if (affiliate != null && affiliate.getAffiliation() == NodeAffiliate.Affiliation.outcast) {
            throw new MMXException(StatusCode.FORBIDDEN.getMessage(channel), StatusCode.FORBIDDEN.getCode());
        }
        // Check if the specified subId belongs to an existing node subscription
        NodeSubscription subscription = null;
        if (subId != null) {
            if ((((subscription = node.getSubscription(from)) == null)
                    && ((subscription = node.getSubscription(owner)) == null))
                    || !subId.equals(subscription.getID())) {
                throw new MMXException(StatusCode.NOT_ACCEPTABLE.getMessage("Invalid subscription ID"),
                        StatusCode.NOT_ACCEPTABLE.getCode());
            }
            if (subscription != null && !subscription.isActive()) {
                throw new MMXException(StatusCode.NOT_AUTHORIZED.getMessage("channel not suscribed"),
                        StatusCode.NOT_AUTHORIZED.getCode());
            }
        }

        List<PublishedItem> pubItems = PubSubPersistenceManagerExt.getPublishedItems((LeafNode) node, offset,
                maxItems, since, until, ascending);
        List<MMXPublishedItem> mmxItems = new ArrayList<MMXPublishedItem>(pubItems.size());
        for (PublishedItem pubItem : pubItems) {
            MMXPublishedItem mmxItem = new MMXPublishedItem(pubItem.getID(), pubItem.getPublisher().toBareJID(),
                    pubItem.getCreationDate(), pubItem.getPayloadXML());
            mmxItems.add(mmxItem);
        }
        int total = PubSubPersistenceManagerExt.getPublishedItemCount((LeafNode) node, since, until);
        ChannelAction.FetchResponse resp = new ChannelAction.FetchResponse(rqt.getUserId(), channel, total,
                mmxItems);
        return resp;
    }

    public ChannelAction.FetchResponse getItems(JID from, String appId, ChannelAction.ItemsByIdsRequest rqt)
            throws MMXException {
        String channel = ChannelHelper.normalizePath(rqt.getChannel());
        String realChannel = ChannelHelper.makeChannel(appId, rqt.getUserId(), channel);
        Node node = mPubSubModule.getNode(realChannel);
        if (node == null) {
            throw new MMXException(StatusCode.CHANNEL_NOT_FOUND.getMessage(channel),
                    StatusCode.CHANNEL_NOT_FOUND.getCode());
        }
        if (node.isCollectionNode()) {
            throw new MMXException("Cannot get items from a collection channel",
                    StatusCode.NOT_IMPLEMENTED.getCode());
        }

        LeafNode leafNode = (LeafNode) node;
        List<String> itemIds = rqt.getItemIds();
        if (itemIds == null || itemIds.isEmpty()) {
            throw new MMXException(StatusCode.BAD_REQUEST.getMessage("no item ID's"),
                    StatusCode.BAD_REQUEST.getCode());
        }
        // Check if sender and subscriber JIDs match or if a valid "trusted proxy" is being used
        // Assumed that the owner of the subscription is the bare JID of the subscription JID.
        JID owner = from.asBareJID();
        if (!node.getAccessModel().canAccessItems(node, owner, from)) {
            throw new MMXException(StatusCode.FORBIDDEN.getMessage(channel), StatusCode.FORBIDDEN.getCode());
        }
        // Check that the requester is not an outcast
        NodeAffiliate affiliate = node.getAffiliate(owner);
        if (affiliate != null && affiliate.getAffiliation() == NodeAffiliate.Affiliation.outcast) {
            throw new MMXException(StatusCode.FORBIDDEN.getMessage(channel), StatusCode.FORBIDDEN.getCode());
        }

        // TODO: do we need to check for subscription first?
        List<MMXPublishedItem> mmxItems = new ArrayList<MMXPublishedItem>(itemIds.size());
        for (String itemId : itemIds) {
            if (itemId == null) {
                throw new MMXException(StatusCode.BAD_REQUEST.getMessage("null item ID"),
                        StatusCode.BAD_REQUEST.getCode());
            }
            PublishedItem pubItem = leafNode.getPublishedItem(itemId);
            if (pubItem == null) {
                // Ignored the invalid item ID.
                continue;
            }
            MMXPublishedItem mmxItem = new MMXPublishedItem(pubItem.getID(), pubItem.getPublisher().toBareJID(),
                    pubItem.getCreationDate(), pubItem.getPayloadXML());
            mmxItems.add(mmxItem);
        }
        ChannelAction.FetchResponse resp = new ChannelAction.FetchResponse(rqt.getUserId(), channel,
                mmxItems.size(), mmxItems);
        return resp;
    }

    public ChannelAction.SubscribersResponse getSubscribers(JID from, String appId,
            ChannelAction.SubscribersRequest rqt) throws MMXException {
        String channel = ChannelHelper.normalizePath(rqt.getChannel());
        String realChannel = ChannelHelper.makeChannel(appId, rqt.getUserId(), channel);
        Node node = mPubSubModule.getNode(realChannel);
        if (node == null) {
            throw new MMXException(StatusCode.CHANNEL_NOT_FOUND.getMessage(channel),
                    StatusCode.CHANNEL_NOT_FOUND.getCode());
        }

        if (rqt.getUserId() != null) {
            //do the affiliation check only for personal channels
            JID requester = from.asBareJID();
            // Check if the requester has any affiliations but not outcast affiliation.
            NodeAffiliate nodeAffiliate = node.getAffiliate(requester);
            if (nodeAffiliate == null || nodeAffiliate.getAffiliation() == NodeAffiliate.Affiliation.outcast) {
                throw new MMXException(StatusCode.FORBIDDEN.getMessage(channel), StatusCode.FORBIDDEN.getCode());
            }
        }

        Collection<NodeSubscription> allSubscriptions = node.getAllSubscriptions();
        /**
         * all subscriptions has all subscriptions in all possible states. We need
         * cull out subscriptions in state == subscribed.
         */
        int count = 0;
        TreeSet<String> subscriberUserNameSet = new TreeSet<String>();
        for (NodeSubscription ns : allSubscriptions) {
            if ((ns.getState() != null) && (ns.getState() == NodeSubscription.State.subscribed)) {
                JID subscriberJID = ns.getJID();
                String subscriberJIDNode = subscriberJID.getNode();
                String username = subscriberJIDNode;
                subscriberUserNameSet.add(username);
                count++;
            }
        }
        List<com.magnet.mmx.protocol.UserInfo> userInfoList = new LinkedList<com.magnet.mmx.protocol.UserInfo>();
        if (count > rqt.getOffset()) {
            UserDAO userDAO = new UserDAOImpl(getConnectionProvider());
            int addedCount = 0; //for applying the limit
            int index = 0;
            for (String username : subscriberUserNameSet) {
                if (index++ < rqt.getOffset()) {
                    continue;
                }

                if (rqt.getLimit() > 0 && addedCount >= rqt.getLimit()) {
                    break;
                }
                //TODO: Improve this
                UserEntity userEntity = userDAO.getUser(username);
                if (userEntity != null) {
                    com.magnet.mmx.protocol.UserInfo userInfo = UserEntity.toUserInfo(userEntity);
                    userInfoList.add(userInfo);
                    addedCount++;
                }
            }
        }

        ChannelAction.SubscribersResponse resp = new ChannelAction.SubscribersResponse().setTotal(count)
                .setSubscribers(userInfoList);
        resp.setCode(StatusCode.SUCCESS.getCode()).setMessage(StatusCode.SUCCESS.getMessage());
        return resp;
    }

    public ChannelAction.SubscribersResponse getSubscribersFromNode(JID from, String appId, int offset, int size,
            Node node) throws MMXException {
        Collection<NodeSubscription> allSubscriptions = node.getAllSubscriptions();
        /**
         * all subscriptions has all subscriptions in all possible states. We need
         * cull out subscriptions in state == subscribed.
         */
        int count = 0;
        TreeSet<String> subscriberUserNameSet = new TreeSet<String>();
        for (NodeSubscription ns : allSubscriptions) {
            if ((ns.getState() != null) && (ns.getState() == NodeSubscription.State.subscribed)) {
                JID subscriberJID = ns.getJID();
                String subscriberJIDNode = subscriberJID.getNode();
                String username = subscriberJIDNode;
                subscriberUserNameSet.add(username);
                count++;
            }
        }
        List<com.magnet.mmx.protocol.UserInfo> userInfoList = new LinkedList<com.magnet.mmx.protocol.UserInfo>();
        if (count > offset) {
            UserDAO userDAO = new UserDAOImpl(getConnectionProvider());
            int addedCount = 0; //for applying the limit
            int index = 0;
            for (String username : subscriberUserNameSet) {
                if (index++ < offset) {
                    continue;
                }

                if (size > 0 && addedCount >= size) {
                    break;
                }
                //TODO: Improve this
                UserEntity userEntity = userDAO.getUser(username);
                if (userEntity != null) {
                    com.magnet.mmx.protocol.UserInfo userInfo = UserEntity.toUserInfo(userEntity);
                    userInfoList.add(userInfo);
                    addedCount++;
                }
            }
        }

        ChannelAction.SubscribersResponse resp = new ChannelAction.SubscribersResponse().setTotal(count)
                .setSubscribers(userInfoList);
        resp.setCode(StatusCode.SUCCESS.getCode()).setMessage(StatusCode.SUCCESS.getMessage());
        return resp;
    }

    public ConnectionProvider getConnectionProvider() {
        return new OpenFireDBConnectionProvider();
    }

    private TopicRoleDAO getTopicRoleDAO() {
        return new TopicRoleDAOImpl(new OpenFireDBConnectionProvider());
    }

    /**
     * Check if user with supplied roles has access to the channel identified by nodeId.
     * This API shouldn't be called for personal channel. It works with global channels only.
     * @param nodeId
     * @param userRoles
     * @return
     */
    private boolean isAllowed(String nodeId, List<String> userRoles) {
        TopicRoleDAO roleDAO = getTopicRoleDAO();
        List<String> channelRoles = roleDAO.getTopicRoles("pubsub", nodeId);
        boolean userHasRole = false;
        Iterator<String> userRoleIterator = userRoles.iterator();

        while (userRoleIterator.hasNext() && !userHasRole) {
            String userRole = userRoleIterator.next();
            int index = Collections.binarySearch(channelRoles, userRole);
            userHasRole = index > -1;
        }
        return userHasRole;
    }

    /**
     * Enum for the status codes
     */
    public static enum StatusCode {
        SUCCESS(200, "Success"), INVALID_COMMAND(400, "Invalid command: "), INVALID_DATE(400,
                "Invalid date"), CHANNEL_EXISTS(409, "Channel already exists: "), CHANNEL_NOT_FOUND(404,
                        "Channel not found: "), APP_NODE_NOT_FOUND(500,
                                "Internal error; application root node is missing: "), SUBSCRIPTION_EXISTS(409,
                                        "Subscription already exists: "), SUBSCRIPTION_NOT_FOUND(404,
                                                "Subscription not found: "), ITEM_NOT_FOUND(404,
                                                        "Item not found: "), GONE(410,
                                                                "Subscription is no longer available"), CONFLICT(
                                                                        409,
                                                                        "Conflict in the request"), FORBIDDEN(403,
                                                                                "Request is denied: "), BAD_REQUEST(
                                                                                        400,
                                                                                        "Bad Request"), VALUE_TOO_LARGE(
                                                                                                400,
                                                                                                "Bad Request: "), NOT_AUTHORIZED(
                                                                                                        401,
                                                                                                        "Not authorized: "), NOT_ACCEPTABLE(
                                                                                                                406,
                                                                                                                "Instant channel creation is disabled"), SERVER_ERROR(
                                                                                                                        500,
                                                                                                                        "Server error; please check the server log"), NOT_IMPLEMENTED(
                                                                                                                                501,
                                                                                                                                "Feature not implemented: "), INVALID_CHANNEL_NAME(
                                                                                                                                        400,
                                                                                                                                        "Channel name should be less than 50 characters long and can only have numbers, letters, hyphen, underscores, and dashes");

        private int code;
        private String message;

        StatusCode(int c, String m) {
            code = c;
            message = m;
        }

        public int getCode() {
            return code;
        }

        public String getMessage() {
            return message;
        }

        public String getMessage(String arg) {
            return message + arg;
        }
    }
}