/*
 * 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 org.jivesoftware.openfire.pubsub;

import java.text.ParseException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.List;

import org.dom4j.Element;
import org.jivesoftware.util.JiveGlobals;
import org.jivesoftware.util.LocaleUtils;
import org.jivesoftware.util.XMPPDateTimeFormat;
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 org.xmpp.packet.Presence;

import static org.jivesoftware.openfire.muc.spi.IQOwnerHandler.parseFirstValueAsBoolean;

/**
 * A subscription to a node. Entities may subscribe to a node to be notified when new events
 * are published to the node. Published events may contain a {@link PublishedItem}. Only
 * nodes that are configured to not deliver payloads with event notifications and to not
 * persist items will let publishers to publish events without items thus not including
 * items in the notifications sent to subscribers.<p>
 *
 * Node subscriptions may need to be configured by the subscriber or approved by a node owner
 * to become active. The node configuration establishes whether configuration or approval are
 * required. In any case, the subscriber will not get event notifications until the subscription
 * is active.<p>
 *
 * Depending on the node configuration it may be possible for the same subscriber to subscribe
 * multiple times to the node. Each subscription may have a different configuration like for
 * instance different keywords. Keywords can be used as a way to filter the type of
 * {@link PublishedItem} to be notified of. When the same subscriber has subscribed multiple
 * times to the node a single notification is going to be sent to the subscriber instead of
 * sending a notification for each subscription.
 *
 * @author Matt Tucker
 */
public class NodeSubscription {

    private static final Logger Log = LoggerFactory.getLogger(NodeSubscription.class);

    private static final XMPPDateTimeFormat xmppDateTime = new XMPPDateTimeFormat();

    /**
     * The node to which this subscription is interested in.
     */
    private Node node;
    /**
     * JID of the entity that will receive the event notifications.
     */
    private JID jid;
    /**
     * JID of the entity that owns this subscription. This JID is the JID of the
     * NodeAffiliate that is subscribed to the node.
     */
    private JID owner;
    /**
     * ID that uniquely identifies the subscription of the user in the node.
     */
    private String id;
    /**
     * Current subscription state.
     */
    private State state;
    /**
     * Flag indicating whether an entity wants to receive or has disabled notifications.
     */
    private boolean deliverNotifications = true;
    /**
     * Flag indicating whether an entity wants to receive digests (aggregations) of
     * notifications or all notifications individually.
     */
    private boolean usingDigest = false;
    /**
     * The minimum number of milliseconds between sending any two notification digests.
     * Default is 24 hours.
     */
    private int digestFrequency = 86400000;
    /**
     * The Date at which a leased subscription will end or has ended. A value of
     * {@code null} means that the subscription will never expire.
     */
    private Date expire = null;
    /**
     * Flag indicating whether an entity wants to receive an XMPP message body in
     * addition to the payload format.
     */
    private boolean includingBody = false;
    /**
     * The presence states for which an entity wants to receive notifications.
     */
    private Collection<String> presenceStates = new ArrayList<>();
    /**
     * When subscribing to collection nodes it is possible to be interested in new nodes
     * added to the collection node or new items published in the children nodes. The default
     * value is "nodes".
     */
    private Type type = Type.nodes;
    /**
     * Receive notification from children up to certain depth. Possible values are 1 or 0.
     * Zero means that there is no depth limit.
     */
    private int depth = 1;
    /**
     * Keyword that the event needs to match. When {@code null} all event are going to
     * be notified to the subscriber.
     */
    private String keyword = null;
    /**
     * Indicates if the subscription is present in the database.
     */
    private boolean savedToDB = false;

    /**
     * Creates a new subscription of the specified user with the node.
     *
     * @param node Node to which this subscription is interested in.
     * @param owner the JID of the entity that owns this subscription.
     * @param jid the JID of the user that owns the subscription.
     * @param state the state of the subscription with the node.
     * @param id the id the uniquely identifies this subscriptin within the node.
     */
    public NodeSubscription(Node node, JID owner, JID jid, State state, String id)
    {
        this.node = node;
        this.jid = jid;
        this.owner = owner;
        this.state = state;
        this.id = id;
    }

    /**
     * Returns the node that holds this subscription.
     *
     * @return the node that holds this subscription.
     */
    public Node getNode() {
        return node;
    }

    /**
     * Returns the ID that uniquely identifies the subscription of the user in the node.
     *
     * @return the ID that uniquely identifies the subscription of the user in the node.
     */
    public String getID() {
        return id;
    }

    /**
     * Returns the JID that is going to receive the event notifications. This JID can be the
     * owner JID or a full JID if the owner wants to receive the notification at a particular
     * resource.<p>
     *
     * Moreover, since subscriber and owner are separated it should be theorically possible to
     * have a different owner JID (e.g. [email protected]) and a subscriber JID
     * (e.g. [email protected]). Note that letting this case to happen may open the pubsub service
     * to get spam or security problems. However, the pubsub service should avoid this case to
     * happen.
     *
     * @return the JID that is going to receive the event notifications.
     */
    public JID getJID() {
        return jid;
    }

    /**
     * Retuns the JID of the entity that owns this subscription. The owner entity will have
     * a {@link NodeAffiliate} for the owner JID. The owner may have more than one subscription
     * with the node based on what this message
     * {@link org.jivesoftware.openfire.pubsub.Node#isMultipleSubscriptionsEnabled()}.
     *
     * @return he JID of the entity that owns this subscription.
     */
    public JID getOwner() {
        return owner;
    }

    /**
     * Returns the current subscription state. Subscriptions with status of pending should be
     * authorized by a node owner.
     *
     * @return the current subscription state.
     */
    public State getState() {
        return state;
    }

    /**
     * Returns true if configuration is required by the node and is still pending to
     * be configured by the subscriber. Otherwise return false. Once a subscription is
     * configured it might need to be approved by a node owner to become active.
     *
     * @return true if configuration is required by the node and is still pending to
     *         be configured by the subscriber.
     */
    public boolean isConfigurationPending() {
        return state == State.unconfigured;
    }

    /**
     * Returns true if the subscription needs to be approved by a node owner to become
     * active. Until the subscription is not activated the subscriber will not receive
     * event notifications.
     *
     * @return true if the subscription needs to be approved by a node owner to become active.
     */
    public boolean isAuthorizationPending() {
        return state == State.pending;
    }

    /**
     * Returns whether an entity wants to receive or has disabled notifications.
     *
     * @return true when notifications should be sent to the subscriber.
     */
    public boolean shouldDeliverNotifications() {
        return deliverNotifications;
    }

    /**
     * Returns whether an entity wants to receive digests (aggregations) of
     * notifications or all notifications individually.
     *
     * @return true when an entity wants to receive digests (aggregations) of notifications.
     */
    public boolean isUsingDigest() {
        return usingDigest;
    }

    /**
     * Returns the minimum number of milliseconds between sending any two notification digests.
     * Default is 24 hours.
     *
     * @return the minimum number of milliseconds between sending any two notification digests.
     */
    public int getDigestFrequency() {
        return digestFrequency;
    }

    /**
     * Returns the Date at which a leased subscription will end or has ended. A value of
     * {@code null} means that the subscription will never expire.
     *
     * @return the Date at which a leased subscription will end or has ended. A value of
     *         {@code null} means that the subscription will never expire.
     */
    public Date getExpire() {
        return expire;
    }

    /**
     * Returns whether an entity wants to receive an XMPP message body in
     * addition to the payload format.
     *
     * @return true when an entity wants to receive an XMPP message body in
     *         addition to the payload format
     */
    public boolean isIncludingBody() {
        return includingBody;
    }

    /**
     * The presence states for which an entity wants to receive notifications. When the owner
     * is in any of the returned presence states then he is allowed to receive notifications.
     *
     * @return the presence states for which an entity wants to receive notifications.
     *         (e.g. available, away, etc.)
     */
    public Collection<String> getPresenceStates() {
        return presenceStates;
    }

    /**
     * Returns if the owner has subscribed to receive notification of new items only
     * or of new nodes only. When subscribed to a Leaf Node then only {@code items}
     * is available.
     *
     * @return whether the owner has subscribed to receive notification of new items only
     *         or of new nodes only.
     */
    public Type getType() {
        return type;
    }

    /**
     * Returns 1 when the subscriber wants to receive notifications only from first-level
     * children of the collection. A value of 0 means that the subscriber wants to receive
     * notifications from all descendents.
     *
     * @return 1 when the subscriber wants to receive notifications only from first-level
     *         children of the collection or 0 when notififying only from all descendents.
     */
    public int getDepth() {
        return depth;
    }

    /**
     * Returns the keyword that the event needs to match. When {@code null} all event
     * are going to be notified to the subscriber.
     *
     * @return the keyword that the event needs to match. When {@code null} all event
     *         are going to be notified to the subscriber.
     */
    public String getKeyword() {
        return keyword;
    }

    void setShouldDeliverNotifications(boolean deliverNotifications) {
        this.deliverNotifications = deliverNotifications;
    }

    void setUsingDigest(boolean usingDigest) {
        this.usingDigest = usingDigest;
    }

    void setDigestFrequency(int digestFrequency) {
        this.digestFrequency = digestFrequency;
    }

    void setExpire(Date expire) {
        this.expire = expire;
    }

    void setIncludingBody(boolean includingBody) {
        this.includingBody = includingBody;
    }

    void setPresenceStates(Collection<String> presenceStates) {
        this.presenceStates = presenceStates;
    }

    void setType(Type type) {
        this.type = type;
    }

    void setDepth(int depth) {
        this.depth = depth;
    }

    void setKeyword(String keyword) {
        this.keyword = keyword;
    }

    void setSavedToDB(boolean savedToDB) {
        this.savedToDB = savedToDB;
    }

    /**
     * Configures the subscription based on the sent {@link DataForm} included in the IQ
     * packet sent by the subscriber. If the subscription was pending of configuration
     * then the last published item is going to be sent to the subscriber.<p>
     *
     * The originalIQ parameter may be {@code null} when using this API internally. When no
     * IQ packet was sent then no IQ result will be sent to the sender. The rest of the
     * functionality is the same.
     *
     * @param originalIQ the IQ packet sent by the subscriber to configure his subscription or
     *        null when using this API internally.
     * @param options the data form containing the new subscription configuration.
     */
    public void configure(IQ originalIQ, DataForm options) {
        boolean wasUnconfigured = isConfigurationPending();
        // Change the subscription configuration based on the completed form
        configure(options);
        if (originalIQ != null) {
            // Return success response
            node.getService().send(IQ.createResultIQ(originalIQ));
        }

        if (wasUnconfigured) {
            // If subscription is pending then send notification to node owners
            // asking to approve the now configured subscription
            if (isAuthorizationPending()) {
                sendAuthorizationRequest();
            }

            // Send last published item (if node is leaf node and subscription status is ok)
            if (node.isSendItemSubscribe() && isActive()) {
                PublishedItem lastItem = node.getLastPublishedItem();
                if (lastItem != null) {
                    sendLastPublishedItem(lastItem);
                }
            }
        }
    }

    void configure(DataForm options) {
        List<String> values;

        boolean wasUsingPresence = !presenceStates.isEmpty();

        // Remove this field from the form
        options.removeField("FORM_TYPE");
        // Process and remove specific collection node fields
        FormField collectionField = options.getField("pubsub#subscription_type");
        if (collectionField != null) {
            values = collectionField.getValues();
            if (values.size() > 0)  {
                type = Type.valueOf(values.get(0));
            }
            options.removeField("pubsub#subscription_type");
        }
        collectionField = options.getField("pubsub#subscription_depth");
        if (collectionField != null) {
            values = collectionField.getValues();
            depth = "all".equals(values.get(0)) ? 0 : 1;
            options.removeField("pubsub#subscription_depth");
        }
        // If there are more fields in the form then process them and set that
        // the subscription has been configured
        for (FormField field : options.getFields()) {
            boolean fieldExists = true;
            if ("pubsub#deliver".equals(field.getVariable())) {
                deliverNotifications = parseFirstValueAsBoolean( field, true ) ;
            }
            else if ("pubsub#digest".equals(field.getVariable())) {
                usingDigest = parseFirstValueAsBoolean( field, true ) ;
            }
            else if ("pubsub#digest_frequency".equals(field.getVariable())) {
                values = field.getValues();
                digestFrequency = values.size() > 0 ? Integer.parseInt(values.get(0)) : 86400000;
            }
            else if ("pubsub#expire".equals(field.getVariable())) {
                values = field.getValues();
                try {
                    expire = xmppDateTime.parseString(values.get(0));
                } catch (ParseException e) {
                    Log.error("Error parsing date", e);
                }
            }
            else if ("pubsub#include_body".equals(field.getVariable())) {
                includingBody = parseFirstValueAsBoolean( field, true ) ;
            }
            else if ("pubsub#show-values".equals(field.getVariable())) {
                // Get the new list of presence states for which an entity wants to
                // receive notifications
                presenceStates = new ArrayList<>();
                for (String value : field.getValues()) {
                    try {
                        presenceStates.add(value);
                    }
                    catch (Exception e) {
                        // Do nothing
                    }
                }
            }
            else if ("x-pubsub#keywords".equals(field.getVariable())) {
                values = field.getValues();
                keyword = values.isEmpty() ? null : values.get(0);
            }
            else {
                fieldExists = false;
            }
            if (fieldExists) {
                // Subscription has been configured so set the next state
                if (node.getAccessModel().isAuthorizationRequired() && !node.isAdmin(owner)) {
                    state = State.pending;
                }
                else {
                    state = State.subscribed;
                }
            }
        }
        if (savedToDB) {
            // Update the subscription in the backend store
            PubSubPersistenceProviderManager.getInstance().getProvider().updateSubscription(node, this);
        }
        // Check if the service needs to subscribe or unsubscribe from the owner presence
        if (!node.isPresenceBasedDelivery() && wasUsingPresence != !presenceStates.isEmpty()) {
            if (presenceStates.isEmpty()) {
                node.getService().presenceSubscriptionNotRequired(node, owner);
            }
            else {
                node.getService().presenceSubscriptionRequired(node, owner);
            }
        }
    }

    /**
     * Returns a data form with the subscription configuration. The data form can be used to
     * edit the subscription configuration.
     *
     * @return data form used by the subscriber to edit the subscription configuration.
     */
    public DataForm getConfigurationForm() {
        DataForm form = new DataForm(DataForm.Type.form);
        form.setTitle(LocaleUtils.getLocalizedString("pubsub.form.subscription.title"));
        List<String> params = new ArrayList<>();
        params.add(node.getUniqueIdentifier().getNodeId());
        form.addInstruction(LocaleUtils.getLocalizedString("pubsub.form.subscription.instruction", params));
        // Add the form fields and configure them for edition
        FormField formField = form.addField();
        formField.setVariable("FORM_TYPE");
        formField.setType(FormField.Type.hidden);
        formField.addValue("http://jabber.org/protocol/pubsub#subscribe_options");

        formField = form.addField();
        formField.setVariable("pubsub#deliver");
        formField.setType(FormField.Type.boolean_type);
        formField.setLabel(LocaleUtils.getLocalizedString("pubsub.form.subscription.deliver"));
        formField.addValue(deliverNotifications);

        formField = form.addField();
        formField.setVariable("pubsub#digest");
        formField.setType(FormField.Type.boolean_type);
        formField.setLabel(LocaleUtils.getLocalizedString("pubsub.form.subscription.digest"));
        formField.addValue(usingDigest);

        formField = form.addField();
        formField.setVariable("pubsub#digest_frequency");
        formField.setType(FormField.Type.text_single);
        formField.setLabel(LocaleUtils.getLocalizedString("pubsub.form.subscription.digest_frequency"));
        formField.addValue(digestFrequency);

        formField = form.addField();
        formField.setVariable("pubsub#expire");
        formField.setType(FormField.Type.text_single);
        formField.setLabel(LocaleUtils.getLocalizedString("pubsub.form.subscription.expire"));
        if (expire != null) {
            formField.addValue(XMPPDateTimeFormat.format(expire));
        }

        formField = form.addField();
        formField.setVariable("pubsub#include_body");
        formField.setType(FormField.Type.boolean_type);
        formField.setLabel(LocaleUtils.getLocalizedString("pubsub.form.subscription.include_body"));
        formField.addValue(includingBody);

        formField = form.addField();
        formField.setVariable("pubsub#show-values");
        formField.setType(FormField.Type.list_multi);
        formField.setLabel(LocaleUtils.getLocalizedString("pubsub.form.subscription.show-values"));
        formField.addOption(null, Presence.Show.away.name());
        formField.addOption(null, Presence.Show.chat.name());
        formField.addOption(null, Presence.Show.dnd.name());
        formField.addOption(null, "online");
        formField.addOption(null, Presence.Show.xa.name());
        for (String value : presenceStates) {
            formField.addValue(value);
        }

        if (node.isCollectionNode()) {
            formField = form.addField();
            formField.setVariable("pubsub#subscription_type");
            formField.setType(FormField.Type.list_single);
            formField.setLabel(LocaleUtils.getLocalizedString("pubsub.form.subscription.subscription_type"));
            formField.addOption(null, Type.items.name());
            formField.addOption(null, Type.nodes.name());
            formField.addValue(type);

            formField = form.addField();
            formField.setVariable("pubsub#subscription_depth");
            formField.setType(FormField.Type.list_single);
            formField.setLabel(LocaleUtils.getLocalizedString("pubsub.form.subscription.subscription_depth"));
            formField.addOption(null, "1");
            formField.addOption(null, "all");
            formField.addValue(depth == 1 ? "1" : "all");
        }

        if (!node.isCollectionNode() || type == Type.items) {
            formField = form.addField();
            formField.setVariable("x-pubsub#keywords");
            formField.setType(FormField.Type.text_single);
            formField.setLabel(LocaleUtils.getLocalizedString("pubsub.form.subscription.keywords"));
            if (keyword != null) {
                formField.addValue(keyword);
            }
        }

        return form;
    }

    /**
     * Returns true if an event notification can be sent to the subscriber for the specified
     * published item based on the subsription configuration and subscriber status.
     *
     * @param leafNode the node that received the publication.
     * @param publishedItem the published item to send or null if the publication didn't
     *        contain an item.
     * @return true if an event notification can be sent to the subscriber for the specified
     *         published item.
     */
    public boolean canSendPublicationEvent(LeafNode leafNode, PublishedItem publishedItem) {
        if (!canSendEvents()) {
            return false;
        }
        // Check that any defined keyword was matched (applies only if an item was published)
        if (publishedItem != null && !isKeywordMatched(publishedItem)) {
            return false;
        }
        // Check special conditions when subscribed to collection node
        if (node.isCollectionNode()) {
            // Check if not subscribe to items
            if (Type.items != type) {
                return false;
            }
            // Check if published node is a first-level child of the subscribed node
            if (getDepth() == 1 && !node.isChildNode(leafNode)) {
                return false;
            }
            // Check if published node is a descendant child of the subscribed node
            if (getDepth() == 0 && !node.isDescendantNode(leafNode)) {
                return false;
            }
        }
        return true;
    }

    /**
     * Returns true if an event notification can be sent to the subscriber of the collection
     * node for a newly created node that was associated to the collection node or a child
     * node that was deleted. The subscription has to be of type {@link Type#nodes}.
     *
     * @param originatingNode the node that was added or deleted from the collection node.
     * @return true if an event notification can be sent to the subscriber of the collection
     *         node.
     */
    boolean canSendChildNodeEvent(Node originatingNode) {
        // Check that this is a subscriber to a collection node
        if (!node.isCollectionNode()) {
            return false;
        }

        if (!canSendEvents()) {
            return false;
        }
        // Check that subscriber is using type "nodes"
        if (Type.nodes != type) {
            return false;
        }
        // Check if added/deleted node is a first-level child of the subscribed node
        if (getDepth() == 1 && !node.isChildNode(originatingNode)) {
            return false;
        }
        // Check if added/deleted node is a descendant child of the subscribed node
        if (getDepth() == 0 && !node.isDescendantNode(originatingNode)) {
            return false;
        }
        return true;
    }

    /**
     * Returns true if node events such as configuration changed or node purged can be
     * sent to the subscriber.
     *
     * @return true if node events such as configuration changed or node purged can be
     *         sent to the subscriber.
     */
    boolean canSendNodeEvents() {
        return canSendEvents();
    }

    /**
     * Returns true if events in general can be sent. This method checks basic
     * conditions common to all type of event notifications (e.g. item was published,
     * node configuration has changed, new child node was added to collection node, etc.).
     *
     * @return true if events in general can be sent.
     */
    private boolean canSendEvents() {
        // Check if the subscription is active
        if (!isActive()) {
            return false;
        }
        // Check if delivery of notifications is disabled
        if (!shouldDeliverNotifications()) {
            return false;
        }
        // Check if delivery is subject to presence-based policy
        if (!getPresenceStates().isEmpty()) {
            Collection<String> shows = node.getService().getShowPresences(jid);
            if (shows.isEmpty() || Collections.disjoint(getPresenceStates(), shows)) {
                return false;
            }
        }
        // Check if node is only sending events when user is online
        if (node.isPresenceBasedDelivery()) {
            // Check that user is online
            if (node.getService().getShowPresences(jid).isEmpty())
            {
                return false;
            }
        }
        return true;
    }

    /**
     * Returns true if the published item matches the keyword filter specified in
     * the subscription. If no keyword was specified then answer true.
     *
     * @param publishedItem the published item to send.
     * @return true if the published item matches the keyword filter specified in
     *         the subscription.
     */
    boolean isKeywordMatched(PublishedItem publishedItem) {
        // Check if keyword was defined and it was not matched
        if (keyword != null && keyword.length() > 0 && !publishedItem.containsKeyword(keyword)) {
            return false;
        }
        return true;
    }

    /**
     * Returns true if the subscription is active. A subscription is considered active if it
     * has not expired, it has been approved and configured.
     *
     * @return true if the subscription is active.
     */
    public boolean isActive() {
        // Check if subscription is approved and configured (if required)
        if (state != State.subscribed) {
            return false;
        }
        // Check if the subscription has expired
        if (expire != null && new Date().after(expire)) {
            // TODO This checking does not replace the fact that we need to implement expiration.
            // TODO A thread that checks expired subscriptions and removes them is needed.
            return false;
        }
        return true;
    }

    /**
     * Sends the current subscription status to the user that tried to create a subscription to
     * the node. The subscription status is sent to the subsciber after the subscription was
     * created or if the subscriber tries to subscribe many times and the node does not support
     * multpiple subscriptions.
     *
     * @param originalRequest the IQ packet sent by the subscriber to create the subscription.
     */
    void sendSubscriptionState(IQ originalRequest) {
        IQ result = IQ.createResultIQ(originalRequest);
        Element child = result.setChildElement("pubsub", "http://jabber.org/protocol/pubsub");
        Element entity = child.addElement("subscription");
        if (!node.isRootCollectionNode()) {
            entity.addAttribute("node", node.getUniqueIdentifier().getNodeId());
        }
        entity.addAttribute("jid", getJID().toString());
        if (node.isMultipleSubscriptionsEnabled()) {
            entity.addAttribute("subid", getID());
        }
        entity.addAttribute("subscription", getState().name());
        Element subscribeOptions = entity.addElement("subscribe-options");
        if (node.isSubscriptionConfigurationRequired() && isConfigurationPending()) {
            subscribeOptions.addElement("required");
        }
        // Send the result
        node.getService().send(result);
    }

    /**
     * Sends an event notification for the last published item to the subscriber. If
     * the subscription has not yet been authorized or is pending to be configured then
     * no notification is going to be sent.<p>
     *
     * Depending on the subscription configuration the event notification may or may not have
     * a payload, may not be sent if a keyword (i.e. filter) was defined and it was not matched.
     *
     * <p>Sending the last published item can also be entirely disabled by setting
     * {@code xmpp.pubsub.disable-delayed-delivery} to {@code <true}.</p>
     *
     * @param publishedItem the last item that was published to the node.
     */
    void sendLastPublishedItem(PublishedItem publishedItem) {
        // Check to see if we've been disabled
        if (JiveGlobals.getBooleanProperty("xmpp.pubsub.disable-delayed-delivery", false)) {
            return;
        }

        // Check if the published item can be sent to the subscriber
        if (!canSendPublicationEvent(publishedItem.getNode(), publishedItem)) {
            return;
        }
        // Send event notification to the subscriber
        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.getUniqueIdentifier().getNodeId());
        Element item = items.addElement("item");
        if (((LeafNode) node).isItemRequired()) {
            item.addAttribute("id", publishedItem.getID());
        }
        if (node.isPayloadDelivered() && publishedItem.getPayload() != null) {
            item.add(publishedItem.getPayload().createCopy());
        }
        // Add a message body (if required)
        if (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, jid);
    }

    /**
     * Returns true if the specified user is allowed to modify or cancel the subscription. Users
     * that are allowed to modify/cancel the subscription are: the entity that is recieving the
     * notifications, the owner of the subscriptions or sysadmins of the pubsub service.
     *
     * @param user the user that is trying to cancel the subscription.
     * @return true if the specified user is allowed to modify or cancel the subscription.
     */
    boolean canModify(JID user) {
        return user.equals(getJID()) || user.toBareJID().equals(getOwner().toBareJID()) || node.getService().isServiceAdmin(user);
    }

    /**
     * Returns the {@link NodeAffiliate} that owns this subscription. Users that have a
     * subscription with the node will ALWAYS have an affiliation even if the
     * affiliation is of type {@code none}.
     *
     * @return the NodeAffiliate that owns this subscription.
     */
    public NodeAffiliate getAffiliate() {
        return node.getAffiliate(getOwner());
    }

    @Override
    public String toString() {
        return super.toString() + " - JID: " + getJID() + " - State: " + getState().name();
    }

    /**
     * The subscription has been approved by a node owner. The subscription is now active so
     * the subscriber is now allowed to get event notifications.
     */
    void approved() {
        if (state == State.subscribed) {
            // Do nothing
            return;
        }
        state = State.subscribed;

        if (savedToDB) {
            // Update the subscription in the backend store
            PubSubPersistenceProviderManager.getInstance().getProvider().updateSubscription(node, this);
        }

        // Send last published item (if node is leaf node and subscription status is ok)
        if (node.isSendItemSubscribe() && isActive()) {
            PublishedItem lastItem = node.getLastPublishedItem();
            if (lastItem != null) {
                sendLastPublishedItem(lastItem);
            }
        }
    }

    /**
     * Sends an request to authorize the pending subscription to the specified owner.
     *
     * @param owner the JID of the user that will get the authorization request.
     */
    public void sendAuthorizationRequest(JID owner) {
        Message authRequest = new Message();
        authRequest.addExtension(node.getAuthRequestForm(this));
        authRequest.setTo(owner);
        authRequest.setFrom(node.getService().getAddress());
        // Send authentication request to node owners
        node.getService().send(authRequest);
    }

    /**
     * Sends an request to authorize the pending subscription to all owners. The first
     * answer sent by a owner will be processed. Rest of the answers will be discarded.
     */
    public void sendAuthorizationRequest() {
        Message authRequest = new Message();
        authRequest.addExtension(node.getAuthRequestForm(this));
        // Send authentication request to node owners
        node.getService().broadcast(node, authRequest, node.getOwners());
    }

    /**
     * Subscriptions to a node may exist in several states. Delivery of event notifications
     * varies according to the subscription state of the user with the node.
     */
    public static enum State {

        /**
         * The node will never send event notifications or payloads to users in this state. Users
         * with subscription state none and affiliation none are going to be deleted.
         */
        none,
        /**
         * An entity has requested to subscribe to a node and the request has not yet been
         * approved by a node owner. The node will not send event notifications or payloads
         * to the entity while it is in this state.
         */
        pending,
        /**
         * An entity has subscribed but its subscription options have not yet been configured.
         * The node will send event notifications or payloads to the entity while it is in this
         * state. Default subscription configuration is going to be assumed.
         */
        unconfigured,
        /**
         * An entity is subscribed to a node. The node will send all event notifications
         * (and, if configured, payloads) to the entity while it is in this state.
         */
        subscribed
    }

    public static enum Type {

        /**
         * Receive notification of new items only.
         */
        items,
        /**
         * Receive notification of new nodes only.
         */
        nodes
    }
}