LeafNode.java 18 KB
Newer Older
Matt Tucker's avatar
Matt Tucker committed
1
/**
2
 * Copyright (C) 2005-2008 Jive Software. All rights reserved.
Matt Tucker's avatar
Matt Tucker committed
3
 *
4 5 6 7 8 9 10 11 12 13 14
 * 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
15 16
 */

17
package org.jivesoftware.openfire.pubsub;
Matt Tucker's avatar
Matt Tucker committed
18

19 20 21 22 23
import java.util.ArrayList;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
24 25
import java.util.UUID;
import java.util.concurrent.atomic.AtomicLong;
26

Matt Tucker's avatar
Matt Tucker committed
27 28
import org.dom4j.Element;
import org.jivesoftware.util.LocaleUtils;
29
import org.jivesoftware.util.cache.CacheFactory;
Matt Tucker's avatar
Matt Tucker committed
30 31
import org.xmpp.forms.DataForm;
import org.xmpp.forms.FormField;
Gaston Dombiak's avatar
Gaston Dombiak committed
32
import org.xmpp.packet.IQ;
Matt Tucker's avatar
Matt Tucker committed
33 34 35 36 37 38 39 40 41 42 43
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 {

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

Matt Tucker's avatar
Matt Tucker committed
47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
    /**
     * 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
64 65 66 67
    /**
     * Flag that indicates whether to send items to new subscribers.
     */
    private boolean sendItemSubscribe;
Matt Tucker's avatar
Matt Tucker committed
68
    /**
69
     * The last item published to this node.  In a cluster this may have occurred on a different cluster node.
Matt Tucker's avatar
Matt Tucker committed
70
     */
71
    private PublishedItem lastPublished;
Matt Tucker's avatar
Matt Tucker committed
72

73
    // 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
74

75
    public LeafNode(PubSubService service, CollectionNode parentNode, String nodeID, JID creator) {
Matt Tucker's avatar
Matt Tucker committed
76 77 78 79 80 81
        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
82
        this.sendItemSubscribe = defaultConfiguration.isSendItemSubscribe();
Matt Tucker's avatar
Matt Tucker committed
83 84
    }

85
    @Override
86
	protected void configure(FormField field) throws NotAcceptableException {
Matt Tucker's avatar
Matt Tucker committed
87 88 89 90 91 92 93 94 95 96 97
        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
98 99 100 101 102
        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
103 104
    }

105 106
    @Override
	void postConfigure(DataForm completedForm) {
Matt Tucker's avatar
Matt Tucker committed
107 108 109 110 111 112 113 114 115 116 117 118 119 120
        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;
            }
        }
    }

121 122
    @Override
	protected void addFormFields(DataForm form, boolean isEditing) {
Matt Tucker's avatar
Matt Tucker committed
123 124
        super.addFormFields(form, isEditing);

125 126 127
        FormField typeField = form.getField("pubsub#node_type");
        typeField.addValue("leaf");
        
Matt Tucker's avatar
Matt Tucker committed
128
        FormField formField = form.addField();
Matt Tucker's avatar
Matt Tucker committed
129 130 131 132 133 134 135 136 137
        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
138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162
        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);

    }

163 164
    @Override
	protected void deletingNode() {
Matt Tucker's avatar
Matt Tucker committed
165 166
    }

167 168 169 170
	public synchronized void setLastPublishedItem(PublishedItem item)
	{
		if ((lastPublished == null) || (item != null) && item.getCreationDate().after(lastPublished.getCreationDate()))
			lastPublished = item;
Matt Tucker's avatar
Matt Tucker committed
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
    }

    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
198
        return isPersistPublishedItems() || isPayloadDelivered();
Matt Tucker's avatar
Matt Tucker committed
199 200 201
    }

    /**
Matt Tucker's avatar
Matt Tucker committed
202 203 204 205 206 207 208
     * 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
209 210 211 212 213 214 215 216 217
     *
     * 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
218
     * node subscribers may also use another thread to ensure good performance.<p>
Matt Tucker's avatar
Matt Tucker committed
219
     *
Matt Tucker's avatar
Matt Tucker committed
220 221
     * @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
222
     */
Matt Tucker's avatar
Matt Tucker committed
223
    public void publishItems(JID publisher, List<Element> itemElements) {
224
        List<PublishedItem> newPublishedItems = new ArrayList<>();
Matt Tucker's avatar
Matt Tucker committed
225
        if (isItemRequired()) {
Matt Tucker's avatar
Matt Tucker committed
226 227
            String itemID;
            Element payload;
228
            PublishedItem newItem;
Matt Tucker's avatar
Matt Tucker committed
229 230 231 232
            for (Element item : itemElements) {
                itemID = item.attributeValue("id");
                List entries = item.elements();
                payload = entries.isEmpty() ? null : (Element) entries.get(0);
233 234 235 236
                
                // Make sure that the published item has a unique ID if NOT assigned by publisher
                if (itemID == null) {
                	itemID = genIdSeed + sequenceCounter.getAndIncrement();
Matt Tucker's avatar
Matt Tucker committed
237
                }
238 239

                // Create a new published item
240
                newItem = new PublishedItem(this, publisher, itemID, new Date(CacheFactory.getClusterTime()));
241 242 243 244 245 246
                newItem.setPayload(payload);
                // Add the new item to the list of published items
                newPublishedItems.add(newItem);
                setLastPublishedItem(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
247 248 249
                if (isPersistPublishedItems()) {
                	PubSubPersistenceManager.savePublishedItem(newItem);
                }
Matt Tucker's avatar
Matt Tucker committed
250 251 252 253 254 255 256
            }
        }

        // 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
257
        Set<NodeAffiliate> affiliatesToNotify = new HashSet<>(affiliates);
Matt Tucker's avatar
Matt Tucker committed
258 259 260 261 262 263 264 265
        // 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
266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284
            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) {
        // Remove deleted items from the database
        for (PublishedItem item : toDelete) {
285
            PubSubPersistenceManager.removePublishedItem(item);
Matt Tucker's avatar
Matt Tucker committed
286 287 288 289 290 291 292 293 294
        }
        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
295
            Set<NodeAffiliate> affiliatesToNotify = new HashSet<>(affiliates);
Matt Tucker's avatar
Matt Tucker committed
296 297 298 299 300 301 302 303 304 305
            // 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
306 307 308
        }
    }

309 310 311 312 313 314 315 316 317 318 319 320
    /**
     * 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);
321 322 323 324
        Element pubsubElem = result.setChildElement("pubsub", "http://jabber.org/protocol/pubsub");
        Element items = pubsubElem.addElement("items");
        items.addAttribute("node", getNodeID());
        
325 326 327 328 329 330 331 332 333 334 335 336 337 338
        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);
    }

339
    @Override
340
    public PublishedItem getPublishedItem(String itemID) {
Matt Tucker's avatar
Matt Tucker committed
341 342 343
        if (!isItemRequired()) {
            return null;
        }
344 345 346 347 348
        synchronized (this) {
            if (lastPublished != null && lastPublished.getID().equals(itemID)) {
                return lastPublished;
            }
        }
349
        return PubSubPersistenceManager.getPublishedItem(this, itemID);
Matt Tucker's avatar
Matt Tucker committed
350 351
    }

352 353
    @Override
	public List<PublishedItem> getPublishedItems() {
354
        return getPublishedItems(getMaxPublishedItems());
Matt Tucker's avatar
Matt Tucker committed
355 356
    }

357
    @Override
358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380
    public synchronized List<PublishedItem> getPublishedItems(int recentItems) {
        List<PublishedItem> publishedItems = PubSubPersistenceManager.getPublishedItems(this, recentItems);
        if (lastPublished != null) {
            // The persistent items may not contain the last item, if it wasn't persisted anymore (e.g. if node configuration changed).
            // Therefore check, if the last item has been persisted.
            boolean persistentItemsContainsLastItem = false;
            for (PublishedItem publishedItem : publishedItems) {
                if (publishedItem.getID().equals(lastPublished.getID())) {
                    persistentItemsContainsLastItem = true;
                    break;
                }
            }
            if (!persistentItemsContainsLastItem) {
                // And if not, include the last item.
                publishedItems.add(0, lastPublished);
                // Recheck the collection size, it might have one more element now (the last item).
                // Remove it, if it exceeds the max items.
                if (publishedItems.size() > recentItems) {
                    publishedItems.remove(publishedItems.size() - 1);
                }
            }
        }
        return publishedItems;
Matt Tucker's avatar
Matt Tucker committed
381 382
    }

383
    @Override
384
	public synchronized PublishedItem getLastPublishedItem() {
385 386 387
    	if (lastPublished == null){
    		lastPublished = PubSubPersistenceManager.getLastPublishedItem(this);
    	}
388
    	return lastPublished;
Matt Tucker's avatar
Matt Tucker committed
389 390
    }

Matt Tucker's avatar
Matt Tucker committed
391 392 393 394 395
    /**
     * 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.
     */
396 397
    @Override
	public boolean isSendItemSubscribe() {
Matt Tucker's avatar
Matt Tucker committed
398 399 400
        return sendItemSubscribe;
    }

Matt Tucker's avatar
Matt Tucker committed
401 402 403 404 405 406 407 408 409 410 411 412
    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
413 414 415 416
    void setSendItemSubscribe(boolean sendItemSubscribe) {
        this.sendItemSubscribe = sendItemSubscribe;
    }

Matt Tucker's avatar
Matt Tucker committed
417 418 419 420 421 422
    /**
     * 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() {
423 424 425 426 427 428 429 430 431
        PubSubPersistenceManager.purgeNode(this);
        // Broadcast purge notification to subscribers
        // Build packet to broadcast to subscribers
        Message message = new Message();
        Element event = message.addChildElement("event", "http://jabber.org/protocol/pubsub#event");
        Element items = event.addElement("purge");
        items.addAttribute("node", nodeID);
        // Send notification that the node configuration has changed
        broadcastNodeEvent(message, false);
432
    }
Matt Tucker's avatar
Matt Tucker committed
433
}