LeafNode.java 16.7 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
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;
31 32
import java.util.UUID;
import java.util.concurrent.atomic.AtomicLong;
33

Matt Tucker's avatar
Matt Tucker committed
34 35 36
import org.dom4j.Element;
import org.jivesoftware.util.LocaleUtils;
import org.jivesoftware.util.StringUtils;
37 38
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Matt Tucker's avatar
Matt Tucker committed
39 40
import org.xmpp.forms.DataForm;
import org.xmpp.forms.FormField;
Gaston Dombiak's avatar
Gaston Dombiak committed
41
import org.xmpp.packet.IQ;
Matt Tucker's avatar
Matt Tucker committed
42 43 44 45 46 47 48 49 50 51 52
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 {

53
	private static final Logger Log = LoggerFactory.getLogger(LeafNode.class);
54 55
	private static final String genIdSeed = UUID.randomUUID().toString();
	private static final AtomicLong sequenceCounter = new AtomicLong();
56

Matt Tucker's avatar
Matt Tucker committed
57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73
    /**
     * 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
74 75 76 77
    /**
     * Flag that indicates whether to send items to new subscribers.
     */
    private boolean sendItemSubscribe;
Matt Tucker's avatar
Matt Tucker committed
78 79 80 81 82
    /**
     * 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.
     */
83
    volatile private PublishedItem lastPublished;
Matt Tucker's avatar
Matt Tucker committed
84

85
    // 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
86

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

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

117 118
    @Override
	void postConfigure(DataForm completedForm) {
Matt Tucker's avatar
Matt Tucker committed
119 120 121 122 123 124 125 126 127 128 129 130 131 132
        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;
            }
        }
    }

133 134
    @Override
	protected void addFormFields(DataForm form, boolean isEditing) {
Matt Tucker's avatar
Matt Tucker committed
135 136 137
        super.addFormFields(form, isEditing);

        FormField formField = form.addField();
Matt Tucker's avatar
Matt Tucker committed
138 139 140 141 142 143 144 145 146
        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
147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171
        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);

    }

172 173
    @Override
	protected void deletingNode() {
Matt Tucker's avatar
Matt Tucker committed
174 175
    }

176 177
    void setLastPublishedItem(PublishedItem item) {
    	lastPublished = item;
Matt Tucker's avatar
Matt Tucker committed
178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204
    }

    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
205
        return isPersistPublishedItems() || isPayloadDelivered();
Matt Tucker's avatar
Matt Tucker committed
206 207 208
    }

    /**
Matt Tucker's avatar
Matt Tucker committed
209 210 211 212 213 214 215
     * 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
216 217 218 219 220 221 222 223 224
     *
     * 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
225
     * node subscribers may also use another thread to ensure good performance.<p>
Matt Tucker's avatar
Matt Tucker committed
226
     *
Matt Tucker's avatar
Matt Tucker committed
227 228
     * @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
229
     */
Matt Tucker's avatar
Matt Tucker committed
230 231
    public void publishItems(JID publisher, List<Element> itemElements) {
        List<PublishedItem> newPublishedItems = new ArrayList<PublishedItem>();
Matt Tucker's avatar
Matt Tucker committed
232
        if (isItemRequired()) {
Matt Tucker's avatar
Matt Tucker committed
233 234
            String itemID;
            Element payload;
235
            PublishedItem newItem;
Matt Tucker's avatar
Matt Tucker committed
236 237 238 239
            for (Element item : itemElements) {
                itemID = item.attributeValue("id");
                List entries = item.elements();
                payload = entries.isEmpty() ? null : (Element) entries.get(0);
240 241 242 243
                
                // 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
244
                }
245 246 247 248 249 250 251 252 253 254

                // 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);
                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
                PubSubPersistenceManager.savePublishedItem(newItem);
Matt Tucker's avatar
Matt Tucker committed
255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270
            }
        }

        // 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
271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289
            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) {
290
            PubSubPersistenceManager.removePublishedItem(item);
Matt Tucker's avatar
Matt Tucker committed
291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310
        }
        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
311 312 313
        }
    }

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

344 345
    @Override
	public PublishedItem getPublishedItem(String itemID) {
Matt Tucker's avatar
Matt Tucker committed
346 347 348
        if (!isItemRequired()) {
            return null;
        }
349
        return PubSubPersistenceManager.getPublishedItem(this, itemID);
Matt Tucker's avatar
Matt Tucker committed
350 351
    }

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

357 358
    @Override
	public List<PublishedItem> getPublishedItems(int recentItems) {
359
        return PubSubPersistenceManager.getPublishedItems(this, recentItems);
Matt Tucker's avatar
Matt Tucker committed
360 361
    }

362 363
    @Override
	public PublishedItem getLastPublishedItem() {
364
    	return lastPublished;
Matt Tucker's avatar
Matt Tucker committed
365 366
    }

Matt Tucker's avatar
Matt Tucker committed
367 368 369 370 371
    /**
     * 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.
     */
372 373
    @Override
	public boolean isSendItemSubscribe() {
Matt Tucker's avatar
Matt Tucker committed
374 375 376
        return sendItemSubscribe;
    }

Matt Tucker's avatar
Matt Tucker committed
377 378 379 380 381 382 383 384 385 386 387 388
    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
389 390 391 392
    void setSendItemSubscribe(boolean sendItemSubscribe) {
        this.sendItemSubscribe = sendItemSubscribe;
    }

Matt Tucker's avatar
Matt Tucker committed
393 394 395 396 397 398
    /**
     * 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() {
399 400 401 402 403 404 405 406 407
        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);
408
    }
Matt Tucker's avatar
Matt Tucker committed
409
}