LeafNode.java 20.9 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 30 31
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

Matt Tucker's avatar
Matt Tucker committed
32 33 34
import org.dom4j.Element;
import org.jivesoftware.util.LocaleUtils;
import org.jivesoftware.util.StringUtils;
35 36
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Matt Tucker's avatar
Matt Tucker committed
37 38
import org.xmpp.forms.DataForm;
import org.xmpp.forms.FormField;
Gaston Dombiak's avatar
Gaston Dombiak committed
39
import org.xmpp.packet.IQ;
Matt Tucker's avatar
Matt Tucker committed
40 41 42 43 44 45 46 47 48 49 50
import org.xmpp.packet.JID;
import org.xmpp.packet.Message;

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

51 52
	private static final Logger Log = LoggerFactory.getLogger(LeafNode.class);

Matt Tucker's avatar
Matt Tucker committed
53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69
    /**
     * 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
70 71 72 73
    /**
     * Flag that indicates whether to send items to new subscribers.
     */
    private boolean sendItemSubscribe;
Matt Tucker's avatar
Matt Tucker committed
74 75 76 77 78
    /**
     * 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.
     */
79
    protected final List<PublishedItem> publishedItems = new ArrayList<PublishedItem>();
Matt Tucker's avatar
Matt Tucker committed
80 81
    protected Map<String, PublishedItem> itemsByID = new HashMap<String, PublishedItem>();

82
    // 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
83

84
    public LeafNode(PubSubService service, CollectionNode parentNode, String nodeID, JID creator) {
Matt Tucker's avatar
Matt Tucker committed
85 86 87 88 89 90
        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
91
        this.sendItemSubscribe = defaultConfiguration.isSendItemSubscribe();
Matt Tucker's avatar
Matt Tucker committed
92 93
    }

94 95
    @Override
	void configure(FormField field) {
Matt Tucker's avatar
Matt Tucker committed
96 97 98 99 100 101 102 103 104 105 106
        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
107 108 109 110 111
        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
112 113
    }

114 115
    @Override
	void postConfigure(DataForm completedForm) {
Matt Tucker's avatar
Matt Tucker committed
116 117 118 119 120 121 122 123 124 125 126 127
        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
128 129
        synchronized (publishedItems) {
            // Remove stored published items based on the new max items
130 131 132
            while (!publishedItems.isEmpty() && isMaxItemsReached())
            {
                removeItem(0);
Matt Tucker's avatar
Matt Tucker committed
133
            }
Matt Tucker's avatar
Matt Tucker committed
134
        }
Matt Tucker's avatar
Matt Tucker committed
135 136
    }

137 138
    @Override
	protected void addFormFields(DataForm form, boolean isEditing) {
Matt Tucker's avatar
Matt Tucker committed
139 140 141
        super.addFormFields(form, isEditing);

        FormField formField = form.addField();
Matt Tucker's avatar
Matt Tucker committed
142 143 144 145 146 147 148 149 150
        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
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
        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);

    }

176 177
    @Override
	protected void deletingNode() {
Matt Tucker's avatar
Matt Tucker committed
178 179 180
        synchronized (publishedItems) {
            // Remove stored published items
            while (!publishedItems.isEmpty()) {
181
                removeItem(0);
Matt Tucker's avatar
Matt Tucker committed
182 183 184 185
            }
        }
    }

Matt Tucker's avatar
Matt Tucker committed
186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217
    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
218
        return isPersistPublishedItems() || isPayloadDelivered();
Matt Tucker's avatar
Matt Tucker committed
219 220 221
    }

    /**
Matt Tucker's avatar
Matt Tucker committed
222 223 224 225 226 227 228
     * 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
229 230 231 232 233 234 235 236 237
     *
     * 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
238
     * node subscribers may also use another thread to ensure good performance.<p>
Matt Tucker's avatar
Matt Tucker committed
239
     *
Matt Tucker's avatar
Matt Tucker committed
240 241
     * @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
242
     */
Matt Tucker's avatar
Matt Tucker committed
243 244
    public void publishItems(JID publisher, List<Element> itemElements) {
        List<PublishedItem> newPublishedItems = new ArrayList<PublishedItem>();
Matt Tucker's avatar
Matt Tucker committed
245
        if (isItemRequired()) {
Matt Tucker's avatar
Matt Tucker committed
246 247
            String itemID;
            Element payload;
248
            PublishedItem newItem;
Matt Tucker's avatar
Matt Tucker committed
249 250 251 252 253 254
            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) {
255
                    // Make sure that the published item has a unique ID if NOT assigned by publisher
Matt Tucker's avatar
Matt Tucker committed
256
                    if (itemID == null) {
257 258 259 260
                    	do {
                    		itemID = StringUtils.randomString(15);
                    	}
                        while (itemsByID.containsKey(itemID));
Matt Tucker's avatar
Matt Tucker committed
261 262 263 264 265 266 267 268
                    }

                    // 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);

269 270 271 272 273 274 275 276 277
                    // Check and remove any existing items that have the matching ID,
                    // generated ID's won't match since we already checked.
                    PublishedItem duplicate = itemsByID.get(newItem.getID());
                    
                    if (duplicate != null)
                    {
                    	removeItem(findIndexById(duplicate.getID()));
                    }

Matt Tucker's avatar
Matt Tucker committed
278 279
                    // 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.
280
                    while (!publishedItems.isEmpty() && isMaxItemsReached())
Matt Tucker's avatar
Matt Tucker committed
281
                    {
282
                        removeItem(0);
Matt Tucker's avatar
Matt Tucker committed
283
                    }
284
                    
Matt Tucker's avatar
Matt Tucker committed
285 286 287
                    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
288
                    service.queueItemToAdd(newItem);
Matt Tucker's avatar
Matt Tucker committed
289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305
                }
            }
        }

        // 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
306 307 308 309
            affiliate.sendPublishedNotifications(message, event, this, newPublishedItems);
        }
    }

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
	/**
     * Must be called from code synchronized on publishedItems
     */
    private int findIndexById(String id) {
    	for (int i=0; i<publishedItems.size(); i++)
    	{
    		PublishedItem item = publishedItems.get(i);
    		
			if (item.getID().equals(id))
				return i;
		}
		return -1;
	}

	/**
     * Must be called from code synchronized on publishedItems
     */
	private void removeItem(int index) {
        PublishedItem removedItem = publishedItems.remove(index);
		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
		service.queueItemToRemove(removedItem);
	}

Matt Tucker's avatar
Matt Tucker committed
335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357
    /**
     * 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
358
            service.queueItemToRemove(item);
Matt Tucker's avatar
Matt Tucker committed
359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378
        }
        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
379 380 381
        }
    }

382 383 384 385 386 387 388 389 390 391 392 393
    /**
     * 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);
394 395 396 397
        Element pubsubElem = result.setChildElement("pubsub", "http://jabber.org/protocol/pubsub");
        Element items = pubsubElem.addElement("items");
        items.addAttribute("node", getNodeID());
        
398 399 400 401 402 403 404 405 406 407 408 409 410 411
        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);
    }

412 413
    @Override
	public PublishedItem getPublishedItem(String itemID) {
Matt Tucker's avatar
Matt Tucker committed
414 415 416 417 418 419 420 421
        if (!isItemRequired()) {
            return null;
        }
        synchronized (publishedItems) {
            return itemsByID.get(itemID);
        }
    }

422 423
    @Override
	public List<PublishedItem> getPublishedItems() {
Matt Tucker's avatar
Matt Tucker committed
424 425 426 427 428
        synchronized (publishedItems) {
            return Collections.unmodifiableList(publishedItems);
        }
    }

429 430
    @Override
	public List<PublishedItem> getPublishedItems(int recentItems) {
Matt Tucker's avatar
Matt Tucker committed
431 432 433 434 435 436 437 438 439 440 441 442 443 444
        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);
            }
        }
    }

445 446
    @Override
	public PublishedItem getLastPublishedItem() {
Matt Tucker's avatar
Matt Tucker committed
447 448 449 450 451 452 453 454
        synchronized (publishedItems) {
            if (publishedItems.isEmpty()) {
                return null;
            }
            return publishedItems.get(publishedItems.size()-1);
        }
    }

Matt Tucker's avatar
Matt Tucker committed
455 456 457 458 459
    /**
     * 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.
     */
460 461
    @Override
	public boolean isSendItemSubscribe() {
Matt Tucker's avatar
Matt Tucker committed
462 463 464
        return sendItemSubscribe;
    }

Matt Tucker's avatar
Matt Tucker committed
465 466 467 468 469 470 471 472 473 474 475 476
    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
477 478 479 480
    void setSendItemSubscribe(boolean sendItemSubscribe) {
        this.sendItemSubscribe = sendItemSubscribe;
    }

Matt Tucker's avatar
Matt Tucker committed
481 482 483 484 485 486 487 488 489 490 491
    /**
     * 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
492 493
                toDelete = new ArrayList<PublishedItem>(
                        publishedItems.subList(0, publishedItems.size() - 1));
Matt Tucker's avatar
Matt Tucker committed
494 495 496 497 498 499 500 501 502 503
                // 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
504
                service.queueItemToRemove(item);
Matt Tucker's avatar
Matt Tucker committed
505 506 507 508 509 510 511 512
            }
            // 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
513
            broadcastNodeEvent(message, false);
Matt Tucker's avatar
Matt Tucker committed
514 515
        }
    }
516 517 518 519 520
    
    private boolean isMaxItemsReached()
    {
    	return (maxPublishedItems > -1 ) && (publishedItems.size() >= maxPublishedItems);
    }
Matt Tucker's avatar
Matt Tucker committed
521
}