Commit 4fca9fe9 authored by Matt Tucker's avatar Matt Tucker Committed by matt

First pubsub code landing.

git-svn-id: http://svn.igniterealtime.org/svn/repos/wildfire/trunk@3580 b35dd754-fafc-0310-a699-88a17e54d16e
parent a9ec7c81
/**
* $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.
*/
package org.jivesoftware.wildfire.pubsub;
import org.xmpp.packet.JID;
import org.xmpp.forms.FormField;
import org.xmpp.forms.DataForm;
import org.jivesoftware.util.LocaleUtils;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
/**
* A type of node that contains nodes and/or other collections but no published
* items. Collections provide the foundation entity to provide a means of representing
* hierarchical node structures.
*
* @author Matt Tucker
*/
public class CollectionNode extends Node {
/**
* Map that contains the child nodes of this node. The key is the child node ID and the
* value is the child node. A map is used to ensure uniqueness and in particular
* a ConcurrentHashMap for concurrency reasons.
*/
private Map<String, Node> nodes = new ConcurrentHashMap<String, Node>();
/**
* Policy that defines who may associate leaf nodes with a collection.
*/
private LeafNodeAssociationPolicy associationPolicy = LeafNodeAssociationPolicy.all;
/**
* Users that are allowed to associate leaf nodes with this collection node. This collection
* is going to be used only when the associationPolicy is <tt>whitelist</tt>.
*/
private Collection<JID> associationTrusted = new ArrayList<JID>();
/**
* Max number of leaf nodes that this collection node might have. A value of -1 means
* that there is no limit.
*/
private int maxLeafNodes = -1;
// TODO Send event notification when a new child node is added (section 9.2)
// TODO Add checking that max number of leaf nodes has been reached
// TODO Add checking that verifies that user that is associating leaf node with collection node is allowed
CollectionNode(PubSubService service, CollectionNode parentNode, String nodeID, JID creator) {
super(service, parentNode, nodeID, creator);
// Configure node with default values (get them from the pubsub service)
DefaultNodeConfiguration defaultConfiguration = service.getDefaultNodeConfiguration(false);
this.associationPolicy = defaultConfiguration.getAssociationPolicy();
this.maxLeafNodes = defaultConfiguration.getMaxLeafNodes();
}
void configure(FormField field) {
List<String> values;
if ("pubsub#leaf_node_association_policy".equals(field.getVariable())) {
values = field.getValues();
if (values.size() > 0) {
associationPolicy = LeafNodeAssociationPolicy.valueOf(values.get(0));
}
}
else if ("pubsub#leaf_node_association_whitelist".equals(field.getVariable())) {
// Get the new list of users that may add leaf nodes to this collection node
associationTrusted = new ArrayList<JID>();
for (String value : field.getValues()) {
try {
associationTrusted.add(new JID(value));
}
catch (Exception e) {}
}
}
else if ("pubsub#leaf_nodes_max".equals(field.getVariable())) {
values = field.getValues();
maxLeafNodes = values.size() > 0 ? Integer.parseInt(values.get(0)) : -1;
}
}
void postConfigure(DataForm completedForm) {
//Do nothing.
}
protected void addFormFields(DataForm form, boolean isEditing) {
super.addFormFields(form, isEditing);
FormField formField = form.addField();
formField.setVariable("pubsub#leaf_node_association_policy");
if (isEditing) {
formField.setType(FormField.Type.list_single);
formField.setLabel(LocaleUtils.getLocalizedString("pubsub.form.conf.leaf_node_association"));
formField.addOption(null, LeafNodeAssociationPolicy.all.name());
formField.addOption(null, LeafNodeAssociationPolicy.owners.name());
formField.addOption(null, LeafNodeAssociationPolicy.whitelist.name());
}
formField.addValue(associationPolicy.name());
formField = form.addField();
formField.setVariable("pubsub#leaf_node_association_whitelist");
if (isEditing) {
formField.setType(FormField.Type.jid_multi);
formField.setLabel(LocaleUtils.getLocalizedString("pubsub.form.conf.leaf_node_whitelist"));
}
for (JID contact : associationTrusted) {
formField.addValue(contact.toString());
}
formField = form.addField();
formField.setVariable("pubsub#leaf_nodes_max");
if (isEditing) {
formField.setType(FormField.Type.text_single);
formField.setLabel(LocaleUtils.getLocalizedString("pubsub.form.conf.leaf_nodes_max"));
}
formField.addValue(maxLeafNodes);
}
void addChildNode(Node child) {
nodes.put(child.getNodeID(), child);
}
void removeChildNode(Node child) {
nodes.remove(child.getNodeID());
}
public boolean isCollectionNode() {
return true;
}
/**
* Returns true if the specified node is a first-level children of this collection
* node.
*
* @param child the node to check if it is a direct child of this node.
* @return true if the specified node is a first-level children of this collection
* node.
*/
public boolean isChildNode(Node child) {
return nodes.containsKey(child.getNodeID());
}
/**
* Returns true if the specified node is a direct child node of this collection node or
* a descendant of the children nodes.
*
* @param child the node to check if it is a descendant of this node.
* @return true if the specified node is a direct child node of this collection node or
* a descendant of the children nodes.
*/
public boolean isDescendantNode(Node child) {
if (isChildNode(child)) {
return true;
}
for (Node node : getNodes()) {
if (node.isDescendantNode(child)) {
return true;
}
}
return false;
}
public Collection<Node> getNodes() {
return nodes.values();
}
/**
* Returns the policy that defines who may associate leaf nodes with a collection.
*
* @return the policy that defines who may associate leaf nodes with a collection.
*/
public LeafNodeAssociationPolicy getAssociationPolicy() {
return associationPolicy;
}
/**
* Returns the users that are allowed to associate leaf nodes with this collection node.
* This collection is going to be used only when the associationPolicy is <tt>whitelist</tt>.
*
* @return the users that are allowed to associate leaf nodes with this collection node.
*/
public Collection<JID> getAssociationTrusted() {
return associationTrusted;
}
/**
* Returns the max number of leaf nodes that this collection node might have. A value of
* -1 means that there is no limit.
*
* @return the max number of leaf nodes that this collection node might have.
*/
public int getMaxLeafNodes() {
return maxLeafNodes;
}
/**
* Sets the policy that defines who may associate leaf nodes with a collection.
*
* @param associationPolicy the policy that defines who may associate leaf nodes
* with a collection.
*/
void setAssociationPolicy(LeafNodeAssociationPolicy associationPolicy) {
this.associationPolicy = associationPolicy;
}
/**
* Sets the users that are allowed to associate leaf nodes with this collection node.
* This collection is going to be used only when the associationPolicy is <tt>whitelist</tt>.
*
* @param associationTrusted the users that are allowed to associate leaf nodes with this
* collection node.
*/
void setAssociationTrusted(Collection<JID> associationTrusted) {
this.associationTrusted = associationTrusted;
}
/**
* Sets the max number of leaf nodes that this collection node might have. A value of
* -1 means that there is no limit.
*
* @param maxLeafNodes the max number of leaf nodes that this collection node might have.
*/
void setMaxLeafNodes(int maxLeafNodes) {
this.maxLeafNodes = maxLeafNodes;
}
/**
* Policy that defines who may associate leaf nodes with a collection.
*/
public static enum LeafNodeAssociationPolicy {
/**
* Anyone may associate leaf nodes with the collection.
*/
all,
/**
* Only collection node owners may associate leaf nodes with the collection.
*/
owners,
/**
* Only those on a whitelist may associate leaf nodes with the collection.
*/
whitelist;
}
}
/**
* $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.
*/
package org.jivesoftware.wildfire.pubsub;
import org.jivesoftware.util.LocaleUtils;
import org.jivesoftware.wildfire.pubsub.models.AccessModel;
import org.jivesoftware.wildfire.pubsub.models.PublisherModel;
import org.xmpp.forms.DataForm;
import org.xmpp.forms.FormField;
/**
* A DefaultNodeConfiguration keeps the default configuration values for leaf or collection
* nodes of a particular publish-subscribe service. New nodes created for the service
* will be initialized with the values defined in the default configuration.
*
* @author Matt Tucker
*/
public class DefaultNodeConfiguration {
/**
* Flag indicating whether this default configutation belongs to a leaf node or not.
*/
private boolean leaf;
/**
* Flag that indicates whether to deliver payloads with event notifications.
*/
private boolean deliverPayloads;
/**
* The maximum payload size in bytes.
*/
private int maxPayloadSize;
/**
* 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;
/**
* Flag that indicates whether to notify subscribers when the node configuration changes.
*/
private boolean notifyConfigChanges;
/**
* Flag that indicates whether to notify subscribers when the node is deleted.
*/
private boolean notifyDelete;
/**
* Flag that indicates whether to notify subscribers when items are removed from the node.
*/
private boolean notifyRetract;
/**
* Flag that indicates whether to deliver notifications to available users only.
*/
private boolean presenceBasedDelivery;
/**
* Flag that indicates whether to send items to new subscribers.
*/
private boolean sendItemSubscribe;
/**
* Publisher model that specifies who is allowed to publish items to the node.
*/
private PublisherModel publisherModel = PublisherModel.open;
/**
* Flag that indicates that subscribing and unsubscribing are enabled.
*/
private boolean subscriptionEnabled;
/**
* Access model that specifies who is allowed to subscribe and retrieve items.
*/
private AccessModel accessModel = AccessModel.open;
/**
* The default language of the node.
*/
private String language = "";
/**
* Policy that defines whether owners or publisher should receive replies to items.
*/
private Node.ItemReplyPolicy replyPolicy = Node.ItemReplyPolicy.owner;
/**
* Policy that defines who may associate leaf nodes with a collection.
*/
private CollectionNode.LeafNodeAssociationPolicy associationPolicy =
CollectionNode.LeafNodeAssociationPolicy.all;
/**
* Max number of leaf nodes that this collection node might have. A value of -1 means
* that there is no limit.
*/
private int maxLeafNodes = -1;
public DefaultNodeConfiguration(boolean isLeafType) {
this.leaf = isLeafType;
}
/**
* Returns true if this default configutation belongs to a leaf node.
*
* @return true if this default configutation belongs to a leaf node.
*/
public boolean isLeaf() {
return leaf;
}
/**
* Returns true if payloads are going to be delivered with event notifications.
*
* @return true if payloads are going to be delivered with event notifications.
*/
public boolean isDeliverPayloads() {
return deliverPayloads;
}
/**
* Returns the maximum payload size in bytes.
*
* @return the maximum payload size in bytes.
*/
public int getMaxPayloadSize() {
return maxPayloadSize;
}
/**
* Returns true if items are going to be persisted in a storage. Note that when the
* variable is false then the last published item is the only items being saved
* to the backend storage.
*
* @return true if items are going to be persisted in a storage.
*/
public boolean isPersistPublishedItems() {
return persistPublishedItems;
}
/**
* Returns the 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.
*
* @return the maximum number of published items to persist.
*/
public int getMaxPublishedItems() {
return maxPublishedItems;
}
/**
* Returns true if subscribers are going to be notified when node configuration changes.
*
* @return true if subscribers are going to be notified when node configuration changes.
*/
public boolean isNotifyConfigChanges() {
return notifyConfigChanges;
}
/**
* Returns true if subscribers are going to be notified when node is deleted.
*
* @return true if subscribers are going to be notified when node is deleted.
*/
public boolean isNotifyDelete() {
return notifyDelete;
}
/**
* Returns true if subscribers are going to be notified when items are removed from the node.
*
* @return true if subscribers are going to be notified when items are removed from the node.
*/
public boolean isNotifyRetract() {
return notifyRetract;
}
/**
* Returns true if notifications are going to be delivered only to available users.
*
* @return true if notifications are going to be delivered only to available users.
*/
public boolean isPresenceBasedDelivery() {
return presenceBasedDelivery;
}
/**
* Returns true if new subscribers are going to receive new items once subscribed.
*
* @return true if new subscribers are going to receive new items once subscribed.
*/
public boolean isSendItemSubscribe() {
return sendItemSubscribe;
}
/**
* Returnes the publisher model that specifies who is allowed to publish items to the node.
*
* @return the publisher model that specifies who is allowed to publish items to the node.
*/
public PublisherModel getPublisherModel() {
return publisherModel;
}
/**
* Returns true if subscribing and unsubscribing are enabled.
*
* @return true if subscribing and unsubscribing are enabled.
*/
public boolean isSubscriptionEnabled() {
return subscriptionEnabled;
}
/**
* Returns the access model that specifies who is allowed to subscribe and retrieve items.
*
* @return the access model that specifies who is allowed to subscribe and retrieve items.
*/
public AccessModel getAccessModel() {
return accessModel;
}
/**
* Returns the default language of the node.
*
* @return the default language of the node.
*/
public String getLanguage() {
return language;
}
/**
* Returns the policy that defines whether owners or publisher should receive
* replies to items.
*
* @return the policy that defines whether owners or publisher should receive
* replies to items.
*/
public Node.ItemReplyPolicy getReplyPolicy() {
return replyPolicy;
}
/**
* Returns the policy that defines who may associate leaf nodes with a collection.
*
* @return the policy that defines who may associate leaf nodes with a collection.
*/
public CollectionNode.LeafNodeAssociationPolicy getAssociationPolicy() {
return associationPolicy;
}
/**
* Returns the max number of leaf nodes that this collection node might have. A value of
* -1 means that there is no limit.
*
* @return the max number of leaf nodes that this collection node might have.
*/
public int getMaxLeafNodes() {
return maxLeafNodes;
}
/**
* Sets if payloads are going to be delivered with event notifications.
*
* @param deliverPayloads true if payloads are going to be delivered with event notifications.
*/
public void setDeliverPayloads(boolean deliverPayloads) {
this.deliverPayloads = deliverPayloads;
}
/**
* Sets the maximum payload size in bytes.
*
* @param maxPayloadSize the maximum payload size in bytes.
*/
public void setMaxPayloadSize(int maxPayloadSize) {
this.maxPayloadSize = maxPayloadSize;
}
/**
* Sets if items are going to be persisted in a storage. Note that when the
* variable is false then the last published item is the only items being saved
* to the backend storage.
*
* @param persistPublishedItems true if items are going to be persisted in a storage.
*/
public void setPersistPublishedItems(boolean persistPublishedItems) {
this.persistPublishedItems = persistPublishedItems;
}
/**
* Sets the 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.
*
* @param maxPublishedItems the maximum number of published items to persist.
*/
public void setMaxPublishedItems(int maxPublishedItems) {
this.maxPublishedItems = maxPublishedItems;
}
/**
* Sets if subscribers are going to be notified when node configuration changes.
*
* @param notifyConfigChanges true if subscribers are going to be notified when node
* configuration changes.
*/
public void setNotifyConfigChanges(boolean notifyConfigChanges) {
this.notifyConfigChanges = notifyConfigChanges;
}
/**
* Sets if subscribers are going to be notified when node is deleted.
*
* @param notifyDelete true if subscribers are going to be notified when node is deleted.
*/
public void setNotifyDelete(boolean notifyDelete) {
this.notifyDelete = notifyDelete;
}
/**
* Sets if subscribers are going to be notified when items are removed from the node.
*
* @param notifyRetract true if subscribers are going to be notified when items are removed
* from the node.
*/
public void setNotifyRetract(boolean notifyRetract) {
this.notifyRetract = notifyRetract;
}
/**
* Sets if notifications are going to be delivered only to available users.
*
* @param presenceBasedDelivery true if notifications are going to be delivered only to
* available users.
*/
public void setPresenceBasedDelivery(boolean presenceBasedDelivery) {
this.presenceBasedDelivery = presenceBasedDelivery;
}
/**
* Sets if new subscribers are going to receive new items once subscribed.
*
* @param sendItemSubscribe true if new subscribers are going to receive new items
* once subscribed.
*/
public void setSendItemSubscribe(boolean sendItemSubscribe) {
this.sendItemSubscribe = sendItemSubscribe;
}
/**
* Sets the publisher model that specifies who is allowed to publish items to the node.
*
* @param publisherModel the publisher model that specifies who is allowed to publish
* items to the node.
*/
public void setPublisherModel(PublisherModel publisherModel) {
this.publisherModel = publisherModel;
}
/**
* Sets if subscribing and unsubscribing are enabled.
*
* @param subscriptionEnabled true if subscribing and unsubscribing are enabled.
*/
public void setSubscriptionEnabled(boolean subscriptionEnabled) {
this.subscriptionEnabled = subscriptionEnabled;
}
/**
* Sets the access model that specifies who is allowed to subscribe and retrieve items.
*
* @param accessModel the access model that specifies who is allowed to subscribe and
* retrieve items.
*/
public void setAccessModel(AccessModel accessModel) {
this.accessModel = accessModel;
}
/**
* Sets the default language of the node.
*
* @param language the default language of the node.
*/
public void setLanguage(String language) {
this.language = language;
}
/**
* Sets the policy that defines whether owners or publisher should receive replies to items.
*
* @param replyPolicy the policy that defines whether owners or publisher should receive
* replies to items.
*/
public void setReplyPolicy(Node.ItemReplyPolicy replyPolicy) {
this.replyPolicy = replyPolicy;
}
/**
* Sets the policy that defines who may associate leaf nodes with a collection.
*
* @param associationPolicy the policy that defines who may associate leaf nodes
* with a collection.
*/
public void setAssociationPolicy(CollectionNode.LeafNodeAssociationPolicy associationPolicy) {
this.associationPolicy = associationPolicy;
}
/**
* Sets the max number of leaf nodes that this collection node might have. A value of
* -1 means that there is no limit.
*
* @param maxLeafNodes the max number of leaf nodes that this collection node might have.
*/
public void setMaxLeafNodes(int maxLeafNodes) {
this.maxLeafNodes = maxLeafNodes;
}
public DataForm getConfigurationForm() {
DataForm form = new DataForm(DataForm.Type.form);
form.setTitle(LocaleUtils.getLocalizedString("pubsub.form.default.title"));
form.addInstruction(LocaleUtils.getLocalizedString("pubsub.form.default.instruction"));
// Add the form fields and configure them for edition
FormField formField = form.addField();
formField.setVariable("FORM_TYPE");
formField.setType(FormField.Type.hidden);
formField.addValue("http://jabber.org/protocol/pubsub#node_config");
formField = form.addField();
formField.setVariable("pubsub#subscribe");
formField.setType(FormField.Type.boolean_type);
formField.setLabel(LocaleUtils.getLocalizedString("pubsub.form.conf.subscribe"));
formField.addValue(subscriptionEnabled);
formField = form.addField();
formField.setVariable("pubsub#deliver_payloads");
formField.setType(FormField.Type.boolean_type);
formField.setLabel(LocaleUtils.getLocalizedString("pubsub.form.conf.deliver_payloads"));
formField.addValue(deliverPayloads);
formField = form.addField();
formField.setVariable("pubsub#send_item_subscribe");
formField.setType(FormField.Type.boolean_type);
formField.setLabel(
LocaleUtils.getLocalizedString("pubsub.form.conf.send_item_subscribe"));
formField.addValue(sendItemSubscribe);
formField = form.addField();
formField.setVariable("pubsub#notify_config");
formField.setType(FormField.Type.boolean_type);
formField.setLabel(LocaleUtils.getLocalizedString("pubsub.form.conf.notify_config"));
formField.addValue(notifyConfigChanges);
formField = form.addField();
formField.setVariable("pubsub#notify_delete");
formField.setType(FormField.Type.boolean_type);
formField.setLabel(LocaleUtils.getLocalizedString("pubsub.form.conf.notify_delete"));
formField.addValue(notifyDelete);
formField = form.addField();
formField.setVariable("pubsub#notify_retract");
formField.setType(FormField.Type.boolean_type);
formField.setLabel(LocaleUtils.getLocalizedString("pubsub.form.conf.notify_retract"));
formField.addValue(notifyRetract);
formField = form.addField();
formField.setVariable("pubsub#presence_based_delivery");
formField.setType(FormField.Type.boolean_type);
formField.setLabel(LocaleUtils.getLocalizedString("pubsub.form.conf.presence_based"));
formField.addValue(presenceBasedDelivery);
if (leaf) {
formField = form.addField();
formField.setVariable("pubsub#persist_items");
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");
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");
formField.setType(FormField.Type.text_single);
formField.setLabel(LocaleUtils.getLocalizedString("pubsub.form.conf.max_payload_size"));
formField.addValue(maxPayloadSize);
}
formField = form.addField();
formField.setVariable("pubsub#access_model");
formField.setType(FormField.Type.list_single);
formField.setLabel(LocaleUtils.getLocalizedString("pubsub.form.conf.access_model"));
formField.addOption(null, AccessModel.authorize.getName());
formField.addOption(null, AccessModel.open.getName());
formField.addOption(null, AccessModel.presence.getName());
formField.addOption(null, AccessModel.roster.getName());
formField.addOption(null, AccessModel.whitelist.getName());
formField.addValue(accessModel.getName());
formField = form.addField();
formField.setVariable("pubsub#publish_model");
formField.setType(FormField.Type.list_single);
formField.setLabel(LocaleUtils.getLocalizedString("pubsub.form.conf.publish_model"));
formField.addOption(null, PublisherModel.publishers.getName());
formField.addOption(null, PublisherModel.subscribers.getName());
formField.addOption(null, PublisherModel.open.getName());
formField.addValue(publisherModel.getName());
formField = form.addField();
formField.setVariable("pubsub#language");
formField.setType(FormField.Type.text_single);
formField.setLabel(LocaleUtils.getLocalizedString("pubsub.form.conf.language"));
formField.addValue(language);
formField = form.addField();
formField.setVariable("pubsub#itemreply");
formField.setType(FormField.Type.list_single);
formField.setLabel(LocaleUtils.getLocalizedString("pubsub.form.conf.itemreply"));
if (replyPolicy != null) {
formField.addValue(replyPolicy.name());
}
if (!leaf) {
formField = form.addField();
formField.setVariable("pubsub#leaf_node_association_policy");
formField.setType(FormField.Type.list_single);
formField.setLabel(LocaleUtils.getLocalizedString("pubsub.form.conf.leaf_node_association"));
formField.addOption(null, CollectionNode.LeafNodeAssociationPolicy.all.name());
formField.addOption(null, CollectionNode.LeafNodeAssociationPolicy.owners.name());
formField.addOption(null, CollectionNode.LeafNodeAssociationPolicy.whitelist.name());
formField.addValue(associationPolicy.name());
formField = form.addField();
formField.setVariable("pubsub#leaf_nodes_max");
formField.setType(FormField.Type.text_single);
formField.setLabel(LocaleUtils.getLocalizedString("pubsub.form.conf.leaf_nodes_max"));
formField.addValue(maxLeafNodes);
}
return form;
}
}
/**
* $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.
*/
package org.jivesoftware.wildfire.pubsub;
import org.dom4j.Element;
import org.jivesoftware.util.LocaleUtils;
import org.jivesoftware.util.StringUtils;
import org.xmpp.forms.DataForm;
import org.xmpp.forms.FormField;
import org.xmpp.packet.JID;
import org.xmpp.packet.Message;
import org.xmpp.packet.IQ;
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;
/**
* 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.
*/
protected List<PublishedItem> publishedItems = new ArrayList<PublishedItem>();
protected Map<String, PublishedItem> itemsByID = new HashMap<String, PublishedItem>();
// TODO Add checking of max payload size
LeafNode(PubSubService service, CollectionNode parentNode, String nodeID, JID creator) {
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();
}
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;
}
}
void postConfigure(DataForm completedForm) {
List<String> values;
// TODO Remove stored published items based on the new max items
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;
}
}
}
protected void addFormFields(DataForm form, boolean isEditing) {
super.addFormFields(form, isEditing);
FormField formField = form.addField();
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);
}
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() {
return isPersistPublishedItems() || isDeliverPayloads();
}
/**
* Sends event notifications 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 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
* node subscribers may also use another thread to ensure good performance.
*
* @param originalIQ the IQ packet used by the publisher to publish the item.
* @param itemID the ID of the item or null if none was published.
* @param payload the payload of the new published item or null if none was published.
*/
public void sendEventNotification(IQ originalIQ, String itemID, Element payload) {
PublishedItem newItem = null;
if (isItemRequired()) {
// 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, originalIQ.getFrom(), itemID, new Date());
newItem.setPayload(payload);
// Add the published item to the list of items to persist (using another thread)
while (!publishedItems.isEmpty() && maxPublishedItems >= publishedItems.size()) {
PublishedItem removedItem = publishedItems.remove(0);
itemsByID.remove(removedItem.getID());
// TODO Add removed item to the queue of items to delete from the database
}
addPublishedItem(newItem);
// TODO Add new published item to the queue of items to add to the database
}
}
// Return success operation
service.send(IQ.createResultIQ(originalIQ));
// Build event notification packet to broadcast to subscribers
Message message = new Message();
Element event = message.addChildElement("event", "http://jabber.org/protocol/pubsub#event");
Element items = event.addElement("items");
items.addAttribute("node", nodeID);
if (newItem != null) {
// Add item information to the event notification if an item was published
Element item = items.addElement("item");
if (isItemRequired()) {
item.addAttribute("id", newItem.getID());
}
if (deliverPayloads) {
item.add(newItem.getPayload().createCopy());
}
}
// 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) {
affiliate.sendEventNotification(message, this, newItem);
}
}
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);
}
}
void setMaxPayloadSize(int maxPayloadSize) {
this.maxPayloadSize = maxPayloadSize;
}
void setPersistPublishedItems(boolean persistPublishedItems) {
this.persistPublishedItems = persistPublishedItems;
}
void setMaxPublishedItems(int maxPublishedItems) {
this.maxPublishedItems = maxPublishedItems;
}
/**
* 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
toDelete = publishedItems.subList(0, publishedItems.size() - 1);
// 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) {
PubSubPersistenceManager.removePublishedItem(service, this, item);
}
// 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
broadcastSubscribers(message, false);
}
}
}
/**
* $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.
*/
package org.jivesoftware.wildfire.pubsub;
import org.dom4j.Element;
import org.jivesoftware.util.LocaleUtils;
import org.jivesoftware.util.StringUtils;
import org.jivesoftware.wildfire.pubsub.models.AccessModel;
import org.jivesoftware.wildfire.pubsub.models.PublisherModel;
import org.xmpp.forms.DataForm;
import org.xmpp.forms.FormField;
import org.xmpp.packet.IQ;
import org.xmpp.packet.JID;
import org.xmpp.packet.Message;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* A virtual location to which information can be published and from which event
* notifications and/or payloads can be received (in other pubsub systems, this may
* be labelled a "topic").
*
* @author Matt Tucker
*/
public abstract class Node {
/**
* Reference to the publish and subscribe service.
*/
protected PubSubService service;
/**
* Keeps the Node that is containing this node.
*/
protected CollectionNode parent;
/**
* The unique identifier for a node within the context of a pubsub service.
*/
protected String nodeID;
/**
* Flag that indicates whether to deliver payloads with event notifications.
*/
protected boolean deliverPayloads;
/**
* Policy that defines whether owners or publisher should receive replies to items.
*/
protected ItemReplyPolicy replyPolicy;
/**
* Flag that indicates whether to notify subscribers when the node configuration changes.
*/
protected boolean notifyConfigChanges;
/**
* Flag that indicates whether to notify subscribers when the node is deleted.
*/
protected boolean notifyDelete;
/**
* Flag that indicates whether to notify subscribers when items are removed from the node.
*/
protected boolean notifyRetract;
/**
* Flag that indicates whether to deliver notifications to available users only.
*/
protected boolean presenceBasedDelivery;
/**
* Flag that indicates whether to send items to new subscribers.
*/
protected boolean sendItemSubscribe;
/**
* Publisher model that specifies who is allowed to publish items to the node.
*/
protected PublisherModel publisherModel = PublisherModel.open;
/**
* Flag that indicates that subscribing and unsubscribing are enabled.
*/
protected boolean subscriptionEnabled;
/**
* Access model that specifies who is allowed to subscribe and retrieve items.
*/
protected AccessModel accessModel = AccessModel.open;
/**
* The roster group(s) allowed to subscribe and retrieve items.
*/
protected Collection<String> rosterGroupsAllowed = new ArrayList<String>();
/**
* List of multi-user chat rooms to specify for replyroom.
*/
protected Collection<JID> replyRooms = new ArrayList<JID>();
/**
* List of JID(s) to specify for replyto.
*/
protected Collection<JID> replyTo = new ArrayList<JID>();
/**
* The type of payload data to be provided at the node. Usually specified by the
* namespace of the payload (if any).
*/
protected String payloadType = "";
/**
* The URL of an XSL transformation which can be applied to payloads in order
* to generate an appropriate message body element.
*/
protected String bodyXSLT = "";
/**
* The URL of an XSL transformation which can be applied to the payload format
* in order to generate a valid Data Forms result that the client could display
* using a generic Data Forms rendering engine.
*/
protected String dataformXSLT = "";
/**
* Indicates if the node is present in the database.
*/
private boolean savedToDB = false;
/**
* The datetime when the node was created.
*/
protected Date creationDate;
/**
* The last date when the ndoe's configuration was modified.
*/
private Date modificationDate;
/**
* The JID of the node creator.
*/
protected JID creator;
/**
* A description of the node.
*/
protected String description = "";
/**
* The default language of the node.
*/
protected String language = "";
/**
* The JIDs of those to contact with questions.
*/
protected Collection<JID> contacts = new ArrayList<JID>();
/**
* The name of the node.
*/
protected String name = "";
/**
* Flag that indicates whether new subscriptions should be configured to be active.
*/
protected boolean subscriptionConfigurationRequired = false;
/**
* The JIDs of those who have an affiliation with this node. When subscriptionModel is
* whitelist then this collection acts as the white list (unless user is an outcast)
*/
protected Collection<NodeAffiliate> affiliates = new CopyOnWriteArrayList<NodeAffiliate>();
/**
* Map that contains the current subscriptions to the node. A user may have more than one
* subscription. Each subscription is uniquely identified by its ID.
* Key: Subscription ID, Value: the subscription.
*/
protected Map<String, NodeSubscription> subscriptionsByID =
new ConcurrentHashMap<String, NodeSubscription>();
Node(PubSubService service, CollectionNode parent, String nodeID, JID creator) {
this.service = service;
this.parent = parent;
this.nodeID = nodeID;
this.creator = creator;
long startTime = System.currentTimeMillis();
this.creationDate = new Date(startTime);
this.modificationDate = new Date(startTime);
// Configure node with default values (get them from the pubsub service)
DefaultNodeConfiguration defaultConfiguration =
service.getDefaultNodeConfiguration(!isCollectionNode());
this.subscriptionEnabled = defaultConfiguration.isSubscriptionEnabled();
this.deliverPayloads = defaultConfiguration.isDeliverPayloads();
this.sendItemSubscribe = defaultConfiguration.isSendItemSubscribe();
this.notifyConfigChanges = defaultConfiguration.isNotifyConfigChanges();
this.notifyDelete = defaultConfiguration.isNotifyDelete();
this.notifyRetract = defaultConfiguration.isNotifyRetract();
this.presenceBasedDelivery = defaultConfiguration.isPresenceBasedDelivery();
this.accessModel = defaultConfiguration.getAccessModel();
this.publisherModel = defaultConfiguration.getPublisherModel();
this.language = defaultConfiguration.getLanguage();
this.replyPolicy = defaultConfiguration.getReplyPolicy();
}
/**
* Adds a new affiliation or updates an existing affiliation of the specified entity JID
* to become a node owner.
*
* @param jid the JID of the user being added as a node owner.
*/
public void addOwner(JID jid) {
addAffiliation(jid, NodeAffiliate.Affiliation.owner);
Collection<NodeSubscription> subscriptions = getSubscriptions(jid);
if (subscriptions.isEmpty()) {
// User does not have a subscription with the node so create a default one
addSubscription(jid, jid, NodeSubscription.State.subscribed, null);
}
else {
// TODO Approve any pending subscription
}
}
/**
* Removes the owner affiliation of the specified entity JID. If the user was an owner
* of this node then the user will not have any affiliation with the node.
*
* @param jid the JID of the user being removed as a node owner.
*/
public void removeOwner(JID jid) {
removeAffiliation(jid, NodeAffiliate.Affiliation.owner);
removeSubscriptions(jid);
}
/**
* Adds a new affiliation or updates an existing affiliation of the specified entity JID
* to become a node publisher.
*
* @param jid the JID of the user being added as a node publisher.
*/
public void addPublisher(JID jid) {
addAffiliation(jid, NodeAffiliate.Affiliation.publisher);
Collection<NodeSubscription> subscriptions = getSubscriptions(jid);
if (subscriptions.isEmpty()) {
// User does not have a subscription with the node so create a default one
addSubscription(jid, jid, NodeSubscription.State.subscribed, null);
}
else {
// TODO Approve any pending subscription
}
}
/**
* Removes the publisher affiliation of the specified entity JID. If the user was a publisher
* of this node then the user will not have any affiliation with the node.
*
* @param jid the JID of the user being removed as a node publisher.
*/
public void removePublisher(JID jid) {
removeAffiliation(jid, NodeAffiliate.Affiliation.publisher);
removeSubscriptions(jid);
}
/**
* Adds a new affiliation or updates an existing affiliation of the specified entity JID
* to become a none affiliate. Affiliates of type none are allowed to subscribe to the node.
*
* @param jid the JID of the user with affiliation "none".
*/
public void addNoneAffiliation(JID jid) {
addAffiliation(jid, NodeAffiliate.Affiliation.none);
}
/**
* Sets that the specified entity is an outcast of the node. Outcast entities are not
* able to publish or subscribe to the node. Existing subscriptions will be deleted.
*
* @param jid the JID of the user that is no longer able to publish or subscribe to the node.
*/
public void addOutcast(JID jid) {
addAffiliation(jid, NodeAffiliate.Affiliation.outcast);
// Delete existing subscriptions
removeSubscriptions(jid);
}
/**
* Removes the banning to subscribe to the node for the specified entity.
*
* @param jid the JID of the user that is no longer an outcast.
*/
public void removeOutcast(JID jid) {
removeAffiliation(jid, NodeAffiliate.Affiliation.outcast);
}
private void addAffiliation(JID jid, NodeAffiliate.Affiliation affiliation) {
boolean created = false;
// Get the current affiliation of the specified JID
NodeAffiliate affiliate = getAffiliate(jid);
// Check if the user already has the same affiliation
if (affiliate != null && affiliation == affiliate.getAffiliation()) {
// Do nothing since the user already has the expected affiliation
return;
}
else if (affiliate != null) {
// Update existing affiliation with new affiliation type
affiliate.setAffiliation(affiliation);
}
else {
// User did not have any affiliation with the node so create a new one
affiliate = new NodeAffiliate(this, jid);
affiliate.setAffiliation(affiliation);
addAffiliate(affiliate);
created = true;
}
if (savedToDB) {
// Add or update the affiliate in the database
PubSubPersistenceManager.saveAffiliation(service, this, affiliate, created);
}
}
private void removeAffiliation(JID jid, NodeAffiliate.Affiliation affiliation) {
// Get the current affiliation of the specified JID
NodeAffiliate affiliate = getAffiliate(jid);
// Check if the user already has the same affiliation
if (affiliate != null && affiliation == affiliate.getAffiliation()) {
// TODO If user has subscriptions then change affiliation to NONE
removeAffiliation(affiliate);
}
}
private void removeAffiliation(NodeAffiliate affiliate) {
// Remove the existing affiliate from the list in memory
affiliates.remove(affiliate);
if (savedToDB) {
// Remove the affiliate from the database
PubSubPersistenceManager.removeAffiliation(service, this, affiliate);
}
}
private NodeSubscription addSubscription(JID owner, JID jid, NodeSubscription.State subscribed,
DataForm options) {
// Generate a subscription ID (override even if one was sent by the client)
String id = StringUtils.randomString(40);
NodeSubscription subscription = new NodeSubscription(service, this, owner, jid, subscribed, id);
// Configure the subscription with the specified configuration (if any)
if (options != null) {
subscription.configure(options);
}
addSubscription(subscription);
if (savedToDB) {
// Add the new subscription to the database
PubSubPersistenceManager.saveSubscription(service, this, subscription, true);
}
return subscription;
}
private void removeSubscriptions(JID owner) {
for (NodeSubscription subscription : getSubscriptions(owner)) {
// Remove the existing subscription from the list in memory
subscriptionsByID.remove(subscription.getID());
if (savedToDB) {
// Remove the subscription from the database
PubSubPersistenceManager.removeSubscription(service, this, subscription);
}
}
}
/**
* Returns the list of subscriptions owned by the specified user. The subscription owner
* may have more than one subscription based on {@link #isMultipleSubscriptionsEnabled()}.
* Each subscription may have a different subscription JID if the owner wants to receive
* notifications in different resources (or even JIDs).
*
* @param owner the owner of the subscriptions.
*/
Collection<NodeSubscription> getSubscriptions(JID owner) {
Collection<NodeSubscription> subscriptions = new ArrayList<NodeSubscription>();
for (NodeSubscription subscription : subscriptionsByID.values()) {
if (owner.equals(subscription.getOwner())) {
subscriptions.add(subscription);
}
}
return subscriptions;
}
/**
* Returns all subscriptions to the node.
*
* @return all subscriptions to the node.
*/
Collection<NodeSubscription> getSubscriptions() {
return subscriptionsByID.values();
}
/**
* Returns the {@link NodeAffiliate} of the specified {@link JID} or <tt>null</tt>
* if none was found. Users that have a subscription with the node will ALWAYS
* have an affiliation even if the affiliation is of type <tt>none</tt>.
*
* @param jid the JID of the user to look his affiliation with this node.
* @return the NodeAffiliate of the specified JID or <tt>null</tt> if none was found.
*/
public NodeAffiliate getAffiliate(JID jid) {
for (NodeAffiliate affiliate : affiliates) {
if (jid.equals(affiliate.getJID())) {
return affiliate;
}
}
return null;
}
public Collection<JID> getOwners() {
Collection<JID> jids = new ArrayList<JID>();
for (NodeAffiliate affiliate : affiliates) {
if (NodeAffiliate.Affiliation.owner == affiliate.getAffiliation()) {
jids.add(affiliate.getJID());
}
}
return jids;
}
public Collection<JID> getPublishers() {
Collection<JID> jids = new ArrayList<JID>();
for (NodeAffiliate affiliate : affiliates) {
if (NodeAffiliate.Affiliation.publisher == affiliate.getAffiliation()) {
jids.add(affiliate.getJID());
}
}
return jids;
}
public void configure(DataForm completedForm) throws NotAcceptableException {
if (DataForm.Type.cancel.equals(completedForm.getType())) {
// Existing node configuration is applied (i.e. nothing is changed)
}
else if (DataForm.Type.submit.equals(completedForm.getType())) {
List<String> values;
String booleanValue;
// Get the new list of owners
FormField ownerField = completedForm.getField("pubsub#owner");
boolean ownersSent = ownerField != null;
List<JID> owners = new ArrayList<JID>();
if (ownersSent) {
for (String value : ownerField.getValues()) {
try {
owners.add(new JID(value));
}
catch (Exception e) {}
}
}
// Answer a not-acceptable error if all the current owners will be removed
if (ownersSent && owners.isEmpty()) {
throw new NotAcceptableException();
}
for (FormField field : completedForm.getFields()) {
if ("FORM_TYPE".equals(field.getVariable())) {
// Ignore this variable
continue;
}
else if ("pubsub#deliver_payloads".equals(field.getVariable())) {
values = field.getValues();
booleanValue = (values.size() > 0 ? values.get(0) : "1");
deliverPayloads = "1".equals(booleanValue);
}
else if ("pubsub#notify_config".equals(field.getVariable())) {
values = field.getValues();
booleanValue = (values.size() > 0 ? values.get(0) : "1");
notifyConfigChanges = "1".equals(booleanValue);
}
else if ("pubsub#notify_delete".equals(field.getVariable())) {
values = field.getValues();
booleanValue = (values.size() > 0 ? values.get(0) : "1");
notifyDelete = "1".equals(booleanValue);
}
else if ("pubsub#notify_retract".equals(field.getVariable())) {
values = field.getValues();
booleanValue = (values.size() > 0 ? values.get(0) : "1");
notifyRetract = "1".equals(booleanValue);
}
else if ("pubsub#presence_based_delivery".equals(field.getVariable())) {
values = field.getValues();
booleanValue = (values.size() > 0 ? values.get(0) : "1");
presenceBasedDelivery = "1".equals(booleanValue);
}
else if ("pubsub#send_item_subscribe".equals(field.getVariable())) {
values = field.getValues();
booleanValue = (values.size() > 0 ? values.get(0) : "1");
sendItemSubscribe = "1".equals(booleanValue);
}
else if ("pubsub#subscribe".equals(field.getVariable())) {
values = field.getValues();
booleanValue = (values.size() > 0 ? values.get(0) : "1");
subscriptionEnabled = "1".equals(booleanValue);
}
else if ("pubsub#subscription_required".equals(field.getVariable())) {
// TODO Replace this variable for the one defined in the JEP (once one is defined)
values = field.getValues();
booleanValue = (values.size() > 0 ? values.get(0) : "1");
subscriptionConfigurationRequired = "1".equals(booleanValue);
}
else if ("pubsub#type".equals(field.getVariable())) {
values = field.getValues();
payloadType = values.size() > 0 ? values.get(0) : " ";
}
else if ("pubsub#body_xslt".equals(field.getVariable())) {
values = field.getValues();
bodyXSLT = values.size() > 0 ? values.get(0) : " ";
}
else if ("pubsub#dataform_xslt".equals(field.getVariable())) {
values = field.getValues();
dataformXSLT = values.size() > 0 ? values.get(0) : " ";
}
else if ("pubsub#access_model".equals(field.getVariable())) {
values = field.getValues();
if (values.size() > 0) {
accessModel = AccessModel.valueOf(values.get(0));
}
}
else if ("pubsub#publish_model".equals(field.getVariable())) {
values = field.getValues();
if (values.size() > 0) {
publisherModel = PublisherModel.valueOf(values.get(0));
}
}
else if ("pubsub#roster_groups_allowed".equals(field.getVariable())) {
// Get the new list of roster group(s) allowed to subscribe and retrieve items
rosterGroupsAllowed = new ArrayList<String>();
for (String value : field.getValues()) {
rosterGroupsAllowed.add(value);
}
}
else if ("pubsub#contact".equals(field.getVariable())) {
// Get the new list of users that may be contacted with questions
contacts = new ArrayList<JID>();
for (String value : field.getValues()) {
try {
contacts.add(new JID(value));
}
catch (Exception e) {}
}
}
else if ("pubsub#description".equals(field.getVariable())) {
values = field.getValues();
description = values.size() > 0 ? values.get(0) : " ";
}
else if ("pubsub#language".equals(field.getVariable())) {
values = field.getValues();
language = values.size() > 0 ? values.get(0) : " ";
}
else if ("pubsub#title".equals(field.getVariable())) {
values = field.getValues();
name = values.size() > 0 ? values.get(0) : " ";
}
else if ("pubsub#itemreply".equals(field.getVariable())) {
values = field.getValues();
if (values.size() > 0) {
replyPolicy = ItemReplyPolicy.valueOf(values.get(0));
}
}
else if ("pubsub#replyroom".equals(field.getVariable())) {
// Get the new list of multi-user chat rooms to specify for replyroom
replyRooms = new ArrayList<JID>();
for (String value : field.getValues()) {
try {
replyRooms.add(new JID(value));
}
catch (Exception e) {}
}
}
else if ("pubsub#replyto".equals(field.getVariable())) {
// Get the new list of JID(s) to specify for replyto
replyTo = new ArrayList<JID>();
for (String value : field.getValues()) {
try {
replyTo.add(new JID(value));
}
catch (Exception e) {}
}
}
else {
// Let subclasses be configured by specified fields
configure(field);
}
}
// Set new list of owners of the node
if (ownersSent) {
// Calculate owners to remove and remove them from the DB
Collection<JID> oldOwners = getOwners();
oldOwners.remove(owners);
for (JID jid : oldOwners) {
removeOwner(jid);
}
// Calculate new owners and add them to the DB
owners.remove(getOwners());
for (JID jid : owners) {
addOwner(jid);
}
}
// TODO Before removing owner or admin check if user was changed from admin to owner or vice versa. This way his susbcriptions are not going to be deleted.
// Set the new list of publishers
FormField publisherField = completedForm.getField("pubsub#publisher");
if (publisherField != null) {
// New list of publishers was sent to update publishers of the node
List<JID> publishers = new ArrayList<JID>();
for (String value : publisherField.getValues()) {
try {
publishers.add(new JID(value));
}
catch (Exception e) {}
}
// Calculate publishers to remove and remove them from the DB
Collection<JID> oldPublishers = getPublishers();
oldPublishers.remove(publishers);
for (JID jid : oldPublishers) {
removePublisher(jid);
}
// Calculate new publishers and add them to the DB
publishers.remove(getPublishers());
for (JID jid : publishers) {
addPublisher(jid);
}
}
// Let subclasses have a chance to finish node configuration based on
// the completed form
postConfigure(completedForm);
// Update the modification date to reflect the last time when the node's configuration
// was modified
modificationDate = new Date();
// Notify subscribers that the node configuration has changed
nodeConfigurationChanged();
}
// Store the new or updated node in the backend store
saveToDB();
}
/**
* Configures the node with the completed form field. Fields that are common to leaf
* and collection nodes are handled in {@link #configure(org.xmpp.forms.DataForm)}.
* Subclasses should implement this method in order to configure the node with form
* fields specific to the node type.
*
* @param field the form field specific to the node type.
*/
abstract void configure(FormField field);
/**
* Node configuration was changed based on the completed form. Subclasses may implement
* this method to finsh node configuration based on the completed form.
*
* @param completedForm the form completed by the node owner.
*/
abstract void postConfigure(DataForm completedForm);
private void nodeConfigurationChanged() {
if (!notifyConfigChanges || !savedToDB) {
// Do nothing if node was just created and configure or if notification
// of config changes is disabled
return;
}
// 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("items");
items.addAttribute("node", nodeID);
Element item = items.addElement("item");
item.addAttribute("id", "configuration");
if (deliverPayloads) {
item.add(getConfigurationChangeForm().getElement());
}
// Send notification that the node configuration has changed
broadcastSubscribers(message, false);
}
/**
* Returns a data form used by the owner to edit the node configuration.
*
* @return data form used by the owner to edit the node configuration.
*/
public DataForm getConfigurationForm() {
DataForm form = new DataForm(DataForm.Type.form);
form.setTitle(LocaleUtils.getLocalizedString("pubsub.form.conf.title"));
List<String> params = new ArrayList<String>();
params.add(getNodeID());
form.addInstruction(LocaleUtils.getLocalizedString("pubsub.form.conf.instruction", params));
// Add the form fields and configure them for edition
addFormFields(form, true);
return form;
}
protected void addFormFields(DataForm form, boolean isEditing) {
FormField formField = form.addField();
formField.setVariable("FORM_TYPE");
formField.setType(FormField.Type.hidden);
formField.addValue("http://jabber.org/protocol/pubsub#node_config");
formField = form.addField();
formField.setVariable("pubsub#title");
if (isEditing) {
formField.setType(FormField.Type.text_single);
formField.setLabel(LocaleUtils.getLocalizedString("pubsub.form.conf.short_name"));
}
formField.addValue(name);
formField = form.addField();
formField.setVariable("pubsub#description");
if (isEditing) {
formField.setType(FormField.Type.text_single);
formField.setLabel(LocaleUtils.getLocalizedString("pubsub.form.conf.description"));
}
formField.addValue(description);
formField = form.addField();
formField.setVariable("pubsub#subscribe");
if (isEditing) {
formField.setType(FormField.Type.boolean_type);
formField.setLabel(LocaleUtils.getLocalizedString("pubsub.form.conf.subscribe"));
}
formField.addValue(subscriptionEnabled);
formField = form.addField();
formField.setVariable("pubsub#subscription_required");
// TODO Replace this variable for the one defined in the JEP (once one is defined)
if (isEditing) {
formField.setType(FormField.Type.boolean_type);
formField.setLabel(LocaleUtils.getLocalizedString("pubsub.form.conf.subscription_required"));
}
formField.addValue(subscriptionConfigurationRequired);
formField = form.addField();
formField.setVariable("pubsub#deliver_payloads");
if (isEditing) {
formField.setType(FormField.Type.boolean_type);
formField.setLabel(LocaleUtils.getLocalizedString("pubsub.form.conf.deliver_payloads"));
}
formField.addValue(deliverPayloads);
formField = form.addField();
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();
formField.setVariable("pubsub#notify_config");
if (isEditing) {
formField.setType(FormField.Type.boolean_type);
formField.setLabel(LocaleUtils.getLocalizedString("pubsub.form.conf.notify_config"));
}
formField.addValue(notifyConfigChanges);
formField = form.addField();
formField.setVariable("pubsub#notify_delete");
if (isEditing) {
formField.setType(FormField.Type.boolean_type);
formField.setLabel(LocaleUtils.getLocalizedString("pubsub.form.conf.notify_delete"));
}
formField.addValue(notifyDelete);
formField = form.addField();
formField.setVariable("pubsub#notify_retract");
if (isEditing) {
formField.setType(FormField.Type.boolean_type);
formField.setLabel(LocaleUtils.getLocalizedString("pubsub.form.conf.notify_retract"));
}
formField.addValue(notifyRetract);
formField = form.addField();
formField.setVariable("pubsub#presence_based_delivery");
if (isEditing) {
formField.setType(FormField.Type.boolean_type);
formField.setLabel(LocaleUtils.getLocalizedString("pubsub.form.conf.presence_based"));
}
formField.addValue(presenceBasedDelivery);
formField = form.addField();
formField.setVariable("pubsub#type");
if (isEditing) {
formField.setType(FormField.Type.text_single);
formField.setLabel(LocaleUtils.getLocalizedString("pubsub.form.conf.type"));
}
formField.addValue(payloadType);
formField = form.addField();
formField.setVariable("pubsub#body_xslt");
if (isEditing) {
formField.setType(FormField.Type.text_single);
formField.setLabel(LocaleUtils.getLocalizedString("pubsub.form.conf.body_xslt"));
}
formField.addValue(bodyXSLT);
formField = form.addField();
formField.setVariable("pubsub#dataform_xslt");
if (isEditing) {
formField.setType(FormField.Type.text_single);
formField.setLabel(LocaleUtils.getLocalizedString("pubsub.form.conf.dataform_xslt"));
}
formField.addValue(dataformXSLT);
formField = form.addField();
formField.setVariable("pubsub#access_model");
if (isEditing) {
formField.setType(FormField.Type.list_single);
formField.setLabel(LocaleUtils.getLocalizedString("pubsub.form.conf.access_model"));
formField.addOption(null, AccessModel.authorize.getName());
formField.addOption(null, AccessModel.open.getName());
formField.addOption(null, AccessModel.presence.getName());
formField.addOption(null, AccessModel.roster.getName());
formField.addOption(null, AccessModel.whitelist.getName());
}
formField.addValue(accessModel.getName());
formField = form.addField();
formField.setVariable("pubsub#publish_model");
if (isEditing) {
formField.setType(FormField.Type.list_single);
formField.setLabel(LocaleUtils.getLocalizedString("pubsub.form.conf.publish_model"));
formField.addOption(null, PublisherModel.publishers.getName());
formField.addOption(null, PublisherModel.subscribers.getName());
formField.addOption(null, PublisherModel.open.getName());
}
formField.addValue(publisherModel.getName());
formField = form.addField();
formField.setVariable("pubsub#roster_groups_allowed");
if (isEditing) {
formField.setType(FormField.Type.list_multi);
formField.setLabel(LocaleUtils.getLocalizedString("pubsub.form.conf.roster_allowed"));
}
for (String group : rosterGroupsAllowed) {
formField.addValue(group);
}
formField = form.addField();
formField.setVariable("pubsub#contact");
if (isEditing) {
formField.setType(FormField.Type.jid_multi);
formField.setLabel(LocaleUtils.getLocalizedString("pubsub.form.conf.contact"));
}
for (JID contact : contacts) {
formField.addValue(contact.toString());
}
formField = form.addField();
formField.setVariable("pubsub#language");
if (isEditing) {
formField.setType(FormField.Type.text_single);
formField.setLabel(LocaleUtils.getLocalizedString("pubsub.form.conf.language"));
}
formField.addValue(language);
formField = form.addField();
formField.setVariable("pubsub#owner");
if (isEditing) {
formField.setType(FormField.Type.jid_multi);
formField.setLabel(LocaleUtils.getLocalizedString("pubsub.form.conf.owner"));
}
for (JID owner : getOwners()) {
formField.addValue(owner.toString());
}
formField = form.addField();
formField.setVariable("pubsub#publisher");
if (isEditing) {
formField.setType(FormField.Type.jid_multi);
formField.setLabel(LocaleUtils.getLocalizedString("pubsub.form.conf.publisher"));
}
for (JID owner : getPublishers()) {
formField.addValue(owner.toString());
}
formField = form.addField();
formField.setVariable("pubsub#itemreply");
if (isEditing) {
formField.setType(FormField.Type.list_single);
formField.setLabel(LocaleUtils.getLocalizedString("pubsub.form.conf.itemreply"));
}
if (replyPolicy != null) {
formField.addValue(replyPolicy.name());
}
formField = form.addField();
formField.setVariable("pubsub#replyroom");
if (isEditing) {
formField.setType(FormField.Type.jid_multi);
formField.setLabel(LocaleUtils.getLocalizedString("pubsub.form.conf.replyroom"));
}
for (JID owner : getReplyRooms()) {
formField.addValue(owner.toString());
}
formField = form.addField();
formField.setVariable("pubsub#replyto");
if (isEditing) {
formField.setType(FormField.Type.jid_multi);
formField.setLabel(LocaleUtils.getLocalizedString("pubsub.form.conf.replyto"));
}
for (JID owner : getReplyTo()) {
formField.addValue(owner.toString());
}
}
/**
* Returns a data form with the node configuration. The returned data form is used for
* notifying node subscribers that the node configuration has changed. The data form is
* ony going to be included if node is configure to include payloads in event
* notifications.
*
* @return a data form with the node configuration.
*/
private DataForm getConfigurationChangeForm() {
DataForm form = new DataForm(DataForm.Type.result);
// Add the form fields and configure them for notification
// (i.e. no label or options are included)
addFormFields(form, false);
return form;
}
public boolean isRootCollectionNode() {
return service.getRootCollectionNode() == this;
}
/**
* Returns true if a user may have more than one subscription with the node. When
* multiple subscriptions is enabled each subscription request, event notification and
* unsubscription request should include a <tt>subid</tt> attribute. By default multiple
* subscriptions is enabled.
*
* @return true if a user may have more than one subscription with the node.
*/
public boolean isMultipleSubscriptionsEnabled() {
return true;
}
public boolean isCollectionNode() {
return false;
}
/**
* Returns true if the specified node is a first-level children of this node.
*
* @param child the node to check if it is a direct child of this node.
* @return true if the specified node is a first-level children of this collection
* node.
*/
public boolean isChildNode(Node child) {
return false;
}
/**
* Returns true if the specified node is a direct child node of this node or
* a descendant of the children nodes.
*
* @param child the node to check if it is a descendant of this node.
* @return true if the specified node is a direct child node of this node or
* a descendant of the children nodes.
*/
public boolean isDescendantNode(Node child) {
return false;
}
/**
* Returns true if the specified user is allowed to administer the node. Node
* administrator are allowed to retrieve the node configuration, change the node
* configuration, purge the node, delete the node and get the node affiliations and
* subscriptions.
*
* @param user the user to check if he is an admin.
* @return true if the specified user is allowed to administer the node.
*/
public boolean isAdmin(JID user) {
if (getOwners().contains(user) || service.isServiceAdmin(user)) {
return true;
}
// Check if we should try again but using the bare JID
if (user.getResource() != null) {
user = new JID(user.toBareJID());
return isAdmin(user);
}
return false;
}
public String getNodeID() {
return nodeID;
}
public String getName() {
return name;
}
public boolean isDeliverPayloads() {
return deliverPayloads;
}
public ItemReplyPolicy getReplyPolicy() {
return replyPolicy;
}
public boolean isNotifyConfigChanges() {
return notifyConfigChanges;
}
public boolean isNotifyDelete() {
return notifyDelete;
}
public boolean isNotifyRetract() {
return notifyRetract;
}
public boolean isPresenceBasedDelivery() {
return presenceBasedDelivery;
}
public boolean isSendItemSubscribe() {
return sendItemSubscribe;
}
public PublisherModel getPublisherModel() {
return publisherModel;
}
public boolean isSubscriptionEnabled() {
return subscriptionEnabled;
}
public boolean isSubscriptionConfigurationRequired() {
return subscriptionConfigurationRequired;
}
public AccessModel getAccessModel() {
return accessModel;
}
public Collection<String> getRosterGroupsAllowed() {
return rosterGroupsAllowed;
}
public Collection<JID> getReplyRooms() {
return replyRooms;
}
public Collection<JID> getReplyTo() {
return replyTo;
}
public String getPayloadType() {
return payloadType;
}
public String getBodyXSLT() {
return bodyXSLT;
}
public String getDataformXSLT() {
return dataformXSLT;
}
public Date getCreationDate() {
return creationDate;
}
public Date getModificationDate() {
return modificationDate;
}
public JID getCreator() {
return creator;
}
public String getDescription() {
return description;
}
public String getLanguage() {
return language;
}
public Collection<JID> getContacts() {
return contacts;
}
public Collection<Node> getNodes() {
return Collections.emptyList();
}
/**
* Returns the collection node that is containing this node. The only node that
* does not have a parent node is the root collection node.
*
* @return the collection node that is containing this node.
*/
public CollectionNode getParent() {
return parent;
}
/**
* Returns the complete hierarchy of parents of this node.
*
* @return the complete hierarchy of parents of this node.
*/
public Collection<CollectionNode> getParents() {
Collection<CollectionNode> parents = new ArrayList<CollectionNode>();
CollectionNode myParent = parent;
while (myParent != null) {
parents.add(myParent);
myParent = myParent.getParent();
}
return parents;
}
void setDeliverPayloads(boolean deliverPayloads) {
this.deliverPayloads = deliverPayloads;
}
void setReplyPolicy(ItemReplyPolicy replyPolicy) {
this.replyPolicy = replyPolicy;
}
void setNotifyConfigChanges(boolean notifyConfigChanges) {
this.notifyConfigChanges = notifyConfigChanges;
}
void setNotifyDelete(boolean notifyDelete) {
this.notifyDelete = notifyDelete;
}
void setNotifyRetract(boolean notifyRetract) {
this.notifyRetract = notifyRetract;
}
void setPresenceBasedDelivery(boolean presenceBasedDelivery) {
this.presenceBasedDelivery = presenceBasedDelivery;
}
void setSendItemSubscribe(boolean sendItemSubscribe) {
this.sendItemSubscribe = sendItemSubscribe;
}
void setPublisherModel(PublisherModel publisherModel) {
this.publisherModel = publisherModel;
}
void setSubscriptionEnabled(boolean subscriptionEnabled) {
this.subscriptionEnabled = subscriptionEnabled;
}
void setSubscriptionConfigurationRequired(boolean subscriptionConfigurationRequired) {
this.subscriptionConfigurationRequired = subscriptionConfigurationRequired;
}
void setAccessModel(AccessModel accessModel) {
this.accessModel = accessModel;
}
void setReplyRooms(Collection<JID> replyRooms) {
this.replyRooms = replyRooms;
}
void setReplyTo(Collection<JID> replyTo) {
this.replyTo = replyTo;
}
void setPayloadType(String payloadType) {
this.payloadType = payloadType;
}
void setBodyXSLT(String bodyXSLT) {
this.bodyXSLT = bodyXSLT;
}
void setDataformXSLT(String dataformXSLT) {
this.dataformXSLT = dataformXSLT;
}
void setSavedToDB(boolean savedToDB) {
this.savedToDB = savedToDB;
if (savedToDB && parent != null) {
// Notify the parent that he has a new child :)
parent.addChildNode(this);
}
}
void setCreationDate(Date creationDate) {
this.creationDate = creationDate;
}
void setModificationDate(Date modificationDate) {
this.modificationDate = modificationDate;
}
void setDescription(String description) {
this.description = description;
}
void setLanguage(String language) {
this.language = language;
}
void setName(String name) {
this.name = name;
}
void setRosterGroupsAllowed(Collection<String> rosterGroupsAllowed) {
this.rosterGroupsAllowed = rosterGroupsAllowed;
}
void setContacts(Collection<JID> contacts) {
this.contacts = contacts;
}
public void saveToDB() {
// Make the room persistent
if (!savedToDB) {
PubSubPersistenceManager.createNode(service, this);
// Set that the now is now in the DB
setSavedToDB(true);
// Save the existing node affiliates to the DB
for (NodeAffiliate affialiate : affiliates) {
PubSubPersistenceManager.saveAffiliation(service, this, affialiate, true);
}
// Add new subscriptions to the database
for (NodeSubscription subscription : subscriptionsByID.values()) {
PubSubPersistenceManager.saveSubscription(service, this, subscription, true);
}
}
else {
PubSubPersistenceManager.updateNode(service, this);
}
}
void addAffiliate(NodeAffiliate affiliate) {
affiliates.add(affiliate);
}
void addSubscription(NodeSubscription subscription) {
subscriptionsByID.put(subscription.getID(), subscription);
}
/**
* Returns the subscription whose subscription JID matches the specified JID or <tt>null</tt>
* if none was found. Accessing subscriptions by subscription JID and not by subscription ID
* is only possible when the node does not allow multiple subscriptions from the same entity.
* If the node allows multiple subscriptions and this message is sent then an
* IllegalStateException exception is going to be thrown.
*
* @param subscriptionJID the JID of the entity that receives event notifications.
* @return the subscription whose subscription JID matches the specified JID or <tt>null</tt>
* if none was found.
* @throws IllegalStateException If this message was used when the node supports multiple
* subscriptions.
*/
NodeSubscription getSubscription(JID subscriptionJID) {
// Check that node does not support multiple subscriptions
if (isMultipleSubscriptionsEnabled()) {
throw new IllegalStateException(
"Multiple subscriptions is enabled so subscriptions should be retrieved using subID.");
}
// TODO implement this
return null;
}
/**
* Returns the subscription whose subscription ID matches the specified ID or <tt>null</tt>
* if none was found. Accessing subscriptions by subscription ID is always possible no matter
* if the node allows one or multiple subscriptions for the same entity. Even when users can
* only subscribe once to the node a subscription ID is going to be internally created though
* never returned to the user.
*
* @param subscriptionID the ID of the subscription.
* @return the subscription whose subscription ID matches the specified ID or <tt>null</tt>
* if none was found.
*/
NodeSubscription getSubscription(String subscriptionID) {
return subscriptionsByID.get(subscriptionID);
}
/**
* Deletes this node from memory and the database. Subscribers are going to be notified
* that the node has been deleted after the node was successfully deleted.
*
* @return true if the node was successfully deleted.
*/
public boolean delete() {
// TODO Should we lock the object to prevent simultaneous edition, publishing, etc.????
// Delete node from the database
if (PubSubPersistenceManager.removeNode(service, this)) {
// Remove this node from the parent node (if any)
if (parent != null) {
parent.removeChildNode(this);
}
// TODO Update child nodes to use the root node or the parent node of this node as the new parent node
for (Node node : getNodes()) {
//node.set
}
// Broadcast delete notification to subscribers (if enabled)
if (notifyDelete) {
// 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("delete");
items.addAttribute("node", nodeID);
// Send notification that the node was deleted
broadcastSubscribers(message, true);
}
// Clear collections in memory (clear them after broadcast was sent)
affiliates.clear();
subscriptionsByID.clear();
return true;
}
return false;
}
/**
* Sends the list of affiliated entities with the node to the owner that sent the IQ
* request.
*
* @param iqRequest IQ request sent by an owner of the node.
*/
public void sendAffiliatedEntities(IQ iqRequest) {
IQ reply = IQ.createResultIQ(iqRequest);
Element childElement = iqRequest.getChildElement().createCopy();
reply.setChildElement(childElement);
for (NodeSubscription subscription : subscriptionsByID.values()) {
Element entity = childElement.addElement("entity");
entity.addAttribute("jid", subscription.getJID().toString());
entity.addAttribute("affiliation", subscription.getAffiliate().getAffiliation().name());
entity.addAttribute("subscription", subscription.getState().name());
entity.addAttribute("subid", subscription.getID());
}
}
protected void broadcastSubscribers(Message message, boolean includeAll) {
Collection<JID> jids = new ArrayList<JID>();
for (NodeSubscription subscription : subscriptionsByID.values()) {
if (includeAll || subscription.isApproved()) {
jids.add(subscription.getJID());
}
}
// Broadcast packet to subscribers
service.broadcast(this, message, jids);
}
protected void sendEventNotification(JID subscriberJID, Message notification,
Collection<String> subIDs) {
Element headers = null;
if (subIDs != null) {
// Notate the event notification with the ID of the affected subscriptions
headers = notification.addChildElement("headers", "http://jabber.org/protocol/shim");
for (String subID : subIDs) {
Element header = headers.addElement("header");
header.addAttribute("name", "pubsub#subid");
header.setText(subID);
}
}
notification.setTo(subscriberJID);
service.send(notification);
if (headers != null) {
// Remove the added child element that includes subscription IDs information
notification.getElement().remove(headers);
}
}
/**
* Creates a new subscription and possibly a new affiliate if the owner of the subscription
* does not have any existing affiliation with the node. The new subscription might require
* to be authorized by a node owner to be active. If new subscriptions are required to be
* configured before being active then the subscription state would be "unconfigured".
*
* @param owner the JID of the affiliate.
* @param subscriber the JID where event notifications are going to be sent.
* @param authorizationRequired true if the new subscriptions needs to be authorized by
* a node owner.
* @param options the data form with the subscription configuration or null if subscriber
* didn't provide a configuration.
*/
void createSubscription(IQ originalIQ, JID owner, JID subscriber, boolean authorizationRequired,
DataForm options) {
// Create a new affiliation if required
if (getAffiliate(owner) == null) {
addNoneAffiliation(owner);
}
// Figure out subscription status
NodeSubscription.State subState = NodeSubscription.State.subscribed;
if (authorizationRequired) {
// Node owner needs to authorize subscription request so status is pending
subState = NodeSubscription.State.pending;
}
else if (isSubscriptionConfigurationRequired()) {
// User has to configure the subscription to make it active
subState = NodeSubscription.State.unconfigured;
}
// Create new subscription
NodeSubscription subscription = addSubscription(owner, subscriber, subState, options);
// Reply with subscription and affiliation status indicating if subscription
// must be configured
subscription.sendSubscriptionState(originalIQ);
// Send last published item (if node is leaf node and subscription status is ok)
if (isSendItemSubscribe()) {
PublishedItem lastItem = getLastPublishedItem();
if (lastItem != null) {
subscription.sendLastPublishedItem(lastItem);
}
}
}
/**
* Cancels an existing subscription to the node. If the subscriber does not have any
* other subscription to the node and his affiliation was of type <tt>none</tt> then
* remove the existing affiliation too.
*
* @param subscription the subscription to cancel.
*/
void cancelSubscription(NodeSubscription subscription) {
// Remove subscription from memory
subscriptionsByID.remove(subscription.getID());
// Check if user has affiliation of type "none" and there are no more subscriptions
NodeAffiliate affiliate = subscription.getAffiliate();
if (affiliate != null && affiliate.getAffiliation() == NodeAffiliate.Affiliation.none &&
getSubscriptions(subscription.getOwner()).isEmpty()) {
// Remove affiliation of type "none"
removeAffiliation(affiliate);
}
if (savedToDB) {
// Remove the subscription from the database
PubSubPersistenceManager.removeSubscription(service, this, subscription);
}
}
/**
* Returns the {@link PublishedItem} whose ID matches the specified item ID or <tt>null</tt>
* if none was found. Item ID may or may not exist and it depends on the node's configuration.
* When the node is configured to not include payloads in event notifications and
* published items are not persistent then item ID is not used. In this case a <tt>null</tt>
* value will always be returned.
*
* @return the PublishedItem whose ID matches the specified item ID or null if none was found.
*/
public PublishedItem getPublishedItem(String itemID) {
return null;
}
/**
* Returns the list of {@link PublishedItem} that were published to the node. The
* returned collection cannot be modified. Collection nodes does not support publishing
* of items so an empty list will be returned in that case.
*
* @return the list of PublishedItem that were published to the node.
*/
public List<PublishedItem> getPublishedItems() {
return Collections.emptyList();
}
/**
* Returns a list of {@link PublishedItem} with the most recent N items published to
* the node. The returned collection cannot be modified. Collection nodes does not
* support publishing of items so an empty list will be returned in that case.
*
* @return a list of PublishedItem with the most recent N items published to
* the node.
*/
public List<PublishedItem> getPublishedItems(int recentItems) {
return Collections.emptyList();
}
public String toString() {
return super.toString() + " - ID: " + getNodeID();
}
/**
* Returns the last {@link PublishedItem} that was published to the node or <tt>null</tt> if
* the node does not have published items. Collection nodes does not support publishing
* of items so a <tt>null</tt> will be returned in that case.
*
* @return the PublishedItem that was published to the node or <tt>null</tt> if
* the node does not have published items.
*/
public PublishedItem getLastPublishedItem() {
return null;
}
/**
* Policy that defines whether owners or publisher should receive replies to items.
*/
public static enum ItemReplyPolicy {
/**
* Statically specify a replyto of the node owner(s).
*/
owner,
/**
* Dynamically specify a replyto of the item publisher.
*/
publisher;
}
}
/**
* $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.
*/
package org.jivesoftware.wildfire.pubsub;
import org.xmpp.packet.JID;
import org.xmpp.packet.Message;
import java.util.*;
/**
* A NodeAffiliate keeps information about the affiliation of an entity with a node. Possible
* affiliations are: owner, publisher, none or outcast. All except for outcast affiliations
* may have a {@link NodeSubscription} with the node.
*
* @author Matt Tucker
*/
public class NodeAffiliate {
private JID jid;
private Node node;
private Affiliation affiliation;
NodeAffiliate(Node node, JID jid) {
this.node = node;
this.jid = jid;
}
public Node getNode() {
return node;
}
public JID getJID() {
return jid;
}
public Affiliation getAffiliation() {
return affiliation;
}
void setAffiliation(Affiliation affiliation) {
this.affiliation = affiliation;
}
/**
* Returns the list of subscriptions of the affiliate in the node.
*
* @return the list of subscriptions of the affiliate in the node.
*/
public Collection<NodeSubscription> getSubscriptions() {
return node.getSubscriptions(jid);
}
/**
* Sends an event notification to each affected subscription of the affiliate. If the owner
* has many subscriptions from the same full JID then a single notification is going to be
* sent including a detail of the subscription IDs for which the notification is being sent.<p>
*
* The original publication to the node may or may not contain a {@link PublishedItem}. The
* subscriptions of the affiliation will be filtered based on the published item (if one was
* specified), the subscription status and originating node.
*
* @param notification the message to send containing the event notification.
* @param node the node that received a new publication.
* @param publishedItem the item was was published in the publication or null if none
* was published.
*/
public void sendEventNotification(Message notification, LeafNode node,
PublishedItem publishedItem) {
// Filter affiliate subscriptions and only use approved and configured ones
List<NodeSubscription> notifySubscriptions = new ArrayList<NodeSubscription>();
for (NodeSubscription subscription : getSubscriptions()) {
if (subscription.canSendEventNotification(node, publishedItem)) {
notifySubscriptions.add(subscription);
}
}
if (node.isMultipleSubscriptionsEnabled()) {
// Group subscriptions with the same subscriber JID
Map<JID, Collection<String>> groupedSubs = new HashMap<JID, Collection<String>>();
for (NodeSubscription subscription : notifySubscriptions) {
Collection<String> subIDs = groupedSubs.get(subscription.getJID());
if (subIDs == null) {
subIDs = new ArrayList<String>();
groupedSubs.put(subscription.getJID(), subIDs);
}
subIDs.add(subscription.getID());
}
// Send an event notification to each subscriber with a different JID
for (JID subscriberJID : groupedSubs.keySet()) {
// Get ID of affected subscriptions
Collection<String> subIDs = groupedSubs.get(subscriberJID);
// Send the notification to the subscriber
node.sendEventNotification(subscriberJID, notification, subIDs);
}
}
else {
// Affiliate should have at most one subscription so send the notification to
// the subscriber
if (!notifySubscriptions.isEmpty()) {
NodeSubscription subscription = notifySubscriptions.get(0);
node.sendEventNotification(subscription.getJID(), notification, null);
}
}
}
public String toString() {
return super.toString() + " - JID: " + getJID() + " - Affiliation: " +
getAffiliation().name();
}
/**
* Affiliation with a node defines user permissions.
*/
public static enum Affiliation {
/**
* An owner can publish, delete and purge items as well as configure and delete the node.
*/
owner,
/**
* A publisher can subscribe and publish items to the node.
*/
publisher,
/**
* A user with no affiliation can susbcribe to the node.
*/
none,
/**
* Outcast users are not allowed to subscribe to the node.
*/
outcast;
}
}
/**
* $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.
*/
package org.jivesoftware.wildfire.pubsub;
import org.dom4j.Element;
import org.jivesoftware.util.LocaleUtils;
import org.jivesoftware.util.Log;
import org.jivesoftware.util.FastDateFormat;
import org.xmpp.forms.DataForm;
import org.xmpp.forms.FormField;
import org.xmpp.packet.JID;
import org.xmpp.packet.Message;
import org.xmpp.packet.IQ;
import org.xmpp.packet.Presence;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;
/**
* A subscription to a node. Entities may subscribe to a node to be notified when new events
* are published to the node. Published events may contain a {@link PublishedItem}. Only
* nodes that are configured to not deliver payloads with event notifications and to not
* persist items will let publishers to publish events without items thus not including
* items in the notifications sent to subscribers.<p>
*
* Node subscriptions may need to be configured by the subscriber or approved by a node owner
* to become active. The node configuration establishes whether configuration or approval are
* required. In any case, the subscriber will not get event notifications until the subscription
* is active.<p>
*
* Depending on the node configuration it may be possible for the same subscriber to subscribe
* multiple times to the node. Each subscription may have a different configuration like for
* instance different keywords. Keywords can be used as a way to filter the type of
* {@link PublishedItem} to be notified of. When the same subscriber has subscribed multiple
* times to the node a single notification is going to be sent to the subscriber instead of
* sending a notification for each subscription.
*
* @author Matt Tucker
*/
public class NodeSubscription {
private static SimpleDateFormat dateFormat;
private static FastDateFormat fastDateFormat;
/**
* Reference to the publish and subscribe service.
*/
private PubSubService service;
/**
* The node to which this subscription is interested in.
*/
private Node node;
/**
* JID of the entity that will receive the event notifications.
*/
private JID jid;
/**
* JID of the entity that owns this subscription. This JID is the JID of the
* NodeAffiliate that is subscribed to the node.
*/
private JID owner;
/**
* ID that uniquely identifies the subscription of the user in the node.
*/
private String id;
/**
* Current subscription state.
*/
private State state;
/**
* Flag that indicates if configuration is required by the node and is still pending to
* be configured by the subscriber.
*/
private boolean configurationPending = false;
/**
* Flag indicating whether an entity wants to receive or has disabled notifications.
*/
private boolean deliverNotifications = true;
/**
* Flag indicating whether an entity wants to receive digests (aggregations) of
* notifications or all notifications individually.
*/
private boolean usingDigest = false;
/**
* The minimum number of milliseconds between sending any two notification digests.
* Default is 24 hours.
*/
private int digestFrequency = 86400000;
/**
* The Date at which a leased subscription will end or has ended. A value of
* <tt>null</tt> means that the subscription will never expire.
*/
private Date expire = null;
/**
* Flag indicating whether an entity wants to receive an XMPP message body in
* addition to the payload format.
*/
private boolean includingBody = false;
/**
* The presence states for which an entity wants to receive notifications.
*/
private Collection<String> presenceStates = new ArrayList<String>();
/**
* When subscribing to collection nodes it is possible to be interested in new nodes
* added to the collection node or new items published in the children nodes. The default
* value is "nodes".
*/
private Type type = Type.nodes;
/**
* Receive notification from children up to certain depth. Possible values are 1 or 0.
* Zero means that there is no depth limit.
*/
private int depth = 1;
/**
* Keyword that the event needs to match. When <tt>null</tt> all event are going to
* be notified to the subscriber.
*/
private String keyword = null;
/**
* Indicates if the subscription is present in the database.
*/
private boolean savedToDB = false;
// TODO Do not send event notifications (e.g. node purge, node deleted) to unconfigured subscriptions ????
// TODO Send last published item when a subscription is authorized. We may need to move this logic to another place
// TODO Implement presence subscription from the node to the subscriber to figure out if event notifications can be sent
static {
dateFormat = new SimpleDateFormat("yyyy-MM-DD'T'HH:mm:ss.SSS'Z'");
dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
fastDateFormat = FastDateFormat
.getInstance("yyyy-MM-DD'T'HH:mm:ss.SSS'Z'", TimeZone.getTimeZone("UTC"));
}
/**
* Creates a new subscription of the specified user with the node.
*
* @param service the pubsub service hosting the node where this subscription lives.
* @param node Node to which this subscription is interested in.
* @param owner the JID of the entity that owns this subscription.
* @param jid the JID of the user that owns the subscription.
* @param state the state of the subscription with the node.
* @param id the id the uniquely identifies this subscriptin within the node.
*/
NodeSubscription(PubSubService service, Node node, JID owner, JID jid, State state, String id) {
this.service = service;
this.node = node;
this.jid = jid;
this.owner = owner;
this.state = state;
this.id = id;
if (node.isSubscriptionConfigurationRequired()) {
// Subscription configuration is required and it's still pending
setConfigurationPending(true);
}
}
/**
* Returns the node that holds this subscription.
*
* @return the node that holds this subscription.
*/
public Node getNode() {
return node;
}
/**
* Returns the ID that uniquely identifies the subscription of the user in the node.
*
* @return the ID that uniquely identifies the subscription of the user in the node.
*/
public String getID() {
return id;
}
/**
* Returns the JID that is going to receive the event notifications. This JID can be the
* owner JID or a full JID if the owner wants to receive the notification at a particular
* resource.<p>
*
* Moreover, since subscriber and owner are separated it should be theorically possible to
* have a different owner JID (e.g. gato@server1.com) and a subscriber JID
* (e.g. gato@server2.com). Note that letting this case to happen may open the pubsub service
* to get spam or security problems. However, the pubsub service should avoid this case to
* happen.
*
* @return the JID that is going to receive the event notifications.
*/
public JID getJID() {
return jid;
}
/**
* Retuns the JID of the entity that owns this subscription. The owner entity will have
* a {@link NodeAffiliate} for the owner JID. The owner may have more than one subscription
* with the node based on what this message
* {@link org.jivesoftware.wildfire.pubsub.Node#isMultipleSubscriptionsEnabled()}.
*
* @return he JID of the entity that owns this subscription.
*/
public JID getOwner() {
return owner;
}
/**
* Returns the current subscription state. Subscriptions with status of pending should be
* authorized by a node owner.
*
* @return the current subscription state.
*/
public State getState() {
return state;
}
/**
* Returns true if configuration is required by the node and is still pending to
* be configured by the subscriber. Otherwise return false.
*
* @return true if configuration is required by the node and is still pending to
* be configured by the subscriber.
*/
public boolean isConfigurationPending() {
return configurationPending;
}
/**
* Returns true if the subscription was approved by a node owner. Nodes that don't
* require node owners to approve subscription assume that all subscriptions are approved.
*
* @return true if the subscription was approved by a node owner.
*/
public boolean isApproved() {
return State.subscribed == state;
}
/**
* Returns whether an entity wants to receive or has disabled notifications.
*
* @return true when notifications should be sent to the subscriber.
*/
public boolean shouldDeliverNotifications() {
return deliverNotifications;
}
/**
* Returns whether an entity wants to receive digests (aggregations) of
* notifications or all notifications individually.
*
* @return true when an entity wants to receive digests (aggregations) of notifications.
*/
public boolean isUsingDigest() {
return usingDigest;
}
/**
* Returns the minimum number of milliseconds between sending any two notification digests.
* Default is 24 hours.
*
* @return the minimum number of milliseconds between sending any two notification digests.
*/
public int getDigestFrequency() {
return digestFrequency;
}
/**
* Returns the Date at which a leased subscription will end or has ended. A value of
* <tt>null</tt> means that the subscription will never expire.
*
* @return the Date at which a leased subscription will end or has ended. A value of
* <tt>null</tt> means that the subscription will never expire.
*/
public Date getExpire() {
return expire;
}
/**
* Returns whether an entity wants to receive an XMPP message body in
* addition to the payload format.
*
* @return true when an entity wants to receive an XMPP message body in
* addition to the payload format
*/
public boolean isIncludingBody() {
return includingBody;
}
/**
* The presence states for which an entity wants to receive notifications. When the owner
* is in any of the returned presence states then he is allowed to receive notifications.
*
* @return the presence states for which an entity wants to receive notifications.
* (e.g. available, away, etc.)
*/
public Collection<String> getPresenceStates() {
return presenceStates;
}
/**
* Returns if the owner has subscribed to receive notification of new items only
* or of new nodes only. When subscribed to a Leaf Node then only <tt>items</tt>
* is available.
*
* @return whether the owner has subscribed to receive notification of new items only
* or of new nodes only.
*/
public Type getType() {
return type;
}
/**
* Returns 1 when the subscriber wants to receive notifications only from first-level
* children of the collection. A value of 0 means that the subscriber wants to receive
* notifications from all descendents.
*
* @return 1 when the subscriber wants to receive notifications only from first-level
* children of the collection or 0 when notififying only from all descendents.
*/
public int getDepth() {
return depth;
}
/**
* Returns the keyword that the event needs to match. When <tt>null</tt> all event
* are going to be notified to the subscriber.
*
* @return the keyword that the event needs to match. When <tt>null</tt> all event
* are going to be notified to the subscriber.
*/
public String getKeyword() {
return keyword;
}
void setConfigurationPending(boolean configurationPending) {
this.configurationPending = configurationPending;
}
void setShouldDeliverNotifications(boolean deliverNotifications) {
this.deliverNotifications = deliverNotifications;
}
void setUsingDigest(boolean usingDigest) {
this.usingDigest = usingDigest;
}
void setDigestFrequency(int digestFrequency) {
this.digestFrequency = digestFrequency;
}
void setExpire(Date expire) {
this.expire = expire;
}
void setIncludingBody(boolean includingBody) {
this.includingBody = includingBody;
}
void setPresenceStates(Collection<String> presenceStates) {
this.presenceStates = presenceStates;
}
void setType(Type type) {
this.type = type;
}
void setDepth(int depth) {
this.depth = depth;
}
void setKeyword(String keyword) {
this.keyword = keyword;
}
void setSavedToDB(boolean savedToDB) {
this.savedToDB = savedToDB;
}
void configure(DataForm options) {
List<String> values;
String booleanValue;
// Remove this field from the form
options.removeField("FORM_TYPE");
// Process and remove specific collection node fields
FormField collectionField = options.getField("pubsub#subscription_type");
if (collectionField != null) {
values = collectionField.getValues();
if (values.size() > 0) {
type = Type.valueOf(values.get(0));
}
options.removeField("pubsub#subscription_type");
}
collectionField = options.getField("pubsub#subscription_depth");
if (collectionField != null) {
values = collectionField.getValues();
depth = "all".equals(values.get(0)) ? 0 : 1;
options.removeField("pubsub#subscription_depth");
}
// If there are more fields in the form then process them and set that
// the subscription has been configured
for (FormField field : options.getFields()) {
boolean fieldExists = true;
if ("pubsub#deliver".equals(field.getVariable())) {
values = field.getValues();
booleanValue = (values.size() > 0 ? values.get(0) : "1");
deliverNotifications = "1".equals(booleanValue);
}
else if ("pubsub#digest".equals(field.getVariable())) {
values = field.getValues();
booleanValue = (values.size() > 0 ? values.get(0) : "1");
usingDigest = "1".equals(booleanValue);
}
else if ("pubsub#digest_frequency".equals(field.getVariable())) {
values = field.getValues();
digestFrequency = values.size() > 0 ? Integer.parseInt(values.get(0)) : 86400000;
}
else if ("pubsub#expire".equals(field.getVariable())) {
values = field.getValues();
synchronized (dateFormat) {
try {
expire = dateFormat.parse(values.get(0));
}
catch (ParseException e) {
Log.error("Error parsing date", e);
}
}
}
else if ("pubsub#include_body".equals(field.getVariable())) {
values = field.getValues();
booleanValue = (values.size() > 0 ? values.get(0) : "1");
includingBody = "1".equals(booleanValue);
}
else if ("pubsub#show-values".equals(field.getVariable())) {
// Get the new list of presence states for which an entity wants to
// receive notifications
presenceStates = new ArrayList<String>();
for (String value : field.getValues()) {
try {
presenceStates.add(value);
}
catch (Exception e) {}
}
}
else if ("x-pubsub#keywords".equals(field.getVariable())) {
values = field.getValues();
keyword = values.isEmpty() ? null : values.get(0);
}
else {
fieldExists = false;
}
if (fieldExists) {
// Mark that the subscription has been configured
setConfigurationPending(false);
}
}
if (savedToDB) {
// Update the subscription in the backend store
PubSubPersistenceManager.saveSubscription(service, node, this, false);
}
}
/**
* Returns a data form with the subscription configuration. The data form can be used to
* edit the subscription configuration.
*
* @return data form used by the subscriber to edit the subscription configuration.
*/
DataForm getConfigurationForm() {
DataForm form = new DataForm(DataForm.Type.form);
form.setTitle(LocaleUtils.getLocalizedString("pubsub.form.subscription.title"));
List<String> params = new ArrayList<String>();
params.add(node.getNodeID());
form.addInstruction(LocaleUtils.getLocalizedString("pubsub.form.subscription.instruction", params));
// Add the form fields and configure them for edition
FormField formField = form.addField();
formField.setVariable("FORM_TYPE");
formField.setType(FormField.Type.hidden);
formField.addValue("http://jabber.org/protocol/pubsub#subscribe_options");
formField = form.addField();
formField.setVariable("pubsub#deliver");
formField.setType(FormField.Type.boolean_type);
formField.setLabel(LocaleUtils.getLocalizedString("pubsub.form.subscription.deliver"));
formField.addValue(deliverNotifications);
formField = form.addField();
formField.setVariable("pubsub#digest");
formField.setType(FormField.Type.boolean_type);
formField.setLabel(LocaleUtils.getLocalizedString("pubsub.form.subscription.digest"));
formField.addValue(usingDigest);
formField = form.addField();
formField.setVariable("pubsub#digest_frequency");
formField.setType(FormField.Type.text_single);
formField.setLabel(LocaleUtils.getLocalizedString("pubsub.form.subscription.digest_frequency"));
formField.addValue(digestFrequency);
formField = form.addField();
formField.setVariable("pubsub#expire");
formField.setType(FormField.Type.text_single);
formField.setLabel(LocaleUtils.getLocalizedString("pubsub.form.subscription.expire"));
if (expire != null) {
formField.addValue(fastDateFormat.format(expire));
}
formField = form.addField();
formField.setVariable("pubsub#include_body");
formField.setType(FormField.Type.boolean_type);
formField.setLabel(LocaleUtils.getLocalizedString("pubsub.form.subscription.include_body"));
formField.addValue(includingBody);
formField = form.addField();
formField.setVariable("pubsub#show-values");
formField.setType(FormField.Type.list_multi);
formField.setLabel(LocaleUtils.getLocalizedString("pubsub.form.subscription.show-values"));
formField.addOption(null, Presence.Show.away.name());
formField.addOption(null, Presence.Show.chat.name());
formField.addOption(null, Presence.Show.dnd.name());
formField.addOption(null, "online");
formField.addOption(null, Presence.Show.xa.name());
for (String value : presenceStates) {
formField.addValue(value);
}
if (node.isCollectionNode()) {
formField = form.addField();
formField.setVariable("pubsub#subscription_type");
formField.setType(FormField.Type.list_single);
formField.setLabel(LocaleUtils.getLocalizedString("pubsub.form.subscription.subscription_type"));
formField.addOption(null, Type.items.name());
formField.addOption(null, Type.nodes.name());
formField.addValue(type);
formField = form.addField();
formField.setVariable("pubsub#subscription_depth");
formField.setType(FormField.Type.list_single);
formField.setLabel(LocaleUtils.getLocalizedString("pubsub.form.subscription.subscription_depth"));
formField.addOption(null, "1");
formField.addOption(null, "all");
formField.addValue(depth == 1 ? "1" : "all");
}
if (!node.isCollectionNode() || type == Type.items) {
formField = form.addField();
formField.setVariable("x-pubsub#keywords");
formField.setType(FormField.Type.text_single);
formField.setLabel(LocaleUtils.getLocalizedString("pubsub.form.subscription.keywords"));
if (keyword != null) {
formField.addValue(keyword);
}
}
return form;
}
/**
* Returns true if an event notification can be sent to the subscriber for the specified
* published item based on the subsription configuration and subscriber status.
*
* @param leafNode the node that received the publication.
* @param publishedItem the published item to send or null if the publication didn't
* contain an item.
* @return true if an event notification can be sent to the subscriber for the specified
* published item.
*/
boolean canSendEventNotification(LeafNode leafNode, PublishedItem publishedItem) {
// Check if the subscription is active
if (!isActive()) {
return false;
}
// Check if delivery of notifications is disabled
if (!shouldDeliverNotifications()) {
return false;
}
// Check if delivery is subject to presence-based policy
if (!getPresenceStates().isEmpty()) {
String show = service.getShowPresence(jid);
if (show == null || !getPresenceStates().contains(show)) {
return false;
}
}
// Check that any defined keyword was matched (applies only if an item was published)
if (publishedItem != null && !isKeywordMatched(publishedItem)) {
return false;
}
// Check special conditions when subscribed to collection node
if (node.isCollectionNode()) {
// Check if not subscribe to items
if (Type.items != type) {
return false;
}
// Check if published node is a first-level child of the subscribed node
if (getDepth() == 1 && !node.isChildNode(leafNode)) {
return false;
}
// Check if published node is a descendant child of the subscribed node
if (getDepth() == 0 && !node.isDescendantNode(leafNode)) {
return false;
}
}
return true;
}
/**
* Returns true if the published item matches the keyword filter specified in
* the subscription. If no keyword was specified then answer true.
*
* @param publishedItem the published item to send.
* @return true if the published item matches the keyword filter specified in
* the subscription.
*/
private boolean isKeywordMatched(PublishedItem publishedItem) {
// Check if keyword was defined and it was not matched
if (keyword != null && keyword.length() > 0 && !publishedItem.containsKeyword(keyword)) {
return false;
}
return true;
}
/**
* Returns true if the subscription is active. A subscription is considered active if it
* has not expired, it has been approved and configured.
*
* @return true if the subscription is active.
*/
private boolean isActive() {
// Check if subscription is approved and configured (if required)
if (!isApproved() || this.isConfigurationPending()) {
return false;
}
// Check if the subscription has expired
if (expire != null && new Date().after(expire)) {
// TODO This checking does not replace the fact that we need to implement expiration.
// TODO A thread that checks expired subscriptions and removes them is needed.
return false;
}
return true;
}
/**
* Sends the current subscription status to the user that tried to create a subscription to
* the node. The subscription status is sent to the subsciber after the subscription was
* created or if the subscriber tries to subscribe many times and the node does not support
* multpiple subscriptions.
*
* @param originalRequest the IQ packet sent by the subscriber to create the subscription.
*/
void sendSubscriptionState(IQ originalRequest) {
IQ result = IQ.createResultIQ(originalRequest);
Element child = result.setChildElement("pubsub", "http://jabber.org/protocol/pubsub");
Element entity = child.addElement("entity");
if (!node.isRootCollectionNode()) {
entity.addAttribute("node", node.getNodeID());
}
entity.addAttribute("jid", getJID().toString());
NodeAffiliate nodeAffiliate = getAffiliate();
entity.addAttribute("affiliation", nodeAffiliate.getAffiliation().name());
if (node.isMultipleSubscriptionsEnabled()) {
entity.addAttribute("subid", getID());
}
entity.addAttribute("subscription", getState().name());
Element subscribeOptions = entity.addElement("subscribe-options");
if (node.isSubscriptionConfigurationRequired() && isConfigurationPending()) {
subscribeOptions.addElement("required");
}
// Send the result
service.send(result);
}
/**
* Sends an IQ result with the list of items published to the node to the subscriber. The
* items to include in the result is subject to the subscription configuration. If the
* subscription is still pending to be approved or unconfigured then no items will be included.
*
* @param originalRequest the IQ packet sent by the subscriber 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");
if (isActive()) {
for (PublishedItem publishedItem : publishedItems) {
// Check if this published item can be included in the result
if (!isKeywordMatched(publishedItem)) {
continue;
}
Element item = items.addElement("item");
if (((LeafNode) node).isItemRequired()) {
item.addAttribute("id", publishedItem.getID());
}
if ((forceToIncludePayload || node.isDeliverPayloads()) &&
publishedItem.getPayload() != null) {
item.add(publishedItem.getPayload().createCopy());
}
}
}
// Send the result
service.send(result);
}
/**
* Sends an event notification for the last published item to the subscriber. If
* the subscription has not yet been authorized or is pending to be configured then
* no notification is going to be sent.<p>
*
* Depending on the subscription configuration the event notification may or may not have
* a payload, may not be sent if a keyword (i.e. filter) was defined and it was not matched.
*
* @param publishedItem the last item that was published to the node.
*/
void sendLastPublishedItem(PublishedItem publishedItem) {
// Check if the published item can be sent to the subscriber
if (!canSendEventNotification(publishedItem.getNode(), publishedItem)) {
return;
}
// Send event notification to the subscriber
Message notification = new Message();
Element event = notification.getElement()
.addElement("event", "http://jabber.org/protocol/pubsub#event");
Element items = event.addElement("items");
items.addAttribute("node", node.getNodeID());
Element item = items.addElement("item");
if (((LeafNode) node).isItemRequired()) {
item.addAttribute("id", publishedItem.getID());
}
if (node.isDeliverPayloads() && publishedItem.getPayload() != null) {
item.add(publishedItem.getPayload().createCopy());
}
// Add a message body (if required)
if (isIncludingBody()) {
notification.setBody(LocaleUtils.getLocalizedString("pubsub.notification.message.body"));
}
// Include date when published item was created
notification.getElement().addElement("x", "jabber:x:delay")
.addAttribute("stamp", fastDateFormat.format(publishedItem.getCreationDate()));
// Send the event notification to the subscriber
service.sendNotification(node, notification, jid);
}
/**
* Returns true if the specified user is allowed to modify or cancel the subscription. Users
* that are allowed to modify/cancel the subscription are: the entity that is recieving the
* notifications, the owner of the subscriptions or sysadmins of the pubsub service.
*
* @param user the user that is trying to cancel the subscription.
* @return true if the specified user is allowed to modify or cancel the subscription.
*/
boolean canModify(JID user) {
return user.equals(getJID()) || user.equals(getOwner()) || service.isServiceAdmin(user);
}
/**
* Returns the {@link NodeAffiliate} that owns this subscription. Users that have a
* subscription with the node will ALWAYS have an affiliation even if the
* affiliation is of type <tt>none</tt>.
*
* @return the NodeAffiliate that owns this subscription.
*/
public NodeAffiliate getAffiliate() {
return node.getAffiliate(getOwner());
}
public String toString() {
return super.toString() + " - JID: " + getJID() + " - State: " + getState().name();
}
/**
* Subscriptions to a node may exist in several states. Delivery of event notifications
* varies according to the subscription state of the user with the node.
*/
public static enum State {
/**
* The node will never send event notifications or payloads to users in this state. Users
* with subscription state none and affiliation none are going to be deleted.
*/
none,
/**
* An entity has requested to subscribe to a node and the request has not yet been
* approved by a node owner. The node will not send event notifications or payloads
* to the entity while it is in this state.
*/
pending,
/**
* An entity has subscribed but its subscription options have not yet been configured.
* The node will send event notifications or payloads to the entity while it is in this
* state. Default subscription configuration is going to be assumed.
*/
unconfigured,
/**
* An entity is subscribed to a node. The node will send all event notifications
* (and, if configured, payloads) to the entity while it is in this state.
*/
subscribed;
}
public static enum Type {
/**
* Receive notification of new items only.
*/
items,
/**
* Receive notification of new nodes only.
*/
nodes;
}
}
/**
* $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.
*/
package org.jivesoftware.wildfire.pubsub;
import java.io.PrintStream;
import java.io.PrintWriter;
/**
* Exception used for representing that the specified node configuration is not acceptable. A
* not acceptable error could occur if owner tries to leave the node without owners. A 406 error
* code is returned to the user that requested the invalid operation.
*
* @author Matt Tucker
*/
public class NotAcceptableException extends Exception {
private static final long serialVersionUID = 1L;
private Throwable nestedThrowable = null;
public NotAcceptableException() {
super();
}
public NotAcceptableException(String msg) {
super(msg);
}
public NotAcceptableException(Throwable nestedThrowable) {
this.nestedThrowable = nestedThrowable;
}
public NotAcceptableException(String msg, Throwable nestedThrowable) {
super(msg);
this.nestedThrowable = nestedThrowable;
}
public void printStackTrace() {
super.printStackTrace();
if (nestedThrowable != null) {
nestedThrowable.printStackTrace();
}
}
public void printStackTrace(PrintStream ps) {
super.printStackTrace(ps);
if (nestedThrowable != null) {
nestedThrowable.printStackTrace(ps);
}
}
public void printStackTrace(PrintWriter pw) {
super.printStackTrace(pw);
if (nestedThrowable != null) {
nestedThrowable.printStackTrace(pw);
}
}
}
/**
* $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.
*/
package org.jivesoftware.wildfire.pubsub;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.dom4j.QName;
import org.jivesoftware.util.Log;
import org.jivesoftware.util.StringUtils;
import org.jivesoftware.wildfire.PacketRouter;
import org.jivesoftware.wildfire.user.UserManager;
import org.jivesoftware.wildfire.pubsub.models.AccessModel;
import org.xmpp.forms.DataForm;
import org.xmpp.forms.FormField;
import org.xmpp.packet.*;
import java.util.Iterator;
import java.util.List;
import java.util.Collection;
import java.util.ArrayList;
/**
* A PubSubEngine is responsible for handling packets sent to the pub-sub service.
*
* @author Matt Tucker
*/
public class PubSubEngine {
private PubSubService service;
/**
* The packet router for the server.
*/
private PacketRouter router = null;
public PubSubEngine(PubSubService pubSubService, PacketRouter router) {
this.service = pubSubService;
this.router = router;
}
/**
* Handles IQ packets sent to the pubsub service. Requests of disco#info and disco#items
* are not being handled by the engine. Instead the service itself should handle disco packets.
*
* @param iq the IQ packet sent to the pubsub service.
* @return true if the IQ packet was handled by the engine.
*/
public boolean process(IQ iq) {
// Ignore IQs of type ERROR or RESULT
if (IQ.Type.error == iq.getType() || IQ.Type.result == iq.getType()) {
return true;
}
Element childElement = iq.getChildElement();
String namespace = null;
if (childElement != null) {
namespace = childElement.getNamespaceURI();
}
if ("http://jabber.org/protocol/pubsub".equals(namespace)) {
Element action = childElement.element("publish");
if (action != null) {
// Entity publishes an item
publishItemToNode(iq, action);
return true;
}
action = childElement.element("subscribe");
if (action != null) {
// Entity subscribes to a node
subscribeNode(iq, childElement, action);
return true;
}
action = childElement.element("options");
if (action != null) {
if (IQ.Type.get == iq.getType()) {
// Subscriber requests subscription options form
getSubscriptionConfiguration(iq, childElement, action);
}
else {
// Subscriber submits completed options form
configureSubscription(iq, action);
}
return true;
}
action = childElement.element("create");
if (action != null) {
// Entity is requesting to create a new node
createNode(iq, childElement, action);
return true;
}
action = childElement.element("unsubscribe");
if (action != null) {
// Entity unsubscribes from a node
unsubscribeNode(iq, action);
return true;
}
action = childElement.element("affiliations");
if (action != null) {
// Entity requests all current affiliations
getAffiliations(iq, childElement);
return true;
}
action = childElement.element("items");
if (action != null) {
// Subscriber requests all active items
getPublishedItems(iq, action);
return true;
}
action = childElement.element("retract");
if (action != null) {
// TODO Entity deletes an item
return true;
}
// Unknown action requested
sendErrorPacket(iq, PacketError.Condition.bad_request, null);
return true;
}
else if ("http://jabber.org/protocol/pubsub#owner".equals(namespace)) {
Element action = childElement.element("configure");
if (action != null) {
String nodeID = action.attributeValue("node");
if (nodeID == null) {
// if user is not sysadmin then return nodeid-required error
if (!service.isServiceAdmin(iq.getFrom())) {
// Configure elements must have a node attribute so answer an error
Element pubsubError = DocumentHelper.createElement(QName.get(
"nodeid-required", "http://jabber.org/protocol/pubsub#errors"));
sendErrorPacket(iq, PacketError.Condition.bad_request, pubsubError);
return true;
}
else {
// Sysadmin is trying to configure root collection node
nodeID = service.getRootCollectionNode().getNodeID();
}
}
if (IQ.Type.get == iq.getType()) {
// Owner requests configuration form of a node
getNodeConfiguration(iq, childElement, nodeID);
}
else {
// Owner submits or cancels node configuration form
configureNode(iq, action, nodeID);
}
return true;
}
action = childElement.element("default");
if (action != null) {
// Owner requests default configuration options for
// leaf or collection nodes
getDefaultNodeConfiguration(iq, childElement, action);
return true;
}
action = childElement.element("delete");
if (action != null) {
// Owner deletes a node
deleteNode(iq, action);
return true;
}
action = childElement.element("entities");
if (action != null) {
// Owner requests all affiliated entities
getAffiliatedEntities(iq, action);
return true;
}
action = childElement.element("purge");
if (action != null) {
// Owner purges items from a node
purgeNode(iq, action);
return true;
}
// Unknown action requested so return error to sender
sendErrorPacket(iq, PacketError.Condition.bad_request, null);
return true;
}
return false;
}
/**
* Handles Presence packets sent to the pubsub service.
*
* @param presence the Presence packet sent to the pubsub service.
*/
public void process(Presence presence) {
// TODO Handle received presence of users the service has subscribed
}
/**
* Handles Message packets sent to the pubsub service.
*
* @param message the Message packet sent to the pubsub service.
*/
public void process(Message message) {
// TODO Process Messages of type error to identify possible subscribers that no longer exist
// See "Handling Notification-Related Errors" section
}
private void publishItemToNode(IQ iq, Element publishElement) {
String nodeID = publishElement.attributeValue("node");
Node node = null;
if (nodeID == null) {
// No node was specified. Return bad_request error
Element pubsubError = DocumentHelper.createElement(QName.get(
"nodeid-required", "http://jabber.org/protocol/pubsub#errors"));
sendErrorPacket(iq, PacketError.Condition.item_not_found, pubsubError);
return;
}
else {
// Look for the specified node
node = service.getNode(nodeID);
if (node == null) {
// Node does not exist. Return item-not-found error
sendErrorPacket(iq, PacketError.Condition.item_not_found, null);
return;
}
}
JID from = iq.getFrom();
// TODO Assuming that owner is the bare JID (as defined in the JEP). This can be replaced with an explicit owner specified in the packet
JID owner = new JID(from.toBareJID());
if (!node.getPublisherModel().canPublish(node, owner) && !service.isServiceAdmin(owner)) {
// Entity does not have sufficient privileges to publish to node
sendErrorPacket(iq, PacketError.Condition.forbidden, null);
return;
}
if (node.isCollectionNode()) {
// Node is a collection node. Return feature-not-implemented error
Element pubsubError = DocumentHelper.createElement(
QName.get("unsupported", "http://jabber.org/protocol/pubsub#errors"));
pubsubError.addAttribute("feature", "publish");
sendErrorPacket(iq, PacketError.Condition.feature_not_implemented, pubsubError);
return;
}
LeafNode leafNode = (LeafNode) node;
Element item = publishElement.element("item");
List entries = null;
Element payload = null;
String itemID = null;
if (item != null) {
entries = item.elements();
payload = entries.isEmpty() ? null : (Element) entries.get(0);
itemID = item.attributeValue("id");
}
// Check that an item was included if node persist items or includes payload
if (item == null && leafNode.isItemRequired()) {
Element pubsubError = DocumentHelper.createElement(QName.get(
"item-required", "http://jabber.org/protocol/pubsub#errors"));
sendErrorPacket(iq, PacketError.Condition.bad_request, pubsubError);
return;
}
// Check that no item was included if node doesn't persist items and doesn't
// includes payload
if (item != null && !leafNode.isItemRequired()) {
Element pubsubError = DocumentHelper.createElement(QName.get(
"item-forbidden", "http://jabber.org/protocol/pubsub#errors"));
sendErrorPacket(iq, PacketError.Condition.bad_request, pubsubError);
return;
}
// Check that a payload was included if node is configured to include payload
// in notifications
if (payload == null && leafNode.isDeliverPayloads()) {
Element pubsubError = DocumentHelper.createElement(QName.get(
"payload-required", "http://jabber.org/protocol/pubsub#errors"));
sendErrorPacket(iq, PacketError.Condition.bad_request, pubsubError);
return;
}
// Check that the payload (if any) contains only one child element
if (entries.size() > 1) {
Element pubsubError = DocumentHelper.createElement(QName.get(
"invalid-payload", "http://jabber.org/protocol/pubsub#errors"));
sendErrorPacket(iq, PacketError.Condition.bad_request, pubsubError);
return;
}
// Publish item and send event notifications to subscribers
leafNode.sendEventNotification(iq, itemID, payload);
}
private void subscribeNode(IQ iq, Element childElement, Element subscribeElement) {
String nodeID = subscribeElement.attributeValue("node");
Node node = null;
if (nodeID == null) {
// Entity subscribes to root collection node
node = service.getRootCollectionNode();
}
else {
// Look for the specified node
node = service.getNode(nodeID);
if (node == null) {
// Node does not exist. Return item-not-found error
sendErrorPacket(iq, PacketError.Condition.item_not_found, null);
return;
}
}
// Check if sender and subscriber JIDs match or if a valid "trusted proxy" is being used
JID from = iq.getFrom();
JID subscriberJID = new JID(subscribeElement.attributeValue("jid"));
if (!from.toBareJID().equals(subscriberJID.toBareJID()) && !service.isServiceAdmin(from)) {
// JIDs do not match and requestor is not a service admin so return an error
Element pubsubError = DocumentHelper.createElement(
QName.get("invalid-jid", "http://jabber.org/protocol/pubsub#errors"));
sendErrorPacket(iq, PacketError.Condition.bad_request, pubsubError);
return;
}
// TODO Assumed that the owner of the subscription is the bare JID of the subscription JID. Waiting StPeter answer for explicit field.
JID owner = new JID(subscriberJID.toBareJID());
// Check if the node's access model allows the subscription to proceed
AccessModel accessModel = node.getAccessModel();
if (!accessModel.canSubscribe(node, owner, subscriberJID)) {
sendErrorPacket(iq, accessModel.getSubsriptionError(),
accessModel.getSubsriptionErrorDetail());
return;
}
// Check if the subscriber is an anonymous user
if (!UserManager.getInstance().isRegisteredUser(subscriberJID)) {
// Anonymous users cannot subscribe to the node. Return forbidden error
sendErrorPacket(iq, PacketError.Condition.forbidden, null);
return;
}
// Check if the subscription owner is a user with outcast affiliation
NodeAffiliate nodeAffiliate = node.getAffiliate(owner);
if (nodeAffiliate != null &&
nodeAffiliate.getAffiliation() == NodeAffiliate.Affiliation.outcast) {
// Subscriber is an outcast. Return forbidden error
sendErrorPacket(iq, PacketError.Condition.forbidden, null);
return;
}
// Check that subscriptions to the node are enabled
if (!node.isSubscriptionEnabled() && !service.isServiceAdmin(from)) {
// Sender is not a sysadmin and subscription is disabled so return an error
sendErrorPacket(iq, PacketError.Condition.not_allowed, null);
return;
}
// Get any configuration form included in the options element (if any)
DataForm optionsForm = null;
Element options = childElement.element("options");
if (options != null) {
Element formElement = options.element(QName.get("x", "jabber:x:data"));
if (formElement != null) {
optionsForm = new DataForm(formElement);
}
}
// If leaf node does not support multiple subscriptions then check whether subscriber is
// creating another subscription or not
if (!node.isCollectionNode() && !node.isMultipleSubscriptionsEnabled()) {
NodeSubscription existingSubscription = node.getSubscription(subscriberJID);
if (existingSubscription != null) {
// User is trying to create another subscription so
// return current subscription state
existingSubscription.sendSubscriptionState(iq);
return;
}
}
// Check if subscribing twice to a collection node using same subscription type
if (node.isCollectionNode()) {
// By default assume that new subscription is of type node
boolean isNodeType = true;
if (optionsForm != null) {
FormField field = optionsForm.getField("pubsub#subscription_type");
if (field != null) {
if ("items".equals(field.getValues().get(0))) {
isNodeType = false;
}
}
}
if (nodeAffiliate != null && isNodeType) {
for (NodeSubscription subscription : nodeAffiliate.getSubscriptions()) {
if (isNodeType) {
// User is requesting a subscription of type "nodes"
if (NodeSubscription.Type.nodes == subscription.getType()) {
// Cannot have 2 subscriptions of the same type. Return conflict error
sendErrorPacket(iq, PacketError.Condition.conflict, null);
return;
}
}
else if (!node.isMultipleSubscriptionsEnabled()) {
// User is requesting a subscription of type "items" and
// multiple subscriptions is not allowed
if (NodeSubscription.Type.items == subscription.getType()) {
// User is trying to create another subscription so
// return current subscription state
subscription.sendSubscriptionState(iq);
return;
}
}
}
}
}
// Create a subscription and an affiliation if the subscriber doesn't have one
node.createSubscription(iq, owner, subscriberJID, accessModel.isAuthorizationRequired(),
optionsForm);
}
private void unsubscribeNode(IQ iq, Element unsubscribeElement) {
String nodeID = unsubscribeElement.attributeValue("node");
String subID = unsubscribeElement.attributeValue("subid");
Node node = null;
if (nodeID == null) {
// Entity unsubscribes from root collection node
node = service.getRootCollectionNode();
}
else {
// Look for the specified node
node = service.getNode(nodeID);
if (node == null) {
// Node does not exist. Return item-not-found error
sendErrorPacket(iq, PacketError.Condition.item_not_found, null);
return;
}
}
NodeSubscription subscription = null;
if (node.isMultipleSubscriptionsEnabled()) {
if (subID == null) {
// No subid was specified and the node supports multiple subscriptions
Element pubsubError = DocumentHelper.createElement(
QName.get("subid-required", "http://jabber.org/protocol/pubsub#errors"));
sendErrorPacket(iq, PacketError.Condition.bad_request, pubsubError);
return;
}
else {
// Check if the specified subID belongs to an existing node subscription
subscription = node.getSubscription(subID);
if (subscription == null) {
Element pubsubError = DocumentHelper.createElement(
QName.get("invalid-subid", "http://jabber.org/protocol/pubsub#errors"));
sendErrorPacket(iq, PacketError.Condition.not_acceptable, pubsubError);
return;
}
}
}
else {
String jidAttribute = unsubscribeElement.attributeValue("jid");
// Check if the specified JID has a subscription with the node
if (jidAttribute == null) {
// No JID was specified so return an error indicating that jid is required
Element pubsubError = DocumentHelper.createElement(
QName.get("jid-required", "http://jabber.org/protocol/pubsub#errors"));
sendErrorPacket(iq, PacketError.Condition.bad_request, pubsubError);
return;
}
JID subscriberJID = new JID(jidAttribute);
subscription = node.getSubscription(subscriberJID);
if (subscription == null) {
Element pubsubError = DocumentHelper.createElement(
QName.get("not-subscribed", "http://jabber.org/protocol/pubsub#errors"));
sendErrorPacket(iq, PacketError.Condition.unexpected_request, pubsubError);
return;
}
}
JID from = iq.getFrom();
// Check that unsubscriptions to the node are enabled
if (!node.isSubscriptionEnabled() && !service.isServiceAdmin(from)) {
// Sender is not a sysadmin and unsubscription is disabled so return an error
sendErrorPacket(iq, PacketError.Condition.not_allowed, null);
return;
}
// TODO Assumed that the owner of the subscription is the bare JID of the subscription JID. Waiting StPeter answer for explicit field.
JID owner = new JID(from.toBareJID());
// A subscription was found so check if the user is allowed to cancel the subscription
if (!subscription.canModify(from) && !subscription.canModify(owner)) {
// Requestor is prohibited from unsubscribing entity
sendErrorPacket(iq, PacketError.Condition.forbidden, null);
return;
}
// Cancel subscription
node.cancelSubscription(subscription);
// Send reply with success
router.route(IQ.createResultIQ(iq));
}
private void getSubscriptionConfiguration(IQ iq, Element childElement, Element optionsElement) {
String nodeID = optionsElement.attributeValue("node");
String subID = optionsElement.attributeValue("subid");
Node node = null;
if (nodeID == null) {
// Entity requests subscription options of root collection node
node = service.getRootCollectionNode();
}
else {
// Look for the specified node
node = service.getNode(nodeID);
if (node == null) {
// Node does not exist. Return item-not-found error
sendErrorPacket(iq, PacketError.Condition.item_not_found, null);
return;
}
}
NodeSubscription subscription = null;
if (node.isMultipleSubscriptionsEnabled()) {
if (subID == null) {
// No subid was specified and the node supports multiple subscriptions
Element pubsubError = DocumentHelper.createElement(
QName.get("subid-required", "http://jabber.org/protocol/pubsub#errors"));
sendErrorPacket(iq, PacketError.Condition.bad_request, pubsubError);
return;
}
else {
// Check if the specified subID belongs to an existing node subscription
subscription = node.getSubscription(subID);
if (subscription == null) {
Element pubsubError = DocumentHelper.createElement(
QName.get("invalid-subid", "http://jabber.org/protocol/pubsub#errors"));
sendErrorPacket(iq, PacketError.Condition.not_acceptable, pubsubError);
return;
}
}
}
else {
// Check if the specified JID has a subscription with the node
String jidAttribute = optionsElement.attributeValue("jid");
if (jidAttribute == null) {
// No JID was specified so return an error indicating that jid is required
Element pubsubError = DocumentHelper.createElement(
QName.get("jid-required", "http://jabber.org/protocol/pubsub#errors"));
sendErrorPacket(iq, PacketError.Condition.bad_request, pubsubError);
return;
}
JID subscriberJID = new JID(jidAttribute);
subscription = node.getSubscription(subscriberJID);
if (subscription == null) {
Element pubsubError = DocumentHelper.createElement(
QName.get("not-subscribed", "http://jabber.org/protocol/pubsub#errors"));
sendErrorPacket(iq, PacketError.Condition.unexpected_request, pubsubError);
return;
}
}
// A subscription was found so check if the user is allowed to get the subscription options
if (!subscription.canModify(iq.getFrom())) {
// Requestor is prohibited from getting the subscription options
sendErrorPacket(iq, PacketError.Condition.forbidden, null);
return;
}
// Return data form containing subscription configuration to the subscriber
IQ reply = IQ.createResultIQ(iq);
Element replyChildElement = childElement.createCopy();
reply.setChildElement(replyChildElement);
replyChildElement.element("options").add(subscription.getConfigurationForm().getElement());
router.route(reply);
}
private void configureSubscription(IQ iq, Element optionsElement) {
String nodeID = optionsElement.attributeValue("node");
String subID = optionsElement.attributeValue("subid");
Node node = null;
if (nodeID == null) {
// Entity submits new subscription options of root collection node
node = service.getRootCollectionNode();
}
else {
// Look for the specified node
node = service.getNode(nodeID);
if (node == null) {
// Node does not exist. Return item-not-found error
sendErrorPacket(iq, PacketError.Condition.item_not_found, null);
return;
}
}
NodeSubscription subscription = null;
if (node.isMultipleSubscriptionsEnabled()) {
if (subID == null) {
// No subid was specified and the node supports multiple subscriptions
Element pubsubError = DocumentHelper.createElement(
QName.get("subid-required", "http://jabber.org/protocol/pubsub#errors"));
sendErrorPacket(iq, PacketError.Condition.bad_request, pubsubError);
return;
}
else {
// Check if the specified subID belongs to an existing node subscription
subscription = node.getSubscription(subID);
if (subscription == null) {
Element pubsubError = DocumentHelper.createElement(
QName.get("invalid-subid", "http://jabber.org/protocol/pubsub#errors"));
sendErrorPacket(iq, PacketError.Condition.not_acceptable, pubsubError);
return;
}
}
}
else {
// Check if the specified JID has a subscription with the node
String jidAttribute = optionsElement.attributeValue("jid");
if (jidAttribute == null) {
// No JID was specified so return an error indicating that jid is required
Element pubsubError = DocumentHelper.createElement(
QName.get("jid-required", "http://jabber.org/protocol/pubsub#errors"));
sendErrorPacket(iq, PacketError.Condition.bad_request, pubsubError);
return;
}
JID subscriberJID = new JID(jidAttribute);
subscription = node.getSubscription(subscriberJID);
if (subscription == null) {
Element pubsubError = DocumentHelper.createElement(
QName.get("not-subscribed", "http://jabber.org/protocol/pubsub#errors"));
sendErrorPacket(iq, PacketError.Condition.unexpected_request, pubsubError);
return;
}
}
// A subscription was found so check if the user is allowed to submits
// new subscription options
if (!subscription.canModify(iq.getFrom())) {
// Requestor is prohibited from setting new subscription options
sendErrorPacket(iq, PacketError.Condition.forbidden, null);
return;
}
Element formElement = optionsElement.element(QName.get("x", "jabber:x:data"));
if (formElement != null) {
boolean wasUnconfigured = subscription.isConfigurationPending();
// Change the subscription configuration based on the completed form
subscription.configure(new DataForm(formElement));
// Return success response
router.route(IQ.createResultIQ(iq));
// Send last published item if subscription is now configured (and authorized)
if (wasUnconfigured && !subscription.isConfigurationPending()) {
PublishedItem lastItem = node.getLastPublishedItem();
if (lastItem != null) {
subscription.sendLastPublishedItem(lastItem);
}
}
}
else {
// No data form was included so return bad request error
sendErrorPacket(iq, PacketError.Condition.bad_request, null);
}
}
private void getAffiliations(IQ iq, Element childElement) {
// TODO Assuming that owner is the bare JID (as defined in the JEP). This can be replaced with an explicit owner specified in the packet
JID owner = new JID(iq.getFrom().toBareJID());
// Collect subscriptions (and implicit affiliations) or affiliations for
// which a subscription does not exists (e.g. outcast user)
Collection<NodeSubscription> subscriptions = new ArrayList<NodeSubscription>();
Collection<NodeAffiliate> affiliations = new ArrayList<NodeAffiliate>();
for (Node node : service.getNodes()) {
Collection<NodeSubscription> nodeSubscriptions = node.getSubscriptions(owner);
if (!nodeSubscriptions.isEmpty()) {
subscriptions.addAll(nodeSubscriptions);
}
else {
NodeAffiliate nodeAffiliate = node.getAffiliate(owner);
if (nodeAffiliate != null) {
affiliations.add(nodeAffiliate);
}
}
}
// Create reply to send
IQ reply = IQ.createResultIQ(iq);
Element replyChildElement = childElement.createCopy();
reply.setChildElement(replyChildElement);
if (subscriptions.isEmpty() && affiliations.isEmpty()) {
// User does not have any affiliation or subscription with the pubsub service
reply.setError(PacketError.Condition.item_not_found);
}
else {
Element affiliationsElement = replyChildElement.element("affiliations");
// Add information about affiliations without subscriptions
for (NodeAffiliate affiliate : affiliations) {
Element entity = affiliationsElement.addElement("entity");
// Do not include the node id when node is the root collection node
if (!affiliate.getNode().isRootCollectionNode()) {
entity.addAttribute("node", affiliate.getNode().getNodeID());
}
entity.addAttribute("jid", affiliate.getJID().toString());
entity.addAttribute("affiliation", affiliate.getAffiliation().name());
entity.addAttribute("subscription", NodeSubscription.State.none.name());
}
// Add information about subscriptions including existing affiliations
for (NodeSubscription subscription : subscriptions) {
Element entity = affiliationsElement.addElement("entity");
Node node = subscription.getNode();
NodeAffiliate nodeAffiliate = subscription.getAffiliate();
// Do not include the node id when node is the root collection node
if (!node.isRootCollectionNode()) {
entity.addAttribute("node", node.getNodeID());
}
entity.addAttribute("jid", subscription.getJID().toString());
entity.addAttribute("affiliation", nodeAffiliate.getAffiliation().name());
entity.addAttribute("subscription", subscription.getState().name());
if (node.isMultipleSubscriptionsEnabled()) {
entity.addAttribute("subid", subscription.getID());
}
}
}
// Send reply
router.route(reply);
}
private void getPublishedItems(IQ iq, Element itemsElement) {
String nodeID = itemsElement.attributeValue("node");
String subID = itemsElement.attributeValue("subid");
Node node = null;
if (nodeID == null) {
// Entity subscribes to root collection node
node = service.getRootCollectionNode();
}
else {
// Look for the specified node
node = service.getNode(nodeID);
if (node == null) {
// Node does not exist. Return item-not-found error
sendErrorPacket(iq, PacketError.Condition.item_not_found, null);
return;
}
}
if (node.isCollectionNode()) {
// Node is a collection node. Return feature-not-implemented error
Element pubsubError = DocumentHelper.createElement(
QName.get("unsupported", "http://jabber.org/protocol/pubsub#errors"));
pubsubError.addAttribute("feature", "retrieve-items");
sendErrorPacket(iq, PacketError.Condition.feature_not_implemented, pubsubError);
return;
}
// Check if sender and subscriber JIDs match or if a valid "trusted proxy" is being used
JID subscriberJID = iq.getFrom();
// TODO Assumed that the owner of the subscription is the bare JID of the subscription JID. Waiting StPeter answer for explicit field.
JID owner = new JID(subscriberJID.toBareJID());
// Check if the node's access model allows the subscription to proceed
AccessModel accessModel = node.getAccessModel();
if (!accessModel.canAccessItems(node, owner, subscriberJID)) {
sendErrorPacket(iq, accessModel.getSubsriptionError(),
accessModel.getSubsriptionErrorDetail());
return;
}
// Get the user's subscription
NodeSubscription subscription = null;
if (node.isMultipleSubscriptionsEnabled()) {
if (subID == null) {
// No subid was specified and the node supports multiple subscriptions
Element pubsubError = DocumentHelper.createElement(
QName.get("subid-required", "http://jabber.org/protocol/pubsub#errors"));
sendErrorPacket(iq, PacketError.Condition.bad_request, pubsubError);
return;
}
else {
// Check if the specified subID belongs to an existing node subscription
subscription = node.getSubscription(subID);
if (subscription == null) {
Element pubsubError = DocumentHelper.createElement(
QName.get("invalid-subid", "http://jabber.org/protocol/pubsub#errors"));
sendErrorPacket(iq, PacketError.Condition.not_acceptable, pubsubError);
return;
}
}
}
else {
subscription = node.getSubscription(subscriberJID);
if (subscription == null) {
// TODO Current version does not allow anyone to get published items. A subscription should exist.
Element pubsubError = DocumentHelper.createElement(
QName.get("not-subscribed", "http://jabber.org/protocol/pubsub#errors"));
sendErrorPacket(iq, PacketError.Condition.not_authorized, pubsubError);
return;
}
}
// Get list of items to send to the user
boolean forceToIncludePayload = false;
List<PublishedItem> items = null;
String max_items = itemsElement.attributeValue("max_items");
int recentItems = 0;
if (max_items != null) {
try {
// Parse the recent number of items requested
recentItems = Integer.parseInt(max_items);
}
catch (NumberFormatException e) {
// There was an error parsing the number so assume that all items were requested
Log.warn("Assuming that all items were requested", e);
max_items = null;
}
}
if (max_items != null) {
// Get the N most recent published items
items = node.getPublishedItems(recentItems);
}
else {
List requestedItems = itemsElement.elements("item");
if (requestedItems.isEmpty()) {
// Get all the active items that were published to the node
items = node.getPublishedItems();
}
else {
// Indicate that payload should be included (if exists) no matter
// the node configuration
forceToIncludePayload = true;
// Get the items as requested by the user
for (Iterator it = requestedItems.iterator(); it.hasNext();) {
Element element = (Element) it.next();
String itemID = element.attributeValue("id");
PublishedItem item = node.getPublishedItem(itemID);
if (item != null) {
items.add(item);
}
}
}
}
// Send items to the user
subscription.sendPublishedItems(iq, items, forceToIncludePayload);
}
private void createNode(IQ iq, Element childElement, Element createElement) {
// Get sender of the IQ packet
JID from = iq.getFrom();
// Verify that sender has permissions to create nodes
if (!service.canCreateNode(from)) {
// The user is not allowed to create nodes so return an error
sendErrorPacket(iq, PacketError.Condition.forbidden, null);
return;
}
DataForm completedForm = null;
CollectionNode parentNode = null;
String nodeID = createElement.attributeValue("node");
String newNodeID = nodeID;
if (nodeID == null) {
// User requested an instant node
if (!service.isInstantNodeSupported()) {
// Instant nodes creation is not allowed so return an error
Element pubsubError = DocumentHelper.createElement(
QName.get("nodeid-required", "http://jabber.org/protocol/pubsub#errors"));
sendErrorPacket(iq, PacketError.Condition.not_acceptable, pubsubError);
return;
}
do {
// Create a new nodeID and make sure that the random generated string does not
// match an existing node. Probability to match an existing node are very very low
// but they exist :)
newNodeID = StringUtils.randomString(15);
}
while (service.getNode(newNodeID) != null);
}
// Check if user requested to configure the node (using a data form)
Element configureElement = childElement.element("configure");
if (configureElement != null) {
// Get the data form that contains the parent nodeID
completedForm = getSentConfigurationForm(configureElement);
if (completedForm != null) {
// Calculate newNodeID when new node is affiliated with a Collection
FormField field = completedForm.getField("pubsub#collection");
if (field != null) {
List<String> values = field.getValues();
if (!values.isEmpty()) {
String parentNodeID = values.get(0);
Node tempNode = service.getNode(parentNodeID);
if (tempNode == null) {
// Requested parent node was not found so return an error
sendErrorPacket(iq, PacketError.Condition.item_not_found, null);
return;
}
else if (!tempNode.isCollectionNode()) {
// Requested parent node is not a collection node so return an error
sendErrorPacket(iq, PacketError.Condition.not_acceptable, null);
return;
}
parentNode = (CollectionNode) tempNode;
// If requested new nodeID does not contain parent nodeID then add
// the parent nodeID to the beginging of the new nodeID
if (!newNodeID.startsWith(parentNodeID)) {
newNodeID = parentNodeID + "/" + newNodeID;
}
}
}
}
}
// If no parent was defined then use the root collection node
if (parentNode == null && service.isCollectionNodesSupported()) {
parentNode = service.getRootCollectionNode();
// Calculate new nodeID for the new node
if (!newNodeID.startsWith(parentNode.getNodeID() + "/")) {
newNodeID = parentNode.getNodeID() + "/" + newNodeID;
}
}
// Check that the requested nodeID does not exist
Node existingNode = service.getNode(newNodeID);
if (existingNode != null) {
// There is a conflict since a node with the same ID already exists
sendErrorPacket(iq, PacketError.Condition.conflict, null);
return;
}
// Check if user requested to create a new collection node
boolean collectionType = "collection".equals(createElement.attributeValue("type"));
if (collectionType && !service.isCollectionNodesSupported()) {
// Cannot create a collection node since the service doesn't support it
Element pubsubError = DocumentHelper.createElement(
QName.get("unsupported", "http://jabber.org/protocol/pubsub#errors"));
pubsubError.addAttribute("feature", "collections");
sendErrorPacket(iq, PacketError.Condition.feature_not_implemented, pubsubError);
return;
}
// Create and configure the node
boolean conflict = false;
Node newNode = null;
try {
// TODO Assumed that the owner of the subscription is the bare JID of the subscription JID. Waiting StPeter answer for explicit field.
JID owner = new JID(from.toBareJID());
synchronized (newNodeID.intern()) {
if (service.getNode(newNodeID) == null) {
// Create the node
if (collectionType) {
newNode = new CollectionNode(service, parentNode, newNodeID, from);
}
else {
newNode = new LeafNode(service, parentNode, newNodeID, from);
}
// Add the creator as the node owner
newNode.addOwner(owner);
// Configure and save the node to the backend store
if (completedForm != null) {
newNode.configure(completedForm);
}
else {
newNode.saveToDB();
}
// Add the new node to the list of available nodes
service.addNode(newNode);
}
else {
conflict = true;
}
}
if (conflict) {
// There is a conflict since a node with the same ID already exists
sendErrorPacket(iq, PacketError.Condition.conflict, null);
}
else {
// Return success to the node owner
IQ reply = IQ.createResultIQ(iq);
// Include new nodeID if it has changed from the original nodeID
if (!newNode.getNodeID().equals(nodeID)) {
Element elem =
reply.setChildElement("pubsub", "http://jabber.org/protocol/pubsub");
elem.addElement("create").addAttribute("node", newNode.getNodeID());
}
router.route(reply);
// TODO Trigger notifications that parent has new child
}
}
catch (NotAcceptableException e) {
// Node should have at least one owner. Return not-acceptable error.
sendErrorPacket(iq, PacketError.Condition.not_acceptable, null);
}
}
private void getNodeConfiguration(IQ iq, Element childElement, String nodeID) {
Node node = service.getNode(nodeID);
if (node == null) {
// Node does not exist. Return item-not-found error
sendErrorPacket(iq, PacketError.Condition.item_not_found, null);
return;
}
if (!node.isAdmin(iq.getFrom())) {
// Requesting entity is prohibited from configuring this node. Return forbidden error
sendErrorPacket(iq, PacketError.Condition.forbidden, null);
return;
}
// Return data form containing node configuration to the owner
IQ reply = IQ.createResultIQ(iq);
Element replyChildElement = childElement.createCopy();
reply.setChildElement(replyChildElement);
replyChildElement.element("configure").add(node.getConfigurationForm().getElement());
router.route(reply);
}
private void getDefaultNodeConfiguration(IQ iq, Element childElement, Element defaultElement) {
String type = defaultElement.attributeValue("type");
type = type == null ? "leaf" : type;
boolean isLeafType = "leaf".equals(type);
DefaultNodeConfiguration config = service.getDefaultNodeConfiguration(isLeafType);
if (config == null) {
// Service does not support the requested node type so return an error
Element pubsubError = DocumentHelper.createElement(
QName.get("unsupported", "http://jabber.org/protocol/pubsub#errors"));
pubsubError.addAttribute("feature", isLeafType ? "leaf" : "collections");
sendErrorPacket(iq, PacketError.Condition.feature_not_implemented, pubsubError);
return;
}
// Return data form containing default node configuration
IQ reply = IQ.createResultIQ(iq);
Element replyChildElement = childElement.createCopy();
reply.setChildElement(replyChildElement);
replyChildElement.element("default").add(config.getConfigurationForm().getElement());
router.route(reply);
}
private void configureNode(IQ iq, Element configureElement, String nodeID) {
Node node = service.getNode(nodeID);
if (node == null) {
// Node does not exist. Return item-not-found error
sendErrorPacket(iq, PacketError.Condition.item_not_found, null);
return;
}
if (!node.isAdmin(iq.getFrom())) {
// Requesting entity is not allowed to get node configuration. Return forbidden error
sendErrorPacket(iq, PacketError.Condition.forbidden, null);
return;
}
// Get the data form that contains the parent nodeID
DataForm completedForm = getSentConfigurationForm(configureElement);
if (completedForm != null) {
try {
// Update node configuration with the provided data form
// (and update the backend store)
node.configure(completedForm);
// Return that node configuration was successful
router.route(IQ.createResultIQ(iq));
}
catch (NotAcceptableException e) {
// Node should have at least one owner. Return not-acceptable error.
sendErrorPacket(iq, PacketError.Condition.not_acceptable, null);
}
}
else {
// No data form was included so return bad-request error
sendErrorPacket(iq, PacketError.Condition.bad_request, null);
}
}
private void deleteNode(IQ iq, Element deleteElement) {
String nodeID = deleteElement.attributeValue("node");
if (nodeID == null) {
// NodeID was not provided. Return bad-request error
sendErrorPacket(iq, PacketError.Condition.bad_request, null);
return;
}
Node node = service.getNode(nodeID);
if (node == null) {
// Node does not exist. Return item-not-found error
sendErrorPacket(iq, PacketError.Condition.item_not_found, null);
return;
}
if (!node.isAdmin(iq.getFrom())) {
// Requesting entity is prohibited from deleting this node. Return forbidden error
sendErrorPacket(iq, PacketError.Condition.forbidden, null);
return;
}
// Delete the node
if (node.delete()) {
// Remove the node from memory
service.removeNode(node.getNodeID());
// Return that node was deleted successfully
router.route(IQ.createResultIQ(iq));
}
else {
// Some error occured while trying to delete the node
sendErrorPacket(iq, PacketError.Condition.internal_server_error, null);
}
}
private void purgeNode(IQ iq, Element purgeElement) {
String nodeID = purgeElement.attributeValue("node");
if (nodeID == null) {
// NodeID was not provided. Return bad-request error
sendErrorPacket(iq, PacketError.Condition.bad_request, null);
return;
}
Node node = service.getNode(nodeID);
if (node == null) {
// Node does not exist. Return item-not-found error
sendErrorPacket(iq, PacketError.Condition.item_not_found, null);
return;
}
if (!node.isAdmin(iq.getFrom())) {
// Requesting entity is prohibited from configuring this node. Return forbidden error
sendErrorPacket(iq, PacketError.Condition.forbidden, null);
return;
}
if (!((LeafNode) node).isPersistPublishedItems()) {
// Node does not persist items. Return feature-not-implemented error
Element pubsubError = DocumentHelper.createElement(
QName.get("unsupported", "http://jabber.org/protocol/pubsub#errors"));
pubsubError.addAttribute("feature", "persistent-items");
sendErrorPacket(iq, PacketError.Condition.feature_not_implemented, pubsubError);
return;
}
if (node.isCollectionNode()) {
// Node is a collection node. Return feature-not-implemented error
Element pubsubError = DocumentHelper.createElement(
QName.get("unsupported", "http://jabber.org/protocol/pubsub#errors"));
pubsubError.addAttribute("feature", "purge-nodes");
sendErrorPacket(iq, PacketError.Condition.feature_not_implemented, pubsubError);
return;
}
// Purge the node
((LeafNode) node).purge();
// Return that node purged successfully
router.route(IQ.createResultIQ(iq));
}
private void getAffiliatedEntities(IQ iq, Element affiliatedElement) {
String nodeID = affiliatedElement.attributeValue("node");
if (nodeID == null) {
// NodeID was not provided. Return bad-request error.
sendErrorPacket(iq, PacketError.Condition.bad_request, null);
return;
}
Node node = service.getNode(nodeID);
if (node == null) {
// Node does not exist. Return item-not-found error.
sendErrorPacket(iq, PacketError.Condition.item_not_found, null);
return;
}
if (!node.isAdmin(iq.getFrom())) {
// Requesting entity is prohibited from getting affiliates list. Return forbidden error.
sendErrorPacket(iq, PacketError.Condition.forbidden, null);
return;
}
// Ask the node to send the list of affiliated entities to the owner
node.sendAffiliatedEntities(iq);
}
/**
* Generate a conflict packet to indicate that the nickname being requested/used is already in
* use by another user.
*
* @param packet the packet to be bounced.
*/
private void sendErrorPacket(IQ packet, PacketError.Condition error, Element pubsubError) {
IQ reply = IQ.createResultIQ(packet);
reply.setChildElement(packet.getChildElement().createCopy());
reply.setError(error);
if (pubsubError != null) {
// Add specific pubsub error if available
reply.getError().getElement().add(pubsubError);
}
router.route(reply);
}
/**
* Returns the data form included in the configure element sent by the node owner or
* <tt>null</tt> if none was included or access model was defined. If the
* owner just wants to set the access model to use for the node and optionally set the
* list of roster groups (i.e. contacts present in the node owner roster in the
* specified groups are allowed to access the node) allowed to access the node then
* instead of including a data form the owner can just specify the "access" attribute
* of the configure element and optionally include a list of group elements. In this case,
* the method will create a data form including the specified data. This is a nice way
* to accept both ways to configure a node but always returning a data form.
*
* @param configureElement the configure element sent by the owner.
* @return the data form included in the configure element sent by the node owner or
* <tt>null</tt> if none was included or access model was defined.
*/
private DataForm getSentConfigurationForm(Element configureElement) {
DataForm completedForm = null;
FormField formField = null;
Element formElement = configureElement.element(QName.get("x", "jabber:x:data"));
if (formElement != null) {
completedForm = new DataForm(formElement);
}
String accessModel = configureElement.attributeValue("access");
if (accessModel != null) {
if (completedForm == null) {
// Create a form (i.e. simulate that the user sent a form with roster groups)
completedForm = new DataForm(DataForm.Type.submit);
// Add the hidden field indicating that this is a node config form
formField = completedForm.addField();
formField.setVariable("FORM_TYPE");
formField.setType(FormField.Type.hidden);
formField.addValue("http://jabber.org/protocol/pubsub#node_config");
}
if (completedForm.getField("pubsub#access_model") == null) {
// Add the field that will specify the access model of the node
formField = completedForm.addField();
formField.setVariable("pubsub#access_model");
formField.addValue(accessModel);
}
else {
Log.debug("Owner sent access model in data form and as attribute: " +
configureElement.asXML());
}
// Check if a list of groups was specified
List groups = configureElement.elements("group");
if (!groups.isEmpty()) {
// Add the field that will contain the specified groups
formField = completedForm.addField();
formField.setVariable("pubsub#roster_groups_allowed");
// Add each group as a value of the groups field
for (Iterator it = groups.iterator(); it.hasNext();) {
formField.addValue(((Element) it.next()).getTextTrim());
}
}
}
return completedForm;
}
}
/**
* $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.
*/
package org.jivesoftware.wildfire.pubsub;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.jivesoftware.util.JiveGlobals;
import org.jivesoftware.util.LocaleUtils;
import org.jivesoftware.util.Log;
import org.jivesoftware.util.StringUtils;
import org.jivesoftware.wildfire.PacketRouter;
import org.jivesoftware.wildfire.RoutableChannelHandler;
import org.jivesoftware.wildfire.RoutingTable;
import org.jivesoftware.wildfire.XMPPServer;
import org.jivesoftware.wildfire.auth.UnauthorizedException;
import org.jivesoftware.wildfire.container.BasicModule;
import org.jivesoftware.wildfire.disco.DiscoInfoProvider;
import org.jivesoftware.wildfire.disco.DiscoItemsProvider;
import org.jivesoftware.wildfire.disco.DiscoServerItem;
import org.jivesoftware.wildfire.disco.ServerItemsProvider;
import org.jivesoftware.wildfire.forms.DataForm;
import org.jivesoftware.wildfire.forms.FormField;
import org.jivesoftware.wildfire.forms.spi.XDataFormImpl;
import org.jivesoftware.wildfire.forms.spi.XFormFieldImpl;
import org.jivesoftware.wildfire.pubsub.models.AccessModel;
import org.jivesoftware.wildfire.pubsub.models.PublisherModel;
import org.xmpp.packet.*;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* Module that implements JEP-60: Publish-Subscribe. By default node collections and
* instant nodes are supported.
*
* @author Matt Tucker
*/
public class PubSubModule extends BasicModule implements ServerItemsProvider, DiscoInfoProvider,
DiscoItemsProvider, RoutableChannelHandler, PubSubService {
/**
* the chat service's hostname
*/
private String serviceName = null;
/**
* Collection node that acts as the root node of the entire node hierarchy.
*/
private CollectionNode rootCollectionNode = null;
/**
* Nodes managed by this manager, table: key nodeID (String); value Node
*/
private Map<String, Node> nodes = new ConcurrentHashMap<String, Node>();
/**
* Returns the permission policy for creating nodes. A true value means that not anyone can
* create a node, only the JIDs listed in <code>allowedToCreate</code> are allowed to create
* nodes.
*/
private boolean nodeCreationRestricted = false;
/**
* Bare jids of users that are allowed to create nodes. An empty list means that anyone can
* create nodes.
*/
private Collection<String> allowedToCreate = new CopyOnWriteArrayList<String>();
/**
* Bare jids of users that are system administrators of the PubSub service. A sysadmin
* has the same permissions as a node owner.
*/
private Collection<String> sysadmins = new CopyOnWriteArrayList<String>();
/**
* The packet router for the server.
*/
private PacketRouter router = null;
private RoutingTable routingTable = null;
/**
* Default configuration to use for newly created leaf nodes.
*/
private DefaultNodeConfiguration leafDefaultConfiguration;
/**
* Default configuration to use for newly created collection nodes.
*/
private DefaultNodeConfiguration collectionDefaultConfiguration;
/**
* Private component that actually performs the pubsub work.
*/
private PubSubEngine engine = null;
/**
* Keep a registry of the presence's show value of users that subscribed to a node of
* the pubsub service. The Map will have a value only for only users.
*/
private Map<JID, String> presences = new ConcurrentHashMap<JID, String>();
public PubSubModule() {
super("Publish Subscribe Service");
}
public void process(Packet packet) {
// TODO Remove this method when moving PubSub as a component and removing module code
// The MUC service will receive all the packets whose domain matches the domain of the MUC
// service. This means that, for instance, a disco request should be responded by the
// service itself instead of relying on the server to handle the request.
try {
// Check if the packet is a disco request or a packet with namespace iq:register
if (packet instanceof IQ) {
if (!engine.process((IQ) packet)) {
process((IQ) packet);
}
}
else if (packet instanceof Presence) {
engine.process((Presence) packet);
}
else {
engine.process((Message) packet);
}
}
catch (Exception e) {
Log.error(LocaleUtils.getLocalizedString("admin.error"), e);
if (packet instanceof IQ) {
// Send internal server error
IQ reply = IQ.createResultIQ((IQ) packet);
reply.setError(PacketError.Condition.internal_server_error);
send(reply);
}
}
}
private void process(IQ iq) {
// Ignore IQs of type ERROR
if (IQ.Type.error == iq.getType()) {
return;
}
Element childElement = iq.getChildElement();
String namespace = null;
if (childElement != null) {
namespace = childElement.getNamespaceURI();
}
if ("http://jabber.org/protocol/disco#info".equals(namespace)) {
try {
// TODO PubSub should have an IQDiscoInfoHandler of its own when PubSub becomes
// a component
IQ reply = XMPPServer.getInstance().getIQDiscoInfoHandler().handleIQ(iq);
router.route(reply);
}
catch (UnauthorizedException e) {
// Do nothing. This error should never happen
}
}
else if ("http://jabber.org/protocol/disco#items".equals(namespace)) {
try {
// TODO PubSub should have an IQDiscoItemsHandler of its own when PubSub becomes
// a component
IQ reply = XMPPServer.getInstance().getIQDiscoItemsHandler().handleIQ(iq);
router.route(reply);
}
catch (UnauthorizedException e) {
// Do nothing. This error should never happen
}
}
else {
// TODO Handle unknown namespace
}
}
public String getServiceID() {
return "pubsub";
}
public boolean canCreateNode(JID creator) {
// Node creation is always allowed for sysadmin
if (isNodeCreationRestricted() && !isServiceAdmin(creator)) {
// The user is not allowed to create nodes
return false;
}
// TODO Check that the user is not an anonymous user
return true;
}
public boolean isServiceAdmin(JID user) {
return sysadmins.contains(user.toBareJID()) || allowedToCreate.contains(user.toBareJID());
}
public boolean isInstantNodeSupported() {
return true;
}
public boolean isCollectionNodesSupported() {
return true;
}
public CollectionNode getRootCollectionNode() {
return rootCollectionNode;
}
public DefaultNodeConfiguration getDefaultNodeConfiguration(boolean leafType) {
if (leafType) {
return leafDefaultConfiguration;
}
return collectionDefaultConfiguration;
}
public String getShowPresence(JID subscriber) {
// TODO Implement subscribe to presence and update registry of show values.
// TODO Remove presence subscription when user removed his subscription or the node was deleted.
return presences.get(subscriber);
}
public String getServiceName() {
return serviceName;
}
public String getServiceDomain() {
return serviceName + "." + XMPPServer.getInstance().getServerInfo().getName();
}
public JID getAddress() {
// TODO Cache this JID for performance?
return new JID(null, getServiceDomain(), null);
}
public Collection<String> getUsersAllowedToCreate() {
return allowedToCreate;
}
public Collection<String> getSysadmins() {
return sysadmins;
}
public void addSysadmin(String userJID) {
sysadmins.add(userJID.trim().toLowerCase());
// Update the config.
String[] jids = new String[sysadmins.size()];
jids = (String[])sysadmins.toArray(jids);
JiveGlobals.setProperty("xmpp.pubsub.sysadmin.jid", fromArray(jids));
}
public void removeSysadmin(String userJID) {
sysadmins.remove(userJID.trim().toLowerCase());
// Update the config.
String[] jids = new String[sysadmins.size()];
jids = (String[])sysadmins.toArray(jids);
JiveGlobals.setProperty("xmpp.pubsub.sysadmin.jid", fromArray(jids));
}
public boolean isNodeCreationRestricted() {
return nodeCreationRestricted;
}
public void setNodeCreationRestricted(boolean nodeCreationRestricted) {
this.nodeCreationRestricted = nodeCreationRestricted;
JiveGlobals.setProperty("xmpp.pubsub.create.anyone", Boolean.toString(nodeCreationRestricted));
}
public void addUserAllowedToCreate(String userJID) {
// Update the list of allowed JIDs to create nodes.
allowedToCreate.add(userJID.trim().toLowerCase());
// Update the config.
String[] jids = new String[allowedToCreate.size()];
jids = (String[])allowedToCreate.toArray(jids);
JiveGlobals.setProperty("xmpp.pubsub.create.jid", fromArray(jids));
}
public void removeUserAllowedToCreate(String userJID) {
// Update the list of allowed JIDs to create nodes.
allowedToCreate.remove(userJID.trim().toLowerCase());
// Update the config.
String[] jids = new String[allowedToCreate.size()];
jids = (String[])allowedToCreate.toArray(jids);
JiveGlobals.setProperty("xmpp.pubsub.create.jid", fromArray(jids));
}
public void initialize(XMPPServer server) {
super.initialize(server);
serviceName = JiveGlobals.getProperty("xmpp.pubsub.service");
if (serviceName == null) {
serviceName = "pubsub";
}
// Load the list of JIDs that are sysadmins of the PubSub service
String property = JiveGlobals.getProperty("xmpp.pubsub.sysadmin.jid");
String[] jids;
if (property != null) {
jids = property.split(",");
for (int i = 0; i < jids.length; i++) {
sysadmins.add(jids[i].trim().toLowerCase());
}
}
nodeCreationRestricted =
Boolean.parseBoolean(JiveGlobals.getProperty("xmpp.pubsub.create.anyone", "false"));
// Load the list of JIDs that are allowed to create nodes
property = JiveGlobals.getProperty("xmpp.pubsub.create.jid");
if (property != null) {
jids = property.split(",");
for (int i = 0; i < jids.length; i++) {
allowedToCreate.add(jids[i].trim().toLowerCase());
}
}
routingTable = server.getRoutingTable();
router = server.getPacketRouter();
engine = new PubSubEngine(this, server.getPacketRouter());
// Load default configuration for leaf nodes
leafDefaultConfiguration = PubSubPersistenceManager.loadDefaultConfiguration(this, true);
if (leafDefaultConfiguration == null) {
// Create and save default configuration for leaf nodes;
leafDefaultConfiguration = new DefaultNodeConfiguration(true);
leafDefaultConfiguration.setAccessModel(AccessModel.open);
leafDefaultConfiguration.setPublisherModel(PublisherModel.publishers);
leafDefaultConfiguration.setDeliverPayloads(
JiveGlobals.getBooleanProperty("xmpp.pubsub.default.deliverPayloads", false));
leafDefaultConfiguration.setLanguage(
JiveGlobals.getProperty("xmpp.pubsub.default.language", "English"));
leafDefaultConfiguration.setMaxPayloadSize(
JiveGlobals.getIntProperty("xmpp.pubsub.default.language", 5120));
leafDefaultConfiguration.setNotifyConfigChanges(JiveGlobals.getBooleanProperty(
"xmpp.pubsub.default.notify.configChanges", true));
leafDefaultConfiguration.setNotifyDelete(
JiveGlobals.getBooleanProperty("xmpp.pubsub.default.notify.delete", true));
leafDefaultConfiguration.setNotifyRetract(
JiveGlobals.getBooleanProperty("xmpp.pubsub.default.notify.retract", true));
leafDefaultConfiguration.setPersistPublishedItems(
JiveGlobals.getBooleanProperty("xmpp.pubsub.default.persistItems", false));
leafDefaultConfiguration.setMaxPublishedItems(
JiveGlobals.getIntProperty("xmpp.pubsub.default.maxPublishedItems", -1));
leafDefaultConfiguration.setPresenceBasedDelivery(JiveGlobals.getBooleanProperty(
"xmpp.pubsub.default.presenceBasedDelivery", false));
leafDefaultConfiguration.setSendItemSubscribe(
JiveGlobals.getBooleanProperty("xmpp.pubsub.default.sendItemSubscribe", true));
leafDefaultConfiguration.setSubscriptionEnabled(JiveGlobals.getBooleanProperty(
"xmpp.pubsub.default.subscriptionEnabled", true));
leafDefaultConfiguration.setReplyPolicy(null);
PubSubPersistenceManager.createDefaultConfiguration(this, leafDefaultConfiguration);
}
// Load default configuration for collection nodes
collectionDefaultConfiguration =
PubSubPersistenceManager.loadDefaultConfiguration(this, false);
if (collectionDefaultConfiguration == null ) {
// Create and save default configuration for collection nodes;
collectionDefaultConfiguration = new DefaultNodeConfiguration(false);
collectionDefaultConfiguration.setAccessModel(AccessModel.open);
collectionDefaultConfiguration.setPublisherModel(PublisherModel.publishers);
collectionDefaultConfiguration.setDeliverPayloads(
JiveGlobals.getBooleanProperty("xmpp.pubsub.default.deliverPayloads", false));
collectionDefaultConfiguration.setLanguage(
JiveGlobals.getProperty("xmpp.pubsub.default.language", "English"));
collectionDefaultConfiguration.setNotifyConfigChanges(JiveGlobals.getBooleanProperty(
"xmpp.pubsub.default.notify.configChanges", true));
collectionDefaultConfiguration.setNotifyDelete(
JiveGlobals.getBooleanProperty("xmpp.pubsub.default.notify.delete", true));
collectionDefaultConfiguration.setNotifyRetract(
JiveGlobals.getBooleanProperty("xmpp.pubsub.default.notify.retract", true));
collectionDefaultConfiguration.setPresenceBasedDelivery(JiveGlobals.getBooleanProperty(
"xmpp.pubsub.default.presenceBasedDelivery", false));
collectionDefaultConfiguration.setSendItemSubscribe(
JiveGlobals.getBooleanProperty("xmpp.pubsub.default.sendItemSubscribe", false));
collectionDefaultConfiguration.setSubscriptionEnabled(JiveGlobals.getBooleanProperty(
"xmpp.pubsub.default.subscriptionEnabled", true));
leafDefaultConfiguration.setReplyPolicy(null);
leafDefaultConfiguration
.setAssociationPolicy(CollectionNode.LeafNodeAssociationPolicy.all);
leafDefaultConfiguration.setMaxLeafNodes(
JiveGlobals.getIntProperty("xmpp.pubsub.default.maxLeafNodes", -1));
PubSubPersistenceManager
.createDefaultConfiguration(this, collectionDefaultConfiguration);
}
// Load nodes to memory
PubSubPersistenceManager.loadNodes(this);
// Ensure that we have a root collection node
String rootNodeID = JiveGlobals.getProperty("xmpp.pubsub.root.nodeID", "");
if (nodes.isEmpty()) {
// Create root collection node
String creator = JiveGlobals.getProperty("xmpp.pubsub.root.creator");
JID creatorJID = creator != null ? new JID(creator) : server.getAdmins().iterator().next();
rootCollectionNode = new CollectionNode(this, null, rootNodeID, creatorJID);
// Add the creator as the node owner
rootCollectionNode.addOwner(creatorJID);
// Save new root node
rootCollectionNode.saveToDB();
// Add the new root node to the list of available nodes
addNode(rootCollectionNode);
}
else {
rootCollectionNode = (CollectionNode) getNode(rootNodeID);
}
}
public void start() {
super.start();
// Add the route to this service
routingTable.addRoute(getAddress(), this);
ArrayList<String> params = new ArrayList<String>();
params.clear();
params.add(getServiceDomain());
Log.info(LocaleUtils.getLocalizedString("startup.starting.pubsub", params));
}
public void stop() {
super.stop();
// Remove the route to this service
routingTable.removeRoute(getAddress());
// TODO this
//savePublishedItems();
}
public Iterator<DiscoServerItem> getItems() {
ArrayList<DiscoServerItem> items = new ArrayList<DiscoServerItem>();
items.add(new DiscoServerItem() {
public String getJID() {
return getServiceDomain();
}
public String getName() {
return "Publish-Subscribe service";
}
public String getAction() {
return null;
}
public String getNode() {
return null;
}
public DiscoInfoProvider getDiscoInfoProvider() {
return PubSubModule.this;
}
public DiscoItemsProvider getDiscoItemsProvider() {
return PubSubModule.this;
}
});
return items.iterator();
}
public Iterator<Element> getIdentities(String name, String node, JID senderJID) {
ArrayList<Element> identities = new ArrayList<Element>();
if (name == null && node == null) {
// Answer the identity of the PubSub service
Element identity = DocumentHelper.createElement("identity");
identity.addAttribute("category", "pubsub");
identity.addAttribute("name", "Publish-Subscribe service");
identity.addAttribute("type", "generic");
identities.add(identity);
}
else if (name == null && node != null) {
// Answer the identity of a given node
Node pubNode = getNode(node);
if (node != null && canDiscoverNode(pubNode)) {
Element identity = DocumentHelper.createElement("identity");
identity.addAttribute("category", "pubsub");
identity.addAttribute("type", pubNode.isCollectionNode() ? "collection" : "leaf");
identities.add(identity);
}
}
return identities.iterator();
}
public Iterator<String> getFeatures(String name, String node, JID senderJID) {
ArrayList<String> features = new ArrayList<String>();
if (name == null && node == null) {
// Answer the features of the PubSub service
features.add("http://jabber.org/protocol/pubsub");
// Collection nodes are supported
features.add("http://jabber.org/protocol/pubsub#collections");
// Configuration of node options is supported
features.add("http://jabber.org/protocol/pubsub#config-node");
// Configuration of node options is supported
features.add("http://jabber.org/protocol/pubsub#config-node");
// Creation of nodes is supported
features.add("http://jabber.org/protocol/pubsub#create-nodes");
// Any publisher (not only the originating publisher) may delete an item
features.add("http://jabber.org/protocol/pubsub#delete-any");
// Deletion of nodes is supported
features.add("http://jabber.org/protocol/pubsub#delete-nodes");
// Creation of instant nodes is supported
features.add("http://jabber.org/protocol/pubsub#instant-nodes");
// Publishers may specify item identifiers
features.add("http://jabber.org/protocol/pubsub#item-ids");
// Time-based subscriptions are supported
//features.add("http://jabber.org/protocol/pubsub#leased-subscription");
// Node meta-data is supported
features.add("http://jabber.org/protocol/pubsub#meta-data");
// A single entity may subscribe to a node multiple times
features.add("http://jabber.org/protocol/pubsub#multi-subscribe");
// The outcast affiliation is supported
features.add("http://jabber.org/protocol/pubsub#outcast-affiliation");
// Persistent items are supported
features.add("http://jabber.org/protocol/pubsub#persistent-items");
// Presence-based delivery of event notifications is supported
features.add("http://jabber.org/protocol/pubsub#presence-notifications");
// The publisher affiliation is supported
features.add("http://jabber.org/protocol/pubsub#publisher-affiliation");
// Purging of nodes is supported
features.add("http://jabber.org/protocol/pubsub#purge-nodes");
// Item retraction is supported
features.add("http://jabber.org/protocol/pubsub#retract-items");
// Retrieval of current affiliations is supported
features.add("http://jabber.org/protocol/pubsub#retrieve-affiliations");
// Item retrieval is supported
features.add("http://jabber.org/protocol/pubsub#retrieve-items");
// Subscribing and unsubscribing are supported
features.add("http://jabber.org/protocol/pubsub#subscribe");
// Configuration of subscription options is supported
features.add("http://jabber.org/protocol/pubsub#subscription-options");
}
else if (name == null && node != null) {
// Answer the features of a given node
// TODO lock the node while gathering this info???
Node pubNode = getNode(node);
if (node != null && canDiscoverNode(pubNode)) {
features.add("http://jabber.org/protocol/pubsub");
}
}
return features.iterator();
}
public XDataFormImpl getExtendedInfo(String name, String node, JID senderJID) {
if (name == null && node != null) {
// Answer the extended info of a given node
// TODO lock the node while gathering this info???
Node pubNode = getNode(node);
if (node != null && canDiscoverNode(pubNode)) {
XDataFormImpl dataForm = new XDataFormImpl(DataForm.TYPE_RESULT);
XFormFieldImpl field = new XFormFieldImpl("FORM_TYPE");
field.setType(FormField.TYPE_HIDDEN);
field.addValue("http://jabber.org/protocol/muc#roominfo");
dataForm.addField(field);
field = new XFormFieldImpl("muc#roominfo_description");
field.setLabel(LocaleUtils.getLocalizedString("muc.extended.info.desc"));
//field.addValue(room.getDescription());
dataForm.addField(field);
field = new XFormFieldImpl("muc#roominfo_subject");
field.setLabel(LocaleUtils.getLocalizedString("muc.extended.info.subject"));
//field.addValue(room.getSubject());
dataForm.addField(field);
field = new XFormFieldImpl("muc#roominfo_occupants");
field.setLabel(LocaleUtils.getLocalizedString("muc.extended.info.occupants"));
//field.addValue(Integer.toString(room.getOccupantsCount()));
dataForm.addField(field);
field = new XFormFieldImpl("x-muc#roominfo_creationdate");
field.setLabel(LocaleUtils.getLocalizedString("muc.extended.info.creationdate"));
//field.addValue(dateFormatter.format(room.getCreationDate()));
dataForm.addField(field);
return dataForm;
}
}
return null;
}
public boolean hasInfo(String name, String node, JID senderJID) {
if (name == null && node == node) {
// We always have info about the MUC service
return true;
}
else if (name == null && node != null) {
// We only have info if the node exists
return hasNode(node);
}
return false;
}
public Iterator<Element> getItems(String name, String node, JID senderJID) {
List<Element> answer = new ArrayList<Element>();
if (name == null && node == null) {
Element item;
// Answer all public nodes as items
for (Node pubNode : nodes.values()) {
if (canDiscoverNode(pubNode)) {
item = DocumentHelper.createElement("item");
item.addAttribute("jid", getServiceDomain());
item.addAttribute("node", pubNode.getNodeID());
item.addAttribute("name", pubNode.getName());
answer.add(item);
}
}
}
else if (name == null && node != null) {
Node pubNode = getNode(node);
if (pubNode != null && canDiscoverNode(pubNode)) {
if (pubNode.isCollectionNode()) {
Element item;
// Answer all nested nodes as items
for (Node nestedNode : pubNode.getNodes()) {
if (canDiscoverNode(nestedNode)) {
item = DocumentHelper.createElement("item");
item.addAttribute("jid", getServiceDomain());
item.addAttribute("node", nestedNode.getNodeID());
item.addAttribute("name", nestedNode.getName());
answer.add(item);
}
}
}
else {
// This is a leaf node so answer the published items which exist on the service
Element item;
for (PublishedItem publishedItem : ((LeafNode) pubNode).getPublishedItems()) {
item = DocumentHelper.createElement("item");
item.addAttribute("jid", getServiceDomain());
item.addAttribute("name", publishedItem.getID());
answer.add(item);
}
}
}
}
return answer.iterator();
}
public void broadcast(Node node, Message message, Collection<JID> jids) {
// TODO Possibly use a thread pool for sending packets (based on the jids size)
message.setFrom(getAddress());
for (JID jid : jids) {
message.setTo(jid);
message.setID(
node.getNodeID() + "__" + jid.toBareJID() + "__" + StringUtils.randomString(5));
router.route(message);
}
}
public void send(Packet packet) {
router.route(packet);
}
public void sendNotification(Node node, Message message, JID jid) {
message.setFrom(getAddress());
message.setTo(jid);
message.setID(
node.getNodeID() + "__" + jid.toBareJID() + "__" + StringUtils.randomString(5));
router.route(message);
}
public Node getNode(String nodeID) {
return nodes.get(nodeID);
}
public Collection<Node> getNodes() {
return nodes.values();
}
private boolean hasNode(String nodeID) {
return getNode(nodeID) != null;
}
public void addNode(Node node) {
nodes.put(node.getNodeID(), node);
}
public void removeNode(String nodeID) {
nodes.remove(nodeID);
}
private boolean canDiscoverNode(Node pubNode) {
return false; //TODO this
}
/**
* Converts an array to a comma-delimitted String.
*
* @param array the array.
* @return a comma delimtted String of the array values.
*/
private static String fromArray(String [] array) {
StringBuilder buf = new StringBuilder();
for (int i=0; i<array.length; i++) {
buf.append(array[i]);
if (i != array.length-1) {
buf.append(",");
}
}
return buf.toString();
}
}
/**
* $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.
*/
package org.jivesoftware.wildfire.pubsub;
import org.dom4j.io.SAXReader;
import org.jivesoftware.database.DbConnectionManager;
import org.jivesoftware.util.Log;
import org.jivesoftware.util.StringUtils;
import org.jivesoftware.wildfire.pubsub.models.AccessModel;
import org.jivesoftware.wildfire.pubsub.models.PublisherModel;
import org.xmpp.packet.JID;
import java.io.StringReader;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.*;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
/**
* A manager responsible for ensuring node persistence.
*
* @author Matt Tucker
*/
public class PubSubPersistenceManager {
private static final String LOAD_NODES =
"SELECT nodeID, leaf, creationDate, modificationDate, parent, deliverPayloads, " +
"maxPayloadSize, persistItems, maxItems, notifyConfigChanges, notifyDelete, " +
"notifyRetract, presenceBased, sendItemSubscribe, publisherModel, " +
"subscriptionEnabled, configSubscription, contacts, rosterGroups, accessModel, " +
"payloadType, bodyXSLT, dataformXSLT, creator, description, language, name, " +
"replyPolicy, replyRooms, replyTo, associationPolicy, associationTrusted, " +
"maxLeafNodes FROM pubsubNode WHERE serviceID=? ORDER BY nodeID";
private static final String UPDATE_NODE =
"UPDATE pubsubNode SET modificationDate=?, parent=?, deliverPayloads=?, " +
"maxPayloadSize=?, persistItems=?, maxItems=?, " +
"notifyConfigChanges=?, notifyDelete=?, notifyRetract=?, presenceBased=?, " +
"sendItemSubscribe=?, publisherModel=?, subscriptionEnabled=?, configSubscription=?, " +
"contacts=?, rosterGroups=?, accessModel=?, payloadType=?, bodyXSLT=?, " +
"dataformXSLT=?, description=?, language=?, name=?, replyPolicy=?, replyRooms=?, " +
"replyTo=?, associationPolicy=?, associationTrusted=?, maxLeafNodes=? " +
"WHERE serviceID=? AND nodeID=?";
private static final String ADD_NODE =
"INSERT INTO pubsubNode (serviceID, nodeID, leaf, creationDate, modificationDate, " +
"parent, deliverPayloads, maxPayloadSize, persistItems, maxItems, " +
"notifyConfigChanges, notifyDelete, notifyRetract, presenceBased, " +
"sendItemSubscribe, publisherModel, subscriptionEnabled, configSubscription, " +
"accessModel, contacts, rosterGroups, payloadType, bodyXSLT, dataformXSLT, " +
"creator, description, language, name, replyPolicy, replyRooms, replyTo, " +
"associationPolicy, associationTrusted, maxLeafNodes) " +
"VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)";
private static final String DELETE_NODE =
"DELETE FROM pubsubNode WHERE serviceID=? AND nodeID=?";
private static final String LOAD_AFFILIATIONS =
"SELECT nodeID,jid,affiliation FROM pubsubAffiliation WHERE serviceID=? " +
"ORDER BY nodeID";
private static final String ADD_AFFILIATION =
"INSERT INTO pubsubAffiliation (serviceID,nodeID,jid,affiliation) VALUES (?,?,?,?)";
private static final String UPDATE_AFFILIATION =
"UPDATE pubsubAffiliation SET affiliation=? WHERE serviceID=? AND nodeID=? AND jid=?";
private static final String DELETE_AFFILIATION =
"DELETE FROM pubsubAffiliation WHERE serviceID=? AND nodeID=? AND jid=?";
private static final String DELETE_AFFILIATIONS =
"DELETE FROM pubsubAffiliation WHERE serviceID=? AND nodeID=?";
private static final String LOAD_SUBSCRIPTIONS =
"SELECT nodeID, id, jid, owner, state, confPending, deliver, digest, " +
"digest_frequency, expire, includeBody, showValues, subscriptionType, " +
"subscriptionDepth, keyword FROM pubsubSubscription WHERE serviceID=? ORDER BY nodeID";
private static final String ADD_SUBSCRIPTION =
"INSERT INTO pubsubSubscription (serviceID, nodeID, id, jid, owner, state, " +
"confPending, deliver, digest, digest_frequency, expire, includeBody, showValues, " +
"subscriptionType, subscriptionDepth, keyword) " +
"VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)";
private static final String UPDATE_SUBSCRIPTION =
"UPDATE pubsubSubscription SET owner=?, state=?, confPending=? deliver=?, digest=?, " +
"digest_frequency=?, expire=?, includeBody=?, showValues=?, subscriptionType=?, " +
"subscriptionDepth=?, keyword=? WHERE serviceID=? AND nodeID=? AND id=?";
private static final String DELETE_SUBSCRIPTION =
"DELETE FROM pubsubSubscription WHERE serviceID=? AND nodeID=? AND id=?";
private static final String DELETE_SUBSCRIPTIONS =
"DELETE FROM pubsubSubscription WHERE serviceID=? AND nodeID=?";
private static final String LOAD_ITEMS =
"SELECT id,jid,creationDate,payload FROM pubsubItem " +
"WHERE serviceID=? AND nodeID=? ORDER BY creationDate";
private static final String ADD_ITEM =
"INSERT INTO pubsubItem (serviceID,nodeID,id,jid,creationDate,payload) " +
"VALUES (?,?,?,?,?,?)";
private static final String DELETE_ITEM =
"DELETE FROM pubsubItem WHERE serviceID=? AND nodeID=? AND id=?";
private static final String DELETE_ITEMS =
"DELETE FROM pubsubItem WHERE serviceID=? AND nodeID=?";
private static final String LOAD_DEFAULT_CONF =
"SELECT deliverPayloads, maxPayloadSize, persistItems, maxItems, " +
"notifyConfigChanges, notifyDelete, notifyRetract, presenceBased, " +
"sendItemSubscribe, publisherModel, subscriptionEnabled, accessModel, language, " +
"replyPolicy, associationPolicy, maxLeafNodes " +
"FROM pubsubDefaultConf WHERE serviceID=? AND leaf=?";
private static final String UPDATE_DEFAULT_CONF =
"UPDATE pubsubDefaultConf SET deliverPayloads=?, maxPayloadSize=?, persistItems=?, " +
"maxItems=?, notifyConfigChanges=?, notifyDelete=?, notifyRetract=?, " +
"presenceBased=?, sendItemSubscribe=?, publisherModel=?, subscriptionEnabled=?, " +
"accessModel=?, language=? replyPolicy=?, associationPolicy=?, maxLeafNodes=? " +
"WHERE serviceID=? AND leaf=?";
private static final String ADD_DEFAULT_CONF =
"INSERT INTO pubsubDefaultConf (serviceID, leaf, deliverPayloads, maxPayloadSize, " +
"persistItems, maxItems, notifyConfigChanges, notifyDelete, notifyRetract, " +
"presenceBased, sendItemSubscribe, publisherModel, subscriptionEnabled, " +
"accessModel, language, replyPolicy, associationPolicy, maxLeafNodes) " +
"VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)";
/**
* Pool of SAX Readers. SAXReader is not thread safe so we need to have a pool of readers.
*/
private static BlockingQueue<SAXReader> xmlReaders = new LinkedBlockingQueue<SAXReader>();
static {
// Initialize the pool of sax readers
for (int i=0; i<50; i++) {
xmlReaders.add(new SAXReader());
}
}
/**
* Creates and stores the node configuration in the database.
*
* @param service The pubsub service that is hosting the node.
* @param node The newly created node.
*/
public static void createNode(PubSubService service, Node node) {
Connection con = null;
PreparedStatement pstmt = null;
try {
con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement(ADD_NODE);
pstmt.setString(1, service.getServiceID());
pstmt.setString(2, node.getNodeID());
pstmt.setInt(3, (node.isCollectionNode() ? 0 : 1));
pstmt.setString(4, StringUtils.dateToMillis(node.getCreationDate()));
pstmt.setString(5, StringUtils.dateToMillis(node.getModificationDate()));
pstmt.setString(6, node.getParent() != null ? node.getParent().getNodeID() : null);
pstmt.setInt(7, (node.isDeliverPayloads() ? 1 : 0));
if (!node.isCollectionNode()) {
pstmt.setInt(8, ((LeafNode) node).getMaxPayloadSize());
pstmt.setInt(9, (((LeafNode) node).isPersistPublishedItems() ? 1 : 0));
pstmt.setInt(10, ((LeafNode) node).getMaxPublishedItems());
}
else {
pstmt.setInt(8, 0);
pstmt.setInt(9, 0);
pstmt.setInt(10, 0);
}
pstmt.setInt(11, (node.isNotifyConfigChanges() ? 1 : 0));
pstmt.setInt(12, (node.isNotifyDelete() ? 1 : 0));
pstmt.setInt(13, (node.isNotifyRetract() ? 1 : 0));
pstmt.setInt(14, (node.isPresenceBasedDelivery() ? 1 : 0));
pstmt.setInt(15, (node.isSendItemSubscribe() ? 1 : 0));
pstmt.setString(16, node.getPublisherModel().getName());
pstmt.setInt(17, (node.isSubscriptionEnabled() ? 1 : 0));
pstmt.setInt(18, (node.isSubscriptionConfigurationRequired() ? 1 : 0));
pstmt.setString(19, node.getAccessModel().getName());
pstmt.setString(20, encodeJIDs(node.getContacts()));
pstmt.setString(21, encodeGroups(node.getRosterGroupsAllowed()));
pstmt.setString(22, node.getPayloadType());
pstmt.setString(23, node.getBodyXSLT());
pstmt.setString(24, node.getDataformXSLT());
pstmt.setString(25, node.getCreator().toString());
pstmt.setString(26, node.getDescription());
pstmt.setString(27, node.getLanguage());
pstmt.setString(28, node.getName());
if (node.getReplyPolicy() != null) {
pstmt.setString(29, node.getReplyPolicy().name());
}
else {
pstmt.setString(29, null);
}
pstmt.setString(30, encodeJIDs(node.getReplyRooms()));
pstmt.setString(31, encodeJIDs(node.getReplyTo()));
if (node.isCollectionNode()) {
pstmt.setString(32, ((CollectionNode)node).getAssociationPolicy().name());
pstmt.setString(33, encodeJIDs(((CollectionNode)node).getAssociationTrusted()));
pstmt.setInt(34, ((CollectionNode)node).getMaxLeafNodes());
}
else {
pstmt.setString(32, null);
pstmt.setString(33, null);
pstmt.setInt(34, 0);
}
pstmt.executeUpdate();
}
catch (SQLException sqle) {
Log.error(sqle);
}
finally {
try {if (pstmt != null) {pstmt.close();}}
catch (Exception e) {Log.error(e);}
try {if (con != null) {con.close();}}
catch (Exception e) {Log.error(e);}
}
}
/**
* Updates the node configuration in the database.
*
* @param service The pubsub service that is hosting the node.
* @param node The updated node.
*/
public static void updateNode(PubSubService service, Node node) {
Connection con = null;
PreparedStatement pstmt = null;
try {
con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement(UPDATE_NODE);
pstmt.setString(1, StringUtils.dateToMillis(node.getModificationDate()));
pstmt.setString(2, node.getParent() != null ? node.getParent().getNodeID() : null);
pstmt.setInt(3, (node.isDeliverPayloads() ? 1 : 0));
if (!node.isCollectionNode()) {
pstmt.setInt(4, ((LeafNode) node).getMaxPayloadSize());
pstmt.setInt(5, (((LeafNode) node).isPersistPublishedItems() ? 1 : 0));
pstmt.setInt(6, ((LeafNode) node).getMaxPublishedItems());
}
else {
pstmt.setInt(4, 0);
pstmt.setInt(5, 0);
pstmt.setInt(6, 0);
}
pstmt.setInt(7, (node.isNotifyConfigChanges() ? 1 : 0));
pstmt.setInt(8, (node.isNotifyDelete() ? 1 : 0));
pstmt.setInt(9, (node.isNotifyRetract() ? 1 : 0));
pstmt.setInt(10, (node.isPresenceBasedDelivery() ? 1 : 0));
pstmt.setInt(11, (node.isSendItemSubscribe() ? 1 : 0));
pstmt.setString(12, node.getPublisherModel().getName());
pstmt.setInt(13, (node.isSubscriptionEnabled() ? 1 : 0));
pstmt.setInt(14, (node.isSubscriptionConfigurationRequired() ? 1 : 0));
pstmt.setString(15, encodeJIDs(node.getContacts()));
pstmt.setString(16, encodeGroups(node.getRosterGroupsAllowed()));
pstmt.setString(17, node.getAccessModel().getName());
pstmt.setString(18, node.getPayloadType());
pstmt.setString(19, node.getBodyXSLT());
pstmt.setString(20, node.getDataformXSLT());
pstmt.setString(21, node.getDescription());
pstmt.setString(22, node.getLanguage());
pstmt.setString(23, node.getName());
if (node.getReplyPolicy() != null) {
pstmt.setString(24, node.getReplyPolicy().name());
}
else {
pstmt.setString(24, null);
}
pstmt.setString(25, encodeJIDs(node.getReplyRooms()));
pstmt.setString(26, encodeJIDs(node.getReplyTo()));
if (node.isCollectionNode()) {
pstmt.setString(27, ((CollectionNode) node).getAssociationPolicy().name());
pstmt.setString(28, encodeJIDs(((CollectionNode) node).getAssociationTrusted()));
pstmt.setInt(29, ((CollectionNode) node).getMaxLeafNodes());
}
else {
pstmt.setString(27, null);
pstmt.setString(28, null);
pstmt.setInt(29, 0);
}
pstmt.setString(30, service.getServiceID());
pstmt.setString(31, node.getNodeID());
pstmt.executeUpdate();
}
catch (SQLException sqle) {
Log.error(sqle);
}
finally {
try {if (pstmt != null) {pstmt.close();}}
catch (Exception e) {Log.error(e);}
try {if (con != null) {con.close();}}
catch (Exception e) {Log.error(e);}
}
}
/**
* Removes the specified node from the DB.
*
* @param service The pubsub service that is hosting the node.
* @param node The node that is being deleted.
* @return true If the operation was successful.
*/
public static boolean removeNode(PubSubService service, Node node) {
Connection con = null;
PreparedStatement pstmt = null;
boolean abortTransaction = false;
try {
con = DbConnectionManager.getTransactionConnection();
// Remove the affiliate from the table of node affiliates
pstmt = con.prepareStatement(DELETE_NODE);
pstmt.setString(1, service.getServiceID());
pstmt.setString(2, node.getNodeID());
pstmt.executeUpdate();
pstmt.close();
// Remove published items of the node being deleted
pstmt = con.prepareStatement(DELETE_ITEMS);
pstmt.setString(1, service.getServiceID());
pstmt.setString(2, node.getNodeID());
pstmt.executeUpdate();
pstmt.close();
// Remove all affiliates from the table of node affiliates
pstmt = con.prepareStatement(DELETE_AFFILIATIONS);
pstmt.setString(1, service.getServiceID());
pstmt.setString(2, node.getNodeID());
pstmt.executeUpdate();
pstmt.close();
// Remove users that were subscribed to the node
pstmt = con.prepareStatement(DELETE_SUBSCRIPTIONS);
pstmt.setString(1, service.getServiceID());
pstmt.setString(2, node.getNodeID());
pstmt.executeUpdate();
}
catch (SQLException sqle) {
Log.error(sqle);
abortTransaction = true;
}
finally {
try {if (pstmt != null) {pstmt.close();}}
catch (Exception e) {Log.error(e);}
DbConnectionManager.closeTransactionConnection(con, abortTransaction);
}
return !abortTransaction;
}
/**
* Loads all nodes from the database and adds them to the PubSub service.
*
* @param service the pubsub service that is hosting the nodes.
*/
public static void loadNodes(PubSubService service) {
Connection con = null;
PreparedStatement pstmt = null;
Map<String, Node> nodes = new HashMap<String, Node>();
try {
con = DbConnectionManager.getConnection();
// Get all nodes at once (with 1 query)
pstmt = con.prepareStatement(LOAD_NODES);
pstmt.setString(1, service.getServiceID());
ResultSet rs = pstmt.executeQuery();
// Rebuild all loaded nodes
while(rs.next()) {
loadNode(service, nodes, rs);
}
rs.close();
pstmt.close();
// Get affiliations of all nodes
pstmt = con.prepareStatement(LOAD_AFFILIATIONS);
pstmt.setString(1, service.getServiceID());
rs = pstmt.executeQuery();
// Add to each node the correspondiding affiliates
while(rs.next()) {
loadAffiliations(nodes, rs);
}
rs.close();
pstmt.close();
// Get subscriptions to all nodes
pstmt = con.prepareStatement(LOAD_SUBSCRIPTIONS);
pstmt.setString(1, service.getServiceID());
rs = pstmt.executeQuery();
// Add to each node the correspondiding subscriptions
while(rs.next()) {
loadSubscriptions(service, nodes, rs);
}
rs.close();
}
catch (SQLException sqle) {
Log.error(sqle);
}
finally {
try { if (pstmt != null) pstmt.close(); }
catch (Exception e) { Log.error(e); }
try { if (con != null) con.close(); }
catch (Exception e) { Log.error(e); }
}
for (Node node : nodes.values()) {
// Set now that the node is persistent in the database. Note: We need to
// set this now since otherwise the node's affiliations will be saved to the database
// "again" while adding them to the node!
node.setSavedToDB(true);
// Add the node to the service
service.addNode(node);
}
}
private static void loadNode(PubSubService service, Map<String, Node> loadedNodes,
ResultSet rs) {
Node node = null;
try {
String nodeID = rs.getString(1);
boolean leaf = rs.getInt(2) == 1;
String parent = rs.getString(5);
JID creator = new JID(rs.getString(24));
CollectionNode parentNode = null;
if (parent != null) {
// Check if the parent has already been loaded
parentNode = (CollectionNode) loadedNodes.get(parent);
if (parentNode == null) {
// Parent is not in memory so try to load it
Log.warn("Node not loaded due to missing parent. NodeID: " + nodeID);
return;
}
}
if (leaf) {
// Retrieving a leaf node
node = new LeafNode(service, parentNode, nodeID, creator);
}
else {
// Retrieving a collection node
node = new CollectionNode(service, parentNode, nodeID, creator);
}
node.setCreationDate(new Date(Long.parseLong(rs.getString(3).trim())));
node.setModificationDate(new Date(Long.parseLong(rs.getString(4).trim())));
node.setDeliverPayloads(rs.getInt(6) == 1);
if (leaf) {
((LeafNode) node).setMaxPayloadSize(rs.getInt(7));
((LeafNode) node).setPersistPublishedItems(rs.getInt(8) == 1);
((LeafNode) node).setMaxPublishedItems(rs.getInt(9));
}
node.setNotifyConfigChanges(rs.getInt(10) == 1);
node.setNotifyDelete(rs.getInt(11) == 1);
node.setNotifyRetract(rs.getInt(12) == 1);
node.setPresenceBasedDelivery(rs.getInt(13) == 1);
node.setSendItemSubscribe(rs.getInt(14) == 1);
node.setPublisherModel(PublisherModel.valueOf(rs.getString(15)));
node.setSubscriptionEnabled(rs.getInt(16) == 1);
node.setSubscriptionConfigurationRequired(rs.getInt(17) == 1);
node.setContacts(decodeJIDs(rs.getString(18)));
node.setRosterGroupsAllowed(decodeGroups(rs.getString(19)));
node.setAccessModel(AccessModel.valueOf(rs.getString(20)));
node.setPayloadType(rs.getString(21));
node.setBodyXSLT(rs.getString(22));
node.setDataformXSLT(rs.getString(23));
node.setDescription(rs.getString(25));
node.setLanguage(rs.getString(26));
node.setName(rs.getString(27));
if (rs.getString(28) != null) {
node.setReplyPolicy(Node.ItemReplyPolicy.valueOf(rs.getString(28)));
}
node.setReplyRooms(decodeJIDs(rs.getString(29)));
node.setReplyTo(decodeJIDs(rs.getString(30)));
if (!leaf) {
((CollectionNode) node).setAssociationPolicy(
CollectionNode.LeafNodeAssociationPolicy.valueOf(rs.getString(31)));
((CollectionNode) node).setAssociationTrusted(decodeJIDs(rs.getString(32)));
((CollectionNode) node).setMaxLeafNodes(rs.getInt(33));
}
// Add the load to the list of loaded nodes
loadedNodes.put(node.getNodeID(), node);
}
catch (SQLException sqle) {
Log.error(sqle);
}
return;
}
private static void loadAffiliations(Map<String, Node> nodes, ResultSet rs) {
try {
String nodeID = rs.getString(1);
Node node = nodes.get(nodeID);
if (node == null) {
Log.warn("Affiliations found for a non-existent node: " + nodeID);
return;
}
NodeAffiliate affiliate = new NodeAffiliate(node, new JID(rs.getString(2)));
affiliate.setAffiliation(NodeAffiliate.Affiliation.valueOf(rs.getString(3)));
node.addAffiliate(affiliate);
}
catch (SQLException sqle) {
Log.error(sqle);
}
}
private static void loadSubscriptions(PubSubService service, Map<String, Node> nodes,
ResultSet rs) {
try {
String nodeID = rs.getString(1);
Node node = nodes.get(nodeID);
if (node == null) {
Log.warn("Subscription found for a non-existent node: " + nodeID);
return;
}
String subID = rs.getString(2);
JID subscriber = new JID(rs.getString(3));
JID owner = new JID(rs.getString(4));
NodeSubscription.State state = NodeSubscription.State.valueOf(rs.getString(5));
NodeSubscription subscription =
new NodeSubscription(service, node, owner, subscriber, state, subID);
subscription.setConfigurationPending(rs.getInt(6) == 1);
subscription.setShouldDeliverNotifications(rs.getInt(7) == 1);
subscription.setUsingDigest(rs.getInt(8) == 1);
subscription.setDigestFrequency(rs.getInt(9));
if (rs.getString(10) != null) {
subscription.setExpire(new Date(Long.parseLong(rs.getString(10).trim())));
}
subscription.setIncludingBody(rs.getInt(11) == 1);
subscription.setPresenceStates(decodeWithComma(rs.getString(12)));
subscription.setType(NodeSubscription.Type.valueOf(rs.getString(13)));
subscription.setDepth(rs.getInt(14));
subscription.setKeyword(rs.getString(15));
// Indicate the subscription that is has already been saved to the database
subscription.setSavedToDB(true);
node.addSubscription(subscription);
}
catch (SQLException sqle) {
Log.error(sqle);
}
}
/**
* Update the DB with the new affiliation of the user in the node.
*
* @param service The pubsub service that is hosting the node.
* @param node The node where the affiliation of the user was updated.
* @param affiliate The new affiliation of the user in the node.
* @param create True if this is a new affiliate.
*/
public static void saveAffiliation(PubSubService service, Node node, NodeAffiliate affiliate,
boolean create) {
Connection con = null;
PreparedStatement pstmt = null;
try {
con = DbConnectionManager.getConnection();
if (create) {
// Add the user to the generic affiliations table
pstmt = con.prepareStatement(ADD_AFFILIATION);
pstmt.setString(1, service.getServiceID());
pstmt.setString(2, node.getNodeID());
pstmt.setString(3, affiliate.getJID().toString());
pstmt.setString(4, affiliate.getAffiliation().name());
pstmt.executeUpdate();
}
else {
if (NodeAffiliate.Affiliation.none == affiliate.getAffiliation()) {
// Remove the affiliate from the table of node affiliates
pstmt = con.prepareStatement(DELETE_AFFILIATION);
pstmt.setString(1, service.getServiceID());
pstmt.setString(2, node.getNodeID());
pstmt.setString(3, affiliate.getJID().toString());
pstmt.executeUpdate();
}
else {
// Update the affiliate's data in the backend store
pstmt = con.prepareStatement(UPDATE_AFFILIATION);
pstmt.setString(1, affiliate.getAffiliation().name());
pstmt.setString(2, service.getServiceID());
pstmt.setString(3, node.getNodeID());
pstmt.setString(4, affiliate.getJID().toString());
pstmt.executeUpdate();
}
}
}
catch (SQLException sqle) {
Log.error(sqle);
}
finally {
try {if (pstmt != null) {pstmt.close();}}
catch (Exception e) {Log.error(e);}
try {if (con != null) {con.close();}}
catch (Exception e) {Log.error(e);}
}
}
/**
* Removes the affiliation and subsription state of the user from the DB.
*
* @param service The pubsub service that is hosting the node.
* @param node The node where the affiliation of the user was updated.
* @param affiliate The existing affiliation and subsription state of the user in the node.
*/
public static void removeAffiliation(PubSubService service, Node node,
NodeAffiliate affiliate) {
Connection con = null;
PreparedStatement pstmt = null;
try {
con = DbConnectionManager.getConnection();
// Remove the affiliate from the table of node affiliates
pstmt = con.prepareStatement(DELETE_AFFILIATION);
pstmt.setString(1, service.getServiceID());
pstmt.setString(2, node.getNodeID());
pstmt.setString(3, affiliate.getJID().toString());
pstmt.executeUpdate();
}
catch (SQLException sqle) {
Log.error(sqle);
}
finally {
try {if (pstmt != null) {pstmt.close();}}
catch (Exception e) {Log.error(e);}
try {if (con != null) {con.close();}}
catch (Exception e) {Log.error(e);}
}
}
/**
* Updates the DB with the new subsription of the user to the node.
*
* @param service The pubsub service that is hosting the node.
* @param node The node where the user has subscribed to.
* @param subscription The new subscription of the user to the node.
* @param create True if this is a new affiliate.
*/
public static void saveSubscription(PubSubService service, Node node,
NodeSubscription subscription, boolean create) {
Connection con = null;
PreparedStatement pstmt = null;
try {
con = DbConnectionManager.getConnection();
if (create) {
// Add the subscription of the user to the database
pstmt = con.prepareStatement(ADD_SUBSCRIPTION);
pstmt.setString(1, service.getServiceID());
pstmt.setString(2, node.getNodeID());
pstmt.setString(3, subscription.getID());
pstmt.setString(4, subscription.getJID().toString());
pstmt.setString(5, subscription.getOwner().toString());
pstmt.setString(6, subscription.getState().name());
pstmt.setInt(7, (subscription.isConfigurationPending() ? 1 : 0));
pstmt.setInt(8, (subscription.shouldDeliverNotifications() ? 1 : 0));
pstmt.setInt(9, (subscription.isUsingDigest() ? 1 : 0));
pstmt.setInt(10, subscription.getDigestFrequency());
Date expireDate = subscription.getExpire();
if (expireDate == null) {
pstmt.setString(11, null);
}
else {
pstmt.setString(11, StringUtils.dateToMillis(expireDate));
}
pstmt.setInt(12, (subscription.isIncludingBody() ? 1 : 0));
pstmt.setString(13, encodeWithComma(subscription.getPresenceStates()));
pstmt.setString(14, subscription.getType().name());
pstmt.setInt(15, subscription.getDepth());
pstmt.setString(16, subscription.getKeyword());
pstmt.executeUpdate();
// Indicate the subscription that is has been saved to the database
subscription.setSavedToDB(true);
}
else {
if (NodeSubscription.State.none == subscription.getState()) {
// Remove the subscription of the user from the table
pstmt = con.prepareStatement(DELETE_SUBSCRIPTION);
pstmt.setString(1, service.getServiceID());
pstmt.setString(2, node.getNodeID());
pstmt.setString(2, subscription.getID());
pstmt.executeUpdate();
}
else {
// Update the subscription of the user in the backend store
pstmt = con.prepareStatement(UPDATE_SUBSCRIPTION);
pstmt.setString(1, subscription.getOwner().toString());
pstmt.setString(2, subscription.getState().name());
pstmt.setInt(3, (subscription.isConfigurationPending() ? 1 : 0));
pstmt.setInt(4, (subscription.shouldDeliverNotifications() ? 1 : 0));
pstmt.setInt(5, (subscription.isUsingDigest() ? 1 : 0));
pstmt.setInt(6, subscription.getDigestFrequency());
Date expireDate = subscription.getExpire();
if (expireDate == null) {
pstmt.setString(7, null);
}
else {
pstmt.setString(7, StringUtils.dateToMillis(expireDate));
}
pstmt.setInt(8, (subscription.isIncludingBody() ? 1 : 0));
pstmt.setString(9, encodeWithComma(subscription.getPresenceStates()));
pstmt.setString(10, subscription.getType().name());
pstmt.setInt(11, subscription.getDepth());
pstmt.setString(12, subscription.getKeyword());
pstmt.setString(13, service.getServiceID());
pstmt.setString(14, node.getNodeID());
pstmt.setString(15, subscription.getID());
pstmt.executeUpdate();
}
}
}
catch (SQLException sqle) {
Log.error(sqle);
}
finally {
try {if (pstmt != null) {pstmt.close();}}
catch (Exception e) {Log.error(e);}
try {if (con != null) {con.close();}}
catch (Exception e) {Log.error(e);}
}
}
/**
* Removes the subscription of the user from the DB.
*
* @param service The pubsub service that is hosting the node.
* @param node The node where the user was subscribed to.
* @param subscription The existing subsription of the user to the node.
*/
public static void removeSubscription(PubSubService service, Node node,
NodeSubscription subscription) {
Connection con = null;
PreparedStatement pstmt = null;
try {
con = DbConnectionManager.getConnection();
// Remove the affiliate from the table of node affiliates
pstmt = con.prepareStatement(DELETE_SUBSCRIPTION);
pstmt.setString(1, service.getServiceID());
pstmt.setString(2, node.getNodeID());
pstmt.setString(3, subscription.getID());
pstmt.executeUpdate();
}
catch (SQLException sqle) {
Log.error(sqle);
}
finally {
try {if (pstmt != null) {pstmt.close();}}
catch (Exception e) {Log.error(e);}
try {if (con != null) {con.close();}}
catch (Exception e) {Log.error(e);}
}
}
/**
* Loads and adds the published items to the specified node.
*
* @param service the pubsub service that is hosting the node.
* @param node the leaf node to load its published items.
*/
public static void loadItems(PubSubService service, LeafNode node) {
Connection con = null;
PreparedStatement pstmt = null;
SAXReader xmlReader = null;
try {
// Get a sax reader from the pool
xmlReader = xmlReaders.take();
con = DbConnectionManager.getConnection();
// Get published items of the specified node
pstmt = con.prepareStatement(LOAD_ITEMS);
pstmt.setString(1, service.getServiceID());
pstmt.setString(2, node.getNodeID());
ResultSet rs = pstmt.executeQuery();
// Rebuild loaded published items
while(rs.next()) {
String itemID = rs.getString(1);
JID publisher = new JID(rs.getString(2));
Date creationDate = new Date(Long.parseLong(rs.getString(3).trim()));
// Create the item
PublishedItem item = new PublishedItem(node, publisher, itemID, creationDate);
// Add the extra fields to the published item
if (rs.getString(4) != null) {
item.setPayload(
xmlReader.read(new StringReader(rs.getString(4))).getRootElement());
}
// Add the published item to the node
node.addPublishedItem(item);
}
rs.close();
}
catch (Exception sqle) {
Log.error(sqle);
}
finally {
// Return the sax reader to the pool
if (xmlReader != null) {
xmlReaders.add(xmlReader);
}
try { if (pstmt != null) pstmt.close(); }
catch (Exception e) { Log.error(e); }
try { if (con != null) con.close(); }
catch (Exception e) { Log.error(e); }
}
}
/**
* Removes the specified published item from the DB.
*
* @param service the pubsub service that is hosting the node.
* @param node The node where the published item was published.
* @param item The published item to delete.
*/
public static void removePublishedItem(PubSubService service, Node node, PublishedItem item) {
Connection con = null;
PreparedStatement pstmt = null;
try {
con = DbConnectionManager.getConnection();
// Remove the published item from the database
pstmt = con.prepareStatement(DELETE_ITEM);
pstmt.setString(1, service.getServiceID());
pstmt.setString(2, node.getNodeID());
pstmt.setString(3, item.getID());
pstmt.executeUpdate();
}
catch (SQLException sqle) {
Log.error(sqle);
}
finally {
try {if (pstmt != null) {pstmt.close();}}
catch (Exception e) {Log.error(e);}
try {if (con != null) {con.close();}}
catch (Exception e) {Log.error(e);}
}
}
/**
* Loads from the database the default node configuration for the specified node type
* and pubsub service.
*
* @param service the default node configuration used by this pubsub service.
* @param isLeafType true if loading default configuration for leaf nodes.
* @return the loaded default node configuration for the specified node type and service
* or <tt>null</tt> if none was found.
*/
public static DefaultNodeConfiguration loadDefaultConfiguration(PubSubService service,
boolean isLeafType) {
Connection con = null;
PreparedStatement pstmt = null;
DefaultNodeConfiguration config = null;
try {
con = DbConnectionManager.getConnection();
// Get default node configuration for the specified service
pstmt = con.prepareStatement(LOAD_DEFAULT_CONF);
pstmt.setString(1, service.getServiceID());
pstmt.setInt(2, (isLeafType ? 1 : 0));
ResultSet rs = pstmt.executeQuery();
if (rs.next()) {
config = new DefaultNodeConfiguration(isLeafType);
// Rebuild loaded default node configuration
config.setDeliverPayloads(rs.getInt(1) == 1);
config.setMaxPayloadSize(rs.getInt(2));
config.setPersistPublishedItems(rs.getInt(3) == 1);
config.setMaxPublishedItems(rs.getInt(4));
config.setNotifyConfigChanges(rs.getInt(5) == 1);
config.setNotifyDelete(rs.getInt(6) == 1);
config.setNotifyRetract(rs.getInt(7) == 1);
config.setPresenceBasedDelivery(rs.getInt(8) == 1);
config.setSendItemSubscribe(rs.getInt(9) == 1);
config.setPublisherModel(PublisherModel.valueOf(rs.getString(10)));
config.setSubscriptionEnabled(rs.getInt(11) == 1);
config.setAccessModel(AccessModel.valueOf(rs.getString(12)));
config.setLanguage(rs.getString(13));
if (rs.getString(14) != null) {
config.setReplyPolicy(Node.ItemReplyPolicy.valueOf(rs.getString(14)));
}
config.setAssociationPolicy(
CollectionNode.LeafNodeAssociationPolicy.valueOf(rs.getString(15)));
config.setMaxLeafNodes(rs.getInt(16));
}
rs.close();
}
catch (Exception sqle) {
Log.error(sqle);
}
finally {
try { if (pstmt != null) pstmt.close(); }
catch (Exception e) { Log.error(e); }
try { if (con != null) con.close(); }
catch (Exception e) { Log.error(e); }
}
return config;
}
/**
* Creates a new default node configuration for the specified service.
*
* @param service the default node configuration used by this pubsub service.
* @param config the default node configuration to create in the database.
*/
public static void createDefaultConfiguration(PubSubService service,
DefaultNodeConfiguration config) {
Connection con = null;
PreparedStatement pstmt = null;
try {
con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement(ADD_DEFAULT_CONF);
pstmt.setString(1, service.getServiceID());
pstmt.setInt(2, (config.isLeaf() ? 1 : 0));
pstmt.setInt(3, (config.isDeliverPayloads() ? 1 : 0));
pstmt.setInt(4, config.getMaxPayloadSize());
pstmt.setInt(5, (config.isPersistPublishedItems() ? 1 : 0));
pstmt.setInt(6, config.getMaxPublishedItems());
pstmt.setInt(7, (config.isNotifyConfigChanges() ? 1 : 0));
pstmt.setInt(8, (config.isNotifyDelete() ? 1 : 0));
pstmt.setInt(9, (config.isNotifyRetract() ? 1 : 0));
pstmt.setInt(10, (config.isPresenceBasedDelivery() ? 1 : 0));
pstmt.setInt(11, (config.isSendItemSubscribe() ? 1 : 0));
pstmt.setString(12, config.getPublisherModel().getName());
pstmt.setInt(13, (config.isSubscriptionEnabled() ? 1 : 0));
pstmt.setString(14, config.getAccessModel().getName());
pstmt.setString(15, config.getLanguage());
if (config.getReplyPolicy() != null) {
pstmt.setString(16, config.getReplyPolicy().name());
}
else {
pstmt.setString(16, null);
}
pstmt.setString(17, config.getAssociationPolicy().name());
pstmt.setInt(18, config.getMaxLeafNodes());
pstmt.executeUpdate();
}
catch (SQLException sqle) {
Log.error(sqle);
}
finally {
try {if (pstmt != null) {pstmt.close();}}
catch (Exception e) {Log.error(e);}
try {if (con != null) {con.close();}}
catch (Exception e) {Log.error(e);}
}
}
/**
* Updates the default node configuration for the specified service.
*
* @param service the default node configuration used by this pubsub service.
* @param config the default node configuration to update in the database.
*/
public static void updateDefaultConfiguration(PubSubService service,
DefaultNodeConfiguration config) {
Connection con = null;
PreparedStatement pstmt = null;
try {
con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement(UPDATE_DEFAULT_CONF);
pstmt.setInt(1, (config.isDeliverPayloads() ? 1 : 0));
pstmt.setInt(2, config.getMaxPayloadSize());
pstmt.setInt(3, (config.isPersistPublishedItems() ? 1 : 0));
pstmt.setInt(4, config.getMaxPublishedItems());
pstmt.setInt(5, (config.isNotifyConfigChanges() ? 1 : 0));
pstmt.setInt(6, (config.isNotifyDelete() ? 1 : 0));
pstmt.setInt(7, (config.isNotifyRetract() ? 1 : 0));
pstmt.setInt(8, (config.isPresenceBasedDelivery() ? 1 : 0));
pstmt.setInt(9, (config.isSendItemSubscribe() ? 1 : 0));
pstmt.setString(10, config.getPublisherModel().getName());
pstmt.setInt(11, (config.isSubscriptionEnabled() ? 1 : 0));
pstmt.setString(12, config.getAccessModel().getName());
pstmt.setString(13, config.getLanguage());
if (config.getReplyPolicy() != null) {
pstmt.setString(14, config.getReplyPolicy().name());
}
else {
pstmt.setString(14, null);
}
pstmt.setString(15, config.getAssociationPolicy().name());
pstmt.setInt(16, config.getMaxLeafNodes());
pstmt.setString(17, service.getServiceID());
pstmt.setInt(18, (config.isLeaf() ? 1 : 0));
pstmt.executeUpdate();
}
catch (SQLException sqle) {
Log.error(sqle);
}
finally {
try {if (pstmt != null) {pstmt.close();}}
catch (Exception e) {Log.error(e);}
try {if (con != null) {con.close();}}
catch (Exception e) {Log.error(e);}
}
}
/*public static Node loadNode(PubSubService service, String nodeID) {
Connection con = null;
Node node = null;
try {
con = DbConnectionManager.getConnection();
node = loadNode(service, nodeID, con);
}
catch (SQLException sqle) {
Log.error(sqle);
}
finally {
try { if (con != null) con.close(); }
catch (Exception e) { Log.error(e); }
}
return node;
}
private static Node loadNode(PubSubService service, String nodeID, Connection con) {
Node node = null;
PreparedStatement pstmt = null;
try {
pstmt = con.prepareStatement(LOAD_NODE);
pstmt.setString(1, nodeID);
ResultSet rs = pstmt.executeQuery();
if (!rs.next()) {
// No node was found for the specified nodeID so return null
return null;
}
boolean leaf = rs.getInt(1) == 1;
String parent = rs.getString(4);
JID creator = new JID(rs.getString(20));
CollectionNode parentNode = null;
if (parent != null) {
// Check if the parent has already been loaded
parentNode = (CollectionNode) service.getNode(parent);
if (parentNode == null) {
// Parent is not in memory so try to load it
synchronized (parent.intern()) {
// Check again if parent has not been already loaded (concurrency issues)
parentNode = (CollectionNode) service.getNode(parent);
if (parentNode == null) {
// Parent was never loaded so load it from the database now
parentNode = (CollectionNode) loadNode(service, parent, con);
}
}
}
}
if (leaf) {
// Retrieving a leaf node
node = new LeafNode(parentNode, nodeID, creator);
}
else {
// Retrieving a collection node
node = new CollectionNode(parentNode, nodeID, creator);
}
node.setCreationDate(new Date(Long.parseLong(rs.getString(2).trim())));
node.setModificationDate(new Date(Long.parseLong(rs.getString(3).trim())));
node.setDeliverPayloads(rs.getInt(5) == 1);
node.setMaxPayloadSize(rs.getInt(6));
node.setPersistPublishedItems(rs.getInt(7) == 1);
node.setMaxPublishedItems(rs.getInt(8));
node.setNotifyConfigChanges(rs.getInt(9) == 1);
node.setNotifyDelete(rs.getInt(10) == 1);
node.setNotifyRetract(rs.getInt(11) == 1);
node.setPresenceBasedDelivery(rs.getInt(12) == 1);
node.setSendItemSubscribe(rs.getInt(13) == 1);
node.setPublisherModel(Node.PublisherModel.valueOf(rs.getString(14)));
node.setSubscriptionEnabled(rs.getInt(15) == 1);
node.setAccessModel(Node.AccessModel.valueOf(rs.getString(16)));
node.setPayloadType(rs.getString(17));
node.setBodyXSLT(rs.getString(18));
node.setDataformXSLT(rs.getString(19));
node.setDescription(rs.getString(21));
node.setLanguage(rs.getString(22));
node.setName(rs.getString(23));
rs.close();
pstmt.close();
pstmt = con.prepareStatement(LOAD_HISTORY);
// Recreate the history until two days ago
long from = System.currentTimeMillis() - (86400000 * 2);
pstmt.setString(1, StringUtils.dateToMillis(new Date(from)));
pstmt.setLong(2, room.getID());
rs = pstmt.executeQuery();
while (rs.next()) {
String senderJID = rs.getString(1);
String nickname = rs.getString(2);
Date sentDate = new Date(Long.parseLong(rs.getString(3).trim()));
String subject = rs.getString(4);
String body = rs.getString(5);
// Recreate the history only for the rooms that have the conversation logging
// enabled
if (room.isLogEnabled()) {
room.getRoomHistory().addOldMessage(senderJID, nickname, sentDate, subject,
body);
}
}
rs.close();
pstmt.close();
pstmt = con.prepareStatement(LOAD_NODE_AFFILIATIONS);
pstmt.setString(1, node.getNodeID());
rs = pstmt.executeQuery();
while (rs.next()) {
NodeAffiliate affiliate = new NodeAffiliate(new JID(rs.getString(1)));
affiliate.setAffiliation(NodeAffiliate.Affiliation.valueOf(rs.getString(2)));
affiliate.setSubscription(NodeAffiliate.State.valueOf(rs.getString(3)));
node.addAffiliate(affiliate);
}
rs.close();
// Set now that the room's configuration is updated in the database. Note: We need to
// set this now since otherwise the room's affiliations will be saved to the database
// "again" while adding them to the room!
node.setSavedToDB(true);
// Add the retrieved node to the pubsub service
service.addChildNode(node);
}
catch (SQLException sqle) {
Log.error(sqle);
}
finally {
try { if (pstmt != null) pstmt.close(); }
catch (Exception e) { Log.error(e); }
try { if (con != null) con.close(); }
catch (Exception e) { Log.error(e); }
}
return node;
}*/
private static String encodeJIDs(Collection<JID> jids) {
StringBuilder sb = new StringBuilder(90);
for (JID jid : jids) {
sb.append(jid.toString()).append(",");
}
if (!jids.isEmpty()) {
sb.setLength(sb.length()-1);
}
return sb.toString();
}
private static Collection<JID> decodeJIDs(String jids) {
Collection<JID> decodedJIDs = new ArrayList<JID>();
StringTokenizer tokenizer = new StringTokenizer(jids, ",");
while (tokenizer.hasMoreTokens()) {
decodedJIDs.add(new JID(tokenizer.nextToken()));
}
return decodedJIDs;
}
private static String encodeGroups(Collection<String> groups) {
StringBuilder sb = new StringBuilder(90);
for (String group : groups) {
sb.append(group).append("\u2008");
}
if (!groups.isEmpty()) {
sb.setLength(sb.length()-1);
}
return sb.toString();
}
private static Collection<String> decodeGroups(String groups) {
Collection<String> decodedGroups = new ArrayList<String>();
StringTokenizer tokenizer = new StringTokenizer(groups, "\u2008");
while (tokenizer.hasMoreTokens()) {
decodedGroups.add(tokenizer.nextToken());
}
return decodedGroups;
}
private static String encodeWithComma(Collection<String> strings) {
StringBuilder sb = new StringBuilder(90);
for (String group : strings) {
sb.append(group).append(",");
}
if (!strings.isEmpty()) {
sb.setLength(sb.length()-1);
}
return sb.toString();
}
private static Collection<String> decodeWithComma(String strings) {
Collection<String> decodedStrings = new ArrayList<String>();
StringTokenizer tokenizer = new StringTokenizer(strings, ",");
while (tokenizer.hasMoreTokens()) {
decodedStrings.add(tokenizer.nextToken());
}
return decodedStrings;
}
}
/**
* $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.
*/
package org.jivesoftware.wildfire.pubsub;
import org.xmpp.packet.JID;
import org.xmpp.packet.Message;
import org.xmpp.packet.Packet;
import java.util.Collection;
/**
* A PubSubService is responsible for keeping the hosted nodes by the service, the default
* configuration to use for newly created nodes and specify the policy to use regarding
* node management.<p>
*
* Implementations of PubSubService are expected to collaborate with a {@link PubSubEngine}
* that will take care of handling packets sent to the service.<p>
*
* The separation between <code>PubSubService</code> and <code>PubSubEngine</code> allows to
* reuse the handling of packets and at the same time be able to create different pubsub
* services with different configurations. Examples of different pubsub services are:
* JEP-60: Publish-Subscribe and JEP-163: Personal Eventing Protocol.
*
* @author Matt Tucker
*/
public interface PubSubService {
/**
* Returns a String that uniquely identifies this pubsub service. This information is
* being used when storing node information in the database so it's possible to have
* nodes with the same ID but under different pubsub services.
*
* @return a String that uniquely identifies this pubsub service.
*/
String getServiceID();
/**
* Returns true if the pubsub service allows the specified user to create nodes.
*
* @return true if the pubsub service allows the specified user to create nodes.
*/
boolean canCreateNode(JID creator);
/**
* Returns true if the specified user is a sysadmin of the pubsub service or has
* admin privileges.
*
* @param user the user to check if he has admin privileges.
* @return true if the specified user is a sysadmin of the pubsub service or has
* admin privileges.
*/
boolean isServiceAdmin(JID user);
/**
* Returns true if the pubsub service allows users to create nodes without specifying
* the node ID. The service will create a random node ID and assigne it to the node.
*
* @return true if the pubsub service allows users to create nodes without specifying
* the node ID.
*/
boolean isInstantNodeSupported();
/**
* Returns true if the pubsub service supports collection nodes. When collection nodes is
* supported it is possible to create hierarchy of nodes where a {@link CollectionNode}
* may only hold children nodes of type {@link CollectionNode} or {@link LeafNode}. On the
* other hand, {@link LeafNode} can only hold {@link PublishedItem}.
*
* @return true if the pubsub service supports collection nodes.
*/
boolean isCollectionNodesSupported();
/**
* Returns the {@link CollectionNode} that acts as the root node of the entire
* node hierarchy. The returned node does not have a node identifier. If collection
* nodes is not supported then return <tt>null</tt>.
*
* @return the CollectionNode that acts as the root node of the entire node hierarchy
* or <tt>null</tt> if collection nodes is not supported.
*/
CollectionNode getRootCollectionNode();
/**
* Returns the {@link Node} that matches the specified node ID or <tt>null</tt> if
* none was found.
*
* @param nodeID the ID that uniquely identifies the node in the pubsub service.
* @return the Node that matches the specified node ID or <tt>null</tt> if none was found.
*/
Node getNode(String nodeID);
/**
* Retuns the collection of nodes hosted by the pubsub service. The collection does
* not support modifications.
*
* @return the collection of nodes hosted by the pubsub service.
*/
Collection<Node> getNodes();
/**
* Adds an already persistent node to the service.
*
* @param node the persistent node to add to the service.
*/
void addNode(Node node);
/**
* Removes the specified node from the service. Most probaly the node was deleted from
* the database as well.<p>
*
* A future version may support unloading of inactive nodes even though they may still
* exist in the database.
*
* @param nodeID the ID that uniquely identifies the node in the pubsub service.
*/
void removeNode(String nodeID);
/**
* Broadcasts the specified Message containing an event notification to a list
* of subscribers to the specified node. Each message being sent has to have a unique
* ID value so that the service can properly track any notification-related errors
* that may occur.
*
* @param node the node that triggered the event notification.
* @param message the message containing the event notification.
* @param jids the list of entities to get the event notification.
*/
void broadcast(Node node, Message message, Collection<JID> jids);
/**
* Sends the specified packet.
*
* @param packet the packet to send.
*/
void send(Packet packet);
/**
* Sends the specified Message containing an event notification to a specific
* subscriber of the specified node. The message being sent has to have a unique
* ID value so that the service can properly track any notification-related errors
* that may occur.
*
* @param node the node that triggered the event notification.
* @param message the message containing the event notification.
* @param jid the entity to get the event notification.
*/
void sendNotification(Node node, Message message, JID jid);
/**
* Returns the default node configuration for the specified node type or <tt>null</tt>
* if the specified node type is not supported by the service.
*
* @param leafType true when requesting default configuration of leaf nodes
* @return the default node configuration for the specified node type or <tt>null</tt>
* if the specified node type is not supported by the service.
*/
DefaultNodeConfiguration getDefaultNodeConfiguration(boolean leafType);
/**
* Returns the show value of the last know presence of the specified subscriber. If the user
* is offline then a <tt>null</tt> value is returned. Available show status is represented
* by a <tt>online</tt> value. The rest of the possible show values as defined in RFC 3921.
*
* @param subscriber the JID of the subscriber. This is not the JID of the affiliate.
* @return null when offline, online when user if available or show values as defined
* in RFC 3921.
*/
String getShowPresence(JID subscriber);
}
/**
* $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.
*/
package org.jivesoftware.wildfire.pubsub;
import org.xmpp.packet.JID;
import org.dom4j.Element;
import java.util.Date;
/**
* A published item to a node. Once an item was published to a node, node subscribers will be
* notified of the new published item. The item publisher may be allowed to delete published
* items. After a published item was deleted node subscribers will get an event notification.<p>
*
* Published items may be persisted to the database depending on the node configuration.
* Actually, even when the node is configured to not persist items the last published
* item is going to be persisted to the database. The reason for this is that the node
* may need to send the last published item to new subscribers.
*
* @author Matt Tucker
*/
public class PublishedItem {
/**
* JID of the entity that published the item to the node.
*/
private JID publisher;
/**
* The node where the item was published.
*/
private LeafNode node;
/**
* ID that uniquely identifies the published item in the node.
*/
private String id;
/**
* The datetime when the items was published.
*/
private Date creationDate;
/**
* The payload included when publishing the item.
*/
private Element payload;
/**
* XML representation of the payload. This is actually a cache that avoids
* doing Element#asXML.
*/
private String payloadXML;
PublishedItem(LeafNode node, JID publisher, String id, Date creationDate) {
this.node = node;
this.publisher = publisher;
this.id = id;
this.creationDate = creationDate;
}
/**
* Returns the {@link LeafNode} where this item was published.
*
* @return the leaf node where this item was published.
*/
public LeafNode getNode() {
return node;
}
/**
* Returns the ID that uniquely identifies the published item in the node.
*
* @return the ID that uniquely identifies the published item in the node.
*/
public String getID() {
return id;
}
/**
* Returns the JID of the entity that published the item to the node.
*
* @return the JID of the entity that published the item to the node.
*/
public JID getPublisher() {
return publisher;
}
/**
* Returns the datetime when the items was published.
*
* @return the datetime when the items was published.
*/
public Date getCreationDate() {
return creationDate;
}
/**
* Returns the payload included when publishing the item. A published item may or may not
* have a payload. Transient nodes that are configured to not broadcast payloads may allow
* published items to have no payload.
*
* @return the payload included when publishing the item or <tt>null</tt> if none was found.
*/
public Element getPayload() {
return payload;
}
/**
* Sets the payload included when publishing the item. A published item may or may not
* have a payload. Transient nodes that are configured to not broadcast payloads may allow
* published items to have no payload.
*
* @param payload the payload included when publishing the item or <tt>null</tt>
* if none was found.
*/
void setPayload(Element payload) {
this.payload = payload;
// Update XML representation of the payload
if (payload == null) {
payloadXML = null;
}
else {
payloadXML = payload.asXML();
}
}
/**
* Returns true if payload contains the specified keyword. If the item has no payload
* or keyword is <tt>null</tt> then return true.
*
* @param keyword the keyword to look for in the payload.
* @return true if payload contains the specified keyword.
*/
boolean containsKeyword(String keyword) {
if (payloadXML == null || keyword == null) {
return true;
}
return payloadXML.contains(keyword);
}
}
/**
* $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.
*/
package org.jivesoftware.wildfire.pubsub.models;
import org.dom4j.Element;
import org.jivesoftware.wildfire.pubsub.Node;
import org.xmpp.packet.JID;
import org.xmpp.packet.PacketError;
/**
* Policy that defines who is allowed to subscribe and retrieve items.
*
* @author Matt Tucker
*/
public abstract class AccessModel {
public final static AccessModel whitelist = new WhitelistAccess();
public final static AccessModel open = new OpenAccess();
public final static AccessModel authorize = new AuthorizeAccess();
public final static AccessModel presence = new PresenceAccess();
public final static AccessModel roster = new RosterAccess();
/**
* Returns the specific subclass of AccessModel as specified by the access
* model name. If an unknown name is specified then an IllegalArgumentException
* is going to be thrown.
*
* @param name the name of the subsclass.
* @return the specific subclass of AccessModel as specified by the access
* model name.
*/
public static AccessModel valueOf(String name) {
if ("open".equals(name)) {
return open;
}
else if ("whitelist".equals(name)) {
return whitelist;
}
else if ("authorize".equals(name)) {
return authorize;
}
else if ("presence".equals(name)) {
return presence;
}
else if ("roster".equals(name)) {
return roster;
}
throw new IllegalArgumentException("Unknown access model: " + name);
}
/**
* Returns the name as defined by the JEP-60 spec.
*
* @return the name as defined by the JEP-60 spec.
*/
public abstract String getName();
/**
* Returns true if the entity is allowed to subscribe to the specified node.
*
* @param node the node that the subscriber is trying to subscribe to.
* @param owner the JID of the owner of the subscription.
* @param subscriber the JID of the subscriber.
* @return true if the subscriber is allowed to subscribe to the specified node.
*/
public abstract boolean canSubscribe(Node node, JID owner, JID subscriber);
/**
* Returns true if the entity is allowed to get the node published items.
*
* @param node the node that the entity is trying to get the node's items.
* @param owner the JID of the owner of the subscription.
* @param subscriber the JID of the subscriber.
* @return true if the subscriber is allowed to get the node's published items.
*/
public abstract boolean canAccessItems(Node node, JID owner, JID subscriber);
/**
* Returns the error condition that should be returned to the subscriber when
* subscription is not allowed.
*
* @return the error condition that should be returned to the subscriber when
* subscription is not allowed.
*/
public abstract PacketError.Condition getSubsriptionError();
/**
* Returns the error element that should be returned to the subscriber as
* error detail when subscription is not allowed. The returned element is created
* each time this message is sent so it is safe to include the returned element in
* the parent element.
*
* @return the error element that should be returned to the subscriber as
* error detail when subscription is not allowed.
*/
public abstract Element getSubsriptionErrorDetail();
/**
* Returns true if the new subscription should be authorized by a node owner.
*
* @return true if the new subscription should be authorized by a node owner.
*/
public abstract boolean isAuthorizationRequired();
}
/**
* $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.
*/
package org.jivesoftware.wildfire.pubsub.models;
import org.jivesoftware.wildfire.pubsub.Node;
import org.jivesoftware.wildfire.pubsub.NodeSubscription;
import org.jivesoftware.wildfire.pubsub.NodeAffiliate;
import org.xmpp.packet.JID;
import org.xmpp.packet.PacketError;
import org.dom4j.Element;
import org.dom4j.DocumentHelper;
import org.dom4j.QName;
/**
* Subscription requests must be approved and only subscribers may retrieve items.
*
* @author Matt Tucker
*/
public class AuthorizeAccess extends AccessModel {
AuthorizeAccess() {
}
public boolean canSubscribe(Node node, JID owner, JID subscriber) {
return true;
}
public boolean canAccessItems(Node node, JID owner, JID subscriber) {
NodeAffiliate nodeAffiliate = node.getAffiliate(owner);
if (nodeAffiliate == null) {
// This is an unknown entity to the node so deny access
return false;
}
// Any subscription of this entity that was approved will give him access
// to retrieve the node items
for (NodeSubscription subscription : nodeAffiliate.getSubscriptions()) {
if (subscription.isApproved()) {
return true;
}
}
// No approved subscription was found so deny access
return false;
}
public String getName() {
return "authorize";
}
public PacketError.Condition getSubsriptionError() {
return PacketError.Condition.not_authorized;
}
public Element getSubsriptionErrorDetail() {
return DocumentHelper.createElement(QName.get("not-subscribed",
"http://jabber.org/protocol/pubsub#errors"));
}
public boolean isAuthorizationRequired() {
return true;
}
}
/**
* $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.
*/
package org.jivesoftware.wildfire.pubsub.models;
import org.jivesoftware.wildfire.pubsub.Node;
import org.jivesoftware.wildfire.pubsub.NodeAffiliate;
import org.xmpp.packet.JID;
/**
* Publishers and owners may publish items to the node.
*
* @author Matt Tucker
*/
public class OnlyPublishers extends PublisherModel {
public boolean canPublish(Node node, JID entity) {
NodeAffiliate nodeAffiliate = node.getAffiliate(entity);
return nodeAffiliate != null && (
nodeAffiliate.getAffiliation() == NodeAffiliate.Affiliation.publisher ||
nodeAffiliate.getAffiliation() == NodeAffiliate.Affiliation.owner);
}
public String getName() {
return "publishers";
}
}
/**
* $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.
*/
package org.jivesoftware.wildfire.pubsub.models;
import org.jivesoftware.wildfire.pubsub.Node;
import org.jivesoftware.wildfire.pubsub.NodeAffiliate;
import org.jivesoftware.wildfire.pubsub.NodeSubscription;
import org.xmpp.packet.JID;
/**
* Subscribers, publishers and owners may publish items to the node.
*
* @author Matt Tucker
*/
public class OnlySubscribers extends PublisherModel {
public boolean canPublish(Node node, JID entity) {
NodeAffiliate nodeAffiliate = node.getAffiliate(entity);
// Deny access if user does not have any relation with the node or is an outcast
if (nodeAffiliate == null ||
nodeAffiliate.getAffiliation() == NodeAffiliate.Affiliation.outcast) {
return false;
}
// Grant access if user is an owner of publisher
if (nodeAffiliate.getAffiliation() == NodeAffiliate.Affiliation.publisher ||
nodeAffiliate.getAffiliation() == NodeAffiliate.Affiliation.owner) {
return true;
}
// Grant access if at least one subscription of this user was approved
for (NodeSubscription subscription : nodeAffiliate.getSubscriptions()) {
if (subscription.isApproved()) {
return true;
}
}
return false;
}
public String getName() {
return "subscribers";
}
}
/**
* $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.
*/
package org.jivesoftware.wildfire.pubsub.models;
import org.jivesoftware.wildfire.pubsub.Node;
import org.xmpp.packet.JID;
import org.xmpp.packet.PacketError;
import org.dom4j.Element;
/**
* Anyone may subscribe and retrieve items.
*
* @author Matt Tucker
*/
public class OpenAccess extends AccessModel {
OpenAccess() {
}
public boolean canSubscribe(Node node, JID owner, JID subscriber) {
return true;
}
public boolean canAccessItems(Node node, JID owner, JID subscriber) {
return true;
}
public String getName() {
return "open";
}
public PacketError.Condition getSubsriptionError() {
// Return nothing since users can always subscribe to the node
return null;
}
public Element getSubsriptionErrorDetail() {
// Return nothing since users can always subscribe to the node
return null;
}
public boolean isAuthorizationRequired() {
return false;
}
}
/**
* $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.
*/
package org.jivesoftware.wildfire.pubsub.models;
import org.jivesoftware.wildfire.pubsub.Node;
import org.xmpp.packet.JID;
/**
* Anyone may publish items to the node.
*
* @author Matt Tucker
*/
public class OpenPublisher extends PublisherModel {
public boolean canPublish(Node node, JID entity) {
return true;
}
public String getName() {
return "open";
}
}
/**
* $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.
*/
package org.jivesoftware.wildfire.pubsub.models;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.dom4j.QName;
import org.jivesoftware.util.Log;
import org.jivesoftware.wildfire.XMPPServer;
import org.jivesoftware.wildfire.pubsub.Node;
import org.jivesoftware.wildfire.roster.Roster;
import org.jivesoftware.wildfire.roster.RosterItem;
import org.jivesoftware.wildfire.user.UserNotFoundException;
import org.xmpp.packet.JID;
import org.xmpp.packet.PacketError;
/**
* Anyone with a presence subscription of both or from may subscribe and retrieve items.
*
* @author Matt Tucker
*/
public class PresenceAccess extends AccessModel {
PresenceAccess() {
}
public boolean canSubscribe(Node node, JID owner, JID subscriber) {
// Get the only owner of the node
JID nodeOwner = node.getOwners().iterator().next();
// Get the roster of the node owner
XMPPServer server = XMPPServer.getInstance();
// Check that the node owner is a local user
if (server.isLocal(nodeOwner)) {
try {
Roster roster = server.getRosterManager().getRoster(nodeOwner.getNode());
RosterItem item = roster.getRosterItem(owner);
// Check that the subscriber is subscribe to the node owner's presence
return RosterItem.SUB_BOTH == item.getSubStatus() ||
RosterItem.SUB_FROM == item.getSubStatus();
}
catch (UserNotFoundException e) {
return false;
}
}
else {
// Owner of the node is a remote user. This should never happen.
Log.warn("Node with access model Presence has a remote user as owner: " +
node.getNodeID());
return false;
}
}
public boolean canAccessItems(Node node, JID owner, JID subscriber) {
return canSubscribe(node, owner, subscriber);
}
public String getName() {
return "presence";
}
public PacketError.Condition getSubsriptionError() {
return PacketError.Condition.not_authorized;
}
public Element getSubsriptionErrorDetail() {
return DocumentHelper.createElement(QName.get("presence-subscription-required",
"http://jabber.org/protocol/pubsub#errors"));
}
public boolean isAuthorizationRequired() {
return false;
}
}
/**
* $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.
*/
package org.jivesoftware.wildfire.pubsub.models;
import org.jivesoftware.wildfire.pubsub.Node;
import org.xmpp.packet.JID;
/**
* Policy that defines who is allowed to publish items to the node.
*
* @author Matt Tucker
*/
public abstract class PublisherModel {
public final static PublisherModel open = new OpenPublisher();
public final static PublisherModel publishers = new OnlyPublishers();
public final static PublisherModel subscribers = new OnlySubscribers();
/**
* Returns the specific subclass of PublisherModel as specified by the publisher
* model name. If an unknown name is specified then an IllegalArgumentException
* is going to be thrown.
*
* @param name the name of the subsclass.
* @return the specific subclass of PublisherModel as specified by the access
* model name.
*/
public static PublisherModel valueOf(String name) {
if ("open".equals(name)) {
return open;
}
else if ("publishers".equals(name)) {
return publishers;
}
else if ("subscribers".equals(name)) {
return subscribers;
}
throw new IllegalArgumentException("Unknown publisher model: " + name);
}
/**
* Returns the name as defined by the JEP-60 spec.
*
* @return the name as defined by the JEP-60 spec.
*/
public abstract String getName();
/**
* Returns true if the entity is allowed to publish items to the specified node.
*
* @param node the node that may get a new published item by the specified entity.
* @param entity the JID of the entity that wants to publish an item to the node.
* @return true if the subscriber is allowed to publish items to the specified node.
*/
public abstract boolean canPublish(Node node, JID entity);
}
/**
* $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.
*/
package org.jivesoftware.wildfire.pubsub.models;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.dom4j.QName;
import org.jivesoftware.util.Log;
import org.jivesoftware.wildfire.XMPPServer;
import org.jivesoftware.wildfire.group.Group;
import org.jivesoftware.wildfire.pubsub.Node;
import org.jivesoftware.wildfire.roster.Roster;
import org.jivesoftware.wildfire.roster.RosterItem;
import org.jivesoftware.wildfire.user.UserNotFoundException;
import org.xmpp.packet.JID;
import org.xmpp.packet.PacketError;
import java.util.ArrayList;
import java.util.List;
/**
* Anyone in the specified roster group(s) may subscribe and retrieve items.
*
* @author Matt Tucker
*/
public class RosterAccess extends AccessModel {
RosterAccess() {
}
public boolean canSubscribe(Node node, JID owner, JID subscriber) {
// Get the only owner of the node
JID nodeOwner = node.getOwners().iterator().next();
// Get the roster of the node owner
XMPPServer server = XMPPServer.getInstance();
// Check that the node owner is a local user
if (server.isLocal(nodeOwner)) {
try {
Roster roster = server.getRosterManager().getRoster(nodeOwner.getNode());
RosterItem item = roster.getRosterItem(owner);
// Check that the subscriber is subscribe to the node owner's presence
boolean isSubscribed = RosterItem.SUB_BOTH == item.getSubStatus() ||
RosterItem.SUB_FROM == item.getSubStatus();
if (isSubscribed) {
// Get list of groups where the contact belongs
List<String> contactGroups = new ArrayList<String>(item.getGroups());
for (Group group : item.getSharedGroups()) {
contactGroups.add(group.getName());
}
for (Group group : item.getInvisibleSharedGroups()) {
contactGroups.add(group.getName());
}
// Check if subscriber is present in the allowed groups of the node
return contactGroups.removeAll(node.getRosterGroupsAllowed());
}
}
catch (UserNotFoundException e) {
}
}
else {
// Owner of the node is a remote user. This should never happen.
Log.warn("Node with access model Roster has a remote user as owner: " +
node.getNodeID());
}
return false;
}
public boolean canAccessItems(Node node, JID owner, JID subscriber) {
return canSubscribe(node, owner, subscriber);
}
public String getName() {
return "roster";
}
public PacketError.Condition getSubsriptionError() {
return PacketError.Condition.not_authorized;
}
public Element getSubsriptionErrorDetail() {
return DocumentHelper.createElement(
QName.get("not-in-roster-group", "http://jabber.org/protocol/pubsub#errors"));
}
public boolean isAuthorizationRequired() {
return false;
}
}
/**
* $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.
*/
package org.jivesoftware.wildfire.pubsub.models;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.dom4j.QName;
import org.jivesoftware.wildfire.pubsub.Node;
import org.jivesoftware.wildfire.pubsub.NodeAffiliate;
import org.xmpp.packet.JID;
import org.xmpp.packet.PacketError;
/**
* Only those on a whitelist may subscribe and retrieve items.
*
* @author Matt Tucker
*/
public class WhitelistAccess extends AccessModel {
WhitelistAccess() {
}
public boolean canSubscribe(Node node, JID owner, JID subscriber) {
// User is in the whitelist if he has an affiliation and it is not of type outcast
NodeAffiliate nodeAffiliate = node.getAffiliate(owner);
return nodeAffiliate != null &&
nodeAffiliate.getAffiliation() != NodeAffiliate.Affiliation.outcast;
}
public boolean canAccessItems(Node node, JID owner, JID subscriber) {
return canSubscribe(node, owner, subscriber);
}
public String getName() {
return "whitelist";
}
public PacketError.Condition getSubsriptionError() {
return PacketError.Condition.not_allowed;
}
public Element getSubsriptionErrorDetail() {
return DocumentHelper.createElement(
QName.get("closed-node", "http://jabber.org/protocol/pubsub#errors"));
}
public boolean isAuthorizationRequired() {
return false;
}
}
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
</head>
<body>
<p>Implementation of Publish-Subscribe (JEP-0060).</p>
</body>
</html>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment