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.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.*; import org.dom4j.Element; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.xmpp.forms.DataForm; import org.xmpp.packet.IQ; import org.xmpp.packet.JID; import org.xmpp.packet.Message; import org.xmpp.packet.Packet; import com.adspore.contracts.IContext; import com.adspore.contracts.IProperties; import com.adspore.splat.SplatComponent; import com.adspore.splat.SplatConstants; import com.adspore.splat.xep0030.DiscoInfoProvider; import com.adspore.splat.xep0030.DiscoItem; import com.adspore.splat.xep0030.DiscoItemsProvider; import com.adspore.splat.xep0060.models.AccessModel; import com.adspore.splat.xep0060.models.PublisherModel; import com.adspore.util.StringUtils; public class PubSubService implements DiscoInfoProvider, DiscoItemsProvider { private static final Logger LOG = LoggerFactory.getLogger(PubSubService.class); protected final IContext mContext; protected final SplatComponent mComponent; protected CollectionNode mRootCollectionNode = null; protected Map<String, Node> mNodes = new HashMap<String, Node>(); protected static final Set<Element> mServiceIdentities = new CopyOnWriteArraySet<Element>(); protected static final Set<String> mServiceFeatures = new CopyOnWriteArraySet<String>(); private ScheduledExecutorService mPublisherThreadPool; // = /** * Returns the permission policy for creating nodes. A true value means that not anyone can * create a node, only the JIDs listed in <code>allowedToCreate</code> are allowed to create * nodes. */ protected boolean mNodeCreationRestricted = false; /** * Flag that indicates 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. */ protected boolean mMultipleSubscriptionsEnabled = true; /** * Bare jids of users that are allowed to create nodes. An empty list means that anyone can * create nodes. */ protected Collection<String> allowedToCreate = new CopyOnWriteArrayList<String>(); /** * Default configuration to use for newly created leaf nodes. */ protected static DefaultNodeConfiguration mDefaultLeafConfig; /** * Default configuration to use for newly created collection nodes. */ protected static DefaultNodeConfiguration mDefaultCollectionConfig; /** * Flag that indicates if the service is enabled. */ protected boolean mServiceEnabled = true; static { // Create and save default configuration for leaf nodes; mDefaultLeafConfig = new DefaultNodeConfiguration(true); mDefaultLeafConfig.setAccessModel(AccessModel.open); mDefaultLeafConfig.setPublisherModel(PublisherModel.publishers); mDefaultLeafConfig.setDeliverPayloads(true); mDefaultLeafConfig.setLanguage("English"); mDefaultLeafConfig.setMaxPayloadSize(5120); mDefaultLeafConfig.setNotifyConfigChanges(true); mDefaultLeafConfig.setNotifyDelete(true); mDefaultLeafConfig.setNotifyRetract(true); mDefaultLeafConfig.setPersistPublishedItems(false); mDefaultLeafConfig.setMaxPublishedItems(-1); mDefaultLeafConfig.setPresenceBasedDelivery(false); mDefaultLeafConfig.setSendItemSubscribe(true); mDefaultLeafConfig.setSubscriptionEnabled(true); mDefaultLeafConfig.setReplyPolicy(null); // Create and save default configuration for collection nodes; mDefaultCollectionConfig = new DefaultNodeConfiguration(false); mDefaultCollectionConfig.setAccessModel(AccessModel.open); mDefaultCollectionConfig.setPublisherModel(PublisherModel.publishers); mDefaultCollectionConfig.setDeliverPayloads(true); mDefaultCollectionConfig.setLanguage("English"); mDefaultCollectionConfig.setNotifyConfigChanges(true); mDefaultCollectionConfig.setNotifyDelete(true); mDefaultCollectionConfig.setNotifyRetract(true); mDefaultCollectionConfig.setPresenceBasedDelivery(false); mDefaultCollectionConfig.setSubscriptionEnabled(true); mDefaultCollectionConfig.setReplyPolicy(null); mDefaultCollectionConfig.setAssociationPolicy(CollectionNode.LeafNodeAssociationPolicy.all); mDefaultCollectionConfig.setMaxLeafNodes(-1); // TODO: Figure out if a service can have more than one identity object // <identity> //Element toAdd = DocumentHelper.createElement("identity"); //toAdd.addAttribute("category", "pubsub"); //toAdd.addAttribute("type", "service"); //mServiceIdentities.add(toAdd); // <feature> mServiceFeatures.add("http://jabber.org/protocol/pubsub"); // Collection nodes are supported mServiceFeatures.add("http://jabber.org/protocol/pubsub#collections"); // Configuration of node options is supported mServiceFeatures.add("http://jabber.org/protocol/pubsub#config-node"); // Simultaneous creation and configuration of nodes is supported mServiceFeatures.add("http://jabber.org/protocol/pubsub#create-and-configure"); // Creation of nodes is supported mServiceFeatures.add("http://jabber.org/protocol/pubsub#create-nodes"); // Deletion of nodes is supported mServiceFeatures.add("http://jabber.org/protocol/pubsub#delete-nodes"); // Retrieval of pending subscription approvals is supported mServiceFeatures.add("http://jabber.org/protocol/pubsub#get-pending"); // Creation of instant nodes is supported mServiceFeatures.add("http://jabber.org/protocol/pubsub#instant-nodes"); // Publishers may specify item identifiers mServiceFeatures.add("http://jabber.org/protocol/pubsub#item-ids"); // TODO Time-based subscriptions are supported (clean up thread missing, rest is supported) //features.add("http://jabber.org/protocol/pubsub#leased-subscription"); // Node meta-data is supported mServiceFeatures.add("http://jabber.org/protocol/pubsub#meta-data"); // Node owners may modify affiliations mServiceFeatures.add("http://jabber.org/protocol/pubsub#modify-affiliations"); // Node owners may manage subscriptions. mServiceFeatures.add("http://jabber.org/protocol/pubsub#manage-subscriptions"); // A single entity may subscribe to a node multiple times mServiceFeatures.add("http://jabber.org/protocol/pubsub#multi-subscribe"); // The outcast affiliation is supported mServiceFeatures.add("http://jabber.org/protocol/pubsub#outcast-affiliation"); // Persistent items are supported mServiceFeatures.add("http://jabber.org/protocol/pubsub#persistent-items"); // Presence-based delivery of event notifications is supported mServiceFeatures.add("http://jabber.org/protocol/pubsub#presence-notifications"); // Publishing items is supported (note: not valid for collection nodes) mServiceFeatures.add("http://jabber.org/protocol/pubsub#publish"); // The publisher affiliation is supported mServiceFeatures.add("http://jabber.org/protocol/pubsub#publisher-affiliation"); // Purging of nodes is supported mServiceFeatures.add("http://jabber.org/protocol/pubsub#purge-nodes"); // Item retraction is supported mServiceFeatures.add("http://jabber.org/protocol/pubsub#retract-items"); // Retrieval of current affiliations is supported mServiceFeatures.add("http://jabber.org/protocol/pubsub#retrieve-affiliations"); // Retrieval of default node configuration is supported. mServiceFeatures.add("http://jabber.org/protocol/pubsub#retrieve-default"); // Item retrieval is supported mServiceFeatures.add("http://jabber.org/protocol/pubsub#retrieve-items"); // Retrieval of current subscriptions is supported. mServiceFeatures.add("http://jabber.org/protocol/pubsub#retrieve-subscriptions"); // Subscribing and unsubscribing are supported mServiceFeatures.add("http://jabber.org/protocol/pubsub#subscribe"); // Configuration of subscription options is supported mServiceFeatures.add("http://jabber.org/protocol/pubsub#subscription-options"); // Default access model for nodes created on the service String modelName = getDefaultNodeConfiguration(true).getAccessModel().getName(); mServiceFeatures.add("http://jabber.org/protocol/pubsub#default_access_model_" + modelName); } /** * Prevent users from creating without proper dependency injection. */ @SuppressWarnings("unused") private PubSubService() { mContext = null; mComponent = null; } /** * Creates a new PubSub module suitable for use with the SplatComponent * @param context * @param splatComponent */ public PubSubService(IContext context, SplatComponent splatComponent) { mContext = context; mComponent = splatComponent; } /** * Performs post construction initialization of the PubSub Service, should be called after the component has connected * to the server, as it requires the component's JID and domain to be initialized. */ public void initialize() { loadProperties(); loadDefaultProviders(); createDefaultNodes(); } /** * PROPERTIES */ private void loadProperties() { final IProperties props = mContext.getContextProperties(); // Setup the publishing thread pool, worker threads responsible for publishing node items to listeners. int publisherThreadCount = props.getIntProp(SplatConstants.SPLAT_COMPONENT_PUBSUB_THREAD_COUNT, 10); LOG.info("#initialize()... Publisher Threadpool size:" + publisherThreadCount); mPublisherThreadPool = new ScheduledThreadPoolExecutor(publisherThreadCount, new NamedThreadFactory("Splat:PubSub")); ; // Load the list of JIDs that are allowed to create nodes String creators = props.getStringProp(SplatConstants.SPLAT_COMPONENT_PUBSUB_CREATORS, ""); String[] jids; if (creators != null) { jids = creators.split(","); for (String jid : jids) { final String jidToAdd = jid.trim().toLowerCase(); allowedToCreate.add(jidToAdd); LOG.info("#initialize()... Added admin JID: {}", jidToAdd); } } mMultipleSubscriptionsEnabled = props .getBooleanProp(SplatConstants.SPLAT_COMPONENT_PUBSUB_MULTI_SUBSCRIPTION_ENABLED, true); LOG.info("#initialize()... Multiple subscriptions enabled: {}", mMultipleSubscriptionsEnabled); mNodeCreationRestricted = props .getBooleanProp(SplatConstants.SPLAT_COMPONENT_PUBSUB_NODE_CREATION_RESTRICTED, false); LOG.info("#initialize()... Node creation restricted: {}", mNodeCreationRestricted); } /** * PROVIDERS */ private void loadDefaultProviders() { // Set Handlers.. mComponent.getDiscoInfoHandler().setProvider(mComponent.getJID().toBareJID(), this); mComponent.getDiscoItemsHandler().setProvider(mComponent.getJID().toBareJID(), this); } private void createDefaultNodes() { // Create root collection node final JID componentJID = mComponent.getJID(); mRootCollectionNode = new CollectionNode(this, null, "", componentJID); // Add the creator as the node owner mRootCollectionNode.addOwner(componentJID); } public boolean canCreateNode(JID creator) { // Node creation is always allowed for sysadmin if (isNodeCreationRestricted() && !isServiceAdmin(creator)) { return false; } return true; } @Override public boolean supportsFeature(String featureNamespace) { return mServiceFeatures.contains(featureNamespace); } public boolean isServiceAdmin(JID user) { return mComponent.getSysadmins().contains(user.toBareJID()) || allowedToCreate.contains(user.toBareJID()); } public boolean isInstantNodeSupported() { return true; } public boolean isCollectionNodesSupported() { return true; } public CollectionNode getRootCollectionNode() { return mRootCollectionNode; } public static DefaultNodeConfiguration getDefaultNodeConfiguration(boolean leafType) { if (leafType) { return mDefaultLeafConfig; } return mDefaultCollectionConfig; } public Collection<String> getShowPresences(JID subscriber) { return PubSubEngine.getShowPresences(this, subscriber); } public void presenceSubscriptionNotRequired(Node node, JID user) { PubSubEngine.presenceSubscriptionNotRequired(this, node, user); } public void presenceSubscriptionRequired(Node node, JID user) { PubSubEngine.presenceSubscriptionRequired(this, node, user); } public JID getAddress() { return mComponent.getJID(); } public Collection<String> getUsersAllowedToCreate() { return allowedToCreate; } public boolean isNodeCreationRestricted() { return mNodeCreationRestricted; } public boolean isMultipleSubscriptionsEnabled() { return mMultipleSubscriptionsEnabled; } public void setNodeCreationRestricted(boolean nodeCreationRestricted) { this.mNodeCreationRestricted = nodeCreationRestricted; } public void addUserAllowedToCreate(String userJID) { // Update the list of allowed JIDs to create nodes. allowedToCreate.add(userJID.trim().toLowerCase()); } public void removeUserAllowedToCreate(String userJID) { // Update the list of allowed JIDs to create nodes. allowedToCreate.remove(userJID.trim().toLowerCase()); } public Future<String> submitTask(Callable<String> task) { return mPublisherThreadPool.submit(task); } public void broadcast(Node node, Message message, Collection<JID> jids) { // TODO Possibly use a thread pool for sending packets (based on the jids size) message.setFrom(getAddress()); for (JID jid : jids) { message.setTo(jid); message.setID(node.getNodeID() + "__" + jid.toBareJID() + "__" + StringUtils.randomString(5)); send(message); } } public void sendNotification(Node node, Message message, JID jid) { message.setFrom(getAddress()); message.setTo(jid); message.setID(node.getNodeID() + "__" + jid.toBareJID() + "__" + StringUtils.randomString(5)); send(message); } private boolean canDiscoverNode(Node pubNode) { return true; } /** * Converts an array to a comma-delimitted String. * * @param array the array. * @return a comma delimtted String of the array values. */ private static String fromArray(String[] array) { StringBuilder buf = new StringBuilder(); for (int i = 0; i < array.length; i++) { buf.append(array[i]); if (i != array.length - 1) { buf.append(","); } } return buf.toString(); } public void send(Packet packet) { mComponent.send(packet); } /** * Indicates that this module has information about the requested node. * The node's full path is stored in the targetJID's resource, permitting nesting. */ public boolean hasInfo(JID targetJID, JID senderJID) { if (null == targetJID.getResource() || targetJID.getResource().equals("") || mNodes.containsKey(targetJID.getResource())) { return true; } else { return false; } } /** * Indicates that this module has items for the requested node. */ public boolean hasItems(JID targetJID, JID senderJID) { if (null != targetJID && null == targetJID.getNode()) { if (null == targetJID.getResource()) { return !mRootCollectionNode.getNodes().isEmpty(); } else { return mNodes.containsKey(targetJID.getResource()); } } return false; } /** * Returns Discovery Items based on the the targetJID and the sender JID's ability * to discover those items. */ public Iterator<DiscoItem> getItems(JID targetJID, JID senderJID) { List<DiscoItem> result = new ArrayList<DiscoItem>(); if (null == targetJID.getResource()) { Collection<Node> rootCollection = mRootCollectionNode.getNodes(); Iterator<Node> itor = rootCollection.iterator(); while (itor.hasNext()) { Node nestedNode = itor.next(); if (canDiscoverNode(nestedNode)) { result.add( new DiscoItem(mComponent.getJID(), nestedNode.getName(), nestedNode.getNodeID(), null)); } } LOG.debug("#getItems()...ROOT: {} items returned", result.size()); } else if (null != targetJID.getResource()) { // Specific Node within hierarchy if (mNodes.containsKey(targetJID.getResource())) { Node targetNode = mNodes.get(targetJID.getResource()); if (targetNode.isCollectionNode()) { Collection<Node> collection = targetNode.getNodes(); Iterator<Node> itor = collection.iterator(); while (itor.hasNext()) { Node nestedNode = itor.next(); if (canDiscoverNode(nestedNode)) { result.add(new DiscoItem(mComponent.getJID(), nestedNode.getName(), nestedNode.getNodeID(), null)); } } LOG.debug("#getItems()...Target is Collection: {}, {} items returned", targetJID.getResource(), result.size()); } else { for (PublishedItem publishedItem : targetNode.getPublishedItems()) { DiscoItem toAdd = new DiscoItem(mComponent.getJID(), publishedItem.getID(), null, null); result.add(toAdd); } LOG.debug("#getItems()...Target is Leaf: {}, {} items returned", targetJID.getResource(), result.size()); } } } return result.iterator(); } public Iterator<Element> getIdentities(JID targetJID, JID senderJID) { final ArrayList<Element> identities = new ArrayList<Element>(); if (null == targetJID.getResource()) { return mServiceIdentities.iterator(); } else if (mNodes.containsKey(targetJID.getResource())) { // Answer the identity of a given node Node pubNode = getNode(targetJID.getResource()); if (canDiscoverNode(pubNode)) { identities.add(pubNode.getIdentity()); } } return identities.iterator(); } public Iterator<String> getFeatures(JID targetJID, JID senderJID) { final ArrayList<String> result = new ArrayList<String>(); if (null == targetJID.getResource()) { return mServiceFeatures.iterator(); } else if (mNodes.containsKey(targetJID.getResource())) { result.add("http://jabber.org/protocol/pubsub"); } return result.iterator(); } public DataForm getExtendedInfo(JID targetJID, JID senderJID) { if (null != targetJID.getResource()) { Node pubNode = getNode(targetJID.getResource()); if (canDiscoverNode(pubNode)) { return pubNode.getMetadataForm(); } } return null; } /** * Direct access to the PubSub node map, returns the Node with the specified name * if it exists within the map, null otherwise. * @param nodeID * @return */ public Node getNode(String nodeID) { return mNodes.get(nodeID); } /** * Direct access to the PubSub node map, returns all the current nodes. * @return */ public Collection<Node> getNodes() { return mNodes.values(); } /** * Direct access to the PubSub node map, adds the specified node to the tree * @param node */ public void addNode(Node node) { mNodes.put(node.getNodeID(), node); } /** * Adds the specified node into the PubSub tree, using the provided node's nodeId as the * path for placement. Warning! This operation will reset the node's parentNode to a collection * node indicated in the path. If you need to maintain symantic purity of the node path, use * the more direct 'addNode' operation and build the subpath elements. * @param node * @return */ public boolean addNodePathed(Node node) { String[] pathElements = node.getNodeID().split("/"); StringBuilder sb = new StringBuilder(); // Used to rebuild the path, element by element CollectionNode currentNode = mRootCollectionNode; CollectionNode parentNode = null; for (int inx = 0; (inx < pathElements.length - 1); inx++) { sb.append(pathElements[inx]); parentNode = currentNode; try { currentNode = (CollectionNode) mNodes.get(sb.toString()); } catch (ClassCastException e) { LOG.error( "Namespace collision when adding pathed node! Existing leaf prevents new collection node at: {}", sb.toString()); return false; } if (null == currentNode) { LOG.debug("Adding new collection node to build up path: {}", sb.toString()); currentNode = new CollectionNode(this, parentNode, sb.toString(), getAddress()); addNode(currentNode); if (null != parentNode) { parentNode.addChildNode(currentNode); } } sb.append("/").toString(); } mNodes.put(node.getNodeID(), node); currentNode.addChildNode(node); node.changeParent(currentNode); return true; } /** * Direct access to the PubSub node map, removes the specified Node. * @param nodeID */ public void removeNode(String nodeID) { mNodes.remove(nodeID); } /** * Processes the IQ internally within the PubSub engine, just as if it had been * received from the incoming IQ handler within the AbstractComponent. Permits * the caller to block on the result rather than sending the message out/back * to the component and have it handled on a separate thread. * @param toProcess * @return */ public IQ sendLocal(IQ toProcess) { return PubSubEngine.process(this, toProcess); } /** * Helper method for creating named threads rather than anonymous threads, permits better debugging. */ private static class NamedThreadFactory implements ThreadFactory { private final String mName; public NamedThreadFactory(String name) { mName = name; } @Override public Thread newThread(Runnable r) { return new Thread(r, mName); } } }