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

21
package org.jivesoftware.openfire.pubsub;
Matt Tucker's avatar
Matt Tucker committed
22

23 24 25 26 27 28 29
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.List;

Matt Tucker's avatar
Matt Tucker committed
30 31
import org.dom4j.Element;
import org.jivesoftware.util.LocaleUtils;
32
import org.jivesoftware.util.XMPPDateTimeFormat;
33 34
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Matt Tucker's avatar
Matt Tucker committed
35 36
import org.xmpp.forms.DataForm;
import org.xmpp.forms.FormField;
Matt Tucker's avatar
Matt Tucker committed
37
import org.xmpp.packet.IQ;
Matt Tucker's avatar
Matt Tucker committed
38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
import org.xmpp.packet.JID;
import org.xmpp.packet.Message;
import org.xmpp.packet.Presence;

/**
 * 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 {

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

67
    private static final XMPPDateTimeFormat xmppDateTime = new XMPPDateTimeFormat();
68

Matt Tucker's avatar
Matt Tucker committed
69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147
    /**
     * 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
     * <tt>null</tt> 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<String>();
    /**
     * 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 <tt>null</tt> 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.
     */
148 149
	public NodeSubscription(Node node, JID owner, JID jid, State state, String id)
	{
Matt Tucker's avatar
Matt Tucker committed
150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195
        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. gato@server1.com) and a subscriber JID
     * (e.g. gato@server2.com). 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
196
     * {@link org.jivesoftware.openfire.pubsub.Node#isMultipleSubscriptionsEnabled()}.
Matt Tucker's avatar
Matt Tucker committed
197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215
     *
     * @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
Matt Tucker's avatar
Matt Tucker committed
216 217
     * 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.
Matt Tucker's avatar
Matt Tucker committed
218 219 220 221 222
     *
     * @return true if configuration is required by the node and is still pending to
     *         be configured by the subscriber.
     */
    public boolean isConfigurationPending() {
Matt Tucker's avatar
Matt Tucker committed
223
        return state == State.unconfigured;
Matt Tucker's avatar
Matt Tucker committed
224 225 226
    }

    /**
Matt Tucker's avatar
Matt Tucker committed
227 228 229
     * 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.
Matt Tucker's avatar
Matt Tucker committed
230
     *
Matt Tucker's avatar
Matt Tucker committed
231
     * @return true if the subscription needs to be approved by a node owner to become active.
Matt Tucker's avatar
Matt Tucker committed
232
     */
Matt Tucker's avatar
Matt Tucker committed
233 234
    public boolean isAuthorizationPending() {
        return state == State.pending;
Matt Tucker's avatar
Matt Tucker committed
235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373
    }

    /**
     * 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
     * <tt>null</tt> means that the subscription will never expire.
     *
     * @return the Date at which a leased subscription will end or has ended. A value of
     *         <tt>null</tt> 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 <tt>items</tt>
     * 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 <tt>null</tt> all event
     * are going to be notified to the subscriber.
     *
     * @return the keyword that the event needs to match. When <tt>null</tt> 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;
    }

Matt Tucker's avatar
Matt Tucker committed
374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392
    /**
     * 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 <tt>null</tt> 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
393
			node.getService().send(IQ.createResultIQ(originalIQ));
Matt Tucker's avatar
Matt Tucker committed
394
        }
Matt Tucker's avatar
Matt Tucker committed
395 396 397 398 399 400

        if (wasUnconfigured) {
            // If subscription is pending then send notification to node owners
            // asking to approve the now configured subscription
            if (isAuthorizationPending()) {
                sendAuthorizationRequest();
Matt Tucker's avatar
Matt Tucker committed
401 402
            }

Matt Tucker's avatar
Matt Tucker committed
403 404 405 406 407 408 409 410
            // 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);
                }
            }
        }
Matt Tucker's avatar
Matt Tucker committed
411 412
    }

Matt Tucker's avatar
Matt Tucker committed
413 414 415
    void configure(DataForm options) {
        List<String> values;
        String booleanValue;
416 417 418

        boolean wasUsingPresence = !presenceStates.isEmpty();

Matt Tucker's avatar
Matt Tucker committed
419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455
        // 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())) {
                values = field.getValues();
                booleanValue = (values.size() > 0 ? values.get(0) : "1");
                deliverNotifications = "1".equals(booleanValue);
            }
            else if ("pubsub#digest".equals(field.getVariable())) {
                values = field.getValues();
                booleanValue = (values.size() > 0 ? values.get(0) : "1");
                usingDigest = "1".equals(booleanValue);
            }
            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();
456 457 458 459
                try {
                    expire = xmppDateTime.parseString(values.get(0));
                } catch (ParseException e) {
                    Log.error("Error parsing date", e);
Matt Tucker's avatar
Matt Tucker committed
460 461 462 463 464 465 466 467 468 469 470 471 472 473 474
                }
            }
            else if ("pubsub#include_body".equals(field.getVariable())) {
                values = field.getValues();
                booleanValue = (values.size() > 0 ? values.get(0) : "1");
                includingBody = "1".equals(booleanValue);
            }
            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<String>();
                for (String value : field.getValues()) {
                    try {
                        presenceStates.add(value);
                    }
475 476 477
                    catch (Exception e) {
                        // Do nothing
                    }
Matt Tucker's avatar
Matt Tucker committed
478 479 480 481 482 483 484 485 486 487
                }
            }
            else if ("x-pubsub#keywords".equals(field.getVariable())) {
                values = field.getValues();
                keyword = values.isEmpty() ? null : values.get(0);
            }
            else {
                fieldExists = false;
            }
            if (fieldExists) {
Matt Tucker's avatar
Matt Tucker committed
488
                // Subscription has been configured so set the next state
489
                if (node.getAccessModel().isAuthorizationRequired() && !node.isAdmin(owner)) {
Matt Tucker's avatar
Matt Tucker committed
490 491 492 493 494
                    state = State.pending;
                }
                else {
                    state = State.subscribed;
                }
Matt Tucker's avatar
Matt Tucker committed
495 496 497 498
            }
        }
        if (savedToDB) {
            // Update the subscription in the backend store
499
            PubSubPersistenceManager.saveSubscription(node, this, false);
Matt Tucker's avatar
Matt Tucker committed
500
        }
501 502 503
        // Check if the service needs to subscribe or unsubscribe from the owner presence
        if (!node.isPresenceBasedDelivery() && wasUsingPresence != !presenceStates.isEmpty()) {
            if (presenceStates.isEmpty()) {
504
				node.getService().presenceSubscriptionNotRequired(node, owner);
505 506
            }
            else {
507
				node.getService().presenceSubscriptionRequired(node, owner);
508 509
            }
        }
Matt Tucker's avatar
Matt Tucker committed
510 511 512 513 514 515 516 517
    }

    /**
     * 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.
     */
Matt Tucker's avatar
Matt Tucker committed
518
    public DataForm getConfigurationForm() {
Matt Tucker's avatar
Matt Tucker committed
519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552
        DataForm form = new DataForm(DataForm.Type.form);
        form.setTitle(LocaleUtils.getLocalizedString("pubsub.form.subscription.title"));
        List<String> params = new ArrayList<String>();
        params.add(node.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) {
553
            formField.addValue(XMPPDateTimeFormat.format(expire));
Matt Tucker's avatar
Matt Tucker committed
554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615
        }

        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.
     */
616
    public boolean canSendPublicationEvent(LeafNode leafNode, PublishedItem publishedItem) {
617
        if (!canSendEvents()) {
Matt Tucker's avatar
Matt Tucker committed
618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641
            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;
    }

642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674
    /**
     * 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;
    }

Matt Tucker's avatar
Matt Tucker committed
675 676 677 678 679 680 681 682
    /**
     * 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() {
683 684 685 686 687 688 689 690 691 692 693
        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() {
Matt Tucker's avatar
Matt Tucker committed
694 695 696 697 698 699 700 701 702 703
        // 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()) {
704
			Collection<String> shows = node.getService().getShowPresences(jid);
705 706 707 708 709 710 711
            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
712 713
			if (node.getService().getShowPresences(jid).isEmpty())
			{
Matt Tucker's avatar
Matt Tucker committed
714 715 716 717 718 719
                return false;
            }
        }
        return true;
    }

Matt Tucker's avatar
Matt Tucker committed
720 721 722 723 724 725 726 727
    /**
     * 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.
     */
728
    boolean isKeywordMatched(PublishedItem publishedItem) {
Matt Tucker's avatar
Matt Tucker committed
729 730 731 732 733 734 735 736 737 738 739 740 741
        // 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.
     */
Matt Tucker's avatar
Matt Tucker committed
742
    public boolean isActive() {
Matt Tucker's avatar
Matt Tucker committed
743
        // Check if subscription is approved and configured (if required)
Matt Tucker's avatar
Matt Tucker committed
744
        if (state != State.subscribed) {
Matt Tucker's avatar
Matt Tucker committed
745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766
            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");
767
        Element entity = child.addElement("subscription");
Matt Tucker's avatar
Matt Tucker committed
768 769 770 771 772 773 774 775 776 777 778 779 780
        if (!node.isRootCollectionNode()) {
            entity.addAttribute("node", node.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
781
		node.getService().send(result);
Matt Tucker's avatar
Matt Tucker committed
782 783 784 785 786 787 788 789 790 791 792 793 794 795
    }

    /**
     * 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.
     *
     * @param publishedItem the last item that was published to the node.
     */
    void sendLastPublishedItem(PublishedItem publishedItem) {
        // Check if the published item can be sent to the subscriber
796
        if (!canSendPublicationEvent(publishedItem.getNode(), publishedItem)) {
Matt Tucker's avatar
Matt Tucker committed
797 798 799 800 801 802 803 804 805 806 807 808
            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.getNodeID());
        Element item = items.addElement("item");
        if (((LeafNode) node).isItemRequired()) {
            item.addAttribute("id", publishedItem.getID());
        }
Matt Tucker's avatar
Matt Tucker committed
809
        if (node.isPayloadDelivered() && publishedItem.getPayload() != null) {
Matt Tucker's avatar
Matt Tucker committed
810 811 812 813 814 815 816
            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
817
        notification.getElement().addElement("delay", "urn:xmpp:delay")
818
                .addAttribute("stamp", XMPPDateTimeFormat.format(publishedItem.getCreationDate()));
Matt Tucker's avatar
Matt Tucker committed
819
        // Send the event notification to the subscriber
820
		node.getService().sendNotification(node, notification, jid);
Matt Tucker's avatar
Matt Tucker committed
821 822 823 824 825 826 827 828 829 830 831
    }

    /**
     * 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) {
832
		return user.equals(getJID()) || user.equals(getOwner()) || node.getService().isServiceAdmin(user);
Matt Tucker's avatar
Matt Tucker committed
833 834 835 836 837 838 839 840 841 842 843 844 845
    }

    /**
     * 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 <tt>none</tt>.
     *
     * @return the NodeAffiliate that owns this subscription.
     */
    public NodeAffiliate getAffiliate() {
        return node.getAffiliate(getOwner());
    }

846 847
    @Override
	public String toString() {
Matt Tucker's avatar
Matt Tucker committed
848 849 850
        return super.toString() + " - JID: " + getJID() + " - State: " + getState().name();
    }

Matt Tucker's avatar
Matt Tucker committed
851 852 853 854 855 856 857 858 859 860 861 862 863
    /**
     * 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
864
            PubSubPersistenceManager.saveSubscription(node, this, false);
Matt Tucker's avatar
Matt Tucker committed
865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884
        }

        // 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);
885
		authRequest.setFrom(node.getService().getAddress());
Matt Tucker's avatar
Matt Tucker committed
886
        // Send authentication request to node owners
887
		node.getService().send(authRequest);
Matt Tucker's avatar
Matt Tucker committed
888 889 890 891 892 893 894 895 896 897
    }

    /**
     * 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
898
		node.getService().broadcast(node, authRequest, node.getOwners());
Matt Tucker's avatar
Matt Tucker committed
899 900
    }

Matt Tucker's avatar
Matt Tucker committed
901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927
    /**
     * 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.
         */
928
        subscribed
Matt Tucker's avatar
Matt Tucker committed
929 930 931 932 933 934 935 936 937 938 939
    }

    public static enum Type {

        /**
         * Receive notification of new items only.
         */
        items,
        /**
         * Receive notification of new nodes only.
         */
940
        nodes
Matt Tucker's avatar
Matt Tucker committed
941 942
    }
}