LeafNode.java 19.8 KB
Newer Older
Matt Tucker's avatar
Matt Tucker committed
1 2 3 4 5 6 7 8 9 10 11
/**
 * $RCSfile: $
 * $Revision: $
 * $Date: $
 *
 * Copyright (C) 2006 Jive Software. All rights reserved.
 *
 * This software is published under the terms of the GNU Public License (GPL),
 * a copy of which is included in this distribution.
 */

12
package org.jivesoftware.openfire.pubsub;
Matt Tucker's avatar
Matt Tucker committed
13 14 15 16 17 18

import org.dom4j.Element;
import org.jivesoftware.util.LocaleUtils;
import org.jivesoftware.util.StringUtils;
import org.xmpp.forms.DataForm;
import org.xmpp.forms.FormField;
Gaston Dombiak's avatar
Gaston Dombiak committed
19
import org.xmpp.packet.IQ;
Matt Tucker's avatar
Matt Tucker committed
20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
import org.xmpp.packet.JID;
import org.xmpp.packet.Message;

import java.util.*;

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

    /**
     * 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;
Matt Tucker's avatar
Matt Tucker committed
50 51 52 53
    /**
     * Flag that indicates whether to send items to new subscribers.
     */
    private boolean sendItemSubscribe;
Matt Tucker's avatar
Matt Tucker committed
54 55 56 57 58
    /**
     * List of items that were published to the node and that are still active. If the node is
     * not configured to persist items then the last published item will be kept. The list is
     * sorted cronologically.
     */
59
    protected final List<PublishedItem> publishedItems = new ArrayList<PublishedItem>();
Matt Tucker's avatar
Matt Tucker committed
60 61
    protected Map<String, PublishedItem> itemsByID = new HashMap<String, PublishedItem>();

62
    // TODO Add checking of max payload size. Return <not-acceptable> plus a application specific error condition of <payload-too-big/>.
Matt Tucker's avatar
Matt Tucker committed
63

64
    public LeafNode(PubSubService service, CollectionNode parentNode, String nodeID, JID creator) {
Matt Tucker's avatar
Matt Tucker committed
65 66 67 68 69 70
        super(service, parentNode, nodeID, creator);
        // Configure node with default values (get them from the pubsub service)
        DefaultNodeConfiguration defaultConfiguration = service.getDefaultNodeConfiguration(true);
        this.persistPublishedItems = defaultConfiguration.isPersistPublishedItems();
        this.maxPublishedItems = defaultConfiguration.getMaxPublishedItems();
        this.maxPayloadSize = defaultConfiguration.getMaxPayloadSize();
Matt Tucker's avatar
Matt Tucker committed
71
        this.sendItemSubscribe = defaultConfiguration.isSendItemSubscribe();
Matt Tucker's avatar
Matt Tucker committed
72 73 74 75 76 77 78 79 80 81 82 83 84 85
    }

    void configure(FormField field) {
        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;
        }
Matt Tucker's avatar
Matt Tucker committed
86 87 88 89 90
        else if ("pubsub#send_item_subscribe".equals(field.getVariable())) {
            values = field.getValues();
            booleanValue = (values.size() > 0 ? values.get(0) : "1");
            sendItemSubscribe = "1".equals(booleanValue);
        }
Matt Tucker's avatar
Matt Tucker committed
91 92 93 94 95 96 97 98 99 100 101 102 103 104 105
    }

    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;
            }
        }
Matt Tucker's avatar
Matt Tucker committed
106 107 108 109 110 111 112
        synchronized (publishedItems) {
            // Remove stored published items based on the new max items
            while (!publishedItems.isEmpty() && publishedItems.size() > maxPublishedItems) {
                PublishedItem removedItem = publishedItems.remove(0);
                itemsByID.remove(removedItem.getID());
                // Add the removed item to the queue of items to delete from the database. The
                // queue is going to be processed by another thread
Gaston Dombiak's avatar
Gaston Dombiak committed
113
                service.queueItemToRemove(removedItem);
Matt Tucker's avatar
Matt Tucker committed
114
            }
Matt Tucker's avatar
Matt Tucker committed
115
        }
Matt Tucker's avatar
Matt Tucker committed
116 117 118 119 120 121
    }

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

        FormField formField = form.addField();
Matt Tucker's avatar
Matt Tucker committed
122 123 124 125 126 127 128 129 130
        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();
Matt Tucker's avatar
Matt Tucker committed
131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155
        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);

    }

Matt Tucker's avatar
Matt Tucker committed
156 157 158 159 160 161 162 163
    protected void deletingNode() {
        synchronized (publishedItems) {
            // Remove stored published items
            while (!publishedItems.isEmpty()) {
                PublishedItem removedItem = publishedItems.remove(0);
                itemsByID.remove(removedItem.getID());
                // Add the removed item to the queue of items to delete from the database. The
                // queue is going to be processed by another thread
Gaston Dombiak's avatar
Gaston Dombiak committed
164
                service.queueItemToRemove(removedItem);
Matt Tucker's avatar
Matt Tucker committed
165 166 167 168
            }
        }
    }

Matt Tucker's avatar
Matt Tucker committed
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 196 197 198 199 200
    void addPublishedItem(PublishedItem item) {
        synchronized (publishedItems) {
            publishedItems.add(item);
            itemsByID.put(item.getID(), 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() {
Matt Tucker's avatar
Matt Tucker committed
201
        return isPersistPublishedItems() || isPayloadDelivered();
Matt Tucker's avatar
Matt Tucker committed
202 203 204
    }

    /**
Matt Tucker's avatar
Matt Tucker committed
205 206 207 208 209 210 211
     * 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>
Matt Tucker's avatar
Matt Tucker committed
212 213 214 215 216 217 218 219 220
     *
     * 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
Matt Tucker's avatar
Matt Tucker committed
221
     * node subscribers may also use another thread to ensure good performance.<p>
Matt Tucker's avatar
Matt Tucker committed
222
     *
Matt Tucker's avatar
Matt Tucker committed
223 224
     * @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.
Matt Tucker's avatar
Matt Tucker committed
225
     */
Matt Tucker's avatar
Matt Tucker committed
226 227
    public void publishItems(JID publisher, List<Element> itemElements) {
        List<PublishedItem> newPublishedItems = new ArrayList<PublishedItem>();
Matt Tucker's avatar
Matt Tucker committed
228
        if (isItemRequired()) {
Matt Tucker's avatar
Matt Tucker committed
229 230
            String itemID;
            Element payload;
231
            PublishedItem newItem;
Matt Tucker's avatar
Matt Tucker committed
232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253
            for (Element item : itemElements) {
                itemID = item.attributeValue("id");
                List entries = item.elements();
                payload = entries.isEmpty() ? null : (Element) entries.get(0);
                // Create a published item from the published data and add it to the node and the db
                synchronized (publishedItems) {
                    // Make sure that the published item has an ID and that it's unique in the node
                    if (itemID == null) {
                        itemID = StringUtils.randomString(15);
                    }
                    while (itemsByID.get(itemID) != null) {
                        itemID = StringUtils.randomString(15);
                    }

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

                    // Add the published item to the list of items to persist (using another thread)
                    // but check that we don't exceed the limit. Remove oldest items if required.
Matt Tucker's avatar
Matt Tucker committed
254 255
                    while (!publishedItems.isEmpty() && publishedItems.size() >= maxPublishedItems)
                    {
Matt Tucker's avatar
Matt Tucker committed
256 257 258 259
                        PublishedItem removedItem = publishedItems.remove(0);
                        itemsByID.remove(removedItem.getID());
                        // Add the removed item to the queue of items to delete from the database. The
                        // queue is going to be processed by another thread
Gaston Dombiak's avatar
Gaston Dombiak committed
260
                        service.queueItemToRemove(removedItem);
Matt Tucker's avatar
Matt Tucker committed
261 262 263 264
                    }
                    addPublishedItem(newItem);
                    // Add the new published item to the queue of items to add to the database. The
                    // queue is going to be processed by another thread
Gaston Dombiak's avatar
Gaston Dombiak committed
265
                    service.queueItemToAdd(newItem);
Matt Tucker's avatar
Matt Tucker committed
266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282
                }
            }
        }

        // 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());
            }
        }
        // TODO Use another thread for this (if # of subscribers is > X)????
        for (NodeAffiliate affiliate : affiliatesToNotify) {
Matt Tucker's avatar
Matt Tucker committed
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
            affiliate.sendPublishedNotifications(message, event, this, newPublishedItems);
        }
    }

    /**
     * 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) {
        synchronized (publishedItems) {
            for (PublishedItem item : toDelete) {
                // Remove items to delete from memory
                publishedItems.remove(item);
                // Update fast look up cache of published items
                itemsByID.remove(item.getID());
            }
        }
        // Remove deleted items from the database
        for (PublishedItem item : toDelete) {
Gaston Dombiak's avatar
Gaston Dombiak committed
310
            service.queueItemToRemove(item);
Matt Tucker's avatar
Matt Tucker committed
311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330
        }
        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);
            }
Matt Tucker's avatar
Matt Tucker committed
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
    /**
     * 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 childElement = originalRequest.getChildElement().createCopy();
        result.setChildElement(childElement);
        Element items = childElement.element("items");
        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
        service.send(result);
    }

Matt Tucker's avatar
Matt Tucker committed
363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401
    public PublishedItem getPublishedItem(String itemID) {
        if (!isItemRequired()) {
            return null;
        }
        synchronized (publishedItems) {
            return itemsByID.get(itemID);
        }
    }

    public List<PublishedItem> getPublishedItems() {
        synchronized (publishedItems) {
            return Collections.unmodifiableList(publishedItems);
        }
    }

    public List<PublishedItem> getPublishedItems(int recentItems) {
        synchronized (publishedItems) {
            int size = publishedItems.size();
            if (recentItems > size) {
                // User requested more items than the one the node has so return the current list
                return Collections.unmodifiableList(publishedItems);
            }
            else {
                // Return the number of recent items the user requested
                List<PublishedItem> recent = publishedItems.subList(size - recentItems, size);
                return new ArrayList<PublishedItem>(recent);
            }
        }
    }

    public PublishedItem getLastPublishedItem() {
        synchronized (publishedItems) {
            if (publishedItems.isEmpty()) {
                return null;
            }
            return publishedItems.get(publishedItems.size()-1);
        }
    }

Matt Tucker's avatar
Matt Tucker committed
402 403 404 405 406 407 408 409 410
    /**
     * 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.
     */
    public boolean isSendItemSubscribe() {
        return sendItemSubscribe;
    }

Matt Tucker's avatar
Matt Tucker committed
411 412 413 414 415 416 417 418 419 420 421 422
    void setMaxPayloadSize(int maxPayloadSize) {
        this.maxPayloadSize = maxPayloadSize;
    }

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

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

Matt Tucker's avatar
Matt Tucker committed
423 424 425 426
    void setSendItemSubscribe(boolean sendItemSubscribe) {
        this.sendItemSubscribe = sendItemSubscribe;
    }

Matt Tucker's avatar
Matt Tucker committed
427 428 429 430 431 432 433 434 435 436 437
    /**
     * 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() {
        List<PublishedItem> toDelete = null;
        // Calculate items to delete
        synchronized (publishedItems) {
            if (publishedItems.size() > 1) {
                // Remove all items except the last one
438 439
                toDelete = new ArrayList<PublishedItem>(
                        publishedItems.subList(0, publishedItems.size() - 1));
Matt Tucker's avatar
Matt Tucker committed
440 441 442 443 444 445 446 447 448 449
                // Remove items to delete from memory
                publishedItems.removeAll(toDelete);
                // Update fast look up cache of published items
                itemsByID = new HashMap<String, PublishedItem>();
                itemsByID.put(publishedItems.get(0).getID(), publishedItems.get(0));
            }
        }
        if (toDelete != null) {
            // Delete purged items from the database
            for (PublishedItem item : toDelete) {
Gaston Dombiak's avatar
Gaston Dombiak committed
450
                service.queueItemToRemove(item);
Matt Tucker's avatar
Matt Tucker committed
451 452 453 454 455 456 457 458
            }
            // 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", nodeID);
            // Send notification that the node configuration has changed
Matt Tucker's avatar
Matt Tucker committed
459
            broadcastNodeEvent(message, false);
Matt Tucker's avatar
Matt Tucker committed
460 461 462
        }
    }
}