Java tutorial
/** * $RCSfile: $ * $Revision: $ * $Date: $ * * 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 com.adspore.splat.xep0060; 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.slf4j.Logger; import org.slf4j.LoggerFactory; 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 org.xmpp.packet.Packet; import com.adspore.splat.xep0060.models.AccessModel; import com.adspore.splat.xep0060.models.PublisherModel; import com.adspore.util.LocaleUtils; import com.adspore.util.StringUtils; /** * Adapted from Openfire source and refactored to work with AbstractComponent based * implementation. * * * 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 { private static final Logger mLogger = LoggerFactory.getLogger(Node.class); /** * Reference to the publish and subscribe service. */ protected PubSubService mService; /** * Keeps the Node that is containing this node. */ protected CollectionNode mParent; /** * The unique identifier for a node within the context of a pubsub service. */ protected String mNodeID; /** * Flag that indicates whether to deliver payloads with event notifications. */ protected boolean mDeliverPayloads; /** * Policy that defines whether owners or publisher should receive replies to items. */ protected ItemReplyPolicy mReplyPolicy; /** * Flag that indicates whether to notify subscribers when the node configuration changes. */ protected boolean mNotifyConfigChanges; /** * Flag that indicates whether to notify subscribers when the node is deleted. */ protected boolean mNotifyDelete; /** * Flag that indicates whether to notify subscribers when items are removed from the node. */ protected boolean mNotifyRetract; /** * Flag that indicates whether to deliver notifications to available users only. */ protected boolean mPresenceBasedDelivery; /** * Publisher model that specifies who is allowed to publish items to the node. */ protected PublisherModel mPublisherModel = PublisherModel.open; /** * Flag that indicates that subscribing and unsubscribing are enabled. */ protected boolean mSubscriptionEnabled; /** * Access model that specifies who is allowed to subscribe and retrieve items. */ protected AccessModel mAccessModel = 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 mCreationDate; /** * The last date when the ndoe's configuration was modified. */ private Date mModificationDate; /** * The JID of the node creator. */ protected JID mCreator; /** * A description of the node. */ protected String description = ""; /** * The default language of the node. */ protected String mLanguage = ""; /** * 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>(); /** * 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<String, NodeSubscription> subscriptionsByJID = new ConcurrentHashMap<String, NodeSubscription>(); Node(PubSubService service, CollectionNode parent, String nodeID, JID creator) { mService = service; mParent = parent; mNodeID = nodeID; mCreator = creator; long mStartTime = System.currentTimeMillis(); mCreationDate = new Date(mStartTime); mModificationDate = new Date(mStartTime); // Configure node with default values (get them from the pubsub service) DefaultNodeConfiguration defaultConfiguration = mService.getDefaultNodeConfiguration(!isCollectionNode()); mSubscriptionEnabled = defaultConfiguration.isSubscriptionEnabled(); mDeliverPayloads = defaultConfiguration.isDeliverPayloads(); mNotifyConfigChanges = defaultConfiguration.isNotifyConfigChanges(); mNotifyDelete = defaultConfiguration.isNotifyDelete(); mNotifyRetract = defaultConfiguration.isNotifyRetract(); mPresenceBasedDelivery = defaultConfiguration.isPresenceBasedDelivery(); mAccessModel = defaultConfiguration.getAccessModel(); mPublisherModel = defaultConfiguration.getPublisherModel(); mLanguage = defaultConfiguration.getLanguage(); mReplyPolicy = 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); mLogger.debug("Would have saved affiliations"); } // Update the other members with the new affiliation // TODO: Verify that it's not required, The abstractComponent plugin isn't // going to use clustering.... //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) { mLogger.debug("Would have removed affiliation"); // 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<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 all subscriptions to the node. If multiple subscriptions are enabled, * this method returns the subscriptions by <tt>subId</tt>, otherwise it returns * the subscriptions by {@link JID}. * * @return All subscriptions to the node. */ public Collection<NodeSubscription> getAllSubscriptions() { if (isMultipleSubscriptionsEnabled()) { return subscriptionsByID.values(); } else { return subscriptionsByJID.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; } /** * 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<JID> getOwners() { Collection<JID> jids = new ArrayList<JID>(); 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 com.adspore.splat.xep0060.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<JID> getPublishers() { Collection<JID> jids = new ArrayList<JID>(); 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<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) { // 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"); mDeliverPayloads = "1".equals(booleanValue); } else if ("pubsub#notify_config".equals(field.getVariable())) { values = field.getValues(); booleanValue = (values.size() > 0 ? values.get(0) : "1"); mNotifyConfigChanges = "1".equals(booleanValue); } else if ("pubsub#notify_delete".equals(field.getVariable())) { values = field.getValues(); booleanValue = (values.size() > 0 ? values.get(0) : "1"); mNotifyDelete = "1".equals(booleanValue); } else if ("pubsub#notify_retract".equals(field.getVariable())) { values = field.getValues(); booleanValue = (values.size() > 0 ? values.get(0) : "1"); mNotifyRetract = "1".equals(booleanValue); } else if ("pubsub#presence_based_delivery".equals(field.getVariable())) { values = field.getValues(); booleanValue = (values.size() > 0 ? values.get(0) : "1"); mPresenceBasedDelivery = "1".equals(booleanValue); } else if ("pubsub#subscribe".equals(field.getVariable())) { values = field.getValues(); booleanValue = (values.size() > 0 ? values.get(0) : "1"); mSubscriptionEnabled = "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) { mAccessModel = AccessModel.valueOf(values.get(0)); } } else if ("pubsub#publish_model".equals(field.getVariable())) { values = field.getValues(); if (values.size() > 0) { mPublisherModel = 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()) { addAllowedRosterGroup(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 { 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(); mLanguage = 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) { mReplyPolicy = 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 { 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<JID>(); 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 = mService.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<JID> 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<JID> publishers = new ArrayList<JID>(); 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<JID> 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 mModificationDate = 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", mNodeID); if (mDeliverPayloads) { 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<String> params = new ArrayList<String>(); 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")); } // FIXME: When called at the root collection node, mParent is null, causing NPE if (null != mParent && !mParent.isRootCollectionNode()) { formField.addValue(mParent.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(mSubscriptionEnabled); 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(mDeliverPayloads); 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(mNotifyConfigChanges); 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(mNotifyDelete); 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(mNotifyRetract); 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(mPresenceBasedDelivery); 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(mAccessModel.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(mPublisherModel.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(mLanguage); 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 (mReplyPolicy != null) { formField.addValue(mReplyPolicy.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 mService.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 mService.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) || mService.isServiceAdmin(user)) { return true; } // // Check if we should try again but using the bare JID // if (user.getResource() != null) { // // TODO: Fixme.... incompatability between orig. JID and org.xmpp JID // // For now, just fails with checking two times.... // //user = user.asBareJID(); // return isAdmin(user); // } return false; } /** * 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 mNodeID; } /** * 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 mDeliverPayloads; } public ItemReplyPolicy getReplyPolicy() { return mReplyPolicy; } /** * 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 mNotifyConfigChanges; } /** * 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 mNotifyDelete; } /** * 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 mNotifyRetract; } /** * 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 mPresenceBasedDelivery; } /** * 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<NodeSubscription> subscriptions = getSubscriptions(user); if (!subscriptions.isEmpty()) { if (mPresenceBasedDelivery) { // 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<JID> getPresenceBasedSubscribers() { Collection<JID> affiliatesJID = new ArrayList<JID>(); if (mPresenceBasedDelivery) { // 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<NodeSubscription> 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 mPublisherModel; } /** * Returns true if users are allowed to subscribe and unsubscribe. * * @return true if users are allowed to subscribe and unsubscribe. */ public boolean isSubscriptionEnabled() { return mSubscriptionEnabled; } /** * 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 mAccessModel; } /** * Returns the roster group(s) allowed to subscribe and retrieve items. This information * is going to be used only when using the * {@link com.adspore.splat.xep0060.models.RosterAccess} access model. * * @return the roster group(s) allowed to subscribe and retrieve items. */ public Collection<String> 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<JID> getReplyRooms() { return Collections.unmodifiableCollection(replyRooms); } void addReplyRoom(JID roomJID) { replyRooms.add(roomJID); } public Collection<JID> 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 mCreationDate; } /** * 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 mModificationDate; } /** * 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 mCreator; } /** * 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 mLanguage; } public PubSubService getService() { return mService; } /** * 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<JID> 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<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 mParent; } /** * 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 = mParent; 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.mDeliverPayloads = deliverPayloads; } void setReplyPolicy(ItemReplyPolicy replyPolicy) { this.mReplyPolicy = 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.mNotifyConfigChanges = 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.mNotifyDelete = 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.mNotifyRetract = notifyRetract; } void setPresenceBasedDelivery(boolean presenceBasedDelivery) { this.mPresenceBasedDelivery = 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.mPublisherModel = 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.mSubscriptionEnabled = 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.mAccessModel = accessModel; } /** * Sets the roster group(s) allowed to subscribe and retrieve items. This information * is going to be used only when using the * {@link com.adspore.splat.xep0060.models.RosterAccess} access model. * * @param rosterGroupsAllowed the roster group(s) allowed to subscribe and retrieve items. */ void setRosterGroupsAllowed(Collection<String> rosterGroupsAllowed) { this.rosterGroupsAllowed = rosterGroupsAllowed; } void setReplyRooms(Collection<JID> replyRooms) { this.replyRooms = replyRooms; } void setReplyTo(Collection<JID> 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 && mParent != null) { // Notify the parent that he has a new child :) mParent.addChildNode(this); } } /** * Sets the datetime when the node was created. * * @param creationDate the datetime when the node was created. */ void setCreationDate(Date creationDate) { this.mCreationDate = 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.mModificationDate = 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.mLanguage = 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<JID> 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) { mLogger.debug("#saveToDb()... Would have saved affiliation"); //PubSubPersistenceManager.saveAffiliation(this, affialiate, true); } // Add new subscriptions to the database for (NodeSubscription subscription : subscriptionsByID.values()) { mLogger.debug("#saveToDb()...Would have saved subscription"); //PubSubPersistenceManager.saveSubscription(this, subscription, true); } // Add the new node to the list of available nodes mService.addNode(this); // Notify the parent (if any) that a new node has been added if (mParent != null) { mParent.childNodeAdded(this); } } else { mLogger.debug("#saveToDb()... Would have updated node"); //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 <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 subscriberJID 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. */ 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 <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() { // Delete node from the database if (true) { // Remove this node from the parent node (if any) if (mParent != null) { mParent.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", mNodeID); // Send notification that the node was deleted // TODO: This doesn't appear to be the same notification scheme as the publishing of events broadcastNodeEvent(message, true); } // Notify the parent (if any) that the node has been removed from the parent node if (mParent != null) { mParent.childNodeDeleted(this); } // Remove presence subscription when node was deleted. cancelPresenceSubscriptions(); // Remove the node from memory mService.removeNode(getNodeID()); // 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.<p> * * 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 (mParent == newParent) { return; } if (mParent != null) { // Remove this node from the current parent node mParent.removeChildNode(this); } // Set the new parent of this node mParent = newParent; if (mParent != null) { // Add this node to the new parent node mParent.addChildNode(this); } if (savedToDB) { mLogger.debug("changeParent()... would have updated node"); //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()))) { mService.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()) { mService.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 mService.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 mService.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. */ // FIXME: Looks like this only occurs when directly subscribed protected void broadcastNodeEvent(Message message, boolean includeAll) { Collection<JID> jids = new ArrayList<JID>(); for (NodeSubscription subscription : subscriptionsByID.values()) { if (includeAll || subscription.canSendNodeEvents()) { jids.add(subscription.getJID()); } } // Broadcast packet to subscribers // TODO: Fixme //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<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); } } // 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 // // FIXME: Verify this is correct solution... Commented out. Map<String, String> allSubPresences = mService.mComponent.getBarePresences().get(subscriberJID.toBareJID()); if (null != allSubPresences && allSubPresences.containsKey(subscriberJID.toString())) { String status = allSubPresences.get(subscriberJID.toString()); if (status.equals("online")) { mService.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".<p> * * The originalIQ parameter may be <tt>null</tt> 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 IQ createSubscription(IQ originalIQ, JID owner, JID subscriber, boolean authorizationRequired, DataForm options) { IQ result = null; // 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); mLogger.debug("#createSubscription()... Would have saved subscription"); } if (originalIQ != null) { // Reply with subscription and affiliation status indicating if subscription // must be configured (only when subscription was made through an IQ packet) result = subscription.sendSubscriptionState(originalIQ); } /* * If subscription is pending then send notification to node owners asking to approve * new subscription. This authorization request is to the owners of the node, not * the caller (user needing authorization) */ if (subscription.isAuthorizationPending()) { subscription.sendAuthorizationRequest(); } // 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. mService.presenceSubscriptionRequired(this, owner); } } return result; } /** * 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. * @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); mLogger.debug("#cancelSubscription()... Would have removed subscription"); } // TODO: No longer supporting clustering with AbstractComponent Plugin. //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()) { mService.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 <tt>none</tt> then * remove the existing affiliation too. * * @param subscription the subscription to cancel. */ public void cancelSubscription(NodeSubscription subscription) { // TODO: No longer supporting clustering with AbstractComponent Plugin. //cancelSubscription(subscription, ClusterManager.isClusteringEnabled()); } /** * 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. * * @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<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. * * @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<PublishedItem> 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 com.adspore.splat.xep0060.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<NodeSubscription> getPendingSubscriptions() { if (mAccessModel.isAuthorizationRequired()) { List<NodeSubscription> pendingSubscriptions = new ArrayList<NodeSubscription>(); 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 <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; } public void send(Packet packet) { mService.send(packet); } abstract public Element getIdentity(); /** * 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(); // TODO: No longer supporting clustering with AbstractComponent Plugin. //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 + mNodeID.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 (mNodeID.equals(compareNode.mNodeID)); } /** * 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 }; }