org.jivesoftware.openfire.pubsub.Node Maven / Gradle / Ivy
/*
* Copyright (C) 2005-2008 Jive Software. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.openfire.pubsub;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import org.dom4j.Element;
import org.jivesoftware.openfire.SessionManager;
import org.jivesoftware.openfire.cluster.ClusterManager;
import org.jivesoftware.openfire.pubsub.cluster.AffiliationTask;
import org.jivesoftware.openfire.pubsub.cluster.CancelSubscriptionTask;
import org.jivesoftware.openfire.pubsub.cluster.ModifySubscriptionTask;
import org.jivesoftware.openfire.pubsub.cluster.NewSubscriptionTask;
import org.jivesoftware.openfire.pubsub.cluster.RemoveNodeTask;
import org.jivesoftware.openfire.pubsub.models.AccessModel;
import org.jivesoftware.openfire.pubsub.models.PublisherModel;
import org.jivesoftware.util.LocaleUtils;
import org.jivesoftware.util.StringUtils;
import org.jivesoftware.util.cache.CacheFactory;
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;
/**
* 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;
/**
* 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 rosterGroupsAllowed = new ArrayList<>();
/**
* List of multi-user chat rooms to specify for replyroom.
*/
protected Collection replyRooms = new ArrayList<>();
/**
* List of JID(s) to specify for replyto.
*/
protected Collection replyTo = new ArrayList<>();
/**
* 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 contacts = new ArrayList<>();
/**
* 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 affiliates = new CopyOnWriteArrayList<>();
/**
* 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 subscriptionsByID =
new ConcurrentHashMap<>();
/**
* Map that contains the current subscriptions to the node. This map should be used only
* when node is not configured to allow multiple subscriptions. When multiple subscriptions
* is not allowed the subscriptions can be searched by the subscriber JID. Otherwise searches
* should be done using the subscription ID.
* Key: Subscriber full JID, Value: the subscription.
*/
protected Map subscriptionsByJID =
new ConcurrentHashMap<>();
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.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.
* @return the newly created or modified affiliation to the node.
*/
public NodeAffiliate addOwner(JID jid) {
NodeAffiliate nodeAffiliate = addAffiliation(jid, NodeAffiliate.Affiliation.owner);
// Approve any pending subscription
for (NodeSubscription subscription : getSubscriptions(jid)) {
if (subscription.isAuthorizationPending()) {
subscription.approved();
}
}
return nodeAffiliate;
}
/**
* Removes the owner affiliation of the specified entity JID. If the user that is
* no longer an owner was subscribed to the node then his affiliation will be of
* type {@link NodeAffiliate.Affiliation#none}.
*
* @param jid the JID of the user being removed as a node owner.
*/
public void removeOwner(JID jid) {
// Get the current affiliation of the specified JID
NodeAffiliate affiliate = getAffiliate(jid);
if (affiliate.getSubscriptions().isEmpty()) {
removeAffiliation(jid, NodeAffiliate.Affiliation.owner);
removeSubscriptions(jid);
}
else {
// The user has subscriptions so change affiliation to NONE
addNoneAffiliation(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.
* @return the newly created or modified affiliation to the node.
*/
public NodeAffiliate addPublisher(JID jid) {
return addAffiliation(jid, NodeAffiliate.Affiliation.publisher);
}
/**
* Removes the publisher affiliation of the specified entity JID. If the user that is
* no longer a publisher was subscribed to the node then his affiliation will be of
* type {@link NodeAffiliate.Affiliation#none}.
*
* @param jid the JID of the user being removed as a node publisher.
*/
public void removePublisher(JID jid) {
// Get the current affiliation of the specified JID
NodeAffiliate affiliate = getAffiliate(jid);
if (affiliate.getSubscriptions().isEmpty()) {
removeAffiliation(jid, NodeAffiliate.Affiliation.publisher);
removeSubscriptions(jid);
}
else {
// The user has subscriptions so change affiliation to NONE
addNoneAffiliation(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".
* @return the newly created or modified affiliation to the node.
*/
public NodeAffiliate addNoneAffiliation(JID jid) {
return 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.
* @return the newly created or modified affiliation to the node.
*/
public NodeAffiliate addOutcast(JID jid) {
NodeAffiliate nodeAffiliate = addAffiliation(jid, NodeAffiliate.Affiliation.outcast);
// Delete existing subscriptions
removeSubscriptions(jid);
return nodeAffiliate;
}
/**
* 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 NodeAffiliate 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 affiliate;
}
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(this, affiliate, created);
}
// Update the other members with the new affiliation
CacheFactory.doClusterTask(new AffiliationTask(this, jid, affiliation));
return affiliate;
}
private void removeAffiliation(JID jid, NodeAffiliate.Affiliation affiliation) {
// Get the current affiliation of the specified JID
NodeAffiliate affiliate = getAffiliate(jid);
// Check if the current affiliation of the user is the one to remove
if (affiliate != null && affiliation == affiliate.getAffiliation()) {
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(this, affiliate);
}
}
/**
* Removes all subscriptions owned by the specified entity.
*
* @param owner the owner of the subscriptions to be cancelled.
*/
private void removeSubscriptions(JID owner) {
for (NodeSubscription subscription : getSubscriptions(owner)) {
cancelSubscription(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.
* @return the list of subscriptions owned by the specified user.
*/
public Collection getSubscriptions(JID owner) {
Collection subscriptions = new ArrayList<>();
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 getSubscriptions() {
return subscriptionsByID.values();
}
/**
* Returns all subscriptions to the node. If multiple subscriptions are enabled,
* this method returns the subscriptions by subId, otherwise it returns
* the subscriptions by {@link JID}.
*
* @return All subscriptions to the node.
*/
public Collection getAllSubscriptions() {
if (isMultipleSubscriptionsEnabled()) {
return subscriptionsByID.values();
} else {
return subscriptionsByJID.values();
}
}
/**
* Returns all affiliates of the node.
*
* @return All affiliates of the node.
*/
public Collection getAllAffiliates() {
return affiliates;
}
/**
* Returns the {@link NodeAffiliate} of the specified {@link JID} or null
* if none was found. Users that have a subscription with the node will ALWAYS
* have an affiliation even if the affiliation is of type none.
*
* @param jid the JID of the user to look his affiliation with this node.
* @return the NodeAffiliate of the specified JID or null if none was found.
*/
public NodeAffiliate getAffiliate(JID jid) {
for (NodeAffiliate affiliate : affiliates) {
if (jid.equals(affiliate.getJID())) {
return affiliate;
}
}
return null;
}
/**
* Returns a collection with the JID of the node owners. Entities that are node owners have
* an affiliation of {@link NodeAffiliate.Affiliation#owner}. Owners are allowed to purge
* and delete the node. Moreover, owners may also get The collection can be modified
* since it represents a snapshot.
*
* @return a collection with the JID of the node owners.
*/
public Collection getOwners() {
Collection jids = new ArrayList<>();
for (NodeAffiliate affiliate : affiliates) {
if (NodeAffiliate.Affiliation.owner == affiliate.getAffiliation()) {
jids.add(affiliate.getJID());
}
}
return jids;
}
/**
* Returns a collection with the JID of the enitities with an affiliation of
* {@link NodeAffiliate.Affiliation#publisher}. When using the publisher model
* {@link org.jivesoftware.openfire.pubsub.models.OpenPublisher} anyone may publish
* to the node so this collection may be empty or may not contain the complete list
* of publishers. The returned collection can be modified since it represents a snapshot.
*
* @return a collection with the JID of the enitities with an affiliation of publishers.
*/
public Collection getPublishers() {
Collection jids = new ArrayList<>();
for (NodeAffiliate affiliate : affiliates) {
if (NodeAffiliate.Affiliation.publisher == affiliate.getAffiliation()) {
jids.add(affiliate.getJID());
}
}
return jids;
}
/**
* Changes the node configuration based on the completed data form. Only owners or
* sysadmins are allowed to change the node configuration. The completed data form
* cannot remove all node owners. An exception is going to be thrown if the new form
* tries to leave the node without owners.
*
* @param completedForm the completed data form.
* @throws NotAcceptableException if completed data form tries to leave the node without owners.
*/
public void configure(DataForm completedForm) throws NotAcceptableException {
boolean wasPresenceBased = isPresenceBasedDelivery();
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 values;
String booleanValue;
// Get the new list of owners
FormField ownerField = completedForm.getField("pubsub#owner");
boolean ownersSent = ownerField != null;
List owners = new ArrayList<>();
if (ownersSent) {
for (String value : ownerField.getValues()) {
try {
owners.add(new JID(value));
}
catch (Exception e) {
// Do nothing
}
}
}
// 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())) {
// Do nothing
}
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#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<>();
for (String value : field.getValues()) {
addAllowedRosterGroup(value);
}
}
else if ("pubsub#contact".equals(field.getVariable())) {
// Get the new list of users that may be contacted with questions
contacts = new ArrayList<>();
for (String value : field.getValues()) {
try {
addContact(new JID(value));
}
catch (Exception e) {
// Do nothing
}
}
}
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<>();
for (String value : field.getValues()) {
try {
addReplyRoom(new JID(value));
}
catch (Exception e) {
// Do nothing
}
}
}
else if ("pubsub#replyto".equals(field.getVariable())) {
// Get the new list of JID(s) to specify for replyto
replyTo = new ArrayList<>();
for (String value : field.getValues()) {
try {
addReplyTo(new JID(value));
}
catch (Exception e) {
// Do nothing
}
}
}
else if ("pubsub#collection".equals(field.getVariable())) {
// Set the parent collection node
values = field.getValues();
String newParent = values.size() > 0 ? values.get(0) : " ";
Node newParentNode = service.getNode(newParent);
if (!(newParentNode instanceof CollectionNode))
{
throw new NotAcceptableException("Specified node in field pubsub#collection [" + newParent + "] " + ((newParentNode == null) ? "does not exist" : "is not a collection node"));
}
changeParent((CollectionNode) newParentNode);
}
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 oldOwners = getOwners();
oldOwners.removeAll(owners);
for (JID jid : oldOwners) {
removeOwner(jid);
}
// Calculate new owners and add them to the DB
owners.removeAll(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 subscriptions 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 publishers = new ArrayList<>();
for (String value : publisherField.getValues()) {
try {
publishers.add(new JID(value));
}
catch (Exception e) {
// Do nothing
}
}
// Calculate publishers to remove and remove them from the DB
Collection oldPublishers = getPublishers();
oldPublishers.removeAll(publishers);
for (JID jid : oldPublishers) {
removePublisher(jid);
}
// Calculate new publishers and add them to the DB
publishers.removeAll(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();
// Check if we need to subscribe or unsubscribe from affiliate presences
if (wasPresenceBased != isPresenceBasedDelivery()) {
if (isPresenceBasedDelivery()) {
addPresenceSubscriptions();
}
else {
cancelPresenceSubscriptions();
}
}
}
/**
* 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.
* @throws NotAcceptableException if field cannot be configured because of invalid data.
*/
protected abstract void configure(FormField field) throws NotAcceptableException;
/**
* 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);
/**
* The node configuration has changed. If this is the first time the node is configured
* after it was created (i.e. is not yet persistent) then do nothing. Otherwise, send
* a notification to the node subscribers informing that the configuration has changed.
*/
private void nodeConfigurationChanged() {
if (!isNotifiedOfConfigChanges() || !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 config = event.addElement("configuration");
config.addAttribute("node", nodeID);
if (deliverPayloads) {
config.add(getConfigurationChangeForm().getElement());
}
// Send notification that the node configuration has changed
broadcastNodeEvent(message, false);
}
/**
* Returns the data form to be included in the authorization request to be sent to
* node owners when a new subscription needs to be approved.
*
* @param subscription the new subscription that needs to be approved.
* @return the data form to be included in the authorization request.
*/
DataForm getAuthRequestForm(NodeSubscription subscription) {
DataForm form = new DataForm(DataForm.Type.form);
form.setTitle(LocaleUtils.getLocalizedString("pubsub.form.authorization.title"));
form.addInstruction(
LocaleUtils.getLocalizedString("pubsub.form.authorization.instruction"));
FormField formField = form.addField();
formField.setVariable("FORM_TYPE");
formField.setType(FormField.Type.hidden);
formField.addValue("http://jabber.org/protocol/pubsub#subscribe_authorization");
formField = form.addField();
formField.setVariable("pubsub#subid");
formField.setType(FormField.Type.hidden);
formField.addValue(subscription.getID());
formField = form.addField();
formField.setVariable("pubsub#node");
formField.setType(FormField.Type.text_single);
formField.setLabel(LocaleUtils.getLocalizedString("pubsub.form.authorization.node"));
formField.addValue(getNodeID());
formField = form.addField();
formField.setVariable("pubsub#subscriber_jid");
formField.setType(FormField.Type.jid_single);
formField.setLabel(LocaleUtils.getLocalizedString("pubsub.form.authorization.subscriber"));
formField.addValue(subscription.getJID().toString());
formField = form.addField();
formField.setVariable("pubsub#allow");
formField.setType(FormField.Type.boolean_type);
formField.setLabel(LocaleUtils.getLocalizedString("pubsub.form.authorization.allow"));
formField.addValue(Boolean.FALSE);
return form;
}
/**
* 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 params = new ArrayList<>();
params.add(getNodeID());
form.addInstruction(LocaleUtils.getLocalizedString("pubsub.form.conf.instruction", params));
FormField formField = form.addField();
formField.setVariable("FORM_TYPE");
formField.setType(FormField.Type.hidden);
formField.addValue("http://jabber.org/protocol/pubsub#node_config");
// Add the form fields and configure them for edition
addFormFields(form, true);
return form;
}
/**
* Adds the required form fields to the specified form. When editing is true the field type
* and a label is included in each fields. The form being completed will contain the current
* node configuration. This information can be used for editing the node or for notifing that
* the node configuration has changed.
*
* @param form the form containing the node configuration.
* @param isEditing true when the form will be used to edit the node configuration.
*/
protected void addFormFields(DataForm form, boolean isEditing) {
FormField 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#node_type");
if (isEditing) {
formField.setType(FormField.Type.text_single);
formField.setLabel(LocaleUtils.getLocalizedString("pubsub.form.conf.node_type"));
}
formField = form.addField();
formField.setVariable("pubsub#collection");
if (isEditing) {
formField.setType(FormField.Type.text_single);
formField.setLabel(LocaleUtils.getLocalizedString("pubsub.form.conf.collection"));
}
if (parent != null && !parent.isRootCollectionNode()) {
formField.addValue(parent.getNodeID());
}
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#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"));
formField.addOption(null, ItemReplyPolicy.owner.name());
formField.addOption(null, ItemReplyPolicy.publisher.name());
}
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);
FormField formField = form.addField();
formField.setVariable("FORM_TYPE");
formField.setType(FormField.Type.hidden);
formField.addValue("http://jabber.org/protocol/pubsub#node_config");
// Add the form fields and configure them for notification
// (i.e. no label or options are included)
addFormFields(form, false);
return form;
}
/**
* Returns a data form containing the node configuration that is going to be used for
* service discovery.
*
* @return a data form with the node configuration.
*/
public DataForm getMetadataForm() {
DataForm form = new DataForm(DataForm.Type.result);
FormField formField = form.addField();
formField.setVariable("FORM_TYPE");
formField.setType(FormField.Type.hidden);
formField.addValue("http://jabber.org/protocol/pubsub#meta-data");
// Add the form fields
addFormFields(form, true);
return form;
}
/**
* Returns true if this node is the root node of the pubsub service.
*
* @return true if this node is the root node of the pubsub service.
*/
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 subid 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 service.isMultipleSubscriptionsEnabled();
}
/**
* Returns true if this node is a node container. Node containers may only contain nodes
* but are not allowed to get items published.
*
* @return true if this node is a node container.
*/
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 = user.asBareJID();
return isAdmin(user);
}
return false;
}
/**
* Returns the {@link PubSubService} to which this node belongs.
*
* @return the pubsub service.
*/
public PubSubService getService() {
return service;
}
/**
* Returns the unique identifier for a node within the context of a pubsub service.
*
* @return the unique identifier for a node within the context of a pubsub service.
*/
public String getNodeID() {
return nodeID;
}
/**
* Returns the name of the node. The node may not have a configured name. The node's
* name can be changed by submiting a completed data form.
*
* @return the name of the node.
*/
public String getName() {
return name;
}
/**
* Returns true if event notifications will include payloads. Payloads are included when
* publishing new items. However, new items may not always include a payload depending
* on the node configuration. Nodes can be configured to not deliver payloads for performance
* reasons.
*
* @return true if event notifications will include payloads.
*/
public boolean isPayloadDelivered() {
return deliverPayloads;
}
public ItemReplyPolicy getReplyPolicy() {
return replyPolicy;
}
/**
* Returns true if subscribers will be notified when the node configuration changes.
*
* @return true if subscribers will be notified when the node configuration changes.
*/
public boolean isNotifiedOfConfigChanges() {
return notifyConfigChanges;
}
/**
* Returns true if subscribers will be notified when the node is deleted.
*
* @return true if subscribers will be notified when the node is deleted.
*/
public boolean isNotifiedOfDelete() {
return notifyDelete;
}
/**
* Returns true if subscribers will be notified when items are removed from the node.
*
* @return true if subscribers will be notified when items are removed from the node.
*/
public boolean isNotifiedOfRetract() {
return notifyRetract;
}
/**
* Returns true if notifications are going to be delivered to available users only.
*
* @return true if notifications are going to be delivered to available users only.
*/
public boolean isPresenceBasedDelivery() {
return presenceBasedDelivery;
}
/**
* Returns true if notifications to the specified user will be delivered when the
* user is online.
*
* @param user the JID of the affiliate that has to be subscribed to the node.
* @return true if notifications are going to be delivered when the user is online.
*/
public boolean isPresenceBasedDelivery(JID user) {
Collection subscriptions = getSubscriptions(user);
if (!subscriptions.isEmpty()) {
if (presenceBasedDelivery) {
// Node sends notifications only to only users so return true
return true;
}
else {
// Check if there is a subscription configured to only send notifications
// based on the user presence
for (NodeSubscription subscription : subscriptions) {
if (!subscription.getPresenceStates().isEmpty()) {
return true;
}
}
}
}
// User is not subscribed to the node so presence subscription is not required
return false;
}
/**
* Returns the JID of the affiliates that are receiving notifications based on their
* presence status.
*
* @return the JID of the affiliates that are receiving notifications based on their
* presence status.
*/
Collection getPresenceBasedSubscribers() {
Collection affiliatesJID = new ArrayList<>();
if (presenceBasedDelivery) {
// Add JID of all affiliates that are susbcribed to the node
for (NodeAffiliate affiliate : affiliates) {
if (!affiliate.getSubscriptions().isEmpty()) {
affiliatesJID.add(affiliate.getJID());
}
}
}
else {
// Add JID of those affiliates that have a subscription that only wants to be
// notified based on the subscriber presence
for (NodeAffiliate affiliate : affiliates) {
Collection subscriptions = affiliate.getSubscriptions();
for (NodeSubscription subscription : subscriptions) {
if (!subscription.getPresenceStates().isEmpty()) {
affiliatesJID.add(affiliate.getJID());
break;
}
}
}
}
return affiliatesJID;
}
/**
* Returns true if the last published item is going to be sent to new subscribers.
*
* @return true if the last published item is going to be sent to new subscribers.
*/
public boolean isSendItemSubscribe() {
return false;
}
/**
* Returns 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 users are allowed to subscribe and unsubscribe.
*
* @return true if users are allowed to subscribe and unsubscribe.
*/
public boolean isSubscriptionEnabled() {
return subscriptionEnabled;
}
/**
* Returns true if new subscriptions should be configured to be active. Inactive
* subscriptions will not get event notifications. However, subscribers will be
* notified when a node is deleted no matter the subscription status.
*
* @return true if new subscriptions should be configured to be active.
*/
public boolean isSubscriptionConfigurationRequired() {
return subscriptionConfigurationRequired;
}
/**
* 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 roster group(s) allowed to subscribe and retrieve items. This information
* is going to be used only when using the
* {@link org.jivesoftware.openfire.pubsub.models.RosterAccess} access model.
*
* @return the roster group(s) allowed to subscribe and retrieve items.
*/
public Collection getRosterGroupsAllowed() {
return Collections.unmodifiableCollection(rosterGroupsAllowed);
}
/**
* Adds a new roster group that is allowed to subscribe and retrieve items.
* The new roster group is not going to be added to the database. Instead it is just
* kept in memory.
*
* @param groupName the new roster group that is allowed to subscribe and retrieve items.
*/
void addAllowedRosterGroup(String groupName) {
rosterGroupsAllowed.add(groupName);
}
public Collection getReplyRooms() {
return Collections.unmodifiableCollection(replyRooms);
}
void addReplyRoom(JID roomJID) {
replyRooms.add(roomJID);
}
public Collection getReplyTo() {
return Collections.unmodifiableCollection(replyTo);
}
void addReplyTo(JID entity) {
replyTo.add(entity);
}
/**
* Returns the type of payload data to be provided at the node. Usually specified by the
* namespace of the payload (if any).
*
* @return the type of payload data to be provided at the node.
*/
public String getPayloadType() {
return payloadType;
}
/**
* Returns the URL of an XSL transformation which can be applied to payloads in order
* to generate an appropriate message body element.
*
* @return the URL of an XSL transformation which can be applied to payloads.
*/
public String getBodyXSLT() {
return bodyXSLT;
}
/**
* Returns 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.
*
* @return the URL of an XSL transformation which can be applied to the payload format.
*/
public String getDataformXSLT() {
return dataformXSLT;
}
/**
* Returns the datetime when the node was created.
*
* @return the datetime when the node was created.
*/
public Date getCreationDate() {
return creationDate;
}
/**
* Returns the last date when the ndoe's configuration was modified.
*
* @return the last date when the ndoe's configuration was modified.
*/
public Date getModificationDate() {
return modificationDate;
}
/**
* Returns the JID of the node creator. This is usually the sender's full JID of the
* IQ packet used for creating the node.
*
* @return the JID of the node creator.
*/
public JID getCreator() {
return creator;
}
/**
* Returns the description of the node. This information is really optional and can be
* modified by submiting a completed data form with the new node configuration.
*
* @return the description of the node.
*/
public String getDescription() {
return description;
}
/**
* Returns the default language of the node. This information is really optional and can be
* modified by submiting a completed data form with the new node configuration.
*
* @return the default language of the node.
*/
public String getLanguage() {
return language;
}
/**
* Returns the JIDs of those to contact with questions. This information is not used by
* the pubsub service. It is meant to be "discovered" by users and redirect any question
* to the returned people to contact.
*
* @return the JIDs of those to contact with questions.
*/
public Collection getContacts() {
return Collections.unmodifiableCollection(contacts);
}
/**
* Adds a new user as a candidate to answer questions about the node.
*
* @param user the JID of the new user.
*/
void addContact(JID user) {
contacts.add(user);
}
/**
* Returns the list of nodes contained by this node. Only {@link CollectionNode} may
* contain other nodes.
*
* @return the list of nodes contained by this node.
*/
public Collection 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 getParents() {
Collection parents = new ArrayList<>();
CollectionNode myParent = parent;
while (myParent != null) {
parents.add(myParent);
myParent = myParent.getParent();
}
return parents;
}
/**
* Sets whether event notifications will include payloads. Payloads are included when
* publishing new items. However, new items may not always include a payload depending
* on the node configuration. Nodes can be configured to not deliver payloads for performance
* reasons.
*
* @param deliverPayloads true if event notifications will include payloads.
*/
void setPayloadDelivered(boolean deliverPayloads) {
this.deliverPayloads = deliverPayloads;
}
void setReplyPolicy(ItemReplyPolicy replyPolicy) {
this.replyPolicy = replyPolicy;
}
/**
* Sets whether subscribers will be notified when the node configuration changes.
*
* @param notifyConfigChanges true if subscribers will be notified when the node
* configuration changes.
*/
void setNotifiedOfConfigChanges(boolean notifyConfigChanges) {
this.notifyConfigChanges = notifyConfigChanges;
}
/**
* Sets whether subscribers will be notified when the node is deleted.
*
* @param notifyDelete true if subscribers will be notified when the node is deleted.
*/
void setNotifiedOfDelete(boolean notifyDelete) {
this.notifyDelete = notifyDelete;
}
/**
* Sets whether subscribers will be notified when items are removed from the node.
*
* @param notifyRetract true if subscribers will be notified when items are removed from
* the node.
*/
void setNotifiedOfRetract(boolean notifyRetract) {
this.notifyRetract = notifyRetract;
}
void setPresenceBasedDelivery(boolean presenceBasedDelivery) {
this.presenceBasedDelivery = presenceBasedDelivery;
}
/**
* 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.
*/
void setPublisherModel(PublisherModel publisherModel) {
this.publisherModel = publisherModel;
}
/**
* Sets whether users are allowed to subscribe and unsubscribe.
*
* @param subscriptionEnabled true if users are allowed to subscribe and unsubscribe.
*/
void setSubscriptionEnabled(boolean subscriptionEnabled) {
this.subscriptionEnabled = subscriptionEnabled;
}
/**
* Sets whether new subscriptions should be configured to be active. Inactive
* subscriptions will not get event notifications. However, subscribers will be
* notified when a node is deleted no matter the subscription status.
*
* @param subscriptionConfigurationRequired true if new subscriptions should be
* configured to be active.
*/
void setSubscriptionConfigurationRequired(boolean subscriptionConfigurationRequired) {
this.subscriptionConfigurationRequired = subscriptionConfigurationRequired;
}
/**
* 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.
*/
void setAccessModel(AccessModel accessModel) {
this.accessModel = accessModel;
}
/**
* Sets the roster group(s) allowed to subscribe and retrieve items. This information
* is going to be used only when using the
* {@link org.jivesoftware.openfire.pubsub.models.RosterAccess} access model.
*
* @param rosterGroupsAllowed the roster group(s) allowed to subscribe and retrieve items.
*/
void setRosterGroupsAllowed(Collection rosterGroupsAllowed) {
this.rosterGroupsAllowed = rosterGroupsAllowed;
}
void setReplyRooms(Collection replyRooms) {
this.replyRooms = replyRooms;
}
void setReplyTo(Collection replyTo) {
this.replyTo = replyTo;
}
/**
* Sets the type of payload data to be provided at the node. Usually specified by the
* namespace of the payload (if any).
*
* @param payloadType the type of payload data to be provided at the node.
*/
void setPayloadType(String payloadType) {
this.payloadType = payloadType;
}
/**
* Sets the URL of an XSL transformation which can be applied to payloads in order
* to generate an appropriate message body element.
*
* @param bodyXSLT the URL of an XSL transformation which can be applied to payloads.
*/
void setBodyXSLT(String bodyXSLT) {
this.bodyXSLT = bodyXSLT;
}
/**
* Sets 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.
*
* @param dataformXSLT the URL of an XSL transformation which can be applied to the
* payload format.
*/
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);
}
}
/**
* Sets the datetime when the node was created.
*
* @param creationDate the datetime when the node was created.
*/
void setCreationDate(Date creationDate) {
this.creationDate = creationDate;
}
/**
* Sets the last date when the ndoe's configuration was modified.
*
* @param modificationDate the last date when the ndoe's configuration was modified.
*/
void setModificationDate(Date modificationDate) {
this.modificationDate = modificationDate;
}
/**
* Sets the description of the node. This information is really optional and can be
* modified by submiting a completed data form with the new node configuration.
*
* @param description the description of the node.
*/
void setDescription(String description) {
this.description = description;
}
/**
* Sets the default language of the node. This information is really optional and can be
* modified by submiting a completed data form with the new node configuration.
*
* @param language the default language of the node.
*/
void setLanguage(String language) {
this.language = language;
}
/**
* Sets the name of the node. The node may not have a configured name. The node's
* name can be changed by submiting a completed data form.
*
* @param name the name of the node.
*/
void setName(String name) {
this.name = name;
}
/**
* Sets the JIDs of those to contact with questions. This information is not used by
* the pubsub service. It is meant to be "discovered" by users and redirect any question
* to the returned people to contact.
*
* @param contacts the JIDs of those to contact with questions.
*/
void setContacts(Collection contacts) {
this.contacts = contacts;
}
/**
* Saves the node configuration to the backend store.
*/
public void saveToDB() {
// Make the room persistent
if (!savedToDB) {
PubSubPersistenceManager.createNode(this);
// Set that the node is now in the DB
setSavedToDB(true);
// Save the existing node affiliates to the DB
for (NodeAffiliate affialiate : affiliates) {
PubSubPersistenceManager.saveAffiliation(this, affialiate, true);
}
// Add new subscriptions to the database
for (NodeSubscription subscription : subscriptionsByID.values()) {
PubSubPersistenceManager.saveSubscription(this, subscription, true);
}
// Add the new node to the list of available nodes
service.addNode(this);
// Notify the parent (if any) that a new node has been added
if (parent != null) {
parent.childNodeAdded(this);
}
}
else {
PubSubPersistenceManager.updateNode(this);
}
}
public void addAffiliate(NodeAffiliate affiliate) {
affiliates.add(affiliate);
}
public void addSubscription(NodeSubscription subscription)
{
subscriptionsByID.put(subscription.getID(), subscription);
subscriptionsByJID.put(subscription.getJID().toString(), subscription);
}
/**
* Returns the subscription whose subscription JID matches the specified JID or null
* 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 subscriberJID the JID of the entity that receives event notifications.
* @return the subscription whose subscription JID matches the specified JID or null
* if none was found.
* @throws IllegalStateException If this message was used when the node supports multiple
* subscriptions.
*/
public NodeSubscription getSubscription(JID subscriberJID) {
// Check that node does not support multiple subscriptions
if (isMultipleSubscriptionsEnabled() && (getSubscriptions(subscriberJID).size() > 1)) {
throw new IllegalStateException("Multiple subscriptions is enabled so subscriptions " +
"should be retrieved using subID.");
}
return subscriptionsByJID.get(subscriberJID.toString());
}
/**
* Returns the subscription whose subscription ID matches the specified ID or null
* 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 null
* if none was found.
*/
public 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() {
// Delete node from the database
if (PubSubPersistenceManager.removeNode(this)) {
// Remove this node from the parent node (if any)
if (parent != null) {
parent.removeChildNode(this);
}
deletingNode();
// Broadcast delete notification to subscribers (if enabled)
if (isNotifiedOfDelete()) {
// 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
broadcastNodeEvent(message, true);
}
// Notify the parent (if any) that the node has been removed from the parent node
if (parent != null) {
parent.childNodeDeleted(this);
}
// Remove presence subscription when node was deleted.
cancelPresenceSubscriptions();
// Remove the node from memory
service.removeNode(getNodeID());
CacheFactory.doClusterTask(new RemoveNodeTask(this));
// Clear collections in memory (clear them after broadcast was sent)
affiliates.clear();
subscriptionsByID.clear();
subscriptionsByJID.clear();
return true;
}
return false;
}
/**
* Notification message indicating that the node is being deleted. Subclasses should
* implement this method to delete any subclass specific information.
*/
protected abstract void deletingNode();
/**
* Changes the parent node of this node. The node ID of the node will not be modified
* based on the new parent so pubsub implementations where node ID has a semantic
* meaning will end up affecting the meaning of the node hierarchy and possibly messing
* up the meaning of the hierarchy.
*
* No notifications are sent due to the new parent adoption process.
*
* @param newParent the new parent node of this node.
*/
protected void changeParent(CollectionNode newParent) {
if (parent == newParent) {
return;
}
if (parent != null) {
// Remove this node from the current parent node
parent.removeChildNode(this);
}
// Set the new parent of this node
parent = newParent;
if (parent != null) {
// Add this node to the new parent node
parent.addChildNode(this);
}
if (savedToDB) {
PubSubPersistenceManager.updateNode(this);
}
}
/**
* Unsubscribe from affiliates presences if node is only sending notifications to
* only users or only unsubscribe from those subscribers that configured their
* subscription to send notifications based on their presence show value.
*/
private void addPresenceSubscriptions() {
for (NodeAffiliate affiliate : affiliates) {
if (affiliate.getAffiliation() != NodeAffiliate.Affiliation.outcast &&
(isPresenceBasedDelivery() || (!affiliate.getSubscriptions().isEmpty()))) {
service.presenceSubscriptionRequired(this, affiliate.getJID());
}
}
}
/**
* Unsubscribe from affiliates presences if node is only sending notifications to
* only users or only unsubscribe from those subscribers that configured their
* subscription to send notifications based on their presence show value.
*/
private void cancelPresenceSubscriptions() {
for (NodeSubscription subscription : getSubscriptions()) {
if (isPresenceBasedDelivery() || !subscription.getPresenceStates().isEmpty()) {
service.presenceSubscriptionNotRequired(this, subscription.getOwner());
}
}
}
/**
* Sends the list of affiliations with the node to the owner that sent the IQ
* request.
*
* @param iqRequest IQ request sent by an owner of the node.
*/
void sendAffiliations(IQ iqRequest) {
IQ reply = IQ.createResultIQ(iqRequest);
Element childElement = iqRequest.getChildElement().createCopy();
reply.setChildElement(childElement);
Element affiliations = childElement.element("affiliations");
for (NodeAffiliate affiliate : affiliates) {
if (affiliate.getAffiliation() == NodeAffiliate.Affiliation.none) {
continue;
}
Element entity = affiliations.addElement("affiliation");
entity.addAttribute("jid", affiliate.getJID().toString());
entity.addAttribute("affiliation", affiliate.getAffiliation().name());
}
// Send reply
service.send(reply);
}
/**
* Sends the list of subscriptions with the node to the owner that sent the IQ
* request.
*
* @param iqRequest IQ request sent by an owner of the node.
*/
void sendSubscriptions(IQ iqRequest) {
IQ reply = IQ.createResultIQ(iqRequest);
Element childElement = iqRequest.getChildElement().createCopy();
reply.setChildElement(childElement);
Element subscriptions = childElement.element("subscriptions");
for (NodeAffiliate affiliate : affiliates) {
for (NodeSubscription subscription : affiliate.getSubscriptions()) {
if (subscription.isAuthorizationPending()) {
continue;
}
Element entity = subscriptions.addElement("subscription");
entity.addAttribute("jid", subscription.getJID().toString());
//entity.addAttribute("affiliation", affiliate.getAffiliation().name());
entity.addAttribute("subscription", subscription.getState().name());
if (isMultipleSubscriptionsEnabled()) {
entity.addAttribute("subid", subscription.getID());
}
}
}
// Send reply
service.send(reply);
}
/**
* Broadcasts a node event to subscribers of the node.
*
* @param message the message containing the node event.
* @param includeAll true if all subscribers will be notified no matter their
* subscriptions status or configuration.
*/
protected void broadcastNodeEvent(Message message, boolean includeAll) {
Collection jids = new ArrayList<>();
for (NodeSubscription subscription : subscriptionsByID.values()) {
if (includeAll || subscription.canSendNodeEvents()) {
jids.add(subscription.getJID());
}
}
// Broadcast packet to subscribers
service.broadcast(this, message, jids);
}
/**
* Sends an event notification to the specified subscriber. The event notification may
* include information about the affected subscriptions.
*
* @param subscriberJID the subscriber JID that will get the notification.
* @param notification the message to send to the subscriber.
* @param subIDs the list of affected subscription IDs or null when node does not
* allow multiple subscriptions.
*/
protected void sendEventNotification(JID subscriberJID, Message notification,
Collection 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", "SubID");
header.setText(subID);
}
}
// Verify that the subscriber JID is currently available to receive notification
// messages. This is required because the message router will deliver packets via
// the bare JID if a session for the full JID is not available. The "isActiveRoute"
// condition below will prevent inadvertent delivery of multiple copies of each
// event notification to the user, possibly multiple times (e.g. route.all-resources).
// (Refer to http://issues.igniterealtime.org/browse/OF-14 for more info.)
//
// This approach is informed by the following XEP-0060 implementation guidelines:
// 12.2 "Intended Recipients for Notifications" - only deliver to subscriber JID
// 12.4 "Not Routing Events to Offline Storage" - no offline storage for notifications
//
// Note however that this may be somewhat in conflict with the following:
// 12.3 "Presence-Based Delivery of Events" - automatically detect user's presence
//
if (subscriberJID.getResource() == null ||
SessionManager.getInstance().getSession(subscriberJID) != null) {
service.sendNotification(this, notification, subscriberJID);
}
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".
*
* The originalIQ parameter may be null when using this API internally. When no
* IQ packet was sent then no IQ result will be sent to the sender. The rest of the
* functionality is the same.
*
* @param originalIQ the IQ packet sent by the entity to subscribe to the node or
* null when using this API internally.
* @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.
*/
public 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 (isSubscriptionConfigurationRequired()) {
// User has to configure the subscription to make it active
subState = NodeSubscription.State.unconfigured;
}
else if (authorizationRequired && !isAdmin(owner)) {
// Node owner needs to authorize subscription request so status is pending
subState = NodeSubscription.State.pending;
}
// Generate a subscription ID (override even if one was sent by the client)
String id = StringUtils.randomString(40);
// Create new subscription
NodeSubscription subscription = new NodeSubscription(this, owner, subscriber, subState, 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(this, subscription, true);
}
if (originalIQ != null) {
// Reply with subscription and affiliation status indicating if subscription
// must be configured (only when subscription was made through an IQ packet)
subscription.sendSubscriptionState(originalIQ);
}
// If subscription is pending then send notification to node owners asking to approve
// new subscription
if (subscription.isAuthorizationPending()) {
subscription.sendAuthorizationRequest();
}
// Update the other members with the new subscription
CacheFactory.doClusterTask(new NewSubscriptionTask(subscription));
// Send last published item (if node is leaf node and subscription status is ok)
if (isSendItemSubscribe() && subscription.isActive()) {
PublishedItem lastItem = getLastPublishedItem();
if (lastItem != null) {
subscription.sendLastPublishedItem(lastItem);
}
}
// Check if we need to subscribe to the presence of the owner
if (isPresenceBasedDelivery() && getSubscriptions(subscription.getOwner()).size() == 1) {
if (subscription.getPresenceStates().isEmpty()) {
// Subscribe to the owner's presence since the node is only sending events to
// online subscribers and this is the first subscription of the user and the
// subscription is not filtering notifications based on presence show values.
service.presenceSubscriptionRequired(this, owner);
}
}
}
/**
* 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 none then
* remove the existing affiliation too.
*
* @param subscription the subscription to cancel.
* @param sendToCluster True to forward cancel order to cluster peers
*/
public void cancelSubscription(NodeSubscription subscription, boolean sendToCluster) {
// Remove subscription from memory
subscriptionsByID.remove(subscription.getID());
subscriptionsByJID.remove(subscription.getJID().toString());
// 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(subscription);
}
if (sendToCluster) {
CacheFactory.doClusterTask(new CancelSubscriptionTask(subscription));
}
// Check if we need to unsubscribe from the presence of the owner
if (isPresenceBasedDelivery() && getSubscriptions(subscription.getOwner()).isEmpty()) {
service.presenceSubscriptionNotRequired(this, subscription.getOwner());
}
}
/**
* 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 none then
* remove the existing affiliation too.
*
* @param subscription the subscription to cancel.
*/
public void cancelSubscription(NodeSubscription subscription) {
cancelSubscription(subscription, ClusterManager.isClusteringEnabled());
}
/**
* Returns the {@link PublishedItem} whose ID matches the specified item ID or null
* 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 null
* value will always be returned.
*
* @param itemID the ID of the item to retrieve.
* @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 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.
*
* @param recentItems number of recent items to retrieve.
* @return a list of PublishedItem with the most recent N items published to
* the node.
*/
public List getPublishedItems(int recentItems) {
return Collections.emptyList();
}
/**
* Returns a list with the subscriptions to the node that are pending to be approved by
* a node owner. If the node is not using the access model
* {@link org.jivesoftware.openfire.pubsub.models.AuthorizeAccess} then the result will
* be an empty collection.
*
* @return a list with the subscriptions to the node that are pending to be approved by
* a node owner.
*/
public Collection getPendingSubscriptions() {
if (accessModel.isAuthorizationRequired()) {
List pendingSubscriptions = new ArrayList<>();
for (NodeSubscription subscription : subscriptionsByID.values()) {
if (subscription.isAuthorizationPending()) {
pendingSubscriptions.add(subscription);
}
}
return pendingSubscriptions;
}
return Collections.emptyList();
}
@Override
public String toString() {
return super.toString() + " - ID: " + getNodeID();
}
/**
* Returns the last {@link PublishedItem} that was published to the node or null if
* the node does not have published items. Collection nodes does not support publishing
* of items so a null will be returned in that case.
*
* @return the PublishedItem that was published to the node or null if
* the node does not have published items.
*/
public PublishedItem getLastPublishedItem() {
return null;
}
/**
* Approves or cancels a subscriptions that was pending to be approved by a node owner.
* Subscriptions that were not approved will be deleted. Approved subscriptions will be
* activated (i.e. will be able to receive event notifications) as long as the subscriber
* is not required to configure the subscription.
*
* @param subscription the subscriptions that was approved or rejected.
* @param approved true when susbcription was approved. Otherwise the subscription was rejected.
*/
public void approveSubscription(NodeSubscription subscription, boolean approved) {
if (!subscription.isAuthorizationPending()) {
// Do nothing if the subscription is no longer pending
return;
}
if (approved) {
// Mark that the subscription to the node has been approved
subscription.approved();
CacheFactory.doClusterTask(new ModifySubscriptionTask(subscription));
}
else {
// Cancel the subscription to the node
cancelSubscription(subscription);
}
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + nodeID.hashCode();
result = prime * result + service.getServiceID().hashCode();
return result;
}
@Override
public boolean equals(Object obj) {
if (obj == this)
return true;
if (getClass() != obj.getClass())
return false;
Node compareNode = (Node) obj;
return (service.getServiceID().equals(compareNode.service.getServiceID()) && nodeID.equals(compareNode.nodeID));
}
/**
* 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;
}
}