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

Java tutorial

Introduction

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

Source

/**
 * $RCSfile: $
 * $Revision: $
 * $Date: $
 *
 * Copyright (C) 2005-2008 Jive Software. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.adspore.splat.xep0060;

import java.util.*;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;

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

import com.adspore.splat.xep0060.IPublishCriteria.ItemPublishOperation;
import com.adspore.util.LocaleUtils;

/**
 * A type of node that contains published items only. It is NOT a container for
 * other nodes.
 *
 * @author Matt Tucker
 */
public class LeafNode extends Node {
    private static final Logger LOG = LoggerFactory.getLogger(LeafNode.class);

    private static final String genIdSeed = UUID.randomUUID().toString();
    private static final AtomicLong sequenceCounter = new AtomicLong();

    /**
     * Flag that indicates whether to persist items to storage. Note that when the
     * variable is false then the last published item is the only items being saved
     * to the backend storage.
     */
    private boolean persistPublishedItems;

    /**
     * Maximum number of published items to persist. Note that all nodes are going to persist
     * their published items. The only difference is the number of the last published items
     * to be persisted. Even nodes that are configured to not use persitent items are going
     * to save the last published item.
     */
    private int maxPublishedItems;

    /**
     * The maximum payload size in bytes.
     */
    private int maxPayloadSize;

    /**
     * Flag that indicates whether to send items to new subscribers.
     */
    private boolean sendItemSubscribe;

    /**
     * Configure a single Identity element that can be used as a template for all further
     * Collection nodes.
     */
    public static final Element mIdentity;
    static {
        mIdentity = DocumentHelper.createElement("identity");
        mIdentity.addAttribute("category", "pubsub");
        mIdentity.addAttribute("type", "leaf");
    }

    /**
     * The last item published to this node.  In a cluster this may have occurred on a different cluster node.
     */
    private Map<String, PublishedItem> mPublishedItems = new ConcurrentHashMap<String, PublishedItem>();

    // TODO Add checking of max payload size. Return <not-acceptable> plus a application specific error condition of <payload-too-big/>.

    public LeafNode(PubSubService service, CollectionNode parentNode, String nodeID, JID creator) {
        super(service, parentNode, nodeID, creator);
        // Configure node with default values (get them from the pubsub service)
        DefaultNodeConfiguration defaultConfiguration = mService.getDefaultNodeConfiguration(true);
        this.persistPublishedItems = defaultConfiguration.isPersistPublishedItems();
        this.maxPublishedItems = defaultConfiguration.getMaxPublishedItems();
        this.maxPayloadSize = defaultConfiguration.getMaxPayloadSize();
        this.sendItemSubscribe = defaultConfiguration.isSendItemSubscribe();
    }

    @Override
    public Element getIdentity() {
        return mIdentity;
    }

    @Override
    protected void configure(FormField field) throws NotAcceptableException {
        List<String> values;
        String booleanValue;
        if ("pubsub#persist_items".equals(field.getVariable())) {
            values = field.getValues();
            booleanValue = (values.size() > 0 ? values.get(0) : "1");
            persistPublishedItems = "1".equals(booleanValue);
        } else if ("pubsub#max_payload_size".equals(field.getVariable())) {
            values = field.getValues();
            maxPayloadSize = values.size() > 0 ? Integer.parseInt(values.get(0)) : 5120;
        } else if ("pubsub#send_item_subscribe".equals(field.getVariable())) {
            values = field.getValues();
            booleanValue = (values.size() > 0 ? values.get(0) : "1");
            sendItemSubscribe = "1".equals(booleanValue);
        }
    }

    @Override
    void postConfigure(DataForm completedForm) {
        List<String> values;
        if (!persistPublishedItems) {
            // Always save the last published item when not configured to use persistent items
            maxPublishedItems = 1;
        } else {
            FormField field = completedForm.getField("pubsub#max_items");
            if (field != null) {
                values = field.getValues();
                maxPublishedItems = values.size() > 0 ? Integer.parseInt(values.get(0)) : 50;
            }
        }
    }

    @Override
    protected void addFormFields(DataForm form, boolean isEditing) {
        super.addFormFields(form, isEditing);

        FormField typeField = form.getField("pubsub#node_type");
        typeField.addValue("leaf");

        FormField formField = form.addField();
        formField.setVariable("pubsub#send_item_subscribe");
        if (isEditing) {
            formField.setType(FormField.Type.boolean_type);
            formField.setLabel(LocaleUtils.getLocalizedString("pubsub.form.conf.send_item_subscribe"));
        }
        formField.addValue(sendItemSubscribe);

        formField = form.addField();
        formField.setVariable("pubsub#persist_items");
        if (isEditing) {
            formField.setType(FormField.Type.boolean_type);
            formField.setLabel(LocaleUtils.getLocalizedString("pubsub.form.conf.persist_items"));
        }
        formField.addValue(persistPublishedItems);

        formField = form.addField();
        formField.setVariable("pubsub#max_items");
        if (isEditing) {
            formField.setType(FormField.Type.text_single);
            formField.setLabel(LocaleUtils.getLocalizedString("pubsub.form.conf.max_items"));
        }
        formField.addValue(maxPublishedItems);

        formField = form.addField();
        formField.setVariable("pubsub#max_payload_size");
        if (isEditing) {
            formField.setType(FormField.Type.text_single);
            formField.setLabel(LocaleUtils.getLocalizedString("pubsub.form.conf.max_payload_size"));
        }
        formField.addValue(maxPayloadSize);

    }

    @Override
    protected void deletingNode() {
    }

    public synchronized void setLastPublishedItem(PublishedItem item) {
        mPublishedItems.put(item.getItemKey(), item);
    }

    public int getMaxPayloadSize() {
        return maxPayloadSize;
    }

    public boolean isPersistPublishedItems() {
        return persistPublishedItems;
    }

    public int getMaxPublishedItems() {
        return maxPublishedItems;
    }

    /**
     * Returns true if an item element is required to be included when publishing an
     * item to this node. When an item is included then the item will have an item ID
     * that will be included when sending items to node subscribers.<p>
     *
     * Leaf nodes that are transient and do not deliver payloads with event notifications
     * do not require an item element. If a user tries to publish an item to a node
     * that does not require items then an error will be returned.
     *
     * @return true if an item element is required to be included when publishing an
     *         item to this node.
     */
    public boolean isItemRequired() {
        return isPersistPublishedItems() || isPayloadDelivered();
    }

    /**
     * Publishes the list of items to the node. Event notifications will be sent to subscribers
     * for the new published event. The published event may or may not include an item. When the
     * node is not persistent and does not require payloads then an item is not going to be created
     * nore included in the event notification.<p>
     *
     * When an affiliate has many subscriptions to the node, the affiliate will get a
     * notification for each set of items that affected the same list of subscriptions.<p>
     *
     * When an item is included in the published event then a new {@link PublishedItem} is
     * going to be created and added to the list of published item. Each published item will
     * have a unique ID in the node scope. The new published item will be added to the end
     * of the published list to keep the cronological order. When the max number of published
     * items is exceeded then the oldest published items will be removed.<p>
     *
     * For performance reasons the newly added published items and the deleted items (if any)
     * are saved to the database using a background thread. Sending event notifications to
     * node subscribers may also use another thread to ensure good performance.<p>
     *
     * @param publisher the full JID of the user that sent the new published event.
     * @param itemElements list of dom4j elements that contain info about the published items.
     */
    public void publishItems(JID publisher, List<Element> itemElements) {
        List<PublishedItem> newPublishedItems = new ArrayList<PublishedItem>();
        if (isItemRequired()) {
            String itemID;
            Element payload;
            PublishedItem newItem;
            for (Element item : itemElements) {
                itemID = item.attributeValue("id");
                List entries = item.elements();
                payload = entries.isEmpty() ? null : (Element) entries.get(0);

                // Make sure that the published item has a unique ID if NOT assigned by publisher
                if (itemID == null) {
                    itemID = genIdSeed + sequenceCounter.getAndIncrement();
                }

                // Create a new published item
                newItem = new PublishedItem(this, publisher, itemID, new Date(mService.mContext.getContextTime()));
                newItem.setPayload(payload);
                // Add the new item to the list of published items
                newPublishedItems.add(newItem);
                setLastPublishedItem(newItem);
            }
        }

        // Build event notification packet to broadcast to subscribers
        Message message = new Message();
        Element event = message.addChildElement("event", "http://jabber.org/protocol/pubsub#event");

        // Broadcast event notification to subscribers and parent node subscribers
        Set<NodeAffiliate> affiliatesToNotify = new HashSet<NodeAffiliate>(affiliates);

        // Get affiliates that are subscribed to a parent in the hierarchy of parent nodes
        for (CollectionNode parentNode : getParents()) {
            for (NodeSubscription subscription : parentNode.getSubscriptions()) {
                affiliatesToNotify.add(subscription.getAffiliate());
            }
        }
        mService.submitTask(
                new NotifyAffiliatesCallable(affiliatesToNotify, message, event, this, newPublishedItems));
    }

    /**
     *
     */
    private class NotifyAffiliatesCallable implements Callable<String> {
        private final Collection<NodeAffiliate> mAffiliates;
        private final Message mMessage;
        private final Element mEvent;
        private final LeafNode mLeafNode;
        private final List<PublishedItem> mItems;

        public NotifyAffiliatesCallable(Collection<NodeAffiliate> affiliates, Message message, Element event,
                LeafNode leafNode, List<PublishedItem> items) {
            mAffiliates = affiliates;
            mMessage = message;
            mEvent = event;
            mLeafNode = leafNode;
            mItems = items;
        }

        @Override
        public String call() throws Exception {
            String result = "SUCCESS";
            try {
                for (NodeAffiliate affiliate : mAffiliates) {
                    affiliate.sendPublishedNotifications(mMessage, mEvent, mLeafNode, mItems);
                }
            } catch (Exception e) {
                result = "FAIL:" + e.getMessage() + " " + e.getCause();
            }
            return result;
        }
    }

    /**
     * Publishes an item or retracts an item based on the caller provided criteria. Provides a means
     * of controlling PublishedItem visibility on a user by user basis outside of the subscription
     * scheme.
     * @param publisher JID of the entity responsible for the publishing/retracting/ignoring operation
     * @param publishCriteria Caller supplied test that will be applied to each subscriber
     * @param itemElements List of 'item' elements which are to be published.
     */
    public void publishOrRetractByCriteria(JID publisher, IPublishCriteria publishCriteria,
            List<Element> itemElements) {
        List<PublishedItem> newPublishedItems = new ArrayList<PublishedItem>();

        if (isItemRequired()) {
            String itemID;
            Element payload;
            PublishedItem newItem;
            for (Element item : itemElements) {
                itemID = item.attributeValue("id");
                List entries = item.elements();
                payload = entries.isEmpty() ? null : (Element) entries.get(0);

                // Make sure that the published item has a unique ID if NOT assigned by publisher
                if (itemID == null) {
                    itemID = genIdSeed + sequenceCounter.getAndIncrement();
                }

                // Create a new published item
                newItem = new PublishedItem(this, publisher, itemID, new Date(mService.mContext.getContextTime()));
                newItem.setPayload(payload);
                // Add the new item to the list of published items
                newPublishedItems.add(newItem);
                setLastPublishedItem(newItem);
            }
        }

        // Build both event notification packets to broadcast to subscribers
        Message message = new Message();
        Element event = message.addChildElement("event", "http://jabber.org/protocol/pubsub#event");

        Set<NodeAffiliate> affiliatesToNotify = new HashSet<NodeAffiliate>(affiliates);
        for (CollectionNode parentNode : getParents()) {
            for (NodeSubscription subscription : parentNode.getSubscriptions()) {
                affiliatesToNotify.add(subscription.getAffiliate());
            }
        }

        for (NodeAffiliate affiliate : affiliatesToNotify) {
            ItemPublishOperation operation = publishCriteria.getOperationForAffiliate(this, affiliate,
                    newPublishedItems);
            switch (operation) {
            case publish:
                if (isPayloadDelivered()) {
                    affiliate.sendPublishedNotifications(message, event, this, newPublishedItems);
                } else {
                    affiliate.sendPublishedNotifications(message, event, this, Collections.EMPTY_LIST);
                }
                break;

            case retract:
                if (isNotifiedOfRetract()) {
                    affiliate.sendDeletionNotifications(message, event, this, newPublishedItems);
                }
                break;

            case none:
                //   Do nothing, don't send message but don't retract either..'no-op'
                break;
            }
        }
    }

    /**
     * Deletes the list of published items from the node. Event notifications may be sent to
     * subscribers for the deleted items. When an affiliate has many subscriptions to the node,
     * the affiliate will get a notification for each set of items that affected the same list
     * of subscriptions.<p>
     *
     * For performance reasons the deleted published items are saved to the database
     * using a background thread. Sending event notifications to node subscribers may
     * also use another thread to ensure good performance.<p>
     *
     * @param toDelete list of items that were deleted from the node.
     */
    public void deleteItems(List<PublishedItem> toDelete) {
        // Remove deleted items from the database
        for (PublishedItem item : toDelete) {
            LOG.debug("Would have removed node");
            mPublishedItems.remove(item.getItemKey());
        }
        if (isNotifiedOfRetract()) {
            // Broadcast notification deletion to subscribers
            // Build packet to broadcast to subscribers
            Message message = new Message();
            Element event = message.addChildElement("event", "http://jabber.org/protocol/pubsub#event");
            // Send notification that items have been deleted to subscribers and parent node
            // subscribers
            Set<NodeAffiliate> affiliatesToNotify = new HashSet<NodeAffiliate>(affiliates);
            // Get affiliates that are subscribed to a parent in the hierarchy of parent nodes
            for (CollectionNode parentNode : getParents()) {
                for (NodeSubscription subscription : parentNode.getSubscriptions()) {
                    affiliatesToNotify.add(subscription.getAffiliate());
                }
            }
            // TODO Use another thread for this (if # of subscribers is > X)????
            for (NodeAffiliate affiliate : affiliatesToNotify) {
                affiliate.sendDeletionNotifications(message, event, this, toDelete);
            }
        }
    }

    /**
     * Sends an IQ result with the list of items published to the node. Item ID and payload
     * may be included in the result based on the node configuration.
     *
     * @param originalRequest the IQ packet sent by a subscriber (or anyone) to get the node items.
     * @param publishedItems the list of published items to send to the subscriber.
     * @param forceToIncludePayload true if the item payload should be include if one exists. When
     *        false the decision is up to the node.
     */
    void sendPublishedItems(IQ originalRequest, List<PublishedItem> publishedItems, boolean forceToIncludePayload) {
        IQ result = IQ.createResultIQ(originalRequest);
        Element pubsubElem = result.setChildElement("pubsub", "http://jabber.org/protocol/pubsub");
        Element items = pubsubElem.addElement("items");
        items.addAttribute("node", getNodeID());

        for (PublishedItem publishedItem : publishedItems) {
            Element item = items.addElement("item");
            if (isItemRequired()) {
                item.addAttribute("id", publishedItem.getID());
            }
            if ((forceToIncludePayload || isPayloadDelivered()) && publishedItem.getPayload() != null) {
                item.add(publishedItem.getPayload().createCopy());
            }
        }
        // Send the result
        mService.send(result);
    }

    @Override
    public PublishedItem getPublishedItem(String itemID) {
        if (!isItemRequired()) {
            return mPublishedItems.get(itemID);
        }
        return null;
    }

    @Override
    public List<PublishedItem> getPublishedItems() {
        ArrayList<PublishedItem> result = new ArrayList<PublishedItem>();
        result.addAll(mPublishedItems.values());
        return result;
    }

    @Override
    public List<PublishedItem> getPublishedItems(int recentItems) {
        if (recentItems < 1) {
            return Collections.emptyList();
        }

        List<PublishedItem> result = getPublishedItems();
        Collections.sort(result, new DateSort());
        return result.subList(0, (recentItems - 1));
    }

    private static class DateSort implements Comparator<PublishedItem> {
        public int compare(PublishedItem o1, PublishedItem o2) {
            if (o1.getCreationDate().before(o2.getCreationDate())) {
                return -1;
            } else if (o1.getCreationDate().after(o2.getCreationDate())) {
                return 1;
            } else {
                return 0;
            }
        }
    }

    @Override
    public synchronized PublishedItem getLastPublishedItem() {
        if (mPublishedItems.isEmpty()) {
            return null;
        }

        List<PublishedItem> result = getPublishedItems();
        Collections.sort(result, new DateSort());
        return result.get(0);
    }

    /**
     * Returns true if the last published item is going to be sent to new subscribers.
     *
     * @return true if the last published item is going to be sent to new subscribers.
     */
    @Override
    public boolean isSendItemSubscribe() {
        return sendItemSubscribe;
    }

    void setMaxPayloadSize(int maxPayloadSize) {
        this.maxPayloadSize = maxPayloadSize;
    }

    void setPersistPublishedItems(boolean persistPublishedItems) {
        this.persistPublishedItems = persistPublishedItems;
    }

    void setMaxPublishedItems(int maxPublishedItems) {
        this.maxPublishedItems = maxPublishedItems;
    }

    void setSendItemSubscribe(boolean sendItemSubscribe) {
        this.sendItemSubscribe = sendItemSubscribe;
    }

    /**
     * Purges items that were published to the node. Only owners can request this operation.
     * This operation is only available for nodes configured to store items in the database. All
     * published items will be deleted with the exception of the last published item.
     */
    public void purge() {
        //PubSubPersistenceManager.purgeNode(this);
        // Broadcast purge notification to subscribers
        // Build packet to broadcast to subscribers
        Message message = new Message();
        Element event = message.addChildElement("event", "http://jabber.org/protocol/pubsub#event");
        Element items = event.addElement("purge");
        items.addAttribute("node", mNodeID);
        // Send notification that the node configuration has changed
        broadcastNodeEvent(message, false);
    }
}